Files
DeepSeek-V4.1-Flash 266ab657be fix(perf): 公式映射导致确认/适应视图/拖拽排序/组复选框四处卡死根治
四次卡顿是**同一个根因**:公式信号(不保序)的幅值范围与包络带各写了一份
「逐点扫描整条信号」,且都没有缓存。2580 万采样时实测 1589ms + 1481ms,
而幅值范围指纹包含公式文本 / 信号顺序 / 可见性 / lane.autoRange
⇒ 确认编辑、适应视图、拖拽排序、组复选框每一个都会让它失效、每次都重付 3.1 秒。

修法:
- 新增 DataSet::FormulaTransformCache / FormulaTransformFor:一次扫描同时算出
  绘制值域与逐分组包络,缓存到数据层(std::deque,指针稳定)。
- TabConfig::ComputeLaneRange 的公式分支改为查该缓存(保留非法公式的逐点回退)。
- PlotView::AcquireFormulaEnvelope 改为直接复用数据层同一条目,
  删除 PlotView 自有的 FormulaEnvelopeEntry / m_formulaEnvelopes 与两个重复常量。
- FormulaEval 新增 FormulaProgram(编译一次、批量求值):always_inline、
  常量折叠(2/1.25 => 1.6)、幂次改写(x^2 => x*x、x^3、x^0.5 => sqrt)、
  按分组两级循环(去掉每点一次整数除法)。
- EvaluateFormula 复用 thread_local FormulaProgram(按内容缓存),两条路径语义一致。

实测(2580 万采样,纯控制台探针):确认公式 3070ms => 379ms;改 x^2 1.9s => 0.19s;
适应视图 / 拖拽排序 / 组复选框 1589ms => 0.0015ms(命中缓存)。
回归:缓存命中/失效/值域、14 组公式语义与逐位一致性、非法/空公式回退全部 PASS;
release/debug 零警告;Release TargetCheck.exe 890,880 B(< 1MB)。

未做:跨组移动共享时间轴仍深拷贝 197MB(55-66ms,值语义决定,风险大于收益);
首次 379ms 仍同步阻塞(后台化的复杂度暂不划算)。
2026-11-04 02:02:03 +08:00

