按组件目录结构把四个巨型实现文件拆分为多个协作文件(PlotView / SignalPanel / Application / DataModel)

承接上一条提交(0871eb0)—— 该提交只完成了 Components/Shared 与注释/命名清理,并**主动未做**
大文件拆分;本提交把大文件拆分补齐,至此“按功能拆分逻辑块到各组件自己的 Inc/Src”已落地。

## 一、拆分结果

| 原文件 | 拆分后 |
| --- | --- |
| Components/PlotView/Src/PlotView.cpp(13 695 行 / 544 KB) | PlotView.cpp(787) + PlotViewPaint.cpp(6 367) + PlotViewInteraction.cpp(2 715) + PlotViewFeatures.cpp(3 005) + PlotViewInternal.hpp(936) |
| Components/SignalPanel/Src/SignalPanel.cpp(4 184 行) | SignalPanel.cpp(1 141) + SignalPanelPaint.cpp(697) + SignalPanelInteraction.cpp(2 095) + SignalPanelInternal.hpp |
| Components/Application/Src/Application.cpp(3 135 行) | Application.cpp(1 427) + ApplicationCommands.cpp(1 184) + ApplicationLoading.cpp(270) + ApplicationInternal.hpp |
| Utilities/Src/DataModel.cpp(2 615 行) | DataModel.cpp(1 583,含自由函数区) + DataModelTab.cpp(732) + DataModelInternal.hpp |

分片职责(以 PlotView 为例):
  core        视图核心:构造/析构、上下文、布局、坐标映射、视界与平移、网格结构与归属
  paint       渲染与像素缓存:全部绘制、场景/装饰缓存、每网格图层、宽基底与滚动补条
  interaction 交互与编辑:命中测试、鼠标/键盘、光标形状、内联输入框、浮动面板层序
  features    视图特性与派生数据:光标读数/提示/缩放、自适应、吸附、单点、派生曲线线程池、
              边沿统计、实验性功能参数
(SignalPanel 为 core/paint/interaction;Application 为 core/commands/loading;
  DataModel 为 core/tab。)

## 二、关键手法(这是能安全拆开的前提)

原文件的辅助区都是**匿名命名空间**(PlotView 约 870 行常量与工具函数),被所有成员函数广泛引用。
直接拆文件会让各 .cpp 拿不到这些工具,因此:

1. 把该块整块搬进 `<组件>Internal.hpp`,从 `namespace { }` 改为**具名**命名空间
   (pvdetail / spdetail / appdetail / dmdetail);
2. 块内**每个函数与常量都标 inline** ⇒ 多个翻译单元共用同一个定义:既不重复符号,
   也不产生 N 份函数定义;
3. 头部末尾 `using namespace <ns>;` ⇒ 各 .cpp 函数体里原有的非限定调用(IntersectRect /
   NiceStep / ClampDouble …)**一行都不用改**;
4. 成员定义按「方法名 → 分片」归类后**整块搬移** —— 全程**纯代码搬移**,没有改动任何一行函数体。

## 三、拆分过程中踩到并修正的四个坑(已固化到 .preflight/perf/split_component.ps1)

1. `[[maybe_unused]]` 等属性**必须排在 inline 之前**(`inline [[x]] T` 非法);
   单独的 `{`/`}` 行、注释行、`struct`/`enum`/`using`/`template` 行不能加 inline。
2. 匿名命名空间的**结束行可能带注释**(`} // namespace`),只认 `^\t}$` 会一路匹配到后面
   某个成员函数的结束括号,把大段成员定义误当成“辅助区”搬走。
3. **首个成员定义之前的所有行**必须跟着进核心文件:DataModel.cpp 在匿名命名空间之后、
   首个成员定义之前还有约 390 行命名空间外的自由函数(SignalStats / ComputeStats /
   CacheSparseSamples / ApplySinglePointDefaultStyle …),漏掉会被静默丢弃。
4. 内部头必须带上原文件真正用到的全部头:DataModel 需要 Theme.hpp / <cstring> / <cstdio> /
   <deque> / <atomic> / <cwctype> / <new>(原文件靠这些头才编译得过)。

