feat(clip): 新增「选区截取」实验性功能 + 筛选态组复选框只作用于筛选结果 + 重采样统计不被省略

一、重采样面板统计信息不再被省略为 `…`
   - 根因:EdgePanelWidth 里数值列按"典型样例"(1h 1min 2s(1.23456 GHz))估宽,
     真实文本(16ms 182.614us(61.795 Hz))更长 => 绘制矩形不够宽 => 尾部被裁。
   - 新增 FormatIntervalWithFrequency() 作为「间隔(频率)」文本的唯一实现,
     宽度计算与绘制共用它;EdgePanelWidth 改为逐信号实测真实文本宽度取最大值。

二、筛选状态下组复选框只作用于「筛选出来的信号」
   - TabConfig 新增 SetGroupVisibleSubset / GroupAllVisibleSubset / GroupHasVisibleSubset,
     只读写掩码内元素,未通过筛选的信号可见性保持不变。
   - SignalPanel::BuildFilterMask 提供逐信号掩码(未筛选时返回 false,走整组语义)。
   - 组复选框的点击、绘制、行位图缓存键三处统一改用子集判定
     (只改绘制不改缓存键会导致勾选后复选框不刷新)。

三、新增实验性功能「选区截取」(EdgePanelKind::CLIP = 6,PLOT_CMD_LANE_CLIP = 14)
   - 菜单位置:实验性功能二级菜单,「重采样」之下、「自由标记」上方,带勾选。
   - 面板:逐信号一行(信号色叠底条目栏 + 右侧复选框,不可展开),内容可滚动;
     底部两个不滚动的按钮「截取为新信号」与「原始信号截取」;
     未启用双光标时两个按钮为空矩形并给出提示。
   - 截取为新信号:区间内原始数据(不烘焙)+ 继承源信号整套调整,
     名称为 <源信号名> (AAAA - BBBB)(显示时刻、保留三位小数、带 s),插入源信号下方,
     使用 AppendGroupAxis 新建的独立时间轴。
   - 原始信号截取:DataSet::ClipSignalRange 就地裁剪时间轴与采样,
     用新建+swap+shrink_to_fit 真正归还内存,并重算统计与轴的 minTime/maxTime/monotonic。
     调用前先 PauseDerivedAsync() 避免后台派生读到已释放采样。
   - 时间轴偏移参与区间比较:双光标是显示时刻(time+offsetTime),
     BuildClippedSignal 比较时加偏移,ClipSignalInPlace 换算回采样时刻(low-offsetTime)。
   - 同步点已全部补齐:LaneCarry(三处)/SplitLane/PruneLaneSignalItems/RemapAfterSignalInsert/
     purgeRefs/PanelSlotKey/CollectDebugBoxes/SceneContentHash/kEdgePanelKinds/kOrder/
     EdgePanelLayoutFor/EDGE_PANEL_MOVE/m_pendingPanelClose/MEAN_SCROLL/滚轮/悬停/
     SetLanePanelCollapsed/LanePanelCollapsed/QueryCursor。

make release 零 warning,产物 860,160 B。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-03 16:04:48 +08:00
1 parent 5d305cb10f
commit 62220477c0
18 files changed
+1263 -67

No files matched your search

