主题自适应配色 + 预览曲线连续配色 + 样式区复位按钮 + 均值滤波面板重做 + 面板缓存环带残留 + 调试层序 + 自由标记选取与性能
① 主题自适应(`Theme.cpp`):新增 `SrgbLuminance` / `ContrastRatio` / `EnsureContrast` /
`MixToLuminance` / `ThemeLaneBackground`;四套主题的结构色(gridWeak 1.30 / gridStrong 1.65 /
axisLine 2.10 / borderStrong 1.70)按底色自动校正,Solarised Light 逐位不变;网格叠底改用
`ThemeLaneBackground()`(4 处),深色主题下网格层与绘图区底色可辨。
② 派生预览曲线(`HighContrastDerived`)重写为**连续**方案:方向沿信号所在侧远离底色,
目标亮度 = max(与底色对比度 3.2, 信号亮度 ± 0.18),再按亮度平滑混色;调用点传入真实网格底色
(旧实现默认写死 Solarised Light 底色 ⇒ 深色主题永远选到更暗的那一侧、几乎看不见)。
③ 信号样式区「线宽 / 透明度」两行右侧新增**复位按钮**(`UiPaintResetIcon` 成为唯一实现,
菜单/弱峰面板/样式区共用同一套笔画);默认线宽统一到 `DefaultSignalLineWidth(finiteCount)`
(单点信号取 10.0)。按钮右边界与内容列对齐,内容列过窄时整列不显示。
④ 「均值滤波」面板重做:信号色叠底标题栏 + 复选框 + 展开三角 + 「均值点数 / 迭代次数」两行滑块
(点击数值可改滑块上限、右侧复位)+ 内容区滚动;新增 `MeanFilterItem::{maxPoints,maxIterations,
collapsed}` 与 `Lane::meanFilterScroll`,缓存键同步补全。
⑤ 浮动面板命中像素缓存时只贴**面板圆角形状之内**的像素(`CreateRoundRectRgn` 裁剪)并按本帧重画投影
⇒ 中键移视界后面板周围不再出现一圈旧波形。
⑥ 显示布局边界:每个浮动面板**独占一个绘制层**(按 `m_panelOrder` 递增),绘制侧
`UiSetTextLayer` 在同一循环内同步推进 ⇒ 下层面板的边框/文字不再画到上层面板上。
⑦ 自由标记:采样候选距离改为**二维欧氏距离**(单点信号不再在整幅宽度内吸住光标),
纵向光标画在鼠标位置、圆点落在真实采样点(新增 `FreeMarkerProbe::pointX`);
`MinorCellSparse` 只对胜出信号算一次;折线缓存容量改为 `max(64, 信号数×2)` 且查找改为
单键哈希 ⇒ MF4 的 285 信号合并网格中 `ProbeFreeMarker` 平均 0.11ms。
验证:`.preflight` 探针重建(`panelcheck` 71/71、`markercheck` 13/13、`ringcheck` 6/6,合计 90 条,
LTO 目标文件上复跑同样全绿);`make release` / `make debug` 零警告;产物 815,104 B。
文档:`agent_mem.md` 第十六轮(含补丁脚本与编码/行尾踩坑)。
This commit is contained in:
1 parent
71195c4c48
commit
bdd703b88b
24 files changed
+1815
-256
No files matched your search
@@ -69,6 +69,17 @@ namespace jdi
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + nameWidth +
|
||||
kEdgePanelMeanCheckSize * m_scale + 6.0f * m_scale);
|
||||
}
|
||||
if (meanFilter)
|
||||
{
|
||||
// 「均值滤波」的条目 = **信号色叠底标题栏**(三角 + 名称 + 复选框)
|
||||
// 与两行滑块(标签 + 轨道 + 数值 + 复位按钮),右侧还要给滚动条留位。
|
||||
const float groupWidth = kEdgePanelPad * 2.0f * m_scale +
|
||||
kEdgePanelDisclosureWidth * m_scale + 6.0f * m_scale +
|
||||
nameWidth + 6.0f * m_scale +
|
||||
kEdgePanelMeanCheckSize * m_scale +
|
||||
(kFreeMarkerScrollWidth + 4.0f) * m_scale;
|
||||
width = std::max(width, groupWidth);
|
||||
}
|
||||
std::size_t rising = 0;
|
||||
std::size_t falling = 0;
|
||||
bool complete = true;
|
||||
@@ -116,7 +127,7 @@ namespace jdi
|
||||
if (centerAxis || meanFilter)
|
||||
{
|
||||
// 参数行 = 两个「说明文字 + 数值框」,必须保证文字不被数值框压住。
|
||||
// (MEAN / CENTER:「均值点数 X」+「迭代次数 N」。标签与数值框各占半行。)
|
||||
// (CENTER:「均值点数 X」+「迭代次数 N」。MEAN 已改为滑块,见下面单独的分支。)
|
||||
const float labelXWidth = UiMeasureTextWidth(m_fonts->uiSmall, L"均值点数 X");
|
||||
const float labelN = UiMeasureTextWidth(m_fonts->uiSmall, L"迭代次数 N");
|
||||
const float fieldWidth =
|
||||
@@ -127,6 +138,23 @@ namespace jdi
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth);
|
||||
}
|
||||
|
||||
if (meanFilter)
|
||||
{
|
||||
// 两行滑块 = 「标签列 + 轨道(最小宽度)+ 数值列 + 复位按钮」(两行共用同一列宽),
|
||||
// 右侧再给滚动条留位(上百个信号时内容必然超高)。
|
||||
const float labelColumn =
|
||||
std::max(UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterPointsSliderLabel),
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterIterationsSliderLabel));
|
||||
const float valueColumn =
|
||||
std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"100000"),
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, L"1000"));
|
||||
const float sliderRowWidth = labelColumn + 6.0f * m_scale + kWeakPeakTrackMinWidth * m_scale +
|
||||
6.0f * m_scale + valueColumn + kWeakPeakResetGap * m_scale +
|
||||
kWeakPeakResetButtonSize * m_scale;
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + sliderRowWidth +
|
||||
(kFreeMarkerScrollWidth + 4.0f) * m_scale);
|
||||
}
|
||||
|
||||
if (weakPeak)
|
||||
{
|
||||
// 滑块行 = 「标签列 + 轨道(最小宽度)+ 数值列 + 复位按钮」(等级与迭代次数两行共用同一列宽)。
|
||||
@@ -233,7 +261,9 @@ namespace jdi
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
const bool freeMarker = (kind == EdgePanelKind::MARK);
|
||||
// 「均值滤波」与「弱峰调整」共用同一套「逐条目 + 底部按钮」排布。
|
||||
const bool itemized = (centerAxis || meanFilter || weakPeak);
|
||||
// ⚠️ 「均值滤波」从 2026-10-30 起改为**可滚动的分组列表**(信号色叠底标题栏 +
|
||||
// 展开后的两行滑块),与「自由标记」同一套浏览方式 ⇒ 不再走这里的 `itemized` 分支。
|
||||
const bool itemized = (centerAxis || weakPeak);
|
||||
const bool hasButton = (meanFilter || weakPeak);
|
||||
const bool enabled = meanFilter ? lane.meanFilter
|
||||
: (weakPeak ? lane.weakPeak
|
||||
@@ -286,12 +316,43 @@ namespace jdi
|
||||
// + 参数部分(**只在该条目被勾选时才占位**,因此面板高度随勾选数量动态变化):
|
||||
// * MEAN / CENTER:一行「均值点数 X + 迭代次数 N」输入框(中心轴再多一行统计结果);
|
||||
// * PEAK:两行滑块(调整幅度 % / 迭代次数 N),每个信号各有一套自己的参数。
|
||||
const bool itemHasParams = (centerAxis || meanFilter || weakPeak);
|
||||
const bool itemHasParams = (centerAxis || weakPeak);
|
||||
const float weakPeakSliderRow = kWeakPeakSliderRowHeight * m_scale;
|
||||
float meanRowHeight = 0.0f;
|
||||
float itemsHeight = 0.0f;
|
||||
float meanContentHeight = 0.0f;
|
||||
float height = 0.0f;
|
||||
if (itemized)
|
||||
if (meanFilter)
|
||||
{
|
||||
// 「均值滤波」:内容区 = 逐信号的**信号色叠底标题栏**(内容总是显示,
|
||||
// 用于勾选/取消预览)+(勾选且未收起时)两行参数滑块。
|
||||
// 内容超高时出现滚动条;底部「构建为新信号」按钮固定不滚动。
|
||||
const float groupRow = kEdgePanelMeanRowHeight * m_scale;
|
||||
const float sliderRow = weakPeakSliderRow;
|
||||
float content = 0.0f;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || !resolved.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
content += groupRow;
|
||||
const Lane::MeanFilterItem* item = lane.FindMeanFilterItem(ref);
|
||||
if (item != nullptr && !item->collapsed)
|
||||
{
|
||||
content += sliderRow * 2.0f;
|
||||
}
|
||||
}
|
||||
meanContentHeight = content;
|
||||
const float budget = m_plotRect.Height - (titleHeight + pad * 3.0f + buttonHeight);
|
||||
const float maxContent =
|
||||
std::max(3.0f * groupRow,
|
||||
std::min(kFreeMarkerContentMaxHeight * m_scale, std::max(3.0f * groupRow, budget)));
|
||||
const float view = std::min(content, maxContent);
|
||||
height = titleHeight + pad + view + pad + buttonHeight + pad;
|
||||
}
|
||||
else if (itemized)
|
||||
{
|
||||
meanRowHeight = kEdgePanelMeanRowHeight * m_scale;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
@@ -501,7 +562,119 @@ namespace jdi
|
||||
{
|
||||
return layout;
|
||||
}
|
||||
if (itemized)
|
||||
if (meanFilter)
|
||||
{
|
||||
// 「均值滤波」:可滚动内容区(逐信号的信号色叠底标题栏 + 展开后的两行滑块)+ 底部按钮。
|
||||
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
|
||||
layout.button = gd::RectF(left + pad, top + height - pad - buttonRowHeight,
|
||||
std::max(0.0f, width - pad * 2.0f), buttonRowHeight);
|
||||
|
||||
const float groupRow = kEdgePanelMeanRowHeight * m_scale;
|
||||
const float sliderRow = kWeakPeakSliderRowHeight * m_scale;
|
||||
const float scrollW = kFreeMarkerScrollWidth * m_scale;
|
||||
const float viewHeight =
|
||||
std::max(0.0f, height - titleHeight - pad * 3.0f - buttonRowHeight);
|
||||
const bool scrollable = meanContentHeight > viewHeight + 0.5f;
|
||||
const float maxScroll = std::max(0.0f, meanContentHeight - viewHeight);
|
||||
const float scroll = std::max(0.0f, std::min(lane.meanFilterScroll, maxScroll));
|
||||
const float innerWidth =
|
||||
std::max(0.0f, width - pad * 2.0f - (scrollable ? (scrollW + 4.0f * m_scale) : 0.0f));
|
||||
layout.content = gd::RectF(left + pad, top + titleHeight + pad, innerWidth, viewHeight);
|
||||
layout.markerContentHeight = meanContentHeight;
|
||||
layout.scrollOffset = scroll;
|
||||
if (scrollable)
|
||||
{
|
||||
layout.scrollTrack =
|
||||
gd::RectF(left + pad + innerWidth + 4.0f * m_scale, layout.content.Y, scrollW,
|
||||
viewHeight);
|
||||
const float thumbHeight =
|
||||
std::max(18.0f * m_scale, viewHeight * (viewHeight / meanContentHeight));
|
||||
const float thumbTop =
|
||||
layout.content.Y +
|
||||
(viewHeight - thumbHeight) * ((maxScroll > 0.0f) ? (scroll / maxScroll) : 0.0f);
|
||||
layout.scrollThumb = gd::RectF(layout.scrollTrack.X, thumbTop, scrollW, thumbHeight);
|
||||
}
|
||||
|
||||
// 两行滑块的列宽(与 `EdgePanelWidth` 同一组算式 ⇒ 布局与宽度不会漂移)。
|
||||
const float labelColumn =
|
||||
std::max(UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterPointsSliderLabel),
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterIterationsSliderLabel));
|
||||
const float valueColumn =
|
||||
std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"100000"),
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, L"1000"));
|
||||
const float resetSize = kWeakPeakResetButtonSize * m_scale;
|
||||
const float resetGap = kWeakPeakResetGap * m_scale;
|
||||
const float trackHeight = kWeakPeakTrackHeight * m_scale;
|
||||
const float closeSize = kEdgePanelMeanCheckSize * m_scale;
|
||||
const float contentLeft = layout.content.X;
|
||||
const float trackLeft = contentLeft + labelColumn + 6.0f * m_scale;
|
||||
const float resetLeft = contentLeft + innerWidth - resetSize;
|
||||
const float trackWidth =
|
||||
std::max(10.0f * m_scale,
|
||||
(resetLeft - resetGap - valueColumn - 6.0f * m_scale) - trackLeft);
|
||||
|
||||
float rowY = layout.content.Y - scroll;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || !resolved.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const Lane::MeanFilterItem* item = lane.FindMeanFilterItem(ref);
|
||||
const bool checked = (item != nullptr);
|
||||
layout.rowRefs.push_back(ref);
|
||||
// 组头(信号色叠底):整行宽度,左侧三角(仅勾选时)+ 名称,右端复选框。
|
||||
layout.rowHeaders.push_back(
|
||||
gd::RectF(contentLeft, rowY, innerWidth, groupRow));
|
||||
layout.rowNameRects.push_back(gd::RectF(contentLeft, rowY, innerWidth, groupRow));
|
||||
layout.rowTriangles.push_back(
|
||||
checked ? gd::RectF(contentLeft + 3.0f * m_scale,
|
||||
rowY + (groupRow - kEdgePanelDisclosureWidth * m_scale) * 0.5f,
|
||||
kEdgePanelDisclosureWidth * m_scale,
|
||||
kEdgePanelDisclosureWidth * m_scale)
|
||||
: gd::RectF());
|
||||
layout.rowChecks.push_back(
|
||||
gd::RectF(contentLeft + innerWidth - closeSize - 4.0f * m_scale,
|
||||
rowY + (groupRow - closeSize) * 0.5f, closeSize, closeSize));
|
||||
layout.rowPoints.push_back(gd::RectF());
|
||||
layout.rowIterations.push_back(gd::RectF());
|
||||
layout.rowStats.push_back(gd::RectF());
|
||||
rowY += groupRow;
|
||||
|
||||
// 参数行:只有勾选且未收起时才有几何(未勾选的行两者都是空矩形)。
|
||||
if (!checked || item->collapsed)
|
||||
{
|
||||
layout.rowLevelTracks.push_back(gd::RectF());
|
||||
layout.rowLevelValues.push_back(gd::RectF());
|
||||
layout.rowLevelResets.push_back(gd::RectF());
|
||||
layout.rowIterTracks.push_back(gd::RectF());
|
||||
layout.rowIterValues.push_back(gd::RectF());
|
||||
layout.rowIterResets.push_back(gd::RectF());
|
||||
continue;
|
||||
}
|
||||
const gd::RectF pointsTrack(trackLeft, rowY + (sliderRow - trackHeight) * 0.5f,
|
||||
trackWidth, trackHeight);
|
||||
layout.rowLevelTracks.push_back(pointsTrack);
|
||||
layout.rowLevelValues.push_back(
|
||||
gd::RectF(pointsTrack.X + pointsTrack.Width + 6.0f * m_scale, rowY, valueColumn,
|
||||
sliderRow));
|
||||
layout.rowLevelResets.push_back(
|
||||
gd::RectF(resetLeft, rowY + (sliderRow - resetSize) * 0.5f, resetSize, resetSize));
|
||||
rowY += sliderRow;
|
||||
|
||||
const gd::RectF iterTrack(trackLeft, rowY + (sliderRow - trackHeight) * 0.5f,
|
||||
trackWidth, trackHeight);
|
||||
layout.rowIterTracks.push_back(iterTrack);
|
||||
layout.rowIterValues.push_back(
|
||||
gd::RectF(iterTrack.X + iterTrack.Width + 6.0f * m_scale, rowY, valueColumn,
|
||||
sliderRow));
|
||||
layout.rowIterResets.push_back(
|
||||
gd::RectF(resetLeft, rowY + (sliderRow - resetSize) * 0.5f, resetSize, resetSize));
|
||||
rowY += sliderRow;
|
||||
}
|
||||
}
|
||||
else if (itemized)
|
||||
{
|
||||
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
|
||||
if (hasButton)
|
||||
@@ -1209,7 +1382,11 @@ namespace jdi
|
||||
double bestValue = 0.0;
|
||||
double bestEnum = 0.0;
|
||||
float bestY = 0.0f;
|
||||
bool bestSnapped = false;
|
||||
// 胜出信号的轴 / 数据 / 时间偏移:循环结束后**只为它**做一次「自动点线」稀疏判定
|
||||
// (判定代价与信号数量成正比,而它只影响一个小圆环 ⇒ 绝不能每个信号都算)。
|
||||
const TimeAxis* bestAxis = nullptr;
|
||||
const SignalData* bestData = nullptr;
|
||||
double bestOffsetTime = 0.0;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
@@ -1218,9 +1395,10 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 「自动点线」判据成立时吸附到最近采样点(与光标吸附同一规则)。
|
||||
// ⚠️ 用户明确要求:只要判据成立就吸附,**不**管当前是散点还是用户自己改过的样式。
|
||||
const bool sparse = MinorCellSparse(*resolved.axis, *resolved.data, resolved.style->offsetTime);
|
||||
// 「自动点线」判据只用于决定要不要画“已吸附”圆环 ⇒ **不在循环里逐个信号算**:
|
||||
// 它的代价是“每个小格两次二分”(视界 50 格 × 上百信号 = 数万次比较/帧),
|
||||
// 而**只有最后胜出的那一个信号**需要它(用户 2026-10-30 报告的卡顿根因之一)。
|
||||
// 见循环结束后的统一判定。
|
||||
|
||||
// ---- ① 取“鼠标到这条曲线”的距离(复用绘制阶段的折线顶点)----
|
||||
float nearestX = fx;
|
||||
@@ -1348,13 +1526,22 @@ namespace jdi
|
||||
ApplyDrawTransform(*resolved.style, candidateSample);
|
||||
if (std::isfinite(candidateValue))
|
||||
{
|
||||
// ⚠️⚠️ 距离必须包含**横向**分量(用户 2026-10-30 报告):
|
||||
// 只看纵向距离时,一个“单数据点信号”只要它的点在鼠标的**高度**附近,
|
||||
// 就会在整幅宽度内一直胜出 —— 光标被它“吸住”,无法标注另一个常规信号。
|
||||
// 改为鼠标到**该采样点屏幕位置**的欧氏距离后:稠密信号的横向分量≈ 0(仍等价于
|
||||
// “纵向最近”);单点/稀疏信号只有鼠标真的靠近它时才会被选中。
|
||||
const double pointX = static_cast<double>(TimeToX(candidateTime));
|
||||
const double dx = pointX - static_cast<double>(fx);
|
||||
const double vertical = std::fabs(
|
||||
static_cast<double>(ValueToY(candidateValue, visual)) -
|
||||
static_cast<double>(fy));
|
||||
if (!candidateOk || vertical < distance)
|
||||
const double horizontal = std::fabs(dx);
|
||||
const double sampleDistance = std::sqrt(vertical * vertical + horizontal * horizontal);
|
||||
if (!candidateOk || sampleDistance < distance)
|
||||
{
|
||||
distance = vertical;
|
||||
nearestX = fx;
|
||||
distance = sampleDistance;
|
||||
nearestX = static_cast<float>(pointX);
|
||||
time = candidateTime;
|
||||
sampled = candidateSample;
|
||||
candidateOk = true;
|
||||
@@ -1368,9 +1555,6 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
// 固定点必须落在真实采样时刻上(两个候选都已吸附到采样点)。
|
||||
// 表示“已吸附”的圆环仍只在「自动点线」判据成立(`sparse`)时画:
|
||||
// 密采样信号里吸附量远小于 1 像素,圆环会一直挂着,反而像是画错了。
|
||||
const bool snapped = sparse;
|
||||
const double value = ApplyDrawTransform(*resolved.style, sampled);
|
||||
if (!std::isfinite(value))
|
||||
{
|
||||
@@ -1386,7 +1570,9 @@ namespace jdi
|
||||
bestValue = value;
|
||||
bestEnum = sampled;
|
||||
bestY = y;
|
||||
bestSnapped = snapped;
|
||||
bestAxis = resolved.axis;
|
||||
bestData = resolved.data;
|
||||
bestOffsetTime = resolved.style->offsetTime;
|
||||
probe.ref = ref;
|
||||
}
|
||||
|
||||
@@ -1398,11 +1584,20 @@ namespace jdi
|
||||
}
|
||||
probe.valid = true;
|
||||
probe.time = bestTime;
|
||||
probe.x = TimeToX(bestTime);
|
||||
// ⚠️ 纵向光标**画在鼠标所在的 x**(用户 2026-10-30 明确要求:纵向光标跟随鼠标,
|
||||
// 被标注的数据点才是"离鼠标最近"的那个)。旧实现把竖线画在吸附后的采样时刻,
|
||||
// 于是稀疏/单点信号会让整条竖线"跳"到数据点上,观感像光标被吸走了。
|
||||
probe.x = fx;
|
||||
probe.pointX = TimeToX(bestTime);
|
||||
probe.value = bestValue;
|
||||
probe.enumValue = bestEnum;
|
||||
probe.y = bestY;
|
||||
probe.snapped = bestSnapped;
|
||||
// 「已吸附」圆环只在「自动点线」稀疏判据成立时画(密采样信号里吸附量 < 1 像素,
|
||||
// 圆环会一直挂着反而像画错了)。**只对胜出的这一个信号算**(见循环里的说明)。
|
||||
if (bestAxis != nullptr && bestData != nullptr)
|
||||
{
|
||||
probe.snapped = MinorCellSparse(*bestAxis, *bestData, bestOffsetTime);
|
||||
}
|
||||
return probe;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user