边沿计数改用「绘制值」+ 派生信号烘入时间偏移 + 图例托底统一为 GX 徽标款 + 调整二级面板向上展开
一、两个「边沿计数」按「画出来的曲线」判定(原先按原始采样值 → 应用过「调整」的信号恒为 0)
- 用户复述的权威定义:设定一个横向(Y 轴)光标,信号绘制出来的曲线从下往上穿过该幅值 = 上升沿,
从上往下穿过 = 下降沿。
- 症状:VERY_BIG_DATA 的「编码器B相」设系数 50 + 时间轴偏移 -20s,把光标拖到画面中间,
很规整的方波却统计出 0/0。
- 根因:EdgeCountsFor / EdgeCenterCountsFor 只比较 signal.values 的原始采样值,而横向光标读的是
屏幕上的值(已乘系数、已加偏移)→ 原始值域与阈值完全不相交。
- 修法:判定值一律取 ApplyDrawTransform(style, values[SourceIndexFor(style, count, index)]);
* 中心轴两侧必须套用同一套变换(drawn - drawnCenter);
* 水平镜像时曲线数组按源下标存储、而统计区间是显示下标 → 请求曲线窗口前必须把
[needFirst, needEnd) 映射为 [count-needEnd, count-needFirst);
* 缓存必须带样式指纹:EdgeCountEntry / EdgeCenterCountEntry 新增 styleKey
(EdgeStyleKey():gain/offsetY 按 1e6 定点 + absolute/mirrorVertical/mirrorHorizontal 位),
否则改完「调整」仍会命中变换前的旧计数。
二、时间区间之外的采样不得计入(区间边界点)
- 症状:A 在阈值以下、B 超过阈值;双光标下一个光标正好在 B 点,旧实现把 B 算成一个上升沿。
- 根因:EdgeIndexRange 在左端多取了一个采样点(if (begin > 0) --begin;)。
- 修法:删掉该外扩,收紧为「只有两个端点都落在 [begin, end) 内的采样对才计入」
(begin = lower_bound(rangeMin)、end = upper_bound(rangeMax),调用方从 begin+1 枚举到 end-1)。
- 结果:把区间起点从 times[crossing-1] 移到 times[crossing] 恰好少一个上升沿。
三、派生信号把 offsetTime 也烘进采样值(推翻 §9.19 的「唯一例外」)
- 用户要求:新信号默认不启用任何「调整」时就必须与网格里的预览逐点重合。
- 修法(Application::BuildDerivedSignal,在「值变换 + 水平镜像反序」之后追加一步):
新信号第 i 个采样 = 已变换的曲线在 time[i] - offsetTime 处的取值(取最近的采样点);
单调时间轴用只前进的游标(整段 O(n));平移后越界的采样置 NaN(与预览一致);
新信号样式全部保持默认(gain=1 / absolute=false / 两个镜像=false / offsetY=0 / offsetTime=0)。
- 退化路径:时间轴非单调或时间列缺失时无法按下标推进取样,退回把 offsetTime 带在样式上
(两条路径的显示效果相同)。
四、图例托底统一为 GX 编号徽标同款 + 新增更小字体 uiTiny
- 托底高度 = kLaneBadgeHeight、上下边界与 LaneBadgeRect() 完全一致、圆角 = 高度的一半(胶囊形)、
配色与描边与徽标相同(选中态 accentSoft/accent)、水平内边距 = 胶囊半径。
- 文本改用新增字体 UiFonts::uiTiny(10 逻辑 px):原 uiSmall(12px) 几乎顶满 17px 高的胶囊。
- LegendItem 拆出 plateWidth(托底宽 = 内容宽 + 2×内边距),条目步进 = plateWidth + kLegendItemGap;
kLegendPlateRadius 已删除(半径一律由徽标高度推出)。
五、「系数/偏移」「镜像」二级面板改为向上展开
- PopupMenu::Open(..., bool expandUp = false):true 时 (x, y) 是菜单的左下角、菜单向上展开;
BuildLayout 增加同参数并记录 m_buildExpandUp(RebuildLayout 必须复用,否则数值回写触发的重排
会让面板跳回向下展开)。
- SignalPanel::OpenGainMenu / OpenMirrorMenu:锚点 = (button.X, button.Y - kAdjustMenuGap),
即「面板左下角对准按钮左上角、再略微上去一点」(kAdjustMenuGap = 4 逻辑像素)。
六、验证(全部离屏,不占用用户的鼠标 / 键盘 / 屏幕)
- .preflight/uitest 新增 [AR]:
* 边沿计数 plain / gain=50 / offsetY / mirrorVertical / mirrorHorizontal 逐个与「独立参考实现」
逐位一致;offsetY 用例给出 A/B:按原始采样值算会得到 7/6 个边沿(正是用户报告的缺陷);
* 区间边界:起点从 crossing-1 移到 crossing 恰好少一个上升沿;
* 中心轴:absolute + offsetY 的样式下与参考实现逐位一致(32234 个边沿量级);
* 派生信号:无偏移时全部 128 万采样逐点等于 transform(滤波);带 +10 个平均采样周期的偏移时,
抽样点等于 transform(滤波) 在 time[i] - offsetTime(最近采样)处的取值;样式全默认;
* 图例托底:height == 徽标高度、Y == 徽标 Y、radius == height/2、色块与文本都在托底之内;
* 调整面板:菜单左边界 == 按钮左边界、菜单底 == 按钮顶 - 4*scale、且不覆盖按钮(两个按钮各测一次);
* GX 拖拽让位:±0.6H 各让位 1 格(目标 0 / 3),±0.4H 与恰好 ±0.5H 都不让位(严格「超过」)。
- 同步修正 harness 里两处「旧规则」断言(否则必然 FAIL 并误导后来者):
[AP] 「派生信号继承调整变换」→ 「样式默认 + 采样值按 time[i]-offsetTime 烘焙」;
[AN] 「图例带无托底(网格线可透出)」→ 「装饰矩形逐块、无一块跨越整条带、未被托底覆盖的列
仍能看到底层内容」。
- 合计 722/722 通过;make release 零警告(本机 GCC 15.2.0 UCRT;Target.exe 被占用时链接会报
Permission denied,已用临时 -o 名字验证链接正确性)。
规则同步:Documents/agent_mem.md 新增 §9.21,§9.19 的「offsetTime 仍需继承」标注为已作废,
新增 §11.2 第 21–25 条硬约束,并补变更日志一行。
This commit is contained in:
1 parent
1b3ab4ac09
commit
a2a38236fe
9 files changed
+434
-118
No files matched your search
@@ -1307,8 +1307,12 @@ namespace jdi
|
||||
*
|
||||
* 曲线由「均值点数 X + 迭代次数 N」确定,与「实验性功能 → 均值滤波」的预览曲线逐位一致。
|
||||
*
|
||||
* ⚠️ 判定对象是**画出来的两条曲线**:信号与中心曲线都要先按 `style` 做水平镜像映射、
|
||||
* 再套用 `ApplyDrawTransform`(系数 → 绝对值 → 垂直镜像 → Y 偏移)。
|
||||
*
|
||||
* @param [IN] file 信号所属文件。
|
||||
* @param [IN] signal 目标信号。
|
||||
* @param [IN] style 该信号的外观(含水平镜像与「调整」变换)。
|
||||
* @param [IN] points 均值点数 X。
|
||||
* @param [IN] iterations 迭代次数 N。
|
||||
* @param [IN] hasRange 是否只统计某个时间区间(双光标模式下为光标区间)。
|
||||
@@ -1317,9 +1321,9 @@ namespace jdi
|
||||
* @param [OUT] rising 上升沿数量(信号从下往上越过均值滤波曲线)。
|
||||
* @param [OUT] falling 下降沿数量(信号从上往下穿过均值滤波曲线)。
|
||||
*/
|
||||
void EdgeCenterCountsFor(const DataFile& file, const SignalData& signal, int points, int iterations,
|
||||
bool hasRange, double rangeMin, double rangeMax, std::size_t& rising,
|
||||
std::size_t& falling) const;
|
||||
void EdgeCenterCountsFor(const DataFile& file, const SignalData& signal, const SignalStyle& style,
|
||||
int points, int iterations, bool hasRange, double rangeMin,
|
||||
double rangeMax, std::size_t& rising, std::size_t& falling) const;
|
||||
|
||||
/**
|
||||
* @brief 切换某个信号在「中心轴边沿计数」面板中的勾选状态。
|
||||
@@ -1591,8 +1595,19 @@ namespace jdi
|
||||
|
||||
/**
|
||||
* @brief 统计某信号穿越横向光标幅值的上/下边沿数量,结果带缓存。
|
||||
*
|
||||
* 判定定义(用户明确要求):把信号**绘制出来的曲线**与横向光标所在的幅值比较,
|
||||
* 曲线从**下往上**穿过该幅值 → 上升沿;从**上往下**穿过 → 下降沿。
|
||||
*
|
||||
* ⚠️ 因此判定值必须是**画出来的值**:先按 `style` 做水平镜像映射,再套用
|
||||
* `ApplyDrawTransform`(系数 → 绝对值 → 垂直镜像 → Y 偏移)。
|
||||
* 只比较原始采样值会导致「应用过调整的信号数不到任何边沿」
|
||||
*(横向光标读的是屏幕上的值,与原始值域完全不同)。
|
||||
* ⚠️ 时间区间**之外**的采样一律不参与计数:只有两个端点都在区间内的采样对才计入。
|
||||
*
|
||||
* @param [IN] file 信号所属文件。
|
||||
* @param [IN] signal 目标信号。
|
||||
* @param [IN] style 该信号的外观(含水平镜像与「调整」变换)。
|
||||
* @param [IN] edgeValue 横向光标所在的幅值。
|
||||
* @param [IN] hasRange 是否只统计某个时间区间(双光标模式下为光标区间)。
|
||||
* @param [IN] rangeMin 区间左端点时间。
|
||||
@@ -1600,9 +1615,9 @@ namespace jdi
|
||||
* @param [OUT] rising 上升沿数量(从下往上穿越)。
|
||||
* @param [OUT] falling 下降沿数量(从上往下穿越)。
|
||||
*/
|
||||
void EdgeCountsFor(const DataFile& file, const SignalData& signal, double edgeValue,
|
||||
bool hasRange, double rangeMin, double rangeMax, std::size_t& rising,
|
||||
std::size_t& falling) const;
|
||||
void EdgeCountsFor(const DataFile& file, const SignalData& signal, const SignalStyle& style,
|
||||
double edgeValue, bool hasRange, double rangeMin, double rangeMax,
|
||||
std::size_t& rising, std::size_t& falling) const;
|
||||
|
||||
/**
|
||||
* @brief 计算实验性功能面板所需的宽度(排版与绘制共用,保证文字完整不被省略)。
|
||||
@@ -1816,6 +1831,7 @@ namespace jdi
|
||||
{
|
||||
const DataFile* file = nullptr; /**< 所属数据分组。 */
|
||||
const SignalData* signal = nullptr; /**< 目标信号。 */
|
||||
std::uint64_t styleKey = 0; /**< 「调整」变换 + 水平镜像的指纹(变了必须整段重算)。 */
|
||||
double edgeValue = 0.0; /**< 判定阈值(横向光标幅值)。 */
|
||||
bool hasRange = false; /**< 是否限定在时间区间内统计。 */
|
||||
bool reusable = false; /**< 计数是否完全由 [begin, end) 决定。 */
|
||||
@@ -1867,6 +1883,7 @@ namespace jdi
|
||||
{
|
||||
const DataFile* file = nullptr; /**< 所属数据分组。 */
|
||||
const SignalData* signal = nullptr; /**< 目标信号。 */
|
||||
std::uint64_t styleKey = 0; /**< 「调整」变换 + 水平镜像的指纹(变了必须整段重算)。 */
|
||||
int points = 5; /**< 均值点数 X。 */
|
||||
int iterations = 1; /**< 迭代次数 N。 */
|
||||
bool hasRange = false; /**< 是否限定在时间区间内统计。 */
|
||||
|
||||
@@ -80,7 +80,6 @@ namespace jdi
|
||||
const float kLegendSwatchSize = 8.0f; /**< 图例色块尺寸。 */
|
||||
const float kLegendItemGap = 14.0f; /**< 图例条目之间的间距。 */
|
||||
const float kLegendMaxTextWidth = 200.0f; /**< 图例条目的文本宽度上限(超出则省略)。 */
|
||||
const float kLegendPlateRadius = 4.0f; /**< 图例条目托底的圆角半径(逻辑像素)。 */
|
||||
const float kLaneBadgeWidth = 30.0f; /**< 网格编号徽标宽度(与信号面板保持一致)。 */
|
||||
const float kLaneBadgeHeight = 17.0f; /**< 网格编号徽标高度。 */
|
||||
const float kLaneBadgeGap = 8.0f; /**< 编号徽标与图例条目之间的间距。 */
|
||||
@@ -297,11 +296,39 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 「调整」变换 + 水平镜像的指纹(用于边沿统计缓存键)。
|
||||
*
|
||||
* 边沿判定基于**画出来的曲线**,因此任何一个变换项改变都可能改变计数结果;
|
||||
* 把这些项折叠成一个指纹后,样式一变就能立刻失效缓存项(否则会沿用变换前的旧计数)。
|
||||
*
|
||||
* @param [IN] style 信号外观。
|
||||
* @return 指纹值。
|
||||
*/
|
||||
std::uint64_t EdgeStyleKey(const SignalStyle& style)
|
||||
{
|
||||
std::uint64_t key = 1469598103934665603ull;
|
||||
const auto mix = [&key](std::uint64_t value)
|
||||
{
|
||||
key ^= value;
|
||||
key *= 1099511628211ull;
|
||||
};
|
||||
mix(static_cast<std::uint64_t>(
|
||||
std::llround(static_cast<double>(style.gain) * 1.0e6)));
|
||||
mix(static_cast<std::uint64_t>(
|
||||
std::llround(static_cast<double>(style.offsetY) * 1.0e6)));
|
||||
mix(style.absolute ? 1u : 0u);
|
||||
mix(style.mirrorVertical ? 2u : 0u);
|
||||
mix(style.mirrorHorizontal ? 4u : 0u);
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 计算统计区间对应的采样下标区间 [begin, end)。
|
||||
*
|
||||
* 时间轴单调时用二分定位,并在区间左侧多取一个采样点,
|
||||
* 使跨越区间边界的边沿也能被计入。
|
||||
* 时间轴单调时用二分定位。⚠️ 区间**之外**的采样一律不参与计数,
|
||||
* 因此这里**不做任何外扩**:只有两端都落在 `[begin, end)` 内的采样对才会被累计
|
||||
*(调用方从 `begin + 1` 开始枚举采样对)。
|
||||
*
|
||||
* @param [IN] file 所属数据分组。
|
||||
* @param [IN] count 采样点数量。
|
||||
@@ -333,14 +360,14 @@ namespace jdi
|
||||
std::lower_bound(times.begin(), times.begin() + static_cast<std::ptrdiff_t>(timeCount),
|
||||
rangeMin) -
|
||||
times.begin());
|
||||
// ⚠️ **不要**在左端多取一个采样点:那会让“跨越区间边界的边沿”也算进来,
|
||||
// 而用户明确要求“在光标选区外的点不要算入计数规则”。
|
||||
// (例:光标恰好落在越过阈值的第一个采样点 B 上时,A→B 这一段处于选区之外,
|
||||
// 不计入;旧实现在左端多取一个点,于是 B 被错认成上升沿。)
|
||||
end = static_cast<std::size_t>(
|
||||
std::upper_bound(times.begin(), times.begin() + static_cast<std::ptrdiff_t>(timeCount),
|
||||
rangeMax) -
|
||||
times.begin());
|
||||
if (begin > 0)
|
||||
{
|
||||
--begin;
|
||||
}
|
||||
end = std::min(end, timeCount);
|
||||
begin = std::min(begin, end);
|
||||
return true;
|
||||
@@ -2673,8 +2700,9 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlotView::EdgeCenterCountsFor(const DataFile& file, const SignalData& signal, int points,
|
||||
int iterations, bool hasRange, double rangeMin, double rangeMax,
|
||||
void PlotView::EdgeCenterCountsFor(const DataFile& file, const SignalData& signal,
|
||||
const SignalStyle& style, int points, int iterations,
|
||||
bool hasRange, double rangeMin, double rangeMax,
|
||||
std::size_t& rising, std::size_t& falling) const
|
||||
{
|
||||
rising = 0;
|
||||
@@ -2687,14 +2715,21 @@ namespace jdi
|
||||
std::size_t end = 0;
|
||||
const bool indexed = EdgeIndexRange(file, count, hasRange, rangeMin, rangeMax, begin, end);
|
||||
|
||||
// 按「文件 + 信号 + 均值点数 X + 迭代次数 N + 是否限定区间」定位缓存项
|
||||
// ⚠️ 判定基于**画出来的两条曲线**:信号与中心曲线都要先按下标映射(水平镜像)
|
||||
// 再套用「调整」变换 —— 否则应用过调整的信号会按原始值域判定,统计结果完全错误。
|
||||
const bool mirrored = style.mirrorHorizontal;
|
||||
const bool transform = (style.gain != 1.0f || style.absolute || style.mirrorVertical ||
|
||||
style.offsetY != 0.0f);
|
||||
const std::uint64_t styleKey = EdgeStyleKey(style);
|
||||
|
||||
// 按「文件 + 信号 + 样式指纹 + 均值点数 X + 迭代次数 N + 是否限定区间」定位缓存项
|
||||
//(**时间区间不进缓存键**)。
|
||||
EdgeCenterCountEntry* entry = nullptr;
|
||||
for (EdgeCenterCountEntry& candidate : m_edgeCenterCountCache)
|
||||
{
|
||||
if (candidate.file == &file && candidate.signal == &signal &&
|
||||
candidate.hasRange == hasRange && candidate.points == radius &&
|
||||
candidate.iterations == stages)
|
||||
candidate.styleKey == styleKey && candidate.hasRange == hasRange &&
|
||||
candidate.points == radius && candidate.iterations == stages)
|
||||
{
|
||||
entry = &candidate;
|
||||
break;
|
||||
@@ -2709,6 +2744,7 @@ namespace jdi
|
||||
EdgeCenterCountEntry created;
|
||||
created.file = &file;
|
||||
created.signal = &signal;
|
||||
created.styleKey = styleKey;
|
||||
created.points = radius;
|
||||
created.iterations = stages;
|
||||
created.hasRange = hasRange;
|
||||
@@ -2775,29 +2811,40 @@ namespace jdi
|
||||
|
||||
// 与「实验性功能 → 均值滤波」共用同一条曲线(含缓存),
|
||||
// 但**只取必需区间 ± 保护带**的窗口。
|
||||
// ⚠️ 曲线数组按**源下标**存储(`signal.values` 的顺序),而上面算出的区间是**显示下标**:
|
||||
// 水平镜像时显示下标 i 对应源下标 count-1-i,必须把窗口映射过去再请求,
|
||||
// 否则会取到完全无关的一段(甚至什么都不画)。
|
||||
const std::size_t windowBegin = mirrored ? (count - needEnd) : needFirst;
|
||||
const std::size_t windowEnd = mirrored ? (count - needFirst) : needEnd;
|
||||
std::size_t spanBegin = 0;
|
||||
const std::vector<float>* curvePtr = CurveSpanFor(
|
||||
DerivedCurveKind::MEAN_FILTER, signal, radius, stages, 0, needFirst, needEnd, spanBegin);
|
||||
DerivedCurveKind::MEAN_FILTER, signal, radius, stages, 0, windowBegin, windowEnd, spanBegin);
|
||||
if (curvePtr == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::vector<float>& curve = *curvePtr;
|
||||
|
||||
// 判定基准:信号与中心平均值曲线之差(区间之外的采样返回 NaN)。
|
||||
const auto deltaAt = [&signal, &file, &curve, spanBegin, count, hasRange, rangeMin,
|
||||
rangeMax](std::size_t index) -> double
|
||||
// 判定基准:**绘制后**的信号与**绘制后**的中心曲线之差(区间之外的采样返回 NaN)。
|
||||
// ⚠️ 两侧必须套用**同一套**变换,否则比较的不是屏幕上看到的两条曲线。
|
||||
const auto deltaAt = [&signal, &file, &curve, &style, spanBegin, count, mirrored, transform,
|
||||
hasRange, rangeMin, rangeMax](std::size_t index) -> double
|
||||
{
|
||||
if (index >= count)
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
if (index < spanBegin || (index - spanBegin) >= curve.size())
|
||||
const std::size_t sourceIndex = mirrored ? ((count - 1) - index) : index;
|
||||
if (sourceIndex >= count)
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
const double value = signal.values[index];
|
||||
const float center = curve[index - spanBegin];
|
||||
if (sourceIndex < spanBegin || (sourceIndex - spanBegin) >= curve.size())
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
const double value = signal.values[sourceIndex];
|
||||
const float center = curve[sourceIndex - spanBegin];
|
||||
if (!std::isfinite(value) || !std::isfinite(center))
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
@@ -2810,7 +2857,15 @@ namespace jdi
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
}
|
||||
return value - static_cast<double>(center);
|
||||
const double drawn = transform ? ApplyDrawTransform(style, value) : value;
|
||||
const double drawnCenter =
|
||||
transform ? ApplyDrawTransform(style, static_cast<double>(center))
|
||||
: static_cast<double>(center);
|
||||
if (!std::isfinite(drawn) || !std::isfinite(drawnCenter))
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
return drawn - drawnCenter;
|
||||
};
|
||||
|
||||
std::size_t deltaRising = 0;
|
||||
@@ -2839,24 +2894,43 @@ namespace jdi
|
||||
entry->falling = falling;
|
||||
}
|
||||
|
||||
void PlotView::EdgeCountsFor(const DataFile& file, const SignalData& signal, double edgeValue,
|
||||
bool hasRange, double rangeMin, double rangeMax, std::size_t& rising,
|
||||
std::size_t& falling) const
|
||||
void PlotView::EdgeCountsFor(const DataFile& file, const SignalData& signal, const SignalStyle& style,
|
||||
double edgeValue, bool hasRange, double rangeMin, double rangeMax,
|
||||
std::size_t& rising, std::size_t& falling) const
|
||||
{
|
||||
rising = 0;
|
||||
falling = 0;
|
||||
|
||||
const std::vector<double>& values = signal.values;
|
||||
// ⚠️ 阈值(横向光标幅值)是**屏幕上的值**,因此判定必须基于**画出来的曲线**:
|
||||
// 先按下标映射(水平镜像)取源采样,再套用「调整」变换(系数 → 绝对值 → 垂直镜像 →
|
||||
// Y 偏移)。只比较原始采样值时,“系数 50”这类信号的原始值域与阈值完全不相交,
|
||||
// 表现就是“明明是很规整的方波,统计出来却全是 0”(用户报告)。
|
||||
const std::size_t count = values.size();
|
||||
const bool mirrored = style.mirrorHorizontal;
|
||||
const bool transform = (style.gain != 1.0f || style.absolute || style.mirrorVertical ||
|
||||
style.offsetY != 0.0f);
|
||||
const std::uint64_t styleKey = EdgeStyleKey(style);
|
||||
std::size_t begin = 0;
|
||||
std::size_t end = 0;
|
||||
const bool indexed = EdgeIndexRange(file, values.size(), hasRange, rangeMin, rangeMax, begin, end);
|
||||
const bool indexed = EdgeIndexRange(file, count, hasRange, rangeMin, rangeMax, begin, end);
|
||||
|
||||
// 判定基准:采样值相对阈值的偏差。区间之外的采样返回 NaN(等价于原实现
|
||||
// 判定基准:**绘制值**相对阈值的偏差。区间之外的采样返回 NaN(等价于原实现
|
||||
// 「区间外采样不参与判定且打断连续」的语义)。
|
||||
const auto deltaAt = [&values, &file, hasRange, rangeMin, rangeMax](std::size_t index) -> double
|
||||
const auto deltaAt = [&values, &file, &style, mirrored, transform, count, hasRange, rangeMin,
|
||||
rangeMax](std::size_t index) -> double
|
||||
{
|
||||
const double value = values[index];
|
||||
if (!std::isfinite(value))
|
||||
if (index >= count)
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
const std::size_t sourceIndex = mirrored ? ((count - 1) - index) : index;
|
||||
if (sourceIndex >= count)
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
const double raw = values[sourceIndex];
|
||||
if (!std::isfinite(raw))
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
@@ -2868,15 +2942,17 @@ namespace jdi
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
}
|
||||
return value;
|
||||
const double drawn = transform ? ApplyDrawTransform(style, raw) : raw;
|
||||
return std::isfinite(drawn) ? drawn : std::numeric_limits<double>::quiet_NaN();
|
||||
};
|
||||
|
||||
// 按「文件 + 信号 + 阈值 + 是否限定区间」定位缓存项(**时间区间不进缓存键**)。
|
||||
// 按「文件 + 信号 + 样式指纹 + 阈值 + 是否限定区间」定位缓存项(**时间区间不进缓存键**)。
|
||||
EdgeCountEntry* entry = nullptr;
|
||||
for (EdgeCountEntry& candidate : m_edgeCountCache)
|
||||
{
|
||||
if (candidate.file == &file && candidate.signal == &signal &&
|
||||
candidate.hasRange == hasRange && candidate.edgeValue == edgeValue)
|
||||
candidate.styleKey == styleKey && candidate.hasRange == hasRange &&
|
||||
candidate.edgeValue == edgeValue)
|
||||
{
|
||||
entry = &candidate;
|
||||
break;
|
||||
@@ -2891,6 +2967,7 @@ namespace jdi
|
||||
EdgeCountEntry created;
|
||||
created.file = &file;
|
||||
created.signal = &signal;
|
||||
created.styleKey = styleKey;
|
||||
created.edgeValue = edgeValue;
|
||||
created.hasRange = hasRange;
|
||||
m_edgeCountCache.push_back(created);
|
||||
@@ -3004,13 +3081,14 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
EdgeCenterCountsFor(*resolved.file, *resolved.data, item->points, item->iterations,
|
||||
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling);
|
||||
EdgeCenterCountsFor(*resolved.file, *resolved.data, *resolved.style, item->points,
|
||||
item->iterations, EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(),
|
||||
rising, falling);
|
||||
}
|
||||
else
|
||||
{
|
||||
EdgeCountsFor(*resolved.file, *resolved.data, lane.edgeValue, EdgeRangeActive(),
|
||||
EdgeRangeMin(), EdgeRangeMax(), rising, falling);
|
||||
EdgeCountsFor(*resolved.file, *resolved.data, *resolved.style, lane.edgeValue,
|
||||
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling);
|
||||
}
|
||||
const float countWidth =
|
||||
UiMeasureTextWidth(m_fonts->monoSmall,
|
||||
@@ -6373,11 +6451,11 @@ namespace jdi
|
||||
{
|
||||
if (hit.ref == m_hoverLegend)
|
||||
{
|
||||
const float radius = (hit.radius > 0.0f) ? hit.radius : kLegendPlateRadius * m_scale;
|
||||
const float radius = (hit.radius > 0.0f) ? hit.radius : (kLaneBadgeHeight * 0.5f * m_scale);
|
||||
UiFillRoundRect(g, hit.rect, radius, m_theme->accentSoft);
|
||||
UiDrawRoundRect(g, hit.rect, radius, m_theme->accent, 1.0f);
|
||||
UiDrawSwatch(g, hit.swatch, hit.swatchColor, hit.swatchOpacity, 1.5f * m_scale, m_scale);
|
||||
UiText(g, hit.label, m_fonts->uiSmall, hit.text, m_theme->textStrong, TextAlign::LEFT);
|
||||
UiText(g, hit.label, m_fonts->uiTiny, hit.text, m_theme->textStrong, TextAlign::LEFT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -6935,19 +7013,40 @@ namespace jdi
|
||||
return m_laneDragFrom;
|
||||
}
|
||||
|
||||
// 判据 = **鼠标指针**落在哪个网格的哪一半:指针进入某网格的"下半"即把插入位置
|
||||
// 定在它之后(`index` = 该网格下标 + 1)。
|
||||
// 判据 = **被拖网格自己的上下边界**(用户明确要求):
|
||||
// * 上边界越过"上方第 m 个网格"的中心水平线 → 让位 m 个位置(插入位置 = from - m);
|
||||
// * 下边界越过"下方第 m 个网格"的中心水平线 → 让位 m 个位置(插入位置 = from + m + 1,
|
||||
// 因为被拖网格会先从原位置摘除,越过后应落在该网格之后)。
|
||||
//
|
||||
// ⚠️ 旧实现用"被拖网格的**中心**"(`mouseY - grabOffset + laneHeight/2`)做判据,
|
||||
// 等价于要求把整个网格拖过去一格才会吸附 —— 例如只有两个网格时,把 G2 往上拖
|
||||
// 必须一直拖到鼠标接近 G1 的最上沿才会让位(用户报告"要拖到另一个网格的位置附近
|
||||
// 才有吸附提示")。改为与鼠标位置挂钩后:**鼠标越过 G1 的中心水平线,G1 立刻让位
|
||||
// 并出现吸附提示**(依次类推对任意网格数成立)。
|
||||
const float firstTop = m_lanes.front().rect.Y;
|
||||
const float mouse = static_cast<float>(mouseY);
|
||||
int index = static_cast<int>(std::floor((mouse - firstTop) / laneHeight + 0.5f));
|
||||
index = std::max(0, std::min(index, count));
|
||||
return index;
|
||||
// ⚠️ 旧判据只看**鼠标指针**落在哪个网格的哪一半。因为被拖网格整体跟随指针,而指针通常
|
||||
// 按在网格上沿的 GX 徽标上,于是"向下拖必须把整个网格拖过下方网格才吸附、
|
||||
// 向上拖只要半个网格" —— 上下不对称(用户报告)。改用网格边界后上下完全对称。
|
||||
//
|
||||
// 数学:令 shift = 被拖网格相对原槽位的位移,H = 网格高。
|
||||
// 上边界 = baseTop + shift,上方第 m 个网格的中心线 = baseTop - (m - 0.5)H
|
||||
// ⇒ 满足 shift < (0.5 - m)H 的最大 m 即让位格数(m < 0.5 - shift/H);
|
||||
// 下边界 = baseTop + shift + H,下方第 m 个网格的中心线 = baseTop + (m + 0.5)H
|
||||
// ⇒ 满足 shift > (m - 0.5)H 的最大 m(m < shift/H + 0.5)。
|
||||
// ⚠️ 两侧条件互斥(不可能同时成立),因此最多只会向一侧吸附。
|
||||
const float baseTop = m_lanes[static_cast<std::size_t>(m_laneDragFrom)].rect.Y;
|
||||
const float shift =
|
||||
static_cast<float>(mouseY) - m_laneDragGrabOffset - baseTop;
|
||||
const float upRatio = 0.5f - shift / laneHeight; // m < upRatio
|
||||
const float downRatio = shift / laneHeight + 0.5f; // m < downRatio
|
||||
// "严格小于" ⇒ 最大整数 m = ceil(ratio) - 1。
|
||||
int up = static_cast<int>(std::ceil(upRatio)) - 1;
|
||||
int down = static_cast<int>(std::ceil(downRatio)) - 1;
|
||||
up = std::max(0, std::min(up, m_laneDragFrom));
|
||||
down = std::max(0, std::min(down, count - 1 - m_laneDragFrom));
|
||||
if (up > 0)
|
||||
{
|
||||
return m_laneDragFrom - up;
|
||||
}
|
||||
if (down > 0)
|
||||
{
|
||||
return m_laneDragFrom + down + 1;
|
||||
}
|
||||
return m_laneDragFrom;
|
||||
}
|
||||
|
||||
void PlotView::FinishLaneReorder()
|
||||
@@ -9173,9 +9272,16 @@ namespace jdi
|
||||
const float badgeHeight = kLaneBadgeHeight * m_scale;
|
||||
float cursorX = badgeRect.X + badgeRect.Width + kLaneBadgeGap * m_scale;
|
||||
|
||||
// 图例条目托底的内缩外扩量:托底矩形**同时就是命中矩形**(看到的与点得到的严格一致)。
|
||||
const float platePadX = 3.0f * m_scale;
|
||||
const float platePadY = 2.0f * m_scale;
|
||||
// 图例条目的托底 = **与 GX 编号徽标完全同款**(用户明确要求):
|
||||
// * 高度与徽标相同(`bgadeRect` 的上下边界一模一样,因此"上下边距相同");
|
||||
// * 圆角 = 高度的一半(胶囊形),与徽标的 `badgeHeight * 0.5f` 一致;
|
||||
// * 配色与描边宽度也与徽标一致(`cardBg/border`,选中态 `accentSoft/accent`);
|
||||
// * 水平内边距 = 胶囊半径(若小于半径,内容会溢出圆角之外)。
|
||||
// * 文本改用**更小的字体**(`uiTiny`):原用 `uiSmall` 时文字几乎顶满胶囊高度。
|
||||
// ⚠️ 托底矩形**同时就是命中矩形**(看到的与点得到的严格一致)。
|
||||
const float plateHeight = badgeHeight;
|
||||
const float plateTop = badgeRect.Y;
|
||||
const float platePadX = plateHeight * 0.5f;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 1) 几何:算出"放得下"的条目及其矩形(**唯一来源**,命中 / 装饰 / 绘制共用)。
|
||||
@@ -9184,7 +9290,8 @@ namespace jdi
|
||||
{
|
||||
SignalRef ref;
|
||||
float left = 0.0f; // 色块左缘
|
||||
float width = 0.0f; // 条目内容宽度
|
||||
float width = 0.0f; // 条目**内容**宽度(色块 + 间距 + 文本)
|
||||
float plateWidth = 0.0f; // 托底宽度(内容 + 两侧内边距)
|
||||
float textWidth = 0.0f;
|
||||
std::wstring label;
|
||||
bool selected = false;
|
||||
@@ -9208,35 +9315,35 @@ namespace jdi
|
||||
label += L" · " + resolved.file->Title();
|
||||
}
|
||||
const float maxTextWidth = kLegendMaxTextWidth * m_scale;
|
||||
const float fullTextWidth = UiMeasureTextWidth(m_fonts->uiSmall, label);
|
||||
const float fullTextWidth = UiMeasureTextWidth(m_fonts->uiTiny, label);
|
||||
const float textWidth = std::min(fullTextWidth, maxTextWidth);
|
||||
const float itemWidth = swatchSize + 5.0f * m_scale + textWidth + kLegendItemGap * m_scale;
|
||||
if (probeX + itemWidth > maxX)
|
||||
const float contentWidth = swatchSize + 5.0f * m_scale + textWidth;
|
||||
const float plateWidth = contentWidth + platePadX * 2.0f;
|
||||
if (probeX + plateWidth > maxX)
|
||||
{
|
||||
break; // 空间不足:其余条目省略,下方用 "+N" 提示
|
||||
}
|
||||
LegendItem item;
|
||||
item.ref = ref;
|
||||
item.left = probeX;
|
||||
item.width = itemWidth;
|
||||
item.left = probeX + platePadX; // 色块左缘(托底内缩一个内边距)
|
||||
item.width = contentWidth;
|
||||
item.plateWidth = plateWidth;
|
||||
item.textWidth = textWidth;
|
||||
item.label = (fullTextWidth > maxTextWidth)
|
||||
? UiEllipsize(label, m_fonts->uiSmall, maxTextWidth)
|
||||
? UiEllipsize(label, m_fonts->uiTiny, maxTextWidth)
|
||||
: label;
|
||||
item.selected = (SelectedRef() == ref);
|
||||
items.push_back(std::move(item));
|
||||
probeX += itemWidth;
|
||||
probeX += plateWidth + kLegendItemGap * m_scale;
|
||||
}
|
||||
contentRight = probeX;
|
||||
}
|
||||
const auto PlateRect = [&](const LegendItem& item)
|
||||
{
|
||||
return gd::RectF(item.left - platePadX, top - platePadY, item.width + platePadX * 2.0f,
|
||||
height + platePadY * 2.0f);
|
||||
return gd::RectF(item.left - platePadX, plateTop, item.plateWidth, plateHeight);
|
||||
};
|
||||
// 条目托底 = 与 GX 编号徽标同一套风格的**圆角矩形**(用户明确要求:
|
||||
// "相当于一个 GX 标签,只是内容换成了图例信息")。
|
||||
const float plateRadius = kLegendPlateRadius * m_scale;
|
||||
// 条目托底 = 与 GX 编号徽标同一套风格的**胶囊形圆角矩形**(半径 = 高度的一半)。
|
||||
const float plateRadius = plateHeight * 0.5f;
|
||||
// 「+N」(图例空间不足)与「(无可见信号)」提示的矩形。
|
||||
// ⚠️ 它们同样属于装饰层、必须登记进 `m_decorRects`:否则会被画进装饰位图
|
||||
// 却因不在贴回列表里而永远看不见。
|
||||
@@ -9271,9 +9378,9 @@ namespace jdi
|
||||
hit.rect = PlateRect(item);
|
||||
hit.radius = plateRadius;
|
||||
hit.swatch =
|
||||
gd::RectF(item.left, top + (height - swatchSize) * 0.5f, swatchSize, swatchSize);
|
||||
hit.text = gd::RectF(item.left + swatchSize + 5.0f * m_scale, top,
|
||||
std::max(8.0f, item.textWidth + 2.0f), height);
|
||||
gd::RectF(item.left, plateTop + (plateHeight - swatchSize) * 0.5f, swatchSize, swatchSize);
|
||||
hit.text = gd::RectF(item.left + swatchSize + 5.0f * m_scale, plateTop,
|
||||
std::max(8.0f, item.textWidth + 2.0f), plateHeight);
|
||||
hit.label = item.label;
|
||||
// 「选中」与「悬停」用同一套配色(用户明确要求),因此文字色在这里也按选中态取。
|
||||
hit.textColor = item.selected ? m_theme->textStrong : m_theme->text;
|
||||
@@ -9343,23 +9450,24 @@ namespace jdi
|
||||
// 改名编辑态:整块托底变成输入框(与左侧列表双击改名一致)。
|
||||
if (m_editTarget == PlotEditTarget::SIGNAL_NAME && m_editSignal == item.ref)
|
||||
{
|
||||
UiDrawEditField(g, plate, m_edit, m_fonts->uiSmall, m_scale);
|
||||
UiDrawEditField(g, plate, m_edit, m_fonts->uiTiny, m_scale);
|
||||
continue;
|
||||
}
|
||||
|
||||
UiDrawSwatch(g,
|
||||
gd::RectF(item.left, top + (height - swatchSize) * 0.5f, swatchSize, swatchSize),
|
||||
gd::RectF(item.left, plateTop + (plateHeight - swatchSize) * 0.5f,
|
||||
swatchSize, swatchSize),
|
||||
resolved.style->color, resolved.style->opacity, 1.5f * m_scale, m_scale);
|
||||
UiText(g, item.label, m_fonts->uiSmall,
|
||||
gd::RectF(item.left + swatchSize + 5.0f * m_scale, top,
|
||||
std::max(8.0f, item.textWidth + 2.0f), height),
|
||||
UiText(g, item.label, m_fonts->uiTiny,
|
||||
gd::RectF(item.left + swatchSize + 5.0f * m_scale, plateTop,
|
||||
std::max(8.0f, item.textWidth + 2.0f), plateHeight),
|
||||
item.selected ? m_theme->textStrong : m_theme->text, TextAlign::LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasEmptyText)
|
||||
{
|
||||
UiText(g, L"(无可见信号)", m_fonts->uiSmall, emptyTextRect, m_theme->textMuted,
|
||||
UiText(g, L"(无可见信号)", m_fonts->uiTiny, emptyTextRect, m_theme->textMuted,
|
||||
TextAlign::LEFT);
|
||||
}
|
||||
else if (hasMoreText)
|
||||
@@ -9367,7 +9475,7 @@ namespace jdi
|
||||
// 图例空间不足时提示剩余信号数量,避免误认为信号丢失。
|
||||
const std::wstring moreText =
|
||||
L"+" + std::to_wstring(visibleCount - static_cast<int>(items.size()));
|
||||
UiText(g, moreText, m_fonts->uiSmall, moreTextRect, m_theme->textMuted, TextAlign::LEFT);
|
||||
UiText(g, moreText, m_fonts->uiTiny, moreTextRect, m_theme->textMuted, TextAlign::LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11524,9 +11632,9 @@ namespace jdi
|
||||
const int statsOrder = (item != nullptr) ? item->iterations : 1;
|
||||
std::size_t rising = 0;
|
||||
std::size_t falling = 0;
|
||||
EdgeCenterCountsFor(*resolved.file, *resolved.data, statsPoints, statsOrder,
|
||||
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising,
|
||||
falling);
|
||||
EdgeCenterCountsFor(*resolved.file, *resolved.data, *resolved.style,
|
||||
statsPoints, statsOrder, EdgeRangeActive(),
|
||||
EdgeRangeMin(), EdgeRangeMax(), rising, falling);
|
||||
const std::wstring counts = L"上升沿 " + std::to_wstring(rising) +
|
||||
L" 下降沿 " + std::to_wstring(falling);
|
||||
UiText(g, counts, m_fonts->monoSmall, statsRect, m_theme->textMuted,
|
||||
@@ -11557,8 +11665,8 @@ namespace jdi
|
||||
|
||||
std::size_t rising = 0;
|
||||
std::size_t falling = 0;
|
||||
EdgeCountsFor(*resolved.file, *resolved.data, tabLane.edgeValue, EdgeRangeActive(),
|
||||
EdgeRangeMin(), EdgeRangeMax(), rising, falling);
|
||||
EdgeCountsFor(*resolved.file, *resolved.data, *resolved.style, tabLane.edgeValue,
|
||||
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling);
|
||||
const std::wstring counts = L"上升沿 " + std::to_wstring(rising) + L" 下降沿 " +
|
||||
std::to_wstring(falling);
|
||||
UiText(g, counts, m_fonts->monoSmall,
|
||||
|
||||
Reference in new issue
Block a user