Files
DeepSeek V4.1 Flash 62220477c0 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。
2026-11-03 16:04:48 +08:00

477 lines
18 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#ifndef __DATAMODEL_INTERNAL_HPP__
#define __DATAMODEL_INTERNAL_HPP__
// DataModel 组件内部共享的常量与工具函数(私有头,仅本组件的 .cpp 使用)。
//
// 本组件的实现按功能拆成多个协作文件,它们共用这一份内部工具集。
//
// ⚠️ 为让多个翻译单元都能使用,原匿名命名空间改为**具名**命名空间 `dmdetail`,
// 并把其中每个函数与常量标为 `inline` —— 单一定义、单一副本,不会产生代码重复。
// 各实现文件在包含本头之后用 `using namespace dmdetail;` 引入这些工具,
// 因此函数体里原有的非限定调用一行都不用改。
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstdio>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <cwctype>
#include <deque>
#include <limits>
#include <new>
#include <string>
#include <utility>
#include <vector>
#include "DataModel.hpp"
#include "Theme.hpp"
#include "Utf8Util.hpp"
namespace jdi
{
namespace dmdetail
{
/**
* @brief DataModel 内部工具集(常量与自由函数)。
*
* 全部符号都是 `inline` ⇒ 多个翻译单元共用同一个定义:既不重复符号,也不产生多份代码。
* 不要把它们移回匿名命名空间(那样每个 .cpp 都会各自复制一份)。
*/
inline const double kMissing = std::numeric_limits<double>::quiet_NaN(); /**< 缺失值占位。 */
inline const double kDefaultLineWidth = 1.6; /**< 默认线宽。 */
/**
* @brief 为网格范围补充留白,避免波形紧贴边界。
*
* ⚠️ 实际规则实现在 `DataModel.hpp` 的 `PadValueRange()`:它同时被
* 「选区 Y 轴自适应」使用,两处必须逐位一致(否则两种自适应会算出不同区间)。
*
* @param [IN, OUT] yMin 范围下限。
* @param [IN, OUT] yMax 范围上限。
*/
inline void PadRange(double& yMin, double& yMax)
{
PadValueRange(yMin, yMax);
}
/**
* @brief 清掉网格里**不属于本网格信号**的逐信号条目(均值滤波 / 中心轴 / 弱峰调整 / 自由标记)。
*
* 这些条目按信号保存,但只有“该信号就在本网格里”时才有意义。
* 网格重建(合并 → 拆分)时上一帧的条目是按网格整体携带的,若不过滤就会出现:
* 拆分出来的新网格里仍然画着**其它信号**的固定点(用户 2026-10-21 报告:
* 第二个网格上出现“野生的”自由标记点,而浮动面板里又没有任何条目)。
*
* @param [IN,OUT] lane 目标网格(`signals` 必须已经就位)。
*/
inline void PruneLaneSignalItems(Lane& lane)
{
const auto inLane = [&lane](const SignalRef& ref)
{
return std::find(lane.signals.begin(), lane.signals.end(), ref) != lane.signals.end();
};
const auto prune = [&inLane](auto& items)
{
items.erase(std::remove_if(items.begin(), items.end(),
[&inLane](const auto& item) { return !inLane(item.ref); }),
items.end());
};
prune(lane.meanFilterItems);
prune(lane.edgeCenterItems);
prune(lane.weakPeakItems);
prune(lane.freeMarkerItems);
prune(lane.resampleItems);
prune(lane.clipItems);
lane.freeMarkerCollapsedGroups.erase(
std::remove_if(lane.freeMarkerCollapsedGroups.begin(), lane.freeMarkerCollapsedGroups.end(),
[&inLane](const SignalRef& ref) { return !inLane(ref); }),
lane.freeMarkerCollapsedGroups.end());
}
/**
* @brief 依据全部可见信号重新生成网格划分。
* @param [IN] visible 各信号的可见性(按 [分组][信号] 索引)。
* @param [IN] lanes 网格集合(原地更新)。
* @param [IN] mode 网格划分策略。
*/
inline void RebuildLanes(const std::vector<std::vector<bool>>& visible, std::vector<Lane>& lanes,
LaneMode mode)
{
// 随网格携带的“网格自身状态”(按信号继承):重建网格时不能丢。
struct LaneCarry
{
bool matched = false; /**< 是否找到该信号原本所在的网格(false = 新出现的信号)。 */
/**
* @brief 该信号是否是原网格的**首个信号**(= 这个网格的“实验性功能”持有者)。
*
* ⚠️ 实验性功能(边沿计数 / 中心轴 / 均值滤波 / 弱峰调整 / 自由标记)是**网格级**开关,
* 但一个网格可能与其它信号共用(合并网格)。若把“开着”的状态按信号逐个继承,
* 那么「合并网格 → 拆分网格」之后**每个**拆出来的网格都会带着这些功能
* (用户 2026-10-21 报告:第二个网格也自动启用了实验性功能、并出现了
* 其它信号的固定点镜像)。因此只有**首个信号**继承功能状态,其余信号视为默认(关闭)。
*/
bool owner = false;
bool fitYToSelection = false;
bool edgeCount = false;
double edgeValue = 0.0;
bool edgeValueValid = false;
LanePanelState edgePanel;
bool edgeCenter = false;
std::vector<Lane::EdgeCenterItem> edgeCenterItems;
LanePanelState edgeCenterPanel;
bool meanFilter = false;
std::vector<Lane::MeanFilterItem> meanFilterItems;
LanePanelState meanFilterPanel;
bool weakPeak = false;
std::vector<Lane::WeakPeakItem> weakPeakItems;
LanePanelState weakPeakPanel;
bool freeMarker = false;
bool freeMarkerCursor = false;
std::vector<Lane::FreeMarkerItem> freeMarkerItems;
LanePanelState freeMarkerPanel;
float freeMarkerOpacity = 1.0f;
float freeMarkerScroll = 0.0f;
int freeMarkerSelected = -1;
bool freeMarkerShowDelta = false;
bool freeMarkerShowEnum = false;
std::vector<SignalRef> freeMarkerCollapsedGroups;
/** @brief 「重采样」开关与逐信号参数(详见 `Lane::resample`)。 */
bool resample = false;
std::vector<Lane::ResampleItem> resampleItems;
LanePanelState resamplePanel;
float resampleScroll = 0.0f;
/** @brief 「选区截取」开关与逐信号目标标记(详见 `Lane::clip`)。 */
bool clip = false;
std::vector<Lane::ClipItem> clipItems;
LanePanelState clipPanel;
float clipScroll = 0.0f;
/** @brief 「均值滤波」面板内容的滚动偏移(详见 `Lane::meanFilterScroll`)。 */
float meanFilterScroll = 0.0f;
/** @brief 「中心轴边沿计数」面板内容的滚动偏移(详见 `Lane::edgeCenterScroll`)。 */
float edgeCenterScroll = 0.0f;
/** @brief 「弱峰调整」面板内容的滚动偏移(详见 `Lane::weakPeakScroll`)。 */
float weakPeakScroll = 0.0f;
/** @brief 「边沿计数」面板内容的滚动偏移(详见 `Lane::edgeCountScroll`)。 */
float edgeCountScroll = 0.0f;
};
// 先记下每个信号原来所在网格的网格自身状态:
// 勾选可见性会重建网格,若不携带,用户刚打开的开关会被悄悄清掉。
//
// ⚠️ 同时记下「重建之前是否**每个**网格都开着 `fitYToSelection`」:
// 那就是「全局 Y 轴自适应」的正式定义。新勾选信号产生的**新网格**必须继承它,
// 否则全局开关看着是开的、新网格却不按当前视界自适应(用户报告的缺陷)。
bool previousAllAutoFit = !lanes.empty();
for (const Lane& lane : lanes)
{
if (!lane.fitYToSelection)
{
previousAllAutoFit = false;
break;
}
}
std::vector<std::pair<SignalRef, LaneCarry>> previousCarry;
for (const Lane& lane : lanes)
{
LaneCarry carry;
carry.matched = true;
carry.fitYToSelection = lane.fitYToSelection;
carry.edgeCount = lane.edgeCount;
carry.edgeValue = lane.edgeValue;
carry.edgeValueValid = lane.edgeValueValid;
carry.edgePanel = lane.edgePanel;
carry.edgeCenter = lane.edgeCenter;
carry.edgeCenterItems = lane.edgeCenterItems;
carry.edgeCenterPanel = lane.edgeCenterPanel;
carry.edgeCenterScroll = lane.edgeCenterScroll;
carry.weakPeakScroll = lane.weakPeakScroll;
carry.edgeCountScroll = lane.edgeCountScroll;
carry.meanFilter = lane.meanFilter;
carry.meanFilterItems = lane.meanFilterItems;
carry.meanFilterPanel = lane.meanFilterPanel;
carry.meanFilterScroll = lane.meanFilterScroll;
carry.weakPeak = lane.weakPeak;
carry.weakPeakItems = lane.weakPeakItems;
carry.weakPeakPanel = lane.weakPeakPanel;
carry.freeMarker = lane.freeMarker;
carry.freeMarkerCursor = lane.freeMarkerCursor;
carry.freeMarkerItems = lane.freeMarkerItems;
carry.freeMarkerPanel = lane.freeMarkerPanel;
carry.freeMarkerOpacity = lane.freeMarkerOpacity;
carry.freeMarkerScroll = lane.freeMarkerScroll;
carry.freeMarkerSelected = lane.freeMarkerSelected;
carry.freeMarkerShowDelta = lane.freeMarkerShowDelta;
carry.freeMarkerShowEnum = lane.freeMarkerShowEnum;
carry.freeMarkerCollapsedGroups = lane.freeMarkerCollapsedGroups;
carry.resample = lane.resample;
carry.resampleItems = lane.resampleItems;
carry.resamplePanel = lane.resamplePanel;
carry.resampleScroll = lane.resampleScroll;
carry.clip = lane.clip;
carry.clipItems = lane.clipItems;
carry.clipPanel = lane.clipPanel;
carry.clipScroll = lane.clipScroll;
for (std::size_t signalIndex = 0; signalIndex < lane.signals.size(); ++signalIndex)
{
LaneCarry entry = carry;
// 只有首个信号是这些网格级开关的持有者(见 `LaneCarry::owner` 说明)。
entry.owner = (signalIndex == 0);
previousCarry.emplace_back(lane.signals[signalIndex], entry);
}
}
const auto inheritedCarry = [&previousCarry](const SignalRef& ref)
{
for (const std::pair<SignalRef, LaneCarry>& entry : previousCarry)
{
if (entry.first == ref)
{
return entry.second;
}
}
return LaneCarry();
};
const auto applyCarry = [&inheritedCarry, previousAllAutoFit](Lane& lane, const SignalRef& ref)
{
const LaneCarry carry = inheritedCarry(ref);
// 新出现的信号(之前没有网格):继承「全局 Y 轴自适应」是否开启。
lane.fitYToSelection = carry.matched ? carry.fitYToSelection : previousAllAutoFit;
// ⚠️ 实验性功能的状态**只由原网格的首个信号继承**(见 `LaneCarry::owner`):
// 否则「合并网格 → 拆分网格」会把一个网格的功能复制到全部拆分结果上。
if (!carry.matched || !carry.owner)
{
return; // 其它信号:保持 `Lane` 的默认值(全部功能关闭、条目为空)。
}
lane.edgeCount = carry.edgeCount;
lane.edgeValue = carry.edgeValue;
lane.edgeValueValid = carry.edgeValueValid;
lane.edgePanel.open = carry.edgePanel.open;
lane.edgePanel.collapsed = carry.edgePanel.collapsed;
lane.edgePanel.x = carry.edgePanel.x;
lane.edgePanel.y = carry.edgePanel.y;
lane.edgeCenter = carry.edgeCenter;
lane.edgeCenterItems = carry.edgeCenterItems;
lane.edgeCenterPanel.open = carry.edgeCenterPanel.open;
lane.edgeCenterPanel.collapsed = carry.edgeCenterPanel.collapsed;
lane.edgeCenterPanel.x = carry.edgeCenterPanel.x;
lane.edgeCenterPanel.y = carry.edgeCenterPanel.y;
lane.edgeCenterScroll = carry.edgeCenterScroll;
lane.weakPeakScroll = carry.weakPeakScroll;
lane.edgeCountScroll = carry.edgeCountScroll;
lane.meanFilter = carry.meanFilter;
lane.meanFilterItems = carry.meanFilterItems;
lane.meanFilterPanel.open = carry.meanFilterPanel.open;
lane.meanFilterPanel.collapsed = carry.meanFilterPanel.collapsed;
lane.meanFilterPanel.x = carry.meanFilterPanel.x;
lane.meanFilterPanel.y = carry.meanFilterPanel.y;
lane.weakPeak = carry.weakPeak;
lane.weakPeakItems = carry.weakPeakItems;
lane.weakPeakPanel.open = carry.weakPeakPanel.open;
lane.weakPeakPanel.collapsed = carry.weakPeakPanel.collapsed;
lane.weakPeakPanel.x = carry.weakPeakPanel.x;
lane.weakPeakPanel.y = carry.weakPeakPanel.y;
lane.freeMarker = carry.freeMarker;
lane.freeMarkerCursor = carry.freeMarkerCursor;
lane.freeMarkerItems = carry.freeMarkerItems;
lane.freeMarkerPanel.open = carry.freeMarkerPanel.open;
lane.freeMarkerPanel.collapsed = carry.freeMarkerPanel.collapsed;
lane.freeMarkerPanel.x = carry.freeMarkerPanel.x;
lane.freeMarkerPanel.y = carry.freeMarkerPanel.y;
lane.freeMarkerOpacity = carry.freeMarkerOpacity;
lane.freeMarkerScroll = carry.freeMarkerScroll;
lane.freeMarkerSelected = carry.freeMarkerSelected;
lane.freeMarkerShowDelta = carry.freeMarkerShowDelta;
lane.freeMarkerShowEnum = carry.freeMarkerShowEnum;
lane.freeMarkerCollapsedGroups = carry.freeMarkerCollapsedGroups;
lane.meanFilterScroll = carry.meanFilterScroll;
lane.resample = carry.resample;
lane.resampleItems = carry.resampleItems;
lane.resamplePanel.open = carry.resamplePanel.open;
lane.resamplePanel.collapsed = carry.resamplePanel.collapsed;
lane.resamplePanel.x = carry.resamplePanel.x;
lane.resamplePanel.y = carry.resamplePanel.y;
lane.resampleScroll = carry.resampleScroll;
lane.clip = carry.clip;
lane.clipItems = carry.clipItems;
lane.clipPanel.open = carry.clipPanel.open;
lane.clipPanel.collapsed = carry.clipPanel.collapsed;
lane.clipPanel.x = carry.clipPanel.x;
lane.clipPanel.y = carry.clipPanel.y;
lane.clipScroll = carry.clipScroll;
};
// 收集全部可见信号引用。
std::vector<SignalRef> visibleRefs;
for (std::size_t groupIndex = 0; groupIndex < visible.size(); ++groupIndex)
{
for (std::size_t signalIndex = 0; signalIndex < visible[groupIndex].size(); ++signalIndex)
{
if (visible[groupIndex][signalIndex])
{
SignalRef ref;
ref.groupIndex = static_cast<int>(groupIndex);
ref.signalIndex = static_cast<int>(signalIndex);
visibleRefs.push_back(ref);
}
}
}
// 判断某引用对应的信号是否可见(越界视为不可见)。
const auto isVisibleRef = [&visible](const SignalRef& ref)
{
if (ref.groupIndex < 0 || static_cast<std::size_t>(ref.groupIndex) >= visible.size())
{
return false;
}
const std::vector<bool>& fileVisible = visible[static_cast<std::size_t>(ref.groupIndex)];
if (ref.signalIndex < 0 || static_cast<std::size_t>(ref.signalIndex) >= fileVisible.size())
{
return false;
}
return fileVisible[static_cast<std::size_t>(ref.signalIndex)];
};
// 某信号当前所属的网格下标(-1 表示尚未分配网格)。
const auto laneIndexOf = [&lanes](const SignalRef& ref)
{
for (std::size_t index = 0; index < lanes.size(); ++index)
{
const std::vector<SignalRef>& refs = lanes[index].signals;
if (std::find(refs.begin(), refs.end(), ref) != refs.end())
{
return static_cast<int>(index);
}
}
return -1;
};
if (mode == LaneMode::PER_SIGNAL)
{
// 每条可见信号独占一个网格:网格数量严格跟随可见信号数量。
// **顺序按"原有网格位置"优先**:已经分配过网格的信号保持其相对次序,
// 新勾选(此前没有网格)的信号统一追加到最下方。
// 若直接按 visibleRefs(文件/列序)重建,新勾选的信号会按其列序
// 插到中间,位置看起来"不确定"。
std::vector<SignalRef> ordered;
ordered.reserve(visibleRefs.size());
for (const SignalRef& ref : visibleRefs)
{
if (laneIndexOf(ref) >= 0)
{
ordered.push_back(ref);
}
}
std::stable_sort(ordered.begin(), ordered.end(),
[&laneIndexOf](const SignalRef& a, const SignalRef& b)
{ return laneIndexOf(a) < laneIndexOf(b); });
for (const SignalRef& ref : visibleRefs)
{
if (laneIndexOf(ref) < 0)
{
ordered.push_back(ref);
}
}
lanes.clear();
lanes.reserve(ordered.size());
for (const SignalRef& ref : ordered)
{
Lane lane;
lane.autoRange = true;
applyCarry(lane, ref);
lane.signals.push_back(ref);
lanes.push_back(std::move(lane));
}
}
else if (mode == LaneMode::MERGED)
{
// 全部可见信号合并到同一网格;无可见信号时不保留空网格。
bool previousAutoRange = true;
double previousYMin = -1.0;
double previousYMax = 1.0;
if (lanes.size() == 1)
{
previousAutoRange = lanes[0].autoRange;
previousYMin = lanes[0].yMin;
previousYMax = lanes[0].yMax;
}
lanes.clear();
if (!visibleRefs.empty())
{
Lane lane;
lane.autoRange = previousAutoRange;
lane.yMin = previousYMin;
lane.yMax = previousYMax;
applyCarry(lane, visibleRefs.front());
lane.signals = visibleRefs;
lanes.push_back(std::move(lane));
}
}
else
{
// 自定义划分:仅移除不可见信号与由此产生的空网格,保持用户排布。
for (Lane& lane : lanes)
{
std::vector<SignalRef>& refs = lane.signals;
refs.erase(std::remove_if(refs.begin(), refs.end(),
[&isVisibleRef](const SignalRef& ref)
{ return !isVisibleRef(ref); }),
refs.end());
}
lanes.erase(std::remove_if(lanes.begin(), lanes.end(),
[](const Lane& lane) { return lane.signals.empty(); }),
lanes.end());
// 未分配网格的可见信号各自独占一个新网格。
// 注意:不能把它们塞进已有的第一个网格——那会导致“拖拽合并后新勾选的信号
// 被悄悄并入合并网格”,用户看不到新增的独立泳道。
for (const SignalRef& ref : visibleRefs)
{
bool placed = false;
for (const Lane& lane : lanes)
{
if (std::find(lane.signals.begin(), lane.signals.end(), ref) != lane.signals.end())
{
placed = true;
break;
}
}
if (placed)
{
continue;
}
Lane lane;
lane.autoRange = true;
applyCarry(lane, ref);
lane.signals.push_back(ref);
lanes.push_back(std::move(lane));
}
}
// 清掉“不属于本网格信号”的逐信号条目(合并网格时它们会被整块携带过来)。
// 统一重命名,保证编号连续。
for (std::size_t index = 0; index < lanes.size(); ++index)
{
PruneLaneSignalItems(lanes[index]);
lanes[index].name = L"网格 " + std::to_wstring(index + 1);
}
}
} // namespace dmdetail
using namespace dmdetail;
} // namespace jdi
#endif // __DATAMODEL_INTERNAL_HPP__