主题自适应配色 + 预览曲线连续配色 + 样式区复位按钮 + 均值滤波面板重做 + 面板缓存环带残留 + 调试层序 + 自由标记选取与性能
① 主题自适应(`Theme.cpp`):新增 `SrgbLuminance` / `ContrastRatio` / `EnsureContrast` /
`MixToLuminance` / `ThemeLaneBackground`;四套主题的结构色(gridWeak 1.30 / gridStrong 1.65 /
axisLine 2.10 / borderStrong 1.70)按底色自动校正,Solarised Light 逐位不变;网格叠底改用
`ThemeLaneBackground()`(4 处),深色主题下网格层与绘图区底色可辨。
② 派生预览曲线(`HighContrastDerived`)重写为**连续**方案:方向沿信号所在侧远离底色,
目标亮度 = max(与底色对比度 3.2, 信号亮度 ± 0.18),再按亮度平滑混色;调用点传入真实网格底色
(旧实现默认写死 Solarised Light 底色 ⇒ 深色主题永远选到更暗的那一侧、几乎看不见)。
③ 信号样式区「线宽 / 透明度」两行右侧新增**复位按钮**(`UiPaintResetIcon` 成为唯一实现,
菜单/弱峰面板/样式区共用同一套笔画);默认线宽统一到 `DefaultSignalLineWidth(finiteCount)`
(单点信号取 10.0)。按钮右边界与内容列对齐,内容列过窄时整列不显示。
④ 「均值滤波」面板重做:信号色叠底标题栏 + 复选框 + 展开三角 + 「均值点数 / 迭代次数」两行滑块
(点击数值可改滑块上限、右侧复位)+ 内容区滚动;新增 `MeanFilterItem::{maxPoints,maxIterations,
collapsed}` 与 `Lane::meanFilterScroll`,缓存键同步补全。
⑤ 浮动面板命中像素缓存时只贴**面板圆角形状之内**的像素(`CreateRoundRectRgn` 裁剪)并按本帧重画投影
⇒ 中键移视界后面板周围不再出现一圈旧波形。
⑥ 显示布局边界:每个浮动面板**独占一个绘制层**(按 `m_panelOrder` 递增),绘制侧
`UiSetTextLayer` 在同一循环内同步推进 ⇒ 下层面板的边框/文字不再画到上层面板上。
⑦ 自由标记:采样候选距离改为**二维欧氏距离**(单点信号不再在整幅宽度内吸住光标),
纵向光标画在鼠标位置、圆点落在真实采样点(新增 `FreeMarkerProbe::pointX`);
`MinorCellSparse` 只对胜出信号算一次;折线缓存容量改为 `max(64, 信号数×2)` 且查找改为
单键哈希 ⇒ MF4 的 285 信号合并网格中 `ProbeFreeMarker` 平均 0.11ms。
验证:`.preflight` 探针重建(`panelcheck` 71/71、`markercheck` 13/13、`ringcheck` 6/6,合计 90 条,
LTO 目标文件上复跑同样全绿);`make release` / `make debug` 零警告;产物 815,104 B。
文档:`agent_mem.md` 第十六轮(含补丁脚本与编码/行尾踩坑)。
This commit is contained in:
1 parent
71195c4c48
commit
bdd703b88b
24 files changed
+1815
-256
No files matched your search
@@ -113,29 +113,65 @@ namespace jdi
|
||||
//(详见 `PolylineCacheEntry::lineWidth` 的说明;漏掉它会让线宽改动在**稠密信号**上不生效)。
|
||||
const float lineWidth = (resolved.style != nullptr) ? resolved.style->lineWidth : 1.0f;
|
||||
|
||||
// 全部键字段的 64 位哈希(顺序固定;只用于**查找**,命中后再做廉价校验)。
|
||||
std::uint64_t key = 1469598103934665603ull;
|
||||
const auto mixHash = [&key](std::uint64_t value)
|
||||
{
|
||||
key ^= value;
|
||||
key *= 1099511628211ull;
|
||||
};
|
||||
const auto mixHashFloat = [&mixHash](float value)
|
||||
{
|
||||
std::uint32_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
mixHash(static_cast<std::uint64_t>(bits));
|
||||
};
|
||||
const auto mixHashDouble = [&mixHash](double value)
|
||||
{
|
||||
std::uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
mixHash(bits);
|
||||
};
|
||||
mixHash(static_cast<std::uint64_t>(static_cast<std::uint32_t>(ref.groupIndex)) + 2ull);
|
||||
mixHash(static_cast<std::uint64_t>(static_cast<std::uint32_t>(ref.signalIndex)) + 2ull);
|
||||
mixHash(reinterpret_cast<std::uintptr_t>(resolved.group));
|
||||
mixHash(static_cast<std::uint64_t>(sampleCount));
|
||||
mixHashDouble(viewMin);
|
||||
mixHashDouble(viewMax);
|
||||
mixHashFloat(m_plotRect.X);
|
||||
mixHashFloat(m_plotRect.Width);
|
||||
mixHashFloat(lane.rect.Y);
|
||||
mixHashFloat(lane.rect.Height);
|
||||
mixHashDouble(lane.yMin);
|
||||
mixHashDouble(lane.yMax);
|
||||
mixHashFloat(gain);
|
||||
mixHash((absolute ? 1ull : 0ull) | (mirrorVertical ? 2ull : 0ull) |
|
||||
(mirrorHorizontal ? 4ull : 0ull));
|
||||
mixHashFloat(offsetY);
|
||||
mixHashDouble(offsetTime);
|
||||
mixHashFloat(lineWidth);
|
||||
|
||||
for (const PolylineCacheEntry& entry : m_polylineCache)
|
||||
{
|
||||
if (entry.ref == ref && entry.group == resolved.group && entry.sampleCount == sampleCount &&
|
||||
entry.viewMin == viewMin && entry.viewMax == viewMax && entry.plotX == m_plotRect.X &&
|
||||
entry.plotWidth == m_plotRect.Width && entry.laneY == lane.rect.Y &&
|
||||
entry.laneHeight == lane.rect.Height && entry.laneYMin == lane.yMin &&
|
||||
entry.laneYMax == lane.yMax && entry.gain == gain && entry.absolute == absolute &&
|
||||
entry.mirrorVertical == mirrorVertical && entry.mirrorHorizontal == mirrorHorizontal &&
|
||||
entry.offsetY == offsetY && entry.offsetTime == offsetTime &&
|
||||
entry.lineWidth == lineWidth)
|
||||
// 快路径:只比哈希。命中后再校验三项最可能变化的字段(碰撞代价可忽略)。
|
||||
if (entry.key != key || entry.sampleCount != sampleCount || !(entry.ref == ref) ||
|
||||
entry.viewMin != viewMin || entry.viewMax != viewMax)
|
||||
{
|
||||
return &entry;
|
||||
continue;
|
||||
}
|
||||
return &entry;
|
||||
}
|
||||
|
||||
// 未命中:重建并写入缓存(deque 保证已有元素的引用在追加后依然有效)。
|
||||
// 缓存只用于消除重复计算,容量上限约等于“网格数 × 每网格信号数”的两倍。
|
||||
if (m_polylineCache.size() >= kPolylineCacheLimit)
|
||||
// ⚠️ 容量必须**覆盖所有网格的全部信号**:旧实现固定 64 条,283 个信号合并到
|
||||
// 同一网格时会不断"清空重建"⇒ 绘制与自由标记探测每帧重算全部折线(严重卡顿)。
|
||||
if (m_polylineCache.size() >= m_polylineCacheCapacity)
|
||||
{
|
||||
m_polylineCache.clear();
|
||||
}
|
||||
m_polylineCache.push_back(PolylineCacheEntry());
|
||||
PolylineCacheEntry& entry = m_polylineCache.back();
|
||||
entry.key = key;
|
||||
entry.ref = ref;
|
||||
entry.group = resolved.group;
|
||||
entry.sampleCount = sampleCount;
|
||||
@@ -656,104 +692,102 @@ namespace jdi
|
||||
|
||||
// 下面这三样在 `paintOverlays` 里绘制在**浮动面板之后**(压在面板之上):
|
||||
// 滚动条、右下角「回到最上方」悬浮按钮、内联输入框。
|
||||
// ⇒ 必须比面板层(`baseLayer + 1`)再高一层,否则与实际像素的层序相反。
|
||||
boxes.SetLayer(baseLayer + 2);
|
||||
boxes.Add(m_scrollBarRect, 5);
|
||||
boxes.Add(BackToTopRect(), 5);
|
||||
boxes.Add(EditFieldRect(), 5);
|
||||
// ⇒ 它们的层号在下面「面板循环」之后统一给(比最上面板再高一层),
|
||||
// 否则与实际像素的层序相反。这里先复位到组件基层。
|
||||
boxes.SetLayer(baseLayer);
|
||||
|
||||
// ⚠️⚠️ **浮动面板必须另起一个绘制层,并把面板矩形登记为遮挡矩形**:
|
||||
// ⚠️⚠️ **浮动面板必须另起绘制层,并把面板矩形登记为遮挡矩形**:
|
||||
// 面板是不透明浮层、且常常**跨多个网格上下排列**,与网格区域重叠。
|
||||
// 若与网格同层,网格的矩形边线会横穿面板 —— 用户 2026-10-29 报告的
|
||||
// "网格的布局边界穿透显示在浮动面板上"。`layer` 决定谁压谁,
|
||||
// `AddOccluder` 才真正把下层 box 从面板区域里**挖掉**。
|
||||
bool panelLayerPushed = false;
|
||||
|
||||
// 层级 2/3/4:5 类实验性功能浮动面板的内部控件。
|
||||
// 这些矩形由 `EdgePanelLayoutFor` 统一算出(绘制与命中共用),一个循环即可全部上报。
|
||||
//
|
||||
// ⚠️⚠️ **每个面板各占一个绘制层,且顺序必须是 `m_panelOrder`(底 → 顶)**:
|
||||
// 旧实现把所有面板都放在 `baseLayer + 1` 一层,于是同一层内只能按 `depth` 排序 ——
|
||||
// 下层面板的"行/文本"(depth 4)会画在**上层**面板的本体(depth 2/3)之上,
|
||||
// 表现为"被遮挡的下层面板的边框/文字穿透到上层面板上"(用户 2026-10-30 报告)。
|
||||
// 层号与 `paintOverlays` 里 `UiSetTextLayer` 的推进**完全同源**。
|
||||
//
|
||||
// 说明(已知边界):面板层号从 `baseLayer + 1` 起逐面板递增,因此**同时显示 15 个以上**
|
||||
// 面板时会撞上浮层层号(`OverlayLayer(1)` = 组件数 × 3 + 3)。那时只有"菜单不再挖掉
|
||||
// 面板边框"这一条调试观感受影响;实际布局下同时可见的面板是几个到十几个。
|
||||
SyncPanelOrder();
|
||||
const std::size_t panelCount = m_panelOrder.size();
|
||||
const int topOverlayLayer = baseLayer + 1 + static_cast<int>(panelCount);
|
||||
|
||||
// ⚠️ **先用最便宜的判据跳过"没有面板"的网格**:`EdgePanelLayoutFor` 内部要取
|
||||
// 标签页配置、逐项判断种类与开关,单次约 2~3µs;283 个网格 × 5 种 = 1415 次调用
|
||||
// 实测约 **4ms/帧**(这是"开调试后卡"的主要来源之一)。
|
||||
// 只有该网格**至少有一个面板处于展开状态**时才值得去算布局。
|
||||
const std::vector<Lane>& laneConfigs =
|
||||
(m_tab != nullptr) ? m_tab->Lanes() : m_localTab.Lanes();
|
||||
for (std::size_t index = 0; index < m_lanes.size() && index < laneConfigs.size(); ++index)
|
||||
// 现在只对**层序表里真正显示出来的面板**算布局(层序表本来就只含展开且启用的面板)。
|
||||
for (std::size_t ordinal = 0; ordinal < panelCount; ++ordinal)
|
||||
{
|
||||
const Lane& laneConfig = laneConfigs[index];
|
||||
const bool anyPanelOpen = laneConfig.edgePanel.open ||
|
||||
laneConfig.edgeCenterPanel.open ||
|
||||
laneConfig.meanFilterPanel.open ||
|
||||
laneConfig.weakPeakPanel.open ||
|
||||
laneConfig.freeMarkerPanel.open;
|
||||
if (!anyPanelOpen)
|
||||
const PanelRef& panelRef = m_panelOrder[ordinal];
|
||||
const EdgePanelLayout layout =
|
||||
EdgePanelLayoutFor(panelRef.laneIndex, static_cast<EdgePanelKind>(panelRef.kind));
|
||||
if (!UiRectHasArea(layout.panel))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const int laneIndex = static_cast<int>(index);
|
||||
for (int kindValue = static_cast<int>(EdgePanelKind::COUNT);
|
||||
kindValue <= static_cast<int>(EdgePanelKind::MARK); ++kindValue)
|
||||
// 本面板独占一层(层号越大越靠上)。
|
||||
boxes.SetLayer(baseLayer + 1 + static_cast<int>(ordinal));
|
||||
boxes.AddOccluder(layout.panel);
|
||||
// 面板内部元素的调试框同样裁到面板本身:面板滚动的内容区里,
|
||||
// 滚出可视区的行其矩形仍在(与弹出菜单的条目同一类问题)。
|
||||
boxes.SetClip(layout.panel);
|
||||
boxes.Add(layout.panel, 2);
|
||||
boxes.Add(layout.title, 3);
|
||||
boxes.Add(layout.close, 3);
|
||||
boxes.Add(layout.disclosure, 3);
|
||||
boxes.Add(layout.badge, 3);
|
||||
boxes.Add(layout.edit, 3);
|
||||
boxes.Add(layout.button, 3);
|
||||
boxes.Add(layout.content, 3);
|
||||
boxes.Add(layout.scrollTrack, 3);
|
||||
boxes.Add(layout.scrollThumb, 4);
|
||||
boxes.Add(layout.opacityTrack, 3);
|
||||
boxes.Add(layout.opacityValue, 3);
|
||||
boxes.Add(layout.showDelta, 3);
|
||||
boxes.Add(layout.showEnum, 3);
|
||||
|
||||
boxes.AddAll(layout.rowChecks, 4);
|
||||
boxes.AddAll(layout.rowPoints, 4);
|
||||
boxes.AddAll(layout.rowIterations, 4);
|
||||
boxes.AddAll(layout.rowStats, 4);
|
||||
boxes.AddAll(layout.rowLevelTracks, 4);
|
||||
boxes.AddAll(layout.rowLevelValues, 4);
|
||||
boxes.AddAll(layout.rowIterTracks, 4);
|
||||
boxes.AddAll(layout.rowIterValues, 4);
|
||||
boxes.AddAll(layout.rowLevelResets, 4);
|
||||
boxes.AddAll(layout.rowIterResets, 4);
|
||||
boxes.AddAll(layout.rowNameRects, 4);
|
||||
boxes.AddAll(layout.rowNameCloses, 4);
|
||||
boxes.AddAll(layout.rowTriangles, 4);
|
||||
boxes.AddAll(layout.rowHeaders, 4);
|
||||
|
||||
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
|
||||
{
|
||||
const EdgePanelLayout layout =
|
||||
EdgePanelLayoutFor(laneIndex, static_cast<EdgePanelKind>(kindValue));
|
||||
if (!UiRectHasArea(layout.panel))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!panelLayerPushed)
|
||||
{
|
||||
boxes.SetLayer(baseLayer + 1);
|
||||
panelLayerPushed = true;
|
||||
}
|
||||
boxes.AddOccluder(layout.panel);
|
||||
// 面板内部元素的调试框同样裁到面板本身:面板滚动的内容区里,
|
||||
// 滚出可视区的行其矩形仍在(与弹出菜单的条目同一类问题)。
|
||||
boxes.SetClip(layout.panel);
|
||||
boxes.Add(layout.panel, 2);
|
||||
boxes.Add(layout.title, 3);
|
||||
boxes.Add(layout.close, 3);
|
||||
boxes.Add(layout.disclosure, 3);
|
||||
boxes.Add(layout.badge, 3);
|
||||
boxes.Add(layout.edit, 3);
|
||||
boxes.Add(layout.button, 3);
|
||||
boxes.Add(layout.content, 3);
|
||||
boxes.Add(layout.scrollTrack, 3);
|
||||
boxes.Add(layout.scrollThumb, 4);
|
||||
boxes.Add(layout.opacityTrack, 3);
|
||||
boxes.Add(layout.opacityValue, 3);
|
||||
boxes.Add(layout.showDelta, 3);
|
||||
boxes.Add(layout.showEnum, 3);
|
||||
|
||||
boxes.AddAll(layout.rowChecks, 4);
|
||||
boxes.AddAll(layout.rowPoints, 4);
|
||||
boxes.AddAll(layout.rowIterations, 4);
|
||||
boxes.AddAll(layout.rowStats, 4);
|
||||
boxes.AddAll(layout.rowLevelTracks, 4);
|
||||
boxes.AddAll(layout.rowLevelValues, 4);
|
||||
boxes.AddAll(layout.rowIterTracks, 4);
|
||||
boxes.AddAll(layout.rowIterValues, 4);
|
||||
boxes.AddAll(layout.rowLevelResets, 4);
|
||||
boxes.AddAll(layout.rowIterResets, 4);
|
||||
boxes.AddAll(layout.rowNameRects, 4);
|
||||
boxes.AddAll(layout.rowNameCloses, 4);
|
||||
|
||||
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
|
||||
{
|
||||
boxes.Add(group.header, 3);
|
||||
boxes.Add(group.tableHeader, 3);
|
||||
boxes.Add(group.triangle, 4);
|
||||
boxes.Add(group.close, 4);
|
||||
}
|
||||
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
|
||||
{
|
||||
boxes.Add(row.rect, 4);
|
||||
boxes.Add(row.close, 4);
|
||||
}
|
||||
boxes.Add(group.header, 3);
|
||||
boxes.Add(group.tableHeader, 3);
|
||||
boxes.Add(group.triangle, 4);
|
||||
boxes.Add(group.close, 4);
|
||||
boxes.Add(group.check, 4);
|
||||
}
|
||||
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
|
||||
{
|
||||
boxes.Add(row.rect, 4);
|
||||
boxes.Add(row.close, 4);
|
||||
}
|
||||
}
|
||||
// 收尾:解除面板裁剪并**复位层号**(宿主后续组件依赖它)。
|
||||
// 浮动面板之上的叠加层(滚动条 / 回到最上方 / 内联输入框)必须比**最上面板**再高一层。
|
||||
boxes.SetClip(gd::RectF());
|
||||
boxes.SetLayer(topOverlayLayer);
|
||||
boxes.Add(m_scrollBarRect, 5);
|
||||
boxes.Add(BackToTopRect(), 5);
|
||||
boxes.Add(EditFieldRect(), 5);
|
||||
boxes.SetLayer(baseLayer);
|
||||
}
|
||||
|
||||
@@ -848,17 +882,19 @@ namespace jdi
|
||||
// 这样浮动面板仍压在最上层,与其它实验性功能的层序一致)。
|
||||
PaintFreeMarker(g, m_lanes);
|
||||
// 从这里开始都在面板之上:文本层同步 +1(与 `BuildBoxes` 的 `baseLayer + 1` 一致)。
|
||||
// ⚠️ `PaintEdgeCursor` 内部会**逐面板**再把层号 +1(每个面板独占一层),
|
||||
// 并在返回前把层号推到**最上面板之上**,因此这里不再写死 `+2`。
|
||||
UiSetTextLayer(textLayer + 1);
|
||||
PaintEdgeCursor(g, m_lanes);
|
||||
// 之后这几样绘制在浮动面板**之上**:与 `BuildBoxes` 的 `baseLayer + 2` 对应。
|
||||
UiSetTextLayer(textLayer + 2);
|
||||
PaintSignalInteraction(g);
|
||||
PaintHint(g);
|
||||
// 网格 Y 轴上下限的输入框、以及「弱峰调整 → 迭代次数上限」的输入框都绘制在最上层
|
||||
//(后者覆盖在浮动面板的滑块行之上)。
|
||||
if (m_editTarget == PlotEditTarget::LANE_Y_MAX ||
|
||||
m_editTarget == PlotEditTarget::LANE_Y_MIN ||
|
||||
m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX)
|
||||
m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX ||
|
||||
m_editTarget == PlotEditTarget::MEAN_POINTS_MAX ||
|
||||
m_editTarget == PlotEditTarget::MEAN_ITERATIONS_MAX)
|
||||
{
|
||||
PaintEditField(g);
|
||||
}
|
||||
@@ -1105,7 +1141,9 @@ namespace jdi
|
||||
UiFillRect(g, m_area, m_theme->plotBg);
|
||||
|
||||
// 波形区域底色。
|
||||
UiFillRect(g, m_plotRect, m_theme->cardBg);
|
||||
// ⚠️ 「网格叠底」必须用 `ThemeLaneBackground()`:主题的 `cardBg` 与 `plotBg` 过于接近时
|
||||
// (深色主题)它会自动拉开一点点,保证网格这一层结构在任何主题下都看得见。
|
||||
UiFillRect(g, m_plotRect, ThemeLaneBackground(*m_theme));
|
||||
|
||||
// 图例命中矩形每帧重建,避免残留已消失的信号。
|
||||
m_legendHits.clear();
|
||||
@@ -1580,7 +1618,8 @@ namespace jdi
|
||||
const gd::RectF gridClip = laneClip;
|
||||
/** 标尺数值文本的裁剪矩形(GDI 文本需要显式裁剪,否则会越过绘图区)。 */
|
||||
const gd::RectF clipRectForLabels = gridClip;
|
||||
const std::uint32_t minorLineColor = UiBlendOver(m_theme->cardBg, WithAlpha(m_theme->gridWeak, kMinorLineAlpha));
|
||||
const std::uint32_t laneBackground = ThemeLaneBackground(*m_theme);
|
||||
const std::uint32_t minorLineColor = UiBlendOver(laneBackground, WithAlpha(m_theme->gridWeak, kMinorLineAlpha));
|
||||
const std::uint32_t weakLineColor = m_theme->gridWeak | 0xFF000000u;
|
||||
const std::uint32_t strongLineColor = m_theme->gridStrong | 0xFF000000u;
|
||||
|
||||
@@ -3145,12 +3184,14 @@ namespace jdi
|
||||
const float limitTop = lane.rect.Y - lane.rect.Height * 4.0f;
|
||||
const float limitBottom = lane.rect.Y + lane.rect.Height * 5.0f;
|
||||
|
||||
// 派生曲线的颜色:与原信号**同色相**但大幅拉开明度,使其与原始波形形成高对比
|
||||
// (原信号改颜色时曲线随之改变,因为它是从信号色派生出来的);透明度与原信号一致。
|
||||
// 派生曲线的颜色:与原信号**同色相**、亮度按**当前网格底色**自适应拉开,
|
||||
// 使其与原始波形、与背景都形成足够对比(原信号改颜色时曲线随之改变,因为它是从信号色
|
||||
// 派生出来的);透明度与原信号一致。
|
||||
const unsigned int baseAlpha = static_cast<unsigned int>(
|
||||
std::max(0.0, std::min(1.0, static_cast<double>(resolved.style->opacity))) * 255.0 + 0.5);
|
||||
const std::uint32_t derived = WithAlpha(HighContrastDerived(StyleColor(*resolved.style)),
|
||||
baseAlpha);
|
||||
const std::uint32_t derived = WithAlpha(
|
||||
HighContrastDerived(StyleColor(*resolved.style), ThemeLaneBackground(*m_theme)),
|
||||
baseAlpha);
|
||||
|
||||
// 顶点预算与波形一致(每像素列两个顶点)。
|
||||
// 交互期间进一步降到 1/2:派生曲线往往有 10 条(5 网格 × 均值滤波 + 弱峰调整),
|
||||
@@ -3480,6 +3521,10 @@ namespace jdi
|
||||
mix(lane.freeMarkerPanel.collapsed ? 1ull : 0ull);
|
||||
mix(lane.edgeValueValid ? 1ull : 0ull);
|
||||
mixDouble(lane.edgeValue);
|
||||
// 「自由标记」面板内容区的滚动偏移(历史行为:计入全局键)。
|
||||
// ⚠️ 「均值滤波」的滚动偏移只进**该面板自己的**槽键(见 `PanelSlotKey`),
|
||||
// 否则滚动一个面板会让屏幕上所有面板一起重画。
|
||||
mixFloat(lane.freeMarkerScroll);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3573,6 +3618,8 @@ namespace jdi
|
||||
mixRect(group.tableHeader);
|
||||
mixRect(group.triangle);
|
||||
mixRect(group.close);
|
||||
// 「均值滤波」的组头右端是复选框(「自由标记」为空矩形,不影响它的键)。
|
||||
mixRect(group.check);
|
||||
mix(group.collapsed ? 1ull : 0ull);
|
||||
mix(group.pinned ? 1ull : 0ull);
|
||||
mix(static_cast<std::uint64_t>(group.ref.groupIndex + 2));
|
||||
@@ -3616,6 +3663,11 @@ namespace jdi
|
||||
const Lane::MeanFilterItem* meanItem = lane.FindMeanFilterItem(ref);
|
||||
mix(static_cast<std::uint64_t>(meanItem != nullptr ? meanItem->points + 2 : 0));
|
||||
mix(static_cast<std::uint64_t>(meanItem != nullptr ? meanItem->iterations + 2 : 0));
|
||||
// 「均值滤波」改为滑块后,**滑块上限**决定圆钮位置、**收起状态**决定是否显示参数行
|
||||
// ⇒ 两者都直接改变面板像素,必须进键(否则拖完滑块仍会贴回旧像素)。
|
||||
mix(static_cast<std::uint64_t>(meanItem != nullptr ? meanItem->maxPoints + 2 : 0));
|
||||
mix(static_cast<std::uint64_t>(meanItem != nullptr ? meanItem->maxIterations + 2 : 0));
|
||||
mix((meanItem != nullptr && meanItem->collapsed) ? 1ull : 0ull);
|
||||
const Lane::EdgeCenterItem* centerItem = lane.FindEdgeCenterItem(ref);
|
||||
mix(static_cast<std::uint64_t>(centerItem != nullptr ? centerItem->points + 2 : 0));
|
||||
mix(static_cast<std::uint64_t>(centerItem != nullptr ? centerItem->iterations + 2 : 0));
|
||||
@@ -3704,6 +3756,17 @@ namespace jdi
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
// 「均值滤波」:信号色叠底标题栏 / 展开三角 / 内容区滚动偏移都要入键
|
||||
//(滚动改变的是面板内部像素,位置与尺寸不变)。
|
||||
for (const gd::RectF& rect : layout.rowHeaders)
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
for (const gd::RectF& rect : layout.rowTriangles)
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
mixFloat(layout.scrollOffset);
|
||||
|
||||
// 鼠标落在面板内时,✕ / 复选框 / 滑块 / 底部按钮的悬停态都依赖精确坐标。
|
||||
const float margin = 6.0f * m_scale;
|
||||
@@ -4067,12 +4130,23 @@ namespace jdi
|
||||
//(与"可拖拽"的光晕区分:这里只是单点圆环,不描整条曲线)。
|
||||
const float radius = 3.4f * m_scale;
|
||||
gd::SolidBrush dot(ToGdiColor(signalColor));
|
||||
g.FillEllipse(&dot, x - radius, probe.y - radius, radius * 2.0f, radius * 2.0f);
|
||||
// 取值圆点与"已吸附"圆环都画在**数据点本身**的横坐标上(`pointX`),
|
||||
// 而纵向光标画在鼠标位置(`x`)。稀疏信号下两者会分开 —— 这是刻意的:
|
||||
// 光标跟随鼠标、提示落在真实采样点上(用户 2026-10-30 要求)。
|
||||
float pointX = probe.pointX;
|
||||
if (!(pointX > 0.0f))
|
||||
{
|
||||
pointX = x; // 兼容:未填 `pointX` 时退回光标位置
|
||||
}
|
||||
g.FillEllipse(&dot, pointX - radius, probe.y - radius, radius * 2.0f,
|
||||
radius * 2.0f);
|
||||
if (probe.snapped)
|
||||
{
|
||||
gd::Pen ring(ToGdiColor(lineColor), 1.2f * m_scale);
|
||||
g.DrawEllipse(&ring, x - radius - 3.0f * m_scale, probe.y - radius - 3.0f * m_scale,
|
||||
(radius + 3.0f * m_scale) * 2.0f, (radius + 3.0f * m_scale) * 2.0f);
|
||||
g.DrawEllipse(&ring, pointX - radius - 3.0f * m_scale,
|
||||
probe.y - radius - 3.0f * m_scale,
|
||||
(radius + 3.0f * m_scale) * 2.0f,
|
||||
(radius + 3.0f * m_scale) * 2.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4094,6 +4168,9 @@ namespace jdi
|
||||
const std::size_t count = std::min(lanes.size(), tabLanes.size());
|
||||
const std::uint32_t color = CursorColor();
|
||||
const UiInk ink = UiContrastInk(color);
|
||||
// 进入本函数时的文本层号 = 宿主给本组件分配的面板层起始(`baseLayer + 1`)。
|
||||
// 每个面板在循环里各用 `panelTextLayer + 面板序数`,函数结束前把层号推到**最上面板之上**。
|
||||
const int panelTextLayer = UiGetTextLayer();
|
||||
|
||||
// ---- 横向光标(线与两端把手 + 左侧幅值读数)----
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
@@ -4283,6 +4360,10 @@ namespace jdi
|
||||
const PanelRef& panelRef = m_panelOrder[panelOrdinal];
|
||||
const EdgePanelKind kind = static_cast<EdgePanelKind>(panelRef.kind);
|
||||
const std::size_t index = static_cast<std::size_t>(panelRef.laneIndex);
|
||||
// 文本几何层号:**每个面板独占一层**,与 `BuildBoxes` 完全同源
|
||||
//(`baseLayer + 1 + 面板序数`,面板序数按 `m_panelOrder` 底 → 顶)。
|
||||
// 少了这一步,下层面板的文字框会记在比上层面板更低的层里被反复挖洞/穿透。
|
||||
UiSetTextLayer(panelTextLayer + static_cast<int>(panelOrdinal));
|
||||
const bool centerAxis = (kind == EdgePanelKind::CENTER);
|
||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
@@ -4353,11 +4434,43 @@ namespace jdi
|
||||
}
|
||||
if (hit != nullptr && hit->key != 0 && hit->key == slotKey)
|
||||
{
|
||||
// ⚠️⚠️ **只贴「面板圆角形状之内」的像素**(用户 2026-10-30 报告:
|
||||
// 中键移动视界时面板周围带着一圈**旧视界的波形像素**)。
|
||||
// 原因是缓存位图按 `panel ± cacheMargin` 取块,块的边缘那几像素是
|
||||
// **上一帧的场景**(波形/网格线);`PanelGlobalKey` 刻意不含视界
|
||||
// (否则平移会重画全部面板),于是整块贴回时就把旧波形"复印"了一圈。
|
||||
// 现在:① 先把**投影**按本帧重新画一遍(它在圆角形状之外,不能靠缓存);
|
||||
// ② 用圆角区域裁剪后只 BitBlt 面板本体(圆角之外仍是本帧场景)。
|
||||
if (!m_fastFrame)
|
||||
{
|
||||
UiFillRoundRect(g, gd::RectF(panel.X + 1.0f * m_scale,
|
||||
panel.Y + 2.0f * m_scale, panel.Width,
|
||||
panel.Height),
|
||||
5.0f * m_scale, 0x14000000u);
|
||||
}
|
||||
HDC target = g.GetHDC();
|
||||
if (target != nullptr)
|
||||
{
|
||||
BitBlt(target, hit->x0, hit->y0, hit->x1 - hit->x0, hit->y1 - hit->y0,
|
||||
m_panelDc, hit->x0, hit->y0, SRCCOPY);
|
||||
const int bx0 = static_cast<int>(std::floor(panel.X)) - 1;
|
||||
const int by0 = static_cast<int>(std::floor(panel.Y)) - 1;
|
||||
const int bx1 = static_cast<int>(std::ceil(panel.X + panel.Width)) + 1;
|
||||
const int by1 = static_cast<int>(std::ceil(panel.Y + panel.Height)) + 1;
|
||||
HRGN rounded = CreateRoundRectRgn(
|
||||
bx0, by0, bx1 + 1, by1 + 1,
|
||||
static_cast<int>(std::lround(10.0f * m_scale)),
|
||||
static_cast<int>(std::lround(10.0f * m_scale)));
|
||||
const int savedDc = SaveDC(target);
|
||||
if (rounded != nullptr)
|
||||
{
|
||||
ExtSelectClipRgn(target, rounded, RGN_AND);
|
||||
}
|
||||
BitBlt(target, bx0, by0, bx1 - bx0, by1 - by0, m_panelDc, bx0, by0,
|
||||
SRCCOPY);
|
||||
if (rounded != nullptr)
|
||||
{
|
||||
DeleteObject(rounded);
|
||||
}
|
||||
RestoreDC(target, savedDc);
|
||||
g.ReleaseHDC(target);
|
||||
}
|
||||
// 贴图不会真的画字 ⇒ 必须把上一帧渲染时存下的文本框**回放**给
|
||||
@@ -4701,6 +4814,191 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 「均值滤波」面板:逐信号的**信号色叠底标题栏**(左侧展开三角 + 信号名 +
|
||||
// 右端复选框)。复选框勾选后才出现三角、才能展开下面的两行参数滑块
|
||||
// (「均值点数」/「迭代次数」);内容超高时可滚动(滚动条在内容区右侧)。
|
||||
if (meanFilter)
|
||||
{
|
||||
const int meanSave = g.Save();
|
||||
// 内容必须裁到内容区:滚动时行会越出可视范围。
|
||||
g.SetClip(layout.content, gd::CombineModeIntersect);
|
||||
|
||||
const float valueColumn =
|
||||
std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"100000"),
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, L"1000"));
|
||||
|
||||
// 复选框自绘(主题色在信号色叠底上看不清):用**自适应对比色**描边/填充。
|
||||
const auto paintMeanCheck = [this, &g](const gd::RectF& rect, bool checked,
|
||||
const UiInk& ink,
|
||||
std::uint32_t background)
|
||||
{
|
||||
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const float radius = 2.5f * m_scale;
|
||||
if (!checked)
|
||||
{
|
||||
UiDrawRoundRect(g, rect, radius, ink.text, 1.2f * m_scale);
|
||||
return;
|
||||
}
|
||||
UiFillRoundRect(g, rect, radius, ink.text);
|
||||
// 对勾:两段折线,颜色取叠底色(与填充色必成对比)。
|
||||
const float left = rect.X + rect.Width * 0.22f;
|
||||
const float middleX = rect.X + rect.Width * 0.44f;
|
||||
const float right = rect.X + rect.Width * 0.78f;
|
||||
const float highY = rect.Y + rect.Height * 0.30f;
|
||||
const float lowY = rect.Y + rect.Height * 0.72f;
|
||||
UiDrawLine(g, left, rect.Y + rect.Height * 0.52f, middleX, lowY, background,
|
||||
1.6f * m_scale);
|
||||
UiDrawLine(g, middleX, lowY, right, highY, background, 1.6f * m_scale);
|
||||
};
|
||||
|
||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||
{
|
||||
const SignalRef& ref = layout.rowRefs[rowIndex];
|
||||
if (rowIndex >= layout.rowHeaders.size() ||
|
||||
rowIndex >= layout.rowChecks.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const gd::RectF& header = layout.rowHeaders[rowIndex];
|
||||
if (header.Y + header.Height <= layout.content.Y ||
|
||||
header.Y >= layout.content.Y + layout.content.Height)
|
||||
{
|
||||
continue; // 完全滚出可视范围
|
||||
}
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
const UiInk ink = UiContrastInk(signalColor);
|
||||
const Lane::MeanFilterItem* item = tabLane.FindMeanFilterItem(ref);
|
||||
const bool checked = (item != nullptr);
|
||||
|
||||
// 叠底:先铺一层不透明同色再叠 0xE8(与「自由标记」分组栏同一做法),
|
||||
// 这样滚动时下层内容不会透出来。
|
||||
UiFillRoundRect(g, header, 3.0f * m_scale, signalColor);
|
||||
UiFillRoundRect(g, header, 3.0f * m_scale, WithAlpha(signalColor, 0xE8));
|
||||
|
||||
// 左侧三角(仅勾选时存在):朝下 = 展开、朝右 = 收起。
|
||||
const gd::RectF& triangle = layout.rowTriangles[rowIndex];
|
||||
if (triangle.Width > 0.0f && item != nullptr)
|
||||
{
|
||||
UiDrawTriangleIcon(g, triangle, ink.text,
|
||||
item->collapsed ? IconDirection::RIGHT
|
||||
: IconDirection::DOWN);
|
||||
}
|
||||
|
||||
// 名称:从三角右侧到复选框左侧。
|
||||
const gd::RectF& check = layout.rowChecks[rowIndex];
|
||||
const float nameLeft = (triangle.Width > 0.0f)
|
||||
? (triangle.X + triangle.Width + 6.0f * m_scale)
|
||||
: (header.X + 8.0f * m_scale);
|
||||
const float nameWidth =
|
||||
std::max(10.0f, check.X - 6.0f * m_scale - nameLeft);
|
||||
UiTextClipped(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall, nameWidth),
|
||||
m_fonts->uiSmall,
|
||||
gd::RectF(nameLeft, header.Y, nameWidth, header.Height), ink.text,
|
||||
TextAlign::LEFT, layout.content);
|
||||
paintMeanCheck(check, checked, ink, signalColor);
|
||||
|
||||
if (!checked || item->collapsed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 两行参数滑块(标签 / 轨道 / 数值 / 复位按钮)。
|
||||
const auto paintMeanSlider = [&](const gd::RectF& track,
|
||||
const gd::RectF& valueRect,
|
||||
const gd::RectF& resetRect,
|
||||
const wchar_t* label, int current, int minimum,
|
||||
int maximum, bool active)
|
||||
{
|
||||
if (track.Width <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const float labelLeft = track.X - 6.0f * m_scale -
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, label);
|
||||
UiText(g, label, m_fonts->uiSmall,
|
||||
gd::RectF(labelLeft, track.Y - 8.0f * m_scale,
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, label),
|
||||
track.Height + 16.0f * m_scale),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
const double ratio =
|
||||
(maximum > minimum)
|
||||
? (static_cast<double>(current - minimum) /
|
||||
static_cast<double>(maximum - minimum))
|
||||
: 0.0;
|
||||
UiDrawSlider(g, track, ratio, m_theme->accent, active, m_scale);
|
||||
UiText(g, std::to_wstring(current), m_fonts->monoSmall,
|
||||
gd::RectF(valueRect.X, valueRect.Y, valueColumn, valueRect.Height),
|
||||
m_theme->textStrong, TextAlign::LEFT);
|
||||
// 复位按钮:悬停高亮 + 与「系数/偏移」菜单同款图标。
|
||||
const bool hot =
|
||||
(m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) &&
|
||||
UiPointInRect(resetRect, static_cast<float>(m_mouseX),
|
||||
static_cast<float>(m_mouseY));
|
||||
if (hot)
|
||||
{
|
||||
UiFillRoundRect(
|
||||
g, gd::RectF(resetRect.X - 1.0f * m_scale,
|
||||
resetRect.Y - 1.0f * m_scale,
|
||||
resetRect.Width + 2.0f * m_scale,
|
||||
resetRect.Height + 2.0f * m_scale),
|
||||
3.0f * m_scale, WithAlpha(m_theme->accent, 0x33));
|
||||
}
|
||||
PaintPanelResetIcon(g, resetRect,
|
||||
hot ? m_theme->accent : m_theme->textMuted, m_scale);
|
||||
};
|
||||
|
||||
const int pointsMax = std::max(kMeanFilterPointsMin,
|
||||
std::min(kMeanFilterPointsMaxLimit,
|
||||
item->maxPoints));
|
||||
const int points =
|
||||
std::max(kMeanFilterPointsMin, std::min(pointsMax, item->points));
|
||||
const int iterationsMax = std::max(kMeanFilterIterationsMin,
|
||||
std::min(kMeanFilterIterationsMaxLimit,
|
||||
item->maxIterations));
|
||||
const int iterations = std::max(
|
||||
kMeanFilterIterationsMin, std::min(iterationsMax, item->iterations));
|
||||
const bool pointsActive = (m_dragMode == PlotDragMode::MEAN_POINTS) &&
|
||||
(m_meanDragLane == static_cast<int>(index)) &&
|
||||
(m_meanDragRef == ref);
|
||||
const bool iterationsActive =
|
||||
(m_dragMode == PlotDragMode::MEAN_ITERATIONS) &&
|
||||
(m_meanDragLane == static_cast<int>(index)) && (m_meanDragRef == ref);
|
||||
if (rowIndex < layout.rowLevelTracks.size())
|
||||
{
|
||||
paintMeanSlider(layout.rowLevelTracks[rowIndex],
|
||||
rowIndex < layout.rowLevelValues.size()
|
||||
? layout.rowLevelValues[rowIndex]
|
||||
: gd::RectF(),
|
||||
rowIndex < layout.rowLevelResets.size()
|
||||
? layout.rowLevelResets[rowIndex]
|
||||
: gd::RectF(),
|
||||
kMeanFilterPointsSliderLabel, points,
|
||||
kMeanFilterPointsMin, pointsMax, pointsActive);
|
||||
}
|
||||
if (rowIndex < layout.rowIterTracks.size())
|
||||
{
|
||||
paintMeanSlider(layout.rowIterTracks[rowIndex],
|
||||
rowIndex < layout.rowIterValues.size()
|
||||
? layout.rowIterValues[rowIndex]
|
||||
: gd::RectF(),
|
||||
rowIndex < layout.rowIterResets.size()
|
||||
? layout.rowIterResets[rowIndex]
|
||||
: gd::RectF(),
|
||||
kMeanFilterIterationsSliderLabel, iterations,
|
||||
kMeanFilterIterationsMin, iterationsMax, iterationsActive);
|
||||
}
|
||||
}
|
||||
|
||||
g.Restore(meanSave);
|
||||
}
|
||||
// 「自由标记」面板:按信号分组的**分组栏**(信号色托底 + 三角 + 名称 +
|
||||
// 「N 标记点」+ ✕),下属固定点条目六列对齐(序号 / 取值 / 枚举 / 增量 / 时刻 / 间隔),
|
||||
// 行尾各带一个 ✕。内容区有滚动偏移时按内容区裁剪。
|
||||
@@ -5166,6 +5464,10 @@ namespace jdi
|
||||
m_panelSlots = std::move(nextSlots);
|
||||
m_panelGlobalKey = panelGlobalKey;
|
||||
m_panelValid = panelCacheReady;
|
||||
|
||||
// 面板之后绘制的叠加层(信号拖拽虚影 / 提示 / 输入框 / 滚动条 / 回到最上方)
|
||||
// 都在**所有面板之上** ⇒ 文本层号推到最上面板的上一层(与 `BuildBoxes` 同源)。
|
||||
UiSetTextLayer(panelTextLayer + static_cast<int>(m_panelOrder.size()));
|
||||
}
|
||||
|
||||
void PlotView::PaintEditField(gd::Graphics& g)
|
||||
@@ -5181,10 +5483,21 @@ namespace jdi
|
||||
|
||||
// 「迭代次数 N 上限」:输入框**以空内容开始**,未输入任何内容时以灰色提示当前上限值。
|
||||
// 提示文案 = “输入上限(当前N)”(用户明确要求:旧实现只显示一个孤立的数字,缺少说明)。
|
||||
if (m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX && m_edit.Text().empty())
|
||||
// 「均值滤波」两行滑块的上限输入框共用同一套提示文案。
|
||||
if ((m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX ||
|
||||
m_editTarget == PlotEditTarget::MEAN_POINTS_MAX ||
|
||||
m_editTarget == PlotEditTarget::MEAN_ITERATIONS_MAX) &&
|
||||
m_edit.Text().empty())
|
||||
{
|
||||
const int currentMax = m_peakEditCurrentMax > 0 ? m_peakEditCurrentMax
|
||||
: kWeakPeakIterationsDefaultMax;
|
||||
const bool peakTarget = (m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX);
|
||||
const int currentMax =
|
||||
peakTarget
|
||||
? ((m_peakEditCurrentMax > 0) ? m_peakEditCurrentMax : kWeakPeakIterationsDefaultMax)
|
||||
: ((m_meanEditCurrentMax > 0)
|
||||
? m_meanEditCurrentMax
|
||||
: ((m_editTarget == PlotEditTarget::MEAN_POINTS_MAX)
|
||||
? kMeanFilterPointsDefaultMax
|
||||
: kMeanFilterIterationsDefaultMax));
|
||||
const std::wstring hint = L"输入上限(当前" + std::to_wstring(currentMax) + L")";
|
||||
const float hintLeft = fieldRect.X + 11.0f * m_scale;
|
||||
UiTextAtClipped(g, hint, m_fonts->monoSmall, hintLeft,
|
||||
|
||||
Reference in new issue
Block a user