Files
Just-DataInspector/Utilities/Src/DataModelTab.cpp
T
DeepSeek V4.1 Flash 34365b3c1c perf(size): 用自写整数格式化替换全部 std::to_wstring / std::to_string
背景:Code Review 报告 §10.3 / §11.5 建议「替换 std::to_wstring / std::to_string,
可去掉 gdtoa 链(≈33 KB)」。本轮把它做完,并给出这条建议的**真实边界**。

一、新增 `Utf8Util::FormatInteger*`(手写十进制展开,不经 printf)
* `FormatInteger(long long)` / `FormatIntegerUnsigned(unsigned long long)` /
  `FormatIntegerNarrow(long long)`(后者供 MatLoader 拼 HDF5 成员路径)。
* 语义与 `std::to_wstring` / `std::to_string` 完全一致:`0` → `"0"`、负数带 `-`、
  `LLONG_MIN` 用模 2^64 语义取绝对值(不会溢出)。
* 这样做的直接理由:预编译 libstdc++ 的 `to_string` 内部转调 `__mingw_swprintf`,
  工程里只要留一处,MinGW 的 printf 引擎(`mingw_wpformat.o`)+ gdtoa 就必然被链入。

二、全仓替换(76 处 `to_wstring` + 2 处 `to_string`,20 个文件)
* `MatLoader.cpp` 的 `TrimNumber` 整数分支也改为 `FormatInteger`(它曾是产物里
  **最后一个**引用 MinGW printf 的调用点)。
* 需要的地方补 `#include "Utf8Util.hpp"`(仅 `MatLoader.cpp` 一个新 include,其余由
  `PlotViewInternal` / `SignalPanelInternal` / `ApplicationInternal` / `DataModelInternal`
  已经包含)。

三、⚠️ 实测结论:gdtoa **去不掉**(把这件事彻底查清并写进 Makefile 注释)
* 只替换 `to_wstring`/`to_string`:**−1,024 B**(787,968 → 786,944)。
* 再叠加 `-D__USE_MINGW_ANSI_STDIO=0`(把 `swprintf` 让给系统 UCRT):**0 字节**。
* 根因(定位到文件与行):libstdc++ 的
  `lib/gcc/.../include/c++/x86_64-w64-mingw32/bits/os_defines.h:48-51` **无条件**执行
  `#undef __USE_MINGW_ANSI_STDIO` + `#define __USE_MINGW_ANSI_STDIO 1`
  (注释:libstdc++ depends on POSIX-definitions of those functions)。
  `g++ -E -dM` 实测最终值恒为 1 ⇒ **任何 C++ TU 的 `swprintf` / `strtod` 都是 MinGW 版本**,
  命令行无法覆盖。
* 要真省这 33 KB 只能自己实现「浮点 → 十进制(正确舍入)」与「浮点解析的
  十六进制/inf/nan 分支」,等价于重写 gdtoa。**判定:不做**(风险 >> 收益)。
* 该开关已回退,Makefile 里保留一段说明,避免后续接手者重复这两次实验。

四、验证
* 新增单元测试 `integer_replacement_matches_std`:12 个有符号样本(含 LLONG_MIN/MAX)
  + 5 个无符号样本(含 ULLONG_MAX),与 `std::to_wstring` **逐字符对照**,
  并校验窄/宽版本内容一致。
* `UnitTests`:`checks=260 failed=0 cases=19`(新增 41 条断言)。
* `OffscreenChecks -all`:6 个检查全 PASS,`checks=87 failed=0`。
* `panels -mode compare`:7 个面板像素哈希与几何**逐项不变**
  (整数文本从 `to_wstring` 换到手写实现后,渲染结果零差异 —— 这是最强的语义证据)。
* release 全量零 error / 零 warning;产物 787,968 → **786,944 B**。
2026-11-04 14:31:02 +08:00

1065 lines
32 KiB
C++
Raw 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"网格 " + FormatInteger(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"网格 " + FormatInteger(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"网格 " + FormatInteger(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