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

- 网格内固定点:取值胶囊按已放置矩形做纵向避让(不再互相遮挡);行尾 ✕ 改为与
  胶囊同高的正方形;移除固定点的纵向半透明竖线
- 新增「标记透明度」(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

+225 -25
View File
@@ -941,6 +941,12 @@ namespace jdi
{
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)))
{
return LoadCursorW(nullptr, IDC_SIZEALL);
@@ -953,15 +959,27 @@ namespace jdi
// 「构建为新信号」按钮:手型光标作为可点击提示。
return LoadCursorW(nullptr, IDC_HAND);
}
// 「自由标记」面板:行尾 ✕ 与信号名右侧 ✕ 都是可点击控件(手型)。
// 「自由标记」面板:行尾 ✕ / 分组栏 ✕ 与三角都是可点击控件(手型),
// 滚动条与「标记透明度」滑块则是拖动类控件(上下 / 左右箭头)。
if (static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MARK)
{
// 底部「启用光标模式 / 关闭光标模式」切换按钮。
// 底部「光标选取模式」切换按钮。
if (layout.button.Width > 0.0f &&
UiPointInRect(layout.button, static_cast<float>(x), static_cast<float>(y)))
{
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)
{
if (row.close.Width > 0.0f &&
@@ -970,13 +988,18 @@ namespace jdi
return LoadCursorW(nullptr, IDC_HAND);
}
}
for (const gd::RectF& rect : layout.rowNameCloses)
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
{
if (rect.Width > 0.0f &&
UiPointInRect(rect, static_cast<float>(x), static_cast<float>(y)))
if (group.close.Width > 0.0f &&
UiPointInRect(group.close, static_cast<float>(x), static_cast<float>(y)))
{
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;
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:
default:
break;
@@ -1500,11 +1574,13 @@ namespace jdi
}
}
// 面板内 ✕ 的悬停(信号名 ✕ 与固定点行 ✕)。
// 面板内 ✕ 的悬停(分组栏 ✕ 与固定点行 ✕)与分组栏三角的悬停。
{
int hoverLane = -1;
std::size_t hoverItem = static_cast<std::size_t>(-1);
SignalRef hoverSignal;
int triLane = -1;
SignalRef triSignal;
int markLane = -1;
int markKind = 0;
if (m_tab != nullptr &&
@@ -1512,10 +1588,14 @@ namespace jdi
static_cast<EdgePanelKind>(markKind) == 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)
{
if (row.close.Width > 0.0f &&
if (insideContent && row.close.Width > 0.0f &&
UiPointInRect(row.close, static_cast<float>(x), static_cast<float>(y)))
{
hoverLane = markLane;
@@ -1525,18 +1605,22 @@ namespace jdi
}
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 (rect.Width > 0.0f &&
UiPointInRect(rect, static_cast<float>(x), static_cast<float>(y)) &&
rowIndex < layout.rowRefs.size())
if (insideContent && group.close.Width > 0.0f &&
UiPointInRect(group.close, static_cast<float>(x), static_cast<float>(y)))
{
hoverLane = markLane;
hoverItem = static_cast<std::size_t>(-1);
hoverSignal = layout.rowRefs[rowIndex];
hoverSignal = group.ref;
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;
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;
}
// 标题栏左侧的「展开 / 折起」三角指示器:单击切换面板的折起状态。
// 折起后面板只剩标题栏(再次点击三角、或双击标题栏可展开)。
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 ||
@@ -2154,37 +2254,80 @@ namespace jdi
}
}
// 「自由标记」面板:行尾 ✕ 移除该固定点;信号名右侧 ✕ 清除该信号的全部固定点。
// 「自由标记」面板:底部的切换按钮 / 「标记透明度」滑块 / 内容滚动条,
// 随后是分组栏(单击展开收起、右侧 ✕ 清空该信号)与固定点行 ✕。
// 与其它逐行 ✕ / 复位控件同一语义:**按下即生效**(不进拖拽、不穿透)。
if (kind == EdgePanelKind::MARK && m_tab != nullptr)
{
Lane& tabLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
// 底部切换按钮「启用光标模式 / 关闭光标模式」(默认关闭)。
// 底部切换按钮「光标选取模式」(默认关闭)。
if (layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy))
{
ToggleLaneFreeMarkerCursor(index);
m_dragMode = PlotDragMode::NONE;
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_freeMarkerPanelHoverLane = -1;
m_freeMarkerPanelHoverItem = static_cast<std::size_t>(-1);
m_freeMarkerPanelHoverSignal = SignalRef();
return true;
}
}
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())
if (insideContent && UiPointInRect(group.header, fx, fy))
{
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_freeMarkerPanelHoverLane = -1;
m_freeMarkerPanelHoverItem = static_cast<std::size_t>(-1);
@@ -2490,6 +2633,10 @@ namespace jdi
// 弱峰滑块拖拽结束:清掉目标(避免留下上一轮的信号引用)。
m_peakDragLane = -1;
m_peakDragRef = SignalRef();
// 「标记透明度」滑块 / 面板内容滚动条拖拽结束。
m_markerOpacityDragLane = -1;
m_markerScrollDragLane = -1;
m_markerScrollGrabOffset = 0.0f;
// 松开左键:粘滞提示结束计时,开始"停留后淡出"。
ReleaseHint();
return changed;
@@ -2504,6 +2651,37 @@ namespace jdi
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 时水平平移时间轴。
if (shift)
{
@@ -2542,6 +2720,27 @@ namespace jdi
int panelKind = 0;
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 &&
@@ -2553,6 +2752,7 @@ namespace jdi
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
{
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)) ||
row.itemIndex >= tabLane.freeMarkerItems.size())
{