+115 -41
View File
@@ -29,12 +29,13 @@ namespace jdi
const bool meanFilter = (kind == EdgePanelKind::MEAN);
const bool weakPeak = (kind == EdgePanelKind::PEAK);
const bool resamplePanel = (kind == EdgePanelKind::RESAMPLE);
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」= 可滚动的分组列表
const bool clipPanel = (kind == EdgePanelKind::CLIP);
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」「选区截取」= 可滚动的分组列表
//(信号色叠底条目栏 + 右侧复选框 + 展开后的参数行);
// **「边沿计数」= 表格**(表头:信号名 / 上升沿计数 / 下降沿计数)。
// ⚠️ 旧式「逐条目」排版(无滚动的名称+参数行)已彻底删除:上百个信号时它会算出
// 上万像素高的面板,超出绘图区后**面板像素缓存一次都存不进去** ⇒ 每帧整块重画。
const bool listStyle = (centerAxis || meanFilter || weakPeak || resamplePanel);
const bool listStyle = (centerAxis || meanFilter || weakPeak || resamplePanel || clipPanel);
const bool countTable = (kind == EdgePanelKind::COUNT);
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “弱峰调整”
@@ -168,19 +169,35 @@ namespace jdi
(kFreeMarkerScrollWidth + 4.0f) * m_scale);
if (resamplePanel)
{
// 「重采样」四行 = 「标签列 + 数值列」(两行统计 + 两行输入框),
// 标签取四者中最宽的一个,数值列按间隔 / 频率的典型文本宽度取。
// ⚠️ 「最大/最小数据间隔」行末尾还带「(频率)」尾缀(用户 2026-11-03 要求),
// 取值列必须按**含尾缀**的文本量宽,否则尾缀会被裁掉。
// 「重采样」四行 = 「标签列 + 数值列」(两行统计 + 两行输入框)。
// 标签取四者中最宽的一个;数值列必须按**真实会显示的文本**量宽
// (用户 2026-11-03 要求:统计信息不能被省略成 `…`)。
const float resampleLabelColumn =
std::max(std::max(UiMeasureTextWidth(m_fonts->uiSmall, kResampleMaxIntervalLabel),
UiMeasureTextWidth(m_fonts->uiSmall, kResampleMinIntervalLabel)),
std::max(UiMeasureTextWidth(m_fonts->uiSmall, kResampleIntervalLabel),
UiMeasureTextWidth(m_fonts->uiSmall, kResampleFrequencyLabel)));
const float resampleValueColumn =
std::max(std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"1.23456 GHz"),
UiMeasureTextWidth(m_fonts->monoSmall, L"1h 1min 2s")),
UiMeasureTextWidth(m_fonts->monoSmall, L"1h 1min 2s(1.23456 GHz)"));
// ⚠️ 逐信号实测「最大 / 最小数据间隔」的真实文本宽度(含(频率)尾缀),
// 而不是按 `1h 1min 2s(1.23456 GHz)` 这类**典型样例**估算 ——
// 真实值(如 `16ms 182.614us(61.795 Hz)`)往往更长,估算不足就会被截断。
float resampleValueColumn = 0.0f;
for (const SignalRef& ref : lane.signals)
{
const Lane::ResampleItem* item = lane.FindResampleItem(ref);
if (item == nullptr)
{
continue;
}
resampleValueColumn = std::max(
resampleValueColumn,
UiMeasureTextWidth(m_fonts->monoSmall, FormatIntervalWithFrequency(item->maxInterval)));
resampleValueColumn = std::max(
resampleValueColumn,
UiMeasureTextWidth(m_fonts->monoSmall, FormatIntervalWithFrequency(item->minInterval)));
}
// 兜底下限:没有勾选任何信号时也保持一个可用的最小宽度。
resampleValueColumn = std::max(resampleValueColumn,
UiMeasureTextWidth(m_fonts->monoSmall, L"1h 1min 2s(1.23456 GHz)"));
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + resampleLabelColumn +
8.0f * m_scale + resampleValueColumn +
(kFreeMarkerScrollWidth + 4.0f) * m_scale);
@@ -205,6 +222,16 @@ namespace jdi
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + buttonWidth);
}
if (clipPanel)
{
// 「选区截取」底部是**两个并排**按钮,宽度必须容得下两段文字 + 间隙 + 内外边距。
const float gap = 6.0f * m_scale;
const float twoButtons =
(UiMeasureTextWidth(m_fonts->uiSmall, kClipBuildLabel) + 28.0f * m_scale) + gap +
(UiMeasureTextWidth(m_fonts->uiSmall, kClipDestructiveLabel) + 28.0f * m_scale);
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + twoButtons);
}
if (countTable)
{
// 「边沿计数」表格:右侧要给内容滚动条留位(上百个信号时内容必然超高)。
@@ -300,25 +327,31 @@ namespace jdi
const bool weakPeak = (kind == EdgePanelKind::PEAK);
const bool freeMarker = (kind == EdgePanelKind::MARK);
const bool resamplePanel = (kind == EdgePanelKind::RESAMPLE);
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」共用同一套**可滚动分组列表**排布
//(信号色叠底条目栏 + 展开后的参数行);「边沿计数」自 2026-11-02 起走**表格**分支。
const bool listStyle = (meanFilter || centerAxis || weakPeak || resamplePanel);
const bool clipPanel = (kind == EdgePanelKind::CLIP);
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」「选区截取」共用同一套
//**可滚动分组列表**排布(信号色叠底条目栏 + 展开后的参数行);
//「边沿计数」自 2026-11-02 起走**表格**分支。
//⚠️ 「选区截取」的条目**不可展开**(只需右侧复选框),因此只占一行。
const bool listStyle = (meanFilter || centerAxis || weakPeak || resamplePanel || clipPanel);
const bool countTable = (kind == EdgePanelKind::COUNT);
const bool hasButton = (meanFilter || weakPeak || resamplePanel);
const bool hasButton = (meanFilter || weakPeak || resamplePanel || clipPanel);
const bool enabled = meanFilter ? lane.meanFilter
: (weakPeak ? lane.weakPeak
: (freeMarker ? lane.freeMarker
: (resamplePanel ? lane.resample
: (centerAxis ? lane.edgeCenter
: lane.edgeCount))));
: (clipPanel ? lane.clip
: (centerAxis ? lane.edgeCenter
: lane.edgeCount)))));
const bool open = meanFilter ? lane.meanFilterPanel.open
: (weakPeak ? lane.weakPeakPanel.open
: (freeMarker ? lane.freeMarkerPanel.open
: (resamplePanel
? lane.resamplePanel.open
: (centerAxis
? lane.edgeCenterPanel.open
: lane.edgePanel.open))));
: (clipPanel
? lane.clipPanel.open
: (centerAxis
? lane.edgeCenterPanel.open
: lane.edgePanel.open)))));
if (!enabled || !open)
{
return layout;
@@ -329,9 +362,11 @@ namespace jdi
: (freeMarker ? lane.freeMarkerPanel.collapsed
: (resamplePanel
? lane.resamplePanel.collapsed
: (centerAxis
? lane.edgeCenterPanel.collapsed
: lane.edgePanel.collapsed))));
: (clipPanel
? lane.clipPanel.collapsed
: (centerAxis
? lane.edgeCenterPanel.collapsed
: lane.edgePanel.collapsed)))));
layout.collapsed = collapsed;
int rowCount = 0;
@@ -356,7 +391,11 @@ namespace jdi
// 因此不再有独立的底部编辑行;「边沿计数」走独立的**表格**分支。
const float buttonHeight = hasButton ? (kEdgePanelButtonHeight * m_scale) : 0.0f;
// 面板底部(内容区之下)占的纵向空间:有按钮时 = pad + 按钮 + pad,无按钮时只有 pad。
const float footerHeight = hasButton ? (buttonHeight + pad) : 0.0f;
// ⚠️ 「选区截取」底部是**两个**按钮行(截取为新信号 / 原始信号截取),
// 因此 footer 要按两行算(用户 2026-11-03 要求)。
const float footerHeight =
clipPanel ? (buttonHeight * 2.0f + 6.0f * m_scale + pad)
: (hasButton ? (buttonHeight + pad) : 0.0f);
const float bottomPad = pad;
const float weakPeakSliderRow = kWeakPeakSliderRowHeight * m_scale;
@@ -397,6 +436,10 @@ namespace jdi
continue;
}
content += groupRow;
if (clipPanel)
{
continue; // 「选区截取」的条目**不可展开**,每条只占一行
}
bool expanded = false;
if (resamplePanel)
{
@@ -534,17 +577,21 @@ namespace jdi
: (freeMarker ? lane.freeMarkerPanel.x
: (resamplePanel
? lane.resamplePanel.x
: (centerAxis
? lane.edgeCenterPanel.x
: lane.edgePanel.x))));
: (clipPanel
? lane.clipPanel.x
: (centerAxis
? lane.edgeCenterPanel.x
: lane.edgePanel.x)))));
const double storedY = meanFilter ? lane.meanFilterPanel.y
: (weakPeak ? lane.weakPeakPanel.y
: (freeMarker ? lane.freeMarkerPanel.y
: (resamplePanel
? lane.resamplePanel.y
: (centerAxis
? lane.edgeCenterPanel.y
: lane.edgePanel.y))));
: (clipPanel
? lane.clipPanel.y
: (centerAxis
? lane.edgeCenterPanel.y
: lane.edgePanel.y)))));
int openPanelOrdinal = 0;
{
// ⚠️ 错开量用「**当前真正显示出来的面板**中排在本面板之前的个数」:
@@ -570,12 +617,15 @@ namespace jdi
return lane.freeMarker && lane.freeMarkerPanel.open;
case EdgePanelKind::RESAMPLE:
return lane.resample && lane.resamplePanel.open;
case EdgePanelKind::CLIP:
return lane.clip && lane.clipPanel.open;
}
return false;
};
static const EdgePanelKind kOrder[6] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
static const EdgePanelKind kOrder[7] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
EdgePanelKind::MARK, EdgePanelKind::RESAMPLE};
EdgePanelKind::MARK, EdgePanelKind::RESAMPLE,
EdgePanelKind::CLIP};
int openSeen = 0;
bool found = false;
for (std::size_t lane = 0; lane < m_lanes.size() && !found; ++lane)
@@ -737,7 +787,22 @@ namespace jdi
// 「均值滤波 / 弱峰调整」:可滚动内容区(信号色叠底条目栏 + 展开后的两行滑块)
// +(均值滤波 / 弱峰调整)底部按钮。
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
if (hasButton)
if (clipPanel)
{
// 「选区截取」:底部**两个**并排的固定按钮(不参与滚动)。
// ⚠️ 未启用双光标模式时两者都是空矩形 ⇒ 不可点(面板只显示提示文字)。
const bool doubleCursor = (Tab().CursorMode() == PlotCursorMode::DOUBLE);
if (doubleCursor)
{
const float gap = 6.0f * m_scale;
const float halfWidth = std::max(0.0f, (width - pad * 2.0f - gap) * 0.5f);
const float buttonTop = top + height - pad - buttonRowHeight;
layout.clipBuildButton = gd::RectF(left + pad, buttonTop, halfWidth, buttonRowHeight);
layout.clipDestructiveButton =
gd::RectF(left + pad + halfWidth + gap, buttonTop, halfWidth, buttonRowHeight);
}
}
else if (hasButton)
{
layout.button = gd::RectF(left + pad, top + height - pad - buttonRowHeight,
std::max(0.0f, width - pad * 2.0f), buttonRowHeight);
@@ -746,7 +811,9 @@ namespace jdi
const float groupRow = kEdgePanelMeanRowHeight * m_scale;
const float sliderRow = kWeakPeakSliderRowHeight * m_scale;
const float scrollW = kFreeMarkerScrollWidth * m_scale;
const float footerHeight = hasButton ? (buttonRowHeight + pad) : 0.0f;
const float footerHeight =
clipPanel ? (buttonRowHeight * 2.0f + 6.0f * m_scale + pad)
: (hasButton ? (buttonRowHeight + pad) : 0.0f);
const float viewHeight =
std::max(0.0f, height - titleHeight - pad - pad - footerHeight);
const bool scrollable = meanContentHeight > viewHeight + 0.5f;
@@ -754,7 +821,8 @@ namespace jdi
const float storedScroll = meanFilter ? lane.meanFilterScroll
: (weakPeak ? lane.weakPeakScroll
: (resamplePanel ? lane.resampleScroll
: lane.edgeCenterScroll));
: (clipPanel ? lane.clipScroll
: lane.edgeCenterScroll)));
const float scroll = std::max(0.0f, std::min(storedScroll, maxScroll));
const float innerWidth =
std::max(0.0f, width - pad * 2.0f - (scrollable ? (scrollW + 4.0f * m_scale) : 0.0f));
@@ -812,7 +880,12 @@ namespace jdi
}
bool checked = false;
bool itemCollapsed = false;
if (resamplePanel)
if (clipPanel)
{
// 「选区截取」:只有勾选状态,条目**不可展开**(用户 2026-11-03 要求)。
checked = lane.ClipEnabled(ref);
}
else if (resamplePanel)
{
const Lane::ResampleItem* item = lane.FindResampleItem(ref);
checked = (item != nullptr);
@@ -842,11 +915,12 @@ namespace jdi
gd::RectF(contentLeft, rowY, innerWidth, groupRow));
layout.rowNameRects.push_back(gd::RectF(contentLeft, rowY, innerWidth, groupRow));
layout.rowTriangles.push_back(
checked ? gd::RectF(contentLeft + 3.0f * m_scale,
rowY + (groupRow - kEdgePanelRowTriangleSize * m_scale) * 0.5f,
kEdgePanelRowTriangleSize * m_scale,
kEdgePanelRowTriangleSize * m_scale)
: gd::RectF());
(checked && !clipPanel)
? gd::RectF(contentLeft + 3.0f * m_scale,
rowY + (groupRow - kEdgePanelRowTriangleSize * m_scale) * 0.5f,
kEdgePanelRowTriangleSize * m_scale,
kEdgePanelRowTriangleSize * m_scale)
: gd::RectF());
layout.rowChecks.push_back(
gd::RectF(contentLeft + innerWidth - closeSize - 4.0f * m_scale,
rowY + (groupRow - closeSize) * 0.5f, closeSize, closeSize));
@@ -856,7 +930,7 @@ namespace jdi
rowY += groupRow;
// 参数行:只有勾选且未收起时才有几何(未勾选的行两者都是空矩形)。
if (!checked || itemCollapsed)
if (!checked || itemCollapsed || clipPanel)
{
layout.rowLevelTracks.push_back(gd::RectF());
layout.rowLevelValues.push_back(gd::RectF());