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:
1 parent
5d305cb10f
commit
62220477c0
18 files changed
+1263
-67
No files matched your search
@@ -337,6 +337,41 @@ namespace jdi
|
||||
*/
|
||||
bool BuildResampledSignal(int groupIndex, int signalIndex, double interval);
|
||||
|
||||
/**
|
||||
* @brief 「选区截取 → 截取为新信号」:按 `[begin, end]` 区间截取出**新信号**。
|
||||
*
|
||||
* 语义(用户 2026-11-03 明确要求):
|
||||
* * 采样值 = 源信号在该区间内的**原始数据**(不烘焙任何调整);
|
||||
* * 名称 = `<源信号名> (AAAA - BBBB)`,其中 AAAA / BBBB 是选区左右端时刻,
|
||||
* 以秒为单位、**保留三位小数**并带 `s` 单位(如 `(1.234s - 2.345s)`);
|
||||
* * 插入到源信号下方,并**继承源信号的整套调整**(公式/系数/偏移/镜像/绝对值)。
|
||||
*
|
||||
* @param [IN] groupIndex 源信号所属分组下标。
|
||||
* @param [IN] signalIndex 源信号下标。
|
||||
* @param [IN] begin 选区左端(显示时刻,秒)。
|
||||
* @param [IN] end 选区右端(显示时刻,秒)。
|
||||
* @return 构建并插入成功返回 true。
|
||||
*/
|
||||
bool BuildClippedSignal(int groupIndex, int signalIndex, double begin, double end);
|
||||
|
||||
/**
|
||||
* @brief 「选区截取 → 原始信号截取」:**破坏性**丢弃 `[begin, end]` 之外的采样。
|
||||
*
|
||||
* 与 `BuildClippedSignal` 不同,这里直接改写源信号的采样数据与所属时间轴:
|
||||
* * 保留区间内的采样,区间外的采样与对应时间全部丢弃并释放(`shrink_to_fit`);
|
||||
* * 名称不变,也不新增信号;
|
||||
* * 重算统计(min/max/finiteCount/粗粒度包络)。
|
||||
*
|
||||
* ⚠️ 该操作**不可撤销**,因此面板按钮语义上等价于"就地截断原始信号"。
|
||||
*
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
* @param [IN] signalIndex 信号下标。
|
||||
* @param [IN] begin 选区左端(显示时刻,秒)。
|
||||
* @param [IN] end 选区右端(显示时刻,秒)。
|
||||
* @return 成功返回 true。
|
||||
*/
|
||||
bool ClipSignalInPlace(int groupIndex, int signalIndex, double begin, double end);
|
||||
|
||||
/**
|
||||
* @brief 把一条信号插入到「源信号」的下方,并同步各标签页的引用与可见性。
|
||||
*
|
||||
|
||||
@@ -949,6 +949,165 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 选区截取
|
||||
|
||||
bool Application::BuildClippedSignal(int groupIndex, int signalIndex, double begin, double end)
|
||||
{
|
||||
const SignalGroup* group = m_dataSet.GroupAt(groupIndex);
|
||||
if (group == nullptr || signalIndex < 0 ||
|
||||
signalIndex >= static_cast<int>(group->signals.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!std::isfinite(begin) || !std::isfinite(end))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const double low = std::min(begin, end);
|
||||
const double high = std::max(begin, end);
|
||||
if (!(high > low))
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"选区截取失败:双光标区间宽度为 0。");
|
||||
return false;
|
||||
}
|
||||
|
||||
const SignalData& source = group->signals[static_cast<std::size_t>(signalIndex)];
|
||||
const TimeAxis& axis = group->AxisOf(source);
|
||||
const std::wstring baseName = source.name;
|
||||
|
||||
// 源信号的整套「调整」快照(插入会移动信号存储,之后不能再解引用 `source`)。
|
||||
SignalStyle sourceStyle;
|
||||
bool hasSourceStyle = false;
|
||||
{
|
||||
const SignalStyle* style = m_dataSet.Style(SignalRef{groupIndex, signalIndex});
|
||||
if (style != nullptr)
|
||||
{
|
||||
sourceStyle = *style;
|
||||
hasSourceStyle = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 逐点收集**区间内**的原始采样(不烘焙任何调整)。
|
||||
// ⚠️⚠️ 比较必须用**显示时刻**(`time + offsetTime`):双光标给出的是屏幕上看到的时刻,
|
||||
// 而采样画在 `time + offsetTime` 处(`offsetTime` 不进采样值,只平移绘制位置)。
|
||||
// 若直接拿 `time` 与选区比较,带时间轴偏移的信号会截错区间(用户 2026-11-03)。
|
||||
const double timeShift = hasSourceStyle ? sourceStyle.offsetTime : 0.0;
|
||||
const std::size_t count = std::min(axis.time.size(), source.values.size());
|
||||
std::vector<double> keptTimes;
|
||||
std::vector<double> keptValues;
|
||||
keptTimes.reserve(count);
|
||||
keptValues.reserve(count);
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
const double time = axis.time[index];
|
||||
if (!std::isfinite(time))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double shown = time + timeShift;
|
||||
if (shown < low || shown > high)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
keptTimes.push_back(time);
|
||||
keptValues.push_back(source.values[index]);
|
||||
}
|
||||
if (keptValues.size() < 2)
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"选区截取失败:区间内的有效采样点不足两个。");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 名称尾缀:`(AAAA - BBBB)`,秒、保留三位小数(用户 2026-11-03 明确要求)。
|
||||
// 数值取**显示时刻**(与双光标读数一致),因此加上 `timeShift`。
|
||||
const std::wstring suffix =
|
||||
L" (" + FormatDecimalFixed(keptTimes.front() + timeShift, 3) + L"s - " +
|
||||
FormatDecimalFixed(keptTimes.back() + timeShift, 3) + L"s)";
|
||||
|
||||
// 3. 新建时间轴(截取后的采样刻度无法复用源信号的时间轴)。
|
||||
TimeAxis newAxis;
|
||||
newAxis.name = axis.name;
|
||||
newAxis.time = std::move(keptTimes);
|
||||
newAxis.monotonic = true;
|
||||
newAxis.finiteCount = newAxis.time.size();
|
||||
newAxis.minTime = newAxis.time.front();
|
||||
newAxis.maxTime = newAxis.time.back();
|
||||
std::size_t axisIndex = 0;
|
||||
if (!m_dataSet.AppendGroupAxis(groupIndex, std::move(newAxis), axisIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 组装新信号并插入到源信号下方。
|
||||
SignalData derived;
|
||||
derived.name = baseName + suffix;
|
||||
derived.axisIndex = axisIndex;
|
||||
derived.values = std::move(keptValues);
|
||||
|
||||
int newIndex = -1;
|
||||
if (!InsertSignalBelowSource(groupIndex, signalIndex, std::move(derived), true, newIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. 继承源信号的**整套调整**(不烘值 ⇒ 新曲线与源信号在区间内逐像素重合)。
|
||||
if (hasSourceStyle)
|
||||
{
|
||||
if (SignalStyle* derivedStyle = m_dataSet.Style(SignalRef{groupIndex, newIndex}))
|
||||
{
|
||||
derivedStyle->color = sourceStyle.color;
|
||||
derivedStyle->customColor = sourceStyle.customColor;
|
||||
derivedStyle->opacity = sourceStyle.opacity;
|
||||
derivedStyle->lineWidth = sourceStyle.lineWidth;
|
||||
derivedStyle->mode = sourceStyle.mode;
|
||||
derivedStyle->formula = sourceStyle.formula;
|
||||
derivedStyle->gain = sourceStyle.gain;
|
||||
derivedStyle->absolute = sourceStyle.absolute;
|
||||
derivedStyle->mirrorVertical = sourceStyle.mirrorVertical;
|
||||
derivedStyle->mirrorHorizontal = sourceStyle.mirrorHorizontal;
|
||||
derivedStyle->offsetY = sourceStyle.offsetY;
|
||||
derivedStyle->offsetTime = sourceStyle.offsetTime;
|
||||
}
|
||||
}
|
||||
|
||||
SyncTabsToDataSet();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::ClipSignalInPlace(int groupIndex, int signalIndex, double begin, double end)
|
||||
{
|
||||
if (!std::isfinite(begin) || !std::isfinite(end))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const double low = std::min(begin, end);
|
||||
const double high = std::max(begin, end);
|
||||
if (!(high > low))
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"选区截取失败:双光标区间宽度为 0。");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 破坏性截取会移动/释放采样 ⇒ 先暂停后台派生计算,避免它读到已释放的数据。
|
||||
m_plot.PauseDerivedAsync();
|
||||
// ⚠️ 双光标给的是**显示时刻**,而采样时刻是 `time`;这里换算回采样时刻区间
|
||||
// (`offsetTime` 不烘进采样值,只平移绘制位置)。
|
||||
double timeShift = 0.0;
|
||||
if (const SignalStyle* style = m_dataSet.Style(SignalRef{groupIndex, signalIndex}))
|
||||
{
|
||||
timeShift = style->offsetTime;
|
||||
}
|
||||
const bool ok = m_dataSet.ClipSignalRange(groupIndex, signalIndex, low - timeShift,
|
||||
high - timeShift);
|
||||
if (!ok)
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"选区截取失败:区间内的有效采样点不足两个。");
|
||||
return false;
|
||||
}
|
||||
SyncTabsToDataSet();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::InsertSignalBelowSource(int groupIndex, int sourceIndex, SignalData&& signal,
|
||||
bool makeVisible, int& newIndex)
|
||||
{
|
||||
@@ -1165,6 +1324,42 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 「选区截取」:两个按钮(截取为新信号 / 原始信号截取)共用同一份区间。
|
||||
{
|
||||
int laneIndex = -1;
|
||||
std::vector<PlotView::ClipBuild> items;
|
||||
if (m_plot.TakePendingClipBuild(laneIndex, items) && !items.empty())
|
||||
{
|
||||
// 同一文件内必须按下标降序处理(插入会让后续下标 +1)。
|
||||
std::sort(items.begin(), items.end(),
|
||||
[](const PlotView::ClipBuild& a, const PlotView::ClipBuild& b)
|
||||
{
|
||||
if (a.groupIndex != b.groupIndex)
|
||||
{
|
||||
return a.groupIndex < b.groupIndex;
|
||||
}
|
||||
return a.signalIndex > b.signalIndex;
|
||||
});
|
||||
int built = 0;
|
||||
for (const PlotView::ClipBuild& item : items)
|
||||
{
|
||||
if (item.destructive
|
||||
? ClipSignalInPlace(item.groupIndex, item.signalIndex, item.begin, item.end)
|
||||
: BuildClippedSignal(item.groupIndex, item.signalIndex, item.begin, item.end))
|
||||
{
|
||||
++built;
|
||||
}
|
||||
}
|
||||
if (built > 0)
|
||||
{
|
||||
SetStatusMessage((items.front().destructive ? L"已截取 " : L"已截取为新信号 ") +
|
||||
std::to_wstring(built) + L" 条。",
|
||||
false);
|
||||
}
|
||||
return built > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 两类「构建为新信号」请求(均值滤波 / 弱峰调整)机制完全一致,
|
||||
// 统一整理成同一份构建清单后按下标降序处理。
|
||||
std::vector<DerivedSignalRequest> requests;
|
||||
@@ -1397,6 +1592,14 @@ namespace jdi
|
||||
resample.id = PLOT_CMD_LANE_RESAMPLE;
|
||||
resample.checked = m_plot.LaneResample(laneIndex);
|
||||
experimental.submenu.push_back(resample);
|
||||
|
||||
// 「选区截取」位于「重采样」下方、「自由标记」上方(用户 2026-11-03 要求)。
|
||||
MenuItem clip;
|
||||
clip.label = L"选区截取";
|
||||
clip.id = PLOT_CMD_LANE_CLIP;
|
||||
clip.checked = m_plot.LaneClip(laneIndex);
|
||||
experimental.submenu.push_back(clip);
|
||||
|
||||
experimental.submenu.push_back(freeMarker);
|
||||
}
|
||||
items.push_back(experimental);
|
||||
@@ -1701,6 +1904,10 @@ namespace jdi
|
||||
// 「实验性功能 → 重采样」:切换重采样参数面板。
|
||||
m_plot.ToggleLaneResample(laneIndex);
|
||||
break;
|
||||
case PLOT_CMD_LANE_CLIP:
|
||||
// 「实验性功能 → 选区截取」:切换选区截取面板。
|
||||
m_plot.ToggleLaneClip(laneIndex);
|
||||
break;
|
||||
case PLOT_CMD_LANE_SPLIT:
|
||||
// 「拆分当前网格」:信号按个拆成独立网格,新网格顺次插入其下方。
|
||||
m_plot.SplitLane(laneIndex);
|
||||
|
||||
Reference in new issue
Block a user