自由标记体验重做 + 全部实验性面板可折起

- 网格内固定点:取值胶囊按已放置矩形做纵向避让(不再互相遮挡);行尾 ✕ 改为与
  胶囊同高的正方形;移除固定点的纵向半透明竖线
- 新增「标记透明度」(0–100%),作用于本网格全部固定点的圆点/胶囊/描边/文字/✕;
  半透明时文字改走 GDI+(GDI DrawTextW 忽略 alpha),100% 时像素零变化;
  ≤2% 时不绘制且不登记 ✕ 命中矩形(看不见的按钮不可点)
- 面板按信号分组:信号色托底分组栏(展开三角 + 信号名 + N 标记点 + ✕,文字/三角/✕
  一律用自适应对比色),点击分组栏收起/展开;条目五列(值/枚举/增量/时刻/间隔),
  首项增列与 Δt 显示 N/A,整个面板没有枚举文本时枚举列整列缺省;行尾仍有 ✕
- 面板内容超高时右侧出现滚动条(分组栏与条目一起滚动),滚轮在内容区翻页;
  底部按钮与「标记透明度」滑块固定在底部、不参与滚动
- 底部按钮改为「光标选取模式」切换类按钮(外观与工具栏切换按钮一致,激活高亮),
  激活时其上方出现「标记透明度」滑动条(与信号样式区的透明度滑块同款)
- 所有实验性功能浮动面板支持展开/折起:标题栏 GX 徽标左侧新增「三角 + 横线」指示器,
  单击切换、双击标题栏切换;折起的面板只有标题栏
- 数据层:LanePanelState.collapsed、Lane.freeMarkerOpacity/freeMarkerScroll/
  freeMarkerCollapsedGroups,LaneCarry 与 SplitLane 一并继承
- 验证:新增离屏 .preflight/perf/mark2check 69/69(含两组 A/B);回归 datacheck 69/69、
  verify 49/49、panelanim 158/158、errcheck 19/19;make release/debug 零警告
This commit is contained in:
DeepSeek-V4.1-Flash committed 2026-10-21 02:59:05 +08:00
1 parent 067128f46c
commit 3400ac7c29
9 files changed
+1225 -202

No files matched your search

