组名平滑悬浮 + 单点信号初始视图 + 光标吸附统一判定(自动点线同源)
一、信号列表「组名悬浮」平滑化、且不再遮住下一个组名
* 悬浮位置改为 min(列表上边界, 本组最后一行下边界 − 组名高度):
- 本组还有行在列表里时钉在列表上边界;
- 本组最后一行开始滑出上边界时,组名跟着一起上移、随滚动条自然离界
(不再像旧实现那样突然消失);
- 「本组最后一行的下边界」恰好就是下一个分组行的自然位置,因此悬浮组名的下边界
永远不超过下一组名 —— 旧实现固定钉在列表上边界会把下一组名盖住,让别人以为
后面的信号属于上一组。
* 折叠的分组不再悬浮(折叠时本组“最后一行”就是它自己,悬浮位置 = 自然位置,
于是随滚动自然划过,不会停在最上方)。
* 底部边界线只在真的“浮”在其它行之上时才画,避免与正常绘制出现像素差。
二、单点信号的初始视图
* 新增 PlotView::FitSinglePointSignal():当前没有任何信号被勾选、而勾选的第一个信号
只有一个有效数据点时,把该数据点居中放置,时间跨度 = 1s、Y 轴跨度 = 1
(各为一个大格),并关闭该网格的整段自适应以免被覆盖。
Application::SynchronizeView 在首次勾选时先尝试它,失败才走 FitAll()。
* AutoRangeLaneToView 的退化区间(可见区间内只有一个取值)改用与 PadRange 一致的留白
max(1, |v|×0.1),不再产生“几乎为 0”的区间(那会让幅值网格线挤成一团、看不出数据点)。
* 需求作废:上一轮“2…16 个有效采样点默认改为点线 + 2px”已取消(放大自动点线已覆盖该场景);
只保留“只有一个有效采样点 → 点线 + 最大线宽”。
* SignalData::singlePointTime 升级为 sparseTimes/sparseValues(有效点 ≤ 1024 时在加载/插入
时缓存全部有效点),同时服务单点初始视图与光标吸附。
三、光标吸附的统一判定
* N = GridMinorCellCount():网格底衬一横排的小格数,与网格绘制用同一套
NiceStep + 五等分算出(因此不随缩放等级大幅变化)。
* SignalSnapEnabled():当且仅当某信号在当前一屏内的数据点个数 < N×4 时为它开启吸附;
判据随视界实时重算 ⇒ 疏密变化的信号在稀疏区自动吸附、移回稠密区自动自由移动。
额外要求视界内至少有一个可见数据点(否则“0 < N×4”会把数据全在视界之外的信号
误判为稀疏)。统计只在可见下标区间内进行且攒够阈值即早退,代价有界。
* SnapTimeToSample():判定只针对鼠标所在网格(同一个时间轴上不同网格结果可以不同);
同一网格内所有符合判定的信号的数据点都参与吸附比较;
吸附半径 = 一个网格小格的宽度,且不超过半个局部采样间距 ⇒ 数据点很稀疏
(一屏只有一两个点)时光标在点之间仍可自由移动(吸附是提示,不是卡住)。
* 删除旧的 SampleSpacingPixels(6px 下门限 + 半屏宽上门限)。
四、放大自动切点线改为与吸附判定完全同源
* UpdateAutoScatterStyles() 改用 SignalSnapEnabled:进入判定自动切「点线」,
不再满足(适应视图、或视界从稀疏区移到稠密区)自动恢复原样式;
自动期间用户手动改过则不恢复(尊重用户选择,判定放在恢复之前)。
* 删除旧的 AxisSparseInView(“采样间距 > 半屏宽”)。
五、验证(离屏 .preflight/check:SW_HIDE 窗口、不注入真实鼠标键盘、进程/线程 BELOW_NORMAL)
55/55 通过(连跑 3 次稳定):[A] 组名悬浮(钉住/平滑上移/整组滚过后换下一组/折叠组不悬浮/
悬浮下边界不超过下一组名)、[B] 单点初始视图(跨度 1s 与 1、居中、点线 + 最大线宽、
两信号时不再走单点路径)、[C] 吸附判定(满视图都关闭/稀疏窗口开启/无可见点不开启/
同一时间轴不同网格结果不同/同网格多信号的点都参与/稠密↔稀疏双向动态切换/
单点靠近吸附远离自由)、[D] 自动点线(同源判定/恢复/用户手动改过不覆盖)。
A/B:临时禁用「悬浮位置公式 / 单点初始视图 / 吸附判定」后重跑 ⇒ 14 项如实失败。
make release 零警告(用户正运行 Target.exe,链接用临时 -o 名字验证通过)。
六、规则同步
Documents/agent_mem.md 新增 §9.30、§11.2 第 58–61 条硬约束与变更日志。
This commit is contained in:
1 parent
9f6de3bba4
commit
57cc714c1d
8 files changed
+808
-230
No files matched your search
+24
-17
@@ -35,25 +35,26 @@ namespace jdi
|
||||
constexpr float kSignalLineWidthMin = 0.5f;
|
||||
constexpr float kSignalLineWidthMax = 10.0f;
|
||||
|
||||
/**
|
||||
* @brief 「少数数据点信号」的有效采样点数量上限(含)。
|
||||
*
|
||||
* 有效采样点不超过该值的信号,初始样式直接使用「点线」(`LINE_SCATTER`)并加粗到
|
||||
* `kSparseSignalLineWidth`:采样点这么少时线段模式几乎什么也看不见
|
||||
*(只有一个点时甚至完全画不出来)。正常密度的信号保持「线段 + 1px」。
|
||||
*/
|
||||
constexpr std::size_t kSparseSignalSampleLimit = 16;
|
||||
|
||||
/** @brief 稀疏信号(有效采样点 2 … `kSparseSignalSampleLimit`)的初始线宽。 */
|
||||
constexpr float kSparseSignalLineWidth = 2.0f;
|
||||
|
||||
/**
|
||||
* @brief **只含一个有效采样点**的信号的初始线宽(取样式允许的最大线宽)。
|
||||
*
|
||||
* 单个数据点在「点线」模式下就是一个圆点,半径与线宽成正比;取最大值才能看清。
|
||||
* ⚠️ 曾经还对「有效采样点 2…16」的信号也设了「点线 + 2px」的默认样式,
|
||||
* 该需求已**作废**(用户 2026-10-15 明确取消):放大后自动切「点线」的机制
|
||||
* 已经覆盖了那个场景,无需再改默认样式。
|
||||
*/
|
||||
constexpr float kSinglePointSignalLineWidth = kSignalLineWidthMax;
|
||||
|
||||
/**
|
||||
* @brief 「稀疏信号」缓存有效采样点时刻/取值的上限(有效点不超过该值才缓存)。
|
||||
*
|
||||
* 光标吸附需要“当前视界内有多少个数据点”以及“离光标最近的那个数据点”。
|
||||
* 对有效点很少的信号(典型:单点信号、只有零星采样的列)事先把它们的时刻与取值缓存下来,
|
||||
* 判定与查找都退化成二分 → 不必在鼠标移动时扫描整条采样数组(千万级信号无法承受)。
|
||||
* 1024 个有效点 × 16 字节 ≈ 16 KB/条,可忽略。
|
||||
*/
|
||||
constexpr std::size_t kSparseTimesLimit = 1024;
|
||||
|
||||
/**
|
||||
* @brief 绘图区拖拽缩放模式(由工具栏按钮切换)。
|
||||
*/
|
||||
@@ -354,13 +355,19 @@ namespace jdi
|
||||
std::size_t finiteCount = 0; /**< 有限采样点数量。 */
|
||||
|
||||
/**
|
||||
* @brief 只含**一个**有效采样点时的该点时刻(不存在该情况时为 NaN)。
|
||||
* @brief 有效采样点很少时的**全部有效点**(按时间升序;有效点很多时为空)。
|
||||
*
|
||||
* 在加载/插入信号时与 `ComputeStats` 同一趟扫描就地填充,供光标吸附使用:
|
||||
* 只有一个数据点的信号,光标在其它区域自由移动、靠近该点时吸附过去
|
||||
*(吸附而非“卡住”)。没有这个缓存就只能在鼠标移动时线性扫描采样值,大信号下不可接受。
|
||||
* 仅当 `finiteCount <= kSparseTimesLimit` 时在加载/插入信号时就地填充,用于:
|
||||
* * 光标吸附:直接二分得到“视界内有几个数据点”“离光标最近的数据点”;
|
||||
* * 单点信号的初始视图:Centering 需要那个点的时刻与取值。
|
||||
* ⚠️ 不要在鼠标移动时改成扫描整条采样数组来找有效点(千万级信号承受不住)。
|
||||
*
|
||||
* 说明:`sparseTimes.size() == 1` 即“只有一个数据点的信号”。
|
||||
*/
|
||||
double singlePointTime = std::numeric_limits<double>::quiet_NaN();
|
||||
std::vector<double> sparseTimes;
|
||||
|
||||
/** @brief `sparseTimes` 对应的取值(同序)。 */
|
||||
std::vector<double> sparseValues;
|
||||
|
||||
/**
|
||||
* @brief 粗粒度包络层:每 coarseStride 个采样一组的 min/max。
|
||||
|
||||
+71
-37
@@ -370,34 +370,86 @@ namespace jdi
|
||||
double minValue = 0.0; /**< 有限采样值最小值。 */
|
||||
double maxValue = 0.0; /**< 有限采样值最大值。 */
|
||||
std::size_t finiteCount = 0; /**< 有限采样点数量。 */
|
||||
std::size_t firstFiniteIndex = 0; /**< 第一个有限采样点的下标(无有效值时无意义)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 按有效采样点数量设置「稀疏信号」的**初始**绘制样式。
|
||||
* @brief 为**只有一个有效采样点**的信号设置初始绘制样式。
|
||||
*
|
||||
* 有效采样点极少时线段模式几乎什么也看不见(只有一个点时更是完全画不出东西),
|
||||
* 因此这类信号初始就用「点线」并适当加粗:
|
||||
* * **只有一个有效采样点** → 点线 + `kSinglePointSignalLineWidth`(最大线宽):那个点本身
|
||||
* 就是一个圆点,半径与线宽成正比,不取最大值根本看不清;
|
||||
* * **有效采样点 2 … `kSparseSignalSampleLimit`** → 点线 + `kSparseSignalLineWidth`(2px)。
|
||||
* 单个数据点在「线段」模式下完全画不出来,因此初始就用「点线」并取**最大线宽**
|
||||
* (点线模式下数据点是一个圆点,半径与线宽成正比)。
|
||||
*
|
||||
* 其余信号保持既有默认(线段 + 1px)。只在**新建信号**时调用(用户改过样式后不再覆盖)。
|
||||
* ⚠️ 曾经还对「有效采样点 2…16」的信号也改用「点线 + 2px」—— 该需求已**作废**
|
||||
*(用户 2026-10-15 明确取消:放大后自动切点线的机制已覆盖那个场景)。
|
||||
*
|
||||
* 只在**新建信号**时调用(用户改过样式后不再覆盖)。
|
||||
*
|
||||
* @param [IN] finiteCount 有效采样点数量。
|
||||
* @param [IN,OUT] style 待初始化的显示外观。
|
||||
*/
|
||||
void ApplySparseDefaultStyle(std::size_t finiteCount, SignalStyle& style)
|
||||
void ApplySinglePointDefaultStyle(std::size_t finiteCount, SignalStyle& style)
|
||||
{
|
||||
if (finiteCount == 1)
|
||||
{
|
||||
style.mode = DrawMode::LINE_SCATTER;
|
||||
style.lineWidth = kSinglePointSignalLineWidth;
|
||||
}
|
||||
else if (finiteCount <= kSparseSignalSampleLimit)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 缓存「有效采样点很少」的信号的**全部有效点**(时刻 + 取值,按时间升序)。
|
||||
*
|
||||
* 供光标吸附(二分查找最近数据点、O(log n) 判定稀疏)与单点信号的初始视图使用。
|
||||
* 有效点很多时(> `kSparseTimesLimit`)不做任何缓存(用不到,也没必要占内存)。
|
||||
*
|
||||
* @param [IN] axis 该信号使用的时间轴。
|
||||
* @param [IN,OUT] signal 目标信号。
|
||||
*/
|
||||
void CacheSparseSamples(const TimeAxis& axis, SignalData& signal)
|
||||
{
|
||||
signal.sparseTimes.clear();
|
||||
signal.sparseValues.clear();
|
||||
if (signal.finiteCount == 0 || signal.finiteCount > kSparseTimesLimit)
|
||||
{
|
||||
style.mode = DrawMode::LINE_SCATTER;
|
||||
style.lineWidth = kSparseSignalLineWidth;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t count = std::min(axis.time.size(), signal.values.size());
|
||||
signal.sparseTimes.reserve(signal.finiteCount);
|
||||
signal.sparseValues.reserve(signal.finiteCount);
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
const double value = signal.values[index];
|
||||
const double time = axis.time[index];
|
||||
if (!std::isfinite(value) || !std::isfinite(time))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
signal.sparseTimes.push_back(time);
|
||||
signal.sparseValues.push_back(value);
|
||||
}
|
||||
|
||||
// 非单调时间轴:按时间排序,保证后续二分查找合法
|
||||
//(单调时间轴的采样本身就是升序,无需排序)。
|
||||
if (!axis.monotonic && signal.sparseTimes.size() > 1)
|
||||
{
|
||||
std::vector<std::size_t> order(signal.sparseTimes.size());
|
||||
for (std::size_t index = 0; index < order.size(); ++index)
|
||||
{
|
||||
order[index] = index;
|
||||
}
|
||||
std::stable_sort(order.begin(), order.end(), [&signal](std::size_t a, std::size_t b)
|
||||
{ return signal.sparseTimes[a] < signal.sparseTimes[b]; });
|
||||
std::vector<double> times;
|
||||
std::vector<double> values;
|
||||
times.reserve(order.size());
|
||||
values.reserve(order.size());
|
||||
for (const std::size_t index : order)
|
||||
{
|
||||
times.push_back(signal.sparseTimes[index]);
|
||||
values.push_back(signal.sparseValues[index]);
|
||||
}
|
||||
signal.sparseTimes = std::move(times);
|
||||
signal.sparseValues = std::move(values);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,10 +487,6 @@ namespace jdi
|
||||
const double value = values[index];
|
||||
if (std::isfinite(value))
|
||||
{
|
||||
if (stats.finiteCount == 0)
|
||||
{
|
||||
stats.firstFiniteIndex = index; // 供「单点信号」缓存其时刻
|
||||
}
|
||||
++stats.finiteCount;
|
||||
low = std::min(low, value);
|
||||
high = std::max(high, value);
|
||||
@@ -619,15 +667,8 @@ namespace jdi
|
||||
signal.minValue = stats.minValue;
|
||||
signal.maxValue = stats.maxValue;
|
||||
signal.finiteCount = stats.finiteCount;
|
||||
// 只含一个有效采样点时缓存它的时刻(光标吸附用)。
|
||||
if (stats.finiteCount == 1)
|
||||
{
|
||||
const TimeAxis& ownAxis = group.axes[std::min(axisIndex, group.axes.size() - 1)];
|
||||
if (stats.firstFiniteIndex < ownAxis.time.size())
|
||||
{
|
||||
signal.singlePointTime = ownAxis.time[stats.firstFiniteIndex];
|
||||
}
|
||||
}
|
||||
// 有效点很少时缓存全部有效点(光标吸附 / 单点信号初始视图用)。
|
||||
CacheSparseSamples(group.AxisAt(signal.axisIndex), signal);
|
||||
|
||||
// 显示外观随信号一起初始化:调色板按「全局信号序号」取色,
|
||||
// 使同一信号在任意标签页中都保持同一默认颜色。
|
||||
@@ -651,8 +692,8 @@ namespace jdi
|
||||
signal.style.opacity = 1.0f;
|
||||
signal.style.lineWidth = 1.0f;
|
||||
signal.style.mode = DrawMode::LINE;
|
||||
// 采样点极少的信号(含只有一个点)改用「点线」并加粗,否则几乎不可见。
|
||||
ApplySparseDefaultStyle(stats.finiteCount, signal.style);
|
||||
// 只有一个有效采样点的信号改用「点线」+ 最大线宽,否则根本看不见。
|
||||
ApplySinglePointDefaultStyle(stats.finiteCount, signal.style);
|
||||
}
|
||||
|
||||
group.signals.push_back(std::move(signal));
|
||||
@@ -717,15 +758,8 @@ namespace jdi
|
||||
signal.minValue = stats.minValue;
|
||||
signal.maxValue = stats.maxValue;
|
||||
signal.finiteCount = stats.finiteCount;
|
||||
// 只含一个有效采样点时缓存它的时刻(光标吸附用);派生信号沿用源信号的时间轴。
|
||||
if (stats.finiteCount == 1 && !group.axes.empty())
|
||||
{
|
||||
const TimeAxis& ownAxis = group.AxisAt(signal.axisIndex);
|
||||
if (stats.firstFiniteIndex < ownAxis.time.size())
|
||||
{
|
||||
signal.singlePointTime = ownAxis.time[stats.firstFiniteIndex];
|
||||
}
|
||||
}
|
||||
// 有效点很少时缓存全部有效点;派生信号沿用源信号的时间轴。
|
||||
CacheSparseSamples(group.AxisAt(signal.axisIndex), signal);
|
||||
|
||||
// 2. 显示外观按「全局信号序号」取调色板颜色(与普通新信号完全一致的规则)。
|
||||
const int insertPos = afterSignalIndex + 1;
|
||||
@@ -748,7 +782,7 @@ namespace jdi
|
||||
}
|
||||
|
||||
// 采样点极少的派生信号同样改用「点线」并加粗(与普通加载路径同一规则)。
|
||||
ApplySparseDefaultStyle(stats.finiteCount, signal.style);
|
||||
ApplySinglePointDefaultStyle(stats.finiteCount, signal.style);
|
||||
}
|
||||
|
||||
group.signals.insert(group.signals.begin() + insertPos, std::move(signal));
|
||||
|
||||
Reference in new issue
Block a user