修复浮动面板事件穿透 / 自由标记总是预览最近信号 / 固定点位于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:
1 parent
14b18d5c39
commit
df715a035e
6 files changed
+442
-60
No files matched your search
@@ -1346,6 +1346,14 @@ namespace jdi
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 浮动面板:右键属于面板自身,**不得穿透**到下层网格(否则会在面板上弹出
|
||||
// 网格的 Y 轴菜单)。必须先于下方的"绘图区右键 → 网格菜单"判定。
|
||||
if (m_plot.OnRButtonDown(x, y))
|
||||
{
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 绘图区右键:弹出该网格的 Y 轴设置菜单(含上下限输入)。
|
||||
if (m_plot.HitTest(x, y))
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -3021,6 +3021,8 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
|
||||
|
||||
| 2026-10-23 | DeepSeek-V4.1-Flash(**本机**) | **目录规范化(Inc/Src)+「自由标记」常规模式回归普通鼠标 + 两个显示开关 + 序号列/列分隔线 + 行悬停压暗(详见 §12 末尾「2026-10-23 第五轮」与 §11.2 第 221–229 条)**。① **目录规范化**:四个 `*Internal.hpp` 从 `Src` 迁到 `Inc`(`DataModelInternal` / `ApplicationInternal` / `PlotViewInternal` / `SignalPanelInternal`),并在 `Makefile` 固化「`Inc` 只放头文件、`Src` 只放 `.cpp`」;因 include 全是扁平形式,无需改任何 include。② **常规模式 = 普通鼠标**:`ProbeFreeMarker` 在未开启「自由标记点选取」时直接早退(不预览/不画纵向光标/单击不固定),并补上 `if (!hitCurve) continue;`(否则纵向光标整列都在时 `probe.ref` 恒有效 ⇒ 任意位置都能固定);固定点仍恒吸附到真实采样时刻,但“已吸附”圆环只在稀疏判据成立时画。③ **两个显示开关**:「自由标记点选取」下方新增「显示增量 | 显示枚举文本」(默认关闭,半宽同行),只影响已固定点胶囊,预览胶囊恒为“值 = 枚举”。④ **面板六列**:新增恒存在的「序号」列(组内 1 起),枚举列存在时无枚举/对不上的点显示 `N/A`,列间画竖线;悬停某行时其它固定点(数据点 + 胶囊)封顶 50% 不透明度、移出恢复。⑤ **移除固定点 ✕ 的悬停浮动提示**(`PaintFreeMarkerTooltip` 实现与声明全部删除);开启选取模式的网格上信号悬停提示只显示信号名。⑥ **两处缓存键补齐**:`FreeMarkerContents` 混入枚举表指纹(旧键会让加上枚举表后仍显示 `N/A`)、`PanelSlotKey` 混入两个新按钮矩形与开关状态。验证:新增离屏 `.preflight/mark5check` **52/52**(含“预览胶囊像素差 = 0”“行悬停压暗 848px → 移出恢复 0”“选取模式气泡变窄 1023→830”);回归 `markcheck` 67/67、`mark4check` 40/40、`fixcheck` 37/37、`round3check` 31/31、`round4check` 34/34、`round5check` 5/5、`editcheck` 20/20、`convcheck` 18/18、`diag` 19/19,`check` 136/137(唯一失败项为该探针既有的不稳定项,`drift=15818` 与改动前逐位相同);`make release`/`make debug` 零警告;`TargetCheck.exe` **857,600 字节(837.5 KiB)**。 |
|
||||
|
||||
| 2026-10-27 | DeepSeek-V4.1-Flash(**本机**) | **浮动面板事件穿透 + 「自由标记」总是预览最近信号 + 固定点位于 GX/图例之下 + 预览曲线取值插值 + 体积红线 900KB(详见 §12 末尾「2026-10-27 第六轮」与 §11.2 第 230–234 条)**。① **面板右键/中键穿透**:新增 `PlotView::OnRButtonDown`(`TopPanelAtPoint` 命中即置顶并消费,宿主不再弹网格 Y 轴菜单);`OnMButtonDown` 在面板上不再进入平移。② **自由标记探测去掉“必须贴近曲线”的门槛**:两个候选(折线垂直距离 / 同一时刻采样点纵向距离)取更近者 ⇒ 网格内只有一条信号、鼠标离曲线很远时也能预览并单击固定(A/B:恢复旧门限 ⇒ 5 项断言失败)。③ **固定标记点必须位于 GX 徽标与图例之下**:用 `gd::Region::Exclude` 把图例托底与徽标矩形从 `PaintFreeMarker` 的绘制区里排除,胶囊文字在相交时改走 GDI+(GDI 文字不认裁剪);实测图例区域像素差 **0**(旧的“画完再重绘图例”方案残留 240px 抗锯齿差)。④ **预览曲线取值点悬空**:`paintDerivedCapsule` 改为在光标时刻对派生曲线**按时间插值**(先 `ApplyDrawTransform` 再线性插值,与折线逐点一致);阶跃区实测标注点与曲线差 **0.0px**(旧实现偏 36–39px),中心轴/均值滤波/弱峰调整共用同一段代码。⑤ **体积红线由 800KB 放宽到 900KB**(到达阈值时提醒用户即可,不是禁止超过)。验证:离屏临时探针 **25/25**(已删除,不留在仓库);`make release`/`make debug` 零警告;`Target.exe` **860,672 字节(840.5 KiB)**。 |
|
||||
|
||||
## 11. 跨设备协作须知(起源设备视角)
|
||||
|
||||
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
|
||||
@@ -4057,6 +4059,27 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
|
||||
229. ⚠️ **固定点必须落在真实采样时刻上**:`ProbeFreeMarker` 命中曲线时**始终**吸附到最近采样点
|
||||
(`snapToNearestSample`);只有“已吸附”圆环(`probe.snapped`)仍只在稀疏判据 `sparse` 成立时才显示
|
||||
(密采样时吸附量 < 1 像素,圆环常驻会像是画错了)。
|
||||
230. ⚠️⚠️ **浮动面板必须消费右键 / 中键**:`PlotView::OnRButtonDown` 用 `TopPanelAtPoint` 判定,
|
||||
命中即 `BringPanelToFront` 并返回 true(宿主**不得**再弹网格的 Y 轴菜单);`OnMButtonDown`
|
||||
在面板上**不得**进入平移(`m_dragMode = NONE` 后消费)。层序与左键 / 双击完全一致。
|
||||
⚠️ 任何“上层元素”都必须在宿主分发前判定,否则会“在面板上右键却弹出下面网格的菜单”。
|
||||
231. ⚠️⚠️ **「自由标记点选取」的探测没有“必须贴近曲线”的门槛**(用户 2026-10-27 明确要求):
|
||||
总是预览**网格内离鼠标较近的那个信号**的数据点(同一时刻下纵向更近者优先)。
|
||||
实现 = 两个候选取更近:① 鼠标到画出来的折线的垂直距离(阶跃竖直段只有它能命中);
|
||||
② 同一时刻(鼠标时刻吸附到最近采样)该采样点的**纵向距离**。旧实现 `if (!hitCurve) continue;`
|
||||
⇒ 网格里只有一条信号且鼠标离曲线较远时既无预览也无法单击固定。
|
||||
232. ⚠️⚠️ **固定标记点必须位于 GX 编号徽标与图例“之下”**:做法是把 `m_legendHits` 的托底矩形与
|
||||
`LaneBadgeRect` 全部 `gd::Region::Exclude` 出绘制区(`PaintFreeMarker` 开头),
|
||||
**不要**改用“画完标记点再把图例重画一遍”—— 重画的抗锯齿边缘与装饰缓存 `AlphaBlend`
|
||||
的结果不完全一致(实测图例胶囊圆角处仍有 240 像素差)。
|
||||
⚠️ **GDI 文字不认 GDI+ 裁剪**:胶囊文字与遮挡矩形相交时必须走 GDI+(`PaintFreeMarkerCapsule`
|
||||
的 `vectorText` 参数),否则文字会“穿透”到图例上面。
|
||||
233. ⚠️⚠️ **预览曲线的取值必须按时间在相邻采样点之间插值**,且插值要在**套用「调整」变换之后**
|
||||
(`PaintDerivedCurve` 逐顶点 `ApplyDrawTransform` 后用直线连接;绝对值/垂直镜像不是线性的)。
|
||||
只取“最近采样点”的曲线值会让标注点离开曲线(用户 2026-10-27 报的“预览曲线数值点悬空”,
|
||||
实测阶跃区偏 36~39 像素)。`probe` / 中心轴 / 均值滤波 / 弱峰调整共用 `paintDerivedCapsule`。
|
||||
234. ⚠️ **产物体积红线已放宽到 900KB**(用户 2026-10-27):**到达 900KB 必须提醒用户**,
|
||||
以便确认功能带来的体积膨胀是否值得;这不是“不许超过”,旧 800KB 红线作废。
|
||||
|
||||
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)
|
||||
|
||||
@@ -7794,3 +7817,68 @@ DG3 data=0 cycle=0 (空组,一个字符串通道)
|
||||
判断“是否命中曲线/是否有可固定的点”必须看 `probe.ref.IsValid()`;
|
||||
在固定点上做像素 A/B 时,固定点若落在悬浮面板下方(绘图区右侧)像素永远不会变 ⇒ 固定点的时间要放在左侧。
|
||||
|
||||
|
||||
## 本轮(2026-10-27 第六轮,本机):浮动面板事件穿透 + 自由标记“总是预览最近信号” + 固定点位于 GX/图例之下 + 预览曲线取值插值 + 体积红线 900KB
|
||||
|
||||
> 用户需求(逐条):
|
||||
> ① 浮动面板上右键会触发下方网格的右键菜单;面板上的点击 / 右键 / 拖拽等操作不得穿透到下层。
|
||||
> ② 「自由标记点选取」应**总是**预览网格内离鼠标较近的那条信号的数据点(同一时刻 A 比 B 更近 ⇒ 预览 A),
|
||||
> 现状是“即使只有一个信号,也必须把鼠标贴到曲线附近才会出现预览点”,远离曲线时无法预览、也无法左键固定。
|
||||
> ③ 固定数据标记点不得遮挡网格的 GX 标签与图例:数据点与数据显示应位于它们**下方**。
|
||||
> ④ 预览曲线(均值滤波等)的光标数值点会“悬空”(光标落在两个采样点之间时不落在曲线上),要求在所有带预览曲线的功能中修好。
|
||||
> ⑤ 产物体积红线由 800KB 放宽到 **900KB**(到达阈值提醒即可,不是禁止超过)。
|
||||
|
||||
### A. 浮动面板不得被右键 / 中键穿透(§11.2 第 230 条)
|
||||
|
||||
- 新增 `PlotView::OnRButtonDown(int x, int y)`:`TopPanelAtPoint` 命中即 `BringPanelToFront` + `m_dragMode = NONE`,
|
||||
返回 true(该次右键被面板消费)。`Application::WM_RBUTTONDOWN` 在“绘图区右键 → 网格菜单”**之前**调用它。
|
||||
- `PlotView::OnMButtonDown` 开头同样判定浮动面板:命中即消费,**不进入平移**(否则会在面板上按住中键拖走整个绘图区)。
|
||||
- 左键 / 双击的层序原本就正确(`OnLButtonDown` 的面板分支、`OnDoubleClick` 的面板分支),本轮未改。
|
||||
|
||||
### B. 「自由标记」总是预览网格内离鼠标较近的信号(§11.2 第 231 条)
|
||||
|
||||
- `ProbeFreeMarker` 删掉 `if (!hitCurve) continue;`,改成**两个候选取更近**:
|
||||
* 曲线候选 = 鼠标到画出来的折线的垂直距离(阶跃竖直段只有它能命中);
|
||||
* 采样候选 = 鼠标时刻吸附到最近采样后,该采样点的**屏幕纵向距离**。
|
||||
两者都不设“必须命中”的门槛,`tolerance` 只用于决定是否画“已吸附”圆环(`snapped = sparse`)。
|
||||
- 逐信号的 `distance` 取两者的较小值,因此“同一时刻下谁离鼠标更近就预览谁”在单信号与多信号下都成立。
|
||||
- `snapToNearestSample` 仍然对两个候选生效 ⇒ 固定点始终落在真实采样时刻上(第 229 条不变)。
|
||||
|
||||
### C. 固定标记点位于 GX 徽标与图例之下(§11.2 第 232 条)
|
||||
|
||||
- `PaintFreeMarker` 开头(在 `g.SetClip(m_plotRect, Intersect)` 之后)收集遮挡矩形:`m_legendHits` 的全部托底 +
|
||||
每个网格的 `LaneBadgeRect`,逐个 `gd::Region::Exclude` 出当前裁剪区,再 `SetClip(..., CombineModeReplace)`。
|
||||
标记点(圆点 / 胶囊 / ✕ / 预览胶囊)因此**完全画不进**这些区域,图例与徽标的像素(含胶囊圆角之外的抗锯齿边)保持原样。
|
||||
- ⚠️ **GDI 文字不认 GDI+ 裁剪**:`PaintFreeMarkerCapsule` 新增 `vectorText` 参数,胶囊矩形与任一遮挡矩形相交时
|
||||
强制走 GDI+(`DrawFreeMarkerAlphaText`),否则文字会盖到图例上面。
|
||||
- ⚠️ 试过并放弃的替代方案:画完标记点后**把图例原样重画一遍**(`PaintLegendOverlay`)。
|
||||
它与装饰缓存的 `AlphaBlend` 结果不完全一致,实测图例胶囊圆角处残留 **240 像素**差;纯裁剪方案为 **0**。
|
||||
- 只在“本网格确实有固定点 / 探针正停在本网格”时才做区域运算,没有标记时零开销。
|
||||
|
||||
### D. 预览曲线取值点落回曲线(§11.2 第 233 条)
|
||||
|
||||
- `paintDerivedCapsule` 不再取“最近采样点”的曲线值,改为在光标准确时刻对派生曲线**按时间线性插值**:
|
||||
`locate(displayTime)` 用二分求两侧采样下标与比例(非单调时间轴退回最近采样),
|
||||
再**先 `ApplyDrawTransform` 后插值**(绝对值 / 垂直镜像不是线性变换,先插值会偏离折线)。
|
||||
- 请求窗口由“用到的那些源下标”拼出(仍收窄,不会因光标远离视界而重算整条曲线)。
|
||||
- 双光标增量(`(+Δ 中心轴)`)同样走插值;显示数值现在也是**变换后**的值(与画出来的曲线一致)。
|
||||
- 中心轴 / 均值滤波 / 弱峰调整共用同一段代码,因此三处一起修好。
|
||||
|
||||
### E. 体积红线 900KB(§11.2 第 234 条)
|
||||
|
||||
- `Makefile` 的 `RELEASE_OPT` 注释更新:**到达 900KB 必须提醒用户**(不是禁止超过),旧 800KB 红线作废。
|
||||
|
||||
### F. 验证(全离屏,未占用鼠标 / 键盘 / 屏幕)
|
||||
|
||||
- 临时离屏探针(仓库外 `%TEMP%\jdi_probe`,链接 `Build/Release/*.o` 去掉 `Main.o`)**25/25**:
|
||||
* [A] 面板上右键被 `PlotView` 消费且**不弹菜单**、面板外右键仍弹菜单;面板上中键不进入平移、面板外仍平移;
|
||||
* [B] 单信号 + 鼠标远离曲线 ⇒ `probe.ref` 有效、单击可固定、时间吸附到采样;两信号同网格 ⇒ 视鼠标高度分别选中 hold / step;
|
||||
* [C] 加一个会压在图例上的固定点前后两帧:图例托底 + GX 徽标矩形内像素差 **0**,同时整幅差异 > 5 万像素(证明标记点确实画了);
|
||||
* [D] `SCATTER_TEST.csv` 的 step、视界 0.998056–1.00091、光标 0.999277、均值点数 2 / 迭代 1:
|
||||
均值滤波 0.0px、中心轴 0.0px、弱峰调整 0.2px(与曲线逐像素贴合);插值点与最近采样点相差 39px / 197px。
|
||||
- **A/B 判别力**:临时恢复 `if (!hitCurve) continue;` ⇒ [B] 5 项如实失败;临时把插值改回“最近采样点” ⇒ 均值/中心轴各自偏 36px 并如实失败。
|
||||
- `make release` / `make debug` **零警告**;`Target.exe` **860,672 字节(840.5 KiB < 900KB)**,比上一轮 +3 KB。
|
||||
- ⚠️ 探针坑:`CycleCursorMode()` 是**循环**(无→单→双→无),在多组用例里每组都调一次会让后几组“没有光标”而误判为失败;
|
||||
面板 / `LaneVisual` / `LegendHit` / `EdgePanelLayout` 都是 `PlotView` 的嵌套类型,探针里必须写 `PlotView::` 前缀。
|
||||
- ⚠️ 本轮开始前发现工作区里 `PlotViewPaint.cpp` 与 `Documents/agent_mem.md` 被**回退到 `8f735d5`**(丢失第四/五轮全部改动),
|
||||
已 `git checkout --` 恢复到 `HEAD` 后再施工;提交前务必确认这两份文件与 HEAD 一致。
|
||||
@@ -12,7 +12,9 @@ SUB_DIR_DEBUG = Debug
|
||||
|
||||
# Release 优化等级
|
||||
# ⚠️ 用 `-Oz` 而不是 `-Os`:本项目热点全在 GDI/GDI+ 绘制上,`-Oz` 与 `-Os` 的实测帧耗时
|
||||
# 没有可辨识差异,但产物小 ~6KB(用户 2026-10-19 要求 Release 二进制 < 800KB)。
|
||||
# 没有可辨识差异,但产物小 ~6KB。
|
||||
# ⚠️ 产物体积上限(用户 2026-10-27 更新):**900KB**。这不是"不许超过",而是**到达 900KB
|
||||
# 就必须提醒用户**,以便确认功能带来的体积膨胀是否值得(旧红线 800KB 已作废)。
|
||||
RELEASE_OPT = -Oz
|
||||
|
||||
# Debug 优化等级
|
||||
|
||||
Reference in new issue
Block a user