refactor(panels): 平行数组合并为 std::vector<PanelRow>(消除错位与字段重载)

对应 Code_Review_Report §15.6 第 1 项(方案 B:有结构,无列视图)。

结构
- EdgePanelLayout 的 16 个平行 std::vector 合并为 std::vector<PanelRow> rows,
  PanelRow 带 17 个具名成员:ref / header / nameRect / nameClose / triangle / check /
  stats / levelTrack / levelValue / levelReset / iterTrack / iterValue / iterReset /
  infoMaxInterval / infoMinInterval / intervalField / frequencyField。
- 唯一新增入口:AppendRow(ref, header, nameRect, triangle, check)(返回新行引用)与
  AppendMarkerGroupRow(ref, nameRect, nameClose)(「自由标记」分组行)。
  参数行/重采样行一律 layout.rows.back().xxx = … 赋值。
- 原 int rows(可见信号数)与新容器同名 ⇒ 改名 signalCount,并注明它与 rows.size()
  在 MARK 面板里不相等(只有有固定点的信号才有行)。

消掉的两类问题
1. 某分支漏 push 一列 ⇒ 该列之后所有行读到别人的矩形(静默错位)。现在一行一次成型。
2. rowRefs 字段重载(列表式 = 行信号 / MARK = 分组信号,其余 12 列必须为空)。
   现在每行自带 ref,语义由“填哪些字段”表达;不变式简化为语义断言
   (每行必须有名称区;MARK 行不得带 header/check/stats/滑块/重采样字段)。

顺带清理
- 合并后 rowIndex < layout.rowXxx.size() 全部变成恒真,批量删除 50+ 处
  (含 15 元组长度数组、? x : gd::RectF() 三元判空、if(...){extendRow(...)} 包裹)。
- 调试框与缓存键改为逐行遍历(新增字段只需在 PanelRow 与这两处循环各加一行)。

等价性证明(rowgeom 键名刻意保持不变,故可逐键比对)
  old keys = 919, new keys = 965
  仅存在于旧 = 0;已有键值变化 = 20(全部是 ".count":0 -> N)
  仅存在于新 = 46(全部是新增列的 [NNN],值 0.00,0.00,0.00,0.00)
⇒ 逐行逐字段的真实几何零位移;7 个面板像素哈希(含 resample=2BFC74B3D9B3239C)逐项不变。

验证
- make release:零 warning;784,896 → 782,336 B(−2,560);make debug / make -C Tools 同样零 warning
- UnitTests:checks=260 failed=0 cases=19
- OffscreenChecks -all:7 检查 / 133 断言(上轮 127)全 PASS
- panels -mode compare:7 面板像素哈希逐项不变
- rowgeom -mode compare:965+7 键 0 差异;-data MF4-Record.MF4 时 1268+7 键 0 差异
- 读点:旧 243 处 layout.rowXxx[…] → 现 95 处 layout.rows[…](84 命中判定 / 11 绘制)

过程记录:清理恒真判空时曾用整段替换把「均值滤波」双滑块段误换成「弱峰」双滑块,
编译器报 brace 不平衡,用备份回滚后重做 —— 整段替换必须先精确读出上下文。

文档:Plan §十一、Progress 第 39 轮、Report §16、agent_mem 第 39 轮、Tools/README §4.1/§4.4
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-04 16:18:56 +08:00
1 parent f07ac74988
commit 1463f9d3f8
11 files changed
+672 -591

No files matched your search