## 四、附带完成的命名审计

PlotView 中残留的 File 语义命名一并修正:LaneSpansMultipleFiles → LaneSpansMultipleGroups、
firstFile → firstGroup、multiFile → multiGroup、SceneCacheKey::haloFile → haloGroup。

## 五、验证(全部离屏:SW_HIDE + 不注入真实输入 + 进程/线程 BELOW_NORMAL)

- .preflight/perf/verify.exe(PlotView 渲染/交互,含逐像素断言)49/49
- .preflight/perf/panelanim.exe(信号面板:卷尺式折起/展开、筛选态、悬浮组名、目标行)156/156
- .preflight/perf/datacheck.exe(数据层:单轴/多时间轴 CSV、MF4、重命名语义)69/69
- .preflight/perf/edgecheck.exe(边沿统计与面板排版)46/46
- make release / make debug 零警告。
即:拆分后**行为与拆分前完全一致**(纯代码搬移,四套探针全绿)。

## 六、体积(诚实记录)

  Target.exe: 754 176 → 764 416 B(+10 240 B,+1.4%)
因为 inline 工具函数会在每个引用它的翻译单元里各生成一份(PE/COFF 链接器没有 ICF 合并)。
这再次印证:拆分解决的是**可维护性**,不是体积(本轮的 −24.6% 全部来自 ICO 256×256 条目改 PNG)。

## 七、文档

Documents/agent_mem.md:§9.37 新增“七、多文件拆分(已完成)”与“八、仍未完成”(只剩
Lane → LanePanelState、模态提示解耦、MF4 压缩块三项);§11.2 新增第 91、92 条(多文件化的
唯一安全手法 + 体积代价)。Documents/refactor_report_2026-10-16.md 同步更新 §五/§七/§八。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-16 04:02:36 +08:00
1 parent 0871eb0cba
commit e6b7bf66ec
19 files changed
+19130 -18771

No files matched your search

