Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
940b8e67e6 | ||
|
|
fc77b0262f | ||
|
|
2b02518f85 | ||
|
|
2f243dd557 | ||
|
|
c3343907ec |
No files matched your search
@@ -395,6 +395,7 @@ namespace jdi
|
|||||||
Target m_activeTarget = Target::NONE; /**< 当前鼠标作用目标。 */
|
Target m_activeTarget = Target::NONE; /**< 当前鼠标作用目标。 */
|
||||||
bool m_trackingLeave = false; /**< 是否已注册鼠标离开通知。 */
|
bool m_trackingLeave = false; /**< 是否已注册鼠标离开通知。 */
|
||||||
bool m_hintTimerActive = false; /**< 提示浮层淡出定时器是否已启动。 */
|
bool m_hintTimerActive = false; /**< 提示浮层淡出定时器是否已启动。 */
|
||||||
|
std::wstring m_windowTitleText; /**< 上次写入窗口标题的完整文本(用于避免重复设置)。 */
|
||||||
int m_tabMenuIndex = -1; /**< 标签栏右键菜单的目标标签页下标。 */
|
int m_tabMenuIndex = -1; /**< 标签栏右键菜单的目标标签页下标。 */
|
||||||
int m_plotMenuLane = -1; /**< 绘图区右键菜单的目标网格下标。 */
|
int m_plotMenuLane = -1; /**< 绘图区右键菜单的目标网格下标。 */
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
#include <commdlg.h>
|
#include <commdlg.h>
|
||||||
|
#include <psapi.h>
|
||||||
#include <shellapi.h>
|
#include <shellapi.h>
|
||||||
#include <windowsx.h>
|
#include <windowsx.h>
|
||||||
|
|
||||||
@@ -27,7 +28,16 @@ namespace jdi
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
const wchar_t* kWindowClassName = L"JustDataInspectorMainWindow"; /**< 窗口类名。 */
|
const wchar_t* kWindowClassName = L"JustDataInspectorMainWindow"; /**< 窗口类名。 */
|
||||||
const wchar_t* kWindowTitle = L"Just DataInspector"; /**< 窗口标题(仅显示应用名称)。 */
|
const wchar_t* kWindowTitle = L"Just DataInspector"; /**< 窗口标题中的应用名称部分。 */
|
||||||
|
/**
|
||||||
|
* @brief 应用名称与内存占用之间的分隔竖线。
|
||||||
|
*
|
||||||
|
* 与信号分组标题里「组X | 组名」的分隔方式一致:竖线两侧各留出空隙
|
||||||
|
*(`kGroupSeparatorGap` 是那个分隔线的左右间距,这里用两个空格达到同等观感)。
|
||||||
|
*/
|
||||||
|
const wchar_t* kTitleSeparator = L" | ";
|
||||||
|
/** @brief 内存占用的显示前缀(与具体数值之间保留一个空格)。 */
|
||||||
|
const wchar_t* kRamTitlePrefix = L"RAM : ";
|
||||||
const float kSplitterWidth = 4.0f; /**< 分隔条宽度(逻辑像素)。 */
|
const float kSplitterWidth = 4.0f; /**< 分隔条宽度(逻辑像素)。 */
|
||||||
const float kMinPanelWidth = 260.0f; /**< 面板最小宽度(逻辑像素)。 */
|
const float kMinPanelWidth = 260.0f; /**< 面板最小宽度(逻辑像素)。 */
|
||||||
const float kMaxPanelWidth = 400.0f; /**< 面板最大宽度(逻辑像素)。 */
|
const float kMaxPanelWidth = 400.0f; /**< 面板最大宽度(逻辑像素)。 */
|
||||||
@@ -37,6 +47,29 @@ namespace jdi
|
|||||||
const UINT kMessageRemoveSignal = WM_APP + 2; /**< 面板请求移除单个信号的自定义消息。 */
|
const UINT kMessageRemoveSignal = WM_APP + 2; /**< 面板请求移除单个信号的自定义消息。 */
|
||||||
const UINT_PTR kHintTimerId = 1; /**< 提示浮层淡出用的定时器 ID。 */
|
const UINT_PTR kHintTimerId = 1; /**< 提示浮层淡出用的定时器 ID。 */
|
||||||
const UINT kHintTimerPeriodMs = 33; /**< 提示浮层淡出的重绘间隔(约 30fps)。 */
|
const UINT kHintTimerPeriodMs = 33; /**< 提示浮层淡出的重绘间隔(约 30fps)。 */
|
||||||
|
const UINT_PTR kRamTimerId = 2; /**< 标题栏内存占用刷新的定时器 ID。 */
|
||||||
|
const UINT kRamTimerPeriodMs = 1000; /**< 标题栏内存占用的刷新间隔(1 秒)。 */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 查询本进程当前占用的物理内存(工作集)。
|
||||||
|
*
|
||||||
|
* 与任务管理器里显示的“内存”一致:进程已驻留在物理内存中的字节数。
|
||||||
|
* 使用 `K32GetProcessMemoryInfo`(kernel32 导出)以免为 `psapi` 增加一条链接依赖。
|
||||||
|
*
|
||||||
|
* @return 工作集字节数;查询失败返回 0。
|
||||||
|
*/
|
||||||
|
unsigned long long ProcessWorkingSetBytes()
|
||||||
|
{
|
||||||
|
PROCESS_MEMORY_COUNTERS_EX counters = {};
|
||||||
|
counters.cb = sizeof(counters);
|
||||||
|
if (K32GetProcessMemoryInfo(GetCurrentProcess(),
|
||||||
|
reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&counters),
|
||||||
|
sizeof(counters)) == FALSE)
|
||||||
|
{
|
||||||
|
return 0ull;
|
||||||
|
}
|
||||||
|
return static_cast<unsigned long long>(counters.WorkingSetSize);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 只失效绘图区所在的矩形。
|
* @brief 只失效绘图区所在的矩形。
|
||||||
@@ -345,6 +378,8 @@ namespace jdi
|
|||||||
NewTab();
|
NewTab();
|
||||||
|
|
||||||
Relayout();
|
Relayout();
|
||||||
|
// 标题栏内存占用按固定间隔刷新(不使用每帧刷新,避免频繁改动窗口标题)。
|
||||||
|
SetTimer(m_hwnd, kRamTimerId, kRamTimerPeriodMs, nullptr);
|
||||||
ShowWindow(m_hwnd, showCommand);
|
ShowWindow(m_hwnd, showCommand);
|
||||||
UpdateWindow(m_hwnd);
|
UpdateWindow(m_hwnd);
|
||||||
LoadCommandLineFiles();
|
LoadCommandLineFiles();
|
||||||
@@ -862,8 +897,19 @@ namespace jdi
|
|||||||
|
|
||||||
void Application::UpdateWindowTitle()
|
void Application::UpdateWindowTitle()
|
||||||
{
|
{
|
||||||
// 标题栏只显示应用名称,不附带标签页、文件数量等无关信息。
|
// 标题栏 = 应用名称 + 分隔竖线 + 内存占用(单位在 KB/MB/GB 间自适应,保留两位小数)。
|
||||||
SetWindowTextW(m_hwnd, kWindowTitle);
|
// 除内存占用外不再附带标签页、文件数量等无关信息。
|
||||||
|
std::wstring title = kWindowTitle;
|
||||||
|
title += kTitleSeparator;
|
||||||
|
title += kRamTitlePrefix;
|
||||||
|
title += FormatByteSize(ProcessWorkingSetBytes());
|
||||||
|
// 数值未变化时不调用 SetWindowTextW,避免无谓地重绘标题栏。
|
||||||
|
if (title == m_windowTitleText)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_windowTitleText = title;
|
||||||
|
SetWindowTextW(m_hwnd, m_windowTitleText.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::OpenFileDialog()
|
void Application::OpenFileDialog()
|
||||||
@@ -1479,6 +1525,29 @@ namespace jdi
|
|||||||
const SignalData& source = file->signals[static_cast<std::size_t>(request.signalIndex)];
|
const SignalData& source = file->signals[static_cast<std::size_t>(request.signalIndex)];
|
||||||
const std::wstring baseName = source.Title();
|
const std::wstring baseName = source.Title();
|
||||||
|
|
||||||
|
// 0. 先把源信号的「调整」变换快照下来(下面 `InsertDerivedSignal` 会移动信号存储,
|
||||||
|
// 之后再取 `source` 会悬垂)。
|
||||||
|
//
|
||||||
|
// ⚠️ **新信号必须继承这套变换**,否则会出现“预览是调整后的波形、构建出来的却按原始状态绘制”
|
||||||
|
//(用户报告的不一致):网格里的预览曲线绘制的是 `变换(滤波结果)`,而构建出的信号只存了
|
||||||
|
// `滤波结果`;把变换一并继承后,两者绘制完全相同。
|
||||||
|
// 另外**时间轴偏移只能靠继承**:派生信号与源信号共用同一个文件时间轴,无法把时间平移烘进数据。
|
||||||
|
SignalStyle inheritedTransform;
|
||||||
|
{
|
||||||
|
SignalRef sourceRef;
|
||||||
|
sourceRef.fileIndex = request.fileIndex;
|
||||||
|
sourceRef.signalIndex = request.signalIndex;
|
||||||
|
if (const SignalStyle* sourceStyle = m_dataSet.Style(sourceRef))
|
||||||
|
{
|
||||||
|
inheritedTransform.gain = sourceStyle->gain;
|
||||||
|
inheritedTransform.absolute = sourceStyle->absolute;
|
||||||
|
inheritedTransform.mirrorVertical = sourceStyle->mirrorVertical;
|
||||||
|
inheritedTransform.mirrorHorizontal = sourceStyle->mirrorHorizontal;
|
||||||
|
inheritedTransform.offsetY = sourceStyle->offsetY;
|
||||||
|
inheritedTransform.offsetTime = sourceStyle->offsetTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 计算派生采样值(与网格内的预览曲线共用同一实现,因此逐位一致)。
|
// 1. 计算派生采样值(与网格内的预览曲线共用同一实现,因此逐位一致)。
|
||||||
// 多次迭代均值滤波形如「<原信号名称> (N点均值 M次迭代)」,
|
// 多次迭代均值滤波形如「<原信号名称> (N点均值 M次迭代)」,
|
||||||
// 弱峰调整形如「<原信号名称> (弱峰调整 M% N次迭代)」。
|
// 弱峰调整形如「<原信号名称> (弱峰调整 M% N次迭代)」。
|
||||||
@@ -1498,7 +1567,7 @@ namespace jdi
|
|||||||
std::to_wstring(request.iterations) + L"次迭代)";
|
std::to_wstring(request.iterations) + L"次迭代)";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 组装新信号。
|
// 2. 组装新信号(外观先沿用默认值,插入后再继承源信号的「调整」变换)。
|
||||||
derived.rawHeader = derived.name;
|
derived.rawHeader = derived.name;
|
||||||
derived.unit = source.unit;
|
derived.unit = source.unit;
|
||||||
derived.values.assign(derived_values.begin(), derived_values.end());
|
derived.values.assign(derived_values.begin(), derived_values.end());
|
||||||
@@ -1517,6 +1586,18 @@ namespace jdi
|
|||||||
tab.RemapAfterSignalInsert(request.fileIndex, newIndex);
|
tab.RemapAfterSignalInsert(request.fileIndex, newIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3b. 新信号继承源信号的「调整」变换(系数 / 绝对值 / 镜像 / 偏移),
|
||||||
|
// 使其绘制结果与网格里的预览曲线**完全一致**。
|
||||||
|
if (SignalStyle* derivedStyle = m_dataSet.Style(SignalRef{request.fileIndex, newIndex}))
|
||||||
|
{
|
||||||
|
derivedStyle->gain = inheritedTransform.gain;
|
||||||
|
derivedStyle->absolute = inheritedTransform.absolute;
|
||||||
|
derivedStyle->mirrorVertical = inheritedTransform.mirrorVertical;
|
||||||
|
derivedStyle->mirrorHorizontal = inheritedTransform.mirrorHorizontal;
|
||||||
|
derivedStyle->offsetY = inheritedTransform.offsetY;
|
||||||
|
derivedStyle->offsetTime = inheritedTransform.offsetTime;
|
||||||
|
}
|
||||||
|
|
||||||
// 4. 在当前标签页中勾选新信号,并把它所在的网格移到原信号网格的下方。
|
// 4. 在当前标签页中勾选新信号,并把它所在的网格移到原信号网格的下方。
|
||||||
if (makeVisible && !m_tabs.empty())
|
if (makeVisible && !m_tabs.empty())
|
||||||
{
|
{
|
||||||
@@ -2748,6 +2829,12 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
case WM_TIMER:
|
case WM_TIMER:
|
||||||
{
|
{
|
||||||
|
// 标题栏内存占用:只改窗口标题,不动客户区,因此无需重绘。
|
||||||
|
if (wParam == kRamTimerId)
|
||||||
|
{
|
||||||
|
UpdateWindowTitle();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
// 提示浮层淡出:仅重绘,不再做其它处理;Render 会在淡出结束后停表。
|
// 提示浮层淡出:仅重绘,不再做其它处理;Render 会在淡出结束后停表。
|
||||||
if (wParam == kHintTimerId)
|
if (wParam == kHintTimerId)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -291,6 +291,13 @@ namespace jdi
|
|||||||
* 仅在横向放大到采样点间距足够大(可精确分辨)时生效,
|
* 仅在横向放大到采样点间距足够大(可精确分辨)时生效,
|
||||||
* 避免缩小时光标出现“抖动”难以微调。
|
* 避免缩小时光标出现“抖动”难以微调。
|
||||||
*
|
*
|
||||||
|
* ⚠️ 吸附有**上下两个门限**:
|
||||||
|
* * 下门限:采样点间距 ≥ `kCursorSnapMinPixels`(缩小时不吸附,否则难以微调);
|
||||||
|
* * 上门限:视界内至少能容纳 **2 个**采样点(`pixels <= plotWidth / 2`)。
|
||||||
|
* 视界比采样间隔还窄时“最近采样点”在视界之外,吸附会把光标一把甩到视界边缘
|
||||||
|
* 并钉住(表现为“一拖光标就飞到视界边缘、跟不动鼠标”)。
|
||||||
|
* * 并且只接受**落在当前可见区间内**的采样点。
|
||||||
|
*
|
||||||
* @param [IN] time 原始时间。
|
* @param [IN] time 原始时间。
|
||||||
* @param [OUT] snapped 吸附后的时间。
|
* @param [OUT] snapped 吸附后的时间。
|
||||||
* @return 成功吸附返回 true。
|
* @return 成功吸附返回 true。
|
||||||
@@ -925,6 +932,49 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
void ClampCursorToView(int which);
|
void ClampCursorToView(int which);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算当前数据下**最小可用的时间跨度**(时间视界的下限)。
|
||||||
|
*
|
||||||
|
* 下限由两件事共同决定:
|
||||||
|
* * 数据本身:`数据跨度 × 1e-9`(与滚轮缩放一致)——小于它就是把视界缩进采样间隔内部;
|
||||||
|
* * `double` 的可表示精度:视界宽度小于 `ULP(时间) × 画布宽度` 时,
|
||||||
|
* 「像素 → 时间」的映射会退化(相邻像素映射到同一个 double),
|
||||||
|
* 鼠标每动一下只能落在少数几个可表示的时间值上,
|
||||||
|
* 表现为**光标跟不上鼠标、一拖就跨几百像素或直接贴到视界边缘**。
|
||||||
|
*
|
||||||
|
* @param [IN] low 视界下限(用于估算时间量级)。
|
||||||
|
* @param [IN] high 视界上限。
|
||||||
|
* @return 最小可用跨度。
|
||||||
|
*/
|
||||||
|
double MinimumTimeSpan(double low, double high) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief **时间视界的唯一写入入口**(所有不变量的守门人)。
|
||||||
|
*
|
||||||
|
* 负责:拒绝非有限值、纠正左右颠倒、把跨度抬到 `MinimumTimeSpan` 之上、
|
||||||
|
* 以及最后按数据范围做边界收敛。
|
||||||
|
*
|
||||||
|
* ⚠️ 缩放(滚轮 / 选区放大)、平移、适应视图都必须经过这里:
|
||||||
|
* 过去每个路径各自维护“跨度下限”,新增的路径(如选区放大)漏掉了这件事,
|
||||||
|
* 于是“能一直放大到视界只有几个 ULP”这类问题反复出现。
|
||||||
|
*
|
||||||
|
* @param [IN] low 新的视界下限。
|
||||||
|
* @param [IN] high 新的视界上限。
|
||||||
|
*/
|
||||||
|
void SetTimeView(double low, double high);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief **光标时间位置的统一写入入口**(拖拽光标时必须经由此处)。
|
||||||
|
*
|
||||||
|
* 负责:拒绝非有限值、按 `SnapTimeToSample` 吸附加视界内采样点、
|
||||||
|
* 以及把结果夹回可见区间。三步缺一不可——光标必须在**可见区间内**且与鼠标位置对应,
|
||||||
|
* 否则用户看到的是“拖拽时光标自己飞到视界边缘”。
|
||||||
|
*
|
||||||
|
* @param [IN] which 光标序号(1 或 2)。
|
||||||
|
* @param [IN] time 目标时间(鼠标位置换算)。
|
||||||
|
*/
|
||||||
|
void PlaceCursorTime(int which, double time);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 计算双光标之间的时间差(绝对值)。
|
* @brief 计算双光标之间的时间差(绝对值)。
|
||||||
* @return 时间差;非双光标模式返回 0。
|
* @return 时间差;非双光标模式返回 0。
|
||||||
@@ -1443,9 +1493,11 @@ namespace jdi
|
|||||||
* @param [IN] count 可用采样点数。
|
* @param [IN] count 可用采样点数。
|
||||||
* @param [OUT] begin 区间起点(含)。
|
* @param [OUT] begin 区间起点(含)。
|
||||||
* @param [OUT] end 区间终点(不含)。
|
* @param [OUT] end 区间终点(不含)。
|
||||||
|
* @param [IN] timeShift 信号的时间偏移:采样画在 `t + timeShift` 处,因此可见采样对应的
|
||||||
|
* 源时刻区间为 `[ViewMin - timeShift, ViewMax - timeShift]`(偏移 0 时同视界)。
|
||||||
*/
|
*/
|
||||||
void VisibleIndexRange(const DataFile& file, std::size_t count, std::size_t& begin,
|
void VisibleIndexRange(const DataFile& file, std::size_t count, std::size_t& begin,
|
||||||
std::size_t& end) const;
|
std::size_t& end, double timeShift = 0.0) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 绘制某条信号的多次迭代均值滤波曲线(与中心平均值曲线同一配色派生规则)。
|
* @brief 绘制某条信号的多次迭代均值滤波曲线(与中心平均值曲线同一配色派生规则)。
|
||||||
@@ -1838,6 +1890,12 @@ namespace jdi
|
|||||||
float laneHeight = 0.0f; /**< 网格高度。 */
|
float laneHeight = 0.0f; /**< 网格高度。 */
|
||||||
double laneYMin = 0.0; /**< 网格幅值下界。 */
|
double laneYMin = 0.0; /**< 网格幅值下界。 */
|
||||||
double laneYMax = 0.0; /**< 网格幅值上界。 */
|
double laneYMax = 0.0; /**< 网格幅值上界。 */
|
||||||
|
float gain = 1.0f; /**< 绘制前的系数。 */
|
||||||
|
bool absolute = false; /**< 绘制前是否取绝对值。 */
|
||||||
|
bool mirrorVertical = false; /**< 绘制前是否按 Y=0 上下镜像。 */
|
||||||
|
bool mirrorHorizontal = false; /**< 是否水平镜像(取值下标反序)。 */
|
||||||
|
float offsetY = 0.0f; /**< Y 轴偏移。 */
|
||||||
|
double offsetTime = 0.0; /**< 时间轴偏移(秒)。 */
|
||||||
std::vector<gd::PointF> points; /**< 折线顶点。 */
|
std::vector<gd::PointF> points; /**< 折线顶点。 */
|
||||||
std::vector<std::size_t> breaks; /**< 每段折线在 points 中的起始下标。 */
|
std::vector<std::size_t> breaks; /**< 每段折线在 points 中的起始下标。 */
|
||||||
|
|
||||||
@@ -2001,12 +2059,22 @@ namespace jdi
|
|||||||
float opacity = 1.0f; /**< 不透明度。 */
|
float opacity = 1.0f; /**< 不透明度。 */
|
||||||
float lineWidth = 1.0f; /**< 线宽。 */
|
float lineWidth = 1.0f; /**< 线宽。 */
|
||||||
int mode = 0; /**< 绘制模式。 */
|
int mode = 0; /**< 绘制模式。 */
|
||||||
|
float gain = 1.0f; /**< 系数。 */
|
||||||
|
bool absolute = false; /**< 是否取绝对值。 */
|
||||||
|
bool mirrorVertical = false; /**< 是否按 Y=0 上下镜像。 */
|
||||||
|
bool mirrorHorizontal = false; /**< 是否水平镜像(数据反序)。 */
|
||||||
|
float offsetY = 0.0f; /**< Y 轴偏移。 */
|
||||||
|
double offsetTime = 0.0; /**< 时间轴偏移(秒)。 */
|
||||||
|
|
||||||
/** @brief 相等比较(用于判断"逐信号外观样式是否变化")。 */
|
/** @brief 相等比较(用于判断"逐信号外观样式是否变化")。 */
|
||||||
bool operator==(const SceneStyleEntry& other) const
|
bool operator==(const SceneStyleEntry& other) const
|
||||||
{
|
{
|
||||||
return color == other.color && opacity == other.opacity &&
|
return color == other.color && opacity == other.opacity &&
|
||||||
lineWidth == other.lineWidth && mode == other.mode;
|
lineWidth == other.lineWidth && mode == other.mode &&
|
||||||
|
gain == other.gain && absolute == other.absolute &&
|
||||||
|
mirrorVertical == other.mirrorVertical &&
|
||||||
|
mirrorHorizontal == other.mirrorHorizontal && offsetY == other.offsetY &&
|
||||||
|
offsetTime == other.offsetTime;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "PlotView.hpp"
|
#include "PlotView.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -205,13 +205,15 @@ namespace jdi
|
|||||||
* @param [IN] time 时间轴。
|
* @param [IN] time 时间轴。
|
||||||
* @param [IN] values 采样值。
|
* @param [IN] values 采样值。
|
||||||
* @param [IN] count 可用采样点数量。
|
* @param [IN] count 可用采样点数量。
|
||||||
|
* @param [IN] style 显示外观(水平镜像时按**源下标反序**取值)。
|
||||||
* @param [IN] firstIndex 第一个时间不小于查询时刻的采样下标(相交线段为 firstIndex-1 → firstIndex)。
|
* @param [IN] firstIndex 第一个时间不小于查询时刻的采样下标(相交线段为 firstIndex-1 → firstIndex)。
|
||||||
* @param [IN] t 查询时刻。
|
* @param [IN] t 查询时刻。
|
||||||
* @param [OUT] out 输出取值。
|
* @param [OUT] out 输出取值。
|
||||||
* @return 折线覆盖该时刻且取到有效值返回 true。
|
* @return 折线覆盖该时刻且取到有效值返回 true。
|
||||||
*/
|
*/
|
||||||
bool PolylineValueAt(const std::vector<double>& time, const std::vector<double>& values,
|
bool PolylineValueAt(const std::vector<double>& time, const std::vector<double>& values,
|
||||||
std::size_t count, std::size_t firstIndex, double t, double& out)
|
std::size_t count, const SignalStyle& style, std::size_t firstIndex,
|
||||||
|
double t, double& out)
|
||||||
{
|
{
|
||||||
if (firstIndex == 0 || firstIndex >= count)
|
if (firstIndex == 0 || firstIndex >= count)
|
||||||
{
|
{
|
||||||
@@ -220,8 +222,9 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
const double t0 = time[firstIndex - 1];
|
const double t0 = time[firstIndex - 1];
|
||||||
const double t1 = time[firstIndex];
|
const double t1 = time[firstIndex];
|
||||||
const double v0 = values[firstIndex - 1];
|
// 水平镜像时画面上的相邻两点取源数组中头尾对调后的两个采样。
|
||||||
const double v1 = values[firstIndex];
|
const double v0 = values[SourceIndexFor(style, count, firstIndex - 1)];
|
||||||
|
const double v1 = values[SourceIndexFor(style, count, firstIndex)];
|
||||||
if (!std::isfinite(t0) || !std::isfinite(t1) || !std::isfinite(v0) || !std::isfinite(v1) ||
|
if (!std::isfinite(t0) || !std::isfinite(t1) || !std::isfinite(v0) || !std::isfinite(v1) ||
|
||||||
!(t1 > t0))
|
!(t1 > t0))
|
||||||
{
|
{
|
||||||
@@ -498,143 +501,6 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 时间间隔的显示单位。
|
|
||||||
*/
|
|
||||||
struct DurationUnit
|
|
||||||
{
|
|
||||||
double scale = 1.0; /**< 1 个该单位等于多少秒。 */
|
|
||||||
const wchar_t* name = L"s"; /**< 单位名称。 */
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 把时间间隔格式化为"Δt 语义"的复合单位文本。
|
|
||||||
*
|
|
||||||
* 规则(用户明确要求):
|
|
||||||
* - 顶级单位高于 s(即 h / min)时**一路整合到 s**,例如 3662s → "1h 1min 2s";
|
|
||||||
* - 顶级单位为 s 及以下时,若不是该单位的整数,则**向下一级递进**,
|
|
||||||
* 例如 5.06s → "5s 60ms";整秒数不加下级单位,例如 5s → "5s";
|
|
||||||
* - 顶级单位为 ps 时**不再递进次级单位**,例如 5.123ps → "5.123 ps"。
|
|
||||||
*
|
|
||||||
* @param [IN] seconds 时间间隔(秒)。
|
|
||||||
* @return 不含 "Δt" 前缀的间隔文本。
|
|
||||||
*/
|
|
||||||
std::wstring FormatDurationCompound(double seconds)
|
|
||||||
{
|
|
||||||
if (!std::isfinite(seconds))
|
|
||||||
{
|
|
||||||
return L"—";
|
|
||||||
}
|
|
||||||
const double value = std::fabs(seconds);
|
|
||||||
if (value <= 0.0)
|
|
||||||
{
|
|
||||||
return L"0s";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 顶级单位高于 s(h / min):整合为 h/min/s(整数秒)----
|
|
||||||
// 注意条件必须是 >= 60s:1~60s 属于"顶级单位为 s"的情况,需按下一条规则处理
|
|
||||||
// (非整数秒要递进到 ms,例如 5.06s → "5s 60ms")。
|
|
||||||
if (value >= 60.0)
|
|
||||||
{
|
|
||||||
const long long totalSeconds = static_cast<long long>(std::llround(value));
|
|
||||||
long long hours = 0;
|
|
||||||
long long minutes = 0;
|
|
||||||
long long secs = totalSeconds;
|
|
||||||
if (totalSeconds >= 3600)
|
|
||||||
{
|
|
||||||
hours = totalSeconds / 3600;
|
|
||||||
secs = totalSeconds % 3600;
|
|
||||||
}
|
|
||||||
if (secs >= 60)
|
|
||||||
{
|
|
||||||
minutes = secs / 60;
|
|
||||||
secs = secs % 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::wstring text;
|
|
||||||
const auto append = [&text](long long amount, const wchar_t* unit)
|
|
||||||
{
|
|
||||||
if (amount <= 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!text.empty())
|
|
||||||
{
|
|
||||||
text += L" ";
|
|
||||||
}
|
|
||||||
text += std::to_wstring(amount);
|
|
||||||
text += unit;
|
|
||||||
};
|
|
||||||
append(hours, L"h");
|
|
||||||
append(minutes, L"min");
|
|
||||||
append(secs, L"s");
|
|
||||||
if (!text.empty())
|
|
||||||
{
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 顶级单位为 s:整数秒 + 可选 ms ----
|
|
||||||
if (value >= 1.0)
|
|
||||||
{
|
|
||||||
const double whole = std::floor(value);
|
|
||||||
const double fraction = value - whole;
|
|
||||||
std::wstring text = FormatValue(whole) + L"s";
|
|
||||||
const double remainderMs = fraction * 1000.0;
|
|
||||||
if (remainderMs >= 1.0e-6)
|
|
||||||
{
|
|
||||||
text += L" ";
|
|
||||||
text += FormatValue(remainderMs);
|
|
||||||
text += L"ms";
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 顶级单位低于 s:ms / us / ns / ps ----
|
|
||||||
const DurationUnit subUnits[] = {
|
|
||||||
{1.0e-3, L"ms"}, {1.0e-6, L"us"}, {1.0e-9, L"ns"}, {1.0e-12, L"ps"},
|
|
||||||
};
|
|
||||||
const std::size_t subCount = sizeof(subUnits) / sizeof(subUnits[0]);
|
|
||||||
std::size_t index = subCount - 1;
|
|
||||||
for (std::size_t i = 0; i < subCount; ++i)
|
|
||||||
{
|
|
||||||
if (value >= subUnits[i].scale)
|
|
||||||
{
|
|
||||||
index = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 顶级单位为 ps:不再递进次级单位(保留小数)。
|
|
||||||
if (index + 1 >= subCount)
|
|
||||||
{
|
|
||||||
return FormatValue(value / subUnits[index].scale) + L" " + subUnits[index].name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const double scaled = value / subUnits[index].scale;
|
|
||||||
double whole = std::floor(scaled);
|
|
||||||
double fraction = scaled - whole;
|
|
||||||
// 浮点误差可能导致 fraction 极接近 1(例如 4.9999999),归一化一下。
|
|
||||||
if (fraction >= 1.0 - 1.0e-9)
|
|
||||||
{
|
|
||||||
whole += 1.0;
|
|
||||||
fraction = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::wstring text = FormatValue(whole);
|
|
||||||
text += subUnits[index].name;
|
|
||||||
// 仍是该单位的整数 → 不显示下级单位。
|
|
||||||
const double remainder = fraction * subUnits[index].scale;
|
|
||||||
const double inNext = remainder / subUnits[index + 1].scale;
|
|
||||||
if (inNext >= 1.0e-6)
|
|
||||||
{
|
|
||||||
text += L" ";
|
|
||||||
text += FormatValue(inNext);
|
|
||||||
text += subUnits[index + 1].name;
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 双光标差值的完整显示文本("Δt:" + 复合单位间隔)。
|
* @brief 双光标差值的完整显示文本("Δt:" + 复合单位间隔)。
|
||||||
* @param [IN] seconds 时间间隔(秒)。
|
* @param [IN] seconds 时间间隔(秒)。
|
||||||
@@ -659,23 +525,6 @@ namespace jdi
|
|||||||
return FormatValue(seconds) + L"s";
|
return FormatValue(seconds) + L"s";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 把时间间隔格式化为"秒"数值文本(不含单位),用于内联编辑。
|
|
||||||
*
|
|
||||||
* 编辑时的量级**始终是秒**:即使显示为 "1.5min",双击编辑的内容也是 90。
|
|
||||||
*
|
|
||||||
* @param [IN] seconds 时间间隔(秒)。
|
|
||||||
* @return 形如 "60" 的文本。
|
|
||||||
*/
|
|
||||||
std::wstring FormatDurationSeconds(double seconds)
|
|
||||||
{
|
|
||||||
if (!std::isfinite(seconds))
|
|
||||||
{
|
|
||||||
return L"0";
|
|
||||||
}
|
|
||||||
return FormatValue(std::fabs(seconds));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 按比例缩放颜色的 alpha 通道(用于提示浮层淡出)。
|
* @brief 按比例缩放颜色的 alpha 通道(用于提示浮层淡出)。
|
||||||
* @param [IN] argb 原始 0xAARRGGBB 颜色。
|
* @param [IN] argb 原始 0xAARRGGBB 颜色。
|
||||||
@@ -1027,12 +876,13 @@ namespace jdi
|
|||||||
double PlotView::XToTime(float x) const
|
double PlotView::XToTime(float x) const
|
||||||
{
|
{
|
||||||
const double viewMin = Tab().ViewMin();
|
const double viewMin = Tab().ViewMin();
|
||||||
if (m_plotRect.Width <= 0.0f)
|
const double span = Tab().ViewMax() - viewMin;
|
||||||
|
if (m_plotRect.Width <= 0.0f || !(span > 0.0))
|
||||||
{
|
{
|
||||||
return viewMin;
|
return viewMin;
|
||||||
}
|
}
|
||||||
const double ratio = static_cast<double>(x - m_plotRect.X) / static_cast<double>(m_plotRect.Width);
|
const double ratio = static_cast<double>(x - m_plotRect.X) / static_cast<double>(m_plotRect.Width);
|
||||||
return viewMin + ratio * (Tab().ViewMax() - viewMin);
|
return viewMin + ratio * span;
|
||||||
}
|
}
|
||||||
|
|
||||||
float PlotView::ValueToY(double value, const LaneVisual& lane) const
|
float PlotView::ValueToY(double value, const LaneVisual& lane) const
|
||||||
@@ -1105,9 +955,57 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double PlotView::MinimumTimeSpan(double low, double high) const
|
||||||
|
{
|
||||||
|
double dataMin = 0.0;
|
||||||
|
double dataMax = 1.0;
|
||||||
|
TimeBounds(dataMin, dataMax);
|
||||||
|
const double dataSpan = std::max(dataMax - dataMin, kMinTimeSpan);
|
||||||
|
|
||||||
|
// double 在“时间量级”上的最小刻度(ULP):视界宽度低于 `ULP × 画布宽度` 时,
|
||||||
|
// 一列像素对应的 double 已经无法区分,映射彻底退化。
|
||||||
|
const double magnitude = std::max(std::fabs(low), std::fabs(high));
|
||||||
|
const double ulp =
|
||||||
|
std::nextafter(magnitude, std::numeric_limits<double>::infinity()) - magnitude;
|
||||||
|
const double width = std::max(64.0, static_cast<double>(m_plotRect.Width));
|
||||||
|
|
||||||
|
return std::max(dataSpan * 1.0e-9, std::max(ulp, kMinTimeSpan) * width);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotView::SetTimeView(double low, double high)
|
||||||
|
{
|
||||||
|
if (!std::isfinite(low) || !std::isfinite(high))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (high < low)
|
||||||
|
{
|
||||||
|
std::swap(low, high);
|
||||||
|
}
|
||||||
|
|
||||||
|
const double minimum = MinimumTimeSpan(low, high);
|
||||||
|
if (!(high - low >= minimum))
|
||||||
|
{
|
||||||
|
// 低于下限:以中心为锚点对称展开(而不是静默地放大到不可表示)。
|
||||||
|
const double center = (low + high) * 0.5;
|
||||||
|
low = center - minimum * 0.5;
|
||||||
|
high = center + minimum * 0.5;
|
||||||
|
if (!(high - low > 0.0))
|
||||||
|
{
|
||||||
|
// 连下限都无法表示(时间量级远超可分辨范围):放弃这次修改,
|
||||||
|
// 保留上一个仍然可用的视界比制造一个空视界更安全。
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Tab().ViewMin() = low;
|
||||||
|
Tab().ViewMax() = high;
|
||||||
|
ClampTimeView();
|
||||||
|
}
|
||||||
|
|
||||||
void PlotView::ZoomTimeAt(double factor, double anchorTime)
|
void PlotView::ZoomTimeAt(double factor, double anchorTime)
|
||||||
{
|
{
|
||||||
if (m_dataSet == nullptr || m_dataSet->Empty() || !(factor > 0.0))
|
if (m_dataSet == nullptr || m_dataSet->Empty() || !(factor > 0.0) || !std::isfinite(anchorTime))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1118,12 +1016,18 @@ namespace jdi
|
|||||||
const double dataSpan = std::max(dataMax - dataMin, kMinTimeSpan);
|
const double dataSpan = std::max(dataMax - dataMin, kMinTimeSpan);
|
||||||
|
|
||||||
const double span = Tab().ViewMax() - Tab().ViewMin();
|
const double span = Tab().ViewMax() - Tab().ViewMin();
|
||||||
double newSpan = ClampDouble(span / factor, dataSpan * 1.0e-9, dataSpan * 100.0);
|
if (!(span > 0.0))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 下限与选区放大共用同一个来源(见 `MinimumTimeSpan`),
|
||||||
|
// 否则“滚轮缩放有限制、选区放大没有”会让视界退化到不可用。
|
||||||
|
const double minimum = MinimumTimeSpan(Tab().ViewMin(), Tab().ViewMax());
|
||||||
|
const double newSpan = ClampDouble(span / factor, minimum, dataSpan * 100.0);
|
||||||
|
|
||||||
const double ratio = (span > 0.0) ? (anchorTime - Tab().ViewMin()) / span : 0.5;
|
const double ratio = (anchorTime - Tab().ViewMin()) / span;
|
||||||
Tab().ViewMin() = anchorTime - ratio * newSpan;
|
const double low = anchorTime - ratio * newSpan;
|
||||||
Tab().ViewMax() = Tab().ViewMin() + newSpan;
|
SetTimeView(low, low + newSpan);
|
||||||
ClampTimeView();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlotView::ZoomTimeStep(double factor)
|
void PlotView::ZoomTimeStep(double factor)
|
||||||
@@ -1136,9 +1040,7 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
const double span = Tab().ViewMax() - Tab().ViewMin();
|
const double span = Tab().ViewMax() - Tab().ViewMin();
|
||||||
const double delta = span * fraction;
|
const double delta = span * fraction;
|
||||||
Tab().ViewMin() += delta;
|
SetTimeView(Tab().ViewMin() + delta, Tab().ViewMax() + delta);
|
||||||
Tab().ViewMax() += delta;
|
|
||||||
ClampTimeView();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlotView::ScrollLanesByFraction(double fraction)
|
void PlotView::ScrollLanesByFraction(double fraction)
|
||||||
@@ -1152,8 +1054,7 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
if (m_dataSet == nullptr || m_dataSet->Empty())
|
if (m_dataSet == nullptr || m_dataSet->Empty())
|
||||||
{
|
{
|
||||||
Tab().ViewMin() = 0.0;
|
SetTimeView(0.0, 1.0);
|
||||||
Tab().ViewMax() = 1.0;
|
|
||||||
Tab().ViewInitialized() = true;
|
Tab().ViewInitialized() = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1168,8 +1069,7 @@ namespace jdi
|
|||||||
high += padding;
|
high += padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
Tab().ViewMin() = low;
|
SetTimeView(low, high);
|
||||||
Tab().ViewMax() = high;
|
|
||||||
Tab().ViewInitialized() = true;
|
Tab().ViewInitialized() = true;
|
||||||
Tab().LaneScroll() = 0.0f;
|
Tab().LaneScroll() = 0.0f;
|
||||||
// 只放下进行中的选区与拖拽,**不改变「选区放大」等缩放模式的启用状态**
|
// 只放下进行中的选区与拖拽,**不改变「选区放大」等缩放模式的启用状态**
|
||||||
@@ -1458,8 +1358,7 @@ namespace jdi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Tab().ViewMin() = low;
|
SetTimeView(low, high);
|
||||||
Tab().ViewMax() = high;
|
|
||||||
Tab().ViewInitialized() = true;
|
Tab().ViewInitialized() = true;
|
||||||
// 与“适应视图”保持一致:退出选区放大模式,避免模式按钮与实际状态不符。
|
// 与“适应视图”保持一致:退出选区放大模式,避免模式按钮与实际状态不符。
|
||||||
Tab().ZoomMode() = PlotZoomMode::NONE;
|
Tab().ZoomMode() = PlotZoomMode::NONE;
|
||||||
@@ -1555,6 +1454,12 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const double minPixels = static_cast<double>(kCursorSnapMinPixels) * static_cast<double>(m_scale);
|
const double minPixels = static_cast<double>(kCursorSnapMinPixels) * static_cast<double>(m_scale);
|
||||||
|
// 吸附的上门限:视界内至少要放得下 2 个采样点。
|
||||||
|
// 视界比采样间隔还窄时(选区放大很容易做到),"最近采样点"落在视界之外或半个视界之外,
|
||||||
|
// 吸附会把光标一把甩到视界边缘并钉住 —— 用户报告的"拖光标直接飞到视界外"。
|
||||||
|
const double maxPixels = 0.5 * static_cast<double>(m_plotRect.Width);
|
||||||
|
const double viewMin = Tab().ViewMin();
|
||||||
|
const double viewMax = Tab().ViewMax();
|
||||||
|
|
||||||
bool found = false;
|
bool found = false;
|
||||||
double bestDistance = std::numeric_limits<double>::infinity();
|
double bestDistance = std::numeric_limits<double>::infinity();
|
||||||
@@ -1564,7 +1469,7 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
// 只有采样点在屏幕上足够稀疏时才吸附,否则缩小视图后光标会“跳格”。
|
// 只有采样点在屏幕上足够稀疏时才吸附,否则缩小视图后光标会“跳格”。
|
||||||
double pixels = 0.0;
|
double pixels = 0.0;
|
||||||
if (!SampleSpacingPixels(file, time, pixels) || pixels < minPixels)
|
if (!SampleSpacingPixels(file, time, pixels) || pixels < minPixels || pixels > maxPixels)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1579,6 +1484,11 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// 只吸附**当前可见区间内**的采样点:视界之外的采样点不是合法的落点。
|
||||||
|
if (candidate < viewMin || candidate > viewMax)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const double distance = std::fabs(candidate - time);
|
const double distance = std::fabs(candidate - time);
|
||||||
if (distance < bestDistance)
|
if (distance < bestDistance)
|
||||||
@@ -1596,6 +1506,32 @@ namespace jdi
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlotView::PlaceCursorTime(int which, double time)
|
||||||
|
{
|
||||||
|
if (!std::isfinite(time))
|
||||||
|
{
|
||||||
|
// NaN/Inf 会让后面所有比较都失效(`ClampDouble` 也拦不住 NaN),
|
||||||
|
// 光标会被画到画布之外:直接丢弃这次位置更新。
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double snapped = 0.0;
|
||||||
|
if (SnapTimeToSample(time, snapped))
|
||||||
|
{
|
||||||
|
time = snapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (which == 2)
|
||||||
|
{
|
||||||
|
Tab().CursorTimeAlt() = time;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Tab().CursorTime() = time;
|
||||||
|
}
|
||||||
|
ClampCursorToView(which);
|
||||||
|
}
|
||||||
|
|
||||||
PlotZoomMode PlotView::CurrentZoomMode() const
|
PlotZoomMode PlotView::CurrentZoomMode() const
|
||||||
{
|
{
|
||||||
return Tab().ZoomMode();
|
return Tab().ZoomMode();
|
||||||
@@ -1779,9 +1715,11 @@ namespace jdi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Tab().ViewMin() = timeA;
|
// 统一入口:内含最小跨度下限。
|
||||||
Tab().ViewMax() = timeB;
|
// ⚠️ 选区放大过去**没有**这个下限,可以一直放大到视界宽度只有几个 double ULP;
|
||||||
ClampTimeView();
|
// 到那时「像素 → 时间」已经退化(相邻像素映射到同一个 double),
|
||||||
|
// 用户看到的就是“拖光标跟不动、一下跳到视界边缘/几百像素外”。
|
||||||
|
SetTimeView(timeA, timeB);
|
||||||
// 注意:不在这里移动光标——即使选区不包含光标,也只把它的读数贴在网格边缘显示。
|
// 注意:不在这里移动光标——即使选区不包含光标,也只把它的读数贴在网格边缘显示。
|
||||||
|
|
||||||
// 「选区 Y 轴自适应」开关开启的网格:时间选区放大后自动把 Y 轴匹配到新的可见区间。
|
// 「选区 Y 轴自适应」开关开启的网格:时间选区放大后自动把 Y 轴匹配到新的可见区间。
|
||||||
@@ -2201,7 +2139,7 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
void PlotView::VisibleIndexRange(const DataFile& file, std::size_t count, std::size_t& begin,
|
void PlotView::VisibleIndexRange(const DataFile& file, std::size_t count, std::size_t& begin,
|
||||||
std::size_t& end) const
|
std::size_t& end, double timeShift) const
|
||||||
{
|
{
|
||||||
begin = 0;
|
begin = 0;
|
||||||
end = count;
|
end = count;
|
||||||
@@ -2209,15 +2147,16 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 时间偏移:采样画在 `t + timeShift` 处,因此落在当前视界内的采样其源时刻区间要整体左移。
|
||||||
const std::vector<double>& time = file.time;
|
const std::vector<double>& time = file.time;
|
||||||
begin = static_cast<std::size_t>(
|
begin = static_cast<std::size_t>(
|
||||||
std::lower_bound(time.begin(), time.end(), Tab().ViewMin()) - time.begin());
|
std::lower_bound(time.begin(), time.end(), Tab().ViewMin() - timeShift) - time.begin());
|
||||||
if (begin > 0)
|
if (begin > 0)
|
||||||
{
|
{
|
||||||
--begin;
|
--begin;
|
||||||
}
|
}
|
||||||
end = static_cast<std::size_t>(
|
end = static_cast<std::size_t>(
|
||||||
std::lower_bound(time.begin(), time.end(), Tab().ViewMax()) - time.begin());
|
std::lower_bound(time.begin(), time.end(), Tab().ViewMax() - timeShift) - time.begin());
|
||||||
if (end < count)
|
if (end < count)
|
||||||
{
|
{
|
||||||
++end;
|
++end;
|
||||||
@@ -3199,19 +3138,24 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::size_t count = std::min(file->time.size(), signal->values.size());
|
const std::size_t count = std::min(file->time.size(), signal->values.size());
|
||||||
|
const SignalStyle* style = m_dataSet->Style(ref);
|
||||||
|
// 时间偏移:可见采样对应的源时刻区间要整体左移(画在 t + shift 处)。
|
||||||
|
const double timeShift = (style != nullptr) ? style->offsetTime : 0.0;
|
||||||
|
const double sourceMin = viewMin - timeShift;
|
||||||
|
const double sourceMax = viewMax - timeShift;
|
||||||
std::size_t begin = 0;
|
std::size_t begin = 0;
|
||||||
std::size_t end = count;
|
std::size_t end = count;
|
||||||
if (file->timeMonotonic)
|
if (file->timeMonotonic)
|
||||||
{
|
{
|
||||||
// 时间单调时直接二分定位区间,百万级采样也只需扫过可见段。
|
// 时间单调时直接二分定位区间,百万级采样也只需扫过可见段。
|
||||||
begin = static_cast<std::size_t>(
|
begin = static_cast<std::size_t>(
|
||||||
std::lower_bound(file->time.begin(), file->time.end(), viewMin) - file->time.begin());
|
std::lower_bound(file->time.begin(), file->time.end(), sourceMin) - file->time.begin());
|
||||||
if (begin > 0)
|
if (begin > 0)
|
||||||
{
|
{
|
||||||
--begin;
|
--begin;
|
||||||
}
|
}
|
||||||
end = static_cast<std::size_t>(
|
end = static_cast<std::size_t>(
|
||||||
std::lower_bound(file->time.begin(), file->time.end(), viewMax) - file->time.begin());
|
std::lower_bound(file->time.begin(), file->time.end(), sourceMax) - file->time.begin());
|
||||||
if (end < count)
|
if (end < count)
|
||||||
{
|
{
|
||||||
++end;
|
++end;
|
||||||
@@ -3223,12 +3167,22 @@ namespace jdi
|
|||||||
if (!file->timeMonotonic)
|
if (!file->timeMonotonic)
|
||||||
{
|
{
|
||||||
const double t = file->time[index];
|
const double t = file->time[index];
|
||||||
if (!std::isfinite(t) || t < viewMin || t > viewMax)
|
if (!std::isfinite(t) || t < sourceMin || t > sourceMax)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const double value = signal->values[index];
|
// ⚠️ 自适应必须基于**画出来的曲线**:套用与绘制完全相同的变换
|
||||||
|
//(系数 → 绝对值 → 镜像),否则调整系数/镜像后自适应范围会按原始数据算错
|
||||||
|
//(用户报告"自适应区域错误,看上去还是按照原始数据来的")。
|
||||||
|
// 水平镜像时画面上的第 index 个位置取源下标 count-1-index 的值。
|
||||||
|
const std::size_t sourceIndex = SourceIndexFor(*m_dataSet->Style(ref), count, index);
|
||||||
|
const double raw = signal->values[sourceIndex];
|
||||||
|
if (!std::isfinite(raw))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const double value = ApplyDrawTransform(*m_dataSet->Style(ref), raw);
|
||||||
if (std::isfinite(value))
|
if (std::isfinite(value))
|
||||||
{
|
{
|
||||||
low = std::min(low, value);
|
low = std::min(low, value);
|
||||||
@@ -4266,27 +4220,13 @@ namespace jdi
|
|||||||
case PlotDragMode::CURSOR_MOVE:
|
case PlotDragMode::CURSOR_MOVE:
|
||||||
{
|
{
|
||||||
// 从时间读数标签上抓起时保留横向抓取偏移,避免一按下光标就跳到鼠标中心。
|
// 从时间读数标签上抓起时保留横向抓取偏移,避免一按下光标就跳到鼠标中心。
|
||||||
double target = XToTime(fx - static_cast<float>(m_cursorGrabOffset));
|
const double target = XToTime(fx - static_cast<float>(m_cursorGrabOffset));
|
||||||
double snapped = target;
|
|
||||||
if (SnapTimeToSample(target, snapped))
|
|
||||||
{
|
|
||||||
target = snapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool second =
|
const bool second =
|
||||||
(m_hoverCursor == 2 && Tab().CursorMode() == PlotCursorMode::DOUBLE);
|
(m_hoverCursor == 2 && Tab().CursorMode() == PlotCursorMode::DOUBLE);
|
||||||
if (second)
|
// 吸附 + 夹取统一在 `PlaceCursorTime` 里完成(光标位置不变量只在那里维护):
|
||||||
{
|
// * 吸附只接受可见区间内、且视界内还能分辨的采样点;
|
||||||
Tab().CursorTimeAlt() = target;
|
// * 结果必定落在当前可见区间内(否则用户看到的是一拖就"飞到视界边缘")。
|
||||||
}
|
PlaceCursorTime(second ? 2 : 1, target);
|
||||||
else
|
|
||||||
{
|
|
||||||
Tab().CursorTime() = target;
|
|
||||||
}
|
|
||||||
// 抓取读数标签时可能带有一个较大的横向抓取偏移(放大到一定程度后该偏移
|
|
||||||
// 换算成的时间可能落到视界之外)——拖拽结束前必须把光标夹回当前可见区间,
|
|
||||||
// 否则用户会看到光标线"跑到视界外"而无法继续操作。
|
|
||||||
ClampCursorToView(second ? 2 : 1);
|
|
||||||
m_dragMoved = true;
|
m_dragMoved = true;
|
||||||
m_cursorHover = true;
|
m_cursorHover = true;
|
||||||
return true;
|
return true;
|
||||||
@@ -4654,22 +4594,17 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 靠近时间光标线时优先进入拖动,避免误触发平移或缩放选区。
|
// 图例与 GX 编号徽标叠在波形之上、且直接位于网格顶部——用户明确要求它们**优先响应**:
|
||||||
{
|
// 必须先于「时间光标线 / 光标读数 / 边沿计数横向光标 / 选区放大」判定,否则:
|
||||||
int cursorWhich = 0;
|
// * 光标线恰好经过徽标/图例时,按下会变成拖光标;
|
||||||
if (IsOnCursorLine(x, cursorWhich) && UiPointInRect(m_area, fx, fy))
|
// * 选区放大模式下会变成“开始框选”,用户看到的就是“GX 拖不动、图例点不中”。
|
||||||
{
|
// 层序上仍然让位于浮动面板(面板绘制在徽标与图例之上),因此先挡一次面板命中。
|
||||||
m_dragMode = PlotDragMode::CURSOR_MOVE;
|
int guardLane = -1;
|
||||||
m_hoverCursor = cursorWhich;
|
int guardKind = 0;
|
||||||
// 点中的光标成为“活动光标”:它触发的内容将画在上层。
|
const bool guardPanel = TopPanelAtPoint(fx, fy, guardLane, guardKind);
|
||||||
m_activeCursor = cursorWhich;
|
|
||||||
m_cursorHover = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 网格次序拖拽:按在图例的 GX 编号徽标上即可整块拎起该网格上下移动。
|
// 网格次序拖拽:按在图例的 GX 编号徽标上即可整块拎起该网格上下移动。
|
||||||
// 位置在浮动面板之后(面板压在波形之上,应优先命中),但在信号拖拽之前。
|
if (!guardPanel)
|
||||||
{
|
{
|
||||||
const int badgeLane = LaneBadgeAtPoint(x, y);
|
const int badgeLane = LaneBadgeAtPoint(x, y);
|
||||||
if (badgeLane >= 0)
|
if (badgeLane >= 0)
|
||||||
@@ -4686,6 +4621,36 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 点击图例条目等价于在左侧列表中选中该信号(样式区随之切换目标)。
|
||||||
|
// **必须在选区放大判定之前**:选区放大模式只改变拖拽语义,
|
||||||
|
// 不应影响图例的悬停与点选(否则用户在选区放大模式下无法切换信号)。
|
||||||
|
if (!guardPanel)
|
||||||
|
{
|
||||||
|
const SignalRef legendHit = LegendAtPoint(x, y);
|
||||||
|
if (legendHit.IsValid())
|
||||||
|
{
|
||||||
|
SelectSignal(legendHit);
|
||||||
|
m_dragMode = PlotDragMode::NONE;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 靠近时间光标线时进入拖动。
|
||||||
|
// (放在徽标/图例之后:两者重叠时徽标与图例优先,与用户要求一致。)
|
||||||
|
if (!guardPanel)
|
||||||
|
{
|
||||||
|
int cursorWhich = 0;
|
||||||
|
if (IsOnCursorLine(x, cursorWhich) && UiPointInRect(m_area, fx, fy))
|
||||||
|
{
|
||||||
|
m_dragMode = PlotDragMode::CURSOR_MOVE;
|
||||||
|
m_hoverCursor = cursorWhich;
|
||||||
|
// 点中的光标成为“活动光标”:它触发的内容将画在上层。
|
||||||
|
m_activeCursor = cursorWhich;
|
||||||
|
m_cursorHover = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 实验性功能浮动面板(面板压在波形之上,必须先于信号拖拽判定):
|
// 实验性功能浮动面板(面板压在波形之上,必须先于信号拖拽判定):
|
||||||
// * ✕ → 关闭该网格对应的功能;
|
// * ✕ → 关闭该网格对应的功能;
|
||||||
// * 逐条目参数输入框 → 进入内联编辑;
|
// * 逐条目参数输入框 → 进入内联编辑;
|
||||||
@@ -4976,19 +4941,6 @@ namespace jdi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击图例条目等价于在左侧列表中选中该信号(样式区随之切换目标)。
|
|
||||||
// **必须在选区放大判定之前**:选区放大模式只改变拖拽语义,
|
|
||||||
// 不应影响图例的悬停与点选(否则用户在选区放大模式下无法切换信号)。
|
|
||||||
{
|
|
||||||
const SignalRef legendHit = LegendAtPoint(x, y);
|
|
||||||
if (legendHit.IsValid())
|
|
||||||
{
|
|
||||||
SelectSignal(legendHit);
|
|
||||||
m_dragMode = PlotDragMode::NONE;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const PlotZoomMode mode = Tab().ZoomMode();
|
const PlotZoomMode mode = Tab().ZoomMode();
|
||||||
if (mode == PlotZoomMode::SELECT)
|
if (mode == PlotZoomMode::SELECT)
|
||||||
{
|
{
|
||||||
@@ -5330,6 +5282,13 @@ namespace jdi
|
|||||||
const std::size_t sampleCount = std::min(resolved.file->time.size(), resolved.data->values.size());
|
const std::size_t sampleCount = std::min(resolved.file->time.size(), resolved.data->values.size());
|
||||||
const double viewMin = Tab().ViewMin();
|
const double viewMin = Tab().ViewMin();
|
||||||
const double viewMax = Tab().ViewMax();
|
const double viewMax = Tab().ViewMax();
|
||||||
|
// 绘制前的数值变换(系数/绝对值/镜像/偏移)会改变顶点,必须进缓存键。
|
||||||
|
const float gain = (resolved.style != nullptr) ? resolved.style->gain : 1.0f;
|
||||||
|
const bool absolute = (resolved.style != nullptr) && resolved.style->absolute;
|
||||||
|
const bool mirrorVertical = (resolved.style != nullptr) && resolved.style->mirrorVertical;
|
||||||
|
const bool mirrorHorizontal = (resolved.style != nullptr) && resolved.style->mirrorHorizontal;
|
||||||
|
const float offsetY = (resolved.style != nullptr) ? resolved.style->offsetY : 0.0f;
|
||||||
|
const double offsetTime = (resolved.style != nullptr) ? resolved.style->offsetTime : 0.0;
|
||||||
|
|
||||||
for (const PolylineCacheEntry& entry : m_polylineCache)
|
for (const PolylineCacheEntry& entry : m_polylineCache)
|
||||||
{
|
{
|
||||||
@@ -5337,7 +5296,9 @@ namespace jdi
|
|||||||
entry.viewMin == viewMin && entry.viewMax == viewMax && entry.plotX == m_plotRect.X &&
|
entry.viewMin == viewMin && entry.viewMax == viewMax && entry.plotX == m_plotRect.X &&
|
||||||
entry.plotWidth == m_plotRect.Width && entry.laneY == lane.rect.Y &&
|
entry.plotWidth == m_plotRect.Width && entry.laneY == lane.rect.Y &&
|
||||||
entry.laneHeight == lane.rect.Height && entry.laneYMin == lane.yMin &&
|
entry.laneHeight == lane.rect.Height && entry.laneYMin == lane.yMin &&
|
||||||
entry.laneYMax == lane.yMax)
|
entry.laneYMax == lane.yMax && entry.gain == gain && entry.absolute == absolute &&
|
||||||
|
entry.mirrorVertical == mirrorVertical && entry.mirrorHorizontal == mirrorHorizontal &&
|
||||||
|
entry.offsetY == offsetY && entry.offsetTime == offsetTime)
|
||||||
{
|
{
|
||||||
return &entry;
|
return &entry;
|
||||||
}
|
}
|
||||||
@@ -5362,6 +5323,12 @@ namespace jdi
|
|||||||
entry.laneHeight = lane.rect.Height;
|
entry.laneHeight = lane.rect.Height;
|
||||||
entry.laneYMin = lane.yMin;
|
entry.laneYMin = lane.yMin;
|
||||||
entry.laneYMax = lane.yMax;
|
entry.laneYMax = lane.yMax;
|
||||||
|
entry.gain = gain;
|
||||||
|
entry.absolute = absolute;
|
||||||
|
entry.mirrorVertical = mirrorVertical;
|
||||||
|
entry.mirrorHorizontal = mirrorHorizontal;
|
||||||
|
entry.offsetY = offsetY;
|
||||||
|
entry.offsetTime = offsetTime;
|
||||||
|
|
||||||
BuildPolyline(resolved, lane, entry.points, entry.breaks, entry.band, entry.bandBreaks,
|
BuildPolyline(resolved, lane, entry.points, entry.breaks, entry.band, entry.bandBreaks,
|
||||||
entry.bandMode, entry.bandSpiky);
|
entry.bandMode, entry.bandSpiky);
|
||||||
@@ -5397,18 +5364,24 @@ namespace jdi
|
|||||||
const double viewMin = Tab().ViewMin();
|
const double viewMin = Tab().ViewMin();
|
||||||
const double viewMax = Tab().ViewMax();
|
const double viewMax = Tab().ViewMax();
|
||||||
|
|
||||||
|
// 时间偏移:采样画在 `t + offsetTime` 处,因此“落在当前视界内的采样”
|
||||||
|
// 其源时刻位于 `[viewMin - offsetTime, viewMax - offsetTime]`。
|
||||||
|
const double timeShift = resolved.style->offsetTime;
|
||||||
|
const double sourceMin = viewMin - timeShift;
|
||||||
|
const double sourceMax = viewMax - timeShift;
|
||||||
|
|
||||||
std::size_t begin = 0;
|
std::size_t begin = 0;
|
||||||
std::size_t end = count;
|
std::size_t end = count;
|
||||||
|
|
||||||
if (resolved.file->timeMonotonic)
|
if (resolved.file->timeMonotonic)
|
||||||
{
|
{
|
||||||
const std::vector<double>::const_iterator first = std::lower_bound(time.begin(), time.end(), viewMin);
|
const std::vector<double>::const_iterator first = std::lower_bound(time.begin(), time.end(), sourceMin);
|
||||||
begin = static_cast<std::size_t>(first - time.begin());
|
begin = static_cast<std::size_t>(first - time.begin());
|
||||||
if (begin > 0)
|
if (begin > 0)
|
||||||
{
|
{
|
||||||
--begin;
|
--begin;
|
||||||
}
|
}
|
||||||
const std::vector<double>::const_iterator last = std::lower_bound(time.begin(), time.end(), viewMax);
|
const std::vector<double>::const_iterator last = std::lower_bound(time.begin(), time.end(), sourceMax);
|
||||||
end = static_cast<std::size_t>(last - time.begin());
|
end = static_cast<std::size_t>(last - time.begin());
|
||||||
if (end < count)
|
if (end < count)
|
||||||
{
|
{
|
||||||
@@ -5462,8 +5435,9 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
const float marginPx =
|
const float marginPx =
|
||||||
std::max(4.0f, std::max(1.0f, resolved.style->lineWidth) * m_scale + 3.0f);
|
std::max(4.0f, std::max(1.0f, resolved.style->lineWidth) * m_scale + 3.0f);
|
||||||
const double ta = XToTime(m_stripX0 - marginPx);
|
// 条带边界是**绘制时刻**,换算成源时刻要减去时间偏移。
|
||||||
const double tb = XToTime(m_stripX1 + marginPx);
|
const double ta = XToTime(m_stripX0 - marginPx) - timeShift;
|
||||||
|
const double tb = XToTime(m_stripX1 + marginPx) - timeShift;
|
||||||
const double clipLo = std::min(ta, tb);
|
const double clipLo = std::min(ta, tb);
|
||||||
const double clipHi = std::max(ta, tb);
|
const double clipHi = std::max(ta, tb);
|
||||||
std::size_t sBegin = static_cast<std::size_t>(
|
std::size_t sBegin = static_cast<std::size_t>(
|
||||||
@@ -5511,8 +5485,16 @@ namespace jdi
|
|||||||
const std::vector<double>& coarseMin = resolved.data->coarseMin;
|
const std::vector<double>& coarseMin = resolved.data->coarseMin;
|
||||||
const std::vector<double>& coarseMax = resolved.data->coarseMax;
|
const std::vector<double>& coarseMax = resolved.data->coarseMax;
|
||||||
const std::size_t coarseStride = resolved.data->coarseStride;
|
const std::size_t coarseStride = resolved.data->coarseStride;
|
||||||
|
// ⚠️ 粗粒度分组只存了逐组的 min/max:取绝对值 / 垂直镜像会把两者互换,
|
||||||
|
// 此时分组层无法再给出正确的包络,必须回退到逐采样点变换(见 `DrawTransformOrderPreserving`)。
|
||||||
|
// Y 偏移是保序的(整体加常数),因此仍可用分组层 —— 只需把偏移一并加上。
|
||||||
const bool hasCoarse =
|
const bool hasCoarse =
|
||||||
(coarseStride > 0) && !coarseMin.empty() && (coarseMin.size() == coarseMax.size());
|
(coarseStride > 0) && !coarseMin.empty() && (coarseMin.size() == coarseMax.size()) &&
|
||||||
|
DrawTransformOrderPreserving(*resolved.style);
|
||||||
|
// 保序变换(正系数 + Y 偏移)下:分组极值 × 系数 + 偏移 即为变换后的极值
|
||||||
|
//(系数 1、偏移 0 时逐位不变)。
|
||||||
|
const double coarseScale = static_cast<double>(resolved.style->gain);
|
||||||
|
const double coarseOffset = static_cast<double>(resolved.style->offsetY);
|
||||||
|
|
||||||
bool inSegment = false;
|
bool inSegment = false;
|
||||||
|
|
||||||
@@ -5535,21 +5517,24 @@ namespace jdi
|
|||||||
std::size_t searchFrom = begin;
|
std::size_t searchFrom = begin;
|
||||||
for (std::size_t column = 0; column < columns; ++column)
|
for (std::size_t column = 0; column < columns; ++column)
|
||||||
{
|
{
|
||||||
|
// `t0/t1` 是**绘制时刻**的列边界;源时刻要减去时间偏移(偏移为 0 时二者相同)。
|
||||||
const double t0 = viewMin + viewSpan * (static_cast<double>(column) / static_cast<double>(columns));
|
const double t0 = viewMin + viewSpan * (static_cast<double>(column) / static_cast<double>(columns));
|
||||||
const double t1 =
|
const double t1 =
|
||||||
viewMin + viewSpan * (static_cast<double>(column + 1) / static_cast<double>(columns));
|
viewMin + viewSpan * (static_cast<double>(column + 1) / static_cast<double>(columns));
|
||||||
|
const double s0 = t0 - timeShift;
|
||||||
|
const double s1 = t1 - timeShift;
|
||||||
|
|
||||||
// 列边界随 column 单调递增,从上一列的位置继续二分,
|
// 列边界随 column 单调递增,从上一列的位置继续二分,
|
||||||
// 把每次搜索的范围压到剩余区间,避免 2400 次全区间二分带来的缓存缺失。
|
// 把每次搜索的范围压到剩余区间,避免 2400 次全区间二分带来的缓存缺失。
|
||||||
const std::size_t i0 = static_cast<std::size_t>(
|
const std::size_t i0 = static_cast<std::size_t>(
|
||||||
std::lower_bound(time.begin() + static_cast<std::ptrdiff_t>(searchFrom), rangeEnd, t0) -
|
std::lower_bound(time.begin() + static_cast<std::ptrdiff_t>(searchFrom), rangeEnd, s0) -
|
||||||
time.begin());
|
time.begin());
|
||||||
searchFrom = i0;
|
searchFrom = i0;
|
||||||
const std::size_t i1 = (column + 1 == columns)
|
const std::size_t i1 = (column + 1 == columns)
|
||||||
? end
|
? end
|
||||||
: static_cast<std::size_t>(
|
: static_cast<std::size_t>(
|
||||||
std::lower_bound(time.begin() + static_cast<std::ptrdiff_t>(i0),
|
std::lower_bound(time.begin() + static_cast<std::ptrdiff_t>(i0),
|
||||||
rangeEnd, t1) -
|
rangeEnd, s1) -
|
||||||
time.begin());
|
time.begin());
|
||||||
|
|
||||||
double low = std::numeric_limits<double>::infinity();
|
double low = std::numeric_limits<double>::infinity();
|
||||||
@@ -5559,15 +5544,20 @@ namespace jdi
|
|||||||
bool broken = false;
|
bool broken = false;
|
||||||
|
|
||||||
// (一)落在本列区间内的采样点。
|
// (一)落在本列区间内的采样点。
|
||||||
|
// 水平镜像时取的是**源下标**(区间同样反转),因此先把列区间映射到源下标区间。
|
||||||
|
const std::size_t srcA = std::min(SourceIndexFor(*resolved.style, count, i0),
|
||||||
|
SourceIndexFor(*resolved.style, count, (i1 > 0) ? (i1 - 1) : 0));
|
||||||
|
const std::size_t srcB = std::max(SourceIndexFor(*resolved.style, count, i0),
|
||||||
|
SourceIndexFor(*resolved.style, count, (i1 > 0) ? (i1 - 1) : 0));
|
||||||
if (i1 > i0 && hasCoarse && (i1 - i0) >= 2 * coarseStride)
|
if (i1 > i0 && hasCoarse && (i1 - i0) >= 2 * coarseStride)
|
||||||
{
|
{
|
||||||
const std::size_t firstBucket = i0 / coarseStride;
|
const std::size_t firstBucket = srcA / coarseStride;
|
||||||
const std::size_t lastBucket =
|
const std::size_t lastBucket =
|
||||||
std::min((i1 - 1) / coarseStride, coarseMin.size() - 1);
|
std::min(srcB / coarseStride, coarseMin.size() - 1);
|
||||||
for (std::size_t bucket = firstBucket; bucket <= lastBucket; ++bucket)
|
for (std::size_t bucket = firstBucket; bucket <= lastBucket; ++bucket)
|
||||||
{
|
{
|
||||||
const double bucketLow = coarseMin[bucket];
|
const double bucketLow = coarseMin[bucket] * coarseScale + coarseOffset;
|
||||||
const double bucketHigh = coarseMax[bucket];
|
const double bucketHigh = coarseMax[bucket] * coarseScale + coarseOffset;
|
||||||
if (!std::isfinite(bucketLow))
|
if (!std::isfinite(bucketLow))
|
||||||
{
|
{
|
||||||
// 该分组全部是无效值:包络到此断开,后续另起一段。
|
// 该分组全部是无效值:包络到此断开,后续另起一段。
|
||||||
@@ -5588,9 +5578,9 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
else if (i1 > i0)
|
else if (i1 > i0)
|
||||||
{
|
{
|
||||||
for (std::size_t index = i0; index < i1; ++index)
|
for (std::size_t index = srcA; index <= srcB; ++index)
|
||||||
{
|
{
|
||||||
const double value = values[index];
|
const double value = ApplyDrawTransform(*resolved.style, values[index]);
|
||||||
if (!std::isfinite(value))
|
if (!std::isfinite(value))
|
||||||
{
|
{
|
||||||
// 该列含无效值:包络绘制到本列结束,后续另起一段。
|
// 该列含无效值:包络绘制到本列结束,后续另起一段。
|
||||||
@@ -5622,30 +5612,33 @@ namespace jdi
|
|||||||
// 其极值必然出现在采样点或列边界上,因此「列内采样点 + 两侧边界取值」就是
|
// 其极值必然出现在采样点或列边界上,因此「列内采样点 + 两侧边界取值」就是
|
||||||
// 该列包络的精确解,且相邻两列在边界处取到同一个值,包络带严丝合缝。
|
// 该列包络的精确解,且相邻两列在边界处取到同一个值,包络带严丝合缝。
|
||||||
double boundary = 0.0;
|
double boundary = 0.0;
|
||||||
if (PolylineValueAt(time, values, count, i0, t0, boundary))
|
if (PolylineValueAt(time, values, count, *resolved.style, i0, s0, boundary))
|
||||||
{
|
{
|
||||||
|
boundary = ApplyDrawTransform(*resolved.style, boundary);
|
||||||
if (boundary < low)
|
if (boundary < low)
|
||||||
{
|
{
|
||||||
low = boundary;
|
low = boundary;
|
||||||
lowOrder = (i0 > 0) ? (i0 - 1) : 0;
|
lowOrder = (i0 > 0) ? SourceIndexFor(*resolved.style, count, i0 - 1) : 0;
|
||||||
}
|
}
|
||||||
if (boundary > high)
|
if (boundary > high)
|
||||||
{
|
{
|
||||||
high = boundary;
|
high = boundary;
|
||||||
highOrder = (i0 > 0) ? (i0 - 1) : 0;
|
highOrder = (i0 > 0) ? SourceIndexFor(*resolved.style, count, i0 - 1) : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (PolylineValueAt(time, values, count, i1, t1, boundary))
|
if (PolylineValueAt(time, values, count, *resolved.style, i1, s1, boundary))
|
||||||
{
|
{
|
||||||
|
boundary = ApplyDrawTransform(*resolved.style, boundary);
|
||||||
|
const std::size_t order = SourceIndexFor(*resolved.style, count, (i1 > 0) ? (i1 - 1) : 0);
|
||||||
if (boundary < low)
|
if (boundary < low)
|
||||||
{
|
{
|
||||||
low = boundary;
|
low = boundary;
|
||||||
lowOrder = i1;
|
lowOrder = order;
|
||||||
}
|
}
|
||||||
if (boundary > high)
|
if (boundary > high)
|
||||||
{
|
{
|
||||||
high = boundary;
|
high = boundary;
|
||||||
highOrder = i1;
|
highOrder = order;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5722,14 +5715,17 @@ namespace jdi
|
|||||||
for (std::size_t index = begin; index < end; index += stride)
|
for (std::size_t index = begin; index < end; index += stride)
|
||||||
{
|
{
|
||||||
const double t = time[index];
|
const double t = time[index];
|
||||||
const double value = values[index];
|
// 水平镜像 = 数据反序:画面上第 index 个位置取源下标 count-1-index 的值。
|
||||||
|
const double value = ApplyDrawTransform(
|
||||||
|
*resolved.style, values[SourceIndexFor(*resolved.style, count, index)]);
|
||||||
if (!std::isfinite(t) || !std::isfinite(value))
|
if (!std::isfinite(t) || !std::isfinite(value))
|
||||||
{
|
{
|
||||||
inSegment = false;
|
inSegment = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float x = TimeToX(t);
|
// 时间偏移:该采样画在 `t + offsetTime` 处(例如 t=1s、偏移 -0.5s → 画在 0.5s)。
|
||||||
|
const float x = TimeToX(t + timeShift);
|
||||||
float y = ValueToY(value, lane);
|
float y = ValueToY(value, lane);
|
||||||
y = static_cast<float>(ClampDouble(y, limitTop, limitBottom));
|
y = static_cast<float>(ClampDouble(y, limitTop, limitBottom));
|
||||||
|
|
||||||
@@ -6386,6 +6382,14 @@ namespace jdi
|
|||||||
mixFloat(resolved.style->opacity);
|
mixFloat(resolved.style->opacity);
|
||||||
mixFloat(resolved.style->lineWidth);
|
mixFloat(resolved.style->lineWidth);
|
||||||
mix(static_cast<std::uint64_t>(resolved.style->mode));
|
mix(static_cast<std::uint64_t>(resolved.style->mode));
|
||||||
|
// 绘制前的数值变换(系数 / 绝对值 / 镜像)会改变波形形状。
|
||||||
|
mixFloat(resolved.style->gain);
|
||||||
|
mix(resolved.style->absolute ? 1ull : 0ull);
|
||||||
|
mix(resolved.style->mirrorVertical ? 1ull : 0ull);
|
||||||
|
mix(resolved.style->mirrorHorizontal ? 1ull : 0ull);
|
||||||
|
// 偏移(Y 平移 + 时间平移)同样会改变波形。
|
||||||
|
mixFloat(resolved.style->offsetY);
|
||||||
|
mixFloat(static_cast<float>(resolved.style->offsetTime));
|
||||||
}
|
}
|
||||||
// 图例文案参与绘制,重命名必须让缓存失效。
|
// 图例文案参与绘制,重命名必须让缓存失效。
|
||||||
mixText(resolved.data->Title());
|
mixText(resolved.data->Title());
|
||||||
@@ -6431,6 +6435,12 @@ namespace jdi
|
|||||||
entry.opacity = resolved.style->opacity;
|
entry.opacity = resolved.style->opacity;
|
||||||
entry.lineWidth = resolved.style->lineWidth;
|
entry.lineWidth = resolved.style->lineWidth;
|
||||||
entry.mode = static_cast<int>(resolved.style->mode);
|
entry.mode = static_cast<int>(resolved.style->mode);
|
||||||
|
entry.gain = resolved.style->gain;
|
||||||
|
entry.absolute = resolved.style->absolute;
|
||||||
|
entry.mirrorVertical = resolved.style->mirrorVertical;
|
||||||
|
entry.mirrorHorizontal = resolved.style->mirrorHorizontal;
|
||||||
|
entry.offsetY = resolved.style->offsetY;
|
||||||
|
entry.offsetTime = resolved.style->offsetTime;
|
||||||
}
|
}
|
||||||
lane.push_back(entry);
|
lane.push_back(entry);
|
||||||
}
|
}
|
||||||
@@ -7289,6 +7299,14 @@ namespace jdi
|
|||||||
mixFloat(resolved.style->opacity);
|
mixFloat(resolved.style->opacity);
|
||||||
mixFloat(resolved.style->lineWidth);
|
mixFloat(resolved.style->lineWidth);
|
||||||
mix(static_cast<std::uint64_t>(resolved.style->mode));
|
mix(static_cast<std::uint64_t>(resolved.style->mode));
|
||||||
|
// 「调整」变换会改变图层里的波形,也必须让图层失效
|
||||||
|
//(否则贴回的旧像素会与当前样式不一致)。
|
||||||
|
mixFloat(resolved.style->gain);
|
||||||
|
mix(resolved.style->absolute ? 1ull : 0ull);
|
||||||
|
mix(resolved.style->mirrorVertical ? 1ull : 0ull);
|
||||||
|
mix(resolved.style->mirrorHorizontal ? 1ull : 0ull);
|
||||||
|
mixFloat(resolved.style->offsetY);
|
||||||
|
mixFloat(static_cast<float>(resolved.style->offsetTime));
|
||||||
}
|
}
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
@@ -8851,7 +8869,7 @@ namespace jdi
|
|||||||
int visibleCount = 0;
|
int visibleCount = 0;
|
||||||
int drawnCount = 0;
|
int drawnCount = 0;
|
||||||
|
|
||||||
// 先测量图例总宽度,再用半透明底板掠开底下的网格线,提升可读性。
|
// 先测量图例总宽度:只用来确定装饰缓存矩形(不再绘制任何托底/底板)。
|
||||||
float legendWidth = 0.0f;
|
float legendWidth = 0.0f;
|
||||||
for (const SignalRef& ref : laneSignals)
|
for (const SignalRef& ref : laneSignals)
|
||||||
{
|
{
|
||||||
@@ -8877,23 +8895,24 @@ namespace jdi
|
|||||||
legendWidth += itemWidth;
|
legendWidth += itemWidth;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
// 底板从编号徽标开始一直盖到最后一个图例条目(编号徽标一并被掠开底下的网格线)。
|
// 底板范围(编号徽标 + 图例条目):这里**只记录装饰矩形**用于缓存贴回。
|
||||||
const float plateRight = cursorX + legendWidth + (legendWidth > 0.0f ? 2.0f * m_scale : 0.0f);
|
const float plateRight = cursorX + legendWidth + (legendWidth > 0.0f ? 2.0f * m_scale : 0.0f);
|
||||||
// 记录图例装饰矩形(含编号徽标):从徽标左缘到图例右缘。
|
|
||||||
{
|
{
|
||||||
const float left = std::min(badgeRect.X, legendBand.X - 4.0f * m_scale) - 2.0f * m_scale;
|
const float left = std::min(badgeRect.X, legendBand.X - 4.0f * m_scale) - 2.0f * m_scale;
|
||||||
const float right = std::max(badgeRect.X + badgeRect.Width, plateRight) + 2.0f * m_scale;
|
const float right = std::max(badgeRect.X + badgeRect.Width, plateRight) + 2.0f * m_scale;
|
||||||
m_decorRects.push_back(gd::RectF(left, top - 4.0f * m_scale, right - left,
|
m_decorRects.push_back(gd::RectF(left, top - 4.0f * m_scale, right - left,
|
||||||
height + 8.0f * m_scale));
|
height + 8.0f * m_scale));
|
||||||
}
|
}
|
||||||
// 命中装饰缓存:整块图例(底板 + 徽标 + 色块 + 文字)改由装饰层贴回,跳过重绘。
|
// ⚠️ **不再绘制不透明托底**(用户要求去掉):原来的底板会把徽标与图例下方的
|
||||||
|
// 网格线整块擦掉,使这一条看起来像一个"可点击的控件",而它右侧的空白区域
|
||||||
|
// 实际并不响应点击(选区放大模式下会变成开始框选)。现在图例与徽标直接叠在
|
||||||
|
// 网格线之上,命中范围与看到的控件一一对应。
|
||||||
|
//
|
||||||
|
// 命中装饰缓存:整块图例(徽标 + 色块 + 文字)改由装饰层贴回,跳过重绘。
|
||||||
if (m_useDecorCache)
|
if (m_useDecorCache)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UiFillRoundRect(g, gd::RectF(legendBand.X - 4.0f * m_scale, top - 2.0f * m_scale,
|
|
||||||
plateRight - legendBand.X + 4.0f * m_scale, height + 4.0f * m_scale),
|
|
||||||
3.0f * m_scale, m_theme->cardBg | 0xFF000000u);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 编号徽标绘制在底板之上(与信号面板的 GX 徽标使用同一套配色与圆角)。
|
// 编号徽标绘制在底板之上(与信号面板的 GX 徽标使用同一套配色与圆角)。
|
||||||
@@ -9362,10 +9381,13 @@ namespace jdi
|
|||||||
|
|
||||||
const bool interpolate = (resolved.style->mode != DrawMode::SCATTER);
|
const bool interpolate = (resolved.style->mode != DrawMode::SCATTER);
|
||||||
double value = 0.0;
|
double value = 0.0;
|
||||||
if (!SampleSignalAt(*resolved.file, *resolved.data, time, interpolate, value))
|
if (!SampleDisplayedSignalAt(*resolved.file, *resolved.data, *resolved.style, time,
|
||||||
|
interpolate, value))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// 取值读数必须与画出来的波形一致:同样套用系数/绝对值/镜像。
|
||||||
|
value = ApplyDrawTransform(*resolved.style, value);
|
||||||
|
|
||||||
const float rawY = ValueToY(value, lane);
|
const float rawY = ValueToY(value, lane);
|
||||||
// 上下越界的数据点不再被丢弃:与左右越界一致,贴边绘制指示方向的三角。
|
// 上下越界的数据点不再被丢弃:与左右越界一致,贴边绘制指示方向的三角。
|
||||||
@@ -9386,8 +9408,10 @@ namespace jdi
|
|||||||
if (showDelta)
|
if (showDelta)
|
||||||
{
|
{
|
||||||
double otherValue = 0.0;
|
double otherValue = 0.0;
|
||||||
if (SampleSignalAt(*resolved.file, *resolved.data, otherTime, interpolate, otherValue))
|
if (SampleDisplayedSignalAt(*resolved.file, *resolved.data, *resolved.style, otherTime,
|
||||||
|
interpolate, otherValue))
|
||||||
{
|
{
|
||||||
|
otherValue = ApplyDrawTransform(*resolved.style, otherValue);
|
||||||
const double delta = value - otherValue;
|
const double delta = value - otherValue;
|
||||||
// 增量 = 右侧光标取值 − 左侧光标取值,包在圆括号里并按差值显式给出正负号。
|
// 增量 = 右侧光标取值 − 左侧光标取值,包在圆括号里并按差值显式给出正负号。
|
||||||
text += L" (";
|
text += L" (";
|
||||||
@@ -10266,7 +10290,7 @@ namespace jdi
|
|||||||
|
|
||||||
std::size_t begin = 0;
|
std::size_t begin = 0;
|
||||||
std::size_t end = 0;
|
std::size_t end = 0;
|
||||||
VisibleIndexRange(*resolved.file, count, begin, end);
|
VisibleIndexRange(*resolved.file, count, begin, end, resolved.style->offsetTime);
|
||||||
if (end <= begin)
|
if (end <= begin)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -10307,20 +10331,23 @@ namespace jdi
|
|||||||
|
|
||||||
for (std::size_t index = begin; index < end; index += stride)
|
for (std::size_t index = begin; index < end; index += stride)
|
||||||
{
|
{
|
||||||
if (index < curveBegin || (index - curveBegin) >= curve.size())
|
// 水平镜像 = 数据反序:遍历下标映射到源下标后再取曲线值。
|
||||||
|
const std::size_t sourceIndex = SourceIndexFor(*resolved.style, count, index);
|
||||||
|
if (sourceIndex < curveBegin || (sourceIndex - curveBegin) >= curve.size())
|
||||||
{
|
{
|
||||||
inSegment = false;
|
inSegment = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const float sampled = curve[index - curveBegin];
|
const float sampled = curve[sourceIndex - curveBegin];
|
||||||
if (!std::isfinite(sampled))
|
if (!std::isfinite(sampled))
|
||||||
{
|
{
|
||||||
inSegment = false;
|
inSegment = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
float y = ValueToY(static_cast<double>(sampled), lane);
|
float y = ValueToY(ApplyDrawTransform(*resolved.style, static_cast<double>(sampled)), lane);
|
||||||
y = static_cast<float>(ClampDouble(y, limitTop, limitBottom));
|
y = static_cast<float>(ClampDouble(y, limitTop, limitBottom));
|
||||||
const float x = TimeToX(time[index]);
|
// 时间偏移:该采样画在 `t + offsetTime` 处。
|
||||||
|
const float x = TimeToX(time[index] + resolved.style->offsetTime);
|
||||||
if (!inSegment)
|
if (!inSegment)
|
||||||
{
|
{
|
||||||
breaks.push_back(points.size());
|
breaks.push_back(points.size());
|
||||||
|
|||||||
@@ -34,7 +34,14 @@ namespace jdi
|
|||||||
CMD_COLOR_BASE = 100, /**< 预设颜色基准(配合色块下标)。 */
|
CMD_COLOR_BASE = 100, /**< 预设颜色基准(配合色块下标)。 */
|
||||||
CMD_MODE_BASE = 200, /**< 绘制样式基准(配合下标)。 */
|
CMD_MODE_BASE = 200, /**< 绘制样式基准(配合下标)。 */
|
||||||
CMD_LANE_BASE = 300, /**< 网格归属基准(配合下标)。 */
|
CMD_LANE_BASE = 300, /**< 网格归属基准(配合下标)。 */
|
||||||
CMD_LANE_NEW = 400 /**< 新建网格。 */
|
CMD_LANE_NEW = 400, /**< 新建网格。 */
|
||||||
|
CMD_GAIN_EDIT = 500, /**< 「系数」二级面板里的系数输入框(幂等提交)。 */
|
||||||
|
CMD_GAIN_RESET = 501, /**< 「系数调整」标题栏右侧的还原按钮:把系数重置为 1。 */
|
||||||
|
CMD_OFFSET_RESET = 502, /**< 「偏移调整」标题栏右侧的还原按钮:两个偏移都恢复为默认值。 */
|
||||||
|
CMD_OFFSET_Y = 503, /**< 「偏移调整」:Y 轴偏移输入框。 */
|
||||||
|
CMD_OFFSET_TIME = 504, /**< 「偏移调整」:时间轴偏移输入框。 */
|
||||||
|
CMD_MIRROR_HORIZONTAL = 510, /**< 「镜像」二级面板:水平镜像(数据反序)。 */
|
||||||
|
CMD_MIRROR_VERTICAL = 511 /**< 「镜像」二级面板:垂直镜像(按 Y=0 上下翻转)。 */
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -339,6 +346,9 @@ namespace jdi
|
|||||||
OPACITY_VALUE, /**< 不透明度数值框(可点击输入)。 */
|
OPACITY_VALUE, /**< 不透明度数值框(可点击输入)。 */
|
||||||
WIDTH_SLIDER, /**< 线宽滑块。 */
|
WIDTH_SLIDER, /**< 线宽滑块。 */
|
||||||
WIDTH_VALUE, /**< 线宽数值框(可点击输入)。 */
|
WIDTH_VALUE, /**< 线宽数值框(可点击输入)。 */
|
||||||
|
GAIN_ADJUST, /**< 「系数」按钮(点击展开系数二级面板)。 */
|
||||||
|
MIRROR_ADJUST, /**< 「镜像」按钮(点击展开镜像二级面板)。 */
|
||||||
|
ABSOLUTE_TOGGLE, /**< 「绝对值」切换按钮。 */
|
||||||
TOGGLE_ALL, /**< 全部显示/隐藏按钮。 */
|
TOGGLE_ALL, /**< 全部显示/隐藏按钮。 */
|
||||||
FILTER_CLEAR /**< 「筛选信号」输入框右侧的清空 ✕。 */
|
FILTER_CLEAR /**< 「筛选信号」输入框右侧的清空 ✕。 */
|
||||||
};
|
};
|
||||||
@@ -489,6 +499,35 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
bool OpenColorPickerPopup(const SignalStyle& style);
|
bool OpenColorPickerPopup(const SignalStyle& style);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打开「系数」的**二级面板**(一个整行输入框 + 标题栏右侧的还原按钮)。
|
||||||
|
*
|
||||||
|
* 与自定义颜色入口同一套交互:点击按钮 → 抬起时展开浮动面板 → 面板内的输入框可编辑
|
||||||
|
* 绘制前乘上的系数(默认 1,至少显示两位小数),回车 / 点击其它位置提交,面板保持打开
|
||||||
|
*(与网格 Y 轴上下限的菜单内联输入框一致)。“还原”图标按钮把系数重置为 1。
|
||||||
|
*
|
||||||
|
* @return 面板已打开返回 true。
|
||||||
|
*/
|
||||||
|
bool OpenGainMenu();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打开「镜像」的**二级面板**(两行带复选框的选项:水平镜像 / 垂直镜像)。
|
||||||
|
*
|
||||||
|
* 两项都是“切换项”,点击后勾选状态立即生效且**菜单保持打开**,方便连续勾选;
|
||||||
|
* 只要任一项被勾选,「镜像」按钮即显示为激活态。
|
||||||
|
*
|
||||||
|
* @return 面板已打开返回 true。
|
||||||
|
*/
|
||||||
|
bool OpenMirrorMenu();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 清除样式区的悬停高亮。
|
||||||
|
*
|
||||||
|
* 二级面板(弹出菜单)一打开就会接管鼠标事件,样式区不再收到 `WM_MOUSEMOVE`,
|
||||||
|
* 因此必须在打开面板时主动清掉悬停态,否则该按钮会一直保持"鼠标按住"般的高亮。
|
||||||
|
*/
|
||||||
|
void ClearStyleHover();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 判断当前标签页中全部信号是否均为可见状态。
|
* @brief 判断当前标签页中全部信号是否均为可见状态。
|
||||||
* @return 全部可见且至少有一条信号返回 true。
|
* @return 全部可见且至少有一条信号返回 true。
|
||||||
@@ -592,13 +631,16 @@ namespace jdi
|
|||||||
|
|
||||||
std::array<gd::RectF, 3> m_modeRects; /**< 绘制样式按钮矩形。 */
|
std::array<gd::RectF, 3> m_modeRects; /**< 绘制样式按钮矩形。 */
|
||||||
|
|
||||||
// 样式区固定四行:颜色 / 样式 / 透明度 / 线宽。各行控件共用同一内容列宽度与左边界。
|
// 样式区固定五行:颜色 / 样式 / 调整(系数调整·取绝对值·中心反转)/ 线宽 / 透明度。
|
||||||
std::array<gd::RectF, 4> m_styleRowRects; /**< 四行整体矩形(含标签列)。 */
|
// 各行控件共用同一内容列宽度与左边界。
|
||||||
|
std::array<gd::RectF, 5> m_styleRowRects; /**< 五行整体矩形(含标签列)。 */
|
||||||
|
|
||||||
gd::RectF m_opacityTrackRect; /**< 不透明度滑块轨道矩形。 */
|
gd::RectF m_opacityTrackRect; /**< 不透明度滑块轨道矩形。 */
|
||||||
gd::RectF m_opacityValueRect; /**< 不透明度数值文本矩形。 */
|
gd::RectF m_opacityValueRect; /**< 不透明度数值文本矩形。 */
|
||||||
gd::RectF m_widthTrackRect; /**< 线宽滑块轨道矩形。 */
|
gd::RectF m_widthTrackRect; /**< 线宽滑块轨道矩形。 */
|
||||||
gd::RectF m_widthValueRect; /**< 线宽数值文本矩形(双击可输入)。 */
|
gd::RectF m_widthValueRect; /**< 线宽数值文本矩形(双击可输入)。 */
|
||||||
|
/** 「调整」行的三个等宽按钮:0 = 系数调整,1 = 取绝对值,2 = 中心反转。 */
|
||||||
|
std::array<gd::RectF, 3> m_adjustRects;
|
||||||
|
|
||||||
std::vector<ListRow> m_rows; /**< 列表行集合(惰性缓存)。 */
|
std::vector<ListRow> m_rows; /**< 列表行集合(惰性缓存)。 */
|
||||||
bool m_rowsDirty = true; /**< 列表行集合是否需要重建。 */
|
bool m_rowsDirty = true; /**< 列表行集合是否需要重建。 */
|
||||||
@@ -640,6 +682,8 @@ namespace jdi
|
|||||||
bool m_pendingLaneMenu = false; /**< 左键抬起时是否弹出网格归属菜单。 */
|
bool m_pendingLaneMenu = false; /**< 左键抬起时是否弹出网格归属菜单。 */
|
||||||
SignalRef m_pendingLaneRef; /**< 待弹出菜单的目标信号。 */
|
SignalRef m_pendingLaneRef; /**< 待弹出菜单的目标信号。 */
|
||||||
bool m_pendingColorPicker = false; /**< 左键抬起时是否弹出拾色器。 */
|
bool m_pendingColorPicker = false; /**< 左键抬起时是否弹出拾色器。 */
|
||||||
|
bool m_pendingGainMenu = false; /**< 左键抬起时是否弹出「系数」的二级面板。 */
|
||||||
|
bool m_pendingMirrorMenu = false; /**< 左键抬起时是否弹出「镜像」的二级面板。 */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 待确认的「关闭类」按钮(✕)。
|
* @brief 待确认的「关闭类」按钮(✕)。
|
||||||
|
|||||||
@@ -88,15 +88,15 @@ namespace jdi
|
|||||||
return p == pattern.size();
|
return p == pattern.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 样式编辑区:固定四行网格(颜色 / 样式 / 透明度 / 线宽),行高、间距与内容列宽度统一,
|
// 样式编辑区:固定五行网格(颜色 / 样式 / 调整 / 线宽 / 透明度),
|
||||||
// 保证面板宽度变化时四行控件始终对齐,且不会被无限制拉伸。
|
// 行高、间距与内容列宽度统一,保证面板宽度变化时各行控件始终对齐,且不会被无限制拉伸。
|
||||||
const float kStylePadH = 12.0f; /**< 样式区左右内边距。 */
|
const float kStylePadH = 12.0f; /**< 样式区左右内边距。 */
|
||||||
const float kStylePadV = 12.0f; /**< 样式区上下内边距。 */
|
const float kStylePadV = 12.0f; /**< 样式区上下内边距。 */
|
||||||
const float kStyleRowHeight = 26.0f; /**< 样式区单行高度。 */
|
const float kStyleRowHeight = 26.0f; /**< 样式区单行高度。 */
|
||||||
const float kStyleRowGap = 10.0f; /**< 样式区行间距。 */
|
const float kStyleRowGap = 10.0f; /**< 样式区行间距。 */
|
||||||
const float kStyleLabelWidth = 44.0f; /**< 样式行标签列宽度。 */
|
const float kStyleLabelWidth = 44.0f; /**< 样式行标签列宽度。 */
|
||||||
const float kStyleLabelGap = 8.0f; /**< 标签列与内容列间距。 */
|
const float kStyleLabelGap = 8.0f; /**< 标签列与内容列间距。 */
|
||||||
const float kStyleRowCount = 4.0f; /**< 样式区固定行数。 */
|
const float kStyleRowCount = 5.0f; /**< 样式区固定行数。 */
|
||||||
const float kStyleContentMax = 260.0f; /**< 内容列最大宽度(再拉宽面板也不再延伸)。 */
|
const float kStyleContentMax = 260.0f; /**< 内容列最大宽度(再拉宽面板也不再延伸)。 */
|
||||||
const float kSwatchEdge = 18.0f; /**< 颜色色块边长上限(含自定义颜色入口)。 */
|
const float kSwatchEdge = 18.0f; /**< 颜色色块边长上限(含自定义颜色入口)。 */
|
||||||
const float kSwatchMin = 11.0f; /**< 颜色色块边长下限。 */
|
const float kSwatchMin = 11.0f; /**< 颜色色块边长下限。 */
|
||||||
@@ -105,10 +105,26 @@ namespace jdi
|
|||||||
const float kSliderValueWidth = 52.0f; /**< 滑块行数值文本宽度(透明度与线宽一致)。 */
|
const float kSliderValueWidth = 52.0f; /**< 滑块行数值文本宽度(透明度与线宽一致)。 */
|
||||||
const float kSliderValueGap = 8.0f; /**< 滑块轨道与数值文本间距。 */
|
const float kSliderValueGap = 8.0f; /**< 滑块轨道与数值文本间距。 */
|
||||||
const float kTrackHeight = 6.0f; /**< 滑块轨道高度。 */
|
const float kTrackHeight = 6.0f; /**< 滑块轨道高度。 */
|
||||||
|
const float kAdjustGap = 6.0f; /**< 「调整」行三个按钮之间的间距。 */
|
||||||
|
|
||||||
|
/** 「调整」行的按钮下标(与 `m_adjustRects` 同序)。 */
|
||||||
|
const int kAdjustGain = 0; /**< 系数(点击展开系数二级面板)。 */
|
||||||
|
const int kAdjustMirror = 1; /**< 镜像(点击展开镜像二级面板)。 */
|
||||||
|
const int kAdjustAbsolute = 2; /**< 绝对值。 */
|
||||||
|
const int kAdjustCount = 3; /**< 按钮数量。 */
|
||||||
|
|
||||||
const float kWidthMin = 0.5f; /**< 允许的最小线宽(px)。 */
|
const float kWidthMin = 0.5f; /**< 允许的最小线宽(px)。 */
|
||||||
const float kWidthMax = 10.0f; /**< 允许的最大线宽(px)。 */
|
const float kWidthMax = 10.0f; /**< 允许的最大线宽(px)。 */
|
||||||
|
|
||||||
|
/** @brief 系数调整允许的绝对值上限(防止输入极端值把波形完全挤出可视区)。 */
|
||||||
|
const double kGainLimit = 10000.0;
|
||||||
|
|
||||||
|
/** @brief 系数为默认值 1(不做任何缩放)时按钮显示为未启用样式。 */
|
||||||
|
bool GainIsDefault(float gain)
|
||||||
|
{
|
||||||
|
return std::fabs(gain - 1.0f) < 1.0e-6f;
|
||||||
|
}
|
||||||
|
|
||||||
/** @brief 可选绘制样式顺序(不含阶梯方式)。 */
|
/** @brief 可选绘制样式顺序(不含阶梯方式)。 */
|
||||||
const DrawMode kModeOptions[] = {DrawMode::LINE, DrawMode::SCATTER, DrawMode::LINE_SCATTER};
|
const DrawMode kModeOptions[] = {DrawMode::LINE, DrawMode::SCATTER, DrawMode::LINE_SCATTER};
|
||||||
const int kModeOptionCount = static_cast<int>(sizeof(kModeOptions) / sizeof(kModeOptions[0]));
|
const int kModeOptionCount = static_cast<int>(sizeof(kModeOptions) / sizeof(kModeOptions[0]));
|
||||||
@@ -149,12 +165,59 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return (value < low) ? low : ((value > high) ? high : value);
|
return (value < low) ? low : ((value > high) ? high : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 数值文本:**至少两位小数**,精度更高时按实际精度显示。
|
||||||
|
*
|
||||||
|
* 例:`1` → `1.00`、`2.5` → `2.50`、`0.125` → `0.125`、`1.2345678` → `1.2345678`。
|
||||||
|
* 实现:从 2 位小数开始递增位数,直到文本能精确表示该数值
|
||||||
|
*(容差 1e-7 相对量,与 float 精度匹配)。
|
||||||
|
*
|
||||||
|
* @param [IN] value 数值。
|
||||||
|
* @return 显示文本。
|
||||||
|
*/
|
||||||
|
std::wstring FormatNumberText(double value)
|
||||||
|
{
|
||||||
|
const double tolerance = std::max(1.0e-12, std::fabs(value) * 1.0e-7);
|
||||||
|
for (int decimals = 2; decimals <= 9; ++decimals)
|
||||||
|
{
|
||||||
|
const std::wstring text = jdi::FormatNumber(value, decimals);
|
||||||
|
const double parsed = std::wcstod(text.c_str(), nullptr);
|
||||||
|
if (std::fabs(parsed - value) <= tolerance)
|
||||||
|
{
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jdi::FormatNumber(value, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 带正负符号的数值文本:正数也带 `+`(非编辑态显示偏移量时使用)。
|
||||||
|
* @param [IN] value 数值。
|
||||||
|
* @return 形如 `+1.50` / `-0.25` / `+0.00` 的文本。
|
||||||
|
*/
|
||||||
|
std::wstring FormatSignedNumber(double value)
|
||||||
|
{
|
||||||
|
const std::wstring text = FormatNumberText(std::fabs(value));
|
||||||
|
return ((value < 0.0) ? L"-" : L"+") + text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 把系数格式化为界面文本(与偏移量同一套两位小数规则)。
|
||||||
|
* @param [IN] gain 系数。
|
||||||
|
* @return 显示文本。
|
||||||
|
*/
|
||||||
|
std::wstring FormatGainText(float gain)
|
||||||
|
{
|
||||||
|
return FormatNumberText(static_cast<double>(gain));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SignalPanel::SignalPanel()
|
SignalPanel::SignalPanel()
|
||||||
{
|
{
|
||||||
m_colorRects.fill(gd::RectF());
|
m_colorRects.fill(gd::RectF());
|
||||||
m_modeRects.fill(gd::RectF());
|
m_modeRects.fill(gd::RectF());
|
||||||
|
m_adjustRects.fill(gd::RectF());
|
||||||
}
|
}
|
||||||
|
|
||||||
void SignalPanel::Setup(HWND host, const UiFonts* fonts, const Theme* theme, PopupMenu* menu,
|
void SignalPanel::Setup(HWND host, const UiFonts* fonts, const Theme* theme, PopupMenu* menu,
|
||||||
@@ -181,6 +244,8 @@ namespace jdi
|
|||||||
m_editField = StyleEditField::NONE;
|
m_editField = StyleEditField::NONE;
|
||||||
m_pendingLaneMenu = false;
|
m_pendingLaneMenu = false;
|
||||||
m_pendingColorPicker = false;
|
m_pendingColorPicker = false;
|
||||||
|
m_pendingGainMenu = false;
|
||||||
|
m_pendingMirrorMenu = false;
|
||||||
m_pendingClosePart = RowPart::NONE;
|
m_pendingClosePart = RowPart::NONE;
|
||||||
m_pendingCloseRow = -1;
|
m_pendingCloseRow = -1;
|
||||||
m_pendingFilterClear = false;
|
m_pendingFilterClear = false;
|
||||||
@@ -257,6 +322,17 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return LoadCursorW(nullptr, IDC_HAND);
|
return LoadCursorW(nullptr, IDC_HAND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 样式区的可点击控件(「调整」行三个按钮、线宽/透明度数值框)也用手型光标。
|
||||||
|
const float fx = static_cast<float>(x);
|
||||||
|
const float fy = static_cast<float>(y);
|
||||||
|
if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy) ||
|
||||||
|
UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy) ||
|
||||||
|
UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy) || UiPointInRect(m_widthValueRect, fx, fy) ||
|
||||||
|
UiPointInRect(m_opacityValueRect, fx, fy))
|
||||||
|
{
|
||||||
|
return LoadCursorW(nullptr, IDC_HAND);
|
||||||
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,7 +693,7 @@ namespace jdi
|
|||||||
const float rowStep = (kStyleRowHeight + kStyleRowGap) * m_scale;
|
const float rowStep = (kStyleRowHeight + kStyleRowGap) * m_scale;
|
||||||
const float firstTop = m_styleRect.Y + kStylePadV * m_scale;
|
const float firstTop = m_styleRect.Y + kStylePadV * m_scale;
|
||||||
|
|
||||||
// 内容列宽度带上下限:面板变窄时收缩,面板变宽时不再延伸,四行始终共用同一宽度。
|
// 内容列宽度带上下限:面板变窄时收缩,面板变宽时不再延伸,五行始终共用同一宽度。
|
||||||
const float available = std::max(40.0f, m_styleRect.Width - padH * 2.0f - labelWidth - labelGap);
|
const float available = std::max(40.0f, m_styleRect.Width - padH * 2.0f - labelWidth - labelGap);
|
||||||
const float contentWidth = std::min(available, kStyleContentMax * m_scale);
|
const float contentWidth = std::min(available, kStyleContentMax * m_scale);
|
||||||
|
|
||||||
@@ -634,8 +710,9 @@ namespace jdi
|
|||||||
|
|
||||||
const gd::RectF& colorRow = m_styleRowRects[0];
|
const gd::RectF& colorRow = m_styleRowRects[0];
|
||||||
const gd::RectF& modeRow = m_styleRowRects[1];
|
const gd::RectF& modeRow = m_styleRowRects[1];
|
||||||
const gd::RectF& opacityRow = m_styleRowRects[2];
|
const gd::RectF& adjustRow = m_styleRowRects[2];
|
||||||
const gd::RectF& widthRow = m_styleRowRects[3];
|
const gd::RectF& widthRow = m_styleRowRects[3];
|
||||||
|
const gd::RectF& opacityRow = m_styleRowRects[4];
|
||||||
|
|
||||||
// ---- 颜色行:预设色块 + 同尺寸自定义颜色入口(合计恰好铺满内容列宽度)----
|
// ---- 颜色行:预设色块 + 同尺寸自定义颜色入口(合计恰好铺满内容列宽度)----
|
||||||
int paletteCount = 0;
|
int paletteCount = 0;
|
||||||
@@ -663,7 +740,7 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
m_colorCount = std::max(1, count);
|
m_colorCount = std::max(1, count);
|
||||||
|
|
||||||
// 颜色行与其余三行共用同一左边界(不再居中),保证四行纵向对齐。
|
// 颜色行与其余各行共用同一左边界(不再居中),保证五行纵向对齐。
|
||||||
const float colorRowLeft = contentLeft;
|
const float colorRowLeft = contentLeft;
|
||||||
|
|
||||||
const float swatchTop = colorRow.Y + (rowHeight - swatchSize) * 0.5f;
|
const float swatchTop = colorRow.Y + (rowHeight - swatchSize) * 0.5f;
|
||||||
@@ -689,20 +766,32 @@ namespace jdi
|
|||||||
modeWidth, rowHeight);
|
modeWidth, rowHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 透明度行与线宽行:几何完全一致,保证纵向宽度对齐 ----
|
// ---- 「调整」行:系数调整 / 取绝对值 / 中心反转(三个铺满内容列的等宽按钮)----
|
||||||
|
const float adjustGap = kAdjustGap * m_scale;
|
||||||
|
const float adjustWidth =
|
||||||
|
std::max(20.0f, (contentWidth - adjustGap * static_cast<float>(kAdjustCount - 1)) /
|
||||||
|
static_cast<float>(kAdjustCount));
|
||||||
|
for (int index = 0; index < kAdjustCount; ++index)
|
||||||
|
{
|
||||||
|
m_adjustRects[static_cast<std::size_t>(index)] =
|
||||||
|
gd::RectF(contentLeft + static_cast<float>(index) * (adjustWidth + adjustGap), adjustRow.Y,
|
||||||
|
adjustWidth, rowHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 线宽行与透明度行:几何完全一致,保证纵向宽度对齐(线宽在上、透明度在下)----
|
||||||
const float valueWidth = std::min(kSliderValueWidth * m_scale, contentWidth * 0.4f);
|
const float valueWidth = std::min(kSliderValueWidth * m_scale, contentWidth * 0.4f);
|
||||||
const float trackWidth = std::max(24.0f, contentWidth - valueWidth - kSliderValueGap * m_scale);
|
const float trackWidth = std::max(24.0f, contentWidth - valueWidth - kSliderValueGap * m_scale);
|
||||||
const float trackHeight = kTrackHeight * m_scale;
|
const float trackHeight = kTrackHeight * m_scale;
|
||||||
|
|
||||||
m_opacityTrackRect = gd::RectF(contentLeft, opacityRow.Y + (rowHeight - trackHeight) * 0.5f,
|
|
||||||
trackWidth, trackHeight);
|
|
||||||
m_opacityValueRect = gd::RectF(contentLeft + trackWidth + kSliderValueGap * m_scale, opacityRow.Y,
|
|
||||||
valueWidth, rowHeight);
|
|
||||||
|
|
||||||
m_widthTrackRect =
|
m_widthTrackRect =
|
||||||
gd::RectF(contentLeft, widthRow.Y + (rowHeight - trackHeight) * 0.5f, trackWidth, trackHeight);
|
gd::RectF(contentLeft, widthRow.Y + (rowHeight - trackHeight) * 0.5f, trackWidth, trackHeight);
|
||||||
m_widthValueRect = gd::RectF(contentLeft + trackWidth + kSliderValueGap * m_scale, widthRow.Y,
|
m_widthValueRect = gd::RectF(contentLeft + trackWidth + kSliderValueGap * m_scale, widthRow.Y,
|
||||||
valueWidth, rowHeight);
|
valueWidth, rowHeight);
|
||||||
|
|
||||||
|
m_opacityTrackRect = gd::RectF(contentLeft, opacityRow.Y + (rowHeight - trackHeight) * 0.5f,
|
||||||
|
trackWidth, trackHeight);
|
||||||
|
m_opacityValueRect = gd::RectF(contentLeft + trackWidth + kSliderValueGap * m_scale, opacityRow.Y,
|
||||||
|
valueWidth, rowHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SignalPanel::Layout(const gd::RectF& area)
|
void SignalPanel::Layout(const gd::RectF& area)
|
||||||
@@ -1207,27 +1296,31 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 第三行:透明度(数值可点击输入)----
|
// ---- 第三行:调整(系数 / 镜像 / 绝对值,三个切换类按钮)----
|
||||||
|
// 结算顺序固定为「系数 → 绝对值 → 镜像」;「系数」「镜像」点击后展开二级面板。
|
||||||
{
|
{
|
||||||
paintRowLabel(2, L"透明度");
|
paintRowLabel(2, L"调整");
|
||||||
|
|
||||||
const bool active = m_draggingOpacity || (m_hoverStyle == StyleHit::OPACITY_SLIDER);
|
const auto paintToggle = [&](int index, const wchar_t* label, bool checked, StyleHit target)
|
||||||
UiDrawSlider(g, m_opacityTrackRect, static_cast<double>(style->opacity), m_theme->accent, active,
|
|
||||||
m_scale);
|
|
||||||
|
|
||||||
if (m_editField == StyleEditField::OPACITY)
|
|
||||||
{
|
{
|
||||||
UiDrawEditField(g, m_opacityValueRect, m_edit, m_fonts->monoSmall, m_scale);
|
const gd::RectF& rect = m_adjustRects[static_cast<std::size_t>(index)];
|
||||||
}
|
const bool hot = (m_hoverStyle == target);
|
||||||
else
|
UiFillRoundRect(g, rect, kCorner * m_scale,
|
||||||
{
|
checked ? m_theme->accentSoft : (hot ? m_theme->hover : m_theme->cardBg));
|
||||||
const int percent = static_cast<int>(style->opacity * 100.0f + 0.5f);
|
UiDrawRoundRect(g, rect, kCorner * m_scale,
|
||||||
UiText(g, std::to_wstring(percent) + L"%", m_fonts->monoSmall, m_opacityValueRect,
|
checked ? m_theme->accent : m_theme->border, 1.0f);
|
||||||
m_theme->textStrong, TextAlign::RIGHT);
|
UiText(g, label, m_fonts->uiSmall, rect,
|
||||||
}
|
checked ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
|
||||||
|
};
|
||||||
|
// 系数/偏移:系数不等于默认值 1、或任一偏移不为 0 时显示为"已启用"。
|
||||||
|
paintToggle(kAdjustGain, L"系数/偏移",
|
||||||
|
!GainIsDefault(style->gain) || style->Offset(), StyleHit::GAIN_ADJUST);
|
||||||
|
// 镜像:水平或垂直任一被勾选即显示为激活态。
|
||||||
|
paintToggle(kAdjustMirror, L"镜像", style->Mirrored(), StyleHit::MIRROR_ADJUST);
|
||||||
|
paintToggle(kAdjustAbsolute, L"绝对值", style->absolute, StyleHit::ABSOLUTE_TOGGLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 第四行:线宽(与透明度行几何一致,数值可点击输入)----
|
// ---- 第四行:线宽(数值可点击输入)----
|
||||||
{
|
{
|
||||||
paintRowLabel(3, L"线宽");
|
paintRowLabel(3, L"线宽");
|
||||||
|
|
||||||
@@ -1253,6 +1346,26 @@ namespace jdi
|
|||||||
UiText(g, shown, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong, TextAlign::RIGHT);
|
UiText(g, shown, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong, TextAlign::RIGHT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 第五行:透明度(数值可点击输入)----
|
||||||
|
{
|
||||||
|
paintRowLabel(4, L"透明度");
|
||||||
|
|
||||||
|
const bool active = m_draggingOpacity || (m_hoverStyle == StyleHit::OPACITY_SLIDER);
|
||||||
|
UiDrawSlider(g, m_opacityTrackRect, static_cast<double>(style->opacity), m_theme->accent, active,
|
||||||
|
m_scale);
|
||||||
|
|
||||||
|
if (m_editField == StyleEditField::OPACITY)
|
||||||
|
{
|
||||||
|
UiDrawEditField(g, m_opacityValueRect, m_edit, m_fonts->monoSmall, m_scale);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const int percent = static_cast<int>(style->opacity * 100.0f + 0.5f);
|
||||||
|
UiText(g, std::to_wstring(percent) + L"%", m_fonts->monoSmall, m_opacityValueRect,
|
||||||
|
m_theme->textStrong, TextAlign::RIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SignalPanel::AreAllSignalsVisible() const
|
bool SignalPanel::AreAllSignalsVisible() const
|
||||||
@@ -1874,6 +1987,129 @@ namespace jdi
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SignalPanel::OpenGainMenu()
|
||||||
|
{
|
||||||
|
if (m_menu == nullptr || m_tab == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const SignalStyle* style = StyleOf(SelectedRef());
|
||||||
|
if (style == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二级面板:两张“标题(带还原图标)+ 输入框”的组合:
|
||||||
|
// 1. 「系数调整」:一整行系数输入框;
|
||||||
|
// 2. 「偏移调整」:下方两行分别为「Y轴」与「时间轴」的偏移输入框。
|
||||||
|
//(与网格 Y 轴上下限的菜单内联编辑同一套做法:提交后面板保持打开,可连续微调。)
|
||||||
|
std::vector<MenuItem> items;
|
||||||
|
|
||||||
|
MenuItem gainHeader;
|
||||||
|
gainHeader.kind = MenuItemKind::HEADER_ACTION;
|
||||||
|
gainHeader.label = L"系数调整";
|
||||||
|
gainHeader.id = CMD_GAIN_RESET; // 点击标题栏右侧的还原图标 → 系数重置为 1
|
||||||
|
items.push_back(gainHeader);
|
||||||
|
|
||||||
|
const std::wstring gainText = FormatGainText(style->gain);
|
||||||
|
MenuItem gainEdit;
|
||||||
|
gainEdit.kind = MenuItemKind::EDIT;
|
||||||
|
gainEdit.label.clear(); // 不显示说明文字:输入框独占整行
|
||||||
|
gainEdit.id = CMD_GAIN_EDIT;
|
||||||
|
gainEdit.value = gainText;
|
||||||
|
gainEdit.editValue = gainText;
|
||||||
|
gainEdit.filter = EditFilter::NUMBER;
|
||||||
|
items.push_back(gainEdit);
|
||||||
|
|
||||||
|
MenuItem offsetHeader;
|
||||||
|
offsetHeader.kind = MenuItemKind::HEADER_ACTION;
|
||||||
|
offsetHeader.label = L"偏移调整";
|
||||||
|
offsetHeader.id = CMD_OFFSET_RESET; // 点击还原图标 → 两个偏移都恢复默认值
|
||||||
|
items.push_back(offsetHeader);
|
||||||
|
|
||||||
|
// 「Y轴」:对绘制值加上一个常数偏移(波形上下移动),非编辑态带正负号。
|
||||||
|
MenuItem offsetY;
|
||||||
|
offsetY.kind = MenuItemKind::EDIT;
|
||||||
|
offsetY.label = L"Y轴";
|
||||||
|
offsetY.id = CMD_OFFSET_Y;
|
||||||
|
offsetY.value = FormatSignedNumber(static_cast<double>(style->offsetY));
|
||||||
|
offsetY.editValue = FormatNumberText(static_cast<double>(style->offsetY));
|
||||||
|
offsetY.filter = EditFilter::NUMBER;
|
||||||
|
items.push_back(offsetY);
|
||||||
|
|
||||||
|
// 「时间轴」:把采样沿时间轴平移(画在 t + offsetTime)。
|
||||||
|
// 非编辑态显示与双光标差值同一套复合单位文本;编辑态显示以秒为单位的数值,
|
||||||
|
// 并在输入框最右侧提示单位 `s`。
|
||||||
|
MenuItem offsetTime;
|
||||||
|
offsetTime.kind = MenuItemKind::EDIT;
|
||||||
|
offsetTime.label = L"时间轴";
|
||||||
|
offsetTime.id = CMD_OFFSET_TIME;
|
||||||
|
offsetTime.value = L"Δt:" + FormatDurationCompound(style->offsetTime);
|
||||||
|
offsetTime.editValue = FormatNumberText(style->offsetTime);
|
||||||
|
offsetTime.editSuffix = L"s";
|
||||||
|
offsetTime.filter = EditFilter::NUMBER;
|
||||||
|
items.push_back(offsetTime);
|
||||||
|
|
||||||
|
// 锚定在按钮下沿:面板紧贴按钮展开(与拾色器"以触发器矩形为锚点"的观感一致)。
|
||||||
|
const gd::RectF& button = m_adjustRects[kAdjustGain];
|
||||||
|
const gd::RectF bounds = (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area;
|
||||||
|
m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, static_cast<int>(button.X),
|
||||||
|
static_cast<int>(button.Y + button.Height), items);
|
||||||
|
// ⚠️ 菜单一旦打开就会接管鼠标事件,样式区不会再收到 WM_MOUSEMOVE →
|
||||||
|
// 必须在这里主动清掉按钮的悬停态,否则按钮会"像鼠标一直按着"一样持续高亮。
|
||||||
|
ClearStyleHover();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SignalPanel::OpenMirrorMenu()
|
||||||
|
{
|
||||||
|
if (m_menu == nullptr || m_tab == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const SignalStyle* style = StyleOf(SelectedRef());
|
||||||
|
if (style == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 两行带复选框的选项:点击即切换、且**保持菜单打开**(可连续勾选两项)。
|
||||||
|
std::vector<MenuItem> items;
|
||||||
|
|
||||||
|
MenuItem horizontal;
|
||||||
|
horizontal.kind = MenuItemKind::NORMAL;
|
||||||
|
horizontal.label = L"水平镜像";
|
||||||
|
horizontal.id = CMD_MIRROR_HORIZONTAL;
|
||||||
|
horizontal.checked = style->mirrorHorizontal;
|
||||||
|
horizontal.keepOpen = true;
|
||||||
|
items.push_back(horizontal);
|
||||||
|
|
||||||
|
MenuItem vertical;
|
||||||
|
vertical.kind = MenuItemKind::NORMAL;
|
||||||
|
vertical.label = L"垂直镜像";
|
||||||
|
vertical.id = CMD_MIRROR_VERTICAL;
|
||||||
|
vertical.checked = style->mirrorVertical;
|
||||||
|
vertical.keepOpen = true;
|
||||||
|
items.push_back(vertical);
|
||||||
|
|
||||||
|
const gd::RectF& button = m_adjustRects[kAdjustMirror];
|
||||||
|
const gd::RectF bounds = (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area;
|
||||||
|
m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, static_cast<int>(button.X),
|
||||||
|
static_cast<int>(button.Y + button.Height), items);
|
||||||
|
ClearStyleHover();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalPanel::ClearStyleHover()
|
||||||
|
{
|
||||||
|
if (m_hoverStyle == StyleHit::NONE && m_hoverStyleIndex < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_hoverStyle = StyleHit::NONE;
|
||||||
|
m_hoverStyleIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
bool SignalPanel::CloseTransientUi()
|
bool SignalPanel::CloseTransientUi()
|
||||||
{
|
{
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
@@ -1895,6 +2131,8 @@ namespace jdi
|
|||||||
m_pendingClosePart = RowPart::NONE;
|
m_pendingClosePart = RowPart::NONE;
|
||||||
m_pendingCloseRow = -1;
|
m_pendingCloseRow = -1;
|
||||||
m_pendingFilterClear = false;
|
m_pendingFilterClear = false;
|
||||||
|
m_pendingGainMenu = false;
|
||||||
|
m_pendingMirrorMenu = false;
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1986,6 +2224,18 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
hoverStyle = StyleHit::WIDTH_VALUE;
|
hoverStyle = StyleHit::WIDTH_VALUE;
|
||||||
}
|
}
|
||||||
|
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_adjustRects[kAdjustGain], fx, fy))
|
||||||
|
{
|
||||||
|
hoverStyle = StyleHit::GAIN_ADJUST;
|
||||||
|
}
|
||||||
|
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy))
|
||||||
|
{
|
||||||
|
hoverStyle = StyleHit::MIRROR_ADJUST;
|
||||||
|
}
|
||||||
|
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy))
|
||||||
|
{
|
||||||
|
hoverStyle = StyleHit::ABSOLUTE_TOGGLE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hoverRow == m_hoverRow && hoverStyle == m_hoverStyle && hoverStyleIndex == m_hoverStyleIndex &&
|
if (hoverRow == m_hoverRow && hoverStyle == m_hoverStyle && hoverStyleIndex == m_hoverStyleIndex &&
|
||||||
@@ -2061,6 +2311,8 @@ namespace jdi
|
|||||||
|
|
||||||
m_pendingLaneMenu = false;
|
m_pendingLaneMenu = false;
|
||||||
m_pendingColorPicker = false;
|
m_pendingColorPicker = false;
|
||||||
|
m_pendingGainMenu = false;
|
||||||
|
m_pendingMirrorMenu = false;
|
||||||
|
|
||||||
// 点击当前输入框以外的位置时提交输入。
|
// 点击当前输入框以外的位置时提交输入。
|
||||||
if (m_editField != StyleEditField::NONE &&
|
if (m_editField != StyleEditField::NONE &&
|
||||||
@@ -2126,6 +2378,23 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return BeginEdit(StyleEditField::OPACITY);
|
return BeginEdit(StyleEditField::OPACITY);
|
||||||
}
|
}
|
||||||
|
// 「调整」行:系数与镜像(按下只登记,**抬起**时才展开各自的二级面板,
|
||||||
|
// 与自定义颜色入口同一套交互)、绝对值(单击即切换)。
|
||||||
|
if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy))
|
||||||
|
{
|
||||||
|
m_pendingGainMenu = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy))
|
||||||
|
{
|
||||||
|
m_pendingMirrorMenu = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy))
|
||||||
|
{
|
||||||
|
style->absolute = !style->absolute;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 列表行。
|
// 列表行。
|
||||||
@@ -2265,6 +2534,29 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 「系数」/「镜像」:按下时只登记,抬起时指针仍在同一按钮上才展开二级面板
|
||||||
|
//(与自定义颜色入口一致:按住后移开再松开视为放弃)。
|
||||||
|
if (m_pendingGainMenu)
|
||||||
|
{
|
||||||
|
m_pendingGainMenu = false;
|
||||||
|
const float fx = static_cast<float>(x);
|
||||||
|
const float fy = static_cast<float>(y);
|
||||||
|
if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy) && m_tab != nullptr)
|
||||||
|
{
|
||||||
|
changed = OpenGainMenu() || changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_pendingMirrorMenu)
|
||||||
|
{
|
||||||
|
m_pendingMirrorMenu = false;
|
||||||
|
const float fx = static_cast<float>(x);
|
||||||
|
const float fy = static_cast<float>(y);
|
||||||
|
if (UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy) && m_tab != nullptr)
|
||||||
|
{
|
||||||
|
changed = OpenMirrorMenu() || changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (m_pendingLaneMenu)
|
if (m_pendingLaneMenu)
|
||||||
{
|
{
|
||||||
m_pendingLaneMenu = false;
|
m_pendingLaneMenu = false;
|
||||||
@@ -2583,6 +2875,133 @@ namespace jdi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 「系数」/「偏移」二级面板里的输入框提交:就地更新,面板保持打开
|
||||||
|
//(与网格 Y 轴上下限的菜单内联编辑一致),并把夹取后的真实值回写到输入框上。
|
||||||
|
if (result.editCommit)
|
||||||
|
{
|
||||||
|
SignalStyle* style = StyleOf(SelectedRef());
|
||||||
|
if (style != nullptr)
|
||||||
|
{
|
||||||
|
const std::wstring trimmed = Trim(result.editText);
|
||||||
|
wchar_t* stop = nullptr;
|
||||||
|
const double parsed =
|
||||||
|
trimmed.empty() ? 0.0 : std::wcstod(trimmed.c_str(), &stop);
|
||||||
|
// 非法/空输入保持原值(只把真实值回写到输入框,避免用户看到错误的数字)。
|
||||||
|
const bool valid = !trimmed.empty() && stop != trimmed.c_str() && std::isfinite(parsed);
|
||||||
|
switch (result.id)
|
||||||
|
{
|
||||||
|
case CMD_GAIN_EDIT:
|
||||||
|
{
|
||||||
|
if (valid)
|
||||||
|
{
|
||||||
|
style->gain =
|
||||||
|
static_cast<float>(std::max(-kGainLimit, std::min(parsed, kGainLimit)));
|
||||||
|
}
|
||||||
|
if (m_menu != nullptr)
|
||||||
|
{
|
||||||
|
m_menu->UpdateItemValue(CMD_GAIN_EDIT, FormatGainText(style->gain), FormatGainText(style->gain));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
case CMD_OFFSET_Y:
|
||||||
|
{
|
||||||
|
if (valid)
|
||||||
|
{
|
||||||
|
style->offsetY = static_cast<float>(
|
||||||
|
std::max(-static_cast<double>(kGainLimit), std::min(parsed, kGainLimit)));
|
||||||
|
}
|
||||||
|
if (m_menu != nullptr)
|
||||||
|
{
|
||||||
|
m_menu->UpdateItemValue(
|
||||||
|
CMD_OFFSET_Y, FormatSignedNumber(static_cast<double>(style->offsetY)),
|
||||||
|
FormatNumberText(static_cast<double>(style->offsetY)));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
case CMD_OFFSET_TIME:
|
||||||
|
{
|
||||||
|
if (valid)
|
||||||
|
{
|
||||||
|
style->offsetTime =
|
||||||
|
std::max(-kGainLimit, std::min(parsed, kGainLimit));
|
||||||
|
}
|
||||||
|
if (m_menu != nullptr)
|
||||||
|
{
|
||||||
|
m_menu->UpdateItemValue(CMD_OFFSET_TIME,
|
||||||
|
L"Δt:" + FormatDurationCompound(style->offsetTime),
|
||||||
|
FormatNumberText(style->offsetTime));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题栏右侧的「还原」图标:系数重置为默认值 1(面板保持打开,输入框立即回显)。
|
||||||
|
if (result.id == CMD_GAIN_RESET)
|
||||||
|
{
|
||||||
|
if (SignalStyle* style = StyleOf(SelectedRef()))
|
||||||
|
{
|
||||||
|
style->gain = 1.0f;
|
||||||
|
if (m_menu != nullptr)
|
||||||
|
{
|
||||||
|
m_menu->UpdateItemValue(CMD_GAIN_EDIT, FormatGainText(style->gain), FormatGainText(style->gain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 「偏移调整」的「还原」图标:Y 与时间偏移都恢复为默认值 0。
|
||||||
|
if (result.id == CMD_OFFSET_RESET)
|
||||||
|
{
|
||||||
|
if (SignalStyle* style = StyleOf(SelectedRef()))
|
||||||
|
{
|
||||||
|
style->offsetY = 0.0f;
|
||||||
|
style->offsetTime = 0.0;
|
||||||
|
if (m_menu != nullptr)
|
||||||
|
{
|
||||||
|
m_menu->UpdateItemValue(CMD_OFFSET_Y, FormatSignedNumber(0.0), L"0.00");
|
||||||
|
m_menu->UpdateItemValue(CMD_OFFSET_TIME, L"Δt:" + FormatDurationCompound(0.0),
|
||||||
|
L"0.00");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 「镜像」二级面板:两行带复选框的切换项(面板保持打开,勾选状态就地回写)。
|
||||||
|
if (result.id == CMD_MIRROR_HORIZONTAL || result.id == CMD_MIRROR_VERTICAL)
|
||||||
|
{
|
||||||
|
if (SignalStyle* style = StyleOf(SelectedRef()))
|
||||||
|
{
|
||||||
|
const bool horizontal = (result.id == CMD_MIRROR_HORIZONTAL);
|
||||||
|
bool& flag = horizontal ? style->mirrorHorizontal : style->mirrorVertical;
|
||||||
|
flag = !flag;
|
||||||
|
if (!horizontal)
|
||||||
|
{
|
||||||
|
// ⚠️ Y 偏移以**未镜像空间**为准:垂直镜像开关变化时同步取反偏移量,
|
||||||
|
// 使绘制等价于「先加上偏移再镜像」(用户明确要求)。
|
||||||
|
style->offsetY = -style->offsetY;
|
||||||
|
if (m_menu != nullptr)
|
||||||
|
{
|
||||||
|
m_menu->UpdateItemValue(
|
||||||
|
CMD_OFFSET_Y, FormatSignedNumber(static_cast<double>(style->offsetY)),
|
||||||
|
FormatNumberText(static_cast<double>(style->offsetY)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_menu != nullptr)
|
||||||
|
{
|
||||||
|
m_menu->UpdateItemLabel(result.id, horizontal ? L"水平镜像" : L"垂直镜像", flag, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool needRelayout = false;
|
bool needRelayout = false;
|
||||||
|
|
||||||
switch (result.id)
|
switch (result.id)
|
||||||
|
|||||||
@@ -219,6 +219,17 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
gd::RectF EditFieldRect() const;
|
gd::RectF EditFieldRect() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 把“文本区矩形”扩成**编辑态输入框**矩形(向外扩出输入框的内边距)。
|
||||||
|
*
|
||||||
|
* ⚠️ 数值区正好 3 个字符宽、色号正好 6+1 个字符宽,若直接拿文本区当输入框,
|
||||||
|
* 输入框内部的 6px 内边距会把最后一个字符裁掉。
|
||||||
|
*
|
||||||
|
* @param [IN] textRect 文本区矩形(数值区或色号区)。
|
||||||
|
* @return 输入框矩形。
|
||||||
|
*/
|
||||||
|
gd::RectF PaddedEditRect(const gd::RectF& textRect) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 查询坐标命中的滑块下标。
|
* @brief 查询坐标命中的滑块下标。
|
||||||
* @param [IN] x 客户区横坐标。
|
* @param [IN] x 客户区横坐标。
|
||||||
@@ -227,6 +238,17 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
int SliderAtPoint(int x, int y) const;
|
int SliderAtPoint(int x, int y) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算通道标签(R/G/B)的矩形。
|
||||||
|
*
|
||||||
|
* 标签**右边界**到轨道左端的距离与轨道右端到数值区的距离相等
|
||||||
|
* (用户要求:滑块两侧留白一致,三个字母纵向对齐)。
|
||||||
|
*
|
||||||
|
* @param [IN] index 通道下标(0=R,1=G,2=B)。
|
||||||
|
* @return 标签矩形;下标非法时返回空矩形。
|
||||||
|
*/
|
||||||
|
gd::RectF ChannelLabelRect(int index) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 应用滑块拖拽位置到对应通道。
|
* @brief 应用滑块拖拽位置到对应通道。
|
||||||
* @param [IN] index 滑块下标。
|
* @param [IN] index 滑块下标。
|
||||||
|
|||||||
+177
-1
@@ -1,6 +1,8 @@
|
|||||||
#ifndef __DATA_MODEL_HPP__
|
#ifndef __DATA_MODEL_HPP__
|
||||||
#define __DATA_MODEL_HPP__
|
#define __DATA_MODEL_HPP__
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -90,7 +92,7 @@ namespace jdi
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 单个信号的显示外观(颜色/透明度/线宽/绘制方式)。
|
* @brief 单个信号的显示外观(颜色/透明度/线宽/绘制方式/数值变换)。
|
||||||
*
|
*
|
||||||
* 外观属于信号自身而非某个标签页——同一信号在所有标签页中必须显示效果一致,
|
* 外观属于信号自身而非某个标签页——同一信号在所有标签页中必须显示效果一致,
|
||||||
* 因此在任一视图里修改颜色/线宽/透明度/样式,其它视图会自动同步。
|
* 因此在任一视图里修改颜色/线宽/透明度/样式,其它视图会自动同步。
|
||||||
@@ -103,8 +105,161 @@ namespace jdi
|
|||||||
float opacity = 1.0f; /**< 不透明度,取值 0.0-1.0,默认 1.0。 */
|
float opacity = 1.0f; /**< 不透明度,取值 0.0-1.0,默认 1.0。 */
|
||||||
float lineWidth = 1.0f; /**< 线宽(逻辑像素,未乘 DPI 缩放)。 */
|
float lineWidth = 1.0f; /**< 线宽(逻辑像素,未乘 DPI 缩放)。 */
|
||||||
DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */
|
DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制前的变换(依次作用,见 `ApplyDrawTransform`):
|
||||||
|
* 1. `gain` 系数:采样值乘以该系数(默认 1,即不改变);
|
||||||
|
* 2. `absolute` 绝对值:|value|;
|
||||||
|
* 3. 镜像:`mirrorVertical` 按 Y = 0 上下镜像(value → −value),
|
||||||
|
* `mirrorHorizontal` 为**数据反序**(把采样序列头尾对调后绘制,即水平翻转);
|
||||||
|
* 4. 偏移:`offsetY` 给绘制值加上一个常数(波形整体上下移动),
|
||||||
|
* `offsetTime` 把采样沿时间轴平移(绘制在 `t + offsetTime` 处)。
|
||||||
|
*
|
||||||
|
* ⚠️ 结算顺序固定为「系数 → 绝对值 → 镜像 → 偏移」(用户明确要求)。
|
||||||
|
* 其中镜像的两个方向互相正交(垂直改**值**、水平改**取值下标**),偏移同理
|
||||||
|
*(`offsetY` 改值、`offsetTime` 改**绘制时刻**)。
|
||||||
|
* ⚠️ `offsetY` 以**未镜像空间**为准:启用/取消垂直镜像时会同步取反(见
|
||||||
|
* `SignalStyle::offsetY` 的使用处),使绘制等价于「先加上偏移再镜像」。
|
||||||
|
*/
|
||||||
|
float gain = 1.0f; /**< 系数,默认 1.0(不改变)。 */
|
||||||
|
bool absolute = false; /**< 是否对绘制值取绝对值,默认关闭。 */
|
||||||
|
bool mirrorVertical = false; /**< 是否按 Y=0 上下镜像(值取反),默认关闭。 */
|
||||||
|
bool mirrorHorizontal = false; /**< 是否水平镜像(数据反序绘制),默认关闭。 */
|
||||||
|
float offsetY = 0.0f; /**< Y 轴偏移(叠加到绘制值上),默认 0。 */
|
||||||
|
double offsetTime = 0.0; /**< 时间轴偏移(秒,绘制在 t + offsetTime 处),默认 0。 */
|
||||||
|
|
||||||
|
/** @brief 镜像功能的任一分量是否启用(决定「镜像」按钮的激活态)。 */
|
||||||
|
bool Mirrored() const { return mirrorVertical || mirrorHorizontal; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 是否存在任何**偏移**(决定「系数/偏移」按钮的激活态之一)。
|
||||||
|
* @return Y 或时间偏移不为 0 返回 true。
|
||||||
|
*/
|
||||||
|
bool Offset() const
|
||||||
|
{
|
||||||
|
return std::fabs(offsetY) > 1.0e-9f || std::fabs(offsetTime) > 1.0e-15;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 把原始采样值变换为**绘制用值**。
|
||||||
|
*
|
||||||
|
* 顺序固定为「系数 → 绝对值 → 垂直镜像 → Y 偏移」,例如系数 2、原始值 1.5 →
|
||||||
|
* 2×1.5 = 3;取绝对值仍为 3;垂直镜像则 −3;Y 偏移 +1 得 −2。
|
||||||
|
* NaN 经乘/取绝对值/取反/加偏移后仍为 NaN(保持断线语义)。
|
||||||
|
*
|
||||||
|
* ⚠️ 水平镜像与时间偏移**不在这里处理**:它们分别改变“取值下标”与“绘制时刻”,
|
||||||
|
* 必须在遍历采样时处理(见 `SourceIndexFor` 与 `offsetTime` 的使用处)。
|
||||||
|
*
|
||||||
|
* @param [IN] style 信号外观。
|
||||||
|
* @param [IN] value 原始采样值。
|
||||||
|
* @return 绘制用值。
|
||||||
|
*/
|
||||||
|
inline double ApplyDrawTransform(const SignalStyle& style, double value)
|
||||||
|
{
|
||||||
|
if (style.gain != 1.0f)
|
||||||
|
{
|
||||||
|
value *= static_cast<double>(style.gain);
|
||||||
|
}
|
||||||
|
if (style.absolute)
|
||||||
|
{
|
||||||
|
value = (value < 0.0) ? -value : value;
|
||||||
|
}
|
||||||
|
if (style.mirrorVertical)
|
||||||
|
{
|
||||||
|
value = -value;
|
||||||
|
}
|
||||||
|
if (style.offsetY != 0.0f)
|
||||||
|
{
|
||||||
|
value += static_cast<double>(style.offsetY);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 把遍历下标映射为**源采样下标**(处理水平镜像的"数据反序")。
|
||||||
|
*
|
||||||
|
* 水平镜像 = 头尾对调:遍历下标 `index` 取源下标 `count - 1 - index`。
|
||||||
|
* 关闭时原样返回,因此该函数在有/无镜像时都是 O(1)、无分支代价可忽略。
|
||||||
|
*
|
||||||
|
* @param [IN] style 信号外观。
|
||||||
|
* @param [IN] count 采样点数。
|
||||||
|
* @param [IN] index 遍历下标。
|
||||||
|
* @return 源采样下标。
|
||||||
|
*/
|
||||||
|
inline std::size_t SourceIndexFor(const SignalStyle& style, std::size_t count, std::size_t index)
|
||||||
|
{
|
||||||
|
if (!style.mirrorHorizontal || count == 0)
|
||||||
|
{
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
return (count - 1) - index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断数值变换是否**保持大小顺序**(正系数、不取绝对值、不垂直镜像)。
|
||||||
|
*
|
||||||
|
* 只有保序时,粗粒度包络层(每组的 min/max)才能直接按系数缩放后继续使用;
|
||||||
|
* 取绝对值 / 垂直镜像会把最小值与最大值互换,此时必须回退到逐采样点变换。
|
||||||
|
*
|
||||||
|
* @param [IN] style 信号外观。
|
||||||
|
* @return 变换单调递增返回 true。
|
||||||
|
*/
|
||||||
|
inline bool DrawTransformOrderPreserving(const SignalStyle& style)
|
||||||
|
{
|
||||||
|
return (style.gain > 0.0f) && !style.absolute && !style.mirrorVertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 把**数值区间**按绘制变换映射为变换后的区间(用于整段 Y 轴自适应)。
|
||||||
|
*
|
||||||
|
* 各步作用在区间端点上是精确的(系数为线性、绝对值为 |x|、镜像与偏移为线性),
|
||||||
|
* 因此 `[minValue, maxValue]` 映射后就是真实的数据范围,无需扫描采样点。
|
||||||
|
* ⚠️ 水平镜像与时间偏移都不改变值的范围,因此不影响本函数。
|
||||||
|
*
|
||||||
|
* @param [IN] style 信号外观。
|
||||||
|
* @param [IN,OUT] low 区间下限,返回变换后的下限。
|
||||||
|
* @param [IN,OUT] high 区间上限,返回变换后的上限。
|
||||||
|
*/
|
||||||
|
inline void TransformValueRange(const SignalStyle& style, double& low, double& high)
|
||||||
|
{
|
||||||
|
if (low > high)
|
||||||
|
{
|
||||||
|
std::swap(low, high);
|
||||||
|
}
|
||||||
|
const double gain = static_cast<double>(style.gain);
|
||||||
|
if (gain != 1.0)
|
||||||
|
{
|
||||||
|
low *= gain;
|
||||||
|
high *= gain;
|
||||||
|
if (low > high)
|
||||||
|
{
|
||||||
|
std::swap(low, high);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (style.absolute)
|
||||||
|
{
|
||||||
|
// |x| 在 [low, high] 上的取值范围:跳 0 时从 0 起,否则从较近的一端起。
|
||||||
|
const double first = std::fabs(low);
|
||||||
|
const double second = std::fabs(high);
|
||||||
|
const bool spansZero = (low <= 0.0) && (high >= 0.0);
|
||||||
|
low = spansZero ? 0.0 : std::min(first, second);
|
||||||
|
high = std::max(first, second);
|
||||||
|
}
|
||||||
|
if (style.mirrorVertical)
|
||||||
|
{
|
||||||
|
const double flipped = -low;
|
||||||
|
low = -high;
|
||||||
|
high = flipped;
|
||||||
|
}
|
||||||
|
if (style.offsetY != 0.0f)
|
||||||
|
{
|
||||||
|
const double offset = static_cast<double>(style.offsetY);
|
||||||
|
low += offset;
|
||||||
|
high += offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 单条信号的元数据与采样值(纯数据,不含可见性配置)。
|
* @brief 单条信号的元数据与采样值(纯数据,不含可见性配置)。
|
||||||
*/
|
*/
|
||||||
@@ -566,6 +721,27 @@ namespace jdi
|
|||||||
bool SampleSignalAt(const DataFile& file, const SignalData& signal, double time, bool interpolate,
|
bool SampleSignalAt(const DataFile& file, const SignalData& signal, double time, bool interpolate,
|
||||||
double& value);
|
double& value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 取某一时刻**画面上显示**的取值(含水平镜像的"数据反序"与时间偏移)。
|
||||||
|
*
|
||||||
|
* 与 `SampleSignalAt` 的区别:
|
||||||
|
* * 水平镜像时用 `SourceIndexFor` 把下标映射到源采样(头尾对调);
|
||||||
|
* * 时间偏移时先按 `time - style.offsetTime` 求出源采样时刻(因为绘制在 `t + offsetTime`)。
|
||||||
|
* 因此返回的正是当前绘制出来的那条曲线在该时刻的取值 —— 光标读数、数据点标注都必须用它,
|
||||||
|
* 否则读数会与看到的波形不一致。
|
||||||
|
* ⚠️ 系数 / 绝对值 / 垂直镜像 / `offsetY` 不在这里处理,由 `ApplyDrawTransform` 负责。
|
||||||
|
*
|
||||||
|
* @param [IN] file 信号所属的已加载数据分组(提供时间轴)。
|
||||||
|
* @param [IN] signal 目标信号。
|
||||||
|
* @param [IN] style 显示外观(只用 `mirrorHorizontal`)。
|
||||||
|
* @param [IN] time 采样时刻。
|
||||||
|
* @param [IN] interpolate 是否在相邻采样点之间做线性插值。
|
||||||
|
* @param [OUT] value 输出采样值,缺失时为 NaN。
|
||||||
|
* @return 时间点落在数据范围内且取到有效值返回 true。
|
||||||
|
*/
|
||||||
|
bool SampleDisplayedSignalAt(const DataFile& file, const SignalData& signal,
|
||||||
|
const SignalStyle& style, double time, bool interpolate, double& value);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 计算 N 点滑动平均(窗口 = [i-N, i+N],边界按实际窗口长度)。
|
* @brief 计算 N 点滑动平均(窗口 = [i-N, i+N],边界按实际窗口长度)。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -115,6 +115,18 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
void UiDrawRoundRect(gd::Graphics& g, const gd::RectF& rect, float radius, std::uint32_t argb, float width);
|
void UiDrawRoundRect(gd::Graphics& g, const gd::RectF& rect, float radius, std::uint32_t argb, float width);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 构造圆角矩形路径(与 `UiFillRoundRect` / `UiDrawRoundRect` 使用完全相同的几何)。
|
||||||
|
*
|
||||||
|
* 需要把一张位图 / 渐变**裁剪进圆角形状**时使用:
|
||||||
|
* `g.SetClip(&path)` 之后再填充,就不会在圆角之外漏出直角矩形的角。
|
||||||
|
*
|
||||||
|
* @param [IN,OUT] path 目标路径对象(不会被清空,直接追加)。
|
||||||
|
* @param [IN] rect 目标矩形。
|
||||||
|
* @param [IN] radius 圆角半径。
|
||||||
|
*/
|
||||||
|
void UiBuildRoundRectPath(gd::GraphicsPath& path, const gd::RectF& rect, float radius);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 绘制一条直线。
|
* @brief 绘制一条直线。
|
||||||
* @param [IN,OUT] g 绘图上下文。
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ namespace jdi
|
|||||||
HEADER = 2, /**< 分组标题(不可点击)。 */
|
HEADER = 2, /**< 分组标题(不可点击)。 */
|
||||||
SWATCHES = 3, /**< 颜色色块行。 */
|
SWATCHES = 3, /**< 颜色色块行。 */
|
||||||
EDIT = 4, /**< 带内联输入框的项:在菜单内部直接编辑数值(菜单不关闭)。 */
|
EDIT = 4, /**< 带内联输入框的项:在菜单内部直接编辑数值(菜单不关闭)。 */
|
||||||
SUBMENU = 5 /**< 带二级子菜单的项:鼠标悬停时向右展开。 */
|
SUBMENU = 5, /**< 带二级子菜单的项:鼠标悬停时向右展开。 */
|
||||||
|
HEADER_ACTION = 6 /**< 分组标题 + **右侧图标按钮**(如「还原」),图标可点击。 */
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,8 +55,28 @@ namespace jdi
|
|||||||
std::uint32_t swatches[16] = {}; /**< 色块颜色。 */
|
std::uint32_t swatches[16] = {}; /**< 色块颜色。 */
|
||||||
int checkedSwatch = -1; /**< 被选中的色块下标。 */
|
int checkedSwatch = -1; /**< 被选中的色块下标。 */
|
||||||
std::wstring value; /**< EDIT 项的当前数值文本(非编辑态显示)。 */
|
std::wstring value; /**< EDIT 项的当前数值文本(非编辑态显示)。 */
|
||||||
|
/**
|
||||||
|
* EDIT 项**进入编辑时的初始文本**;为空则使用 `value`。
|
||||||
|
*
|
||||||
|
* 用于「非编辑态的显示文本」与「编辑态的数值」不一致的场合,例如时间轴偏移:
|
||||||
|
* 平时显示 `Δt:5s 60ms`,进入编辑后应显示以秒为单位的数值 `5.06`。
|
||||||
|
*/
|
||||||
|
std::wstring editValue;
|
||||||
|
/**
|
||||||
|
* EDIT 项**编辑态**在输入框最右侧提示的单位文本(如 `s`);为空则不提示。
|
||||||
|
*
|
||||||
|
* 非编辑态不显示(那时显示的是 `value` 的完整文案)。
|
||||||
|
*/
|
||||||
|
std::wstring editSuffix;
|
||||||
EditFilter filter = EditFilter::ANY; /**< EDIT 项的输入过滤规则。 */
|
EditFilter filter = EditFilter::ANY; /**< EDIT 项的输入过滤规则。 */
|
||||||
std::vector<MenuItem> submenu; /**< 二级子菜单项(kind == SUBMENU 时有效)。 */
|
std::vector<MenuItem> submenu; /**< 二级子菜单项(kind == SUBMENU 时有效)。 */
|
||||||
|
/**
|
||||||
|
* 普通项被点击后是否**保持菜单打开**。
|
||||||
|
*
|
||||||
|
* 用于「带复选框的切换项」(如镜像的水平/垂直):可以连续勾选多项,
|
||||||
|
* 勾选状态通过 `UpdateItemLabel` 回写。默认 false(点击即关):与既有行为一致。
|
||||||
|
*/
|
||||||
|
bool keepOpen = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,10 +244,14 @@ namespace jdi
|
|||||||
/**
|
/**
|
||||||
* @brief 更新指定命令项显示的值文本(菜单保持打开,立即重新排布)。
|
* @brief 更新指定命令项显示的值文本(菜单保持打开,立即重新排布)。
|
||||||
* @param [IN] id 命令标识。
|
* @param [IN] id 命令标识。
|
||||||
* @param [IN] value 新的值文本。
|
* @param [IN] value 新的值文本(非编辑态显示)。
|
||||||
|
* @param [IN] editValue 新的**编辑初值**(为空时保持原值)。
|
||||||
|
* ⚠️ 非编辑态文本与编辑态文本不一致的行(如时间轴偏移:`Δt:1h 1min 2s` ↔ `3662`)
|
||||||
|
* 必须一并更新,否则下次进入编辑会拿到陈旧的初值。
|
||||||
* @return 找到对应项并更新返回 true。
|
* @return 找到对应项并更新返回 true。
|
||||||
*/
|
*/
|
||||||
bool UpdateItemValue(int id, const std::wstring& value);
|
bool UpdateItemValue(int id, const std::wstring& value,
|
||||||
|
const std::wstring& editValue = std::wstring());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 更新指定普通项的文字与可用/选中状态(用于实时反映状态)。
|
* @brief 更新指定普通项的文字与可用/选中状态(用于实时反映状态)。
|
||||||
@@ -283,6 +308,28 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
gd::RectF EditFieldRect(int index) const;
|
gd::RectF EditFieldRect(int index) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算 EDIT 项**编辑态单位后缀**所占的宽度(0 表示无后缀)。
|
||||||
|
*
|
||||||
|
* 排版、命中测试与绘制共用:输入框正文区会向右让出这么多宽度,
|
||||||
|
* 后缀文本绘制在它右侧(靠右对齐)。
|
||||||
|
*
|
||||||
|
* @param [IN] item 菜单项。
|
||||||
|
* @return 后缀宽度(像素)。
|
||||||
|
*/
|
||||||
|
float EditSuffixWidth(const MenuItem& item) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算 `HEADER_ACTION` 项右侧**图标按钮**(如「还原」)的矩形。
|
||||||
|
*
|
||||||
|
* 图标紧贴该行右端(右边距与其它行一致、行内垂直居中),大小按缩放比例推导。
|
||||||
|
* 鼠标悬停时该图标会高亮,点击即下发该项的命令。
|
||||||
|
*
|
||||||
|
* @param [IN] index 项目下标。
|
||||||
|
* @return 图标矩形;非 `HEADER_ACTION` 项或非法下标返回空矩形。
|
||||||
|
*/
|
||||||
|
gd::RectF HeaderIconRect(int index) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 计算 EDIT 项输入框的“自然宽度”(排版与绘制共用同一公式)。
|
* @brief 计算 EDIT 项输入框的“自然宽度”(排版与绘制共用同一公式)。
|
||||||
* @param [IN] item 菜单项。
|
* @param [IN] item 菜单项。
|
||||||
@@ -350,9 +397,18 @@ namespace jdi
|
|||||||
|
|
||||||
gd::RectF m_rect; /**< 菜单整体矩形。 */
|
gd::RectF m_rect; /**< 菜单整体矩形。 */
|
||||||
std::vector<gd::RectF> m_itemRects; /**< 各项矩形。 */
|
std::vector<gd::RectF> m_itemRects; /**< 各项矩形。 */
|
||||||
|
/**
|
||||||
|
* 同一次排布中,**带说明文字的 EDIT 项**共用的输入框宽度(0 表示没有这类项)。
|
||||||
|
*
|
||||||
|
* 宽度取这些项里最宽的自然宽度,并在排布时按“不能挤掉说明文字 / 不超过内容区”夹好。
|
||||||
|
* 同一面板内的输入框因此一样宽、右边界对齐(用户要求「Y轴」与「时间轴」等宽对齐,
|
||||||
|
* 而不是各自按自己的文本长度自适应)。
|
||||||
|
*/
|
||||||
|
float m_editFieldWidth = 0.0f;
|
||||||
std::vector<std::vector<gd::RectF>> m_swatchRects; /**< 各项内的色块矩形。 */
|
std::vector<std::vector<gd::RectF>> m_swatchRects; /**< 各项内的色块矩形。 */
|
||||||
|
|
||||||
int m_hoverIndex = -1; /**< 当前悬停项。 */
|
int m_hoverIndex = -1; /**< 当前悬停项。 */
|
||||||
|
int m_hoverIconIndex = -1; /**< 当前悬停的 `HEADER_ACTION` 图标所在项下标。 */
|
||||||
int m_hoverSwatch = -1; /**< 当前悬停色块。 */
|
int m_hoverSwatch = -1; /**< 当前悬停色块。 */
|
||||||
int m_pressedIndex = -1; /**< 按下时的项目下标。 */
|
int m_pressedIndex = -1; /**< 按下时的项目下标。 */
|
||||||
int m_pressedSwatch = -1; /**< 按下时的色块下标。 */
|
int m_pressedSwatch = -1; /**< 按下时的色块下标。 */
|
||||||
|
|||||||
@@ -97,6 +97,30 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
std::wstring FormatCount(unsigned long long value);
|
std::wstring FormatCount(unsigned long long value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 把时间间隔格式化为「Δt 语义」的复合单位文本(不含 "Δt:" 前缀)。
|
||||||
|
*
|
||||||
|
* 规则(用户明确要求,与双光标差值的显示完全一致):
|
||||||
|
* - 顶级单位高于 s(即 h / min)时**一路整合到 s**,例如 3662s → "1h 1min 2s";
|
||||||
|
* - 顶级单位为 s 及以下时,若不是该单位的整数,则**向下一级递进**,
|
||||||
|
* 例如 5.06s → "5s 60ms";整秒数不加下级单位,例如 5s → "5s";
|
||||||
|
* - 顶级单位为 ps 时不再递进次级单位,例如 5.123ps → "5.123 ps"。
|
||||||
|
*
|
||||||
|
* @param [IN] seconds 时间间隔(秒)。
|
||||||
|
* @return 形如 "1h 1min 2s" / "5s 60ms" 的间隔文本。
|
||||||
|
*/
|
||||||
|
std::wstring FormatDurationCompound(double seconds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 把时间间隔格式化为「秒」数值文本(不含单位),用于内联编辑。
|
||||||
|
*
|
||||||
|
* 编辑时的量级**始终是秒**:即使显示为 "1h 1min 2s",进入编辑后也是 3602。
|
||||||
|
*
|
||||||
|
* @param [IN] seconds 时间间隔(秒)。
|
||||||
|
* @return 形如 "3602" 的文本。
|
||||||
|
*/
|
||||||
|
std::wstring FormatDurationSeconds(double seconds);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 计算“美观”的坐标轴刻度步长(1/2/5 乘以 10 的整数次幂)。
|
* @brief 计算“美观”的坐标轴刻度步长(1/2/5 乘以 10 的整数次幂)。
|
||||||
* @param [IN] range 坐标轴数值跨度,必须为正数。
|
* @param [IN] range 坐标轴数值跨度,必须为正数。
|
||||||
|
|||||||
@@ -18,11 +18,14 @@ namespace jdi
|
|||||||
const float kTitleGap = 8.0f; /**< 标题与内容区间距。 */
|
const float kTitleGap = 8.0f; /**< 标题与内容区间距。 */
|
||||||
const float kRowHeight = 26.0f; /**< 单个滑块行高度。 */
|
const float kRowHeight = 26.0f; /**< 单个滑块行高度。 */
|
||||||
const float kRowGap = 2.0f; /**< 滑块行间距。 */
|
const float kRowGap = 2.0f; /**< 滑块行间距。 */
|
||||||
const float kPreviewGap = 10.0f; /**< 滑块区与预览区间距。 */
|
const float kPreviewHeight = 24.0f; /**< 预览行(预览色块 + 色号)高度。 */
|
||||||
const float kPreviewHeight = 24.0f; /**< 预览行高度。 */
|
|
||||||
const float kLabelWidth = 16.0f; /**< 通道标签宽度。 */
|
const float kLabelWidth = 16.0f; /**< 通道标签宽度。 */
|
||||||
const float kLabelGap = 6.0f; /**< 标签与滑块间距。 */
|
/**
|
||||||
const float kValueWidth = 40.0f; /**< 数值文本宽度。 */
|
* 滑块**两侧**的间隙(左:通道字母 ↔ 轨道;右:轨道 ↔ 数值区)。
|
||||||
|
*
|
||||||
|
* ⚠️ 必须共用同一个常量:用户明确要求轨道左右两侧的留白相等。
|
||||||
|
*/
|
||||||
|
const float kSliderGap = 8.0f;
|
||||||
const float kTrackHeight = 8.0f; /**< 滑块轨道高度。 */
|
const float kTrackHeight = 8.0f; /**< 滑块轨道高度。 */
|
||||||
const float kCorner = 6.0f; /**< 面板圆角半径。 */
|
const float kCorner = 6.0f; /**< 面板圆角半径。 */
|
||||||
/** 色域方块 / 色相条 / 预览色块 / 色号框共用同一圆角(视觉上统一)。 */
|
/** 色域方块 / 色相条 / 预览色块 / 色号框共用同一圆角(视觉上统一)。 */
|
||||||
@@ -32,7 +35,7 @@ namespace jdi
|
|||||||
const float kGamutGap = 10.0f; /**< 色域方块与色相条间距。 */
|
const float kGamutGap = 10.0f; /**< 色域方块与色相条间距。 */
|
||||||
const float kColumnGap = 12.0f; /**< 色相条与滑块列间距。 */
|
const float kColumnGap = 12.0f; /**< 色相条与滑块列间距。 */
|
||||||
const float kSwatchWidth = 52.0f; /**< 预览色块宽度。 */
|
const float kSwatchWidth = 52.0f; /**< 预览色块宽度。 */
|
||||||
const float kSwatchHexGap = 8.0f; /**< 预览色块与色号间距。 */
|
const float kSwatchHexGap = 8.0f; /**< 预览色块与色号间距(下限,空间不足时以右对齐优先)。 */
|
||||||
const float kGamutBorder = 1.0f; /**< 色域方块/色相条描边宽度。 */
|
const float kGamutBorder = 1.0f; /**< 色域方块/色相条描边宽度。 */
|
||||||
|
|
||||||
/** @brief 通道显示名称。 */
|
/** @brief 通道显示名称。 */
|
||||||
@@ -220,13 +223,14 @@ namespace jdi
|
|||||||
|
|
||||||
void ColorPickerPopup::BuildLayout(const gd::RectF& bounds, const gd::RectF& anchor)
|
void ColorPickerPopup::BuildLayout(const gd::RectF& bounds, const gd::RectF& anchor)
|
||||||
{
|
{
|
||||||
// 布局:左侧一列放"色域方块 + 色相条",右侧一列放 R/G/B 三行
|
// 布局:左侧一列放"色域方块 + 色相条",右侧一列**上方**放"预览色块 + 色号"、
|
||||||
//(每行 = 通道标签 + 滑块 + 数值),两列下方再放"预览色块 + 色号"一行。
|
// **下方**放 R/G/B 三行(每行 = 通道标签 + 滑块 + 数值)。
|
||||||
|
// 右列总高 = 预览行 + 间隙 + 三行滑块,与左侧"色域方块"高度**严格相等**,
|
||||||
|
// 因此两列上下边对齐、整体版面协调(不再有单起一行的底部预览条)。
|
||||||
// 所有控件都从面板内边距起算,因此不会出现越界。
|
// 所有控件都从面板内边距起算,因此不会出现越界。
|
||||||
const float width = kPanelWidth * m_scale;
|
const float width = kPanelWidth * m_scale;
|
||||||
const float height = (kPadV * 2.0f + kTitleHeight + kTitleGap + kGamutSize + kPreviewGap +
|
const float height =
|
||||||
kPreviewHeight) *
|
(kPadV * 2.0f + kTitleHeight + kTitleGap + kGamutSize) * m_scale;
|
||||||
m_scale;
|
|
||||||
|
|
||||||
const float gap = 4.0f * m_scale;
|
const float gap = 4.0f * m_scale;
|
||||||
|
|
||||||
@@ -260,38 +264,49 @@ namespace jdi
|
|||||||
m_hueRect = gd::RectF(m_gamutRect.X + m_gamutRect.Width + kGamutGap * m_scale, contentTop,
|
m_hueRect = gd::RectF(m_gamutRect.X + m_gamutRect.Width + kGamutGap * m_scale, contentTop,
|
||||||
kHueWidth * m_scale, kGamutSize * m_scale);
|
kHueWidth * m_scale, kGamutSize * m_scale);
|
||||||
|
|
||||||
// 右列:R/G/B 三行。整列在方块高度内纵向居中,与左侧方块视觉平衡。
|
// 右列:预览行在滑块之上,整列高度与左侧方块一致。
|
||||||
const float leftColumnWidth =
|
const float leftColumnWidth = (kGamutSize + kGamutGap + kHueWidth) * m_scale;
|
||||||
(kGamutSize + kGamutGap + kHueWidth) * m_scale;
|
|
||||||
const float columnLeft = m_gamutRect.X + leftColumnWidth + kColumnGap * m_scale;
|
const float columnLeft = m_gamutRect.X + leftColumnWidth + kColumnGap * m_scale;
|
||||||
const float columnWidth = std::max(60.0f * m_scale, contentRight - columnLeft);
|
const float columnWidth = std::max(60.0f * m_scale, contentRight - columnLeft);
|
||||||
const float rowsHeight = (kRowHeight * 3.0f + kRowGap * 2.0f) * m_scale;
|
|
||||||
const float firstRowTop =
|
|
||||||
contentTop + std::max(0.0f, (kGamutSize * m_scale - rowsHeight) * 0.5f);
|
|
||||||
|
|
||||||
const float trackLeft = columnLeft + (kLabelWidth + kLabelGap) * m_scale;
|
// 预览行与滑块之间的间隙:正好补齐"方块高度 − 预览行高 − 三行滑块高",
|
||||||
const float trackWidth =
|
// 使右列总高与左侧色域方块严格相等(常量变化时自动适配)。
|
||||||
std::max(24.0f, columnWidth - (kLabelWidth + kLabelGap + kValueWidth + 6.0f) * m_scale);
|
const float previewRowGap =
|
||||||
|
std::max(0.0f, kGamutSize - kPreviewHeight - (kRowHeight * 3.0f + kRowGap * 2.0f)) * m_scale;
|
||||||
|
|
||||||
|
// 通道数值区固定占 **3 个字符宽**(0-255 最多三位):与数字本身的宽度无关,
|
||||||
|
// 这样三行数值的右边界严格一致(右对齐后三列数字纵向对齐)。
|
||||||
|
const float valueWidth =
|
||||||
|
std::max(1.0f, UiMeasureTextWidth(m_fonts->monoSmall, L"000"));
|
||||||
|
// 轨道两侧用**同一个**间隙(用户要求两侧留白相等)。
|
||||||
|
const float sliderGap = kSliderGap * m_scale;
|
||||||
|
const float labelWidth = kLabelWidth * m_scale;
|
||||||
|
const float trackLeft = columnLeft + labelWidth + sliderGap;
|
||||||
|
const float trackWidth = std::max(
|
||||||
|
24.0f, columnWidth - (labelWidth + sliderGap + sliderGap + valueWidth));
|
||||||
|
const float valueLeft = trackLeft + trackWidth + sliderGap;
|
||||||
|
const float valueRight = valueLeft + valueWidth;
|
||||||
|
|
||||||
|
// 预览色块的**左边界与轨道左边界对齐**(长宽不变)。
|
||||||
|
m_previewRect =
|
||||||
|
gd::RectF(trackLeft, contentTop, kSwatchWidth * m_scale, kPreviewHeight * m_scale);
|
||||||
|
// 色号 "#RRGGBB" 固定 **6+1 个字符宽**,并且**右边界与三行数值的右边界对齐**。
|
||||||
|
const float hexWidth = UiMeasureTextWidth(m_fonts->monoSmall, L"#000000");
|
||||||
|
const float hexLeft = std::max(m_previewRect.X + m_previewRect.Width + kSwatchHexGap * m_scale,
|
||||||
|
valueRight - hexWidth);
|
||||||
|
m_hexRect = gd::RectF(hexLeft, contentTop, valueRight - hexLeft, kPreviewHeight * m_scale);
|
||||||
|
|
||||||
|
const float sliderTop = m_previewRect.Y + m_previewRect.Height + previewRowGap;
|
||||||
|
|
||||||
for (int index = 0; index < 3; ++index)
|
for (int index = 0; index < 3; ++index)
|
||||||
{
|
{
|
||||||
const float rowTop = firstRowTop + static_cast<float>(index) * (kRowHeight + kRowGap) * m_scale;
|
const float rowTop = sliderTop + static_cast<float>(index) * (kRowHeight + kRowGap) * m_scale;
|
||||||
m_tracks[static_cast<std::size_t>(index)] =
|
m_tracks[static_cast<std::size_t>(index)] =
|
||||||
gd::RectF(trackLeft, rowTop + (kRowHeight * m_scale - kTrackHeight * m_scale) * 0.5f,
|
gd::RectF(trackLeft, rowTop + (kRowHeight * m_scale - kTrackHeight * m_scale) * 0.5f,
|
||||||
trackWidth, kTrackHeight * m_scale);
|
trackWidth, kTrackHeight * m_scale);
|
||||||
m_values[static_cast<std::size_t>(index)] =
|
m_values[static_cast<std::size_t>(index)] =
|
||||||
gd::RectF(trackLeft + trackWidth + 6.0f * m_scale, rowTop, kValueWidth * m_scale,
|
gd::RectF(valueLeft, rowTop, valueWidth, kRowHeight * m_scale);
|
||||||
kRowHeight * m_scale);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下行:预览色块 + 色号,与上方的"方块 + 色相条"同宽。
|
|
||||||
const float previewTop = contentTop + (kGamutSize + kPreviewGap) * m_scale;
|
|
||||||
m_previewRect =
|
|
||||||
gd::RectF(contentLeft, previewTop, kSwatchWidth * m_scale, kPreviewHeight * m_scale);
|
|
||||||
m_hexRect = gd::RectF(m_previewRect.X + m_previewRect.Width + kSwatchHexGap * m_scale,
|
|
||||||
previewTop,
|
|
||||||
std::max(40.0f * m_scale, leftColumnWidth - (kSwatchWidth + kSwatchHexGap) * m_scale),
|
|
||||||
kPreviewHeight * m_scale);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ColorPickerPopup::Open(const gd::RectF& bounds, const gd::RectF& anchor, std::uint32_t color)
|
void ColorPickerPopup::Open(const gd::RectF& bounds, const gd::RectF& anchor, std::uint32_t color)
|
||||||
@@ -448,19 +463,42 @@ namespace jdi
|
|||||||
switch (m_editField)
|
switch (m_editField)
|
||||||
{
|
{
|
||||||
case EditField::R:
|
case EditField::R:
|
||||||
return m_values[0];
|
return PaddedEditRect(m_values[0]);
|
||||||
case EditField::G:
|
case EditField::G:
|
||||||
return m_values[1];
|
return PaddedEditRect(m_values[1]);
|
||||||
case EditField::B:
|
case EditField::B:
|
||||||
return m_values[2];
|
return PaddedEditRect(m_values[2]);
|
||||||
case EditField::HEX:
|
case EditField::HEX:
|
||||||
return m_hexRect;
|
return PaddedEditRect(m_hexRect);
|
||||||
case EditField::NONE:
|
case EditField::NONE:
|
||||||
default:
|
default:
|
||||||
return gd::RectF();
|
return gd::RectF();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gd::RectF ColorPickerPopup::PaddedEditRect(const gd::RectF& textRect) const
|
||||||
|
{
|
||||||
|
// 与 `UiDrawEditField` 的内边距(6*scale)一致:文本区向外扩这么多,
|
||||||
|
// 编辑态才能完整显示 3 位数值 / 7 位色号(否则最后一个字符被裁掉)。
|
||||||
|
const float padding = 6.0f * m_scale;
|
||||||
|
return gd::RectF(textRect.X - padding, textRect.Y, textRect.Width + padding * 2.0f,
|
||||||
|
textRect.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
gd::RectF ColorPickerPopup::ChannelLabelRect(int index) const
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= 3)
|
||||||
|
{
|
||||||
|
return gd::RectF();
|
||||||
|
}
|
||||||
|
const std::size_t slot = static_cast<std::size_t>(index);
|
||||||
|
const gd::RectF& track = m_tracks[slot];
|
||||||
|
// 间隙直接由实际几何推出(轨道右端 → 数值区左端),因此标签侧与数值侧**必然**相等。
|
||||||
|
const float gap = std::max(0.0f, m_values[slot].X - (track.X + track.Width));
|
||||||
|
return gd::RectF(track.X - gap - kLabelWidth * m_scale, m_values[slot].Y,
|
||||||
|
kLabelWidth * m_scale, kRowHeight * m_scale);
|
||||||
|
}
|
||||||
|
|
||||||
bool ColorPickerPopup::BeginEdit(EditField field)
|
bool ColorPickerPopup::BeginEdit(EditField field)
|
||||||
{
|
{
|
||||||
if (!m_open || field == EditField::NONE)
|
if (!m_open || field == EditField::NONE)
|
||||||
@@ -797,16 +835,18 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 数值与色号文本优先于滑块:两者几何重叠时要能进入输入状态。
|
// 数值与色号文本优先于滑块:两者几何重叠时要能进入输入状态。
|
||||||
|
// 命中区用**编辑框**矩形(比 3 / 7 个字符宽的文本区宽出输入框内边距):
|
||||||
|
// 文本区只有 3 个字符宽,命中区太小不便点击。
|
||||||
for (int index = 0; index < 3; ++index)
|
for (int index = 0; index < 3; ++index)
|
||||||
{
|
{
|
||||||
if (UiPointInRect(m_values[static_cast<std::size_t>(index)], static_cast<float>(x),
|
if (UiPointInRect(PaddedEditRect(m_values[static_cast<std::size_t>(index)]),
|
||||||
static_cast<float>(y)))
|
static_cast<float>(x), static_cast<float>(y)))
|
||||||
{
|
{
|
||||||
BeginEdit(static_cast<EditField>(index + 1));
|
BeginEdit(static_cast<EditField>(index + 1));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (UiPointInRect(m_hexRect, static_cast<float>(x), static_cast<float>(y)))
|
if (UiPointInRect(PaddedEditRect(m_hexRect), static_cast<float>(x), static_cast<float>(y)))
|
||||||
{
|
{
|
||||||
BeginEdit(EditField::HEX);
|
BeginEdit(EditField::HEX);
|
||||||
return true;
|
return true;
|
||||||
@@ -925,35 +965,57 @@ namespace jdi
|
|||||||
// ---- 色域方块(横轴饱和度、纵轴明度)----
|
// ---- 色域方块(横轴饱和度、纵轴明度)----
|
||||||
// 方块按当前色相预渲染到缓存位图里,避免每帧重画上百条渐变;
|
// 方块按当前色相预渲染到缓存位图里,避免每帧重画上百条渐变;
|
||||||
// 色相变化时才重建一次。
|
// 色相变化时才重建一次。
|
||||||
|
//
|
||||||
|
// ⚠️ 填充必须**裁剪进圆角路径**里:
|
||||||
|
// * 缓存的位图是直角矩形,直接贴上去会在四个角的多出来像素(用户报告的"角落里出现
|
||||||
|
// 莫名其妙的黑色像素点"——方块左下/右下本身就是接近黑色的饱和/明度极值)。
|
||||||
|
// * `WrapModeTileFlipXY` 让采样到边界之外的插值镜像取边,不再混入透明黑,
|
||||||
|
// 边界也就不会再被"压暗"出一圈深色。
|
||||||
if (EnsureGamutCache())
|
if (EnsureGamutCache())
|
||||||
{
|
{
|
||||||
g.DrawImage(m_gamutCache.get(), m_gamutRect);
|
gd::GraphicsPath gamutPath;
|
||||||
|
UiBuildRoundRectPath(gamutPath, m_gamutRect, kSwatchCorner * m_scale);
|
||||||
|
gd::ImageAttributes attributes;
|
||||||
|
attributes.SetWrapMode(gd::WrapModeTileFlipXY, gd::Color(), FALSE);
|
||||||
|
const int saveState = g.Save();
|
||||||
|
g.SetClip(&gamutPath);
|
||||||
|
g.DrawImage(m_gamutCache.get(), m_gamutRect, 0.0f, 0.0f,
|
||||||
|
static_cast<gd::REAL>(m_gamutCacheSize),
|
||||||
|
static_cast<gd::REAL>(m_gamutCacheSize), gd::UnitPixel, &attributes);
|
||||||
|
g.Restore(saveState);
|
||||||
}
|
}
|
||||||
UiDrawRoundRect(g, m_gamutRect, kSwatchCorner * m_scale, m_theme->border, kGamutBorder);
|
UiDrawRoundRect(g, m_gamutRect, kSwatchCorner * m_scale, m_theme->border, kGamutBorder);
|
||||||
|
|
||||||
// ---- 色相条(纵向 0-1):6 段线性渐变拼出完整色相环 ----
|
// ---- 色相条(纵向 0-1)----
|
||||||
{
|
{
|
||||||
for (int segment = 0; segment < 6; ++segment)
|
// 一条带 7 个色标的线性渐变画满整条(0、1/6 … 1,每段内 RGB 线性):
|
||||||
|
// 与"6 段拼色"的颜色完全一致,但不会有段与段之间的接缝横线。
|
||||||
|
gd::Color stops[7];
|
||||||
|
gd::REAL positions[7];
|
||||||
|
for (int index = 0; index < 7; ++index)
|
||||||
{
|
{
|
||||||
const double hue0 = static_cast<double>(segment) / 6.0;
|
const double hue = static_cast<double>(index) / 6.0;
|
||||||
const double hue1 = static_cast<double>(segment + 1) / 6.0;
|
const std::array<double, 3> rgb = FromHsv((hue > 1.0) ? 1.0 : hue, 1.0, 1.0);
|
||||||
const std::array<double, 3> rgb0 = FromHsv(hue0, 1.0, 1.0);
|
stops[index] = ToGdiColor(0xFF000000u | (ToByte(rgb[0]) << 16) | (ToByte(rgb[1]) << 8) |
|
||||||
const std::array<double, 3> rgb1 = FromHsv(hue1, 1.0, 1.0);
|
ToByte(rgb[2]));
|
||||||
const std::uint32_t color0 =
|
positions[index] = static_cast<gd::REAL>(static_cast<double>(index) / 6.0);
|
||||||
0xFF000000u | (ToByte(rgb0[0]) << 16) | (ToByte(rgb0[1]) << 8) | ToByte(rgb0[2]);
|
|
||||||
const std::uint32_t color1 =
|
|
||||||
0xFF000000u | (ToByte(rgb1[0]) << 16) | (ToByte(rgb1[1]) << 8) | ToByte(rgb1[2]);
|
|
||||||
|
|
||||||
const float top = m_hueRect.Y + m_hueRect.Height * static_cast<float>(hue0);
|
|
||||||
const float bottom = m_hueRect.Y + m_hueRect.Height * static_cast<float>(hue1);
|
|
||||||
const gd::Point from(static_cast<INT>(std::lround(m_hueRect.X)),
|
|
||||||
static_cast<INT>(std::lround(top)));
|
|
||||||
const gd::Point to(static_cast<INT>(std::lround(m_hueRect.X)),
|
|
||||||
static_cast<INT>(std::lround(bottom)));
|
|
||||||
gd::LinearGradientBrush brush(from, to, ToGdiColor(color0), ToGdiColor(color1));
|
|
||||||
// 渐变端点会各自向两侧延伸半段,逐段微重叠地填充即可无缝拼接。
|
|
||||||
g.FillRectangle(&brush, m_hueRect.X, top, m_hueRect.Width, bottom - top);
|
|
||||||
}
|
}
|
||||||
|
gd::LinearGradientBrush brush(
|
||||||
|
gd::Point(static_cast<INT>(std::lround(m_hueRect.X)),
|
||||||
|
static_cast<INT>(std::lround(m_hueRect.Y))),
|
||||||
|
gd::Point(static_cast<INT>(std::lround(m_hueRect.X)),
|
||||||
|
static_cast<INT>(std::lround(m_hueRect.Y + m_hueRect.Height))),
|
||||||
|
stops[0], stops[6]);
|
||||||
|
brush.SetInterpolationColors(stops, positions, 7);
|
||||||
|
|
||||||
|
// 与色域方块**共用同一个圆角**(`kSwatchCorner`),并且同样裁剪进圆角路径:
|
||||||
|
// 直角填充会在圆角描边之外露出四个尖角(用户报告的"四个角的杂色像素点")。
|
||||||
|
gd::GraphicsPath huePath;
|
||||||
|
UiBuildRoundRectPath(huePath, m_hueRect, kSwatchCorner * m_scale);
|
||||||
|
const int saveState = g.Save();
|
||||||
|
g.SetClip(&huePath);
|
||||||
|
g.FillRectangle(&brush, m_hueRect);
|
||||||
|
g.Restore(saveState);
|
||||||
UiDrawRoundRect(g, m_hueRect, kSwatchCorner * m_scale, m_theme->border, kGamutBorder);
|
UiDrawRoundRect(g, m_hueRect, kSwatchCorner * m_scale, m_theme->border, kGamutBorder);
|
||||||
|
|
||||||
// ---- 色域方块上的选点:中心就是当前选中的颜色,外面只加一圈白色描边 ----
|
// ---- 色域方块上的选点:中心就是当前选中的颜色,外面只加一圈白色描边 ----
|
||||||
@@ -989,17 +1051,19 @@ namespace jdi
|
|||||||
const std::size_t slot = static_cast<std::size_t>(index);
|
const std::size_t slot = static_cast<std::size_t>(index);
|
||||||
const gd::RectF& track = m_tracks[slot];
|
const gd::RectF& track = m_tracks[slot];
|
||||||
|
|
||||||
// 通道标签放在滑块同一行的左侧,与轨道/数值纵向对齐。
|
// 通道标签放在滑块同一行的左侧,**右对齐**到"轨道左端 − 间隙":
|
||||||
const gd::RectF labelRect(track.X - (kLabelWidth + kLabelGap) * m_scale, m_values[slot].Y,
|
// 这样 R/G/B 三个字母的右边界与轨道的距离严格等于轨道到数值区的距离
|
||||||
kLabelWidth * m_scale, kRowHeight * m_scale);
|
// (用户要求两侧留白相同,且三个字母纵向对齐)。
|
||||||
UiText(g, kChannelNames[index], m_fonts->uiSmall, labelRect, m_theme->textMuted, TextAlign::LEFT);
|
const gd::RectF labelRect = ChannelLabelRect(index);
|
||||||
|
UiText(g, kChannelNames[index], m_fonts->uiSmall, labelRect, m_theme->textMuted,
|
||||||
|
TextAlign::RIGHT);
|
||||||
|
|
||||||
const bool active = (m_draggingSlider == index) || (m_hoverSlider == index);
|
const bool active = (m_draggingSlider == index) || (m_hoverSlider == index);
|
||||||
DrawChannelSlider(g, track, m_channels[slot], kChannelColors[index], active, m_scale);
|
DrawChannelSlider(g, track, m_channels[slot], kChannelColors[index], active, m_scale);
|
||||||
|
|
||||||
if (m_editField == static_cast<EditField>(index + 1))
|
if (m_editField == static_cast<EditField>(index + 1))
|
||||||
{
|
{
|
||||||
UiDrawEditField(g, m_values[slot], m_edit, m_fonts->monoSmall, m_scale);
|
UiDrawEditField(g, PaddedEditRect(m_values[slot]), m_edit, m_fonts->monoSmall, m_scale);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1015,14 +1079,16 @@ namespace jdi
|
|||||||
|
|
||||||
if (m_editField == EditField::HEX)
|
if (m_editField == EditField::HEX)
|
||||||
{
|
{
|
||||||
UiDrawEditField(g, m_hexRect, m_edit, m_fonts->monoSmall, m_scale);
|
UiDrawEditField(g, PaddedEditRect(m_hexRect), m_edit, m_fonts->monoSmall, m_scale);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
wchar_t buffer[16];
|
wchar_t buffer[16];
|
||||||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"#%02X%02X%02X", (color >> 16) & 0xFFu,
|
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"#%02X%02X%02X", (color >> 16) & 0xFFu,
|
||||||
(color >> 8) & 0xFFu, color & 0xFFu);
|
(color >> 8) & 0xFFu, color & 0xFFu);
|
||||||
UiText(g, std::wstring(buffer), m_fonts->monoSmall, m_hexRect, m_theme->text, TextAlign::LEFT);
|
// 与三行数值一样**右对齐**:色号的最后一位与数值的最后一位纵向对齐。
|
||||||
|
UiText(g, std::wstring(buffer), m_fonts->monoSmall, m_hexRect, m_theme->text,
|
||||||
|
TextAlign::RIGHT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1583,6 +1583,84 @@ namespace jdi
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SampleDisplayedSignalAt(const DataFile& file, const SignalData& signal,
|
||||||
|
const SignalStyle& style, double time, bool interpolate, double& value)
|
||||||
|
{
|
||||||
|
// 未启用水平镜像 / 时间偏移时与 SampleSignalAt 完全等价
|
||||||
|
//(避免两套实现产生细微差异)。
|
||||||
|
if (!style.mirrorHorizontal && style.offsetTime == 0.0)
|
||||||
|
{
|
||||||
|
return SampleSignalAt(file, signal, time, interpolate, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
value = kMissing;
|
||||||
|
if (file.time.empty() || signal.values.empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 绘制位置 = 源时刻 + offsetTime,因此查询源数据要用「绘制时刻 − offsetTime」。
|
||||||
|
const double query = time - style.offsetTime;
|
||||||
|
if (query < file.minTime || query > file.maxTime)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<double>& values = signal.values;
|
||||||
|
const std::size_t count = std::min(file.time.size(), values.size());
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 画面上第 i 个位置用源下标 srcIndex(i) 的值(水平镜像 = 数据反序)。
|
||||||
|
const auto sample = [&](std::size_t index) -> double
|
||||||
|
{
|
||||||
|
return values[SourceIndexFor(style, count, index)];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!file.timeMonotonic)
|
||||||
|
{
|
||||||
|
const int nearest = FindNearestSample(file, query);
|
||||||
|
if (nearest < 0 || static_cast<std::size_t>(nearest) >= count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
value = sample(static_cast<std::size_t>(nearest));
|
||||||
|
return std::isfinite(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<double>::const_iterator lower =
|
||||||
|
std::lower_bound(file.time.begin(), file.time.end(), query);
|
||||||
|
const std::size_t rightIndex = static_cast<std::size_t>(lower - file.time.begin());
|
||||||
|
if (rightIndex == 0)
|
||||||
|
{
|
||||||
|
value = sample(0);
|
||||||
|
return std::isfinite(value);
|
||||||
|
}
|
||||||
|
if (rightIndex >= count)
|
||||||
|
{
|
||||||
|
value = sample(count - 1);
|
||||||
|
return std::isfinite(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t leftIndex = rightIndex - 1;
|
||||||
|
const double t0 = file.time[leftIndex];
|
||||||
|
const double t1 = file.time[rightIndex];
|
||||||
|
const double v0 = sample(leftIndex);
|
||||||
|
const double v1 = sample(rightIndex);
|
||||||
|
|
||||||
|
if (!interpolate || !std::isfinite(v0) || !std::isfinite(v1) || !(t1 > t0))
|
||||||
|
{
|
||||||
|
const double leftDistance = std::fabs(query - t0);
|
||||||
|
const double rightDistance = std::fabs(t1 - query);
|
||||||
|
value = (leftDistance <= rightDistance) ? v0 : v1;
|
||||||
|
return std::isfinite(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const double ratio = (query - t0) / (t1 - t0);
|
||||||
|
value = v0 + (v1 - v0) * ratio;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================ TabConfig ============================
|
// ============================ TabConfig ============================
|
||||||
|
|
||||||
TabConfig::TabConfig()
|
TabConfig::TabConfig()
|
||||||
@@ -2234,8 +2312,17 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
low = std::min(low, signal->minValue);
|
// ⚠️ 自适应必须基于**画出来的曲线**:先按绘制变换(系数 → 绝对值 → 镜像)把该信号的
|
||||||
high = std::max(high, signal->maxValue);
|
// 原始值域映射成显示值域,否则调整系数/镜像后自适应范围仍按原始数据算(范围错误)。
|
||||||
|
const SignalStyle* style = dataSet.Style(ref);
|
||||||
|
double signalLow = signal->minValue;
|
||||||
|
double signalHigh = signal->maxValue;
|
||||||
|
if (style != nullptr)
|
||||||
|
{
|
||||||
|
TransformValueRange(*style, signalLow, signalHigh);
|
||||||
|
}
|
||||||
|
low = std::min(low, signalLow);
|
||||||
|
high = std::max(high, signalHigh);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!std::isfinite(low) || !std::isfinite(high))
|
if (!std::isfinite(low) || !std::isfinite(high))
|
||||||
|
|||||||
+37
-38
@@ -29,45 +29,44 @@ namespace jdi
|
|||||||
* 固定开销反而不划算,因此在阈值以下仍走 GDI+。
|
* 固定开销反而不划算,因此在阈值以下仍走 GDI+。
|
||||||
*/
|
*/
|
||||||
const float kGdiFillAreaThreshold = 20000.0f;
|
const float kGdiFillAreaThreshold = 20000.0f;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
// 圆角矩形路径是公开的几何构建器:`UiFillRoundRect` / `UiDrawRoundRect` / 需要把位图或渐变
|
||||||
* @brief 构造圆角矩形路径。
|
// **裁剪进同一个圆角形状**的调用方共用它,保证"填充的形状"与"描边的形状"逐点一致。
|
||||||
* @param [IN,OUT] path 目标路径对象。
|
void UiBuildRoundRectPath(gd::GraphicsPath& path, const gd::RectF& rect, float radius)
|
||||||
* @param [IN] rect 目标矩形。
|
{
|
||||||
* @param [IN] radius 圆角半径。
|
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
|
||||||
*/
|
|
||||||
void BuildRoundRectPath(gd::GraphicsPath& path, const gd::RectF& rect, float radius)
|
|
||||||
{
|
{
|
||||||
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
|
return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
float diameter = radius * 2.0f;
|
|
||||||
if (diameter > rect.Width)
|
|
||||||
{
|
|
||||||
diameter = rect.Width;
|
|
||||||
}
|
|
||||||
if (diameter > rect.Height)
|
|
||||||
{
|
|
||||||
diameter = rect.Height;
|
|
||||||
}
|
|
||||||
if (diameter < 1.0f)
|
|
||||||
{
|
|
||||||
path.AddRectangle(rect);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const float right = rect.X + rect.Width;
|
|
||||||
const float bottom = rect.Y + rect.Height;
|
|
||||||
|
|
||||||
path.AddArc(rect.X, rect.Y, diameter, diameter, 180.0f, 90.0f);
|
|
||||||
path.AddArc(right - diameter, rect.Y, diameter, diameter, 270.0f, 90.0f);
|
|
||||||
path.AddArc(right - diameter, bottom - diameter, diameter, diameter, 0.0f, 90.0f);
|
|
||||||
path.AddArc(rect.X, bottom - diameter, diameter, diameter, 90.0f, 90.0f);
|
|
||||||
path.CloseFigure();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float diameter = radius * 2.0f;
|
||||||
|
if (diameter > rect.Width)
|
||||||
|
{
|
||||||
|
diameter = rect.Width;
|
||||||
|
}
|
||||||
|
if (diameter > rect.Height)
|
||||||
|
{
|
||||||
|
diameter = rect.Height;
|
||||||
|
}
|
||||||
|
if (diameter < 1.0f)
|
||||||
|
{
|
||||||
|
path.AddRectangle(rect);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float right = rect.X + rect.Width;
|
||||||
|
const float bottom = rect.Y + rect.Height;
|
||||||
|
|
||||||
|
path.AddArc(rect.X, rect.Y, diameter, diameter, 180.0f, 90.0f);
|
||||||
|
path.AddArc(right - diameter, rect.Y, diameter, diameter, 270.0f, 90.0f);
|
||||||
|
path.AddArc(right - diameter, bottom - diameter, diameter, diameter, 0.0f, 90.0f);
|
||||||
|
path.AddArc(rect.X, bottom - diameter, diameter, diameter, 90.0f, 90.0f);
|
||||||
|
path.CloseFigure();
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* @brief 创建指定字号的界面字体。
|
* @brief 创建指定字号的界面字体。
|
||||||
* @param [IN] face 字体名称。
|
* @param [IN] face 字体名称。
|
||||||
@@ -224,7 +223,7 @@ namespace jdi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
gd::GraphicsPath path;
|
gd::GraphicsPath path;
|
||||||
BuildRoundRectPath(path, rect, radius);
|
UiBuildRoundRectPath(path, rect, radius);
|
||||||
gd::SolidBrush brush(ToGdiColor(argb));
|
gd::SolidBrush brush(ToGdiColor(argb));
|
||||||
g.FillPath(&brush, &path);
|
g.FillPath(&brush, &path);
|
||||||
}
|
}
|
||||||
@@ -236,7 +235,7 @@ namespace jdi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
gd::GraphicsPath path;
|
gd::GraphicsPath path;
|
||||||
BuildRoundRectPath(path, rect, radius);
|
UiBuildRoundRectPath(path, rect, radius);
|
||||||
gd::Pen pen(ToGdiColor(argb), width);
|
gd::Pen pen(ToGdiColor(argb), width);
|
||||||
g.DrawPath(&pen, &path);
|
g.DrawPath(&pen, &path);
|
||||||
}
|
}
|
||||||
@@ -255,7 +254,7 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
BuildRoundRectPath(path, rect, radius);
|
UiBuildRoundRectPath(path, rect, radius);
|
||||||
}
|
}
|
||||||
if (path.GetPointCount() > 0)
|
if (path.GetPointCount() > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
+240
-15
@@ -27,6 +27,54 @@ namespace jdi
|
|||||||
const float kEditFieldPadV = 5.0f; /**< 输入框上下内缩。 */
|
const float kEditFieldPadV = 5.0f; /**< 输入框上下内缩。 */
|
||||||
const float kSubArrowSize = 4.5f; /**< 子菜单指示箭头尺寸。 */
|
const float kSubArrowSize = 4.5f; /**< 子菜单指示箭头尺寸。 */
|
||||||
const float kSubOverlap = 2.0f; /**< 子菜单与主菜单重叠的宽度(避免中间露出缝隙)。 */
|
const float kSubOverlap = 2.0f; /**< 子菜单与主菜单重叠的宽度(避免中间露出缝隙)。 */
|
||||||
|
const float kHeaderIconSize = 16.0f; /**< `HEADER_ACTION` 右侧图标按钮的边长。 */
|
||||||
|
const float kHeaderIconGap = 6.0f; /**< 标题文字与图标按钮之间的最小间隔。 */
|
||||||
|
const float kRowFieldWidth = 132.0f; /**< 整行输入框(无说明文字)的默认宽度。 */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 绘制「还原/重置」图标(逆时针回环箭头)。
|
||||||
|
*
|
||||||
|
* 用一段圆弧 + 一个三角箭头拼成,与界面里其它自绘图标(加号、叉号)保持同一笔画风格。
|
||||||
|
* 不依赖任何字体或资源,因此在任何 DPI 下都清晰。
|
||||||
|
*
|
||||||
|
* @param [IN,OUT] g 绘图上下文。
|
||||||
|
* @param [IN] rect 图标外接矩形(正方形)。
|
||||||
|
* @param [IN] color 笔画颜色。
|
||||||
|
* @param [IN] scale DPI 缩放比例。
|
||||||
|
*/
|
||||||
|
void PaintResetIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t color, float scale)
|
||||||
|
{
|
||||||
|
const float inset = 2.5f * scale;
|
||||||
|
gd::RectF box(rect.X + inset, rect.Y + inset, rect.Width - inset * 2.0f,
|
||||||
|
rect.Height - inset * 2.0f);
|
||||||
|
if (box.Width <= 1.0f || box.Height <= 1.0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gd::Pen pen(gd::Color((color >> 24) & 0xFFu, (color >> 16) & 0xFFu, (color >> 8) & 0xFFu,
|
||||||
|
color & 0xFFu),
|
||||||
|
std::max(1.0f, 1.5f * scale));
|
||||||
|
pen.SetStartCap(gd::LineCapRound);
|
||||||
|
pen.SetEndCap(gd::LineCapRound);
|
||||||
|
// 缺口留在右上角(从 300° 到 20° 处断开),与常见的"重置/刷新"图标一致。
|
||||||
|
g.DrawArc(&pen, box.X, box.Y, box.Width, box.Height, 300.0f, 300.0f);
|
||||||
|
|
||||||
|
// 箭头:位于缺口末端(约 300° 方向),指向前进方向。
|
||||||
|
const float cx = box.X + box.Width * 0.5f;
|
||||||
|
const float cy = box.Y + box.Height * 0.5f;
|
||||||
|
const float radius = box.Width * 0.5f;
|
||||||
|
const float angle = 300.0f * 3.14159265f / 180.0f;
|
||||||
|
const float tipX = cx + std::cos(angle) * radius;
|
||||||
|
const float tipY = cy + std::sin(angle) * radius;
|
||||||
|
const float size = std::max(2.5f, 3.2f * scale);
|
||||||
|
gd::PointF arrow[3];
|
||||||
|
arrow[0] = gd::PointF(tipX, tipY);
|
||||||
|
arrow[1] = gd::PointF(tipX - size, tipY - size * 0.25f);
|
||||||
|
arrow[2] = gd::PointF(tipX - size * 0.25f, tipY - size);
|
||||||
|
gd::SolidBrush brush(gd::Color((color >> 24) & 0xFFu, (color >> 16) & 0xFFu,
|
||||||
|
(color >> 8) & 0xFFu, color & 0xFFu));
|
||||||
|
g.FillPolygon(&brush, arrow, 3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PopupMenu::PopupMenu() = default;
|
PopupMenu::PopupMenu() = default;
|
||||||
@@ -49,6 +97,7 @@ namespace jdi
|
|||||||
case MenuItemKind::SEPARATOR:
|
case MenuItemKind::SEPARATOR:
|
||||||
return kSeparatorHeight * m_scale;
|
return kSeparatorHeight * m_scale;
|
||||||
case MenuItemKind::HEADER:
|
case MenuItemKind::HEADER:
|
||||||
|
case MenuItemKind::HEADER_ACTION:
|
||||||
return kHeaderHeight * m_scale;
|
return kHeaderHeight * m_scale;
|
||||||
case MenuItemKind::SWATCHES:
|
case MenuItemKind::SWATCHES:
|
||||||
return kSwatchRowHeight * m_scale;
|
return kSwatchRowHeight * m_scale;
|
||||||
@@ -88,11 +137,14 @@ namespace jdi
|
|||||||
static_cast<float>(std::max(0, count - 1)) * kSwatchGap * m_scale;
|
static_cast<float>(std::max(0, count - 1)) * kSwatchGap * m_scale;
|
||||||
contentWidth = std::max(contentWidth, width);
|
contentWidth = std::max(contentWidth, width);
|
||||||
}
|
}
|
||||||
else if (item.kind == MenuItemKind::HEADER)
|
else if (item.kind == MenuItemKind::HEADER || item.kind == MenuItemKind::HEADER_ACTION)
|
||||||
{
|
{
|
||||||
// 标题与普通项共用同一文字列与右侧标记列,保证整个菜单的左右边距一致。
|
// 标题与普通项共用同一文字列与右侧标记列,保证整个菜单的左右边距一致。
|
||||||
|
const float iconExtra = (item.kind == MenuItemKind::HEADER_ACTION)
|
||||||
|
? (kHeaderIconSize + kHeaderIconGap) * m_scale
|
||||||
|
: 0.0f;
|
||||||
const float width = (kPadH + kItemPad) * 2.0f * m_scale + kMarkerColumn * m_scale +
|
const float width = (kPadH + kItemPad) * 2.0f * m_scale + kMarkerColumn * m_scale +
|
||||||
kMarkerGap * m_scale +
|
kMarkerGap * m_scale + iconExtra +
|
||||||
UiMeasureTextWidth(m_fonts->uiSmallBold, item.label);
|
UiMeasureTextWidth(m_fonts->uiSmallBold, item.label);
|
||||||
contentWidth = std::max(contentWidth, width);
|
contentWidth = std::max(contentWidth, width);
|
||||||
}
|
}
|
||||||
@@ -117,6 +169,33 @@ namespace jdi
|
|||||||
const float width = contentWidth;
|
const float width = contentWidth;
|
||||||
const float height = totalHeight;
|
const float height = totalHeight;
|
||||||
|
|
||||||
|
// 带说明文字的 EDIT 项共用**同一个输入框宽度**:取其中最大的自然宽度,
|
||||||
|
// 再按“给最宽的说明文字留位 / 不超过内容区 3/4”夹取。
|
||||||
|
// ⚠️ 若每项各自按自己的文本长度算,同一面板里就会出现一个宽一个窄
|
||||||
|
// (时间轴行的文本更长、还带单位后缀,于是它的输入框比 Y 轴行宽一截)。
|
||||||
|
float labeledFieldWidth = 0.0f;
|
||||||
|
float widestLabel = 0.0f;
|
||||||
|
for (const MenuItem& item : m_items)
|
||||||
|
{
|
||||||
|
if (item.kind != MenuItemKind::EDIT || item.label.empty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
labeledFieldWidth = std::max(labeledFieldWidth, NaturalEditFieldWidth(item));
|
||||||
|
widestLabel = std::max(widestLabel,
|
||||||
|
UiMeasureTextWidth(m_fonts->uiSmall, item.label) +
|
||||||
|
kEditFieldGap * m_scale);
|
||||||
|
}
|
||||||
|
if (labeledFieldWidth > 0.0f)
|
||||||
|
{
|
||||||
|
const float rowContent =
|
||||||
|
std::max(40.0f * m_scale, (width - 2.0f * (kPadH + kItemPad) * m_scale));
|
||||||
|
const float limit = std::max(40.0f * m_scale,
|
||||||
|
std::min(rowContent * 0.75f, rowContent - widestLabel));
|
||||||
|
labeledFieldWidth = std::min(labeledFieldWidth, limit);
|
||||||
|
}
|
||||||
|
m_editFieldWidth = labeledFieldWidth;
|
||||||
|
|
||||||
float left = static_cast<float>(x);
|
float left = static_cast<float>(x);
|
||||||
float top = static_cast<float>(y);
|
float top = static_cast<float>(y);
|
||||||
|
|
||||||
@@ -163,8 +242,13 @@ namespace jdi
|
|||||||
|
|
||||||
float PopupMenu::NaturalEditFieldWidth(const MenuItem& item) const
|
float PopupMenu::NaturalEditFieldWidth(const MenuItem& item) const
|
||||||
{
|
{
|
||||||
return std::max(kEditFieldWidth * m_scale,
|
const std::wstring& widest =
|
||||||
UiMeasureTextWidth(m_fonts->uiSmall, item.value) + 20.0f * m_scale);
|
(item.editValue.size() > item.value.size()) ? item.editValue : item.value;
|
||||||
|
const float natural = std::max(kEditFieldWidth * m_scale,
|
||||||
|
UiMeasureTextWidth(m_fonts->uiSmall, widest) + 20.0f * m_scale);
|
||||||
|
const float withSuffix = natural + EditSuffixWidth(item);
|
||||||
|
// 无说明文字 = 整行输入框(如「系数调整」面板):给一个更宽的默认值,视觉上独占整行。
|
||||||
|
return item.label.empty() ? std::max(withSuffix, kRowFieldWidth * m_scale) : withSuffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PopupMenu::CloseSubmenu()
|
void PopupMenu::CloseSubmenu()
|
||||||
@@ -292,6 +376,15 @@ namespace jdi
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float PopupMenu::EditSuffixWidth(const MenuItem& item) const
|
||||||
|
{
|
||||||
|
if (item.kind != MenuItemKind::EDIT || item.editSuffix.empty())
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
return UiMeasureTextWidth(m_fonts->uiSmall, item.editSuffix) + 8.0f * m_scale;
|
||||||
|
}
|
||||||
|
|
||||||
gd::RectF PopupMenu::EditFieldRect(int index) const
|
gd::RectF PopupMenu::EditFieldRect(int index) const
|
||||||
{
|
{
|
||||||
if (index < 0 || index >= static_cast<int>(m_items.size()))
|
if (index < 0 || index >= static_cast<int>(m_items.size()))
|
||||||
@@ -306,23 +399,56 @@ namespace jdi
|
|||||||
|
|
||||||
// 输入框靠右对齐到条目内容区右边界;宽度取自然宽度,但必须给说明文字留出自然宽度,
|
// 输入框靠右对齐到条目内容区右边界;宽度取自然宽度,但必须给说明文字留出自然宽度,
|
||||||
// 避免菜单被最大宽度夹取后输入框把左侧标签挤成省略号。
|
// 避免菜单被最大宽度夹取后输入框把左侧标签挤成省略号。
|
||||||
|
// ⚠️ 说明文字为空(例如「系数调整」面板的整行输入框)时不设 3/4 上限,
|
||||||
|
// 让输入框独占整行。
|
||||||
|
// ⚠️ 带说明文字的项统一使用排布阶段算好的 `m_editFieldWidth`:
|
||||||
|
// 同一面板里所有这类输入框必须一样宽、右边界对齐。
|
||||||
const gd::RectF& rect = m_itemRects[static_cast<std::size_t>(index)];
|
const gd::RectF& rect = m_itemRects[static_cast<std::size_t>(index)];
|
||||||
const float contentWidth = std::max(40.0f * m_scale, rect.Width - 2.0f * kItemPad * m_scale);
|
const float contentWidth = std::max(40.0f * m_scale, rect.Width - 2.0f * kItemPad * m_scale);
|
||||||
const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kEditFieldGap * m_scale;
|
const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kEditFieldGap * m_scale;
|
||||||
float width = NaturalEditFieldWidth(item);
|
float width = NaturalEditFieldWidth(item);
|
||||||
width = std::min(width, std::max(40.0f * m_scale, contentWidth - labelWidth));
|
width = std::min(width, std::max(40.0f * m_scale, contentWidth - labelWidth));
|
||||||
width = std::min(width, contentWidth * 0.75f);
|
if (item.label.empty())
|
||||||
|
{
|
||||||
|
// 整行输入框(无说明文字):占满内容区,视觉上就是一个独立的一行输入框。
|
||||||
|
width = contentWidth;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (m_editFieldWidth > 0.0f)
|
||||||
|
{
|
||||||
|
width = m_editFieldWidth;
|
||||||
|
}
|
||||||
|
width = std::min(width, std::max(40.0f * m_scale, contentWidth - labelWidth));
|
||||||
|
width = std::min(width, contentWidth * 0.75f);
|
||||||
|
}
|
||||||
|
|
||||||
const float height = rect.Height - kEditFieldPadV * 2.0f * m_scale;
|
const float height = rect.Height - kEditFieldPadV * 2.0f * m_scale;
|
||||||
return gd::RectF(rect.X + rect.Width - kItemPad * m_scale - width,
|
return gd::RectF(rect.X + rect.Width - kItemPad * m_scale - width,
|
||||||
rect.Y + (rect.Height - height) * 0.5f, width, height);
|
rect.Y + (rect.Height - height) * 0.5f, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gd::RectF PopupMenu::HeaderIconRect(int index) const
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= static_cast<int>(m_items.size()))
|
||||||
|
{
|
||||||
|
return gd::RectF();
|
||||||
|
}
|
||||||
|
const MenuItem& item = m_items[static_cast<std::size_t>(index)];
|
||||||
|
if (item.kind != MenuItemKind::HEADER_ACTION)
|
||||||
|
{
|
||||||
|
return gd::RectF();
|
||||||
|
}
|
||||||
|
const gd::RectF& rect = m_itemRects[static_cast<std::size_t>(index)];
|
||||||
|
const float size = std::min(kHeaderIconSize * m_scale, std::max(6.0f, rect.Height - 4.0f * m_scale));
|
||||||
|
return gd::RectF(rect.X + rect.Width - kItemPad * m_scale - size,
|
||||||
|
rect.Y + (rect.Height - size) * 0.5f, size, size);
|
||||||
|
}
|
||||||
|
|
||||||
bool PopupMenu::IsOnEditField(int x, int y) const
|
bool PopupMenu::IsOnEditField(int x, int y) const
|
||||||
{
|
{
|
||||||
if (m_editIndex < 0)
|
if (m_editIndex < 0)
|
||||||
{
|
{ return false;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
const gd::RectF rect = EditFieldRect(m_editIndex);
|
const gd::RectF rect = EditFieldRect(m_editIndex);
|
||||||
return UiPointInRect(rect, static_cast<float>(x), static_cast<float>(y));
|
return UiPointInRect(rect, static_cast<float>(x), static_cast<float>(y));
|
||||||
@@ -340,8 +466,8 @@ namespace jdi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 输入框初值取当前显示值,整段选中,便于直接键入新数值。
|
// 输入框初值取「编辑初值」(未设置时用显示值),整段选中,便于直接键入新数值。
|
||||||
m_edit.Begin(item.value, true, item.filter, 0u);
|
m_edit.Begin(item.editValue.empty() ? item.value : item.editValue, true, item.filter, 0u);
|
||||||
m_editIndex = index;
|
m_editIndex = index;
|
||||||
m_hoverIndex = index;
|
m_hoverIndex = index;
|
||||||
return true;
|
return true;
|
||||||
@@ -365,8 +491,10 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
MenuItem& item = m_items[static_cast<std::size_t>(index)];
|
MenuItem& item = m_items[static_cast<std::size_t>(index)];
|
||||||
// 立即回写显示值,使菜单上的数值与数据层一致(宿主会在派发后再纠正为夹取后的真实值)。
|
// 立即回写显示值,使菜单上的数值与数据层一致(宿主会在派发后再纠正为夹取后的真实值);
|
||||||
|
// 键入文本同时记为**编辑初值**(`value` 与 `editValue` 不一致的行也能正确回显上次输入)。
|
||||||
item.value = text;
|
item.value = text;
|
||||||
|
item.editValue = text;
|
||||||
|
|
||||||
result.valid = true;
|
result.valid = true;
|
||||||
result.scope = m_scope;
|
result.scope = m_scope;
|
||||||
@@ -391,17 +519,23 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PopupMenu::UpdateItemValue(int id, const std::wstring& value)
|
bool PopupMenu::UpdateItemValue(int id, const std::wstring& value, const std::wstring& editValue)
|
||||||
{
|
{
|
||||||
for (MenuItem& item : m_items)
|
for (MenuItem& item : m_items)
|
||||||
{
|
{
|
||||||
if (item.id == id && item.kind == MenuItemKind::EDIT)
|
if (item.id == id && item.kind == MenuItemKind::EDIT)
|
||||||
{
|
{
|
||||||
if (item.value == value)
|
const bool valueSame = (item.value == value);
|
||||||
|
const bool editSame = editValue.empty() || (item.editValue == editValue);
|
||||||
|
if (valueSame && editSame)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
item.value = value;
|
item.value = value;
|
||||||
|
if (!editValue.empty())
|
||||||
|
{
|
||||||
|
item.editValue = editValue;
|
||||||
|
}
|
||||||
RebuildLayout();
|
RebuildLayout();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -509,6 +643,7 @@ namespace jdi
|
|||||||
m_scope = scope;
|
m_scope = scope;
|
||||||
m_open = true;
|
m_open = true;
|
||||||
m_hoverIndex = -1;
|
m_hoverIndex = -1;
|
||||||
|
m_hoverIconIndex = -1;
|
||||||
m_hoverSwatch = -1;
|
m_hoverSwatch = -1;
|
||||||
m_pressedIndex = -1;
|
m_pressedIndex = -1;
|
||||||
m_pressedSwatch = -1;
|
m_pressedSwatch = -1;
|
||||||
@@ -529,6 +664,7 @@ namespace jdi
|
|||||||
m_itemRects.clear();
|
m_itemRects.clear();
|
||||||
m_swatchRects.clear();
|
m_swatchRects.clear();
|
||||||
m_hoverIndex = -1;
|
m_hoverIndex = -1;
|
||||||
|
m_hoverIconIndex = -1;
|
||||||
m_hoverSwatch = -1;
|
m_hoverSwatch = -1;
|
||||||
m_pressedIndex = -1;
|
m_pressedIndex = -1;
|
||||||
m_pressedSwatch = -1;
|
m_pressedSwatch = -1;
|
||||||
@@ -581,8 +717,7 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
if (!UiPointInRect(m_itemRects[index], fx, fy))
|
if (!UiPointInRect(m_itemRects[index], fx, fy))
|
||||||
{
|
{
|
||||||
continue;
|
continue; }
|
||||||
}
|
|
||||||
|
|
||||||
if (item.kind == MenuItemKind::SWATCHES)
|
if (item.kind == MenuItemKind::SWATCHES)
|
||||||
{
|
{
|
||||||
@@ -723,6 +858,29 @@ namespace jdi
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `HEADER_ACTION` 的图标按钮单独跟踪悬停状态(用于高亮,其余标题区域不响应)。
|
||||||
|
{
|
||||||
|
int iconHover = -1;
|
||||||
|
for (std::size_t itemIndex = 0; itemIndex < m_items.size(); ++itemIndex)
|
||||||
|
{
|
||||||
|
if (m_items[itemIndex].kind != MenuItemKind::HEADER_ACTION)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const gd::RectF icon = HeaderIconRect(static_cast<int>(itemIndex));
|
||||||
|
if (icon.Width > 0.0f && UiPointInRect(icon, fx, fy))
|
||||||
|
{
|
||||||
|
iconHover = static_cast<int>(itemIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (iconHover != m_hoverIconIndex)
|
||||||
|
{
|
||||||
|
m_hoverIconIndex = iconHover;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 悬停到带子菜单的项 → 向右展开;移开到其它条目或离开菜单 → 收起。
|
// 悬停到带子菜单的项 → 向右展开;移开到其它条目或离开菜单 → 收起。
|
||||||
if (onSubmenuParent)
|
if (onSubmenuParent)
|
||||||
{
|
{
|
||||||
@@ -857,6 +1015,22 @@ namespace jdi
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `HEADER_ACTION`:只有**点在右侧图标按钮上**才下发命令(标题区域不响应);
|
||||||
|
// 菜单保持打开(这是一个可反复使用的小按钮,如「还原系数」)。
|
||||||
|
if (item.kind == MenuItemKind::HEADER_ACTION)
|
||||||
|
{
|
||||||
|
const gd::RectF icon = HeaderIconRect(index);
|
||||||
|
if (icon.Width > 0.0f && UiPointInRect(icon, static_cast<float>(x), static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
result.valid = true;
|
||||||
|
result.scope = m_scope;
|
||||||
|
result.id = item.id;
|
||||||
|
result.swatchIndex = -1;
|
||||||
|
result.itemRect = icon;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 编辑项:单击进入菜单内联编辑,菜单保持打开。
|
// 编辑项:单击进入菜单内联编辑,菜单保持打开。
|
||||||
if (item.kind == MenuItemKind::EDIT)
|
if (item.kind == MenuItemKind::EDIT)
|
||||||
{
|
{
|
||||||
@@ -880,6 +1054,12 @@ namespace jdi
|
|||||||
result.swatchIndex = swatchIndex;
|
result.swatchIndex = swatchIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 带复选框的切换项(如镜像的水平/垂直)选择后**保持菜单打开**,便于连续勾选。
|
||||||
|
if (item.keepOpen)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Close();
|
Close();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1043,6 +1223,29 @@ namespace jdi
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.kind == MenuItemKind::HEADER_ACTION)
|
||||||
|
{
|
||||||
|
// 标题 + 右侧「还原」图标按钮:文字与普通标题同一列;图标可点击(悬停高亮)。
|
||||||
|
const gd::RectF icon = HeaderIconRect(static_cast<int>(index));
|
||||||
|
const float labelWidth =
|
||||||
|
std::max(8.0f, icon.X - rect.X - kItemPad * m_scale - kHeaderIconGap * m_scale);
|
||||||
|
UiText(g, UiEllipsize(item.label, m_fonts->uiSmallBold, labelWidth), m_fonts->uiSmallBold,
|
||||||
|
gd::RectF(rect.X + kItemPad * m_scale, rect.Y, labelWidth, rect.Height),
|
||||||
|
m_theme->textMuted, TextAlign::LEFT);
|
||||||
|
if (icon.Width > 0.0f)
|
||||||
|
{
|
||||||
|
const bool iconHovered = (static_cast<int>(index) == m_hoverIconIndex);
|
||||||
|
if (iconHovered)
|
||||||
|
{
|
||||||
|
UiFillRoundRect(g, gd::RectF(icon.X - 2.0f * m_scale, icon.Y - 2.0f * m_scale,
|
||||||
|
icon.Width + 4.0f * m_scale, icon.Height + 4.0f * m_scale),
|
||||||
|
4.0f * m_scale, WithAlpha(m_theme->accent, 0x33));
|
||||||
|
}
|
||||||
|
PaintResetIcon(g, icon, iconHovered ? m_theme->accent : m_theme->text, m_scale);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const bool hovered = (static_cast<int>(index) == m_hoverIndex) &&
|
const bool hovered = (static_cast<int>(index) == m_hoverIndex) &&
|
||||||
(item.kind != MenuItemKind::SWATCHES || m_hoverSwatch < 0);
|
(item.kind != MenuItemKind::SWATCHES || m_hoverSwatch < 0);
|
||||||
|
|
||||||
@@ -1063,9 +1266,31 @@ namespace jdi
|
|||||||
UiText(g, UiEllipsize(item.label, m_fonts->uiSmall, labelWidth), m_fonts->uiSmall,
|
UiText(g, UiEllipsize(item.label, m_fonts->uiSmall, labelWidth), m_fonts->uiSmall,
|
||||||
gd::RectF(labelLeft, rect.Y, labelWidth, rect.Height), labelColor, TextAlign::LEFT);
|
gd::RectF(labelLeft, rect.Y, labelWidth, rect.Height), labelColor, TextAlign::LEFT);
|
||||||
|
|
||||||
|
const float suffix = EditSuffixWidth(item);
|
||||||
if (static_cast<int>(index) == m_editIndex)
|
if (static_cast<int>(index) == m_editIndex)
|
||||||
{
|
{
|
||||||
UiDrawEditField(g, fieldRect, m_edit, m_fonts->uiSmall, m_scale);
|
// 编辑态:输入框底板覆盖整行,正文区向右让出后缀宽度,
|
||||||
|
// 后缀(单位提示)靠右对齐画在它右侧。
|
||||||
|
// ⚠️ 必须用 `UiDrawEditFieldContent`(只画内容)——`UiDrawEditField`
|
||||||
|
// 会把底板/描边按传入矩形绘制,那样带后缀时外框会短一截。
|
||||||
|
const gd::RectF textRect =
|
||||||
|
(suffix > 0.0f)
|
||||||
|
? gd::RectF(fieldRect.X, fieldRect.Y, std::max(12.0f, fieldRect.Width - suffix),
|
||||||
|
fieldRect.Height)
|
||||||
|
: fieldRect;
|
||||||
|
UiFillRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->cardBg);
|
||||||
|
UiDrawRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->accent, 1.6f);
|
||||||
|
UiDrawEditFieldContent(g, textRect, m_edit, m_fonts->uiSmall, m_scale);
|
||||||
|
if (suffix > 0.0f)
|
||||||
|
{
|
||||||
|
const float suffixLeft = textRect.X + textRect.Width;
|
||||||
|
UiText(g, item.editSuffix, m_fonts->uiSmall,
|
||||||
|
gd::RectF(suffixLeft, fieldRect.Y,
|
||||||
|
std::max(4.0f, fieldRect.X + fieldRect.Width - suffixLeft -
|
||||||
|
4.0f * m_scale),
|
||||||
|
fieldRect.Height),
|
||||||
|
m_theme->textMuted, TextAlign::RIGHT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
+133
-2
@@ -24,6 +24,13 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return c >= L'A' && c <= L'Z';
|
return c >= L'A' && c <= L'Z';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief 时间间隔的显示单位。 */
|
||||||
|
struct DurationUnit
|
||||||
|
{
|
||||||
|
double scale = 1.0; /**< 1 个该单位等于多少秒。 */
|
||||||
|
const wchar_t* name = L"s"; /**< 单位名称。 */
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::wstring Utf8ToWide(const char* text, int length)
|
std::wstring Utf8ToWide(const char* text, int length)
|
||||||
@@ -339,9 +346,133 @@ namespace jdi
|
|||||||
return std::wstring(buffer);
|
return std::wstring(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::wstring FormatByteSize(unsigned long long bytes)
|
std::wstring FormatDurationCompound(double seconds)
|
||||||
{
|
{
|
||||||
// 单位自适应:GB / MB / KB(阈值按 1024 进制)。
|
if (!std::isfinite(seconds))
|
||||||
|
{
|
||||||
|
return L"—";
|
||||||
|
}
|
||||||
|
const double value = std::fabs(seconds);
|
||||||
|
if (value <= 0.0)
|
||||||
|
{
|
||||||
|
return L"0s";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 顶级单位高于 s(h / min):整合为 h/min/s(整数秒)----
|
||||||
|
// 注意条件必须是 >= 60s:1~60s 属于"顶级单位为 s"的情况,需按下一条规则处理
|
||||||
|
// (非整数秒要递进到 ms,例如 5.06s → "5s 60ms")。
|
||||||
|
if (value >= 60.0)
|
||||||
|
{
|
||||||
|
const long long totalSeconds = static_cast<long long>(std::llround(value));
|
||||||
|
long long hours = 0;
|
||||||
|
long long minutes = 0;
|
||||||
|
long long secs = totalSeconds;
|
||||||
|
if (totalSeconds >= 3600)
|
||||||
|
{
|
||||||
|
hours = totalSeconds / 3600;
|
||||||
|
secs = totalSeconds % 3600;
|
||||||
|
}
|
||||||
|
if (secs >= 60)
|
||||||
|
{
|
||||||
|
minutes = secs / 60;
|
||||||
|
secs = secs % 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring text;
|
||||||
|
const auto append = [&text](long long amount, const wchar_t* unit)
|
||||||
|
{
|
||||||
|
if (amount <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!text.empty())
|
||||||
|
{
|
||||||
|
text += L" ";
|
||||||
|
}
|
||||||
|
text += std::to_wstring(amount);
|
||||||
|
text += unit;
|
||||||
|
};
|
||||||
|
append(hours, L"h");
|
||||||
|
append(minutes, L"min");
|
||||||
|
append(secs, L"s");
|
||||||
|
if (!text.empty())
|
||||||
|
{
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 顶级单位为 s:整数秒 + 可选 ms ----
|
||||||
|
if (value >= 1.0)
|
||||||
|
{
|
||||||
|
const double whole = std::floor(value);
|
||||||
|
const double fraction = value - whole;
|
||||||
|
std::wstring text = FormatValue(whole) + L"s";
|
||||||
|
const double remainderMs = fraction * 1000.0;
|
||||||
|
if (remainderMs >= 1.0e-6)
|
||||||
|
{
|
||||||
|
text += L" ";
|
||||||
|
text += FormatValue(remainderMs);
|
||||||
|
text += L"ms";
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 顶级单位低于 s:ms / us / ns / ps ----
|
||||||
|
const DurationUnit subUnits[] = {
|
||||||
|
{1.0e-3, L"ms"}, {1.0e-6, L"us"}, {1.0e-9, L"ns"}, {1.0e-12, L"ps"},
|
||||||
|
};
|
||||||
|
const std::size_t subCount = sizeof(subUnits) / sizeof(subUnits[0]);
|
||||||
|
std::size_t index = subCount - 1;
|
||||||
|
for (std::size_t i = 0; i < subCount; ++i)
|
||||||
|
{
|
||||||
|
if (value >= subUnits[i].scale)
|
||||||
|
{
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 顶级单位为 ps:不再递进次级单位(保留小数)。
|
||||||
|
if (index + 1 >= subCount)
|
||||||
|
{
|
||||||
|
return FormatValue(value / subUnits[index].scale) + L" " + subUnits[index].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double scaled = value / subUnits[index].scale;
|
||||||
|
double whole = std::floor(scaled);
|
||||||
|
double fraction = scaled - whole;
|
||||||
|
// 浮点误差可能导致 fraction 极接近 1(例如 4.9999999),归一化一下。
|
||||||
|
if (fraction >= 1.0 - 1.0e-9)
|
||||||
|
{
|
||||||
|
whole += 1.0;
|
||||||
|
fraction = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring text = FormatValue(whole);
|
||||||
|
text += subUnits[index].name;
|
||||||
|
// 仍是该单位的整数 → 不显示下级单位。
|
||||||
|
const double remainder = fraction * subUnits[index].scale;
|
||||||
|
const double inNext = remainder / subUnits[index + 1].scale;
|
||||||
|
if (inNext >= 1.0e-6)
|
||||||
|
{
|
||||||
|
text += L" ";
|
||||||
|
text += FormatValue(inNext);
|
||||||
|
text += subUnits[index + 1].name;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring FormatDurationSeconds(double seconds)
|
||||||
|
{
|
||||||
|
if (!std::isfinite(seconds))
|
||||||
|
{
|
||||||
|
return L"0";
|
||||||
|
}
|
||||||
|
return FormatValue(std::fabs(seconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring FormatByteSize(unsigned long long bytes)
|
||||||
|
{ // 单位自适应:GB / MB / KB(阈值按 1024 进制)。
|
||||||
constexpr double kKilo = 1024.0;
|
constexpr double kKilo = 1024.0;
|
||||||
constexpr double kMega = kKilo * 1024.0;
|
constexpr double kMega = kKilo * 1024.0;
|
||||||
constexpr double kGiga = kMega * 1024.0;
|
constexpr double kGiga = kMega * 1024.0;
|
||||||
|
|||||||
Reference in new issue
Block a user