绘图交互完善 + 稠密区域拖拽性能修复(VERY_BIG_DATA 2ms/大格不再卡顿)
一、功能修正 - 修复「光标数据点在上下视界外不绘制」的问题,并让上下越界与左右越界一样用三角表示: 新增 `DataMarker` / `PaintDataMarker()`,统一处理「可视区内画圆点、越界则钉在网格边上 画三角指示方向」的四向情形(左/右/上/下)。光标取值与中心轴取值都改走这一套, 因此纵向被裁掉的数据点不再消失。 - 「边沿计数(中心轴)」的中心平均值曲线改为**跟随原信号的样式**: 与原始信号同色但更透明,线宽同样跟随(lineWidth + 0.6,下限 1.4), 原信号改颜色/线宽/透明度后曲线同步变化(曲线参与场景指纹,样式变化即失效重绘)。 光标显示中心值时的胶囊也改用**原始信号颜色**(一一对应),并**始终按线段模式**着色, 不再使用光标色。 - 「拆分当前网格」在可点击时追加尾缀「(N个信号)」(N = 该网格内信号数), 只有 1 条信号(不可点击)时不加尾缀。 二、新增:左侧信号筛选框 - 信号列表上方新增「筛选信号」输入框:空内容时显示灰色占位提示,单击进入编辑, 点击别处或回车结束编辑;列表内容随输入**实时**变化。 - 支持 `*` 通配符(`*` 等价于"任意字符",因此 `*` 单独出现等价于不过滤),忽略大小写。 - 筛选结果**仍以组为单位**,组默认展开,只显示匹配的信号;组内无匹配信号时整组不显示。 - 复用统一的 `TextEdit` 编辑模型,因此光标/选区/Ctrl+A/C/X/V 等行为与其它输入框一致。 三、性能:修复 VERY_BIG_DATA.CSV 在 2ms/大格密集区域拖拽卡顿 以「显示全部 5 条信号 + 时间轴缩放到 2ms 一大格」为场景逐段打点定位后修复: - **主因:稠密包络的锯齿描边光栅化**。抽稀时每列产生 min/max 两个顶点交替的锯齿, 2000+ 条近乎满幅的斜线段在 GDI+ 抗锯齿下要光栅化几十万像素(实测每网格 14ms, 5 网格约 70ms/帧)。 改为**包络带一次填充**:逐列记录上/下包络,收段时拼成「上包络正向 + 下包络反向」的 闭合多边形,用一条 `GraphicsPath` + `FillPath` 绘制(显式 `FillModeWinding`, 自交包络不会挖洞)。基准测试(1224 列):14.26ms → **1.69ms(约 8.4 倍)**, 且**保留抗锯齿**、静态帧与拖拽帧完全一致(不是"交互降级")。 逐像素对比确认观感等价(墨量覆盖率 90.8% vs 91.3%)。 包络带同时用于静态绘制与 MOVE_SIGNAL 的拖拽虚影。 - **大面积不透明填充改走 GDI**:`UiFillRect` 在面积 ≥ 20000px² 且颜色不透明时使用 `GetHDC + FillRect`(GDI+ FillRectangle 约 2.2ns/像素,整窗背景 1.7M 像素要 0.9ms、 面板背景 1.9ms)。半透明填充仍走 GDI+(GDI 不做 alpha 混色);小面积仍走 GDI+ (避免 GetHDC 固定开销)。顺序安全:GetHDC 会先刷入已排队的 GDI+ 内容。 - **`UiTextWidth` 不再触发 GDI+ 批次刷新**:改为复用 `UiMeasureTextWidth` (`GetDC(nullptr)`),`UiEllipsize` 逐字符回退时的十几次刷新被完全消除。 实测(125% 缩放、1725x996、5 信号、2ms 一大格): 绘图区绘制 47~87ms → **3.6~4.5ms**;整帧最大 196.8ms → **约 17~21ms(≈50~60fps)**, 与空闲帧基本持平,即"拖拽与不拖拽一样流畅"。 验证(VERY_BIG_DATA.CSV;面板行坐标因新增筛选框整体下移,复选框客户区 (61,250) 起、行距 33): - 筛选框:输入 `B` 后只剩「编码器B相」「电流钳采集(ABSx10)」,组仍展开;清空即恢复。 - 拆分:合并 3 条信号后右键显示「拆分当前网格(3个信号)」;单信号网格该项置灰无尾缀。 - 光标/中心轴:竖线光标取值与中心值胶囊均按四向越界正确显示三角/圆点。 - 稠密区域:5 信号全选 + 缩放到 2ms 一大格,左键拖拽信号、左键平移、中键平移均流畅; 渲染观感与修改前等价(叠图对比)。 - make clean && make all 无警告无错误。
This commit is contained in:
1 parent
82a29362eb
commit
fe72afc83c
6 files changed
+721
-126
No files matched your search
@@ -867,7 +867,9 @@ namespace jdi
|
||||
* @param [OUT] breaks 输出每段折线的起始下标(用于处理缺失值断线)。
|
||||
*/
|
||||
void BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks) const;
|
||||
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks,
|
||||
std::vector<gd::PointF>& band, std::vector<std::size_t>& bandBreaks,
|
||||
bool& bandMode) const;
|
||||
|
||||
/**
|
||||
* @brief 绘制单个网格。
|
||||
@@ -982,6 +984,42 @@ namespace jdi
|
||||
void PaintCenterCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
int smooth) const;
|
||||
|
||||
/**
|
||||
* @brief 数据点标记的位置与"越界方向"(用于决定画圆点还是贴边三角)。
|
||||
*/
|
||||
struct DataMarker
|
||||
{
|
||||
float x = 0.0f; /**< 标记锚点横坐标(已夹在可视区内)。 */
|
||||
float y = 0.0f; /**< 标记锚点纵坐标(已夹在可视区内)。 */
|
||||
bool offLeft = false; /**< 数据点位于左侧视界之外。 */
|
||||
bool offRight = false; /**< 数据点位于右侧视界之外。 */
|
||||
bool offUp = false; /**< 数据点位于上方视界之外。 */
|
||||
bool offDown = false; /**< 数据点位于下方视界之外。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 计算数据点标记:把越界的数据点"钉"在网格可见边缘,并记录越界方向。
|
||||
* @param [IN] rawX 数据点的原始横坐标。
|
||||
* @param [IN] rawY 数据点的原始纵坐标。
|
||||
* @param [IN] pinX 横向贴边位置(光标在视界外时的钉住横坐标)。
|
||||
* @param [IN] clipTop 网格可见区上边界。
|
||||
* @param [IN] clipBottom 网格可见区下边界。
|
||||
* @return 标记几何。
|
||||
*/
|
||||
DataMarker MakeDataMarker(float rawX, float rawY, float pinX, float clipTop,
|
||||
float clipBottom) const;
|
||||
|
||||
/**
|
||||
* @brief 绘制数据点标记:视界内为圆点,越界时在对应边缘绘制指示方向的三角。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] marker 标记几何。
|
||||
* @param [IN] radius 圆点半径。
|
||||
* @param [IN] fillColor 前景色(圆点/三角内部)。
|
||||
* @param [IN] ringColor 描边色(圆环/三角外框)。
|
||||
*/
|
||||
void PaintDataMarker(gd::Graphics& g, const DataMarker& marker, float radius,
|
||||
std::uint32_t fillColor, std::uint32_t ringColor) const;
|
||||
|
||||
/**
|
||||
* @brief 放置一个光标取值胶囊:先夹到网格可见范围内,再向下(必要时向上)避让
|
||||
* 已占用的标注矩形,最后登记到 `m_cursorLabelRects`。
|
||||
@@ -1254,6 +1292,21 @@ namespace jdi
|
||||
double laneYMax = 0.0; /**< 网格幅值上界。 */
|
||||
std::vector<gd::PointF> points; /**< 折线顶点。 */
|
||||
std::vector<std::size_t> breaks; /**< 每段折线在 points 中的起始下标。 */
|
||||
|
||||
/**
|
||||
* @brief 是否处于「稠密包络」模式(采样点密度超过像素分辨率)。
|
||||
*
|
||||
* 此时 `points` 是每列 min/max 交替的锯齿,逐段描边的光栅化开销极大
|
||||
* (实测每网格约 14ms);改用下面的「包络带」一次填充后约 1.7ms,
|
||||
* 且保留抗锯齿,静态与拖拽帧的观感完全一致。
|
||||
*/
|
||||
bool bandMode = false;
|
||||
|
||||
/** 包络带顶点:每段为「上包络正向 + 下包络反向」,闭合后即为该段的填充多边形。 */
|
||||
std::vector<gd::PointF> band;
|
||||
|
||||
/** 每段包络带在 `band` 中的起始下标。 */
|
||||
std::vector<std::size_t> bandBreaks;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -295,7 +295,6 @@ namespace jdi
|
||||
return WithAlpha(style.color, static_cast<unsigned int>(opacity * 255.0 + 0.5));
|
||||
}
|
||||
}
|
||||
|
||||
PlotView::PlotView() = default;
|
||||
|
||||
PlotView::~PlotView()
|
||||
@@ -984,8 +983,7 @@ namespace jdi
|
||||
}
|
||||
|
||||
std::size_t PlotView::LaneSignalCount(int laneIndex) const
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 ||
|
||||
{ if (m_tab == nullptr || laneIndex < 0 ||
|
||||
laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return 0;
|
||||
@@ -3596,16 +3594,22 @@ namespace jdi
|
||||
entry.laneYMin = lane.yMin;
|
||||
entry.laneYMax = lane.yMax;
|
||||
|
||||
BuildPolyline(resolved, lane, entry.points, entry.breaks);
|
||||
BuildPolyline(resolved, lane, entry.points, entry.breaks, entry.band, entry.bandBreaks,
|
||||
entry.bandMode);
|
||||
// BuildPolyline 通过引用填充,push_back 过程中不会再次进入本函数,引用安全。
|
||||
return &m_polylineCache.back();
|
||||
}
|
||||
|
||||
void PlotView::BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks) const
|
||||
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks,
|
||||
std::vector<gd::PointF>& band, std::vector<std::size_t>& bandBreaks,
|
||||
bool& bandMode) const
|
||||
{
|
||||
points.clear();
|
||||
breaks.clear();
|
||||
band.clear();
|
||||
bandBreaks.clear();
|
||||
bandMode = false;
|
||||
|
||||
if (resolved.data == nullptr || resolved.file == nullptr)
|
||||
{
|
||||
@@ -3683,6 +3687,38 @@ namespace jdi
|
||||
(coarseStride > 0) && !coarseMin.empty() && (coarseMin.size() == coarseMax.size());
|
||||
|
||||
bool inSegment = false;
|
||||
|
||||
// 稠密包络带:逐列记录上/下包络,收段时拼成「上包络正向 + 下包络反向」的
|
||||
// 闭合多边形。绘制时一次 FillPath 即可,实测比逐段描边快约 8 倍
|
||||
// (1224 列:14.3ms → 1.7ms),且**保留抗锯齿**,
|
||||
// 因此静态帧与拖拽帧的观感完全一致(不是"交互降级")。
|
||||
bandMode = true;
|
||||
const float strokeHalf =
|
||||
std::max(1.0f, resolved.style->lineWidth) * m_scale * 0.5f;
|
||||
std::vector<gd::PointF> segmentTop;
|
||||
std::vector<gd::PointF> segmentBottom;
|
||||
const auto flushBandSegment = [&band, &bandBreaks, &segmentTop, &segmentBottom]()
|
||||
{
|
||||
if (segmentTop.size() < 2)
|
||||
{
|
||||
segmentTop.clear();
|
||||
segmentBottom.clear();
|
||||
return;
|
||||
}
|
||||
bandBreaks.push_back(band.size());
|
||||
for (const gd::PointF& point : segmentTop)
|
||||
{
|
||||
band.push_back(point);
|
||||
}
|
||||
// 下包络反向,形成闭合环。
|
||||
for (std::size_t index = segmentBottom.size(); index-- > 0;)
|
||||
{
|
||||
band.push_back(segmentBottom[index]);
|
||||
}
|
||||
segmentTop.clear();
|
||||
segmentBottom.clear();
|
||||
};
|
||||
|
||||
std::size_t searchFrom = begin;
|
||||
for (std::size_t column = 0; column < columns; ++column)
|
||||
{
|
||||
@@ -3706,6 +3742,7 @@ namespace jdi
|
||||
{
|
||||
// 该列没有采样点:说明数据在此处存在时间间隙,断开当前折线。
|
||||
inSegment = false;
|
||||
flushBandSegment();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3771,6 +3808,7 @@ namespace jdi
|
||||
{
|
||||
// 本列全是无效值。
|
||||
inSegment = false;
|
||||
flushBandSegment();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3780,6 +3818,10 @@ namespace jdi
|
||||
const float yBottom =
|
||||
static_cast<float>(ClampDouble(ValueToY(low, lane), limitTop, limitBottom));
|
||||
|
||||
// 包络带每列都记录(上下各外扩半个线宽,保证细波动也有可见粗细)。
|
||||
segmentTop.push_back(gd::PointF(x, yTop - strokeHalf));
|
||||
segmentBottom.push_back(gd::PointF(x, yBottom + strokeHalf));
|
||||
|
||||
if (yBottom - yTop < kEnvelopeMinPixelSpan)
|
||||
{
|
||||
// 波动不足半个像素:只留一个顶点,避免无意义的重复顶点。
|
||||
@@ -3813,9 +3855,11 @@ namespace jdi
|
||||
if (broken)
|
||||
{
|
||||
inSegment = false;
|
||||
flushBandSegment();
|
||||
}
|
||||
}
|
||||
|
||||
flushBandSegment();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4431,6 +4475,7 @@ namespace jdi
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(gd::RectF(m_area.X, clipTop, m_area.Width, clipBottom - clipTop));
|
||||
|
||||
|
||||
// 幅值网格线(先画线,标尺稍后作为覆盖层绘制,避免被波形遮挡)。
|
||||
// 刻度密度随网格高度自适应:所有网格高度相同 → 各网格的刻度数量一致,
|
||||
// 不会出现“有的网格只有 1 条标签、有的有 3 条”的不一致观感。
|
||||
@@ -4561,21 +4606,55 @@ namespace jdi
|
||||
|
||||
if (drawLine && !breaks.empty())
|
||||
{
|
||||
gd::Pen pen(ToGdiColor(StyleColor(*resolved.style)),
|
||||
resolved.style->lineWidth * m_scale);
|
||||
pen.SetLineJoin(gd::LineJoinRound);
|
||||
pen.SetStartCap(gd::LineCapRound);
|
||||
pen.SetEndCap(gd::LineCapRound);
|
||||
|
||||
for (std::size_t k = 0; k < breaks.size(); ++k)
|
||||
const std::uint32_t lineColor = StyleColor(*resolved.style);
|
||||
if (cached->bandMode && !cached->bandBreaks.empty())
|
||||
{
|
||||
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)
|
||||
// 稠密包络:一次 FillPath 填充包络带。
|
||||
// **必须显式设为缠绕填充规则**:包络带自交(相邻列上下包络交叉)时,
|
||||
// 交替(奇偶)规则会在交叠处挖出空洞。
|
||||
gd::GraphicsPath bandPath;
|
||||
bandPath.SetFillMode(gd::FillModeWinding);
|
||||
for (std::size_t k = 0; k < cached->bandBreaks.size(); ++k)
|
||||
{
|
||||
continue;
|
||||
const std::size_t start = cached->bandBreaks[k];
|
||||
const std::size_t stop = (k + 1 < cached->bandBreaks.size())
|
||||
? cached->bandBreaks[k + 1]
|
||||
: cached->band.size();
|
||||
const std::size_t total = stop - start;
|
||||
if (total < 4 || (total % 2) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::size_t half = total / 2;
|
||||
bandPath.StartFigure();
|
||||
bandPath.AddLines(cached->band.data() + start, static_cast<INT>(half));
|
||||
bandPath.AddLines(cached->band.data() + start + half,
|
||||
static_cast<INT>(half));
|
||||
bandPath.CloseFigure();
|
||||
}
|
||||
if (bandPath.GetPointCount() > 0)
|
||||
{
|
||||
gd::SolidBrush brush(ToGdiColor(lineColor));
|
||||
g.FillPath(&brush, &bandPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
gd::Pen pen(ToGdiColor(lineColor), resolved.style->lineWidth * m_scale);
|
||||
pen.SetLineJoin(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<INT>(stop - start));
|
||||
}
|
||||
g.DrawLines(&pen, points.data() + start, static_cast<INT>(stop - start));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5106,9 +5185,9 @@ namespace jdi
|
||||
const bool showDelta = doubleMode && (which == rightCursor);
|
||||
|
||||
// 光标在可视区外时,把标注"钉"在对应的网格边上,保证取值仍然可见。
|
||||
// 具体是否越界(左右/上下)由 MakeDataMarker 逐点判定。
|
||||
const float pinX = CursorDisplayX(which);
|
||||
const float rawX = TimeToX(time);
|
||||
const bool pinned = (std::fabs(rawX - pinX) > 0.5f);
|
||||
|
||||
for (std::size_t laneIndex = 0; laneIndex < lanes.size(); ++laneIndex)
|
||||
{
|
||||
@@ -5137,40 +5216,15 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
|
||||
const float y = ValueToY(value, lane);
|
||||
if (y < clipTop - 2.0f || y > clipBottom + 2.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float rawY = ValueToY(value, lane);
|
||||
// 上下越界的数据点不再被丢弃:与左右越界一致,贴边绘制指示方向的三角。
|
||||
const DataMarker marker = MakeDataMarker(rawX, rawY, pinX, clipTop, clipBottom);
|
||||
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
|
||||
// 数据点:底色环 + 信号色实心点(贴边时改画成三角指示方向)。
|
||||
// 数据点:视界内为圆点,任一方向越界时改为贴边三角。
|
||||
const float radius = 3.4f * m_scale;
|
||||
gd::SolidBrush ring(ToGdiColor(m_theme->cardBg));
|
||||
gd::SolidBrush fill(ToGdiColor(signalColor));
|
||||
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, y - arm);
|
||||
tri[1] = gd::PointF(baseX, y + arm);
|
||||
tri[2] = gd::PointF(pinX, y);
|
||||
g.FillPolygon(&ring, tri, 3);
|
||||
gd::PointF inner[3];
|
||||
inner[0] = gd::PointF(baseX + (offLeft ? 1.4f : -1.4f) * m_scale, y - arm * 0.72f);
|
||||
inner[1] = gd::PointF(baseX + (offLeft ? 1.4f : -1.4f) * m_scale, y + arm * 0.72f);
|
||||
inner[2] = gd::PointF(pinX - (offLeft ? -1.4f : 1.4f) * m_scale, y);
|
||||
g.FillPolygon(&fill, inner, 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
g.FillEllipse(&ring, pinX - 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, pinX - radius, y - radius, radius * 2.0f, radius * 2.0f);
|
||||
}
|
||||
PaintDataMarker(g, marker, radius, signalColor, m_theme->cardBg);
|
||||
|
||||
// 取值标注:底色用信号色、文字用反色,并加一层细描边提升对比度。
|
||||
std::wstring text = FormatValue(value);
|
||||
@@ -5198,7 +5252,7 @@ namespace jdi
|
||||
labelLeft = std::max(
|
||||
m_plotRect.X + 2.0f * m_scale,
|
||||
std::min(labelLeft, m_plotRect.X + m_plotRect.Width - textWidth - 2.0f * m_scale));
|
||||
float labelTop = y - textHeight * 0.5f;
|
||||
float labelTop = marker.y - textHeight * 0.5f;
|
||||
labelTop = std::max(clipTop + 1.0f * m_scale,
|
||||
std::min(labelTop, clipBottom - textHeight - 1.0f * m_scale));
|
||||
gd::RectF labelRect(labelLeft, labelTop, textWidth, textHeight);
|
||||
@@ -5221,40 +5275,14 @@ namespace jdi
|
||||
double centerValue = 0.0;
|
||||
if (CenterValueAt(*resolved.file, *resolved.data, smooth, time, centerValue))
|
||||
{
|
||||
const float centerY = ValueToY(centerValue, lane);
|
||||
if (centerY >= clipTop - 2.0f && centerY <= clipBottom + 2.0f)
|
||||
const float rawCenterY = ValueToY(centerValue, lane);
|
||||
const DataMarker centerMarker =
|
||||
MakeDataMarker(rawX, rawCenterY, pinX, clipTop, clipBottom);
|
||||
{
|
||||
// 中心轴数据点与常规数据点一样:在可视区内是圆点,
|
||||
// **光标在可视区之外(贴边显示)时同样改为三角**指示方向。
|
||||
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);
|
||||
}
|
||||
// 中心轴数据点的配色与原始信号一致(便于一一对应);
|
||||
// 但**始终按线段模式**绘制(不跟随原信号的散点模式)。
|
||||
const std::uint32_t centerColor = StyleColor(*resolved.style);
|
||||
PaintDataMarker(g, centerMarker, radius, centerColor, m_theme->cardBg);
|
||||
|
||||
// 取值文本:数值在前,尾缀「(中心轴)」;双光标的右侧光标还要
|
||||
// 附上增量,形如「(+0.0008 中心轴)」(参考常规信号的增量显示)。
|
||||
@@ -5286,13 +5314,13 @@ namespace jdi
|
||||
m_plotRect.X + 2.0f * m_scale,
|
||||
std::min(centerLeft, m_plotRect.X + m_plotRect.Width -
|
||||
centerTextWidth - 2.0f * m_scale));
|
||||
float centerTop = centerY - textHeight * 0.5f;
|
||||
float centerTop = centerMarker.y - textHeight * 0.5f;
|
||||
centerTop = std::max(
|
||||
clipTop + 1.0f * m_scale,
|
||||
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(centerColor, 0xE0));
|
||||
@@ -5485,6 +5513,73 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
PlotView::DataMarker PlotView::MakeDataMarker(float rawX, float rawY, float pinX, float clipTop,
|
||||
float clipBottom) const
|
||||
{
|
||||
DataMarker marker;
|
||||
// 上下越界时同样"钉"在网格可见边缘(留出三角形的高度,保证整体可见)。
|
||||
const float margin = 7.0f * m_scale;
|
||||
marker.x = pinX;
|
||||
marker.y = std::max(clipTop + margin, std::min(rawY, clipBottom - margin));
|
||||
marker.offLeft = (rawX < marker.x - 0.5f);
|
||||
marker.offRight = (rawX > marker.x + 0.5f);
|
||||
marker.offUp = (rawY < marker.y - 0.5f);
|
||||
marker.offDown = (rawY > marker.y + 0.5f);
|
||||
return marker;
|
||||
}
|
||||
|
||||
void PlotView::PaintDataMarker(gd::Graphics& g, const DataMarker& marker, float radius,
|
||||
std::uint32_t fillColor, std::uint32_t ringColor) const
|
||||
{
|
||||
gd::SolidBrush ring(ToGdiColor(ringColor));
|
||||
gd::SolidBrush fill(ToGdiColor(fillColor));
|
||||
const float arm = 5.0f * m_scale;
|
||||
const float inset = 1.4f * m_scale;
|
||||
|
||||
// 左右越界:三角底边在外侧、尖端朝向网格内部。
|
||||
if (marker.offLeft || marker.offRight)
|
||||
{
|
||||
const bool offLeft = marker.offLeft;
|
||||
const float baseX = offLeft ? (marker.x - arm) : (marker.x + arm);
|
||||
const float inward = offLeft ? inset : -inset;
|
||||
gd::PointF outer[3];
|
||||
outer[0] = gd::PointF(baseX, marker.y - arm);
|
||||
outer[1] = gd::PointF(baseX, marker.y + arm);
|
||||
outer[2] = gd::PointF(marker.x, marker.y);
|
||||
g.FillPolygon(&ring, outer, 3);
|
||||
gd::PointF inner[3];
|
||||
inner[0] = gd::PointF(baseX + inward, marker.y - arm * 0.72f);
|
||||
inner[1] = gd::PointF(baseX + inward, marker.y + arm * 0.72f);
|
||||
inner[2] = gd::PointF(marker.x + inward, marker.y);
|
||||
g.FillPolygon(&fill, inner, 3);
|
||||
return;
|
||||
}
|
||||
|
||||
// 上下越界:三角底边在上/下方、尖端朝向网格内部(与左右越界同一套观感)。
|
||||
if (marker.offUp || marker.offDown)
|
||||
{
|
||||
const bool offUp = marker.offUp;
|
||||
const float baseY = offUp ? (marker.y - arm) : (marker.y + arm);
|
||||
const float inward = offUp ? inset : -inset;
|
||||
gd::PointF outer[3];
|
||||
outer[0] = gd::PointF(marker.x - arm, baseY);
|
||||
outer[1] = gd::PointF(marker.x + arm, baseY);
|
||||
outer[2] = gd::PointF(marker.x, marker.y);
|
||||
g.FillPolygon(&ring, outer, 3);
|
||||
gd::PointF inner[3];
|
||||
inner[0] = gd::PointF(marker.x - arm * 0.72f, baseY + inward);
|
||||
inner[1] = gd::PointF(marker.x + arm * 0.72f, baseY + inward);
|
||||
inner[2] = gd::PointF(marker.x, marker.y + inward);
|
||||
g.FillPolygon(&fill, inner, 3);
|
||||
return;
|
||||
}
|
||||
|
||||
// 视界内:常规圆点。
|
||||
g.FillEllipse(&ring, marker.x - radius - 1.2f * m_scale, marker.y - radius - 1.2f * m_scale,
|
||||
(radius + 1.2f * m_scale) * 2.0f, (radius + 1.2f * m_scale) * 2.0f);
|
||||
g.FillEllipse(&fill, marker.x - radius, marker.y - radius, radius * 2.0f, radius * 2.0f);
|
||||
}
|
||||
|
||||
void PlotView::PlaceCursorLabel(gd::RectF& rect, float preferredTop, float textHeight,
|
||||
float clipTop, float clipBottom)
|
||||
{
|
||||
@@ -5627,10 +5722,14 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// 中心平均值曲线使用与光标(「边沿计数」横向光标、中心轴取值胶囊)一致的颜色样式:
|
||||
// 即光标的强调色。曲线与其取值标注、光标本身因此形成统一的视觉语言。
|
||||
const std::uint32_t derived = CursorColor(1);
|
||||
gd::Pen pen(ToGdiColor(derived), std::max(1.8f, resolved.style->lineWidth + 0.8f) * m_scale);
|
||||
// 中心平均值曲线:与原始信号**同色**,只是更透明一些;线宽等其它样式同样跟随原信号,
|
||||
// 因此原信号改颜色/线宽/透明度时曲线会自动同步。
|
||||
const unsigned int baseAlpha = static_cast<unsigned int>(
|
||||
std::max(0.0, std::min(1.0, static_cast<double>(resolved.style->opacity))) * 255.0 + 0.5);
|
||||
const std::uint32_t derived =
|
||||
WithAlpha(StyleColor(*resolved.style),
|
||||
static_cast<unsigned int>(static_cast<double>(baseAlpha) * 0.55 + 0.5));
|
||||
gd::Pen pen(ToGdiColor(derived), std::max(1.0f, resolved.style->lineWidth) * m_scale);
|
||||
pen.SetLineJoin(gd::LineJoinRound);
|
||||
pen.SetStartCap(gd::LineCapRound);
|
||||
pen.SetEndCap(gd::LineCapRound);
|
||||
@@ -5933,20 +6032,52 @@ namespace jdi
|
||||
|
||||
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)
|
||||
if (ghostSource->bandMode && !ghostSource->bandBreaks.empty())
|
||||
{
|
||||
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)
|
||||
// 稠密包络虚影同样用一次 FillPath(见 PaintLane 的说明)。
|
||||
gd::GraphicsPath bandPath;
|
||||
bandPath.SetFillMode(gd::FillModeWinding);
|
||||
for (std::size_t k = 0; k < ghostSource->bandBreaks.size(); ++k)
|
||||
{
|
||||
continue;
|
||||
const std::size_t start = ghostSource->bandBreaks[k];
|
||||
const std::size_t stop = (k + 1 < ghostSource->bandBreaks.size())
|
||||
? ghostSource->bandBreaks[k + 1]
|
||||
: ghostSource->band.size();
|
||||
const std::size_t total = stop - start;
|
||||
if (total < 4 || (total % 2) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::size_t half = total / 2;
|
||||
bandPath.StartFigure();
|
||||
bandPath.AddLines(ghostSource->band.data() + start, static_cast<INT>(half));
|
||||
bandPath.AddLines(ghostSource->band.data() + start + half,
|
||||
static_cast<INT>(half));
|
||||
bandPath.CloseFigure();
|
||||
}
|
||||
if (bandPath.GetPointCount() > 0)
|
||||
{
|
||||
gd::SolidBrush brush(ToGdiColor(ghostColor));
|
||||
g.FillPath(&brush, &bandPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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)
|
||||
{
|
||||
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));
|
||||
}
|
||||
g.DrawLines(&ghost, points.data() + start, static_cast<INT>(stop - start));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user