fix(formula,resample): 公式映射卡顿根治 + 语法/输入框完善 + 重采样面板三处修正

一、「公式映射」卡顿(用户报告:只是点进输入框就卡,编辑过公式后彻底没法用)
1. 真因:菜单内联编辑提交时无条件回写初值 ⇒ 点进输入框再点别处就把
   formula = "f(t)" 写进样式;而 HasFormula() 只判 !empty(),于是
   DrawTransformOrderPreserving() 变 false、ComputeLaneRange() 每帧逐点扫描
   整条信号(2580 万采样 ≈ 960ms/帧)。
   修法:新增 FormulaIsIdentity()(空串与 f(t) 等价);提交时归一化后恒等即清空;
   BuildLaneVisuals() 为「网格幅值范围」加指纹缓存(数据指针/长度/统计值 +
   影响数值的样式字段),键不变直接复用(实测 0.0005ms)。
2. 真实公式仍然贵:新增 PlotView::AcquireFormulaEnvelope()(变换后的逐分组
   包络,65536 组,按需算一次并缓存),BuildPolyline 的包络带改用它。
   实测:静止 215→21ms、中键拖动 8ms、缩放最坏 843→81ms;极值仍逐点统计
   (不会漏尖峰)。
3. AcquireProgram 只比 data() 指针 + 长度 ⇒ std::wstring 短串走 SSO 时,
   等长改公式会一直复用旧程序(f(t)^2→f(t)^3 波形不更新)。改为按内容判定。

二、公式语法(Utilities/FormulaEval)
- 新增 ^ 幂(右结合,2^3^2=512、2^-1=0.5、-2^2=-4),支持 2^f(t) 与 f(t)^2;
- 常量 e / pi(大小写均接受,保存前统一小写);
- 数据点只允许 X / x / f(t):单独的 f 不再接受(且不归一化,直接报错);
- 数字扫描修正:2e 里的 e 不再被当成指数吞掉。

三、「公式映射」输入框(SignalPanelMenu)
- Y= 画在输入框内部(非编辑态),进入编辑态只显示公式本身;
- label 留空 ⇒ 输入框占满整行,与下方「系数调整」同宽。

四、「重采样」面板
- 修复点进间隔/频率输入框后输入框整块消失(paintOverlays 的 PaintEditField
  名单漏了 RESAMPLE_INTERVAL / RESAMPLE_FREQUENCY);
- 最大/最小数据间隔:3 位有效数字 + Δt 复合单位,末尾追加(频率)尾缀;
- 间隔显示自适应单位、编辑态用秒;频率显示自适应单位、编辑态用 Hz(均不带单位);
- 面板宽度按含(频率)尾缀的文本量宽。

五、十进制格式化(Utf8Util)
- 新增 FormatDecimal(绝不产生科学计数法);FormatDurationCompound /
  FormatDurationSeconds / FormatFrequency 改用它,Δt/间隔/频率读数不再出现 1e-05。

验证:离屏探针 .preflight/perf/probe/formulacheck_main.cpp 75/75;
make release / make debug 零 warning;Release 856,064 B。
This commit is contained in:
DeepSeek-V4.1-Flash committed 2026-11-03 02:55:22 +08:00
1 parent 955b233e63
commit 74456b9305
13 files changed
+682 -74

No files matched your search

