#include "PlotView.hpp" #include #include #include #include "Utf8Util.hpp" namespace jdi { namespace { const int kTimeTickTarget = 8; /**< 时间轴期望刻度数。 */ const int kYTickTarget = 4; /**< 幅值轴期望刻度数。 */ const float kScrollBarThickness = 9.0f; /**< 滚动条宽度(逻辑像素)。 */ const float kLanePadding = 8.0f; /**< 网格内边距。 */ const float kAxisLabelGap = 10.0f; /**< 轴上文字与轴线的间距。 */ const size_t kMaxPolylinePoints = 20000;/**< 单条折线最大顶点数。 */ const size_t kMaxScatterPoints = 2500; /**< 单条散点最大绘制点数。 */ const float kMinScatterRadius = 1.6f; /**< 散点最小半径(逻辑像素)。 */ const float kMaxScatterRadius = 4.0f; /**< 散点最大半径(逻辑像素)。 */ const double kMinTimeSpan = 1.0e-12; /**< 允许的最小时间跨度。 */ /** * @brief 将数值限制到合法区间。 * @param [IN] value 输入值。 * @param [IN] low 下限。 * @param [IN] high 上限。 * @return 限制后的数值。 */ double ClampDouble(double value, double low, double high) { if (value < low) { return low; } if (value > high) { return high; } return value; } } PlotView::PlotView() = default; void PlotView::Setup(const UiFonts* fonts, const Theme* theme) { m_fonts = fonts; m_theme = theme; } void PlotView::SetDocument(SignalSet* document) { m_document = document; m_laneScroll = 0.0f; m_cursorVisible = false; m_cursorPinned = false; m_hoverLane = -1; if (m_document != nullptr && m_document->IsLoaded()) { FitTime(); } } void PlotView::SetScale(float scale) { m_scale = (scale > 0.1f) ? scale : 1.0f; } void PlotView::Layout(const gd::RectF& area) { m_area = area; m_yAxisWidth = 74.0f * m_scale; m_timeAxisHeight = 32.0f * m_scale; m_laneGap = 6.0f * m_scale; m_minLaneHeight = 46.0f * m_scale; const float topPadding = 6.0f * m_scale; const float availHeight = std::max(0.0f, area.Height - m_timeAxisHeight - topPadding); // 预估是否需要纵向滚动条:仅取决于网格数量与最小网格高度。 std::size_t laneCount = 0; if (m_document != nullptr && m_document->IsLoaded()) { laneCount = m_document->Lanes().size(); } if (laneCount > 0) { const float needed = static_cast(laneCount) * m_minLaneHeight + m_laneGap * static_cast(laneCount - 1); m_scrollBarVisible = (needed > availHeight); } else { m_scrollBarVisible = false; } const float rightReserve = m_scrollBarVisible ? (kScrollBarThickness * m_scale + 6.0f * m_scale) : (6.0f * m_scale); const float plotWidth = std::max(0.0f, area.Width - m_yAxisWidth - rightReserve); m_plotRect = gd::RectF(area.X + m_yAxisWidth, area.Y + topPadding, plotWidth, availHeight); m_timeAxisRect = gd::RectF(m_plotRect.X, m_plotRect.Y + m_plotRect.Height, m_plotRect.Width, m_timeAxisHeight); m_scrollBarRect = gd::RectF(area.X + area.Width - kScrollBarThickness * m_scale - 3.0f * m_scale, m_plotRect.Y, kScrollBarThickness * m_scale, m_plotRect.Height); BuildLaneVisuals(); } float PlotView::LaneContentHeight() const { return m_laneContentHeight; } void PlotView::ClampLaneScroll() { const float maxOffset = std::max(0.0f, m_laneContentHeight - m_plotRect.Height); m_laneScroll = ClampDouble(m_laneScroll, 0.0, maxOffset); } void PlotView::BuildLaneVisuals() { m_lanes.clear(); if (m_document == nullptr || !m_document->IsLoaded()) { m_laneContentHeight = 0.0f; return; } const std::vector& lanes = m_document->Lanes(); const std::size_t count = lanes.size(); if (count == 0) { m_laneContentHeight = 0.0f; return; } const float gap = m_laneGap * m_scale; float laneHeight = (m_plotRect.Height - gap * static_cast(count - 1)) / static_cast(count); if (laneHeight < m_minLaneHeight * m_scale) { laneHeight = m_minLaneHeight * m_scale; } m_laneContentHeight = laneHeight * static_cast(count) + gap * static_cast(count - 1); ClampLaneScroll(); m_lanes.resize(count); for (std::size_t index = 0; index < count; ++index) { LaneVisual& visual = m_lanes[index]; visual.rect = gd::RectF(m_plotRect.X, m_plotRect.Y + static_cast(index) * (laneHeight + gap) - m_laneScroll, m_plotRect.Width, laneHeight); m_document->ComputeLaneRange(static_cast(index), visual.yMin, visual.yMax); if (!(visual.yMax > visual.yMin)) { visual.yMin = 0.0; visual.yMax = 1.0; } visual.empty = true; for (const int signalIndex : lanes[index].signalIndices) { if (signalIndex >= 0 && signalIndex < static_cast(m_document->Signals().size()) && m_document->Signals()[static_cast(signalIndex)].visible) { visual.empty = false; break; } } } } float PlotView::TimeToX(double time) const { const double span = m_viewMax - m_viewMin; if (!(span > 0.0) || m_plotRect.Width <= 0.0f) { return m_plotRect.X; } return m_plotRect.X + static_cast((time - m_viewMin) / span * m_plotRect.Width); } double PlotView::XToTime(float x) const { if (m_plotRect.Width <= 0.0f) { return m_viewMin; } const double ratio = static_cast(x - m_plotRect.X) / static_cast(m_plotRect.Width); return m_viewMin + ratio * (m_viewMax - m_viewMin); } float PlotView::ValueToY(double value, const LaneVisual& lane) const { const double span = lane.yMax - lane.yMin; if (!(span > 0.0)) { return lane.rect.Y + lane.rect.Height * 0.5f; } double ratio = (value - lane.yMin) / span; ratio = ClampDouble(ratio, -50.0, 50.0); return lane.rect.Y + lane.rect.Height * static_cast(1.0 - ratio); } double PlotView::YToValue(float y, const LaneVisual& lane) const { if (lane.rect.Height <= 0.0f) { return lane.yMin; } const double ratio = 1.0 - static_cast(y - lane.rect.Y) / static_cast(lane.rect.Height); return lane.yMin + ratio * (lane.yMax - lane.yMin); } int PlotView::LaneAtY(int y) const { const float fy = static_cast(y); for (std::size_t index = 0; index < m_lanes.size(); ++index) { const gd::RectF& rect = m_lanes[index].rect; if (fy >= rect.Y && fy <= rect.Y + rect.Height) { return static_cast(index); } } return -1; } void PlotView::ClampTimeView() { if (m_document == nullptr || !m_document->IsLoaded()) { return; } double span = m_viewMax - m_viewMin; if (!(span > 0.0)) { span = kMinTimeSpan; } const double dataSpan = std::max(m_document->MaxTime() - m_document->MinTime(), kMinTimeSpan); const double pad = span * 2.0; const double minAllowed = m_document->MinTime() - pad; const double maxAllowed = m_document->MaxTime() + pad; if (m_viewMin < minAllowed) { m_viewMin = minAllowed; m_viewMax = m_viewMin + span; } if (m_viewMax > maxAllowed) { m_viewMax = maxAllowed; m_viewMin = m_viewMax - span; } (void)dataSpan; } void PlotView::ZoomTimeAt(double factor, double anchorTime) { if (m_document == nullptr || !m_document->IsLoaded() || !(factor > 0.0)) { return; } const double dataSpan = std::max(m_document->MaxTime() - m_document->MinTime(), kMinTimeSpan); const double minSpan = dataSpan * 1.0e-9; const double maxSpan = dataSpan * 100.0; const double span = m_viewMax - m_viewMin; double newSpan = span / factor; newSpan = ClampDouble(newSpan, minSpan, maxSpan); const double ratio = (span > 0.0) ? (anchorTime - m_viewMin) / span : 0.5; m_viewMin = anchorTime - ratio * newSpan; m_viewMax = m_viewMin + newSpan; ClampTimeView(); } void PlotView::ZoomTimeStep(double factor) { const double anchor = (m_viewMin + m_viewMax) * 0.5; ZoomTimeAt(factor, anchor); } void PlotView::PanTimeByFraction(double fraction) { const double span = m_viewMax - m_viewMin; const double delta = span * fraction; m_viewMin += delta; m_viewMax += delta; ClampTimeView(); } void PlotView::ScrollLanesByFraction(double fraction) { m_laneScroll += static_cast(fraction * static_cast(m_laneContentHeight)); ClampLaneScroll(); } void PlotView::FitTime() { if (m_document == nullptr || !m_document->IsLoaded()) { m_viewMin = 0.0; m_viewMax = 1.0; return; } m_viewMin = m_document->MinTime(); m_viewMax = m_document->MaxTime(); if (!(m_viewMax - m_viewMin > 0.0)) { const double center = m_viewMin; const double padding = std::max(1.0e-6, std::fabs(center) * 0.01); m_viewMin = center - padding; m_viewMax = center + padding; } } void PlotView::FitAll() { FitTime(); m_laneScroll = 0.0f; if (m_document != nullptr) { for (Lane& lane : m_document->Lanes()) { lane.autoRange = true; } } } void PlotView::ClearCursor() { m_cursorVisible = false; m_cursorPinned = false; } bool PlotView::HitTest(int x, int y) const { return UiPointInRect(m_area, static_cast(x), static_cast(y)); } HCURSOR PlotView::QueryCursor(int x, int y) const { switch (m_dragMode) { case PlotDragMode::PAN_TIME: case PlotDragMode::BOX_ZOOM: return LoadCursorW(nullptr, IDC_SIZEALL); case PlotDragMode::ZOOM_Y: case PlotDragMode::SCROLL_LANES: return LoadCursorW(nullptr, IDC_SIZENS); default: break; } if (m_scrollBarVisible && UiPointInRect(m_scrollBarRect, static_cast(x), static_cast(y))) { return LoadCursorW(nullptr, IDC_SIZENS); } if (static_cast(x) < m_plotRect.X && UiPointInRect(m_area, static_cast(x), static_cast(y))) { if (LaneAtY(y) >= 0) { return LoadCursorW(nullptr, IDC_SIZENS); } } if (UiPointInRect(m_plotRect, static_cast(x), static_cast(y))) { return LoadCursorW(nullptr, IDC_CROSS); } return nullptr; } bool PlotView::OnMouseMove(int x, int y) { if (m_document == nullptr || !m_document->IsLoaded()) { return false; } const float fx = static_cast(x); const float fy = static_cast(y); switch (m_dragMode) { case PlotDragMode::PAN_TIME: { if (m_plotRect.Width <= 0.0f) { return false; } const double span = m_viewMax - m_viewMin; const double delta = -static_cast(x - m_dragLastX) / static_cast(m_plotRect.Width) * span; m_viewMin += delta; m_viewMax += delta; ClampTimeView(); m_dragLastX = x; m_dragLastY = y; if (std::abs(x - m_dragStartX) > 3 || std::abs(y - m_dragStartY) > 3) { m_dragMoved = true; } return true; } case PlotDragMode::ZOOM_Y: { if (m_dragLane < 0 || m_dragLane >= static_cast(m_lanes.size())) { return false; } const gd::RectF& rect = m_lanes[static_cast(m_dragLane)].rect; if (rect.Height <= 0.0f) { return false; } // 上拖放大、下拖缩小,并保持按下点处的幅值不动。 const double travel = static_cast(m_dragStartY - y) / static_cast(rect.Height); const double factor = std::exp(travel * 2.0); const double span = m_dragStartYMax - m_dragStartYMin; if (!(span > 0.0)) { return false; } double newSpan = span / factor; const double dataSpan = std::max(std::fabs(m_dragStartYMax), std::fabs(m_dragStartYMin)); const double minSpan = std::max(dataSpan * 1.0e-9, 1.0e-12); newSpan = ClampDouble(newSpan, minSpan, dataSpan * 100.0 + 1.0); Lane& lane = m_document->Lanes()[static_cast(m_dragLane)]; lane.autoRange = false; lane.yMin = m_dragAnchorValue - m_dragAnchorRatio * newSpan; lane.yMax = lane.yMin + newSpan; if (std::abs(y - m_dragStartY) > 2) { m_dragMoved = true; } return true; } case PlotDragMode::BOX_ZOOM: { const float left = static_cast(std::min(x, m_dragStartX)); const float top = static_cast(std::min(y, m_dragStartY)); m_boxRect = gd::RectF(left, top, static_cast(std::abs(x - m_dragStartX)), static_cast(std::abs(y - m_dragStartY))); m_dragMoved = true; return true; } case PlotDragMode::SCROLL_LANES: { if (m_plotRect.Height <= 0.0f || m_laneContentHeight <= m_plotRect.Height) { return false; } const double travel = static_cast(y - m_dragLastY) / static_cast(m_plotRect.Height); m_laneScroll += static_cast(travel * m_laneContentHeight); ClampLaneScroll(); m_dragLastY = y; m_dragMoved = true; return true; } case PlotDragMode::NONE: default: break; } bool changed = false; const int lane = LaneAtY(y); if (lane != m_hoverLane) { m_hoverLane = lane; changed = true; } const bool insidePlot = UiPointInRect(m_plotRect, fx, fy); if (insidePlot && !m_cursorPinned) { const double time = XToTime(fx); if (!m_cursorVisible || std::fabs(time - m_cursorTime) > 0.0) { m_cursorTime = time; m_cursorVisible = true; changed = true; } } return changed; } bool PlotView::OnLButtonDown(int x, int y) { if (m_document == nullptr || !m_document->IsLoaded()) { return false; } m_dragStartX = x; m_dragStartY = y; m_dragLastX = x; m_dragLastY = y; m_dragMoved = false; m_dragStartViewMin = m_viewMin; m_dragStartViewMax = m_viewMax; if (m_scrollBarVisible && UiPointInRect(m_scrollBarRect, static_cast(x), static_cast(y))) { m_dragMode = PlotDragMode::SCROLL_LANES; return true; } const int laneIndex = LaneAtY(y); if (laneIndex < 0) { m_dragMode = PlotDragMode::NONE; return false; } if (static_cast(x) < m_plotRect.X) { m_dragMode = PlotDragMode::ZOOM_Y; m_dragLane = laneIndex; const LaneVisual& visual = m_lanes[static_cast(laneIndex)]; Lane& lane = m_document->Lanes()[static_cast(laneIndex)]; lane.autoRange = false; lane.yMin = visual.yMin; lane.yMax = visual.yMax; m_dragStartYMin = visual.yMin; m_dragStartYMax = visual.yMax; m_dragAnchorValue = YToValue(static_cast(y), visual); const double span = visual.yMax - visual.yMin; m_dragAnchorRatio = (span > 0.0) ? (visual.yMax - m_dragAnchorValue) / span : 0.5; return true; } if (UiPointInRect(m_plotRect, static_cast(x), static_cast(y))) { m_dragMode = PlotDragMode::PAN_TIME; return true; } m_dragMode = PlotDragMode::NONE; return false; } bool PlotView::OnLButtonUp(int x, int y) { bool changed = (m_dragMode != PlotDragMode::NONE); if (m_dragMode == PlotDragMode::PAN_TIME && !m_dragMoved && UiPointInRect(m_plotRect, static_cast(x), static_cast(y))) { const double time = XToTime(static_cast(x)); if (m_cursorPinned && std::fabs(time - m_cursorTime) < 1.0e-12) { m_cursorPinned = false; m_cursorVisible = false; } else { m_cursorTime = time; m_cursorVisible = true; m_cursorPinned = true; } changed = true; } m_dragMode = PlotDragMode::NONE; m_dragLane = -1; m_dragMoved = false; return changed; } bool PlotView::OnRButtonDown(int x, int y) { if (m_document == nullptr || !m_document->IsLoaded()) { return false; } if (!UiPointInRect(m_plotRect, static_cast(x), static_cast(y))) { return false; } m_dragMode = PlotDragMode::BOX_ZOOM; m_dragStartX = x; m_dragStartY = y; m_dragLastX = x; m_dragLastY = y; m_dragMoved = false; m_dragLane = LaneAtY(y); m_boxRect = gd::RectF(static_cast(x), static_cast(y), 0.0f, 0.0f); return true; } bool PlotView::OnRButtonUp(int x, int y) { if (m_dragMode != PlotDragMode::BOX_ZOOM) { return false; } const bool moved = m_dragMoved; const int laneIndex = m_dragLane; const gd::RectF box = m_boxRect; m_dragMode = PlotDragMode::NONE; m_dragMoved = false; m_dragLane = -1; if (!moved || box.Width < 6.0f * m_scale) { // 未形成有效框:清除已固定光标。 if (!m_cursorPinned) { m_cursorVisible = false; } return true; } double timeA = XToTime(box.X); double timeB = XToTime(box.X + box.Width); if (timeB < timeA) { std::swap(timeA, timeB); } if (timeB - timeA > 0.0) { m_viewMin = timeA; m_viewMax = timeB; ClampTimeView(); } if (laneIndex >= 0 && laneIndex < static_cast(m_lanes.size()) && box.Height > 12.0f * m_scale) { const LaneVisual& visual = m_lanes[static_cast(laneIndex)]; const double valueTop = YToValue(box.Y, visual); const double valueBottom = YToValue(box.Y + box.Height, visual); if (std::fabs(valueTop - valueBottom) > 0.0) { Lane& lane = m_document->Lanes()[static_cast(laneIndex)]; lane.autoRange = false; lane.yMin = std::min(valueTop, valueBottom); lane.yMax = std::max(valueTop, valueBottom); } } (void)y; return true; } bool PlotView::OnMouseWheel(int x, int y, int delta, bool ctrl, bool shift, bool alt) { if (m_document == nullptr || !m_document->IsLoaded() || delta == 0) { return false; } const double notches = static_cast(delta) / 120.0; if (alt) { m_laneScroll -= static_cast(notches * 48.0 * m_scale); ClampLaneScroll(); return true; } if (shift) { const double span = m_viewMax - m_viewMin; const double sweep = -notches * span * 0.15; m_viewMin += sweep; m_viewMax += sweep; ClampTimeView(); return true; } if (ctrl) { int laneIndex = LaneAtY(y); if (laneIndex < 0) { laneIndex = m_hoverLane; } if (laneIndex < 0 || laneIndex >= static_cast(m_lanes.size())) { return false; } const LaneVisual& visual = m_lanes[static_cast(laneIndex)]; Lane& lane = m_document->Lanes()[static_cast(laneIndex)]; LaneVisual mutableVisual = visual; if (lane.autoRange) { lane.autoRange = false; } lane.yMin = visual.yMin; lane.yMax = visual.yMax; const double anchorValue = YToValue(static_cast(y), mutableVisual); const double span = visual.yMax - visual.yMin; if (!(span > 0.0)) { return false; } const double factor = std::pow(1.25, notches); double newSpan = span / factor; const double scaleRef = std::max(std::fabs(visual.yMax), std::fabs(visual.yMin)); const double minSpan = std::max(scaleRef * 1.0e-9, 1.0e-12); newSpan = ClampDouble(newSpan, minSpan, scaleRef * 100.0 + 1.0); const double ratio = (visual.yMax - anchorValue) / span; lane.yMin = anchorValue - ratio * newSpan; lane.yMax = lane.yMin + newSpan; return true; } const double anchorTime = XToTime(static_cast(x)); ZoomTimeAt(std::pow(1.25, notches), anchorTime); return true; } bool PlotView::OnDoubleClick(int x, int y) { if (!UiPointInRect(m_area, static_cast(x), static_cast(y))) { return false; } FitAll(); return true; } bool PlotView::OnMouseLeave() { bool changed = false; if (m_hoverLane != -1) { m_hoverLane = -1; changed = true; } if (!m_cursorPinned && m_cursorVisible) { m_cursorVisible = false; changed = true; } return changed; } void PlotView::BuildPolyline(const Signal& signal, const LaneVisual& lane, bool stepMode, std::vector& points, std::vector& breaks) const { points.clear(); breaks.clear(); if (m_document == nullptr) { return; } const std::vector& time = m_document->Time(); const std::vector& values = signal.values; const std::size_t count = std::min(time.size(), values.size()); if (count == 0 || m_plotRect.Width <= 1.0f) { return; } std::size_t begin = 0; std::size_t end = count; if (m_document->TimeMonotonic()) { const std::vector::const_iterator first = std::lower_bound(time.begin(), time.end(), m_viewMin); begin = static_cast(first - time.begin()); if (begin > 0) { --begin; } const std::vector::const_iterator last = std::lower_bound(time.begin(), time.end(), m_viewMax); end = static_cast(last - time.begin()); if (end < count) { ++end; } } if (end <= begin) { return; } std::size_t stride = 1; const std::size_t visibleCount = end - begin; if (visibleCount > kMaxPolylinePoints) { stride = (visibleCount + kMaxPolylinePoints - 1) / kMaxPolylinePoints; } const float limitTop = lane.rect.Y - lane.rect.Height * 4.0f; const float limitBottom = lane.rect.Y + lane.rect.Height * 5.0f; bool inSegment = false; double previousY = 0.0; for (std::size_t index = begin; index < end; index += stride) { const double t = time[index]; const double value = values[index]; if (!std::isfinite(t) || !std::isfinite(value)) { inSegment = false; continue; } const float x = TimeToX(t); float y = ValueToY(value, lane); y = ClampDouble(y, limitTop, limitBottom); if (!inSegment) { breaks.push_back(points.size()); points.push_back(gd::PointF(x, y)); inSegment = true; } else if (stepMode) { points.push_back(gd::PointF(x, static_cast(previousY))); points.push_back(gd::PointF(x, y)); } else { points.push_back(gd::PointF(x, y)); } previousY = y; } } void PlotView::Paint(gd::Graphics& g) { if (m_theme == nullptr || m_fonts == nullptr) { return; } UiFillRect(g, m_area, m_theme->plotBg); if (m_document == nullptr || !m_document->IsLoaded()) { UiText(g, L"打开 CSV 文件以查看仿真数据波形", m_fonts->ui, m_area, m_theme->textMuted, TextAlign::CENTER); return; } BuildLaneVisuals(); const double span = m_viewMax - m_viewMin; const double timeStep = NiceStep((span > 0.0) ? span : 1.0, kTimeTickTarget); std::vector timeTicks; BuildTicks(m_viewMin, m_viewMax, timeStep, timeTicks); // 波形区域边框。 UiFillRect(g, m_plotRect, m_theme->cardBg); for (std::size_t index = 0; index < m_lanes.size(); ++index) { PaintLane(g, static_cast(index), m_lanes[index], timeTicks, timeStep); } PaintTimeAxis(g, timeTicks, timeStep); PaintCursor(g, m_lanes); if (m_dragMode == PlotDragMode::BOX_ZOOM && m_dragMoved) { UiFillRect(g, m_boxRect, WithAlpha(m_theme->accent, 0x2A)); UiDrawRoundRect(g, m_boxRect, 2.0f * m_scale, m_theme->accent, 1.0f); } PaintScrollBar(g); // 绘图区外框。 gd::Pen borderPen(ToGdiColor(m_theme->border), 1.0f); g.DrawRectangle(&borderPen, m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height); } void PlotView::PaintLane(gd::Graphics& g, int laneIndex, const LaneVisual& lane, const std::vector& timeTicks, double timeStep) { const int saveState = g.Save(); g.SetClip(gd::RectF(m_area.X, lane.rect.Y, m_area.Width, lane.rect.Height)); // 悬停高亮。 if (laneIndex == m_hoverLane && m_dragMode == PlotDragMode::NONE) { UiFillRect(g, lane.rect, MixColor(m_theme->cardBg, m_theme->accent, 0.035)); } // 幅值网格线与标签。 const double yStep = NiceStep(lane.yMax - lane.yMin, kYTickTarget); std::vector yTicks; BuildTicks(lane.yMin, lane.yMax, yStep, yTicks); const bool showLabels = (lane.rect.Height >= 34.0f * m_scale); float lastLabelY = -1.0e9f; for (const double tick : yTicks) { const float y = ValueToY(tick, lane); if (y < lane.rect.Y - 1.0f || y > lane.rect.Y + lane.rect.Height + 1.0f) { continue; } const bool zeroLine = (tick == 0.0); UiDrawLine(g, lane.rect.X, y, lane.rect.X + lane.rect.Width, y, zeroLine ? m_theme->gridStrong : m_theme->gridWeak, 1.0f); if (!showLabels || std::fabs(y - lastLabelY) < 15.0f * m_scale) { continue; } lastLabelY = y; const gd::RectF labelRect(m_area.X, y - 9.0f * m_scale, m_yAxisWidth - kAxisLabelGap * m_scale, 18.0f * m_scale); UiText(g, FormatTick(tick, yStep), m_fonts->monoSmall, labelRect, zeroLine ? m_theme->text : m_theme->textMuted, TextAlign::RIGHT); } // 时间网格线。 for (const double tick : timeTicks) { const float x = TimeToX(tick); if (x < lane.rect.X || x > lane.rect.X + lane.rect.Width) { continue; } UiDrawLine(g, x, lane.rect.Y, x, lane.rect.Y + lane.rect.Height, m_theme->gridWeak, 1.0f); } // 波形绘制。 if (m_document->Lanes()[static_cast(laneIndex)].signalIndices.empty()) { UiText(g, L"(空网格)", m_fonts->uiSmall, lane.rect, m_theme->textMuted, TextAlign::CENTER); } else { const int waveSave = g.Save(); g.SetClip(lane.rect, gd::CombineModeIntersect); std::vector points; std::vector breaks; for (const int signalIndex : m_document->Lanes()[static_cast(laneIndex)].signalIndices) { if (signalIndex < 0 || signalIndex >= static_cast(m_document->Signals().size())) { continue; } const Signal& signal = m_document->Signals()[static_cast(signalIndex)]; if (!signal.visible) { continue; } const bool stepMode = (signal.mode == DrawMode::STEP); BuildPolyline(signal, lane, stepMode, points, breaks); const bool drawLine = (signal.mode != DrawMode::SCATTER); const bool drawScatter = (signal.mode == DrawMode::SCATTER || signal.mode == DrawMode::LINE_SCATTER); if (drawLine && !breaks.empty()) { gd::Pen pen(ToGdiColor(signal.color), signal.lineWidth * m_scale); pen.SetLineJoin(stepMode ? gd::LineJoinMiter : gd::LineJoinRound); pen.SetStartCap(gd::LineCapRound); pen.SetEndCap(gd::LineCapRound); for (std::size_t k = 0; k < breaks.size(); ++k) { const std::size_t start = breaks[k]; const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size(); if (stop <= start + 1) { continue; } g.DrawLines(&pen, points.data() + start, static_cast(stop - start)); } } if (drawScatter && !points.empty()) { const float radius = static_cast( ClampDouble(std::max(1.0f, signal.lineWidth) * (kMinScatterRadius + 0.6f), kMinScatterRadius, kMaxScatterRadius) * m_scale); const std::size_t scatterStride = std::max(1, points.size() / kMaxScatterPoints); gd::SolidBrush brush(ToGdiColor(signal.color)); for (std::size_t k = 0; k < points.size(); k += scatterStride) { g.FillEllipse(&brush, points[k].X - radius, points[k].Y - radius, radius * 2.0f, radius * 2.0f); } } } g.Restore(waveSave); } // 波形区边框与网格标题。 UiDrawLine(g, lane.rect.X, lane.rect.Y + lane.rect.Height, lane.rect.X + lane.rect.Width, lane.rect.Y + lane.rect.Height, m_theme->gridStrong, 1.0f); UiDrawLine(g, m_area.X, lane.rect.Y, m_area.X + m_area.Width, lane.rect.Y, m_theme->border, 1.0f); if (lane.rect.Height >= 24.0f * m_scale) { PaintLaneLegend(g, laneIndex, lane); if (m_cursorVisible && !lane.empty && lane.rect.Height >= 40.0f * m_scale) { PaintLaneReadout(g, laneIndex, lane); } } g.Restore(saveState); (void)timeStep; } void PlotView::PaintLaneLegend(gd::Graphics& g, int laneIndex, const LaneVisual& lane) { const std::vector& lanes = m_document->Lanes(); if (laneIndex < 0 || laneIndex >= static_cast(lanes.size())) { return; } const float top = lane.rect.Y + 4.0f * m_scale; const float height = 18.0f * m_scale; float cursorX = lane.rect.X + kLanePadding * m_scale; const float maxX = lane.rect.X + lane.rect.Width - kLanePadding * m_scale; bool anyVisible = false; int visibleCount = 0; int drawnCount = 0; // 先测量图例总宽度,再用半透明底板掠开底下的网格线,提升可读性。 float legendWidth = 0.0f; for (const int signalIndex : lanes[static_cast(laneIndex)].signalIndices) { if (signalIndex < 0 || signalIndex >= static_cast(m_document->Signals().size())) { continue; } const Signal& signal = m_document->Signals()[static_cast(signalIndex)]; if (!signal.visible) { continue; } std::wstring label = signal.name; if (!signal.unit.empty()) { label += L" [" + signal.unit + L"]"; } const float itemWidth = 8.0f * m_scale + 5.0f * m_scale + UiTextWidth(g, label, m_fonts->uiSmall) + 14.0f * m_scale; if (cursorX + legendWidth + itemWidth > maxX) { break; } legendWidth += itemWidth; } if (legendWidth > 0.0f) { UiFillRoundRect(g, gd::RectF(cursorX - 5.0f * m_scale, top - 1.0f * m_scale, legendWidth + 6.0f * m_scale, height + 2.0f * m_scale), 3.0f * m_scale, WithAlpha(m_theme->cardBg, 0xD8)); } // 绘制图例条目(与测量阶段保持一致的裁剪条件)。 float drawnWidth = 0.0f; for (const int signalIndex : lanes[static_cast(laneIndex)].signalIndices) { if (signalIndex < 0 || signalIndex >= static_cast(m_document->Signals().size())) { continue; } const Signal& signal = m_document->Signals()[static_cast(signalIndex)]; if (!signal.visible) { continue; } anyVisible = true; ++visibleCount; std::wstring label = signal.name; if (!signal.unit.empty()) { label += L" [" + signal.unit + L"]"; } const float textWidth = UiTextWidth(g, label, m_fonts->uiSmall); const float itemWidth = 8.0f * m_scale + 5.0f * m_scale + textWidth + 14.0f * m_scale; if (drawnWidth + itemWidth > legendWidth + 0.5f) { break; } drawnWidth += itemWidth; const float swatchSize = 8.0f * m_scale; UiFillRoundRect(g, gd::RectF(cursorX, top + (height - swatchSize) * 0.5f, swatchSize, swatchSize), 1.5f * m_scale, signal.color); cursorX += swatchSize + 5.0f * m_scale; UiText(g, label, m_fonts->uiSmall, gd::RectF(cursorX, top, textWidth + 2.0f, height), m_theme->text, TextAlign::LEFT); cursorX += textWidth + 14.0f * m_scale; ++drawnCount; } if (!anyVisible) { UiText(g, L"(无可见信号)", m_fonts->uiSmall, gd::RectF(lane.rect.X + kLanePadding * m_scale, top, lane.rect.Width, height), m_theme->textMuted, TextAlign::LEFT); } else if (drawnCount < visibleCount) { // 图例空间不足时提示剩余信号数量,避免误认为信号丢失。 const std::wstring moreText = L"+" + std::to_wstring(visibleCount - drawnCount); if (cursorX + 20.0f * m_scale <= maxX) { UiText(g, moreText, m_fonts->uiSmall, gd::RectF(cursorX, top, 24.0f * m_scale, height), m_theme->textMuted, TextAlign::LEFT); } } } void PlotView::PaintLaneReadout(gd::Graphics& g, int laneIndex, const LaneVisual& lane) { const std::vector& lanes = m_document->Lanes(); if (laneIndex < 0 || laneIndex >= static_cast(lanes.size())) { return; } if (m_cursorTime < m_viewMin || m_cursorTime > m_viewMax) { return; } struct ReadoutLine { std::wstring text; std::uint32_t color; float value; }; std::vector lines; for (const int signalIndex : lanes[static_cast(laneIndex)].signalIndices) { if (signalIndex < 0 || signalIndex >= static_cast(m_document->Signals().size())) { continue; } const Signal& signal = m_document->Signals()[static_cast(signalIndex)]; if (!signal.visible) { continue; } const bool interpolate = (signal.mode != DrawMode::SCATTER); double value = 0.0; if (!m_document->SampleAt(signalIndex, m_cursorTime, interpolate, value)) { continue; } ReadoutLine line; line.text = signal.name + L" = " + FormatValue(value); line.color = signal.color; line.value = ValueToY(value, lane); lines.push_back(std::move(line)); } if (lines.empty()) { return; } const float lineHeight = 17.0f * m_scale; const float maxLines = std::max(1.0f, std::floor((lane.rect.Height - 12.0f * m_scale) / lineHeight)); const std::size_t shown = std::min(lines.size(), static_cast(maxLines)); float boxWidth = 0.0f; for (std::size_t index = 0; index < shown; ++index) { boxWidth = std::max(boxWidth, UiTextWidth(g, lines[index].text, m_fonts->monoSmall)); } boxWidth += 20.0f * m_scale; const float boxHeight = static_cast(shown) * lineHeight + 8.0f * m_scale; const gd::RectF box(lane.rect.X + lane.rect.Width - kLanePadding * m_scale - boxWidth, lane.rect.Y + 4.0f * m_scale, boxWidth, boxHeight); UiFillRoundRect(g, box, 4.0f * m_scale, WithAlpha(m_theme->readoutBg, 0xEE)); UiDrawRoundRect(g, box, 4.0f * m_scale, m_theme->border, 1.0f); float textY = box.Y + 4.0f * m_scale; for (std::size_t index = 0; index < shown; ++index) { const gd::RectF lineRect(box.X + 10.0f * m_scale, textY, boxWidth - 14.0f * m_scale, lineHeight); UiText(g, lines[index].text, m_fonts->monoSmall, lineRect, m_theme->textStrong, TextAlign::LEFT); textY += lineHeight; } } void PlotView::PaintTimeAxis(gd::Graphics& g, const std::vector& timeTicks, double timeStep) { const float axisY = m_plotRect.Y + m_plotRect.Height; UiDrawLine(g, m_plotRect.X, axisY, m_plotRect.X + m_plotRect.Width, axisY, m_theme->axisLine, 1.0f); for (const double tick : timeTicks) { const float x = TimeToX(tick); if (x < m_plotRect.X - 1.0f || x > m_plotRect.X + m_plotRect.Width + 1.0f) { continue; } UiDrawLine(g, x, axisY, x, axisY + 4.0f * m_scale, m_theme->axisLine, 1.0f); const gd::RectF labelRect(x - 48.0f * m_scale, axisY + 4.0f * m_scale, 96.0f * m_scale, m_timeAxisRect.Height - 6.0f * m_scale); UiText(g, FormatTick(tick, timeStep), m_fonts->monoSmall, labelRect, m_theme->textMuted, TextAlign::CENTER); } // 时间轴列名(过长时自动省略)。 const std::wstring axisName = m_document->TimeColumnName().empty() ? std::wstring(L"时间") : m_document->TimeColumnName(); UiText(g, axisName, m_fonts->monoSmall, gd::RectF(m_area.X, axisY, m_yAxisWidth - kAxisLabelGap * m_scale, m_timeAxisRect.Height), m_theme->textMuted, TextAlign::RIGHT); } void PlotView::PaintCursor(gd::Graphics& g, const std::vector& lanes) { if (!m_cursorVisible) { return; } const float x = TimeToX(m_cursorTime); if (x < m_plotRect.X - 2.0f || x > m_plotRect.X + m_plotRect.Width + 2.0f) { return; } UiDrawDashedLine(g, x, m_plotRect.Y, x, m_plotRect.Y + m_plotRect.Height, m_theme->cursor, 1.0f, m_cursorPinned ? gd::DashStyleSolid : gd::DashStyleDash); // 波形上的取值标记。 for (std::size_t laneIndex = 0; laneIndex < lanes.size(); ++laneIndex) { const LaneVisual& lane = lanes[laneIndex]; if (lane.empty) { continue; } const std::vector& indices = m_document->Lanes()[laneIndex].signalIndices; for (const int signalIndex : indices) { if (signalIndex < 0 || signalIndex >= static_cast(m_document->Signals().size())) { continue; } const Signal& signal = m_document->Signals()[static_cast(signalIndex)]; if (!signal.visible) { continue; } double value = 0.0; if (!m_document->SampleAt(signalIndex, m_cursorTime, signal.mode != DrawMode::SCATTER, value)) { continue; } const float y = ValueToY(value, lane); if (y < lane.rect.Y - 2.0f || y > lane.rect.Y + lane.rect.Height + 2.0f) { continue; } const float radius = 3.4f * m_scale; gd::SolidBrush ring(ToGdiColor(m_theme->cardBg)); gd::SolidBrush fill(ToGdiColor(signal.color)); g.FillEllipse(&ring, x - radius - 1.2f * m_scale, y - radius - 1.2f * m_scale, (radius + 1.2f * m_scale) * 2.0f, (radius + 1.2f * m_scale) * 2.0f); g.FillEllipse(&fill, x - radius, y - radius, radius * 2.0f, radius * 2.0f); } } // 时间轴上的光标标签。 const std::wstring label = FormatValue(m_cursorTime); const float textWidth = UiTextWidth(g, label, m_fonts->monoSmall); const float pillWidth = textWidth + 14.0f * m_scale; const float pillHeight = 19.0f * m_scale; float pillX = x - pillWidth * 0.5f; pillX = std::max(m_plotRect.X, std::min(pillX, m_plotRect.X + m_plotRect.Width - pillWidth)); const gd::RectF pill(pillX, m_plotRect.Y + m_plotRect.Height + 2.0f * m_scale, pillWidth, pillHeight); UiFillRoundRect(g, pill, 3.0f * m_scale, m_theme->cursor); UiText(g, label, m_fonts->monoSmall, pill, m_theme->textOnAccent, TextAlign::CENTER); } void PlotView::PaintScrollBar(gd::Graphics& g) { if (!m_scrollBarVisible || m_laneContentHeight <= m_plotRect.Height) { return; } UiFillRoundRect(g, m_scrollBarRect, kScrollBarThickness * m_scale * 0.5f, m_theme->gridWeak); const float ratio = std::min(1.0f, m_plotRect.Height / m_laneContentHeight); const float thumbHeight = std::max(24.0f * m_scale, m_scrollBarRect.Height * ratio); const float maxScroll = m_laneContentHeight - m_plotRect.Height; const float progress = (maxScroll > 0.0f) ? (m_laneScroll / maxScroll) : 0.0f; const float thumbY = m_scrollBarRect.Y + progress * (m_scrollBarRect.Height - thumbHeight); const gd::RectF thumb(m_scrollBarRect.X, thumbY, m_scrollBarRect.Width, thumbHeight); UiFillRoundRect(g, thumb, kScrollBarThickness * m_scale * 0.5f, m_theme->borderStrong); } } // namespace jdi