#include "DataModel.hpp" #include #include #include #include #include #include #include #include #include #include #include // Sleep(低内存设备的“少量多次”节流) #include "Theme.hpp" #include "Utf8Util.hpp" namespace jdi { namespace { const double kMissing = std::numeric_limits::quiet_NaN(); /**< 缺失值占位。 */ const double kDefaultLineWidth = 1.6; /**< 默认线宽。 */ /** * @brief 为网格范围补充留白,避免波形紧贴边界。 * @param [IN, OUT] yMin 范围下限。 * @param [IN, OUT] yMax 范围上限。 */ void PadRange(double& yMin, double& yMax) { if (!std::isfinite(yMin) || !std::isfinite(yMax)) { yMin = 0.0; yMax = 1.0; return; } const double span = yMax - yMin; if (span <= 0.0) { const double magnitude = std::max(1.0, std::fabs(yMin) * 0.1); yMin -= magnitude; yMax += magnitude; return; } const double pad = span * 0.06; yMin -= pad; yMax += pad; } /** * @brief 依据全部可见信号重新生成网格划分。 * @param [IN] visible 各信号的可见性(按 [文件][信号] 索引)。 * @param [IN] lanes 网格集合(原地更新)。 * @param [IN] mode 网格划分策略。 */ void RebuildLanes(const std::vector>& visible, std::vector& 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 edgeCenterItems; bool edgeCenterPanelOpen = true; double edgeCenterPanelX = -1.0; double edgeCenterPanelY = -1.0; bool meanFilter = false; std::vector meanFilterItems; bool meanFilterPanelOpen = true; double meanFilterPanelX = -1.0; double meanFilterPanelY = -1.0; bool weakPeak = false; std::vector 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> 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& 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 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(groupIndex); ref.signalIndex = static_cast(signalIndex); visibleRefs.push_back(ref); } } } // 判断某引用对应的信号是否可见(越界视为不可见)。 const auto isVisibleRef = [&visible](const SignalRef& ref) { if (ref.groupIndex < 0 || static_cast(ref.groupIndex) >= visible.size()) { return false; } const std::vector& fileVisible = visible[static_cast(ref.groupIndex)]; if (ref.signalIndex < 0 || static_cast(ref.signalIndex) >= fileVisible.size()) { return false; } return fileVisible[static_cast(ref.signalIndex)]; }; // 某信号当前所属的网格下标(-1 表示尚未分配网格)。 const auto laneIndexOf = [&lanes](const SignalRef& ref) { for (std::size_t index = 0; index < lanes.size(); ++index) { const std::vector& refs = lanes[index].signals; if (std::find(refs.begin(), refs.end(), ref) != refs.end()) { return static_cast(index); } } return -1; }; if (mode == LaneMode::PER_SIGNAL) { // 每条可见信号独占一个网格:网格数量严格跟随可见信号数量。 // **顺序按"原有网格位置"优先**:已经分配过网格的信号保持其相对次序, // 新勾选(此前没有网格)的信号统一追加到最下方。 // 若直接按 visibleRefs(文件/列序)重建,新勾选的信号会按其列序 // 插到中间,位置看起来"不确定"。 std::vector 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& 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); } } } const wchar_t* DrawModeName(DrawMode mode) { switch (mode) { case DrawMode::LINE: return L"线段"; case DrawMode::SCATTER: return L"散点"; case DrawMode::LINE_SCATTER: return L"点线"; default: return L"线段"; } } // ============================ SignalGroup 构建 ============================ /** * @brief 粗粒度包络层每个分组覆盖的采样数。 * * 16KB 的双精度分组可覆盖 1024 个采样(内存开销 = 采样数的 1.6%), * 但会让"每列采样数略多于分组跨度"的场景退化为逐点扫描。 * 取 256 时:额外内存约为采样数的 0.8%,最坏情况的扫描量也能压低到 * 每列 ≤256 个采样(2400 列 ≈ 0.6M 次读取,约 1ms)。 */ const std::size_t kCoarseStride = 256; /** * @brief 单条信号的取值范围统计结果。 */ struct SignalStats { double minValue = 0.0; /**< 有限采样值最小值。 */ double maxValue = 0.0; /**< 有限采样值最大值。 */ std::size_t finiteCount = 0; /**< 有限采样点数量。 */ std::size_t firstFiniteIndex = 0; /**< 第一个有限采样点的下标(无有效值时无意义)。 */ }; /** * @brief 按有效采样点数量设置「稀疏信号」的**初始**绘制样式。 * * 有效采样点极少时线段模式几乎什么也看不见(只有一个点时更是完全画不出东西), * 因此这类信号初始就用「点线」并适当加粗: * * **只有一个有效采样点** → 点线 + `kSinglePointSignalLineWidth`(最大线宽):那个点本身 * 就是一个圆点,半径与线宽成正比,不取最大值根本看不清; * * **有效采样点 2 … `kSparseSignalSampleLimit`** → 点线 + `kSparseSignalLineWidth`(2px)。 * * 其余信号保持既有默认(线段 + 1px)。只在**新建信号**时调用(用户改过样式后不再覆盖)。 * * @param [IN] finiteCount 有效采样点数量。 * @param [IN,OUT] style 待初始化的显示外观。 */ void ApplySparseDefaultStyle(std::size_t finiteCount, SignalStyle& style) { if (finiteCount == 1) { style.mode = DrawMode::LINE_SCATTER; style.lineWidth = kSinglePointSignalLineWidth; } else if (finiteCount <= kSparseSignalSampleLimit) { style.mode = DrawMode::LINE_SCATTER; style.lineWidth = kSparseSignalLineWidth; } } /** * @brief 统计一列采样的最小/最大值与有效点数量,并顺带构建粗粒度包络层。 * * 两者在同一个循环内完成,避免为粗粒度层多扫一遍数据(大文件下那是一整趟内存遍历)。 * * @param [IN] values 采样列。 * @param [OUT] coarseMin 粗粒度分组最小值输出。 * @param [OUT] coarseMax 粗粒度分组最大值输出。 * @return 统计结果。 */ SignalStats ComputeStats(const std::vector& values, std::vector& coarseMin, std::vector& coarseMax) { SignalStats stats; double low = std::numeric_limits::infinity(); double high = -std::numeric_limits::infinity(); const std::size_t count = values.size(); const std::size_t buckets = (count + kCoarseStride - 1) / kCoarseStride; coarseMin.assign(buckets, std::numeric_limits::infinity()); coarseMax.assign(buckets, -std::numeric_limits::infinity()); std::size_t bucket = 0; std::size_t bucketEnd = kCoarseStride; for (std::size_t index = 0; index < count; ++index) { if (index >= bucketEnd) { ++bucket; bucketEnd += kCoarseStride; } const double value = values[index]; if (std::isfinite(value)) { if (stats.finiteCount == 0) { stats.firstFiniteIndex = index; // 供「单点信号」缓存其时刻 } ++stats.finiteCount; low = std::min(low, value); high = std::max(high, value); if (value < coarseMin[bucket]) { coarseMin[bucket] = value; } if (value > coarseMax[bucket]) { coarseMax[bucket] = value; } } } if (stats.finiteCount == 0) { stats.minValue = 0.0; stats.maxValue = 0.0; } else { stats.minValue = low; stats.maxValue = high; } return stats; } bool BuildSignalGroup(LoadedSource&& source, std::size_t paletteBase, SignalGroup& group, std::wstring& error, const LoadProgress& progress) { error.clear(); group = SignalGroup(); if (source.blocks.empty() || !source.HasData()) { error = L"数据源为空。"; return false; } // 分组默认名 = 源文件名(含扩展名),用户可重命名。 group.name = source.sourceName; int paletteCount = 0; const std::uint32_t* palette = SignalPalette(paletteCount); // 时间轴去重表:内容完全相同(长度与全部数值逐位相同)的时间轴只保留一条。 // 这样 CSV 里多个同刻度的 `time` 列、MF4 里多个同基线的通道组不会重复占内存。 std::vector axisHashToIndex; std::vector axisHashValues; const auto timeHash = [](const std::vector& time) -> std::uint64_t { std::uint64_t hash = 1469598103934665603ull; for (const double value : time) { std::uint64_t bits = 0; std::memcpy(&bits, &value, sizeof(bits)); hash ^= bits; hash *= 1099511628211ull; } hash ^= static_cast(time.size()); hash *= 1099511628211ull; return hash; }; const auto sameTime = [](const std::vector& a, const std::vector& b) -> bool { if (a.size() != b.size()) { return false; } for (std::size_t i = 0; i < a.size(); ++i) { const double x = a[i]; const double y = b[i]; if (x != y) { // NaN 视为“相同”(缺失位置一致),其余必须完全相等。 if (!(std::isnan(x) && std::isnan(y))) { return false; } } } return true; }; std::size_t blockIndex = 0; for (TableBlock& block : source.blocks) { ++blockIndex; DataTable& table = block.table; if (!table.HasData()) { continue; } const std::size_t columnCount = table.headers.size(); if (table.columns.size() < columnCount || columnCount < 2) { continue; // 只有时间列:没有可绘制信号 } // 时间列固定为块的第 0 列(加载器已保证)。 std::vector& timeColumn = table.columns[0]; // 统计时间轴的取值范围与单调性。 TimeAxis axis; axis.name = table.headers[0]; { bool monotonic = true; bool hasPrevious = false; double previousTime = 0.0; double low = std::numeric_limits::infinity(); double high = -std::numeric_limits::infinity(); std::size_t finiteCount = 0; for (const double value : timeColumn) { if (!std::isfinite(value)) { continue; } ++finiteCount; low = std::min(low, value); high = std::max(high, value); if (hasPrevious && value < previousTime) { monotonic = false; } previousTime = value; hasPrevious = true; } if (finiteCount < 1) { continue; // 该块的时间轴完全无效 } axis.monotonic = monotonic; axis.finiteCount = finiteCount; axis.minTime = low; axis.maxTime = high; } axis.time = std::move(timeColumn); // 去重:命中已有时间轴则复用它,否则新增一条。 std::size_t axisIndex = group.axes.size(); { const std::uint64_t hash = timeHash(axis.time); for (std::size_t candidate = 0; candidate < axisHashValues.size(); ++candidate) { if (axisHashValues[candidate] == hash && sameTime(group.axes[axisHashToIndex[candidate]].time, axis.time)) { axisIndex = axisHashToIndex[candidate]; break; } } if (axisIndex == group.axes.size()) { group.axes.push_back(std::move(axis)); axisHashValues.push_back(hash); axisHashToIndex.push_back(axisIndex); } } // 构建信号列:每列整块移动,随后就地统计取值范围。 group.signals.reserve(group.signals.size() + columnCount - 1); for (std::size_t column = 1; column < columnCount; ++column) { SignalData signal; signal.name = Trim(table.headers[column]); if (signal.name.empty()) { signal.name = L"Column" + std::to_wstring(column + 1); } signal.axisIndex = axisIndex; signal.values = std::move(table.columns[column]); const SignalStats stats = ComputeStats(signal.values, signal.coarseMin, signal.coarseMax); signal.coarseStride = kCoarseStride; signal.minValue = stats.minValue; signal.maxValue = stats.maxValue; signal.finiteCount = stats.finiteCount; // 只含一个有效采样点时缓存它的时刻(光标吸附用)。 if (stats.finiteCount == 1) { const TimeAxis& ownAxis = group.axes[std::min(axisIndex, group.axes.size() - 1)]; if (stats.firstFiniteIndex < ownAxis.time.size()) { signal.singlePointTime = ownAxis.time[stats.firstFiniteIndex]; } } // 显示外观随信号一起初始化:调色板按「全局信号序号」取色, // 使同一信号在任意标签页中都保持同一默认颜色。 { const std::size_t globalIndex = paletteBase + group.signals.size(); if (palette != nullptr && paletteCount > 0) { const std::size_t count = static_cast(paletteCount); const std::size_t slot = globalIndex % count; signal.style.color = palette[slot]; // 自定义色初值取调色板中「向后递进」的下一个颜色。 // 若初值等于本信号自身的预设色,样式区里点击该预设色块会被判定成 // “当前用的是自定义色”,高亮会直接跳到自定义色块上(此前的缺陷)。 signal.style.customColor = palette[(slot + 1) % count]; } else { signal.style.color = 0xFF268BD2u; signal.style.customColor = 0xFF859900u; } signal.style.opacity = 1.0f; signal.style.lineWidth = 1.0f; signal.style.mode = DrawMode::LINE; // 采样点极少的信号(含只有一个点)改用「点线」并加粗,否则几乎不可见。 ApplySparseDefaultStyle(stats.finiteCount, signal.style); } group.signals.push_back(std::move(signal)); } if (progress) { LoadProgressInfo info; const double ratio = static_cast(blockIndex) / static_cast(std::max(1, source.blocks.size())); info.ratio = 0.75 + 0.25 * ratio; info.hasBytes = false; if (!progress(info)) { error = L"已取消加载。"; return false; } } } if (group.signals.empty() || group.axes.empty()) { error = L"除时间轴外未发现任何数据列。"; return false; } return true; } // ============================ DataSet ============================ bool DataSet::AddGroup(SignalGroup&& group, int& groupIndex) { groupIndex = -1; if (group.signals.empty() || group.axes.empty()) { return false; } m_groups.push_back(std::move(group)); groupIndex = static_cast(m_groups.size()) - 1; return true; } bool DataSet::InsertDerivedSignal(int groupIndex, int afterSignalIndex, SignalData&& signal, int& newSignalIndex) { newSignalIndex = -1; if (groupIndex < 0 || groupIndex >= static_cast(m_groups.size())) { return false; } SignalGroup& group = m_groups[static_cast(groupIndex)]; if (afterSignalIndex < -1 || afterSignalIndex >= static_cast(group.signals.size())) { return false; } // 1. 采样值统计与粗粒度包络层:与正常加载路径使用同一套统计逻辑, // 否则派生信号在大文件下会退化成逐点扫描(包络构建变慢)。 const SignalStats stats = ComputeStats(signal.values, signal.coarseMin, signal.coarseMax); signal.coarseStride = kCoarseStride; signal.minValue = stats.minValue; signal.maxValue = stats.maxValue; signal.finiteCount = stats.finiteCount; // 只含一个有效采样点时缓存它的时刻(光标吸附用);派生信号沿用源信号的时间轴。 if (stats.finiteCount == 1 && !group.axes.empty()) { const TimeAxis& ownAxis = group.AxisAt(signal.axisIndex); if (stats.firstFiniteIndex < ownAxis.time.size()) { signal.singlePointTime = ownAxis.time[stats.firstFiniteIndex]; } } // 2. 显示外观按「全局信号序号」取调色板颜色(与普通新信号完全一致的规则)。 const int insertPos = afterSignalIndex + 1; { // 新信号的全局序号 = 本组之前的信号总数 + 它在组内的下标。 std::size_t globalIndex = static_cast(insertPos); for (int index = 0; index < groupIndex; ++index) { globalIndex += m_groups[static_cast(index)].signals.size(); } int paletteCount = 0; const std::uint32_t* palette = SignalPalette(paletteCount); if (palette != nullptr && paletteCount > 0) { const std::size_t count = static_cast(paletteCount); const std::size_t slot = globalIndex % count; signal.style.color = palette[slot]; signal.style.customColor = palette[(slot + 1) % count]; } // 采样点极少的派生信号同样改用「点线」并加粗(与普通加载路径同一规则)。 ApplySparseDefaultStyle(stats.finiteCount, signal.style); } group.signals.insert(group.signals.begin() + insertPos, std::move(signal)); // 3. 全局选中引用:同一组内位于插入点之后的信号下标整体后移。 if (m_selected.groupIndex == groupIndex && m_selected.signalIndex >= insertPos) { ++m_selected.signalIndex; } newSignalIndex = insertPos; return true; } bool DataSet::RemoveGroup(int groupIndex) { if (groupIndex < 0 || groupIndex >= static_cast(m_groups.size())) { return false; } // 用移动到末位后弹出的方式移除,使被移除文件的时间轴与全部信号缓冲区在本调用内 // 立即析构归还操作系统,不再等待后续插入覆盖;最后收缩容量,避免残留空闲块。 const std::size_t position = static_cast(groupIndex); for (std::size_t index = position; index + 1 < m_groups.size(); ++index) { m_groups[index] = std::move(m_groups[index + 1]); } m_groups.pop_back(); m_groups.shrink_to_fit(); return true; } bool DataSet::RemoveSignal(int groupIndex, int signalIndex) { if (groupIndex < 0 || groupIndex >= static_cast(m_groups.size())) { return false; } SignalGroup& group = m_groups[static_cast(groupIndex)]; if (signalIndex < 0 || signalIndex >= static_cast(group.signals.size())) { return false; } // 与 RemoveGroup 同一思路:先把待移除项移到末位(move 赋值时其原有缓冲当场析构), // 再 pop_back + shrink_to_fit,使该信号的全部采样内存在本调用内立即归还给系统, // 而不是留在容量里等后续插入覆盖。 std::vector& signals = group.signals; const std::size_t position = static_cast(signalIndex); for (std::size_t index = position; index + 1 < signals.size(); ++index) { signals[index] = std::move(signals[index + 1]); } signals.pop_back(); signals.shrink_to_fit(); return true; } void DataSet::RemapSelectionAfterSignalRemoval(int groupIndex, int signalIndex) { if (groupIndex < 0 || signalIndex < 0 || m_selected.groupIndex != groupIndex) { return; } if (m_selected.signalIndex == signalIndex) { // 选中的信号本身被移除:取消选中。 m_selected = SignalRef(); } else if (m_selected.signalIndex > signalIndex) { // 其后信号整体前移一位。 --m_selected.signalIndex; } } std::size_t DataSet::SignalCount() const { std::size_t count = 0; for (const SignalGroup& group : m_groups) { count += group.signals.size(); } return count; } const SignalGroup* DataSet::GroupAt(int groupIndex) const { if (groupIndex < 0 || groupIndex >= static_cast(m_groups.size())) { return nullptr; } return &m_groups[static_cast(groupIndex)]; } bool DataSet::SetSignalDisplayName(int groupIndex, int signalIndex, const std::wstring& name) { if (groupIndex < 0 || groupIndex >= static_cast(m_groups.size())) { return false; } SignalGroup& group = m_groups[static_cast(groupIndex)]; if (signalIndex < 0 || signalIndex >= static_cast(group.signals.size())) { return false; } // 统一去除首尾空白;结果为空则视为恢复源名称。 std::wstring trimmed = Trim(name); SignalData& signal = group.signals[static_cast(signalIndex)]; if (trimmed == signal.displayName) { return false; } signal.displayName = std::move(trimmed); return true; } bool DataSet::SetGroupDisplayName(int groupIndex, const std::wstring& name) { if (groupIndex < 0 || groupIndex >= static_cast(m_groups.size())) { return false; } std::wstring trimmed = Trim(name); SignalGroup& group = m_groups[static_cast(groupIndex)]; if (trimmed == group.displayName) { return false; } group.displayName = std::move(trimmed); return true; } const SignalData* DataSet::Resolve(const SignalRef& ref) const { const SignalGroup* group = GroupAt(ref.groupIndex); if (group == nullptr) { return nullptr; } if (ref.signalIndex < 0 || ref.signalIndex >= static_cast(group->signals.size())) { return nullptr; } return &group->signals[static_cast(ref.signalIndex)]; } const TimeAxis* DataSet::Axis(const SignalRef& ref) const { const SignalGroup* group = GroupAt(ref.groupIndex); if (group == nullptr) { return nullptr; } if (ref.signalIndex < 0 || ref.signalIndex >= static_cast(group->signals.size()) || group->axes.empty()) { return nullptr; } return &group->AxisOf(group->signals[static_cast(ref.signalIndex)]); } const SignalStyle* DataSet::Style(const SignalRef& ref) const { const SignalData* signal = Resolve(ref); return (signal != nullptr) ? &signal->style : nullptr; } SignalStyle* DataSet::Style(const SignalRef& ref) { const SignalGroup* group = GroupAt(ref.groupIndex); if (group == nullptr) { return nullptr; } if (ref.signalIndex < 0 || ref.signalIndex >= static_cast(group->signals.size())) { return nullptr; } // 这里需要写入,因此借助非 const 路径重新定位信号。 SignalGroup& mutableGroup = m_groups[static_cast(ref.groupIndex)]; return &mutableGroup.signals[static_cast(ref.signalIndex)].style; } double DataSet::MinTime() const { double value = std::numeric_limits::infinity(); for (const SignalGroup& group : m_groups) { double low = 0.0; double high = 0.0; if (group.TimeRange(low, high)) { value = std::min(value, low); } } return std::isfinite(value) ? value : 0.0; } double DataSet::MaxTime() const { double value = -std::numeric_limits::infinity(); for (const SignalGroup& group : m_groups) { double low = 0.0; double high = 0.0; if (group.TimeRange(low, high)) { value = std::max(value, high); } } return std::isfinite(value) ? value : 1.0; } void DataSet::RemapSelectionAfterGroupRemoval(int groupIndex) { if (groupIndex < 0) { return; } if (m_selected.groupIndex == groupIndex) { m_selected = SignalRef(); } else if (m_selected.groupIndex > groupIndex) { --m_selected.groupIndex; } } bool DataSet::ValidateSelection() { if (!m_selected.IsValid()) { return false; } if (Resolve(m_selected) != nullptr) { return false; } m_selected = SignalRef(); return true; } // ============================ 采样辅助 ============================ namespace { /** * @brief 滑动平均的共用实现(输入只要可转成 double)。 * * 窗口 = [max(0,i-N), min(count-1,i+N)],每前进一步只增减边界处的两个采样点, * 整体 O(n),不额外占用前缀和内存。 */ template void MovingAverageImpl(const std::vector& values, int radius, std::vector& out); } // namespace void ComputeMovingAverage(const std::vector& values, int radius, std::vector& out) { MovingAverageImpl(values, radius, out); } namespace { template void MovingAverageImpl(const std::vector& values, int radius, std::vector& out) { const int window = std::max(0, radius); const std::size_t count = values.size(); out.assign(count, std::numeric_limits::quiet_NaN()); if (count == 0 || window == 0) { // 半径 0 表示不平滑:原样复制(中心点无效处仍为 NaN)。 for (std::size_t index = 0; index < count; ++index) { const double value = static_cast(values[index]); if (std::isfinite(value)) { out[index] = static_cast(value); } } return; } // 滑动窗口求和:每前进一步只增减边界处的两个采样点,整体 O(n)。 double sum = 0.0; std::size_t finiteCount = 0; std::size_t windowLow = 0; std::size_t windowHigh = 0; // 闭区间右端。 const auto addValue = [&sum, &finiteCount](double value) { if (std::isfinite(value)) { sum += value; ++finiteCount; } }; const auto removeValue = [&sum, &finiteCount](double value) { if (std::isfinite(value)) { sum -= value; --finiteCount; } }; for (std::size_t index = 0; index < count; ++index) { const std::size_t low = (index > static_cast(window)) ? (index - static_cast(window)) : 0; const std::size_t high = std::min(count - 1, index + static_cast(window)); if (index == 0) { windowLow = low; windowHigh = high; for (std::size_t i = windowLow; i <= windowHigh; ++i) { addValue(static_cast(values[i])); } } else { while (windowLow < low) { removeValue(static_cast(values[windowLow])); ++windowLow; } while (windowHigh < high) { ++windowHigh; addValue(static_cast(values[windowHigh])); } } // 中心点本身无效(NaN)时该处不产生有效结果。 const double center = static_cast(values[index]); if (finiteCount > 0 && std::isfinite(center)) { out[index] = static_cast(sum / static_cast(finiteCount)); } } } } // namespace namespace { /** * @brief `thread_local` 复用缓冲的**保留容量上限**(元素个数,约 4M)。 * * 这些缓冲用 `thread_local` 复用,是为了省掉"每次调用都分配 + 零初始化上千万元素" * 的开销(多阶弱峰调整实测 991ms → 577ms)。但**保留容量必须封顶**: * 后台派生曲线线程池有 4 条线程,每条各自持有一份 `thread_local` 缓冲; * 一条"整段信号"(VERY_BIG_DATA ≈ 2580 万采样)的弱峰调整需要 * `current` / `next`(float,各 ≈103MB)、极值表与位移表(≈数百 MB), * 于是**每条线程常驻数百 MB、4 条线程就是 2GB 以上**,而 `thread_local` * 只在**线程退出**时才析构 → 用户报告"启用预览后内存 1.42GB→4.21GB, * 关掉预览也不下降,直到关闭整个文件分组才释放"。 * * 因此只对"中/小规模"(≤ 上限)保留容量;**超大计算**改为一次性分配、用完即释放 —— * 这类整段计算本身是秒级的一次性操作(结果会被曲线缓存复用),多一次分配完全可接受, * 换来的是内存不再随线程数放大。 */ const std::size_t kReuseBufferMaxElements = 4u * 1024u * 1024u; /** @brief 容量超过上限就释放缓冲内存(`std::vector` 的容量不会随 `clear()` 归还)。 */ template void TrimReuseBuffer(std::vector& buffer) { if (buffer.capacity() > kReuseBufferMaxElements) { std::vector empty; buffer.swap(empty); } } /** * @brief 作用域守卫:离开作用域时对 `thread_local` 复用缓冲做一次容量裁剪。 * * 用它(而不是在每个 `return` 前手写一遍)可以保证**所有**退出路径都会裁剪, * 不会因为新增提前返回而漏掉、导致内存再次失控。 */ template class ReuseBufferScope { public: explicit ReuseBufferScope(std::vector& buffer) : m_buffer(buffer) {} ~ReuseBufferScope() { TrimReuseBuffer(m_buffer); } ReuseBufferScope(const ReuseBufferScope&) = delete; ReuseBufferScope& operator=(const ReuseBufferScope&) = delete; private: std::vector& m_buffer; }; } // namespace void ComputeMovingAverageIterated(const std::vector& values, int radius, int iterations, std::vector& out, const std::atomic* cancel, bool throttle) { const int stages = std::max(1, iterations); // 第 1 阶:从原始采样值出发。 ComputeMovingAverage(values, radius, out); if (stages == 1) { return; } // 后续各阶以上一阶的结果为输入(double 中转,避免 float→double 的精度损失累积)。 // 中转缓冲复用容量(线程本地),避免每阶重新分配上千万元素的向量。 // ⚠️ 用 `ReuseBufferScope` 保证离开作用域时对超大容量做裁剪(见其说明)。 thread_local std::vector bufferBuffer; ReuseBufferScope reuseScope(bufferBuffer); std::vector& buffer = bufferBuffer; buffer.assign(values.size(), std::numeric_limits::quiet_NaN()); std::vector stage; for (int iteration = 1; iteration < stages; ++iteration) { if (cancel != nullptr && cancel->load(std::memory_order_relaxed)) { return; // 过时任务:立即退出(调用方会丢弃结果) } for (std::size_t index = 0; index < out.size(); ++index) { buffer[index] = std::isfinite(out[index]) ? static_cast(out[index]) : std::numeric_limits::quiet_NaN(); } ComputeMovingAverage(buffer, radius, stage); out.swap(stage); if (throttle) { // 低内存设备的“少量多次”:每阶之间小睡一下,把内存带宽与 CPU 让给前台。 Sleep(1); } } } namespace { /** * @brief 「弱峰调整」内部使用的极值点(波峰 / 波谷)。 * * ⚠️ **字段顺序是刻意的**:`double value` 放在最前、`index` 用 `uint32`、`type` 用 `int8`, * 使结构体只占 **16 字节**(原来的 `size_t+double+int` 是 24 字节,含 7 字节填充)。 * 一个千万级方波信号的极值数量与采样数同量级(编码器信号几乎每个跳变都是极值), * 这张表就是「弱峰调整」工作缓冲的主导项(24 B/极值 ≈ 24 B/采样)—— * 瘦身后同样规模的整段计算可省下约 1/3 的峰值内存。 * `uint32` 下标对本项目安全:4.29e9 个 `double` 需要 34 GB 采样内存,不可能达到。 */ struct PeakExtremum { double value = 0.0; /**< 采样值(保持 double:改成 float 会让结果与历史版本不一致)。 */ std::uint32_t index = 0; /**< 采样下标。 */ std::int8_t type = 0; /**< +1 = 波峰,-1 = 波谷。 */ }; /** * @brief 三组识别规则各自的调整幅度(每一组都在 -2 – +2,即面板上的 -200% – +200%)。 * * 滑动条的 ±400% 会按"梯度"同时驱动三组规则(见 `ComputeWeakPeakAdjust`): * 越宽松的规则参与得越晚、在同一滑动条位置上的幅度越小。 */ struct WeakPeakAmounts { double first = 0.0; /**< 第一类(严格:两侧均值都要满足)。 */ double second = 0.0; /**< 第二类(其中一侧满足即可)。 */ double third = 0.0; /**< 第三类(更宽松:只与左右同类极值比较)。 */ }; /** * @brief 「弱峰调整」的单阶处理:把偏弱的波峰上拉 / 偏弱的波谷下拉, * 或(负幅度)把弱峰本身削弱、压平。 * * **不做任何前置滤波**(需要平滑时用户可以先用「均值滤波」构建一条滤波后的信号): * 极值点直接在输入采样上通过单调段的方向翻转识别。三组识别规则(见 `WeakPeakAmounts`) * 各自有自己的幅度;某个极值若同时满足多组规则,取**幅度绝对值最大**的那一组 *(= 最严格的那一组),这样"越严格越先被推到目标值"。 * * @param [IN] input 本阶的输入采样值。 * @param [IN] amounts 三组规则的调整幅度(各 -2 – +2,已夹取;0 = 该组不动)。 * @param [OUT] output 本阶的输出(长度与 input 相同;原值为 NaN 处保持 NaN)。 * @param [OUT] extremes 复用的极值表(容量保留,避免每阶重新分配)。 * @param [IN] cancel 可选的取消标志(为 true 时**立即返回 false**)。 * @return 确实有采样点被改动返回 true;没有任何点需要调整时返回 false。 */ bool ApplyWeakPeakStage(const std::vector& input, const WeakPeakAmounts& amounts, std::vector& output, std::vector& extremes, const std::atomic* cancel) { const std::size_t count = input.size(); output.assign(count, std::numeric_limits::quiet_NaN()); if (count == 0) { return false; } // 先把有效采样原样拷入输出,之后按“极值点之间的平滑偏移”逐点叠加。 for (std::size_t index = 0; index < count; ++index) { const float value = input[index]; if (std::isfinite(value)) { output[index] = value; } } // 1) 识别波峰 / 波谷:扫描单调段,方向翻转处的上一个采样点即为极值点。 // 波峰(先升后降)与波谷(先降后升)必然交替出现, // 因此同类极值就是极值序列里下标 ±2 的两个。 extremes.clear(); // 适度预留:编码器方波约每 8 个采样一个极值,预留 1/8 能让最常见的密集信号 // 一次到位(避免 push_back 反复扩容带来的 1.5x 峰值内存);宁少勿多, // 因为 unreserve 的代价(一次 realloc)远小于无谓地占住内存。 if (extremes.capacity() < count / 8) { try { extremes.reserve(count / 8); } catch (const std::bad_alloc&) { // 预留失败不影响正确性:让 push_back 自行增长(低内存时不报错)。 } } { int direction = 0; // 当前单调段方向(+1 上升、-1 下降)。 bool hasPrevious = false; std::size_t previousIndex = 0; double previousValue = 0.0; for (std::size_t index = 0; index < count; ++index) { // 取消检查的粒度:每 1M 个采样一次(单次检查开销可忽略, // 但能让“过时任务”在几十毫秒内退出)。 if (cancel != nullptr && (index & 0xFFFFFu) == 0 && cancel->load(std::memory_order_relaxed)) { return false; } const double value = static_cast(input[index]); if (!std::isfinite(value)) { // 无效采样作为断点:不跨越它判定极值。 hasPrevious = false; continue; } if (!hasPrevious) { hasPrevious = true; previousIndex = index; previousValue = value; continue; } const int step = (value > previousValue) ? 1 : ((value < previousValue) ? -1 : 0); if (step != 0) { if (direction == 0) { direction = step; } else if (step != direction) { // 方向在此翻转:上一个采样点就是极值(原方向向上则为波峰)。 PeakExtremum extreme; extreme.index = static_cast(previousIndex); extreme.value = previousValue; extreme.type = static_cast(direction); extremes.push_back(extreme); direction = step; } } previousIndex = index; previousValue = value; } } // 识别不到足够多的极值(常量 / 单调 / 采样点太少):本阶无需任何改动。 if (extremes.size() < 5) { return false; } // 2) 逐个极值点判定它属于哪几类规则点,并算出它自己的目标偏移量。 const std::size_t extremeCount = extremes.size(); thread_local std::vector shiftBuffer; ReuseBufferScope reuseScope(shiftBuffer); std::vector& shift = shiftBuffer; shift.assign(extremeCount, 0.0); for (std::size_t k = 2; k + 2 < extremeCount; ++k) { const PeakExtremum& current = extremes[k]; const PeakExtremum& left = extremes[k - 2]; const PeakExtremum& right = extremes[k + 2]; if (left.type != current.type || right.type != current.type) { continue; } if (!std::isfinite(current.value) || !std::isfinite(left.value) || !std::isfinite(right.value)) { continue; } // 相邻的**异类**极值(波峰旁边的波谷 / 波谷旁边的波峰)。 const PeakExtremum& nearLeft = extremes[k - 1]; const PeakExtremum& nearRight = extremes[k + 1]; if (nearLeft.type == current.type || nearRight.type == current.type) { continue; // 极值序列本应交替;异常时不动这一点 } if (!std::isfinite(nearLeft.value) || !std::isfinite(nearRight.value)) { continue; } const bool isPeak = (current.type > 0); // 「左侧/右侧相邻的一个同类极值 + 一个异类极值」的均值。 const double leftMean = (left.value + nearLeft.value) * 0.5; const double rightMean = (nearRight.value + right.value) * 0.5; // 同类极值里"较温和"的一侧:波峰取较低者、波谷取较高者。 const double milderSame = isPeak ? std::min(left.value, right.value) : std::max(left.value, right.value); const double meanSame = (left.value + right.value) * 0.5; // 异类极值里"较近"的一个:波峰取较高的波谷、波谷取较低的波峰。 const double nearerOpposite = isPeak ? std::max(nearLeft.value, nearRight.value) : std::min(nearLeft.value, nearRight.value); const double meanOpposite = (nearLeft.value + nearRight.value) * 0.5; // 三组识别规则(以弱峰为例,弱谷把"低于"换成"高于"、max/min 互换): // 第一类:**同时**低于左右两侧的(相邻同类极值 + 相邻异类极值)均值; // 第二类:低于其中**任意一侧**的该均值(从"且"放宽为"或"); // 第三类:只与左右**同类**极值比较 —— 低于左右两个同类极值即算弱峰。 const bool firstClass = isPeak ? ((current.value < leftMean) && (current.value < rightMean)) : ((current.value > leftMean) && (current.value > rightMean)); const bool secondClass = isPeak ? ((current.value < leftMean) || (current.value < rightMean)) : ((current.value > leftMean) || (current.value > rightMean)); const bool thirdClass = isPeak ? ((current.value < left.value) && (current.value < right.value)) : ((current.value > left.value) && (current.value > right.value)); // 同时满足多组时取**幅度绝对值最大**的那一组(= 最严格的那一组先被推到目标值)。 double amount = 0.0; if (firstClass) { amount = amounts.first; } if (secondClass && std::fabs(amounts.second) > std::fabs(amount)) { amount = amounts.second; } if (thirdClass && std::fabs(amounts.third) > std::fabs(amount)) { amount = amounts.third; } if (amount == 0.0) { continue; // 不需要调整 } double destination = current.value; if (amount > 0.0) { // ---- 正幅度(增强)---- // 0 – 100%:当前值 → 左右**同类**极值中较温和的一侧(波峰取较低者、波谷取较高者); // 100 – 200%:该侧 → 左右**同类**极值的**均值**。 // ⚠️ 波峰从 `milderSame` 走向 `meanSame`(向上)、波谷同样从 `milderSame` // 走向 `meanSame`(此时 `meanSame` 可能比 `milderSame` **低**), // 即两者的插值方向一致(都是"milderSame → meanSame"),无需分别取符号。 destination = (amount <= 1.0) ? (current.value + amount * (milderSame - current.value)) : (milderSame + (amount - 1.0) * (meanSame - milderSame)); } else { // ---- 负幅度(削弱)---- // 0 – -100%:当前值 → 相邻**异类**极值中较近的一个 // (波峰取较高的波谷、波谷取较低的波峰); // -100 – -200%:该侧 → 相邻异类极值的**均值**。 const double magnitude = std::min(2.0, -amount); destination = (magnitude <= 1.0) ? (current.value + magnitude * (nearerOpposite - current.value)) : (nearerOpposite + (magnitude - 1.0) * (meanOpposite - nearerOpposite)); // ⚠️ 削弱**不得反向增强**:目标被夹在"不越过当前值"的一侧。 if (isPeak) { destination = std::min(destination, current.value); } else { destination = std::max(destination, current.value); } } shift[k] = destination - current.value; } // 3) 把偏移量平滑地摊到极值点之间的数据点上: // 相邻两个极值点之间用平滑步进过渡(两端导数为 0,波形不会出现折角), // 数据首尾则从 0 渐变到最近的极值点偏移。 // 注意:极值序列的首尾各两个元素的偏移量恒为 0(缺少同类的左右邻居), // 因此这里只需要填充 shift 非零的区间。 bool changed = false; const auto applySpan = [&input, &output, count](std::size_t begin, std::size_t end, double beginShift, double endShift) { if (end <= begin) { if (begin < count && std::isfinite(input[begin])) { output[begin] = static_cast(input[begin] + beginShift); } return; } const double span = static_cast(end - begin); for (std::size_t index = begin; index <= end; ++index) { const float value = input[index]; if (!std::isfinite(value)) { continue; } const double t = static_cast(index - begin) / span; const double weight = t * t * (3.0 - 2.0 * t); output[index] = static_cast(value + beginShift * (1.0 - weight) + endShift * weight); } }; for (std::size_t k = 0; k + 1 < extremeCount; ++k) { if (shift[k] == 0.0 && shift[k + 1] == 0.0) { continue; } applySpan(extremes[k].index, extremes[k + 1].index, shift[k], shift[k + 1]); changed = true; } return changed; } } // namespace void ComputeWeakPeakAdjust(const std::vector& values, double amount, int iterations, std::vector& out, const std::atomic* cancel, bool throttle) { const std::size_t count = values.size(); out.assign(count, std::numeric_limits::quiet_NaN()); if (count == 0) { return; } // 第 1 阶:从原始采样值出发(非有限值保持 NaN)。 // 工作缓冲用线程本地变量复用容量:单条曲线动辄上千万采样,若每次调用都重新分配 // 并零初始化,单条「弱峰调整」的耗时会被明显放大(内存分配 + page fault)。 // ⚠️ 用 `ReuseBufferScope` 保证**离开作用域时裁剪超大容量**:否则 4 条后台线程 // 会各自常驻数百 MB(thread_local 只在线程退出时才析构),内存随线程数放大。 thread_local std::vector currentBuffer; thread_local std::vector nextBuffer; thread_local std::vector extremesBuffer; ReuseBufferScope currentScope(currentBuffer); ReuseBufferScope nextScope(nextBuffer); ReuseBufferScope extremesScope(extremesBuffer); std::vector& current = currentBuffer; current.assign(count, std::numeric_limits::quiet_NaN()); for (std::size_t index = 0; index < count; ++index) { const double value = values[index]; if (std::isfinite(value)) { current[index] = static_cast(value); } } // 「迭代次数 N」与「均值滤波」的迭代次数语义一致:把同一处理**重复应用 N 次**, // 每一阶以上一阶的结果为输入。一阶时改定量已严格等于「幅度」对应的目标值; // 多次迭代时相邻的弱峰会互相抬高彼此的基准,从而把较宽的弱峰区逐步恢复(或削弱)。 const int stages = std::max(1, std::min(iterations, 1000)); // 滑动条量程 ±400%(`amount` = 百分比 / 100,即 -4 – +4),按"梯度"同时驱动三组规则: // 第一类:始终参与,幅度 = clamp(|a|, 0, 2); // 第二类:|a| > 100% 起参与,幅度 = clamp(|a| - 1, 0, 2); // 第三类:|a| > 200% 起参与,幅度 = clamp(|a| - 2, 0, 2)。 // 于是滑动条 0–100% 只有第一类在动;100–200% 第一类走完 100–200%、第二类开始走 0–100%; // 200–300% 第一类已到上限、第二类走 100–200%、第三类开始走 0–100%; // 300–400% 前两类都已到上限,只有第三类在走 100–200%。负值完全对称。 const double clamped = std::max(-4.0, std::min(4.0, amount)); const double magnitude = std::fabs(clamped); const double sign = (clamped < 0.0) ? -1.0 : 1.0; WeakPeakAmounts amounts; amounts.first = sign * std::min(magnitude, 2.0); amounts.second = sign * std::max(0.0, std::min(2.0, magnitude - 1.0)); amounts.third = sign * std::max(0.0, std::min(2.0, magnitude - 2.0)); std::vector& next = nextBuffer; for (int stage = 0; stage < stages; ++stage) { if (cancel != nullptr && cancel->load(std::memory_order_relaxed)) { return; // 过时任务:立即退出(`out` 保留为 NaN,调用方会丢弃它) } if (!ApplyWeakPeakStage(current, amounts, next, extremesBuffer, cancel)) { if (cancel != nullptr && cancel->load(std::memory_order_relaxed)) { return; // 被取消(而非“本阶无改动”) } // 这一阶没有任何采样点需要调整:后续迭代次数也不会再有变化,提前结束。 break; } current.swap(next); if (throttle) { // 低内存设备的“少量多次”:每阶之间小睡一下,把内存带宽与 CPU 让给前台。 Sleep(1); } } out.swap(current); } const Lane::MeanFilterItem* Lane::FindMeanFilterItem(const SignalRef& ref) const { for (const MeanFilterItem& item : meanFilterItems) { if (item.ref == ref) { return &item; } } return nullptr; } Lane::MeanFilterItem& Lane::EnsureMeanFilterItem(const SignalRef& ref) { for (MeanFilterItem& item : meanFilterItems) { if (item.ref == ref) { return item; } } MeanFilterItem item; item.ref = ref; meanFilterItems.push_back(item); return meanFilterItems.back(); } void Lane::RemoveMeanFilterItem(const SignalRef& ref) { meanFilterItems.erase( std::remove_if(meanFilterItems.begin(), meanFilterItems.end(), [&ref](const MeanFilterItem& item) { return item.ref == ref; }), meanFilterItems.end()); } const Lane::EdgeCenterItem* Lane::FindEdgeCenterItem(const SignalRef& ref) const { for (const EdgeCenterItem& item : edgeCenterItems) { if (item.ref == ref) { return &item; } } return nullptr; } Lane::EdgeCenterItem& Lane::EnsureEdgeCenterItem(const SignalRef& ref) { for (EdgeCenterItem& item : edgeCenterItems) { if (item.ref == ref) { return item; } } EdgeCenterItem item; item.ref = ref; edgeCenterItems.push_back(item); return edgeCenterItems.back(); } void Lane::RemoveEdgeCenterItem(const SignalRef& ref) { edgeCenterItems.erase( std::remove_if(edgeCenterItems.begin(), edgeCenterItems.end(), [&ref](const EdgeCenterItem& item) { return item.ref == ref; }), edgeCenterItems.end()); } const Lane::WeakPeakItem* Lane::FindWeakPeakItem(const SignalRef& ref) const { for (const WeakPeakItem& item : weakPeakItems) { if (item.ref == ref) { return &item; } } return nullptr; } Lane::WeakPeakItem& Lane::EnsureWeakPeakItem(const SignalRef& ref) { for (WeakPeakItem& item : weakPeakItems) { if (item.ref == ref) { return item; } } WeakPeakItem item; item.ref = ref; weakPeakItems.push_back(item); return weakPeakItems.back(); } void Lane::RemoveWeakPeakItem(const SignalRef& ref) { weakPeakItems.erase( std::remove_if(weakPeakItems.begin(), weakPeakItems.end(), [&ref](const WeakPeakItem& item) { return item.ref == ref; }), weakPeakItems.end()); } int FindNearestSample(const TimeAxis& axis, double time) { const std::size_t count = axis.time.size(); if (count == 0) { return -1; } if (axis.monotonic) { const std::vector::const_iterator lower = std::lower_bound(axis.time.begin(), axis.time.end(), time); if (lower == axis.time.begin()) { return 0; } if (lower == axis.time.end()) { return static_cast(count - 1); } const std::size_t rightIndex = static_cast(lower - axis.time.begin()); const std::size_t leftIndex = rightIndex - 1; const double leftDistance = std::fabs(time - axis.time[leftIndex]); const double rightDistance = std::fabs(axis.time[rightIndex] - time); return (leftDistance <= rightDistance) ? static_cast(leftIndex) : static_cast(rightIndex); } int best = 0; double bestDistance = std::numeric_limits::infinity(); for (std::size_t index = 0; index < count; ++index) { if (!std::isfinite(axis.time[index])) { continue; } const double distance = std::fabs(axis.time[index] - time); if (distance < bestDistance) { bestDistance = distance; best = static_cast(index); } } return best; } bool SampleSignalAt(const TimeAxis& axis, const SignalData& signal, double time, bool interpolate, double& value) { value = kMissing; if (axis.time.empty() || signal.values.empty()) { return false; } if (time < axis.minTime || time > axis.maxTime) { return false; } const std::vector& values = signal.values; if (!axis.monotonic) { const int nearest = FindNearestSample(axis, time); if (nearest < 0 || static_cast(nearest) >= values.size()) { return false; } value = values[static_cast(nearest)]; return std::isfinite(value); } const std::vector::const_iterator lower = std::lower_bound(axis.time.begin(), axis.time.end(), time); const std::size_t rightIndex = static_cast(lower - axis.time.begin()); if (rightIndex == 0) { value = values[0]; return std::isfinite(value); } if (rightIndex >= axis.time.size()) { value = values.back(); return std::isfinite(value); } const std::size_t leftIndex = rightIndex - 1; if (rightIndex >= values.size() || leftIndex >= values.size()) { return false; } const double t0 = axis.time[leftIndex]; const double t1 = axis.time[rightIndex]; const double v0 = values[leftIndex]; const double v1 = values[rightIndex]; if (!interpolate || !std::isfinite(v0) || !std::isfinite(v1) || !(t1 > t0)) { const double leftDistance = std::fabs(time - t0); const double rightDistance = std::fabs(t1 - time); value = (leftDistance <= rightDistance) ? v0 : v1; return std::isfinite(value); } const double ratio = (time - t0) / (t1 - t0); value = v0 + (v1 - v0) * ratio; return true; } bool SampleDisplayedSignalAt(const TimeAxis& axis, const SignalData& signal, const SignalStyle& style, double time, bool interpolate, double& value) { // 未启用水平镜像 / 时间偏移时与 SampleSignalAt 完全等价 //(避免两套实现产生细微差异)。 if (!style.mirrorHorizontal && style.offsetTime == 0.0) { return SampleSignalAt(axis, signal, time, interpolate, value); } value = kMissing; if (axis.time.empty() || signal.values.empty()) { return false; } // 绘制位置 = 源时刻 + offsetTime,因此查询源数据要用「绘制时刻 − offsetTime」。 const double query = time - style.offsetTime; if (query < axis.minTime || query > axis.maxTime) { return false; } const std::vector& values = signal.values; const std::size_t count = std::min(axis.time.size(), values.size()); if (count == 0) { return false; } // 画面上第 i 个位置用源下标 srcIndex(i) 的值(水平镜像 = 数据反序)。 const auto sample = [&](std::size_t index) -> double { return values[SourceIndexFor(style, count, index)]; }; if (!axis.monotonic) { const int nearest = FindNearestSample(axis, query); if (nearest < 0 || static_cast(nearest) >= count) { return false; } value = sample(static_cast(nearest)); return std::isfinite(value); } const std::vector::const_iterator lower = std::lower_bound(axis.time.begin(), axis.time.end(), query); const std::size_t rightIndex = static_cast(lower - axis.time.begin()); if (rightIndex == 0) { value = sample(0); return std::isfinite(value); } if (rightIndex >= count) { value = sample(count - 1); return std::isfinite(value); } const std::size_t leftIndex = rightIndex - 1; const double t0 = axis.time[leftIndex]; const double t1 = axis.time[rightIndex]; const double v0 = sample(leftIndex); const double v1 = sample(rightIndex); if (!interpolate || !std::isfinite(v0) || !std::isfinite(v1) || !(t1 > t0)) { const double leftDistance = std::fabs(query - t0); const double rightDistance = std::fabs(t1 - query); value = (leftDistance <= rightDistance) ? v0 : v1; return std::isfinite(value); } const double ratio = (query - t0) / (t1 - t0); value = v0 + (v1 - v0) * ratio; return true; } 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::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); } // 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。 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(); } 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"网格 " + std::to_wstring(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 (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(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; } } } } 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"网格 " + std::to_wstring(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) { 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; } 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