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

+117 -5
View File
@@ -1066,6 +1066,18 @@ namespace jdi
{
return LoadCursorW(nullptr, IDC_HAND);
}
// 「选区截取」底部两个按钮:可点击控件(双光标关闭时为空矩形 ⇒ 不响应)。
if (layout.clipBuildButton.Width > 0.0f &&
UiPointInRect(layout.clipBuildButton, static_cast<float>(x), static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_HAND);
}
if (layout.clipDestructiveButton.Width > 0.0f &&
UiPointInRect(layout.clipDestructiveButton, static_cast<float>(x),
static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_HAND);
}
if (layout.opacityTrack.Width > 0.0f &&
WeakPeakSliderHit(layout.opacityTrack, m_scale, static_cast<float>(x),
static_cast<float>(y)))
@@ -1109,7 +1121,8 @@ namespace jdi
const bool listCursor = (cursorKind == EdgePanelKind::MEAN ||
cursorKind == EdgePanelKind::CENTER ||
cursorKind == EdgePanelKind::PEAK ||
cursorKind == EdgePanelKind::RESAMPLE);
cursorKind == EdgePanelKind::RESAMPLE ||
cursorKind == EdgePanelKind::CLIP);
const Lane& cursorLane =
Tab().Lanes()[static_cast<std::size_t>(panelLane)];
if (listCursor)
@@ -1136,6 +1149,10 @@ namespace jdi
checked = cursorLane.FindResampleItem(
layout.rowRefs[rowIndex]) != nullptr;
}
else if (cursorKind == EdgePanelKind::CLIP)
{
checked = cursorLane.ClipEnabled(layout.rowRefs[rowIndex]);
}
else
{
checked = cursorLane.FindMeanFilterItem(
@@ -1143,6 +1160,10 @@ namespace jdi
}
return checked ? LoadCursorW(nullptr, IDC_HAND) : nullptr;
}
if (cursorKind == EdgePanelKind::CLIP)
{
continue; // 截取面板的条目只有复选框,没有参数行
}
if (cursorKind == EdgePanelKind::RESAMPLE)
{
// 两行输入框是可点击控件(点击进入就地编辑)。
@@ -1734,6 +1755,10 @@ namespace jdi
lane.resamplePanel.x = left;
lane.resamplePanel.y = top;
break;
case EdgePanelKind::CLIP:
lane.clipPanel.x = left;
lane.clipPanel.y = top;
break;
case EdgePanelKind::COUNT:
default:
lane.edgePanel.x = left;
@@ -1832,6 +1857,9 @@ namespace jdi
case EdgePanelKind::RESAMPLE:
scrolled = SetLaneResampleScroll(m_markerScrollDragLane, target);
break;
case EdgePanelKind::CLIP:
scrolled = SetLaneClipScroll(m_markerScrollDragLane, target);
break;
default:
scrolled = SetLaneMeanFilterScroll(m_markerScrollDragLane, target);
break;
@@ -2014,7 +2042,8 @@ namespace jdi
static_cast<EdgePanelKind>(hoverPanelKind);
if (overEdgePanel && m_tab != nullptr && panelLane >= 0 &&
(hoverKind == EdgePanelKind::MEAN || hoverKind == EdgePanelKind::PEAK ||
hoverKind == EdgePanelKind::CENTER || hoverKind == EdgePanelKind::RESAMPLE))
hoverKind == EdgePanelKind::CENTER || hoverKind == EdgePanelKind::RESAMPLE ||
hoverKind == EdgePanelKind::CLIP))
{
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoverKind);
if (UiPointInRect(layout.content, static_cast<float>(x), static_cast<float>(y)))
@@ -2411,16 +2440,17 @@ namespace jdi
// (前两者是「名称 + 复选框 + 两个参数输入框」,弱峰调整是「名称 + 复选框 + 两行滑块」)。
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER ||
kind == EdgePanelKind::PEAK || kind == EdgePanelKind::COUNT ||
kind == EdgePanelKind::RESAMPLE) &&
kind == EdgePanelKind::RESAMPLE || kind == EdgePanelKind::CLIP) &&
m_tab != nullptr)
{
const bool meanMode = (kind == EdgePanelKind::MEAN);
const bool centerMode = (kind == EdgePanelKind::CENTER);
const bool boostMode = (kind == EdgePanelKind::PEAK);
const bool resampleMode = (kind == EdgePanelKind::RESAMPLE);
const bool clipMode = (kind == EdgePanelKind::CLIP);
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」自 2026-11-02 起共用同一套
//**可滚动列表**交互(信号色叠底条目栏 + 右侧复选框 + 展开后的参数行)。
const bool listMode = (meanMode || centerMode || boostMode || resampleMode);
const bool listMode = (meanMode || centerMode || boostMode || resampleMode || clipMode);
// 「边沿计数」= **表格**(表头 + 每信号一行),只有滚动条可拖。
if (kind == EdgePanelKind::COUNT)
{
@@ -2583,6 +2613,82 @@ namespace jdi
return true; // 面板内的其它位置:消费点击,不穿透
}
}
// 「选区截取」:条目只有**右侧复选框**(不可展开),底部两个固定按钮。
if (clipMode)
{
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)
{
if (!insideContent || rowIndex >= layout.rowChecks.size() ||
layout.rowChecks[rowIndex].Width <= 0.0f ||
!UiPointInRect(layout.rowChecks[rowIndex], fx, fy))
{
continue;
}
ToggleLaneClipSignal(index, layout.rowRefs[rowIndex]);
m_meanRowHoverLane = -1;
m_meanRowHoverRef = SignalRef();
m_dragMode = PlotDragMode::NONE;
return true;
}
// 底部按钮(仅在双光标模式下有几何)。
const bool onBuild = layout.clipBuildButton.Width > 0.0f &&
UiPointInRect(layout.clipBuildButton, fx, fy);
const bool onDestructive = layout.clipDestructiveButton.Width > 0.0f &&
UiPointInRect(layout.clipDestructiveButton, fx, fy);
if (onBuild || onDestructive)
{
if (!doubleCursor)
{
SetHint(kClipNeedDoubleCursorHint);
m_dragMode = PlotDragMode::NONE;
return true;
}
// 截取区间 = 双光标(左 = 较小时间,右 = 较大时间)。
const double begin = std::min(Tab().CursorTime(), Tab().CursorTimeAlt());
const double end = std::max(Tab().CursorTime(), Tab().CursorTimeAlt());
m_pendingClipItems.clear();
for (const SignalRef& ref : clipLane.signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || !resolved.visible ||
!clipLane.ClipEnabled(ref))
{
continue;
}
ClipBuild build;
build.groupIndex = ref.groupIndex;
build.signalIndex = ref.signalIndex;
build.begin = begin;
build.end = end;
build.destructive = onDestructive;
m_pendingClipItems.push_back(build);
}
if (m_pendingClipItems.empty())
{
SetHint(L"选区截取:当前网格没有勾选任何信号。");
}
else
{
SetHint(onDestructive
? L"正在按选区截取原始信号(" +
std::to_wstring(m_pendingClipItems.size()) +
L" 条,区间外数据将被丢弃)…"
: L"正在按选区截取为新信号(" +
std::to_wstring(m_pendingClipItems.size()) + L" 条)…");
m_pendingClipLane = index;
}
m_dragMode = PlotDragMode::NONE;
return true;
}
if (insideContent)
{
m_dragMode = PlotDragMode::NONE;
return true; // 内容区其它位置:消费点击,不穿透
}
}
// 「弱峰调整」:两行滑块 = 调整幅度(-400% … +400%)/ 迭代次数(1 … 自定上限)。
// 语义与该面板旧式排版完全一致,只是外观改成与均值滤波同款的可滚动列表。
if (boostMode)
@@ -3325,6 +3431,9 @@ namespace jdi
// 用户只能改走右键菜单取消功能(用户 2026-11-03 报告)。
SetLaneResample(lane, false);
break;
case EdgePanelKind::CLIP:
SetLaneClip(lane, false);
break;
case EdgePanelKind::COUNT:
default:
SetLaneEdgeCount(lane, false);
@@ -3457,7 +3566,7 @@ namespace jdi
//(上百个信号合并时必备)。
if (hoveredKind == EdgePanelKind::MEAN || hoveredKind == EdgePanelKind::CENTER ||
hoveredKind == EdgePanelKind::PEAK || hoveredKind == EdgePanelKind::COUNT ||
hoveredKind == EdgePanelKind::RESAMPLE)
hoveredKind == EdgePanelKind::RESAMPLE || hoveredKind == EdgePanelKind::CLIP)
{
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoveredKind);
if (layout.content.Width > 0.0f &&
@@ -3495,6 +3604,9 @@ namespace jdi
case EdgePanelKind::RESAMPLE:
SetLaneResampleScroll(panelLane, target);
break;
case EdgePanelKind::CLIP:
SetLaneClipScroll(panelLane, target);
break;
default:
SetLaneMeanFilterScroll(panelLane, target);
break;