主题自适应配色 + 预览曲线连续配色 + 样式区复位按钮 + 均值滤波面板重做 + 面板缓存环带残留 + 调试层序 + 自由标记选取与性能
① 主题自适应(`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
@@ -436,6 +436,19 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// 折线缓存容量 = 全部信号数 × 2(下限 `kPolylineCacheLimit`)。
|
||||
// ⚠️ 必须随信号总数增长:固定 64 条在"上百个信号合并到一个网格"时会持续清表重建,
|
||||
// 使绘制与自由标记探测每帧重算全部折线(用户 2026-10-30 报告的卡顿)。
|
||||
{
|
||||
std::size_t totalSignals = 0;
|
||||
for (const Lane& lane : lanes)
|
||||
{
|
||||
totalSignals += lane.signals.size();
|
||||
}
|
||||
const std::size_t wanted = totalSignals * 2ull;
|
||||
m_polylineCacheCapacity = (wanted > kPolylineCacheLimit) ? wanted : kPolylineCacheLimit;
|
||||
}
|
||||
|
||||
const float gap = 0.0f;
|
||||
float laneHeight = m_plotRect.Height / static_cast<float>(count);
|
||||
if (laneHeight < m_minLaneHeight * m_scale)
|
||||
|
||||
@@ -458,7 +458,7 @@ namespace jdi
|
||||
if (cardBottom > cardTop)
|
||||
{
|
||||
UiFillRect(g, gd::RectF(patch.X, cardTop, patch.Width, cardBottom - cardTop),
|
||||
m_theme->cardBg);
|
||||
ThemeLaneBackground(*m_theme));
|
||||
}
|
||||
|
||||
const double viewMin = Tab().ViewMin();
|
||||
|
||||
@@ -1789,6 +1789,23 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::SetLaneMeanFilterScroll(int laneIndex, float scroll)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
const float clamped = std::max(0.0f, scroll);
|
||||
if (std::fabs(lane.meanFilterScroll - clamped) < 0.5f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 滚动只影响面板内部像素,缓冲区/场景都不变(面板像素缓存按键失效即可)。
|
||||
lane.meanFilterScroll = clamped;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::ToggleLaneFreeMarkerGroup(int laneIndex, const SignalRef& ref)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
|
||||
@@ -389,9 +389,11 @@ namespace jdi
|
||||
m_editTarget == PlotEditTarget::EDGE_CENTER_ITERATIONS ||
|
||||
m_editTarget == PlotEditTarget::MEAN_FILTER_POINTS ||
|
||||
m_editTarget == PlotEditTarget::MEAN_FILTER_ITERATIONS ||
|
||||
m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX)
|
||||
m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX ||
|
||||
m_editTarget == PlotEditTarget::MEAN_POINTS_MAX ||
|
||||
m_editTarget == PlotEditTarget::MEAN_ITERATIONS_MAX)
|
||||
{
|
||||
// 「均值点数 X」/「迭代次数 N」(均值滤波、中心轴)与「迭代次数 N 上限」(弱峰调整):
|
||||
// 「均值点数 X」/「迭代次数 N」(均值滤波、中心轴)与「滑块上限」(弱峰调整 / 均值滤波):
|
||||
// 输入框都落在对应的字段/滑块行矩形上。
|
||||
if (m_editFieldRect.Width <= 0.0f)
|
||||
{
|
||||
@@ -641,6 +643,44 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
// 「均值滤波」两行滑块的**上限**:与「弱峰调整 → 迭代次数上限」同一交互语义
|
||||
//(点击滑块右侧数值 → 整行变成输入框,输入正整数作为新的量程上限)。
|
||||
// 规则:空内容 / 非法值(低于下限或超出允许范围)→ 保持原上限不变;
|
||||
// 生效后若当前值超过新上限,自动夹到新上限。
|
||||
if (target == PlotEditTarget::MEAN_POINTS_MAX ||
|
||||
target == PlotEditTarget::MEAN_ITERATIONS_MAX)
|
||||
{
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
if (lane.FindMeanFilterItem(m_editMeanRef) == nullptr)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (parsedOk)
|
||||
{
|
||||
const bool isPoints = (target == PlotEditTarget::MEAN_POINTS_MAX);
|
||||
const int minimum = isPoints ? kMeanFilterPointsMin : kMeanFilterIterationsMin;
|
||||
const int limit = isPoints ? kMeanFilterPointsMaxLimit : kMeanFilterIterationsMaxLimit;
|
||||
const long long value = static_cast<long long>(std::llround(parsed));
|
||||
if (value >= minimum && value <= limit)
|
||||
{
|
||||
Lane::MeanFilterItem& item = lane.EnsureMeanFilterItem(m_editMeanRef);
|
||||
if (isPoints)
|
||||
{
|
||||
item.maxPoints = static_cast<int>(value);
|
||||
item.points = std::max(minimum, std::min(item.maxPoints, item.points));
|
||||
}
|
||||
else
|
||||
{
|
||||
item.maxIterations = static_cast<int>(value);
|
||||
item.iterations =
|
||||
std::max(minimum, std::min(item.maxIterations, item.iterations));
|
||||
}
|
||||
m_meanCurveCache.clear();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 「均值点数 X」/「迭代次数 N」:作用于被勾选信号的均值滤波曲线,夹在合理范围内。
|
||||
// 「中心轴边沿计数」用**完全相同的参数与语义**,只是作用于自己的条目列表。
|
||||
const bool centerTarget =
|
||||
@@ -882,7 +922,9 @@ namespace jdi
|
||||
return LoadCursorW(nullptr, IDC_SIZEWE);
|
||||
case PlotDragMode::PEAK_LEVEL:
|
||||
case PlotDragMode::PEAK_ITERATIONS:
|
||||
// 「弱峰调整」滑块拖拽 → 左右箭头。
|
||||
case PlotDragMode::MEAN_POINTS:
|
||||
case PlotDragMode::MEAN_ITERATIONS:
|
||||
// 「弱峰调整 / 均值滤波」参数滑块拖拽 → 左右箭头。
|
||||
return LoadCursorW(nullptr, IDC_SIZEWE);
|
||||
case PlotDragMode::MOVE_SIGNAL:
|
||||
// 拖动信号(参考 Photoshop 平移元素的抓握光标)。
|
||||
@@ -1393,6 +1435,72 @@ namespace jdi
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case PlotDragMode::MEAN_POINTS:
|
||||
case PlotDragMode::MEAN_ITERATIONS:
|
||||
{
|
||||
// 「均值滤波」某信号的参数滑块:量程 = 下限 … 该信号自己的上限。
|
||||
if (m_tab == nullptr || m_meanDragLane < 0 ||
|
||||
m_meanDragLane >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
break;
|
||||
}
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(m_meanDragLane)];
|
||||
if (lane.FindMeanFilterItem(m_meanDragRef) == nullptr)
|
||||
{
|
||||
break;
|
||||
}
|
||||
Lane::MeanFilterItem& item = lane.EnsureMeanFilterItem(m_meanDragRef);
|
||||
const bool pointsMode = (m_dragMode == PlotDragMode::MEAN_POINTS);
|
||||
const EdgePanelLayout layout =
|
||||
EdgePanelLayoutFor(m_meanDragLane, EdgePanelKind::MEAN);
|
||||
int rowIndex = -1;
|
||||
for (std::size_t candidate = 0; candidate < layout.rowRefs.size(); ++candidate)
|
||||
{
|
||||
if (layout.rowRefs[candidate] == m_meanDragRef)
|
||||
{
|
||||
rowIndex = static_cast<int>(candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rowIndex < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
const std::vector<gd::RectF>& tracks =
|
||||
pointsMode ? layout.rowLevelTracks : layout.rowIterTracks;
|
||||
if (static_cast<std::size_t>(rowIndex) >= tracks.size() ||
|
||||
tracks[static_cast<std::size_t>(rowIndex)].Width <= 0.0f)
|
||||
{
|
||||
break;
|
||||
}
|
||||
const double ratio =
|
||||
UiSliderRatioFromX(tracks[static_cast<std::size_t>(rowIndex)], fx);
|
||||
if (pointsMode)
|
||||
{
|
||||
const int maximum = std::max(
|
||||
kMeanFilterPointsMin, std::min(kMeanFilterPointsMaxLimit, item.maxPoints));
|
||||
item.points = std::max(
|
||||
kMeanFilterPointsMin,
|
||||
std::min(maximum,
|
||||
kMeanFilterPointsMin +
|
||||
static_cast<int>(std::lround(ratio * (maximum - kMeanFilterPointsMin)))));
|
||||
}
|
||||
else
|
||||
{
|
||||
const int maximum =
|
||||
std::max(kMeanFilterIterationsMin,
|
||||
std::min(kMeanFilterIterationsMaxLimit, item.maxIterations));
|
||||
item.iterations = std::max(
|
||||
kMeanFilterIterationsMin,
|
||||
std::min(maximum,
|
||||
kMeanFilterIterationsMin +
|
||||
static_cast<int>(std::lround(
|
||||
ratio * (maximum - kMeanFilterIterationsMin)))));
|
||||
}
|
||||
m_meanCurveCache.clear();
|
||||
m_dragMoved = true;
|
||||
return true;
|
||||
}
|
||||
case PlotDragMode::LANE_REORDER:
|
||||
{
|
||||
// 被拖拽的网格自由跟随鼠标;目标位置变化时其它网格实时让位(吸附)。
|
||||
@@ -1526,6 +1634,35 @@ namespace jdi
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case PlotDragMode::MEAN_SCROLL:
|
||||
{
|
||||
// 「均值滤波」面板内容滚动条:按滑块在轨道里的位置换算滚动偏移。
|
||||
if (m_tab == nullptr || m_markerScrollDragLane < 0 ||
|
||||
m_markerScrollDragLane >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
break;
|
||||
}
|
||||
const EdgePanelLayout layout =
|
||||
EdgePanelLayoutFor(m_markerScrollDragLane, EdgePanelKind::MEAN);
|
||||
const float travel = layout.scrollTrack.Height - layout.scrollThumb.Height;
|
||||
const float maxScroll =
|
||||
std::max(0.0f, layout.markerContentHeight - layout.content.Height);
|
||||
if (layout.scrollTrack.Width <= 0.0f || travel <= 0.0f || maxScroll <= 0.0f)
|
||||
{
|
||||
break;
|
||||
}
|
||||
const float thumbTop =
|
||||
std::max(layout.scrollTrack.Y,
|
||||
std::min(static_cast<float>(y) - m_markerScrollGrabOffset,
|
||||
layout.scrollTrack.Y + travel));
|
||||
const float ratio = (thumbTop - layout.scrollTrack.Y) / travel;
|
||||
if (SetLaneMeanFilterScroll(m_markerScrollDragLane, ratio * maxScroll))
|
||||
{
|
||||
m_dragMoved = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case PlotDragMode::NONE:
|
||||
default:
|
||||
break;
|
||||
@@ -2054,6 +2191,161 @@ namespace jdi
|
||||
{
|
||||
const bool meanMode = (kind == EdgePanelKind::MEAN);
|
||||
const bool boostMode = (kind == EdgePanelKind::PEAK);
|
||||
// 「均值滤波」:组头三角(展开 / 收起参数行)+ 两行参数滑块
|
||||
//(轨道拖动 / 点击数值改上限 / 右侧复位按钮)。
|
||||
// ⚠️ 所有命中都要求落在**内容区**之内:滚动到可视范围之外的行仍保留矩形,
|
||||
// 若不加这条限制,它们会抢走底部「构建为新信号」按钮的点击。
|
||||
if (meanMode)
|
||||
{
|
||||
Lane& meanLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
|
||||
// 内容区右侧的滚动条滑块:按下即进入拖动(与「自由标记」面板同一交互)。
|
||||
if (layout.scrollThumb.Width > 0.0f &&
|
||||
UiPointInRect(layout.scrollThumb, fx, fy))
|
||||
{
|
||||
m_dragMode = PlotDragMode::MEAN_SCROLL;
|
||||
m_markerScrollDragLane = index;
|
||||
m_markerScrollGrabOffset = fy - layout.scrollThumb.Y;
|
||||
return true;
|
||||
}
|
||||
const bool insideContent =
|
||||
UiPointInRect(layout.content, fx, fy);
|
||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||
{
|
||||
const SignalRef& ref = layout.rowRefs[rowIndex];
|
||||
if (!insideContent)
|
||||
{
|
||||
break;
|
||||
}
|
||||
// 只允许命中**完全落在内容区里**的行(部分滚出的行其矩形会与内容区
|
||||
// 边界重叠,放行会造出"看得见一半却能点"的幽灵热区)。
|
||||
if (rowIndex < layout.rowHeaders.size())
|
||||
{
|
||||
const gd::RectF& headerRect = layout.rowHeaders[rowIndex];
|
||||
if (headerRect.Y < layout.content.Y - 0.5f ||
|
||||
headerRect.Y + headerRect.Height >
|
||||
layout.content.Y + layout.content.Height + 0.5f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const bool hasParams =
|
||||
rowIndex < layout.rowLevelTracks.size() &&
|
||||
layout.rowLevelTracks[rowIndex].Width > 0.0f &&
|
||||
meanLane.FindMeanFilterItem(ref) != nullptr;
|
||||
if (hasParams)
|
||||
{
|
||||
Lane::MeanFilterItem& item = meanLane.EnsureMeanFilterItem(ref);
|
||||
// 复位:均值点数 → 默认值(同时把滑块上限恢复为默认上限)。
|
||||
if (rowIndex < layout.rowLevelResets.size() &&
|
||||
layout.rowLevelResets[rowIndex].Width > 0.0f &&
|
||||
UiPointInRect(layout.rowLevelResets[rowIndex], fx, fy))
|
||||
{
|
||||
item.points = kMeanFilterPointsDefault;
|
||||
item.maxPoints = kMeanFilterPointsDefaultMax;
|
||||
m_meanCurveCache.clear();
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
// 复位:迭代次数 → 1(上限恢复为默认上限)。
|
||||
if (rowIndex < layout.rowIterResets.size() &&
|
||||
layout.rowIterResets[rowIndex].Width > 0.0f &&
|
||||
UiPointInRect(layout.rowIterResets[rowIndex], fx, fy))
|
||||
{
|
||||
item.iterations = kMeanFilterIterationsMin;
|
||||
item.maxIterations = kMeanFilterIterationsDefaultMax;
|
||||
m_meanCurveCache.clear();
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
// 点击「均值点数」右侧数值 → 输入滑块上限。
|
||||
if (rowIndex < layout.rowLevelValues.size() &&
|
||||
layout.rowLevelValues[rowIndex].Width > 0.0f &&
|
||||
UiPointInRect(layout.rowLevelValues[rowIndex], fx, fy))
|
||||
{
|
||||
m_editMeanRef = ref;
|
||||
m_meanEditCurrentMax = item.maxPoints;
|
||||
const gd::RectF& track = layout.rowLevelTracks[rowIndex];
|
||||
const gd::RectF& value = layout.rowLevelValues[rowIndex];
|
||||
const gd::RectF rowRect(track.X, value.Y,
|
||||
value.X + value.Width - track.X,
|
||||
value.Height);
|
||||
BeginEdit(PlotEditTarget::MEAN_POINTS_MAX, index, L"", rowRect);
|
||||
m_edit.Begin(L"", false, EditFilter::BYTE, 0u);
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
// 点击「迭代次数」右侧数值 → 输入滑块上限。
|
||||
if (rowIndex < layout.rowIterValues.size() &&
|
||||
layout.rowIterValues[rowIndex].Width > 0.0f &&
|
||||
UiPointInRect(layout.rowIterValues[rowIndex], fx, fy))
|
||||
{
|
||||
m_editMeanRef = ref;
|
||||
m_meanEditCurrentMax = item.maxIterations;
|
||||
const gd::RectF& track = layout.rowIterTracks[rowIndex];
|
||||
const gd::RectF& value = layout.rowIterValues[rowIndex];
|
||||
const gd::RectF rowRect(track.X, value.Y,
|
||||
value.X + value.Width - track.X,
|
||||
value.Height);
|
||||
BeginEdit(PlotEditTarget::MEAN_ITERATIONS_MAX, index, L"", rowRect);
|
||||
m_edit.Begin(L"", false, EditFilter::BYTE, 0u);
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
// 拖动「均值点数」滑块。
|
||||
if (rowIndex < layout.rowLevelTracks.size() &&
|
||||
WeakPeakSliderHit(layout.rowLevelTracks[rowIndex], m_scale, fx, fy))
|
||||
{
|
||||
const int maximum = std::max(
|
||||
kMeanFilterPointsMin,
|
||||
std::min(kMeanFilterPointsMaxLimit, item.maxPoints));
|
||||
m_dragMode = PlotDragMode::MEAN_POINTS;
|
||||
m_meanDragLane = index;
|
||||
m_meanDragRef = ref;
|
||||
const double ratio = UiSliderRatioFromX(layout.rowLevelTracks[rowIndex], fx);
|
||||
item.points = std::max(
|
||||
kMeanFilterPointsMin,
|
||||
std::min(maximum,
|
||||
kMeanFilterPointsMin +
|
||||
static_cast<int>(std::lround(
|
||||
ratio *
|
||||
(maximum - kMeanFilterPointsMin)))));
|
||||
m_meanCurveCache.clear();
|
||||
return true;
|
||||
}
|
||||
// 拖动「迭代次数」滑块。
|
||||
if (rowIndex < layout.rowIterTracks.size() &&
|
||||
WeakPeakSliderHit(layout.rowIterTracks[rowIndex], m_scale, fx, fy))
|
||||
{
|
||||
const int maximum = std::max(
|
||||
kMeanFilterIterationsMin,
|
||||
std::min(kMeanFilterIterationsMaxLimit, item.maxIterations));
|
||||
m_dragMode = PlotDragMode::MEAN_ITERATIONS;
|
||||
m_meanDragLane = index;
|
||||
m_meanDragRef = ref;
|
||||
const double ratio = UiSliderRatioFromX(layout.rowIterTracks[rowIndex], fx);
|
||||
item.iterations = std::max(
|
||||
kMeanFilterIterationsMin,
|
||||
std::min(maximum,
|
||||
kMeanFilterIterationsMin +
|
||||
static_cast<int>(std::lround(
|
||||
ratio *
|
||||
(maximum - kMeanFilterIterationsMin)))));
|
||||
m_meanCurveCache.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 组头左侧三角:切换参数行的收起 / 展开(只有勾选后才有三角)。
|
||||
if (rowIndex < layout.rowTriangles.size() &&
|
||||
layout.rowTriangles[rowIndex].Width > 0.0f &&
|
||||
UiPointInRect(layout.rowTriangles[rowIndex], fx, fy))
|
||||
{
|
||||
Lane::MeanFilterItem& item = meanLane.EnsureMeanFilterItem(ref);
|
||||
item.collapsed = !item.collapsed;
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Lane& tabLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
|
||||
|
||||
// 「弱峰调整」:先判两行滑块(数值文本 → 改上限;轨道 → 拖动)。
|
||||
@@ -2707,6 +2999,9 @@ namespace jdi
|
||||
// 弱峰滑块拖拽结束:清掉目标(避免留下上一轮的信号引用)。
|
||||
m_peakDragLane = -1;
|
||||
m_peakDragRef = SignalRef();
|
||||
// 「均值滤波」参数滑块拖拽结束:同样清掉目标。
|
||||
m_meanDragLane = -1;
|
||||
m_meanDragRef = SignalRef();
|
||||
// 「标记透明度」滑块 / 面板内容滚动条拖拽结束。
|
||||
m_markerOpacityDragLane = -1;
|
||||
m_markerScrollDragLane = -1;
|
||||
@@ -2755,24 +3050,49 @@ namespace jdi
|
||||
int panelLane = -1;
|
||||
int panelKind = 0;
|
||||
if (TopPanelAtPoint(static_cast<float>(m_mouseX), static_cast<float>(m_mouseY),
|
||||
panelLane, panelKind) &&
|
||||
static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MARK)
|
||||
panelLane, panelKind))
|
||||
{
|
||||
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, EdgePanelKind::MARK);
|
||||
if (layout.content.Width > 0.0f &&
|
||||
UiPointInRect(layout.content, static_cast<float>(m_mouseX),
|
||||
static_cast<float>(m_mouseY)))
|
||||
const EdgePanelKind hoveredKind = static_cast<EdgePanelKind>(panelKind);
|
||||
// 「均值滤波」面板:内容区超高时用滚轮翻页(上百个信号合并时必备)。
|
||||
if (hoveredKind == EdgePanelKind::MEAN)
|
||||
{
|
||||
const float maxScroll =
|
||||
std::max(0.0f, layout.markerContentHeight - layout.content.Height);
|
||||
if (maxScroll > 0.0f)
|
||||
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, EdgePanelKind::MEAN);
|
||||
if (layout.content.Width > 0.0f &&
|
||||
UiPointInRect(layout.content, static_cast<float>(m_mouseX),
|
||||
static_cast<float>(m_mouseY)))
|
||||
{
|
||||
const float next = static_cast<float>(Tab().Lanes()[static_cast<std::size_t>(
|
||||
panelLane)]
|
||||
.freeMarkerScroll -
|
||||
notches * 40.0 * m_scale);
|
||||
SetLaneFreeMarkerScroll(panelLane, std::max(0.0f, std::min(next, maxScroll)));
|
||||
return true;
|
||||
const float maxScroll =
|
||||
std::max(0.0f, layout.markerContentHeight - layout.content.Height);
|
||||
if (maxScroll > 0.0f)
|
||||
{
|
||||
const float current =
|
||||
Tab().Lanes()[static_cast<std::size_t>(panelLane)].meanFilterScroll;
|
||||
const float next = current - static_cast<float>(notches * 40.0 * m_scale);
|
||||
SetLaneMeanFilterScroll(panelLane,
|
||||
std::max(0.0f, std::min(next, maxScroll)));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hoveredKind == EdgePanelKind::MARK)
|
||||
{
|
||||
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, EdgePanelKind::MARK);
|
||||
if (layout.content.Width > 0.0f &&
|
||||
UiPointInRect(layout.content, static_cast<float>(m_mouseX),
|
||||
static_cast<float>(m_mouseY)))
|
||||
{
|
||||
const float maxScroll =
|
||||
std::max(0.0f, layout.markerContentHeight - layout.content.Height);
|
||||
if (maxScroll > 0.0f)
|
||||
{
|
||||
const float next = static_cast<float>(Tab().Lanes()[static_cast<std::size_t>(
|
||||
panelLane)]
|
||||
.freeMarkerScroll -
|
||||
notches * 40.0 * m_scale);
|
||||
SetLaneFreeMarkerScroll(panelLane,
|
||||
std::max(0.0f, std::min(next, maxScroll)));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,29 +113,65 @@ namespace jdi
|
||||
//(详见 `PolylineCacheEntry::lineWidth` 的说明;漏掉它会让线宽改动在**稠密信号**上不生效)。
|
||||
const float lineWidth = (resolved.style != nullptr) ? resolved.style->lineWidth : 1.0f;
|
||||
|
||||
// 全部键字段的 64 位哈希(顺序固定;只用于**查找**,命中后再做廉价校验)。
|
||||
std::uint64_t key = 1469598103934665603ull;
|
||||
const auto mixHash = [&key](std::uint64_t value)
|
||||
{
|
||||
key ^= value;
|
||||
key *= 1099511628211ull;
|
||||
};
|
||||
const auto mixHashFloat = [&mixHash](float value)
|
||||
{
|
||||
std::uint32_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
mixHash(static_cast<std::uint64_t>(bits));
|
||||
};
|
||||
const auto mixHashDouble = [&mixHash](double value)
|
||||
{
|
||||
std::uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
mixHash(bits);
|
||||
};
|
||||
mixHash(static_cast<std::uint64_t>(static_cast<std::uint32_t>(ref.groupIndex)) + 2ull);
|
||||
mixHash(static_cast<std::uint64_t>(static_cast<std::uint32_t>(ref.signalIndex)) + 2ull);
|
||||
mixHash(reinterpret_cast<std::uintptr_t>(resolved.group));
|
||||
mixHash(static_cast<std::uint64_t>(sampleCount));
|
||||
mixHashDouble(viewMin);
|
||||
mixHashDouble(viewMax);
|
||||
mixHashFloat(m_plotRect.X);
|
||||
mixHashFloat(m_plotRect.Width);
|
||||
mixHashFloat(lane.rect.Y);
|
||||
mixHashFloat(lane.rect.Height);
|
||||
mixHashDouble(lane.yMin);
|
||||
mixHashDouble(lane.yMax);
|
||||
mixHashFloat(gain);
|
||||
mixHash((absolute ? 1ull : 0ull) | (mirrorVertical ? 2ull : 0ull) |
|
||||
(mirrorHorizontal ? 4ull : 0ull));
|
||||
mixHashFloat(offsetY);
|
||||
mixHashDouble(offsetTime);
|
||||
mixHashFloat(lineWidth);
|
||||
|
||||
for (const PolylineCacheEntry& entry : m_polylineCache)
|
||||
{
|
||||
if (entry.ref == ref && entry.group == resolved.group && entry.sampleCount == sampleCount &&
|
||||
entry.viewMin == viewMin && entry.viewMax == viewMax && entry.plotX == m_plotRect.X &&
|
||||
entry.plotWidth == m_plotRect.Width && entry.laneY == lane.rect.Y &&
|
||||
entry.laneHeight == lane.rect.Height && entry.laneYMin == lane.yMin &&
|
||||
entry.laneYMax == lane.yMax && entry.gain == gain && entry.absolute == absolute &&
|
||||
entry.mirrorVertical == mirrorVertical && entry.mirrorHorizontal == mirrorHorizontal &&
|
||||
entry.offsetY == offsetY && entry.offsetTime == offsetTime &&
|
||||
entry.lineWidth == lineWidth)
|
||||
// 快路径:只比哈希。命中后再校验三项最可能变化的字段(碰撞代价可忽略)。
|
||||
if (entry.key != key || entry.sampleCount != sampleCount || !(entry.ref == ref) ||
|
||||
entry.viewMin != viewMin || entry.viewMax != viewMax)
|
||||
{
|
||||
return &entry;
|
||||
continue;
|
||||
}
|
||||
return &entry;
|
||||
}
|
||||
|
||||
// 未命中:重建并写入缓存(deque 保证已有元素的引用在追加后依然有效)。
|
||||
// 缓存只用于消除重复计算,容量上限约等于“网格数 × 每网格信号数”的两倍。
|
||||
if (m_polylineCache.size() >= kPolylineCacheLimit)
|
||||
// ⚠️ 容量必须**覆盖所有网格的全部信号**:旧实现固定 64 条,283 个信号合并到
|
||||
// 同一网格时会不断"清空重建"⇒ 绘制与自由标记探测每帧重算全部折线(严重卡顿)。
|
||||
if (m_polylineCache.size() >= m_polylineCacheCapacity)
|
||||
{
|
||||
m_polylineCache.clear();
|
||||
}
|
||||
m_polylineCache.push_back(PolylineCacheEntry());
|
||||
PolylineCacheEntry& entry = m_polylineCache.back();
|
||||
entry.key = key;
|
||||
entry.ref = ref;
|
||||
entry.group = resolved.group;
|
||||
entry.sampleCount = sampleCount;
|
||||
@@ -656,104 +692,102 @@ namespace jdi
|
||||
|
||||
// 下面这三样在 `paintOverlays` 里绘制在**浮动面板之后**(压在面板之上):
|
||||
// 滚动条、右下角「回到最上方」悬浮按钮、内联输入框。
|
||||
// ⇒ 必须比面板层(`baseLayer + 1`)再高一层,否则与实际像素的层序相反。
|
||||
boxes.SetLayer(baseLayer + 2);
|
||||
boxes.Add(m_scrollBarRect, 5);
|
||||
boxes.Add(BackToTopRect(), 5);
|
||||
boxes.Add(EditFieldRect(), 5);
|
||||
// ⇒ 它们的层号在下面「面板循环」之后统一给(比最上面板再高一层),
|
||||
// 否则与实际像素的层序相反。这里先复位到组件基层。
|
||||
boxes.SetLayer(baseLayer);
|
||||
|
||||
// ⚠️⚠️ **浮动面板必须另起一个绘制层,并把面板矩形登记为遮挡矩形**:
|
||||
// ⚠️⚠️ **浮动面板必须另起绘制层,并把面板矩形登记为遮挡矩形**:
|
||||
// 面板是不透明浮层、且常常**跨多个网格上下排列**,与网格区域重叠。
|
||||
// 若与网格同层,网格的矩形边线会横穿面板 —— 用户 2026-10-29 报告的
|
||||
// "网格的布局边界穿透显示在浮动面板上"。`layer` 决定谁压谁,
|
||||
// `AddOccluder` 才真正把下层 box 从面板区域里**挖掉**。
|
||||
bool panelLayerPushed = false;
|
||||
|
||||
// 层级 2/3/4:5 类实验性功能浮动面板的内部控件。
|
||||
// 这些矩形由 `EdgePanelLayoutFor` 统一算出(绘制与命中共用),一个循环即可全部上报。
|
||||
//
|
||||
// ⚠️⚠️ **每个面板各占一个绘制层,且顺序必须是 `m_panelOrder`(底 → 顶)**:
|
||||
// 旧实现把所有面板都放在 `baseLayer + 1` 一层,于是同一层内只能按 `depth` 排序 ——
|
||||
// 下层面板的"行/文本"(depth 4)会画在**上层**面板的本体(depth 2/3)之上,
|
||||
// 表现为"被遮挡的下层面板的边框/文字穿透到上层面板上"(用户 2026-10-30 报告)。
|
||||
// 层号与 `paintOverlays` 里 `UiSetTextLayer` 的推进**完全同源**。
|
||||
//
|
||||
// 说明(已知边界):面板层号从 `baseLayer + 1` 起逐面板递增,因此**同时显示 15 个以上**
|
||||
// 面板时会撞上浮层层号(`OverlayLayer(1)` = 组件数 × 3 + 3)。那时只有"菜单不再挖掉
|
||||
// 面板边框"这一条调试观感受影响;实际布局下同时可见的面板是几个到十几个。
|
||||
SyncPanelOrder();
|
||||
const std::size_t panelCount = m_panelOrder.size();
|
||||
const int topOverlayLayer = baseLayer + 1 + static_cast<int>(panelCount);
|
||||
|
||||
// ⚠️ **先用最便宜的判据跳过"没有面板"的网格**:`EdgePanelLayoutFor` 内部要取
|
||||
// 标签页配置、逐项判断种类与开关,单次约 2~3µs;283 个网格 × 5 种 = 1415 次调用
|
||||
// 实测约 **4ms/帧**(这是"开调试后卡"的主要来源之一)。
|
||||
// 只有该网格**至少有一个面板处于展开状态**时才值得去算布局。
|
||||
const std::vector<Lane>& laneConfigs =
|
||||
(m_tab != nullptr) ? m_tab->Lanes() : m_localTab.Lanes();
|
||||
for (std::size_t index = 0; index < m_lanes.size() && index < laneConfigs.size(); ++index)
|
||||
// 现在只对**层序表里真正显示出来的面板**算布局(层序表本来就只含展开且启用的面板)。
|
||||
for (std::size_t ordinal = 0; ordinal < panelCount; ++ordinal)
|
||||
{
|
||||
const Lane& laneConfig = laneConfigs[index];
|
||||
const bool anyPanelOpen = laneConfig.edgePanel.open ||
|
||||
laneConfig.edgeCenterPanel.open ||
|
||||
laneConfig.meanFilterPanel.open ||
|
||||
laneConfig.weakPeakPanel.open ||
|
||||
laneConfig.freeMarkerPanel.open;
|
||||
if (!anyPanelOpen)
|
||||
const PanelRef& panelRef = m_panelOrder[ordinal];
|
||||
const EdgePanelLayout layout =
|
||||
EdgePanelLayoutFor(panelRef.laneIndex, static_cast<EdgePanelKind>(panelRef.kind));
|
||||
if (!UiRectHasArea(layout.panel))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const int laneIndex = static_cast<int>(index);
|
||||
for (int kindValue = static_cast<int>(EdgePanelKind::COUNT);
|
||||
kindValue <= static_cast<int>(EdgePanelKind::MARK); ++kindValue)
|
||||
// 本面板独占一层(层号越大越靠上)。
|
||||
boxes.SetLayer(baseLayer + 1 + static_cast<int>(ordinal));
|
||||
boxes.AddOccluder(layout.panel);
|
||||
// 面板内部元素的调试框同样裁到面板本身:面板滚动的内容区里,
|
||||
// 滚出可视区的行其矩形仍在(与弹出菜单的条目同一类问题)。
|
||||
boxes.SetClip(layout.panel);
|
||||
boxes.Add(layout.panel, 2);
|
||||
boxes.Add(layout.title, 3);
|
||||
boxes.Add(layout.close, 3);
|
||||
boxes.Add(layout.disclosure, 3);
|
||||
boxes.Add(layout.badge, 3);
|
||||
boxes.Add(layout.edit, 3);
|
||||
boxes.Add(layout.button, 3);
|
||||
boxes.Add(layout.content, 3);
|
||||
boxes.Add(layout.scrollTrack, 3);
|
||||
boxes.Add(layout.scrollThumb, 4);
|
||||
boxes.Add(layout.opacityTrack, 3);
|
||||
boxes.Add(layout.opacityValue, 3);
|
||||
boxes.Add(layout.showDelta, 3);
|
||||
boxes.Add(layout.showEnum, 3);
|
||||
|
||||
boxes.AddAll(layout.rowChecks, 4);
|
||||
boxes.AddAll(layout.rowPoints, 4);
|
||||
boxes.AddAll(layout.rowIterations, 4);
|
||||
boxes.AddAll(layout.rowStats, 4);
|
||||
boxes.AddAll(layout.rowLevelTracks, 4);
|
||||
boxes.AddAll(layout.rowLevelValues, 4);
|
||||
boxes.AddAll(layout.rowIterTracks, 4);
|
||||
boxes.AddAll(layout.rowIterValues, 4);
|
||||
boxes.AddAll(layout.rowLevelResets, 4);
|
||||
boxes.AddAll(layout.rowIterResets, 4);
|
||||
boxes.AddAll(layout.rowNameRects, 4);
|
||||
boxes.AddAll(layout.rowNameCloses, 4);
|
||||
boxes.AddAll(layout.rowTriangles, 4);
|
||||
boxes.AddAll(layout.rowHeaders, 4);
|
||||
|
||||
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
|
||||
{
|
||||
const EdgePanelLayout layout =
|
||||
EdgePanelLayoutFor(laneIndex, static_cast<EdgePanelKind>(kindValue));
|
||||
if (!UiRectHasArea(layout.panel))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!panelLayerPushed)
|
||||
{
|
||||
boxes.SetLayer(baseLayer + 1);
|
||||
panelLayerPushed = true;
|
||||
}
|
||||
boxes.AddOccluder(layout.panel);
|
||||
// 面板内部元素的调试框同样裁到面板本身:面板滚动的内容区里,
|
||||
// 滚出可视区的行其矩形仍在(与弹出菜单的条目同一类问题)。
|
||||
boxes.SetClip(layout.panel);
|
||||
boxes.Add(layout.panel, 2);
|
||||
boxes.Add(layout.title, 3);
|
||||
boxes.Add(layout.close, 3);
|
||||
boxes.Add(layout.disclosure, 3);
|
||||
boxes.Add(layout.badge, 3);
|
||||
boxes.Add(layout.edit, 3);
|
||||
boxes.Add(layout.button, 3);
|
||||
boxes.Add(layout.content, 3);
|
||||
boxes.Add(layout.scrollTrack, 3);
|
||||
boxes.Add(layout.scrollThumb, 4);
|
||||
boxes.Add(layout.opacityTrack, 3);
|
||||
boxes.Add(layout.opacityValue, 3);
|
||||
boxes.Add(layout.showDelta, 3);
|
||||
boxes.Add(layout.showEnum, 3);
|
||||
|
||||
boxes.AddAll(layout.rowChecks, 4);
|
||||
boxes.AddAll(layout.rowPoints, 4);
|
||||
boxes.AddAll(layout.rowIterations, 4);
|
||||
boxes.AddAll(layout.rowStats, 4);
|
||||
boxes.AddAll(layout.rowLevelTracks, 4);
|
||||
boxes.AddAll(layout.rowLevelValues, 4);
|
||||
boxes.AddAll(layout.rowIterTracks, 4);
|
||||
boxes.AddAll(layout.rowIterValues, 4);
|
||||
boxes.AddAll(layout.rowLevelResets, 4);
|
||||
boxes.AddAll(layout.rowIterResets, 4);
|
||||
boxes.AddAll(layout.rowNameRects, 4);
|
||||
boxes.AddAll(layout.rowNameCloses, 4);
|
||||
|
||||
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
|
||||
{
|
||||
boxes.Add(group.header, 3);
|
||||
boxes.Add(group.tableHeader, 3);
|
||||
boxes.Add(group.triangle, 4);
|
||||
boxes.Add(group.close, 4);
|
||||
}
|
||||
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
|
||||
{
|
||||
boxes.Add(row.rect, 4);
|
||||
boxes.Add(row.close, 4);
|
||||
}
|
||||
boxes.Add(group.header, 3);
|
||||
boxes.Add(group.tableHeader, 3);
|
||||
boxes.Add(group.triangle, 4);
|
||||
boxes.Add(group.close, 4);
|
||||
boxes.Add(group.check, 4);
|
||||
}
|
||||
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
|
||||
{
|
||||
boxes.Add(row.rect, 4);
|
||||
boxes.Add(row.close, 4);
|
||||
}
|
||||
}
|
||||
// 收尾:解除面板裁剪并**复位层号**(宿主后续组件依赖它)。
|
||||
// 浮动面板之上的叠加层(滚动条 / 回到最上方 / 内联输入框)必须比**最上面板**再高一层。
|
||||
boxes.SetClip(gd::RectF());
|
||||
boxes.SetLayer(topOverlayLayer);
|
||||
boxes.Add(m_scrollBarRect, 5);
|
||||
boxes.Add(BackToTopRect(), 5);
|
||||
boxes.Add(EditFieldRect(), 5);
|
||||
boxes.SetLayer(baseLayer);
|
||||
}
|
||||
|
||||
@@ -848,17 +882,19 @@ namespace jdi
|
||||
// 这样浮动面板仍压在最上层,与其它实验性功能的层序一致)。
|
||||
PaintFreeMarker(g, m_lanes);
|
||||
// 从这里开始都在面板之上:文本层同步 +1(与 `BuildBoxes` 的 `baseLayer + 1` 一致)。
|
||||
// ⚠️ `PaintEdgeCursor` 内部会**逐面板**再把层号 +1(每个面板独占一层),
|
||||
// 并在返回前把层号推到**最上面板之上**,因此这里不再写死 `+2`。
|
||||
UiSetTextLayer(textLayer + 1);
|
||||
PaintEdgeCursor(g, m_lanes);
|
||||
// 之后这几样绘制在浮动面板**之上**:与 `BuildBoxes` 的 `baseLayer + 2` 对应。
|
||||
UiSetTextLayer(textLayer + 2);
|
||||
PaintSignalInteraction(g);
|
||||
PaintHint(g);
|
||||
// 网格 Y 轴上下限的输入框、以及「弱峰调整 → 迭代次数上限」的输入框都绘制在最上层
|
||||
//(后者覆盖在浮动面板的滑块行之上)。
|
||||
if (m_editTarget == PlotEditTarget::LANE_Y_MAX ||
|
||||
m_editTarget == PlotEditTarget::LANE_Y_MIN ||
|
||||
m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX)
|
||||
m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX ||
|
||||
m_editTarget == PlotEditTarget::MEAN_POINTS_MAX ||
|
||||
m_editTarget == PlotEditTarget::MEAN_ITERATIONS_MAX)
|
||||
{
|
||||
PaintEditField(g);
|
||||
}
|
||||
@@ -1105,7 +1141,9 @@ namespace jdi
|
||||
UiFillRect(g, m_area, m_theme->plotBg);
|
||||
|
||||
// 波形区域底色。
|
||||
UiFillRect(g, m_plotRect, m_theme->cardBg);
|
||||
// ⚠️ 「网格叠底」必须用 `ThemeLaneBackground()`:主题的 `cardBg` 与 `plotBg` 过于接近时
|
||||
// (深色主题)它会自动拉开一点点,保证网格这一层结构在任何主题下都看得见。
|
||||
UiFillRect(g, m_plotRect, ThemeLaneBackground(*m_theme));
|
||||
|
||||
// 图例命中矩形每帧重建,避免残留已消失的信号。
|
||||
m_legendHits.clear();
|
||||
@@ -1580,7 +1618,8 @@ namespace jdi
|
||||
const gd::RectF gridClip = laneClip;
|
||||
/** 标尺数值文本的裁剪矩形(GDI 文本需要显式裁剪,否则会越过绘图区)。 */
|
||||
const gd::RectF clipRectForLabels = gridClip;
|
||||
const std::uint32_t minorLineColor = UiBlendOver(m_theme->cardBg, WithAlpha(m_theme->gridWeak, kMinorLineAlpha));
|
||||
const std::uint32_t laneBackground = ThemeLaneBackground(*m_theme);
|
||||
const std::uint32_t minorLineColor = UiBlendOver(laneBackground, WithAlpha(m_theme->gridWeak, kMinorLineAlpha));
|
||||
const std::uint32_t weakLineColor = m_theme->gridWeak | 0xFF000000u;
|
||||
const std::uint32_t strongLineColor = m_theme->gridStrong | 0xFF000000u;
|
||||
|
||||
@@ -3145,12 +3184,14 @@ namespace jdi
|
||||
const float limitTop = lane.rect.Y - lane.rect.Height * 4.0f;
|
||||
const float limitBottom = lane.rect.Y + lane.rect.Height * 5.0f;
|
||||
|
||||
// 派生曲线的颜色:与原信号**同色相**但大幅拉开明度,使其与原始波形形成高对比
|
||||
// (原信号改颜色时曲线随之改变,因为它是从信号色派生出来的);透明度与原信号一致。
|
||||
// 派生曲线的颜色:与原信号**同色相**、亮度按**当前网格底色**自适应拉开,
|
||||
// 使其与原始波形、与背景都形成足够对比(原信号改颜色时曲线随之改变,因为它是从信号色
|
||||
// 派生出来的);透明度与原信号一致。
|
||||
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(HighContrastDerived(StyleColor(*resolved.style)),
|
||||
baseAlpha);
|
||||
const std::uint32_t derived = WithAlpha(
|
||||
HighContrastDerived(StyleColor(*resolved.style), ThemeLaneBackground(*m_theme)),
|
||||
baseAlpha);
|
||||
|
||||
// 顶点预算与波形一致(每像素列两个顶点)。
|
||||
// 交互期间进一步降到 1/2:派生曲线往往有 10 条(5 网格 × 均值滤波 + 弱峰调整),
|
||||
@@ -3480,6 +3521,10 @@ namespace jdi
|
||||
mix(lane.freeMarkerPanel.collapsed ? 1ull : 0ull);
|
||||
mix(lane.edgeValueValid ? 1ull : 0ull);
|
||||
mixDouble(lane.edgeValue);
|
||||
// 「自由标记」面板内容区的滚动偏移(历史行为:计入全局键)。
|
||||
// ⚠️ 「均值滤波」的滚动偏移只进**该面板自己的**槽键(见 `PanelSlotKey`),
|
||||
// 否则滚动一个面板会让屏幕上所有面板一起重画。
|
||||
mixFloat(lane.freeMarkerScroll);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3573,6 +3618,8 @@ namespace jdi
|
||||
mixRect(group.tableHeader);
|
||||
mixRect(group.triangle);
|
||||
mixRect(group.close);
|
||||
// 「均值滤波」的组头右端是复选框(「自由标记」为空矩形,不影响它的键)。
|
||||
mixRect(group.check);
|
||||
mix(group.collapsed ? 1ull : 0ull);
|
||||
mix(group.pinned ? 1ull : 0ull);
|
||||
mix(static_cast<std::uint64_t>(group.ref.groupIndex + 2));
|
||||
@@ -3616,6 +3663,11 @@ namespace jdi
|
||||
const Lane::MeanFilterItem* meanItem = lane.FindMeanFilterItem(ref);
|
||||
mix(static_cast<std::uint64_t>(meanItem != nullptr ? meanItem->points + 2 : 0));
|
||||
mix(static_cast<std::uint64_t>(meanItem != nullptr ? meanItem->iterations + 2 : 0));
|
||||
// 「均值滤波」改为滑块后,**滑块上限**决定圆钮位置、**收起状态**决定是否显示参数行
|
||||
// ⇒ 两者都直接改变面板像素,必须进键(否则拖完滑块仍会贴回旧像素)。
|
||||
mix(static_cast<std::uint64_t>(meanItem != nullptr ? meanItem->maxPoints + 2 : 0));
|
||||
mix(static_cast<std::uint64_t>(meanItem != nullptr ? meanItem->maxIterations + 2 : 0));
|
||||
mix((meanItem != nullptr && meanItem->collapsed) ? 1ull : 0ull);
|
||||
const Lane::EdgeCenterItem* centerItem = lane.FindEdgeCenterItem(ref);
|
||||
mix(static_cast<std::uint64_t>(centerItem != nullptr ? centerItem->points + 2 : 0));
|
||||
mix(static_cast<std::uint64_t>(centerItem != nullptr ? centerItem->iterations + 2 : 0));
|
||||
@@ -3704,6 +3756,17 @@ namespace jdi
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
// 「均值滤波」:信号色叠底标题栏 / 展开三角 / 内容区滚动偏移都要入键
|
||||
//(滚动改变的是面板内部像素,位置与尺寸不变)。
|
||||
for (const gd::RectF& rect : layout.rowHeaders)
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
for (const gd::RectF& rect : layout.rowTriangles)
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
mixFloat(layout.scrollOffset);
|
||||
|
||||
// 鼠标落在面板内时,✕ / 复选框 / 滑块 / 底部按钮的悬停态都依赖精确坐标。
|
||||
const float margin = 6.0f * m_scale;
|
||||
@@ -4067,12 +4130,23 @@ namespace jdi
|
||||
//(与"可拖拽"的光晕区分:这里只是单点圆环,不描整条曲线)。
|
||||
const float radius = 3.4f * m_scale;
|
||||
gd::SolidBrush dot(ToGdiColor(signalColor));
|
||||
g.FillEllipse(&dot, x - radius, probe.y - radius, radius * 2.0f, radius * 2.0f);
|
||||
// 取值圆点与"已吸附"圆环都画在**数据点本身**的横坐标上(`pointX`),
|
||||
// 而纵向光标画在鼠标位置(`x`)。稀疏信号下两者会分开 —— 这是刻意的:
|
||||
// 光标跟随鼠标、提示落在真实采样点上(用户 2026-10-30 要求)。
|
||||
float pointX = probe.pointX;
|
||||
if (!(pointX > 0.0f))
|
||||
{
|
||||
pointX = x; // 兼容:未填 `pointX` 时退回光标位置
|
||||
}
|
||||
g.FillEllipse(&dot, pointX - radius, probe.y - radius, radius * 2.0f,
|
||||
radius * 2.0f);
|
||||
if (probe.snapped)
|
||||
{
|
||||
gd::Pen ring(ToGdiColor(lineColor), 1.2f * m_scale);
|
||||
g.DrawEllipse(&ring, x - radius - 3.0f * m_scale, probe.y - radius - 3.0f * m_scale,
|
||||
(radius + 3.0f * m_scale) * 2.0f, (radius + 3.0f * m_scale) * 2.0f);
|
||||
g.DrawEllipse(&ring, pointX - radius - 3.0f * m_scale,
|
||||
probe.y - radius - 3.0f * m_scale,
|
||||
(radius + 3.0f * m_scale) * 2.0f,
|
||||
(radius + 3.0f * m_scale) * 2.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4094,6 +4168,9 @@ namespace jdi
|
||||
const std::size_t count = std::min(lanes.size(), tabLanes.size());
|
||||
const std::uint32_t color = CursorColor();
|
||||
const UiInk ink = UiContrastInk(color);
|
||||
// 进入本函数时的文本层号 = 宿主给本组件分配的面板层起始(`baseLayer + 1`)。
|
||||
// 每个面板在循环里各用 `panelTextLayer + 面板序数`,函数结束前把层号推到**最上面板之上**。
|
||||
const int panelTextLayer = UiGetTextLayer();
|
||||
|
||||
// ---- 横向光标(线与两端把手 + 左侧幅值读数)----
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
@@ -4283,6 +4360,10 @@ namespace jdi
|
||||
const PanelRef& panelRef = m_panelOrder[panelOrdinal];
|
||||
const EdgePanelKind kind = static_cast<EdgePanelKind>(panelRef.kind);
|
||||
const std::size_t index = static_cast<std::size_t>(panelRef.laneIndex);
|
||||
// 文本几何层号:**每个面板独占一层**,与 `BuildBoxes` 完全同源
|
||||
//(`baseLayer + 1 + 面板序数`,面板序数按 `m_panelOrder` 底 → 顶)。
|
||||
// 少了这一步,下层面板的文字框会记在比上层面板更低的层里被反复挖洞/穿透。
|
||||
UiSetTextLayer(panelTextLayer + static_cast<int>(panelOrdinal));
|
||||
const bool centerAxis = (kind == EdgePanelKind::CENTER);
|
||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
@@ -4353,11 +4434,43 @@ namespace jdi
|
||||
}
|
||||
if (hit != nullptr && hit->key != 0 && hit->key == slotKey)
|
||||
{
|
||||
// ⚠️⚠️ **只贴「面板圆角形状之内」的像素**(用户 2026-10-30 报告:
|
||||
// 中键移动视界时面板周围带着一圈**旧视界的波形像素**)。
|
||||
// 原因是缓存位图按 `panel ± cacheMargin` 取块,块的边缘那几像素是
|
||||
// **上一帧的场景**(波形/网格线);`PanelGlobalKey` 刻意不含视界
|
||||
// (否则平移会重画全部面板),于是整块贴回时就把旧波形"复印"了一圈。
|
||||
// 现在:① 先把**投影**按本帧重新画一遍(它在圆角形状之外,不能靠缓存);
|
||||
// ② 用圆角区域裁剪后只 BitBlt 面板本体(圆角之外仍是本帧场景)。
|
||||
if (!m_fastFrame)
|
||||
{
|
||||
UiFillRoundRect(g, gd::RectF(panel.X + 1.0f * m_scale,
|
||||
panel.Y + 2.0f * m_scale, panel.Width,
|
||||
panel.Height),
|
||||
5.0f * m_scale, 0x14000000u);
|
||||
}
|
||||
HDC target = g.GetHDC();
|
||||
if (target != nullptr)
|
||||
{
|
||||
BitBlt(target, hit->x0, hit->y0, hit->x1 - hit->x0, hit->y1 - hit->y0,
|
||||
m_panelDc, hit->x0, hit->y0, SRCCOPY);
|
||||
const int bx0 = static_cast<int>(std::floor(panel.X)) - 1;
|
||||
const int by0 = static_cast<int>(std::floor(panel.Y)) - 1;
|
||||
const int bx1 = static_cast<int>(std::ceil(panel.X + panel.Width)) + 1;
|
||||
const int by1 = static_cast<int>(std::ceil(panel.Y + panel.Height)) + 1;
|
||||
HRGN rounded = CreateRoundRectRgn(
|
||||
bx0, by0, bx1 + 1, by1 + 1,
|
||||
static_cast<int>(std::lround(10.0f * m_scale)),
|
||||
static_cast<int>(std::lround(10.0f * m_scale)));
|
||||
const int savedDc = SaveDC(target);
|
||||
if (rounded != nullptr)
|
||||
{
|
||||
ExtSelectClipRgn(target, rounded, RGN_AND);
|
||||
}
|
||||
BitBlt(target, bx0, by0, bx1 - bx0, by1 - by0, m_panelDc, bx0, by0,
|
||||
SRCCOPY);
|
||||
if (rounded != nullptr)
|
||||
{
|
||||
DeleteObject(rounded);
|
||||
}
|
||||
RestoreDC(target, savedDc);
|
||||
g.ReleaseHDC(target);
|
||||
}
|
||||
// 贴图不会真的画字 ⇒ 必须把上一帧渲染时存下的文本框**回放**给
|
||||
@@ -4701,6 +4814,191 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 「均值滤波」面板:逐信号的**信号色叠底标题栏**(左侧展开三角 + 信号名 +
|
||||
// 右端复选框)。复选框勾选后才出现三角、才能展开下面的两行参数滑块
|
||||
// (「均值点数」/「迭代次数」);内容超高时可滚动(滚动条在内容区右侧)。
|
||||
if (meanFilter)
|
||||
{
|
||||
const int meanSave = g.Save();
|
||||
// 内容必须裁到内容区:滚动时行会越出可视范围。
|
||||
g.SetClip(layout.content, gd::CombineModeIntersect);
|
||||
|
||||
const float valueColumn =
|
||||
std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"100000"),
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, L"1000"));
|
||||
|
||||
// 复选框自绘(主题色在信号色叠底上看不清):用**自适应对比色**描边/填充。
|
||||
const auto paintMeanCheck = [this, &g](const gd::RectF& rect, bool checked,
|
||||
const UiInk& ink,
|
||||
std::uint32_t background)
|
||||
{
|
||||
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const float radius = 2.5f * m_scale;
|
||||
if (!checked)
|
||||
{
|
||||
UiDrawRoundRect(g, rect, radius, ink.text, 1.2f * m_scale);
|
||||
return;
|
||||
}
|
||||
UiFillRoundRect(g, rect, radius, ink.text);
|
||||
// 对勾:两段折线,颜色取叠底色(与填充色必成对比)。
|
||||
const float left = rect.X + rect.Width * 0.22f;
|
||||
const float middleX = rect.X + rect.Width * 0.44f;
|
||||
const float right = rect.X + rect.Width * 0.78f;
|
||||
const float highY = rect.Y + rect.Height * 0.30f;
|
||||
const float lowY = rect.Y + rect.Height * 0.72f;
|
||||
UiDrawLine(g, left, rect.Y + rect.Height * 0.52f, middleX, lowY, background,
|
||||
1.6f * m_scale);
|
||||
UiDrawLine(g, middleX, lowY, right, highY, background, 1.6f * m_scale);
|
||||
};
|
||||
|
||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||
{
|
||||
const SignalRef& ref = layout.rowRefs[rowIndex];
|
||||
if (rowIndex >= layout.rowHeaders.size() ||
|
||||
rowIndex >= layout.rowChecks.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const gd::RectF& header = layout.rowHeaders[rowIndex];
|
||||
if (header.Y + header.Height <= layout.content.Y ||
|
||||
header.Y >= layout.content.Y + layout.content.Height)
|
||||
{
|
||||
continue; // 完全滚出可视范围
|
||||
}
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
const UiInk ink = UiContrastInk(signalColor);
|
||||
const Lane::MeanFilterItem* item = tabLane.FindMeanFilterItem(ref);
|
||||
const bool checked = (item != nullptr);
|
||||
|
||||
// 叠底:先铺一层不透明同色再叠 0xE8(与「自由标记」分组栏同一做法),
|
||||
// 这样滚动时下层内容不会透出来。
|
||||
UiFillRoundRect(g, header, 3.0f * m_scale, signalColor);
|
||||
UiFillRoundRect(g, header, 3.0f * m_scale, WithAlpha(signalColor, 0xE8));
|
||||
|
||||
// 左侧三角(仅勾选时存在):朝下 = 展开、朝右 = 收起。
|
||||
const gd::RectF& triangle = layout.rowTriangles[rowIndex];
|
||||
if (triangle.Width > 0.0f && item != nullptr)
|
||||
{
|
||||
UiDrawTriangleIcon(g, triangle, ink.text,
|
||||
item->collapsed ? IconDirection::RIGHT
|
||||
: IconDirection::DOWN);
|
||||
}
|
||||
|
||||
// 名称:从三角右侧到复选框左侧。
|
||||
const gd::RectF& check = layout.rowChecks[rowIndex];
|
||||
const float nameLeft = (triangle.Width > 0.0f)
|
||||
? (triangle.X + triangle.Width + 6.0f * m_scale)
|
||||
: (header.X + 8.0f * m_scale);
|
||||
const float nameWidth =
|
||||
std::max(10.0f, check.X - 6.0f * m_scale - nameLeft);
|
||||
UiTextClipped(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall, nameWidth),
|
||||
m_fonts->uiSmall,
|
||||
gd::RectF(nameLeft, header.Y, nameWidth, header.Height), ink.text,
|
||||
TextAlign::LEFT, layout.content);
|
||||
paintMeanCheck(check, checked, ink, signalColor);
|
||||
|
||||
if (!checked || item->collapsed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 两行参数滑块(标签 / 轨道 / 数值 / 复位按钮)。
|
||||
const auto paintMeanSlider = [&](const gd::RectF& track,
|
||||
const gd::RectF& valueRect,
|
||||
const gd::RectF& resetRect,
|
||||
const wchar_t* label, int current, int minimum,
|
||||
int maximum, bool active)
|
||||
{
|
||||
if (track.Width <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const float labelLeft = track.X - 6.0f * m_scale -
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, label);
|
||||
UiText(g, label, m_fonts->uiSmall,
|
||||
gd::RectF(labelLeft, track.Y - 8.0f * m_scale,
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, label),
|
||||
track.Height + 16.0f * m_scale),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
const double ratio =
|
||||
(maximum > minimum)
|
||||
? (static_cast<double>(current - minimum) /
|
||||
static_cast<double>(maximum - minimum))
|
||||
: 0.0;
|
||||
UiDrawSlider(g, track, ratio, m_theme->accent, active, m_scale);
|
||||
UiText(g, std::to_wstring(current), m_fonts->monoSmall,
|
||||
gd::RectF(valueRect.X, valueRect.Y, valueColumn, valueRect.Height),
|
||||
m_theme->textStrong, TextAlign::LEFT);
|
||||
// 复位按钮:悬停高亮 + 与「系数/偏移」菜单同款图标。
|
||||
const bool hot =
|
||||
(m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) &&
|
||||
UiPointInRect(resetRect, static_cast<float>(m_mouseX),
|
||||
static_cast<float>(m_mouseY));
|
||||
if (hot)
|
||||
{
|
||||
UiFillRoundRect(
|
||||
g, gd::RectF(resetRect.X - 1.0f * m_scale,
|
||||
resetRect.Y - 1.0f * m_scale,
|
||||
resetRect.Width + 2.0f * m_scale,
|
||||
resetRect.Height + 2.0f * m_scale),
|
||||
3.0f * m_scale, WithAlpha(m_theme->accent, 0x33));
|
||||
}
|
||||
PaintPanelResetIcon(g, resetRect,
|
||||
hot ? m_theme->accent : m_theme->textMuted, m_scale);
|
||||
};
|
||||
|
||||
const int pointsMax = std::max(kMeanFilterPointsMin,
|
||||
std::min(kMeanFilterPointsMaxLimit,
|
||||
item->maxPoints));
|
||||
const int points =
|
||||
std::max(kMeanFilterPointsMin, std::min(pointsMax, item->points));
|
||||
const int iterationsMax = std::max(kMeanFilterIterationsMin,
|
||||
std::min(kMeanFilterIterationsMaxLimit,
|
||||
item->maxIterations));
|
||||
const int iterations = std::max(
|
||||
kMeanFilterIterationsMin, std::min(iterationsMax, item->iterations));
|
||||
const bool pointsActive = (m_dragMode == PlotDragMode::MEAN_POINTS) &&
|
||||
(m_meanDragLane == static_cast<int>(index)) &&
|
||||
(m_meanDragRef == ref);
|
||||
const bool iterationsActive =
|
||||
(m_dragMode == PlotDragMode::MEAN_ITERATIONS) &&
|
||||
(m_meanDragLane == static_cast<int>(index)) && (m_meanDragRef == ref);
|
||||
if (rowIndex < layout.rowLevelTracks.size())
|
||||
{
|
||||
paintMeanSlider(layout.rowLevelTracks[rowIndex],
|
||||
rowIndex < layout.rowLevelValues.size()
|
||||
? layout.rowLevelValues[rowIndex]
|
||||
: gd::RectF(),
|
||||
rowIndex < layout.rowLevelResets.size()
|
||||
? layout.rowLevelResets[rowIndex]
|
||||
: gd::RectF(),
|
||||
kMeanFilterPointsSliderLabel, points,
|
||||
kMeanFilterPointsMin, pointsMax, pointsActive);
|
||||
}
|
||||
if (rowIndex < layout.rowIterTracks.size())
|
||||
{
|
||||
paintMeanSlider(layout.rowIterTracks[rowIndex],
|
||||
rowIndex < layout.rowIterValues.size()
|
||||
? layout.rowIterValues[rowIndex]
|
||||
: gd::RectF(),
|
||||
rowIndex < layout.rowIterResets.size()
|
||||
? layout.rowIterResets[rowIndex]
|
||||
: gd::RectF(),
|
||||
kMeanFilterIterationsSliderLabel, iterations,
|
||||
kMeanFilterIterationsMin, iterationsMax, iterationsActive);
|
||||
}
|
||||
}
|
||||
|
||||
g.Restore(meanSave);
|
||||
}
|
||||
// 「自由标记」面板:按信号分组的**分组栏**(信号色托底 + 三角 + 名称 +
|
||||
// 「N 标记点」+ ✕),下属固定点条目六列对齐(序号 / 取值 / 枚举 / 增量 / 时刻 / 间隔),
|
||||
// 行尾各带一个 ✕。内容区有滚动偏移时按内容区裁剪。
|
||||
@@ -5166,6 +5464,10 @@ namespace jdi
|
||||
m_panelSlots = std::move(nextSlots);
|
||||
m_panelGlobalKey = panelGlobalKey;
|
||||
m_panelValid = panelCacheReady;
|
||||
|
||||
// 面板之后绘制的叠加层(信号拖拽虚影 / 提示 / 输入框 / 滚动条 / 回到最上方)
|
||||
// 都在**所有面板之上** ⇒ 文本层号推到最上面板的上一层(与 `BuildBoxes` 同源)。
|
||||
UiSetTextLayer(panelTextLayer + static_cast<int>(m_panelOrder.size()));
|
||||
}
|
||||
|
||||
void PlotView::PaintEditField(gd::Graphics& g)
|
||||
@@ -5181,10 +5483,21 @@ namespace jdi
|
||||
|
||||
// 「迭代次数 N 上限」:输入框**以空内容开始**,未输入任何内容时以灰色提示当前上限值。
|
||||
// 提示文案 = “输入上限(当前N)”(用户明确要求:旧实现只显示一个孤立的数字,缺少说明)。
|
||||
if (m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX && m_edit.Text().empty())
|
||||
// 「均值滤波」两行滑块的上限输入框共用同一套提示文案。
|
||||
if ((m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX ||
|
||||
m_editTarget == PlotEditTarget::MEAN_POINTS_MAX ||
|
||||
m_editTarget == PlotEditTarget::MEAN_ITERATIONS_MAX) &&
|
||||
m_edit.Text().empty())
|
||||
{
|
||||
const int currentMax = m_peakEditCurrentMax > 0 ? m_peakEditCurrentMax
|
||||
: kWeakPeakIterationsDefaultMax;
|
||||
const bool peakTarget = (m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX);
|
||||
const int currentMax =
|
||||
peakTarget
|
||||
? ((m_peakEditCurrentMax > 0) ? m_peakEditCurrentMax : kWeakPeakIterationsDefaultMax)
|
||||
: ((m_meanEditCurrentMax > 0)
|
||||
? m_meanEditCurrentMax
|
||||
: ((m_editTarget == PlotEditTarget::MEAN_POINTS_MAX)
|
||||
? kMeanFilterPointsDefaultMax
|
||||
: kMeanFilterIterationsDefaultMax));
|
||||
const std::wstring hint = L"输入上限(当前" + std::to_wstring(currentMax) + L")";
|
||||
const float hintLeft = fieldRect.X + 11.0f * m_scale;
|
||||
UiTextAtClipped(g, hint, m_fonts->monoSmall, hintLeft,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1446,7 +1446,7 @@ namespace jdi
|
||||
g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
|
||||
|
||||
UiFillRect(g, m_area, m_theme->plotBg);
|
||||
UiFillRect(g, m_area, m_theme->cardBg);
|
||||
UiFillRect(g, m_area, ThemeLaneBackground(*m_theme));
|
||||
|
||||
// 竖向网格刻度沿用**前台视界跨度**(而非宽窗口的跨度),否则竖网格线会只有一半。
|
||||
// ⚠️ 同时把它作为竖向网格的**主刻步长**传给 `PaintLane`(见 `m_renderTimeStep`):
|
||||
@@ -1948,7 +1948,7 @@ namespace jdi
|
||||
//(用户 2026-10-17 报告:滚动过渡动画在巨型信号处卡顿)。
|
||||
// 这一情形在滚动到**从未构建过图层的区域**时必然出现(上百个网格时绝大多数
|
||||
// 网格的图层都还没建立):旧实现在这里 `return false` ⇒ 落到 `Paint` 末尾的
|
||||
// **整幅重绘** ⇒ 把**本帧全部可见网格的波形**(可能包含上千�万采样的巨型信号)
|
||||
// **整幅重绘** ⇒ 把**本帧全部可见网格的波形**(可能包含上千万采样的巨型信号)
|
||||
// 每帧重画一遍 —— 实测单帧 **20~35ms**,而手动拖滚动条只要 ~6ms。
|
||||
// 正确行为:本帧只画**网格底衬**(廉价,见下方),波形等视界真正停下之后再按预算补
|
||||
//(这正是用户要求的“没来得及画信号时就按只有网格叠底的状态继续滚动”)。
|
||||
|
||||
Reference in new issue
Block a user