+102 -135
View File
@@ -1127,36 +1127,36 @@ namespace jdi
Tab().Lanes()[static_cast<std::size_t>(panelLane)];
if (listCursor)
{
for (std::size_t rowIndex = 0; rowIndex < layout.rowHeaders.size(); ++rowIndex)
for (std::size_t rowIndex = 0; rowIndex < layout.rows.size(); ++rowIndex)
{
if (layout.rowHeaders[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowHeaders[rowIndex], static_cast<float>(x),
if (layout.rows[rowIndex].header.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].header, static_cast<float>(x),
static_cast<float>(y)))
{
bool checked = false;
if (cursorKind == EdgePanelKind::PEAK)
{
checked = cursorLane.FindWeakPeakItem(
layout.rowRefs[rowIndex]) != nullptr;
layout.rows[rowIndex].ref) != nullptr;
}
else if (cursorKind == EdgePanelKind::CENTER)
{
checked = cursorLane.FindEdgeCenterItem(
layout.rowRefs[rowIndex]) != nullptr;
layout.rows[rowIndex].ref) != nullptr;
}
else if (cursorKind == EdgePanelKind::RESAMPLE)
{
checked = cursorLane.FindResampleItem(
layout.rowRefs[rowIndex]) != nullptr;
layout.rows[rowIndex].ref) != nullptr;
}
else if (cursorKind == EdgePanelKind::CLIP)
{
checked = cursorLane.ClipEnabled(layout.rowRefs[rowIndex]);
checked = cursorLane.ClipEnabled(layout.rows[rowIndex].ref);
}
else
{
checked = cursorLane.FindMeanFilterItem(
layout.rowRefs[rowIndex]) != nullptr;
layout.rows[rowIndex].ref) != nullptr;
}
// ⚠️⚠️ **「选区截取」的条目栏不可展开**(只有右侧复选框有效):
// 因此条目栏本体**绝不能**给手型 —— 否则会误导用户去点它
@@ -1174,14 +1174,12 @@ namespace jdi
if (cursorKind == EdgePanelKind::RESAMPLE)
{
// 两行输入框是可点击控件(点击进入就地编辑)。
if (rowIndex < layout.rowIntervalField.size() &&
UiPointInRect(layout.rowIntervalField[rowIndex], static_cast<float>(x),
if (UiPointInRect(layout.rows[rowIndex].intervalField, static_cast<float>(x),
static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_HAND);
}
if (rowIndex < layout.rowFrequencyField.size() &&
UiPointInRect(layout.rowFrequencyField[rowIndex], static_cast<float>(x),
if (UiPointInRect(layout.rows[rowIndex].frequencyField, static_cast<float>(x),
static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_HAND);
@@ -1189,50 +1187,43 @@ namespace jdi
continue;
}
const bool hasParams =
rowIndex < layout.rowLevelTracks.size() &&
layout.rowLevelTracks[rowIndex].Width > 0.0f;
layout.rows[rowIndex].levelTrack.Width > 0.0f;
if (!hasParams)
{
continue; // 参数行未展开(未勾选 / 已收起)⇒ 该行没有可交互控件
}
// 「均值点数 / 调整幅度」数值:均值滤波与中心轴可点击改上限(弱峰不能)。
if (cursorKind != EdgePanelKind::PEAK &&
rowIndex < layout.rowLevelValues.size() &&
layout.rowLevelValues[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowLevelValues[rowIndex], static_cast<float>(x),
layout.rows[rowIndex].levelValue.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].levelValue, static_cast<float>(x),
static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_HAND);
}
if (rowIndex < layout.rowIterValues.size() &&
layout.rowIterValues[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowIterValues[rowIndex], static_cast<float>(x),
if (layout.rows[rowIndex].iterValue.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].iterValue, static_cast<float>(x),
static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_HAND);
}
if (rowIndex < layout.rowLevelResets.size() &&
layout.rowLevelResets[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowLevelResets[rowIndex], static_cast<float>(x),
if (layout.rows[rowIndex].levelReset.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].levelReset, static_cast<float>(x),
static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_HAND);
}
if (rowIndex < layout.rowIterResets.size() &&
layout.rowIterResets[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowIterResets[rowIndex], static_cast<float>(x),
if (layout.rows[rowIndex].iterReset.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].iterReset, static_cast<float>(x),
static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_HAND);
}
if (rowIndex < layout.rowLevelTracks.size() &&
WeakPeakSliderHit(layout.rowLevelTracks[rowIndex], m_scale,
if (WeakPeakSliderHit(layout.rows[rowIndex].levelTrack, m_scale,
static_cast<float>(x), static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_SIZEWE);
}
if (rowIndex < layout.rowIterTracks.size() &&
WeakPeakSliderHit(layout.rowIterTracks[rowIndex], m_scale,
if (WeakPeakSliderHit(layout.rows[rowIndex].iterTrack, m_scale,
static_cast<float>(x), static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_SIZEWE);
@@ -1542,13 +1533,13 @@ namespace jdi
}
Lane::WeakPeakItem& item = lane.EnsureWeakPeakItem(m_peakDragRef);
const int rowIndex = PeakRowIndexOf(layout, m_peakDragRef);
if (rowIndex < 0 || rowIndex >= static_cast<int>(layout.rowLevelTracks.size()) ||
layout.rowLevelTracks[static_cast<std::size_t>(rowIndex)].Width <= 0.0f)
if (rowIndex < 0 || rowIndex >= static_cast<int>(layout.rows.size()) ||
layout.rows[static_cast<std::size_t>(rowIndex)].levelTrack.Width <= 0.0f)
{
break;
}
const double ratio =
UiSliderRatioFromX(layout.rowLevelTracks[static_cast<std::size_t>(rowIndex)], fx);
UiSliderRatioFromX(layout.rows[static_cast<std::size_t>(rowIndex)].levelTrack, fx);
// 滑块量程 -200% – +200%:0 在正中间。
// 滑块量程 -400% – +400%(三组规则的梯度区间):0 在正中间。
item.amountPercent = std::max(
@@ -1575,13 +1566,13 @@ namespace jdi
}
Lane::WeakPeakItem& item = lane.EnsureWeakPeakItem(m_peakDragRef);
const int rowIndex = PeakRowIndexOf(layout, m_peakDragRef);
if (rowIndex < 0 || rowIndex >= static_cast<int>(layout.rowIterTracks.size()) ||
layout.rowIterTracks[static_cast<std::size_t>(rowIndex)].Width <= 0.0f)
if (rowIndex < 0 || rowIndex >= static_cast<int>(layout.rows.size()) ||
layout.rows[static_cast<std::size_t>(rowIndex)].iterTrack.Width <= 0.0f)
{
break;
}
const double ratio =
UiSliderRatioFromX(layout.rowIterTracks[static_cast<std::size_t>(rowIndex)], fx);
UiSliderRatioFromX(layout.rows[static_cast<std::size_t>(rowIndex)].iterTrack, fx);
const int peakMax = std::max(kWeakPeakIterationsMin,
std::min(kWeakPeakIterationsMaxLimit, item.maxIterations));
item.iterations = std::max(
@@ -1629,9 +1620,9 @@ namespace jdi
const EdgePanelLayout layout = EdgePanelLayoutFor(
m_meanDragLane, m_meanDragKind);
int rowIndex = -1;
for (std::size_t candidate = 0; candidate < layout.rowRefs.size(); ++candidate)
for (std::size_t candidate = 0; candidate < layout.rows.size(); ++candidate)
{
if (layout.rowRefs[candidate] == m_meanDragRef)
if (layout.rows[candidate].ref == m_meanDragRef)
{
rowIndex = static_cast<int>(candidate);
break;
@@ -1641,15 +1632,15 @@ namespace jdi
{
break;
}
const std::vector<gd::RectF>& tracks =
pointsMode ? layout.rowLevelTracks : layout.rowIterTracks;
if (static_cast<std::size_t>(rowIndex) >= tracks.size() ||
tracks[static_cast<std::size_t>(rowIndex)].Width <= 0.0f)
// 两行滑块的轨道由 `pointsMode` 决定:这里取**引用**而不是"整个数组",
// 因为行字段现在同属一个 `PanelRow`(不再有可切换的平行数组)。
const gd::RectF& track = pointsMode ? layout.rows[static_cast<std::size_t>(rowIndex)].levelTrack
: layout.rows[static_cast<std::size_t>(rowIndex)].iterTrack;
if (track.Width <= 0.0f)
{
break;
}
const double ratio =
UiSliderRatioFromX(tracks[static_cast<std::size_t>(rowIndex)], fx);
const double ratio = UiSliderRatioFromX(track, fx);
if (pointsMode)
{
// ⚠️ 下限:均值滤波 = 1(用户 2026-10-30 要求不能为 0);中心轴 = 0(0 = 不平滑)。
@@ -2006,20 +1997,16 @@ namespace jdi
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoverKind);
if (UiPointInRect(layout.content, static_cast<float>(x), static_cast<float>(y)))
{
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
for (std::size_t rowIndex = 0; rowIndex < layout.rows.size(); ++rowIndex)
{
if (rowIndex >= layout.rowHeaders.size())
{
break;
}
const gd::RectF& headerRect = layout.rowHeaders[rowIndex];
const gd::RectF& headerRect = layout.rows[rowIndex].header;
// 半截滚出的行**也算命中**:绘制时行本来就会被裁剪成半截显示,
// 若这里要求“完整落在内容区内”,就会出现“看得见却悬停不到”的不一致。
// 命中点本身已被限定在内容区里,因此不会出现“看得见一半却点到”的幽灵热区。
if (UiPointInRect(headerRect, static_cast<float>(x), static_cast<float>(y)))
{
hoverLane = panelLane;
hoverRef = layout.rowRefs[rowIndex];
hoverRef = layout.rows[rowIndex].ref;
break;
}
}
@@ -2042,17 +2029,17 @@ namespace jdi
static_cast<EdgePanelKind>(hoverPanelKind) == EdgePanelKind::PEAK)
{
const EdgePanelLayout resetLayout = EdgePanelLayoutFor(panelLane, EdgePanelKind::PEAK);
for (std::size_t rowIndex = 0; rowIndex < resetLayout.rowLevelResets.size(); ++rowIndex)
for (std::size_t rowIndex = 0; rowIndex < resetLayout.rows.size(); ++rowIndex)
{
const gd::RectF& level = resetLayout.rowLevelResets[rowIndex];
const gd::RectF& level = resetLayout.rows[rowIndex].levelReset;
if (level.Width > 0.0f && UiPointInRect(level, fx, static_cast<float>(y)))
{
overPeakReset = true;
break;
}
if (rowIndex < resetLayout.rowIterResets.size())
if (rowIndex < resetLayout.rows.size())
{
const gd::RectF& iter = resetLayout.rowIterResets[rowIndex];
const gd::RectF& iter = resetLayout.rows[rowIndex].iterReset;
if (iter.Width > 0.0f && UiPointInRect(iter, fx, static_cast<float>(y)))
{
overPeakReset = true;
@@ -2456,39 +2443,37 @@ namespace jdi
// 条目栏 = 复选框切换 + 已勾选时整行收起/展开(与其它列表式面板一致)。
if (resampleMode)
{
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
for (std::size_t rowIndex = 0; rowIndex < layout.rows.size(); ++rowIndex)
{
if (!insideContent)
{
break;
}
const SignalRef& ref = layout.rowRefs[rowIndex];
const SignalRef& ref = layout.rows[rowIndex].ref;
const Lane::ResampleItem* item = meanLane.FindResampleItem(ref);
if (item == nullptr)
{
continue;
}
// 点击「重采样间隔」输入框 → 编辑**秒**数(不带单位、不用科学计数法)。
if (rowIndex < layout.rowIntervalField.size() &&
layout.rowIntervalField[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowIntervalField[rowIndex], fx, fy))
if (layout.rows[rowIndex].intervalField.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].intervalField, fx, fy))
{
m_editMeanRef = ref;
m_editMeanKind = kind;
const gd::RectF& field = layout.rowIntervalField[rowIndex];
const gd::RectF& field = layout.rows[rowIndex].intervalField;
BeginEdit(PlotEditTarget::RESAMPLE_INTERVAL, index,
FormatDurationSeconds(item->interval), field);
m_dragMode = PlotDragMode::NONE;
return true;
}
// 点击「重采样频率」输入框 → 编辑频率(Hz,与间隔联动;不带单位)。
if (rowIndex < layout.rowFrequencyField.size() &&
layout.rowFrequencyField[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowFrequencyField[rowIndex], fx, fy))
if (layout.rows[rowIndex].frequencyField.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].frequencyField, fx, fy))
{
m_editMeanRef = ref;
m_editMeanKind = kind;
const gd::RectF& field = layout.rowFrequencyField[rowIndex];
const gd::RectF& field = layout.rows[rowIndex].frequencyField;
const double hz = (item->interval > 0.0) ? (1.0 / item->interval) : 0.0;
BeginEdit(PlotEditTarget::RESAMPLE_FREQUENCY, index,
FormatDecimal(hz, 12), field);
@@ -2497,19 +2482,17 @@ namespace jdi
}
}
// 条目栏(信号色叠底):复选框切换;已勾选时整行切换参数行收起/展开。
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
for (std::size_t rowIndex = 0; rowIndex < layout.rows.size(); ++rowIndex)
{
const SignalRef& ref = layout.rowRefs[rowIndex];
if (!insideContent || rowIndex >= layout.rowHeaders.size() ||
layout.rowHeaders[rowIndex].Width <= 0.0f ||
!UiPointInRect(layout.rowHeaders[rowIndex], fx, fy))
const SignalRef& ref = layout.rows[rowIndex].ref;
if (!insideContent || layout.rows[rowIndex].header.Width <= 0.0f ||
!UiPointInRect(layout.rows[rowIndex].header, fx, fy))
{
continue;
}
const bool onCheckBox =
rowIndex < layout.rowChecks.size() &&
layout.rowChecks[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowChecks[rowIndex], fx, fy);
layout.rows[rowIndex].check.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].check, fx, fy);
const Lane::ResampleItem* existing = meanLane.FindResampleItem(ref);
if (onCheckBox)
{
@@ -2577,15 +2560,14 @@ namespace jdi
const bool doubleCursor = (Tab().CursorMode() == PlotCursorMode::DOUBLE);
Lane& clipLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
// 条目复选框:切换该信号是否为截取目标。
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
for (std::size_t rowIndex = 0; rowIndex < layout.rows.size(); ++rowIndex)
{
if (!insideContent || rowIndex >= layout.rowChecks.size() ||
layout.rowChecks[rowIndex].Width <= 0.0f ||
!UiPointInRect(layout.rowChecks[rowIndex], fx, fy))
if (!insideContent || layout.rows[rowIndex].check.Width <= 0.0f ||
!UiPointInRect(layout.rows[rowIndex].check, fx, fy))
{
continue;
}
ToggleLaneClipSignal(index, layout.rowRefs[rowIndex]);
ToggleLaneClipSignal(index, layout.rows[rowIndex].ref);
m_meanRowHoverLane = -1;
m_meanRowHoverRef = SignalRef();
m_dragMode = PlotDragMode::NONE;
@@ -2651,26 +2633,24 @@ namespace jdi
// 语义与该面板旧式排版完全一致,只是外观改成与均值滤波同款的可滚动列表。
if (boostMode)
{
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
for (std::size_t rowIndex = 0; rowIndex < layout.rows.size(); ++rowIndex)
{
const SignalRef& ref = layout.rowRefs[rowIndex];
const SignalRef& ref = layout.rows[rowIndex].ref;
if (!insideContent)
{
break;
}
const Lane::WeakPeakItem* peakItem = meanLane.FindWeakPeakItem(ref);
const bool hasParams =
peakItem != nullptr && rowIndex < layout.rowLevelTracks.size() &&
layout.rowLevelTracks[rowIndex].Width > 0.0f;
peakItem != nullptr && layout.rows[rowIndex].levelTrack.Width > 0.0f;
if (!hasParams)
{
continue;
}
Lane::WeakPeakItem& item = meanLane.EnsureWeakPeakItem(ref);
// 复位:调整幅度 → 0%
if (rowIndex < layout.rowLevelResets.size() &&
layout.rowLevelResets[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowLevelResets[rowIndex], fx, fy))
if (layout.rows[rowIndex].levelReset.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].levelReset, fx, fy))
{
item.amountPercent = 0;
m_weakPeakCurveCache.clear();
@@ -2678,9 +2658,8 @@ namespace jdi
return true;
}
// 复位:迭代次数 → 1(上限恢复为默认上限)。
if (rowIndex < layout.rowIterResets.size() &&
layout.rowIterResets[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowIterResets[rowIndex], fx, fy))
if (layout.rows[rowIndex].iterReset.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].iterReset, fx, fy))
{
item.iterations = kWeakPeakIterationsMin;
item.maxIterations = kWeakPeakIterationsDefaultMax;
@@ -2690,14 +2669,13 @@ namespace jdi
}
// 迭代次数行的**数值文本**:点击后整行变成输入框以修改滑块上限。
// ⚠️ 必须放在轨道命中之前(数值区在轨道右侧,命中区不重叠,但顺序更安全)。
if (rowIndex < layout.rowIterValues.size() &&
layout.rowIterValues[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowIterValues[rowIndex], fx, fy))
if (layout.rows[rowIndex].iterValue.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].iterValue, fx, fy))
{
m_editMeanRef = ref;
m_peakEditCurrentMax = item.maxIterations;
const gd::RectF& track = layout.rowIterTracks[rowIndex];
const gd::RectF& value = layout.rowIterValues[rowIndex];
const gd::RectF& track = layout.rows[rowIndex].iterTrack;
const gd::RectF& value = layout.rows[rowIndex].iterValue;
const gd::RectF rowRect(track.X, value.Y, value.X + value.Width - track.X,
value.Height);
BeginEdit(PlotEditTarget::PEAK_ITERATIONS_MAX, index, L"", rowRect);
@@ -2706,14 +2684,13 @@ namespace jdi
return true;
}
// 拖动「迭代次数」滑块。
if (rowIndex < layout.rowIterTracks.size() &&
WeakPeakSliderHit(layout.rowIterTracks[rowIndex], m_scale, fx, fy))
if (WeakPeakSliderHit(layout.rows[rowIndex].iterTrack, m_scale, fx, fy))
{
m_dragMode = PlotDragMode::PEAK_ITERATIONS;
m_peakDragLane = index;
m_peakDragRef = ref;
const double ratio =
UiSliderRatioFromX(layout.rowIterTracks[rowIndex], fx);
UiSliderRatioFromX(layout.rows[rowIndex].iterTrack, fx);
const int peakMax = std::max(
kWeakPeakIterationsMin,
std::min(kWeakPeakIterationsMaxLimit, item.maxIterations));
@@ -2727,14 +2704,13 @@ namespace jdi
return true;
}
// 拖动「调整幅度」滑块(0 在正中间 ⇒ ±400%)。
if (rowIndex < layout.rowLevelTracks.size() &&
WeakPeakSliderHit(layout.rowLevelTracks[rowIndex], m_scale, fx, fy))
if (WeakPeakSliderHit(layout.rows[rowIndex].levelTrack, m_scale, fx, fy))
{
m_dragMode = PlotDragMode::PEAK_LEVEL;
m_peakDragLane = index;
m_peakDragRef = ref;
item.amountPercent = static_cast<int>(std::lround(
(UiSliderRatioFromX(layout.rowLevelTracks[rowIndex], fx) * 2.0 - 1.0) *
(UiSliderRatioFromX(layout.rows[rowIndex].levelTrack, fx) * 2.0 - 1.0) *
100.0 * kWeakPeakSliderScale));
item.amountPercent = std::max(
kWeakPeakAmountMin,
@@ -2746,9 +2722,9 @@ namespace jdi
}
else
{
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
for (std::size_t rowIndex = 0; rowIndex < layout.rows.size(); ++rowIndex)
{
const SignalRef& ref = layout.rowRefs[rowIndex];
const SignalRef& ref = layout.rows[rowIndex].ref;
if (!insideContent)
{
break;
@@ -2756,16 +2732,14 @@ namespace jdi
// 行可以“半截”显示(绘制时会按内容区裁剪),因此这里不再要求整行落在内容区;
// 命中点已由上面的 `insideContent` 限定在内容区内,不会抢走底部按钮的点击。
const bool hasParams =
rowIndex < layout.rowLevelTracks.size() &&
layout.rowLevelTracks[rowIndex].Width > 0.0f &&
layout.rows[rowIndex].levelTrack.Width > 0.0f &&
findItem(ref) != nullptr;
if (hasParams)
{
Lane::MeanFilterItem& item = ensureItem(ref);
// 复位:均值点数 → 默认值(同时把滑块上限恢复为默认上限)。
if (rowIndex < layout.rowLevelResets.size() &&
layout.rowLevelResets[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowLevelResets[rowIndex], fx, fy))
if (layout.rows[rowIndex].levelReset.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].levelReset, fx, fy))
{
item.points = kMeanFilterPointsDefault;
item.maxPoints = kMeanFilterPointsDefaultMax;
@@ -2778,9 +2752,8 @@ namespace jdi
return true;
}
// 复位:迭代次数 → 1(上限恢复为默认上限)。
if (rowIndex < layout.rowIterResets.size() &&
layout.rowIterResets[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowIterResets[rowIndex], fx, fy))
if (layout.rows[rowIndex].iterReset.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].iterReset, fx, fy))
{
item.iterations = kMeanFilterIterationsMin;
item.maxIterations = kMeanFilterIterationsDefaultMax;
@@ -2793,15 +2766,14 @@ namespace jdi
return true;
}
// 点击「均值点数」右侧数值 → 输入滑块上限。
if (rowIndex < layout.rowLevelValues.size() &&
layout.rowLevelValues[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowLevelValues[rowIndex], fx, fy))
if (layout.rows[rowIndex].levelValue.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].levelValue, fx, fy))
{
m_editMeanRef = ref;
m_meanEditCurrentMax = item.maxPoints;
m_editMeanKind = kind;
const gd::RectF& track = layout.rowLevelTracks[rowIndex];
const gd::RectF& value = layout.rowLevelValues[rowIndex];
const gd::RectF& track = layout.rows[rowIndex].levelTrack;
const gd::RectF& value = layout.rows[rowIndex].levelValue;
const gd::RectF rowRect(track.X, value.Y,
value.X + value.Width - track.X,
value.Height);
@@ -2811,15 +2783,14 @@ namespace jdi
return true;
}
// 点击「迭代次数」右侧数值 → 输入滑块上限。
if (rowIndex < layout.rowIterValues.size() &&
layout.rowIterValues[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowIterValues[rowIndex], fx, fy))
if (layout.rows[rowIndex].iterValue.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].iterValue, fx, fy))
{
m_editMeanRef = ref;
m_meanEditCurrentMax = item.maxIterations;
m_editMeanKind = kind;
const gd::RectF& track = layout.rowIterTracks[rowIndex];
const gd::RectF& value = layout.rowIterValues[rowIndex];
const gd::RectF& track = layout.rows[rowIndex].iterTrack;
const gd::RectF& value = layout.rows[rowIndex].iterValue;
const gd::RectF rowRect(track.X, value.Y,
value.X + value.Width - track.X,
value.Height);
@@ -2829,8 +2800,7 @@ namespace jdi
return true;
}
// 拖动「均值点数」滑块。
if (rowIndex < layout.rowLevelTracks.size() &&
WeakPeakSliderHit(layout.rowLevelTracks[rowIndex], m_scale, fx, fy))
if (WeakPeakSliderHit(layout.rows[rowIndex].levelTrack, m_scale, fx, fy))
{
const int floorValue = centerMode ? 0 : kMeanFilterPointsMin;
const int maximum =
@@ -2839,7 +2809,7 @@ namespace jdi
m_meanDragLane = index;
m_meanDragRef = ref;
m_meanDragKind = kind;
const double ratio = UiSliderRatioFromX(layout.rowLevelTracks[rowIndex], fx);
const double ratio = UiSliderRatioFromX(layout.rows[rowIndex].levelTrack, fx);
item.points = std::max(
floorValue,
std::min(maximum,
@@ -2852,8 +2822,7 @@ namespace jdi
return true;
}
// 拖动「迭代次数」滑块。
if (rowIndex < layout.rowIterTracks.size() &&
WeakPeakSliderHit(layout.rowIterTracks[rowIndex], m_scale, fx, fy))
if (WeakPeakSliderHit(layout.rows[rowIndex].iterTrack, m_scale, fx, fy))
{
const int maximum = std::max(
kMeanFilterIterationsMin,
@@ -2862,7 +2831,7 @@ namespace jdi
m_meanDragLane = index;
m_meanDragRef = ref;
m_meanDragKind = kind;
const double ratio = UiSliderRatioFromX(layout.rowIterTracks[rowIndex], fx);
const double ratio = UiSliderRatioFromX(layout.rows[rowIndex].iterTrack, fx);
item.iterations = std::max(
kMeanFilterIterationsMin,
std::min(maximum,
@@ -2886,23 +2855,21 @@ namespace jdi
// * **未启用 → 什么都不做**(用户 2026-10-30 明确要求:未勾选前点击条目栏
// 既不能自动勾选、也不能展开 / 收起,只能点复选框;此时鼠标指针也保持箭头)。
// 仍然消费这次点击(不穿透到底层的平移 / 选区等)。
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
for (std::size_t rowIndex = 0; rowIndex < layout.rows.size(); ++rowIndex)
{
if (!insideContent)
{
break;
}
const SignalRef& ref = layout.rowRefs[rowIndex];
if (rowIndex >= layout.rowHeaders.size() ||
layout.rowHeaders[rowIndex].Width <= 0.0f ||
!UiPointInRect(layout.rowHeaders[rowIndex], fx, fy))
const SignalRef& ref = layout.rows[rowIndex].ref;
if (layout.rows[rowIndex].header.Width <= 0.0f ||
!UiPointInRect(layout.rows[rowIndex].header, fx, fy))
{
continue;
}
const bool onCheckBox =
rowIndex < layout.rowChecks.size() &&
layout.rowChecks[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowChecks[rowIndex], fx, fy);
layout.rows[rowIndex].check.Width > 0.0f &&
UiPointInRect(layout.rows[rowIndex].check, fx, fy);
bool checked = false;
bool collapsed = false;
if (boostMode)
@@ -4225,9 +4192,9 @@ namespace jdi
int PlotView::PeakRowIndexOf(const EdgePanelLayout& layout, const SignalRef& ref) const
{
for (std::size_t index = 0; index < layout.rowRefs.size(); ++index)
for (std::size_t index = 0; index < layout.rows.size(); ++index)
{
if (layout.rowRefs[index] == ref)
if (layout.rows[index].ref == ref)
{
return static_cast<int>(index);
}
+45 -76
View File
@@ -814,18 +814,28 @@ namespace jdi
boxes.Add(layout.clipBuildButton, 3);
boxes.Add(layout.clipDestructiveButton, 3);
boxes.AddAll(layout.rowChecks, 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);
// ⚠️ 行内元素**逐行**登记(不再有"整个数组一起 AddAll"):
// 一行的字段是一个整体,逐字段登记才不会漏(旧的 12 次 AddAll 里
// 少写一行就是整整一列元素不显示调试框,且没有任何检查会发现)。
for (const EdgePanelLayout::PanelRow& row : layout.rows)
{
boxes.Add(row.header, 3);
boxes.Add(row.nameRect, 4);
boxes.Add(row.nameClose, 4);
boxes.Add(row.triangle, 4);
boxes.Add(row.check, 4);
boxes.Add(row.stats, 4);
boxes.Add(row.levelTrack, 4);
boxes.Add(row.levelValue, 4);
boxes.Add(row.levelReset, 4);
boxes.Add(row.iterTrack, 4);
boxes.Add(row.iterValue, 4);
boxes.Add(row.iterReset, 4);
boxes.Add(row.infoMaxInterval, 4);
boxes.Add(row.infoMinInterval, 4);
boxes.Add(row.intervalField, 4);
boxes.Add(row.frequencyField, 4);
}
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
{
@@ -4081,14 +4091,28 @@ namespace jdi
mixRect(layout.opacityValue);
mix(layout.collapsed ? 1ull : 0ull);
mix(layout.markerHasEnumColumn ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(layout.rows));
for (const gd::RectF& rect : layout.rowChecks)
mix(static_cast<std::uint64_t>(layout.signalCount));
// 行的几何**逐行混入**(旧实现按列分组混 12~16 次,漏一列就少一层敏感度,
// 而且“哪些列入键”分散在四个地方)。现在一个 `PanelRow` 是一个整体,
// 新增字段只需在 `PanelRow` 与这一个循环里各加一行。
for (const EdgePanelLayout::PanelRow& row : layout.rows)
{
mixRect(rect);
}
for (const gd::RectF& rect : layout.rowStats)
{
mixRect(rect);
mixRect(row.header);
mixRect(row.nameRect);
mixRect(row.nameClose);
mixRect(row.triangle);
mixRect(row.check);
mixRect(row.stats);
mixRect(row.levelTrack);
mixRect(row.levelValue);
mixRect(row.levelReset);
mixRect(row.iterTrack);
mixRect(row.iterValue);
mixRect(row.iterReset);
mixRect(row.infoMaxInterval);
mixRect(row.infoMinInterval);
mixRect(row.intervalField);
mixRect(row.frequencyField);
}
// 「自由标记」:固定点行/名称 ✕ 的矩形、五列边界、表头/吸顶状态与每个固定点的身份 + 时刻都要入键。
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
@@ -4097,14 +4121,6 @@ namespace jdi
mixRect(row.close);
mix(static_cast<std::uint64_t>(row.itemIndex + 2));
}
for (const gd::RectF& rect : layout.rowNameCloses)
{
mixRect(rect);
}
for (const gd::RectF& rect : layout.rowNameRects)
{
mixRect(rect);
}
for (const EdgePanelLayout::MarkerColumn& column : layout.markerColumns)
{
mixFloat(column.left);
@@ -4150,8 +4166,9 @@ namespace jdi
mix(bits);
}
}
for (const SignalRef& ref : layout.rowRefs)
for (const EdgePanelLayout::PanelRow& row : layout.rows)
{
const SignalRef& ref = row.ref;
mix(ref.IsValid() ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(ref.groupIndex));
mix(static_cast<std::uint64_t>(ref.signalIndex));
@@ -4220,54 +4237,6 @@ namespace jdi
}
// 弱峰调整的滑块行矩形(每信号两行)。
for (const gd::RectF& rect : layout.rowLevelTracks)
{
mixRect(rect);
}
for (const gd::RectF& rect : layout.rowIterTracks)
{
mixRect(rect);
}
for (const gd::RectF& rect : layout.rowIterValues)
{
mixRect(rect);
}
// 弱峰调整每行滑块右侧的复位按钮(悬停高亮依赖它的精确矩形)。
for (const gd::RectF& rect : layout.rowLevelResets)
{
mixRect(rect);
}
for (const gd::RectF& rect : layout.rowIterResets)
{
mixRect(rect);
}
// 「均值滤波」:信号色叠底标题栏 / 展开三角 / 内容区滚动偏移都要入键
//(滚动改变的是面板内部像素,位置与尺寸不变)。
for (const gd::RectF& rect : layout.rowHeaders)
{
mixRect(rect);
}
for (const gd::RectF& rect : layout.rowTriangles)
{
mixRect(rect);
}
for (const gd::RectF& rect : layout.rowInfoMaxInterval)
{
mixRect(rect);
}
for (const gd::RectF& rect : layout.rowInfoMinInterval)
{
mixRect(rect);
}
for (const gd::RectF& rect : layout.rowIntervalField)
{
mixRect(rect);
}
for (const gd::RectF& rect : layout.rowFrequencyField)
{
mixRect(rect);
}
mixFloat(layout.scrollOffset);
// 鼠标落在面板内时,✕ / 复选框 / 滑块 / 底部按钮的悬停态都依赖精确坐标。
+57 -142
View File
@@ -51,20 +51,15 @@ namespace jdi
// 行内容必须裁到内容区:滚动时行会越出可视范围。
g.SetClip(layout.content, gd::CombineModeIntersect);
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
for (std::size_t rowIndex = 0; rowIndex < layout.rows.size(); ++rowIndex)
{
if (rowIndex >= layout.rowHeaders.size() ||
rowIndex >= layout.rowNameRects.size())
{
break;
}
const SignalRef& ref = layout.rowRefs[rowIndex];
const SignalRef& ref = layout.rows[rowIndex].ref;
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr)
{
continue;
}
const gd::RectF& row = layout.rowHeaders[rowIndex];
const gd::RectF& row = layout.rows[rowIndex].header;
if (row.Y + row.Height <= layout.content.Y ||
row.Y >= layout.content.Y + layout.content.Height)
{
@@ -77,7 +72,7 @@ namespace jdi
UiFillRoundRect(g, row, 3.0f * m_scale, WithAlpha(signalColor, 0xE8));
// 信号名(第一列):超出列宽时省略。
const gd::RectF& nameRect = layout.rowNameRects[rowIndex];
const gd::RectF& nameRect = layout.rows[rowIndex].nameRect;
UiTextClipped(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall,
std::max(8.0f, nameRect.Width)),
m_fonts->uiSmall, nameRect, ink.text, TextAlign::LEFT,
@@ -188,75 +183,40 @@ namespace jdi
UiDrawLine(g, middleX, lowY, right, highY, background, 1.6f * m_scale);
};
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
for (std::size_t rowIndex = 0; rowIndex < layout.rows.size(); ++rowIndex)
{
const SignalRef& ref = layout.rowRefs[rowIndex];
if (rowIndex >= layout.rowHeaders.size() ||
rowIndex >= layout.rowChecks.size())
{
break;
}
const SignalRef& ref = layout.rows[rowIndex].ref;
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr)
{
continue;
}
const gd::RectF& header = layout.rowHeaders[rowIndex];
const gd::RectF& header = layout.rows[rowIndex].header;
// 行的**可见性**必须按“整行的纵向范围”判定,不能只看条目栏:
// 条目栏已滚出内容区,但它下面两行参数滑块仍可能还在可视区内
//(用户 2026-10-30 报告:此时两个滑块会直接消失 —— 显示错误)。
float rowTop = header.Y;
float rowBottom = header.Y + header.Height;
// 整行的纵向范围 = 条目栏 ∪ 全部参数行(未使用的字段恒为空矩形,直接 extent 即可;
// 旧结构里每一项都要先判断"数组可能更短",现在一行就是一个整体)。
const EdgePanelLayout::PanelRow& row = layout.rows[rowIndex];
float rowTop = row.header.Y;
float rowBottom = row.header.Y + row.header.Height;
const auto extendRow = [&rowBottom](const gd::RectF& rect) {
if (rect.Width > 0.0f && rect.Height > 0.0f)
{
rowBottom = std::max(rowBottom, rect.Y + rect.Height);
}
};
if (rowIndex < layout.rowLevelTracks.size())
{
extendRow(layout.rowLevelTracks[rowIndex]);
}
if (rowIndex < layout.rowLevelValues.size())
{
extendRow(layout.rowLevelValues[rowIndex]);
}
if (rowIndex < layout.rowLevelResets.size())
{
extendRow(layout.rowLevelResets[rowIndex]);
}
if (rowIndex < layout.rowIterTracks.size())
{
extendRow(layout.rowIterTracks[rowIndex]);
}
if (rowIndex < layout.rowIterValues.size())
{
extendRow(layout.rowIterValues[rowIndex]);
}
if (rowIndex < layout.rowIterResets.size())
{
extendRow(layout.rowIterResets[rowIndex]);
}
if (rowIndex < layout.rowStats.size())
{
extendRow(layout.rowStats[rowIndex]);
}
if (rowIndex < layout.rowInfoMaxInterval.size())
{
extendRow(layout.rowInfoMaxInterval[rowIndex]);
}
if (rowIndex < layout.rowInfoMinInterval.size())
{
extendRow(layout.rowInfoMinInterval[rowIndex]);
}
if (rowIndex < layout.rowIntervalField.size())
{
extendRow(layout.rowIntervalField[rowIndex]);
}
if (rowIndex < layout.rowFrequencyField.size())
{
extendRow(layout.rowFrequencyField[rowIndex]);
}
extendRow(row.levelTrack);
extendRow(row.levelValue);
extendRow(row.levelReset);
extendRow(row.iterTrack);
extendRow(row.iterValue);
extendRow(row.iterReset);
extendRow(row.stats);
extendRow(row.infoMaxInterval);
extendRow(row.infoMinInterval);
extendRow(row.intervalField);
extendRow(row.frequencyField);
if (rowBottom <= layout.content.Y ||
rowTop >= layout.content.Y + layout.content.Height)
{
@@ -300,7 +260,7 @@ namespace jdi
}
// 左侧三角(仅勾选时存在):朝下 = 展开、朝右 = 收起。
const gd::RectF& triangle = layout.rowTriangles[rowIndex];
const gd::RectF& triangle = layout.rows[rowIndex].triangle;
if (triangle.Width > 0.0f && checked)
{
UiDrawTriangleIcon(g, triangle, ink.text,
@@ -309,7 +269,7 @@ namespace jdi
}
// 名称:从三角右侧到复选框左侧。
const gd::RectF& check = layout.rowChecks[rowIndex];
const gd::RectF& check = layout.rows[rowIndex].check;
const float nameLeft = (triangle.Width > 0.0f)
? (triangle.X + triangle.Width + 6.0f * m_scale)
: (header.X + 8.0f * m_scale);
@@ -336,8 +296,7 @@ namespace jdi
}
// 「中心轴边沿计数」:参数行最上面是**同一行**的「上升沿 / 下降沿」统计。
if (centerAxis && rowIndex < layout.rowStats.size() &&
layout.rowStats[rowIndex].Width > 0.0f)
if (centerAxis && layout.rows[rowIndex].stats.Width > 0.0f)
{
std::size_t rising = 0;
std::size_t falling = 0;
@@ -348,7 +307,7 @@ namespace jdi
item->points, item->iterations, EdgeRangeActive(),
EdgeRangeMin(), EdgeRangeMax(), rising, falling, complete);
}
paintCounts(layout.rowStats[rowIndex], rising, falling, complete,
paintCounts(layout.rows[rowIndex].stats, rising, falling, complete,
layout.content);
}
@@ -466,34 +425,18 @@ namespace jdi
? (static_cast<double>(peakIter - kWeakPeakIterationsMin) /
static_cast<double>(peakMax - kWeakPeakIterationsMin))
: 0.0;
if (rowIndex < layout.rowLevelTracks.size() &&
layout.rowLevelTracks[rowIndex].Width > 0.0f)
if (row.levelTrack.Width > 0.0f)
{
paintPeakSlider(
layout.rowLevelTracks[rowIndex],
rowIndex < layout.rowLevelValues.size()
? layout.rowLevelValues[rowIndex]
: gd::RectF(),
rowIndex < layout.rowLevelResets.size()
? layout.rowLevelResets[rowIndex]
: gd::RectF(),
kWeakPeakLevelLabel, levelRatio,
(peakAmount > 0 ? L"+" : L"") + FormatInteger(peakAmount) + L"%",
levelActive, false);
paintPeakSlider(row.levelTrack, row.levelValue, row.levelReset,
kWeakPeakLevelLabel, levelRatio,
(peakAmount > 0 ? L"+" : L"") + FormatInteger(peakAmount) + L"%",
levelActive, false);
}
if (rowIndex < layout.rowIterTracks.size() &&
layout.rowIterTracks[rowIndex].Width > 0.0f)
if (row.iterTrack.Width > 0.0f)
{
paintPeakSlider(
layout.rowIterTracks[rowIndex],
rowIndex < layout.rowIterValues.size()
? layout.rowIterValues[rowIndex]
: gd::RectF(),
rowIndex < layout.rowIterResets.size()
? layout.rowIterResets[rowIndex]
: gd::RectF(),
kWeakPeakIterationsSliderLabel, iterRatio,
FormatInteger(peakIter), iterActive, editingMax);
paintPeakSlider(row.iterTrack, row.iterValue, row.iterReset,
kWeakPeakIterationsSliderLabel, iterRatio,
FormatInteger(peakIter), iterActive, editingMax);
}
}
else
@@ -518,30 +461,12 @@ namespace jdi
(m_dragMode == PlotDragMode::MEAN_ITERATIONS) &&
(m_meanDragLane == static_cast<int>(index)) && (m_meanDragRef == ref) &&
(m_meanDragKind == kind);
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,
pointsFloor, 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);
}
paintMeanSlider(row.levelTrack, row.levelValue, row.levelReset,
kMeanFilterPointsSliderLabel, points, pointsFloor, pointsMax,
pointsActive);
paintMeanSlider(row.iterTrack, row.iterValue, row.iterReset,
kMeanFilterIterationsSliderLabel, iterations,
kMeanFilterIterationsMin, iterationsMax, iterationsActive);
}
}
@@ -640,36 +565,26 @@ namespace jdi
}
return text;
};
if (rowIndex < layout.rowInfoMaxInterval.size())
{
paintInfoRow(layout.rowInfoMaxInterval[rowIndex], kResampleMaxIntervalLabel,
hasInterval ? intervalWithFrequency(item.maxInterval) : L"—");
}
if (rowIndex < layout.rowInfoMinInterval.size())
{
paintInfoRow(layout.rowInfoMinInterval[rowIndex], kResampleMinIntervalLabel,
hasInterval ? intervalWithFrequency(item.minInterval) : L"—");
}
// ⚠️ 四行均在本行的 `PanelRow` 里,不再需要 “数组可能更短” 的判空(旧平行数组结构的遗留)。
const EdgePanelLayout::PanelRow& row = layout.rows[rowIndex];
paintInfoRow(row.infoMaxInterval, kResampleMaxIntervalLabel,
hasInterval ? intervalWithFrequency(item.maxInterval) : L"—");
paintInfoRow(row.infoMinInterval, kResampleMinIntervalLabel,
hasInterval ? intervalWithFrequency(item.minInterval) : L"—");
const double interval = (item.interval > 0.0) ? item.interval : item.minInterval;
const bool editingInterval = (m_editTarget == PlotEditTarget::RESAMPLE_INTERVAL) &&
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
const bool editingFrequency = (m_editTarget == PlotEditTarget::RESAMPLE_FREQUENCY) &&
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
if (rowIndex < layout.rowIntervalField.size())
{
// 非编辑态:自适应单位 + 小数(与「最大/最小数据间隔」同一套单位形式)。
// 编辑态:只显示**秒**数值(不带单位),由 `PaintEditField` 绘制。
paintInputRow(layout.rowIntervalField[rowIndex], kResampleIntervalLabel,
FormatIntervalText(interval), editingInterval);
}
if (rowIndex < layout.rowFrequencyField.size())
{
// ⚠️ 频率同样用**真实间隔**换算(与时间间隔行同源)⇒ `270.517 Hz`。
const double hertz = (interval > 0.0) ? (1.0 / interval) : 0.0;
// 非编辑态:频率单位自适应(Hz/kHz/MHz/GHz);编辑态:只显示 Hz 数值。
paintInputRow(layout.rowFrequencyField[rowIndex], kResampleFrequencyLabel,
(hertz > 0.0) ? FormatFrequencyText(hertz) : L"—", editingFrequency);
}
// 非编辑态:自适应单位 + 小数(与「最大/最小数据间隔」同一套单位形式)。
// 编辑态:只显示**秒**数值(不带单位),由 `PaintEditField` 绘制。
paintInputRow(row.intervalField, kResampleIntervalLabel, FormatIntervalText(interval),
editingInterval);
// ⚠️ 频率同样用**真实间隔**换算(与时间间隔行同源)⇒ `270.517 Hz`。
const double hertz = (interval > 0.0) ? (1.0 / interval) : 0.0;
// 非编辑态:频率单位自适应(Hz/kHz/MHz/GHz);编辑态:只显示 Hz 数值。
paintInputRow(row.frequencyField, kResampleFrequencyLabel,
(hertz > 0.0) ? FormatFrequencyText(hertz) : L"—", editingFrequency);
}
+17 -19
View File
@@ -380,7 +380,7 @@ namespace jdi
{
return layout;
}
layout.rows = rowCount;
layout.signalCount = rowCount;
const float width = EdgePanelWidth(laneIndex, kind);
const float titleHeight = kEdgePanelTitleHeight * m_scale;
@@ -735,7 +735,7 @@ namespace jdi
{
continue;
}
// ⚠️ 行的追加走 `AppendRow()`(**唯一入口**):18 个平行数组一次补齐,
// ⚠️ 行的追加走 `AppendRow()`(**唯一入口**):一行的全部字段一次成型,
// 不会出现"某个分支漏推一列 ⇒ 该列之后所有行读到别人的矩形"。
layout.AppendRow(ref, gd::RectF(contentLeft, rowY, innerWidth, rowH),
gd::RectF(contentLeft + 4.0f * m_scale, rowY,
@@ -877,9 +877,9 @@ namespace jdi
itemCollapsed = (item != nullptr && item->collapsed);
}
// 组头(信号色叠底):整行宽度,左侧三角(仅勾选时)+ 名称,右端复选框。
// ⚠️ 行的追加走 `AppendRow()`(**唯一入口**):18 个平行数组一次补齐,
// ⚠️ 行的追加走 `AppendRow()`(**唯一入口**):一行的全部字段一次成型,
// 不会出现"某个分支漏推一列 ⇒ 该列之后所有行读到别人的矩形"。
// 下面的参数行 / 重采样行一律用 `rowXxx.back() = …` **赋值**(长度天然对齐)。
// 下面的参数行 / 重采样行一律用 `layout.rows.back().xxx = …` **赋值**(同一行,天然对齐)。
layout.AppendRow(
ref, gd::RectF(contentLeft, rowY, innerWidth, groupRow),
gd::RectF(contentLeft, rowY, innerWidth, groupRow),
@@ -903,43 +903,43 @@ namespace jdi
// 「重采样」四行:① 最大间隔(只读)② 最小间隔(只读)
// ③ 重采样间隔(输入)④ 重采样频率(输入,与③联动)。
// 每行 = 左侧标签 + 右侧数值/输入框区域。
layout.rowInfoMaxInterval.back() = gd::RectF(contentLeft, rowY, innerWidth, sliderRow);
layout.rows.back().infoMaxInterval = gd::RectF(contentLeft, rowY, innerWidth, sliderRow);
rowY += sliderRow;
layout.rowInfoMinInterval.back() = gd::RectF(contentLeft, rowY, innerWidth, sliderRow);
layout.rows.back().infoMinInterval = gd::RectF(contentLeft, rowY, innerWidth, sliderRow);
rowY += sliderRow;
// 输入行的**数值矩形**靠右对齐,宽度取剩余可用宽度(绘制框与编辑框同源)。
const float fieldLeft = contentLeft + rowLabelColumn + 8.0f * m_scale;
const float fieldWidth =
std::max(24.0f * m_scale, innerWidth - rowLabelColumn - 8.0f * m_scale);
layout.rowIntervalField.back() = gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow);
layout.rows.back().intervalField = gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow);
rowY += sliderRow;
layout.rowFrequencyField.back() = gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow);
layout.rows.back().frequencyField = gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow);
rowY += sliderRow;
continue;
}
// 「中心轴边沿计数」:参数行最上面是**同一行**的「上升沿 / 下降沿」统计。
if (centerAxis && !layout.rowStats.empty())
if (centerAxis && !layout.rows.empty())
{
layout.rowStats.back() = gd::RectF(contentLeft, rowY, innerWidth, groupRow);
layout.rows.back().stats = gd::RectF(contentLeft, rowY, innerWidth, groupRow);
rowY += groupRow;
}
const gd::RectF pointsTrack(trackLeft, rowY + (sliderRow - trackHeight) * 0.5f,
trackWidth, trackHeight);
layout.rowLevelTracks.back() = pointsTrack;
layout.rowLevelValues.back() =
layout.rows.back().levelTrack = pointsTrack;
layout.rows.back().levelValue =
gd::RectF(pointsTrack.X + pointsTrack.Width + 6.0f * m_scale, rowY, valueColumn,
sliderRow);
layout.rowLevelResets.back() =
layout.rows.back().levelReset =
gd::RectF(resetLeft, rowY + (sliderRow - resetSize) * 0.5f, resetSize, resetSize);
rowY += sliderRow;
const gd::RectF iterTrack(trackLeft, rowY + (sliderRow - trackHeight) * 0.5f,
trackWidth, trackHeight);
layout.rowIterTracks.back() = iterTrack;
layout.rowIterValues.back() =
layout.rows.back().iterTrack = iterTrack;
layout.rows.back().iterValue =
gd::RectF(iterTrack.X + iterTrack.Width + 6.0f * m_scale, rowY, valueColumn,
sliderRow);
layout.rowIterResets.back() =
layout.rows.back().iterReset =
gd::RectF(resetLeft, rowY + (sliderRow - resetSize) * 0.5f, resetSize, resetSize);
rowY += sliderRow;
}
@@ -1136,9 +1136,7 @@ namespace jdi
// 只登记"在可视内容区里"的分组栏与条目(滚出视界的条目不得抢走
// 底部按钮 / 滑块的点击)。
layout.markerGroups.push_back(group);
layout.rowRefs.push_back(ref);
layout.rowNameRects.push_back(group.header);
layout.rowNameCloses.push_back(group.close);
layout.AppendMarkerGroupRow(ref, group.header, group.close);
}
rowY += groupRow;
if (group.collapsed)