修复浮动面板事件穿透 / 自由标记总是预览最近信号 / 固定点位于GX与图例之下 / 预览曲线取值插值;体积红线放宽到900KB

1) 浮动面板不得被右键与中键穿透(用户报告:在面板上右键会弹出下面网格的右键菜单)
   - 新增 PlotView::OnRButtonDown:TopPanelAtPoint 命中即置顶并消费该次右键;
     Application::WM_RBUTTONDOWN 在「绘图区右键 → 网格菜单」之前调用它。
   - OnMButtonDown 开头同样判定面板:命中即消费,不再进入平移。

2) 「自由标记点选取」总是预览网格内离鼠标较近的信号(用户报告:单信号时也必须贴近曲线)
   - ProbeFreeMarker 删除 `if (!hitCurve) continue;`,改为两候选取更近:
     ① 鼠标到画出来的折线的垂直距离(阶跃竖直段只有它能命中);
     ② 同一时刻(鼠标时刻吸附最近采样)采样点的屏幕纵向距离。
   - 门槛 tolerance 只用于「已吸附」圆环;固定点仍恒吸附到真实采样时刻。

3) 固定标记点位于 GX 编号徽标与图例之下(用户要求:数据显示不得遮挡它们)
   - PaintFreeMarker 把 m_legendHits 托底矩形与 LaneBadgeRect 用 gd::Region::Exclude
     排除出绘制区;仅在确有固定点/预览时才计算。
   - PaintFreeMarkerCapsule 新增 vectorText:胶囊与遮挡矩形相交时文字改走 GDI+
     (GDI DrawTextW 不认 GDI+ 裁剪区)。

4) 预览曲线(中心轴/均值滤波/弱峰调整)的光标取值点落回曲线
   - paintDerivedCapsule 改为在光标准确时刻按时间线性插值,且先 ApplyDrawTransform 再插值
     (绝对值/垂直镜像非线性,先插值会偏离折线);双光标增量同样插值。

5) 产物体积红线由 800KB 放宽到 900KB(到达阈值时提醒,不是禁止超过)。

验证(全离屏,未占用鼠标/键盘/屏幕):仓库外临时探针 25/25(A 面板右键/中键不穿透且面板外仍生效;
B 远离曲线可预览与单击固定、两信号按纵向距离择优;C 图例与 GX 区域像素差 0、整幅差异 5 万像素;
D 阶跃区标注点与曲线差 0.0px,插值点与最近采样点相差 39px/197px);A/B 恢复旧门槛 ⇒ 5 项失败,
改回最近采样取值 ⇒ 均值/中心轴各偏 36px 失败。make release / make debug 零警告;
Target.exe 860,672 字节(840.5 KiB)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-27 20:14:49 +08:00
1 parent 14b18d5c39
commit df715a035e
6 files changed
+442 -60

No files matched your search

