一、采样间隔/频率显示不一致(用户报告 1ms vs 1000us、100us 0ns) * FormatDurationCompound 改为「先量化到整数皮秒、再整数分解」, 彻底消除同一数值因浮点噪声落到不同单位/留下零尾数的问题; 秒级分支与亚秒分支共用 totalPs 方案,次级进位/单位升级集中在一处。 * FormatFrequency 的单位选择改为按「舍入后的显示值」判定(999.9999999Hz 与 1000Hz 均为 1.000kHz),并做向下校正避免 0.001kHz。 二、选区截取面板 * 提示文本改为用户指定原文「启用双光标模式以进行选区截取」。 * EdgePanelWidth 计入该提示的实际文本宽度;绘制改用 UiTextClipped 不再省略。 * viewHeight 补回漏减的 bottomPad,修掉条目区与按钮之间的空白。 三、未展开的信号条目栏悬停不再变成手型光标。 四、开启「显示布局边界」后 GX 标签可正常拖拽排序 * 抽出 PaintLaneReorderOverlay(吸附槽位 + 被拖轮廓),贴图与整幅重绘共用。 * 整幅重绘在 LANE_REORDER 时按 LaneDragOffset 逐格平移重画(被拖网格最后画), 修掉调试模式下不跟随鼠标、无吸附提示、只能盲拖的问题。 五、信号右键新增「移动到…」二级菜单(位于「仅显示该信号」之上) * 条目为「分组 N(组名)」,当前组打勾;无标题行;底部「新建分组」。 * MenuItem::submenuMaxRows + PopupMenu 独立子菜单滚动(滚动条、滚轮、视口命中过滤), IsScrollable 计入子菜单可滚状态。 * 数据层 DataSet::MoveSignalToGroup/MoveSignal 让时间轴随信号搬运 (独占则移动、共享则复制);TabConfig::RemapAfterSignalMove 改写引用而非先删后插。 * Application::MoveSignalCore 统一入口,保证派生计算暂停、引用修正、选中迁移与同步一致。 六、信号列表支持按住拖拽排序(组内重排 / 跨组移动) * 4px 阈值进入拖拽;条目脱离原位跟随鼠标(阴影 + 强调色描边 + 插入指示线)。 * 其它信号行/分组行按插入位置做避让位移动画(RowRect 统一叠加插值位移, 宿主 kScrollTimerId 驱动 AdvanceSignalDragAnimation)。 * 数据层 DataSet::MoveSignal 支持任意位置;TabConfig::RemapAfterSignalReorder 线性重映射。 * 拖拽中禁用组名悬浮;双击改名与 InvalidateRows 均正确终止拖拽态。 验证:删 .o 全量重编译零 error / 零 warning;产物 873,984 B(< 1MB 预警阈值)。 本轮未使用鼠标/键盘/屏幕,未运行任何 GUI 探针,仅编译验证 + 源码级核对。
1041 lines
31 KiB
C++
1041 lines
31 KiB
C++
// 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) 可见性表:目标分组在目标位置插入一列(沿用源分组里的可见性),源分组删除一列。
|
||
const std::size_t toSlot = static_cast<std::size_t>(to.groupIndex);
|
||
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`)⇒ 端点映射会算错,
|
||
// 只能逐点扫描(只在自适应重算时发生,不是每帧路径)。
|
||
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
|
||
|