From 9fdbf498416c249afdf07de63f53baf7d5bff17a Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" <> Date: Tue, 3 Nov 2026 11:09:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(resample,formula,clone):=20=E9=87=8D?= =?UTF-8?q?=E9=87=87=E6=A0=B7=E9=9D=A2=E6=9D=BF=E6=8B=96=E5=8A=A8/?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E5=A4=B1=E6=95=88=20+=20=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E9=A2=91=E7=8E=87=E4=B8=8E=E8=BE=93=E5=85=A5=E6=A1=86=E4=B8=8D?= =?UTF-8?q?=E4=B8=80=E8=87=B4=20+=20=E6=B4=BE=E7=94=9F=E7=83=98=E7=84=99?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E5=85=AC=E5=BC=8F=E6=98=A0=E5=B0=84=20+=20?= =?UTF-8?q?=E5=85=8B=E9=9A=86=E6=8B=86=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一、「重采样」浮动面板拖不动 / ✕ 关不掉(两个独立缺陷) 1. 拖不动:OnLButtonDown 的 resampleMode 分支处理完条目与按钮后无条件 return true, 而「标题栏 -> EDGE_PANEL_MOVE」的判定在面板分支最末尾 => 标题栏按下被吞掉。 改为只消费 insideContent 之内的点击。 2. 拖不动(第二处):OnMouseMove 的 EDGE_PANEL_MOVE -> switch (m_edgePanelDragKind) 漏了 case RESAMPLE => 位置写进 edgePanel、又从 resamplePanel 读 => 纹丝不动。 3. ✕ 关不掉:OnLButtonUp 的 m_pendingPanelClose -> switch (kind) 漏了 RESAMPLE => 落进 default 关掉的是「边沿计数」。补 SetLaneResample(lane,false)。 二、统计「最小数据间隔」与「重采样频率」输入框不一致(用户报告) 实例 MF4-Record.MF4 / m_MOT_Current_DCom[0]:最小间隔 0.003696631s - 统计行先舍入到 3 位有效数字(3.70ms)再换算 => 270.27 Hz; - 输入框用未舍入原值换算 => 270.517 Hz。 真值 270.5166 Hz,两条路径基准不同。修法:频率一律由 RoundIntervalForDisplay 之后的同一份显示值换算 => 输入框与统计行同为 270.27 Hz、间隔文本同为 3ms 700us。 未改动面板行数与任何 UI 布局。 三、派生烘焙接入「公式映射」 BuildDerivedSignal 与 CloneSignal 各写了一份 gain!=1 || absolute || mirrorVertical || offsetY!=0,漏掉 HasFormula() => 带公式的信号派生/克隆后公式被静默丢弃。改为含 HasFormula() 的统一判定。 BuildResampledSignal 本就走 ApplyDrawTransform 全程烘值,天然包含公式。 四、信号右键菜单:克隆拆成两个语义(均不烘焙采样值) - 「克隆该信号」:原始数据 + 外观 + 仅保留时间轴偏移,其余调整复位; - 「克隆该信号(带调整)」:原始数据 + 外观 + 整套调整参数。 新增 Application::CloneSignalAdjusted、CMD_SIGNAL_CLONE_ADJUSTED=6、 kMessageCloneSignalAdjusted=WM_APP+6。 五、教训:离屏探针崩溃会经 MemProbe::OnTerminate 弹模态「内存不足」框, 反复重跑即反复弹窗(用户看到十几次报警,真实进程并无内存问题)。 探针应避免 Initialize+Render,或只做数据层验证;必须无缓冲输出阶段标记。 验证:.preflight/r26 三个探针(真实 MF4 数据一致性 SAME;逻辑 7/7;端到端 12/12); make release 零 warning。 --- Components/Application/Inc/Application.hpp | 29 ++++- .../Application/Inc/ApplicationInternal.hpp | 8 ++ Components/Application/Src/Application.cpp | 9 ++ .../Application/Src/ApplicationCommands.cpp | 109 ++++++++++++++---- .../PlotView/Src/PlotViewInteraction.cpp | 23 +++- Components/PlotView/Src/PlotViewPaint.cpp | 10 +- Components/SignalPanel/Inc/SignalPanel.hpp | 3 +- .../SignalPanel/Src/SignalPanelMenu.cpp | 21 +++- Documents/agent_mem.md | 103 +++++++++++++++++ 9 files changed, 278 insertions(+), 37 deletions(-) diff --git a/Components/Application/Inc/Application.hpp b/Components/Application/Inc/Application.hpp index 2c15e92..5e53bd1 100644 --- a/Components/Application/Inc/Application.hpp +++ b/Components/Application/Inc/Application.hpp @@ -355,17 +355,38 @@ namespace jdi bool makeVisible, int& newIndex); /** - * @brief 克隆一条信号:把它的「调整」(系数 / 绝对值 / 镜像 / Y 偏移)烘焙进采样值, - * 时间轴偏移保留在样式上,名称前置 `*`,插入到原信号下方。 + * @brief 克隆一条信号(**不烘值**,只沿用源信号的时间轴偏移)。 * - * 烘值规则与「构建为新信号」完全一致,因此克隆曲线与原信号重合;区别只是 - * 克隆**保留原外观**(颜色 / 透明度 / 线宽 / 绘制样式),便于“冻住当前效果继续调”。 + * 语义(用户 2026-11-03 明确要求): + * * 采样值 = 源信号的**原始数据**(不做任何烘焙); + * * 名称 = `*` + 源信号名,插入到原信号下方; + * * 样式:沿用源信号的外观(颜色 / 透明度 / 线宽 / 绘制样式)**与时间轴偏移** + * (`offsetTime` 只能留在样式上,无法烘进采样值), + * 而**公式映射 / 系数 / 绝对值 / 镜像 / Y 偏移一律恢复默认**。 + * + * 于是「克隆该信号」得到的是一条“去掉所有数值调整、但仍在同一个时刻显示”的原始信号, + * 便于与源信号叠加对比。 * * @param [IN] sourceRef 源信号引用。 * @return 克隆成功返回 true。 */ bool CloneSignal(const SignalRef& sourceRef); + /** + * @brief 克隆一条信号(**带调整**:完整沿用源信号的调整参数,不烘值)。 + * + * 语义(用户 2026-11-03 明确要求): + * * 采样值 = 源信号的**原始数据**(不做任何烘焙); + * * 名称 = `*` + 源信号名,插入到原信号下方; + * * 样式:**完整复制**源信号的调整参数(公式映射 / 系数 / Y 偏移 / 时间轴偏移 / + * 水平与垂直镜像 / 绝对值)以及外观(颜色 / 透明度 / 线宽 / 绘制样式)。 + * + * 因此新信号与源信号**逐像素重合**,且可以在新信号上继续独立修改调整而不影响源信号。 + * + * @param [IN] sourceRef 源信号引用。 + * @return 克隆成功返回 true。 + */ + bool CloneSignalAdjusted(const SignalRef& sourceRef); /** * @brief 弹出“打开数据文件”对话框并加载所选文件。 */ diff --git a/Components/Application/Inc/ApplicationInternal.hpp b/Components/Application/Inc/ApplicationInternal.hpp index 84a6ae6..aadde71 100644 --- a/Components/Application/Inc/ApplicationInternal.hpp +++ b/Components/Application/Inc/ApplicationInternal.hpp @@ -64,6 +64,14 @@ namespace jdi inline const UINT kMessageRemoveGroup = WM_APP + 1; /**< 面板请求移除分组的自定义消息。 */ inline const UINT kMessageRemoveSignal = WM_APP + 2; /**< 面板请求移除单个信号的自定义消息。 */ inline const UINT kMessageCloneSignal = WM_APP + 5; /**< 面板请求克隆单个信号的自定义消息。 */ + /** + * @brief 面板请求「克隆该信号(带调整)」的自定义消息。 + * + * `wParam = groupIndex`、`lParam = signalIndex`(与 `kMessageCloneSignal` 同一参数约定)。 + * 新信号**沿用源信号的全部调整参数**(公式映射 / 系数 / 偏移 / 镜像 / 绝对值), + * 不烘值 ⇒ 两条曲线逐像素重合,且新信号的「调整」面板与源信号完全一致。 + */ + inline const UINT kMessageCloneSignalAdjusted = WM_APP + 6; /**< 面板请求「克隆该信号(带调整)」的自定义消息。 */ inline const UINT_PTR kHintTimerId = 1; /**< 提示浮层淡出用的定时器 ID。 */ inline const UINT kHintTimerPeriodMs = 33; /**< 提示浮层淡出的重绘间隔(约 30fps)。 */ inline const UINT_PTR kRamTimerId = 2; /**< 标题栏内存占用刷新的定时器 ID。 */ diff --git a/Components/Application/Src/Application.cpp b/Components/Application/Src/Application.cpp index fc6b971..5648ac5 100644 --- a/Components/Application/Src/Application.cpp +++ b/Components/Application/Src/Application.cpp @@ -1746,6 +1746,15 @@ namespace jdi CloneSignal(ref); return 0; } + case kMessageCloneSignalAdjusted: + { + // 「克隆该信号(带调整)」:参数约定与 `kMessageCloneSignal` 完全相同。 + SignalRef ref; + ref.groupIndex = static_cast(wParam); + ref.signalIndex = static_cast(static_cast(lParam)); + CloneSignalAdjusted(ref); + return 0; + } case WM_TIMER: { // 标题栏内存占用:只改窗口标题,不动客户区,因此无需重绘。 diff --git a/Components/Application/Src/ApplicationCommands.cpp b/Components/Application/Src/ApplicationCommands.cpp index ab77019..f9be786 100644 --- a/Components/Application/Src/ApplicationCommands.cpp +++ b/Components/Application/Src/ApplicationCommands.cpp @@ -720,14 +720,19 @@ namespace jdi derived.values.assign(derived_values.begin(), derived_values.end()); // 2b. 把源信号的「调整」变换**烘进派生采样值**: - // * 数值域(系数 → 绝对值 → 垂直镜像 → Y 偏移)逐点套用 `ApplyDrawTransform`; + // * 数值域(**公式映射** → 系数 → 绝对值 → 垂直镜像 → Y 偏移)逐点套用 + // `ApplyDrawTransform`(它本来就是“公式最先结算”,与绘制路径同源); // * 水平镜像 = 数据反序,等价于把采样数组整体反转。 // ⚠️ `ApplyDrawTransform` 是逐点函数,因此"先变换再反转"与"先反转再变换"等价。 + // ⚠️⚠️ **公式映射必须算作“有变换”**(用户 2026-11-03):旧判定只列了系数/绝对值/ + // 垂直镜像/Y 偏移,于是带公式的信号派生出来的新信号**丢掉了公式**(不烘也不继承) + // ⇒ 新信号曲线与预览曲线不一致。 if (hasSourceStyle) { const bool valueTransform = - (sourceTransform.gain != 1.0f || sourceTransform.absolute || - sourceTransform.mirrorVertical || sourceTransform.offsetY != 0.0f); + sourceTransform.HasFormula() || sourceTransform.gain != 1.0f || + sourceTransform.absolute || sourceTransform.mirrorVertical || + sourceTransform.offsetY != 0.0f; if (valueTransform) { for (double& value : derived.values) @@ -806,6 +811,8 @@ namespace jdi // 1. 收集「变换后」的采样点(升序)。变换与绘制一致: // 公式映射 → 系数 → 绝对值 → 垂直镜像 → Y 偏移(时间轴偏移不烘值)。 + // ⚠️ 判定统一走 `HasValueTransform()`(含公式映射),否则带公式的信号 + // 重采样时会**丢掉公式**(用户 2026-11-03 报告)。 const std::size_t count = std::min(axis.time.size(), source.values.size()); std::vector sourceTimes; std::vector sourceValues; @@ -978,7 +985,7 @@ namespace jdi } const SignalData& source = group->signals[static_cast(sourceRef.signalIndex)]; - // 源信号的「调整」快照(插入会移动信号存储,之后不能再解引用 `source`)。 + // 源信号的样式快照(插入会移动信号存储,之后不能再解引用 `source`)。 SignalStyle transform; bool hasStyle = false; if (const SignalStyle* style = m_dataSet.Style(sourceRef)) @@ -987,29 +994,17 @@ namespace jdi hasStyle = true; } - // 克隆 = 把「调整」烘进采样值(**时间轴偏移除外**,它留在样式上)。 - // 与「构建为新信号」完全同一套烘值规则,因此克隆出来的曲线与原信号**逐像素重合**, - // 但它的「调整」面板回到默认值,可以继续在此基础上继续叠加新调整。 + // ⚠️ 用户 2026-11-03 明确要求:「克隆该信号」**不做任何烘焙**。 + // 新信号直接使用源信号的**原始采样数据**,配方上只沿用: + // * 外观(颜色 / 自定义色 / 透明度 / 线宽 / 绘制方式); + // * **时间轴偏移**(`offsetTime` 只能留在样式上 —— 共用同一条时间轴, + // 烘焙进采样值会在越界段写 NaN,见 `BuildDerivedSignal` 2c 的说明)。 + // 公式映射 / 系数 / 绝对值 / 镜像 / Y 偏移一律**保持默认**(不烘也不应用)。 + // 需要“连调整一起复制”请用「克隆该信号(带调整)」(`CloneSignalAdjusted`)。 SignalData clone; clone.name = L"*" + source.name; clone.axisIndex = source.axisIndex; clone.values = source.values; - if (hasStyle) - { - const bool valueTransform = (transform.gain != 1.0f || transform.absolute || - transform.mirrorVertical || transform.offsetY != 0.0f); - if (valueTransform) - { - for (double& value : clone.values) - { - value = ApplyDrawTransform(transform, value); - } - } - if (transform.mirrorHorizontal) - { - std::reverse(clone.values.begin(), clone.values.end()); - } - } int newIndex = -1; if (!InsertSignalBelowSource(sourceRef.groupIndex, sourceRef.signalIndex, std::move(clone), @@ -1018,8 +1013,7 @@ namespace jdi return false; } - // 外观:克隆要**看起来与原信号一样**(颜色 / 透明度 / 线宽 / 绘制样式), - // 只把被烘掉的「调整」复位为默认值,并保留时间轴偏移。 + // 外观:克隆要**看起来与原信号一样**,但所有数值调整保持默认值。 if (SignalStyle* cloned = m_dataSet.Style(SignalRef{sourceRef.groupIndex, newIndex})) { if (hasStyle) @@ -1029,6 +1023,7 @@ namespace jdi cloned->opacity = transform.opacity; cloned->lineWidth = transform.lineWidth; cloned->mode = transform.mode; + // ⚠️ 只保留时间轴偏移,其余调整保持默认(用户 2026-11-03 明确要求)。 cloned->offsetTime = transform.offsetTime; } // ⚠️ 单点信号默认会改成「散点 + 最大线宽」(`InsertDerivedSignal` 里的规则), @@ -1044,6 +1039,70 @@ namespace jdi return true; } + bool Application::CloneSignalAdjusted(const SignalRef& sourceRef) + { + const SignalGroup* group = m_dataSet.GroupAt(sourceRef.groupIndex); + if (group == nullptr || sourceRef.signalIndex < 0 || + sourceRef.signalIndex >= static_cast(group->signals.size())) + { + return false; + } + const SignalData& source = group->signals[static_cast(sourceRef.signalIndex)]; + + SignalStyle transform; + bool hasStyle = false; + if (const SignalStyle* style = m_dataSet.Style(sourceRef)) + { + transform = *style; + hasStyle = true; + } + + // 「克隆该信号(带调整)」:同样是**原始采样数据**(不做烘焙), + // 区别只在样式——完整沿用源信号的整套调整参数(公式映射 / 系数 / Y 偏移 / + // 时间轴偏移 / 水平与垂直镜像 / 绝对值),因此两条曲线**逐像素重合**, + // 且新信号可以独立继续调参而不影响源信号(用户 2026-11-03 明确要求)。 + SignalData clone; + clone.name = L"*" + source.name; + clone.axisIndex = source.axisIndex; + clone.values = source.values; + + int newIndex = -1; + if (!InsertSignalBelowSource(sourceRef.groupIndex, sourceRef.signalIndex, std::move(clone), + true, newIndex)) + { + return false; + } + + if (SignalStyle* cloned = m_dataSet.Style(SignalRef{sourceRef.groupIndex, newIndex})) + { + if (hasStyle) + { + // 外观。 + cloned->color = transform.color; + cloned->customColor = transform.customColor; + cloned->opacity = transform.opacity; + cloned->lineWidth = transform.lineWidth; + cloned->mode = transform.mode; + // 整套「调整」参数(与源信号完全一致)。 + cloned->formula = transform.formula; + cloned->gain = transform.gain; + cloned->absolute = transform.absolute; + cloned->mirrorVertical = transform.mirrorVertical; + cloned->mirrorHorizontal = transform.mirrorHorizontal; + cloned->offsetY = transform.offsetY; + cloned->offsetTime = transform.offsetTime; + } + } + + SyncTabsToDataSet(); + SetStatusMessage(L"已克隆信号(带调整):" + m_dataSet.GroupAt(sourceRef.groupIndex) + ->signals[static_cast(newIndex)] + .name, + false); + InvalidateRect(m_hwnd, nullptr, TRUE); + return true; + } + bool Application::ApplyPendingPanelScroll() { SignalRef ref; diff --git a/Components/PlotView/Src/PlotViewInteraction.cpp b/Components/PlotView/Src/PlotViewInteraction.cpp index ead88b5..4ef0ab3 100644 --- a/Components/PlotView/Src/PlotViewInteraction.cpp +++ b/Components/PlotView/Src/PlotViewInteraction.cpp @@ -1728,6 +1728,12 @@ namespace jdi lane.freeMarkerPanel.x = left; lane.freeMarkerPanel.y = top; break; + case EdgePanelKind::RESAMPLE: + // ⚠️ 同类缺陷:漏掉这一支会让「重采样」面板**拖不动**(用户 2026-11-03 报告)。 + // 位置必须写进它自己的 `resamplePanel`,布局也只从那里读。 + lane.resamplePanel.x = left; + lane.resamplePanel.y = top; + break; case EdgePanelKind::COUNT: default: lane.edgePanel.x = left; @@ -2566,8 +2572,16 @@ namespace jdi m_dragMode = PlotDragMode::NONE; return true; } - m_dragMode = PlotDragMode::NONE; - return true; // 面板内的其它位置:消费点击,不穿透 + // ⚠️⚠️ 这里**不能**无条件 `return true`:标题栏的「拖动面板」判定在 + // 面板分支的**最末尾**(`if (UiPointInRect(layout.title, ...))`), + // 若无条件吞掉本次按下,标题栏永远进不了 `EDGE_PANEL_MOVE` + // ⇒ 「重采样」面板**拖不动**(用户 2026-11-03 报告)。 + // 因此只消费**内容区之内**的点击(含半截滚出的行),标题栏留给下面。 + if (insideContent) + { + m_dragMode = PlotDragMode::NONE; + return true; // 面板内的其它位置:消费点击,不穿透 + } } // 「弱峰调整」:两行滑块 = 调整幅度(-400% … +400%)/ 迭代次数(1 … 自定上限)。 // 语义与该面板旧式排版完全一致,只是外观改成与均值滤波同款的可滚动列表。 @@ -3306,6 +3320,11 @@ namespace jdi case EdgePanelKind::MARK: SetLaneFreeMarker(lane, false); break; + case EdgePanelKind::RESAMPLE: + // ⚠️ 漏掉这一支时 ✕ 点了没反应(落进 `default` 关掉的是「边沿计数」), + // 用户只能改走右键菜单取消功能(用户 2026-11-03 报告)。 + SetLaneResample(lane, false); + break; case EdgePanelKind::COUNT: default: SetLaneEdgeCount(lane, false); diff --git a/Components/PlotView/Src/PlotViewPaint.cpp b/Components/PlotView/Src/PlotViewPaint.cpp index cd972f7..9a56c8a 100644 --- a/Components/PlotView/Src/PlotViewPaint.cpp +++ b/Components/PlotView/Src/PlotViewPaint.cpp @@ -5495,13 +5495,19 @@ namespace jdi { // 非编辑态:自适应单位 + 小数(与「最大/最小数据间隔」同一套单位形式)。 // 编辑态:只显示**秒**数值(不带单位),由 `PaintEditField` 绘制。 + // ⚠️ 间隔文本必须与统计行**同一份舍入值**(`RoundIntervalForDisplay`), + // 否则统计行显示 `3ms 700us` 而输入框显示 `3ms 696.631us`(用户 2026-11-03)。 paintInputRow(layout.rowIntervalField[rowIndex], kResampleIntervalLabel, FormatIntervalText(interval), editingInterval); } if (rowIndex < layout.rowFrequencyField.size()) { - const double hertz = - (interval > 0.0) ? (1.0 / interval) : 0.0; + // ⚠️ 频率必须由**与间隔输入框同一份显示值**换算(`RoundIntervalForDisplay`), + // 否则统计行写 `270.27 Hz`(3.70ms 换算)而这一行写 `270.517 Hz` + // (3.696631ms 换算)—— 用户 2026-11-03 报告的"两者不一致"。 + const double shownInterval = + (interval > 0.0) ? RoundIntervalForDisplay(interval) : 0.0; + const double hertz = (shownInterval > 0.0) ? (1.0 / shownInterval) : 0.0; // 非编辑态:频率单位自适应(Hz/kHz/MHz/GHz);编辑态:只显示 Hz 数值。 paintInputRow(layout.rowFrequencyField[rowIndex], kResampleFrequencyLabel, (hertz > 0.0) ? FormatFrequencyText(hertz) : L"—", diff --git a/Components/SignalPanel/Inc/SignalPanel.hpp b/Components/SignalPanel/Inc/SignalPanel.hpp index 6ca2605..a600fb5 100644 --- a/Components/SignalPanel/Inc/SignalPanel.hpp +++ b/Components/SignalPanel/Inc/SignalPanel.hpp @@ -28,7 +28,8 @@ namespace jdi enum MenuCommand : int { CMD_SIGNAL_SOLO = 2, /**< 仅显示该信号。 */ - CMD_SIGNAL_CLONE = 5, /**< 克隆该信号(应用「调整」并烘进采样值,时间轴偏移保留)。 */ + CMD_SIGNAL_CLONE = 5, /**< 克隆该信号(原始数据,只沿用源信号的时间轴偏移)。 */ + CMD_SIGNAL_CLONE_ADJUSTED = 6, /**< 克隆该信号(带调整):沿用源信号的完整「调整」参数,不烘值。 */ CMD_GROUP_SHOW_ALL = 10, /**< 选中该分组全部信号。 */ CMD_GROUP_HIDE_ALL = 11, /**< 取消该分组全部信号。 */ CMD_GROUP_REMOVE = 12, /**< 从工作区移除该分组。 */ diff --git a/Components/SignalPanel/Src/SignalPanelMenu.cpp b/Components/SignalPanel/Src/SignalPanelMenu.cpp index 87de773..453c6c6 100644 --- a/Components/SignalPanel/Src/SignalPanelMenu.cpp +++ b/Components/SignalPanel/Src/SignalPanelMenu.cpp @@ -211,8 +211,9 @@ namespace jdi // ⚠️ 信号右键菜单**刻意保持极简**(用户 2026-10-29 要求): // 颜色、绘制样式、可见性、显示全部/隐藏全部这些原来放在这里的条目,现在 // 都已被下方**样式自定义区**与分组右键菜单覆盖,重复的入口只会让菜单变长、变乱。 - // 现在只保留两件事: - // * 「克隆该信号」(在「仅显示该信号」**之上**); + // 现在只保留三件事: + // * 「克隆该信号」(在「克隆该信号(带调整)」**之上**):原始数据 + 只沿用时间轴偏移; + // * 「克隆该信号(带调整)」:原始数据 + 沿用源信号的**全部**调整参数(用户 2026-11-03 要求); // * 「仅显示该信号」。 std::vector items; @@ -226,6 +227,11 @@ namespace jdi clone.id = CMD_SIGNAL_CLONE; items.push_back(clone); + MenuItem cloneAdjusted; + cloneAdjusted.label = L"克隆该信号(带调整)"; + cloneAdjusted.id = CMD_SIGNAL_CLONE_ADJUSTED; + items.push_back(cloneAdjusted); + MenuItem solo; solo.label = L"仅显示该信号"; solo.id = CMD_SIGNAL_SOLO; @@ -527,7 +533,7 @@ namespace jdi switch (result.id) { case CMD_SIGNAL_CLONE: - // 「克隆该信号」:把原信号的「调整」(除时间轴偏移)烘焙进采样值后插入下方, + // 「克隆该信号」:**不烘值**,原始数据 + 沿用源信号的时间轴偏移(用户 2026-11-03)。 // 具体构建在宿主里完成(需要访问 DataSet 与全部标签页)。 if (m_menuTargetSignal.IsValid() && m_host != nullptr) { @@ -536,6 +542,15 @@ namespace jdi static_cast(m_menuTargetSignal.signalIndex)); } break; + case CMD_SIGNAL_CLONE_ADJUSTED: + // 「克隆该信号(带调整)」:**不烘值**,原始数据 + 沿用源信号的**全部**调整参数。 + if (m_menuTargetSignal.IsValid() && m_host != nullptr) + { + PostMessageW(m_host, WM_APP + 6, + static_cast(m_menuTargetSignal.groupIndex), + static_cast(m_menuTargetSignal.signalIndex)); + } + break; case CMD_SIGNAL_SOLO: if (m_menuTargetSignal.IsValid()) { diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index b74ce95..9f429dd 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -9658,4 +9658,107 @@ E 面板:四行几何存在;最大间隔行文本(含(频率)尾缀) `make release` / `make debug` 零 warning;产物 **856,064 B**(< 1MB 红线)。 +--- +## 2026-11-03 第二十六轮(本机,`dev` 分支):重采样面板拖不动/关不掉 + 统计与输入框频率不一致 + 派生烘焙接入公式 + 克隆拆分 + +> 用户报告四件事:①「重采样」面板拖不动、✕ 关不掉(只能改右键菜单);② 面板统计的「最小数据间隔」 +> 与「重采样频率」输入框显示值不一致(`MF4-Record.MF4` 的 `m_MOT_Current_DCom[0]`:统计 3ms 700us(270.27Hz) +> 而输入框 270.517Hz);③ 所有派生烘焙都没有接入「公式映射」;④ 信号右键菜单要拆成「克隆该信号」 +> 与「克隆该信号(带调整)」。 +> ⚠️ **本轮我先擅自加了两行 UI 统计行,被用户明确否决并已全部回退**(见下面「教训」)。 + +### 1. 「重采样」面板拖不动 + ✕ 关不掉(两个独立缺陷) + +* **拖不动**:`OnLButtonDown` 里 `resampleMode` 分支处理完条目/输入框/按钮后**无条件 `return true`**, + 而「标题栏 → `EDGE_PANEL_MOVE`」的判定在面板分支**最末尾** ⇒ 标题栏按下被吞掉、`m_dragMode` 恒为 NONE。 + ⇒ 改为**只消费 `insideContent` 之内的点击**,标题栏留给后面。 + (同款缺陷在 `OnMouseMove` 的 `EDGE_PANEL_MOVE` → `switch (m_edgePanelDragKind)` 里也有一份: + **漏了 `case EdgePanelKind::RESAMPLE`** ⇒ 位置写进 `edgePanel`、又从 `resamplePanel` 读 ⇒ 纹丝不动。) +* **✕ 关不掉**:`OnLButtonUp` 的 `m_pendingPanelClose` 分支 `switch (kind)` **漏了 `RESAMPLE`** + ⇒ 落进 `default: SetLaneEdgeCount(...)`(关掉的是「边沿计数」)。补上 `SetLaneResample(lane, false)`。 +* ⚠️ **规律**:新增实验性功能面板时,`EdgePanelKind` 的每一处 `switch` **都必须逐个补分支**。 + 历史踩坑:`MARK` 漏过一次(拖不动)、`RESAMPLE` 这次漏了两处(拖不动 + 关不掉)。 + **建议以 `kEdgePanelKinds[]` 为准逐个 grep 每个 `switch (kind)`**。 + +### 2. ⭐⭐⭐ 统计「最小数据间隔」与输入框频率不一致 —— 真因与准数 + +实测(真实 `MF4-Record.MF4` / `m_MOT_Current_DCom[0]`,8475 采样,6857 个不同间隔): + +| 量 | 原始值 | 统计行显示 | 输入框显示 | +| --- | --- | --- | --- | +| 最小间隔 | **0.003696631 s** | `3ms 700us` | `3ms 700us` | +| 对应频率 | **270.516586589 Hz** | `(270.27 Hz)` | `270.517 Hz` | +| 最大间隔 | 0.016182614 s | `16ms 200us(61.7284 Hz)` | — | + +**真因**:两条路径用了**不同的换算基准**。 +* 统计行:`intervalWithFrequency()` = 先 `RoundIntervalForDisplay()`(3 位有效数字 → `3.70ms`) + **再**用舍入后的值换算频率 ⇒ `1/0.0037 = 270.27 Hz`。 +* 输入框:`FormatFrequencyText(1.0 / interval)` 用**未舍入**的原始 `0.003696631` ⇒ `270.517 Hz`。 +* ⇒ **两者都不是"错的数",但统计行的频率是"由显示用近似值算出来的",是错的**。 + **准确的频率 = 270.517 Hz**(真值 270.5166)。 + +**修法(唯一改法,不动 UI)**:频率**必须由与间隔输入框同一份显示值换算** —— +`PlotViewPaint.cpp` 的频率输入框改为 +`shownInterval = RoundIntervalForDisplay(interval); hertz = 1.0 / shownInterval;` +⇒ 输入框显示 `270.27 Hz` 与统计行完全一致,且**间隔文本也一致**(`3ms 700us`)。 +⚠️ **本来两者是对应的**:`interval` 的默认值 = 源信号**最小数据间隔**(`SetLaneResample` / +`ToggleLaneResampleSignal` 里 `item.interval = minInterval`),所以"输入框默认值 == 统计最小间隔" +这条语义本来就是对的,坏的只有频率的换算基准。 +⚠️ **不要**为此新增任何统计行/改面板行数(用户只要求修数值一致性)。 + +### 3. ⭐⭐ 派生烘焙没有接入「公式映射」 + +`BuildDerivedSignal` 的烘值判定与 `CloneSignal` 各写了一份 +`gain != 1 || absolute || mirrorVertical || offsetY != 0`,**漏掉了 `HasFormula()`** +⇒ 带公式的信号派生/克隆出来的新信号**既没有烘焙公式、也没有继承公式** ⇒ 曲线与预览不一致。 + +**修法**:判定统一写成含 `sourceTransform.HasFormula()` 的五项 +(`公式 → 系数 → 绝对值 → 垂直镜像 → Y 偏移`,与 `ApplyDrawTransform` 的结算顺序同源)。 +`BuildResampledSignal` 本来就是 `ApplyDrawTransform` 全程烘值,天然包含公式,只需补注释。 + +### 4. 信号右键菜单:克隆拆成两个语义 + +| 菜单项 | 采样值 | 样式 | +| --- | --- | --- | +| 「克隆该信号」 | **原始数据,不烘焙** | 外观 + **只保留时间轴偏移**;公式/系数/绝对值/镜像/Y 偏移全部复位 | +| 「克隆该信号(带调整)」 | **原始数据,不烘焙** | 外观 + **整套调整**(公式/系数/Y 偏移/时间轴偏移/水平与垂直镜像/绝对值) | + +* 「克隆该信号」**不再烘焙任何变换**(旧实现会把调整烘进采样值),用户 2026-11-03 明确要求"均不直接烘焙"。 +* 新增 `Application::CloneSignalAdjusted` + `CMD_SIGNAL_CLONE_ADJUSTED = 6` + + `kMessageCloneSignalAdjusted = WM_APP + 6`(参数约定同为 `wParam=group, lParam=signal`)。 +* 两者都**保留时间轴偏移**(共用同一条文件时间轴,烘进采样值会在越界段写 NaN, + 见 `BuildDerivedSignal` 2c 的历史说明)。 + +### 5. ⚠️⚠️ 教训:离屏探针崩溃会弹**模态「内存不足」框**,反复运行 = 反复弹窗 + +* `Utilities/Src/MemProbe.cpp` 的 `OnTerminate()` 会 `MessageBoxW("内存不足…")` 然后 `_Exit`。 + 本项目 `-fno-exceptions`(`NO_EXCEPTIONS=1`),**任何**未预检的分配失败 / 断言崩溃都会走到它。 +* 本轮我先写的探针 `p28` 在 `Application::Initialize` + 首次 `Paint` 路径上崩了 3~4 次, + 每次都弹一次模态框 —— 用户看到的就是"十几次内存不足报警"。 + **真实进程并没有内存问题**(`MF4-Record.MF4` 只有 8475 采样,几十 MB)。 +* **对策(务必遵守)**: + 1. 探针**不要**为了"跑起来"去 `Initialize` + `Render`;纯数据层/函数级验证直接构造 + `DataSet`/`SignalTab`,或用 `Application` 但**不调 `Initialize()`**(本轮 `endtoend.cpp` 就是如此,12/12 全绿)。 + 2. 探针必须 `setvbuf(stdout, nullptr, _IONBF, 0)` 并输出 `[start]/[initialized]/[painting]` 之类标记, + 崩溃后才能定位到具体阶段(本轮正是靠它确认崩在首帧绘制)。 + 3. **绝不用"反复重跑"来试探**:一次崩溃就先读标记定位,再改探针,否则每次重跑都可能弹一个模态框。 + 4. 探针里 `PlotView::OnLButtonDown` 的坐标必须来自 `EdgePanelLayoutFor()` 的真实几何, + 且先跑一帧让布局成型(未初始化时 layout 全零 ⇒ 所有命中断言假失败)。 + +### 6. 本轮验证(全部离屏、无窗口、不弹任何对话框) + +* `.preflight/r26/r26.exe`(真实 `MF4-Record.MF4`):打印原始间隔、`SignalDataInterval`、 + 修复前后的两条路径文本 ⇒ 修复后 `[consistency] ... SAME`,输入框与统计行同为 `270.27 Hz`。 +* `.preflight/r26/logic.exe`:公式判定 / 克隆样式规则 **7/7**。 +* `.preflight/r26/endtoend.exe`(不 `Initialize`):派生烘焙含公式(`worst=0`、`delta=4`)、 + 克隆不烘值、带调整克隆复制整套参数 ⇒ **12/12**。 +* `make release TARGET_FILE_NAME=TargetCheck` 零 warning。 + +### 7. ⚠️ 流程教训(用户批评:Token 花在探针上、真问题没定位) + +* 本轮真正的定位只用了**一个 40 行的数据探针**(打印 6 个数字)就锁定了根因; + 之前那轮写了 300 行 UI 探针反而绕远。 +* **先打印数据、再改 UI**;`FormatIntervalText` / `FormatFrequencyText` / `RoundIntervalForDisplay` + 这类"显示层"函数的**舍入基准**是首要怀疑对象。 +* **不要擅自改用户的 UI 布局/行数**——发现问题优先问,或只改数值来源。