// 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(groupIndex)); if (group == nullptr) { continue; } // 新增信号默认为未勾选(由用户自行决定查看哪些信号), // 其显示外观已在 DataSet 中初始化完毕。 m_visible[groupIndex].resize(group->signals.size(), false); } // 清理已失效的网格引用(选中状态由 DataSet 统一维护)。 for (Lane& lane : m_lanes) { std::vector& 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(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& 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(to.groupIndex); if (toSlot >= m_visible.size()) { m_visible.resize(toSlot + 1); } if (toSlot < m_visible.size() && static_cast(to.signalIndex) <= m_visible[toSlot].size()) { m_visible[toSlot].insert(m_visible[toSlot].begin() + to.signalIndex, visible); } const std::size_t fromSlot = static_cast(from.groupIndex); if (fromSlot < m_visible.size() && static_cast(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(groupIndex); if (slot < m_visible.size() && static_cast(from) < m_visible[slot].size() && static_cast(to) < m_visible[slot].size()) { std::vector& row = m_visible[slot]; const bool moved = row[static_cast(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(groupIndex); if (slot < m_visible.size()) { std::vector& fileVisible = m_visible[slot]; if (static_cast(signalIndex) < fileVisible.size()) { fileVisible.erase(fileVisible.begin() + signalIndex); } } // 网格归属:删掉该信号本身,并把同一分组内位于其后的引用下标前移一位。 for (Lane& lane : m_lanes) { std::vector& 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::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(ref.groupIndex) >= m_visible.size()) { return false; } const std::vector& fileVisible = m_visible[static_cast(ref.groupIndex)]; if (ref.signalIndex < 0 || static_cast(ref.signalIndex) >= fileVisible.size()) { return false; } return fileVisible[static_cast(ref.signalIndex)]; } void TabConfig::SetVisible(const SignalRef& ref, bool visible) { if (ref.groupIndex < 0 || static_cast(ref.groupIndex) >= m_visible.size()) { return; } std::vector& fileVisible = m_visible[static_cast(ref.groupIndex)]; if (ref.signalIndex < 0 || static_cast(ref.signalIndex) >= fileVisible.size()) { return; } if (fileVisible[static_cast(ref.signalIndex)] == visible) { return; } fileVisible[static_cast(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(groupIndex) >= m_visible.size()) { return; } std::vector& fileVisible = m_visible[static_cast(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& 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& fileVisible : m_visible) { std::fill(fileVisible.begin(), fileVisible.end(), visible); } SyncLanes(); } void TabConfig::SoloSignal(const SignalRef& ref) { for (std::vector& fileVisible : m_visible) { std::fill(fileVisible.begin(), fileVisible.end(), false); } SetVisible(ref, true); } bool TabConfig::GroupHasVisible(int groupIndex) const { if (groupIndex < 0 || static_cast(groupIndex) >= m_visible.size()) { return false; } const std::vector& fileVisible = m_visible[static_cast(groupIndex)]; return std::find(fileVisible.begin(), fileVisible.end(), true) != fileVisible.end(); } bool TabConfig::GroupAllVisible(int groupIndex) const { if (groupIndex < 0 || static_cast(groupIndex) >= m_visible.size()) { return false; } const std::vector& fileVisible = m_visible[static_cast(groupIndex)]; if (fileVisible.empty()) { return false; } return std::find(fileVisible.begin(), fileVisible.end(), false) == fileVisible.end(); } void TabConfig::SetGroupVisibleSubset(int groupIndex, const std::vector& signalMask) { if (groupIndex < 0 || static_cast(groupIndex) >= m_visible.size()) { return; } std::vector& fileVisible = m_visible[static_cast(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& signalMask) const { if (groupIndex < 0 || static_cast(groupIndex) >= m_visible.size()) { return false; } const std::vector& fileVisible = m_visible[static_cast(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& signalMask) const { if (groupIndex < 0 || static_cast(groupIndex) >= m_visible.size()) { return false; } const std::vector& fileVisible = m_visible[static_cast(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& 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& fileVisible : m_visible) { count += static_cast(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& refs = m_lanes[lane].signals; if (std::find(refs.begin(), refs.end(), ref) != refs.end()) { return static_cast(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(m_lanes.size())); if (hasTarget) { const std::vector& targetSignals = m_lanes[static_cast(laneIndex)].signals; for (const SignalRef& candidate : targetSignals) { if (!(candidate == ref)) { targetAnchor = candidate; break; } } } // 手动指定归属意味着切换到自定义划分策略。 m_laneMode = LaneMode::CUSTOM; for (Lane& lane : m_lanes) { std::vector& 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& refs = m_lanes[index].signals; if (std::find(refs.begin(), refs.end(), targetAnchor) != refs.end()) { destination = static_cast(index); break; } } } if (destination >= 0) { m_lanes[static_cast(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(m_lanes.size())) { return -1; } const std::vector signals = m_lanes[static_cast(laneIndex)].signals; if (signals.size() <= 1) { // 只有一条信号时无需拆分(本就独占一个网格)。 return -1; } // 手动排布 → 切换到自定义划分策略。 m_laneMode = LaneMode::CUSTOM; // 用原网格的“网格自身状态”(Y 轴自适应/边沿计数等)作为每一份的初始状态, // 这样拆分出来的网格保持原网格的显示设置。 const Lane source = m_lanes[static_cast(laneIndex)]; std::vector 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(source.freeMarkerSelected) < source.freeMarkerItems.size() && &source.freeMarkerItems[static_cast(source.freeMarkerSelected)] == &item) { lane.freeMarkerSelected = static_cast(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(groupIndex); if (slot < m_visible.size() && static_cast(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(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(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(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(groupIndex) >= m_collapsed.size()) { return false; } return m_collapsed[static_cast(groupIndex)]; } void TabConfig::ToggleCollapsed(int groupIndex) { if (groupIndex < 0) { return; } if (static_cast(groupIndex) >= m_collapsed.size()) { m_collapsed.resize(static_cast(groupIndex) + 1, m_groupsCollapsedByDefault); } m_collapsed[static_cast(groupIndex)] = !m_collapsed[static_cast(groupIndex)]; } void TabConfig::ComputeLaneRange(const DataSet& dataSet, int laneIndex, double& yMin, double& yMax) const { if (laneIndex < 0 || laneIndex >= static_cast(m_lanes.size())) { yMin = 0.0; yMax = 1.0; return; } const Lane& lane = m_lanes[static_cast(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::infinity(); double high = -std::numeric_limits::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::infinity(); double formulaHigh = -std::numeric_limits::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::infinity(); double high = -std::numeric_limits::infinity(); for (std::size_t groupIndex = 0; groupIndex < m_visible.size(); ++groupIndex) { const std::vector& groupVisible = m_visible[groupIndex]; if (std::find(groupVisible.begin(), groupVisible.end(), true) == groupVisible.end()) { continue; } const SignalGroup* group = dataSet.GroupAt(static_cast(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(groupIndex), static_cast(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