修复缩放视界下拖拽与改色卡顿、加载字节进度、光标越界三角与中心轴均值滤波统一

1. 修复选区放大后的拖拽/改色卡顿(先实测再修,不猜)
   - 用离屏探针量化:缩放视界下一帧要画约 250 条网格线,GDI+ DrawLine 每条 0.1~0.19ms,
     整帧 26.4ms;而包络带填充本身只要 0.36ms,真正的瓶颈是逐条直线调用。
   - 网格线、时间轴刻度、绘图区边框、网格分隔线全部改为 GDI 批量 1px 直线绘制
     (一次 SaveDC/IntersectClipRect/创建画笔,逐段 MoveToEx/LineTo);
     纵轴数值胶囊改为把所有圆角矩形合并成一个路径、只调用一次填充。
   - 半透明网格线颜色先用底色预混成不透明色,否则 GDI 无法绘制透明度;
     GDI 绘制不受 GDI+ 裁剪约束,批量绘制一律显式设置裁剪矩形。
   - 实测:缩放视界下场景缓存失效帧 26.4 -> 8.25ms,改色应用 7.1 -> 3.15ms,
     缓存命中帧 2.06 -> 0.30ms。

2. 读取文件时显示"已读取 / 总大小 百分比"
   - 解析进度回调扩展为携带真实字节数(已读字节、文件总字节);
     列重组等没有字节可计的阶段只显示百分比。
   - 新增字节数格式化:单位在 GB / MB / KB 之间自适应,只输出数字与单位;
     已读大小随进度自动切换单位。
   - 进度浮层按"已读取 / 文件总大小  百分比"显示,两个大小与百分比都保留两位小数,
     完全取代原来的整数百分比。

3. 网格编号拖拽重排的绘制修复
   - GX 编号悬停时保持普通箭头光标(不再变成四向箭头),按下拖拽时才使用上下箭头。
   - 拖拽时先把整幅静态场景(含主时间轴)贴回,再把目标槽位高亮、被拖网格的轮廓与
     投影裁剪在绘图区内,因此拖到工具栏甚至更上方、或拖到时间轴及下方都不再出现
     残留与错乱。

4. 网格 Y 轴数值只在完全移出可视区时才不绘制
   - 原来只要碰到边界就整条不画;现在按可见范围裁剪胶囊与数值,
     部分可见时绘制未被遮挡的部分。

5. 边沿计数横向光标的读数与越界指示
   - 幅值读数右移,让开横线左端的三角把手,不再遮挡。
   - 幅值超出视界时(横线贴边显示)在读数上方/下方绘制方向三角,
     并且与读数水平居中对齐。

6. 常规光标读数越界与双光标叠放
   - 光标滞留在视界之外时,时间读数同样绘制方向三角。
   - 双光标读数重叠时按时间值排序:小值在上、大值在下(原来固定把第二个上移,
     左右光标交换后会显示成大的在上)。
   - 双光标同时越界到同一侧时,两条时间读数与时间差值读数在同一列垂直对齐,
     并为差值读数绘制方向三角;叠放间距按三角深度放宽,三角不会再压在相邻读数上。

7. 中心轴边沿计数与均值滤波统一
   - 参数由单一的"平滑点数 N"改为按信号的"均值点数 X + 阶数 N",与实验性功能中的
     均值滤波完全同构(同样的命名、同样的作用、同样的曲线实现),
     两条曲线逐位一致,光标处的取值标注规则也统一。
   - 浮动面板改为自上而下的逐信号条目,每个条目三排:
     第一排信号名与复选框,第二排均值点数与阶数输入框,第三排该信号的上升沿/下降沿统计;
     取消勾选即不显示该信号的参数编辑与统计结果,启用时默认只勾选第一个可见信号。
   - 网格重建继承、拆分网格、信号插入/删除的下标修正与场景缓存指纹都同步覆盖新条目。

测试:离屏自动验证套件新增一组检查(字节数格式化、解析进度字节、编号悬停/拖拽光标、
双光标读数排序与对齐、中心轴三排布局与参数、横向光标越界三角、加载浮层渲染等),
合计 241 项检查全部通过;验证过程不显示窗口、不移动鼠标键盘、不抓屏。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-10 16:47:05 +08:00
1 parent bc8db72f18
commit 8801c8f019
12 files changed
+951 -420

No files matched your search

