#include "DataModel.hpp" #include #include #include #include #include #include #include #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] header 原始列名。 * @return 可能为时间轴返回 true。 */ bool LooksLikeTimeHeader(const std::wstring& header) { const std::wstring text = ToLowerAscii(Trim(header)); if (text.empty()) { return false; } if (text == L"t" || text == L"time" || text == L"timestamp" || text == L"simulation time" || text == L"时间" || text == L"时间轴") { return true; } return ContainsInsensitive(text, L"time") || ContainsInsensitive(text, L"时间"); } /** * @brief 从名称末尾的括号中提取单位(仅接受形如 mA / V / deg/s 的短标记)。 * @param [IN] text 列名(已去除首尾空白)。 * @return 单位文本,无法识别时返回空字符串。 */ std::wstring ExtractUnit(const std::wstring& text) { if (text.size() < 3 || text.back() != L')') { return std::wstring(); } const std::size_t open = text.rfind(L'('); if (open == std::wstring::npos || open + 2 >= text.size()) { return std::wstring(); } const std::wstring inner = Trim(text.substr(open + 1, text.size() - open - 2)); if (inner.empty() || inner.size() > 10) { return std::wstring(); } for (const wchar_t c : inner) { const bool allowed = iswalpha(c) || iswdigit(c) || c == L'%' || c == L'/' || c == L'^' || c == L'-' || c == L'.' || c == L'\u00B0' || c == L'\u00B5' || c == L'\u03A9' || c == L'\u00B2' || c == L'\u00B3' || c == L'\u00B7'; if (!allowed) { return std::wstring(); } } return inner; } /** * @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 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 diffAmp = false; std::vector diffAmpItems; bool diffAmpPanelOpen = true; double diffAmpPanelX = -1.0; double diffAmpPanelY = -1.0; }; // 先记下每个信号原来所在网格的网格自身状态: // 勾选可见性会重建网格,若不携带,用户刚打开的开关会被悄悄清掉。 std::vector> previousCarry; for (const Lane& lane : lanes) { LaneCarry carry; 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.diffAmp = lane.diffAmp; carry.diffAmpItems = lane.diffAmpItems; carry.diffAmpPanelOpen = lane.diffAmpPanelOpen; carry.diffAmpPanelX = lane.diffAmpPanelX; carry.diffAmpPanelY = lane.diffAmpPanelY; 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](Lane& lane, const SignalRef& ref) { const LaneCarry carry = inheritedCarry(ref); lane.fitYToSelection = carry.fitYToSelection; 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.diffAmp = carry.diffAmp; lane.diffAmpItems = carry.diffAmpItems; lane.diffAmpPanelOpen = carry.diffAmpPanelOpen; lane.diffAmpPanelX = carry.diffAmpPanelX; lane.diffAmpPanelY = carry.diffAmpPanelY; }; // 收集全部可见信号引用。 std::vector visibleRefs; for (std::size_t fileIndex = 0; fileIndex < visible.size(); ++fileIndex) { for (std::size_t signalIndex = 0; signalIndex < visible[fileIndex].size(); ++signalIndex) { if (visible[fileIndex][signalIndex]) { SignalRef ref; ref.fileIndex = static_cast(fileIndex); ref.signalIndex = static_cast(signalIndex); visibleRefs.push_back(ref); } } } // 判断某引用对应的信号是否可见(越界视为不可见)。 const auto isVisibleRef = [&visible](const SignalRef& ref) { if (ref.fileIndex < 0 || static_cast(ref.fileIndex) >= visible.size()) { return false; } const std::vector& fileVisible = visible[static_cast(ref.fileIndex)]; 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"线段"; } } // ============================ DataFile 构建 ============================ /** * @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; /**< 有限采样点数量。 */ }; /** * @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)) { ++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 BuildDataFile(CsvTable&& table, const std::wstring& filePath, std::size_t paletteBase, DataFile& file, std::wstring& error, const CsvProgress& progress) { error.clear(); file = DataFile(); if (!table.HasData()) { error = L"数据表为空。"; return false; } const std::size_t columnCount = table.headers.size(); if (table.columns.size() < columnCount) { error = L"数据表列结构不完整。"; return false; } // 1. 识别时间轴列:优先取列名匹配的列,否则回退到第一列。 int timeColumn = -1; for (std::size_t column = 0; column < columnCount; ++column) { if (LooksLikeTimeHeader(table.headers[column])) { timeColumn = static_cast(column); break; } } if (timeColumn < 0) { timeColumn = 0; } // 时间列整列移动,不做逐元素拷贝。 file.time = std::move(table.columns[static_cast(timeColumn)]); std::size_t finiteTimeCount = 0; bool monotonic = true; double previousTime = 0.0; bool hasPrevious = false; for (const double value : file.time) { if (std::isfinite(value)) { ++finiteTimeCount; if (hasPrevious && value < previousTime) { monotonic = false; } previousTime = value; hasPrevious = true; } } if (finiteTimeCount < 2) { error = L"时间轴列(" + table.headers[static_cast(timeColumn)] + L")有效数值不足。"; return false; } file.timeColumnName = table.headers[static_cast(timeColumn)]; file.timeMonotonic = monotonic; file.filePath = filePath; const std::size_t slashPos = filePath.find_last_of(L"\\/"); file.fileName = (slashPos == std::wstring::npos) ? filePath : filePath.substr(slashPos + 1); // 2. 构建信号列:每列整块移动,随后就地统计取值范围。 int paletteCount = 0; const std::uint32_t* palette = SignalPalette(paletteCount); file.signals.reserve(columnCount); for (std::size_t column = 0; column < columnCount; ++column) { if (static_cast(column) == timeColumn) { continue; } SignalData signal; signal.rawHeader = table.headers[column]; { // 名称完整保留文件内的列名(含 "[n]" 序号与括号描述),仅用于展示不做改写。 signal.name = Trim(signal.rawHeader); signal.unit = ExtractUnit(signal.name); } 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; // 显示外观随信号一起初始化:调色板按「全局信号序号」取色, // 使同一信号在任意标签页中都保持同一默认颜色。 { const std::size_t globalIndex = paletteBase + file.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; } file.signals.push_back(std::move(signal)); if (progress) { CsvProgressInfo info; info.ratio = 0.75 + 0.25 * static_cast(column + 1) / static_cast(columnCount); if (!progress(info)) { error = L"已取消加载。"; return false; } } } if (file.signals.empty()) { error = L"除时间轴外未发现任何数据列。"; return false; } // 3. 统计时间范围(时间列为空或全部无效时回退到行数上界)。 double tMin = std::numeric_limits::infinity(); double tMax = -std::numeric_limits::infinity(); for (const double value : file.time) { if (std::isfinite(value)) { tMin = std::min(tMin, value); tMax = std::max(tMax, value); } } file.minTime = tMin; file.maxTime = tMax; return true; } // ============================ DataSet ============================ bool DataSet::AddDataFile(DataFile&& file, int& fileIndex) { fileIndex = -1; if (file.signals.empty()) { return false; } m_files.push_back(std::move(file)); fileIndex = static_cast(m_files.size()) - 1; return true; } bool DataSet::InsertDerivedSignal(int fileIndex, int afterSignalIndex, SignalData&& signal, int& newSignalIndex) { newSignalIndex = -1; if (fileIndex < 0 || fileIndex >= static_cast(m_files.size())) { return false; } DataFile& file = m_files[static_cast(fileIndex)]; if (afterSignalIndex < -1 || afterSignalIndex >= static_cast(file.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; // 2. 显示外观按「全局信号序号」取调色板颜色(与普通新信号完全一致的规则)。 const int insertPos = afterSignalIndex + 1; { // 新信号的全局序号 = 本文件之前的信号总数 + 它在文件内的下标。 std::size_t globalIndex = static_cast(insertPos); for (int index = 0; index < fileIndex; ++index) { globalIndex += m_files[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]; } } file.signals.insert(file.signals.begin() + insertPos, std::move(signal)); // 3. 全局选中引用:同一文件内位于插入点之后的信号下标整体后移。 if (m_selected.fileIndex == fileIndex && m_selected.signalIndex >= insertPos) { ++m_selected.signalIndex; } newSignalIndex = insertPos; return true; } bool DataSet::RemoveFile(int fileIndex) { if (fileIndex < 0 || fileIndex >= static_cast(m_files.size())) { return false; } // 用移动到末位后弹出的方式移除,使被移除文件的时间轴与全部信号缓冲区在本调用内 // 立即析构归还操作系统,不再等待后续插入覆盖;最后收缩容量,避免残留空闲块。 const std::size_t position = static_cast(fileIndex); for (std::size_t index = position; index + 1 < m_files.size(); ++index) { m_files[index] = std::move(m_files[index + 1]); } m_files.pop_back(); m_files.shrink_to_fit(); return true; } bool DataSet::RemoveSignal(int fileIndex, int signalIndex) { if (fileIndex < 0 || fileIndex >= static_cast(m_files.size())) { return false; } DataFile& file = m_files[static_cast(fileIndex)]; if (signalIndex < 0 || signalIndex >= static_cast(file.signals.size())) { return false; } // 与 RemoveFile 同一思路:先把待移除项移到末位(move 赋值时其原有缓冲当场析构), // 再 pop_back + shrink_to_fit,使该信号的全部采样内存在本调用内立即归还给系统, // 而不是留在容量里等后续插入覆盖。 std::vector& signals = file.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 fileIndex, int signalIndex) { if (fileIndex < 0 || signalIndex < 0 || m_selected.fileIndex != fileIndex) { 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 DataFile& file : m_files) { count += file.signals.size(); } return count; } const DataFile* DataSet::FileAt(int fileIndex) const { if (fileIndex < 0 || fileIndex >= static_cast(m_files.size())) { return nullptr; } return &m_files[static_cast(fileIndex)]; } bool DataSet::SetSignalDisplayName(int fileIndex, int signalIndex, const std::wstring& name) { if (fileIndex < 0 || fileIndex >= static_cast(m_files.size())) { return false; } DataFile& file = m_files[static_cast(fileIndex)]; if (signalIndex < 0 || signalIndex >= static_cast(file.signals.size())) { return false; } // 统一去除首尾空白;结果为空则视为恢复原始列名。 std::wstring trimmed = Trim(name); SignalData& signal = file.signals[static_cast(signalIndex)]; if (trimmed == signal.displayName) { return false; } signal.displayName = std::move(trimmed); return true; } bool DataSet::SetFileDisplayName(int fileIndex, const std::wstring& name) { if (fileIndex < 0 || fileIndex >= static_cast(m_files.size())) { return false; } std::wstring trimmed = Trim(name); DataFile& file = m_files[static_cast(fileIndex)]; if (trimmed == file.displayName) { return false; } file.displayName = std::move(trimmed); return true; } const SignalData* DataSet::Resolve(const SignalRef& ref) const { const DataFile* file = FileAt(ref.fileIndex); if (file == nullptr) { return nullptr; } if (ref.signalIndex < 0 || ref.signalIndex >= static_cast(file->signals.size())) { return nullptr; } return &file->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 DataFile* file = FileAt(ref.fileIndex); if (file == nullptr) { return nullptr; } if (ref.signalIndex < 0 || ref.signalIndex >= static_cast(file->signals.size())) { return nullptr; } // 这里需要写入,因此借助非 const 路径重新定位信号。 DataFile& mutableFile = m_files[static_cast(ref.fileIndex)]; return &mutableFile.signals[static_cast(ref.signalIndex)].style; } double DataSet::MinTime() const { double value = std::numeric_limits::infinity(); for (const DataFile& file : m_files) { if (std::isfinite(file.minTime)) { value = std::min(value, file.minTime); } } return std::isfinite(value) ? value : 0.0; } double DataSet::MaxTime() const { double value = -std::numeric_limits::infinity(); for (const DataFile& file : m_files) { if (std::isfinite(file.maxTime)) { value = std::max(value, file.maxTime); } } return std::isfinite(value) ? value : 1.0; } void DataSet::RemapSelectionAfterFileRemoval(int fileIndex) { if (fileIndex < 0) { return; } if (m_selected.fileIndex == fileIndex) { m_selected = SignalRef(); } else if (m_selected.fileIndex > fileIndex) { --m_selected.fileIndex; } } 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),不额外占用前缀和内存。“差异放大”需要在 float 缓冲上再做一次 * 滑动平均,若先转成 double 会因为亿级采样多占一倍内存,因此做成模板。 */ template void MovingAverageImpl(const std::vector& values, int radius, std::vector& out); /** * @brief 滑动窗口最大值(只要最大值)。 * * 与 `SlidingExtrema` 同一个单调队列思路,但**不额外分配最小值数组**: * 亿级采样下一次全量计算会多占 100 MB 以上,而“差异放大”只需要最大值。 * * @param [IN] values 采样值。 * @param [IN] radius 邻域半径(窗口 = [i-radius, i+radius],两端按实际长度)。 * @param [OUT] outMax 邻域最大值(整窗都是 NaN 时为 NaN)。 */ template void SlidingMax(const std::vector& values, std::size_t radius, std::vector& outMax) { const std::size_t count = values.size(); outMax.assign(count, std::numeric_limits::quiet_NaN()); if (count == 0) { return; } std::deque maxQueue; // 递减:队首最大。 std::size_t filled = 0; for (std::size_t index = 0; index < count; ++index) { const std::size_t wantHigh = std::min(count - 1, index + radius); while (filled <= wantHigh) { const double incoming = values[filled]; if (std::isfinite(incoming)) { while (!maxQueue.empty() && values[maxQueue.back()] <= incoming) { maxQueue.pop_back(); } maxQueue.push_back(filled); } ++filled; } const std::size_t low = (index > radius) ? (index - radius) : 0; while (!maxQueue.empty() && maxQueue.front() < low) { maxQueue.pop_front(); } if (!maxQueue.empty()) { outMax[index] = static_cast(values[maxQueue.front()]); } } } } // namespace void ComputeMovingAverage(const std::vector& values, int radius, std::vector& out) { MovingAverageImpl(values, radius, out); } 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)); } } } /** * @brief 滑动窗口极值:一次求出每个中心点邻域(±radius)内的最小值与最大值。 * * 两个单调队列(最小值队列递增、最大值队列递减)同步滑动,O(n) 时间, * 内存只有窗口长度。NaN 不参与(整窗都是 NaN 时输出 NaN)。 * * @param [IN] values 采样值。 * @param [IN] radius 邻域半径(窗口 = [i-radius, i+radius],两端按实际长度)。 * @param [OUT] outMin 邻域最小值。 * @param [OUT] outMax 邻域最大值。 */ template void SlidingExtrema(const std::vector& values, std::size_t radius, std::vector& outMin, std::vector& outMax) { const std::size_t count = values.size(); outMin.assign(count, std::numeric_limits::quiet_NaN()); outMax.assign(count, std::numeric_limits::quiet_NaN()); if (count == 0) { return; } std::deque minQueue; // 递增:队首最小。 std::deque maxQueue; // 递减:队首最大。 std::size_t filled = 0; for (std::size_t index = 0; index < count; ++index) { const std::size_t wantHigh = std::min(count - 1, index + radius); while (filled <= wantHigh) { const double incoming = values[filled]; if (std::isfinite(incoming)) { while (!minQueue.empty() && values[minQueue.back()] >= incoming) { minQueue.pop_back(); } minQueue.push_back(filled); while (!maxQueue.empty() && values[maxQueue.back()] <= incoming) { maxQueue.pop_back(); } maxQueue.push_back(filled); } ++filled; } const std::size_t low = (index > radius) ? (index - radius) : 0; while (!minQueue.empty() && minQueue.front() < low) { minQueue.pop_front(); } while (!maxQueue.empty() && maxQueue.front() < low) { maxQueue.pop_front(); } if (!minQueue.empty()) { outMin[index] = static_cast(values[minQueue.front()]); } if (!maxQueue.empty()) { outMax[index] = static_cast(values[maxQueue.front()]); } } } } // namespace void ComputeMovingAverageIterated(const std::vector& values, int radius, int order, std::vector& out) { const int stages = std::max(1, order); // 第 1 阶:从原始采样值出发。 ComputeMovingAverage(values, radius, out); if (stages == 1) { return; } // 后续各阶以上一阶的结果为输入(double 中转,避免 float→double 的精度损失累积)。 std::vector buffer(values.size(), std::numeric_limits::quiet_NaN()); std::vector stage; for (int iteration = 1; iteration < stages; ++iteration) { 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); } } void ComputeDiffAmplify(const std::vector& values, int window, int gain, std::vector& out) { const std::size_t count = values.size(); out.assign(count, std::numeric_limits::quiet_NaN()); if (count == 0) { return; } // 窗口点数 X:局部“差异强度”的统计**窗口长度**(点数),窗口 = [i−r, i+r], // r = (X−1)/2。窗口长度**应略大于纹波周期的一半**: // 只有当窗口跨越的相位达到 180° 时,窗口内才必定包含一个波峰或波谷, // “局部 |交流分量| 的峰值”因而恒等于纹波振幅 A,与相位无关 //(正常区域恒为 A、弱区恒为 A/2,比值恰为 2);窗口再小就会跟随相位起伏 //(把正常区域的波峰也一起放大),远大于一个周期时又会把弱区与邻近正常区一起统计掉。 // 窗口越短、混叠区(弱区边界各 ±r)越窄,对“只有一两个周期(甚至半周期)变弱”的 // 情形越有效。 const int span = std::max(3, std::min(window, 1000000)); const int strengthRadius = std::max(1, (span - 1) / 2); // 趋势窗口取局部窗口的 2 倍(≈ 一个完整纹波周期):只有按整周期取平均, // 趋势线才恰好是纹波的中心线(交流分量均值为零),待放大的弱区也就**不会** // 因为自己变“扁”而把趋势线一起拉偏。 const int trendRadius = std::max(1, strengthRadius * 2); // 包络窗口固定取局部窗口的 8 倍:用来给出“周边本来的振幅范围”, // 既作为放大基准(中心值)也作为输出的夹取上下界。 const std::size_t envelopeRadius = static_cast(std::max(1, std::min(strengthRadius * 8, 16384))); const double maxGain = static_cast(std::max(1, gain)); // 1) 周边包络:邻域内采样值的最小/最大值(单调队列,O(n))。 // 邻域**必须明显大于一个纹波周期**,否则包络会跟着纹波起伏, // 放大后会把相邻正常区域的相位也一并扭曲。 std::vector envMin; std::vector envMax; SlidingExtrema(values, envelopeRadius, envMin, envMax); // 2) 局部差异强度 = 邻域内 |采样值 − 趋势线| 的**最大值**(即局部纹波振幅)。 // // 这里刻意**不用**另外两种看起来更自然的做法: // * “|采样值 − 趋势线| 的平均”:趋势线是低频量,会被待放大的弱区本身拉偏, // 于是弱区周边本来正常的采样点也会被判成“差异偏小”而被一起放大; // * “原始采样值的峰峰值”:峰峰值要同时覆盖波峰与波谷才与相位无关, // 这要求窗口跨越 360°,窗口会大到把弱区与邻近正常区平均掉 // (实测一个周期宽的弱区只能恢复三分之一)。 // |交流分量| 的最大值只需要窗口跨越 180°(窗口约半个周期)就与相位无关, // 既能准确识别弱区,又因为窗口很短而具备足够好的定位精度。 // // 又因为增益下界恒为 1(见第 4 步),正常区域的增益恰好是 1, // 输出与输入**逐位相同**,因此对其它数据点真正没有任何影响。 // // 极值窗口按**实际长度**(端点截断)统计,端点因此会低估振幅、 // 导致增益被高估;这一端点效应由第 4 步的收敛系数消除。 std::vector trend; ComputeMovingAverage(values, trendRadius, trend); std::vector strength; { // `ac` 只在求局部最大值时用到:单独放在一个作用域里, // 亿级采样下这一份 100 MB 级的中转数组能尽早归还给系统。 std::vector ac(count, std::numeric_limits::quiet_NaN()); for (std::size_t index = 0; index < count; ++index) { const double value = values[index]; if (std::isfinite(value) && std::isfinite(trend[index])) { ac[index] = static_cast(std::fabs(value - static_cast(trend[index]))); } } SlidingMax(ac, static_cast(strengthRadius), strength); } // 3) 目标强度:差异强度的 75 分位。用直方图统计 → O(1) 额外内存, // 且不会被个别尖峰抬高(用均值会),也不会被最尖处完全主导(用最大值会)。 // // ⚠️ 统计时**排除首尾的端点收敛区**(见第 4 步):那里的趋势窗口被截断, // “|交流分量| 的最大值”整体偏大,会把分位数抬高、导致目标强度偏大。 // 待统计的采样点数不足时退回全段统计。 const std::size_t edgeGuard = static_cast(2) * static_cast(trendRadius); // 剩下足够多的采样点时才有必要排除端点区(短信号就直接全段统计)。 constexpr std::size_t kEdgeGuardMinimum = 256; const bool useGuard = (count > edgeGuard * 2 + kEdgeGuardMinimum); const auto inGuard = [count, edgeGuard, useGuard](std::size_t index) { if (!useGuard) { return true; } return (index >= edgeGuard) && ((count - 1 - index) >= edgeGuard); }; float maxStrength = 0.0f; for (std::size_t index = 0; index < count; ++index) { const float value = strength[index]; if (inGuard(index) && std::isfinite(value) && value > maxStrength) { maxStrength = value; } } if (!(maxStrength > 0.0f)) { // 完全平坦(或全为 NaN)的信号:无差异可放大,等价于原样输出。 for (std::size_t index = 0; index < count; ++index) { const double value = values[index]; if (std::isfinite(value)) { out[index] = static_cast(value); } } return; } constexpr int kBinCount = 2048; std::vector histogram(static_cast(kBinCount), 0); std::size_t sampleCount = 0; for (std::size_t index = 0; index < count; ++index) { const float value = strength[index]; if (!inGuard(index) || !std::isfinite(value)) { continue; } int bin = static_cast(static_cast(value) / static_cast(maxStrength) * kBinCount); bin = std::max(0, std::min(bin, kBinCount - 1)); ++histogram[static_cast(bin)]; ++sampleCount; } double target = static_cast(maxStrength); if (sampleCount > 0) { const std::size_t wanted = (sampleCount * 75) / 100; std::size_t accumulated = 0; for (int bin = 0; bin < kBinCount; ++bin) { accumulated += static_cast(histogram[static_cast(bin)]); if (accumulated > wanted) { // 取桶中心,避免目标强度贴到桶的下边界而整体偏小。 target = (static_cast(bin) + 0.5) / kBinCount * static_cast(maxStrength); break; } } } // 4) 逐点增益并合成输出。 // * 基准 = 趋势线(纹波的中心线,与信号整体同步,纹波在其中上下摆动); // * 增益下界为 1 → 局部振幅不比目标值小的采样点**输出与原值逐位相同** // (基准被 1 倍增益完全消掉,因此对其它数据点没有任何影响); // * 放大后的取值再夹到周边包络内 → **任何一点都不会超出原有的振幅范围** // (尤其重要:被放大区域两侧自身的振幅本来就很大,不应被继续抬升)。 for (std::size_t index = 0; index < count; ++index) { const double value = values[index]; if (!std::isfinite(value)) { continue; } const bool hasEnvelope = std::isfinite(envMin[index]) && std::isfinite(envMax[index]); const double base = std::isfinite(trend[index]) ? static_cast(trend[index]) : value; double pointGain = 1.0; const float local = strength[index]; if (std::isfinite(local) && local > 0.0f) { pointGain = target / static_cast(local); } pointGain = std::max(1.0, std::min(pointGain, maxGain)); // 端点收敛:首尾各 2r 个采样点的**趋势窗口与极值窗口都被截断** //(趋势窗口半径 2r、极值窗口半径 r),局部振幅与中心线都被系统性估偏。 // 因此这些采样点一律保持原样(增益恰为 1),再在接下来的 2r 个点内 // 平滑过渡到完整增益,避免记录首尾被整片抬升。 // // ⚠️ 不能靠“镜像延拓”解决:镜像后的窗口覆盖的真实下标集合与截断窗口相同 //(例如 i=16、r=40 时两者都只覆盖真实下标 0..56),因此不会带来任何新信息。 const double taperStart = static_cast(trendRadius); const double taperSpan = std::max(1.0, static_cast(trendRadius)); const double distanceToEdge = static_cast(std::min(index, count - 1 - index)); const double taper = std::max(0.0, std::min(1.0, (distanceToEdge - taperStart) / taperSpan)); pointGain = 1.0 + (pointGain - 1.0) * taper; double amplified = base + pointGain * (value - base); if (hasEnvelope) { amplified = std::max(static_cast(envMin[index]), std::min(amplified, static_cast(envMax[index]))); } out[index] = static_cast(amplified); } } 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::DiffAmpItem* Lane::FindDiffAmpItem(const SignalRef& ref) const { for (const DiffAmpItem& item : diffAmpItems) { if (item.ref == ref) { return &item; } } return nullptr; } Lane::DiffAmpItem& Lane::EnsureDiffAmpItem(const SignalRef& ref) { for (DiffAmpItem& item : diffAmpItems) { if (item.ref == ref) { return item; } } DiffAmpItem item; item.ref = ref; // 默认参数:窗口点数 X = 80(长度)、放大倍数 N = 2。 // 窗口长度需略大于纹波周期的一半(见 ComputeDiffAmplify 的说明), // 80 点可覆盖周期约 160 采样以内的纹波。 item.points = 80; item.order = 2; diffAmpItems.push_back(item); return diffAmpItems.back(); } void Lane::RemoveDiffAmpItem(const SignalRef& ref) { diffAmpItems.erase( std::remove_if(diffAmpItems.begin(), diffAmpItems.end(), [&ref](const DiffAmpItem& item) { return item.ref == ref; }), diffAmpItems.end()); } int FindNearestSample(const DataFile& file, double time) { const std::size_t count = file.time.size(); if (count == 0) { return -1; } if (file.timeMonotonic) { const std::vector::const_iterator lower = std::lower_bound(file.time.begin(), file.time.end(), time); if (lower == file.time.begin()) { return 0; } if (lower == file.time.end()) { return static_cast(count - 1); } const std::size_t rightIndex = static_cast(lower - file.time.begin()); const std::size_t leftIndex = rightIndex - 1; const double leftDistance = std::fabs(time - file.time[leftIndex]); const double rightDistance = std::fabs(file.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(file.time[index])) { continue; } const double distance = std::fabs(file.time[index] - time); if (distance < bestDistance) { bestDistance = distance; best = static_cast(index); } } return best; } bool SampleSignalAt(const DataFile& file, const SignalData& signal, double time, bool interpolate, double& value) { value = kMissing; if (file.time.empty() || signal.values.empty()) { return false; } if (time < file.minTime || time > file.maxTime) { return false; } const std::vector& values = signal.values; if (!file.timeMonotonic) { const int nearest = FindNearestSample(file, 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(file.time.begin(), file.time.end(), time); const std::size_t rightIndex = static_cast(lower - file.time.begin()); if (rightIndex == 0) { value = values[0]; return std::isfinite(value); } if (rightIndex >= file.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 = file.time[leftIndex]; const double t1 = file.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; } // ============================ TabConfig ============================ TabConfig::TabConfig() : m_name(L"视图 1") { } void TabConfig::SyncToDataSet(const DataSet& dataSet) { const std::size_t fileCount = dataSet.FileCount(); m_visible.resize(fileCount); m_collapsed.resize(fileCount, false); for (std::size_t fileIndex = 0; fileIndex < fileCount; ++fileIndex) { const DataFile* file = dataSet.FileAt(static_cast(fileIndex)); if (file == nullptr) { continue; } // 新增信号默认为未勾选(由用户自行决定查看哪些信号), // 其显示外观已在 DataSet 中初始化完毕。 m_visible[fileIndex].resize(file->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::RemapAfterFileRemoval(int fileIndex) { if (fileIndex < 0) { return; } const std::size_t slot = static_cast(fileIndex); if (slot < m_visible.size()) { m_visible.erase(m_visible.begin() + fileIndex); } if (slot < m_collapsed.size()) { m_collapsed.erase(m_collapsed.begin() + fileIndex); } for (Lane& lane : m_lanes) { std::vector& refs = lane.signals; refs.erase(std::remove_if(refs.begin(), refs.end(), [fileIndex](const SignalRef& ref) { return ref.fileIndex == fileIndex; }), refs.end()); for (SignalRef& ref : refs) { if (ref.fileIndex > fileIndex) { --ref.fileIndex; } } } 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 fileIndex, int signalIndex) { if (fileIndex < 0 || signalIndex < 0) { return; } // 可见性表:删掉该信号对应的那一列。 const std::size_t slot = static_cast(fileIndex); 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(), [fileIndex, signalIndex](const SignalRef& ref) { return ref.fileIndex == fileIndex && ref.signalIndex == signalIndex; }), refs.end()); for (SignalRef& ref : refs) { if (ref.fileIndex == fileIndex && ref.signalIndex > signalIndex) { --ref.signalIndex; } } // 均值滤波 / 中心轴边沿计数的条目同样持有信号引用,必须一起删除/前移。 const auto purgeRefs = [fileIndex, signalIndex](std::vector& items) { items.erase(std::remove_if(items.begin(), items.end(), [fileIndex, signalIndex](const Lane::MeanFilterItem& item) { return item.ref.fileIndex == fileIndex && item.ref.signalIndex == signalIndex; }), items.end()); for (Lane::MeanFilterItem& item : items) { if (item.ref.fileIndex == fileIndex && item.ref.signalIndex > signalIndex) { --item.ref.signalIndex; } } }; purgeRefs(lane.meanFilterItems); purgeRefs(lane.edgeCenterItems); purgeRefs(lane.diffAmpItems); } // 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。 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.fileIndex < 0 || static_cast(ref.fileIndex) >= m_visible.size()) { return false; } const std::vector& fileVisible = m_visible[static_cast(ref.fileIndex)]; 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.fileIndex < 0 || static_cast(ref.fileIndex) >= m_visible.size()) { return; } std::vector& fileVisible = m_visible[static_cast(ref.fileIndex)]; 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::SetFileVisible(int fileIndex, bool visible) { if (fileIndex < 0 || static_cast(fileIndex) >= m_visible.size()) { return; } std::vector& fileVisible = m_visible[static_cast(fileIndex)]; // 合并模式下“新增”的信号同样应各自独占网格(与 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::FileHasVisible(int fileIndex) const { if (fileIndex < 0 || static_cast(fileIndex) >= m_visible.size()) { return false; } const std::vector& fileVisible = m_visible[static_cast(fileIndex)]; return std::find(fileVisible.begin(), fileVisible.end(), true) != fileVisible.end(); } bool TabConfig::FileAllVisible(int fileIndex) const { if (fileIndex < 0 || static_cast(fileIndex) >= m_visible.size()) { return false; } const std::vector& fileVisible = m_visible[static_cast(fileIndex)]; 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.diffAmp = source.diffAmp; lane.diffAmpPanelOpen = source.diffAmpPanelOpen; lane.diffAmpPanelX = source.diffAmpPanelX; lane.diffAmpPanelY = source.diffAmpPanelY; lane.meanFilter = source.meanFilter; lane.meanFilterPanelOpen = source.meanFilterPanelOpen; lane.meanFilterPanelX = source.meanFilterPanelX; lane.meanFilterPanelY = source.meanFilterPanelY; // 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 差异放大条目。 lane.meanFilterItems.clear(); lane.edgeCenterItems.clear(); lane.diffAmpItems.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::DiffAmpItem& item : source.diffAmpItems) { if (item.ref == ref) { lane.diffAmpItems.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 fileIndex, int insertPos) { if (fileIndex < 0 || insertPos < 0) { return; } // 可见性表:在插入点补一列(新信号默认不可见,由调用方按需勾选)。 const std::size_t slot = static_cast(fileIndex); 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.fileIndex == fileIndex && ref.signalIndex >= insertPos) { ++ref.signalIndex; } } // 均值滤波 / 中心轴条目同样持有信号引用,必须一起后移。 for (Lane::MeanFilterItem& item : lane.meanFilterItems) { if (item.ref.fileIndex == fileIndex && item.ref.signalIndex >= insertPos) { ++item.ref.signalIndex; } } for (Lane::EdgeCenterItem& item : lane.edgeCenterItems) { if (item.ref.fileIndex == fileIndex && item.ref.signalIndex >= insertPos) { ++item.ref.signalIndex; } } for (Lane::DiffAmpItem& item : lane.diffAmpItems) { if (item.ref.fileIndex == fileIndex && 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 fileIndex) const { if (fileIndex < 0 || static_cast(fileIndex) >= m_collapsed.size()) { return false; } return m_collapsed[static_cast(fileIndex)]; } void TabConfig::ToggleCollapsed(int fileIndex) { if (fileIndex < 0) { return; } if (static_cast(fileIndex) >= m_collapsed.size()) { m_collapsed.resize(static_cast(fileIndex) + 1, false); } m_collapsed[static_cast(fileIndex)] = !m_collapsed[static_cast(fileIndex)]; } 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; } low = std::min(low, signal->minValue); high = std::max(high, signal->maxValue); } 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 fileIndex = 0; fileIndex < m_visible.size(); ++fileIndex) { const std::vector& fileVisible = m_visible[fileIndex]; if (std::find(fileVisible.begin(), fileVisible.end(), true) == fileVisible.end()) { continue; } const DataFile* file = dataSet.FileAt(static_cast(fileIndex)); if (file == nullptr) { continue; } if (std::isfinite(file->minTime)) { low = std::min(low, file->minTime); } if (std::isfinite(file->maxTime)) { high = std::max(high, file->maxTime); } } if (!std::isfinite(low) || !std::isfinite(high) || !(high > low)) { return false; } minTime = low; maxTime = high; return true; } } // namespace jdi