feat(resample): 实验性功能新增「重采样」(位于「自由标记」上方)
- Lane::ResampleItem + resample/resampleItems/resamplePanel/resampleScroll, 并在 LaneCarry / SplitLane / PruneLaneSignalItems / RemapAfterSignalInsert|Removal 同步 - 面板:可滚动分组列表(信号色条目栏 + 复选框 + 展开三角),展开后 4 行: 最大/最小数据间隔(只读,勾选时一次性缓存,绘制不扫描采样)+ 重采样间隔/频率(输入) - 输入框联动:改间隔同步频率,改频率同步间隔(输入单位分别为秒 / Hz) - 「构建为新信号」:DataSet::AppendGroupAxis 追加新时间轴 + InsertDerivedSignal, 采样值先按公式/系数/绝对值/镜像/Y偏移变换再等间隔线性插值,时间轴偏移不烘值 - 新信号名 <原信号> (重采样 <自适应单位频率>);Utf8Util::FormatFrequency 为唯一实现 - 非单调轴 / 点太少 / 间隔过大 / 点数超 2000 万 ⇒ 拒绝构建并给出自绘提示 - 顺带加入 FormulaEval(公式编译/求值,供 ApplyDrawTransform 与后续 UI 使用)
This commit is contained in:
1 parent
30e9d65c30
commit
1e1c2432ae
19 files changed
+1979
-56
No files matched your search
@@ -323,6 +323,20 @@ namespace jdi
|
||||
*/
|
||||
bool BuildDerivedSignal(const DerivedSignalRequest& request, bool makeVisible);
|
||||
|
||||
/**
|
||||
* @brief 把一条信号按固定间隔重采样,并插入为一个新信号(带独立的新时间轴)。
|
||||
*
|
||||
* 采样值先按该信号的「公式映射 + 系数 / 绝对值 / 垂直镜像 / Y 偏移」变换(与绘制一致,
|
||||
* 时间轴偏移除外),再在 `[tMin, tMax]` 上按 `interval` 等间隔线性插值。
|
||||
* 新信号名 = `<原信号> (重采样 <自适应单位的频率>)`,插入到源信号下方。
|
||||
*
|
||||
* @param [IN] groupIndex 源信号所属分组下标。
|
||||
* @param [IN] signalIndex 源信号下标。
|
||||
* @param [IN] interval 重采样间隔(秒,> 0)。
|
||||
* @return 构建并插入成功返回 true。
|
||||
*/
|
||||
bool BuildResampledSignal(int groupIndex, int signalIndex, double interval);
|
||||
|
||||
/**
|
||||
* @brief 把一条信号插入到「源信号」的下方,并同步各标签页的引用与可见性。
|
||||
*
|
||||
|
||||
@@ -775,6 +775,173 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::BuildResampledSignal(int groupIndex, int signalIndex, double interval)
|
||||
{
|
||||
const SignalGroup* group = m_dataSet.GroupAt(groupIndex);
|
||||
if (group == nullptr || signalIndex < 0 ||
|
||||
signalIndex >= static_cast<int>(group->signals.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!std::isfinite(interval) || interval <= 0.0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const SignalData& source = group->signals[static_cast<std::size_t>(signalIndex)];
|
||||
const TimeAxis& axis = group->AxisOf(source);
|
||||
const std::wstring baseName = source.name;
|
||||
|
||||
// 0. 快照源信号的「调整」变换(插入会移动信号存储,之后取 `source` 会悬垂)。
|
||||
SignalStyle sourceStyle;
|
||||
bool hasSourceStyle = false;
|
||||
{
|
||||
const SignalStyle* style = m_dataSet.Style(SignalRef{groupIndex, signalIndex});
|
||||
if (style != nullptr)
|
||||
{
|
||||
sourceStyle = *style;
|
||||
hasSourceStyle = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 收集「变换后」的采样点(升序)。变换与绘制一致:
|
||||
// 公式映射 → 系数 → 绝对值 → 垂直镜像 → Y 偏移(时间轴偏移不烘值)。
|
||||
const std::size_t count = std::min(axis.time.size(), source.values.size());
|
||||
std::vector<double> sourceTimes;
|
||||
std::vector<double> sourceValues;
|
||||
sourceTimes.reserve(count);
|
||||
sourceValues.reserve(count);
|
||||
bool monotonic = true;
|
||||
double previous = 0.0;
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
const double time = axis.time[index];
|
||||
const double value = source.values[index];
|
||||
if (!std::isfinite(time) || !std::isfinite(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!sourceTimes.empty())
|
||||
{
|
||||
if (time < previous)
|
||||
{
|
||||
monotonic = false;
|
||||
break;
|
||||
}
|
||||
if (time == previous)
|
||||
{
|
||||
continue; // 重复时刻只保留第一个
|
||||
}
|
||||
}
|
||||
previous = time;
|
||||
sourceTimes.push_back(time);
|
||||
sourceValues.push_back(hasSourceStyle ? ApplyDrawTransform(sourceStyle, value) : value);
|
||||
}
|
||||
if (!monotonic || sourceTimes.size() < 2)
|
||||
{
|
||||
m_plot.ShowPersistentHint(!monotonic ? L"重采样失败:该信号的时间轴不是单调递增的。"
|
||||
: L"重采样失败:有效采样点不足两个。");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 生成目标时间刻度(等间隔)。
|
||||
const double t0 = sourceTimes.front();
|
||||
const double t1 = sourceTimes.back();
|
||||
const double span = t1 - t0;
|
||||
if (!(span > 0.0))
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"重采样失败:该信号的时间跨度为 0。");
|
||||
return false;
|
||||
}
|
||||
// 采样点数上限:防止用户把间隔输得太小导致内存爆炸。
|
||||
constexpr double kMaxResamplePoints = 20000000.0;
|
||||
const double stepsExact = std::floor(span / interval + 1.0e-9) + 1.0;
|
||||
if (!(stepsExact >= 2.0))
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"重采样失败:重采样间隔大于信号时间跨度。");
|
||||
return false;
|
||||
}
|
||||
if (stepsExact > kMaxResamplePoints)
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"重采样失败:目标采样点过多(超过 2000 万),请增大重采样间隔。");
|
||||
return false;
|
||||
}
|
||||
const std::size_t targetCount = static_cast<std::size_t>(stepsExact);
|
||||
|
||||
TimeAxis newAxis;
|
||||
newAxis.name = axis.name;
|
||||
newAxis.time.resize(targetCount);
|
||||
newAxis.monotonic = true;
|
||||
std::vector<double> resampled(targetCount);
|
||||
std::size_t cursor = 0;
|
||||
const std::size_t last = sourceTimes.size() - 1;
|
||||
for (std::size_t index = 0; index < targetCount; ++index)
|
||||
{
|
||||
const double time = std::min(t1, t0 + static_cast<double>(index) * interval);
|
||||
newAxis.time[index] = time;
|
||||
// 单调推进游标(O(n) 总代价)。
|
||||
while (cursor < last && sourceTimes[cursor + 1] < time)
|
||||
{
|
||||
++cursor;
|
||||
}
|
||||
if (cursor >= last)
|
||||
{
|
||||
resampled[index] = sourceValues[last];
|
||||
continue;
|
||||
}
|
||||
const double ta = sourceTimes[cursor];
|
||||
const double tb = sourceTimes[cursor + 1];
|
||||
if (time <= ta)
|
||||
{
|
||||
resampled[index] = sourceValues[cursor];
|
||||
continue;
|
||||
}
|
||||
if (tb <= ta)
|
||||
{
|
||||
resampled[index] = sourceValues[cursor];
|
||||
continue;
|
||||
}
|
||||
const double ratio = (time - ta) / (tb - ta);
|
||||
const double a = sourceValues[cursor];
|
||||
const double b = sourceValues[cursor + 1];
|
||||
resampled[index] = a + (b - a) * ratio;
|
||||
}
|
||||
newAxis.minTime = t0;
|
||||
newAxis.maxTime = newAxis.time.back();
|
||||
newAxis.finiteCount = targetCount;
|
||||
|
||||
// 3. 追加新时间轴(重采样后的采样刻度无法复用源信号的时间轴)。
|
||||
std::size_t axisIndex = 0;
|
||||
if (!m_dataSet.AppendGroupAxis(groupIndex, std::move(newAxis), axisIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 组装新信号并插入到源信号下方。
|
||||
SignalData derived;
|
||||
derived.name = baseName + L" (重采样 " + FormatFrequency(1.0 / interval) + L")";
|
||||
derived.axisIndex = axisIndex;
|
||||
derived.values = std::move(resampled);
|
||||
|
||||
int newIndex = -1;
|
||||
if (!InsertSignalBelowSource(groupIndex, signalIndex, std::move(derived), true, newIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. 时间轴偏移**不烘值**:留在新信号的样式上(与其它派生信号同一规则)。
|
||||
if (hasSourceStyle && sourceStyle.offsetTime != 0.0)
|
||||
{
|
||||
if (SignalStyle* derivedStyle = m_dataSet.Style(SignalRef{groupIndex, newIndex}))
|
||||
{
|
||||
derivedStyle->offsetTime = sourceStyle.offsetTime;
|
||||
}
|
||||
}
|
||||
|
||||
SyncTabsToDataSet();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::InsertSignalBelowSource(int groupIndex, int sourceIndex, SignalData&& signal,
|
||||
bool makeVisible, int& newIndex)
|
||||
{
|
||||
@@ -902,6 +1069,43 @@ namespace jdi
|
||||
|
||||
bool Application::ApplyPendingDerivedBuild()
|
||||
{
|
||||
// 「重采样」的构建流程与均值/弱峰不同(要新建时间轴),单独处理。
|
||||
{
|
||||
int laneIndex = -1;
|
||||
std::vector<PlotView::ResampleBuild> items;
|
||||
if (m_plot.TakePendingResampleBuild(laneIndex, items) && !items.empty())
|
||||
{
|
||||
// **同一文件内必须按下标降序处理**(每次插入只让插入点之后的信号下标 +1)。
|
||||
std::sort(items.begin(), items.end(),
|
||||
[](const PlotView::ResampleBuild& a, const PlotView::ResampleBuild& b)
|
||||
{
|
||||
if (a.groupIndex != b.groupIndex)
|
||||
{
|
||||
return a.groupIndex < b.groupIndex;
|
||||
}
|
||||
return a.signalIndex > b.signalIndex;
|
||||
});
|
||||
int built = 0;
|
||||
double lastInterval = 0.0;
|
||||
for (const PlotView::ResampleBuild& item : items)
|
||||
{
|
||||
if (BuildResampledSignal(item.groupIndex, item.signalIndex, item.interval))
|
||||
{
|
||||
++built;
|
||||
lastInterval = item.interval;
|
||||
}
|
||||
}
|
||||
if (built > 0)
|
||||
{
|
||||
SetStatusMessage(L"已构建 " + std::to_wstring(built) + L" 条重采样信号(频率 " +
|
||||
FormatFrequency(lastInterval > 0.0 ? 1.0 / lastInterval : 0.0) +
|
||||
L")。",
|
||||
false);
|
||||
}
|
||||
return built > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 两类「构建为新信号」请求(均值滤波 / 弱峰调整)机制完全一致,
|
||||
// 统一整理成同一份构建清单后按下标降序处理。
|
||||
std::vector<DerivedSignalRequest> requests;
|
||||
@@ -1127,6 +1331,13 @@ namespace jdi
|
||||
freeMarker.label = L"自由标记";
|
||||
freeMarker.id = PLOT_CMD_LANE_FREE_MARKER;
|
||||
freeMarker.checked = m_plot.LaneFreeMarker(laneIndex);
|
||||
|
||||
// 「重采样」位于「自由标记」**上方**(用户 2026-11-02 要求)。
|
||||
MenuItem resample;
|
||||
resample.label = L"重采样";
|
||||
resample.id = PLOT_CMD_LANE_RESAMPLE;
|
||||
resample.checked = m_plot.LaneResample(laneIndex);
|
||||
experimental.submenu.push_back(resample);
|
||||
experimental.submenu.push_back(freeMarker);
|
||||
}
|
||||
items.push_back(experimental);
|
||||
@@ -1355,6 +1566,15 @@ namespace jdi
|
||||
case MenuScope::SIGNAL_PANEL:
|
||||
{
|
||||
const bool needRelayout = m_panel.OnMenuCommand(result);
|
||||
// 「公式映射」等就地输入可能产生**非模态**警告(不使用 Windows 消息框),
|
||||
// 由绘图区底部的自绘提示浮层显示(与加载失败提示同一套)。
|
||||
{
|
||||
std::wstring hint;
|
||||
if (m_panel.TakePendingHint(hint))
|
||||
{
|
||||
m_plot.ShowPersistentHint(hint);
|
||||
}
|
||||
}
|
||||
if (needRelayout)
|
||||
{
|
||||
Relayout();
|
||||
@@ -1418,6 +1638,10 @@ namespace jdi
|
||||
// 「实验性功能 → 自由标记」:切换网格内跟随鼠标的纵向光标与固定数据点面板。
|
||||
m_plot.ToggleLaneFreeMarker(laneIndex);
|
||||
break;
|
||||
case PLOT_CMD_LANE_RESAMPLE:
|
||||
// 「实验性功能 → 重采样」:切换重采样参数面板。
|
||||
m_plot.ToggleLaneResample(laneIndex);
|
||||
break;
|
||||
case PLOT_CMD_LANE_SPLIT:
|
||||
// 「拆分当前网格」:信号按个拆成独立网格,新网格顺次插入其下方。
|
||||
m_plot.SplitLane(laneIndex);
|
||||
|
||||
@@ -65,7 +65,8 @@ namespace jdi
|
||||
MARKER_SCROLL, /**< 拖动「自由标记」面板内容的滚动条滑块。 */
|
||||
MEAN_POINTS, /**< 拖动「均值滤波」某信号的「均值点数」滑块(0 … 该信号自定义上限)。 */
|
||||
MEAN_ITERATIONS,/**< 拖动「均值滤波」某信号的「迭代次数」滑块(1 … 该信号自定义上限)。 */
|
||||
MEAN_SCROLL /**< 拖动「均值滤波」面板内容的滚动条滑块。 */
|
||||
MEAN_SCROLL,/**< 拖动「均值滤波」面板内容的滚动条滑块。 */
|
||||
RESAMPLE_SCROLL /**< 拖动「重采样」面板内容的滚动条滑块。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -103,6 +104,8 @@ namespace jdi
|
||||
MEAN_POINTS_MAX,
|
||||
/** @brief 编辑「均值滤波」某信号「迭代次数」滑块的**上限**(正整数)。 */
|
||||
MEAN_ITERATIONS_MAX,
|
||||
RESAMPLE_INTERVAL, /**< 编辑「重采样」某信号的**重采样间隔**(秒)。 */
|
||||
RESAMPLE_FREQUENCY, /**< 编辑「重采样」某信号的**重采样频率**(Hz,与间隔联动)。 */
|
||||
FREE_MARKER_TIME, /**< 编辑「自由标记」某固定点的**自身时间**(秒)。 */
|
||||
FREE_MARKER_DELTA /**< 编辑「自由标记」某固定点相对前一固定点的**时间间隔**(秒)。 */
|
||||
};
|
||||
@@ -123,7 +126,8 @@ namespace jdi
|
||||
PLOT_CMD_LANE_SPLIT = 9, /**< 拆分当前网格:把网格内信号按个拆成独立网格并顺次下移编号。 */
|
||||
PLOT_CMD_LANE_MEAN_FILTER = 10, /**< 切换本网格的「实验性功能 → 均值滤波」开关。 */
|
||||
PLOT_CMD_LANE_WEAK_PEAK = 11, /**< 切换本网格的「实验性功能 → 弱峰调整」开关。 */
|
||||
PLOT_CMD_LANE_FREE_MARKER = 12 /**< 切换本网格的「实验性功能 → 自由标记」开关。 */
|
||||
PLOT_CMD_LANE_FREE_MARKER = 12, /**< 切换本网格的「实验性功能 → 自由标记」开关。 */
|
||||
PLOT_CMD_LANE_RESAMPLE = 13 /**< 切换本网格的「实验性功能 → 重采样」开关。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -135,7 +139,8 @@ namespace jdi
|
||||
CENTER = 1, /**< 边沿计数(中心轴)。 */
|
||||
MEAN = 2, /**< 均值滤波。 */
|
||||
PEAK = 3, /**< 弱峰调整。 */
|
||||
MARK = 4 /**< 自由标记。 */
|
||||
MARK = 4, /**< 自由标记。 */
|
||||
RESAMPLE = 5 /**< 重采样(把非固定频率采样规整为固定频率)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1022,6 +1027,66 @@ namespace jdi
|
||||
*/
|
||||
bool ToggleLaneFreeMarker(int laneIndex);
|
||||
|
||||
/**
|
||||
* @brief 「重采样 → 构建为新信号」的单个请求(一个被勾选的信号 + 自己的间隔)。
|
||||
*/
|
||||
struct ResampleBuild
|
||||
{
|
||||
int groupIndex = -1; /**< 信号所属分组下标。 */
|
||||
int signalIndex = -1; /**< 组内信号下标。 */
|
||||
double interval = 0.0; /**< 重采样间隔(秒,> 0)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 取出「重采样 → 构建为新信号」请求(由面板底部按钮触发)。
|
||||
* @param [OUT] laneIndex 触发该请求的网格下标。
|
||||
* @param [OUT] items 需要构建的信号及其间隔(按信号在网格内的顺序)。
|
||||
* @return 存在待处理请求返回 true(取出后清空)。
|
||||
*/
|
||||
bool TakePendingResampleBuild(int& laneIndex, std::vector<ResampleBuild>& items);
|
||||
|
||||
/**
|
||||
* @brief 开启 / 关闭某网格的「重采样」功能。
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
* @param [IN] enabled 是否开启。
|
||||
* @return 状态发生变化返回 true。
|
||||
*/
|
||||
bool SetLaneResample(int laneIndex, bool enabled);
|
||||
|
||||
/** @brief 查询某网格的「重采样」开关。 */
|
||||
bool LaneResample(int laneIndex) const;
|
||||
|
||||
/**
|
||||
* @brief 切换某网格的「重采样」开关。
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
* @return 切换后的状态。
|
||||
*/
|
||||
bool ToggleLaneResample(int laneIndex);
|
||||
|
||||
/** @brief 切换某信号是否参与重采样(面板条目栏右侧复选框)。 */
|
||||
bool ToggleLaneResampleSignal(int laneIndex, const SignalRef& ref);
|
||||
|
||||
/** @brief 查询某信号是否参与重采样。 */
|
||||
bool LaneResampleSignalEnabled(int laneIndex, const SignalRef& ref) const;
|
||||
|
||||
/**
|
||||
* @brief 设置「重采样」面板内容的滚动偏移(自动夹到合法范围)。
|
||||
* @param [IN] laneIndex 网格下标。 @param [IN] scroll 目标偏移(像素)。
|
||||
*/
|
||||
bool SetLaneResampleScroll(int laneIndex, float scroll);
|
||||
|
||||
/**
|
||||
* @brief 查询某信号的最大 / 最小数据间隔(相邻有效采样点的时间差极值)。
|
||||
*
|
||||
* 只统计**时间轴与采样值都有效**的相邻采样对;不足两个有效采样时返回 false。
|
||||
*
|
||||
* @param [IN] ref 目标信号。
|
||||
* @param [OUT] maxInterval 最大间隔(秒)。
|
||||
* @param [OUT] minInterval 最小间隔(秒)。
|
||||
* @return 至少存在一个有效间隔返回 true。
|
||||
*/
|
||||
bool SignalDataInterval(const SignalRef& ref, double& maxInterval, double& minInterval) const;
|
||||
|
||||
/**
|
||||
* @brief 设置某网格是否处于「自由标记点选取」(浮动面板底部的切换按钮)。
|
||||
*
|
||||
@@ -1902,6 +1967,17 @@ namespace jdi
|
||||
*/
|
||||
std::vector<gd::RectF> rowLevelResets;
|
||||
std::vector<gd::RectF> rowIterResets;
|
||||
/**
|
||||
* @brief 「重采样」面板每个被勾选信号的四行内容(与 `rowRefs` 同序)。
|
||||
*
|
||||
* `rowInfoMaxInterval` / `rowInfoMinInterval` = 只读统计(最大 / 最小数据间隔);
|
||||
* `rowIntervalField` / `rowFrequencyField` = 两行**联动输入框**(间隔秒 / 频率 Hz)。
|
||||
* 未勾选或非 RESAMPLE 面板时对应元素为空矩形(数组也可能为空)。
|
||||
*/
|
||||
std::vector<gd::RectF> rowInfoMaxInterval;
|
||||
std::vector<gd::RectF> rowInfoMinInterval;
|
||||
std::vector<gd::RectF> rowIntervalField;
|
||||
std::vector<gd::RectF> rowFrequencyField;
|
||||
/**
|
||||
* @brief 「均值滤波」面板每个信号的**信号色叠底标题栏**矩形(与 `rowRefs` 同序)。
|
||||
*
|
||||
@@ -2896,6 +2972,8 @@ namespace jdi
|
||||
int m_pendingWeakPeakLane = -1; /**< 待构建的弱峰调整信号所属网格下标(-1 表示无请求)。 */
|
||||
std::vector<MeanFilterBuild> m_pendingMeanFilterItems; /**< 待构建的信号及其参数。 */
|
||||
std::vector<WeakPeakBuild> m_pendingWeakPeakItems; /**< 待构建的弱峰调整信号及其参数。 */
|
||||
int m_pendingResampleLane = -1; /**< 待构建的重采样信号所属网格下标(-1 表示无请求)。 */
|
||||
std::vector<ResampleBuild> m_pendingResampleItems; /**< 待构建的重采样信号及其间隔。 */
|
||||
bool m_mouseOverPanel = false; /**< 上一帧鼠标是否落在某个实验性功能面板上。 */
|
||||
int m_peakDragLane = -1; /**< 正在拖动弱峰滑块的面板所属网格。 */
|
||||
SignalRef m_peakDragRef; /**< 正在拖动弱峰滑块的**信号**(每个信号一套参数)。 */
|
||||
@@ -3585,6 +3663,7 @@ namespace jdi
|
||||
bool mirrorHorizontal = false; /**< 是否水平镜像(数据反序)。 */
|
||||
float offsetY = 0.0f; /**< Y 轴偏移。 */
|
||||
double offsetTime = 0.0; /**< 时间轴偏移(秒)。 */
|
||||
std::wstring formula; /**< 「公式映射」表达式(空 = 不映射)。 */
|
||||
|
||||
/** @brief 相等比较(用于判断"逐信号外观样式是否变化")。 */
|
||||
bool operator==(const SceneStyleEntry& other) const
|
||||
@@ -3594,7 +3673,7 @@ namespace jdi
|
||||
gain == other.gain && absolute == other.absolute &&
|
||||
mirrorVertical == other.mirrorVertical &&
|
||||
mirrorHorizontal == other.mirrorHorizontal && offsetY == other.offsetY &&
|
||||
offsetTime == other.offsetTime;
|
||||
offsetTime == other.offsetTime && formula == other.formula;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3729,6 +3808,7 @@ namespace jdi
|
||||
bool mirrorHorizontal = false; /**< 是否水平镜像。 */
|
||||
float offsetY = 0.0f; /**< Y 偏移。 */
|
||||
double offsetTime = 0.0; /**< 时间偏移。 */
|
||||
std::wstring formula; /**< 「公式映射」表达式(空 = 不映射)。 */
|
||||
int originX = 0; /**< 位图左上角(屏幕 x)。 */
|
||||
int originY = 0; /**< 位图左上角(屏幕 y)。 */
|
||||
std::unique_ptr<gd::Bitmap> bitmap; /**< 32bpp ARGB 光晕图层(直通 alpha)。 */
|
||||
|
||||
@@ -587,6 +587,26 @@ namespace jdi
|
||||
inline const wchar_t* const kWeakPeakTitle = L"弱峰调整"; /**< 弱峰调整面板标题。 */
|
||||
inline const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱峰调整共用)。 */
|
||||
|
||||
// ---- 「重采样」面板 ----
|
||||
/** 「重采样」面板标题。 */
|
||||
inline const wchar_t* const kResampleTitle = L"重采样";
|
||||
/** 只读统计行标签:最大数据间隔。 */
|
||||
inline const wchar_t* const kResampleMaxIntervalLabel = L"最大数据间隔";
|
||||
/** 只读统计行标签:最小数据间隔。 */
|
||||
inline const wchar_t* const kResampleMinIntervalLabel = L"最小数据间隔";
|
||||
/** 输入行标签:重采样间隔(秒)。 */
|
||||
inline const wchar_t* const kResampleIntervalLabel = L"重采样间隔";
|
||||
/** 输入行标签:重采样频率(Hz)。 */
|
||||
inline const wchar_t* const kResampleFrequencyLabel = L"重采样频率";
|
||||
/**
|
||||
* @brief 重采样间隔的合法区间(秒)。
|
||||
*
|
||||
* 下限取 1ns(再小已经没有实际意义的采样率,且会让新信号的采样数爆炸);
|
||||
* 上限取 1e6 秒。
|
||||
*/
|
||||
inline const double kResampleIntervalMin = 1.0e-9;
|
||||
inline const double kResampleIntervalMax = 1.0e6;
|
||||
|
||||
// ---- 「均值滤波」面板的两行参数滑块(用户 2026-10-30 要求)----
|
||||
/**
|
||||
* 标签一律**不带 `X` / `N`**:滑块的量程本身已经说明是数值,符号只会让标签变长。
|
||||
@@ -1034,7 +1054,7 @@ namespace jdi
|
||||
*/
|
||||
inline const EdgePanelKind kEdgePanelKinds[] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
|
||||
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
|
||||
EdgePanelKind::MARK};
|
||||
EdgePanelKind::MARK, EdgePanelKind::RESAMPLE};
|
||||
|
||||
/** 实验性功能浮动面板的种类数(遍历与命中测试统一用它,避免新增功能时漏改)。 */
|
||||
inline const int kEdgePanelKindCount = static_cast<int>(sizeof(kEdgePanelKinds) / sizeof(kEdgePanelKinds[0]));
|
||||
@@ -1274,6 +1294,36 @@ namespace jdi
|
||||
return FormatValue(seconds) + L"s";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 时间间隔的显示文本(**不带** "Δt:" 前缀)。
|
||||
*
|
||||
* 「重采样」面板的「最大 / 最小数据间隔」两行用它:单位随间隔长短自适应
|
||||
*(`FormatDurationCompound`:1h 1min 2s / 5s 60ms / 120us …)。
|
||||
*
|
||||
* @param [IN] seconds 时间间隔(秒)。
|
||||
* @return 文本。
|
||||
*/
|
||||
inline std::wstring FormatIntervalText(double seconds)
|
||||
{
|
||||
if (!std::isfinite(seconds) || seconds <= 0.0)
|
||||
{
|
||||
return L"—";
|
||||
}
|
||||
return FormatDurationCompound(seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 频率的显示文本(单位在 Hz / kHz / MHz / GHz 之间自适应,保留小数)。
|
||||
*
|
||||
* @param [IN] hertz 频率(Hz)。
|
||||
* @return 形如 "1.2 kHz" 的文本;非法值返回 "—"。
|
||||
*/
|
||||
inline std::wstring FormatFrequencyText(double hertz)
|
||||
{
|
||||
// 唯一实现在 `Utf8Util::FormatFrequency`(宿主构建派生信号名时也用它)。
|
||||
return FormatFrequency(hertz);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按比例缩放颜色的 alpha 通道(用于提示浮层淡出)。
|
||||
* @param [IN] argb 原始 0xAARRGGBB 颜色。
|
||||
|
||||
@@ -208,6 +208,8 @@ namespace jdi
|
||||
// 偏移(Y 平移 + 时间平移)同样会改变波形。
|
||||
mixFloat(resolved.style->offsetY);
|
||||
mixFloat(static_cast<float>(resolved.style->offsetTime));
|
||||
// 「公式映射」逐点改变取值 ⇒ 必须进指纹(否则改公式后波形不变)。
|
||||
mixText(resolved.style->formula);
|
||||
}
|
||||
// 图例文案参与绘制,重命名必须让缓存失效。
|
||||
mixText(resolved.data->name);
|
||||
@@ -263,6 +265,7 @@ namespace jdi
|
||||
entry.mirrorHorizontal = resolved.style->mirrorHorizontal;
|
||||
entry.offsetY = resolved.style->offsetY;
|
||||
entry.offsetTime = resolved.style->offsetTime;
|
||||
entry.formula = resolved.style->formula;
|
||||
}
|
||||
lane.push_back(entry);
|
||||
}
|
||||
|
||||
@@ -1811,8 +1811,187 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::SetLaneEdgeCenterScroll(int laneIndex, float scroll)
|
||||
// ============================ 「重采样」 ============================
|
||||
|
||||
bool PlotView::SignalDataInterval(const SignalRef& ref, double& maxInterval,
|
||||
double& minInterval) const
|
||||
{
|
||||
maxInterval = 0.0;
|
||||
minInterval = 0.0;
|
||||
if (m_dataSet == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || resolved.axis == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const TimeAxis& axis = *resolved.axis;
|
||||
const SignalData& signal = *resolved.data;
|
||||
const std::size_t count = std::min(axis.time.size(), signal.values.size());
|
||||
bool havePrevious = false;
|
||||
double previousTime = 0.0;
|
||||
bool any = false;
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
const double time = axis.time[index];
|
||||
if (!std::isfinite(time) || !std::isfinite(signal.values[index]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (havePrevious)
|
||||
{
|
||||
const double gap = time - previousTime;
|
||||
if (gap > 0.0)
|
||||
{
|
||||
if (!any)
|
||||
{
|
||||
maxInterval = gap;
|
||||
minInterval = gap;
|
||||
any = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
maxInterval = std::max(maxInterval, gap);
|
||||
minInterval = std::min(minInterval, gap);
|
||||
}
|
||||
}
|
||||
}
|
||||
previousTime = time;
|
||||
havePrevious = true;
|
||||
}
|
||||
return any;
|
||||
}
|
||||
|
||||
bool PlotView::SetLaneResample(int laneIndex, bool enabled)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
if (lane.resample == enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
lane.resample = enabled;
|
||||
if (enabled)
|
||||
{
|
||||
// 默认只勾选网格内**第一个可见信号**(与其它实验性功能一致)。
|
||||
if (lane.resampleItems.empty())
|
||||
{
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data != nullptr && resolved.visible)
|
||||
{
|
||||
Lane::ResampleItem& item = lane.EnsureResampleItem(ref);
|
||||
double maxInterval = 0.0;
|
||||
double minInterval = 0.0;
|
||||
if (SignalDataInterval(ref, maxInterval, minInterval))
|
||||
{
|
||||
item.maxInterval = maxInterval;
|
||||
item.minInterval = minInterval;
|
||||
item.interval = minInterval;
|
||||
}
|
||||
item.initialized = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
lane.resamplePanel.open = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
lane.resamplePanel.open = false;
|
||||
}
|
||||
EnsureLaneLayersPrefetched();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::LaneResample(int laneIndex) const
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].resample;
|
||||
}
|
||||
|
||||
bool PlotView::ToggleLaneResample(int laneIndex)
|
||||
{
|
||||
const bool enabled = !LaneResample(laneIndex);
|
||||
SetLaneResample(laneIndex, enabled);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
bool PlotView::ToggleLaneResampleSignal(int laneIndex, const SignalRef& ref)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
if (lane.ResampleEnabled(ref))
|
||||
{
|
||||
lane.RemoveResampleItem(ref);
|
||||
return false;
|
||||
}
|
||||
// 勾选时一次性算好最大 / 最小数据间隔(面板绘制只读缓存值)。
|
||||
Lane::ResampleItem& item = lane.EnsureResampleItem(ref);
|
||||
double maxInterval = 0.0;
|
||||
double minInterval = 0.0;
|
||||
if (SignalDataInterval(ref, maxInterval, minInterval))
|
||||
{
|
||||
item.maxInterval = maxInterval;
|
||||
item.minInterval = minInterval;
|
||||
item.interval = minInterval;
|
||||
}
|
||||
item.initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::LaneResampleSignalEnabled(int laneIndex, const SignalRef& ref) const
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].ResampleEnabled(ref);
|
||||
}
|
||||
|
||||
bool PlotView::SetLaneResampleScroll(int laneIndex, float scroll)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
const float clamped = std::max(0.0f, scroll);
|
||||
if (std::fabs(lane.resampleScroll - clamped) < 0.5f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
lane.resampleScroll = clamped;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::TakePendingResampleBuild(int& laneIndex, std::vector<ResampleBuild>& items)
|
||||
{
|
||||
if (m_pendingResampleLane < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
laneIndex = m_pendingResampleLane;
|
||||
items = m_pendingResampleItems;
|
||||
m_pendingResampleLane = -1;
|
||||
m_pendingResampleItems.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool PlotView::SetLaneEdgeCenterScroll(int laneIndex, float scroll) {
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
@@ -1916,6 +2095,9 @@ namespace jdi
|
||||
case EdgePanelKind::MARK:
|
||||
state = &lane.freeMarkerPanel;
|
||||
break;
|
||||
case EdgePanelKind::RESAMPLE:
|
||||
state = &lane.resamplePanel;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -1946,6 +2128,8 @@ namespace jdi
|
||||
return lane.weakPeakPanel.collapsed;
|
||||
case EdgePanelKind::MARK:
|
||||
return lane.freeMarkerPanel.collapsed;
|
||||
case EdgePanelKind::RESAMPLE:
|
||||
return lane.resamplePanel.collapsed;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -3393,7 +3577,8 @@ namespace jdi
|
||||
const bool mirrored = (resolvedStyle != nullptr) && resolvedStyle->mirrorHorizontal;
|
||||
const bool transform = (resolvedStyle != nullptr) &&
|
||||
(resolvedStyle->gain != 1.0f || resolvedStyle->absolute ||
|
||||
resolvedStyle->mirrorVertical || resolvedStyle->offsetY != 0.0f);
|
||||
resolvedStyle->mirrorVertical || resolvedStyle->offsetY != 0.0f ||
|
||||
resolvedStyle->HasFormula());
|
||||
|
||||
// ⚠️ **整段自适应直接取信号已缓存的 min/max**(O(1)):
|
||||
// 当视界覆盖整条信号、且该信号没有任何数值变换时,逐点扫描的结果与
|
||||
|
||||
@@ -391,7 +391,9 @@ namespace jdi
|
||||
m_editTarget == PlotEditTarget::MEAN_FILTER_ITERATIONS ||
|
||||
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)
|
||||
{
|
||||
// 「均值点数 X」/「迭代次数 N」(均值滤波、中心轴)与「滑块上限」(弱峰调整 / 均值滤波):
|
||||
// 输入框都落在对应的字段/滑块行矩形上。
|
||||
@@ -613,6 +615,31 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
// 「重采样」两行输入框:间隔(秒)与频率(Hz)**联动**,改任意一个都同时更新两者。
|
||||
// ⚠️ 输入框内的数值单位:间隔 = 秒、频率 = Hz(显示态再按大小自适应单位)。
|
||||
if (target == PlotEditTarget::RESAMPLE_INTERVAL ||
|
||||
target == PlotEditTarget::RESAMPLE_FREQUENCY)
|
||||
{
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
Lane::ResampleItem* existing = const_cast<Lane::ResampleItem*>(
|
||||
lane.FindResampleItem(m_editMeanRef));
|
||||
if (existing == nullptr)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 频率 → 间隔(或反之),非法值(0 / 负数 / 非有限)不生效。
|
||||
const double interval =
|
||||
(target == PlotEditTarget::RESAMPLE_FREQUENCY) ? ((parsed > 0.0) ? (1.0 / parsed) : -1.0)
|
||||
: parsed;
|
||||
if (interval > 0.0 && std::isfinite(interval))
|
||||
{
|
||||
Lane::ResampleItem& item = lane.EnsureResampleItem(m_editMeanRef);
|
||||
item.interval = std::max(kResampleIntervalMin, std::min(interval, kResampleIntervalMax));
|
||||
item.initialized = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 「弱峰调整」迭代次数滑块的**上限**:用户可直接输入一个正整数作为新的量程上限。
|
||||
// 规则(用户明确要求):
|
||||
// * 输入框只允许输入正整数(过滤策略为 BYTE = 纯数字,粘贴同样会被清洗);
|
||||
@@ -1011,7 +1038,8 @@ namespace jdi
|
||||
return nullptr;
|
||||
}
|
||||
if ((static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MEAN ||
|
||||
static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::PEAK) &&
|
||||
static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::PEAK ||
|
||||
static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::RESAMPLE) &&
|
||||
layout.button.Width > 0.0f &&
|
||||
UiPointInRect(layout.button, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
@@ -1080,7 +1108,8 @@ namespace jdi
|
||||
const EdgePanelKind cursorKind = static_cast<EdgePanelKind>(panelKind);
|
||||
const bool listCursor = (cursorKind == EdgePanelKind::MEAN ||
|
||||
cursorKind == EdgePanelKind::CENTER ||
|
||||
cursorKind == EdgePanelKind::PEAK);
|
||||
cursorKind == EdgePanelKind::PEAK ||
|
||||
cursorKind == EdgePanelKind::RESAMPLE);
|
||||
const Lane& cursorLane =
|
||||
Tab().Lanes()[static_cast<std::size_t>(panelLane)];
|
||||
if (listCursor)
|
||||
@@ -1102,6 +1131,11 @@ namespace jdi
|
||||
checked = cursorLane.FindEdgeCenterItem(
|
||||
layout.rowRefs[rowIndex]) != nullptr;
|
||||
}
|
||||
else if (cursorKind == EdgePanelKind::RESAMPLE)
|
||||
{
|
||||
checked = cursorLane.FindResampleItem(
|
||||
layout.rowRefs[rowIndex]) != nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
checked = cursorLane.FindMeanFilterItem(
|
||||
@@ -1109,6 +1143,23 @@ namespace jdi
|
||||
}
|
||||
return checked ? LoadCursorW(nullptr, IDC_HAND) : nullptr;
|
||||
}
|
||||
if (cursorKind == EdgePanelKind::RESAMPLE)
|
||||
{
|
||||
// 两行输入框是可点击控件(点击进入就地编辑)。
|
||||
if (rowIndex < layout.rowIntervalField.size() &&
|
||||
UiPointInRect(layout.rowIntervalField[rowIndex], static_cast<float>(x),
|
||||
static_cast<float>(y)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
if (rowIndex < layout.rowFrequencyField.size() &&
|
||||
UiPointInRect(layout.rowFrequencyField[rowIndex], static_cast<float>(x),
|
||||
static_cast<float>(y)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const bool hasParams =
|
||||
rowIndex < layout.rowLevelTracks.size() &&
|
||||
layout.rowLevelTracks[rowIndex].Width > 0.0f;
|
||||
@@ -1772,6 +1823,9 @@ namespace jdi
|
||||
case EdgePanelKind::PEAK:
|
||||
scrolled = SetLaneWeakPeakScroll(m_markerScrollDragLane, target);
|
||||
break;
|
||||
case EdgePanelKind::RESAMPLE:
|
||||
scrolled = SetLaneResampleScroll(m_markerScrollDragLane, target);
|
||||
break;
|
||||
default:
|
||||
scrolled = SetLaneMeanFilterScroll(m_markerScrollDragLane, target);
|
||||
break;
|
||||
@@ -1954,7 +2008,7 @@ namespace jdi
|
||||
static_cast<EdgePanelKind>(hoverPanelKind);
|
||||
if (overEdgePanel && m_tab != nullptr && panelLane >= 0 &&
|
||||
(hoverKind == EdgePanelKind::MEAN || hoverKind == EdgePanelKind::PEAK ||
|
||||
hoverKind == EdgePanelKind::CENTER))
|
||||
hoverKind == EdgePanelKind::CENTER || hoverKind == EdgePanelKind::RESAMPLE))
|
||||
{
|
||||
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoverKind);
|
||||
if (UiPointInRect(layout.content, static_cast<float>(x), static_cast<float>(y)))
|
||||
@@ -2350,15 +2404,17 @@ namespace jdi
|
||||
// 「中心轴边沿计数」/「均值滤波」/「弱峰调整」面板:逐信号条目
|
||||
// (前两者是「名称 + 复选框 + 两个参数输入框」,弱峰调整是「名称 + 复选框 + 两行滑块」)。
|
||||
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER ||
|
||||
kind == EdgePanelKind::PEAK || kind == EdgePanelKind::COUNT) &&
|
||||
kind == EdgePanelKind::PEAK || kind == EdgePanelKind::COUNT ||
|
||||
kind == EdgePanelKind::RESAMPLE) &&
|
||||
m_tab != nullptr)
|
||||
{
|
||||
const bool meanMode = (kind == EdgePanelKind::MEAN);
|
||||
const bool centerMode = (kind == EdgePanelKind::CENTER);
|
||||
const bool boostMode = (kind == EdgePanelKind::PEAK);
|
||||
// 「均值滤波」「中心轴边沿计数」「弱峰调整」自 2026-11-02 起共用同一套
|
||||
//**可滚动列表**交互(信号色叠底条目栏 + 右侧复选框 + 展开后的两行参数滑块)。
|
||||
const bool listMode = (meanMode || centerMode || boostMode);
|
||||
const bool resampleMode = (kind == EdgePanelKind::RESAMPLE);
|
||||
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」自 2026-11-02 起共用同一套
|
||||
//**可滚动列表**交互(信号色叠底条目栏 + 右侧复选框 + 展开后的参数行)。
|
||||
const bool listMode = (meanMode || centerMode || boostMode || resampleMode);
|
||||
// 「边沿计数」= **表格**(表头 + 每信号一行),只有滚动条可拖。
|
||||
if (kind == EdgePanelKind::COUNT)
|
||||
{
|
||||
@@ -2402,6 +2458,120 @@ namespace jdi
|
||||
}
|
||||
const bool insideContent =
|
||||
UiPointInRect(layout.content, fx, fy);
|
||||
// 「重采样」:四行内容 = 两行只读统计 + 两行**输入框**(间隔 / 频率)。
|
||||
// 条目栏 = 复选框切换 + 已勾选时整行收起/展开(与其它列表式面板一致)。
|
||||
if (resampleMode)
|
||||
{
|
||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||
{
|
||||
if (!insideContent)
|
||||
{
|
||||
break;
|
||||
}
|
||||
const SignalRef& ref = layout.rowRefs[rowIndex];
|
||||
const Lane::ResampleItem* item = meanLane.FindResampleItem(ref);
|
||||
if (item == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 点击「重采样间隔」输入框 → 编辑秒数。
|
||||
if (rowIndex < layout.rowIntervalField.size() &&
|
||||
layout.rowIntervalField[rowIndex].Width > 0.0f &&
|
||||
UiPointInRect(layout.rowIntervalField[rowIndex], fx, fy))
|
||||
{
|
||||
m_editMeanRef = ref;
|
||||
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);
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
// 点击「重采样频率」输入框 → 编辑频率(Hz,与间隔联动)。
|
||||
if (rowIndex < layout.rowFrequencyField.size() &&
|
||||
layout.rowFrequencyField[rowIndex].Width > 0.0f &&
|
||||
UiPointInRect(layout.rowFrequencyField[rowIndex], fx, fy))
|
||||
{
|
||||
m_editMeanRef = ref;
|
||||
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);
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 条目栏(信号色叠底):复选框切换;已勾选时整行切换参数行收起/展开。
|
||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||
{
|
||||
const SignalRef& ref = layout.rowRefs[rowIndex];
|
||||
if (!insideContent || rowIndex >= layout.rowHeaders.size() ||
|
||||
layout.rowHeaders[rowIndex].Width <= 0.0f ||
|
||||
!UiPointInRect(layout.rowHeaders[rowIndex], fx, fy))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const bool onCheckBox =
|
||||
rowIndex < layout.rowChecks.size() &&
|
||||
layout.rowChecks[rowIndex].Width > 0.0f &&
|
||||
UiPointInRect(layout.rowChecks[rowIndex], fx, fy);
|
||||
const Lane::ResampleItem* existing = meanLane.FindResampleItem(ref);
|
||||
if (onCheckBox)
|
||||
{
|
||||
ToggleLaneResampleSignal(index, ref);
|
||||
m_meanRowHoverLane = -1;
|
||||
m_meanRowHoverRef = SignalRef();
|
||||
}
|
||||
else if (existing != nullptr)
|
||||
{
|
||||
Lane::ResampleItem& editable =
|
||||
meanLane.EnsureResampleItem(ref);
|
||||
editable.collapsed = !editable.collapsed;
|
||||
}
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
// 底部「构建为新信号」按钮。
|
||||
if (layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy))
|
||||
{
|
||||
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(index)];
|
||||
m_pendingResampleItems.clear();
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || !resolved.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const Lane::ResampleItem* item = lane.FindResampleItem(ref);
|
||||
if (item == nullptr || !(item->interval > 0.0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ResampleBuild build;
|
||||
build.groupIndex = ref.groupIndex;
|
||||
build.signalIndex = ref.signalIndex;
|
||||
build.interval = item->interval;
|
||||
m_pendingResampleItems.push_back(build);
|
||||
}
|
||||
if (!m_pendingResampleItems.empty())
|
||||
{
|
||||
const ResampleBuild& first = m_pendingResampleItems.front();
|
||||
SetHint(L"正在构建重采样信号(间隔 " +
|
||||
FormatIntervalText(first.interval) + L",共 " +
|
||||
std::to_wstring(m_pendingResampleItems.size()) + L" 条)…");
|
||||
m_pendingResampleLane = index;
|
||||
}
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true; // 面板内的其它位置:消费点击,不穿透
|
||||
}
|
||||
// 「弱峰调整」:两行滑块 = 调整幅度(-400% … +400%)/ 迭代次数(1 … 自定上限)。
|
||||
// 语义与该面板旧式排版完全一致,只是外观改成与均值滤波同款的可滚动列表。
|
||||
if (boostMode)
|
||||
@@ -3270,7 +3440,8 @@ namespace jdi
|
||||
// 「均值滤波 / 中心轴边沿计数 / 弱峰调整 / 边沿计数」面板:内容区超高时用滚轮翻页
|
||||
//(上百个信号合并时必备)。
|
||||
if (hoveredKind == EdgePanelKind::MEAN || hoveredKind == EdgePanelKind::CENTER ||
|
||||
hoveredKind == EdgePanelKind::PEAK || hoveredKind == EdgePanelKind::COUNT)
|
||||
hoveredKind == EdgePanelKind::PEAK || hoveredKind == EdgePanelKind::COUNT ||
|
||||
hoveredKind == EdgePanelKind::RESAMPLE)
|
||||
{
|
||||
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoveredKind);
|
||||
if (layout.content.Width > 0.0f &&
|
||||
@@ -3289,6 +3460,8 @@ namespace jdi
|
||||
? wheelLane.weakPeakScroll
|
||||
: (hoveredKind == EdgePanelKind::COUNT)
|
||||
? wheelLane.edgeCountScroll
|
||||
: (hoveredKind == EdgePanelKind::RESAMPLE)
|
||||
? wheelLane.resampleScroll
|
||||
: wheelLane.meanFilterScroll;
|
||||
const float next = current - static_cast<float>(notches * 40.0 * m_scale);
|
||||
const float target = std::max(0.0f, std::min(next, maxScroll));
|
||||
@@ -3303,6 +3476,9 @@ namespace jdi
|
||||
case EdgePanelKind::COUNT:
|
||||
SetLaneEdgeCountScroll(panelLane, target);
|
||||
break;
|
||||
case EdgePanelKind::RESAMPLE:
|
||||
SetLaneResampleScroll(panelLane, target);
|
||||
break;
|
||||
default:
|
||||
SetLaneMeanFilterScroll(panelLane, target);
|
||||
break;
|
||||
|
||||
@@ -150,6 +150,19 @@ namespace jdi
|
||||
mixHashFloat(offsetY);
|
||||
mixHashDouble(offsetTime);
|
||||
mixHashFloat(lineWidth);
|
||||
// 「公式映射」会逐点改变取值 ⇒ 必须进折线缓存键(否则改公式后仍贴旧顶点)。
|
||||
{
|
||||
std::uint64_t textHash = 1469598103934665603ull;
|
||||
if (resolved.style != nullptr)
|
||||
{
|
||||
for (const wchar_t character : resolved.style->formula)
|
||||
{
|
||||
textHash ^= static_cast<std::uint64_t>(static_cast<std::uint32_t>(character));
|
||||
textHash *= 1099511628211ull;
|
||||
}
|
||||
}
|
||||
mixHash(textHash);
|
||||
}
|
||||
|
||||
for (const PolylineCacheEntry& entry : m_polylineCache)
|
||||
{
|
||||
@@ -4071,6 +4084,16 @@ namespace jdi
|
||||
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->maxIterations + 2 : 0));
|
||||
// 「弱峰调整」的条目栏三角(展开 / 收起)直接改变面板像素 ⇒ 必须进键。
|
||||
mix((peakItem != nullptr && peakItem->collapsed) ? 1ull : 0ull);
|
||||
// 「重采样」:勾选状态、两个输入框的数值(间隔 / 频率)与收起状态都直接显示在面板上。
|
||||
const Lane::ResampleItem* resampleItem = lane.FindResampleItem(ref);
|
||||
mix(resampleItem != nullptr ? 1ull : 0ull);
|
||||
if (resampleItem != nullptr)
|
||||
{
|
||||
std::uint64_t intervalBits = 0;
|
||||
std::memcpy(&intervalBits, &resampleItem->interval, sizeof(intervalBits));
|
||||
mix(intervalBits);
|
||||
mix(resampleItem->collapsed ? 1ull : 0ull);
|
||||
}
|
||||
}
|
||||
|
||||
// ⚠️⚠️ **边沿统计的数字本身就是面板内容,数字变化必须能让面板重绘**:
|
||||
@@ -4124,6 +4147,22 @@ namespace jdi
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
for (const gd::RectF& rect : layout.rowInfoMaxInterval)
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
for (const gd::RectF& rect : layout.rowInfoMinInterval)
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
for (const gd::RectF& rect : layout.rowIntervalField)
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
for (const gd::RectF& rect : layout.rowFrequencyField)
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
mixFloat(layout.scrollOffset);
|
||||
|
||||
// 鼠标落在面板内时,✕ / 复选框 / 滑块 / 底部按钮的悬停态都依赖精确坐标。
|
||||
@@ -4742,12 +4781,14 @@ namespace jdi
|
||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
const bool freeMarker = (kind == EdgePanelKind::MARK);
|
||||
// ⚠️ 「均值滤波」「中心轴边沿计数」「弱峰调整」= 可滚动的分组列表(条目栏 + 展开后的参数行);
|
||||
const bool resamplePanel = (kind == EdgePanelKind::RESAMPLE);
|
||||
// ⚠️ 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」= 可滚动的分组列表
|
||||
//(条目栏 + 展开后的参数行);
|
||||
// **「边沿计数」= 表格**(表头:信号名 / 上升沿计数 / 下降沿计数),单独一支。
|
||||
// 它们都**不能**再走旧的逐条目分支:那个分支没有任何裁切,滚动时会画到标题栏上。
|
||||
const bool listStyle = (meanFilter || centerAxis || weakPeak);
|
||||
const bool listStyle = (meanFilter || centerAxis || weakPeak || resamplePanel);
|
||||
const bool countTable = (kind == EdgePanelKind::COUNT);
|
||||
const bool hasButton = (meanFilter || weakPeak);
|
||||
const bool hasButton = (meanFilter || weakPeak || resamplePanel);
|
||||
{
|
||||
if (index >= count)
|
||||
{
|
||||
@@ -4944,8 +4985,10 @@ namespace jdi
|
||||
meanFilter ? std::wstring(L"均值滤波")
|
||||
: (weakPeak ? std::wstring(kWeakPeakTitle)
|
||||
: (freeMarker ? std::wstring(L"自由标记")
|
||||
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") +
|
||||
EdgeRangeSuffix())));
|
||||
: (resamplePanel
|
||||
? std::wstring(kResampleTitle)
|
||||
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") +
|
||||
EdgeRangeSuffix()))));
|
||||
UiText(g, title, m_fonts->uiSmall,
|
||||
gd::RectF(badgeRect.X + badgeRect.Width + 8.0f * m_scale, panel.Y,
|
||||
panel.Width - badgeRect.Width - 8.0f * m_scale - titleHeight,
|
||||
@@ -5170,6 +5213,22 @@ namespace jdi
|
||||
{
|
||||
extendRow(layout.rowStats[rowIndex]);
|
||||
}
|
||||
if (rowIndex < layout.rowInfoMaxInterval.size())
|
||||
{
|
||||
extendRow(layout.rowInfoMaxInterval[rowIndex]);
|
||||
}
|
||||
if (rowIndex < layout.rowInfoMinInterval.size())
|
||||
{
|
||||
extendRow(layout.rowInfoMinInterval[rowIndex]);
|
||||
}
|
||||
if (rowIndex < layout.rowIntervalField.size())
|
||||
{
|
||||
extendRow(layout.rowIntervalField[rowIndex]);
|
||||
}
|
||||
if (rowIndex < layout.rowFrequencyField.size())
|
||||
{
|
||||
extendRow(layout.rowFrequencyField[rowIndex]);
|
||||
}
|
||||
if (rowBottom <= layout.content.Y ||
|
||||
rowTop >= layout.content.Y + layout.content.Height)
|
||||
{
|
||||
@@ -5185,10 +5244,15 @@ namespace jdi
|
||||
weakPeak ? nullptr
|
||||
: (centerAxis ? tabLane.FindEdgeCenterItem(ref)
|
||||
: tabLane.FindMeanFilterItem(ref));
|
||||
const bool checked = weakPeak ? (peakItem != nullptr) : (item != nullptr);
|
||||
const Lane::ResampleItem* resampleItem =
|
||||
resamplePanel ? tabLane.FindResampleItem(ref) : nullptr;
|
||||
const bool checked = resamplePanel ? (resampleItem != nullptr)
|
||||
: (weakPeak ? (peakItem != nullptr)
|
||||
: (item != nullptr));
|
||||
const bool rowCollapsed =
|
||||
weakPeak ? (peakItem != nullptr && peakItem->collapsed)
|
||||
: (item != nullptr && item->collapsed);
|
||||
resamplePanel ? (resampleItem != nullptr && resampleItem->collapsed)
|
||||
: (weakPeak ? (peakItem != nullptr && peakItem->collapsed)
|
||||
: (item != nullptr && item->collapsed));
|
||||
|
||||
// 内容区已由 `g.SetClip(layout.content)` 裁切(行可以“半截”显示,滚动更顺滑);
|
||||
// 已经完整滚出可视范围的行在上面就 `continue` 了,这里不再额外门控。
|
||||
@@ -5232,6 +5296,99 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
|
||||
// 「重采样」:四行 = 最大间隔(只读)/ 最小间隔(只读)/
|
||||
// 重采样间隔(输入)/ 重采样频率(输入)。标签在左、数值在右。
|
||||
if (resamplePanel)
|
||||
{
|
||||
const float labelWidth =
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, kResampleMaxIntervalLabel);
|
||||
const auto paintInfoRow = [&](const gd::RectF& row, const wchar_t* label,
|
||||
const std::wstring& text)
|
||||
{
|
||||
if (row.Width <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
UiTextClipped(g, label, m_fonts->uiSmall,
|
||||
gd::RectF(row.X, row.Y, std::max(10.0f, labelWidth),
|
||||
row.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT, layout.content);
|
||||
UiTextClipped(g, text, m_fonts->monoSmall,
|
||||
gd::RectF(row.X + labelWidth + 8.0f * m_scale, row.Y,
|
||||
std::max(10.0f, row.Width - labelWidth -
|
||||
8.0f * m_scale),
|
||||
row.Height),
|
||||
m_theme->textStrong, TextAlign::RIGHT,
|
||||
layout.content);
|
||||
};
|
||||
const auto paintInputRow = [&](const gd::RectF& field, const wchar_t* label,
|
||||
const std::wstring& text, bool editing)
|
||||
{
|
||||
if (field.Width <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 标签与数值同高:标签从内容区左缘画到字段左缘。
|
||||
UiTextClipped(
|
||||
g, label, m_fonts->uiSmall,
|
||||
gd::RectF(layout.content.X, field.Y,
|
||||
std::max(10.0f, field.X - 8.0f * m_scale - layout.content.X),
|
||||
field.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT, layout.content);
|
||||
if (editing)
|
||||
{
|
||||
return; // 正在编辑:内容由 `PaintEditField` 画
|
||||
}
|
||||
const gd::RectF box(field.X, field.Y + 2.0f * m_scale, field.Width,
|
||||
std::max(10.0f, field.Height - 4.0f * m_scale));
|
||||
UiFillRoundRect(g, box, 3.0f * m_scale, m_theme->pressed);
|
||||
UiDrawRoundRect(g, box, 3.0f * m_scale, m_theme->border, 1.0f);
|
||||
UiTextClipped(g, text, m_fonts->monoSmall,
|
||||
gd::RectF(box.X + 4.0f * m_scale, box.Y,
|
||||
std::max(6.0f, box.Width - 8.0f * m_scale),
|
||||
box.Height),
|
||||
m_theme->textStrong, TextAlign::LEFT, layout.content);
|
||||
};
|
||||
double maxInterval = 0.0;
|
||||
double minInterval = 0.0;
|
||||
const bool hasInterval =
|
||||
(resampleItem->maxInterval > 0.0) && (resampleItem->minInterval > 0.0);
|
||||
maxInterval = resampleItem->maxInterval;
|
||||
minInterval = resampleItem->minInterval;
|
||||
if (rowIndex < layout.rowInfoMaxInterval.size())
|
||||
{
|
||||
paintInfoRow(layout.rowInfoMaxInterval[rowIndex], kResampleMaxIntervalLabel,
|
||||
hasInterval ? FormatIntervalText(maxInterval) : L"—");
|
||||
}
|
||||
if (rowIndex < layout.rowInfoMinInterval.size())
|
||||
{
|
||||
paintInfoRow(layout.rowInfoMinInterval[rowIndex], kResampleMinIntervalLabel,
|
||||
hasInterval ? FormatIntervalText(minInterval) : L"—");
|
||||
}
|
||||
const double interval = (resampleItem->interval > 0.0)
|
||||
? resampleItem->interval
|
||||
: minInterval;
|
||||
const bool editingInterval =
|
||||
(m_editTarget == PlotEditTarget::RESAMPLE_INTERVAL) &&
|
||||
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
|
||||
const bool editingFrequency =
|
||||
(m_editTarget == PlotEditTarget::RESAMPLE_FREQUENCY) &&
|
||||
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
|
||||
if (rowIndex < layout.rowIntervalField.size())
|
||||
{
|
||||
paintInputRow(layout.rowIntervalField[rowIndex], kResampleIntervalLabel,
|
||||
FormatValue(interval), editingInterval);
|
||||
}
|
||||
if (rowIndex < layout.rowFrequencyField.size())
|
||||
{
|
||||
const double hertz =
|
||||
(interval > 0.0) ? (1.0 / interval) : 0.0;
|
||||
paintInputRow(layout.rowFrequencyField[rowIndex], kResampleFrequencyLabel,
|
||||
FormatFrequencyText(hertz), editingFrequency);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 「中心轴边沿计数」:参数行最上面是**同一行**的「上升沿 / 下降沿」统计。
|
||||
if (centerAxis && rowIndex < layout.rowStats.size() &&
|
||||
layout.rowStats[rowIndex].Width > 0.0f)
|
||||
|
||||
@@ -28,12 +28,13 @@ namespace jdi
|
||||
const bool centerAxis = (kind == EdgePanelKind::CENTER);
|
||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
// 「均值滤波」「中心轴边沿计数」「弱峰调整」= 可滚动的分组列表
|
||||
const bool resamplePanel = (kind == EdgePanelKind::RESAMPLE);
|
||||
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」= 可滚动的分组列表
|
||||
//(信号色叠底条目栏 + 右侧复选框 + 展开后的参数行);
|
||||
// **「边沿计数」= 表格**(表头:信号名 / 上升沿计数 / 下降沿计数)。
|
||||
// ⚠️ 旧式「逐条目」排版(无滚动的名称+参数行)已彻底删除:上百个信号时它会算出
|
||||
// 上万像素高的面板,超出绘图区后**面板像素缓存一次都存不进去** ⇒ 每帧整块重画。
|
||||
const bool listStyle = (centerAxis || meanFilter || weakPeak);
|
||||
const bool listStyle = (centerAxis || meanFilter || weakPeak || resamplePanel);
|
||||
const bool countTable = (kind == EdgePanelKind::COUNT);
|
||||
|
||||
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “弱峰调整”
|
||||
@@ -41,7 +42,10 @@ namespace jdi
|
||||
const std::wstring title =
|
||||
meanFilter ? std::wstring(L"均值滤波")
|
||||
: (weakPeak ? std::wstring(kWeakPeakTitle)
|
||||
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix()));
|
||||
: (resamplePanel
|
||||
? std::wstring(kResampleTitle)
|
||||
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") +
|
||||
EdgeRangeSuffix())));
|
||||
float width = kEdgePanelPad * 2.0f * m_scale + kLaneBadgeWidth * m_scale +
|
||||
8.0f * m_scale + UiMeasureTextWidth(m_fonts->uiSmall, title) +
|
||||
kEdgePanelTitleHeight * m_scale;
|
||||
@@ -162,6 +166,22 @@ namespace jdi
|
||||
kWeakPeakResetGap * m_scale + kWeakPeakResetButtonSize * m_scale;
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + sliderRowWidth +
|
||||
(kFreeMarkerScrollWidth + 4.0f) * m_scale);
|
||||
if (resamplePanel)
|
||||
{
|
||||
// 「重采样」四行 = 「标签列 + 数值列」(两行统计 + 两行输入框),
|
||||
// 标签取四者中最宽的一个,数值列按间隔 / 频率的典型文本宽度取。
|
||||
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"));
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + resampleLabelColumn +
|
||||
8.0f * m_scale + resampleValueColumn +
|
||||
(kFreeMarkerScrollWidth + 4.0f) * m_scale);
|
||||
}
|
||||
}
|
||||
|
||||
if (weakPeak)
|
||||
@@ -174,9 +194,9 @@ namespace jdi
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth);
|
||||
}
|
||||
|
||||
if (meanFilter || weakPeak)
|
||||
if (meanFilter || weakPeak || resamplePanel)
|
||||
{
|
||||
// 底部按钮行 = 「构建为新信号」(仅均值滤波 / 弱峰调整有),按按钮文字实测宽度确定下限。
|
||||
// 底部按钮行 = 「构建为新信号」(均值滤波 / 弱峰调整 / 重采样有),按按钮文字实测宽度确定下限。
|
||||
const float buttonWidth =
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, kBuildSignalLabel) + 28.0f * m_scale;
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + buttonWidth);
|
||||
@@ -276,21 +296,26 @@ namespace jdi
|
||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
const bool freeMarker = (kind == EdgePanelKind::MARK);
|
||||
// 「均值滤波」「中心轴边沿计数」「弱峰调整」共用同一套**可滚动分组列表**排布
|
||||
const bool resamplePanel = (kind == EdgePanelKind::RESAMPLE);
|
||||
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」共用同一套**可滚动分组列表**排布
|
||||
//(信号色叠底条目栏 + 展开后的参数行);「边沿计数」自 2026-11-02 起走**表格**分支。
|
||||
const bool listStyle = (meanFilter || centerAxis || weakPeak);
|
||||
const bool listStyle = (meanFilter || centerAxis || weakPeak || resamplePanel);
|
||||
const bool countTable = (kind == EdgePanelKind::COUNT);
|
||||
const bool hasButton = (meanFilter || weakPeak);
|
||||
const bool hasButton = (meanFilter || weakPeak || resamplePanel);
|
||||
const bool enabled = meanFilter ? lane.meanFilter
|
||||
: (weakPeak ? lane.weakPeak
|
||||
: (freeMarker ? lane.freeMarker
|
||||
: (centerAxis ? lane.edgeCenter
|
||||
: lane.edgeCount)));
|
||||
: (resamplePanel ? lane.resample
|
||||
: (centerAxis ? lane.edgeCenter
|
||||
: lane.edgeCount))));
|
||||
const bool open = meanFilter ? lane.meanFilterPanel.open
|
||||
: (weakPeak ? lane.weakPeakPanel.open
|
||||
: (freeMarker ? lane.freeMarkerPanel.open
|
||||
: (centerAxis ? lane.edgeCenterPanel.open
|
||||
: lane.edgePanel.open)));
|
||||
: (resamplePanel
|
||||
? lane.resamplePanel.open
|
||||
: (centerAxis
|
||||
? lane.edgeCenterPanel.open
|
||||
: lane.edgePanel.open))));
|
||||
if (!enabled || !open)
|
||||
{
|
||||
return layout;
|
||||
@@ -299,9 +324,11 @@ namespace jdi
|
||||
const bool collapsed = meanFilter ? lane.meanFilterPanel.collapsed
|
||||
: (weakPeak ? lane.weakPeakPanel.collapsed
|
||||
: (freeMarker ? lane.freeMarkerPanel.collapsed
|
||||
: (centerAxis
|
||||
? lane.edgeCenterPanel.collapsed
|
||||
: lane.edgePanel.collapsed)));
|
||||
: (resamplePanel
|
||||
? lane.resamplePanel.collapsed
|
||||
: (centerAxis
|
||||
? lane.edgeCenterPanel.collapsed
|
||||
: lane.edgePanel.collapsed))));
|
||||
layout.collapsed = collapsed;
|
||||
|
||||
int rowCount = 0;
|
||||
@@ -368,7 +395,12 @@ namespace jdi
|
||||
}
|
||||
content += groupRow;
|
||||
bool expanded = false;
|
||||
if (weakPeak)
|
||||
if (resamplePanel)
|
||||
{
|
||||
const Lane::ResampleItem* item = lane.FindResampleItem(ref);
|
||||
expanded = (item != nullptr && !item->collapsed);
|
||||
}
|
||||
else if (weakPeak)
|
||||
{
|
||||
const Lane::WeakPeakItem* item = lane.FindWeakPeakItem(ref);
|
||||
expanded = (item != nullptr && !item->collapsed);
|
||||
@@ -385,10 +417,18 @@ namespace jdi
|
||||
}
|
||||
if (expanded)
|
||||
{
|
||||
content += sliderRow * 2.0f;
|
||||
if (centerAxis)
|
||||
if (resamplePanel)
|
||||
{
|
||||
content += groupRow; // 统计行(上升沿 / 下降沿)
|
||||
// 四行:最大间隔 / 最小间隔 / 重采样间隔 / 重采样频率。
|
||||
content += sliderRow * 4.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
content += sliderRow * 2.0f;
|
||||
if (centerAxis)
|
||||
{
|
||||
content += groupRow; // 统计行(上升沿 / 下降沿)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -489,13 +529,19 @@ namespace jdi
|
||||
const double storedX = meanFilter ? lane.meanFilterPanel.x
|
||||
: (weakPeak ? lane.weakPeakPanel.x
|
||||
: (freeMarker ? lane.freeMarkerPanel.x
|
||||
: (centerAxis ? lane.edgeCenterPanel.x
|
||||
: lane.edgePanel.x)));
|
||||
: (resamplePanel
|
||||
? lane.resamplePanel.x
|
||||
: (centerAxis
|
||||
? lane.edgeCenterPanel.x
|
||||
: lane.edgePanel.x))));
|
||||
const double storedY = meanFilter ? lane.meanFilterPanel.y
|
||||
: (weakPeak ? lane.weakPeakPanel.y
|
||||
: (freeMarker ? lane.freeMarkerPanel.y
|
||||
: (centerAxis ? lane.edgeCenterPanel.y
|
||||
: lane.edgePanel.y)));
|
||||
: (resamplePanel
|
||||
? lane.resamplePanel.y
|
||||
: (centerAxis
|
||||
? lane.edgeCenterPanel.y
|
||||
: lane.edgePanel.y))));
|
||||
int openPanelOrdinal = 0;
|
||||
{
|
||||
// ⚠️ 错开量用「**当前真正显示出来的面板**中排在本面板之前的个数」:
|
||||
@@ -519,12 +565,14 @@ namespace jdi
|
||||
return lane.weakPeak && lane.weakPeakPanel.open;
|
||||
case EdgePanelKind::MARK:
|
||||
return lane.freeMarker && lane.freeMarkerPanel.open;
|
||||
case EdgePanelKind::RESAMPLE:
|
||||
return lane.resample && lane.resamplePanel.open;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
static const EdgePanelKind kOrder[5] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
|
||||
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
|
||||
EdgePanelKind::MARK};
|
||||
static const EdgePanelKind kOrder[6] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
|
||||
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
|
||||
EdgePanelKind::MARK, EdgePanelKind::RESAMPLE};
|
||||
int openSeen = 0;
|
||||
bool found = false;
|
||||
for (std::size_t lane = 0; lane < m_lanes.size() && !found; ++lane)
|
||||
@@ -701,7 +749,9 @@ namespace jdi
|
||||
const bool scrollable = meanContentHeight > viewHeight + 0.5f;
|
||||
const float maxScroll = std::max(0.0f, meanContentHeight - viewHeight);
|
||||
const float storedScroll = meanFilter ? lane.meanFilterScroll
|
||||
: (weakPeak ? lane.weakPeakScroll : lane.edgeCenterScroll);
|
||||
: (weakPeak ? lane.weakPeakScroll
|
||||
: (resamplePanel ? lane.resampleScroll
|
||||
: lane.edgeCenterScroll));
|
||||
const float scroll = std::max(0.0f, std::min(storedScroll, maxScroll));
|
||||
const float innerWidth =
|
||||
std::max(0.0f, width - pad * 2.0f - (scrollable ? (scrollW + 4.0f * m_scale) : 0.0f));
|
||||
@@ -734,6 +784,14 @@ namespace jdi
|
||||
const float resetGap = kWeakPeakResetGap * m_scale;
|
||||
const float trackHeight = kWeakPeakTrackHeight * m_scale;
|
||||
const float closeSize = kEdgePanelMeanCheckSize * m_scale;
|
||||
// 「重采样」四行的标签列(「最大数据间隔」比均值/弱峰的标签更宽):
|
||||
// 与 `EdgePanelWidth` 用同一组实测宽度,保证字段不会压到标签上。
|
||||
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 rowLabelColumn = resamplePanel ? resampleLabelColumn : labelColumn;
|
||||
const float contentLeft = layout.content.X;
|
||||
const float trackLeft = contentLeft + labelColumn + 6.0f * m_scale;
|
||||
const float resetLeft = contentLeft + innerWidth - resetSize;
|
||||
@@ -751,7 +809,13 @@ namespace jdi
|
||||
}
|
||||
bool checked = false;
|
||||
bool itemCollapsed = false;
|
||||
if (weakPeak)
|
||||
if (resamplePanel)
|
||||
{
|
||||
const Lane::ResampleItem* item = lane.FindResampleItem(ref);
|
||||
checked = (item != nullptr);
|
||||
itemCollapsed = (item != nullptr && item->collapsed);
|
||||
}
|
||||
else if (weakPeak)
|
||||
{
|
||||
const Lane::WeakPeakItem* item = lane.FindWeakPeakItem(ref);
|
||||
checked = (item != nullptr);
|
||||
@@ -797,6 +861,42 @@ namespace jdi
|
||||
layout.rowIterTracks.push_back(gd::RectF());
|
||||
layout.rowIterValues.push_back(gd::RectF());
|
||||
layout.rowIterResets.push_back(gd::RectF());
|
||||
if (resamplePanel)
|
||||
{
|
||||
layout.rowInfoMaxInterval.push_back(gd::RectF());
|
||||
layout.rowInfoMinInterval.push_back(gd::RectF());
|
||||
layout.rowIntervalField.push_back(gd::RectF());
|
||||
layout.rowFrequencyField.push_back(gd::RectF());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (resamplePanel)
|
||||
{
|
||||
// 「重采样」四行:① 最大间隔(只读)② 最小间隔(只读)
|
||||
// ③ 重采样间隔(输入)④ 重采样频率(输入,与③联动)。
|
||||
// 每行 = 左侧标签 + 右侧数值/输入框区域。
|
||||
layout.rowLevelTracks.push_back(gd::RectF());
|
||||
layout.rowLevelValues.push_back(gd::RectF());
|
||||
layout.rowLevelResets.push_back(gd::RectF());
|
||||
layout.rowIterTracks.push_back(gd::RectF());
|
||||
layout.rowIterValues.push_back(gd::RectF());
|
||||
layout.rowIterResets.push_back(gd::RectF());
|
||||
layout.rowInfoMaxInterval.push_back(
|
||||
gd::RectF(contentLeft, rowY, innerWidth, sliderRow));
|
||||
rowY += sliderRow;
|
||||
layout.rowInfoMinInterval.push_back(
|
||||
gd::RectF(contentLeft, rowY, innerWidth, sliderRow));
|
||||
rowY += sliderRow;
|
||||
// 输入行的**数值矩形**靠右对齐,宽度取剩余可用宽度(绘制框与编辑框同源)。
|
||||
const float fieldLeft = contentLeft + rowLabelColumn + 8.0f * m_scale;
|
||||
const float fieldWidth =
|
||||
std::max(24.0f * m_scale, innerWidth - rowLabelColumn - 8.0f * m_scale);
|
||||
layout.rowIntervalField.push_back(
|
||||
gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow));
|
||||
rowY += sliderRow;
|
||||
layout.rowFrequencyField.push_back(
|
||||
gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow));
|
||||
rowY += sliderRow;
|
||||
continue;
|
||||
}
|
||||
// 「中心轴边沿计数」:参数行最上面是**同一行**的「上升沿 / 下降沿」统计。
|
||||
|
||||
@@ -142,7 +142,8 @@ namespace jdi
|
||||
cache.mirrorVertical == resolved.style->mirrorVertical &&
|
||||
cache.mirrorHorizontal == resolved.style->mirrorHorizontal &&
|
||||
cache.offsetY == resolved.style->offsetY &&
|
||||
cache.offsetTime == resolved.style->offsetTime;
|
||||
cache.offsetTime == resolved.style->offsetTime &&
|
||||
cache.formula == resolved.style->formula;
|
||||
|
||||
if (!sameKey)
|
||||
{
|
||||
@@ -185,6 +186,7 @@ namespace jdi
|
||||
cache.mirrorHorizontal = resolved.style->mirrorHorizontal;
|
||||
cache.offsetY = resolved.style->offsetY;
|
||||
cache.offsetTime = resolved.style->offsetTime;
|
||||
cache.formula = resolved.style->formula;
|
||||
cache.originX = targetX;
|
||||
cache.originY = targetY;
|
||||
cache.valid = true;
|
||||
@@ -1185,6 +1187,16 @@ namespace jdi
|
||||
mix(resolved.style->mirrorHorizontal ? 1ull : 0ull);
|
||||
mixFloat(resolved.style->offsetY);
|
||||
mixFloat(static_cast<float>(resolved.style->offsetTime));
|
||||
// 「公式映射」逐点改变图层里的波形 ⇒ 必须让图层失效。
|
||||
{
|
||||
std::uint64_t textHash = 1469598103934665603ull;
|
||||
for (const wchar_t character : resolved.style->formula)
|
||||
{
|
||||
textHash ^= static_cast<std::uint64_t>(static_cast<std::uint32_t>(character));
|
||||
textHash *= 1099511628211ull;
|
||||
}
|
||||
mix(textHash);
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,9 @@ namespace jdi
|
||||
CMD_OFFSET_Y = 503, /**< 「偏移调整」:Y 轴偏移输入框。 */
|
||||
CMD_OFFSET_TIME = 504, /**< 「偏移调整」:时间轴偏移输入框。 */
|
||||
CMD_MIRROR_HORIZONTAL = 510, /**< 「镜像」二级面板:水平镜像(数据反序)。 */
|
||||
CMD_MIRROR_VERTICAL = 511 /**< 「镜像」二级面板:垂直镜像(按 Y=0 上下翻转)。 */
|
||||
CMD_MIRROR_VERTICAL = 511, /**< 「镜像」二级面板:垂直镜像(按 Y=0 上下翻转)。 */
|
||||
CMD_FORMULA_EDIT = 520, /**< 「公式映射」输入框(`Y = …`)。 */
|
||||
CMD_FORMULA_RESET = 521 /**< 「公式映射」标题栏右侧的还原按钮:恢复为 `f(t)`。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -269,6 +271,27 @@ namespace jdi
|
||||
return laneIndex >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 取出并清除「需要以非模态浮层提示给用户」的消息。
|
||||
*
|
||||
* 「公式映射」解析失败时用它把警告交给宿主:宿主调用
|
||||
* `PlotView::ShowPersistentHint()` 画在绘图区底部(与"加载文件失败"同一套自绘提示,
|
||||
* **不弹 Windows 消息框**)。
|
||||
*
|
||||
* @param [OUT] text 提示文本(取出后清空)。
|
||||
* @return 存在待提示的消息返回 true。
|
||||
*/
|
||||
bool TakePendingHint(std::wstring& text)
|
||||
{
|
||||
if (m_pendingHint.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
text = m_pendingHint;
|
||||
m_pendingHint.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 向当前输入框送入一个字符。
|
||||
* @param [IN] character 输入字符('\r' 提交、'\b' 退格、0x03 复制)。
|
||||
@@ -1473,6 +1496,9 @@ namespace jdi
|
||||
bool m_plotLaneRequestFlag = false;
|
||||
int m_pendingPlotLane = -1;
|
||||
|
||||
/** 待宿主以非模态浮层提示的文本(见 `TakePendingHint`,例如公式解析失败)。 */
|
||||
std::wstring m_pendingHint;
|
||||
|
||||
/**
|
||||
* @brief 待确认的「关闭类」按钮(✕)。
|
||||
*
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "DataLoader.hpp"
|
||||
#include "DataTable.hpp"
|
||||
#include "FormulaEval.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
@@ -230,6 +231,18 @@ namespace jdi
|
||||
float offsetY = 0.0f; /**< Y 轴偏移(叠加到绘制值上),默认 0。 */
|
||||
double offsetTime = 0.0; /**< 时间轴偏移(秒,绘制在 t + offsetTime 处),默认 0。 */
|
||||
|
||||
/**
|
||||
* @brief 「公式映射」表达式(`Y = <formula>`,自变量写作 `f(t)`;空 = 不映射)。
|
||||
*
|
||||
* 结算顺序为 **公式映射 → 系数 → 绝对值 → 垂直镜像 → Y 偏移 → 时间轴偏移**,
|
||||
* 见 `ApplyDrawTransform`。表达式由 `Utilities/FormulaEval` 编译(带缓存),
|
||||
* 非法表达式的取值原样返回采样值(不会让波形消失)。
|
||||
*/
|
||||
std::wstring formula;
|
||||
|
||||
/** @brief 是否启用了「公式映射」。 */
|
||||
bool HasFormula() const { return !formula.empty(); }
|
||||
|
||||
/** @brief 镜像功能的任一分量是否启用(决定「镜像」按钮的激活态)。 */
|
||||
bool Mirrored() const { return mirrorVertical || mirrorHorizontal; }
|
||||
|
||||
@@ -275,6 +288,12 @@ namespace jdi
|
||||
*/
|
||||
inline double ApplyDrawTransform(const SignalStyle& style, double value)
|
||||
{
|
||||
// 「公式映射」最先结算(用户 2026-11-02 明确要求:公式 → 系数 → 偏移)。
|
||||
// ⚠️ 表达式编译结果按样式对象缓存,**不会**逐采样点重新解析。
|
||||
if (!style.formula.empty())
|
||||
{
|
||||
value = EvaluateFormula(style.formula, value);
|
||||
}
|
||||
if (style.gain != 1.0f)
|
||||
{
|
||||
value *= static_cast<double>(style.gain);
|
||||
@@ -325,7 +344,9 @@ namespace jdi
|
||||
*/
|
||||
inline bool DrawTransformOrderPreserving(const SignalStyle& style)
|
||||
{
|
||||
return (style.gain > 0.0f) && !style.absolute && !style.mirrorVertical;
|
||||
// ⚠️ 「公式映射」不保证保序(`x * x` 在负半轴递减),因此一律按非保序处理,
|
||||
// 让包络绘制回退到逐采样点变换(粗粒度分组层只存 min/max,非保序会算错)。
|
||||
return (style.gain > 0.0f) && !style.absolute && !style.mirrorVertical && !style.HasFormula();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -569,6 +590,19 @@ namespace jdi
|
||||
*/
|
||||
bool AddGroup(SignalGroup&& group, int& groupIndex);
|
||||
|
||||
/**
|
||||
* @brief 在某个分组末尾追加一条**新时间轴**(「重采样」派生的信号使用它)。
|
||||
*
|
||||
* 重采样产生的是**新的时间刻度**,无法复用源信号的时间轴;追加后该分组的
|
||||
* `axes` 多一条,返回它的下标,供随后 `InsertDerivedSignal` 的 `axisIndex` 使用。
|
||||
*
|
||||
* @param [IN] groupIndex 目标分组下标。
|
||||
* @param [IN,OUT] axis 时间轴(内部缓冲会被移走)。
|
||||
* @param [OUT] axisIndex 新时间轴在分组内的下标。
|
||||
* @return 追加成功返回 true(轴为空/分组越界时返回 false)。
|
||||
*/
|
||||
bool AppendGroupAxis(int groupIndex, TimeAxis&& axis, std::size_t& axisIndex);
|
||||
|
||||
/**
|
||||
* @brief 在指定分组的指定信号**下方**插入一条新信号(派生信号)。
|
||||
*
|
||||
@@ -831,6 +865,33 @@ namespace jdi
|
||||
double time = 0.0; /**< 显示时刻(秒)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 「重采样」中某个信号自己的参数。
|
||||
*
|
||||
* 该功能把**非固定频率**采样的信号规整成固定间隔采样(超分 / 降采样),
|
||||
* 再把结果派生为一条新信号。参数按信号保存,同一网格内不同信号可以用不同间隔。
|
||||
*
|
||||
* ⚠️ `interval` 只在**首次勾选**(`initialized == false`)时初始化为该信号的最小
|
||||
* 数据间隔;之后用户手动输入的间隔不会被重新初始化覆盖。
|
||||
*/
|
||||
struct ResampleItem
|
||||
{
|
||||
SignalRef ref; /**< 目标信号。 */
|
||||
double interval = 0.0; /**< 重采样间隔(秒,> 0)。 */
|
||||
/** @brief `interval` 是否已经按「该信号最小数据间隔」初始化过。 */
|
||||
bool initialized = false;
|
||||
/**
|
||||
* @brief 该信号的**最大 / 最小**数据间隔(相邻有效采样点时间差的极值)。
|
||||
*
|
||||
* ⚠️ 在勾选该信号时由 `PlotView` 一次性算好并缓存(千万级信号逐帧重算会卡死),
|
||||
* 面板绘制只读这两个字段。数据加载后不再变化,因此无需失效。
|
||||
*/
|
||||
double maxInterval = 0.0;
|
||||
double minInterval = 0.0;
|
||||
/** @brief 参数行是否被**收起**(只保留信号色叠底的条目栏)。 */
|
||||
bool collapsed = false;
|
||||
};
|
||||
|
||||
std::wstring name; /**< 网格名称。 */
|
||||
std::vector<SignalRef> signals; /**< 该网格包含的信号(按加入顺序)。 */
|
||||
bool autoRange = true; /**< 是否依据信号取值范围自动计算 Y 轴。 */
|
||||
@@ -1102,6 +1163,42 @@ namespace jdi
|
||||
*/
|
||||
float edgeCountScroll = 0.0f;
|
||||
|
||||
/**
|
||||
* @brief 「实验性功能 → 重采样」开关(功能总开关)。
|
||||
*
|
||||
* 开启后本网格出现浮动面板:以网格内每个信号为条目(信号色叠底 + 复选框 +
|
||||
* 展开/收起),展开后显示「最大数据间隔 / 最小数据间隔」两行统计与
|
||||
* 「重采样间隔 / 重采样频率」两行**联动输入框**;底部「构建为新信号」按当前参数
|
||||
* 把勾选的信号重采样成固定频率并插到源信号下方。
|
||||
*
|
||||
* 与其它实验性功能一样属于**网格级**状态,重建网格时按信号继承(只由原网格首个信号持有)。
|
||||
*/
|
||||
bool resample = false;
|
||||
|
||||
/** @brief 参与重采样的信号及其参数(只包含被勾选的信号)。 */
|
||||
std::vector<ResampleItem> resampleItems;
|
||||
|
||||
/** 取某个信号的重采样参数(未勾选时返回空指针)。 */
|
||||
const ResampleItem* FindResampleItem(const SignalRef& ref) const;
|
||||
|
||||
/** 取得或创建某信号的重采样参数(用于勾选)。 */
|
||||
ResampleItem& EnsureResampleItem(const SignalRef& ref);
|
||||
|
||||
/** 取消某信号的重采样勾选。 */
|
||||
void RemoveResampleItem(const SignalRef& ref);
|
||||
|
||||
/** 取某信号的重采样勾选状态。 */
|
||||
bool ResampleEnabled(const SignalRef& ref) const
|
||||
{
|
||||
return FindResampleItem(ref) != nullptr;
|
||||
}
|
||||
|
||||
/** 重采样浮动面板的 UI 状态(展开/折起 + 位置)。 */
|
||||
LanePanelState resamplePanel;
|
||||
|
||||
/** @brief 「重采样」面板内容的滚动偏移(客户区像素,0 = 顶部)。 */
|
||||
float resampleScroll = 0.0f;
|
||||
|
||||
/**
|
||||
* @brief 「自由标记」面板里被**收起**的信号分组(点击分组栏切换)。
|
||||
*
|
||||
|
||||
@@ -87,6 +87,7 @@ namespace jdi
|
||||
prune(lane.edgeCenterItems);
|
||||
prune(lane.weakPeakItems);
|
||||
prune(lane.freeMarkerItems);
|
||||
prune(lane.resampleItems);
|
||||
lane.freeMarkerCollapsedGroups.erase(
|
||||
std::remove_if(lane.freeMarkerCollapsedGroups.begin(), lane.freeMarkerCollapsedGroups.end(),
|
||||
[&inLane](const SignalRef& ref) { return !inLane(ref); }),
|
||||
@@ -140,6 +141,11 @@ namespace jdi
|
||||
bool freeMarkerShowDelta = false;
|
||||
bool freeMarkerShowEnum = false;
|
||||
std::vector<SignalRef> freeMarkerCollapsedGroups;
|
||||
/** @brief 「重采样」开关与逐信号参数(详见 `Lane::resample`)。 */
|
||||
bool resample = false;
|
||||
std::vector<Lane::ResampleItem> resampleItems;
|
||||
LanePanelState resamplePanel;
|
||||
float resampleScroll = 0.0f;
|
||||
/** @brief 「均值滤波」面板内容的滚动偏移(详见 `Lane::meanFilterScroll`)。 */
|
||||
float meanFilterScroll = 0.0f;
|
||||
/** @brief 「中心轴边沿计数」面板内容的滚动偏移(详见 `Lane::edgeCenterScroll`)。 */
|
||||
@@ -199,6 +205,10 @@ namespace jdi
|
||||
carry.freeMarkerShowDelta = lane.freeMarkerShowDelta;
|
||||
carry.freeMarkerShowEnum = lane.freeMarkerShowEnum;
|
||||
carry.freeMarkerCollapsedGroups = lane.freeMarkerCollapsedGroups;
|
||||
carry.resample = lane.resample;
|
||||
carry.resampleItems = lane.resampleItems;
|
||||
carry.resamplePanel = lane.resamplePanel;
|
||||
carry.resampleScroll = lane.resampleScroll;
|
||||
for (std::size_t signalIndex = 0; signalIndex < lane.signals.size(); ++signalIndex)
|
||||
{
|
||||
LaneCarry entry = carry;
|
||||
@@ -271,6 +281,13 @@ namespace jdi
|
||||
lane.freeMarkerShowEnum = carry.freeMarkerShowEnum;
|
||||
lane.freeMarkerCollapsedGroups = carry.freeMarkerCollapsedGroups;
|
||||
lane.meanFilterScroll = carry.meanFilterScroll;
|
||||
lane.resample = carry.resample;
|
||||
lane.resampleItems = carry.resampleItems;
|
||||
lane.resamplePanel.open = carry.resamplePanel.open;
|
||||
lane.resamplePanel.collapsed = carry.resamplePanel.collapsed;
|
||||
lane.resamplePanel.x = carry.resamplePanel.x;
|
||||
lane.resamplePanel.y = carry.resamplePanel.y;
|
||||
lane.resampleScroll = carry.resampleScroll;
|
||||
};
|
||||
|
||||
// 收集全部可见信号引用。
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef __FORMULA_EVAL_HPP__
|
||||
#define __FORMULA_EVAL_HPP__
|
||||
|
||||
// 「公式映射」的表达式编译与求值(信号样式区的 `Y = …` 输入框)。
|
||||
//
|
||||
// 语法(刻意保持极小,避免引入脚本引擎与额外的体积):
|
||||
// expr := term (('+' | '-') term)*
|
||||
// term := unary (('*' | '/') unary)*
|
||||
// unary := ('+' | '-') unary | primary
|
||||
// primary := number | '(' expr ')' | variable
|
||||
// number := 十进制小数(支持 e/E 指数)
|
||||
// variable:= `X` / `x` / `f` / `f(t)` —— 都表示"当前采样点的值"
|
||||
// 另外支持常量 `pi`。
|
||||
//
|
||||
// ⚠️ 求值走 `thread_local` 的单条缓存(按 `std::wstring` 的 data 指针 + 内容判定):
|
||||
// 绘制循环里同一条信号的样式对象是稳定的,因此每个信号只需编译一次;
|
||||
// **绝不能**每个采样点都重新解析(千万级采样会慢到不可用)。
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
/**
|
||||
* @brief 校验公式文本是否可以解析。
|
||||
*
|
||||
* 空文本是合法输入(表示"不做映射")。
|
||||
*
|
||||
* @param [IN] text 公式文本(不含 `Y =` 前缀)。
|
||||
* @param [OUT] error 解析失败时的原因(成功时清空),允许为空。
|
||||
* @return 语法合法返回 true。
|
||||
*/
|
||||
bool ValidateFormula(const std::wstring& text, std::wstring* error);
|
||||
|
||||
/**
|
||||
* @brief 计算公式在给定采样值处的取值。
|
||||
*
|
||||
* 文本为空或无法解析时**原样返回** `x`(保证绘制不会因为非法公式而消失)。
|
||||
* 计算结果非有限(如 1/0)时返回 NaN(保持断线语义)。
|
||||
*
|
||||
* @param [IN] text 公式文本。
|
||||
* @param [IN] x 采样值(公式中的 `f(t)` / `X`)。
|
||||
* @return 映射后的值。
|
||||
*/
|
||||
double EvaluateFormula(const std::wstring& text, double x);
|
||||
|
||||
/**
|
||||
* @brief 把用户输入里的自变量写法统一成 `f(t)`(保存前调用)。
|
||||
*
|
||||
* 规则:把独立的 `X` / `x` / `f` 记号改写为 `f(t)`;`f(t)` 原样保留。
|
||||
* 只做记号级替换(不会动标识符中间或数字里的字符)。
|
||||
*
|
||||
* @param [IN] text 用户输入的公式。
|
||||
* @return 归一化后的文本。
|
||||
*/
|
||||
std::wstring NormalizeFormulaText(const std::wstring& text);
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __FORMULA_EVAL_HPP__
|
||||
@@ -121,6 +121,16 @@ namespace jdi
|
||||
*/
|
||||
std::wstring FormatDurationSeconds(double seconds);
|
||||
|
||||
/**
|
||||
* @brief 把频率格式化为带自适应单位的文本(Hz / kHz / MHz / GHz,保留小数)。
|
||||
*
|
||||
* 用于「重采样」面板与派生信号名称(`<原信号> (重采样 1.2 kHz)`)。
|
||||
*
|
||||
* @param [IN] hertz 频率(Hz)。
|
||||
* @return 形如 "1.2 kHz" 的文本;非法值返回 "—"。
|
||||
*/
|
||||
std::wstring FormatFrequency(double hertz);
|
||||
|
||||
/**
|
||||
* @brief 计算“美观”的坐标轴刻度步长(1/2/5 乘以 10 的整数次幂)。
|
||||
* @param [IN] range 坐标轴数值跨度,必须为正数。
|
||||
|
||||
@@ -421,6 +421,23 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DataSet::AppendGroupAxis(int groupIndex, TimeAxis&& axis, std::size_t& axisIndex)
|
||||
{
|
||||
axisIndex = 0;
|
||||
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (axis.time.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
SignalGroup& group = m_groups[static_cast<std::size_t>(groupIndex)];
|
||||
group.axes.push_back(std::move(axis));
|
||||
axisIndex = group.axes.size() - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DataSet::InsertDerivedSignal(int groupIndex, int afterSignalIndex, SignalData&& signal,
|
||||
int& newSignalIndex)
|
||||
{
|
||||
@@ -1396,6 +1413,43 @@ namespace jdi
|
||||
weakPeakItems.end());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 重采样
|
||||
|
||||
const Lane::ResampleItem* Lane::FindResampleItem(const SignalRef& ref) const
|
||||
{
|
||||
for (const ResampleItem& item : resampleItems)
|
||||
{
|
||||
if (item.ref == ref)
|
||||
{
|
||||
return &item;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Lane::ResampleItem& Lane::EnsureResampleItem(const SignalRef& ref)
|
||||
{
|
||||
for (ResampleItem& item : resampleItems)
|
||||
{
|
||||
if (item.ref == ref)
|
||||
{
|
||||
return item;
|
||||
}
|
||||
}
|
||||
ResampleItem item;
|
||||
item.ref = ref;
|
||||
resampleItems.push_back(item);
|
||||
return resampleItems.back();
|
||||
}
|
||||
|
||||
void Lane::RemoveResampleItem(const SignalRef& ref)
|
||||
{
|
||||
resampleItems.erase(
|
||||
std::remove_if(resampleItems.begin(), resampleItems.end(),
|
||||
[&ref](const ResampleItem& item) { return item.ref == ref; }),
|
||||
resampleItems.end());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 自由标记
|
||||
|
||||
namespace
|
||||
|
||||
@@ -145,6 +145,7 @@ namespace jdi
|
||||
purgeRefs(lane.meanFilterItems);
|
||||
purgeRefs(lane.edgeCenterItems);
|
||||
purgeRefs(lane.weakPeakItems);
|
||||
purgeRefs(lane.resampleItems);
|
||||
}
|
||||
// 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。
|
||||
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
|
||||
@@ -449,6 +450,9 @@ namespace jdi
|
||||
lane.freeMarkerShowEnum = source.freeMarkerShowEnum;
|
||||
lane.freeMarkerPanel = source.freeMarkerPanel;
|
||||
lane.freeMarkerOpacity = source.freeMarkerOpacity;
|
||||
lane.resample = source.resample;
|
||||
lane.resamplePanel = source.resamplePanel;
|
||||
lane.resampleScroll = source.resampleScroll;
|
||||
// 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 弱峰调整 / 自由标记条目。
|
||||
for (const Lane::MeanFilterItem& item : source.meanFilterItems)
|
||||
{
|
||||
@@ -487,6 +491,13 @@ namespace jdi
|
||||
lane.freeMarkerItems.push_back(item);
|
||||
}
|
||||
}
|
||||
for (const Lane::ResampleItem& item : source.resampleItems)
|
||||
{
|
||||
if (item.ref == ref)
|
||||
{
|
||||
lane.resampleItems.push_back(item);
|
||||
}
|
||||
}
|
||||
for (const SignalRef& collapsed : source.freeMarkerCollapsedGroups)
|
||||
{
|
||||
if (collapsed == ref)
|
||||
@@ -557,6 +568,14 @@ namespace jdi
|
||||
++item.ref.signalIndex;
|
||||
}
|
||||
}
|
||||
// 「重采样」条目同样持有信号引用。
|
||||
for (Lane::ResampleItem& item : lane.resampleItems)
|
||||
{
|
||||
if (item.ref.groupIndex == groupIndex && item.ref.signalIndex >= insertPos)
|
||||
{
|
||||
++item.ref.signalIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,7 +710,36 @@ namespace jdi
|
||||
double signalHigh = signal->maxValue;
|
||||
if (style != nullptr)
|
||||
{
|
||||
TransformValueRange(*style, signalLow, signalHigh);
|
||||
if (style->HasFormula())
|
||||
{
|
||||
// ⚠️ 「公式映射」不保证单调(`x*x` / `1/x`)⇒ 端点映射会算错,
|
||||
// 只能逐点扫描(只在自适应重算时发生,不是每帧路径)。
|
||||
double formulaLow = std::numeric_limits<double>::infinity();
|
||||
double formulaHigh = -std::numeric_limits<double>::infinity();
|
||||
for (const double raw : signal->values)
|
||||
{
|
||||
if (!std::isfinite(raw))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double mapped = ApplyDrawTransform(*style, raw);
|
||||
if (!std::isfinite(mapped))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
formulaLow = std::min(formulaLow, mapped);
|
||||
formulaHigh = std::max(formulaHigh, mapped);
|
||||
}
|
||||
if (std::isfinite(formulaLow) && std::isfinite(formulaHigh))
|
||||
{
|
||||
signalLow = formulaLow;
|
||||
signalHigh = formulaHigh;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TransformValueRange(*style, signalLow, signalHigh);
|
||||
}
|
||||
}
|
||||
low = std::min(low, signalLow);
|
||||
high = std::max(high, signalHigh);
|
||||
|
||||
@@ -0,0 +1,586 @@
|
||||
// 「公式映射」表达式的编译 / 求值实现(详见 `FormulaEval.hpp`)。
|
||||
|
||||
#include "FormulaEval.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <cwctype>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
namespace
|
||||
{
|
||||
/** 公式允许的最大长度(防止恶意/误粘贴的超长文本导致解析耗时)。 */
|
||||
constexpr std::size_t kMaxFormulaLength = 512;
|
||||
/** 允许的最大记号数(与长度一起兜底)。 */
|
||||
constexpr std::size_t kMaxFormulaTokens = 512;
|
||||
|
||||
/** 逆波兰式里的一个运算单元。 */
|
||||
struct FormulaOp
|
||||
{
|
||||
enum class Kind : int
|
||||
{
|
||||
NUMBER = 0, /**< 常量。 */
|
||||
VARIABLE, /**< 自变量(采样值)。 */
|
||||
ADD,
|
||||
SUBTRACT,
|
||||
MULTIPLY,
|
||||
DIVIDE,
|
||||
NEGATE
|
||||
};
|
||||
|
||||
Kind kind = Kind::NUMBER;
|
||||
double value = 0.0;
|
||||
};
|
||||
|
||||
/** 单个自变量占用的栈槽(求值时用)。 */
|
||||
constexpr std::size_t kMaxStack = 64;
|
||||
|
||||
/** 记号类型。 */
|
||||
enum class TokenKind : int
|
||||
{
|
||||
NUMBER = 0,
|
||||
VARIABLE,
|
||||
PLUS,
|
||||
MINUS,
|
||||
STAR,
|
||||
SLASH,
|
||||
LEFT_PAREN,
|
||||
RIGHT_PAREN,
|
||||
END
|
||||
};
|
||||
|
||||
struct Token
|
||||
{
|
||||
TokenKind kind = TokenKind::END;
|
||||
double value = 0.0;
|
||||
};
|
||||
|
||||
/** 把一个公式文本切成记号序列(失败返回 false 并写 error)。 */
|
||||
bool Tokenize(const std::wstring& text, std::vector<Token>& tokens, std::wstring* error)
|
||||
{
|
||||
tokens.clear();
|
||||
std::size_t index = 0;
|
||||
const std::size_t length = text.size();
|
||||
if (length > kMaxFormulaLength)
|
||||
{
|
||||
if (error != nullptr)
|
||||
{
|
||||
*error = L"公式过长(最多 512 个字符)。";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
while (index < length)
|
||||
{
|
||||
const wchar_t ch = text[index];
|
||||
if (std::iswspace(static_cast<wint_t>(ch)) != 0)
|
||||
{
|
||||
++index;
|
||||
continue;
|
||||
}
|
||||
if (ch == L'+')
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::PLUS, 0.0});
|
||||
++index;
|
||||
}
|
||||
else if (ch == L'-')
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::MINUS, 0.0});
|
||||
++index;
|
||||
}
|
||||
else if (ch == L'*')
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::STAR, 0.0});
|
||||
++index;
|
||||
}
|
||||
else if (ch == L'/')
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::SLASH, 0.0});
|
||||
++index;
|
||||
}
|
||||
else if (ch == L'(')
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::LEFT_PAREN, 0.0});
|
||||
++index;
|
||||
}
|
||||
else if (ch == L')')
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::RIGHT_PAREN, 0.0});
|
||||
++index;
|
||||
}
|
||||
else if ((ch >= L'0' && ch <= L'9') || ch == L'.')
|
||||
{
|
||||
// 数字:手工扫描,交给 `wcstod` 转换(保证与内联编辑的数值语义一致)。
|
||||
const std::size_t start = index;
|
||||
while (index < length &&
|
||||
((text[index] >= L'0' && text[index] <= L'9') || text[index] == L'.'))
|
||||
{
|
||||
++index;
|
||||
}
|
||||
if (index < length && (text[index] == L'e' || text[index] == L'E'))
|
||||
{
|
||||
++index;
|
||||
if (index < length && (text[index] == L'+' || text[index] == L'-'))
|
||||
{
|
||||
++index;
|
||||
}
|
||||
while (index < length && text[index] >= L'0' && text[index] <= L'9')
|
||||
{
|
||||
++index;
|
||||
}
|
||||
}
|
||||
const std::wstring literal = text.substr(start, index - start);
|
||||
wchar_t* stop = nullptr;
|
||||
const double value = std::wcstod(literal.c_str(), &stop);
|
||||
if (stop == literal.c_str() || !std::isfinite(value))
|
||||
{
|
||||
if (error != nullptr)
|
||||
{
|
||||
*error = L"数字格式不正确。";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
tokens.push_back(Token{TokenKind::NUMBER, value});
|
||||
}
|
||||
else if (std::iswalpha(static_cast<wint_t>(ch)) != 0)
|
||||
{
|
||||
// 标识符:只允许 X / x / f / f(t) / pi。
|
||||
const std::size_t start = index;
|
||||
while (index < length && std::iswalpha(static_cast<wint_t>(text[index])) != 0)
|
||||
{
|
||||
++index;
|
||||
}
|
||||
std::wstring name = text.substr(start, index - start);
|
||||
// 跳过空格后可能是 `(t)`(`f(t)` 的写法)。
|
||||
std::size_t look = index;
|
||||
while (look < length && std::iswspace(static_cast<wint_t>(text[look])) != 0)
|
||||
{
|
||||
++look;
|
||||
}
|
||||
if (look < length && text[look] == L'(')
|
||||
{
|
||||
std::size_t inner = look + 1;
|
||||
while (inner < length && std::iswspace(static_cast<wint_t>(text[inner])) != 0)
|
||||
{
|
||||
++inner;
|
||||
}
|
||||
if (inner < length && (text[inner] == L't' || text[inner] == L'T'))
|
||||
{
|
||||
++inner;
|
||||
while (inner < length && std::iswspace(static_cast<wint_t>(text[inner])) != 0)
|
||||
{
|
||||
++inner;
|
||||
}
|
||||
if (inner < length && text[inner] == L')')
|
||||
{
|
||||
index = inner + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (wchar_t& letter : name)
|
||||
{
|
||||
letter = static_cast<wchar_t>(std::towlower(static_cast<wint_t>(letter)));
|
||||
}
|
||||
if (name == L"x" || name == L"f")
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::VARIABLE, 0.0});
|
||||
}
|
||||
else if (name == L"pi")
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::NUMBER, 3.14159265358979323846});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (error != nullptr)
|
||||
{
|
||||
*error = L"无法识别的标识符(自变量请写 f(t) 或 X)。";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (error != nullptr)
|
||||
{
|
||||
*error = L"公式含有无法识别的字符。";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
tokens.push_back(Token{TokenKind::END, 0.0});
|
||||
if (tokens.size() > kMaxFormulaTokens)
|
||||
{
|
||||
if (error != nullptr)
|
||||
{
|
||||
*error = L"公式过于复杂。";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 递归下降解析器(直接产出逆波兰式)。 */
|
||||
class Parser
|
||||
{
|
||||
public:
|
||||
Parser(const std::vector<Token>& tokens, std::vector<FormulaOp>& output, std::wstring* error)
|
||||
: m_tokens(tokens), m_output(output), m_error(error)
|
||||
{
|
||||
}
|
||||
|
||||
bool Parse()
|
||||
{
|
||||
if (!ParseExpression())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Current().kind != TokenKind::END)
|
||||
{
|
||||
return SetError(L"公式存在多余的内容(请检查括号是否配对)。");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
const Token& Current() const { return m_tokens[m_position]; }
|
||||
void Advance()
|
||||
{
|
||||
if (m_position + 1 < m_tokens.size())
|
||||
{
|
||||
++m_position;
|
||||
}
|
||||
}
|
||||
bool SetError(const wchar_t* message)
|
||||
{
|
||||
if (m_error != nullptr)
|
||||
{
|
||||
*m_error = message;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ParseExpression()
|
||||
{
|
||||
if (!ParseTerm())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
while (Current().kind == TokenKind::PLUS || Current().kind == TokenKind::MINUS)
|
||||
{
|
||||
const bool add = (Current().kind == TokenKind::PLUS);
|
||||
Advance();
|
||||
if (!ParseTerm())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_output.push_back(FormulaOp{add ? FormulaOp::Kind::ADD
|
||||
: FormulaOp::Kind::SUBTRACT,
|
||||
0.0});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseTerm()
|
||||
{
|
||||
if (!ParseUnary())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
while (Current().kind == TokenKind::STAR || Current().kind == TokenKind::SLASH)
|
||||
{
|
||||
const bool multiply = (Current().kind == TokenKind::STAR);
|
||||
Advance();
|
||||
if (!ParseUnary())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_output.push_back(FormulaOp{multiply ? FormulaOp::Kind::MULTIPLY
|
||||
: FormulaOp::Kind::DIVIDE,
|
||||
0.0});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseUnary()
|
||||
{
|
||||
if (Current().kind == TokenKind::PLUS)
|
||||
{
|
||||
Advance();
|
||||
return ParseUnary();
|
||||
}
|
||||
if (Current().kind == TokenKind::MINUS)
|
||||
{
|
||||
Advance();
|
||||
if (!ParseUnary())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_output.push_back(FormulaOp{FormulaOp::Kind::NEGATE, 0.0});
|
||||
return true;
|
||||
}
|
||||
return ParsePrimary();
|
||||
}
|
||||
|
||||
bool ParsePrimary()
|
||||
{
|
||||
const Token& token = Current();
|
||||
if (token.kind == TokenKind::NUMBER)
|
||||
{
|
||||
m_output.push_back(FormulaOp{FormulaOp::Kind::NUMBER, token.value});
|
||||
Advance();
|
||||
return true;
|
||||
}
|
||||
if (token.kind == TokenKind::VARIABLE)
|
||||
{
|
||||
m_output.push_back(FormulaOp{FormulaOp::Kind::VARIABLE, 0.0});
|
||||
Advance();
|
||||
return true;
|
||||
}
|
||||
if (token.kind == TokenKind::LEFT_PAREN)
|
||||
{
|
||||
Advance();
|
||||
if (!ParseExpression())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Current().kind != TokenKind::RIGHT_PAREN)
|
||||
{
|
||||
return SetError(L"缺少右括号。");
|
||||
}
|
||||
Advance();
|
||||
return true;
|
||||
}
|
||||
return SetError(L"公式不完整或存在多余的运算符。");
|
||||
}
|
||||
|
||||
const std::vector<Token>& m_tokens;
|
||||
std::vector<FormulaOp>& m_output;
|
||||
std::wstring* m_error;
|
||||
std::size_t m_position = 0;
|
||||
};
|
||||
|
||||
/** 编译结果(逆波兰式);空表示"不可用"。 */
|
||||
using Program = std::vector<FormulaOp>;
|
||||
|
||||
/** 线程内的单条编译缓存(按 data 指针 + 内容判定)。 */
|
||||
struct FormulaCache
|
||||
{
|
||||
const wchar_t* data = nullptr;
|
||||
std::size_t size = 0;
|
||||
std::wstring text;
|
||||
Program program;
|
||||
bool compiled = false;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 取(必要时编译)公式对应的程序。
|
||||
* @return 合法时返回程序指针;空文本或非法时返回 nullptr。
|
||||
*/
|
||||
const Program* AcquireProgram(const std::wstring& text)
|
||||
{
|
||||
if (text.empty())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
static thread_local FormulaCache cache;
|
||||
if (cache.data == text.data() && cache.size == text.size() && cache.compiled)
|
||||
{
|
||||
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;
|
||||
cache.compiled = true;
|
||||
|
||||
std::vector<Token> tokens;
|
||||
std::wstring error;
|
||||
if (!Tokenize(text, tokens, &error))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
Parser parser(tokens, cache.program, &error);
|
||||
if (!parser.Parse())
|
||||
{
|
||||
cache.program.clear();
|
||||
return nullptr;
|
||||
}
|
||||
cache.valid = true;
|
||||
return &cache.program;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool ValidateFormula(const std::wstring& text, std::wstring* error)
|
||||
{
|
||||
if (error != nullptr)
|
||||
{
|
||||
error->clear();
|
||||
}
|
||||
if (text.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
std::vector<Token> tokens;
|
||||
if (!Tokenize(text, tokens, error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::vector<FormulaOp> program;
|
||||
Parser parser(tokens, program, error);
|
||||
if (!parser.Parse())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
double EvaluateFormula(const std::wstring& text, double x)
|
||||
{
|
||||
const Program* program = AcquireProgram(text);
|
||||
if (program == nullptr)
|
||||
{
|
||||
return x; // 空 / 非法:不做映射(保证波形照常显示)
|
||||
}
|
||||
double stack[kMaxStack];
|
||||
std::size_t depth = 0;
|
||||
for (const FormulaOp& op : *program)
|
||||
{
|
||||
switch (op.kind)
|
||||
{
|
||||
case FormulaOp::Kind::NUMBER:
|
||||
case FormulaOp::Kind::VARIABLE:
|
||||
if (depth >= kMaxStack)
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
stack[depth++] = (op.kind == FormulaOp::Kind::NUMBER) ? op.value : x;
|
||||
break;
|
||||
case FormulaOp::Kind::NEGATE:
|
||||
if (depth < 1)
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
stack[depth - 1] = -stack[depth - 1];
|
||||
break;
|
||||
default:
|
||||
{
|
||||
if (depth < 2)
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
const double b = stack[depth - 1];
|
||||
const double a = stack[depth - 2];
|
||||
--depth;
|
||||
double result = 0.0;
|
||||
switch (op.kind)
|
||||
{
|
||||
case FormulaOp::Kind::ADD:
|
||||
result = a + b;
|
||||
break;
|
||||
case FormulaOp::Kind::SUBTRACT:
|
||||
result = a - b;
|
||||
break;
|
||||
case FormulaOp::Kind::MULTIPLY:
|
||||
result = a * b;
|
||||
break;
|
||||
case FormulaOp::Kind::DIVIDE:
|
||||
result = a / b;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
stack[depth - 1] = result;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (depth != 1)
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
return std::isfinite(stack[0]) ? stack[0] : std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
|
||||
std::wstring NormalizeFormulaText(const std::wstring& text)
|
||||
{
|
||||
std::wstring output;
|
||||
output.reserve(text.size() + 8);
|
||||
const std::size_t length = text.size();
|
||||
std::size_t index = 0;
|
||||
while (index < length)
|
||||
{
|
||||
const wchar_t ch = text[index];
|
||||
if (std::iswalpha(static_cast<wint_t>(ch)) == 0)
|
||||
{
|
||||
output.push_back(ch);
|
||||
++index;
|
||||
continue;
|
||||
}
|
||||
const std::size_t start = index;
|
||||
while (index < length && std::iswalpha(static_cast<wint_t>(text[index])) != 0)
|
||||
{
|
||||
++index;
|
||||
}
|
||||
std::wstring name = text.substr(start, index - start);
|
||||
// 已经是 `f(t)`:跳过随后的 `(t)`,整体原样输出(统一写成小写形式)。
|
||||
std::size_t look = index;
|
||||
while (look < length && std::iswspace(static_cast<wint_t>(text[look])) != 0)
|
||||
{
|
||||
++look;
|
||||
}
|
||||
bool hasArgument = false;
|
||||
if (look < length && text[look] == L'(')
|
||||
{
|
||||
std::size_t inner = look + 1;
|
||||
while (inner < length && std::iswspace(static_cast<wint_t>(text[inner])) != 0)
|
||||
{
|
||||
++inner;
|
||||
}
|
||||
if (inner < length && (text[inner] == L't' || text[inner] == L'T'))
|
||||
{
|
||||
++inner;
|
||||
while (inner < length && std::iswspace(static_cast<wint_t>(text[inner])) != 0)
|
||||
{
|
||||
++inner;
|
||||
}
|
||||
if (inner < length && text[inner] == L')')
|
||||
{
|
||||
hasArgument = true;
|
||||
index = inner + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::wstring lowered = name;
|
||||
for (wchar_t& letter : lowered)
|
||||
{
|
||||
letter = static_cast<wchar_t>(std::towlower(static_cast<wint_t>(letter)));
|
||||
}
|
||||
if (hasArgument && (lowered == L"f" || lowered == L"x"))
|
||||
{
|
||||
// 统一写成小写规范形式 `f(t)`(保留用户其余写法)。
|
||||
output += L"f(t)";
|
||||
}
|
||||
else if (!hasArgument && (lowered == L"x" || lowered == L"f"))
|
||||
{
|
||||
output += L"f(t)";
|
||||
}
|
||||
else
|
||||
{
|
||||
output += name;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
} // namespace jdi
|
||||
@@ -471,6 +471,32 @@ namespace jdi
|
||||
return FormatValue(std::fabs(seconds));
|
||||
}
|
||||
|
||||
std::wstring FormatFrequency(double hertz)
|
||||
{
|
||||
if (!std::isfinite(hertz) || hertz <= 0.0)
|
||||
{
|
||||
return L"—";
|
||||
}
|
||||
const wchar_t* unit = L"Hz";
|
||||
double scaled = hertz;
|
||||
if (hertz >= 1.0e9)
|
||||
{
|
||||
unit = L"GHz";
|
||||
scaled = hertz / 1.0e9;
|
||||
}
|
||||
else if (hertz >= 1.0e6)
|
||||
{
|
||||
unit = L"MHz";
|
||||
scaled = hertz / 1.0e6;
|
||||
}
|
||||
else if (hertz >= 1.0e3)
|
||||
{
|
||||
unit = L"kHz";
|
||||
scaled = hertz / 1.0e3;
|
||||
}
|
||||
return FormatValue(scaled) + L" " + unit;
|
||||
}
|
||||
|
||||
std::wstring FormatByteSize(unsigned long long bytes)
|
||||
{ // 单位自适应:GB / MB / KB(阈值按 1024 进制)。
|
||||
constexpr double kKilo = 1024.0;
|
||||
|
||||
Reference in new issue
Block a user