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

+60
View File
@@ -1794,6 +1794,47 @@ namespace jdi
std::vector<gd::PointF>& band, std::vector<std::size_t>& bandBreaks,
bool& bandMode, bool& bandSpiky) const;
/**
* @brief 「公式映射」信号的逐分组包络缓存条目(见 `AcquireFormulaEnvelope`)。
*
* 键 = 信号 + 采样数组 + 采样数 + 公式文本 + 影响数值的样式字段。
* `minimums` / `maximums` 存的是**已经套用完整绘制变换**(公式 → 系数 → 绝对值 →
* 镜像 → Y 偏移)之后、每 `stride` 个采样一个分组的极值(无有效值的分组为 NaN)。
*/
struct FormulaEnvelopeEntry
{
const void* signal = nullptr; /**< `resolved.data`(区分不同信号)。 */
const void* values = nullptr; /**< `values.data()`(区分重新加载的数据)。 */
std::size_t count = 0; /**< 参与统计的采样数。 */
std::size_t stride = 0; /**< 每个分组覆盖的采样数。 */
std::wstring formula; /**< 公式文本(空 = 不适用)。 */
float gain = 1.0f;
bool absolute = false;
bool mirrorVertical = false;
float offsetY = 0.0f;
std::vector<double> minimums; /**< 每组的绘制值下限。 */
std::vector<double> maximums; /**< 每组的绘制值上限。 */
bool valid = false; /**< 是否已成功建立。 */
};
/**
* @brief 取得(必要时建立)「公式映射」信号的逐分组包络。
*
* ⚠️⚠️ **为什么必须单独建一层**:`SignalData::coarseMin/coarseMax` 只存**原始采样**的
* 分组极值,只有在数值变换**保序**时才能直接缩放使用(见 `DrawTransformOrderPreserving`)。
* 公式(如 `f(t)^2`、`1/f(t)`)不保序,旧实现在这种情况会退化成**逐点扫描整条信号**:
* `VERY_BIG_DATA` 单条 2580 万采样时,一次折线构建 ≈ 850ms(缩放一帧就卡一次)。
* 这里把“变换后的分组极值”算**一次**缓存起来,之后每帧/每次缩放只按分组聚合
* (几十微秒),且极值仍是逐点统计出来的 ⇒ **不会漏掉尖峰**。
*
* 只在公式信号上使用;无公式时返回 nullptr(继续走既有的 `coarseMin/Max` 快路径)。
*
* @param [IN] resolved 已解析的信号视图。
* @return 缓存条目指针(内容有效);不适用或无法建立时返回 nullptr。
*/
const FormulaEnvelopeEntry* AcquireFormulaEnvelope(const ResolvedSignal& resolved) const;
/**
* @brief 绘制单个网格。
* @param [IN,OUT] g 绘图上下文。
@@ -3183,6 +3224,18 @@ namespace jdi
std::vector<gd::RectF> m_cursorLabelRects; /**< 本次绘制已占用的取值标注矩形(用于避让重叠)。 */
std::vector<LaneVisual> m_lanes; /**< 当前网格绘制信息集合。 */
/**
* @brief 「网格幅值范围」缓存指纹(见 `BuildLaneVisuals`)。
*
* `ComputeLaneRange()` 对启用了「公式映射」的信号必须**逐点扫描整条信号**
*(公式不保证单调,端点映射会算错)。而它在 `BuildLaneVisuals` 里**每帧**为
* 每个网格各调一次 ⇒ 千万级采样时每帧几百毫秒。范围只依赖「可见信号 + 数值样式 +
* 数据本身」,与视界无关,因此按指纹缓存:指纹一致就复用 `m_laneRangeYMin/Max`。
*/
std::uint64_t m_laneRangeKey = 0;
std::vector<double> m_laneRangeYMin; /**< 各网格缓存的 yMin(与上述指纹配套)。 */
std::vector<double> m_laneRangeYMax; /**< 各网格缓存的 yMax(与上述指纹配套)。 */
/**
* @brief 「边沿统计」的累积状态(普通边沿计数与中心轴边沿计数**共用**)。
*
@@ -3519,6 +3572,13 @@ namespace jdi
const LaneVisual& lane) const;
mutable std::deque<PolylineCacheEntry> m_polylineCache; /**< 折线顶点缓存(按几何键复用)。 */
/**
* @brief 「公式映射」信号的逐分组包络缓存(见 `AcquireFormulaEnvelope`)。
*
* 只在信号带公式时建立;条目键含公式文本与影响数值的样式字段,
* 公式 / 系数 / 镜像等变化时自然失效。用 `deque` 保证追加时已有引用不失效。
*/
mutable std::deque<FormulaEnvelopeEntry> m_formulaEnvelopes;
/**
* @brief 折线顶点缓存的**容量上限**(条目数)。
*
+37 -2
View File
@@ -607,6 +607,18 @@ namespace jdi
inline const double kResampleIntervalMin = 1.0e-9;
inline const double kResampleIntervalMax = 1.0e6;
// ---- 「公式映射」的分组包络缓存(见 `PlotView::AcquireFormulaEnvelope`)----
/**
* @brief 每个信号的包络分组数上限(分组宽度按采样数反算)。
*
* 65536 组 × 2(min/max)× 4B ≈ 512KB/信号;对 2580 万采样 ⇒ 每组 ~394 个采样,
* 而屏幕上一个像素列覆盖上万采样 ⇒ 分组粒度造成的包络误差远小于 1 像素。
*/
inline const std::size_t kFormulaEnvelopeBuckets = 65536;
/** @brief 包络缓存最多保存的信号数(超出即整表清空,避免无界增长)。 */
inline const std::size_t kFormulaEnvelopeCacheLimit = 16;
// ---- 「均值滤波」面板的两行参数滑块(用户 2026-10-30 要求)----
/**
* 标签一律**不带 `X` / `N`**:滑块的量程本身已经说明是数值,符号只会让标签变长。
@@ -1294,11 +1306,34 @@ namespace jdi
return FormatValue(seconds) + L"s";
}
/**
* @brief 把时间间隔四舍五入到 **3 位有效数字**(供「重采样」面板显示)。
*
* 计算出来的数据间隔常常带一长串浮点尾数(如 `5.0000158us`)。整串显示既没有意义,
* 又会把面板撑得很宽(还要在后面追加「(频率)」尾缀,用户 2026-11-03 要求)。
* 3 位有效数字足以区分相邻的采样间隔,且让显示稳定。
*
* @param [IN] seconds 时间间隔(秒)。
* @return 舍入后的间隔;非法/非正输入原样返回。
*/
inline double RoundIntervalForDisplay(double seconds)
{
if (!std::isfinite(seconds) || seconds <= 0.0)
{
return seconds;
}
const double magnitude = std::floor(std::log10(seconds));
const double scale = std::pow(10.0, 2.0 - magnitude);
const double rounded = std::round(seconds * scale) / scale;
return (rounded > 0.0) ? rounded : seconds;
}
/**
* @brief 时间间隔的显示文本(**不带** "Δt:" 前缀)。
*
* 「重采样」面板的「最大 / 最小数据间隔」两行用它:单位随间隔长短自适应
*(`FormatDurationCompound`:1h 1min 2s / 5s 60ms / 120us …)。
*(`FormatDurationCompound`:1h 1min 2s / 5s 60ms / 120us …),
* 且先舍入到 3 位有效数字(见 `RoundIntervalForDisplay`),**不出现科学计数法**。
*
* @param [IN] seconds 时间间隔(秒)。
* @return 文本。
@@ -1309,7 +1344,7 @@ namespace jdi
{
return L"—";
}
return FormatDurationCompound(seconds);
return FormatDurationCompound(RoundIntervalForDisplay(seconds));
}
/**
+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
@@ -2474,7 +2474,7 @@ namespace jdi
{
continue;
}
// 点击「重采样间隔」输入框 → 编辑秒数。
// 点击「重采样间隔」输入框 → 编辑**秒**数(不带单位、不用科学计数法)。
if (rowIndex < layout.rowIntervalField.size() &&
layout.rowIntervalField[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowIntervalField[rowIndex], fx, fy))
@@ -2483,13 +2483,11 @@ namespace jdi
m_editMeanKind = kind;
const gd::RectF& field = layout.rowIntervalField[rowIndex];
BeginEdit(PlotEditTarget::RESAMPLE_INTERVAL, index,
FormatValue(item->interval), field);
m_edit.Begin(FormatValue(item->interval), true, EditFilter::NUMBER,
0u);
FormatDurationSeconds(item->interval), field);
m_dragMode = PlotDragMode::NONE;
return true;
}
// 点击「重采样频率」输入框 → 编辑频率(Hz,与间隔联动)。
// 点击「重采样频率」输入框 → 编辑频率(Hz,与间隔联动;不带单位)。
if (rowIndex < layout.rowFrequencyField.size() &&
layout.rowFrequencyField[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowFrequencyField[rowIndex], fx, fy))
@@ -2498,9 +2496,8 @@ namespace jdi
m_editMeanKind = kind;
const gd::RectF& field = layout.rowFrequencyField[rowIndex];
const double hz = (item->interval > 0.0) ? (1.0 / item->interval) : 0.0;
BeginEdit(PlotEditTarget::RESAMPLE_FREQUENCY, index, FormatValue(hz),
field);
m_edit.Begin(FormatValue(hz), true, EditFilter::NUMBER, 0u);
BeginEdit(PlotEditTarget::RESAMPLE_FREQUENCY, index,
FormatDecimal(hz, 12), field);
m_dragMode = PlotDragMode::NONE;
return true;
}
+134 -13
View File
@@ -210,6 +210,84 @@ namespace jdi
return &m_polylineCache.back();
}
const PlotView::FormulaEnvelopeEntry* PlotView::AcquireFormulaEnvelope(
const ResolvedSignal& resolved) const
{
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.style->HasFormula())
{
return nullptr;
}
const std::vector<double>& values = resolved.data->values;
const std::size_t count = values.size();
if (count == 0)
{
return nullptr;
}
const SignalStyle& style = *resolved.style;
const void* signalKey = static_cast<const void*>(resolved.data);
const void* valuesKey = static_cast<const void*>(values.data());
// 命中缓存:公式与全部影响数值的样式字段都一致时可直接复用。
for (const FormulaEnvelopeEntry& entry : m_formulaEnvelopes)
{
if (entry.valid && entry.signal == signalKey && entry.values == valuesKey &&
entry.count == count && entry.formula == style.formula && entry.gain == style.gain &&
entry.absolute == style.absolute && entry.mirrorVertical == style.mirrorVertical &&
entry.offsetY == style.offsetY)
{
return &entry;
}
}
// 未命中:建立新条目(容量上限很小:只有“带公式”的信号才会进来)。
if (m_formulaEnvelopes.size() >= kFormulaEnvelopeCacheLimit)
{
m_formulaEnvelopes.clear();
}
m_formulaEnvelopes.emplace_back();
FormulaEnvelopeEntry& entry = m_formulaEnvelopes.back();
entry.signal = signalKey;
entry.values = valuesKey;
entry.count = count;
entry.formula = style.formula;
entry.gain = style.gain;
entry.absolute = style.absolute;
entry.mirrorVertical = style.mirrorVertical;
entry.offsetY = style.offsetY;
// 分组宽度:让分组数落在 `kFormulaEnvelopeBuckets` 量级(内存 = 2 × 分组数 × 4B)。
entry.stride = std::max<std::size_t>(
1, (count + kFormulaEnvelopeBuckets - 1) / kFormulaEnvelopeBuckets);
const std::size_t buckets = (count + entry.stride - 1) / entry.stride;
entry.minimums.assign(buckets, std::numeric_limits<double>::quiet_NaN());
entry.maximums.assign(buckets, std::numeric_limits<double>::quiet_NaN());
// ⚠️ **一次**全量扫描:逐点套用完整绘制变换后按分组取极值。
// 之后每帧 / 每次缩放都只按分组聚合,代价与采样数无关。
for (std::size_t index = 0; index < count; ++index)
{
const double mapped = ApplyDrawTransform(style, values[index]);
if (!std::isfinite(mapped))
{
continue;
}
const std::size_t bucket = index / entry.stride;
const double value = mapped;
double& low = entry.minimums[bucket];
double& high = entry.maximums[bucket];
if (!(low <= high))
{
// 该分组第一次写入(初值是 NaN)。
low = value;
high = value;
continue;
}
low = std::min(low, value);
high = std::max(high, value);
}
entry.valid = true;
return &entry;
}
void PlotView::BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane,
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks,
std::vector<gd::PointF>& band, std::vector<std::size_t>& bandBreaks,
@@ -356,19 +434,38 @@ namespace jdi
// 粗粒度包络层(若已构建):每列只需读取覆盖到的分组,
// 数据量从“可见采样数”降到“像素列数 × 少量分组”,
// 且分组内存的就是真实极值,绘制结果与逐点扫描完全一致。
const std::vector<double>& coarseMin = resolved.data->coarseMin;
const std::vector<double>& coarseMax = resolved.data->coarseMax;
const std::size_t coarseStride = resolved.data->coarseStride;
const std::vector<double>* coarseMinPtr = &resolved.data->coarseMin;
const std::vector<double>* coarseMaxPtr = &resolved.data->coarseMax;
std::size_t coarseStride = resolved.data->coarseStride;
// ⚠️ 粗粒度分组只存了逐组的 min/max:取绝对值 / 垂直镜像会把两者互换,
// 此时分组层无法再给出正确的包络,必须回退到逐采样点变换(见 `DrawTransformOrderPreserving`)。
// Y 偏移是保序的(整体加常数),因此仍可用分组层 —— 只需把偏移一并加上。
const bool hasCoarse =
(coarseStride > 0) && !coarseMin.empty() && (coarseMin.size() == coarseMax.size()) &&
DrawTransformOrderPreserving(*resolved.style);
bool hasCoarse = (coarseStride > 0) && !coarseMinPtr->empty() &&
(coarseMinPtr->size() == coarseMaxPtr->size()) &&
DrawTransformOrderPreserving(*resolved.style);
// 保序变换(正系数 + Y 偏移)下:分组极值 × 系数 + 偏移 即为变换后的极值
//(系数 1、偏移 0 时逐位不变)。
const double coarseScale = static_cast<double>(resolved.style->gain);
const double coarseOffset = static_cast<double>(resolved.style->offsetY);
double coarseScale = static_cast<double>(resolved.style->gain);
double coarseOffset = static_cast<double>(resolved.style->offsetY);
if (!hasCoarse)
{
// ⚠️⚠️ **「公式映射」不能用 `coarseMin/Max` 直接缩放**(公式不保序),
// 但也不能因此退回「逐点扫描整条信号」:2580 万采样时一次折线构建 ≈850ms,
// 缩放 / 平移每重建一帧就卡一次(用户 2026-11-03 报告)。
// 这里改用**变换后的分组包络**(按需算一次并缓存):极值仍是逐点统计的
// ⇒ 不会漏尖峰,而每帧只按分组聚合。
if (const FormulaEnvelopeEntry* envelope = AcquireFormulaEnvelope(resolved))
{
coarseMinPtr = &envelope->minimums;
coarseMaxPtr = &envelope->maximums;
coarseStride = envelope->stride;
coarseScale = 1.0; // 变换已经烘进包络
coarseOffset = 0.0;
hasCoarse = true;
}
}
const std::vector<double>& coarseMin = *coarseMinPtr;
const std::vector<double>& coarseMax = *coarseMaxPtr;
bool inSegment = false;
@@ -972,11 +1069,15 @@ namespace jdi
PaintHint(g);
// 网格 Y 轴上下限的输入框、以及「弱峰调整 → 迭代次数上限」的输入框都绘制在最上层
//(后者覆盖在浮动面板的滑块行之上)。
// ⚠️ 「重采样」的两个输入框(间隔 / 频率)同样**必须**在这里绘制:面板的绘制分支
// 在编辑态会跳过输入框本体(改由本函数画),漏掉这一项就会“双击后输入框消失”。
if (m_editTarget == PlotEditTarget::LANE_Y_MAX ||
m_editTarget == PlotEditTarget::LANE_Y_MIN ||
m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX ||
m_editTarget == PlotEditTarget::MEAN_POINTS_MAX ||
m_editTarget == PlotEditTarget::MEAN_ITERATIONS_MAX)
m_editTarget == PlotEditTarget::MEAN_ITERATIONS_MAX ||
m_editTarget == PlotEditTarget::RESAMPLE_INTERVAL ||
m_editTarget == PlotEditTarget::RESAMPLE_FREQUENCY)
{
PaintEditField(g);
}
@@ -5355,15 +5456,31 @@ namespace jdi
(resampleItem->maxInterval > 0.0) && (resampleItem->minInterval > 0.0);
maxInterval = resampleItem->maxInterval;
minInterval = resampleItem->minInterval;
// 「最大 / 最小数据间隔」= 自适应单位的**小数**文本(绝不用科学计数法),
// 末尾追加一个括号内的「对应频率」尾缀(单位在 Hz/kHz/MHz/GHz 之间自适应)。
// ⚠️ 频率必须由**同一份舍入后的间隔**换算,否则两列会对不上
// (例如显示 `5us` 却给出 `199.99937 kHz`)。
const auto intervalWithFrequency = [](double seconds) -> std::wstring
{
const double shown = RoundIntervalForDisplay(seconds);
std::wstring text = FormatDurationCompound(shown);
if (std::isfinite(shown) && shown > 0.0)
{
text += L"(";
text += FormatFrequencyText(1.0 / shown);
text += L")";
}
return text;
};
if (rowIndex < layout.rowInfoMaxInterval.size())
{
paintInfoRow(layout.rowInfoMaxInterval[rowIndex], kResampleMaxIntervalLabel,
hasInterval ? FormatIntervalText(maxInterval) : L"—");
hasInterval ? intervalWithFrequency(maxInterval) : L"—");
}
if (rowIndex < layout.rowInfoMinInterval.size())
{
paintInfoRow(layout.rowInfoMinInterval[rowIndex], kResampleMinIntervalLabel,
hasInterval ? FormatIntervalText(minInterval) : L"—");
hasInterval ? intervalWithFrequency(minInterval) : L"—");
}
const double interval = (resampleItem->interval > 0.0)
? resampleItem->interval
@@ -5376,15 +5493,19 @@ namespace jdi
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
if (rowIndex < layout.rowIntervalField.size())
{
// 非编辑态:自适应单位 + 小数(与「最大/最小数据间隔」同一套单位形式)。
// 编辑态:只显示**秒**数值(不带单位),由 `PaintEditField` 绘制。
paintInputRow(layout.rowIntervalField[rowIndex], kResampleIntervalLabel,
FormatValue(interval), editingInterval);
FormatIntervalText(interval), editingInterval);
}
if (rowIndex < layout.rowFrequencyField.size())
{
const double hertz =
(interval > 0.0) ? (1.0 / interval) : 0.0;
// 非编辑态:频率单位自适应(Hz/kHz/MHz/GHz);编辑态:只显示 Hz 数值。
paintInputRow(layout.rowFrequencyField[rowIndex], kResampleFrequencyLabel,
FormatFrequencyText(hertz), editingFrequency);
(hertz > 0.0) ? FormatFrequencyText(hertz) : L"—",
editingFrequency);
}
continue;
}
+5 -2
View File
@@ -170,14 +170,17 @@ namespace jdi
{
// 「重采样」四行 = 「标签列 + 数值列」(两行统计 + 两行输入框),
// 标签取四者中最宽的一个,数值列按间隔 / 频率的典型文本宽度取。
// ⚠️ 「最大/最小数据间隔」行末尾还带「(频率)」尾缀(用户 2026-11-03 要求),
// 取值列必须按**含尾缀**的文本量宽,否则尾缀会被裁掉。
const float resampleLabelColumn =
std::max(std::max(UiMeasureTextWidth(m_fonts->uiSmall, kResampleMaxIntervalLabel),
UiMeasureTextWidth(m_fonts->uiSmall, kResampleMinIntervalLabel)),
std::max(UiMeasureTextWidth(m_fonts->uiSmall, kResampleIntervalLabel),
UiMeasureTextWidth(m_fonts->uiSmall, kResampleFrequencyLabel)));
const float resampleValueColumn =
std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"1.23456 GHz"),
UiMeasureTextWidth(m_fonts->monoSmall, L"1h 1min 2s"));
std::max(std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"1.23456 GHz"),
UiMeasureTextWidth(m_fonts->monoSmall, L"1h 1min 2s")),
UiMeasureTextWidth(m_fonts->monoSmall, L"1h 1min 2s(1.23456 GHz)"));
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + resampleLabelColumn +
8.0f * m_scale + resampleValueColumn +
(kFreeMarkerScrollWidth + 4.0f) * m_scale);