+104
View File
@@ -9555,3 +9555,107 @@ make release / make debug 零 warning;Release 814,080 B
⚠️ 探针里必须先 `Tab().SetVisible({0,0}, true)` 再开面板:新加载信号默认不勾选,
否则 `SetLaneResample` 找不到"第一个可见信号"、面板布局为空。
---
## 2026-11-03 第二十五轮(本机,`dev` 分支):公式映射卡顿根治 + 输入框/语法完善 + 重采样面板三处修正
> 用户报告的六组问题(`VERY_BIG_DATA.CSV` 全部信号合并到同一网格):
> ①「映射/偏移」二级面板里**只是点进过**公式输入框,整个应用就变卡;编辑过公式后彻底没法用;
> ② `Y=` 应画在输入框**内部**、输入框与下方「系数」等宽、只允许 `X/x/f(t)` 指代数据点、
> 需要 `^` 幂与常量 `e`/`pi`;③「重采样」面板点进间隔/频率输入框后**输入框整个消失**;
> ④ 最大/最小数据间隔出现科学计数法、且末尾要追加「(频率)」尾缀;
> ⑤ 重采样间隔显示要带自适应单位、编辑态用秒且不带单位;⑥ 频率编辑态用 Hz、显示态自适应单位。
### 1. ⭐⭐⭐ 卡顿真因:**恒等公式 `f(t)` 被当成"启用了公式"**
`PopupMenu` 的内联编辑在**提交时回写当前文本**(即使一个字都没改):点进「公式映射」输入框、
再点别处 ⇒ `EndItemEdit` 用初值 `f(t)` 提交 ⇒ 样式里存进 `formula = L"f(t)"`。
而 `HasFormula()` 只判 `!formula.empty()` ⇒ 于是:
* `DrawTransformOrderPreserving()` 返回 false ⇒ 丢掉粗粒度包络层(`coarseMin/Max`);
* `TabConfig::ComputeLaneRange()` 走「公式不保序 ⇒ 逐点扫描整条信号」分支 ——
而 `BuildLaneVisuals()` **每帧**为每个网格调一次 ⇒ 2580 万采样约 **960ms/帧**。
修法(三层):
1. `FormulaIsIdentity()`(`DataModel.hpp`):空串与 `f(t)` **等价**,`HasFormula()` 两者都返回 false;
2. `SignalPanel::OnMenuCommand(CMD_FORMULA_EDIT)` 提交时,归一化后若恒等即 `formula.clear()`;
3. `PlotView::BuildLaneVisuals()` 对「网格幅值范围」加**指纹缓存**(`m_laneRangeKey` +
`m_laneRangeYMin/Max`):键 = 可见信号集合 + 影响数值的样式字段(含公式文本)+ 数据指针/长度/
min/max/finiteCount。范围与视界无关,键不变就直接复用(实测 `BuildLaneVisuals` 0.0005ms)。
### 2. ⭐⭐ 真实公式仍然贵:**变换后的逐分组包络缓存**
即便公式不是恒等,只要有公式就仍然退化成逐点扫描(一次折线构建 2580 万采样 ≈ **850ms**,
缩放一帧就卡一次)。新增 `PlotView::AcquireFormulaEnvelope()`:
* 首次需要时**全量扫描一次**,把 `ApplyDrawTransform` 之后的取值按分组(`kFormulaEnvelopeBuckets`
= 65536 组,2580 万采样 ⇒ 每组 ~394 个采样)存 min/max(无有效值的组为 NaN);
* 之后 `BuildPolyline` 的包络带路径**改用它**(`coarseScale = 1, coarseOffset = 0`,
变换已烘进包络)⇒ 每帧 / 每次缩放只按分组聚合;
* 极值仍是**逐点统计**的 ⇒ 不会漏掉尖峰(与"抽样降采样"有本质区别);
* 键 = 信号指针 + `values.data()` + 采样数 + 公式文本 + 系数/绝对值/镜像/Y 偏移(变化即失效)。
实测(`VERY_BIG_DATA` 合并网格 + `f(t)^2`):
静止帧 **215ms → 20.9ms**(单帧 5~6ms)、中键拖动 8.0ms、**缩放最坏帧 843ms → 81ms**。
### 3. ⚠️⚠️ 公式编译缓存的隐蔽错误:只比 `data()` 指针
`AcquireProgram()` 原来先比 `data()` 指针 + 长度就复用程序。而 `std::wstring` 的短串走 **SSO**
(缓冲区在对象内部)⇒ 把同一个样式对象的公式改成**等长**的另一条时指针与长度都不变
⇒ 一直复用旧程序(`f(t)^2` 改成 `f(t)^3` 后波形不更新,用户完全看不出公式失效)。
现在**必须比内容**(先比长度、再比内容;短公式只是十几字节的 memcmp)。
### 4. 公式语法(`Utilities/Inc|Src/FormulaEval`)
* 新增 `^` 幂:`power := primary ('^' unary)?`,**右结合**(`2^3^2 = 512`)、
`2^-1 = 0.5`、`-2^2 = -4`(一元负号作用于整个幂);`f(t)^2` 与 `2^f(t)` 都支持;
* 括号优先级沿用(仅小括号);
* 常量:`e`(自然常数)与 `pi`,**大小写都接受**(`E` / `Pi` / `PI`),保存前统一成小写;
* 数据点**只允许** `X` / `x` / `f(t)`:**单独的 `f` 不再接受**(`ValidateFormula` 报错,
`NormalizeFormulaText` **不**把它改写成 `f(t)`,避免"悄悄接受");`t` / `F^2` 等同样拒绝;
* 数字扫描修正:`2e` 里的 `e` 不再被当作指数吞掉(指数必须真的带数字),于是 `2e` 会按
"数字 2 + 常量 e"解析失败(不支持隐式乘法)而给出明确报错。
### 5. 「公式映射」输入框(`SignalPanelMenu.cpp`)
* 非编辑态文本 = `Y=` + 公式(画在**输入框内部**);进入编辑用 `editValue`(**无前缀**);
* `label` 留空 ⇒ 输入框占满整行,与下方「系数调整」**同宽**(`EditFieldRect` 的唯一宽度来源);
* 提交后统一回写 `FormulaDisplayText()` / `FormulaEditText()`(新增于 `DataModel.hpp`)。
### 6. 「重采样」面板三处修正
1. ⭐ **输入框消失**:面板绘制分支在编辑态会跳过输入框本体(改由 `PaintEditField` 画),
但 `paintOverlays` 调用 `PaintEditField` 的名单里**漏了 `RESAMPLE_INTERVAL/FREQUENCY`**
⇒ 双击后输入框整块不见。已补上。
2. **最大/最小数据间隔**:改用 `FormatIntervalText()`(先 `RoundIntervalForDisplay()` 到 **3 位有效数字**,
再按 Δt 语义的自适应复合单位),末尾追加 `(<频率>)` 尾缀(`FormatFrequencyText`,Hz/kHz/MHz/GHz 自适应);
频率必须由**同一份舍入后的间隔**换算,否则两列对不上。
3. **两个输入框**:非编辑态显示自适应单位(间隔 = `FormatIntervalText`,频率 = `FormatFrequencyText`);
编辑态间隔 = **秒**、频率 = **Hz**,纯小数、**不带单位**(`FormatDurationSeconds` / `FormatDecimal`)。
面板宽度按「含(频率)尾缀」的最长文本量宽,避免尾缀被裁。
### 7. 十进制格式化(`Utf8Util`)
新增 `FormatDecimal(value, significantDigits = 6)`:**绝不产生科学计数法**,
小数位数按"保留 N 位有效数字"自适应并去掉尾随 0。`FormatDurationCompound`(含 Δt 显示)、
`FormatDurationSeconds`、`FormatFrequency` 全部改用它与它同源 ⇒ 间隔/频率读数不再出现 `1e-05`。
### 8. 验证(`.preflight/perf/probe/formulacheck_main.cpp`,离屏 `SW_HIDE`)**71/71**
```
A 语法:2^f(t) / f(t)^2 / (f(t)+1)^2 / -f(t)^2 / 2^3^2 / e / pi / E / PI 全部通过;
拒绝 f、f^2、t、"f(t)+"、2(3)、f(t)^;求值 2^3^2=512、-2^2=-4、2^-1=0.5
B 归一化与恒等:X*2→f(t)*2、F(T)^2→f(t)^2、E+Pi→e+pi、1E5→1e5、单独 f 不改写;
HasFormula(): 空 / f(t) → false,f(t)^2 → true;Y= 前缀只在显示态
C 格式化:1e-5 秒 → 0.00001;5.06s → 5s 60ms;25641Hz → 25.641 kHz;1.5e6 → 1.5 MHz;
均不含指数记号
D 性能:VERY_BIG_DATA 合并网格 + f(t)^2 —— 静止 5~6ms(对无公式基线 6ms)、
中键拖动最坏 8.0ms、缩放最坏 81ms
E 面板:四行几何存在;最大间隔行文本(含(频率)尾缀)宽度 140 ≤ 266.5 可用宽度;
进入「重采样间隔」编辑后输入框**确实画出**(该区域 9074 像素变化,
且 `EditFieldRect()` 宽度 = 面板字段宽度 266.5)
```
`make release` / `make debug` 零 warning;产物 **856,064 B**(< 1MB 红线)。