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

+120
View File
@@ -499,6 +499,90 @@ namespace jdi
return true;
}
bool DataSet::ClipSignalRange(int groupIndex, int signalIndex, double low, double high)
{
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
{
return false;
}
SignalGroup& group = m_groups[static_cast<std::size_t>(groupIndex)];
if (signalIndex < 0 || signalIndex >= static_cast<int>(group.signals.size()))
{
return false;
}
SignalData& signal = group.signals[static_cast<std::size_t>(signalIndex)];
if (signal.axisIndex >= group.axes.size())
{
return false;
}
TimeAxis& axis = group.axes[signal.axisIndex];
const std::size_t count = std::min(axis.time.size(), signal.values.size());
// 1. 逐点筛选:保留 [low, high] 内的采样(时间与值按下标一一对应)。
std::vector<double> keptTime;
std::vector<double> keptValue;
keptTime.reserve(count);
keptValue.reserve(count);
for (std::size_t index = 0; index < count; ++index)
{
const double time = axis.time[index];
if (!std::isfinite(time) || time < low || time > high)
{
continue;
}
keptTime.push_back(time);
keptValue.push_back(signal.values[index]);
}
if (keptValue.size() < 2)
{
return false; // 不足两个有效采样:不改动任何数据
}
// 2. 用「新建 + 交换 + shrink_to_fit」释放多余容量(`erase` 不会归还内存)。
{
std::vector<double> trimmed;
trimmed.swap(keptValue);
signal.values.swap(trimmed);
signal.values.shrink_to_fit();
}
{
std::vector<double> trimmedTime;
trimmedTime.swap(keptTime);
axis.time.swap(trimmedTime);
axis.time.shrink_to_fit();
}
// 3. 重算统计与粗粒度包络层(与加载/派生路径同一套实现)。
signal.coarseMin.clear();
signal.coarseMax.clear();
const SignalStats stats = ComputeStats(signal.values, signal.coarseMin, signal.coarseMax);
signal.coarseStride = kCoarseStride;
signal.minValue = stats.minValue;
signal.maxValue = stats.maxValue;
signal.finiteCount = stats.finiteCount;
signal.sparseTimes.clear();
signal.sparseValues.clear();
// 4. 时间轴元数据:截取后区间收窄,必须同步(否则"适应视图"会按旧范围取景)。
axis.minTime = axis.time.front();
axis.maxTime = axis.time.back();
axis.finiteCount = axis.time.size();
axis.monotonic = true;
bool monotonic = true;
for (std::size_t index = 1; index < axis.time.size(); ++index)
{
if (!std::isfinite(axis.time[index]) || axis.time[index] < axis.time[index - 1])
{
monotonic = false;
break;
}
}
axis.monotonic = monotonic;
// 有效采样很少时重新缓存全部有效点(与加载路径一致)。
CacheSparseSamples(axis, signal);
return true;
}
bool DataSet::RemoveGroup(int groupIndex)
{
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
@@ -1450,6 +1534,42 @@ namespace jdi
resampleItems.end());
}
// ---------------------------------------------------------------- 选区截取
const Lane::ClipItem* Lane::FindClipItem(const SignalRef& ref) const
{
for (const ClipItem& item : clipItems)
{
if (item.ref == ref)
{
return &item;
}
}
return nullptr;
}
Lane::ClipItem& Lane::EnsureClipItem(const SignalRef& ref)
{
for (ClipItem& item : clipItems)
{
if (item.ref == ref)
{
return item;
}
}
ClipItem item;
item.ref = ref;
clipItems.push_back(item);
return clipItems.back();
}
void Lane::RemoveClipItem(const SignalRef& ref)
{
clipItems.erase(std::remove_if(clipItems.begin(), clipItems.end(),
[&ref](const ClipItem& item) { return item.ref == ref; }),
clipItems.end());
}
// ---------------------------------------------------------------- 自由标记
namespace
+92
View File
@@ -146,6 +146,7 @@ namespace jdi
purgeRefs(lane.edgeCenterItems);
purgeRefs(lane.weakPeakItems);
purgeRefs(lane.resampleItems);
purgeRefs(lane.clipItems);
}
// 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
@@ -275,6 +276,79 @@ namespace jdi
return std::find(fileVisible.begin(), fileVisible.end(), false) == fileVisible.end();
}
void TabConfig::SetGroupVisibleSubset(int groupIndex, const std::vector<bool>& signalMask)
{
if (groupIndex < 0 || static_cast<std::size_t>(groupIndex) >= m_visible.size())
{
return;
}
std::vector<bool>& fileVisible = m_visible[static_cast<std::size_t>(groupIndex)];
// ⚠️ 只改 `signalMask` 覆盖到的元素(= 组内被筛选出来的信号)。
// 未通过筛选的信号可见性**保持不变**(用户 2026-11-03 明确要求)。
bool anyBecomeVisible = false;
const std::size_t count = std::min(fileVisible.size(), signalMask.size());
for (std::size_t index = 0; index < count; ++index)
{
if (!signalMask[index])
{
continue;
}
if (signalMask[index] && !fileVisible[index])
{
anyBecomeVisible = true;
}
fileVisible[index] = true;
}
// 与 `SetGroupVisible` 同一规则:合并模式下"新增可见"的信号各自独占网格。
if (anyBecomeVisible && m_laneMode == LaneMode::MERGED)
{
m_laneMode = LaneMode::CUSTOM;
}
SyncLanes();
}
bool TabConfig::GroupAllVisibleSubset(int groupIndex, const std::vector<bool>& signalMask) const
{
if (groupIndex < 0 || static_cast<std::size_t>(groupIndex) >= m_visible.size())
{
return false;
}
const std::vector<bool>& fileVisible = m_visible[static_cast<std::size_t>(groupIndex)];
const std::size_t count = std::min(fileVisible.size(), signalMask.size());
bool any = false;
for (std::size_t index = 0; index < count; ++index)
{
if (!signalMask[index])
{
continue;
}
any = true;
if (!fileVisible[index])
{
return false;
}
}
return any;
}
bool TabConfig::GroupHasVisibleSubset(int groupIndex, const std::vector<bool>& signalMask) const
{
if (groupIndex < 0 || static_cast<std::size_t>(groupIndex) >= m_visible.size())
{
return false;
}
const std::vector<bool>& fileVisible = m_visible[static_cast<std::size_t>(groupIndex)];
const std::size_t count = std::min(fileVisible.size(), signalMask.size());
for (std::size_t index = 0; index < count; ++index)
{
if (signalMask[index] && fileVisible[index])
{
return true;
}
}
return false;
}
bool TabConfig::AnyVisible() const
{
for (const std::vector<bool>& fileVisible : m_visible)
@@ -453,6 +527,9 @@ namespace jdi
lane.resample = source.resample;
lane.resamplePanel = source.resamplePanel;
lane.resampleScroll = source.resampleScroll;
lane.clip = source.clip;
lane.clipPanel = source.clipPanel;
lane.clipScroll = source.clipScroll;
// 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 弱峰调整 / 自由标记条目。
for (const Lane::MeanFilterItem& item : source.meanFilterItems)
{
@@ -498,6 +575,13 @@ namespace jdi
lane.resampleItems.push_back(item);
}
}
for (const Lane::ClipItem& item : source.clipItems)
{
if (item.ref == ref)
{
lane.clipItems.push_back(item);
}
}
for (const SignalRef& collapsed : source.freeMarkerCollapsedGroups)
{
if (collapsed == ref)
@@ -576,6 +660,14 @@ namespace jdi
++item.ref.signalIndex;
}
}
// 「选区截取」条目同样持有信号引用。
for (Lane::ClipItem& item : lane.clipItems)
{
if (item.ref.groupIndex == groupIndex && item.ref.signalIndex >= insertPos)
{
++item.ref.signalIndex;
}
}
}
}