+17 -1
View File
@@ -224,6 +224,18 @@ namespace jdi
*/
bool OnLButtonUp(int x, int y);
/**
* @brief 处理鼠标右键按下(**只处理浮动面板**)。
*
* 浮动面板压在波形之上,属最上层元素:右键落在面板上必须被面板消费,
* 不得穿透到下面的网格(否则会在面板上弹出网格的 Y 轴菜单)。
*
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 命中浮动面板(已消费该次右键)返回 true;否则返回 false,由宿主继续处理。
*/
bool OnRButtonDown(int x, int y);
/**
* @brief 处理鼠标中键按下:与左键在常规模式下一致,进入自由平移(水平平移时间轴 +
* 纵向偏移当前网格幅值)。**任何缩放模式下都可用**,用于在不改变当前模式的
@@ -1969,10 +1981,14 @@ namespace jdi
* @param [IN] close ✕ 按钮矩形(宽度为 0 表示不画)。
* @param [IN] closeHot ✕ 是否处于悬停态。
* @param [IN] alphaScale 整体不透明度系数(1 = 不透明;固定点受「标记透明度」控制)。
* @param [IN] vectorText 强制用 GDI+(`DrawString`)画文字,即使完全不透明。
* ⚠️ GDI `DrawTextW`(`UiText`)**不认 GDI+ 裁剪区**;胶囊压在网格的 GX 标签 /
* 图例上时必须走 GDI+,否则文字会“穿透”到图例上方(用户 2026-10-27 要求标记点
* 显示在 GX 标签与图例之下)。正常情形仍用 `UiText`(与既有像素保持一致)。
*/
void PaintFreeMarkerCapsule(gd::Graphics& g, const gd::RectF& label, const std::wstring& text,
std::uint32_t color, const gd::RectF& close, bool closeHot,
float alphaScale = 1.0f);
float alphaScale = 1.0f, bool vectorText = false);
/**
* @brief 计算某个固定点相对**同信号**前一个固定点的增量与其时间间隔。
@@ -2511,6 +2511,19 @@ namespace jdi
return false;
}
// 浮动面板压在波形之上:中键落在面板上时**不穿透**为平移,直接消费掉。
// (否则在面板上按住中键拖动会把整个绘图区拖走,用户 2026-10-27 报告的交互穿透。)
{
int panelLane = -1;
int panelKind = 0;
if (TopPanelAtPoint(static_cast<float>(x), static_cast<float>(y), panelLane, panelKind))
{
BringPanelToFront(panelLane, panelKind);
m_dragMode = PlotDragMode::NONE;
return true;
}
}
// 中键:无论当前是否处于选区放大模式,都直接进入自由平移
// (不改变选区放大模式本身,松手后仍可继续框选)。
m_dragStartX = x;
@@ -2703,6 +2716,28 @@ namespace jdi
return changed;
}
bool PlotView::OnRButtonDown(int x, int y)
{
if (!HasDrawableContent())
{
return false;
}
// 浮动面板是绘图区内**最上层**的元素(与左键、双击的层序完全一致):
// 右键落在面板上必须被面板消费,绝不能穿透到下面的网格 —— 否则用户在面板上
// 右键会在面板底下弹出一整个网格的 Y 轴菜单(用户 2026-10-27 报告)。
int panelLane = -1;
int panelKind = 0;
if (!TopPanelAtPoint(static_cast<float>(x), static_cast<float>(y), panelLane, panelKind))
{
return false;
}
// 与左键一致:点中哪个面板就把它置顶(重叠时右键总是作用在用户看到的最上层那个)。
BringPanelToFront(panelLane, panelKind);
m_dragMode = PlotDragMode::NONE;
return true;
}
bool PlotView::OnMouseWheel(int delta, bool shift)
{
if (!HasDrawableContent() || delta == 0)
+291 -58
View File
@@ -5109,40 +5109,122 @@ namespace jdi
int iterations, int amountPercent,
const std::wstring& suffix)
{
const int sampleIndex = FindNearestSample(*resolved.axis, time);
if (sampleIndex < 0)
// ⚠️⚠️ 取派生曲线在**光标准确时刻**的取值(相邻两个采样点之间按时间线性插值),
// 而不是“最近采样点”的曲线值。旧实现取最近采样点 ⇒ 光标落在两个采样点
// 之间时,标注点会脱离画出来的折线(用户 2026-10-27 报告的“预览曲线数值点悬空”)。
// ⚠️ 插值必须在**屏幕空间**(即先套用「调整」变换再线性插值):
// `PaintDerivedCurve` 逐顶点取 `ApplyDrawTransform` 后用直线连接,
// 而绝对值 / 垂直镜像不是线性变换,先插值再变换会偏离折线。
const std::size_t total =
std::min(resolved.axis->time.size(), resolved.data->values.size());
if (total == 0)
{
return;
}
const int otherIndex =
showDelta ? FindNearestSample(*resolved.axis, otherTime) : -1;
// ⚠️ 派生曲线是在 `signal.values` 的**源下标顺序**上计算的(`ComputeDerivedWindow`
// 直接切 `values[windowBegin..windowEnd)`),而上面拿到的是**时间轴/X 方向**下标。
// 水平镜像(数据反序)时两者相反 ⇒ 必须经 `SourceIndexFor` 映射,否则光标
// 会读到镜像另一端的派生取值(曲线与信号对不上)。
const std::size_t sourceCount = resolved.data->values.size();
const int sourceSample = static_cast<int>(
SourceIndexFor(*resolved.style, sourceCount, static_cast<std::size_t>(sampleIndex)));
const int sourceOther =
(otherIndex >= 0)
? static_cast<int>(SourceIndexFor(*resolved.style, sourceCount,
static_cast<std::size_t>(otherIndex)))
: -1;
// 本函数**只需要光标所在采样点处的派生取值**(以及双光标另一端的取值),
// 因此把请求窗口收窄到「需要的那几个下标」即可,均值滤波半径造成的取值
// 依赖由 `CurveSpanFor` 自身的保护带负责覆盖。
/** 某个显示时刻两侧的采样点(时间序下标)与插值比例。 */
struct CurveSampleRef
{
int first = -1;
int second = -1;
double fraction = 0.0;
};
const auto locate = [&](double displayTime) -> CurveSampleRef
{
CurveSampleRef ref;
const double target = displayTime - resolved.style->offsetTime;
if (resolved.axis->monotonic)
{
const auto lower = std::lower_bound(
resolved.axis->time.begin(),
resolved.axis->time.begin() + static_cast<std::ptrdiff_t>(total),
target);
const std::size_t hi = static_cast<std::size_t>(
lower - resolved.axis->time.begin());
if (hi >= total)
{
ref.first = ref.second = static_cast<int>(total - 1);
return ref;
}
if (hi == 0)
{
ref.first = ref.second = 0;
return ref;
}
ref.first = static_cast<int>(hi - 1);
ref.second = static_cast<int>(hi);
const double t0 =
resolved.axis->time[static_cast<std::size_t>(ref.first)];
const double t1 =
resolved.axis->time[static_cast<std::size_t>(ref.second)];
ref.fraction =
(t1 > t0)
? std::max(0.0, std::min(1.0, (target - t0) / (t1 - t0)))
: 0.0;
return ref;
}
// 非单调时间轴:无法按时间插值,退回最近采样点。
const int nearest = FindNearestSample(*resolved.axis, target);
if (nearest < 0 || static_cast<std::size_t>(nearest) >= total)
{
return CurveSampleRef();
}
ref.first = ref.second = nearest;
return ref;
};
// 时间序下标 → 源下标。派生曲线是在 `signal.values` 的**源下标顺序**上计算的
//(`ComputeDerivedWindow` 直接切 `values[windowBegin..windowEnd)`),
// 水平镜像(数据反序)时两者相反 ⇒ 必须经 `SourceIndexFor` 映射。
const auto sourceIndexAt = [&](int timeIndex) -> int
{
if (timeIndex < 0 ||
static_cast<std::size_t>(timeIndex) >= sourceCount)
{
return -1;
}
return static_cast<int>(SourceIndexFor(
*resolved.style, sourceCount, static_cast<std::size_t>(timeIndex)));
};
const CurveSampleRef here = locate(time);
if (here.first < 0)
{
return;
}
const CurveSampleRef other = showDelta ? locate(otherTime) : CurveSampleRef();
// 本函数**只需要光标附近那几个下标的派生取值**,因此把请求窗口收窄到
// 「需要的那几个下标」即可,均值滤波半径造成的取值依赖由 `CurveSpanFor`
// 自身的保护带负责覆盖。
//
// ⚠️ 绝不能把窗口扩成「光标 ↔ 视界边缘」的并集:光标停在视界之外(例如
// 平移视界把光标甩到很远,或光标远离当前缩放区间)时会得到一个上千万采样的
// 巨大窗口,超过缓存上限后走一次性缓冲 → **每帧重算整段滤波**,
// 实测单帧 300ms+(用户报告的"平移/拖动光标异常卡顿"的直接原因)。
std::size_t begin = static_cast<std::size_t>(sourceSample);
std::size_t end = begin + 1;
if (sourceOther >= 0)
std::size_t begin = sourceCount;
std::size_t end = 0;
const auto includeTime = [&](int timeIndex)
{
const std::size_t other = static_cast<std::size_t>(sourceOther);
begin = std::min(begin, other);
end = std::max(end, other + 1);
const int source = sourceIndexAt(timeIndex);
if (source < 0)
{
return;
}
const std::size_t index = static_cast<std::size_t>(source);
begin = std::min(begin, index);
end = std::max(end, index + 1);
};
includeTime(here.first);
includeTime(here.second);
if (other.first >= 0)
{
includeTime(other.first);
includeTime(other.second);
}
if (end <= begin)
{
return;
}
std::size_t spanBegin = 0;
const std::vector<float>* curvePtr =
@@ -5162,8 +5244,51 @@ namespace jdi
return static_cast<double>((*curvePtr)[static_cast<std::size_t>(index) -
spanBegin]);
};
const double derivedValue = curveValueAt(sourceSample);
if (!std::isfinite(derivedValue))
// 求某个显示时刻的「画出来的曲线值」(已套用「调整」变换,与折线逐点一致)。
const auto curveDisplayedValue = [&](const CurveSampleRef& ref,
double& out) -> bool
{
const int source0 = sourceIndexAt(ref.first);
if (source0 < 0)
{
return false;
}
double value0 = curveValueAt(source0);
if (!std::isfinite(value0))
{
return false;
}
value0 = ApplyDrawTransform(*resolved.style, value0);
if (!std::isfinite(value0))
{
return false;
}
if (ref.second == ref.first || ref.fraction <= 0.0)
{
out = value0;
return true;
}
const int source1 = sourceIndexAt(ref.second);
if (source1 < 0)
{
return false;
}
double value1 = curveValueAt(source1);
if (!std::isfinite(value1))
{
return false;
}
value1 = ApplyDrawTransform(*resolved.style, value1);
if (!std::isfinite(value1))
{
return false;
}
out = value0 + (value1 - value0) * ref.fraction;
return true;
};
double derivedValue = 0.0;
if (!curveDisplayedValue(here, derivedValue))
{
return;
}
@@ -5181,8 +5306,8 @@ namespace jdi
centerText += L"(";
if (showDelta)
{
const double otherDerived = curveValueAt(sourceOther);
if (std::isfinite(otherDerived))
double otherDerived = 0.0;
if (curveDisplayedValue(other, otherDerived))
{
const double delta = derivedValue - otherDerived;
centerText += (delta < 0.0) ? L"-" : L"+";
@@ -6963,7 +7088,8 @@ namespace jdi
void PlotView::PaintFreeMarkerCapsule(gd::Graphics& g, const gd::RectF& label,
const std::wstring& text, std::uint32_t color,
const gd::RectF& close, bool closeHot, float alphaScale)
const gd::RectF& close, bool closeHot, float alphaScale,
bool vectorText)
{
const float scale = std::max(0.0f, std::min(1.0f, alphaScale));
const auto scaled = [scale](std::uint32_t argb, float base) -> std::uint32_t {
@@ -6977,13 +7103,14 @@ namespace jdi
}
UiFillRoundRect(g, label, 3.0f * m_scale, scaled(color, 0xF0));
DrawLabelOutline(g, label, 3.0f * m_scale, scaled(ink.outline, 255.0f));
if (scale >= 0.999f)
if (!vectorText && scale >= 0.999f)
{
UiText(g, text, m_fonts->monoSmall, label, ink.text, TextAlign::CENTER);
}
else
{
// GDI 文本忽略 alpha ⇒ 半透明时必须改用 GDI+ 才能真的淡出(见上面说明)。
// GDI 文本忽略 alpha **也不认 GDI+ 裁剪区** ⇒ 半透明、或压在 GX 标签/图例上时
// 必须改用 GDI+ 才能真的淡出 / 被裁剪(见 `vectorText` 参数说明)。
DrawFreeMarkerAlphaText(g, text, m_fonts->monoSmall, label, scaled(ink.text, 255.0f));
}
if (close.Width <= 0.0f)
@@ -7017,6 +7144,70 @@ namespace jdi
const std::vector<Lane>& tabLanes = m_tab->Lanes();
const std::size_t count = std::min(lanes.size(), tabLanes.size());
// ---- (0) **图层遮挡**:GX 编号徽标与图例条目必须压在所有标记点之上 ----
// 用户 2026-10-27 明确要求:固定标记点(数据点 + 取值胶囊)不得遮挡网格的 GX 标签与图例。
// 图例与徽标画在静态场景 / 装饰缓存里,而标记点画在贴图之后,因此这里从**绘制区域**里
// 把这些矩形整体排除掉(GDI+ 区域裁剪):标记点真正位于它们“下方”,被盖住的部分不画,
// 图例/徽标像素保持原样(连圆角之外的抗锯齿边缘都不会被标记点渗出)。
// ⚠️ GDI 文字(`UiText`)不认 GDI+ 裁剪区 ⇒ 胶囊文字与遮挡矩形相交时改走 GDI+
//(`PaintFreeMarkerCapsule(..., vectorText=true)`)。
// ⚠️ 只为**确实有标记点/预览**的网格做排除,避免每帧对全部网格做区域运算。
std::vector<gd::RectF> occluders;
{
bool needExclusion = (m_freeMarker.valid && m_freeMarker.ref.IsValid());
for (std::size_t index = 0; !needExclusion && index < count; ++index)
{
needExclusion = !tabLanes[index].freeMarkerItems.empty();
}
if (needExclusion)
{
for (const LegendHit& hit : m_legendHits)
{
occluders.push_back(hit.rect);
}
for (std::size_t index = 0; index < count; ++index)
{
const gd::RectF badge = LaneBadgeRect(static_cast<int>(index));
if (badge.Width > 0.0f && badge.Height > 0.0f)
{
occluders.push_back(badge);
}
}
gd::Region clip;
if (!occluders.empty() && g.GetClip(&clip) == gd::Ok)
{
for (const gd::RectF& rect : occluders)
{
const int x0 = static_cast<int>(std::floor(rect.X));
const int y0 = static_cast<int>(std::floor(rect.Y));
const int x1 = static_cast<int>(std::ceil(rect.X + rect.Width));
const int y1 = static_cast<int>(std::ceil(rect.Y + rect.Height));
if (x1 <= x0 || y1 <= y0)
{
continue;
}
gd::Region exclude(gd::Rect(x0, y0, x1 - x0, y1 - y0));
clip.Exclude(&exclude);
}
g.SetClip(&clip, gd::CombineModeReplace);
}
}
}
// 某个矩形是否与任一遮挡矩形相交(用于决定胶囊文字是否必须走可裁剪的 GDI+ 路径)。
const auto overlapsOccluder = [&occluders](const gd::RectF& rect) -> bool
{
for (const gd::RectF& occluder : occluders)
{
if (rect.X < occluder.X + occluder.Width && occluder.X < rect.X + rect.Width &&
rect.Y < occluder.Y + occluder.Height && occluder.Y < rect.Y + rect.Height)
{
return true;
}
}
return false;
};
// ---- (1) 已固定的数据点(含各自的 ✕)----
// ⚠️ 固定点**不画纵向细线**(2026-10-21 要求),且取值胶囊**就在原位显示**、
// 不做重叠避让(用户 2026-10-22 明确要求恢复初版方案:允许遮挡)。
@@ -7108,7 +7299,9 @@ namespace jdi
{
capsuleAlpha = 1.0f;
}
PaintFreeMarkerCapsule(g, label, text, signalColor, close, hot, capsuleAlpha);
// 胶囊压在 GX 标签 / 图例上时文字必须走 GDI+(GDI 文字不认裁剪区)。
PaintFreeMarkerCapsule(g, label, text, signalColor, close, hot, capsuleAlpha,
overlapsOccluder(label));
if (rowHot)
{
UiDrawRoundRect(g, label, 3.0f * m_scale, m_theme->accent, 2.0f * m_scale);
@@ -7185,7 +7378,9 @@ namespace jdi
top = std::max(clipTop + 1.0f * m_scale,
std::min(top, clipBottom - textHeight - 1.0f * m_scale));
const gd::RectF label(left, top, textWidth, textHeight);
PaintFreeMarkerCapsule(g, label, text, signalColor, gd::RectF(), false);
// 同样:压在 GX 标签 / 图例上时文字必须走 GDI+(否则会盖在图例上)。
PaintFreeMarkerCapsule(g, label, text, signalColor, gd::RectF(), false, 1.0f,
overlapsOccluder(label));
// 吸附提示:命中自动点线判据时数据点被吸附到采样点,用一圈描边示意
//(与"可拖拽"的光晕区分:这里只是单点圆环,不描整条曲线)。
@@ -7387,38 +7582,76 @@ namespace jdi
}
}
// ---- ② 决定标注时刻:先取曲线上的最近点,再吸附到采样点 ----
const bool hitCurve = (distance <= static_cast<double>(tolerance));
// ⚠️ 只有**贴着曲线**的那根信号才是可标注目标:否则整列预览时会把“离鼠标很远
// 的那根曲线的采样值”当成预览值(`probe.ref` 会一直有效 ⇒ 任意位置单击都能固定)。
if (!hitCurve)
// ---- ② 决定标注时刻 ----
// ⚠️⚠️ 用户 2026-10-27 明确要求:**总是**预览「网格内离鼠标较近的那个信号」的数据点
//(同一时刻下 A 信号比 B 信号离鼠标更近 ⇒ 预览 A、单击固定的也是 A),
// **不要求鼠标贴近曲线**。
// 旧实现在两种距离都超出容差时直接 `continue`,于是「网格里只有一条信号、鼠标离曲线
// 较远」时既没有预览点,也无法用左键固定(用户报告的核心问题)。
//
// 因此这里准备**两个候选**,取更近的那一个:
// * 曲线候选 = 鼠标到**画出来的折线**的垂直距离(阶跃的竖直跳变段只有它能命中);
// * 采样候选 = 同一时刻(鼠标时刻吸附到最近采样)该采样点在屏幕上的**纵向距离**。
// 两者都不再设“必须命中”的门槛;门槛(`tolerance`)只用来决定是否画「已吸附」圆环。
const bool interpolate = (resolved.style->mode != DrawMode::SCATTER);
double time = 0.0;
double sampled = 0.0;
bool candidateOk = false;
// (a) 曲线候选
{
continue;
}
double time = XToTime(nearestX);
// ⚠️ **命中曲线时始终吸附到最近的采样点**:固定点必须落在真实采样时刻上
//(第四轮的硬约束;否则面板里的「标记值」会变成插值出来的中间值)。
// ⚠️ 但表示“已吸附”的那个圆环**只在「自动点线」判据成立(`sparse`)时**才画:
// 密采样信号里吸附量远小于 1 像素,圆环会一直挂在那里,反而像是画错了。
// ⚠️ 鼠标**没有**命中任何曲线时(上面已 `continue`)不存在吸附问题:那时的纵向
// 光标由 `probe.time`(鼠标横坐标)驱动,光标不能自己跳到采样点上去。
bool snapped = false;
{
double candidate = 0.0;
if (snapToNearestSample(*resolved.axis, *resolved.data, *resolved.style, time, candidate))
double candidateTime = XToTime(nearestX);
if (snapToNearestSample(*resolved.axis, *resolved.data, *resolved.style, candidateTime,
candidateTime))
{
time = candidate;
snapped = sparse;
double candidateSample = 0.0;
if (SampleDisplayedSignalAt(*resolved.axis, *resolved.data, *resolved.style,
candidateTime, interpolate, candidateSample) &&
std::isfinite(candidateSample))
{
time = candidateTime;
sampled = candidateSample;
candidateOk = true;
}
}
}
const bool interpolate = (resolved.style->mode != DrawMode::SCATTER);
double sampled = 0.0;
if (!SampleDisplayedSignalAt(*resolved.axis, *resolved.data, *resolved.style, time,
interpolate, sampled) ||
!std::isfinite(sampled))
// (b) 采样候选:同一时刻的纵向距离(“哪个信号离鼠标更近”的主判据)
{
double candidateTime = probe.time;
if (snapToNearestSample(*resolved.axis, *resolved.data, *resolved.style, candidateTime,
candidateTime))
{
double candidateSample = 0.0;
if (SampleDisplayedSignalAt(*resolved.axis, *resolved.data, *resolved.style,
candidateTime, interpolate, candidateSample) &&
std::isfinite(candidateSample))
{
const double candidateValue =
ApplyDrawTransform(*resolved.style, candidateSample);
if (std::isfinite(candidateValue))
{
const double vertical = std::fabs(
static_cast<double>(ValueToY(candidateValue, visual)) -
static_cast<double>(fy));
if (!candidateOk || vertical < distance)
{
distance = vertical;
nearestX = fx;
time = candidateTime;
sampled = candidateSample;
candidateOk = true;
}
}
}
}
}
if (!candidateOk)
{
continue;
}
// 固定点必须落在真实采样时刻上(两个候选都已吸附到采样点)。
// 表示“已吸附”的圆环仍只在「自动点线」判据成立(`sparse`)时画:
// 密采样信号里吸附量远小于 1 像素,圆环会一直挂着,反而像是画错了。
const bool snapped = sparse;
const double value = ApplyDrawTransform(*resolved.style, sampled);
if (!std::isfinite(value))
{