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:
1 parent
955b233e63
commit
74456b9305
13 files changed
+682
-74
No files matched your search
@@ -29,6 +29,7 @@ namespace jdi
|
||||
SUBTRACT,
|
||||
MULTIPLY,
|
||||
DIVIDE,
|
||||
POWER, /**< 幂(`a ^ b`)。 */
|
||||
NEGATE
|
||||
};
|
||||
|
||||
@@ -48,6 +49,7 @@ namespace jdi
|
||||
MINUS,
|
||||
STAR,
|
||||
SLASH,
|
||||
CARET,
|
||||
LEFT_PAREN,
|
||||
RIGHT_PAREN,
|
||||
END
|
||||
@@ -101,6 +103,11 @@ namespace jdi
|
||||
tokens.push_back(Token{TokenKind::SLASH, 0.0});
|
||||
++index;
|
||||
}
|
||||
else if (ch == L'^')
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::CARET, 0.0});
|
||||
++index;
|
||||
}
|
||||
else if (ch == L'(')
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::LEFT_PAREN, 0.0});
|
||||
@@ -120,16 +127,22 @@ namespace jdi
|
||||
{
|
||||
++index;
|
||||
}
|
||||
// 指数部分必须**真的带数字**才算数字的一部分:
|
||||
// `2e` 里的 `e` 是自然常数(不能把 `e` 吞进食指后当成数字 2 再用掉它)。
|
||||
if (index < length && (text[index] == L'e' || text[index] == L'E'))
|
||||
{
|
||||
++index;
|
||||
if (index < length && (text[index] == L'+' || text[index] == L'-'))
|
||||
std::size_t probe = index + 1;
|
||||
if (probe < length && (text[probe] == L'+' || text[probe] == L'-'))
|
||||
{
|
||||
++index;
|
||||
++probe;
|
||||
}
|
||||
while (index < length && text[index] >= L'0' && text[index] <= L'9')
|
||||
if (probe < length && text[probe] >= L'0' && text[probe] <= L'9')
|
||||
{
|
||||
++index;
|
||||
index = probe;
|
||||
while (index < length && text[index] >= L'0' && text[index] <= L'9')
|
||||
{
|
||||
++index;
|
||||
}
|
||||
}
|
||||
}
|
||||
const std::wstring literal = text.substr(start, index - start);
|
||||
@@ -147,7 +160,7 @@ namespace jdi
|
||||
}
|
||||
else if (std::iswalpha(static_cast<wint_t>(ch)) != 0)
|
||||
{
|
||||
// 标识符:只允许 X / x / f / f(t) / pi。
|
||||
// 标识符:只允许 X / x / f(t) 作为数据点,以及常量 e / pi。
|
||||
const std::size_t start = index;
|
||||
while (index < length && std::iswalpha(static_cast<wint_t>(text[index])) != 0)
|
||||
{
|
||||
@@ -160,6 +173,7 @@ namespace jdi
|
||||
{
|
||||
++look;
|
||||
}
|
||||
bool hasArgument = false;
|
||||
if (look < length && text[look] == L'(')
|
||||
{
|
||||
std::size_t inner = look + 1;
|
||||
@@ -176,6 +190,7 @@ namespace jdi
|
||||
}
|
||||
if (inner < length && text[inner] == L')')
|
||||
{
|
||||
hasArgument = true;
|
||||
index = inner + 1;
|
||||
}
|
||||
}
|
||||
@@ -184,7 +199,7 @@ namespace jdi
|
||||
{
|
||||
letter = static_cast<wchar_t>(std::towlower(static_cast<wint_t>(letter)));
|
||||
}
|
||||
if (name == L"x" || name == L"f")
|
||||
if (name == L"x" || (name == L"f" && hasArgument))
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::VARIABLE, 0.0});
|
||||
}
|
||||
@@ -192,11 +207,15 @@ namespace jdi
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::NUMBER, 3.14159265358979323846});
|
||||
}
|
||||
else if (name == L"e")
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::NUMBER, 2.71828182845904523536});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (error != nullptr)
|
||||
{
|
||||
*error = L"无法识别的标识符(自变量请写 f(t) 或 X)。";
|
||||
*error = L"无法识别的标识符(数据点请写 f(t) 或 X;常量只支持 e / pi)。";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -321,7 +340,34 @@ namespace jdi
|
||||
m_output.push_back(FormulaOp{FormulaOp::Kind::NEGATE, 0.0});
|
||||
return true;
|
||||
}
|
||||
return ParsePrimary();
|
||||
return ParsePower();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 幂运算(优先级高于乘除、低于一元正负号;**右结合**)。
|
||||
*
|
||||
* `power := primary ('^' unary)?`
|
||||
* * `2^3^2` = 2^(3^2)(右结合);
|
||||
* * `2^-1` 允许(指数的符号);
|
||||
* * `-2^2` = -(2^2)(一元负号作用于整个幂)。
|
||||
*/
|
||||
bool ParsePower()
|
||||
{
|
||||
if (!ParsePrimary())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Current().kind != TokenKind::CARET)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
Advance();
|
||||
if (!ParseUnary())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_output.push_back(FormulaOp{FormulaOp::Kind::POWER, 0.0});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParsePrimary()
|
||||
@@ -365,10 +411,9 @@ namespace jdi
|
||||
/** 编译结果(逆波兰式);空表示"不可用"。 */
|
||||
using Program = std::vector<FormulaOp>;
|
||||
|
||||
/** 线程内的单条编译缓存(按 data 指针 + 内容判定)。 */
|
||||
/** 线程内的单条编译缓存(按内容判定)。 */
|
||||
struct FormulaCache
|
||||
{
|
||||
const wchar_t* data = nullptr;
|
||||
std::size_t size = 0;
|
||||
std::wstring text;
|
||||
Program program;
|
||||
@@ -387,19 +432,16 @@ namespace jdi
|
||||
return nullptr;
|
||||
}
|
||||
static thread_local FormulaCache cache;
|
||||
if (cache.data == text.data() && cache.size == text.size() && cache.compiled)
|
||||
// ⚠️⚠️ **必须比较内容,不能只比 `data()` 指针 + 长度**:`std::wstring` 的短串走
|
||||
// **SSO**(缓冲区就在对象内部),把同一个样式对象的公式改成**等长**的另一条时,
|
||||
// `data()` 与 `size()` 都不会变 ⇒ 只比指针会一直复用**旧程序**
|
||||
//(实测:`f(t)^2` 改成 `f(t)^3` 后波形不更新,用户无法察觉公式已失效)。
|
||||
// 比较顺序:先比长度(最便宜),再比内容(短公式只是十几字节的 memcmp)。
|
||||
if (cache.compiled && cache.size == text.size() && cache.text == text)
|
||||
{
|
||||
return cache.valid ? &cache.program : nullptr;
|
||||
}
|
||||
// 指针变化:先比内容,内容相同则直接复用(避免同一公式被多个样式副本反复编译)。
|
||||
if (cache.compiled && cache.text == text)
|
||||
{
|
||||
cache.data = text.data();
|
||||
cache.size = text.size();
|
||||
return cache.valid ? &cache.program : nullptr;
|
||||
}
|
||||
cache.text = text;
|
||||
cache.data = text.data();
|
||||
cache.size = text.size();
|
||||
cache.program.clear();
|
||||
cache.valid = false;
|
||||
@@ -498,6 +540,9 @@ namespace jdi
|
||||
case FormulaOp::Kind::DIVIDE:
|
||||
result = a / b;
|
||||
break;
|
||||
case FormulaOp::Kind::POWER:
|
||||
result = std::pow(a, b);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -567,14 +612,21 @@ namespace jdi
|
||||
{
|
||||
letter = static_cast<wchar_t>(std::towlower(static_cast<wint_t>(letter)));
|
||||
}
|
||||
if (hasArgument && (lowered == L"f" || lowered == L"x"))
|
||||
// 数据点:`X` / `x` / `f(t)`(含 `F(T)`)一律写成规范形式 `f(t)`。
|
||||
// ⚠️ **单独的 `f` 不归一化**:它不是合法写法(用户 2026-11-03 明确要求),
|
||||
// 原样输出交给 `ValidateFormula` 报错,而不是悄悄当成 `f(t)` 接受。
|
||||
if (lowered == L"x" || (lowered == L"f" && hasArgument))
|
||||
{
|
||||
// 统一写成小写规范形式 `f(t)`(保留用户其余写法)。
|
||||
output += L"f(t)";
|
||||
}
|
||||
else if (!hasArgument && (lowered == L"x" || lowered == L"f"))
|
||||
// 常量统一为小写(`E` / `Pi` / `PI` → `e` / `pi`)。
|
||||
else if (lowered == L"e")
|
||||
{
|
||||
output += L"f(t)";
|
||||
output += L"e";
|
||||
}
|
||||
else if (lowered == L"pi")
|
||||
{
|
||||
output += L"pi";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user