fix(clip): 选区截取面板五处缺陷(条目/空白/多信号截取/滚轮)

一、未启用双光标时不再显示信号条目
   EdgePanelLayoutFor 与绘制侧都只判 clipPanel、没判双光标是否启用,
   于是照常累加内容高度、生成行矩形并绘制条目。
   - 高度算式新增 clipReady = clipPanel && CursorMode()==DOUBLE,未就绪时内容只占一行(提示);
   - 行几何循环加 clipRowsVisible,未就绪时不生成任何行矩形 => 绘制自然为空;
   - footer 同步改为"未就绪 = 0"。

二、条目区与底部按钮之间的大段空白(未启用双光标时也有)
   两个叠加根因:
   1) 高度算式的 maxContent 有"至少 3 行"下限 => 只有 1 个信号也硬撑 3 行高;
      改为 min(上限, max(1 行, budget))。
   2) 几何侧 viewHeight 多减了一个 pad(高度算式只含一个 pad + bottomPad);
      统一为 height - titleHeight - pad - footerHeight。
   高度算式与内容区几何必须严格同源。

三、多个信号「原始信号截取」结果不对
   同组信号共享同一条时间轴,ClipSignalRange 每次都裁剪 axis.time
   => 截完第一条后其它信号时间基准被改短 => 采样与时间错位。
   - 新增 DataSet::ForkAxisForSignal:轴非独占时复制一份新轴并让信号指向它;
   - ClipSignalRange 内再加一道守卫:轴仍被共享时直接返回 false(不改任何数据);
   - Application::ClipSignalInPlace 先 fork 再裁剪。

四、有滚动条时滚轮滚不到底
   1) OnMouseWheel 的 current 取值链漏了 CLIP => 读到 meanFilterScroll(恒 0)而写入 clipScroll,
      补上 CLIP 分支;
   2) PanelSlotKey 里 clipScroll 被写成布尔(>0 ? 1 : 0)=> 滚到 1px 后键不再变化,
      改为 mixFloat(lane.clipScroll) 真实值。

删 .o 全量重编译:零 error、零 warning,产物 861,184 B。
按用户要求未占用鼠标/键盘/屏幕,未运行 GUI 探针。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-03 16:39:18 +08:00
1 parent 62220477c0
commit 21a9865365
7 files changed
+199 -12

No files matched your search

