网格管理增强:新信号置底、拆分当前网格、中键自由平移、选区模式图例可用、面板悬停高亮、中心轴样式与越界三角

- 新勾选的信号所在网格固定出现在**最下方**:
  原先 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 无警告无错误。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-09 15:21:10 +08:00
1 parent 5182bd8aa3
commit 82a29362eb
6 files changed
+342 -44

No files matched your search

@@ -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; /**< 后备缓冲设备上下文。 */
+51 -3
View File
@@ -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);
+32 -1
View File
@@ -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 轴自适应”)。
*
+149 -36
View File
@@ -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<int>(m_tab->Lanes().size()))
{
return 0;
}
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].signals.size();
}
bool PlotView::SampleSpacingPixels(const DataFile& file, double time, double& pixels) const
{ pixels = 0.0;
const std::vector<double>& 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<float>(x), static_cast<float>(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(&centerFill, 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(&centerFill, 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);