修复缩放视界下拖拽与改色卡顿、加载字节进度、光标越界三角与中心轴均值滤波统一
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:
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; /**< 当前文件字节数(用于展示体积)。 */
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 文件。
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 绘图上下文。
|
||||
|
||||
@@ -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 待格式化数值。
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user