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
@@ -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 折线顶点缓存的**容量上限**(条目数)。
|
||||
*
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user