File diff suppressed because it is too large. Load diff
+352
View File
@@ -0,0 +1,352 @@
#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 依据全部可见信号重新生成网格划分。
* @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 = 新出现的信号)。 */
bool fitYToSelection = false;
bool edgeCount = false;
double edgeValue = 0.0;
bool edgeValueValid = false;
bool edgePanelOpen = true;
double edgePanelX = -1.0;
double edgePanelY = -1.0;
bool edgeCenter = false;
std::vector<Lane::EdgeCenterItem> edgeCenterItems;
bool edgeCenterPanelOpen = true;
double edgeCenterPanelX = -1.0;
double edgeCenterPanelY = -1.0;
bool meanFilter = false;
std::vector<Lane::MeanFilterItem> meanFilterItems;
bool meanFilterPanelOpen = true;
double meanFilterPanelX = -1.0;
double meanFilterPanelY = -1.0;
bool weakPeak = false;
std::vector<Lane::WeakPeakItem> weakPeakItems;
bool weakPeakPanelOpen = true;
double weakPeakPanelX = -1.0;
double weakPeakPanelY = -1.0;
};
// 先记下每个信号原来所在网格的网格自身状态:
// 勾选可见性会重建网格,若不携带,用户刚打开的开关会被悄悄清掉。
//
// ⚠️ 同时记下「重建之前是否**每个**网格都开着 `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.edgePanelOpen = lane.edgePanelOpen;
carry.edgePanelX = lane.edgePanelX;
carry.edgePanelY = lane.edgePanelY;
carry.edgeCenter = lane.edgeCenter;
carry.edgeCenterItems = lane.edgeCenterItems;
carry.edgeCenterPanelOpen = lane.edgeCenterPanelOpen;
carry.edgeCenterPanelX = lane.edgeCenterPanelX;
carry.edgeCenterPanelY = lane.edgeCenterPanelY;
carry.meanFilter = lane.meanFilter;
carry.meanFilterItems = lane.meanFilterItems;
carry.meanFilterPanelOpen = lane.meanFilterPanelOpen;
carry.meanFilterPanelX = lane.meanFilterPanelX;
carry.meanFilterPanelY = lane.meanFilterPanelY;
carry.weakPeak = lane.weakPeak;
carry.weakPeakItems = lane.weakPeakItems;
carry.weakPeakPanelOpen = lane.weakPeakPanelOpen;
carry.weakPeakPanelX = lane.weakPeakPanelX;
carry.weakPeakPanelY = lane.weakPeakPanelY;
for (const SignalRef& ref : lane.signals)
{
previousCarry.emplace_back(ref, carry);
}
}
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;
lane.edgeCount = carry.edgeCount;
lane.edgeValue = carry.edgeValue;
lane.edgeValueValid = carry.edgeValueValid;
lane.edgePanelOpen = carry.edgePanelOpen;
lane.edgePanelX = carry.edgePanelX;
lane.edgePanelY = carry.edgePanelY;
lane.edgeCenter = carry.edgeCenter;
lane.edgeCenterItems = carry.edgeCenterItems;
lane.edgeCenterPanelOpen = carry.edgeCenterPanelOpen;
lane.edgeCenterPanelX = carry.edgeCenterPanelX;
lane.edgeCenterPanelY = carry.edgeCenterPanelY;
lane.meanFilter = carry.meanFilter;
lane.meanFilterItems = carry.meanFilterItems;
lane.meanFilterPanelOpen = carry.meanFilterPanelOpen;
lane.meanFilterPanelX = carry.meanFilterPanelX;
lane.meanFilterPanelY = carry.meanFilterPanelY;
lane.weakPeak = carry.weakPeak;
lane.weakPeakItems = carry.weakPeakItems;
lane.weakPeakPanelOpen = carry.weakPeakPanelOpen;
lane.weakPeakPanelX = carry.weakPeakPanelX;
lane.weakPeakPanelY = carry.weakPeakPanelY;
};
// 收集全部可见信号引用。
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)
{
lanes[index].name = L"网格 " + std::to_wstring(index + 1);
}
}
} // namespace dmdetail
using namespace dmdetail;
} // namespace jdi
#endif // __DATAMODEL_INTERNAL_HPP__
+732
View File
@@ -0,0 +1,732 @@
// 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::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);
}
// 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。
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();
}
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 (const SignalRef& ref : signals)
{
Lane lane;
lane.autoRange = source.autoRange;
lane.yMin = source.yMin;
lane.yMax = source.yMax;
lane.fitYToSelection = source.fitYToSelection;
lane.edgeCount = source.edgeCount;
lane.edgeValue = source.edgeValue;
lane.edgeValueValid = source.edgeValueValid;
lane.edgePanelOpen = source.edgePanelOpen;
lane.edgePanelX = source.edgePanelX;
lane.edgePanelY = source.edgePanelY;
lane.edgeCenter = source.edgeCenter;
lane.edgeCenterPanelOpen = source.edgeCenterPanelOpen;
lane.edgeCenterPanelX = source.edgeCenterPanelX;
lane.edgeCenterPanelY = source.edgeCenterPanelY;
lane.weakPeak = source.weakPeak;
lane.weakPeakPanelOpen = source.weakPeakPanelOpen;
lane.weakPeakPanelX = source.weakPeakPanelX;
lane.weakPeakPanelY = source.weakPeakPanelY;
lane.meanFilter = source.meanFilter;
lane.meanFilterPanelOpen = source.meanFilterPanelOpen;
lane.meanFilterPanelX = source.meanFilterPanelX;
lane.meanFilterPanelY = source.meanFilterPanelY;
// 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 弱峰调整条目。
lane.meanFilterItems.clear();
lane.edgeCenterItems.clear();
lane.weakPeakItems.clear();
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);
}
}
lane.signals.push_back(ref);
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;
}
}
}
}
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)
{
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;
}
low = std::min(low, axis.minTime);
high = std::max(high, axis.maxTime);
}
}
if (!std::isfinite(low) || !std::isfinite(high) || !(high > low))
{
return false;
}
minTime = low;
maxTime = high;
return true;
}
} // namespace jdi