@@ -396,6 +396,8 @@ namespace jdi
bool m_loading = false; /**< 是否正在显示加载过渡动画。 */
std::wstring m_loadingText; /**< 加载遮罩显示的文件名。 */
float m_loadingProgress = 0.0f; /**< 加载进度(0-1)。 */
unsigned long long m_loadingBytesRead = 0; /**< 已读取字节数(用于进度浮层)。 */
unsigned long long m_loadingTotalBytes = 0; /**< 文件总字节数(0 表示未知)。 */
float m_loadingSpin = 0.0f; /**< 加载动画旋转相位(度)。 */
unsigned long long m_loadingBytes = 0; /**< 当前文件字节数(用于展示体积)。 */
+33 -8
View File
@@ -101,6 +101,8 @@ namespace jdi
std::atomic<bool> done{false}; /**< 工作线程是否已结束。 */
std::atomic<bool> cancelled{false}; /**< 是否请求取消。 */
std::atomic<float> progress{0.0f}; /**< 0-1 进度。 */
std::atomic<unsigned long long> bytesRead{0}; /**< 已读取字节数(解析阶段)。 */
std::atomic<unsigned long long> totalBytes{0}; /**< 文件总字节数(0 表示未知)。 */
bool ok = false; /**< 加载是否成功。 */
std::wstring error; /**< 失败原因。 */
DataFile file; /**< 构建结果。 */
@@ -115,9 +117,14 @@ namespace jdi
void RunAsyncLoad(AsyncLoadState* state, std::wstring path, std::size_t paletteBase)
{
// 解析阶段占总进度的 0-75%,列重组与统计占 75-100%。
CsvProgress parseProgress = [state](double ratio)
CsvProgress parseProgress = [state](const CsvProgressInfo& info)
{
state->progress.store(static_cast<float>(ratio * 0.75), std::memory_order_relaxed);
state->progress.store(static_cast<float>(info.ratio * 0.75), std::memory_order_relaxed);
if (info.hasBytes)
{
state->bytesRead.store(info.bytesRead, std::memory_order_relaxed);
state->totalBytes.store(info.totalBytes, std::memory_order_relaxed);
}
return !state->cancelled.load(std::memory_order_relaxed);
};
@@ -130,9 +137,10 @@ namespace jdi
return;
}
CsvProgress buildProgress = [state](double ratio)
CsvProgress buildProgress = [state](const CsvProgressInfo& info)
{
state->progress.store(static_cast<float>(0.75 + ratio * 0.25), std::memory_order_relaxed);
state->progress.store(static_cast<float>(0.75 + info.ratio * 0.25),
std::memory_order_relaxed);
return !state->cancelled.load(std::memory_order_relaxed);
};
@@ -966,6 +974,8 @@ namespace jdi
m_loading = true;
m_loadingProgress = 0.0f;
m_loadingBytesRead = 0;
m_loadingTotalBytes = 0;
const std::size_t slashPos = path.find_last_of(L"\\/");
m_loadingText = (slashPos == std::wstring::npos) ? path : path.substr(slashPos + 1);
@@ -1004,12 +1014,15 @@ namespace jdi
}
m_loadingProgress = state.progress.load(std::memory_order_relaxed);
m_loadingBytesRead = state.bytesRead.load(std::memory_order_relaxed);
m_loadingTotalBytes = state.totalBytes.load(std::memory_order_relaxed);
InvalidateRect(m_hwnd, nullptr, FALSE);
UpdateWindow(m_hwnd);
Sleep(15);
}
m_loadingProgress = 1.0f;
m_loadingBytesRead = m_loadingTotalBytes;
worker.join();
m_loading = false;
@@ -1090,10 +1103,22 @@ namespace jdi
}
wchar_t buffer[64];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%d%%", static_cast<int>(ratio * 100.0f + 0.5f));
UiText(g, buffer, m_fonts.monoSmall,
gd::RectF(track.X, card.Y + 92.0f * m_scale, track.Width, 20.0f * m_scale), m_theme.text,
TextAlign::LEFT);
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.2f%%", ratio * 100.0);
// 已读取 / 总大小:两者都只显示数字与自适应单位(GB / MB / KB),均保留两位小数;
// 百分比同样是两位小数。总大小未知(如列重组阶段)时只显示百分比。
{
std::wstring statusText = buffer;
if (m_loadingTotalBytes > 0)
{
statusText = FormatByteSize(m_loadingBytesRead) + L" / " +
FormatByteSize(m_loadingTotalBytes) + L" " + buffer;
}
UiText(g, statusText, m_fonts.monoSmall,
gd::RectF(track.X, card.Y + 92.0f * m_scale, track.Width, 20.0f * m_scale),
m_theme.text, TextAlign::LEFT);
}
UiText(g, L"按 Esc 取消", m_fonts.uiSmall,
gd::RectF(track.X, card.Y + 92.0f * m_scale, track.Width, 20.0f * m_scale),
m_theme.textMuted, TextAlign::RIGHT);
+51 -50
View File
@@ -54,7 +54,8 @@ namespace jdi
LANE_Y_MAX, /**< 编辑某网格的 Y 轴最大值。 */
LANE_Y_MIN, /**< 编辑某网格的 Y 轴最小值。 */
SIGNAL_NAME, /**< 编辑图例中某信号的显示名称。 */
EDGE_CENTER_SMOOTH, /**< 编辑「中心轴边沿计数」的平滑点数 N。 */
EDGE_CENTER_POINTS, /**< 编辑「中心轴边沿计数」的均值点数 X。 */
EDGE_CENTER_ORDER, /**< 编辑「中心轴边沿计数」的阶数 N。 */
MEAN_FILTER_POINTS, /**< 编辑「均值滤波」的均值点数 X。 */
MEAN_FILTER_ORDER /**< 编辑「均值滤波」的阶数 N。 */
};
@@ -797,6 +798,23 @@ namespace jdi
*/
double CursorSpan() const;
/**
* @brief 判断指定光标是否滞留在可视区之外(时间读数被钉在网格边缘)。
* @param [IN] which 光标序号(1 或 2)。
* @param [OUT] offRight true 表示偏出**右**侧视界,false 表示偏出左侧。
* @return 越界返回 true;光标在视界内返回 false。
*/
bool CursorOffViewSide(int which, bool& offRight) const;
/**
* @brief 在读数胶囊上方绘制"数据在视界之外"的方向三角指示。
* @param [IN] labelRect 胶囊矩形(三角与其水平居中对齐)。
* @param [IN] offRight true 表示指向右侧(光标在右视界之外)。
* @param [IN] color 三角颜色(与胶囊同色)。
*/
void PaintCursorOffViewTip(gd::Graphics& g, const gd::RectF& labelRect, bool offRight,
std::uint32_t color);
/**
* @brief 计算双光标差值标签的矩形(时间轴上方、两条时间读数的中间位置)。
* @param [OUT] insideAxis 为 true 时表示两条光标过近,标签落在主时间轴内部。
@@ -952,15 +970,17 @@ namespace jdi
gd::RectF button; /**< 面板底部按钮(无按钮时宽度为 0)。 */
int rows = 0; /**< 统计行数(每条可见信号一行)。 */
/**
* @brief 均值滤波面板的逐信号条目矩形(仅 MEAN 面板使用)。
* @brief 「逐条目」面板的矩形(MEAN 与 CENTER 面板共用同一套排布)。
*
* 索引与 `rowRefs` 一一对应(即网格内可见信号的顺序):
* `rowChecks` 为第一排右侧的复选框;`rowPoints` / `rowOrder` 为第二排的两个输入框
* (未勾选的条目两个矩形均为空)。
* `rowChecks` 为第一排右侧的复选框;`rowPoints` / `rowOrder` 为第二排的两个输入框;
* `rowStats` 为第三排的统计结果(中心轴边沿计数专用)。
* 未勾选的条目对应的矩形均为空。
*/
std::vector<gd::RectF> rowChecks;
std::vector<gd::RectF> rowPoints;
std::vector<gd::RectF> rowOrder;
std::vector<gd::RectF> rowStats;
std::vector<SignalRef> rowRefs;
};
@@ -975,8 +995,8 @@ namespace jdi
/**
* @brief 获取多阶(迭代)均值滤波曲线,结果带缓存。
*
* 语义:以「均值点数 X」作滑平半径,对输入重复做 `order` 次滑动平均;
* `order == 1` 时与 `CenterCurveFor` 完全一致。
* 语义:以「均值点数 X」作滑平半径,对输入重复做 `order` 次滑动平均。
* 「实验性功能 → 均值滤波」与「中心轴边沿计数」共用本函数,二者曲线因此逐位一致。
*
* @param [IN] signal 目标信号。
* @param [IN] points 均值点数 X(滑平半径)。
@@ -1016,52 +1036,42 @@ namespace jdi
bool LaneMeanFilterSignalEnabled(int laneIndex, const SignalRef& ref) const;
/**
* @brief 获取多阶(迭代)均值滤波曲线,结果带缓存。
* @param [IN] signal 目标信号。
* @param [IN] smooth 平滑半径 N(0 表示不平滑)。
* @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。
*/
const std::vector<float>& CenterCurveFor(const SignalData& signal, int smooth) const;
/**
* @brief 取某个时刻的中心平均值曲线取值(用于纵向光标的取值标注)。
* @brief 统计信号穿越“均值滤波曲线”的上/下边沿数量,结果带缓存。
*
* 曲线由「均值点数 X + 阶数 N」确定,与「实验性功能 → 均值滤波」的预览曲线逐位一致。
*
* @param [IN] file 信号所属文件。
* @param [IN] signal 目标信号。
* @param [IN] smooth 平滑半径 N。
* @param [IN] time 采样时刻。
* @param [OUT] value 输出取值。
* @return 取到有效值返回 true。
*/
bool CenterValueAt(const DataFile& file, const SignalData& signal, int smooth, double time,
double& value) const;
/**
* @brief 统计信号穿越“中心平均值曲线”的上/下边沿数量,结果带缓存。
* @param [IN] file 信号所属文件。
* @param [IN] signal 目标信号。
* @param [IN] smooth 平滑半径 N。
* @param [IN] points 均值点数 X。
* @param [IN] order 阶数 N。
* @param [IN] hasRange 是否只统计某个时间区间(双光标模式下为光标区间)。
* @param [IN] rangeMin 区间左端点时间。
* @param [IN] rangeMax 区间右端点时间。
* @param [OUT] rising 上升沿数量(信号从下往上越过中心平均值曲线)。
* @param [OUT] falling 下降沿数量(信号从上往下穿过中心平均值曲线)。
* @param [OUT] rising 上升沿数量(信号从下往上越过均值滤波曲线)。
* @param [OUT] falling 下降沿数量(信号从上往下穿过均值滤波曲线)。
*/
void EdgeCenterCountsFor(const DataFile& file, const SignalData& signal, int smooth,
void EdgeCenterCountsFor(const DataFile& file, const SignalData& signal, int points, int order,
bool hasRange, double rangeMin, double rangeMax, std::size_t& rising,
std::size_t& falling) const;
/**
* @brief 绘制某条信号的中心平均值曲线(叠加在原信号所在网格上)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] resolved 信号视图。
* @param [IN] lane 网格绘制信息。
* @param [IN] smooth 平滑半径 N。
* @brief 切换某个信号在「中心轴边沿计数」面板中的勾选状态。
* @param [IN] laneIndex 网格下标。
* @param [IN] ref 目标信号。
* @return 切换后的勾选状态。
*/
void PaintCenterCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
int smooth) const;
bool ToggleLaneEdgeCenterSignal(int laneIndex, const SignalRef& ref);
/**
* @brief 绘制一段派生曲线(中心平均值 / 多阶均值滤波共用)。
* @brief 查询某个信号在「中心轴边沿计数」面板中的勾选状态。
* @param [IN] laneIndex 网格下标。
* @param [IN] ref 目标信号。
* @return 已勾选返回 true。
*/
bool LaneEdgeCenterSignalEnabled(int laneIndex, const SignalRef& ref) const;
/**
* @brief 绘制一段派生曲线(中心轴均值滤波 / 多阶均值滤波共用同一实现)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] resolved 信号视图。
* @param [IN] lane 网格绘制信息。
@@ -1341,16 +1351,6 @@ namespace jdi
};
mutable std::vector<EdgeCountEntry> m_edgeCountCache; /**< 边沿统计缓存(纯缓存,逻辑上不影响对象状态)。 */
/** 中心平均值曲线缓存项(按「信号指针 + 平滑半径」标识)。 */
struct CenterCurveEntry
{
const SignalData* signal = nullptr;
int smooth = 0;
std::vector<float> values; /**< 与 signal->values 等长;缺失处为 NaN。 */
};
/** 中心平均值曲线缓存:**deque** 保证 push_back 后已有元素引用仍然有效。 */
mutable std::deque<CenterCurveEntry> m_centerCurveCache;
/** 多阶均值滤波曲线缓存项(按「信号指针 + 均值点数 + 阶数」标识)。 */
struct MeanCurveEntry
{
@@ -1366,7 +1366,8 @@ namespace jdi
{
const DataFile* file = nullptr; /**< 所属数据分组。 */
const SignalData* signal = nullptr; /**< 目标信号。 */
int smooth = 0; /**< 中心平均值曲线的平滑半径 N。 */
int points = 5; /**< 均值点数 X。 */
int order = 1; /**< 阶数 N。 */
bool hasRange = false; /**< 是否限定在时间区间内统计。 */
bool reusable = false; /**< 计数是否完全由 [begin, end) 决定。 */
std::size_t begin = 0; /**< 已统计的采样区间起点(含)。 */
File diff suppressed because it is too large. Load diff