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

+91 -1
View File
@@ -460,6 +460,76 @@ namespace jdi
const float scrollOffset = Tab().LaneScroll();
// ⚠️⚠️ **幅值范围(`ComputeLaneRange`)必须缓存**(用户 2026-11-03 报告“只是点进过
// 公式输入框就卡死”):本函数**每帧**都会为**每个网格**调用一次 `ComputeLaneRange`,
// 而启用了「公式映射」的信号在里面无法用 min/max 端点映射(公式不保证单调),
// 只能**逐点扫描整条信号** —— `VERY_BIG_DATA` 单条 2580 万采样时单帧就是几百毫秒,
// 拖拽时每帧都付一遍 ⇒ 完全没法用。
//
// 幅值范围只取决于「可见信号集合 + 影响数值的样式字段 + 信号数据本身」,
// **与视界/缩放无关**,因此用指纹缓存住:指纹不变就直接复用上一帧的结果。
// 指纹包含数据指针/长度/统计值与公式文本 ⇒ 重新加载数据、改样式、改公式都会自动失效。
std::uint64_t rangeKey = 1469598103934665603ull;
const auto mixRange = [&rangeKey](std::uint64_t value)
{
rangeKey ^= value;
rangeKey *= 1099511628211ull;
};
const auto mixRangeFloat = [&mixRange](float value)
{
std::uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
mixRange(bits);
};
const auto mixRangeDouble = [&mixRange](double value)
{
std::uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
mixRange(bits);
};
mixRange(reinterpret_cast<std::uintptr_t>(m_dataSet));
mixRange(static_cast<std::uint64_t>(count));
for (const Lane& lane : lanes)
{
mixRange(lane.autoRange ? 1ull : 0ull);
mixRangeDouble(lane.yMin);
mixRangeDouble(lane.yMax);
mixRange(static_cast<std::uint64_t>(lane.signals.size()));
for (const SignalRef& ref : lane.signals)
{
mixRange(static_cast<std::uint64_t>(ref.groupIndex + 2));
mixRange(static_cast<std::uint64_t>(ref.signalIndex + 2));
mixRange(m_tab->IsVisible(ref) ? 1ull : 0ull);
const SignalData* signal = m_dataSet->Resolve(ref);
const SignalStyle* style = m_dataSet->Style(ref);
mixRange(reinterpret_cast<std::uintptr_t>(signal));
mixRange(reinterpret_cast<std::uintptr_t>(style));
if (signal != nullptr)
{
mixRange(reinterpret_cast<std::uintptr_t>(signal->values.data()));
mixRange(static_cast<std::uint64_t>(signal->values.size()));
mixRange(static_cast<std::uint64_t>(signal->finiteCount));
mixRangeDouble(signal->minValue);
mixRangeDouble(signal->maxValue);
}
if (style != nullptr)
{
// 只有**影响数值范围**的字段入键(颜色/线宽等与范围无关)。
mixRangeFloat(style->gain);
mixRange(style->absolute ? 1ull : 0ull);
mixRange(style->mirrorVertical ? 1ull : 0ull);
mixRangeFloat(style->offsetY);
for (const wchar_t character : style->formula)
{
mixRange(static_cast<std::uint64_t>(static_cast<std::uint32_t>(character)));
}
mixRange(0x2Bull); // 公式结尾标记(避免 "f(t)" 与 "f(t)+" 撞键)
}
}
}
const bool reuseRange = (rangeKey == m_laneRangeKey) &&
(m_laneRangeYMin.size() == count) && (m_laneRangeYMax.size() == count);
m_lanes.resize(count);
for (std::size_t index = 0; index < count; ++index)
{
@@ -468,13 +538,33 @@ namespace jdi
m_plotRect.X,
m_plotRect.Y + static_cast<float>(index) * (laneHeight + gap) - scrollOffset,
m_plotRect.Width, laneHeight);
m_tab->ComputeLaneRange(*m_dataSet, static_cast<int>(index), visual.yMin, visual.yMax);
if (reuseRange)
{
visual.yMin = m_laneRangeYMin[index];
visual.yMax = m_laneRangeYMax[index];
}
else
{
m_tab->ComputeLaneRange(*m_dataSet, static_cast<int>(index), visual.yMin, visual.yMax);
}
if (!(visual.yMax > visual.yMin))
{
visual.yMin = 0.0;
visual.yMax = 1.0;
}
}
// 记录本帧的幅值范围,供下一帧命中(`ComputeLaneRange` 对公式信号是 O(采样数))。
if (!reuseRange)
{
m_laneRangeYMin.resize(count);
m_laneRangeYMax.resize(count);
for (std::size_t index = 0; index < count; ++index)
{
m_laneRangeYMin[index] = m_lanes[index].yMin;
m_laneRangeYMax[index] = m_lanes[index].yMax;
}
m_laneRangeKey = rangeKey;
}
}
float PlotView::TimeToX(double time) const