1065 lines
32 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.
// DataModel 实现分片 —— 本文件只包含 DataModel 的**成员函数定义**(纯代码搬移,行为与拆分前完全一致)。
//
// 共用的常量与自由函数在 DataModelInternal.hpp(命名空间 `dmdetail`,全部 inline)。
// 分片按“职责”划分:核心(状态/布局/几何)、渲染、交互与编辑,见各文件名。
#include "DataModelInternal.hpp"
namespace jdi
{
TabConfig::TabConfig()
: m_name(L"视图 1")
{
}
void TabConfig::SyncToDataSet(const DataSet& dataSet)
{
const std::size_t groupCount = dataSet.GroupCount();
m_visible.resize(groupCount);
// ⚠️ 只对**新出现的**分组应用默认折叠状态:已存在的分组保持用户当前的展开/折叠选择。
m_collapsed.resize(groupCount, m_groupsCollapsedByDefault);
for (std::size_t groupIndex = 0; groupIndex < groupCount; ++groupIndex)
{
const SignalGroup* group = dataSet.GroupAt(static_cast<int>(groupIndex));
if (group == nullptr)
{
continue;
}
// 新增信号默认为未勾选(由用户自行决定查看哪些信号),
// 其显示外观已在 DataSet 中初始化完毕。
m_visible[groupIndex].resize(group->signals.size(), false);
}
// 清理已失效的网格引用(选中状态由 DataSet 统一维护)。
for (Lane& lane : m_lanes)
{
std::vector<SignalRef>& refs = lane.signals;
refs.erase(std::remove_if(refs.begin(), refs.end(),
[&dataSet](const SignalRef& ref) { return dataSet.Resolve(ref) == nullptr; }),
refs.end());
}
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
[](const Lane& lane) { return lane.signals.empty(); }),
m_lanes.end());
SyncLanes();
}
void TabConfig::RemapAfterGroupRemoval(int groupIndex)
{
if (groupIndex < 0)
{
return;
}
const std::size_t slot = static_cast<std::size_t>(groupIndex);
if (slot < m_visible.size())
{
m_visible.erase(m_visible.begin() + groupIndex);
}
if (slot < m_collapsed.size())
{
m_collapsed.erase(m_collapsed.begin() + groupIndex);
}
for (Lane& lane : m_lanes)
{
std::vector<SignalRef>& refs = lane.signals;
refs.erase(std::remove_if(refs.begin(), refs.end(),
[groupIndex](const SignalRef& ref)
{ return ref.groupIndex == groupIndex; }),
refs.end());
for (SignalRef& ref : refs)
{
if (ref.groupIndex > groupIndex)
{
--ref.groupIndex;
}
}
}
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
[](const Lane& lane) { return lane.signals.empty(); }),
m_lanes.end());
}
void TabConfig::RemapAfterSignalMove(const SignalRef& from, const SignalRef& to, bool visible)
{
if (from.groupIndex < 0 || to.groupIndex < 0 || from.groupIndex == to.groupIndex)
{
return;
}
// 1) 可见性表:目标分组在目标位置插入一列(沿用源分组里的可见性),源分组删除一列。
// ⚠️⚠️ 目标分组可能是**刚新建**的(「移动到… → 新建分组」):此时 `m_visible`
// 还没有它那一行。必须在这里**自行扩展**(否则插入被跳过 ⇒ 信号到新组后
// 变为未勾选、曲线直接消失)。不能依赖调用方先 `SyncToDataSet`:那次同步会
// 按**旧下标**清理引用,而数据层马上就要整体位移。
const std::size_t toSlot = static_cast<std::size_t>(to.groupIndex);
if (toSlot >= m_visible.size())
{
m_visible.resize(toSlot + 1);
}
if (toSlot < m_visible.size() &&
static_cast<std::size_t>(to.signalIndex) <= m_visible[toSlot].size())
{
m_visible[toSlot].insert(m_visible[toSlot].begin() + to.signalIndex, visible);
}
const std::size_t fromSlot = static_cast<std::size_t>(from.groupIndex);
if (fromSlot < m_visible.size() &&
static_cast<std::size_t>(from.signalIndex) < m_visible[fromSlot].size())
{
m_visible[fromSlot].erase(m_visible[fromSlot].begin() + from.signalIndex);
}
// 2) 全部信号引用:先改写被移动的那一条,再做 ±1 位移(顺序不可调换,见头文件说明)。
const auto adjust = [&from, &to](SignalRef& ref)
{
if (ref.groupIndex == from.groupIndex && ref.signalIndex == from.signalIndex)
{
ref = to;
return;
}
if (ref.groupIndex == to.groupIndex && ref.signalIndex >= to.signalIndex)
{
++ref.signalIndex;
}
else if (ref.groupIndex == from.groupIndex && ref.signalIndex > from.signalIndex)
{
--ref.signalIndex;
}
};
// 条目集合都是「带 `ref` 成员的结构体」,用泛型 lambda 统一处理(与
// `RemapAfterSignalRemoval` 的 `purgeRefs` 同一手法)。
const auto adjustItems = [&adjust](auto& items)
{
for (auto& item : items)
{
adjust(item.ref);
}
};
for (Lane& lane : m_lanes)
{
for (SignalRef& ref : lane.signals)
{
adjust(ref);
}
adjustItems(lane.meanFilterItems);
adjustItems(lane.edgeCenterItems);
adjustItems(lane.weakPeakItems);
adjustItems(lane.resampleItems);
adjustItems(lane.clipItems);
adjustItems(lane.freeMarkerItems);
}
}
void TabConfig::RemapAfterSignalReorder(int groupIndex, int from, int to)
{
if (groupIndex < 0 || from < 0 || to < 0 || from == to)
{
return;
}
// 线性重排映射:被移动的那条改为 `to`,被它跨过的条目各让一位。
const auto mapIndex = [from, to](int index)
{
if (index == from)
{
return to;
}
if (from < to)
{
if (index > from && index <= to)
{
return index - 1;
}
}
else
{
if (index >= to && index < from)
{
return index + 1;
}
}
return index;
};
// 可见性表整体重排(与信号顺序严格一致)。
const std::size_t slot = static_cast<std::size_t>(groupIndex);
if (slot < m_visible.size() && static_cast<std::size_t>(from) < m_visible[slot].size() &&
static_cast<std::size_t>(to) < m_visible[slot].size())
{
std::vector<bool>& row = m_visible[slot];
const bool moved = row[static_cast<std::size_t>(from)];
row.erase(row.begin() + from);
row.insert(row.begin() + to, moved);
}
const auto adjust = [groupIndex, &mapIndex](SignalRef& ref)
{
if (ref.groupIndex == groupIndex)
{
ref.signalIndex = mapIndex(ref.signalIndex);
}
};
const auto adjustItems = [&adjust](auto& items)
{
for (auto& item : items)
{
adjust(item.ref);
}
};
for (Lane& lane : m_lanes)
{
for (SignalRef& ref : lane.signals)
{
adjust(ref);
}
adjustItems(lane.meanFilterItems);
adjustItems(lane.edgeCenterItems);
adjustItems(lane.weakPeakItems);
adjustItems(lane.resampleItems);
adjustItems(lane.clipItems);
adjustItems(lane.freeMarkerItems);
}
}
void TabConfig::RemapAfterSignalRemoval(int groupIndex, int signalIndex)
{
if (groupIndex < 0 || signalIndex < 0)
{
return;
}
// 可见性表:删掉该信号对应的那一列。
const std::size_t slot = static_cast<std::size_t>(groupIndex);
if (slot < m_visible.size())
{
std::vector<bool>& fileVisible = m_visible[slot];
if (static_cast<std::size_t>(signalIndex) < fileVisible.size())
{
fileVisible.erase(fileVisible.begin() + signalIndex);
}
}
// 网格归属:删掉该信号本身,并把同一分组内位于其后的引用下标前移一位。
for (Lane& lane : m_lanes)
{
std::vector<SignalRef>& refs = lane.signals;
refs.erase(std::remove_if(refs.begin(), refs.end(),
[groupIndex, signalIndex](const SignalRef& ref)
{
return ref.groupIndex == groupIndex &&
ref.signalIndex == signalIndex;
}),
refs.end());
for (SignalRef& ref : refs)
{
if (ref.groupIndex == groupIndex && ref.signalIndex > signalIndex)
{
--ref.signalIndex;
}
}
// 均值滤波 / 中心轴边沿计数 / 弱峰调整的条目同样持有信号引用,必须一起删除/前移。
// 三类条目都是「带 `ref` 的结构体」,因此用泛型 lambda 统一处理
//(弱峰调整的参数与均值滤波不同,不能再共用同一个具体类型)。
const auto purgeRefs = [groupIndex, signalIndex](auto& items)
{
using ItemType = typename std::decay_t<decltype(items)>::value_type;
items.erase(std::remove_if(items.begin(), items.end(),
[groupIndex, signalIndex](const ItemType& item)
{
return item.ref.groupIndex == groupIndex &&
item.ref.signalIndex == signalIndex;
}),
items.end());
for (ItemType& item : items)
{
if (item.ref.groupIndex == groupIndex && item.ref.signalIndex > signalIndex)
{
--item.ref.signalIndex;
}
}
};
purgeRefs(lane.meanFilterItems);
purgeRefs(lane.edgeCenterItems);
purgeRefs(lane.weakPeakItems);
purgeRefs(lane.resampleItems);
purgeRefs(lane.clipItems);
}
// 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
[](const Lane& lane) { return lane.signals.empty(); }),
m_lanes.end());
}
bool TabConfig::IsVisible(const SignalRef& ref) const
{
if (ref.groupIndex < 0 || static_cast<std::size_t>(ref.groupIndex) >= m_visible.size())
{
return false;
}
const std::vector<bool>& fileVisible = m_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)];
}
void TabConfig::SetVisible(const SignalRef& ref, bool visible)
{
if (ref.groupIndex < 0 || static_cast<std::size_t>(ref.groupIndex) >= m_visible.size())
{
return;
}
std::vector<bool>& fileVisible = m_visible[static_cast<std::size_t>(ref.groupIndex)];
if (ref.signalIndex < 0 || static_cast<std::size_t>(ref.signalIndex) >= fileVisible.size())
{
return;
}
if (fileVisible[static_cast<std::size_t>(ref.signalIndex)] == visible)
{
return;
}
fileVisible[static_cast<std::size_t>(ref.signalIndex)] = visible;
// 合并模式下新勾选的信号独占一个新网格:切到自定义划分后,
// RebuildLanes 会把尚未分配网格的可见信号各自放入新网格。
if (visible && m_laneMode == LaneMode::MERGED)
{
m_laneMode = LaneMode::CUSTOM;
}
SyncLanes();
}
void TabConfig::ToggleVisible(const SignalRef& ref)
{
SetVisible(ref, !IsVisible(ref));
}
void TabConfig::SetGroupVisible(int groupIndex, bool visible)
{
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)];
// 合并模式下“新增”的信号同样应各自独占网格(与 SetVisible 一致):
// 先切到自定义划分,RebuildLanes 会把尚未分配网格的可见信号追加到最下方。
// 只有确实存在“原本不可见”的信号时才切模式,避免重复点击默默改掉划分策略。
const bool anyHidden =
std::find(fileVisible.begin(), fileVisible.end(), false) != fileVisible.end();
if (visible && anyHidden && m_laneMode == LaneMode::MERGED)
{
m_laneMode = LaneMode::CUSTOM;
}
std::fill(fileVisible.begin(), fileVisible.end(), visible);
SyncLanes();
}
void TabConfig::SetAllVisible(bool visible)
{
bool anyHidden = false;
for (const std::vector<bool>& fileVisible : m_visible)
{
if (std::find(fileVisible.begin(), fileVisible.end(), false) != fileVisible.end())
{
anyHidden = true;
break;
}
}
// 同上:合并模式下新增的信号各自独占网格,且排列在最下方。
if (visible && anyHidden && m_laneMode == LaneMode::MERGED)
{
m_laneMode = LaneMode::CUSTOM;
}
for (std::vector<bool>& fileVisible : m_visible)
{
std::fill(fileVisible.begin(), fileVisible.end(), visible);
}
SyncLanes();
}
void TabConfig::SoloSignal(const SignalRef& ref)
{
for (std::vector<bool>& fileVisible : m_visible)
{
std::fill(fileVisible.begin(), fileVisible.end(), false);
}
SetVisible(ref, true);
}
bool TabConfig::GroupHasVisible(int groupIndex) 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)];
return std::find(fileVisible.begin(), fileVisible.end(), true) != fileVisible.end();
}
bool TabConfig::GroupAllVisible(int groupIndex) 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)];
if (fileVisible.empty())
{
return false;
}
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)
{
if (std::find(fileVisible.begin(), fileVisible.end(), true) != fileVisible.end())
{
return true;
}
}
return false;
}
int TabConfig::VisibleCount() const
{
int count = 0;
for (const std::vector<bool>& fileVisible : m_visible)
{
count += static_cast<int>(std::count(fileVisible.begin(), fileVisible.end(), true));
}
return count;
}
void TabConfig::SetMode(LaneMode mode)
{
m_laneMode = mode;
SyncLanes();
}
void TabConfig::SyncLanes()
{
RebuildLanes(m_visible, m_lanes, m_laneMode);
}
int TabConfig::LaneOfSignal(const SignalRef& ref) const
{
for (std::size_t lane = 0; lane < m_lanes.size(); ++lane)
{
const std::vector<SignalRef>& refs = m_lanes[lane].signals;
if (std::find(refs.begin(), refs.end(), ref) != refs.end())
{
return static_cast<int>(lane);
}
}
return -1;
}
int TabConfig::MoveSignalToLane(const DataSet& dataSet, const SignalRef& ref, int laneIndex)
{
if (dataSet.Resolve(ref) == nullptr)
{
return -1;
}
const int sourceLane = LaneOfSignal(ref);
if (laneIndex >= 0 && laneIndex == sourceLane)
{
// 拖回原网格:无需变动,避免因网格回收导致顺序抖动。
return sourceLane;
}
// 先把目标网格的“锚信号”固定下来:移除信号后空网格会被回收,
// 下标随之变化,必须用不依赖下标的引用找回真正的目标网格
// (否则会变成“与原网格交换位置”而不是合并进去)。
SignalRef targetAnchor;
bool hasTarget = (laneIndex >= 0 && laneIndex < static_cast<int>(m_lanes.size()));
if (hasTarget)
{
const std::vector<SignalRef>& targetSignals = m_lanes[static_cast<std::size_t>(laneIndex)].signals;
for (const SignalRef& candidate : targetSignals)
{
if (!(candidate == ref))
{
targetAnchor = candidate;
break;
}
}
}
// 手动指定归属意味着切换到自定义划分策略。
m_laneMode = LaneMode::CUSTOM;
for (Lane& lane : m_lanes)
{
std::vector<SignalRef>& refs = lane.signals;
refs.erase(std::remove(refs.begin(), refs.end(), ref), refs.end());
}
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
[](const Lane& lane) { return lane.signals.empty(); }),
m_lanes.end());
int destination = -1;
if (hasTarget && targetAnchor.IsValid())
{
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
const std::vector<SignalRef>& refs = m_lanes[index].signals;
if (std::find(refs.begin(), refs.end(), targetAnchor) != refs.end())
{
destination = static_cast<int>(index);
break;
}
}
}
if (destination >= 0)
{
m_lanes[static_cast<std::size_t>(destination)].signals.push_back(ref);
}
else
{
Lane lane;
lane.autoRange = true;
lane.signals.push_back(ref);
m_lanes.push_back(std::move(lane));
}
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
}
return LaneOfSignal(ref);
}
int TabConfig::SplitLane(int laneIndex)
{
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
{
return -1;
}
const std::vector<SignalRef> signals = m_lanes[static_cast<std::size_t>(laneIndex)].signals;
if (signals.size() <= 1)
{
// 只有一条信号时无需拆分(本就独占一个网格)。
return -1;
}
// 手动排布 → 切换到自定义划分策略。
m_laneMode = LaneMode::CUSTOM;
// 用原网格的“网格自身状态”(Y 轴自适应/边沿计数等)作为每一份的初始状态,
// 这样拆分出来的网格保持原网格的显示设置。
const Lane source = m_lanes[static_cast<std::size_t>(laneIndex)];
std::vector<Lane> pieces;
pieces.reserve(signals.size());
for (std::size_t signalSlot = 0; signalSlot < signals.size(); ++signalSlot)
{
const SignalRef& ref = signals[signalSlot];
// ⚠️ 只有**首个信号**所在的网格继承原网格的实验性功能(用户 2026-10-21 要求:
// 拆分后只有“拆分前的网格内的第一个信号”保留已应用的功能)。
// 否则拆分会把一个网格的功能(以及它的逐信号条目)复制到每一份上。
const bool primary = (signalSlot == 0);
Lane lane;
lane.autoRange = source.autoRange;
lane.yMin = source.yMin;
lane.yMax = source.yMax;
lane.fitYToSelection = source.fitYToSelection;
if (primary)
{
lane.edgeCount = source.edgeCount;
lane.edgeValue = source.edgeValue;
lane.edgeValueValid = source.edgeValueValid;
lane.edgePanel = source.edgePanel;
lane.edgeCenter = source.edgeCenter;
lane.edgeCenterPanel = source.edgeCenterPanel;
lane.weakPeak = source.weakPeak;
lane.weakPeakPanel = source.weakPeakPanel;
lane.meanFilter = source.meanFilter;
lane.meanFilterPanel = source.meanFilterPanel;
lane.freeMarker = source.freeMarker;
lane.freeMarkerCursor = source.freeMarkerCursor;
lane.freeMarkerShowDelta = source.freeMarkerShowDelta;
lane.freeMarkerShowEnum = source.freeMarkerShowEnum;
lane.freeMarkerPanel = source.freeMarkerPanel;
lane.freeMarkerOpacity = source.freeMarkerOpacity;
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)
{
if (item.ref == ref)
{
lane.meanFilterItems.push_back(item);
}
}
for (const Lane::EdgeCenterItem& item : source.edgeCenterItems)
{
if (item.ref == ref)
{
lane.edgeCenterItems.push_back(item);
}
}
for (const Lane::WeakPeakItem& item : source.weakPeakItems)
{
if (item.ref == ref)
{
lane.weakPeakItems.push_back(item);
}
}
for (const Lane::FreeMarkerItem& item : source.freeMarkerItems)
{
if (item.ref == ref)
{
// 选中下标按“第几个条目”跟随(过滤会改变下标)。
if (source.freeMarkerSelected >= 0 &&
static_cast<std::size_t>(source.freeMarkerSelected) <
source.freeMarkerItems.size() &&
&source.freeMarkerItems[static_cast<std::size_t>(source.freeMarkerSelected)] ==
&item)
{
lane.freeMarkerSelected = static_cast<int>(lane.freeMarkerItems.size());
}
lane.freeMarkerItems.push_back(item);
}
}
for (const Lane::ResampleItem& item : source.resampleItems)
{
if (item.ref == ref)
{
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)
{
lane.freeMarkerCollapsedGroups.push_back(collapsed);
}
}
}
lane.signals.push_back(ref);
PruneLaneSignalItems(lane);
pieces.push_back(std::move(lane));
}
// 用 pieces 替换原网格(插入到原位置,因此其后方的网格整体顺次后移)。
m_lanes.erase(m_lanes.begin() + laneIndex);
m_lanes.insert(m_lanes.begin() + laneIndex, pieces.begin(), pieces.end());
// 统一重命名,保证编号连续(GX 编号在各显示位置均由此位置推导)。
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
}
return laneIndex;
}
void TabConfig::RemapAfterSignalInsert(int groupIndex, int insertPos)
{
if (groupIndex < 0 || insertPos < 0)
{
return;
}
// 可见性表:在插入点补一列(新信号默认不可见,由调用方按需勾选)。
const std::size_t slot = static_cast<std::size_t>(groupIndex);
if (slot < m_visible.size() && static_cast<std::size_t>(insertPos) <= m_visible[slot].size())
{
m_visible[slot].insert(m_visible[slot].begin() + insertPos, false);
}
// 网格内的信号引用:同一文件中位于插入点之后的引用下标整体后移。
for (Lane& lane : m_lanes)
{
for (SignalRef& ref : lane.signals)
{
if (ref.groupIndex == groupIndex && ref.signalIndex >= insertPos)
{
++ref.signalIndex;
}
}
// 均值滤波 / 中心轴条目同样持有信号引用,必须一起后移。
for (Lane::MeanFilterItem& item : lane.meanFilterItems)
{
if (item.ref.groupIndex == groupIndex && item.ref.signalIndex >= insertPos)
{
++item.ref.signalIndex;
}
}
for (Lane::EdgeCenterItem& item : lane.edgeCenterItems)
{
if (item.ref.groupIndex == groupIndex && item.ref.signalIndex >= insertPos)
{
++item.ref.signalIndex;
}
}
for (Lane::WeakPeakItem& item : lane.weakPeakItems)
{
if (item.ref.groupIndex == groupIndex && item.ref.signalIndex >= insertPos)
{
++item.ref.signalIndex;
}
}
// 「重采样」条目同样持有信号引用。
for (Lane::ResampleItem& item : lane.resampleItems)
{
if (item.ref.groupIndex == groupIndex && item.ref.signalIndex >= insertPos)
{
++item.ref.signalIndex;
}
}
// 「选区截取」条目同样持有信号引用。
for (Lane::ClipItem& item : lane.clipItems)
{
if (item.ref.groupIndex == groupIndex && item.ref.signalIndex >= insertPos)
{
++item.ref.signalIndex;
}
}
}
}
bool TabConfig::MoveLaneOfSignalBelow(const SignalRef& moved, const SignalRef& anchor)
{
const int from = LaneOfSignal(moved);
if (from < 0 || LaneOfSignal(anchor) < 0)
{
return false;
}
Lane lane = std::move(m_lanes[static_cast<std::size_t>(from)]);
m_lanes.erase(m_lanes.begin() + from);
// erase 之后锚点网格的下标可能变化,必须用信号引用重新定位。
const int target = LaneOfSignal(anchor);
if (target < 0)
{
m_lanes.push_back(std::move(lane));
}
else
{
m_lanes.insert(m_lanes.begin() + target + 1, std::move(lane));
}
// 手动排布 → 切换到自定义划分策略,避免后续重建网格时顺序被还原。
m_laneMode = LaneMode::CUSTOM;
RenameLanes();
return true;
}
bool TabConfig::MoveLane(int from, int target)
{
const int count = static_cast<int>(m_lanes.size());
if (from < 0 || from >= count || target < 0 || target > count)
{
return false;
}
// `target` 语义 = 「插入到该下标之前」;向后拖时扣除自身占位后再比较,
// 保证"拖到自己原来的位置"不会产生无意义的移动。
int destination = target;
if (destination > from)
{
--destination;
}
destination = std::max(0, std::min(destination, count - 1));
if (destination == from)
{
return false;
}
Lane moved = std::move(m_lanes[static_cast<std::size_t>(from)]);
m_lanes.erase(m_lanes.begin() + from);
m_lanes.insert(m_lanes.begin() + destination, std::move(moved));
// 手动排布 → 切换到自定义划分策略,避免后续重建网格时顺序被还原。
m_laneMode = LaneMode::CUSTOM;
RenameLanes();
return true;
}
void TabConfig::RenameLanes()
{
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
}
}
bool TabConfig::IsCollapsed(int groupIndex) const
{
if (groupIndex < 0 || static_cast<std::size_t>(groupIndex) >= m_collapsed.size())
{
return false;
}
return m_collapsed[static_cast<std::size_t>(groupIndex)];
}
void TabConfig::ToggleCollapsed(int groupIndex)
{
if (groupIndex < 0)
{
return;
}
if (static_cast<std::size_t>(groupIndex) >= m_collapsed.size())
{
m_collapsed.resize(static_cast<std::size_t>(groupIndex) + 1, m_groupsCollapsedByDefault);
}
m_collapsed[static_cast<std::size_t>(groupIndex)] = !m_collapsed[static_cast<std::size_t>(groupIndex)];
}
void TabConfig::ComputeLaneRange(const DataSet& dataSet, int laneIndex, double& yMin, double& yMax) const
{
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
{
yMin = 0.0;
yMax = 1.0;
return;
}
const Lane& lane = m_lanes[static_cast<std::size_t>(laneIndex)];
if (!lane.autoRange)
{
yMin = lane.yMin;
yMax = lane.yMax;
if (!(yMax > yMin))
{
yMin = 0.0;
yMax = 1.0;
}
return;
}
double low = std::numeric_limits<double>::infinity();
double high = -std::numeric_limits<double>::infinity();
for (const SignalRef& ref : lane.signals)
{
if (!IsVisible(ref))
{
continue;
}
const SignalData* signal = dataSet.Resolve(ref);
if (signal == nullptr || signal->finiteCount == 0)
{
continue;
}
// ⚠️ 自适应必须基于**画出来的曲线**:先按绘制变换(系数 → 绝对值 → 镜像)把该信号的
// 原始值域映射成显示值域,否则调整系数/镜像后自适应范围仍按原始数据算(范围错误)。
const SignalStyle* style = dataSet.Style(ref);
double signalLow = signal->minValue;
double signalHigh = signal->maxValue;
if (style != nullptr)
{
if (style->HasFormula())
{
// ⚠️⚠️ 「公式映射」不保证单调(`x*x` / `1/x`)⇒ 端点映射会算错。
// 逐点扫描整条信号(2580 万采样 ≈ 1.6 秒)**必须**由数据层的
// `FormulaTransformFor` 缓存承担 —— 它在第一次建立时同时算出
// 「绘制值域」与「逐分组包络」,之后(改视界 / 缩放 / 适应视图 /
// 拖拽排序 / 勾选组)全部是 O(1)。旧实现在这里逐点重扫,导致
// 「确认公式」「适应视图」「拖拽信号」「组复选框」都整个应用卡死
//(用户 2026-11-04 报告)。
const DataSet::FormulaTransformCache* transform =
dataSet.FormulaTransformFor(*signal, *style);
if (transform != nullptr && transform->ready)
{
signalLow = transform->low;
signalHigh = transform->high;
}
else
{
// 回退(公式非法 / 缓存不可用):保持原逐点语义,绝不改变显示结果。
double formulaLow = std::numeric_limits<double>::infinity();
double formulaHigh = -std::numeric_limits<double>::infinity();
for (const double raw : signal->values)
{
if (!std::isfinite(raw))
{
continue;
}
const double mapped = ApplyDrawTransform(*style, raw);
if (!std::isfinite(mapped))
{
continue;
}
formulaLow = std::min(formulaLow, mapped);
formulaHigh = std::max(formulaHigh, mapped);
}
if (std::isfinite(formulaLow) && std::isfinite(formulaHigh))
{
signalLow = formulaLow;
signalHigh = formulaHigh;
}
}
}
else
{
TransformValueRange(*style, signalLow, signalHigh);
}
}
low = std::min(low, signalLow);
high = std::max(high, signalHigh);
}
if (!std::isfinite(low) || !std::isfinite(high))
{
low = 0.0;
high = 1.0;
}
PadRange(low, high);
yMin = low;
yMax = high;
}
bool TabConfig::ComputeVisibleTimeBounds(const DataSet& dataSet, double& minTime, double& maxTime) const
{
double low = std::numeric_limits<double>::infinity();
double high = -std::numeric_limits<double>::infinity();
for (std::size_t groupIndex = 0; groupIndex < m_visible.size(); ++groupIndex)
{
const std::vector<bool>& groupVisible = m_visible[groupIndex];
if (std::find(groupVisible.begin(), groupVisible.end(), true) == groupVisible.end())
{
continue;
}
const SignalGroup* group = dataSet.GroupAt(static_cast<int>(groupIndex));
if (group == nullptr)
{
continue;
}
// 只统计**可见信号**真正用到的时间轴:一个分组可能有多条时间轴
//(多 `time` 列 CSV / MF4 多通道组),把没显示的那些也算进来会让“适应视图”
// 拉出一个过宽的时间范围。
for (std::size_t signalIndex = 0; signalIndex < groupVisible.size() &&
signalIndex < group->signals.size();
++signalIndex)
{
if (!groupVisible[signalIndex])
{
continue;
}
const TimeAxis& axis = group->AxisOf(group->signals[signalIndex]);
if (axis.finiteCount == 0)
{
continue;
}
// ⚠️ **必须叠加该信号自己的时间轴偏移**(`SignalStyle::offsetTime`):
// 采样实际画在 `t + offsetTime` 处,视界要覆盖的是「画出来的位置」。
// 少了这一项,「调整 → 时间轴」设置了偏移后点「适应视图」仍会用原始时间范围,
// 用户看到的是曲线整体偏移到视界之外(用户 2026-10-29 报告的缺陷)。
const SignalStyle* style = dataSet.Style(
SignalRef{static_cast<int>(groupIndex), static_cast<int>(signalIndex)});
const double timeShift = (style != nullptr) ? style->offsetTime : 0.0;
low = std::min(low, axis.minTime + timeShift);
high = std::max(high, axis.maxTime + timeShift);
}
}
if (!std::isfinite(low) || !std::isfinite(high) || !(high > low))
{
return false;
}
minTime = low;
maxTime = high;
return true;
}
} // namespace jdi