主题自适应配色 + 预览曲线连续配色 + 样式区复位按钮 + 均值滤波面板重做 + 面板缓存环带残留 + 调试层序 + 自由标记选取与性能
① 主题自适应(`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
@@ -62,7 +62,10 @@ namespace jdi
|
|||||||
PEAK_ITERATIONS, /**< 拖动「弱峰调整」某信号的迭代次数滑块(1 … 该信号自定义上限)。 */
|
PEAK_ITERATIONS, /**< 拖动「弱峰调整」某信号的迭代次数滑块(1 … 该信号自定义上限)。 */
|
||||||
LANE_REORDER, /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */
|
LANE_REORDER, /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */
|
||||||
MARKER_OPACITY, /**< 拖动「自由标记」面板的「标记透明度」滑块。 */
|
MARKER_OPACITY, /**< 拖动「自由标记」面板的「标记透明度」滑块。 */
|
||||||
MARKER_SCROLL /**< 拖动「自由标记」面板内容的滚动条滑块。 */
|
MARKER_SCROLL, /**< 拖动「自由标记」面板内容的滚动条滑块。 */
|
||||||
|
MEAN_POINTS, /**< 拖动「均值滤波」某信号的「均值点数」滑块(0 … 该信号自定义上限)。 */
|
||||||
|
MEAN_ITERATIONS,/**< 拖动「均值滤波」某信号的「迭代次数」滑块(1 … 该信号自定义上限)。 */
|
||||||
|
MEAN_SCROLL /**< 拖动「均值滤波」面板内容的滚动条滑块。 */
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,6 +94,15 @@ namespace jdi
|
|||||||
MEAN_FILTER_POINTS, /**< 编辑「均值滤波」的均值点数 X。 */
|
MEAN_FILTER_POINTS, /**< 编辑「均值滤波」的均值点数 X。 */
|
||||||
MEAN_FILTER_ITERATIONS, /**< 编辑「均值滤波」的迭代次数 N。 */
|
MEAN_FILTER_ITERATIONS, /**< 编辑「均值滤波」的迭代次数 N。 */
|
||||||
PEAK_ITERATIONS_MAX, /**< 编辑「弱峰调整」某信号「迭代次数 N」滑块的**上限**(正整数)。 */
|
PEAK_ITERATIONS_MAX, /**< 编辑「弱峰调整」某信号「迭代次数 N」滑块的**上限**(正整数)。 */
|
||||||
|
/**
|
||||||
|
* @brief 编辑「均值滤波」某信号「均值点数」滑块的**上限**(正整数)。
|
||||||
|
*
|
||||||
|
* 与 `PEAK_ITERATIONS_MAX` 同一交互语义:点击滑块右侧数值 → 整行变成输入框,
|
||||||
|
* 输入框以空内容开始、并显示“输入上限(当前N)”提示。
|
||||||
|
*/
|
||||||
|
MEAN_POINTS_MAX,
|
||||||
|
/** @brief 编辑「均值滤波」某信号「迭代次数」滑块的**上限**(正整数)。 */
|
||||||
|
MEAN_ITERATIONS_MAX,
|
||||||
FREE_MARKER_TIME, /**< 编辑「自由标记」某固定点的**自身时间**(秒)。 */
|
FREE_MARKER_TIME, /**< 编辑「自由标记」某固定点的**自身时间**(秒)。 */
|
||||||
FREE_MARKER_DELTA /**< 编辑「自由标记」某固定点相对前一固定点的**时间间隔**(秒)。 */
|
FREE_MARKER_DELTA /**< 编辑「自由标记」某固定点相对前一固定点的**时间间隔**(秒)。 */
|
||||||
};
|
};
|
||||||
@@ -1079,6 +1091,12 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
bool SetLaneFreeMarkerScroll(int laneIndex, float scroll);
|
bool SetLaneFreeMarkerScroll(int laneIndex, float scroll);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置某网格「均值滤波」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||||||
|
* @return 偏移真的发生变化返回 true。
|
||||||
|
*/
|
||||||
|
bool SetLaneMeanFilterScroll(int laneIndex, float scroll);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 切换某网格「自由标记」面板里某个信号分组的收起状态。
|
* @brief 切换某网格「自由标记」面板里某个信号分组的收起状态。
|
||||||
* @return 切换后为「已收起」返回 true。
|
* @return 切换后为「已收起」返回 true。
|
||||||
@@ -1799,6 +1817,15 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
std::vector<gd::RectF> rowLevelResets;
|
std::vector<gd::RectF> rowLevelResets;
|
||||||
std::vector<gd::RectF> rowIterResets;
|
std::vector<gd::RectF> rowIterResets;
|
||||||
|
/**
|
||||||
|
* @brief 「均值滤波」面板每个信号的**信号色叠底标题栏**矩形(与 `rowRefs` 同序)。
|
||||||
|
*
|
||||||
|
* 排版与「自由标记」面板的信号分组栏一致(左侧展开三角 + 信号名),
|
||||||
|
* 但右端是**复选框**(`rowChecks`)而不是 ✕。未勾选时标题栏仍显示(用于勾选)。
|
||||||
|
*/
|
||||||
|
std::vector<gd::RectF> rowHeaders;
|
||||||
|
/** @brief 「均值滤波」面板每个信号标题栏左侧的**展开 / 收起三角**(仅勾选后出现)。 */
|
||||||
|
std::vector<gd::RectF> rowTriangles;
|
||||||
std::vector<SignalRef> rowRefs;
|
std::vector<SignalRef> rowRefs;
|
||||||
/**
|
/**
|
||||||
* @brief 「自由标记」面板的一行(一个固定点)。
|
* @brief 「自由标记」面板的一行(一个固定点)。
|
||||||
@@ -1843,6 +1870,13 @@ namespace jdi
|
|||||||
gd::RectF tableHeader;
|
gd::RectF tableHeader;
|
||||||
gd::RectF triangle; /**< 展开/收起三角的命中矩形。 */
|
gd::RectF triangle; /**< 展开/收起三角的命中矩形。 */
|
||||||
gd::RectF close; /**< 右侧✕的命中矩形。 */
|
gd::RectF close; /**< 右侧✕的命中矩形。 */
|
||||||
|
/**
|
||||||
|
* @brief 右侧**复选框**的命中矩形(仅「均值滤波」面板使用;「自由标记」为空)。
|
||||||
|
*
|
||||||
|
* 均值滤波面板的组头右端是"是否预览该信号"的复选框(用户 2026-10-30 要求),
|
||||||
|
* 只有它被勾选时左侧的展开三角才出现、才能展开参数行。
|
||||||
|
*/
|
||||||
|
gd::RectF check;
|
||||||
bool collapsed = false;
|
bool collapsed = false;
|
||||||
/**
|
/**
|
||||||
* @brief 该分组栏是否处于**吸顶**(滚动时被钉在内容区顶部)。
|
* @brief 该分组栏是否处于**吸顶**(滚动时被钉在内容区顶部)。
|
||||||
@@ -1862,6 +1896,14 @@ namespace jdi
|
|||||||
gd::RectF content;
|
gd::RectF content;
|
||||||
/** 「自由标记」面板的**内容总高度**(含被滚动出去的条目),用于换算滚动比例。 */
|
/** 「自由标记」面板的**内容总高度**(含被滚动出去的条目),用于换算滚动比例。 */
|
||||||
float markerContentHeight = 0.0f;
|
float markerContentHeight = 0.0f;
|
||||||
|
/**
|
||||||
|
* @brief 内容区当前生效的**滚动偏移**(像素,≥ 0)。
|
||||||
|
*
|
||||||
|
* 「自由标记」与「均值滤波」两个面板的内容区都可能需要滚动;取值来源不同
|
||||||
|
* (`Lane::freeMarkerScroll` / `Lane::meanFilterScroll`),但布局之后一律从这里读,
|
||||||
|
* 绘制与命中共用同一份偏移(避免两处各自夹取导致"画的和点的不一致")。
|
||||||
|
*/
|
||||||
|
float scrollOffset = 0.0f;
|
||||||
gd::RectF scrollTrack; /**< 内容区右侧的滚动条轨道(不需要滚动时为空矩形)。 */
|
gd::RectF scrollTrack; /**< 内容区右侧的滚动条轨道(不需要滚动时为空矩形)。 */
|
||||||
gd::RectF scrollThumb; /**< 滚动条滑块。 */
|
gd::RectF scrollThumb; /**< 滚动条滑块。 */
|
||||||
gd::RectF opacityTrack; /**< 「标记透明度」滑动条轨道(总是显示,与选取模式无关)。 */
|
gd::RectF opacityTrack; /**< 「标记透明度」滑动条轨道(总是显示,与选取模式无关)。 */
|
||||||
@@ -1945,7 +1987,15 @@ namespace jdi
|
|||||||
bool cursorMode = false; /**< 该网格是否处于「自由标记点选取」(决定是否画纵向光标)。 */
|
bool cursorMode = false; /**< 该网格是否处于「自由标记点选取」(决定是否画纵向光标)。 */
|
||||||
int laneIndex = -1; /**< 该网格下标。 */
|
int laneIndex = -1; /**< 该网格下标。 */
|
||||||
double time = 0.0; /**< 标注时刻(显示时刻;吸附时 = 采样时刻)。 */
|
double time = 0.0; /**< 标注时刻(显示时刻;吸附时 = 采样时刻)。 */
|
||||||
float x = 0.0f; /**< 该时刻的屏幕横坐标。 */
|
float x = 0.0f; /**< 纵向光标的横坐标(= 鼠标位置,跟随鼠标)。 */
|
||||||
|
/**
|
||||||
|
* @brief 被标注的那个**数据点**的屏幕横坐标。
|
||||||
|
*
|
||||||
|
* 与 `x`(鼠标位置)分开:纵向光标画在 `x`,而取值圆点必须落在**真实采样点**上
|
||||||
|
*(`TimeToX(time)`)—— 稀疏信号下两者可能相差十几个像素。
|
||||||
|
* 用户 2026-10-30 明确要求"光标跟着鼠标走,提示靠纵向最近的数据点"。
|
||||||
|
*/
|
||||||
|
float pointX = 0.0f;
|
||||||
SignalRef ref; /**< 就近信号(网格内没有可用信号时无效)。 */
|
SignalRef ref; /**< 就近信号(网格内没有可用信号时无效)。 */
|
||||||
double value = 0.0; /**< 已叠加「调整」变换的取值(用于标注与增量)。 */
|
double value = 0.0; /**< 已叠加「调整」变换的取值(用于标注与增量)。 */
|
||||||
double enumValue = 0.0; /**< 采样原值(用于查枚举表)。 */
|
double enumValue = 0.0; /**< 采样原值(用于查枚举表)。 */
|
||||||
@@ -2750,6 +2800,14 @@ namespace jdi
|
|||||||
bool m_mouseOverPanel = false; /**< 上一帧鼠标是否落在某个实验性功能面板上。 */
|
bool m_mouseOverPanel = false; /**< 上一帧鼠标是否落在某个实验性功能面板上。 */
|
||||||
int m_peakDragLane = -1; /**< 正在拖动弱峰滑块的面板所属网格。 */
|
int m_peakDragLane = -1; /**< 正在拖动弱峰滑块的面板所属网格。 */
|
||||||
SignalRef m_peakDragRef; /**< 正在拖动弱峰滑块的**信号**(每个信号一套参数)。 */
|
SignalRef m_peakDragRef; /**< 正在拖动弱峰滑块的**信号**(每个信号一套参数)。 */
|
||||||
|
int m_meanDragLane = -1; /**< 正在拖动「均值滤波」参数滑块的面板所属网格。 */
|
||||||
|
SignalRef m_meanDragRef; /**< 正在拖动「均值滤波」参数滑块的信号(每信号一套参数)。 */
|
||||||
|
/**
|
||||||
|
* @brief 正在编辑「均值滤波」某个滑块上限时,输入框内灰色提示用的**当前上限值**。
|
||||||
|
*
|
||||||
|
* 与 `m_peakEditCurrentMax` 同一语义(输入框以空内容开始,空内容时显示当前上限)。
|
||||||
|
*/
|
||||||
|
int m_meanEditCurrentMax = 0;
|
||||||
/**
|
/**
|
||||||
* @brief 正在编辑「迭代次数 N 上限」时,输入框内灰色提示用的**当前上限值**。
|
* @brief 正在编辑「迭代次数 N 上限」时,输入框内灰色提示用的**当前上限值**。
|
||||||
*
|
*
|
||||||
@@ -3079,6 +3137,16 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
SignalRef ref; /**< 信号引用。 */
|
SignalRef ref; /**< 信号引用。 */
|
||||||
const SignalGroup* group = nullptr; /**< 所属分组(缓存键的一部分)。 */
|
const SignalGroup* group = nullptr; /**< 所属分组(缓存键的一部分)。 */
|
||||||
|
/**
|
||||||
|
* @brief 全部键字段的 64 位哈希(FNV-1a)。
|
||||||
|
*
|
||||||
|
* ⚠️ **缓存查找只比这一个值**:旧实现逐条比较下面十几个字段,
|
||||||
|
* 而"上百个信号合并到同一网格"时(例如 MF4-Record 的 283 个信号)每次查找
|
||||||
|
* 都要扫过整张表 ⇒ 绘制与 `ProbeFreeMarker` 每帧做数万次字段比较,
|
||||||
|
* 鼠标在网格内移动会明显卡顿(用户 2026-10-30 报告)。
|
||||||
|
* 命中哈希后再校验 `ref` / `sampleCount` / 视界三项(极低成本),可忽略碰撞风险。
|
||||||
|
*/
|
||||||
|
std::uint64_t key = 0;
|
||||||
std::size_t sampleCount = 0; /**< 采样点数(数据变化时改变)。 */
|
std::size_t sampleCount = 0; /**< 采样点数(数据变化时改变)。 */
|
||||||
double viewMin = 0.0; /**< 时间轴显示下界。 */
|
double viewMin = 0.0; /**< 时间轴显示下界。 */
|
||||||
double viewMax = 0.0; /**< 时间轴显示上界。 */
|
double viewMax = 0.0; /**< 时间轴显示上界。 */
|
||||||
@@ -3145,6 +3213,14 @@ namespace jdi
|
|||||||
const LaneVisual& lane) const;
|
const LaneVisual& lane) const;
|
||||||
|
|
||||||
mutable std::deque<PolylineCacheEntry> m_polylineCache; /**< 折线顶点缓存(按几何键复用)。 */
|
mutable std::deque<PolylineCacheEntry> m_polylineCache; /**< 折线顶点缓存(按几何键复用)。 */
|
||||||
|
/**
|
||||||
|
* @brief 折线顶点缓存的**容量上限**(条目数)。
|
||||||
|
*
|
||||||
|
* 由 `BuildLaneVisuals` 按"当前全部网格的信号总数 × 2"刷新(下限 `kPolylineCacheLimit`)。
|
||||||
|
* ⚠️ 固定 64 条在"多信号合并网格"下会**持续抖动**(装满即整表清空 ⇒ 下一帧全部重建),
|
||||||
|
* 这正是"上百个信号合并到同一网格时自由标记非常卡"的根因之一。
|
||||||
|
*/
|
||||||
|
mutable std::size_t m_polylineCacheCapacity = 64;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 图层**横向补条**渲染期间的折线裁剪范围(省掉整段折线的重建与描边)。
|
* @brief 图层**横向补条**渲染期间的折线裁剪范围(省掉整段折线的重建与描边)。
|
||||||
|
|||||||
@@ -200,36 +200,8 @@ namespace jdi
|
|||||||
inline void PaintPanelResetIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t color,
|
inline void PaintPanelResetIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t color,
|
||||||
float scale)
|
float scale)
|
||||||
{
|
{
|
||||||
const float inset = 2.5f * scale;
|
// 唯一实现见 `PaintUtil.hpp`(弹出菜单的还原图标、信号样式区的复位按钮共用同一套笔画)。
|
||||||
gd::RectF box(rect.X + inset, rect.Y + inset, rect.Width - inset * 2.0f,
|
UiPaintResetIcon(g, rect, color, scale);
|
||||||
rect.Height - inset * 2.0f);
|
|
||||||
if (box.Width <= 1.0f || box.Height <= 1.0f)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
gd::Pen pen(gd::Color((color >> 24) & 0xFFu, (color >> 16) & 0xFFu, (color >> 8) & 0xFFu,
|
|
||||||
color & 0xFFu),
|
|
||||||
std::max(1.0f, 1.5f * scale));
|
|
||||||
pen.SetStartCap(gd::LineCapRound);
|
|
||||||
pen.SetEndCap(gd::LineCapRound);
|
|
||||||
// 缺口留在右上角(从 300° 到 20° 处断开),与常见的"重置/刷新"图标一致。
|
|
||||||
g.DrawArc(&pen, box.X, box.Y, box.Width, box.Height, 300.0f, 300.0f);
|
|
||||||
|
|
||||||
// 箭头:位于缺口末端(约 300° 方向),指向前进方向。
|
|
||||||
const float cx = box.X + box.Width * 0.5f;
|
|
||||||
const float cy = box.Y + box.Height * 0.5f;
|
|
||||||
const float radius = box.Width * 0.5f;
|
|
||||||
const float angle = 300.0f * 3.14159265f / 180.0f;
|
|
||||||
const float tipX = cx + std::cos(angle) * radius;
|
|
||||||
const float tipY = cy + std::sin(angle) * radius;
|
|
||||||
const float size = std::max(2.5f, 3.2f * scale);
|
|
||||||
gd::PointF arrow[3];
|
|
||||||
arrow[0] = gd::PointF(tipX, tipY);
|
|
||||||
arrow[1] = gd::PointF(tipX - size, tipY - size * 0.25f);
|
|
||||||
arrow[2] = gd::PointF(tipX - size * 0.25f, tipY - size);
|
|
||||||
gd::SolidBrush brush(gd::Color((color >> 24) & 0xFFu, (color >> 16) & 0xFFu,
|
|
||||||
(color >> 8) & 0xFFu, color & 0xFFu));
|
|
||||||
g.FillPolygon(&brush, arrow, 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -579,6 +551,28 @@ namespace jdi
|
|||||||
inline const wchar_t* const kWeakPeakIterationsSliderLabel = L"迭代次数";
|
inline const wchar_t* const kWeakPeakIterationsSliderLabel = L"迭代次数";
|
||||||
inline const wchar_t* const kWeakPeakTitle = L"弱峰调整"; /**< 弱峰调整面板标题。 */
|
inline const wchar_t* const kWeakPeakTitle = L"弱峰调整"; /**< 弱峰调整面板标题。 */
|
||||||
inline const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱峰调整共用)。 */
|
inline const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱峰调整共用)。 */
|
||||||
|
|
||||||
|
// ---- 「均值滤波」面板的两行参数滑块(用户 2026-10-30 要求)----
|
||||||
|
/**
|
||||||
|
* 标签一律**不带 `X` / `N`**:滑块的量程本身已经说明是数值,符号只会让标签变长。
|
||||||
|
* (信号样式区与菜单里仍保留 `均值点数 X` / `迭代次数 N` 的写法,那是输入框语义。)
|
||||||
|
*/
|
||||||
|
inline const wchar_t* const kMeanFilterPointsSliderLabel = L"均值点数";
|
||||||
|
inline const wchar_t* const kMeanFilterIterationsSliderLabel = L"迭代次数";
|
||||||
|
/** 「均值点数」下限(窗口 = 2X+1 ⇒ X=0 就是原信号)。 */
|
||||||
|
inline const int kMeanFilterPointsMin = 0;
|
||||||
|
/** 「均值点数」滑块的默认上限(用户可点击数值改成其它正整数)。 */
|
||||||
|
inline const int kMeanFilterPointsDefaultMax = 50;
|
||||||
|
/** 「均值点数」上限允许的最大值(防止输入一个天文数字把滤波窗口撑爆)。 */
|
||||||
|
inline const int kMeanFilterPointsMaxLimit = 100000;
|
||||||
|
/** 新建条目时的默认均值点数(与 `Lane::MeanFilterItem::points` 初值一致)。 */
|
||||||
|
inline const int kMeanFilterPointsDefault = 5;
|
||||||
|
/** 「迭代次数」下限。 */
|
||||||
|
inline const int kMeanFilterIterationsMin = 1;
|
||||||
|
/** 「迭代次数」滑块的默认上限。 */
|
||||||
|
inline const int kMeanFilterIterationsDefaultMax = 10;
|
||||||
|
/** 「迭代次数」上限允许的最大值。 */
|
||||||
|
inline const int kMeanFilterIterationsMaxLimit = 1000;
|
||||||
/** 「自由标记」面板底部「自由标记点选取」切换按钮文案(激活态 = 光标模式开启)。 */
|
/** 「自由标记」面板底部「自由标记点选取」切换按钮文案(激活态 = 光标模式开启)。 */
|
||||||
inline const wchar_t* const kFreeMarkerCursorModeLabel = L"自由标记点选取";
|
inline const wchar_t* const kFreeMarkerCursorModeLabel = L"自由标记点选取";
|
||||||
/** 「自由标记」面板底部「显示增量」切换按钮文案(只影响已固定点的显示)。 */
|
/** 「自由标记」面板底部「显示增量」切换按钮文案(只影响已固定点的显示)。 */
|
||||||
@@ -1235,39 +1229,77 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 由信号色派生一个「高对比度」颜色:保持色相、大幅拉开明度。
|
* @brief 派生预览曲线相对底色的**最低对比度**(WCAG 比值)。
|
||||||
*
|
*
|
||||||
* 用途:「边沿计数(中心轴)」的平均值曲线叠加在原始波形之上,
|
* 用比值而不是"亮度差":同样的亮度差在很亮的底色上(白)几乎看不出来,
|
||||||
* 若直接用信号色会被波形淹没。这里把信号色向黑或白方向大幅偏移,
|
* 在深色底上却很显眼 —— 比值才是一致可见性的正确度量。
|
||||||
* 并**同时考虑与原始波形色以及与绘图区底色的距离**,取对比更强的一侧,
|
|
||||||
* 保证曲线既能与波形区分、又能压在底图上看得清。
|
|
||||||
*
|
|
||||||
* 结果是从信号色派生的,因此原信号改颜色时曲线颜色会自动跟着变。
|
|
||||||
*
|
|
||||||
* @param [IN] color 原始信号色(0xAARRGGBB)。
|
|
||||||
* @param [IN] background 绘图区底色,用于评估可见度。
|
|
||||||
* @return 派生出的高对比度颜色(alpha 与输入一致,由调用方再覆盖)。
|
|
||||||
*/
|
*/
|
||||||
inline std::uint32_t HighContrastDerived(std::uint32_t color, std::uint32_t background = 0xFFFDF6E3u)
|
inline const double kDerivedPreviewContrast = 3.2;
|
||||||
{
|
|
||||||
const auto squaredDistance = [](std::uint32_t a, std::uint32_t b)
|
|
||||||
{
|
|
||||||
const double dr =
|
|
||||||
static_cast<double>((a >> 16) & 0xFFu) - static_cast<double>((b >> 16) & 0xFFu);
|
|
||||||
const double dg =
|
|
||||||
static_cast<double>((a >> 8) & 0xFFu) - static_cast<double>((b >> 8) & 0xFFu);
|
|
||||||
const double db = static_cast<double>(a & 0xFFu) - static_cast<double>(b & 0xFFu);
|
|
||||||
return dr * dr + dg * dg + db * db;
|
|
||||||
};
|
|
||||||
|
|
||||||
const std::uint32_t darker = MixColor(color, 0xFF000000u, 0.72);
|
/**
|
||||||
const std::uint32_t lighter = MixColor(color, 0xFFFFFFFFu, 0.72);
|
* @brief 派生预览曲线与**源信号色**之间还要拉开的亮度差。
|
||||||
// 评分:与原始波形色的区分度为主(权重 1.0),与底色的可见度为辅(权重 0.6)。
|
*
|
||||||
const double scoreDarker =
|
* 源信号本身已经与底色有足够对比时,预览曲线再往外走这么多,避免"两条线同色叠在一起"。
|
||||||
squaredDistance(darker, color) + 0.6 * squaredDistance(darker, background);
|
*/
|
||||||
const double scoreLighter =
|
inline const double kDerivedPreviewGap = 0.18;
|
||||||
squaredDistance(lighter, color) + 0.6 * squaredDistance(lighter, background);
|
|
||||||
return (scoreDarker >= scoreLighter) ? darker : lighter;
|
/**
|
||||||
|
* @brief 由信号色派生「派生预览曲线」的颜色(与底色、与源信号同时保持足够对比)。
|
||||||
|
*
|
||||||
|
* 规则(**连续**,不含任何"临界点切换"):
|
||||||
|
* 1. 方向:沿"信号色相对底色的那一侧"继续远离底色
|
||||||
|
*(深底上的信号往更亮走、浅底上的信号往更暗走);
|
||||||
|
* 2. 先求"与底色对比度达到 `kDerivedPreviewContrast` 的亮度"(`EnsureContrast`),
|
||||||
|
* 再与"信号亮度 + `kDerivedPreviewGap`"取**更远离**底色的那个;
|
||||||
|
* 3. 用 `MixToLuminance` 把信号色沿亮度轴平滑推到该目标(保持色相)。
|
||||||
|
*
|
||||||
|
* 步 2 的两个候选都是输入的**连续函数**(取 max/min),因此拖拾色器 / 换预设色块 /
|
||||||
|
* 切主题时派生色**连续变化**,不会像旧实现那样"混 72% 黑还是白"按评分在阈值处跳变。
|
||||||
|
*
|
||||||
|
* @param [IN] color 源信号色(0xAARRGGBB,alpha 被忽略)。
|
||||||
|
* @param [IN] background 网格**底色**(填充 `ThemeLaneBackground`)。
|
||||||
|
* @return 派生颜色(不透明;透明度由调用方按信号不透明度重新叠加)。
|
||||||
|
*/
|
||||||
|
inline std::uint32_t HighContrastDerived(std::uint32_t color, std::uint32_t background)
|
||||||
|
{
|
||||||
|
const std::uint32_t base = color | 0xFF000000u;
|
||||||
|
const std::uint32_t back = background | 0xFF000000u;
|
||||||
|
const double backgroundLuminance = SrgbLuminance(back);
|
||||||
|
const double signalLuminance = SrgbLuminance(base);
|
||||||
|
|
||||||
|
// 方向:沿信号所在的"那一侧"继续远离底色。信号与底色几乎同亮度时
|
||||||
|
// 按底色的明暗选一侧(此时源曲线本身几乎看不见,方向没有更好选择)。
|
||||||
|
double direction = 0.0;
|
||||||
|
if (signalLuminance > backgroundLuminance + 1.0e-4)
|
||||||
|
{
|
||||||
|
direction = 1.0;
|
||||||
|
}
|
||||||
|
else if (signalLuminance < backgroundLuminance - 1.0e-4)
|
||||||
|
{
|
||||||
|
direction = -1.0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
direction = (backgroundLuminance < 0.5) ? 1.0 : -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 候选一:与底色对比度达到阈值的那个颜色(已达标时就是原色)。
|
||||||
|
const std::uint32_t required = EnsureContrast(back, base, kDerivedPreviewContrast);
|
||||||
|
const double requiredLuminance = SrgbLuminance(required);
|
||||||
|
// 候选二:在信号亮度的基础上再拉开一段。
|
||||||
|
const double gapLuminance = signalLuminance + direction * kDerivedPreviewGap;
|
||||||
|
|
||||||
|
double target = (direction > 0.0) ? std::max(requiredLuminance, gapLuminance)
|
||||||
|
: std::min(requiredLuminance, gapLuminance);
|
||||||
|
if (target > 1.0)
|
||||||
|
{
|
||||||
|
target = 1.0;
|
||||||
|
}
|
||||||
|
if (target < 0.0)
|
||||||
|
{
|
||||||
|
target = 0.0;
|
||||||
|
}
|
||||||
|
return MixToLuminance(base, target);
|
||||||
}
|
}
|
||||||
/** @brief FNV-1a 混合(面板像素缓存键共用)。 */
|
/** @brief FNV-1a 混合(面板像素缓存键共用)。 */
|
||||||
inline void MixPanelKey(std::uint64_t& hash, std::uint64_t value)
|
inline void MixPanelKey(std::uint64_t& hash, std::uint64_t value)
|
||||||
|
|||||||
@@ -436,6 +436,19 @@ namespace jdi
|
|||||||
return;
|
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;
|
const float gap = 0.0f;
|
||||||
float laneHeight = m_plotRect.Height / static_cast<float>(count);
|
float laneHeight = m_plotRect.Height / static_cast<float>(count);
|
||||||
if (laneHeight < m_minLaneHeight * m_scale)
|
if (laneHeight < m_minLaneHeight * m_scale)
|
||||||
|
|||||||
@@ -458,7 +458,7 @@ namespace jdi
|
|||||||
if (cardBottom > cardTop)
|
if (cardBottom > cardTop)
|
||||||
{
|
{
|
||||||
UiFillRect(g, gd::RectF(patch.X, cardTop, patch.Width, cardBottom - cardTop),
|
UiFillRect(g, gd::RectF(patch.X, cardTop, patch.Width, cardBottom - cardTop),
|
||||||
m_theme->cardBg);
|
ThemeLaneBackground(*m_theme));
|
||||||
}
|
}
|
||||||
|
|
||||||
const double viewMin = Tab().ViewMin();
|
const double viewMin = Tab().ViewMin();
|
||||||
|
|||||||
@@ -1789,6 +1789,23 @@ namespace jdi
|
|||||||
return true;
|
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)
|
bool PlotView::ToggleLaneFreeMarkerGroup(int laneIndex, const SignalRef& ref)
|
||||||
{
|
{
|
||||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
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::EDGE_CENTER_ITERATIONS ||
|
||||||
m_editTarget == PlotEditTarget::MEAN_FILTER_POINTS ||
|
m_editTarget == PlotEditTarget::MEAN_FILTER_POINTS ||
|
||||||
m_editTarget == PlotEditTarget::MEAN_FILTER_ITERATIONS ||
|
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)
|
if (m_editFieldRect.Width <= 0.0f)
|
||||||
{
|
{
|
||||||
@@ -641,6 +643,44 @@ namespace jdi
|
|||||||
return true;
|
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」:作用于被勾选信号的均值滤波曲线,夹在合理范围内。
|
// 「均值点数 X」/「迭代次数 N」:作用于被勾选信号的均值滤波曲线,夹在合理范围内。
|
||||||
// 「中心轴边沿计数」用**完全相同的参数与语义**,只是作用于自己的条目列表。
|
// 「中心轴边沿计数」用**完全相同的参数与语义**,只是作用于自己的条目列表。
|
||||||
const bool centerTarget =
|
const bool centerTarget =
|
||||||
@@ -882,7 +922,9 @@ namespace jdi
|
|||||||
return LoadCursorW(nullptr, IDC_SIZEWE);
|
return LoadCursorW(nullptr, IDC_SIZEWE);
|
||||||
case PlotDragMode::PEAK_LEVEL:
|
case PlotDragMode::PEAK_LEVEL:
|
||||||
case PlotDragMode::PEAK_ITERATIONS:
|
case PlotDragMode::PEAK_ITERATIONS:
|
||||||
// 「弱峰调整」滑块拖拽 → 左右箭头。
|
case PlotDragMode::MEAN_POINTS:
|
||||||
|
case PlotDragMode::MEAN_ITERATIONS:
|
||||||
|
// 「弱峰调整 / 均值滤波」参数滑块拖拽 → 左右箭头。
|
||||||
return LoadCursorW(nullptr, IDC_SIZEWE);
|
return LoadCursorW(nullptr, IDC_SIZEWE);
|
||||||
case PlotDragMode::MOVE_SIGNAL:
|
case PlotDragMode::MOVE_SIGNAL:
|
||||||
// 拖动信号(参考 Photoshop 平移元素的抓握光标)。
|
// 拖动信号(参考 Photoshop 平移元素的抓握光标)。
|
||||||
@@ -1393,6 +1435,72 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
return true;
|
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:
|
case PlotDragMode::LANE_REORDER:
|
||||||
{
|
{
|
||||||
// 被拖拽的网格自由跟随鼠标;目标位置变化时其它网格实时让位(吸附)。
|
// 被拖拽的网格自由跟随鼠标;目标位置变化时其它网格实时让位(吸附)。
|
||||||
@@ -1526,6 +1634,35 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
return false;
|
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:
|
case PlotDragMode::NONE:
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -2054,6 +2191,161 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
const bool meanMode = (kind == EdgePanelKind::MEAN);
|
const bool meanMode = (kind == EdgePanelKind::MEAN);
|
||||||
const bool boostMode = (kind == EdgePanelKind::PEAK);
|
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)];
|
Lane& tabLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
|
||||||
|
|
||||||
// 「弱峰调整」:先判两行滑块(数值文本 → 改上限;轨道 → 拖动)。
|
// 「弱峰调整」:先判两行滑块(数值文本 → 改上限;轨道 → 拖动)。
|
||||||
@@ -2707,6 +2999,9 @@ namespace jdi
|
|||||||
// 弱峰滑块拖拽结束:清掉目标(避免留下上一轮的信号引用)。
|
// 弱峰滑块拖拽结束:清掉目标(避免留下上一轮的信号引用)。
|
||||||
m_peakDragLane = -1;
|
m_peakDragLane = -1;
|
||||||
m_peakDragRef = SignalRef();
|
m_peakDragRef = SignalRef();
|
||||||
|
// 「均值滤波」参数滑块拖拽结束:同样清掉目标。
|
||||||
|
m_meanDragLane = -1;
|
||||||
|
m_meanDragRef = SignalRef();
|
||||||
// 「标记透明度」滑块 / 面板内容滚动条拖拽结束。
|
// 「标记透明度」滑块 / 面板内容滚动条拖拽结束。
|
||||||
m_markerOpacityDragLane = -1;
|
m_markerOpacityDragLane = -1;
|
||||||
m_markerScrollDragLane = -1;
|
m_markerScrollDragLane = -1;
|
||||||
@@ -2755,24 +3050,49 @@ namespace jdi
|
|||||||
int panelLane = -1;
|
int panelLane = -1;
|
||||||
int panelKind = 0;
|
int panelKind = 0;
|
||||||
if (TopPanelAtPoint(static_cast<float>(m_mouseX), static_cast<float>(m_mouseY),
|
if (TopPanelAtPoint(static_cast<float>(m_mouseX), static_cast<float>(m_mouseY),
|
||||||
panelLane, panelKind) &&
|
panelLane, panelKind))
|
||||||
static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MARK)
|
|
||||||
{
|
{
|
||||||
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, EdgePanelKind::MARK);
|
const EdgePanelKind hoveredKind = static_cast<EdgePanelKind>(panelKind);
|
||||||
if (layout.content.Width > 0.0f &&
|
// 「均值滤波」面板:内容区超高时用滚轮翻页(上百个信号合并时必备)。
|
||||||
UiPointInRect(layout.content, static_cast<float>(m_mouseX),
|
if (hoveredKind == EdgePanelKind::MEAN)
|
||||||
static_cast<float>(m_mouseY)))
|
|
||||||
{
|
{
|
||||||
const float maxScroll =
|
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, EdgePanelKind::MEAN);
|
||||||
std::max(0.0f, layout.markerContentHeight - layout.content.Height);
|
if (layout.content.Width > 0.0f &&
|
||||||
if (maxScroll > 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>(
|
const float maxScroll =
|
||||||
panelLane)]
|
std::max(0.0f, layout.markerContentHeight - layout.content.Height);
|
||||||
.freeMarkerScroll -
|
if (maxScroll > 0.0f)
|
||||||
notches * 40.0 * m_scale);
|
{
|
||||||
SetLaneFreeMarkerScroll(panelLane, std::max(0.0f, std::min(next, maxScroll)));
|
const float current =
|
||||||
return true;
|
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` 的说明;漏掉它会让线宽改动在**稠密信号**上不生效)。
|
//(详见 `PolylineCacheEntry::lineWidth` 的说明;漏掉它会让线宽改动在**稠密信号**上不生效)。
|
||||||
const float lineWidth = (resolved.style != nullptr) ? resolved.style->lineWidth : 1.0f;
|
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)
|
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 &&
|
if (entry.key != key || entry.sampleCount != sampleCount || !(entry.ref == ref) ||
|
||||||
entry.plotWidth == m_plotRect.Width && entry.laneY == lane.rect.Y &&
|
entry.viewMin != viewMin || entry.viewMax != viewMax)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
return &entry;
|
continue;
|
||||||
}
|
}
|
||||||
|
return &entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 未命中:重建并写入缓存(deque 保证已有元素的引用在追加后依然有效)。
|
// 未命中:重建并写入缓存(deque 保证已有元素的引用在追加后依然有效)。
|
||||||
// 缓存只用于消除重复计算,容量上限约等于“网格数 × 每网格信号数”的两倍。
|
// ⚠️ 容量必须**覆盖所有网格的全部信号**:旧实现固定 64 条,283 个信号合并到
|
||||||
if (m_polylineCache.size() >= kPolylineCacheLimit)
|
// 同一网格时会不断"清空重建"⇒ 绘制与自由标记探测每帧重算全部折线(严重卡顿)。
|
||||||
|
if (m_polylineCache.size() >= m_polylineCacheCapacity)
|
||||||
{
|
{
|
||||||
m_polylineCache.clear();
|
m_polylineCache.clear();
|
||||||
}
|
}
|
||||||
m_polylineCache.push_back(PolylineCacheEntry());
|
m_polylineCache.push_back(PolylineCacheEntry());
|
||||||
PolylineCacheEntry& entry = m_polylineCache.back();
|
PolylineCacheEntry& entry = m_polylineCache.back();
|
||||||
|
entry.key = key;
|
||||||
entry.ref = ref;
|
entry.ref = ref;
|
||||||
entry.group = resolved.group;
|
entry.group = resolved.group;
|
||||||
entry.sampleCount = sampleCount;
|
entry.sampleCount = sampleCount;
|
||||||
@@ -656,104 +692,102 @@ namespace jdi
|
|||||||
|
|
||||||
// 下面这三样在 `paintOverlays` 里绘制在**浮动面板之后**(压在面板之上):
|
// 下面这三样在 `paintOverlays` 里绘制在**浮动面板之后**(压在面板之上):
|
||||||
// 滚动条、右下角「回到最上方」悬浮按钮、内联输入框。
|
// 滚动条、右下角「回到最上方」悬浮按钮、内联输入框。
|
||||||
// ⇒ 必须比面板层(`baseLayer + 1`)再高一层,否则与实际像素的层序相反。
|
// ⇒ 它们的层号在下面「面板循环」之后统一给(比最上面板再高一层),
|
||||||
boxes.SetLayer(baseLayer + 2);
|
// 否则与实际像素的层序相反。这里先复位到组件基层。
|
||||||
boxes.Add(m_scrollBarRect, 5);
|
|
||||||
boxes.Add(BackToTopRect(), 5);
|
|
||||||
boxes.Add(EditFieldRect(), 5);
|
|
||||||
boxes.SetLayer(baseLayer);
|
boxes.SetLayer(baseLayer);
|
||||||
|
|
||||||
// ⚠️⚠️ **浮动面板必须另起一个绘制层,并把面板矩形登记为遮挡矩形**:
|
// ⚠️⚠️ **浮动面板必须另起绘制层,并把面板矩形登记为遮挡矩形**:
|
||||||
// 面板是不透明浮层、且常常**跨多个网格上下排列**,与网格区域重叠。
|
// 面板是不透明浮层、且常常**跨多个网格上下排列**,与网格区域重叠。
|
||||||
// 若与网格同层,网格的矩形边线会横穿面板 —— 用户 2026-10-29 报告的
|
// 若与网格同层,网格的矩形边线会横穿面板 —— 用户 2026-10-29 报告的
|
||||||
// "网格的布局边界穿透显示在浮动面板上"。`layer` 决定谁压谁,
|
// "网格的布局边界穿透显示在浮动面板上"。`layer` 决定谁压谁,
|
||||||
// `AddOccluder` 才真正把下层 box 从面板区域里**挖掉**。
|
// `AddOccluder` 才真正把下层 box 从面板区域里**挖掉**。
|
||||||
bool panelLayerPushed = false;
|
|
||||||
|
|
||||||
// 层级 2/3/4:5 类实验性功能浮动面板的内部控件。
|
// 层级 2/3/4:5 类实验性功能浮动面板的内部控件。
|
||||||
// 这些矩形由 `EdgePanelLayoutFor` 统一算出(绘制与命中共用),一个循环即可全部上报。
|
// 这些矩形由 `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` 内部要取
|
// ⚠️ **先用最便宜的判据跳过"没有面板"的网格**:`EdgePanelLayoutFor` 内部要取
|
||||||
// 标签页配置、逐项判断种类与开关,单次约 2~3µs;283 个网格 × 5 种 = 1415 次调用
|
// 标签页配置、逐项判断种类与开关,单次约 2~3µs;283 个网格 × 5 种 = 1415 次调用
|
||||||
// 实测约 **4ms/帧**(这是"开调试后卡"的主要来源之一)。
|
// 实测约 **4ms/帧**(这是"开调试后卡"的主要来源之一)。
|
||||||
// 只有该网格**至少有一个面板处于展开状态**时才值得去算布局。
|
// 现在只对**层序表里真正显示出来的面板**算布局(层序表本来就只含展开且启用的面板)。
|
||||||
const std::vector<Lane>& laneConfigs =
|
for (std::size_t ordinal = 0; ordinal < panelCount; ++ordinal)
|
||||||
(m_tab != nullptr) ? m_tab->Lanes() : m_localTab.Lanes();
|
|
||||||
for (std::size_t index = 0; index < m_lanes.size() && index < laneConfigs.size(); ++index)
|
|
||||||
{
|
{
|
||||||
const Lane& laneConfig = laneConfigs[index];
|
const PanelRef& panelRef = m_panelOrder[ordinal];
|
||||||
const bool anyPanelOpen = laneConfig.edgePanel.open ||
|
const EdgePanelLayout layout =
|
||||||
laneConfig.edgeCenterPanel.open ||
|
EdgePanelLayoutFor(panelRef.laneIndex, static_cast<EdgePanelKind>(panelRef.kind));
|
||||||
laneConfig.meanFilterPanel.open ||
|
if (!UiRectHasArea(layout.panel))
|
||||||
laneConfig.weakPeakPanel.open ||
|
|
||||||
laneConfig.freeMarkerPanel.open;
|
|
||||||
if (!anyPanelOpen)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const int laneIndex = static_cast<int>(index);
|
// 本面板独占一层(层号越大越靠上)。
|
||||||
for (int kindValue = static_cast<int>(EdgePanelKind::COUNT);
|
boxes.SetLayer(baseLayer + 1 + static_cast<int>(ordinal));
|
||||||
kindValue <= static_cast<int>(EdgePanelKind::MARK); ++kindValue)
|
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 =
|
boxes.Add(group.header, 3);
|
||||||
EdgePanelLayoutFor(laneIndex, static_cast<EdgePanelKind>(kindValue));
|
boxes.Add(group.tableHeader, 3);
|
||||||
if (!UiRectHasArea(layout.panel))
|
boxes.Add(group.triangle, 4);
|
||||||
{
|
boxes.Add(group.close, 4);
|
||||||
continue;
|
boxes.Add(group.check, 4);
|
||||||
}
|
}
|
||||||
if (!panelLayerPushed)
|
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
|
||||||
{
|
{
|
||||||
boxes.SetLayer(baseLayer + 1);
|
boxes.Add(row.rect, 4);
|
||||||
panelLayerPushed = true;
|
boxes.Add(row.close, 4);
|
||||||
}
|
|
||||||
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.SetClip(gd::RectF());
|
boxes.SetClip(gd::RectF());
|
||||||
|
boxes.SetLayer(topOverlayLayer);
|
||||||
|
boxes.Add(m_scrollBarRect, 5);
|
||||||
|
boxes.Add(BackToTopRect(), 5);
|
||||||
|
boxes.Add(EditFieldRect(), 5);
|
||||||
boxes.SetLayer(baseLayer);
|
boxes.SetLayer(baseLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,17 +882,19 @@ namespace jdi
|
|||||||
// 这样浮动面板仍压在最上层,与其它实验性功能的层序一致)。
|
// 这样浮动面板仍压在最上层,与其它实验性功能的层序一致)。
|
||||||
PaintFreeMarker(g, m_lanes);
|
PaintFreeMarker(g, m_lanes);
|
||||||
// 从这里开始都在面板之上:文本层同步 +1(与 `BuildBoxes` 的 `baseLayer + 1` 一致)。
|
// 从这里开始都在面板之上:文本层同步 +1(与 `BuildBoxes` 的 `baseLayer + 1` 一致)。
|
||||||
|
// ⚠️ `PaintEdgeCursor` 内部会**逐面板**再把层号 +1(每个面板独占一层),
|
||||||
|
// 并在返回前把层号推到**最上面板之上**,因此这里不再写死 `+2`。
|
||||||
UiSetTextLayer(textLayer + 1);
|
UiSetTextLayer(textLayer + 1);
|
||||||
PaintEdgeCursor(g, m_lanes);
|
PaintEdgeCursor(g, m_lanes);
|
||||||
// 之后这几样绘制在浮动面板**之上**:与 `BuildBoxes` 的 `baseLayer + 2` 对应。
|
|
||||||
UiSetTextLayer(textLayer + 2);
|
|
||||||
PaintSignalInteraction(g);
|
PaintSignalInteraction(g);
|
||||||
PaintHint(g);
|
PaintHint(g);
|
||||||
// 网格 Y 轴上下限的输入框、以及「弱峰调整 → 迭代次数上限」的输入框都绘制在最上层
|
// 网格 Y 轴上下限的输入框、以及「弱峰调整 → 迭代次数上限」的输入框都绘制在最上层
|
||||||
//(后者覆盖在浮动面板的滑块行之上)。
|
//(后者覆盖在浮动面板的滑块行之上)。
|
||||||
if (m_editTarget == PlotEditTarget::LANE_Y_MAX ||
|
if (m_editTarget == PlotEditTarget::LANE_Y_MAX ||
|
||||||
m_editTarget == PlotEditTarget::LANE_Y_MIN ||
|
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);
|
PaintEditField(g);
|
||||||
}
|
}
|
||||||
@@ -1105,7 +1141,9 @@ namespace jdi
|
|||||||
UiFillRect(g, m_area, m_theme->plotBg);
|
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();
|
m_legendHits.clear();
|
||||||
@@ -1580,7 +1618,8 @@ namespace jdi
|
|||||||
const gd::RectF gridClip = laneClip;
|
const gd::RectF gridClip = laneClip;
|
||||||
/** 标尺数值文本的裁剪矩形(GDI 文本需要显式裁剪,否则会越过绘图区)。 */
|
/** 标尺数值文本的裁剪矩形(GDI 文本需要显式裁剪,否则会越过绘图区)。 */
|
||||||
const gd::RectF clipRectForLabels = gridClip;
|
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 weakLineColor = m_theme->gridWeak | 0xFF000000u;
|
||||||
const std::uint32_t strongLineColor = m_theme->gridStrong | 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 limitTop = lane.rect.Y - lane.rect.Height * 4.0f;
|
||||||
const float limitBottom = lane.rect.Y + lane.rect.Height * 5.0f;
|
const float limitBottom = lane.rect.Y + lane.rect.Height * 5.0f;
|
||||||
|
|
||||||
// 派生曲线的颜色:与原信号**同色相**但大幅拉开明度,使其与原始波形形成高对比
|
// 派生曲线的颜色:与原信号**同色相**、亮度按**当前网格底色**自适应拉开,
|
||||||
// (原信号改颜色时曲线随之改变,因为它是从信号色派生出来的);透明度与原信号一致。
|
// 使其与原始波形、与背景都形成足够对比(原信号改颜色时曲线随之改变,因为它是从信号色
|
||||||
|
// 派生出来的);透明度与原信号一致。
|
||||||
const unsigned int baseAlpha = static_cast<unsigned int>(
|
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);
|
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)),
|
const std::uint32_t derived = WithAlpha(
|
||||||
baseAlpha);
|
HighContrastDerived(StyleColor(*resolved.style), ThemeLaneBackground(*m_theme)),
|
||||||
|
baseAlpha);
|
||||||
|
|
||||||
// 顶点预算与波形一致(每像素列两个顶点)。
|
// 顶点预算与波形一致(每像素列两个顶点)。
|
||||||
// 交互期间进一步降到 1/2:派生曲线往往有 10 条(5 网格 × 均值滤波 + 弱峰调整),
|
// 交互期间进一步降到 1/2:派生曲线往往有 10 条(5 网格 × 均值滤波 + 弱峰调整),
|
||||||
@@ -3480,6 +3521,10 @@ namespace jdi
|
|||||||
mix(lane.freeMarkerPanel.collapsed ? 1ull : 0ull);
|
mix(lane.freeMarkerPanel.collapsed ? 1ull : 0ull);
|
||||||
mix(lane.edgeValueValid ? 1ull : 0ull);
|
mix(lane.edgeValueValid ? 1ull : 0ull);
|
||||||
mixDouble(lane.edgeValue);
|
mixDouble(lane.edgeValue);
|
||||||
|
// 「自由标记」面板内容区的滚动偏移(历史行为:计入全局键)。
|
||||||
|
// ⚠️ 「均值滤波」的滚动偏移只进**该面板自己的**槽键(见 `PanelSlotKey`),
|
||||||
|
// 否则滚动一个面板会让屏幕上所有面板一起重画。
|
||||||
|
mixFloat(lane.freeMarkerScroll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3573,6 +3618,8 @@ namespace jdi
|
|||||||
mixRect(group.tableHeader);
|
mixRect(group.tableHeader);
|
||||||
mixRect(group.triangle);
|
mixRect(group.triangle);
|
||||||
mixRect(group.close);
|
mixRect(group.close);
|
||||||
|
// 「均值滤波」的组头右端是复选框(「自由标记」为空矩形,不影响它的键)。
|
||||||
|
mixRect(group.check);
|
||||||
mix(group.collapsed ? 1ull : 0ull);
|
mix(group.collapsed ? 1ull : 0ull);
|
||||||
mix(group.pinned ? 1ull : 0ull);
|
mix(group.pinned ? 1ull : 0ull);
|
||||||
mix(static_cast<std::uint64_t>(group.ref.groupIndex + 2));
|
mix(static_cast<std::uint64_t>(group.ref.groupIndex + 2));
|
||||||
@@ -3616,6 +3663,11 @@ namespace jdi
|
|||||||
const Lane::MeanFilterItem* meanItem = lane.FindMeanFilterItem(ref);
|
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->points + 2 : 0));
|
||||||
mix(static_cast<std::uint64_t>(meanItem != nullptr ? meanItem->iterations + 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);
|
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->points + 2 : 0));
|
||||||
mix(static_cast<std::uint64_t>(centerItem != nullptr ? centerItem->iterations + 2 : 0));
|
mix(static_cast<std::uint64_t>(centerItem != nullptr ? centerItem->iterations + 2 : 0));
|
||||||
@@ -3704,6 +3756,17 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
mixRect(rect);
|
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;
|
const float margin = 6.0f * m_scale;
|
||||||
@@ -4067,12 +4130,23 @@ namespace jdi
|
|||||||
//(与"可拖拽"的光晕区分:这里只是单点圆环,不描整条曲线)。
|
//(与"可拖拽"的光晕区分:这里只是单点圆环,不描整条曲线)。
|
||||||
const float radius = 3.4f * m_scale;
|
const float radius = 3.4f * m_scale;
|
||||||
gd::SolidBrush dot(ToGdiColor(signalColor));
|
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)
|
if (probe.snapped)
|
||||||
{
|
{
|
||||||
gd::Pen ring(ToGdiColor(lineColor), 1.2f * m_scale);
|
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,
|
g.DrawEllipse(&ring, pointX - radius - 3.0f * m_scale,
|
||||||
(radius + 3.0f * m_scale) * 2.0f, (radius + 3.0f * m_scale) * 2.0f);
|
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::size_t count = std::min(lanes.size(), tabLanes.size());
|
||||||
const std::uint32_t color = CursorColor();
|
const std::uint32_t color = CursorColor();
|
||||||
const UiInk ink = UiContrastInk(color);
|
const UiInk ink = UiContrastInk(color);
|
||||||
|
// 进入本函数时的文本层号 = 宿主给本组件分配的面板层起始(`baseLayer + 1`)。
|
||||||
|
// 每个面板在循环里各用 `panelTextLayer + 面板序数`,函数结束前把层号推到**最上面板之上**。
|
||||||
|
const int panelTextLayer = UiGetTextLayer();
|
||||||
|
|
||||||
// ---- 横向光标(线与两端把手 + 左侧幅值读数)----
|
// ---- 横向光标(线与两端把手 + 左侧幅值读数)----
|
||||||
for (std::size_t index = 0; index < count; ++index)
|
for (std::size_t index = 0; index < count; ++index)
|
||||||
@@ -4283,6 +4360,10 @@ namespace jdi
|
|||||||
const PanelRef& panelRef = m_panelOrder[panelOrdinal];
|
const PanelRef& panelRef = m_panelOrder[panelOrdinal];
|
||||||
const EdgePanelKind kind = static_cast<EdgePanelKind>(panelRef.kind);
|
const EdgePanelKind kind = static_cast<EdgePanelKind>(panelRef.kind);
|
||||||
const std::size_t index = static_cast<std::size_t>(panelRef.laneIndex);
|
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 centerAxis = (kind == EdgePanelKind::CENTER);
|
||||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||||
@@ -4353,11 +4434,43 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
if (hit != nullptr && hit->key != 0 && hit->key == slotKey)
|
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();
|
HDC target = g.GetHDC();
|
||||||
if (target != nullptr)
|
if (target != nullptr)
|
||||||
{
|
{
|
||||||
BitBlt(target, hit->x0, hit->y0, hit->x1 - hit->x0, hit->y1 - hit->y0,
|
const int bx0 = static_cast<int>(std::floor(panel.X)) - 1;
|
||||||
m_panelDc, hit->x0, hit->y0, SRCCOPY);
|
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);
|
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 标记点」+ ✕),下属固定点条目六列对齐(序号 / 取值 / 枚举 / 增量 / 时刻 / 间隔),
|
// 「N 标记点」+ ✕),下属固定点条目六列对齐(序号 / 取值 / 枚举 / 增量 / 时刻 / 间隔),
|
||||||
// 行尾各带一个 ✕。内容区有滚动偏移时按内容区裁剪。
|
// 行尾各带一个 ✕。内容区有滚动偏移时按内容区裁剪。
|
||||||
@@ -5166,6 +5464,10 @@ namespace jdi
|
|||||||
m_panelSlots = std::move(nextSlots);
|
m_panelSlots = std::move(nextSlots);
|
||||||
m_panelGlobalKey = panelGlobalKey;
|
m_panelGlobalKey = panelGlobalKey;
|
||||||
m_panelValid = panelCacheReady;
|
m_panelValid = panelCacheReady;
|
||||||
|
|
||||||
|
// 面板之后绘制的叠加层(信号拖拽虚影 / 提示 / 输入框 / 滚动条 / 回到最上方)
|
||||||
|
// 都在**所有面板之上** ⇒ 文本层号推到最上面板的上一层(与 `BuildBoxes` 同源)。
|
||||||
|
UiSetTextLayer(panelTextLayer + static_cast<int>(m_panelOrder.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlotView::PaintEditField(gd::Graphics& g)
|
void PlotView::PaintEditField(gd::Graphics& g)
|
||||||
@@ -5181,10 +5483,21 @@ namespace jdi
|
|||||||
|
|
||||||
// 「迭代次数 N 上限」:输入框**以空内容开始**,未输入任何内容时以灰色提示当前上限值。
|
// 「迭代次数 N 上限」:输入框**以空内容开始**,未输入任何内容时以灰色提示当前上限值。
|
||||||
// 提示文案 = “输入上限(当前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
|
const bool peakTarget = (m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX);
|
||||||
: kWeakPeakIterationsDefaultMax;
|
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 std::wstring hint = L"输入上限(当前" + std::to_wstring(currentMax) + L")";
|
||||||
const float hintLeft = fieldRect.X + 11.0f * m_scale;
|
const float hintLeft = fieldRect.X + 11.0f * m_scale;
|
||||||
UiTextAtClipped(g, hint, m_fonts->monoSmall, hintLeft,
|
UiTextAtClipped(g, hint, m_fonts->monoSmall, hintLeft,
|
||||||
|
|||||||
@@ -69,6 +69,17 @@ namespace jdi
|
|||||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + nameWidth +
|
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + nameWidth +
|
||||||
kEdgePanelMeanCheckSize * m_scale + 6.0f * m_scale);
|
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 rising = 0;
|
||||||
std::size_t falling = 0;
|
std::size_t falling = 0;
|
||||||
bool complete = true;
|
bool complete = true;
|
||||||
@@ -116,7 +127,7 @@ namespace jdi
|
|||||||
if (centerAxis || meanFilter)
|
if (centerAxis || meanFilter)
|
||||||
{
|
{
|
||||||
// 参数行 = 两个「说明文字 + 数值框」,必须保证文字不被数值框压住。
|
// 参数行 = 两个「说明文字 + 数值框」,必须保证文字不被数值框压住。
|
||||||
// (MEAN / CENTER:「均值点数 X」+「迭代次数 N」。标签与数值框各占半行。)
|
// (CENTER:「均值点数 X」+「迭代次数 N」。MEAN 已改为滑块,见下面单独的分支。)
|
||||||
const float labelXWidth = UiMeasureTextWidth(m_fonts->uiSmall, L"均值点数 X");
|
const float labelXWidth = UiMeasureTextWidth(m_fonts->uiSmall, L"均值点数 X");
|
||||||
const float labelN = UiMeasureTextWidth(m_fonts->uiSmall, L"迭代次数 N");
|
const float labelN = UiMeasureTextWidth(m_fonts->uiSmall, L"迭代次数 N");
|
||||||
const float fieldWidth =
|
const float fieldWidth =
|
||||||
@@ -127,6 +138,23 @@ namespace jdi
|
|||||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth);
|
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)
|
if (weakPeak)
|
||||||
{
|
{
|
||||||
// 滑块行 = 「标签列 + 轨道(最小宽度)+ 数值列 + 复位按钮」(等级与迭代次数两行共用同一列宽)。
|
// 滑块行 = 「标签列 + 轨道(最小宽度)+ 数值列 + 复位按钮」(等级与迭代次数两行共用同一列宽)。
|
||||||
@@ -233,7 +261,9 @@ namespace jdi
|
|||||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||||
const bool freeMarker = (kind == EdgePanelKind::MARK);
|
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 hasButton = (meanFilter || weakPeak);
|
||||||
const bool enabled = meanFilter ? lane.meanFilter
|
const bool enabled = meanFilter ? lane.meanFilter
|
||||||
: (weakPeak ? lane.weakPeak
|
: (weakPeak ? lane.weakPeak
|
||||||
@@ -286,12 +316,43 @@ namespace jdi
|
|||||||
// + 参数部分(**只在该条目被勾选时才占位**,因此面板高度随勾选数量动态变化):
|
// + 参数部分(**只在该条目被勾选时才占位**,因此面板高度随勾选数量动态变化):
|
||||||
// * MEAN / CENTER:一行「均值点数 X + 迭代次数 N」输入框(中心轴再多一行统计结果);
|
// * MEAN / CENTER:一行「均值点数 X + 迭代次数 N」输入框(中心轴再多一行统计结果);
|
||||||
// * PEAK:两行滑块(调整幅度 % / 迭代次数 N),每个信号各有一套自己的参数。
|
// * PEAK:两行滑块(调整幅度 % / 迭代次数 N),每个信号各有一套自己的参数。
|
||||||
const bool itemHasParams = (centerAxis || meanFilter || weakPeak);
|
const bool itemHasParams = (centerAxis || weakPeak);
|
||||||
const float weakPeakSliderRow = kWeakPeakSliderRowHeight * m_scale;
|
const float weakPeakSliderRow = kWeakPeakSliderRowHeight * m_scale;
|
||||||
float meanRowHeight = 0.0f;
|
float meanRowHeight = 0.0f;
|
||||||
float itemsHeight = 0.0f;
|
float itemsHeight = 0.0f;
|
||||||
|
float meanContentHeight = 0.0f;
|
||||||
float height = 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;
|
meanRowHeight = kEdgePanelMeanRowHeight * m_scale;
|
||||||
for (const SignalRef& ref : lane.signals)
|
for (const SignalRef& ref : lane.signals)
|
||||||
@@ -501,7 +562,119 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return layout;
|
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;
|
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
|
||||||
if (hasButton)
|
if (hasButton)
|
||||||
@@ -1209,7 +1382,11 @@ namespace jdi
|
|||||||
double bestValue = 0.0;
|
double bestValue = 0.0;
|
||||||
double bestEnum = 0.0;
|
double bestEnum = 0.0;
|
||||||
float bestY = 0.0f;
|
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)
|
for (const SignalRef& ref : lane.signals)
|
||||||
{
|
{
|
||||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||||
@@ -1218,9 +1395,10 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 「自动点线」判据成立时吸附到最近采样点(与光标吸附同一规则)。
|
// 「自动点线」判据只用于决定要不要画“已吸附”圆环 ⇒ **不在循环里逐个信号算**:
|
||||||
// ⚠️ 用户明确要求:只要判据成立就吸附,**不**管当前是散点还是用户自己改过的样式。
|
// 它的代价是“每个小格两次二分”(视界 50 格 × 上百信号 = 数万次比较/帧),
|
||||||
const bool sparse = MinorCellSparse(*resolved.axis, *resolved.data, resolved.style->offsetTime);
|
// 而**只有最后胜出的那一个信号**需要它(用户 2026-10-30 报告的卡顿根因之一)。
|
||||||
|
// 见循环结束后的统一判定。
|
||||||
|
|
||||||
// ---- ① 取“鼠标到这条曲线”的距离(复用绘制阶段的折线顶点)----
|
// ---- ① 取“鼠标到这条曲线”的距离(复用绘制阶段的折线顶点)----
|
||||||
float nearestX = fx;
|
float nearestX = fx;
|
||||||
@@ -1348,13 +1526,22 @@ namespace jdi
|
|||||||
ApplyDrawTransform(*resolved.style, candidateSample);
|
ApplyDrawTransform(*resolved.style, candidateSample);
|
||||||
if (std::isfinite(candidateValue))
|
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(
|
const double vertical = std::fabs(
|
||||||
static_cast<double>(ValueToY(candidateValue, visual)) -
|
static_cast<double>(ValueToY(candidateValue, visual)) -
|
||||||
static_cast<double>(fy));
|
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;
|
distance = sampleDistance;
|
||||||
nearestX = fx;
|
nearestX = static_cast<float>(pointX);
|
||||||
time = candidateTime;
|
time = candidateTime;
|
||||||
sampled = candidateSample;
|
sampled = candidateSample;
|
||||||
candidateOk = true;
|
candidateOk = true;
|
||||||
@@ -1368,9 +1555,6 @@ namespace jdi
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 固定点必须落在真实采样时刻上(两个候选都已吸附到采样点)。
|
// 固定点必须落在真实采样时刻上(两个候选都已吸附到采样点)。
|
||||||
// 表示“已吸附”的圆环仍只在「自动点线」判据成立(`sparse`)时画:
|
|
||||||
// 密采样信号里吸附量远小于 1 像素,圆环会一直挂着,反而像是画错了。
|
|
||||||
const bool snapped = sparse;
|
|
||||||
const double value = ApplyDrawTransform(*resolved.style, sampled);
|
const double value = ApplyDrawTransform(*resolved.style, sampled);
|
||||||
if (!std::isfinite(value))
|
if (!std::isfinite(value))
|
||||||
{
|
{
|
||||||
@@ -1386,7 +1570,9 @@ namespace jdi
|
|||||||
bestValue = value;
|
bestValue = value;
|
||||||
bestEnum = sampled;
|
bestEnum = sampled;
|
||||||
bestY = y;
|
bestY = y;
|
||||||
bestSnapped = snapped;
|
bestAxis = resolved.axis;
|
||||||
|
bestData = resolved.data;
|
||||||
|
bestOffsetTime = resolved.style->offsetTime;
|
||||||
probe.ref = ref;
|
probe.ref = ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1398,11 +1584,20 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
probe.valid = true;
|
probe.valid = true;
|
||||||
probe.time = bestTime;
|
probe.time = bestTime;
|
||||||
probe.x = TimeToX(bestTime);
|
// ⚠️ 纵向光标**画在鼠标所在的 x**(用户 2026-10-30 明确要求:纵向光标跟随鼠标,
|
||||||
|
// 被标注的数据点才是"离鼠标最近"的那个)。旧实现把竖线画在吸附后的采样时刻,
|
||||||
|
// 于是稀疏/单点信号会让整条竖线"跳"到数据点上,观感像光标被吸走了。
|
||||||
|
probe.x = fx;
|
||||||
|
probe.pointX = TimeToX(bestTime);
|
||||||
probe.value = bestValue;
|
probe.value = bestValue;
|
||||||
probe.enumValue = bestEnum;
|
probe.enumValue = bestEnum;
|
||||||
probe.y = bestY;
|
probe.y = bestY;
|
||||||
probe.snapped = bestSnapped;
|
// 「已吸附」圆环只在「自动点线」稀疏判据成立时画(密采样信号里吸附量 < 1 像素,
|
||||||
|
// 圆环会一直挂着反而像画错了)。**只对胜出的这一个信号算**(见循环里的说明)。
|
||||||
|
if (bestAxis != nullptr && bestData != nullptr)
|
||||||
|
{
|
||||||
|
probe.snapped = MinorCellSparse(*bestAxis, *bestData, bestOffsetTime);
|
||||||
|
}
|
||||||
return probe;
|
return probe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1446,7 +1446,7 @@ namespace jdi
|
|||||||
g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
|
g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
|
||||||
|
|
||||||
UiFillRect(g, m_area, m_theme->plotBg);
|
UiFillRect(g, m_area, m_theme->plotBg);
|
||||||
UiFillRect(g, m_area, m_theme->cardBg);
|
UiFillRect(g, m_area, ThemeLaneBackground(*m_theme));
|
||||||
|
|
||||||
// 竖向网格刻度沿用**前台视界跨度**(而非宽窗口的跨度),否则竖网格线会只有一半。
|
// 竖向网格刻度沿用**前台视界跨度**(而非宽窗口的跨度),否则竖网格线会只有一半。
|
||||||
// ⚠️ 同时把它作为竖向网格的**主刻步长**传给 `PaintLane`(见 `m_renderTimeStep`):
|
// ⚠️ 同时把它作为竖向网格的**主刻步长**传给 `PaintLane`(见 `m_renderTimeStep`):
|
||||||
@@ -1948,7 +1948,7 @@ namespace jdi
|
|||||||
//(用户 2026-10-17 报告:滚动过渡动画在巨型信号处卡顿)。
|
//(用户 2026-10-17 报告:滚动过渡动画在巨型信号处卡顿)。
|
||||||
// 这一情形在滚动到**从未构建过图层的区域**时必然出现(上百个网格时绝大多数
|
// 这一情形在滚动到**从未构建过图层的区域**时必然出现(上百个网格时绝大多数
|
||||||
// 网格的图层都还没建立):旧实现在这里 `return false` ⇒ 落到 `Paint` 末尾的
|
// 网格的图层都还没建立):旧实现在这里 `return false` ⇒ 落到 `Paint` 末尾的
|
||||||
// **整幅重绘** ⇒ 把**本帧全部可见网格的波形**(可能包含上千�万采样的巨型信号)
|
// **整幅重绘** ⇒ 把**本帧全部可见网格的波形**(可能包含上千万采样的巨型信号)
|
||||||
// 每帧重画一遍 —— 实测单帧 **20~35ms**,而手动拖滚动条只要 ~6ms。
|
// 每帧重画一遍 —— 实测单帧 **20~35ms**,而手动拖滚动条只要 ~6ms。
|
||||||
// 正确行为:本帧只画**网格底衬**(廉价,见下方),波形等视界真正停下之后再按预算补
|
// 正确行为:本帧只画**网格底衬**(廉价,见下方),波形等视界真正停下之后再按预算补
|
||||||
//(这正是用户要求的“没来得及画信号时就按只有网格叠底的状态继续滚动”)。
|
//(这正是用户要求的“没来得及画信号时就按只有网格叠底的状态继续滚动”)。
|
||||||
|
|||||||
@@ -355,6 +355,20 @@ namespace jdi
|
|||||||
void UiFillRoundRects(gd::Graphics& g, const std::vector<gd::RectF>& rects, float radius,
|
void UiFillRoundRects(gd::Graphics& g, const std::vector<gd::RectF>& rects, float radius,
|
||||||
std::uint32_t argb);
|
std::uint32_t argb);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 绘制「还原 / 重置」图标(逆时针回环箭头)。
|
||||||
|
*
|
||||||
|
* **唯一实现**:弹出菜单标题栏的还原图标(`PopupMenu`)、弱峰调整面板每行滑块的复位控件、
|
||||||
|
* 信号样式区「线宽 / 透明度」两行右侧的复位按钮全部调用它,因此同一语义在任何位置
|
||||||
|
* 都是同一套笔画,任意 DPI 下都清晰(不依赖字体或图标资源)。
|
||||||
|
*
|
||||||
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
* @param [IN] rect 图标外接矩形(正方形;边长为 0 时不做任何绘制)。
|
||||||
|
* @param [IN] argb 笔画颜色(0xAARRGGBB)。
|
||||||
|
* @param [IN] scale DPI 缩放比例。
|
||||||
|
*/
|
||||||
|
void UiPaintResetIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float scale);
|
||||||
|
|
||||||
} // namespace jdi
|
} // namespace jdi
|
||||||
|
|
||||||
#endif // __PAINT_UTIL_HPP__
|
#endif // __PAINT_UTIL_HPP__
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
|
#include "PaintUtil.hpp"
|
||||||
|
|
||||||
namespace jdi
|
namespace jdi
|
||||||
{
|
{
|
||||||
@@ -63,36 +64,8 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
inline void PaintResetIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t color, float scale)
|
inline void PaintResetIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t color, float scale)
|
||||||
{
|
{
|
||||||
const float inset = 2.5f * scale;
|
// 唯一实现见 `PaintUtil.hpp`(信号样式区的复位按钮、弱峰调整面板的复位控件共用)。
|
||||||
gd::RectF box(rect.X + inset, rect.Y + inset, rect.Width - inset * 2.0f,
|
UiPaintResetIcon(g, rect, color, scale);
|
||||||
rect.Height - inset * 2.0f);
|
|
||||||
if (box.Width <= 1.0f || box.Height <= 1.0f)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
gd::Pen pen(gd::Color((color >> 24) & 0xFFu, (color >> 16) & 0xFFu, (color >> 8) & 0xFFu,
|
|
||||||
color & 0xFFu),
|
|
||||||
std::max(1.0f, 1.5f * scale));
|
|
||||||
pen.SetStartCap(gd::LineCapRound);
|
|
||||||
pen.SetEndCap(gd::LineCapRound);
|
|
||||||
// 缺口留在右上角(从 300° 到 20° 处断开),与常见的"重置/刷新"图标一致。
|
|
||||||
g.DrawArc(&pen, box.X, box.Y, box.Width, box.Height, 300.0f, 300.0f);
|
|
||||||
|
|
||||||
// 箭头:位于缺口末端(约 300° 方向),指向前进方向。
|
|
||||||
const float cx = box.X + box.Width * 0.5f;
|
|
||||||
const float cy = box.Y + box.Height * 0.5f;
|
|
||||||
const float radius = box.Width * 0.5f;
|
|
||||||
const float angle = 300.0f * 3.14159265f / 180.0f;
|
|
||||||
const float tipX = cx + std::cos(angle) * radius;
|
|
||||||
const float tipY = cy + std::sin(angle) * radius;
|
|
||||||
const float size = std::max(2.5f, 3.2f * scale);
|
|
||||||
gd::PointF arrow[3];
|
|
||||||
arrow[0] = gd::PointF(tipX, tipY);
|
|
||||||
arrow[1] = gd::PointF(tipX - size, tipY - size * 0.25f);
|
|
||||||
arrow[2] = gd::PointF(tipX - size * 0.25f, tipY - size);
|
|
||||||
gd::SolidBrush brush(gd::Color((color >> 24) & 0xFFu, (color >> 16) & 0xFFu,
|
|
||||||
(color >> 8) & 0xFFu, color & 0xFFu));
|
|
||||||
g.FillPolygon(&brush, arrow, 3);
|
|
||||||
}
|
}
|
||||||
} // namespace pmdetail
|
} // namespace pmdetail
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,51 @@ namespace jdi
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 颜色的**相对亮度**(WCAG 2.x 定义:sRGB 线性化后按 0.2126/0.7152/0.0722 加权)。
|
||||||
|
* @param [IN] argb 颜色值(只看 RGB 通道)。
|
||||||
|
* @return 相对亮度,取值 0(黑)… 1(白)。
|
||||||
|
*/
|
||||||
|
double SrgbLuminance(std::uint32_t argb);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 两色之间的**对比度**(WCAG 2.x 定义:`(L1 + 0.05) / (L2 + 0.05)`)。
|
||||||
|
* @param [IN] firstColor 第一色。
|
||||||
|
* @param [IN] secondColor 第二色。
|
||||||
|
* @return 对比度,取值 1(完全相同)… 21(黑白)。
|
||||||
|
*/
|
||||||
|
double ContrastRatio(std::uint32_t firstColor, std::uint32_t secondColor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 保证前景色相对底色有**足够对比度**:不足时沿"远离底色"的方向混向黑/白。
|
||||||
|
*
|
||||||
|
* 用途:绘图区内部的网格线、时间轴等分线、分界线等原本按某一个主题手工调色的元素,
|
||||||
|
* 必须跟着主题背景色自动适配(浅底上加深、深底上提亮),否则换到深色主题后
|
||||||
|
* 网格线只剩几个色阶、几乎看不见。
|
||||||
|
*
|
||||||
|
* ⚠️ **已达标时逐位返回原色**:Solarised Light 的历史配色是人工调过的,
|
||||||
|
* 只要它满足阈值就必须保持逐像素不变(避免"换主题修复"顺带改变默认主题观感)。
|
||||||
|
*
|
||||||
|
* @param [IN] background 底色(不透明)。
|
||||||
|
* @param [IN] foreground 前景色(不透明)。
|
||||||
|
* @param [IN] minRatio 期望的最小对比度(≥ 1,1 表示不限制)。
|
||||||
|
* @return 满足阈值的颜色(RGB 不变、alpha = 0xFF)。
|
||||||
|
*/
|
||||||
|
std::uint32_t EnsureContrast(std::uint32_t background, std::uint32_t foreground, double minRatio);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 把颜色沿"亮度"插值到目标相对亮度(保持色相,二分求解混色比例)。
|
||||||
|
*
|
||||||
|
* 比"固定比例混向黑/白"更可控:调用方指定**目标亮度**,本函数负责求出混色量。
|
||||||
|
* 目标亮度高于当前亮度时混向白、低于时混向黑;求解连续 ⇒ 输入微调时输出也连续
|
||||||
|
*(不会在两个极端色之间跳变)。
|
||||||
|
*
|
||||||
|
* @param [IN] color 原色(不透明)。
|
||||||
|
* @param [IN] targetLuminance 目标相对亮度(0…1)。
|
||||||
|
* @return 亮度接近目标值的颜色(无法达到时取该方向上的极限)。
|
||||||
|
*/
|
||||||
|
std::uint32_t MixToLuminance(std::uint32_t color, double targetLuminance);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 界面主题色板(Solarised Light 派生)。
|
* @brief 界面主题色板(Solarised Light 派生)。
|
||||||
*/
|
*/
|
||||||
@@ -152,6 +197,22 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
void SetCurrentTheme(const Theme* theme);
|
void SetCurrentTheme(const Theme* theme);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 取「绘图区网格底色」——**网格叠底**(每条网格内部的底色)。
|
||||||
|
*
|
||||||
|
* 它与 `Theme::plotBg`(绘图区整体底色)分开:网格之外的时间轴/留白区用 `plotBg`,
|
||||||
|
* 网格内部用本函数。当两者过于接近时,本函数会把网格底色**轻轻地**往远离绘图区
|
||||||
|
* 底色的方向挪一点(仅供深色主题的 Solarised Dark 这类"两者完全同色"的情形),
|
||||||
|
* 保证相邻网格仍能被分界线以外的底色层次区分开。
|
||||||
|
*
|
||||||
|
* ⚠️ 两者本来就有可辨识的亮度差时**逐位返回 `cardBg`**(Solarised Light / Light / Dark
|
||||||
|
* 的默认观感不变)。
|
||||||
|
*
|
||||||
|
* @param [IN] theme 主题。
|
||||||
|
* @return 网格底色(不透明)。
|
||||||
|
*/
|
||||||
|
std::uint32_t ThemeLaneBackground(const Theme& theme);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 获取波形默认配色表。
|
* @brief 获取波形默认配色表。
|
||||||
* @param [OUT] count 配色数量输出。
|
* @param [OUT] count 配色数量输出。
|
||||||
|
|||||||
@@ -676,4 +676,40 @@ namespace jdi
|
|||||||
g.ReleaseHDC(hdc);
|
g.ReleaseHDC(hdc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UiPaintResetIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float scale)
|
||||||
|
{
|
||||||
|
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float inset = 2.5f * scale;
|
||||||
|
const gd::RectF box(rect.X + inset, rect.Y + inset, rect.Width - inset * 2.0f,
|
||||||
|
rect.Height - inset * 2.0f);
|
||||||
|
if (box.Width <= 1.0f || box.Height <= 1.0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gd::Color color = ToGdiColor(argb);
|
||||||
|
gd::Pen pen(color, std::max(1.0f, 1.5f * scale));
|
||||||
|
pen.SetStartCap(gd::LineCapRound);
|
||||||
|
pen.SetEndCap(gd::LineCapRound);
|
||||||
|
// 缺口留在右上角(从 300° 到 20° 处断开),与常见的"重置/刷新"图标一致。
|
||||||
|
g.DrawArc(&pen, box.X, box.Y, box.Width, box.Height, 300.0f, 300.0f);
|
||||||
|
|
||||||
|
// 箭头:位于缺口末端(约 300° 方向),指向前进方向。
|
||||||
|
const float cx = box.X + box.Width * 0.5f;
|
||||||
|
const float cy = box.Y + box.Height * 0.5f;
|
||||||
|
const float radius = box.Width * 0.5f;
|
||||||
|
const float angle = 300.0f * 3.14159265f / 180.0f;
|
||||||
|
const float tipX = cx + std::cos(angle) * radius;
|
||||||
|
const float tipY = cy + std::sin(angle) * radius;
|
||||||
|
const float size = std::max(2.5f, 3.2f * scale);
|
||||||
|
gd::PointF arrow[3];
|
||||||
|
arrow[0] = gd::PointF(tipX, tipY);
|
||||||
|
arrow[1] = gd::PointF(tipX - size, tipY - size * 0.25f);
|
||||||
|
arrow[2] = gd::PointF(tipX - size * 0.25f, tipY - size);
|
||||||
|
gd::SolidBrush brush(color);
|
||||||
|
g.FillPolygon(&brush, arrow, 3);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace jdi
|
} // namespace jdi
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace jdi
|
namespace jdi
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -32,8 +35,32 @@ namespace jdi
|
|||||||
MakeArgb(kBase01), MakeArgb(kBase0),
|
MakeArgb(kBase01), MakeArgb(kBase0),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @brief 全局唯一主题实例。 */
|
/**
|
||||||
const Theme kSolarizedLight = []
|
* @brief 把绘图区内部的"结构性"颜色按**底色**自动校正到足够对比(见 `EnsureContrast`)。
|
||||||
|
*
|
||||||
|
* 阈值(对比度)取"Solarised Light 现有配色刚好达标"的水位:
|
||||||
|
* 该主题的历史配色是人工调过的,达标时 `EnsureContrast` 逐位返回原色 ⇒ 默认主题
|
||||||
|
* **一个像素都不会变**;其它主题(尤其深色)则自动把网格线/时间轴/分界线提亮。
|
||||||
|
*
|
||||||
|
* | 字段 | 阈值 | Solarised Light 现状 |
|
||||||
|
* | --- | ---: | ---: |
|
||||||
|
* | `gridWeak`(网格主刻线 + 五等分线基色) | 1.30 | 约 1.38 |
|
||||||
|
* | `gridStrong`(零线/加重线) | 1.65 | 约 1.80 |
|
||||||
|
* | `axisLine`(时间轴、网格分界线、绘图区外框) | 2.10 | 约 2.43 |
|
||||||
|
* | `borderStrong`(网格间分界线) | 1.70 | 约 2.43 |
|
||||||
|
*/
|
||||||
|
Theme NormalizePlotContrast(Theme theme)
|
||||||
|
{
|
||||||
|
const std::uint32_t plot = theme.plotBg | 0xFF000000u;
|
||||||
|
theme.gridWeak = EnsureContrast(plot, theme.gridWeak | 0xFF000000u, 1.30);
|
||||||
|
theme.gridStrong = EnsureContrast(plot, theme.gridStrong | 0xFF000000u, 1.65);
|
||||||
|
theme.axisLine = EnsureContrast(plot, theme.axisLine | 0xFF000000u, 2.10);
|
||||||
|
theme.borderStrong = EnsureContrast(plot, theme.borderStrong | 0xFF000000u, 1.70);
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief 全局唯一主题实例(Solarised Light;历史默认主题)。 */
|
||||||
|
const Theme kSolarizedLight = NormalizePlotContrast([]
|
||||||
{
|
{
|
||||||
Theme theme;
|
Theme theme;
|
||||||
theme.windowBg = MakeArgb(kBase3);
|
theme.windowBg = MakeArgb(kBase3);
|
||||||
@@ -65,7 +92,7 @@ namespace jdi
|
|||||||
theme.danger = MakeArgb(kRed);
|
theme.danger = MakeArgb(kRed);
|
||||||
theme.warning = MakeArgb(kYellow);
|
theme.warning = MakeArgb(kYellow);
|
||||||
return theme;
|
return theme;
|
||||||
}();
|
}());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 浅色主题(参考 VS Code 2026 的默认浅色配色)。
|
* @brief 浅色主题(参考 VS Code 2026 的默认浅色配色)。
|
||||||
@@ -73,7 +100,7 @@ namespace jdi
|
|||||||
* 与 Solarised Light 的区别:底色是**中性白/浅灰**而不是暖米色,
|
* 与 Solarised Light 的区别:底色是**中性白/浅灰**而不是暖米色,
|
||||||
* 强调色是 VS Code 的系统蓝 `#0078D4`,描边是中性灰。
|
* 强调色是 VS Code 的系统蓝 `#0078D4`,描边是中性灰。
|
||||||
*/
|
*/
|
||||||
const Theme kLight = []
|
const Theme kLight = NormalizePlotContrast([]
|
||||||
{
|
{
|
||||||
Theme theme;
|
Theme theme;
|
||||||
theme.windowBg = MakeArgb(0xFFFFFFu); // editor.background
|
theme.windowBg = MakeArgb(0xFFFFFFu); // editor.background
|
||||||
@@ -96,6 +123,7 @@ namespace jdi
|
|||||||
theme.selection = MakeArgb(0xE8E8E8u);
|
theme.selection = MakeArgb(0xE8E8E8u);
|
||||||
|
|
||||||
// 白底上网格线必须够淡(否则抢波形),但主刻线要能与次级线区分。
|
// 白底上网格线必须够淡(否则抢波形),但主刻线要能与次级线区分。
|
||||||
|
// ⚠️ 这三个颜色会被 `NormalizePlotContrast` 按白底自动校正到可见水位。
|
||||||
theme.gridWeak = MakeArgb(0xEDEDEDu);
|
theme.gridWeak = MakeArgb(0xEDEDEDu);
|
||||||
theme.gridStrong = MakeArgb(0xD6D6D6u);
|
theme.gridStrong = MakeArgb(0xD6D6D6u);
|
||||||
theme.axisLine = MakeArgb(0xCECECEu);
|
theme.axisLine = MakeArgb(0xCECECEu);
|
||||||
@@ -104,12 +132,12 @@ namespace jdi
|
|||||||
theme.danger = MakeArgb(0xE51400u); // errorForeground
|
theme.danger = MakeArgb(0xE51400u); // errorForeground
|
||||||
theme.warning = MakeArgb(0xBF8803u);
|
theme.warning = MakeArgb(0xBF8803u);
|
||||||
return theme;
|
return theme;
|
||||||
}();
|
}());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 深色主题(参考 VS Code Dark+)。
|
* @brief 深色主题(参考 VS Code Dark+)。
|
||||||
*/
|
*/
|
||||||
const Theme kDark = []
|
const Theme kDark = NormalizePlotContrast([]
|
||||||
{
|
{
|
||||||
Theme theme;
|
Theme theme;
|
||||||
theme.windowBg = MakeArgb(0x1E1E1Eu); // editor.background
|
theme.windowBg = MakeArgb(0x1E1E1Eu); // editor.background
|
||||||
@@ -131,6 +159,8 @@ namespace jdi
|
|||||||
theme.pressed = MakeArgb(0x37373Du);
|
theme.pressed = MakeArgb(0x37373Du);
|
||||||
theme.selection = MakeArgb(0x04395Eu);
|
theme.selection = MakeArgb(0x04395Eu);
|
||||||
|
|
||||||
|
// ⚠️ 深色底上的原始取值几乎与底色同亮度(差 2~3 个色阶)⇒ 交给
|
||||||
|
// `NormalizePlotContrast` 按 `plotBg` 自动提亮到可见水位。
|
||||||
theme.gridWeak = MakeArgb(0x2B2B2Bu);
|
theme.gridWeak = MakeArgb(0x2B2B2Bu);
|
||||||
theme.gridStrong = MakeArgb(0x3A3A3Au);
|
theme.gridStrong = MakeArgb(0x3A3A3Au);
|
||||||
theme.axisLine = MakeArgb(0x4A4A4Au);
|
theme.axisLine = MakeArgb(0x4A4A4Au);
|
||||||
@@ -139,7 +169,7 @@ namespace jdi
|
|||||||
theme.danger = MakeArgb(0xF44747u);
|
theme.danger = MakeArgb(0xF44747u);
|
||||||
theme.warning = MakeArgb(0xCCA700u);
|
theme.warning = MakeArgb(0xCCA700u);
|
||||||
return theme;
|
return theme;
|
||||||
}();
|
}());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Solarised Dark(参考 VS Code 的 Solarized Dark)。
|
* @brief Solarised Dark(参考 VS Code 的 Solarized Dark)。
|
||||||
@@ -147,7 +177,7 @@ namespace jdi
|
|||||||
* 底色用 base03、面板用 base02,强调色仍是 Solarised 那一套 —— 因此波形配色表
|
* 底色用 base03、面板用 base02,强调色仍是 Solarised 那一套 —— 因此波形配色表
|
||||||
* (`SignalPalette`)与 Solarised Light 完全一致。
|
* (`SignalPalette`)与 Solarised Light 完全一致。
|
||||||
*/
|
*/
|
||||||
const Theme kSolarizedDark = []
|
const Theme kSolarizedDark = NormalizePlotContrast([]
|
||||||
{
|
{
|
||||||
Theme theme;
|
Theme theme;
|
||||||
theme.windowBg = MakeArgb(kBase03);
|
theme.windowBg = MakeArgb(kBase03);
|
||||||
@@ -177,12 +207,121 @@ namespace jdi
|
|||||||
theme.danger = MakeArgb(kRed);
|
theme.danger = MakeArgb(kRed);
|
||||||
theme.warning = MakeArgb(kYellow);
|
theme.warning = MakeArgb(kYellow);
|
||||||
return theme;
|
return theme;
|
||||||
}();
|
}());
|
||||||
|
|
||||||
/** @brief 全局「当前主题」指针(由宿主设置,见 `SetCurrentTheme`)。 */
|
/** @brief 全局「当前主题」指针(由宿主设置,见 `SetCurrentTheme`)。 */
|
||||||
const Theme* g_currentTheme = &kSolarizedLight;
|
const Theme* g_currentTheme = &kSolarizedLight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double SrgbLuminance(std::uint32_t argb)
|
||||||
|
{
|
||||||
|
// sRGB → 线性光:> 0.03928 用 2.4 次幂,否则用 12.92 线性段(WCAG 2.x)。
|
||||||
|
const auto channel = [](std::uint32_t value) -> double
|
||||||
|
{
|
||||||
|
const double c = static_cast<double>(value & 0xFFu) / 255.0;
|
||||||
|
return (c <= 0.03928) ? (c / 12.92) : std::pow((c + 0.055) / 1.055, 2.4);
|
||||||
|
};
|
||||||
|
const double r = channel((argb >> 16) & 0xFFu);
|
||||||
|
const double g = channel((argb >> 8) & 0xFFu);
|
||||||
|
const double b = channel(argb & 0xFFu);
|
||||||
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ContrastRatio(std::uint32_t firstColor, std::uint32_t secondColor)
|
||||||
|
{
|
||||||
|
const double first = SrgbLuminance(firstColor);
|
||||||
|
const double second = SrgbLuminance(secondColor);
|
||||||
|
const double high = std::max(first, second);
|
||||||
|
const double low = std::min(first, second);
|
||||||
|
return (high + 0.05) / (low + 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t EnsureContrast(std::uint32_t background, std::uint32_t foreground, double minRatio)
|
||||||
|
{
|
||||||
|
if (!(minRatio > 1.0) || ContrastRatio(background, foreground) >= minRatio)
|
||||||
|
{
|
||||||
|
return foreground; // 已达标(或不限制):**逐位返回原色**
|
||||||
|
}
|
||||||
|
// 远离底色:深底就提亮、浅底就加深。二分求解"刚好达标"的混色量,
|
||||||
|
// 使输出随底色**连续**变化(不会在阈值附近两个极端色之间跳)。
|
||||||
|
const bool lighten = SrgbLuminance(background) < 0.5;
|
||||||
|
const std::uint32_t extreme = lighten ? 0xFFFFFFFFu : 0xFF000000u;
|
||||||
|
double low = 0.0;
|
||||||
|
double high = 1.0;
|
||||||
|
for (int iteration = 0; iteration < 18; ++iteration)
|
||||||
|
{
|
||||||
|
const double middle = (low + high) * 0.5;
|
||||||
|
if (ContrastRatio(background, MixColor(foreground, extreme, middle)) < minRatio)
|
||||||
|
{
|
||||||
|
low = middle;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
high = middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MixColor(foreground | 0xFF000000u, extreme, high);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t MixToLuminance(std::uint32_t color, double targetLuminance)
|
||||||
|
{
|
||||||
|
const std::uint32_t base = color | 0xFF000000u;
|
||||||
|
const double current = SrgbLuminance(base);
|
||||||
|
if (!(targetLuminance > 0.0))
|
||||||
|
{
|
||||||
|
targetLuminance = 0.0;
|
||||||
|
}
|
||||||
|
if (targetLuminance > 1.0)
|
||||||
|
{
|
||||||
|
targetLuminance = 1.0;
|
||||||
|
}
|
||||||
|
if (std::fabs(targetLuminance - current) < 1.0e-4)
|
||||||
|
{
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
// 亮度沿"混向白 / 混向黑"单调 ⇒ 二分解出混色量;
|
||||||
|
// 目标亮度等于当前亮度时混色量为 0(等价返回原色),因此两侧在该点**连续衔接**。
|
||||||
|
const std::uint32_t extreme = (targetLuminance > current) ? 0xFFFFFFFFu : 0xFF000000u;
|
||||||
|
double low = 0.0;
|
||||||
|
double high = 1.0;
|
||||||
|
for (int iteration = 0; iteration < 20; ++iteration)
|
||||||
|
{
|
||||||
|
const double middle = (low + high) * 0.5;
|
||||||
|
const double luminance = SrgbLuminance(MixColor(base, extreme, middle));
|
||||||
|
if (targetLuminance > current)
|
||||||
|
{
|
||||||
|
if (luminance < targetLuminance)
|
||||||
|
{
|
||||||
|
low = middle;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
high = middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (luminance > targetLuminance)
|
||||||
|
{
|
||||||
|
low = middle;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
high = middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MixColor(base, extreme, high);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t ThemeLaneBackground(const Theme& theme)
|
||||||
|
{
|
||||||
|
// 网格底色必须与绘图区底色**可辨**(否则几十条网格看起来是一整片)。
|
||||||
|
// 用统一的对比度阈值 1.03:本就有差异的主题(Solarised Light / Light / Dark)
|
||||||
|
// 逐位返回 `cardBg`;两侧完全同色的主题(Solarised Dark 的 base03)会被轻轻推开。
|
||||||
|
return EnsureContrast(theme.plotBg | 0xFF000000u, theme.cardBg | 0xFF000000u, 1.03);
|
||||||
|
}
|
||||||
|
|
||||||
const Theme& ThemeFor(ThemeKind kind)
|
const Theme& ThemeFor(ThemeKind kind)
|
||||||
{
|
{
|
||||||
switch (kind)
|
switch (kind)
|
||||||
|
|||||||
@@ -502,6 +502,20 @@ namespace jdi
|
|||||||
OPACITY_VALUE, /**< 不透明度数值框(可点击输入)。 */
|
OPACITY_VALUE, /**< 不透明度数值框(可点击输入)。 */
|
||||||
WIDTH_SLIDER, /**< 线宽滑块。 */
|
WIDTH_SLIDER, /**< 线宽滑块。 */
|
||||||
WIDTH_VALUE, /**< 线宽数值框(可点击输入)。 */
|
WIDTH_VALUE, /**< 线宽数值框(可点击输入)。 */
|
||||||
|
/**
|
||||||
|
* @brief 线宽行右侧的**复位按钮**(把线宽恢复为该信号的默认值)。
|
||||||
|
*
|
||||||
|
* 默认值:普通信号 = `kDefaultSignalLineWidth`(1.0 逻辑像素),
|
||||||
|
* 单数据点信号 = `kSinglePointSignalLineWidth`(最大线宽:散点半径随线宽增长)。
|
||||||
|
* 图标与「系数/偏移」二级菜单标题栏的还原图标**完全同款**(`UiPaintResetIcon`)。
|
||||||
|
*/
|
||||||
|
WIDTH_RESET,
|
||||||
|
/**
|
||||||
|
* @brief 透明度行右侧的**复位按钮**(把不透明度恢复为 100%)。
|
||||||
|
*
|
||||||
|
* 与 `WIDTH_RESET` 同一套图标与悬停观感,两者右边界与内容列右缘对齐。
|
||||||
|
*/
|
||||||
|
OPACITY_RESET,
|
||||||
GAIN_ADJUST, /**< 「系数」按钮(点击展开系数二级面板)。 */
|
GAIN_ADJUST, /**< 「系数」按钮(点击展开系数二级面板)。 */
|
||||||
MIRROR_ADJUST, /**< 「镜像」按钮(点击展开镜像二级面板)。 */
|
MIRROR_ADJUST, /**< 「镜像」按钮(点击展开镜像二级面板)。 */
|
||||||
ABSOLUTE_TOGGLE, /**< 「绝对值」切换按钮。 */
|
ABSOLUTE_TOGGLE, /**< 「绝对值」切换按钮。 */
|
||||||
@@ -1228,6 +1242,21 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
bool ApplyOpacityFromX(float x);
|
bool ApplyOpacityFromX(float x);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 把当前目标信号的**线宽恢复为默认值**(「线宽」行右侧复位按钮)。
|
||||||
|
*
|
||||||
|
* 默认值与新建信号时同一来源(`DefaultSignalLineWidth(finiteCount)`):
|
||||||
|
* 普通信号 1.0 逻辑像素、**单数据点信号**取最大线宽(散点半径随线宽增长)。
|
||||||
|
* @return 取值发生变化返回 true。
|
||||||
|
*/
|
||||||
|
bool ResetStyleLineWidth();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 把当前目标信号的**不透明度恢复为 100%**(「透明度」行右侧复位按钮)。
|
||||||
|
* @return 取值发生变化返回 true。
|
||||||
|
*/
|
||||||
|
bool ResetStyleOpacity();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 断言某「样式」选项对当前目标信号是否可用(不可用时置灰且不可点)。
|
* @brief 断言某「样式」选项对当前目标信号是否可用(不可用时置灰且不可点)。
|
||||||
*
|
*
|
||||||
@@ -1285,6 +1314,15 @@ namespace jdi
|
|||||||
gd::RectF m_opacityValueRect; /**< 不透明度数值文本矩形。 */
|
gd::RectF m_opacityValueRect; /**< 不透明度数值文本矩形。 */
|
||||||
gd::RectF m_widthTrackRect; /**< 线宽滑块轨道矩形。 */
|
gd::RectF m_widthTrackRect; /**< 线宽滑块轨道矩形。 */
|
||||||
gd::RectF m_widthValueRect; /**< 线宽数值文本矩形(双击可输入)。 */
|
gd::RectF m_widthValueRect; /**< 线宽数值文本矩形(双击可输入)。 */
|
||||||
|
/**
|
||||||
|
* @brief 线宽行右侧的**复位按钮**矩形(正方形,右边界与内容列右缘对齐)。
|
||||||
|
*
|
||||||
|
* 点击把该信号的线宽恢复为默认值(单数据点信号与普通信号的默认线宽不同)。
|
||||||
|
* 宽高为 0 表示当前不显示(没有目标信号)。
|
||||||
|
*/
|
||||||
|
gd::RectF m_widthResetRect;
|
||||||
|
/** @brief 透明度行右侧的**复位按钮**矩形(与 `m_widthResetRect` 同一列、同一尺寸)。 */
|
||||||
|
gd::RectF m_opacityResetRect;
|
||||||
/** 「调整」行的三个等宽按钮(下标 = 内部常量 `kAdjust*`):0 = 系数/偏移,1 = 镜像,2 = 绝对值。 */
|
/** 「调整」行的三个等宽按钮(下标 = 内部常量 `kAdjust*`):0 = 系数/偏移,1 = 镜像,2 = 绝对值。 */
|
||||||
std::array<gd::RectF, 3> m_adjustRects;
|
std::array<gd::RectF, 3> m_adjustRects;
|
||||||
|
|
||||||
|
|||||||
@@ -174,8 +174,15 @@ namespace jdi
|
|||||||
inline const float kSwatchMaxGap = 10.0f; /**< 颜色色块最大间距(用于铺满内容列)。 */
|
inline const float kSwatchMaxGap = 10.0f; /**< 颜色色块最大间距(用于铺满内容列)。 */
|
||||||
inline const float kSliderValueWidth = 52.0f; /**< 滑块行数值文本宽度(透明度与线宽一致)。 */
|
inline const float kSliderValueWidth = 52.0f; /**< 滑块行数值文本宽度(透明度与线宽一致)。 */
|
||||||
inline const float kSliderValueGap = 8.0f; /**< 滑块轨道与数值文本间距。 */
|
inline const float kSliderValueGap = 8.0f; /**< 滑块轨道与数值文本间距。 */
|
||||||
inline const float kTrackHeight = 6.0f; /**< 滑块轨道高度。 */
|
inline const float kTrackHeight = 6.0f; /**< 滑块轨道高度。 */ /**
|
||||||
inline const float kAdjustGap = 6.0f; /**< 「调整」行三个按钮之间的间距。 */
|
* @brief 「线宽 / 透明度」行右侧**复位按钮**的边长(逻辑像素)。
|
||||||
|
*
|
||||||
|
* 与「系数/偏移」二级菜单标题栏的还原图标同一观感(`UiPaintResetIcon`),
|
||||||
|
* 因此尺寸也取同一量级;行高不足时会被夹到行高。
|
||||||
|
*/
|
||||||
|
inline const float kSliderResetSize = 16.0f;
|
||||||
|
/** @brief 数值文本与复位按钮之间的间距。 */
|
||||||
|
inline const float kSliderResetGap = 4.0f; inline const float kAdjustGap = 6.0f; /**< 「调整」行三个按钮之间的间距。 */
|
||||||
inline const float kAdjustMenuGap = 4.0f; /**< 「系数/偏移」「镜像」二级面板与按钮左上角的额外间距。 */
|
inline const float kAdjustMenuGap = 4.0f; /**< 「系数/偏移」「镜像」二级面板与按钮左上角的额外间距。 */
|
||||||
|
|
||||||
/** 「调整」行的按钮下标(与 `m_adjustRects` 同序)。 */
|
/** 「调整」行的按钮下标(与 `m_adjustRects` 同序)。 */
|
||||||
|
|||||||
@@ -119,8 +119,10 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
boxes.Add(m_widthTrackRect, 3);
|
boxes.Add(m_widthTrackRect, 3);
|
||||||
boxes.Add(m_widthValueRect, 3);
|
boxes.Add(m_widthValueRect, 3);
|
||||||
|
boxes.Add(m_widthResetRect, 3);
|
||||||
boxes.Add(m_opacityTrackRect, 3);
|
boxes.Add(m_opacityTrackRect, 3);
|
||||||
boxes.Add(m_opacityValueRect, 3);
|
boxes.Add(m_opacityValueRect, 3);
|
||||||
|
boxes.Add(m_opacityResetRect, 3);
|
||||||
|
|
||||||
// ── 「目标」行(样式区顶部,展示当前选中信号) ────────────────────
|
// ── 「目标」行(样式区顶部,展示当前选中信号) ────────────────────
|
||||||
boxes.Add(m_targetRowRect, 2);
|
boxes.Add(m_targetRowRect, 2);
|
||||||
@@ -972,19 +974,35 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- 线宽行与透明度行:几何完全一致,保证纵向宽度对齐(线宽在上、透明度在下)----
|
// ---- 线宽行与透明度行:几何完全一致,保证纵向宽度对齐(线宽在上、透明度在下)----
|
||||||
const float valueWidth = std::min(kSliderValueWidth * m_scale, contentWidth * 0.4f);
|
// 两行右端各有一个**复位按钮**(用户 2026-10-29 要求):
|
||||||
const float trackWidth = std::max(24.0f, contentWidth - valueWidth - kSliderValueGap * m_scale);
|
// 按钮贴齐内容列右缘,数值文本与轨道在它左侧依次收缩 ⇒ 不会超出内容列。
|
||||||
|
// 内容列太窄(放不下"轨道 + 数值 + 按钮")时整列不显示复位按钮,避免把滑块挤没。
|
||||||
|
const float resetSize = (contentWidth >= kSliderResetSize * 3.0f * m_scale)
|
||||||
|
? std::min(kSliderResetSize * m_scale, rowHeight)
|
||||||
|
: 0.0f;
|
||||||
|
const float resetGap = (resetSize > 0.0f) ? kSliderResetGap * m_scale : 0.0f;
|
||||||
|
const float sliderWidth = contentWidth - (resetSize > 0.0f ? (resetSize + resetGap) : 0.0f);
|
||||||
|
const float valueWidth = std::min(kSliderValueWidth * m_scale, sliderWidth * 0.4f);
|
||||||
|
const float trackWidth = std::max(24.0f, sliderWidth - valueWidth - kSliderValueGap * m_scale);
|
||||||
const float trackHeight = kTrackHeight * m_scale;
|
const float trackHeight = kTrackHeight * m_scale;
|
||||||
|
const float valueLeft = contentLeft + trackWidth + kSliderValueGap * m_scale;
|
||||||
|
const float resetLeft = contentLeft + contentWidth - resetSize;
|
||||||
|
|
||||||
m_widthTrackRect =
|
m_widthTrackRect =
|
||||||
gd::RectF(contentLeft, widthRow.Y + (rowHeight - trackHeight) * 0.5f, trackWidth, trackHeight);
|
gd::RectF(contentLeft, widthRow.Y + (rowHeight - trackHeight) * 0.5f, trackWidth, trackHeight);
|
||||||
m_widthValueRect = gd::RectF(contentLeft + trackWidth + kSliderValueGap * m_scale, widthRow.Y,
|
m_widthValueRect = gd::RectF(valueLeft, widthRow.Y, valueWidth, rowHeight);
|
||||||
valueWidth, rowHeight);
|
m_widthResetRect =
|
||||||
|
(resetSize > 0.0f)
|
||||||
|
? gd::RectF(resetLeft, widthRow.Y + (rowHeight - resetSize) * 0.5f, resetSize, resetSize)
|
||||||
|
: gd::RectF();
|
||||||
|
|
||||||
m_opacityTrackRect = gd::RectF(contentLeft, opacityRow.Y + (rowHeight - trackHeight) * 0.5f,
|
m_opacityTrackRect = gd::RectF(contentLeft, opacityRow.Y + (rowHeight - trackHeight) * 0.5f,
|
||||||
trackWidth, trackHeight);
|
trackWidth, trackHeight);
|
||||||
m_opacityValueRect = gd::RectF(contentLeft + trackWidth + kSliderValueGap * m_scale, opacityRow.Y,
|
m_opacityValueRect = gd::RectF(valueLeft, opacityRow.Y, valueWidth, rowHeight);
|
||||||
valueWidth, rowHeight);
|
m_opacityResetRect =
|
||||||
|
(resetSize > 0.0f)
|
||||||
|
? gd::RectF(resetLeft, opacityRow.Y + (rowHeight - resetSize) * 0.5f, resetSize, resetSize)
|
||||||
|
: gd::RectF();
|
||||||
|
|
||||||
// 「目标」行的子几何依赖目标信号的名称与可见性(可能在未重排时变化),
|
// 「目标」行的子几何依赖目标信号的名称与可见性(可能在未重排时变化),
|
||||||
// 因此每次重排后让缓存失效,下一次绘制 / 命中测试时按需重算。
|
// 因此每次重排后让缓存失效,下一次绘制 / 命中测试时按需重算。
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ namespace jdi
|
|||||||
if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy) ||
|
if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy) ||
|
||||||
UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy) ||
|
UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy) ||
|
||||||
UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy) || UiPointInRect(m_widthValueRect, fx, fy) ||
|
UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy) || UiPointInRect(m_widthValueRect, fx, fy) ||
|
||||||
UiPointInRect(m_opacityValueRect, fx, fy))
|
UiPointInRect(m_opacityValueRect, fx, fy) ||
|
||||||
|
(m_widthResetRect.Width > 0.0f && UiPointInRect(m_widthResetRect, fx, fy)) ||
|
||||||
|
(m_opacityResetRect.Width > 0.0f && UiPointInRect(m_opacityResetRect, fx, fy)))
|
||||||
{
|
{
|
||||||
return LoadCursorW(nullptr, IDC_HAND);
|
return LoadCursorW(nullptr, IDC_HAND);
|
||||||
}
|
}
|
||||||
@@ -652,4 +654,51 @@ namespace jdi
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SignalPanel::ResetStyleLineWidth()
|
||||||
|
{
|
||||||
|
if (m_dataSet == nullptr || m_dataSetWritable == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SignalStyle* style = StyleOf(SelectedRef());
|
||||||
|
if (style == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const SignalData* signal = m_dataSet->Resolve(SelectedRef());
|
||||||
|
if (signal == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 默认线与新建信号同源(`DefaultSignalLineWidth`):单数据点信号用最大线宽,
|
||||||
|
// 其余用 `kDefaultSignalLineWidth`(用户明确要求"单值信号和普通信号的默认线宽不一样")。
|
||||||
|
const float target = DefaultSignalLineWidth(signal->finiteCount);
|
||||||
|
if (std::fabs(style->lineWidth - target) < 0.001f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style->lineWidth = target;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SignalPanel::ResetStyleOpacity()
|
||||||
|
{
|
||||||
|
if (m_dataSetWritable == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SignalStyle* style = StyleOf(SelectedRef());
|
||||||
|
if (style == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (std::fabs(style->opacity - 1.0f) < 0.001f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
style->opacity = 1.0f;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace jdi
|
} // namespace jdi
|
||||||
|
|
||||||
@@ -195,6 +195,17 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
hoverStyle = StyleHit::WIDTH_VALUE;
|
hoverStyle = StyleHit::WIDTH_VALUE;
|
||||||
}
|
}
|
||||||
|
// 两行右侧的复位按钮(在数值框右边,命中区不重叠,但顺序放在数值之后更安全)。
|
||||||
|
if (hoverStyle == StyleHit::NONE && m_widthResetRect.Width > 0.0f &&
|
||||||
|
UiPointInRect(m_widthResetRect, fx, fy))
|
||||||
|
{
|
||||||
|
hoverStyle = StyleHit::WIDTH_RESET;
|
||||||
|
}
|
||||||
|
if (hoverStyle == StyleHit::NONE && m_opacityResetRect.Width > 0.0f &&
|
||||||
|
UiPointInRect(m_opacityResetRect, fx, fy))
|
||||||
|
{
|
||||||
|
hoverStyle = StyleHit::OPACITY_RESET;
|
||||||
|
}
|
||||||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_adjustRects[kAdjustGain], fx, fy))
|
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_adjustRects[kAdjustGain], fx, fy))
|
||||||
{
|
{
|
||||||
hoverStyle = StyleHit::GAIN_ADJUST;
|
hoverStyle = StyleHit::GAIN_ADJUST;
|
||||||
@@ -424,6 +435,20 @@ namespace jdi
|
|||||||
ScrollToSignalAnimated(SelectedRef());
|
ScrollToSignalAnimated(SelectedRef());
|
||||||
return BeginEdit(StyleEditField::OPACITY);
|
return BeginEdit(StyleEditField::OPACITY);
|
||||||
}
|
}
|
||||||
|
// 两行右侧的**复位按钮**:把该行参数恢复为默认值(线宽按信号类型取默认、
|
||||||
|
// 不透明度回 100%)。与「系数/偏移」还原图标同一交互语义:就地回显、不弹任何东西。
|
||||||
|
if (m_widthResetRect.Width > 0.0f && UiPointInRect(m_widthResetRect, fx, fy))
|
||||||
|
{
|
||||||
|
ResetStyleLineWidth();
|
||||||
|
ScrollToSignalAnimated(SelectedRef());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (m_opacityResetRect.Width > 0.0f && UiPointInRect(m_opacityResetRect, fx, fy))
|
||||||
|
{
|
||||||
|
ResetStyleOpacity();
|
||||||
|
ScrollToSignalAnimated(SelectedRef());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// 「调整」行:系数与镜像(按下只登记,**抬起**时才展开各自的二级面板,
|
// 「调整」行:系数与镜像(按下只登记,**抬起**时才展开各自的二级面板,
|
||||||
// 与自定义颜色入口同一套交互)、绝对值(单击即切换)。
|
// 与自定义颜色入口同一套交互)、绝对值(单击即切换)。
|
||||||
if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy))
|
if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy))
|
||||||
@@ -821,6 +846,12 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// 线宽 / 透明度两行右侧的复位按钮是"单次点击即生效"的按钮:双击不得被解释成别的语义。
|
||||||
|
if ((m_widthResetRect.Width > 0.0f && UiPointInRect(m_widthResetRect, fx, fy)) ||
|
||||||
|
(m_opacityResetRect.Width > 0.0f && UiPointInRect(m_opacityResetRect, fx, fy)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
UpdateTargetRowGeometry();
|
UpdateTargetRowGeometry();
|
||||||
if (m_targetNameRect.Width > 0.0f && UiPointInRect(m_targetNameRect, fx, fy))
|
if (m_targetNameRect.Width > 0.0f && UiPointInRect(m_targetNameRect, fx, fy))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -768,6 +768,9 @@ namespace jdi
|
|||||||
m_theme->textMuted, TextAlign::CENTER);
|
m_theme->textMuted, TextAlign::CENTER);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 目标信号本身:线宽复位按钮需要它的有效采样点数(单点信号默认用最大线宽)。
|
||||||
|
const SignalData* signal =
|
||||||
|
(m_dataSet != nullptr) ? m_dataSet->Resolve(SelectedRef()) : nullptr;
|
||||||
|
|
||||||
const float labelWidth = kStyleLabelWidth * m_scale;
|
const float labelWidth = kStyleLabelWidth * m_scale;
|
||||||
|
|
||||||
@@ -779,6 +782,27 @@ namespace jdi
|
|||||||
m_theme->textMuted, TextAlign::LEFT);
|
m_theme->textMuted, TextAlign::LEFT);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 「线宽 / 透明度」两行右侧的**复位按钮**(与「系数/偏移」二级菜单标题栏的还原图标同款)。
|
||||||
|
// `changed` = 当前值是否已偏离默认值:偏离时才用强调色(一眼看出"这里可以点"),
|
||||||
|
// 已是默认值时用弱色(仍可点,但结果不变)。
|
||||||
|
auto paintStyleReset = [&](const gd::RectF& rect, StyleHit hit, bool changed)
|
||||||
|
{
|
||||||
|
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bool hot = (m_hoverStyle == hit);
|
||||||
|
if (hot)
|
||||||
|
{
|
||||||
|
UiFillRoundRect(g, gd::RectF(rect.X - 1.0f * m_scale, rect.Y - 1.0f * m_scale,
|
||||||
|
rect.Width + 2.0f * m_scale, rect.Height + 2.0f * m_scale),
|
||||||
|
3.0f * m_scale, WithAlpha(m_theme->accent, 0x33));
|
||||||
|
}
|
||||||
|
UiPaintResetIcon(g, rect, hot ? m_theme->accent
|
||||||
|
: (changed ? m_theme->text : m_theme->textMuted),
|
||||||
|
m_scale);
|
||||||
|
};
|
||||||
|
|
||||||
// ---- 第一行:颜色(预设色块 + 自定义颜色入口)----
|
// ---- 第一行:颜色(预设色块 + 自定义颜色入口)----
|
||||||
{
|
{
|
||||||
paintRowLabel(0, L"颜色");
|
paintRowLabel(0, L"颜色");
|
||||||
@@ -888,7 +912,7 @@ namespace jdi
|
|||||||
paintToggle(kAdjustAbsolute, L"绝对值", style->absolute, StyleHit::ABSOLUTE_TOGGLE);
|
paintToggle(kAdjustAbsolute, L"绝对值", style->absolute, StyleHit::ABSOLUTE_TOGGLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 第四行:线宽(数值可点击输入)----
|
// ---- 第四行:线宽(数值可点击输入;右侧为复位按钮)----
|
||||||
{
|
{
|
||||||
paintRowLabel(3, L"线宽");
|
paintRowLabel(3, L"线宽");
|
||||||
|
|
||||||
@@ -913,6 +937,9 @@ namespace jdi
|
|||||||
: fullText;
|
: fullText;
|
||||||
UiText(g, shown, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong, TextAlign::RIGHT);
|
UiText(g, shown, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong, TextAlign::RIGHT);
|
||||||
}
|
}
|
||||||
|
paintStyleReset(m_widthResetRect, StyleHit::WIDTH_RESET,
|
||||||
|
style->lineWidth !=
|
||||||
|
DefaultSignalLineWidth(signal != nullptr ? signal->finiteCount : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 第五行:透明度(数值可点击输入)----
|
// ---- 第五行:透明度(数值可点击输入)----
|
||||||
@@ -933,6 +960,8 @@ namespace jdi
|
|||||||
UiText(g, std::to_wstring(percent) + L"%", m_fonts->monoSmall, m_opacityValueRect,
|
UiText(g, std::to_wstring(percent) + L"%", m_fonts->monoSmall, m_opacityValueRect,
|
||||||
m_theme->textStrong, TextAlign::RIGHT);
|
m_theme->textStrong, TextAlign::RIGHT);
|
||||||
}
|
}
|
||||||
|
paintStyleReset(m_opacityResetRect, StyleHit::OPACITY_RESET,
|
||||||
|
std::fabs(style->opacity - 1.0f) > 0.001f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8611,3 +8611,156 @@ PlotView:0 = 网格/图例/Y轴标尺/GX/时间轴/光标读数与取值胶囊
|
|||||||
并把面板位置改到胶囊上方(`lane.freeMarkerPanel.x/y`)。
|
并把面板位置改到胶囊上方(`lane.freeMarkerPanel.x/y`)。
|
||||||
* `lbpanel` 的 "layer 1 只能是面板内容" 断言会随层号约定变化而失效:现在滚动条/回到最上方
|
* `lbpanel` 的 "layer 1 只能是面板内容" 断言会随层号约定变化而失效:现在滚动条/回到最上方
|
||||||
在 `base + 2`,因此各组件必须按 `kLayersPerComponent = 3` 取层。
|
在 `base + 2`,因此各组件必须按 `kLayersPerComponent = 3` 取层。
|
||||||
|
|
||||||
|
## 2026-10-30 第十六轮(本机):主题自适应配色 + 预览曲线连续配色 + 样式区复位按钮 + 均值滤波面板重做 + 面板缓存环带残留 + 调试层序 + 自由标记选取与性能
|
||||||
|
|
||||||
|
> 用户本轮七件事:① 四个主题里除 Solarised Light 外,网格叠底 / 时间轴大格与五等分线 /
|
||||||
|
> 派生预览曲线在深色主题下看不清 ⇒ 结构线要按**背景色**自适应,预览曲线要随信号色**线性**找到
|
||||||
|
> 高对比位置(旧实现在两个极端色之间"黑白相"跳变);② 信号样式区「线宽 / 透明度」两行右侧
|
||||||
|
> 加**复位按钮**(与"系数/偏移"菜单的还原图标同款,右边界对齐内容列,单点信号与普通信号默认线宽不同);
|
||||||
|
> ③ 「均值滤波」面板改成「信号色叠底标题栏 + 复选框 + 展开三角 + 两行滑块 + 复位 + 滚动」;
|
||||||
|
> ④ 中键移视界时浮动面板周围出现一圈旧波形像素;⑤ 显示布局边界时下层浮动面板的边框画到上层;
|
||||||
|
> ⑥ 网格里同时有单点信号与常规信号时,光标被单点"吸住";⑦ 上百信号合并到一个网格时自由标记很卡。
|
||||||
|
|
||||||
|
### 0. 仓库约定变动
|
||||||
|
|
||||||
|
* `.preflight/`(本机离屏探针)已按要求**重建**:`perf/build.ps1` + `perf/probe/*.cpp`。
|
||||||
|
⚠️ `build.ps1` 链接的是 `Build/Release/*.o`;**LTO=1 构建后必须加 `-Lto`**(补
|
||||||
|
`-flto -flto-partition=one`),否则链接失败。
|
||||||
|
* 新增三个补丁脚本(纯 ASCII,配合 UTF-8 数据文件):`patch_insert.ps1`(在匹配行之后插入)、
|
||||||
|
`patch_range.ps1`(替换行区间)、`patch_line.ps1`(替换单行)。**用途见 §9**。
|
||||||
|
|
||||||
|
### 1. 主题自适应:结构线按底色自动校正(`Theme.cpp`)
|
||||||
|
|
||||||
|
新增三个**唯一实现**的调色工具(`Theme.hpp`):
|
||||||
|
|
||||||
|
| 函数 | 语义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `SrgbLuminance(argb)` | WCAG 相对亮度(sRGB 线性化 + 0.2126/0.7152/0.0722) |
|
||||||
|
| `ContrastRatio(a, b)` | `(L1+0.05)/(L2+0.05)`,1…21 |
|
||||||
|
| `EnsureContrast(bg, fg, minRatio)` | 不足阈值时沿"远离底色"方向**二分解出最小混色量**;**已达标逐位返回原色** |
|
||||||
|
| `MixToLuminance(color, target)` | 把颜色沿亮度轴平滑推到目标相对亮度(保持色相) |
|
||||||
|
| `ThemeLaneBackground(theme)` | 网格叠底 = `EnsureContrast(plotBg, cardBg, 1.03)` |
|
||||||
|
|
||||||
|
四个内置主题都过一遍 `NormalizePlotContrast()`(阈值取"Solarised Light 现有值刚好达标"的水位):
|
||||||
|
|
||||||
|
| 字段 | 阈值 | Solarised Light 现状 |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| `gridWeak`(网格主刻线 + 五等分线基色) | 1.30 | 1.38 |
|
||||||
|
| `gridStrong`(零线/加重线) | 1.65 | 1.80 |
|
||||||
|
| `axisLine`(时间轴 / 分界线 / 绘图区外框) | 2.10 | 2.43 |
|
||||||
|
| `borderStrong`(网格之间分界线) | 1.70 | 2.43 |
|
||||||
|
|
||||||
|
* **Solarised Light 四个值逐位不变**(探针钉住),默认观感零变化;深色主题自动提亮。
|
||||||
|
* 「网格叠底」改用 `ThemeLaneBackground()`(4 处:整幅重绘 / 宽基底图层 / 样式补丁 / 五等分线混色基底)。
|
||||||
|
Solarised Dark 的 `cardBg` 与 `plotBg` 本来是同一个 base03 ⇒ 现在被拉开 **1.03**(亮度变化 ≈ 3%),
|
||||||
|
网格这一层才看得出来。**Solarised Light / Light 也会被同样拉开 1.03**(原本完全同色)——
|
||||||
|
这是刻意的统一规则,观感变化极小。
|
||||||
|
|
||||||
|
### 2. 派生预览曲线:连续、双向可比
|
||||||
|
|
||||||
|
`HighContrastDerived(color, background)`(`PlotViewInternal.hpp`)**重写**:
|
||||||
|
|
||||||
|
1. 方向 = 信号相对底色的那一侧继续远离(几乎同亮度时按底色明暗选一侧);
|
||||||
|
2. 候选一 = `EnsureContrast(bg, color, 3.2)`(与底色对比度达标),候选二 = 信号亮度 ± 0.18,
|
||||||
|
取**更远离底色**者;3. `MixToLuminance` 平滑推到该亮度。
|
||||||
|
|
||||||
|
两个候选都是输入的连续函数(max/min)⇒ **拖拾色器 / 换预设色 / 切主题时颜色连续变化**,
|
||||||
|
不再出现旧实现"混 72% 黑还是白、按评分二选一"的**阈值跳变**(探针实测最大亮度跳变 0.012)。
|
||||||
|
|
||||||
|
⚠️ 调用点必须传**网格底色**(`ThemeLaneBackground(*m_theme)`)——旧实现的默认背景写死成
|
||||||
|
Solarised Light 的 `0xFFFDF6E3`,这是"换深色主题后预览曲线永远是更深的那一侧、几乎看不见"的根因。
|
||||||
|
|
||||||
|
### 3. 信号样式区「线宽 / 透明度」复位按钮
|
||||||
|
|
||||||
|
* `SignalStyle` 默认线的**唯一来源**:`DefaultSignalLineWidth(finiteCount)`
|
||||||
|
(普通 = `kDefaultSignalLineWidth` 1.0;**单数据点 = `kSinglePointSignalLineWidth` 10.0**)。
|
||||||
|
`BuildSignalGroup` 的初始化与复位按钮共用它。
|
||||||
|
* 布局:内容列右端先预留 `kSliderResetSize`(16) + `kSliderResetGap`(4),数值框与轨道依次收缩 ⇒
|
||||||
|
**按钮右边界 == 内容列右缘**(探针断言误差 < 0.6px)。内容列太窄时整列不显示按钮。
|
||||||
|
* 图标:`UiPaintResetIcon()`(`PaintUtil`)成为**唯一实现**,
|
||||||
|
`pmdetail::PaintResetIcon` / `pvdetail::PaintPanelResetIcon` 都改为转调它。
|
||||||
|
* 命中:`StyleHit::WIDTH_RESET / OPACITY_RESET`(悬停高亮 + 手型 + 双击不解释为其它语义)。
|
||||||
|
|
||||||
|
### 4. 「均值滤波」面板重做(与「自由标记」同一套浏览方式)
|
||||||
|
|
||||||
|
* 条目 = **信号色叠底标题栏**(左侧展开三角 + 名称 + 右端**复选框**)。勾选前只有标题栏;
|
||||||
|
勾选后出现三角与两行滑块,三角可收起参数行。复选框自绘(对比色描边/填充),
|
||||||
|
主题色在信号色叠底上不可辨。
|
||||||
|
* 两行滑块:`均值点数`(0…上限)与 `迭代次数`(1…上限),**标签不带 X / N**;
|
||||||
|
右侧数值可点击输入**滑块上限**(沿用"输入上限(当前N)"提示与 `EditFilter::BYTE`),
|
||||||
|
最右侧是复位按钮(点数 → 5 且上限 → 50;迭代 → 1 且上限 → 10)。
|
||||||
|
新增 `Lane::MeanFilterItem::{maxPoints, maxIterations, collapsed}`、`Lane::meanFilterScroll`。
|
||||||
|
* 内容区超高时**滚动**(`layout.content` + 滚动条 + 滚轮),底部「构建为新信号」按钮固定。
|
||||||
|
面板高度上限 = 绘图区可用高度 ⇒ 上百信号合并时不再顶出屏幕。
|
||||||
|
* 命中要求"行**完全落在内容区**里",避免滚动出的行抢底部按钮的点击。
|
||||||
|
* 缓存键:`PanelSlotKey` 增补 `rowHeaders / rowTriangles / scrollOffset / MarkerGroup::check /
|
||||||
|
meanItem->maxPoints / maxIterations / collapsed`;`meanFilterScroll` **只进槽键**(不进全局键,
|
||||||
|
否则滚动一个面板会让所有面板重画)。
|
||||||
|
|
||||||
|
### 5. 浮动面板周围那一圈旧波形(中键移视界)
|
||||||
|
|
||||||
|
**根因**:面板像素缓存按 `panel ± 4px` 取块,块的边缘是**上一帧的场景**;
|
||||||
|
`PanelGlobalKey` 刻意不含视界/幅值(否则平移会重画全部面板)⇒ 整块贴回时把旧波形"复印"了一圈。
|
||||||
|
|
||||||
|
**修法**:命中缓存时
|
||||||
|
1. 先按本帧重画**投影**(它在圆角形状之外,不能靠缓存);
|
||||||
|
2. 用 `CreateRoundRectRgn` + `ExtSelectClipRgn(RGN_AND)` 裁成**面板圆角形状**,
|
||||||
|
只 BitBlt **面板本体**那一块 ⇒ 圆角之外仍是本帧场景。
|
||||||
|
|
||||||
|
探针 `ringcheck`:同一视界下「走面板缓存」与「强制整帧重绘」在面板外扩 10px 环带上
|
||||||
|
**逐像素 0 差异**(23850 像素),并且断言这一帧**确实命中了面板缓存**(否则判据空洞)。
|
||||||
|
|
||||||
|
### 6. 显示布局边界:每个浮动面板独占一层
|
||||||
|
|
||||||
|
**根因**:所有面板都被收集在同一层 `baseLayer + 1`,同层内只能按 `depth` 排序 ⇒
|
||||||
|
下层面板的行/文本(depth 4)画在**上层**面板本体(depth 2/3)之上。
|
||||||
|
|
||||||
|
**修法**:按 `m_panelOrder`(底 → 顶)**逐面板 +1 层**,并让绘制侧的
|
||||||
|
`UiSetTextLayer` 在 `PaintEdgeCursor` 里**同一个循环内同步推进**(`panelTextLayer + 序号`),
|
||||||
|
函数返回前把层号推到最上面板之上(滚动条 / 回到最上方 / 输入框用它的下一层)。
|
||||||
|
`BuildBoxes` 与绘制**同源**:都以 `m_panelOrder` 的顺序 + 同一公式算层号。
|
||||||
|
探针断言:两个完全重叠的面板,其遮挡矩形的层号严格递增,且上层有自己的 box 集合。
|
||||||
|
|
||||||
|
### 7. 自由标记:选取规则与性能
|
||||||
|
|
||||||
|
* **选取距离改为二维**:采样候选 = 鼠标到"该采样点屏幕位置"的**欧氏距离**(旧实现只看纵向 ⇒
|
||||||
|
单数据点只要高度接近就在**整幅宽度**内胜出,无法标注其它信号);曲线候选仍是到折线的垂距
|
||||||
|
(阶跃竖直段靠它命中)。
|
||||||
|
* **纵向光标画在鼠标位置**(`probe.x = 鼠标 x`),数据点圆点/吸附环落在**真实采样点**
|
||||||
|
(新增 `FreeMarkerProbe::pointX = TimeToX(time)`)。稀疏信号下两者分开是刻意的。
|
||||||
|
* **稀疏判定只在胜出信号上算一次**(`MinorCellSparse` 每信号 50 格 × 2 次二分,
|
||||||
|
上百信号时是纯浪费 —— 它只决定要不要画吸附环)。
|
||||||
|
* **折线缓存两处根治**(上百信号合并网格时的卡顿主因):
|
||||||
|
① 容量从固定 64 改为 `max(64, 全部信号数 × 2)`(`BuildLaneVisuals` 刷新 `m_polylineCacheCapacity`),
|
||||||
|
否则装满就整表清空 ⇒ 每帧重建全部折线;
|
||||||
|
② 查找改为比**一个 64 位键哈希**(命中后再校验 ref/采样数/视界),不再逐条比较十几个字段。
|
||||||
|
* 实测:MF4-Record 的 **285 个信号合并到一个网格**,`ProbeFreeMarker` 平均 **0.11ms**(探针上限 8ms)。
|
||||||
|
|
||||||
|
### 8. 验证
|
||||||
|
|
||||||
|
```
|
||||||
|
panelcheck : 71/71 (主题对比度 24 条、样式区复位 7 条、均值面板 12 条、调试层序 4 条…)
|
||||||
|
markercheck: 13/13 (单点/常规共存的选取 6 条 + 285 信号探测耗时)
|
||||||
|
ringcheck : 6/6 (面板缓存环带 0 差异 + 缓存确实命中)
|
||||||
|
合计 90 条断言全绿(LTO 目标文件上复跑同样全绿)
|
||||||
|
make release -> 零 error 零 warning;JustDataInspector.exe 815,104 B(+23,552 B ≈ +2.9%)
|
||||||
|
make debug -> 零 error 零 warning
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. 踩坑(新增)
|
||||||
|
|
||||||
|
* ⚠️⚠️ **源文件里的中文不能用编辑工具"凭记忆"当匹配串**:本仓库若干注释里存在
|
||||||
|
"重复滤波次数 ,至少"这类**多一个空格**的文本,肉眼与 `read_file` 都不易察觉,
|
||||||
|
匹配必然失败。可靠做法:先按字节导出该行码点确认,或改用
|
||||||
|
`.preflight/patch_{insert,range,line}.ps1`(纯 ASCII + UTF-8 数据文件 + **按行号**改动)。
|
||||||
|
* ⚠️ 用 .NET `WriteAllText` 重写文件会把 **CRLF 全变成 LF** ⇒ `git` 提示
|
||||||
|
"LF will be replaced by CRLF",整文件变成 4000 行 diff。改完必须把行尾恢复成 CRLF。
|
||||||
|
* ⚠️ PowerShell 的 `$Matches` 是正则匹配自动变量(哈希表),在脚本里给
|
||||||
|
`$Matches += $i` 会报"只能将哈希表添加到另一个哈希表中" ⇒ 换名 `$hits`。
|
||||||
|
* ⚠️ `Start-Process -ArgumentList` 拼接的参数**不会自动加引号**:本工程路径含空格,
|
||||||
|
`-I<路径>` 与 `.o` 路径都必须手工包 `"`。
|
||||||
|
* `PlotView::EdgePanelLayout` / `FreeMarkerProbe` / `LaneVisual` 是**嵌套类型**,探针里必须写
|
||||||
|
`PlotView::` 前缀。
|
||||||
|
* 探针的工程根目录推导:探针若放在 `.preflight\xxx.exe`,从 exe 路径**上溯 2 级**即根目录
|
||||||
|
(上溯 3 级会跑到桌面上,`TestData` 找不到)。
|
||||||
@@ -48,6 +48,24 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
constexpr float kSinglePointSignalLineWidth = kSignalLineWidthMax;
|
constexpr float kSinglePointSignalLineWidth = kSignalLineWidthMax;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 普通信号的**默认线宽**(逻辑像素):新建信号时写入,也是样式区「复位」按钮要恢复的值。
|
||||||
|
*
|
||||||
|
* ⚠️ 只有这一个来源:`BuildSignalGroup` 的初始化与信号面板线宽复位都用它,
|
||||||
|
* 否则改了一处就会与"新建信号"的观感不一致。
|
||||||
|
*/
|
||||||
|
constexpr float kDefaultSignalLineWidth = 1.0f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按有效采样点数取该信号**默认的线宽**(复位按钮与新建信号共用同一判定)。
|
||||||
|
* @param [IN] finiteCount 有效采样点数。
|
||||||
|
* @return 默认线宽(单数据点信号 = 最大线宽,其余 = `kDefaultSignalLineWidth`)。
|
||||||
|
*/
|
||||||
|
inline float DefaultSignalLineWidth(std::size_t finiteCount)
|
||||||
|
{
|
||||||
|
return (finiteCount == 1) ? kSinglePointSignalLineWidth : kDefaultSignalLineWidth;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 「稀疏信号」缓存有效采样点时刻/取值的上限(有效点不超过该值才缓存)。
|
* @brief 「稀疏信号」缓存有效采样点时刻/取值的上限(有效点不超过该值才缓存)。
|
||||||
*
|
*
|
||||||
@@ -752,6 +770,21 @@ namespace jdi
|
|||||||
SignalRef ref; /**< 目标信号。 */
|
SignalRef ref; /**< 目标信号。 */
|
||||||
int points = 5; /**< 均值点数 X(滑平半径,窗口 = 2X+1)。 */
|
int points = 5; /**< 均值点数 X(滑平半径,窗口 = 2X+1)。 */
|
||||||
int iterations = 1; /**< 迭代次数 N(重复滤波次数,至少 1)。 */
|
int iterations = 1; /**< 迭代次数 N(重复滤波次数,至少 1)。 */
|
||||||
|
/**
|
||||||
|
* @brief 「均值点数」滑块的**上限**(用户可点击滑块右侧数值改成其它正整数)。
|
||||||
|
*
|
||||||
|
* 滑块量程 = `1 … maxPoints`,`points` 恒被夹在该区间内。
|
||||||
|
*/
|
||||||
|
int maxPoints = 50;
|
||||||
|
/** @brief 「迭代次数」滑块的**上限**(同上,量程 = `1 … maxIterations`)。 */
|
||||||
|
int maxIterations = 10;
|
||||||
|
/**
|
||||||
|
* @brief 参数行是否被**收起**(只保留信号色叠底的标题栏)。
|
||||||
|
*
|
||||||
|
* 仅「均值滤波」面板使用:勾选后标题栏左侧出现展开三角,单击它切换收起/展开。
|
||||||
|
* 未勾选时没有参数行,本字段无意义。
|
||||||
|
*/
|
||||||
|
bool collapsed = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 中心轴边沿计数的平滑参数:与均值滤波共用同一结构与同一套语义。 */
|
/** 中心轴边沿计数的平滑参数:与均值滤波共用同一结构与同一套语义。 */
|
||||||
@@ -1025,6 +1058,15 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
float freeMarkerScroll = 0.0f;
|
float freeMarkerScroll = 0.0f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 「均值滤波」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||||||
|
*
|
||||||
|
* 面板内容(每个信号的"信号色叠底标题栏 + 参数滑块行")超出可视高度时右侧出现滚动条;
|
||||||
|
* 底部的「构建为新信号」按钮**不参与**滚动(固定在面板底部)。
|
||||||
|
* 上百个信号合并到同一网格时它是必备的(否则面板会被顶出绘图区)。
|
||||||
|
*/
|
||||||
|
float meanFilterScroll = 0.0f;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 「自由标记」面板里被**收起**的信号分组(点击分组栏切换)。
|
* @brief 「自由标记」面板里被**收起**的信号分组(点击分组栏切换)。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ namespace jdi
|
|||||||
bool freeMarkerShowDelta = false;
|
bool freeMarkerShowDelta = false;
|
||||||
bool freeMarkerShowEnum = false;
|
bool freeMarkerShowEnum = false;
|
||||||
std::vector<SignalRef> freeMarkerCollapsedGroups;
|
std::vector<SignalRef> freeMarkerCollapsedGroups;
|
||||||
|
/** @brief 「均值滤波」面板内容的滚动偏移(详见 `Lane::meanFilterScroll`)。 */
|
||||||
|
float meanFilterScroll = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 先记下每个信号原来所在网格的网格自身状态:
|
// 先记下每个信号原来所在网格的网格自身状态:
|
||||||
@@ -174,6 +176,7 @@ namespace jdi
|
|||||||
carry.meanFilter = lane.meanFilter;
|
carry.meanFilter = lane.meanFilter;
|
||||||
carry.meanFilterItems = lane.meanFilterItems;
|
carry.meanFilterItems = lane.meanFilterItems;
|
||||||
carry.meanFilterPanel = lane.meanFilterPanel;
|
carry.meanFilterPanel = lane.meanFilterPanel;
|
||||||
|
carry.meanFilterScroll = lane.meanFilterScroll;
|
||||||
carry.weakPeak = lane.weakPeak;
|
carry.weakPeak = lane.weakPeak;
|
||||||
carry.weakPeakItems = lane.weakPeakItems;
|
carry.weakPeakItems = lane.weakPeakItems;
|
||||||
carry.weakPeakPanel = lane.weakPeakPanel;
|
carry.weakPeakPanel = lane.weakPeakPanel;
|
||||||
@@ -255,6 +258,7 @@ namespace jdi
|
|||||||
lane.freeMarkerShowDelta = carry.freeMarkerShowDelta;
|
lane.freeMarkerShowDelta = carry.freeMarkerShowDelta;
|
||||||
lane.freeMarkerShowEnum = carry.freeMarkerShowEnum;
|
lane.freeMarkerShowEnum = carry.freeMarkerShowEnum;
|
||||||
lane.freeMarkerCollapsedGroups = carry.freeMarkerCollapsedGroups;
|
lane.freeMarkerCollapsedGroups = carry.freeMarkerCollapsedGroups;
|
||||||
|
lane.meanFilterScroll = carry.meanFilterScroll;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 收集全部可见信号引用。
|
// 收集全部可见信号引用。
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ namespace jdi
|
|||||||
style.lineWidth = kSinglePointSignalLineWidth;
|
style.lineWidth = kSinglePointSignalLineWidth;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 缓存「有效采样点很少」的信号的**全部有效点**(时刻 + 取值,按时间升序)。
|
* @brief 缓存「有效采样点很少」的信号的**全部有效点**(时刻 + 取值,按时间升序)。
|
||||||
*
|
*
|
||||||
@@ -376,7 +375,7 @@ namespace jdi
|
|||||||
signal.style.customColor = 0xFF859900u;
|
signal.style.customColor = 0xFF859900u;
|
||||||
}
|
}
|
||||||
signal.style.opacity = 1.0f;
|
signal.style.opacity = 1.0f;
|
||||||
signal.style.lineWidth = 1.0f;
|
signal.style.lineWidth = kDefaultSignalLineWidth;
|
||||||
signal.style.mode = DrawMode::LINE;
|
signal.style.mode = DrawMode::LINE;
|
||||||
// 只有一个有效采样点的信号改用「散点」+ 最大线宽,否则根本看不见。
|
// 只有一个有效采样点的信号改用「散点」+ 最大线宽,否则根本看不见。
|
||||||
ApplySinglePointDefaultStyle(stats.finiteCount, signal.style);
|
ApplySinglePointDefaultStyle(stats.finiteCount, signal.style);
|
||||||
|
|||||||
Reference in new issue
Block a user