fix: 修复图例改名不同步、细化网格标尺并支持标签页重排与曲线实色跟随拖拽

- 图例跟随重命名:绘图区图例文本改用信号当前显示名(Title),
  左侧列表改名后网格图例立即同步。
- 标签页重排与改名:新增拖拽排序(按住标签左右拖动并显示插入指示条,
  重排后保持原激活页)与双击内联改名,新增 MoveTab 正确处理激活下标。
  修复改名后未重新布局导致标签页整条消失的问题(SetTabs 会重置标签矩形)。
  双击判定改为自行按双击时间识别,不再依赖系统 WM_LBUTTONDBLCLK。
- 信号拖拽:虚影改用原信号颜色与线宽(不再画成虚线),保持原有线段/散点样式;
  横向与纵向均自由跟随鼠标位移;拖拽首帧位移为零,虚影始终贴合在鼠标下方。
- 网格与标尺细化:时间轴与幅值轴期望刻度数提高,并在相邻主刻度之间
  补绘 5 等分细线,便于目视估读。
- 图例定位与交互:图例移到各网格左上角,与标尺数字冲突时优先显示图例
  (标尺数值主动避让);图例条目支持单击选中信号(与左侧列表联动、
  样式区随之切换目标)与双击重命名信号。
- 网格划分:合并网格后新勾选的信号不再被并入那个唯一的合并网格,
  而是自动切换为自定义划分并独占一个新网格。
- 工具栏:「每信号一格」改名为「拆分信号网格」。
- 清理:移除已无引用的 UiDrawArrowIcon 与 m_styleContentRect。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-09-30 11:02:09 +08:00
1 parent 261b8ac07d
commit 4e295a2a7d
12 files changed
+800 -109

No files matched your search

