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

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
+16 -3
View File
@@ -35,13 +35,26 @@ namespace jdi
}
};
/**
* @brief 加载进度信息。
*
* 解析阶段会携带真实的"已读 / 总字节数";列重组与统计阶段没有字节可计,
* 此时 `hasBytes` 为 false,界面应只展示百分比。
*/
struct CsvProgressInfo
{
double ratio = 0.0; /**< 0-1 总体完成比例。 */
unsigned long long bytesRead = 0; /**< 已读取字节数。 */
unsigned long long totalBytes = 0; /**< 文件总字节数。 */
bool hasBytes = false; /**< 上述字节数字段是否有效。 */
};
/**
* @brief 解析进度回调。
*
* 在解析过程中按块回调,ratio 为 0.0-1.0 的完成比例;
* 返回 false 表示请求取消,解析会尽快停止并返回失败。
* 在解析过程中按块回调;返回 false 表示请求取消,解析会尽快停止并返回失败。
*/
using CsvProgress = std::function<bool(double ratio)>;
using CsvProgress = std::function<bool(const CsvProgressInfo& info)>;
/**
* @brief 读取并解析一个 CSV 文件。
+29 -4
View File
@@ -360,6 +360,8 @@ namespace jdi
*
* 仅参与列表中的信号才会被勾选预览;参数按信号保存,
* 因此同一条网格内不同信号可以使用不同的均值点数与阶数。
* 「边沿计数(中心轴)」的平滑参数与它**完全同构**(同一套命名与作用),
* 见 `EdgeCenterItem`。
*/
struct MeanFilterItem
{
@@ -368,6 +370,9 @@ namespace jdi
int order = 1; /**< 阶数 N(重复滤波次数,至少 1)。 */
};
/** 中心轴边沿计数的平滑参数:与均值滤波共用同一结构与同一套语义。 */
using EdgeCenterItem = MeanFilterItem;
std::wstring name; /**< 网格名称。 */
std::vector<SignalRef> signals; /**< 该网格包含的信号(按加入顺序)。 */
bool autoRange = true; /**< 是否依据信号取值范围自动计算 Y 轴。 */
@@ -408,14 +413,34 @@ namespace jdi
/**
* @brief 「实验性功能 → 边沿计数(中心轴)」开关。
*
* 开启后为本网格内每条信号额外绘制一条"中心平均值曲线"(每个数据点取自身与左右各
* `edgeCenterSmooth` 个点的平均值),并按“信号穿越中心平均值曲线”统计上/下边沿。
* 开启后为本网格内**被勾选**的信号额外绘制一条均值滤波曲线(与「实验性功能 → 均值滤波」
* 用的是同一套参数:均值点数 X + 阶数 N),并按“信号穿越该滤波曲线”统计上/下边沿。
* 这条曲线只是叠加显示,不会作为信号加入左侧列表。
*/
bool edgeCenter = false;
/** 中心平均值的平滑半径 N(左右各取 N 个数据点,窗口大小 2N+1)。 */
int edgeCenterSmooth = 5;
/**
* @brief 参与中心轴边沿计数的信号及其平滑参数(只包含被勾选的信号)。
*
* 参数语义与 `meanFilterItems` **完全一致**(`points` = 均值点数 X,
* `order` = 阶数 N),因此两条曲线的数值逐位相同。
*/
std::vector<EdgeCenterItem> edgeCenterItems;
/** 取某个信号的中心轴平滑参数(未勾选时返回空指针)。 */
const EdgeCenterItem* FindEdgeCenterItem(const SignalRef& ref) const;
/** 取得或创建某信号的中心轴平滑参数(用于勾选)。 */
EdgeCenterItem& EnsureEdgeCenterItem(const SignalRef& ref);
/** 取消某信号的中心轴边沿计数勾选。 */
void RemoveEdgeCenterItem(const SignalRef& ref);
/** 取某信号的中心轴边沿计数勾选状态。 */
bool EdgeCenterEnabled(const SignalRef& ref) const
{
return FindEdgeCenterItem(ref) != nullptr;
}
/** 中心轴统计浮动面板是否展开。 */
bool edgeCenterPanelOpen = true;
+46
View File
@@ -5,8 +5,10 @@
#include <objidl.h>
#include <gdiplus.h>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "TextEdit.hpp"
#include "Theme.hpp"
@@ -224,6 +226,50 @@ namespace jdi
*/
void UiDrawPlusIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float strokeWidth);
/**
* @brief 用 GDI 一次性绘制若干条**不透明 1px 实线**(每条线段相互独立)。
*
* 性能关键:GDI+ 的 `DrawLine` 实测每条约 0.1–0.19 ms(带抗锯齿的长线更慢),
* 而一张网格通常要画几十条网格线 —— 逐条调用会让单帧多花十几毫秒。
* 这里改用 GDI(一次取 HDC + 一支画笔 + 多个 LineTo),快一两个数量级,
* 而且像素级网格线本来就不需要抗锯齿,观感反而更清晰。
*
* ⚠️ GDI 绘制**不受 GDI+ 裁剪区约束**,因此必须显式传入裁剪矩形
* (内部用 `SaveDC` + `IntersectClipRect` 保证不越界)。
* ⚠️ 只能用于**不透明**颜色(GDI 不做 alpha 混合);半透明色需调用方先与底色混合。
*
* @param [IN,OUT] g 绘图上下文。
* @param [IN] clip 裁剪矩形(客户区坐标)。
* @param [IN] segments 线段数组,每 4 个 float 为一条(x1,y1,x2,y2)。
* @param [IN] segmentCount 线段数量。
* @param [IN] argb 颜色(Alpha 必须为 0xFF,否则不做任何绘制)。
* @param [IN] width 线宽(像素,>= 1;仅精确支持 1,更宽时按整数宽画)。
*/
void UiDrawSolidLines(gd::Graphics& g, const gd::RectF& clip, const float* segments,
std::size_t segmentCount, std::uint32_t argb, int width = 1);
/**
* @brief 把半透明颜色与已知不透明底色混合,得到可直接用于 GDI 的不透明颜色。
* @param [IN] background 不透明底色(0xFFRRGGBB)。
* @param [IN] argb 待叠加颜色(含 alpha)。
* @return 混合后的不透明颜色。
*/
std::uint32_t UiBlendOver(std::uint32_t background, std::uint32_t argb);
/**
* @brief 一次性填充多个圆角矩形(同色同圆角)。
*
* 每帧要画几十个刻度数值底板时,逐个调用 `UiFillRoundRect` 会带来可观的 GDI+ 调用开销;
* 合并成一条 GraphicsPath 后只 FillPath 一次(实测约快 3 倍)。
*
* @param [IN,OUT] g 绘图上下文。
* @param [IN] rects 矩形集合。
* @param [IN] radius 圆角半径。
* @param [IN] argb 填充色。
*/
void UiFillRoundRects(gd::Graphics& g, const std::vector<gd::RectF>& rects, float radius,
std::uint32_t argb);
/**
* @brief 在矩形中心绘制一个叉号(关闭)图标。
* @param [IN,OUT] g 绘图上下文。
+11
View File
@@ -64,6 +64,17 @@ namespace jdi
*/
std::wstring FormatNumber(double value, int decimals);
/**
* @brief 把字节数格式化为自适应单位的文本(GB / MB / KB),保留两位小数。
*
* 单位阀值:1 GB = 1024 MB、1 MB = 1024 KB;小于 1 KB 时也按 KB 显示。
* 只输出"数字 + 单位"(如 "1.69 GB"),便于拼接为"已读取 / 总量"。
*
* @param [IN] bytes 字节数。
* @return 形如 "812.34 MB" 的文本。
*/
std::wstring FormatByteSize(unsigned long long bytes);
/**
* @brief 以自适应有效位格式化数值(NaN/Inf 会被转换为可读文本)。
* @param [IN] value 待格式化数值。
+12 -5
View File
@@ -397,11 +397,18 @@ namespace jdi
if (progress)
{
const double ratio = (fileSize > 0)
? static_cast<double>(state.bytesConsumed) /
static_cast<double>(fileSize)
: 0.0;
if (!progress(ratio > 1.0 ? 1.0 : ratio))
CsvProgressInfo info;
info.bytesRead = state.bytesConsumed;
info.totalBytes = fileSize;
info.hasBytes = true;
info.ratio = (fileSize > 0) ? static_cast<double>(state.bytesConsumed) /
static_cast<double>(fileSize)
: 0.0;
if (info.ratio > 1.0)
{
info.ratio = 1.0;
}
if (!progress(info))
{
cancelled = true;
}
+80 -23
View File
@@ -122,7 +122,7 @@ namespace jdi
double edgePanelX = -1.0;
double edgePanelY = -1.0;
bool edgeCenter = false;
int edgeCenterSmooth = 5;
std::vector<Lane::EdgeCenterItem> edgeCenterItems;
bool edgeCenterPanelOpen = true;
double edgeCenterPanelX = -1.0;
double edgeCenterPanelY = -1.0;
@@ -147,7 +147,7 @@ namespace jdi
carry.edgePanelX = lane.edgePanelX;
carry.edgePanelY = lane.edgePanelY;
carry.edgeCenter = lane.edgeCenter;
carry.edgeCenterSmooth = lane.edgeCenterSmooth;
carry.edgeCenterItems = lane.edgeCenterItems;
carry.edgeCenterPanelOpen = lane.edgeCenterPanelOpen;
carry.edgeCenterPanelX = lane.edgeCenterPanelX;
carry.edgeCenterPanelY = lane.edgeCenterPanelY;
@@ -183,7 +183,7 @@ namespace jdi
lane.edgePanelX = carry.edgePanelX;
lane.edgePanelY = carry.edgePanelY;
lane.edgeCenter = carry.edgeCenter;
lane.edgeCenterSmooth = carry.edgeCenterSmooth;
lane.edgeCenterItems = carry.edgeCenterItems;
lane.edgeCenterPanelOpen = carry.edgeCenterPanelOpen;
lane.edgeCenterPanelX = carry.edgeCenterPanelX;
lane.edgeCenterPanelY = carry.edgeCenterPanelY;
@@ -571,11 +571,15 @@ namespace jdi
file.signals.push_back(std::move(signal));
if (progress && !progress(0.75 + 0.25 * static_cast<double>(column + 1) /
static_cast<double>(columnCount)))
if (progress)
{
error = L"已取消加载。";
return false;
CsvProgressInfo info;
info.ratio = 0.75 + 0.25 * static_cast<double>(column + 1) / static_cast<double>(columnCount);
if (!progress(info))
{
error = L"已取消加载。";
return false;
}
}
}
@@ -1030,6 +1034,41 @@ namespace jdi
meanFilterItems.end());
}
const Lane::EdgeCenterItem* Lane::FindEdgeCenterItem(const SignalRef& ref) const
{
for (const EdgeCenterItem& item : edgeCenterItems)
{
if (item.ref == ref)
{
return &item;
}
}
return nullptr;
}
Lane::EdgeCenterItem& Lane::EnsureEdgeCenterItem(const SignalRef& ref)
{
for (EdgeCenterItem& item : edgeCenterItems)
{
if (item.ref == ref)
{
return item;
}
}
EdgeCenterItem item;
item.ref = ref;
edgeCenterItems.push_back(item);
return edgeCenterItems.back();
}
void Lane::RemoveEdgeCenterItem(const SignalRef& ref)
{
edgeCenterItems.erase(
std::remove_if(edgeCenterItems.begin(), edgeCenterItems.end(),
[&ref](const EdgeCenterItem& item) { return item.ref == ref; }),
edgeCenterItems.end());
}
int FindNearestSample(const DataFile& file, double time)
{
const std::size_t count = file.time.size();
@@ -1254,22 +1293,26 @@ namespace jdi
}
}
// 均值滤波条目同样持有信号引用,必须一起删除/前移。
std::vector<Lane::MeanFilterItem>& items = lane.meanFilterItems;
items.erase(std::remove_if(items.begin(), items.end(),
[fileIndex, signalIndex](const Lane::MeanFilterItem& item)
{
return item.ref.fileIndex == fileIndex &&
item.ref.signalIndex == signalIndex;
}),
items.end());
for (Lane::MeanFilterItem& item : items)
// 均值滤波 / 中心轴边沿计数的条目同样持有信号引用,必须一起删除/前移。
const auto purgeRefs = [fileIndex, signalIndex](std::vector<Lane::MeanFilterItem>& items)
{
if (item.ref.fileIndex == fileIndex && item.ref.signalIndex > signalIndex)
items.erase(std::remove_if(items.begin(), items.end(),
[fileIndex, signalIndex](const Lane::MeanFilterItem& item)
{
return item.ref.fileIndex == fileIndex &&
item.ref.signalIndex == signalIndex;
}),
items.end());
for (Lane::MeanFilterItem& item : items)
{
--item.ref.signalIndex;
if (item.ref.fileIndex == fileIndex && item.ref.signalIndex > signalIndex)
{
--item.ref.signalIndex;
}
}
}
};
purgeRefs(lane.meanFilterItems);
purgeRefs(lane.edgeCenterItems);
}
// 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
@@ -1558,13 +1601,13 @@ namespace jdi
lane.edgePanelX = source.edgePanelX;
lane.edgePanelY = source.edgePanelY;
lane.edgeCenter = source.edgeCenter;
lane.edgeCenterSmooth = source.edgeCenterSmooth;
lane.edgeCenterPanelOpen = source.edgeCenterPanelOpen;
lane.edgeCenterPanelX = source.edgeCenterPanelX;
lane.edgeCenterPanelY = source.edgeCenterPanelY;
lane.meanFilter = source.meanFilter;
// 拆分后每个子网格只保留自己那个信号的均值滤波条目。
// 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴条目。
lane.meanFilterItems.clear();
lane.edgeCenterItems.clear();
for (const Lane::MeanFilterItem& item : source.meanFilterItems)
{
if (item.ref == ref)
@@ -1572,6 +1615,13 @@ namespace jdi
lane.meanFilterItems.push_back(item);
}
}
for (const Lane::EdgeCenterItem& item : source.edgeCenterItems)
{
if (item.ref == ref)
{
lane.edgeCenterItems.push_back(item);
}
}
lane.meanFilterPanelOpen = source.meanFilterPanelOpen;
lane.meanFilterPanelX = source.meanFilterPanelX;
lane.meanFilterPanelY = source.meanFilterPanelY;
@@ -1615,7 +1665,7 @@ namespace jdi
++ref.signalIndex;
}
}
// 均值滤波条目同样持有信号引用,必须一起后移。
// 均值滤波 / 中心轴条目同样持有信号引用,必须一起后移。
for (Lane::MeanFilterItem& item : lane.meanFilterItems)
{
if (item.ref.fileIndex == fileIndex && item.ref.signalIndex >= insertPos)
@@ -1623,6 +1673,13 @@ namespace jdi
++item.ref.signalIndex;
}
}
for (Lane::EdgeCenterItem& item : lane.edgeCenterItems)
{
if (item.ref.fileIndex == fileIndex && item.ref.signalIndex >= insertPos)
{
++item.ref.signalIndex;
}
}
}
}
+103
View File
@@ -194,6 +194,109 @@ namespace jdi
g.DrawPath(&pen, &path);
}
void UiFillRoundRects(gd::Graphics& g, const std::vector<gd::RectF>& rects, float radius,
std::uint32_t argb)
{
if (rects.empty())
{
return;
}
gd::GraphicsPath path;
for (const gd::RectF& rect : rects)
{
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
{
continue;
}
BuildRoundRectPath(path, rect, radius);
}
if (path.GetPointCount() > 0)
{
gd::SolidBrush brush(ToGdiColor(argb));
g.FillPath(&brush, &path);
}
}
std::uint32_t UiBlendOver(std::uint32_t background, std::uint32_t argb)
{
const unsigned int alpha = (argb >> 24) & 0xFFu;
if (alpha >= 0xFFu)
{
return argb | 0xFF000000u;
}
if (alpha == 0u)
{
return background | 0xFF000000u;
}
const double ratio = static_cast<double>(alpha) / 255.0;
std::uint32_t result = 0xFF000000u;
for (int shift = 0; shift <= 16; shift += 8)
{
const double top = static_cast<double>((argb >> shift) & 0xFFu);
const double bottom = static_cast<double>((background >> shift) & 0xFFu);
const double mixed = bottom + (top - bottom) * ratio;
result |= (static_cast<std::uint32_t>(mixed + 0.5) & 0xFFu) << shift;
}
return result;
}
void UiDrawSolidLines(gd::Graphics& g, const gd::RectF& clip, const float* segments,
std::size_t segmentCount, std::uint32_t argb, int width)
{
if (segments == nullptr || segmentCount == 0 || (argb & 0xFF000000u) != 0xFF000000u)
{
return;
}
if (width < 1)
{
width = 1;
}
const int clipLeft = static_cast<int>(std::floor(clip.X));
const int clipTop = static_cast<int>(std::floor(clip.Y));
const int clipRight = static_cast<int>(std::ceil(clip.X + clip.Width));
const int clipBottom = static_cast<int>(std::ceil(clip.Y + clip.Height));
if (clipRight <= clipLeft || clipBottom <= clipTop)
{
return;
}
HDC hdc = g.GetHDC();
if (hdc == nullptr)
{
return;
}
// GDI 绘制不受 GDI+ 裁剪约束,这里显式设置并随后完整还原 DC 状态。
const int saved = SaveDC(hdc);
if (saved != 0)
{
IntersectClipRect(hdc, clipLeft, clipTop, clipRight, clipBottom);
}
HPEN pen = CreatePen(PS_SOLID, width, ToColorRef(argb));
if (pen != nullptr)
{
HGDIOBJ oldPen = SelectObject(hdc, pen);
for (std::size_t index = 0; index < segmentCount; ++index)
{
const float* segment = segments + index * 4;
MoveToEx(hdc, static_cast<int>(std::lround(segment[0])),
static_cast<int>(std::lround(segment[1])), nullptr);
LineTo(hdc, static_cast<int>(std::lround(segment[2])),
static_cast<int>(std::lround(segment[3])));
}
SelectObject(hdc, oldPen);
DeleteObject(pen);
}
if (saved != 0)
{
RestoreDC(hdc, saved);
}
g.ReleaseHDC(hdc);
}
void UiDrawLine(gd::Graphics& g, float x1, float y1, float x2, float y2, std::uint32_t argb, float width)
{
gd::Pen pen(ToGdiColor(argb), width);
+29
View File
@@ -339,6 +339,35 @@ namespace jdi
return std::wstring(buffer);
}
std::wstring FormatByteSize(unsigned long long bytes)
{
// 单位自适应:GB / MB / KB(阈值按 1024 进制)。
constexpr double kKilo = 1024.0;
constexpr double kMega = kKilo * 1024.0;
constexpr double kGiga = kMega * 1024.0;
const double value = static_cast<double>(bytes);
const wchar_t* unit = L"KB";
double scaled = value / kKilo;
if (value >= kGiga)
{
unit = L"GB";
scaled = value / kGiga;
}
else if (value >= kMega)
{
unit = L"MB";
scaled = value / kMega;
}
wchar_t buffer[64];
// ⚠️ 宽字符格式化必须用 `%ls`:MinGW 的 swprintf 按 C99 语义处理 `%s`
//(在宽格式串里 `%s` 表示**窄**字符串),写成 `%s` 会把宽字符串按单字节读取,
// 于是 "KB" 只打印出 "K"。
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.2f %ls", scaled, unit);
return std::wstring(buffer);
}
std::wstring FormatValue(double value)
{
if (std::isnan(value))