From 82a29362eb30fe886bbbb8e43c714198cc956f47 Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" <> Date: Fri, 9 Oct 2026 15:21:10 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BD=91=E6=A0=BC=E7=AE=A1=E7=90=86=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=EF=BC=9A=E6=96=B0=E4=BF=A1=E5=8F=B7=E7=BD=AE=E5=BA=95?= =?UTF-8?q?=E3=80=81=E6=8B=86=E5=88=86=E5=BD=93=E5=89=8D=E7=BD=91=E6=A0=BC?= =?UTF-8?q?=E3=80=81=E4=B8=AD=E9=94=AE=E8=87=AA=E7=94=B1=E5=B9=B3=E7=A7=BB?= =?UTF-8?q?=E3=80=81=E9=80=89=E5=8C=BA=E6=A8=A1=E5=BC=8F=E5=9B=BE=E4=BE=8B?= =?UTF-8?q?=E5=8F=AF=E7=94=A8=E3=80=81=E9=9D=A2=E6=9D=BF=E6=82=AC=E5=81=9C?= =?UTF-8?q?=E9=AB=98=E4=BA=AE=E3=80=81=E4=B8=AD=E5=BF=83=E8=BD=B4=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=E4=B8=8E=E8=B6=8A=E7=95=8C=E4=B8=89=E8=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新勾选的信号所在网格固定出现在**最下方**: 原先 PER_SIGNAL 模式按 visibleRefs(文件/列序)重建网格,新勾选的信号会按其列序插到 中间,位置看起来"不确定"。现在先按「原有网格位置」排序(已分配过网格的信号保持相对次序), 再把尚未分配网格(新勾选)的信号统一追加到末尾。 - 网格右键菜单在「实验性功能」上方新增「拆分当前网格」(`PLOT_CMD_LANE_SPLIT`): 把该网格内的信号按个拆分成独立网格,新网格插入到原网格的**下方依次排列**, 原网格下方的网格顺次后移。网格编号(GX)完全由位置推导,因此左侧列表徽标、图例徽标、 面板标题、菜单标题等所有使用该编号的地方会同步更新,不会错位。 新增 `TabConfig::SplitLane()` / `PlotView::SplitLane()` / `PlotView::LaneSignalCount()` (仅 1 条信号时菜单项置灰)。拆分出来的网格继承原网格的网格自身状态 (Y 轴自适应、边沿计数、面板位置等)。 - 鼠标**中键**按下拖拽也可自由查看区域(水平平移时间轴 + 纵向偏移当前网格幅值): 新增 `PlotView::OnMButtonDown/OnMButtonUp` 与 `Application::WM_MBUTTONDOWN/UP`, **任何缩放模式下都可用**(不改变当前模式)。拖动期间 `WM_MOUSEMOVE` 的按键掩码 加入 `MK_MBUTTON`,光标刷新为抓握图标。 - 选区放大模式下不再阻断图例交互:把「点击图例条目 → 选中信号」的判定移到选区放大分支 **之前**,并让图例悬停(半高亮)在选区放大模式下同样生效(该模式只应改变拖拽语义)。 同时选区放大模式下也可用中键拖拽平移。 - 实验性功能浮动面板悬停时高亮其所属网格(与鼠标直接停在网格上一致): `OnMouseMove` 中若鼠标落在面板上则把 `m_hoverLane` 置为该面板所属网格。 - 「边沿计数(中心轴)」: * 中心平均值曲线的颜色样式改为与光标一致(`CursorColor(1)`,即「边沿计数」横向光标 与中心值胶囊的颜色),曲线与其取值标注、光标形成统一的视觉语言。 * 中心值标注形式改为「数值(中心轴)」(数值在前、尾缀在后); 双光标模式下由右侧光标额外显示增量,形如「数值(+0.0008 中心轴)」, 与常规信号的增量显示保持一致(无尖头括号)。 * 修复:光标处于当前显示区域之外(贴边显示)时,常规数据点会变成三角指示方向, 而中心轴数据点仍是圆点。现在中心轴数据点复用同一分支,越界时同样绘制三角。 验证(TestData/VERY_BIG_DATA.CSV,125%、1725x996): - 置底:先勾选第 5 个信号(电流钳采集(原始))→ G1,再勾选第 1 个信号(编码器A相)→ G2, 新勾选的信号确实出现在最下方。 - 拆分:G1(编码器A相) / G2(飞线IO+编码器B相) / G3(电流钳(原始)) 时拆分 G2, 得到 G1 / G2(飞线IO) / G3(编码器B相) / G4(电流钳(原始)), 原本在中间的电流钳顺延为 G4,左侧徽标、图例徽标全部同步。 - 中键:常规模式与选区放大模式下中键拖拽均能把时间轴平移到 80~160(截图核对)。 - 选区放大模式:悬停图例条目显示半高亮;点击图例条目可切换选中信号(左侧高亮随之切换)。 - 面板悬停:鼠标停在 G1 的「边沿计数」面板上时 G1 区域被高亮(与悬停 G1、G2 两种情形 做区域哈希对比,确认高亮归属正确)。 - 中心轴:曲线为光标橙 `#CB4B16`;面板标题「中心轴边沿计数」、底部「平滑点数 N = 5」; 单光标显示 `4.9417` + `4.94199(中心轴)`; 双光标(标题带「(光标区间内)」,计数 341959/341957)右侧光标显示 `4.9425 (+0.000…)` + `4.94214(+0.00… 中心轴)`,左侧光标无增量; 把 N 改为 4000 后曲线与计数同步更新(2333700/2333700); 用选区放大把光标推到视野外后,中心轴数据点在与信号相同的贴边位置绘制为**橙色三角** (像素扫描 x=479 处得到 6px 高的 `#CB4B16` 三角,而非常规圆点)。 - 回归:[1].CSV 上「边沿计数」面板正常(上升沿 2 / 下降沿 2),中键平移正常。 - make clean && make all 无警告无错误。 --- Components/Application/Inc/Application.hpp | 1 + Components/Application/Src/Application.cpp | 54 +++++- Components/PlotView/Inc/PlotView.hpp | 33 +++- Components/PlotView/Src/PlotView.cpp | 185 +++++++++++++++++---- Utilities/Inc/DataModel.hpp | 12 ++ Utilities/Src/DataModel.cpp | 101 ++++++++++- 6 files changed, 342 insertions(+), 44 deletions(-) diff --git a/Components/Application/Inc/Application.hpp b/Components/Application/Inc/Application.hpp index 9b6abbc..bdabe9f 100644 --- a/Components/Application/Inc/Application.hpp +++ b/Components/Application/Inc/Application.hpp @@ -340,6 +340,7 @@ namespace jdi float m_panelWidth = 366.0f; /**< 面板宽度(逻辑像素)。 */ bool m_draggingSplitter = false; /**< 是否正在拖动分隔条。 */ + bool m_middlePanning = false; /**< 是否正在用鼠标中键自由平移绘图区。 */ int m_splitterGrabOffset = 0; /**< 分隔条拖动的抓取偏移。 */ HDC m_backDc = nullptr; /**< 后备缓冲设备上下文。 */ diff --git a/Components/Application/Src/Application.cpp b/Components/Application/Src/Application.cpp index 98a45f0..646ea94 100644 --- a/Components/Application/Src/Application.cpp +++ b/Components/Application/Src/Application.cpp @@ -1441,9 +1441,15 @@ namespace jdi fitLaneTime.id = PLOT_CMD_LANE_FIT_TIME; items.push_back(fitLaneTime); + // 「拆分当前网格」:把该网格内的信号按个拆成独立网格,插到其下方依次排列。 + MenuItem splitLane; + splitLane.label = L"拆分当前网格"; + splitLane.id = PLOT_CMD_LANE_SPLIT; + splitLane.enabled = m_plot.LaneSignalCount(laneIndex) > 1; + items.push_back(splitLane); + // 实验性功能(二级菜单):横线上方,鼠标悬停即向右展开。 - MenuItem experimental; - experimental.kind = MenuItemKind::SUBMENU; + MenuItem experimental; experimental.kind = MenuItemKind::SUBMENU; experimental.label = L"实验性功能"; { MenuItem edgeCount; @@ -1567,6 +1573,10 @@ namespace jdi // 「实验性功能 → 边沿计数(中心轴)」:切换中心平均值曲线与统计面板。 m_plot.ToggleLaneEdgeCenterCount(laneIndex); break; + case PLOT_CMD_LANE_SPLIT: + // 「拆分当前网格」:信号按个拆成独立网格,新网格顺次插入其下方。 + m_plot.SplitLane(laneIndex); + break; case PLOT_CMD_FIT_ALL: m_plot.FitAll(); break; @@ -1883,7 +1893,7 @@ namespace jdi const int x = GET_X_LPARAM(lParam); const int y = GET_Y_LPARAM(lParam); - const bool buttonDown = (wParam & (MK_LBUTTON | MK_RBUTTON)) != 0; + const bool buttonDown = (wParam & (MK_LBUTTON | MK_RBUTTON | MK_MBUTTON)) != 0; if (HandleMouseMove(x, y, buttonDown)) { InvalidateRect(hwnd, nullptr, FALSE); @@ -2060,6 +2070,44 @@ namespace jdi InvalidateRect(hwnd, nullptr, TRUE); return 0; } + case WM_MBUTTONDOWN: + { + // 鼠标中键:在绘图区内按下即进入自由平移(任何缩放模式下都可用)。 + const int x = GET_X_LPARAM(lParam); + const int y = GET_Y_LPARAM(lParam); + if (m_plot.IsEditing()) + { + // 内联输入中:中键视为提交(与左键点外一致)。 + m_plot.OnEditKey(VK_RETURN); + ApplyPendingRename(); + } + if (m_plot.HitTest(x, y)) + { + SetCapture(hwnd); + m_middlePanning = true; + m_activeTarget = Target::PLOT; + if (m_plot.OnMButtonDown(x, y)) + { + ApplyPendingRename(); + InvalidateRect(hwnd, nullptr, FALSE); + } + UpdateDragCursor(x, y); + } + return 0; + } + case WM_MBUTTONUP: + { + if (m_middlePanning) + { + m_middlePanning = false; + m_plot.OnMButtonUp(); + ReleaseCapture(); + m_activeTarget = Target::NONE; + UpdateDragCursor(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); + InvalidateRect(hwnd, nullptr, TRUE); + } + return 0; + } case WM_LBUTTONDBLCLK: { const int x = GET_X_LPARAM(lParam); diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index d11b447..f032cd8 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -68,7 +68,8 @@ namespace jdi PLOT_CMD_LANE_FIT_TIME = 5, /**< 时间轴自适应到本网格信号范围(按网格时间轴展开)。 */ PLOT_CMD_LANE_AUTO_VIEW = 6, /**< 切换本网格的「选区 Y 轴自适应」开关。 */ PLOT_CMD_LANE_EDGE_COUNT = 7, /**< 切换本网格的「实验性功能 → 边沿计数」开关。 */ - PLOT_CMD_LANE_EDGE_CENTER = 8 /**< 切换本网格的「实验性功能 → 边沿计数(中心轴)」开关。 */ + PLOT_CMD_LANE_EDGE_CENTER = 8, /**< 切换本网格的「实验性功能 → 边沿计数(中心轴)」开关。 */ + PLOT_CMD_LANE_SPLIT = 9 /**< 拆分当前网格:把网格内信号按个拆成独立网格并顺次下移编号。 */ }; /** @@ -149,6 +150,22 @@ namespace jdi */ bool OnLButtonUp(int x, int y); + /** + * @brief 处理鼠标中键按下:与左键在常规模式下一致,进入自由平移(水平平移时间轴 + + * 纵向偏移当前网格幅值)。**任何缩放模式下都可用**,用于在不改变当前模式的 + * 前提下自由查看区域。 + * @param [IN] x 客户区横坐标。 + * @param [IN] y 客户区纵坐标。 + * @return 需要重绘返回 true。 + */ + bool OnMButtonDown(int x, int y); + + /** + * @brief 处理鼠标中键抬起:结束中键平移。 + * @return 需要重绘返回 true。 + */ + bool OnMButtonUp(); + /** * @brief 处理鼠标滚轮(仅用于平移与网格滚动,不再承担缩放)。 * @param [IN] x 客户区横坐标。 @@ -180,6 +197,20 @@ namespace jdi */ bool FitTimeToLane(int laneIndex); + /** + * @brief 「拆分当前网格」:把该网格内的信号按个拆分成独立网格,新网格依次排在其下方。 + * @param [IN] laneIndex 网格下标。 + * @return 拆分成功返回 true。 + */ + bool SplitLane(int laneIndex); + + /** + * @brief 查询某网格内的信号数量(用于菜单项可用状态判定)。 + * @param [IN] laneIndex 网格下标。 + * @return 信号数量,网格不存在时返回 0。 + */ + std::size_t LaneSignalCount(int laneIndex) const; + /** * @brief 依据当前显示时间段内的数据对该网格做 Y 轴自适应(“选区 Y 轴自适应”)。 * diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index be85697..95a205d 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -965,9 +965,36 @@ namespace jdi return true; } - bool PlotView::SampleSpacingPixels(const DataFile& file, double time, double& pixels) const + bool PlotView::SplitLane(int laneIndex) { - pixels = 0.0; + if (m_tab == nullptr) + { + return false; + } + // 拆分由 TabConfig 完成(含网格自身状态的继承与编号刷新)。 + const int first = m_tab->SplitLane(laneIndex); + if (first < 0) + { + return false; + } + // 网格数量变化 → 几何与滚动范围需要重建,静态场景缓存也要失效。 + BuildLaneVisuals(); + InvalidateSceneCache(); + return true; + } + + std::size_t PlotView::LaneSignalCount(int laneIndex) const + { + if (m_tab == nullptr || laneIndex < 0 || + laneIndex >= static_cast(m_tab->Lanes().size())) + { + return 0; + } + return m_tab->Lanes()[static_cast(laneIndex)].signals.size(); + } + + bool PlotView::SampleSpacingPixels(const DataFile& file, double time, double& pixels) const + { pixels = 0.0; const std::vector& samples = file.time; if (samples.size() < 2 || m_plotRect.Width <= 1.0f) @@ -2999,8 +3026,10 @@ namespace jdi bool changed = false; - // 鼠标压在实验性功能面板上时不再触发信号悬停提示(面板本来就盖住波形)。 + // 鼠标压在实验性功能面板上时不再触发信号悬停提示(面板本来就盖住波形), + // 但该面板所属的网格仍然要高亮(与鼠标直接停在网格上一致)。 bool overEdgePanel = false; + int panelLane = -1; for (int pass = 0; pass < 2 && !overEdgePanel; ++pass) { const bool centerAxis = (pass == 1); @@ -3011,12 +3040,14 @@ namespace jdi UiPointInRect(panel, static_cast(x), static_cast(y))) { overEdgePanel = true; + panelLane = index; break; } } } - const int lane = LaneAtY(y); + // 悬停网格:鼠标在面板上时高亮面板所属网格(用户明确要求)。 + const int lane = overEdgePanel ? panelLane : LaneAtY(y); if (lane != m_hoverLane) { m_hoverLane = lane; @@ -3037,15 +3068,16 @@ namespace jdi changed = true; } - // 常规模式下精确检测鼠标是否贴近某条信号曲线,用于给出可拖拽提示。 + // 精确检测鼠标是否贴近某条信号曲线,用于给出可拖拽提示。 // 图例优先于曲线:图例条目是可点选目标(与左侧信号列表一致), // 但悬停图例时**只做半高亮提示**,不显示曲线拖拽提示(也不触发光晕)。 + // **图例悬停在选区放大模式下同样生效**(该模式只改变拖拽语义)。 SignalRef hoverSignal; SignalRef hoverLegend; - if (Tab().ZoomMode() == PlotZoomMode::NONE && !m_cursorHover && !overEdgePanel) + if (!m_cursorHover && !overEdgePanel) { hoverLegend = LegendAtPoint(x, y); - if (!hoverLegend.IsValid()) + if (!hoverLegend.IsValid() && Tab().ZoomMode() == PlotZoomMode::NONE) { hoverSignal = HitTestSignal(x, y); } @@ -3235,6 +3267,19 @@ namespace jdi return false; } + // 点击图例条目等价于在左侧列表中选中该信号(样式区随之切换目标)。 + // **必须在选区放大判定之前**:选区放大模式只改变拖拽语义, + // 不应影响图例的悬停与点选(否则用户在选区放大模式下无法切换信号)。 + { + const SignalRef legendHit = LegendAtPoint(x, y); + if (legendHit.IsValid()) + { + SelectSignal(legendHit); + m_dragMode = PlotDragMode::NONE; + return true; + } + } + const PlotZoomMode mode = Tab().ZoomMode(); if (mode == PlotZoomMode::SELECT) { @@ -3246,18 +3291,6 @@ namespace jdi return true; } - // 常规模式下:点击图例条目等价于在左侧列表中选中该信号(样式区随之切换目标)。 - if (mode == PlotZoomMode::NONE) - { - const SignalRef legendHit = LegendAtPoint(x, y); - if (legendHit.IsValid()) - { - SelectSignal(legendHit); - m_dragMode = PlotDragMode::NONE; - return true; - } - } - // 常规模式下:先检测是否落在信号曲线上;只有命中提示时才允许拖拽该信号。 const SignalRef hit = HitTestSignal(x, y); if (hit.IsValid()) @@ -3276,9 +3309,47 @@ namespace jdi return true; } - bool PlotView::OnLButtonUp(int x, int y) + bool PlotView::OnMButtonDown(int x, int y) { - bool changed = (m_dragMode != PlotDragMode::NONE); + if (!HasDrawableContent()) + { + return false; + } + + // 中键:无论当前是否处于选区放大模式,都直接进入自由平移 + // (不改变选区放大模式本身,松手后仍可继续框选)。 + m_dragStartX = x; + m_dragStartY = y; + m_dragLastX = x; + m_dragLastY = y; + m_dragMoved = false; + m_rubberRect = gd::RectF(); + m_rubberAxis = PlotRubberAxis::UNDECIDED; + m_cursorGrabOffset = 0; + m_edgeDragLane = -1; + m_edgePanelDragLane = -1; + m_edgePanelDragCenter = false; + SetHint(std::wstring()); + m_dragLane = LaneAtY(y); + m_dragMode = PlotDragMode::PAN; + return true; + } + + bool PlotView::OnMButtonUp() + { + const bool changed = (m_dragMode == PlotDragMode::PAN); + if (changed) + { + m_dragMode = PlotDragMode::NONE; + m_dragLane = -1; + m_dragMoved = false; + ReleaseHint(); + } + return changed; + } + + bool PlotView::OnLButtonUp(int x, int y) + { bool changed = (m_dragMode != PlotDragMode::NONE); switch (m_dragMode) { @@ -5153,14 +5224,56 @@ namespace jdi const float centerY = ValueToY(centerValue, lane); if (centerY >= clipTop - 2.0f && centerY <= clipBottom + 2.0f) { - g.FillEllipse(&ring, pinX - radius - 1.2f * m_scale, - centerY - radius - 1.2f * m_scale, - (radius + 1.2f * m_scale) * 2.0f, - (radius + 1.2f * m_scale) * 2.0f); - g.FillEllipse(&fill, pinX - radius, centerY - radius, radius * 2.0f, - radius * 2.0f); + // 中心轴数据点与常规数据点一样:在可视区内是圆点, + // **光标在可视区之外(贴边显示)时同样改为三角**指示方向。 + const std::uint32_t centerColor = CursorColor(1); + gd::SolidBrush centerFill(ToGdiColor(centerColor)); + if (pinned) + { + const float arm = 5.0f * m_scale; + const bool offLeft = (rawX < pinX); + const float baseX = offLeft ? (pinX - arm) : (pinX + arm); + gd::PointF tri[3]; + tri[0] = gd::PointF(baseX, centerY - arm); + tri[1] = gd::PointF(baseX, centerY + arm); + tri[2] = gd::PointF(pinX, centerY); + g.FillPolygon(&ring, tri, 3); + gd::PointF inner[3]; + inner[0] = gd::PointF(baseX + (offLeft ? 1.4f : -1.4f) * m_scale, + centerY - arm * 0.72f); + inner[1] = gd::PointF(baseX + (offLeft ? 1.4f : -1.4f) * m_scale, + centerY + arm * 0.72f); + inner[2] = gd::PointF(pinX - (offLeft ? -1.4f : 1.4f) * m_scale, centerY); + g.FillPolygon(¢erFill, inner, 3); + } + else + { + g.FillEllipse(&ring, pinX - radius - 1.2f * m_scale, + centerY - radius - 1.2f * m_scale, + (radius + 1.2f * m_scale) * 2.0f, + (radius + 1.2f * m_scale) * 2.0f); + g.FillEllipse(¢erFill, pinX - radius, centerY - radius, + radius * 2.0f, radius * 2.0f); + } + + // 取值文本:数值在前,尾缀「(中心轴)」;双光标的右侧光标还要 + // 附上增量,形如「(+0.0008 中心轴)」(参考常规信号的增量显示)。 + std::wstring centerText = FormatValue(centerValue); + centerText += L"("; + if (showDelta) + { + double otherCenter = 0.0; + if (CenterValueAt(*resolved.file, *resolved.data, smooth, otherTime, + otherCenter)) + { + const double delta = centerValue - otherCenter; + centerText += (delta < 0.0) ? L"-" : L"+"; + centerText += FormatValue(std::fabs(delta)); + centerText += L" "; + } + } + centerText += L"中心轴)"; - const std::wstring centerText = L"中 " + FormatValue(centerValue); const float centerTextWidth = UiTextWidth(g, centerText, m_fonts->monoSmall) + 10.0f * m_scale; float centerLeft = pinX + radius + 4.0f * m_scale; @@ -5179,11 +5292,13 @@ namespace jdi std::min(centerTop, clipBottom - textHeight - 1.0f * m_scale)); gd::RectF centerRect(centerLeft, centerTop, centerTextWidth, textHeight); PlaceCursorLabel(centerRect, centerTop, textHeight, clipTop, clipBottom); + // 颜色样式与光标一致(同底色/同描边/同文字配色)。 + const bool centerDark = IsDarkColor(centerColor); UiFillRoundRect(g, centerRect, 3.0f * m_scale, - WithAlpha(signalColor, 0xB8)); - DrawLabelOutline(g, centerRect, 3.0f * m_scale, dark); + WithAlpha(centerColor, 0xE0)); + DrawLabelOutline(g, centerRect, 3.0f * m_scale, centerDark); UiText(g, centerText, m_fonts->monoSmall, centerRect, - dark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER); + centerDark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER); } } } @@ -5512,11 +5627,9 @@ namespace jdi return; } - // 中心平均值曲线与原信号同色,若直接用原色会被波形洩没; - // 根据背景明暗把信号色向黑/白方向拉远,保证叠加后清晰可辨。 - const std::uint32_t derived = - IsDarkColor(m_theme->plotBg) ? MixColor(StyleColor(*resolved.style), 0xFFFFFFFFu, 0.62f) - : MixColor(StyleColor(*resolved.style), 0xFF000000u, 0.58f); + // 中心平均值曲线使用与光标(「边沿计数」横向光标、中心轴取值胶囊)一致的颜色样式: + // 即光标的强调色。曲线与其取值标注、光标本身因此形成统一的视觉语言。 + const std::uint32_t derived = CursorColor(1); gd::Pen pen(ToGdiColor(derived), std::max(1.8f, resolved.style->lineWidth + 0.8f) * m_scale); pen.SetLineJoin(gd::LineJoinRound); pen.SetStartCap(gd::LineCapRound); diff --git a/Utilities/Inc/DataModel.hpp b/Utilities/Inc/DataModel.hpp index 8f4d420..b014e5f 100644 --- a/Utilities/Inc/DataModel.hpp +++ b/Utilities/Inc/DataModel.hpp @@ -567,6 +567,18 @@ namespace jdi */ int MoveSignalToLane(const DataSet& dataSet, const SignalRef& ref, int laneIndex); + /** + * @brief 「拆分当前网格」:把指定网格内的信号按个拆分成独立网格。 + * + * 新网格插入在原网格的**下方依次排列**(原位置 + 1 起),原本在其下方的网格 + * 顺次后移。由于网格编号(GX)完全由位置推导,拆分后所有引用该编号的地方 + * (左侧列表徽标、图例徽标、面板标题、右键菜单标题)会自动保持一致。 + * + * @param [IN] laneIndex 待拆分的网格下标。 + * @return 拆分后该网格内第一条信号所在的网格下标;无法拆分(不存在或只有 1 条信号)返回 -1。 + */ + int SplitLane(int laneIndex); + /** * @brief 查询某文件的分组是否处于折叠状态。 * @param [IN] fileIndex 文件下标。 diff --git a/Utilities/Src/DataModel.cpp b/Utilities/Src/DataModel.cpp index 746d620..63100f8 100644 --- a/Utilities/Src/DataModel.cpp +++ b/Utilities/Src/DataModel.cpp @@ -210,12 +210,50 @@ namespace jdi 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) { // 每条可见信号独占一个网格:网格数量严格跟随可见信号数量。 - lanes.clear(); - lanes.reserve(visibleRefs.size()); + // **顺序按"原有网格位置"优先**:已经分配过网格的信号保持其相对次序, + // 新勾选(此前没有网格)的信号统一追加到最下方。 + // 若直接按 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; @@ -1169,9 +1207,64 @@ namespace jdi return LaneOfSignal(ref); } - bool TabConfig::IsCollapsed(int fileIndex) const + int TabConfig::SplitLane(int laneIndex) { - if (fileIndex < 0 || static_cast(fileIndex) >= m_collapsed.size()) + 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.edgeCenterSmooth = source.edgeCenterSmooth; + lane.edgeCenterPanelOpen = source.edgeCenterPanelOpen; + lane.edgeCenterPanelX = source.edgeCenterPanelX; + lane.edgeCenterPanelY = source.edgeCenterPanelY; + 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; + } + + bool TabConfig::IsCollapsed(int fileIndex) const + { if (fileIndex < 0 || static_cast(fileIndex) >= m_collapsed.size()) { return false; }