+352 -50
View File
@@ -11,8 +11,9 @@ namespace jdi
namespace
{
const int kTimeTickTarget = 8; /**< 时间轴期望刻度数。 */
const int kYTickTarget = 4; /**< 幅值轴期望刻度数。 */
const int kTimeTickTarget = 10; /**< 时间轴期望刻度数。 */
const int kYTickTarget = 6; /**< 幅值轴期望刻度数。 */
const int kMinorDivisions = 5; /**< 相邻主刻度之间的等分数(用于估读)。 */
const float kScrollBarThickness = 9.0f; /**< 滚动条宽度(逻辑像素)。 */
const float kLanePadding = 8.0f; /**< 网格内边距。 */
const size_t kMaxPolylinePoints = 20000; /**< 单条折线最大顶点数。 */
@@ -25,6 +26,10 @@ namespace jdi
const float kLaneDividerWidth = 2.0f; /**< 网格之间分界线的宽度。 */
const float kSignalHitTolerance = 6.0f; /**< 信号曲线命中容差(逻辑像素)。 */
const float kLaneLabelPad = 6.0f; /**< 网格内侧标尺带的内边距。 */
const float kLegendHeight = 19.0f; /**< 图例条目高度(逻辑像素)。 */
const float kLegendPad = 5.0f; /**< 图例相对网格左上角的内缩。 */
const float kLegendSwatchSize = 8.0f; /**< 图例色块尺寸。 */
const float kLegendItemGap = 14.0f; /**< 图例条目之间的间距。 */
/**
* @brief 将数值限制到合法区间。
@@ -47,13 +52,13 @@ namespace jdi
}
/**
* @brief 组装图例文本:直接使用列名原文(已含序号与括号描述)。
* @brief 组装图例文本:使用信号当前显示名(用户重命名后立即生效)。
* @param [IN] data 信号数据。
* @return 图例文本。
*/
std::wstring BuildLegendText(const SignalData& data)
{
return data.name;
return data.Title();
}
/**
@@ -837,6 +842,17 @@ namespace jdi
return gd::RectF(left, m_plotRect.Y + 2.0f * m_scale, width, height);
}
if (m_editTarget == PlotEditTarget::SIGNAL_NAME)
{
// 图例改名:输入框直接落在被双击的图例条目位置上。
if (m_editFieldRect.Width <= 0.0f)
{
return gd::RectF();
}
const float nameWidth = std::max(width, m_editFieldRect.Width);
return gd::RectF(m_editFieldRect.X, m_editFieldRect.Y, nameWidth, height);
}
// 网格 Y 轴上下限输入框贴在该网格内侧标尺带下方。
if (m_editLane < 0 || m_editLane >= static_cast<int>(m_lanes.size()))
{
@@ -854,6 +870,12 @@ namespace jdi
{
return false;
}
// 切换编辑目标时清掉上一轮残留(图例改名需由调用方在进入前先设置 m_editSignal)。
if (target != PlotEditTarget::SIGNAL_NAME)
{
m_editSignal = SignalRef();
m_editFieldRect = gd::RectF();
}
m_editTarget = target;
m_editLane = laneIndex;
m_editText = initialText;
@@ -861,6 +883,20 @@ namespace jdi
return true;
}
bool PlotView::TakePendingRename(SignalRef& ref, std::wstring& name)
{
if (!m_pendingRename)
{
return false;
}
ref = m_pendingRenameRef;
name = m_pendingRenameName;
m_pendingRename = false;
m_pendingRenameRef = SignalRef();
m_pendingRenameName.clear();
return true;
}
bool PlotView::EndEdit(bool commit)
{
if (m_editTarget == PlotEditTarget::NONE)
@@ -870,8 +906,28 @@ namespace jdi
const PlotEditTarget target = m_editTarget;
const int laneIndex = m_editLane;
const SignalRef editSignal = m_editSignal;
const std::wstring text = Trim(m_editText);
m_editTarget = PlotEditTarget::NONE;
m_editLane = -1;
m_editFresh = false;
m_editSignal = SignalRef();
m_editFieldRect = gd::RectF();
m_editText.clear();
// 信号改名:交给宿主窗口落到数据层,绘图区只持有只读数据。
if (target == PlotEditTarget::SIGNAL_NAME)
{
if (commit && !text.empty() && editSignal.IsValid())
{
m_pendingRenameRef = editSignal;
m_pendingRenameName = text;
m_pendingRename = true;
}
return true;
}
double parsed = 0.0;
bool parsedOk = false;
if (commit && !text.empty())
@@ -881,11 +937,6 @@ namespace jdi
parsedOk = (stop != text.c_str()) && (parsed == parsed) && std::isfinite(parsed);
}
m_editTarget = PlotEditTarget::NONE;
m_editLane = -1;
m_editFresh = false;
m_editText.clear();
if (!parsedOk)
{
return true;
@@ -965,6 +1016,25 @@ namespace jdi
return true;
}
// 信号改名接受任意可见字符(保留中文、空格与括号等原始信息)。
if (m_editTarget == PlotEditTarget::SIGNAL_NAME)
{
if (character < L' ')
{
return true;
}
if (m_editFresh)
{
m_editText.clear();
m_editFresh = false;
}
if (m_editText.size() < 96)
{
m_editText.push_back(character);
}
return true;
}
const bool digit = (character >= L'0' && character <= L'9');
const bool special = (character == L'.' || character == L'-' || character == L'+' || character == L'e' ||
character == L'E');
@@ -1185,6 +1255,7 @@ namespace jdi
{
const int target = LaneAtY(y);
m_dragMoved = true;
m_dragMouseX = x;
m_dragMouseY = y;
if (target != m_dragTargetLane)
{
@@ -1215,10 +1286,15 @@ namespace jdi
}
// 常规模式下精确检测鼠标是否贴近某条信号曲线,用于给出可拖拽提示。
// 图例优先于曲线:图例条目本身就是信号的可点选目标(与左侧信号列表一致)。
SignalRef hoverSignal;
if (Tab().ZoomMode() == PlotZoomMode::NONE && !m_cursorHover)
{
hoverSignal = HitTestSignal(x, y);
hoverSignal = LegendAtPoint(x, y);
if (!hoverSignal.IsValid())
{
hoverSignal = HitTestSignal(x, y);
}
}
if (hoverSignal != m_hoverSignal)
{
@@ -1298,12 +1374,27 @@ namespace jdi
return true;
}
// 常规模式下:点击图例条目等价于在左侧列表中选中该信号(样式区随之切换目标)。
if (mode == PlotZoomMode::NONE)
{
const SignalRef legendHit = LegendAtPoint(x, y);
if (legendHit.IsValid())
{
Tab().SetSelected(legendHit);
m_dragMode = PlotDragMode::NONE;
return true;
}
}
// 常规模式下:先检测是否落在信号曲线上;只有命中提示时才允许拖拽该信号。
const SignalRef hit = HitTestSignal(x, y);
if (hit.IsValid())
{
m_dragSignal = hit;
m_dragTargetLane = Tab().LaneOfSignal(hit);
Tab().SetSelected(hit);
m_dragMouseX = x;
m_dragMouseY = y;
m_dragMode = PlotDragMode::MOVE_SIGNAL;
return true;
}
@@ -1407,6 +1498,29 @@ namespace jdi
return BeginEdit(PlotEditTarget::CURSOR_TIME, -1, FormatValue(Tab().CursorTime()));
}
// 命中图例条目:进入信号改名(与左侧列表双击改名的交互一致)。
const SignalRef legendHit = LegendAtPoint(x, y);
if (legendHit.IsValid())
{
const ResolvedSignal resolved = ResolveSignal(legendHit);
if (resolved.data != nullptr)
{
Tab().SetSelected(legendHit);
m_editSignal = legendHit;
// 先把输入框位置落在被双击的条目上,再进入编辑态。
for (const LegendHit& hit : m_legendHits)
{
if (hit.ref == legendHit)
{
m_editFieldRect = hit.rect;
break;
}
}
return BeginEdit(PlotEditTarget::SIGNAL_NAME, -1, resolved.data->Title());
}
}
// 其它位置:回到初始状态(自适应 + 退出缩放模式)。
FitAll();
return true;
@@ -1560,6 +1674,9 @@ namespace jdi
// 波形区域底色。
UiFillRect(g, m_plotRect, m_theme->cardBg);
// 图例命中矩形每帧重建,避免残留已消失的信号。
m_legendHits.clear();
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
PaintLane(g, static_cast<int>(index), m_lanes[index], timeTicks);
@@ -1657,6 +1774,28 @@ namespace jdi
std::vector<double> yTicks;
BuildTicks(lane.yMin, lane.yMax, yStep, yTicks);
// 五等分估读线:在相邻主刻度之间再细分,便于目视读取中间值。
const double yMinorStep = yStep / static_cast<double>(kMinorDivisions);
std::vector<double> yMinorTicks;
BuildTicks(lane.yMin, lane.yMax, yMinorStep, yMinorTicks);
for (const double tick : yMinorTicks)
{
// 与主刻度重合的位置跳过(主刻度会以更重的线条绘制)。
const double majorIndex = tick / yStep;
if (std::fabs(majorIndex - std::floor(majorIndex + 0.5)) < 1.0e-6)
{
continue;
}
const float y = ValueToY(tick, lane);
if (y < lane.rect.Y - 1.0f || y > lane.rect.Y + lane.rect.Height + 1.0f)
{
continue;
}
UiDrawLine(g, lane.rect.X, y, lane.rect.X + lane.rect.Width, y,
WithAlpha(m_theme->gridWeak, 0x70), 1.0f);
}
for (const double tick : yTicks)
{
const float y = ValueToY(tick, lane);
@@ -1670,7 +1809,29 @@ namespace jdi
zeroLine ? m_theme->gridStrong : m_theme->gridWeak, 1.0f);
}
// 时间网格线。
// 时间网格线(同样补充五等分估读线)。
const double timeSpan = Tab().ViewMax() - Tab().ViewMin();
const double timeMajorStep = NiceStep((timeSpan > 0.0) ? timeSpan : 1.0, kTimeTickTarget);
const double timeMinorStep = timeMajorStep / static_cast<double>(kMinorDivisions);
std::vector<double> timeMinorTicks;
BuildTicks(Tab().ViewMin(), Tab().ViewMax(), timeMinorStep, timeMinorTicks);
for (const double tick : timeMinorTicks)
{
const double majorIndex = tick / timeMajorStep;
if (std::fabs(majorIndex - std::floor(majorIndex + 0.5)) < 1.0e-6)
{
continue;
}
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,
WithAlpha(m_theme->gridWeak, 0x70), 1.0f);
}
for (const double tick : timeTicks)
{
const float x = TimeToX(tick);
@@ -1784,6 +1945,9 @@ namespace jdi
// 内侧纵轴标尺作为覆盖层绘制:每个刻度数值带半透明底,保证在波形之上仍清晰可读,
// 同时只在数值背后遮挡波形,不牺牲波形本身的可见范围。
{
// 图例固定占用网格左上角,冲突时优先显示图例,故落在图例带内的刻度数值直接跳过。
const gd::RectF legendBand = LegendBandRect(laneIndex, lane);
const bool showLabels = (lane.rect.Height >= 34.0f * m_scale);
float lastLabelY = -1.0e9f;
@@ -1816,6 +1980,15 @@ namespace jdi
{
continue;
}
// 与图例带冲突时让位(图例优先)。
if (legendBand.Width > 0.0f &&
pill.X < legendBand.X + legendBand.Width &&
pill.X + pill.Width > legendBand.X &&
pill.Y < legendBand.Y + legendBand.Height &&
pill.Y + pill.Height > legendBand.Y)
{
continue;
}
// 文本必须完整落在可见区内,否则会越过绘图区顶部(GDI 文本不受 GDI+ 裁剪约束)。
if (!IsFullyVisible(pill, clipTop, clipBottom))
{
@@ -1842,6 +2015,57 @@ namespace jdi
g.Restore(saveState);
}
gd::RectF PlotView::LegendBandRect(int laneIndex, const LaneVisual& lane) const
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return gd::RectF();
}
// 逐条累加已可见图例的宽度,得到整条图例带(用于让标尺数字避让)。
float width = 0.0f;
for (const SignalRef& ref : m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.style->visible)
{
continue;
}
std::wstring label = BuildLegendText(*resolved.data);
if (LaneSpansMultipleFiles(laneIndex) && resolved.file != nullptr)
{
label += L" · " + resolved.file->Title();
}
const float textWidth = UiMeasureTextWidth(m_fonts->uiSmall, label);
width += kLegendSwatchSize * m_scale + 5.0f * m_scale + textWidth + kLegendItemGap * m_scale;
}
if (width <= 0.0f)
{
return gd::RectF();
}
const float left = lane.rect.X + kLegendPad * m_scale;
const float maxWidth = std::max(0.0f, lane.rect.Width - kLegendPad * 2.0f * m_scale);
return gd::RectF(left, lane.rect.Y + kLegendPad * m_scale - 1.0f * m_scale,
std::min(width + 6.0f * m_scale, maxWidth), kLegendHeight * m_scale + 2.0f * m_scale);
}
SignalRef PlotView::LegendAtPoint(int x, int y) const
{
const float fx = static_cast<float>(x);
const float fy = static_cast<float>(y);
for (const LegendHit& hit : m_legendHits)
{
if (UiPointInRect(hit.rect, fx, fy))
{
return hit.ref;
}
}
return SignalRef();
}
void PlotView::PaintLaneLegend(gd::Graphics& g, int laneIndex, const LaneVisual& lane, float visibleTop,
float visibleBottom)
{
@@ -1850,9 +2074,10 @@ namespace jdi
return;
}
// 图例固定在网格顶部:网格被滚动出可视区时整体跳过,避免文字溢出到工具栏。
const gd::RectF legendBand(lane.rect.X, lane.rect.Y + 4.0f * m_scale, lane.rect.Width,
18.0f * m_scale);
// 图例固定在网格左上角:网格被滚动出可视区时整体跳过,避免文字溢出到工具栏。
const gd::RectF legendBand(lane.rect.X + kLegendPad * m_scale,
lane.rect.Y + kLegendPad * m_scale, lane.rect.Width,
kLegendHeight * m_scale);
if (!IsFullyVisible(legendBand, visibleTop, visibleBottom))
{
return;
@@ -1861,10 +2086,9 @@ namespace jdi
const std::vector<SignalRef>& laneSignals = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].signals;
const bool multiFile = LaneSpansMultipleFiles(laneIndex);
const float top = lane.rect.Y + 4.0f * m_scale;
const float height = 18.0f * m_scale;
// 图例从内侧标尺带的右侧开始,避免与纵轴刻度重叠。
float cursorX = lane.rect.X + m_labelBandWidth + 6.0f * m_scale;
const float top = legendBand.Y;
const float height = kLegendHeight * m_scale;
float cursorX = legendBand.X;
const float maxX = lane.rect.X + lane.rect.Width - kLanePadding * m_scale;
bool anyVisible = false;
@@ -1886,8 +2110,8 @@ namespace jdi
{
label += L" · " + resolved.file->Title();
}
const float itemWidth = 13.0f * m_scale + UiTextWidth(g, label, m_fonts->uiSmall) +
14.0f * m_scale;
const float itemWidth = kLegendSwatchSize * m_scale + 5.0f * m_scale +
UiTextWidth(g, label, m_fonts->uiSmall) + kLegendItemGap * m_scale;
if (cursorX + legendWidth + itemWidth > maxX)
{
break;
@@ -1896,9 +2120,22 @@ namespace jdi
}
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));
UiFillRoundRect(g, gd::RectF(cursorX - 4.0f * m_scale, top - 2.0f * m_scale,
legendWidth + 6.0f * m_scale, height + 4.0f * m_scale),
3.0f * m_scale, WithAlpha(m_theme->cardBg, 0xE8));
}
// 记录本次绘制命中的条目矩形,供点击选中与双击改名使用。
for (auto iterator = m_legendHits.begin(); iterator != m_legendHits.end();)
{
if (m_tab->LaneOfSignal(iterator->ref) == laneIndex)
{
iterator = m_legendHits.erase(iterator);
}
else
{
++iterator;
}
}
// 绘制图例条目(与测量阶段保持一致的裁剪条件)。
@@ -1919,29 +2156,59 @@ namespace jdi
label += L" · " + resolved.file->Title();
}
const float textWidth = UiTextWidth(g, label, m_fonts->uiSmall);
const float itemWidth = 13.0f * m_scale + textWidth + 14.0f * m_scale;
const float itemWidth =
kLegendSwatchSize * m_scale + 5.0f * m_scale + textWidth + kLegendItemGap * m_scale;
if (drawnWidth + itemWidth > legendWidth + 0.5f)
{
break;
}
drawnWidth += itemWidth;
const float swatchSize = 8.0f * m_scale;
const float itemLeft = cursorX;
const float swatchSize = kLegendSwatchSize * m_scale;
// 图例条目与被选中的信号保持一致的高亮反馈(与左侧列表选中态呼应)。
const bool selected = (m_tab->Selected() == ref);
const bool hovered = (ref == m_hoverSignal) || (ref == m_dragSignal);
if (selected || hovered)
{
UiFillRoundRect(g, gd::RectF(itemLeft - 3.0f * m_scale, top - 1.0f * m_scale,
itemWidth + 2.0f * m_scale, height + 2.0f * m_scale),
3.0f * m_scale, selected ? m_theme->selection : m_theme->hover);
}
UiDrawSwatch(g, gd::RectF(cursorX, top + (height - swatchSize) * 0.5f, swatchSize, swatchSize),
resolved.style->color, resolved.style->opacity, 1.5f * m_scale, m_scale);
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;
// 改名编辑态:在原位置绘制输入框。
LegendHit hit;
hit.ref = ref;
hit.rect = gd::RectF(itemLeft - 3.0f * m_scale, top - 2.0f * m_scale,
itemWidth + 2.0f * m_scale, height + 4.0f * m_scale);
m_legendHits.push_back(hit);
if (m_editTarget == PlotEditTarget::SIGNAL_NAME && m_editSignal == ref)
{
m_editFieldRect = hit.rect;
UiDrawEditField(g, hit.rect, m_editText, m_editFresh, m_fonts->uiSmall, m_scale);
cursorX += itemWidth;
++drawnCount;
continue;
}
cursorX += swatchSize + 5.0f * m_scale;
UiText(g, label, m_fonts->uiSmall,
gd::RectF(cursorX, top, std::max(8.0f, textWidth + 2.0f), height),
selected ? m_theme->textStrong : m_theme->text, TextAlign::LEFT);
cursorX += textWidth + kLegendItemGap * m_scale;
++drawnCount;
}
if (!anyVisible)
{
UiText(g, L"(无可见信号)", m_fonts->uiSmall,
gd::RectF(lane.rect.X + m_labelBandWidth + 6.0f * m_scale, top, lane.rect.Width, height),
m_theme->textMuted, TextAlign::LEFT);
gd::RectF(legendBand.X, top, lane.rect.Width, height), m_theme->textMuted,
TextAlign::LEFT);
}
else if (drawnCount < visibleCount)
{
@@ -2238,36 +2505,71 @@ namespace jdi
return;
}
// 虚影:以源网格的幅值映射构建波形,并整体平移跟随鼠标,形成“拖着曲线走”的观感。
LaneVisual ghostLane = m_lanes[static_cast<std::size_t>(sourceLane)];
const float follow = static_cast<float>(m_dragMouseY - m_dragStartY);
ghostLane.rect.Y += follow;
// 虚影:以源网格的幅值映射构建波形,再按鼠标位移整体平移。
// 第一帧位移为 0,因此虚影必然精确落在鼠标下方(曲线形态与抓住时完全一致);
// 之后纵向与横向均自由跟随,形成“拖着曲线走”的观感。
const LaneVisual& sourceVisual = m_lanes[static_cast<std::size_t>(sourceLane)];
const float offsetX = static_cast<float>(m_dragMouseX - m_dragStartX);
const float offsetY = static_cast<float>(m_dragMouseY - m_dragStartY);
{
const float clipTop = std::max(m_plotRect.Y, m_plotRect.Y);
const float clipHeight = m_plotRect.Height;
const int saveState = g.Save();
g.SetClip(gd::RectF(m_plotRect.X, clipTop, m_plotRect.Width, clipHeight),
g.SetClip(gd::RectF(m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height),
gd::CombineModeIntersect);
std::vector<gd::PointF> points;
std::vector<std::size_t> breaks;
BuildPolyline(resolved, ghostLane, points, breaks);
BuildPolyline(resolved, sourceVisual, points, breaks);
gd::Pen ghost(ToGdiColor(WithAlpha(m_theme->accent, 0xC0)),
std::max(1.8f, resolved.style->lineWidth) * m_scale);
ghost.SetDashStyle(gd::DashStyleDash);
ghost.SetStartCap(gd::LineCapRound);
ghost.SetEndCap(gd::LineCapRound);
for (std::size_t k = 0; k < breaks.size(); ++k)
for (gd::PointF& point : points)
{
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)
point.X += offsetX;
point.Y += offsetY;
}
const DrawMode mode = resolved.style->mode;
const bool drawLine = (mode != DrawMode::SCATTER);
const bool drawScatter = (mode == DrawMode::SCATTER || mode == DrawMode::LINE_SCATTER);
// 虚影保持原信号的颜色与样式(不再是虚线),仅以轻微透明区分“尚未落地”。
const std::uint32_t ghostColor = WithAlpha(resolved.style->color,
static_cast<unsigned int>(
std::max(0.35, static_cast<double>(resolved.style->opacity)) *
255.0 + 0.5));
if (drawLine && !breaks.empty())
{
gd::Pen ghost(ToGdiColor(ghostColor),
std::max(1.0f, resolved.style->lineWidth) * m_scale);
ghost.SetLineJoin(gd::LineJoinRound);
ghost.SetStartCap(gd::LineCapRound);
ghost.SetEndCap(gd::LineCapRound);
for (std::size_t k = 0; k < breaks.size(); ++k)
{
continue;
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(&ghost, points.data() + start, static_cast<INT>(stop - start));
}
}
if (drawScatter && !points.empty())
{
const float radius = static_cast<float>(
ClampDouble(static_cast<double>(resolved.style->lineWidth) * kScatterRadiusScale,
kMinScatterRadius, kMaxScatterRadius) *
m_scale);
const std::size_t scatterStride =
std::max<std::size_t>(1, points.size() / kMaxScatterPoints);
gd::SolidBrush brush(ToGdiColor(ghostColor));
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.DrawLines(&ghost, points.data() + start, static_cast<INT>(stop - start));
}
g.Restore(saveState);