+107 -10
View File
@@ -57,7 +57,9 @@ namespace jdi
EDGE_PANEL_MOVE, /**< 拖动边沿计数浮动面板。 */ EDGE_PANEL_MOVE, /**< 拖动边沿计数浮动面板。 */
PEAK_LEVEL, /**< 拖动「弱峰调整」某信号的幅度滑块(-400% – +400%)。 */ PEAK_LEVEL, /**< 拖动「弱峰调整」某信号的幅度滑块(-400% – +400%)。 */
PEAK_ITERATIONS, /**< 拖动「弱峰调整」某信号的迭代次数滑块(1 … 该信号自定义上限)。 */ PEAK_ITERATIONS, /**< 拖动「弱峰调整」某信号的迭代次数滑块(1 … 该信号自定义上限)。 */
LANE_REORDER /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */ LANE_REORDER, /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */
MARKER_OPACITY, /**< 拖动「自由标记」面板的「标记透明度」滑块。 */
MARKER_SCROLL /**< 拖动「自由标记」面板内容的滚动条滑块。 */
}; };
/** /**
@@ -969,6 +971,46 @@ namespace jdi
*/ */
bool ToggleLaneFreeMarkerCursor(int laneIndex); bool ToggleLaneFreeMarkerCursor(int laneIndex);
/**
* @brief 设置某网格「自由标记」固定点的**显示不透明度**(0–1)。
*
* 只影响网格内固定数据点的显示内容(数据点 / 取值胶囊 / ✕),
* 面板自身的像素与交互不受影响。
*/
bool SetLaneFreeMarkerOpacity(int laneIndex, float opacity);
/**
* @brief 设置某网格「自由标记」面板内容的滚动偏移(客户区像素,0 = 顶部)。
* @return 偏移真的发生变化返回 true。
*/
bool SetLaneFreeMarkerScroll(int laneIndex, float scroll);
/**
* @brief 切换某网格「自由标记」面板里某个信号分组的收起状态。
* @return 切换后为「已收起」返回 true。
*/
bool ToggleLaneFreeMarkerGroup(int laneIndex, const SignalRef& ref);
/**
* @brief 切换某个实验性功能浮动面板的**折起**状态(只显示标题栏)。
* @param [IN] laneIndex 网格下标。
* @param [IN] kind 面板种类。
* @param [IN] collapsed 是否折起。
* @return 状态发生变化返回 true。
*/
bool SetLanePanelCollapsed(int laneIndex, EdgePanelKind kind, bool collapsed);
/**
* @brief 查询某个实验性功能浮动面板是否处于折起状态。
*/
bool LanePanelCollapsed(int laneIndex, EdgePanelKind kind) const;
/**
* @brief 切换某个实验性功能浮动面板的折起状态。
* @return 切换后的状态(true = 已折起)。
*/
bool ToggleLanePanelCollapsed(int laneIndex, EdgePanelKind kind);
/** /**
* @brief 直接把文本解析为网格 Y 轴上限/下限并应用(供菜单内联输入框提交使用)。 * @brief 直接把文本解析为网格 Y 轴上限/下限并应用(供菜单内联输入框提交使用)。
* @param [IN] laneIndex 网格下标。 * @param [IN] laneIndex 网格下标。
@@ -1595,6 +1637,15 @@ namespace jdi
gd::RectF panel; /**< 面板整体矩形。 */ gd::RectF panel; /**< 面板整体矩形。 */
gd::RectF title; /**< 标题栏(**唯一可拖拽区域**)。 */ gd::RectF title; /**< 标题栏(**唯一可拖拽区域**)。 */
gd::RectF close; /**< ✕ 关闭按钮。 */ gd::RectF close; /**< ✕ 关闭按钮。 */
/**
* @brief 标题栏左侧的「展开 / 折起」三角指示器(三角 + 后方一条横线)。
*
* 单击它切换面板的折起状态;折起后面板只剩标题栏(内容区完全省略)。
* 它位于「GX」编号徽标的**左边**。
*/
gd::RectF disclosure;
/** 面板当前是否处于折起状态(只显示标题栏)。 */
bool collapsed = false;
/** /**
* @brief 标题栏左侧的「GX」编号徽标。 * @brief 标题栏左侧的「GX」编号徽标。
* *
@@ -1654,6 +1705,35 @@ namespace jdi
}; };
/** 「自由标记」:每个固定点一行(按信号分组、组内按时间升序展开后的顺序)。 */ /** 「自由标记」:每个固定点一行(按信号分组、组内按时间升序展开后的顺序)。 */
std::vector<MarkerRow> markerRows; std::vector<MarkerRow> markerRows;
/**
* @brief 「自由标记」的一个**信号分组栏**(以信号色为托底底色)。
*
* 左侧 = 展开/收起三角 + 信号名;右侧 = 标记点个数(`N 标记点`)+ ✕
*(清除该信号全部固定点)。单击分组栏(✕ 除外)切换收起状态。
* 只有至少有一个固定点的信号才会有分组栏。
*/
struct MarkerGroup
{
SignalRef ref;
gd::RectF header; /**< 分组栏托底矩形。 */
gd::RectF triangle; /**< 展开/收起三角的命中矩形。 */
gd::RectF close; /**< 右侧✕的命中矩形。 */
bool collapsed = false;
};
/** 「自由标记」:信号分组栏(顺序与面板显示顺序一致)。 */
std::vector<MarkerGroup> markerGroups;
/**
* @brief 「自由标记」面板的**可滚动内容区**(分组栏 + 固定点条目)。
*
* 内容高度超过它时出现上下滚动条(不参与滚动的按钮与透明度滑块在它下方)。
*/
gd::RectF content;
/** 「自由标记」面板的**内容总高度**(含被滚动出去的条目),用于换算滚动比例。 */
float markerContentHeight = 0.0f;
gd::RectF scrollTrack; /**< 内容区右侧的滚动条轨道(不需要滚动时为空矩形)。 */
gd::RectF scrollThumb; /**< 滚动条滑块。 */
gd::RectF opacityTrack; /**< 「标记透明度」滑动条轨道(仅光标模式激活时非空)。 */
gd::RectF opacityValue; /**< 「标记透明度」数值文本矩形。 */
/** /**
* @brief 「自由标记」:每个信号名右侧的「清除该信号全部固定点」✕。 * @brief 「自由标记」:每个信号名右侧的「清除该信号全部固定点」✕。
* *
@@ -1667,25 +1747,29 @@ namespace jdi
*/ */
std::vector<gd::RectF> rowNameRects; std::vector<gd::RectF> rowNameRects;
/** /**
* @brief 「自由标记」面板的四列文本区(① 取值 ② 增量 ③ 时刻 ④ 间隔)。 * @brief 「自由标记」面板的五列文本区
* (① 取值 ② 枚举文本 ③ 增量 ④ 时刻 ⑤ 间隔)。
* *
* 每行都在这四列的**同一横坐标**上左对齐输出,因此同一信息在纵向上严格对齐 * 每行都在这五列的**同一横坐标**上左对齐输出,因此同一信息在纵向上严格对齐。
*(用户 2026-10-20 明确要求:"将每个信息元素纵向对齐")。
* 列宽固定(不按文本测量),绘制时用 `UiEllipsize` 截断,避免布局期查询采样。 * 列宽固定(不按文本测量),绘制时用 `UiEllipsize` 截断,避免布局期查询采样。
* 当整个面板里没有任何枚举文本时,② 枚举列的宽度为 0(整列缺省)。
*/ */
struct MarkerColumn struct MarkerColumn
{ {
float left = 0.0f; float left = 0.0f;
float width = 0.0f; float width = 0.0f;
}; };
std::array<MarkerColumn, 4> markerColumns{}; std::array<MarkerColumn, 5> markerColumns{};
/** 四列的语义下标(`markerColumns` 的索引)。 */ /** 面板里是否存在枚举文本(为 false 时 ② 列整列缺省)。 */
bool markerHasEnumColumn = false;
/** 五列的语义下标(`markerColumns` 的索引)。 */
enum MarkerColumnIndex enum MarkerColumnIndex
{ {
MARKER_COL_VALUE = 0, MARKER_COL_VALUE = 0,
MARKER_COL_DELTA = 1, MARKER_COL_ENUM = 1,
MARKER_COL_TIME = 2, MARKER_COL_DELTA = 2,
MARKER_COL_DT = 3 MARKER_COL_TIME = 3,
MARKER_COL_DT = 4
}; };
}; };
@@ -1776,9 +1860,11 @@ namespace jdi
* @param [IN] color 信号色。 * @param [IN] color 信号色。
* @param [IN] close ✕ 按钮矩形(宽度为 0 表示不画)。 * @param [IN] close ✕ 按钮矩形(宽度为 0 表示不画)。
* @param [IN] closeHot ✕ 是否处于悬停态。 * @param [IN] closeHot ✕ 是否处于悬停态。
* @param [IN] alphaScale 整体不透明度系数(1 = 不透明;固定点受「标记透明度」控制)。
*/ */
void PaintFreeMarkerCapsule(gd::Graphics& g, const gd::RectF& label, const std::wstring& text, void PaintFreeMarkerCapsule(gd::Graphics& g, const gd::RectF& label, const std::wstring& text,
std::uint32_t color, const gd::RectF& close, bool closeHot); std::uint32_t color, const gd::RectF& close, bool closeHot,
float alphaScale = 1.0f);
/** /**
* @brief 计算某个固定点相对**同信号**前一个固定点的增量与其时间间隔。 * @brief 计算某个固定点相对**同信号**前一个固定点的增量与其时间间隔。
@@ -2576,6 +2662,17 @@ namespace jdi
/** 正在编辑的固定点(`m_editLane` 网格内的 `freeMarkerItems` 下标)。 */ /** 正在编辑的固定点(`m_editLane` 网格内的 `freeMarkerItems` 下标)。 */
std::size_t m_editMarkerIndex = static_cast<std::size_t>(-1); std::size_t m_editMarkerIndex = static_cast<std::size_t>(-1);
/** 「标记透明度」滑块拖拽的目标网格(-1 = 未拖拽)。 */
int m_markerOpacityDragLane = -1;
/** 面板内容滚动条拖拽的目标网格(-1 = 未拖拽)。 */
int m_markerScrollDragLane = -1;
/** 按下滑块时鼠标相对滚动条滑块上沿的偏移(避免滑块跳动)。 */
float m_markerScrollGrabOffset = 0.0f;
/** 「自由标记」面板:分组栏三角的悬停态(用于给出可点击提示)。 */
int m_freeMarkerTriangleHoverLane = -1;
SignalRef m_freeMarkerTriangleHoverSignal;
int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */ int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */
int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */ int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */
bool m_spanHover = false; /**< 鼠标是否悬停在双光标差值标签上。 */ bool m_spanHover = false; /**< 鼠标是否悬停在双光标差值标签上。 */
@@ -1693,6 +1693,115 @@ namespace jdi
return enabled; return enabled;
} }
bool PlotView::SetLaneFreeMarkerOpacity(int laneIndex, float opacity)
{
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, std::min(1.0f, opacity));
if (std::fabs(lane.freeMarkerOpacity - clamped) < 0.0005f)
{
return false;
}
lane.freeMarkerOpacity = clamped;
return true;
}
bool PlotView::SetLaneFreeMarkerScroll(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.freeMarkerScroll - clamped) < 0.5f)
{
return false;
}
lane.freeMarkerScroll = clamped;
return true;
}
bool PlotView::ToggleLaneFreeMarkerGroup(int laneIndex, const SignalRef& ref)
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
return lane.ToggleFreeMarkerGroupCollapsed(ref);
}
bool PlotView::SetLanePanelCollapsed(int laneIndex, EdgePanelKind kind, bool collapsed)
{
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)];
LanePanelState* state = nullptr;
switch (kind)
{
case EdgePanelKind::COUNT:
state = &lane.edgePanel;
break;
case EdgePanelKind::CENTER:
state = &lane.edgeCenterPanel;
break;
case EdgePanelKind::MEAN:
state = &lane.meanFilterPanel;
break;
case EdgePanelKind::PEAK:
state = &lane.weakPeakPanel;
break;
case EdgePanelKind::MARK:
state = &lane.freeMarkerPanel;
break;
default:
return false;
}
if (state->collapsed == collapsed)
{
return false;
}
state->collapsed = collapsed;
return true;
}
bool PlotView::LanePanelCollapsed(int laneIndex, EdgePanelKind kind) const
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
switch (kind)
{
case EdgePanelKind::COUNT:
return lane.edgePanel.collapsed;
case EdgePanelKind::CENTER:
return lane.edgeCenterPanel.collapsed;
case EdgePanelKind::MEAN:
return lane.meanFilterPanel.collapsed;
case EdgePanelKind::PEAK:
return lane.weakPeakPanel.collapsed;
case EdgePanelKind::MARK:
return lane.freeMarkerPanel.collapsed;
default:
return false;
}
}
bool PlotView::ToggleLanePanelCollapsed(int laneIndex, EdgePanelKind kind)
{
const bool collapsed = !LanePanelCollapsed(laneIndex, kind);
SetLanePanelCollapsed(laneIndex, kind, collapsed);
return collapsed;
}
bool PlotView::MeanFilterValueAt(const TimeAxis& axis, const SignalData& signal, int points, int iterations, bool PlotView::MeanFilterValueAt(const TimeAxis& axis, const SignalData& signal, int points, int iterations,
double time, double& value) const double time, double& value) const
{ {
+225 -25
View File
@@ -941,6 +941,12 @@ namespace jdi
{ {
return LoadCursorW(nullptr, IDC_HAND); return LoadCursorW(nullptr, IDC_HAND);
} }
// 「展开 / 折起」三角指示器:可点击控件 ⇒ 手型光标。
if (layout.disclosure.Width > 0.0f &&
UiPointInRect(layout.disclosure, static_cast<float>(x), static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_HAND);
}
if (UiPointInRect(layout.title, static_cast<float>(x), static_cast<float>(y))) if (UiPointInRect(layout.title, static_cast<float>(x), static_cast<float>(y)))
{ {
return LoadCursorW(nullptr, IDC_SIZEALL); return LoadCursorW(nullptr, IDC_SIZEALL);
@@ -953,15 +959,27 @@ namespace jdi
// 「构建为新信号」按钮:手型光标作为可点击提示。 // 「构建为新信号」按钮:手型光标作为可点击提示。
return LoadCursorW(nullptr, IDC_HAND); return LoadCursorW(nullptr, IDC_HAND);
} }
// 「自由标记」面板:行尾 ✕ 与信号名右侧 ✕ 都是可点击控件(手型)。 // 「自由标记」面板:行尾 ✕ / 分组栏 ✕ 与三角都是可点击控件(手型),
// 滚动条与「标记透明度」滑块则是拖动类控件(上下 / 左右箭头)。
if (static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MARK) if (static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MARK)
{ {
// 底部「启用光标模式 / 关闭光标模式」切换按钮。 // 底部「光标选取模式」切换按钮。
if (layout.button.Width > 0.0f && if (layout.button.Width > 0.0f &&
UiPointInRect(layout.button, static_cast<float>(x), static_cast<float>(y))) UiPointInRect(layout.button, static_cast<float>(x), static_cast<float>(y)))
{ {
return LoadCursorW(nullptr, IDC_HAND); return LoadCursorW(nullptr, IDC_HAND);
} }
if (layout.opacityTrack.Width > 0.0f &&
WeakPeakSliderHit(layout.opacityTrack, m_scale, static_cast<float>(x),
static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_SIZEWE);
}
if (layout.scrollTrack.Width > 0.0f &&
UiPointInRect(layout.scrollTrack, static_cast<float>(x), static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_SIZENS);
}
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows) for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
{ {
if (row.close.Width > 0.0f && if (row.close.Width > 0.0f &&
@@ -970,13 +988,18 @@ namespace jdi
return LoadCursorW(nullptr, IDC_HAND); return LoadCursorW(nullptr, IDC_HAND);
} }
} }
for (const gd::RectF& rect : layout.rowNameCloses) for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
{ {
if (rect.Width > 0.0f && if (group.close.Width > 0.0f &&
UiPointInRect(rect, static_cast<float>(x), static_cast<float>(y))) UiPointInRect(group.close, static_cast<float>(x), static_cast<float>(y)))
{ {
return LoadCursorW(nullptr, IDC_HAND); return LoadCursorW(nullptr, IDC_HAND);
} }
if (UiPointInRect(group.header, static_cast<float>(x), static_cast<float>(y)))
{
// 分组栏本体(含三角)单击 = 展开 / 收起。
return LoadCursorW(nullptr, IDC_HAND);
}
} }
} }
// 「弱峰调整」的逐信号滑块:轨道 → 左右箭头;迭代次数右侧数值 → 手型(可点击改上限)。 // 「弱峰调整」的逐信号滑块:轨道 → 左右箭头;迭代次数右侧数值 → 手型(可点击改上限)。
@@ -1439,6 +1462,57 @@ namespace jdi
m_dragMoved = true; m_dragMoved = true;
return true; return true;
} }
case PlotDragMode::MARKER_OPACITY:
{
// 「标记透明度」滑块:拖动时把滑块位置换算成 0–1 的不透明度。
if (m_tab == nullptr || m_markerOpacityDragLane < 0 ||
m_markerOpacityDragLane >= static_cast<int>(m_tab->Lanes().size()))
{
break;
}
const EdgePanelLayout layout =
EdgePanelLayoutFor(m_markerOpacityDragLane, EdgePanelKind::MARK);
if (layout.opacityTrack.Width <= 0.0f)
{
break;
}
const double ratio = UiSliderRatioFromX(layout.opacityTrack, static_cast<float>(x));
if (SetLaneFreeMarkerOpacity(m_markerOpacityDragLane, static_cast<float>(ratio)))
{
m_dragMoved = true;
return true;
}
return false;
}
case PlotDragMode::MARKER_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::MARK);
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 (SetLaneFreeMarkerScroll(m_markerScrollDragLane, ratio * maxScroll))
{
m_dragMoved = true;
return true;
}
return false;
}
case PlotDragMode::NONE: case PlotDragMode::NONE:
default: default:
break; break;
@@ -1500,11 +1574,13 @@ namespace jdi
} }
} }
// 面板内 ✕ 的悬停(信号名 ✕ 与固定点行 ✕)。 // 面板内 ✕ 的悬停(分组栏 ✕ 与固定点行 ✕)与分组栏三角的悬停。
{ {
int hoverLane = -1; int hoverLane = -1;
std::size_t hoverItem = static_cast<std::size_t>(-1); std::size_t hoverItem = static_cast<std::size_t>(-1);
SignalRef hoverSignal; SignalRef hoverSignal;
int triLane = -1;
SignalRef triSignal;
int markLane = -1; int markLane = -1;
int markKind = 0; int markKind = 0;
if (m_tab != nullptr && if (m_tab != nullptr &&
@@ -1512,10 +1588,14 @@ namespace jdi
static_cast<EdgePanelKind>(markKind) == EdgePanelKind::MARK) static_cast<EdgePanelKind>(markKind) == EdgePanelKind::MARK)
{ {
const EdgePanelLayout layout = EdgePanelLayoutFor(markLane, EdgePanelKind::MARK); const EdgePanelLayout layout = EdgePanelLayoutFor(markLane, EdgePanelKind::MARK);
// 只认**内容区**内的命中:被滚动到内容区之外的条目(它们的矩形可能落在
// 底部按钮 / 滑块区域)不得抢走那些控件的点击。
const bool insideContent =
UiPointInRect(layout.content, static_cast<float>(x), static_cast<float>(y));
// 固定点行 ✕ 优先(更具体)。 // 固定点行 ✕ 优先(更具体)。
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows) for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
{ {
if (row.close.Width > 0.0f && if (insideContent && row.close.Width > 0.0f &&
UiPointInRect(row.close, static_cast<float>(x), static_cast<float>(y))) UiPointInRect(row.close, static_cast<float>(x), static_cast<float>(y)))
{ {
hoverLane = markLane; hoverLane = markLane;
@@ -1525,18 +1605,22 @@ namespace jdi
} }
if (hoverLane < 0) if (hoverLane < 0)
{ {
for (std::size_t rowIndex = 0; rowIndex < layout.rowNameCloses.size(); ++rowIndex) for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
{ {
const gd::RectF& rect = layout.rowNameCloses[rowIndex]; if (insideContent && group.close.Width > 0.0f &&
if (rect.Width > 0.0f && UiPointInRect(group.close, static_cast<float>(x), static_cast<float>(y)))
UiPointInRect(rect, static_cast<float>(x), static_cast<float>(y)) &&
rowIndex < layout.rowRefs.size())
{ {
hoverLane = markLane; hoverLane = markLane;
hoverItem = static_cast<std::size_t>(-1); hoverItem = static_cast<std::size_t>(-1);
hoverSignal = layout.rowRefs[rowIndex]; hoverSignal = group.ref;
break; break;
} }
if (insideContent &&
UiPointInRect(group.triangle, static_cast<float>(x), static_cast<float>(y)))
{
triLane = markLane;
triSignal = group.ref;
}
} }
} }
} }
@@ -1548,6 +1632,13 @@ namespace jdi
m_freeMarkerPanelHoverSignal = hoverSignal; m_freeMarkerPanelHoverSignal = hoverSignal;
changed = true; changed = true;
} }
if (triLane != m_freeMarkerTriangleHoverLane ||
!(triSignal == m_freeMarkerTriangleHoverSignal))
{
m_freeMarkerTriangleHoverLane = triLane;
m_freeMarkerTriangleHoverSignal = triSignal;
changed = true;
}
} }
// 鼠标压在实验性功能面板上时不再触发信号悬停提示(面板本来就盖住波形), // 鼠标压在实验性功能面板上时不再触发信号悬停提示(面板本来就盖住波形),
@@ -1911,6 +2002,15 @@ namespace jdi
return true; return true;
} }
// 标题栏左侧的「展开 / 折起」三角指示器:单击切换面板的折起状态。
// 折起后面板只剩标题栏(再次点击三角、或双击标题栏可展开)。
if (layout.disclosure.Width > 0.0f && UiPointInRect(layout.disclosure, fx, fy))
{
m_dragMode = PlotDragMode::NONE;
ToggleLanePanelCollapsed(index, kind);
return true;
}
// 「中心轴边沿计数」/「均值滤波」/「弱峰调整」面板:逐信号条目 // 「中心轴边沿计数」/「均值滤波」/「弱峰调整」面板:逐信号条目
// (前两者是「名称 + 复选框 + 两个参数输入框」,弱峰调整是「名称 + 复选框 + 两行滑块」)。 // (前两者是「名称 + 复选框 + 两个参数输入框」,弱峰调整是「名称 + 复选框 + 两行滑块」)。
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER || if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER ||
@@ -2154,37 +2254,80 @@ namespace jdi
} }
} }
// 「自由标记」面板:行尾 ✕ 移除该固定点;信号名右侧 ✕ 清除该信号的全部固定点。 // 「自由标记」面板:底部的切换按钮 / 「标记透明度」滑块 / 内容滚动条,
// 随后是分组栏(单击展开收起、右侧 ✕ 清空该信号)与固定点行 ✕。
// 与其它逐行 ✕ / 复位控件同一语义:**按下即生效**(不进拖拽、不穿透)。 // 与其它逐行 ✕ / 复位控件同一语义:**按下即生效**(不进拖拽、不穿透)。
if (kind == EdgePanelKind::MARK && m_tab != nullptr) if (kind == EdgePanelKind::MARK && m_tab != nullptr)
{ {
Lane& tabLane = m_tab->Lanes()[static_cast<std::size_t>(index)]; Lane& tabLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
// 底部切换按钮「启用光标模式 / 关闭光标模式」(默认关闭)。 // 底部切换按钮「光标选取模式」(默认关闭)。
if (layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy)) if (layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy))
{ {
ToggleLaneFreeMarkerCursor(index); ToggleLaneFreeMarkerCursor(index);
m_dragMode = PlotDragMode::NONE; m_dragMode = PlotDragMode::NONE;
return true; return true;
} }
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows) // 「标记透明度」滑块:按下即写入该处数值并进入拖动。
if (layout.opacityTrack.Width > 0.0f &&
WeakPeakSliderHit(layout.opacityTrack, m_scale, fx, fy))
{ {
if (row.close.Width > 0.0f && UiPointInRect(row.close, fx, fy)) SetLaneFreeMarkerOpacity(index,
static_cast<float>(UiSliderRatioFromX(
layout.opacityTrack, fx)));
m_dragMode = PlotDragMode::MARKER_OPACITY;
m_markerOpacityDragLane = index;
return true;
}
// 内容滚动条:按在滑块上就带上抓取偏移,按在轨道上则直接跳转。
if (layout.scrollTrack.Width > 0.0f &&
UiPointInRect(layout.scrollTrack, fx, fy))
{
const float travel = layout.scrollTrack.Height - layout.scrollThumb.Height;
const float maxScroll =
std::max(0.0f, layout.markerContentHeight - layout.content.Height);
if (travel > 0.0f && maxScroll > 0.0f)
{ {
tabLane.RemoveFreeMarkerAt(row.itemIndex); m_dragMode = PlotDragMode::MARKER_SCROLL;
m_markerScrollDragLane = index;
const bool onThumb = UiPointInRect(layout.scrollThumb, fx, fy);
m_markerScrollGrabOffset =
onThumb ? (fy - layout.scrollThumb.Y)
: (layout.scrollThumb.Height * 0.5f);
const float thumbTop =
std::max(layout.scrollTrack.Y,
std::min(fy - m_markerScrollGrabOffset,
layout.scrollTrack.Y + travel));
SetLaneFreeMarkerScroll(
index, ((thumbTop - layout.scrollTrack.Y) / travel) * maxScroll);
return true;
}
}
// 分组栏:✕ → 清空该信号全部固定点;其它位置(含三角)→ 展开 / 收起。
// ⚠️ 只认内容区内的命中:滚动出去的条目不得抢到底部按钮 / 滑块的点击。
const bool insideContent = UiPointInRect(layout.content, fx, fy);
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
{
if (insideContent && group.close.Width > 0.0f && UiPointInRect(group.close, fx, fy))
{
tabLane.RemoveFreeMarkersOf(group.ref);
m_dragMode = PlotDragMode::NONE; m_dragMode = PlotDragMode::NONE;
m_freeMarkerPanelHoverLane = -1; m_freeMarkerPanelHoverLane = -1;
m_freeMarkerPanelHoverItem = static_cast<std::size_t>(-1); m_freeMarkerPanelHoverItem = static_cast<std::size_t>(-1);
m_freeMarkerPanelHoverSignal = SignalRef(); m_freeMarkerPanelHoverSignal = SignalRef();
return true; return true;
} }
} if (insideContent && UiPointInRect(group.header, fx, fy))
for (std::size_t rowIndex = 0; rowIndex < layout.rowNameCloses.size(); ++rowIndex)
{
const gd::RectF& rect = layout.rowNameCloses[rowIndex];
if (rect.Width > 0.0f && UiPointInRect(rect, fx, fy) &&
rowIndex < layout.rowRefs.size())
{ {
tabLane.RemoveFreeMarkersOf(layout.rowRefs[rowIndex]); ToggleLaneFreeMarkerGroup(index, group.ref);
m_dragMode = PlotDragMode::NONE;
return true;
}
}
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
{
if (insideContent && row.close.Width > 0.0f && UiPointInRect(row.close, fx, fy))
{
tabLane.RemoveFreeMarkerAt(row.itemIndex);
m_dragMode = PlotDragMode::NONE; m_dragMode = PlotDragMode::NONE;
m_freeMarkerPanelHoverLane = -1; m_freeMarkerPanelHoverLane = -1;
m_freeMarkerPanelHoverItem = static_cast<std::size_t>(-1); m_freeMarkerPanelHoverItem = static_cast<std::size_t>(-1);
@@ -2490,6 +2633,10 @@ namespace jdi
// 弱峰滑块拖拽结束:清掉目标(避免留下上一轮的信号引用)。 // 弱峰滑块拖拽结束:清掉目标(避免留下上一轮的信号引用)。
m_peakDragLane = -1; m_peakDragLane = -1;
m_peakDragRef = SignalRef(); m_peakDragRef = SignalRef();
// 「标记透明度」滑块 / 面板内容滚动条拖拽结束。
m_markerOpacityDragLane = -1;
m_markerScrollDragLane = -1;
m_markerScrollGrabOffset = 0.0f;
// 松开左键:粘滞提示结束计时,开始"停留后淡出"。 // 松开左键:粘滞提示结束计时,开始"停留后淡出"。
ReleaseHint(); ReleaseHint();
return changed; return changed;
@@ -2504,6 +2651,37 @@ namespace jdi
const double notches = static_cast<double>(delta) / 120.0; const double notches = static_cast<double>(delta) / 120.0;
// 鼠标位于「自由标记」面板的可滚动内容区上:滚轮翻页该面板的内容(不滚动网格)。
// ⚠️ 只认内容区:底部的切换按钮与「标记透明度」滑块不参与滚动,
// 因此鼠标压在它们上面时仍然滚动网格(与“它们不属于内容”的语义一致)。
if (m_tab != nullptr)
{
int panelLane = -1;
int panelKind = 0;
if (TopPanelAtPoint(static_cast<float>(m_mouseX), static_cast<float>(m_mouseY),
panelLane, panelKind) &&
static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MARK)
{
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;
}
}
}
}
// 滚轮不再承担缩放:默认纵向滚动网格,按住 Shift 时水平平移时间轴。 // 滚轮不再承担缩放:默认纵向滚动网格,按住 Shift 时水平平移时间轴。
if (shift) if (shift)
{ {
@@ -2542,6 +2720,27 @@ namespace jdi
int panelKind = 0; int panelKind = 0;
if (TopPanelAtPoint(static_cast<float>(x), static_cast<float>(y), panelLane, panelKind)) if (TopPanelAtPoint(static_cast<float>(x), static_cast<float>(y), panelLane, panelKind))
{ {
// 标题栏双击 = 展开 / 折起该面板(折起的面板只显示标题栏)。
// ⚠️ 折起状态下标题栏是唯一可见区域,因此这里也是“双击重新展开”的入口。
{
const EdgePanelLayout titleLayout =
EdgePanelLayoutFor(panelLane, static_cast<EdgePanelKind>(panelKind));
if (titleLayout.title.Width > 0.0f &&
UiPointInRect(titleLayout.title, static_cast<float>(x), static_cast<float>(y)) &&
!(titleLayout.badge.Width > 0.0f &&
UiPointInRect(titleLayout.badge, static_cast<float>(x),
static_cast<float>(y))) &&
!(titleLayout.close.Width > 0.0f &&
UiPointInRect(titleLayout.close, static_cast<float>(x),
static_cast<float>(y))) &&
!(titleLayout.disclosure.Width > 0.0f &&
UiPointInRect(titleLayout.disclosure, static_cast<float>(x),
static_cast<float>(y))))
{
ToggleLanePanelCollapsed(panelLane, static_cast<EdgePanelKind>(panelKind));
return true;
}
}
// 「自由标记」面板:双击某一固定点行 → 编辑它的时间(左侧是自身时刻、 // 「自由标记」面板:双击某一固定点行 → 编辑它的时间(左侧是自身时刻、
// 右侧是相对前一固定点的间隔,对应行内两列的位置)。 // 右侧是相对前一固定点的间隔,对应行内两列的位置)。
if (static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MARK && m_tab != nullptr && if (static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MARK && m_tab != nullptr &&
@@ -2553,6 +2752,7 @@ namespace jdi
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows) for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
{ {
if (row.rect.Width <= 0.0f || if (row.rect.Width <= 0.0f ||
!UiPointInRect(layout.content, static_cast<float>(x), static_cast<float>(y)) ||
!UiPointInRect(row.rect, static_cast<float>(x), static_cast<float>(y)) || !UiPointInRect(row.rect, static_cast<float>(x), static_cast<float>(y)) ||
row.itemIndex >= tabLane.freeMarkerItems.size()) row.itemIndex >= tabLane.freeMarkerItems.size())
{ {
+46 -21
View File
@@ -331,13 +331,13 @@ namespace jdi
// ---------------------------------------------------------------- 自由标记 // ---------------------------------------------------------------- 自由标记
/** 「自由标记」面板里信号名行的高度(比普通条目略高:右侧多一个 ✕)。 */ /** 「自由标记」面板里信号**分组栏**的高度(左侧三角 + 信号名,右侧标记点数 + ✕)。 */
inline const float kFreeMarkerNameRowHeight = 22.0f; inline const float kFreeMarkerNameRowHeight = 22.0f;
/** 「自由标记」面板里每个固定点行的高度。 */ /** 「自由标记」面板里每个固定点行的高度(五列 + 行尾 ✕)。 */
inline const float kFreeMarkerItemRowHeight = 18.0f; inline const float kFreeMarkerItemRowHeight = 20.0f;
/** 「自由标记」面板与网格内固定点 ✕ 按钮的边长。 */ /** 「自由标记」面板与行尾 ✕ 按钮的边长(网格内取值胶囊后的 ✕ 另按胶囊高度取正方形)。 */
inline const float kFreeMarkerCloseSize = 13.0f; inline const float kFreeMarkerCloseSize = 13.0f;
/** /**
@@ -347,21 +347,45 @@ namespace jdi
* 而后者在 `TopPanelAtPoint` / `SyncPanelOrder` 里被高频调用(每次鼠标移动数次); * 而后者在 `TopPanelAtPoint` / `SyncPanelOrder` 里被高频调用(每次鼠标移动数次);
* 逐行取采样值算宽度会变成"鼠标一动就做几十次采样查询"。 * 逐行取采样值算宽度会变成"鼠标一动就做几十次采样查询"。
* *
* 宽度 = 左右内边距 + 四列(取值/增量/时刻/间隔)+ 行 ✕ ⇒ 保证四列在纵向上对齐。 * 宽度 = 左右内边距 + 五列(取值/枚举/增量/时刻/间隔)+ 行 ✕ ⇒ 保证五列在纵向上对齐。
*/ */
inline const float kFreeMarkerPanelWidth = 415.0f; inline const float kFreeMarkerPanelWidth = 424.0f;
/** /**
* @brief 「自由标记」面板四列的逻辑宽度(**按此顺序**:取值 / 增量 / 时刻 / 间隔)。 * @brief 「自由标记」面板五列的逻辑宽度
* (**按此顺序**:取值 / 枚举文本 / 增量 / 时刻 / 间隔)。
* *
* 固定列宽(不按文本测量,见上),绘制时用 `UiEllipsize` 截断超长文本。 * 固定列宽(不按文本测量,见上),绘制时用 `UiEllipsize` 截断超长文本。
* 四列左对齐且右边界由列宽推得 ⇒ 同一信息在所有条目上纵向上严格对齐。 * 当整个面板里没有任何枚举文本时,枚举这一列会被**整列缺省**(列宽记 0,
* 后面的列左移),其它列的宽度不变。
*/ */
inline const float kFreeMarkerColumnWidths[4] = {124.0f, 86.0f, 78.0f, 90.0f}; inline const float kFreeMarkerColumnWidths[5] = {74.0f, 80.0f, 60.0f, 72.0f, 78.0f};
/** 「自由标记」面板行内相邻两列之间的间距。 */ /** 「自由标记」面板行内相邻两列之间的间距。 */
inline const float kFreeMarkerColumnGap = 2.0f; inline const float kFreeMarkerColumnGap = 2.0f;
/** 「自由标记」面板内容滚动条的宽度(逻辑像素)。 */
inline const float kFreeMarkerScrollWidth = 7.0f;
/**
* @brief 「自由标记」面板**内容区**的最大高度(逻辑像素)。
*
* 超过它就出现上下滚动条(分组栏与固定点条目一起滚动);面板底部的按钮与
* 「标记透明度」滑动条不参与滚动。
*/
inline const float kFreeMarkerContentMaxHeight = 260.0f;
/** 「自由标记」面板底部「标记透明度」滑动条行的高度(与信号样式区的行高同量级)。 */
inline const float kFreeMarkerSliderRowHeight = 24.0f;
/**
* @brief 所有实验性功能浮动面板标题栏左侧「展开/折起」指示器的宽度。
*
* 指示器 = 三角(朝右 = 已折起、朝下 = 已展开)+ 三角后方的一条横线;
* 点击它切换面板的折起状态。`GX` 编号徽标向右让出这块宽度。
*/
inline const float kEdgePanelDisclosureWidth = 20.0f;
/** /**
* @brief 非光标模式下判定"鼠标在信号曲线上"的纵向容差(逻辑像素)。 * @brief 非光标模式下判定"鼠标在信号曲线上"的纵向容差(逻辑像素)。
* *
@@ -416,28 +440,29 @@ namespace jdi
} }
/** /**
* @brief 「自由标记」的时间列文本:`t:<时刻>`。 * @brief 「自由标记」的时间列文本:`<时刻>s`。
* *
* 数值格式与光标时间读数**完全相同**(`FormatValue` + `s` 后缀, * 数值格式与光标时间读数**完全相同**(`FormatValue` + `s` 后缀)。
* 即 `FormatCursorTime` 的口径);这里不调用 `FormatCursorTime` 是因为它在本头里
* 定义在本段**之后**,直接展开可以避免前向声明。
*/ */
inline std::wstring FreeMarkerTimeText(double time) inline std::wstring FreeMarkerTimeText(double time)
{ {
return L"t:" + FormatValue(time) + L"s"; return FormatValue(time) + L"s";
} }
/** 「自由标记」面板里缺省(不适用)的单元格文本。 */
inline const wchar_t* const kFreeMarkerNotAvailable = L"N/A";
/** /**
* @brief 「自由标记」的时间间隔列文本:`Δt:<间隔>`。 * @brief 「自由标记」的时间间隔列文本:`Δt:+<间隔>`。
* *
* 平时用复合单位(自动匹配 ms / s / min,与双光标差值读数一致), * 平时用复合单位(自动匹配 ms / s / min,与双光标差值读数一致),
* 双击后按**秒**编辑。首个固定点没有前一项 ⇒ 返回空串(该列留白)。 * 双击后按**秒**编辑。首个固定点没有前一项 ⇒ 显示 `N/A`(该列不省略,保持列对齐)。
*/ */
inline std::wstring FreeMarkerDeltaText(bool hasPrev, double timeDelta) inline std::wstring FreeMarkerDeltaText(bool hasPrev, double timeDelta)
{ {
if (!hasPrev) if (!hasPrev)
{ {
return std::wstring(); return std::wstring(kFreeMarkerNotAvailable);
} }
// 时间间隔恒为正(按时间排序后当前点总在后方);显示时显式带 `+`。 // 时间间隔恒为正(按时间排序后当前点总在后方);显示时显式带 `+`。
return L"Δt:+" + FormatDurationCompound(std::fabs(timeDelta)); return L"Δt:+" + FormatDurationCompound(std::fabs(timeDelta));
@@ -525,10 +550,10 @@ 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"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱峰调整共用)。 */
/** 「自由标记」面板底部切换按钮:光标模式**未启用**时的文案(点击后启用)。 */ /** 「自由标记」面板底部切换按钮文案(切换按钮的激活态 = 光标模式开启)。 */
inline const wchar_t* const kFreeMarkerCursorOnLabel = L"启用光标模式"; inline const wchar_t* const kFreeMarkerCursorModeLabel = L"光标选取模式";
/** 「自由标记」面板底部切换按钮:光标模式**已启用**时的文案(点击后关闭)。 */ /** 「自由标记」面板底部切换按钮激活时出现在其上方的滑动条标签。 */
inline const wchar_t* const kFreeMarkerCursorOffLabel = L"关闭光标模式"; inline const wchar_t* const kFreeMarkerOpacityLabel = L"标记透明度";
/** /**
* @brief 边沿统计累积状态上限。 * @brief 边沿统计累积状态上限。
* *
+522 -144
View File
@@ -5342,6 +5342,14 @@ namespace jdi
{ {
return layout; return layout;
} }
// 面板的**折起**状态(只显示标题栏)。折起时仍然算宽度与位置,但内容区全部省略。
const bool collapsed = meanFilter ? lane.meanFilterPanel.collapsed
: (weakPeak ? lane.weakPeakPanel.collapsed
: (freeMarker ? lane.freeMarkerPanel.collapsed
: (centerAxis
? lane.edgeCenterPanel.collapsed
: lane.edgePanel.collapsed)));
layout.collapsed = collapsed;
int rowCount = 0; int rowCount = 0;
for (const SignalRef& ref : lane.signals) for (const SignalRef& ref : lane.signals)
@@ -5415,8 +5423,10 @@ namespace jdi
height = titleHeight + pad + static_cast<float>(rowCount) * rowHeight * rowsPerSignal + pad; height = titleHeight + pad + static_cast<float>(rowCount) * rowHeight * rowsPerSignal + pad;
} }
// 「自由标记」面板:逐信号一行名称(**只列出确实有固定点的信号**),下面跟着它的全部固定点; // 「自由标记」面板:按信号分组(**分组栏** + 下属固定点条目),
// 没有任何固定点时给一行灰色提示,最后始终带一行底部切换按钮。 // 只列出确实有固定点的信号;没有任何固定点时给一行灰色提示。
// 内容区超高时出现滚动条;面板底部固定「标记透明度」滑块(仅光标模式激活时)
// 与「光标选取模式」切换按钮(二者**不参与**滚动)。
bool markerAny = false; bool markerAny = false;
for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems) for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems)
{ {
@@ -5428,9 +5438,11 @@ namespace jdi
} }
if (freeMarker) if (freeMarker)
{ {
const float nameRow = kFreeMarkerNameRowHeight * m_scale; const float groupRow = kFreeMarkerNameRowHeight * m_scale;
const float itemRow = kFreeMarkerItemRowHeight * m_scale; const float itemRow = kFreeMarkerItemRowHeight * m_scale;
const float markerButtonHeight = kEdgePanelButtonHeight * m_scale; const float markerButtonHeight = kEdgePanelButtonHeight * m_scale;
const float sliderRow =
lane.freeMarkerCursor ? (kFreeMarkerSliderRowHeight * m_scale) : 0.0f;
float content = 0.0f; float content = 0.0f;
std::vector<std::size_t> order; std::vector<std::size_t> order;
for (const SignalRef& ref : lane.signals) for (const SignalRef& ref : lane.signals)
@@ -5440,22 +5452,37 @@ namespace jdi
{ {
continue; continue;
} }
// ⚠️ 用户明确要求:某个信号没有任何固定点时,它的名称条目**不出现**在面板上。 // ⚠️ 用户明确要求:某个信号没有任何固定点时,它的分组栏**不出现**在面板上。
SortedFreeMarkerIndices(lane, ref, order); SortedFreeMarkerIndices(lane, ref, order);
if (order.empty()) if (order.empty())
{ {
continue; continue;
} }
content += nameRow; content += groupRow;
content += static_cast<float>(order.size()) * itemRow; if (!lane.FreeMarkerGroupCollapsed(ref))
{
content += static_cast<float>(order.size()) * itemRow;
}
} }
if (!markerAny) if (!markerAny)
{ {
// 还没有任何固定点:第一行留作提示文案(绘制时画「在网格内单击以固定数据点」)。 // 还没有任何固定点:留一行提示文案(绘制时画「在网格内单击以固定数据点」)。
content += nameRow; content += groupRow;
} }
// 底部「启用光标模式 / 关闭光标模式」按钮:上下各留一个 pad(与左右内边距一致)。 // 内容区高度上限:太长的列表不能把面板顶出绘图区(超出部分靠滚动条翻页)。
height = titleHeight + pad + content + pad + markerButtonHeight + pad; const float budget =
m_plotRect.Height - (titleHeight + pad * 3.0f + markerButtonHeight + sliderRow);
const float maxContent =
std::max(2.0f * itemRow,
std::min(kFreeMarkerContentMaxHeight * m_scale, std::max(2.0f * itemRow, budget)));
const float view = std::min(content, maxContent);
height = titleHeight + pad + view + pad + sliderRow + markerButtonHeight + pad;
}
// 折起的面板:只剩标题栏(内容区完全不占高度)。
if (collapsed)
{
height = titleHeight;
} }
// 首次使用(位置为负)时自动放到绘图区**右上角**,并按网格在**当前可见网格中的序数** // 首次使用(位置为负)时自动放到绘图区**右上角**,并按网格在**当前可见网格中的序数**
@@ -5546,10 +5573,22 @@ namespace jdi
layout.title = gd::RectF(left, top, width, titleHeight); layout.title = gd::RectF(left, top, width, titleHeight);
const float closeSize = titleHeight; const float closeSize = titleHeight;
layout.close = gd::RectF(left + width - closeSize, top, closeSize, closeSize); layout.close = gd::RectF(left + width - closeSize, top, closeSize, closeSize);
// 标题栏左侧的「展开 / 折起」三角指示器(位于 GX 徽标左边)。
// 折起后面板只剩标题栏,因此这个指示器是唯一的“重新展开”入口(双击标题栏亦可)。
layout.disclosure = gd::RectF(left + pad, top + (titleHeight - kEdgePanelDisclosureWidth * m_scale) * 0.5f,
kEdgePanelDisclosureWidth * m_scale,
kEdgePanelDisclosureWidth * m_scale);
// 标题栏左侧的「GX」编号徽标:绘制、悬停提示、手型光标与“点击滚动到该网格” // 标题栏左侧的「GX」编号徽标:绘制、悬停提示、手型光标与“点击滚动到该网格”
// 全部以这个矩形为准(唯一几何来源,避免“画的”和“点的”漂移)。 // 全部以这个矩形为准(唯一几何来源,避免“画的”和“点的”漂移)。
layout.badge = gd::RectF(left + pad, top + (titleHeight - kLaneBadgeHeight * m_scale) * 0.5f, layout.badge =
kLaneBadgeWidth * m_scale, kLaneBadgeHeight * m_scale); gd::RectF(left + pad + kEdgePanelDisclosureWidth * m_scale,
top + (titleHeight - kLaneBadgeHeight * m_scale) * 0.5f, kLaneBadgeWidth * m_scale,
kLaneBadgeHeight * m_scale);
// 折起状态:到此为止(内容区、按钮、滚动条全部为空)。
if (collapsed)
{
return layout;
}
if (itemized) if (itemized)
{ {
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale; const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
@@ -5688,26 +5727,44 @@ namespace jdi
if (freeMarker) if (freeMarker)
{ {
// 「自由标记」面板:**只列出确实有固定点的信号**,每个信号一行名称 // 「自由标记」面板:**只列出确实有固定点的信号**,每个信号一个**分组栏**
//(右侧带「清除本信号全部固定点」✕),下面逐行列出它的固定点 //(信号色托底 + 左侧展开/收起三角 + 信号名 + 右侧「N 标记点」与 ✕),
//(组内按时间升序 == 从左到右),行尾再带一个 ✕。 // 下面逐行列出它的固定点(组内按时间升序 == 从上到下),行尾再带一个 ✕。
const float nameRow = kFreeMarkerNameRowHeight * m_scale; // 内容区超高时右侧出现滚动条;底部按钮与「标记透明度」滑块**固定**不滚动。
const float groupRow = kFreeMarkerNameRowHeight * m_scale;
const float itemRow = kFreeMarkerItemRowHeight * m_scale; const float itemRow = kFreeMarkerItemRowHeight * m_scale;
const float closeSize = kFreeMarkerCloseSize * m_scale; const float closeSize = kFreeMarkerCloseSize * m_scale;
const float gap = 4.0f * m_scale;
const float markerButtonHeight = kEdgePanelButtonHeight * m_scale; const float markerButtonHeight = kEdgePanelButtonHeight * m_scale;
const float sliderRow = lane.freeMarkerCursor ? (kFreeMarkerSliderRowHeight * m_scale) : 0.0f;
const float scrollW = kFreeMarkerScrollWidth * m_scale;
// 四列(① 取值 ② 增量 ③ 时刻 ④ 间隔)从左往右**固定列宽**排布: // 底部固定区:切换按钮(整行宽度、与面板下边界留一个 pad)。
// 每一行的同类信息都从同一横坐标开始 ⇒ 纵向上严格对齐(用户明确要求)。 layout.button = gd::RectF(left + pad, top + height - pad - markerButtonHeight,
float columnX = left + pad; std::max(0.0f, width - pad * 2.0f), markerButtonHeight);
for (int column = 0; column < 4; ++column) if (sliderRow > 0.0f)
{ {
layout.markerColumns[column].left = columnX; // 「标记透明度」行:与信号样式区的「透明度」行同一套排版
layout.markerColumns[column].width = kFreeMarkerColumnWidths[column] * m_scale; //(左侧标签 + 中间轨道 + 右侧百分比),只是位置固定在按钮上方。
columnX += layout.markerColumns[column].width + kFreeMarkerColumnGap * m_scale; const float rowTop = layout.button.Y - sliderRow;
const float valueWidth =
std::max(36.0f * m_scale,
UiMeasureTextWidth(m_fonts->monoSmall, L"100%") + 6.0f * m_scale);
const float labelWidth =
UiMeasureTextWidth(m_fonts->uiSmall, kFreeMarkerOpacityLabel) + 6.0f * m_scale;
const float trackLeft = left + pad + labelWidth;
const float trackRight = left + width - pad - valueWidth - 6.0f * m_scale;
const float trackHeight = kWeakPeakTrackHeight * m_scale;
layout.opacityTrack =
gd::RectF(trackLeft, rowTop + (sliderRow - trackHeight) * 0.5f,
std::max(20.0f * m_scale, trackRight - trackLeft), trackHeight);
layout.opacityValue =
gd::RectF(trackRight + 6.0f * m_scale, rowTop, valueWidth, sliderRow);
} }
float rowY = top + titleHeight + pad; // 内容区高度(面板高度算式里的 `view` 部分)与"内容总高"决定是否需要滚动条。
const float viewHeight = std::max(
0.0f, height - titleHeight - pad * 3.0f - markerButtonHeight - sliderRow);
float totalContent = 0.0f;
std::vector<std::size_t> order; std::vector<std::size_t> order;
for (const SignalRef& ref : lane.signals) for (const SignalRef& ref : lane.signals)
{ {
@@ -5716,35 +5773,124 @@ namespace jdi
{ {
continue; continue;
} }
// 该信号没有任何固定点 ⇒ 名称条目与数据行都**不出现**(用户明确要求)。
SortedFreeMarkerIndices(lane, ref, order); SortedFreeMarkerIndices(lane, ref, order);
if (order.empty()) if (order.empty())
{ {
continue; continue;
} }
layout.rowRefs.push_back(ref); totalContent += groupRow;
layout.rowNameRects.push_back(gd::RectF(left + pad, rowY, if (!lane.FreeMarkerGroupCollapsed(ref))
std::max(0.0f, width - pad * 2.0f - closeSize - gap), {
nameRow)); totalContent += static_cast<float>(order.size()) * itemRow;
layout.rowNameCloses.push_back( }
gd::RectF(left + width - pad - closeSize, rowY + (nameRow - closeSize) * 0.5f, }
closeSize, closeSize)); if (!markerAny)
rowY += nameRow; {
totalContent += groupRow;
}
const bool scrollable = totalContent > viewHeight + 0.5f;
const float maxScroll = std::max(0.0f, totalContent - viewHeight);
const float scroll = std::max(0.0f, std::min(lane.freeMarkerScroll, 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 = totalContent;
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 / totalContent));
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);
}
// 五列(① 取值 ② 枚举 ③ 增量 ④ 时刻 ⑤ 间隔)从左往右**固定列宽**排布:
// 每一行的同类信息都从同一横坐标开始 ⇒ 纵向上严格对齐(用户明确要求)。
// ⚠️ 整个面板里没有任何枚举文本时,② 枚举列**整列缺省**(列宽记 0,后面的列左移)。
// 这里只查"该信号是否带枚举表"(O(1)),**不**逐条采样(布局函数被高频调用)。
bool hasEnum = false;
for (const SignalRef& ref : lane.signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data != nullptr && resolved.visible && !resolved.data->enumText.Empty() &&
lane.HasFreeMarkers(ref))
{
hasEnum = true;
break;
}
}
layout.markerHasEnumColumn = hasEnum;
float columnX = left + pad;
for (int column = 0; column < 5; ++column)
{
const bool omitted = (column == EdgePanelLayout::MARKER_COL_ENUM) && !hasEnum;
const float columnWidth =
omitted ? 0.0f : (kFreeMarkerColumnWidths[column] * m_scale);
layout.markerColumns[column].left = columnX;
layout.markerColumns[column].width = columnWidth;
columnX += columnWidth + (omitted ? 0.0f : kFreeMarkerColumnGap * m_scale);
}
const float contentBottom = layout.content.Y + layout.content.Height;
float rowY = layout.content.Y - scroll;
for (const SignalRef& ref : lane.signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || !resolved.visible)
{
continue;
}
// 该信号没有任何固定点 ⇒ 分组栏与数据行都**不出现**(用户明确要求)。
SortedFreeMarkerIndices(lane, ref, order);
if (order.empty())
{
continue;
}
EdgePanelLayout::MarkerGroup group;
group.ref = ref;
group.collapsed = lane.FreeMarkerGroupCollapsed(ref);
group.header = gd::RectF(layout.content.X, rowY, innerWidth, groupRow);
group.triangle = gd::RectF(layout.content.X + 3.0f * m_scale,
rowY + (groupRow - 12.0f * m_scale) * 0.5f,
12.0f * m_scale, 12.0f * m_scale);
group.close = gd::RectF(layout.content.X + innerWidth - closeSize - 2.0f * m_scale,
rowY + (groupRow - closeSize) * 0.5f, closeSize, closeSize);
if (group.header.Y + group.header.Height > layout.content.Y &&
group.header.Y < contentBottom)
{
// 只登记"在可视内容区里"的分组栏与条目(滚出视界的条目不得抢走
// 底部按钮 / 滑块的点击)。
layout.markerGroups.push_back(group);
layout.rowRefs.push_back(ref);
layout.rowNameRects.push_back(group.header);
layout.rowNameCloses.push_back(group.close);
}
rowY += groupRow;
if (group.collapsed)
{
continue;
}
for (const std::size_t itemIndex : order) for (const std::size_t itemIndex : order)
{ {
EdgePanelLayout::MarkerRow markerRow; EdgePanelLayout::MarkerRow markerRow;
markerRow.itemIndex = itemIndex; markerRow.itemIndex = itemIndex;
markerRow.rect = gd::RectF(left + pad, rowY, width - pad * 2.0f, itemRow); markerRow.rect = gd::RectF(layout.content.X, rowY, innerWidth, itemRow);
markerRow.close = gd::RectF(left + width - pad - closeSize, markerRow.close = gd::RectF(layout.content.X + innerWidth - closeSize,
rowY + (itemRow - closeSize) * 0.5f, closeSize, closeSize); rowY + (itemRow - closeSize) * 0.5f, closeSize,
layout.markerRows.push_back(markerRow); closeSize);
if (markerRow.rect.Y + markerRow.rect.Height > layout.content.Y &&
markerRow.rect.Y < contentBottom)
{
layout.markerRows.push_back(markerRow);
}
rowY += itemRow; rowY += itemRow;
} }
} }
// 底部切换按钮:整行宽度、与面板下边界留一个 pad。
layout.button = gd::RectF(left + pad, top + height - pad - markerButtonHeight,
std::max(0.0f, width - pad * 2.0f), markerButtonHeight);
} }
return layout; return layout;
} }
@@ -6207,9 +6353,17 @@ namespace jdi
mixRect(layout.panel); mixRect(layout.panel);
mixRect(layout.title); mixRect(layout.title);
mixRect(layout.close); mixRect(layout.close);
mixRect(layout.disclosure);
mixRect(layout.badge); mixRect(layout.badge);
mixRect(layout.edit); mixRect(layout.edit);
mixRect(layout.button); mixRect(layout.button);
mixRect(layout.content);
mixRect(layout.scrollTrack);
mixRect(layout.scrollThumb);
mixRect(layout.opacityTrack);
mixRect(layout.opacityValue);
mix(layout.collapsed ? 1ull : 0ull);
mix(layout.markerHasEnumColumn ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(layout.rows)); mix(static_cast<std::uint64_t>(layout.rows));
for (const gd::RectF& rect : layout.rowChecks) for (const gd::RectF& rect : layout.rowChecks)
{ {
@@ -6247,12 +6401,25 @@ namespace jdi
mixFloat(column.left); mixFloat(column.left);
mixFloat(column.width); mixFloat(column.width);
} }
// 「自由标记」的分组栏(托底矩形 / 三角 / ✕)与各自的收起状态都在面板像素里。
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
{
mixRect(group.header);
mixRect(group.triangle);
mixRect(group.close);
mix(group.collapsed ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(group.ref.groupIndex + 2));
mix(static_cast<std::uint64_t>(group.ref.signalIndex + 2));
}
const Lane& lane = Tab().Lanes()[static_cast<std::size_t>(laneIndex)]; const Lane& lane = Tab().Lanes()[static_cast<std::size_t>(laneIndex)];
if (kind == EdgePanelKind::MARK) if (kind == EdgePanelKind::MARK)
{ {
// 底部切换按钮的文案取决于光标模式 ⇒ 必须进键,否则开关后仍贴回旧像素。 // 底部切换按钮的激活态、透明度滑块与内容滚动偏移直接改变面板像素 ⇒ 必须进键,
// 否则开关 / 拖滑块 / 滚动之后仍会贴回旧像素。
mix(lane.freeMarkerCursor ? 1ull : 0ull); mix(lane.freeMarkerCursor ? 1ull : 0ull);
mixFloat(lane.freeMarkerOpacity);
mixFloat(lane.freeMarkerScroll);
// 固定点的**内容**(哪个信号、什么时刻)直接决定面板文字与行数,必须进键; // 固定点的**内容**(哪个信号、什么时刻)直接决定面板文字与行数,必须进键;
// 否则新增/删除一个固定点后可能贴回旧像素。 // 否则新增/删除一个固定点后可能贴回旧像素。
for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems) for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems)
@@ -6499,10 +6666,11 @@ namespace jdi
: nullptr; : nullptr;
text = FreeMarkerValueText(value, hasPrev, delta, enumText); text = FreeMarkerValueText(value, hasPrev, delta, enumText);
const float closeSize = kFreeMarkerCloseSize * m_scale; // ✕ 按钮 = **与取值胶囊同高的正方形**(用户明确要求:长宽都等于胶囊的上下高度)。
const float gap = kFreeMarkerCloseGap * m_scale;
const float textWidth = UiMeasureTextWidth(m_fonts->monoSmall, text) + 10.0f * m_scale; const float textWidth = UiMeasureTextWidth(m_fonts->monoSmall, text) + 10.0f * m_scale;
const float textHeight = 17.0f * m_scale; const float textHeight = 17.0f * m_scale;
const float closeSize = textHeight;
const float gap = kFreeMarkerCloseGap * m_scale;
const float totalWidth = textWidth + gap + closeSize; const float totalWidth = textWidth + gap + closeSize;
float left = rawX + 6.0f * m_scale; float left = rawX + 6.0f * m_scale;
@@ -6516,28 +6684,93 @@ namespace jdi
top = std::max(clipTop + 1.0f * m_scale, std::min(top, clipBottom - textHeight - 1.0f * m_scale)); top = std::max(clipTop + 1.0f * m_scale, std::min(top, clipBottom - textHeight - 1.0f * m_scale));
label = gd::RectF(left, top, textWidth, textHeight); label = gd::RectF(left, top, textWidth, textHeight);
close = gd::RectF(left + textWidth + gap, top + (textHeight - closeSize) * 0.5f, closeSize, close = gd::RectF(left + textWidth + gap, top, closeSize, closeSize);
closeSize);
return true; return true;
} }
/**
* @brief 用 **GDI+** 绘制一段可带 alpha 的文字(仅用于「标记透明度」低于 100% 的胶囊)。
*
* ⚠️ 界面其余文字一律走 `UiText`(GDI `DrawTextW`)。GDI 文本**忽略 alpha**
*(`SetTextColor` 只吃 RGB)⇒ 直接把 alpha 乘进颜色不会让文字淡出。
* 因此只有在"固定点被调成半透明"这个特殊状态下才改用 GDI+ `DrawString`:
* 那样才能让文字与胶囊一起真正淡出;不透明度为 100% 时仍走 `UiText`,像素零变化。
*
* @param [IN,OUT] g 绘图上下文。
* @param [IN] text 文本。
* @param [IN] font 字体(与 `UiText` 同一 HFONT)。
* @param [IN] rect 文本矩形(居中)。
* @param [IN] argb 颜色(含目标 alpha)。
*/
void DrawFreeMarkerAlphaText(gd::Graphics& g, const std::wstring& text, HFONT font,
const gd::RectF& rect, std::uint32_t argb)
{
if (text.empty() || font == nullptr || rect.Width <= 0.0f || rect.Height <= 0.0f)
{
return;
}
LOGFONTW lf{};
if (GetObjectW(font, static_cast<int>(sizeof(lf)), &lf) != static_cast<int>(sizeof(lf)))
{
UiText(g, text, font, rect, argb, TextAlign::CENTER);
return;
}
HDC hdc = g.GetHDC();
if (hdc == nullptr)
{
return;
}
gd::Font textFont(hdc, &lf);
g.ReleaseHDC(hdc);
if (textFont.GetLastStatus() != gd::Ok)
{
UiText(g, text, font, rect, argb, TextAlign::CENTER);
return;
}
gd::SolidBrush brush(ToGdiColor(argb));
gd::StringFormat format;
format.SetAlignment(gd::StringAlignmentCenter);
format.SetLineAlignment(gd::StringAlignmentCenter);
format.SetTrimming(gd::StringTrimmingEllipsisCharacter);
format.SetFormatFlags(gd::StringFormatFlagsNoWrap);
g.DrawString(text.c_str(), static_cast<INT>(text.size()), &textFont, rect, &format, &brush);
}
void PlotView::PaintFreeMarkerCapsule(gd::Graphics& g, const gd::RectF& label, void PlotView::PaintFreeMarkerCapsule(gd::Graphics& g, const gd::RectF& label,
const std::wstring& text, std::uint32_t color, const std::wstring& text, std::uint32_t color,
const gd::RectF& close, bool closeHot) const gd::RectF& close, bool closeHot, float alphaScale)
{ {
const float scale = std::max(0.0f, std::min(1.0f, alphaScale));
const auto scaled = [scale](std::uint32_t argb, float base) -> std::uint32_t {
const int alpha = static_cast<int>(std::lround(base * scale));
return WithAlpha(argb, (alpha <= 0) ? 0 : (alpha >= 255 ? 255 : alpha));
};
const UiInk ink = UiContrastInk(color); const UiInk ink = UiContrastInk(color);
UiFillRoundRect(g, label, 3.0f * m_scale, WithAlpha(color, 0xF0)); if (scale <= 0.02f)
DrawLabelOutline(g, label, 3.0f * m_scale, ink.outline); {
UiText(g, text, m_fonts->monoSmall, label, ink.text, TextAlign::CENTER); return; // 完全透明:连描边与文字都不画(否则会留下"看不见的"像素)
}
UiFillRoundRect(g, label, 3.0f * m_scale, scaled(color, 0xF0));
DrawLabelOutline(g, label, 3.0f * m_scale, scaled(ink.outline, 255.0f));
if (scale >= 0.999f)
{
UiText(g, text, m_fonts->monoSmall, label, ink.text, TextAlign::CENTER);
}
else
{
// GDI 文本忽略 alpha ⇒ 半透明时必须改用 GDI+ 才能真的淡出(见上面说明)。
DrawFreeMarkerAlphaText(g, text, m_fonts->monoSmall, label, scaled(ink.text, 255.0f));
}
if (close.Width <= 0.0f) if (close.Width <= 0.0f)
{ {
return; return;
} }
// ✕ 按钮:悬停时用 danger 底色 + 叉(与面板里的 ✕ 同一套语义)。 // ✕ 按钮:悬停时用 danger 底色 + 叉(与面板里的 ✕ 同一套语义)。
UiFillRoundRect(g, close, 3.0f * m_scale, UiFillRoundRect(g, close, 3.0f * m_scale,
closeHot ? WithAlpha(m_theme->danger, 0x66) : WithAlpha(color, 0xC0)); closeHot ? WithAlpha(m_theme->danger, 0x66) : scaled(color, 0xC0));
const float crossPad = 3.5f * m_scale; const float crossPad = 4.0f * m_scale;
const std::uint32_t crossColor = closeHot ? m_theme->danger : ink.text; const std::uint32_t crossColor =
closeHot ? m_theme->danger : scaled(ink.text, 255.0f);
UiDrawLine(g, close.X + crossPad, close.Y + crossPad, close.X + close.Width - crossPad, UiDrawLine(g, close.X + crossPad, close.Y + crossPad, close.X + close.Width - crossPad,
close.Y + close.Height - crossPad, crossColor, 1.4f * m_scale); close.Y + close.Height - crossPad, crossColor, 1.4f * m_scale);
UiDrawLine(g, close.X + close.Width - crossPad, close.Y + crossPad, close.X + crossPad, UiDrawLine(g, close.X + close.Width - crossPad, close.Y + crossPad, close.X + crossPad,
@@ -6560,6 +6793,8 @@ namespace jdi
const std::size_t count = std::min(lanes.size(), tabLanes.size()); const std::size_t count = std::min(lanes.size(), tabLanes.size());
// ---- (1) 已固定的数据点(含各自的 ✕)---- // ---- (1) 已固定的数据点(含各自的 ✕)----
// ⚠️ 固定点**不再画纵向细线**(用户 2026-10-21 明确要求);
// 取值胶囊彼此重叠时做**纵向避让**(依次尝试下方 / 上方错开),保证都能读清。
for (std::size_t laneIndex = 0; laneIndex < count; ++laneIndex) for (std::size_t laneIndex = 0; laneIndex < count; ++laneIndex)
{ {
const Lane& lane = tabLanes[laneIndex]; const Lane& lane = tabLanes[laneIndex];
@@ -6567,6 +6802,8 @@ namespace jdi
{ {
continue; continue;
} }
const float markerAlpha = std::max(0.0f, std::min(1.0f, lane.freeMarkerOpacity));
std::vector<gd::RectF> placed;
for (std::size_t itemIndex = 0; itemIndex < lane.freeMarkerItems.size(); ++itemIndex) for (std::size_t itemIndex = 0; itemIndex < lane.freeMarkerItems.size(); ++itemIndex)
{ {
gd::RectF label; gd::RectF label;
@@ -6592,27 +6829,71 @@ namespace jdi
double enumValue = 0.0; double enumValue = 0.0;
const bool valueOk = FreeMarkerValue(lane, item, value, enumValue); const bool valueOk = FreeMarkerValue(lane, item, value, enumValue);
// 纵向细线(仅限本网格,低透明度):让用户看得出固定点在哪一刻。 // ---- 纵向避让:与本次已放置的胶囊重叠时,向下 / 向上交替错开 ----
const float x = TimeToX(item.time); const auto occupied = [&placed](const gd::RectF& rect) {
UiDrawLine(g, x, clipTop, x, clipBottom, WithAlpha(signalColor, 0x70), for (const gd::RectF& other : placed)
1.0f * m_scale); {
if (rect.X < other.X + other.Width && other.X < rect.X + rect.Width &&
rect.Y < other.Y + other.Height && other.Y < rect.Y + rect.Height)
{
return true;
}
}
return false;
};
const auto totalRect = [](const gd::RectF& labelRect, const gd::RectF& closeRect) {
const float right = std::max(labelRect.X + labelRect.Width,
closeRect.X + closeRect.Width);
return gd::RectF(labelRect.X, labelRect.Y, right - labelRect.X,
labelRect.Height);
};
const float step = label.Height + 2.0f * m_scale;
const float minTop = clipTop + 1.0f * m_scale;
const float maxTop = std::max(minTop, clipBottom - label.Height - 1.0f * m_scale);
const float baseTop = label.Y;
float chosenTop = baseTop;
for (int attempt = 0; attempt <= 8; ++attempt)
{
const float magnitude = static_cast<float>((attempt + 1) / 2) * step;
const float offset =
(attempt == 0) ? 0.0f : ((attempt % 2 == 1) ? magnitude : -magnitude);
const float top = std::max(minTop, std::min(baseTop + offset, maxTop));
gd::RectF candidateLabel = label;
gd::RectF candidateClose = close;
candidateLabel.Y = top;
candidateClose.Y = top;
if (!occupied(totalRect(candidateLabel, candidateClose)))
{
chosenTop = top;
break;
}
}
label.Y = chosenTop;
close.Y = chosenTop;
placed.push_back(totalRect(label, close));
// 数据点:与光标的数据点同一画法(圆点)。 // 数据点:与光标的数据点同一画法(圆点),同样受「标记透明度」控制。
const float x = TimeToX(item.time);
const float radius = 3.4f * m_scale; const float radius = 3.4f * m_scale;
gd::SolidBrush dot(ToGdiColor(signalColor)); const int dotAlpha = static_cast<int>(std::lround(255.0f * markerAlpha));
gd::SolidBrush dot(ToGdiColor(WithAlpha(signalColor, dotAlpha)));
const float dotY = valueOk ? ValueToY(value, visual) : (clipTop + clipBottom) * 0.5f; const float dotY = valueOk ? ValueToY(value, visual) : (clipTop + clipBottom) * 0.5f;
g.FillEllipse(&dot, x - radius, dotY - radius, radius * 2.0f, radius * 2.0f); g.FillEllipse(&dot, x - radius, dotY - radius, radius * 2.0f, radius * 2.0f);
// ✕ 按钮的悬停态决定它的配色,因此先算再画胶囊。 // ✕ 按钮的悬停态决定它的配色,因此先算再画胶囊。
const bool hot = (m_freeMarkerHoverLane == static_cast<int>(laneIndex) && const bool hot = (m_freeMarkerHoverLane == static_cast<int>(laneIndex) &&
m_freeMarkerHoverItem == itemIndex); m_freeMarkerHoverItem == itemIndex);
PaintFreeMarkerCapsule(g, label, text, signalColor, close, hot); PaintFreeMarkerCapsule(g, label, text, signalColor, close, hot, markerAlpha);
FreeMarkerHit hit; FreeMarkerHit hit;
hit.laneIndex = static_cast<int>(laneIndex); hit.laneIndex = static_cast<int>(laneIndex);
hit.itemIndex = itemIndex; hit.itemIndex = itemIndex;
hit.close = close; hit.close = close;
m_freeMarkerHits.push_back(hit); if (markerAlpha > 0.02f)
{
// 完全透明时不可见 ⇒ 也不该被点中(否则会“隔着看不见的按钮”移除固定点)。
m_freeMarkerHits.push_back(hit);
}
} }
} }
@@ -7079,6 +7360,33 @@ namespace jdi
UiFillRoundRect(g, gd::RectF(panel.X, panel.Y, panel.Width, titleHeight), 5.0f * m_scale, UiFillRoundRect(g, gd::RectF(panel.X, panel.Y, panel.Width, titleHeight), 5.0f * m_scale,
WithAlpha(m_theme->accent, 0x2A)); WithAlpha(m_theme->accent, 0x2A));
// 标题栏左侧的「展开 / 折起」指示器:三角(朝下 = 已展开、朝右 = 已折起)
// + 三角后方的一条短横线(经典展开/折起标识),位于 GX 徽标左边。
// 单击它切换折起状态;折起后面板只剩标题栏,这里是唯一的“重新展开”按钮。
{
const gd::RectF& disclosureRect = layout.disclosure;
if (disclosureRect.Width > 0.0f)
{
const bool disclosureHot =
(m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) &&
UiPointInRect(disclosureRect, static_cast<float>(m_mouseX),
static_cast<float>(m_mouseY));
const std::uint32_t disclosureColor =
disclosureHot ? m_theme->accent : m_theme->text;
const float centerX = disclosureRect.X + 5.5f * m_scale;
const float centerY = disclosureRect.Y + disclosureRect.Height * 0.5f;
UiDrawTriangleIcon(
g,
gd::RectF(centerX - 4.5f * m_scale, centerY - 4.5f * m_scale,
9.0f * m_scale, 9.0f * m_scale),
disclosureColor,
layout.collapsed ? IconDirection::RIGHT : IconDirection::DOWN);
UiDrawLine(g, centerX + 6.0f * m_scale, centerY,
disclosureRect.X + disclosureRect.Width, centerY, disclosureColor,
1.4f * m_scale);
}
}
// 标题:与左侧信号面板一致的 “GX” 徽标。 // 标题:与左侧信号面板一致的 “GX” 徽标。
// ⚠️ 几何必须取 `layout.badge`(与悬停提示 / 手型光标 / 点击联动同源); // ⚠️ 几何必须取 `layout.badge`(与悬停提示 / 手型光标 / 点击联动同源);
// 悬停时给一圈强调色描边,作为“可点击”的视觉提示(用户 2026-10-19 要求)。 // 悬停时给一圈强调色描边,作为“可点击”的视觉提示(用户 2026-10-19 要求)。
@@ -7125,6 +7433,9 @@ namespace jdi
closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad, crossColor, closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad, crossColor,
1.6f * m_scale); 1.6f * m_scale);
// 折起的面板只保留标题栏:内容区(条目 / 分组栏 / 按钮 / 滚动条)全部省略。
if (!layout.collapsed)
{
// 「中心轴边沿计数」/「均值滤波」/「弱峰调整」面板都采用「逐条目」排布: // 「中心轴边沿计数」/「均值滤波」/「弱峰调整」面板都采用「逐条目」排布:
// 第一排 = 名称 + 复选框,第二排 = 两个参数输入框(仅勾选的条目, // 第一排 = 名称 + 复选框,第二排 = 两个参数输入框(仅勾选的条目,
// 弱峰调整没有参数输入框),中心轴再多第三排 = 对应的统计结果。 // 弱峰调整没有参数输入框),中心轴再多第三排 = 对应的统计结果。
@@ -7345,68 +7656,101 @@ namespace jdi
} }
} }
// 「自由标记」面板:逐信号一行名称(带清除全部固定点的 ✕), // 「自由标记」面板:按信号分组的**分组栏**(信号色托底 + 三角 + 名称 +
// 下面按时间升序列出该信号的固定点(`值(±增量) = 枚举` + 右对齐的 `t:… Δt:…` + 行尾 ✕)。 // 「N 标记点」+ ✕),下属固定点条目五列对齐(取值 / 枚举 / 增量 / 时刻 / 间隔),
// 行尾各带一个 ✕。内容区有滚动偏移时按内容区裁剪。
if (freeMarker) if (freeMarker)
{ {
const float closeSize = kFreeMarkerCloseSize * m_scale; const float groupRowHeight = kFreeMarkerNameRowHeight * m_scale;
const float gap = 4.0f * m_scale;
bool markerAny = false; bool markerAny = false;
for (const Lane::FreeMarkerItem& item : tabLane.freeMarkerItems) for (const Lane::FreeMarkerItem& item : tabLane.freeMarkerItems)
{ {
markerAny = markerAny || item.ref.IsValid(); markerAny = markerAny || item.ref.IsValid();
} }
const int contentState = g.Save();
g.SetClip(layout.content, gd::CombineModeIntersect);
if (!markerAny) if (!markerAny)
{ {
UiText(g, L"在网格内单击可固定数据点", UiText(g, L"在网格内单击可固定数据点", m_fonts->uiSmall,
m_fonts->uiSmall, gd::RectF(layout.content.X, layout.content.Y, layout.content.Width,
gd::RectF(panel.X + pad, panel.Y + titleHeight + pad, groupRowHeight),
panel.Width - pad * 2.0f, kFreeMarkerNameRowHeight * m_scale),
m_theme->textMuted, TextAlign::LEFT); m_theme->textMuted, TextAlign::LEFT);
} }
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
// ---- 分组栏 ----
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
{ {
const SignalRef& ref = layout.rowRefs[rowIndex]; const ResolvedSignal resolved = ResolveSignal(group.ref);
const ResolvedSignal resolved = ResolveSignal(ref); if (resolved.data == nullptr)
if (resolved.data == nullptr || resolved.style == nullptr)
{ {
continue; continue;
} }
const std::uint32_t signalColor = StyleColor(*resolved.style); const std::uint32_t signalColor =
// 名称行矩形直接取自布局(与 ✕ 同一套几何,绘制侧不反推)。 (resolved.style != nullptr) ? StyleColor(*resolved.style) : m_theme->border;
const gd::RectF nameRect = // 托底 = 信号色;文字一律用**自适应对比色**(与信号面板的组名徽标同规则),
(rowIndex < layout.rowNameRects.size()) // 因此无论信号是亮黄还是深蓝都能看清分组栏上的所有内容。
? layout.rowNameRects[rowIndex] const UiInk ink = UiContrastInk(signalColor);
: gd::RectF(panel.X + pad, panel.Y + titleHeight + pad, UiFillRoundRect(g, group.header, 3.0f * m_scale, WithAlpha(signalColor, 0xE8));
panel.Width - pad * 2.0f - (closeSize + gap),
kFreeMarkerNameRowHeight * m_scale);
UiText(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall, nameRect.Width),
m_fonts->uiSmall, nameRect, signalColor, TextAlign::LEFT);
// 信号名右侧的 ✕:清除该信号的**全部**固定点。 // 左侧三角(与信号选择区域组名同款):朝下 = 展开、朝右 = 收起。
if (rowIndex < layout.rowNameCloses.size() && const bool triangleHot =
layout.rowNameCloses[rowIndex].Width > 0.0f) (m_freeMarkerTriangleHoverLane == static_cast<int>(index) &&
m_freeMarkerTriangleHoverSignal == group.ref);
UiDrawTriangleIcon(g, group.triangle,
triangleHot ? m_theme->accentSoft : ink.text,
group.collapsed ? IconDirection::RIGHT
: IconDirection::DOWN);
// 右侧:`N 标记点` + ✕(清除该信号全部固定点)。
std::size_t markerCount = 0;
for (const Lane::FreeMarkerItem& entry : tabLane.freeMarkerItems)
{ {
const gd::RectF& closeRect = layout.rowNameCloses[rowIndex]; if (entry.ref == group.ref)
const bool hot = (m_freeMarkerPanelHoverLane == static_cast<int>(index) && {
m_freeMarkerPanelHoverSignal == ref && ++markerCount;
m_freeMarkerPanelHoverItem == static_cast<std::size_t>(-1)); }
UiFillRoundRect(g, closeRect, 3.0f * m_scale,
hot ? WithAlpha(m_theme->danger, 0x66) : WithAlpha(signalColor, 0xC0));
const float crossPad = 3.5f * m_scale;
const std::uint32_t crossColor = hot ? m_theme->danger : m_theme->textOnAccent;
UiDrawLine(g, closeRect.X + crossPad, closeRect.Y + crossPad,
closeRect.X + closeRect.Width - crossPad,
closeRect.Y + closeRect.Height - crossPad, crossColor,
1.4f * m_scale);
UiDrawLine(g, closeRect.X + closeRect.Width - crossPad,
closeRect.Y + crossPad, closeRect.X + crossPad,
closeRect.Y + closeRect.Height - crossPad, crossColor,
1.4f * m_scale);
} }
const std::wstring countText =
std::to_wstring(markerCount) + L" 标记点";
const float countWidth =
UiMeasureTextWidth(m_fonts->monoSmall, countText) + 2.0f * m_scale;
const float countRight = group.close.X - 5.0f * m_scale;
UiText(g, countText, m_fonts->monoSmall,
gd::RectF(countRight - countWidth, group.header.Y, countWidth,
group.header.Height),
ink.text, TextAlign::RIGHT);
const float nameLeft = group.triangle.X + group.triangle.Width + 6.0f * m_scale;
const float nameWidth =
std::max(10.0f, countRight - countWidth - 6.0f * m_scale - nameLeft);
UiText(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall, nameWidth),
m_fonts->uiSmall,
gd::RectF(nameLeft, group.header.Y, nameWidth, group.header.Height),
ink.text, TextAlign::LEFT);
const bool closeHot = (m_freeMarkerPanelHoverLane == static_cast<int>(index) &&
m_freeMarkerPanelHoverSignal == group.ref &&
m_freeMarkerPanelHoverItem == static_cast<std::size_t>(-1));
if (closeHot)
{
UiFillRoundRect(g, group.close, 3.0f * m_scale,
WithAlpha(m_theme->danger, 0x77));
}
const float crossPad = 3.5f * m_scale;
const std::uint32_t crossColor = closeHot ? m_theme->danger : ink.text;
UiDrawLine(g, group.close.X + crossPad, group.close.Y + crossPad,
group.close.X + group.close.Width - crossPad,
group.close.Y + group.close.Height - crossPad, crossColor,
1.5f * m_scale);
UiDrawLine(g, group.close.X + group.close.Width - crossPad,
group.close.Y + crossPad, group.close.X + crossPad,
group.close.Y + group.close.Height - crossPad, crossColor,
1.5f * m_scale);
} }
// 固定点行(已按信号分组 + 组内时间升序展开)。 // ---- 固定点条目(五列,列起点由布局给定 ⇒ 纵向严格对齐)----
for (const EdgePanelLayout::MarkerRow& markerRow : layout.markerRows) for (const EdgePanelLayout::MarkerRow& markerRow : layout.markerRows)
{ {
const Lane::FreeMarkerItem& item = tabLane.freeMarkerItems[markerRow.itemIndex]; const Lane::FreeMarkerItem& item = tabLane.freeMarkerItems[markerRow.itemIndex];
@@ -7426,48 +7770,43 @@ namespace jdi
FreeMarkerDelta(tabLane, markerRow.itemIndex, value, hasPrev, delta, FreeMarkerDelta(tabLane, markerRow.itemIndex, value, hasPrev, delta,
timeDelta); timeDelta);
} }
const std::wstring* enumText = const std::wstring* enumText = resolved.data->enumText.Find(enumValue);
resolved.data->enumText.Find(enumValue);
const std::wstring valueText = const std::wstring valueText =
FreeMarkerValueText(valueOk ? value : 0.0, hasPrev, delta, enumText); valueOk ? FormatValue(value) : std::wstring(kFreeMarkerNotAvailable);
const std::wstring deltaText =
(valueOk && hasPrev) ? FormatSignedValue(delta)
: std::wstring(kFreeMarkerNotAvailable);
const std::wstring timeText = FreeMarkerTimeText(item.time); const std::wstring timeText = FreeMarkerTimeText(item.time);
const std::wstring deltaText = FreeMarkerDeltaText(hasPrev, timeDelta); const std::wstring timeDeltaText =
FreeMarkerDeltaText(valueOk && hasPrev, timeDelta);
// 四列(① 取值 ② 增量 ③ 时刻 ④ 间隔)各列**左对齐在固定横坐标**上,
// 于是同一信息在所有条目上纵向上严格对齐(用户明确要求);
// 列宽固定,超长文本用 `UiEllipsize` 截断(布局期不做文本测量)。
const auto columnRect = [&](EdgePanelLayout::MarkerColumnIndex column) { const auto columnRect = [&](EdgePanelLayout::MarkerColumnIndex column) {
return gd::RectF(layout.markerColumns[column].left, markerRow.rect.Y, return gd::RectF(layout.markerColumns[column].left, markerRow.rect.Y,
layout.markerColumns[column].width, layout.markerColumns[column].width,
markerRow.rect.Height); markerRow.rect.Height);
}; };
UiText(g, UiEllipsize(valueText, m_fonts->monoSmall, const auto paintCell = [&](EdgePanelLayout::MarkerColumnIndex column,
layout.markerColumns[EdgePanelLayout::MARKER_COL_VALUE].width), const std::wstring& text, std::uint32_t color) {
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_VALUE), if (layout.markerColumns[column].width <= 0.0f || text.empty())
m_theme->text, TextAlign::LEFT); {
if (hasPrev) return;
}
const gd::RectF rect = columnRect(column);
UiText(g, UiEllipsize(text, m_fonts->monoSmall, rect.Width),
m_fonts->monoSmall, rect, color, TextAlign::LEFT);
};
paintCell(EdgePanelLayout::MARKER_COL_VALUE, valueText, m_theme->textStrong);
if (layout.markerHasEnumColumn)
{ {
UiText(g, paintCell(EdgePanelLayout::MARKER_COL_ENUM,
UiEllipsize(FormatSignedValue(delta), m_fonts->monoSmall, (enumText != nullptr) ? *enumText : std::wstring(),
layout.markerColumns[EdgePanelLayout::MARKER_COL_DELTA].width), m_theme->text);
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_DELTA),
m_theme->textMuted, TextAlign::LEFT);
}
UiText(g,
UiEllipsize(timeText, m_fonts->monoSmall,
layout.markerColumns[EdgePanelLayout::MARKER_COL_TIME].width),
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_TIME),
m_theme->textMuted, TextAlign::LEFT);
if (!deltaText.empty())
{
UiText(g,
UiEllipsize(deltaText, m_fonts->monoSmall,
layout.markerColumns[EdgePanelLayout::MARKER_COL_DT].width),
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_DT),
m_theme->textMuted, TextAlign::LEFT);
} }
paintCell(EdgePanelLayout::MARKER_COL_DELTA, deltaText, m_theme->textMuted);
paintCell(EdgePanelLayout::MARKER_COL_TIME, timeText, m_theme->textMuted);
paintCell(EdgePanelLayout::MARKER_COL_DT, timeDeltaText, m_theme->textMuted);
// 行尾 ✕:移除该固定点。 // 行尾 ✕:移除该固定点(与之前一致)。
const gd::RectF& closeRect = markerRow.close; const gd::RectF& closeRect = markerRow.close;
const bool hot = (m_freeMarkerPanelHoverLane == static_cast<int>(index) && const bool hot = (m_freeMarkerPanelHoverLane == static_cast<int>(index) &&
m_freeMarkerPanelHoverItem == markerRow.itemIndex); m_freeMarkerPanelHoverItem == markerRow.itemIndex);
@@ -7484,6 +7823,39 @@ namespace jdi
closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad, closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad,
crossColor, 1.2f * m_scale); crossColor, 1.2f * m_scale);
} }
g.Restore(contentState);
// ---- 内容滚动条(只在内容超高时出现;按钮与滑块不参与滚动)----
if (layout.scrollTrack.Width > 0.0f)
{
UiFillRoundRect(g, layout.scrollTrack, layout.scrollTrack.Width * 0.5f,
WithAlpha(m_theme->border, 0x88));
UiFillRoundRect(g, layout.scrollThumb, layout.scrollThumb.Width * 0.5f,
m_theme->borderStrong);
}
// ---- 「标记透明度」滑动条(仅「光标选取模式」激活时出现,固定在按钮上方)----
if (layout.opacityTrack.Width > 0.0f)
{
UiText(g, kFreeMarkerOpacityLabel, m_fonts->uiSmall,
gd::RectF(panel.X + pad, layout.opacityValue.Y,
layout.opacityTrack.X - panel.X - pad, layout.opacityValue.Height),
m_theme->textMuted, TextAlign::LEFT);
const bool sliderActive =
(m_dragMode == PlotDragMode::MARKER_OPACITY &&
m_markerOpacityDragLane == static_cast<int>(index)) ||
UiPointInRect(layout.opacityTrack, static_cast<float>(m_mouseX),
static_cast<float>(m_mouseY));
// 外观与信号样式区的「透明度」滑动条完全相同(同一 `UiDrawSlider` 口径)。
UiDrawSlider(g, layout.opacityTrack,
static_cast<double>(tabLane.freeMarkerOpacity), m_theme->accent,
sliderActive, m_scale);
const int percent =
static_cast<int>(tabLane.freeMarkerOpacity * 100.0f + 0.5f);
UiText(g, std::to_wstring(percent) + L"%", m_fonts->monoSmall,
layout.opacityValue, m_theme->textStrong, TextAlign::RIGHT);
}
} }
float rowY = panel.Y + titleHeight + pad; float rowY = panel.Y + titleHeight + pad;
@@ -7520,7 +7892,8 @@ namespace jdi
} }
// 逐条目面板底部按钮行:「构建为新信号」(悬停高亮); // 逐条目面板底部按钮行:「构建为新信号」(悬停高亮);
// 「自由标记」面板的按钮是「启用光标模式 / 关闭光标模式」的切换按钮。 // 「自由标记」面板的按钮是「光标选取模式」的**切换类按钮**
//(交互外观与工具栏的切换按钮一致:激活时浅强调色底 + 强调描边 + 强文本色)。
if (hasButton || freeMarker) if (hasButton || freeMarker)
{ {
const gd::RectF& buttonRect = layout.button; const gd::RectF& buttonRect = layout.button;
@@ -7532,16 +7905,21 @@ namespace jdi
(m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) && (m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) &&
UiPointInRect(inner, static_cast<float>(m_mouseX), UiPointInRect(inner, static_cast<float>(m_mouseX),
static_cast<float>(m_mouseY)); static_cast<float>(m_mouseY));
UiFillRoundRect(g, inner, 4.0f * m_scale, const bool checked = freeMarker && tabLane.freeMarkerCursor;
hot ? WithAlpha(m_theme->accent, 0x3A) : m_theme->cardBg); const std::uint32_t background =
UiDrawRoundRect(g, inner, 4.0f * m_scale, m_theme->accent, 1.2f); checked ? (hot ? MixColor(m_theme->accentSoft, m_theme->accent, 0.16)
const std::wstring label = : m_theme->accentSoft)
freeMarker ? (tabLane.freeMarkerCursor ? std::wstring(kFreeMarkerCursorOffLabel) : (hot ? m_theme->hover : m_theme->cardBg);
: std::wstring(kFreeMarkerCursorOnLabel)) UiFillRoundRect(g, inner, 4.0f * m_scale, background);
: std::wstring(kBuildSignalLabel); UiDrawRoundRect(g, inner, 4.0f * m_scale,
UiText(g, label, m_fonts->uiSmall, inner, m_theme->accent, TextAlign::CENTER); checked ? m_theme->accent : m_theme->border, 1.2f);
const std::wstring label = freeMarker ? std::wstring(kFreeMarkerCursorModeLabel)
: std::wstring(kBuildSignalLabel);
UiText(g, label, m_fonts->uiSmall, inner,
checked ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
} }
} }
} // if (!layout.collapsed)
// 面板画完了:把它刚画进后备缓冲区的像素(连同缓存键)存进面板图层, // 面板画完了:把它刚画进后备缓冲区的像素(连同缓存键)存进面板图层,
// 下一帧只要键不变就直接贴图。矩形外的 4 逻辑像素边距用于容纳投影。 // 下一帧只要键不变就直接贴图。矩形外的 4 逻辑像素边距用于容纳投影。
+133
View File
@@ -3011,6 +3011,10 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
| 2026-10-20 | DeepSeek-V4.1-Flash(**本机**) | **新增实验性功能「自由标记」(网格内跟随鼠标的纵向光标 + 单击固定数据点 + 面板)**(详见 §12 末尾「2026-10-20 第七轮」与 §11.2 第 181–188 条)。① **数据层**:`Lane` 新增 `freeMarker` 开关、`FreeMarkerItem{ref, time}` 列表(`time` 存**显示时刻**,与光标同一口径)与 `freeMarkerPanel`;新增 `AddFreeMarker/RemoveFreeMarker/RemoveFreeMarkerAt/RemoveFreeMarkersOf/FindFreeMarker/HasFreeMarkers`(同一信号同一时刻不重复加入,容差 1e-12 相对量);`LaneCarry` 一并携带(重建网格不丢)。② **网格内交互**:`ProbeFreeMarker` 在「开启该功能的网格」内挑**离鼠标最近**的那个数据点(`MinorCellSparse` 成立时经 `SnapTimeToSample` 吸附到采样时刻并画圆环提示);`PaintFreeMarker` 画**仅限本网格**的纵向光标(上把手尖端朝下、下把手尖端朝上,**不显示时间读数**)+ 就近取值胶囊;已固定点画细线 + 圆点 + `值(±增量) = 枚举` 胶囊 + 行尾 ✕(悬停高亮 + 提示气泡)。**超出视界直接不显示**(不像常规数据点那样贴边定格 + 三角)。③ **单击 vs 框选**:`OnLButtonDown` 只登记 `m_freeMarkerPendingFix`,`OnLButtonUp` 里**原地未拖动**才真正 `AddFreeMarker`;选区放大模式下「按下后拖动」照旧进入框选并缩放。④ **面板**:新 `EdgePanelKind::MARK`(`PLOT_CMD_LANE_FREE_MARKER = 12`),按「网格内信号顺序 → 该信号内时间升序」展开;每行 `值(±增量) = 枚举` + 两列**右对齐**的 `t:<时刻>s` / `Δt:+<复合单位>`;信号名右侧 ✕ 清除该信号全部固定点、行尾 ✕ 移除单个;双击行编辑「自身时刻」或「相对前一固定点的间隔」(秒),提交后 `SortedFreeMarkerIndices` 自动重排(越过后续点即交换条目位置)。⑤ **两处修正**:`EdgePanelWidth` 的 MARK 分支**不做采样查询**(它被 `TopPanelAtPoint`/`SyncPanelOrder` 高频调用,逐行取值会拖慢鼠标),改用固定宽度 `kFreeMarkerPanelWidth` + 绘制侧省略号;`PaintFreeMarker` 与跟随光标共用 `PaintFreeMarkerCapsule`(避免两份重复绘制)。验证:新增离屏 `.preflight/markcheck` **31/31**(探测/就近选择/单击固定/三处✕/增量与时间列对齐/选区放大共存/吸附/越界不显示/间隔编辑后重排);回归 `check` 136/137(唯一失败项为既有不稳定项,基线同样失败)、`editcheck` 20/20、`round3check` 31/31、`round4check` 34/34、`round5check` 5/5、`convcheck` 18/18,真实 MF4 仍 283/8/18/24;`make release`/`make debug` 零警告;`TargetCheck.exe` **833,536 字节(814.0 KiB)** —— 比上一轮 **+24 KB**,**首次超出 800 KiB 的旧红线约 14 KB**。 | | 2026-10-20 | DeepSeek-V4.1-Flash(**本机**) | **新增实验性功能「自由标记」(网格内跟随鼠标的纵向光标 + 单击固定数据点 + 面板)**(详见 §12 末尾「2026-10-20 第七轮」与 §11.2 第 181–188 条)。① **数据层**:`Lane` 新增 `freeMarker` 开关、`FreeMarkerItem{ref, time}` 列表(`time` 存**显示时刻**,与光标同一口径)与 `freeMarkerPanel`;新增 `AddFreeMarker/RemoveFreeMarker/RemoveFreeMarkerAt/RemoveFreeMarkersOf/FindFreeMarker/HasFreeMarkers`(同一信号同一时刻不重复加入,容差 1e-12 相对量);`LaneCarry` 一并携带(重建网格不丢)。② **网格内交互**:`ProbeFreeMarker` 在「开启该功能的网格」内挑**离鼠标最近**的那个数据点(`MinorCellSparse` 成立时经 `SnapTimeToSample` 吸附到采样时刻并画圆环提示);`PaintFreeMarker` 画**仅限本网格**的纵向光标(上把手尖端朝下、下把手尖端朝上,**不显示时间读数**)+ 就近取值胶囊;已固定点画细线 + 圆点 + `值(±增量) = 枚举` 胶囊 + 行尾 ✕(悬停高亮 + 提示气泡)。**超出视界直接不显示**(不像常规数据点那样贴边定格 + 三角)。③ **单击 vs 框选**:`OnLButtonDown` 只登记 `m_freeMarkerPendingFix`,`OnLButtonUp` 里**原地未拖动**才真正 `AddFreeMarker`;选区放大模式下「按下后拖动」照旧进入框选并缩放。④ **面板**:新 `EdgePanelKind::MARK`(`PLOT_CMD_LANE_FREE_MARKER = 12`),按「网格内信号顺序 → 该信号内时间升序」展开;每行 `值(±增量) = 枚举` + 两列**右对齐**的 `t:<时刻>s` / `Δt:+<复合单位>`;信号名右侧 ✕ 清除该信号全部固定点、行尾 ✕ 移除单个;双击行编辑「自身时刻」或「相对前一固定点的间隔」(秒),提交后 `SortedFreeMarkerIndices` 自动重排(越过后续点即交换条目位置)。⑤ **两处修正**:`EdgePanelWidth` 的 MARK 分支**不做采样查询**(它被 `TopPanelAtPoint`/`SyncPanelOrder` 高频调用,逐行取值会拖慢鼠标),改用固定宽度 `kFreeMarkerPanelWidth` + 绘制侧省略号;`PaintFreeMarker` 与跟随光标共用 `PaintFreeMarkerCapsule`(避免两份重复绘制)。验证:新增离屏 `.preflight/markcheck` **31/31**(探测/就近选择/单击固定/三处✕/增量与时间列对齐/选区放大共存/吸附/越界不显示/间隔编辑后重排);回归 `check` 136/137(唯一失败项为既有不稳定项,基线同样失败)、`editcheck` 20/20、`round3check` 31/31、`round4check` 34/34、`round5check` 5/5、`convcheck` 18/18,真实 MF4 仍 283/8/18/24;`make release`/`make debug` 零警告;`TargetCheck.exe` **833,536 字节(814.0 KiB)** —— 比上一轮 **+24 KB**,**首次超出 800 KiB 的旧红线约 14 KB**。 |
| 2026-10-21 | DeepSeek-V4.1-Flash(**本机**) | **快进同步远端 5 个提交(`181f95c → 067128f`,35 文件 +3664/−313;未改动任何源码)**:`b315201` MF4 换算 2/3/4/5/6 + `##CA` + 数值 VLSD;`30f853a` `##CA` 全字段/3 维命名 + `cc_type = 8` 区间枚举 + 包络带线宽双向加粗;`53b981a` 全项目大检查(数据层/UI 真 BUG、死代码清理、体积评估);`dd8f63a` 新增实验性「自由标记」;`067128f` 自由标记改进(默认无光标 + 面板四列对齐 + 光标模式切换按钮)。本机验证:`make release TARGET_FILE_NAME=TargetCheck` **零警告**、产物 **857,600 字节**(GCC 16.1 seh 比起源设备 GCC 15.2 UCRT 略大);`.preflight/perf/datacheck` **69/69**,`MF4-Record.MF4` 仍 3 条时间轴 / **283** 信号(与远端记录一致)。 |
| 2026-10-21 | DeepSeek-V4.1-Flash(**本机**) | **「自由标记」体验重做 + 全部实验性面板可折起(详见 §12 末尾「2026-10-21 第二轮」与 §11.2 第 194–201 条)**。① **网格内固定点**:取值胶囊互相重叠时做**纵向避让**;✕ 改为**与胶囊同高的正方形**;**删除纵向半透明竖线**;新增「标记透明度」(0–100%,作用于本网格全部固定点:圆点 / 胶囊 / 文字 / ✕);⚠️ **GDI `DrawTextW` 忽略 alpha**,因此半透明时文字改走 **GDI+ `DrawString`**(不透明度 100% 时仍走 `UiText`,像素零变化),实测 0% 时胶囊区域与“无标记”帧逐像素相同(1433→**0**)。② **面板布局重做**:每个有固定点的信号一个**信号色托底分组栏**(三角 + 名称 + `N 标记点` + ✕,文字/三角/✕ 一律用 `UiContrastInk` 自适应对比色,点击分组栏收起该组);条目改成**五列**(取值 / 枚举 / 增量 / 时刻 / 间隔),首项增列与Δt列显示 `N/A`,**整个面板没有枚举文本时枚举列整列缺省**。③ **内容滚动**:内容超高时右侧出现滚动条(分组栏与条目一起滚动),滚轮在内容区翻页;底部按钮与透明度滑块**不参与滚动**。④ **面板折起**:`LanePanelState` 新增 `collapsed`,标题栏 GX 徽标左侧新增「三角 + 横线」指示器(`kEdgePanelDisclosureWidth`),单击折起/展开、双击标题栏切换;折起后只剩标题栏(五类面板全部支持)。验证:新增离屏 `.preflight/perf/mark2check.exe`(**69/69**,含 A/B:恢复竖线 ⇒ 1 项失败;禁用避让 ⇒ 2 项失败);回归 `datacheck` 69/69、`verify` 49/49、`panelanim` 158/158、`errcheck` 19/19;`make release`/`make debug` 零警告;`TargetCheck.exe` 866,816 字节。 |
## 11. 跨设备协作须知(起源设备视角) ## 11. 跨设备协作须知(起源设备视角)
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。 > **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
@@ -3919,6 +3923,31 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
信号发名称行(`SortedFreeMarkerIndices` 为空就 `continue`),高度分支与矩形分支必须同步 信号发名称行(`SortedFreeMarkerIndices` 为空就 `continue`),高度分支与矩形分支必须同步
跳过,否则面板会多出空白行。 跳过,否则面板会多出空白行。
194. ⚠️⚠️ **GDI 文本(`UiText` → `DrawTextW`)完全不认 alpha**:`SetTextColor` 只接受 RGB,
把 alpha 乘进颜色**不会**让文字淡出(实测 0% 时文字仍满不透明,胶囊区域像素差 300+)。
需要"文字随不透明度淡出"时只能改用 **GDI+ `DrawString`**(见 `DrawFreeMarkerAlphaText`:
由 HFONT 取 `LOGFONTW` 构造 `gd::Font(hdc, &lf)` —— ⚠️ **该构造函数没有 `Unit` 参数**)。
⚠️ 只在"确实需要 alpha"的分支改走 GDI+,100% 时继续用 `UiText`,否则整片文字的光栅化都会变。
195. ⚠️⚠️ **看不见的元素不得可点**:网格内固定点在 `freeMarkerOpacity <= 2%` 时整段不绘制,
因此也**不登记** ✕ 命中矩形(`m_freeMarkerHits`),否则用户会"隔着空气"删掉固定点。
196. ⚠️ **面板内容区必须裁剪、且只登记"与内容区相交"的行**:否则滚出视界的条目的 ✕ / 行矩形
会落在底部按钮与透明度滑块的位置上抢走点击。交互侧另有
`UiPointInRect(layout.content, …)` 兜底。
197. ⚠️⚠️ **`LanePanelState::open` 与 `collapsed` 是两件事**:`open == false` = 功能没开(面板不存在);
`collapsed == true` = 功能开着但只显示标题栏。`EdgePanelLayoutFor` 折起时仍要算完
`title / close / disclosure / badge` 再 return,否则三角指示器与 ✕ 会"消失"。
198. ⚠️ **面板的折起状态必须一起继承**:`LaneCarry`(`RebuildLanes`)与 `SplitLane` 都要拷贝
`*.collapsed`,否则勾选信号重建网格后面板会自己弹回展开态。
199. ⚠️ **`markerColumns` 是 5 列**(值 / 枚举 / 增量 / 时刻 / 间隔)。枚举列缺省时**列宽记 0**
并从排版里跳过(后面的列左移);判断"要不要这一列"只允许查 `SignalEnumText::Empty()`
(O(1)),**绝不能**逐条采样(布局函数在 `TopPanelAtPoint` / `SyncPanelOrder` 里被高频调用)。
200. ⚠️ **面板内容宽度必须给滚动条让位**:`innerWidth = 宽 − 2×pad − (可滚动 ? scrollW + 4 : 0)`,
且五列总宽 + 行 ✕ 必须 ≤ `innerWidth`(当前列宽 `{74,80,60,72,78}` + 4×2 间隙 + 13 ✕ = 391 ≤ 399)。
201. ⚠️ **MARK 面板的滚动偏移在布局里 clamp、不回写状态**(回写会让面板像素缓存键反复抖动);
滚轮与拖滚动条这两条写入路径必须自己按 `markerContentHeight − content.Height` 夹取。
202. ⚠️ **固定点取值胶囊的重叠避让只在绘制侧做**(`PaintFreeMarker` 内按已放置矩形纵向错开),
命中矩形(`m_freeMarkerHits`)由绘制时登记 ⇒ 自动跟随避让后的位置,**不要**在命中侧重算。
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用) ### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)
- **不要占用用户的鼠标 / 键盘 / 屏幕**:优先 `.preflight/uitest`(离屏 harness:`SW_HIDE` 窗口 + 直接注入消息 + `BitBlt` 从 `m_backDc` 截图)或 `.preflight/perf`(离屏计时 / 逐像素对比)。 - **不要占用用户的鼠标 / 键盘 / 屏幕**:优先 `.preflight/uitest`(离屏 harness:`SW_HIDE` 窗口 + 直接注入消息 + `BitBlt` 从 `m_backDc` 截图)或 `.preflight/perf`(离屏计时 / 逐像素对比)。
@@ -7293,6 +7322,110 @@ DG3 data=0 cycle=0 (空组,一个字符串通道)
真实 MF4 仍 **283** 条信号。 真实 MF4 仍 **283** 条信号。
- `make release` / `make debug` **零警告**;`Build/Release/Target.exe` **834,560 字节(815.0 KiB)**。 - `make release` / `make debug` **零警告**;`Build/Release/Target.exe` **834,560 字节(815.0 KiB)**。
---
## 本轮(2026-10-21 第二轮,本机):「自由标记」体验重做 + 全部实验性面板可折起
> 用户需求(逐条):
> ① 网格内固定点的**取值显示互相重叠遮挡** ⇒ 加避让;取值后面的 ✕ 改成**与取值同高的正方形**;
> 固定点**不再显示纵向半透明竖线**;
> ② 面板底部的按钮改为只显示「**光标选取模式**」,交互外观与工具栏的切换类按钮一致(激活高亮),
> 激活时在按钮**上方**出现「**标记透明度**」滑动条(外观与信号样式区的「透明度」同款),
> 它控制本网格内**所有固定点显示内容**的透明度;
> ③ 面板内容重排:与信号列表的「组 → 信号」类似 —— 每个有固定点的信号一个**信号色托底分组栏**
> (左:展开/收起三角 + 信号名;右:`N 标记点` + ✕;文字/三角/✕ 都用**自适应对比色**),
> 点击分组栏展开/收起;条目五列:**当前值 / 枚举文本 / 增量 / 标记点时刻 / 相对前一点的时间增量**,
> 列之间纵向对齐、互不重叠;整个面板都没有枚举文本时**缺省枚举列**;首项增列与 Δt 列显示 `N/A`;
> 每行末尾仍有 ✕;
> ④ 内容过多时面板要有**上下滚动条**(分组栏 + 条目一起滚动),底部按钮与滑动条**不参与滚动**;
> ⑤ **所有**实验性功能浮动面板都支持展开/折起:GX 徽标左边加**三角 + 横线**指示器,
> 单击折起/展开,收起的面板只有标题栏,双击标题栏也可展开。
### A. 数据层(`Utilities`)
- `LanePanelState` 新增 `bool collapsed = false`(`open` 仍然表示“功能是否显示”,二者语义不同)。
- `Lane` 新增:`float freeMarkerOpacity = 1.0f`(网格内固定点显示不透明度)、
`float freeMarkerScroll = 0.0f`(面板内容滚动偏移)、
`std::vector<SignalRef> freeMarkerCollapsedGroups`(面板里被收起的信号分组;**不在列表里 = 展开**)
+ `FreeMarkerGroupCollapsed()` / `ToggleFreeMarkerGroupCollapsed()`。
- `LaneCarry`(`RebuildLanes`)与 `TabConfig::SplitLane` 这两处“网格重建/拆分时的状态继承”都带上新状态,
并对五个面板都补了 `*.collapsed` 的拷贝。
### B. 网格内固定点
- **标签避让**:`PaintFreeMarker` 逐条记录已放置的胶囊外接矩形;新胶囊与它们相交时按
`+1、-1、+2、-2 …` 步长(步长 = 胶囊高 + 2px)**纵向错开**,并夹在网格可视范围(`clipTop..clipBottom`)内。
- **✕ 正方形**:`FreeMarkerLabelRects` 里 `closeSize = textHeight`(胶囊高度),宽高相等。
- **删除纵向竖线**:固定点不再画那条 `WithAlpha(signalColor, 0x70)` 的竖线(跟随光标模式的光标线仍在)。
- **标记透明度**:圆点、胶囊底色、描边、文字、✕ 都乘 `lane.freeMarkerOpacity`;
⚠️ **GDI `DrawTextW` 忽略 alpha**(`SetTextColor` 只吃 RGB)⇒ 半透明时文字改由
`DrawFreeMarkerAlphaText()`(GDI+ `DrawString` + alpha brush)绘制,100% 时仍走 `UiText`
(像素零变化)。不透明度 ≤ 2% 时**整个胶囊什么都不画**,且**不登记 ✕ 命中矩形**
(看不见的按钮不应可点)。
### C. 面板布局(`EdgePanelLayout` / `EdgePanelLayoutFor`)
- 新增字段:`disclosure`(标题栏左侧三角指示器,宽 `kEdgePanelDisclosureWidth = 20`)、
`collapsed`、`content`/`scrollTrack`/`scrollThumb`/`markerContentHeight`、
`opacityTrack`/`opacityValue`、`markerGroups`(`MarkerGroup{ref, header, triangle, close, collapsed}`)、
`markerHasEnumColumn`;`markerColumns` 由 4 列扩到 **5 列**
(`MARKER_COL_VALUE/ENUM/DELTA/TIME/DT`)。
- `GX` 徽标右移一个 `disclosure` 宽度;**折起**时 `height = titleHeight` 并在算完
`title/close/disclosure/badge` 后直接 return(内容区/按钮/滚动条都是空矩形)。
- MARK:内容总高 = Σ(分组栏 + 未收起组的条目),`viewHeight = min(总高, 内容上限)`;
内容上限 = `min(kFreeMarkerContentMaxHeight=260, 绘图区高 − 标题 − 3×pad − 按钮 − 滑块行)`。
超高 ⇒ `scrollTrack` 宽 `kFreeMarkerScrollWidth=7`、滑块高度按内容比例、位置按滚动比例;
同时把内容宽度收窄 `scrollW + 4px`,保证五列 + 行 ✕ 不会与滚动条重叠
(列宽因此调整为 `{74, 80, 60, 72, 78}`,面板宽 `424`)。
- 只登记**与内容区相交**的分组栏与条目矩形 ⇒ 滚出视界的条目不会抢走底部按钮/滑块的点击
(交互侧再用 `UiPointInRect(layout.content, …)` 兜一层)。
- 枚举列:只查“这些信号是否带枚举表”(`SignalEnumText::Empty()`,O(1)),
**不逐条采样**(布局函数被 `TopPanelAtPoint`/`SyncPanelOrder` 高频调用)。
### D. 绘制
- 标题栏:先画「三角(朝下 = 展开 / 朝右 = 折起)+ 三角右侧一条短横线」,再画 GX 徽标与标题。
- MARK 内容区:`g.SetClip(content, Intersect)` 后绘制;分组栏用 `WithAlpha(signalColor, 0xE8)` 托底,
三角 / 名称 / `N 标记点` / ✕ 全部用 `UiContrastInk(signalColor)` 的自适应对比色;
条目五列左对齐在**布局给定的列起点**上(超长 `UiEllipsize`),
`值 | (枚举) | ±增量 or N/A | <时刻>s | Δt:+… or N/A`。
- 底部:按钮改为**切换类按钮**外观(激活 = `accentSoft` 底 + `accent` 描边 + `textStrong`),文案固定
「光标选取模式」;激活时其上方是「标记透明度」行(左侧标签 + 轨道 + 右侧百分比,
与信号样式区的「透明度」行同款 `UiDrawSlider`)。
### E. 交互
- 新增拖拽模式 `MARKER_OPACITY`(拖滑块 → `SetLaneFreeMarkerOpacity`)与
`MARKER_SCROLL`(拖滚动条滑块 → `SetLaneFreeMarkerScroll`,含抓取偏移与轨道点击跳转)。
- `OnMouseWheel`:鼠标在 MARK **内容区**上时滚轮翻页面板(±40px/格),否则照旧滚网格;
底部按钮/滑块区域**不**翻页。
- `OnLButtonDown` 顺序:✕ → GX 徽标 → **disclosure** → 逐条目参数 → MARK 底部按钮 → 透明度滑块 →
滚动条 → 分组栏(✕ 清空该信号 / 其它位置切换收起)→ 行尾 ✕ → 标题栏拖动 → 面板主体消费。
- `OnDoubleClick`:标题栏(排除徽标/✕/三角)**双击切换折起**;MARK 条目仍按列边界编辑时刻/间隔。
- 悬浮:分组栏三角悬停高亮(`m_freeMarkerTriangleHoverLane/Signal`),✕ 悬停沿用
`m_freeMarkerPanelHover*`;光标形状:三角/分组栏/✕ = 手型,透明度滑块 = 左右箭头,滚动条 = 上下箭头。
### F. 验证
- 新增离屏 `.preflight/perf/mark2check.exe` **69/69**:
[A] 固定点:单点无竖线(与“无标记”帧逐像素对比,竖线列差异必须为 0)、✕ 正方形、
三点胶囊纵向避让(✕ 矩形互不相交且 Y 不同);
[B] 面板:一个分组栏(三角/✕ 在托底内)、三行条目、五列不重叠、枚举列缺省(宽 0);
[C] 分组收起后只留分组栏、行数归零、面板变矮,再次点击恢复;
[F] 五类面板折起后高度 == 标题栏高度且内容全空(含 COUNT 面板);
[D] 40 个固定点 ⇒ 内容超可视区、滚动条出现、滑块在轨道内;滚到最大时滑块贴底、
分组栏与条目一起移出、滚轮在内容区可翻页而在按钮区不翻页;
[E] 光标模式开关 ⇒ 滑块出现/消失、面板高度随之变化;点击轨道 25% ⇒ 不透明度 ≈25%;
不透明度 0% 与“无标记”帧逐像素相同(1433 → **0** 像素差异)。
[G] 额外导出 `mark2check_shot*.bmp`(人工目视检查用,不入库)。
- **A/B**:临时恢复固定点竖线 ⇒ 「no translucent vertical line」如实失败;
临时禁用避让(`attempt <= 0`)⇒ 「capsules do not overlap」如实失败。恢复后 69/69。
- 回归:`datacheck` 69/69、`verify` 49/49、`panelanim` 158/158、`errcheck` 19/19;
`make release` / `make debug` **零警告**;`TargetCheck.exe` **866,816 字节**。
⚠️ `verify.exe` 在打印完结果后**退出时**仍会 0xC0000005(`git stash` 回到本轮改动前的基线
同样复现 ⇒ 探针自身的既有问题,与本轮无关)。
+65 -2
View File
@@ -720,7 +720,15 @@ namespace jdi
*/ */
struct LanePanelState struct LanePanelState
{ {
bool open = true; /**< 面板是否展开(点击面板上的 ✕ 后置 false)。 */ bool open = true; /**< 功能面板是否显示(功能关闭时置 false)。 */
/**
* @brief 面板是否被**折起**(只显示标题栏)。
*
* 与 `open` 的区别:`open == false` 表示该功能本身没有开启(面板完全不存在);
* `collapsed == true` 表示功能仍开启、但内容被收起,只留标题栏(点击标题栏左侧的
* 三角指示器切换,双击标题栏也可展开)。
*/
bool collapsed = false;
double x = -1.0; /**< 面板左上角横坐标(客户区像素;<0 表示自动定位)。 */ double x = -1.0; /**< 面板左上角横坐标(客户区像素;<0 表示自动定位)。 */
double y = -1.0; /**< 面板左上角纵坐标(<0 表示自动定位)。 */ double y = -1.0; /**< 面板左上角纵坐标(<0 表示自动定位)。 */
}; };
@@ -976,8 +984,63 @@ namespace jdi
return false; return false;
} }
/** 自由标记浮动面板的 UI 状态(展开 + 位置)。 */ /** 自由标记浮动面板的 UI 状态(展开/折起 + 位置)。 */
LanePanelState freeMarkerPanel; LanePanelState freeMarkerPanel;
/**
* @brief 网格内固定数据点(纵向细线 / 数据点 / 取值胶囊 / 行尾 ✕)的显示不透明度。
*
* 0 = 完全透明(看不见),1 = 完全不透明(默认)。由浮动面板底部「光标选取模式」
* 激活时出现的「标记透明度」滑动条驱动;只影响**本网格**的固定点显示内容,
* 不影响面板自身的像素与鼠标交互。
*/
float freeMarkerOpacity = 1.0f;
/**
* @brief 「自由标记」面板内容的滚动偏移(客户区像素,0 = 顶部)。
*
* 面板内容(信号分组栏 + 固定点条目)超出可视高度时出现滚动条;
* 保持在这个范围之外的内容会被裁剪。面板底部的按钮与「标记透明度」
* 滑动条**不参与**滚动(它们固定在面板底部)。
*/
float freeMarkerScroll = 0.0f;
/**
* @brief 「自由标记」面板里被**收起**的信号分组(点击分组栏切换)。
*
* 只保存被收起的信号引用:不在列表里 = 展开(默认)。收起后该分组只显示
* 分组栏(三角朝右),下属的固定点条目不显示。
*/
std::vector<SignalRef> freeMarkerCollapsedGroups;
/** 某信号分组在「自由标记」面板里是否被收起。 */
bool FreeMarkerGroupCollapsed(const SignalRef& ref) const
{
for (const SignalRef& entry : freeMarkerCollapsedGroups)
{
if (entry == ref)
{
return true;
}
}
return false;
}
/** 切换某信号分组在「自由标记」面板里的收起状态,返回切换后的状态。 */
bool ToggleFreeMarkerGroupCollapsed(const SignalRef& ref)
{
for (std::size_t index = 0; index < freeMarkerCollapsedGroups.size(); ++index)
{
if (freeMarkerCollapsedGroups[index] == ref)
{
freeMarkerCollapsedGroups.erase(
freeMarkerCollapsedGroups.begin() + static_cast<std::ptrdiff_t>(index));
return false;
}
}
freeMarkerCollapsedGroups.push_back(ref);
return true;
}
}; };
/** /**
+14
View File
@@ -92,6 +92,9 @@ namespace jdi
bool freeMarkerCursor = false; bool freeMarkerCursor = false;
std::vector<Lane::FreeMarkerItem> freeMarkerItems; std::vector<Lane::FreeMarkerItem> freeMarkerItems;
LanePanelState freeMarkerPanel; LanePanelState freeMarkerPanel;
float freeMarkerOpacity = 1.0f;
float freeMarkerScroll = 0.0f;
std::vector<SignalRef> freeMarkerCollapsedGroups;
}; };
// 先记下每个信号原来所在网格的网格自身状态: // 先记下每个信号原来所在网格的网格自身状态:
@@ -133,6 +136,9 @@ namespace jdi
carry.freeMarkerCursor = lane.freeMarkerCursor; carry.freeMarkerCursor = lane.freeMarkerCursor;
carry.freeMarkerItems = lane.freeMarkerItems; carry.freeMarkerItems = lane.freeMarkerItems;
carry.freeMarkerPanel = lane.freeMarkerPanel; carry.freeMarkerPanel = lane.freeMarkerPanel;
carry.freeMarkerOpacity = lane.freeMarkerOpacity;
carry.freeMarkerScroll = lane.freeMarkerScroll;
carry.freeMarkerCollapsedGroups = lane.freeMarkerCollapsedGroups;
for (const SignalRef& ref : lane.signals) for (const SignalRef& ref : lane.signals)
{ {
previousCarry.emplace_back(ref, carry); previousCarry.emplace_back(ref, carry);
@@ -158,29 +164,37 @@ namespace jdi
lane.edgeValue = carry.edgeValue; lane.edgeValue = carry.edgeValue;
lane.edgeValueValid = carry.edgeValueValid; lane.edgeValueValid = carry.edgeValueValid;
lane.edgePanel.open = carry.edgePanel.open; lane.edgePanel.open = carry.edgePanel.open;
lane.edgePanel.collapsed = carry.edgePanel.collapsed;
lane.edgePanel.x = carry.edgePanel.x; lane.edgePanel.x = carry.edgePanel.x;
lane.edgePanel.y = carry.edgePanel.y; lane.edgePanel.y = carry.edgePanel.y;
lane.edgeCenter = carry.edgeCenter; lane.edgeCenter = carry.edgeCenter;
lane.edgeCenterItems = carry.edgeCenterItems; lane.edgeCenterItems = carry.edgeCenterItems;
lane.edgeCenterPanel.open = carry.edgeCenterPanel.open; lane.edgeCenterPanel.open = carry.edgeCenterPanel.open;
lane.edgeCenterPanel.collapsed = carry.edgeCenterPanel.collapsed;
lane.edgeCenterPanel.x = carry.edgeCenterPanel.x; lane.edgeCenterPanel.x = carry.edgeCenterPanel.x;
lane.edgeCenterPanel.y = carry.edgeCenterPanel.y; lane.edgeCenterPanel.y = carry.edgeCenterPanel.y;
lane.meanFilter = carry.meanFilter; lane.meanFilter = carry.meanFilter;
lane.meanFilterItems = carry.meanFilterItems; lane.meanFilterItems = carry.meanFilterItems;
lane.meanFilterPanel.open = carry.meanFilterPanel.open; lane.meanFilterPanel.open = carry.meanFilterPanel.open;
lane.meanFilterPanel.collapsed = carry.meanFilterPanel.collapsed;
lane.meanFilterPanel.x = carry.meanFilterPanel.x; lane.meanFilterPanel.x = carry.meanFilterPanel.x;
lane.meanFilterPanel.y = carry.meanFilterPanel.y; lane.meanFilterPanel.y = carry.meanFilterPanel.y;
lane.weakPeak = carry.weakPeak; lane.weakPeak = carry.weakPeak;
lane.weakPeakItems = carry.weakPeakItems; lane.weakPeakItems = carry.weakPeakItems;
lane.weakPeakPanel.open = carry.weakPeakPanel.open; lane.weakPeakPanel.open = carry.weakPeakPanel.open;
lane.weakPeakPanel.collapsed = carry.weakPeakPanel.collapsed;
lane.weakPeakPanel.x = carry.weakPeakPanel.x; lane.weakPeakPanel.x = carry.weakPeakPanel.x;
lane.weakPeakPanel.y = carry.weakPeakPanel.y; lane.weakPeakPanel.y = carry.weakPeakPanel.y;
lane.freeMarker = carry.freeMarker; lane.freeMarker = carry.freeMarker;
lane.freeMarkerCursor = carry.freeMarkerCursor; lane.freeMarkerCursor = carry.freeMarkerCursor;
lane.freeMarkerItems = carry.freeMarkerItems; lane.freeMarkerItems = carry.freeMarkerItems;
lane.freeMarkerPanel.open = carry.freeMarkerPanel.open; lane.freeMarkerPanel.open = carry.freeMarkerPanel.open;
lane.freeMarkerPanel.collapsed = carry.freeMarkerPanel.collapsed;
lane.freeMarkerPanel.x = carry.freeMarkerPanel.x; lane.freeMarkerPanel.x = carry.freeMarkerPanel.x;
lane.freeMarkerPanel.y = carry.freeMarkerPanel.y; lane.freeMarkerPanel.y = carry.freeMarkerPanel.y;
lane.freeMarkerOpacity = carry.freeMarkerOpacity;
lane.freeMarkerScroll = carry.freeMarkerScroll;
lane.freeMarkerCollapsedGroups = carry.freeMarkerCollapsedGroups;
}; };
// 收集全部可见信号引用。 // 收集全部可见信号引用。
+4
View File
@@ -430,18 +430,22 @@ namespace jdi
lane.edgeValue = source.edgeValue; lane.edgeValue = source.edgeValue;
lane.edgeValueValid = source.edgeValueValid; lane.edgeValueValid = source.edgeValueValid;
lane.edgePanel.open = source.edgePanel.open; lane.edgePanel.open = source.edgePanel.open;
lane.edgePanel.collapsed = source.edgePanel.collapsed;
lane.edgePanel.x = source.edgePanel.x; lane.edgePanel.x = source.edgePanel.x;
lane.edgePanel.y = source.edgePanel.y; lane.edgePanel.y = source.edgePanel.y;
lane.edgeCenter = source.edgeCenter; lane.edgeCenter = source.edgeCenter;
lane.edgeCenterPanel.open = source.edgeCenterPanel.open; lane.edgeCenterPanel.open = source.edgeCenterPanel.open;
lane.edgeCenterPanel.collapsed = source.edgeCenterPanel.collapsed;
lane.edgeCenterPanel.x = source.edgeCenterPanel.x; lane.edgeCenterPanel.x = source.edgeCenterPanel.x;
lane.edgeCenterPanel.y = source.edgeCenterPanel.y; lane.edgeCenterPanel.y = source.edgeCenterPanel.y;
lane.weakPeak = source.weakPeak; lane.weakPeak = source.weakPeak;
lane.weakPeakPanel.open = source.weakPeakPanel.open; lane.weakPeakPanel.open = source.weakPeakPanel.open;
lane.weakPeakPanel.collapsed = source.weakPeakPanel.collapsed;
lane.weakPeakPanel.x = source.weakPeakPanel.x; lane.weakPeakPanel.x = source.weakPeakPanel.x;
lane.weakPeakPanel.y = source.weakPeakPanel.y; lane.weakPeakPanel.y = source.weakPeakPanel.y;
lane.meanFilter = source.meanFilter; lane.meanFilter = source.meanFilter;
lane.meanFilterPanel.open = source.meanFilterPanel.open; lane.meanFilterPanel.open = source.meanFilterPanel.open;
lane.meanFilterPanel.collapsed = source.meanFilterPanel.collapsed;
lane.meanFilterPanel.x = source.meanFilterPanel.x; lane.meanFilterPanel.x = source.meanFilterPanel.x;
lane.meanFilterPanel.y = source.meanFilterPanel.y; lane.meanFilterPanel.y = source.meanFilterPanel.y;
// 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 弱峰调整条目。 // 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 弱峰调整条目。