@@ -1097,9 +1097,16 @@ namespace jdi
{
timeShift = style->offsetTime;
}
const bool ok = m_dataSet.ClipSignalRange(groupIndex, signalIndex, low - timeShift,
high - timeShift);
if (!ok)
// ⚠️⚠️ **必须先让该信号独占时间轴**再裁剪:同组信号常常共享同一条时间轴,
// 直接裁剪会把其它信号的时间基准一起改短 ⇒ 它们的采样与时间错位
//(用户 2026-11-03 报告:"多个信号同时截取时截取出来的信号并不对")。
std::size_t ownAxis = 0;
if (!m_dataSet.ForkAxisForSignal(groupIndex, signalIndex, ownAxis))
{
m_plot.ShowPersistentHint(L"选区截取失败:无法为该信号创建独立时间轴。");
return false;
}
if (!m_dataSet.ClipSignalRange(groupIndex, signalIndex, low - timeShift, high - timeShift))
{
m_plot.ShowPersistentHint(L"选区截取失败:区间内的有效采样点不足两个。");
return false;
@@ -3587,6 +3587,8 @@ namespace jdi
? wheelLane.edgeCountScroll
: (hoveredKind == EdgePanelKind::RESAMPLE)
? wheelLane.resampleScroll
: (hoveredKind == EdgePanelKind::CLIP)
? wheelLane.clipScroll
: wheelLane.meanFilterScroll;
const float next = current - static_cast<float>(notches * 40.0 * m_scale);
const float target = std::max(0.0f, std::min(next, maxScroll));
+3 -1
View File
@@ -4209,7 +4209,9 @@ namespace jdi
if (kind == EdgePanelKind::CLIP)
{
mix(Tab().CursorMode() == PlotCursorMode::DOUBLE ? 1ull : 0ull);
mix(lane.clipScroll > 0.0f ? 1ull : 0ull);
// ⚠️ 滚动偏移必须**按真实值**入键(不能退化成布尔):否则滚到 1px 之后
// 键不再变化 ⇒ 后续滚动不会重绘 ⇒ 表现为"滚不动 / 滚不到底"(用户 2026-11-03)。
mixFloat(lane.clipScroll);
}
// ⚠️⚠️ **边沿统计的数字本身就是面板内容,数字变化必须能让面板重绘**:
+34 -8
View File
@@ -393,12 +393,13 @@ namespace jdi
// 面板底部(内容区之下)占的纵向空间:有按钮时 = pad + 按钮 + pad,无按钮时只有 pad。
// ⚠️ 「选区截取」底部是**两个**按钮行(截取为新信号 / 原始信号截取),
// 因此 footer 要按两行算(用户 2026-11-03 要求)。
// ⚠️ **未启用双光标**时两个按钮都不显示 ⇒ footer 归零(否则按钮位置会留一大段空白)。
const bool clipReady = clipPanel && (Tab().CursorMode() == PlotCursorMode::DOUBLE);
const float footerHeight =
clipPanel ? (buttonHeight * 2.0f + 6.0f * m_scale + pad)
clipPanel ? (clipReady ? (buttonHeight * 2.0f + 6.0f * m_scale + pad) : 0.0f)
: (hasButton ? (buttonHeight + pad) : 0.0f);
const float bottomPad = pad;
const float weakPeakSliderRow = kWeakPeakSliderRowHeight * m_scale;
float meanContentHeight = 0.0f;
float height = 0.0f;
float tableHeaderHeight = 0.0f;
@@ -426,8 +427,19 @@ namespace jdi
// 「均值滤波」与「弱峰调整」= 两行滑块。
// 内容超高时出现滚动条;底部「构建为新信号」按钮固定不滚动。
const float groupRow = kEdgePanelMeanRowHeight * m_scale;
const float sliderRow = weakPeakSliderRow;
const float sliderRow = kWeakPeakSliderRowHeight * m_scale;
// ⚠️ 「选区截取」在**未启用双光标**时没有任何可操作内容(只有一行提示文字),
// 因此不按信号数累加行高、也不套用「至少 3 行」的下限
// (用户 2026-11-03 报告:未启用双光标时仍显示信号条目、且能触发滚动)。
const bool clipReady = clipPanel && (Tab().CursorMode() == PlotCursorMode::DOUBLE);
float content = 0.0f;
if (clipPanel && !clipReady)
{
// 提示行固定占一行高度(绘制时在内容区居中显示)。
content = kEdgePanelMeanRowHeight * m_scale;
}
else
{
for (const SignalRef& ref : lane.signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
@@ -478,12 +490,15 @@ namespace jdi
}
}
}
}
meanContentHeight = content;
const float budget =
m_plotRect.Height - (titleHeight + pad + bottomPad + footerHeight);
// ⚠️ 高度必须**精确等于内容高度**(不超过可用预算),不要再套「至少 3 行」的下限:
// 那个下限会让只有 1 个信号的面板留出 2 行空白(用户 2026-11-03 报告
// “信号条目的滚动区域与下方按钮之间有一大段空白”)。
const float maxContent =
std::max(3.0f * groupRow,
std::min(kFreeMarkerContentMaxHeight * m_scale, std::max(3.0f * groupRow, budget)));
std::min(kFreeMarkerContentMaxHeight * m_scale, std::max(groupRow, budget));
const float view = std::min(content, maxContent);
height = titleHeight + pad + view + bottomPad + footerHeight;
}
@@ -811,11 +826,15 @@ namespace jdi
const float groupRow = kEdgePanelMeanRowHeight * m_scale;
const float sliderRow = kWeakPeakSliderRowHeight * m_scale;
const float scrollW = kFreeMarkerScrollWidth * m_scale;
const bool clipReadyHere =
clipPanel && (Tab().CursorMode() == PlotCursorMode::DOUBLE);
const float footerHeight =
clipPanel ? (buttonRowHeight * 2.0f + 6.0f * m_scale + pad)
clipPanel ? (clipReadyHere ? (buttonRowHeight * 2.0f + 6.0f * m_scale + pad) : 0.0f)
: (hasButton ? (buttonRowHeight + pad) : 0.0f);
const float viewHeight =
std::max(0.0f, height - titleHeight - pad - pad - footerHeight);
// ⚠️ 内容区高度必须与高度算式(`height = title + pad + view + bottomPad + footer`)
// 严格同源:只减 **一个** pad(标题下方那次),不要再多减一个,否则内容区偏矮、
// 滚动条提前出现且底部留出空白(用户 2026-11-03 报告)。
const float viewHeight = std::max(0.0f, height - titleHeight - pad - footerHeight);
const bool scrollable = meanContentHeight > viewHeight + 0.5f;
const float maxScroll = std::max(0.0f, meanContentHeight - viewHeight);
const float storedScroll = meanFilter ? lane.meanFilterScroll
@@ -871,8 +890,15 @@ namespace jdi
(resetLeft - resetGap - valueColumn - 6.0f * m_scale) - trackLeft);
float rowY = layout.content.Y - scroll;
// ⚠️ 「选区截取」在**未启用双光标**时不生成任何信号行:面板只显示一句居中提示
// (用户 2026-11-03 明确要求:不应显示信号名条目、更不应能滚动)。
const bool clipRowsVisible = !clipPanel || clipReadyHere;
for (const SignalRef& ref : lane.signals)
{
if (!clipRowsVisible)
{
break;
}
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || !resolved.visible)
{