新增主题配色(4 套)+ 信号右键菜单精简与「克隆该信号」+ 布局边界层级根治 + 单点信号吸附修复
## 为什么
用户 2026-10-29 报告/要求五件事:
1. 派生信号的「时间轴偏移保留在样式上、其它调整烘值」维持现状(确认无需改动);
2. 信号名右键菜单里很多功能已被下方样式自定义区覆盖 ⇒ 只保留「仅显示该信号」,
并在它**上方**新增「克隆该信号」;
3. 设置菜单新增「主题配色」二级菜单(带 √ 状态),顺序 Light / Solarised Light /
Dark / Solarised Dark,默认主题维持现状;
4. 布局边界:中键平移视界时浮动面板上的边界**闪烁**;光标取值胶囊的**文字**框会**穿透**
到浮动面板上;并请核对网格内固定构件(图例 / Y 轴标尺 / GX 标签 / 回到最上方 /
浮动面板)的层级关系;
5. 单数据点信号在拖动光标时的**吸附**功能消失。
## 根因
### 布局边界层级(两个都很隐蔽)
- **文本层号写死**:文本是在绘制过程中由 `UiTextRectSink` 收集的,而宿主把所有文本都记在
固定层 2;组件几何层却是"组件表下标"(`PlotView` = 0)。面板层 = PlotView 层 + 1 = 1,
**低于**文本层 2 ⇒ 面板的遮挡矩形挖不到文本 ⇒ 胶囊文字画在面板表面。
- **面板像素缓存**:面板"先画进图层、命中缓存就贴图",贴图那一帧不画字 ⇒ 文本框凭空消失
⇒ 面板上的边界闪烁。
- 顺带发现 `PlotView::BuildBoxes` 用 `PushLayer()` 却**从不复位**,会把宿主后续所有组件的
层号整体推高。
### 单点信号吸附
`MinorCellSparse` 在"把判定范围收窄到数据实际时间跨度"的分支里有
`if (!(dataMax > dataMin)) return false;`。单个有效采样点时 `dataMax == dataMin`,
于是被判定成"不稀疏" ⇒ 不吸附。
## 改了什么
### 1. 布局边界层级
- `UiTextRectSink` 新增 `OnTextLayer(int)` / `TextLayer()`;新增 `UiSetTextLayer` /
`UiGetTextLayer`;`UiBoxTable` 实现为改 `m_layer`,并暴露 `Layer()`。
- `Application::Render` 在绘制每个组件前下发 `UiSetTextLayer(ComponentLayer(&component))`,
浮层用 `OverlayLayer(1/2)`;新增 `ComponentLayer()` / `OverlayLayer()` /
`kLayersPerComponent = 3`,与 `PaintLayoutBounds` 的层号**完全同源**。
- `PaintLayoutBounds` 改为**显式** `SetLayer(下标 × 3)`(不再依赖 `PushLayer` 累加)。
- `PlotView`:`BuildBoxes` 用 `baseLayer + 1` 放浮动面板(并登记遮挡)、
`baseLayer + 2` 放绘制在面板之上的滚动条 / 回到最上方 / 输入框,结束**复位**层号;
`paintOverlays` 同步分两次抬高文本层。
- 面板像素图层新增 `PanelSlot::textRects` + 转发型收集器 `PanelTextCollector`:
渲染时存文本框,命中缓存贴图时回放 ⇒ 几何与缓存命中无关(消除闪烁)。
### 2. 信号右键菜单 + 克隆
- 菜单只剩:标题、**克隆该信号**、**仅显示该信号**;删除显示/隐藏、显示全部/隐藏全部、
预设色块行、样式三项,以及对应的 `CMD_SIGNAL_TOGGLE/SHOW_ALL/HIDE_ALL`、
`CMD_COLOR_BASE`、`CMD_MODE_BASE` 及其分支(死代码)。
- 新增 `CMD_SIGNAL_CLONE`(post `WM_APP + 5`)→ `Application::CloneSignal`:
把「调整」(系数 / 绝对值 / 镜像 / Y 偏移)烘进采样值、**时间轴偏移保留在样式上**,
名称前置 `*`,插入到源信号**正下方**,并**保留原外观**(颜色/透明度/线宽/绘制样式)。
- 抽出 `Application::InsertSignalBelowSource(...)` 供派生与克隆共用。
### 3. 主题配色
- `ThemeKind{LIGHT, SOLARIZED_LIGHT, DARK, SOLARIZED_DARK}` + `ThemeFor()` / `ThemeName()`;
Light 取 VS Code 2026 默认浅色、Dark 取 Dark+、Solarized Dark 取 VS Code solarized dark。
**默认仍是 Solarised Light**。
- 新增 `CurrentTheme()` / `SetCurrentTheme()`:宿主交出自己的 `m_theme` 地址,
切换主题只改内容;`UiControls` 里没有 `Theme` 形参的原语改用它(原来硬编码
`SolarizedLight()`,换主题不生效)。
- `Application::ApplyTheme()`:改配色 → `PlotView::OnThemeChanged()`(作废场景/宽基底/
装饰/光晕/面板图层)+ `SignalPanel::OnThemeChanged()`(作废行位图)→ 整窗重绘。
- 菜单用 `SUBMENU` + 4 个 `keepOpen` 子项;新增 `PopupMenu::UpdateSubmenuItemLabel()`
回写勾选,并让子菜单分支尊重 `keepOpen`(原为无条件关闭)。
### 4. 单点信号吸附
`MinorCellSparse` 改为只在 `dataMax < dataMin` 时放弃;相等时按"那一个小格"判定
(`need = 5 > finiteCount = 1` ⇒ 稀疏 ⇒ 吸附恢复)。
### 5. 审计
`Makefile` 的 `LAYOUT_BOUNDS` / `JDI_LAYOUT_BOUNDS` **从未被任何 `#if` 引用**
(`LAYOUT_BOUNDS=0` 产物字节数完全相同),属名实不符的死开关 ⇒ 删除并注明原因。
## 怎么验证
新增两支离屏探针(不占鼠标 / 键盘 / 屏幕):
```
.preflight/round15check 28 passed, 0 failed
单点吸附 7 条(含"远处不吸附")、面板层级 5 条、面板文本框跨帧与中键平移稳定 2 条、
克隆 14 条(烘值逐点比对 / 名称前缀 / 偏移与外观保留 / 调整复位)
.preflight/themecheck 17 passed, 0 failed
4 套主题可读性与可区分、默认 = Solarised Light、逐主题换色且无旧底色残留、
切回默认逐像素还原(diff = 0)、子菜单 4 项 / 顺序 / 唯一勾选 / 命令切换 / 菜单保持打开
```
回归全绿:
```
uicheck 69/69 lbcheck 21/21 lbresid 5/5 lbzorder 4/4
lbcovr 7/7 lbpanel 10/10 derivecheck 16/16
rendercheck: 8 个场景哈希与上一轮逐一相同 + FUNC 7/7, exit=0
A FAED59DF284153DB B E6443BFE4E3955F8 C 96A0AA8C402F1729 D E3D5DBD998E43720
E DA0583CB13C26A6F F FFB822159F604D1D G 19F8DE8FE3A8DF18 H DBBDDE4CB2C97C92
```
`make release` / `make debug` 零 error / 零 warning;产物 **791,552 B**
(单一 exe,未拆 DLL,未改 `-Oz`;离 1MB 预警线约 250 KB)。
This commit is contained in:
1 parent
71c6ff91cb
commit
71195c4c48
21 files changed
+907
-151
No files matched your search
@@ -668,6 +668,14 @@ namespace jdi
|
||||
*/
|
||||
void InvalidateDerivedCaches();
|
||||
|
||||
/**
|
||||
* @brief 主题配色变化后作废全部**像素级**缓存。
|
||||
*
|
||||
* 主题色被烘进了场景缓存位图、每网格宽基底图层、装饰层、光晕层与浮动面板层 ——
|
||||
* 换主题后必须整体丢弃,否则这些区域会停留在旧配色上(直到因为别的原因重画)。
|
||||
*/
|
||||
void OnThemeChanged();
|
||||
|
||||
/**
|
||||
* @brief 释放本组件持有的全部绘制资源(位图 / 后台线程)。
|
||||
*
|
||||
@@ -4011,6 +4019,15 @@ namespace jdi
|
||||
int y0 = 0; /**< 上。 */
|
||||
int x1 = 0; /**< 右(不含)。 */
|
||||
int y1 = 0; /**< 下(不含)。 */
|
||||
/**
|
||||
* @brief 该面板内**全部文本的墨迹矩形**(客户区坐标)。
|
||||
*
|
||||
* 面板是"先画进图层缓存、再贴图"的:命中缓存时不会真的画文字,
|
||||
* 于是调试用的文本框这一帧就**凭空消失** —— 观感是"拖动视界时浮动面板上
|
||||
* 的布局边界不停闪烁"(用户 2026-10-29 报告)。
|
||||
* 因此渲染时把文本框一并存下来,贴图时按原坐标回放,几何与是否命中缓存无关。
|
||||
*/
|
||||
std::vector<gd::RectF> textRects;
|
||||
};
|
||||
|
||||
HBITMAP m_panelBitmap = nullptr; /**< 面板图层位图(原点与客户区一致)。 */
|
||||
|
||||
@@ -60,6 +60,17 @@ namespace jdi
|
||||
InvalidateWideBuffer();
|
||||
}
|
||||
|
||||
void PlotView::OnThemeChanged()
|
||||
{
|
||||
// 折线/统计等纯数据缓存与主题无关,但它们的**像素化结果**全都与主题有关:
|
||||
// 场景缓存位图、每网格宽基底图层、装饰层、光晕层、浮动面板层里都烘着主题色。
|
||||
InvalidateDerivedCaches(); // 内部已经包含场景缓存作废
|
||||
InvalidateWideBuffer();
|
||||
ReleaseDecorCache();
|
||||
ReleaseHaloLayer();
|
||||
ReleasePanelCache();
|
||||
}
|
||||
|
||||
bool PlotView::HasDrawableContent() const
|
||||
{
|
||||
return (m_dataSet != nullptr) && !m_dataSet->Empty() && (m_tab != nullptr) && m_tab->AnyVisible();
|
||||
|
||||
@@ -836,7 +836,13 @@ namespace jdi
|
||||
}
|
||||
dataMin = std::max(dataMin, viewMin);
|
||||
dataMax = std::min(dataMax, viewMax);
|
||||
if (!(dataMax > dataMin))
|
||||
// ⚠️ **`dataMax == dataMin` 是合法且常见的情形**(视界内只有 1 个有效采样点,
|
||||
// 典型:单数据点信号)。旧实现在这里直接 `return false`,等于把"只有一个点"
|
||||
// 的信号判成"不稀疏" ⇒ 光标不会吸附到那个点上
|
||||
// (用户 2026-10-29 报告"单数据点信号的吸附没了")。
|
||||
// 正确做法是让它走下面"覆盖那唯一一个小格"的路径:
|
||||
// `firstCell == lastCell`,`need = 5`,而 `finiteCount == 1 < 5` ⇒ 判为稀疏。
|
||||
if (dataMax < dataMin)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,57 @@
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
/**
|
||||
* @brief 面板渲染期间的文本几何收集器:**记录 + 转发**给外层收集器。
|
||||
*
|
||||
* 为什么需要它:浮动面板是"先画进像素图层、命中缓存时直接贴图"的。
|
||||
* 贴图那一帧不会真的画字 —— 若文本框只靠"画的时候顺手收集",它在那一帧就会
|
||||
* **凭空消失**,观感是"拖动视界时面板上的布局边界不停闪烁"(用户 2026-10-29 报告)。
|
||||
*
|
||||
* 所以渲染面板时用本收集器:一边把矩形转发给外层(保持正常行为),
|
||||
* 一边存一份进 `PanelSlot::textRects`;命中缓存贴图时再把存下的矩形回放出去。
|
||||
*
|
||||
* `OnTextLayer` 也必须转发:外层收集器靠它把文本记到正确的绘制层
|
||||
* (面板层比网格层高一层),否则面板文字会被自己登记的遮挡矩形挖掉。
|
||||
*/
|
||||
class PanelTextCollector : public UiTextRectSink
|
||||
{
|
||||
public:
|
||||
explicit PanelTextCollector(UiTextRectSink* outer) : m_outer(outer) {}
|
||||
|
||||
void OnTextRect(const gd::RectF& rect) override
|
||||
{
|
||||
m_rects.push_back(rect);
|
||||
if (m_outer != nullptr)
|
||||
{
|
||||
m_outer->OnTextRect(rect);
|
||||
}
|
||||
}
|
||||
|
||||
void OnTextLayer(int layer) override
|
||||
{
|
||||
if (m_outer != nullptr)
|
||||
{
|
||||
m_outer->OnTextLayer(layer);
|
||||
}
|
||||
}
|
||||
|
||||
int TextLayer() const override { return (m_outer != nullptr) ? m_outer->TextLayer() : 0; }
|
||||
|
||||
/** @brief 外层收集器(面板画完后要还原它)。 */
|
||||
UiTextRectSink* Outer() const { return m_outer; }
|
||||
|
||||
/** @brief 本次渲染收集到的文本框。 */
|
||||
const std::vector<gd::RectF>& Rects() const { return m_rects; }
|
||||
|
||||
private:
|
||||
UiTextRectSink* m_outer = nullptr;
|
||||
std::vector<gd::RectF> m_rects;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
|
||||
const PlotView::PolylineCacheEntry* PlotView::AcquirePolyline(const SignalRef& ref,
|
||||
const ResolvedSignal& resolved,
|
||||
@@ -567,6 +618,10 @@ namespace jdi
|
||||
{
|
||||
UiComponent::BuildBoxes(boxes);
|
||||
|
||||
// 本组件的基础绘制层(= 宿主组件表里的下标)。**必须显式记录并复位**:
|
||||
// 下面为浮动面板临时提了一层,若不复位,宿主后续组件的层号会被整体推高。
|
||||
const int baseLayer = boxes.Layer();
|
||||
|
||||
// 层级 1:每个网格的波形区;层级 2:该网格的 GX 编号徽标。
|
||||
// ⚠️ **绝不能读 `m_decorRects`**:那里存的是"装饰层位图缓存"的登记矩形,
|
||||
// 坐标处在**位图/图层坐标系**(会带负 Y、或超出绘图区),拿它当屏幕矩形会出现
|
||||
@@ -591,16 +646,22 @@ namespace jdi
|
||||
|
||||
// 层级 1/2:绘图区里**不属于任何网格**的固定构件。
|
||||
// 这些曾经全部漏掉(调试时"看得见却没框")—— 它们都已经有成员变量,直接读即可。
|
||||
// ⚠️ 这些构件都绘制在**浮动面板之前**,因此与网格同层(`baseLayer`)。
|
||||
boxes.SetLayer(baseLayer);
|
||||
boxes.Add(m_timeAxisRect, 1);
|
||||
boxes.Add(m_scrollBarRect, 2);
|
||||
boxes.Add(BackToTopRect(), 2);
|
||||
|
||||
// 层级 2/3:时间光标读数标签(避让后的实际矩形,绘制时已登记)。
|
||||
boxes.Add(m_cursorSpanRect, 2);
|
||||
boxes.AddAll(m_cursorLabelRects, 3);
|
||||
|
||||
// 层级 3:内联输入框(正在编辑时才有意义)。
|
||||
boxes.Add(EditFieldRect(), 3);
|
||||
// 下面这三样在 `paintOverlays` 里绘制在**浮动面板之后**(压在面板之上):
|
||||
// 滚动条、右下角「回到最上方」悬浮按钮、内联输入框。
|
||||
// ⇒ 必须比面板层(`baseLayer + 1`)再高一层,否则与实际像素的层序相反。
|
||||
boxes.SetLayer(baseLayer + 2);
|
||||
boxes.Add(m_scrollBarRect, 5);
|
||||
boxes.Add(BackToTopRect(), 5);
|
||||
boxes.Add(EditFieldRect(), 5);
|
||||
boxes.SetLayer(baseLayer);
|
||||
|
||||
// ⚠️⚠️ **浮动面板必须另起一个绘制层,并把面板矩形登记为遮挡矩形**:
|
||||
// 面板是不透明浮层、且常常**跨多个网格上下排列**,与网格区域重叠。
|
||||
@@ -642,7 +703,7 @@ namespace jdi
|
||||
}
|
||||
if (!panelLayerPushed)
|
||||
{
|
||||
boxes.PushLayer();
|
||||
boxes.SetLayer(baseLayer + 1);
|
||||
panelLayerPushed = true;
|
||||
}
|
||||
boxes.AddOccluder(layout.panel);
|
||||
@@ -691,8 +752,9 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
}
|
||||
// 收尾:解除面板裁剪(后续若还有调用者依赖"不裁剪"的默认行为)。
|
||||
// 收尾:解除面板裁剪并**复位层号**(宿主后续组件依赖它)。
|
||||
boxes.SetClip(gd::RectF());
|
||||
boxes.SetLayer(baseLayer);
|
||||
}
|
||||
|
||||
void PlotView::Paint(gd::Graphics& g)
|
||||
@@ -771,6 +833,13 @@ namespace jdi
|
||||
// 三个分支(完全命中 / 局部重画 / 整幅重绘)共用同一段,保证观感完全一致。
|
||||
const auto paintOverlays = [this, &g]()
|
||||
{
|
||||
// 文本几何的层号必须跟着**真实绘制顺序**走:
|
||||
// 这一段前半部分的文字(光标取值胶囊、读数标签…)在浮动面板**之下**(同组件层),
|
||||
// 而 `PaintEdgeCursor` 之后的一切(面板、输入框、滚动条、回到最上方)在面板之上。
|
||||
// 少了这一步,胶囊里的文字框会记在比面板更低的层,然后被面板的遮挡矩形挖掉 ——
|
||||
// 或者反过来(旧实现在所有组件之上统一按固定层收集)穿透到面板表面
|
||||
//(用户 2026-10-29 报告的两处现象)。
|
||||
const int textLayer = UiGetTextLayer();
|
||||
PaintHoverHighlight(g);
|
||||
PaintLegendHighlight(g);
|
||||
PaintRubberBand(g);
|
||||
@@ -778,7 +847,11 @@ namespace jdi
|
||||
// 「自由标记」的跟随光标与已固定数据点(在统计面板**之下**绘制,
|
||||
// 这样浮动面板仍压在最上层,与其它实验性功能的层序一致)。
|
||||
PaintFreeMarker(g, m_lanes);
|
||||
// 从这里开始都在面板之上:文本层同步 +1(与 `BuildBoxes` 的 `baseLayer + 1` 一致)。
|
||||
UiSetTextLayer(textLayer + 1);
|
||||
PaintEdgeCursor(g, m_lanes);
|
||||
// 之后这几样绘制在浮动面板**之上**:与 `BuildBoxes` 的 `baseLayer + 2` 对应。
|
||||
UiSetTextLayer(textLayer + 2);
|
||||
PaintSignalInteraction(g);
|
||||
PaintHint(g);
|
||||
// 网格 Y 轴上下限的输入框、以及「弱峰调整 → 迭代次数上限」的输入框都绘制在最上层
|
||||
@@ -806,6 +879,8 @@ namespace jdi
|
||||
m_plotRect.Height + 4.0f),
|
||||
border, 4, m_theme->axisLine | 0xFF000000u, 2);
|
||||
}
|
||||
// 复原文本层(宿主按组件下发,回收成进入时的值最保险)。
|
||||
UiSetTextLayer(textLayer);
|
||||
};
|
||||
|
||||
// 网格次序拖拽:静态场景缓存仍然有效,但各网格要按位移重新"摆放"。
|
||||
@@ -4261,7 +4336,6 @@ namespace jdi
|
||||
}
|
||||
|
||||
std::uint64_t slotKey = 0;
|
||||
const std::size_t slotIndex = nextSlots.size();
|
||||
if (panelCacheReady)
|
||||
{
|
||||
slotKey = PanelSlotKey(static_cast<int>(index), kind, layout);
|
||||
@@ -4286,18 +4360,34 @@ namespace jdi
|
||||
m_panelDc, hit->x0, hit->y0, SRCCOPY);
|
||||
g.ReleaseHDC(target);
|
||||
}
|
||||
// 贴图不会真的画字 ⇒ 必须把上一帧渲染时存下的文本框**回放**给
|
||||
// 外层收集器,否则调试框这一帧凭空消失(= 面板上边界闪烁)。
|
||||
if (UiTextRectSink* const outerSink = UiGetTextRectSink())
|
||||
{
|
||||
for (const gd::RectF& textRect : hit->textRects)
|
||||
{
|
||||
outerSink->OnTextRect(textRect);
|
||||
}
|
||||
}
|
||||
nextSlots.push_back(*hit);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
const std::size_t storedIndex = nextSlots.size();
|
||||
nextSlots.push_back(PanelSlot());
|
||||
dirtyRects.push_back(cachedRect);
|
||||
// 本面板渲染期间把文本框收集到本地容器(同时**转发**给外层收集器),
|
||||
// 渲染完再存进 slot,供命中缓存时回放。详见 `PanelSlot::textRects`。
|
||||
PanelTextCollector panelTextBoxes(UiGetTextRectSink());
|
||||
|
||||
const float titleHeight = kEdgePanelTitleHeight * m_scale;
|
||||
const float rowHeight = kEdgePanelRowHeight * m_scale;
|
||||
const float pad = kEdgePanelPad * m_scale;
|
||||
|
||||
// 以下整段(面板底色 → 标题栏 → 各控件 → 行内容)的画字都要走本地收集器。
|
||||
UiSetTextRectSink(&panelTextBoxes);
|
||||
|
||||
// 面板主体 + 标题栏。
|
||||
// 投影响用两张大面积的半透明圆角矩形模拟,代价不低;交互期间(快速帧)省略它,
|
||||
// 让拖动参数滑块时面板重绘不再被投影拖累(松手后的全质量帧会补回投影)。
|
||||
@@ -5032,9 +5122,13 @@ namespace jdi
|
||||
}
|
||||
} // if (!layout.collapsed)
|
||||
|
||||
// 面板画完了:把它刚画进后备缓冲区的像素(连同缓存键)存进面板图层,
|
||||
// 面板画完了:恢复外层收集器,并把本面板的文本框存进 slot
|
||||
//(命中缓存时靠它回放,见 `PanelSlot::textRects`)。
|
||||
UiSetTextRectSink(panelTextBoxes.Outer());
|
||||
|
||||
// 把它刚画进后备缓冲区的像素(连同缓存键)存进面板图层,
|
||||
// 下一帧只要键不变就直接贴图。矩形外的 4 逻辑像素边距用于容纳投影。
|
||||
PanelSlot& stored = nextSlots[slotIndex];
|
||||
PanelSlot& stored = nextSlots[storedIndex];
|
||||
stored.laneIndex = static_cast<int>(index);
|
||||
stored.kind = static_cast<int>(kind);
|
||||
stored.key = 0;
|
||||
@@ -5061,6 +5155,7 @@ namespace jdi
|
||||
stored.y0 = y0;
|
||||
stored.x1 = x1;
|
||||
stored.y1 = y1;
|
||||
stored.textRects = panelTextBoxes.Rects();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user