fix(ui): 采样统计显示一致化、GX 拖拽在布局边界下可用、信号「移动到…」与拖拽排序
一、采样间隔/频率显示不一致(用户报告 1ms vs 1000us、100us 0ns) * FormatDurationCompound 改为「先量化到整数皮秒、再整数分解」, 彻底消除同一数值因浮点噪声落到不同单位/留下零尾数的问题; 秒级分支与亚秒分支共用 totalPs 方案,次级进位/单位升级集中在一处。 * FormatFrequency 的单位选择改为按「舍入后的显示值」判定(999.9999999Hz 与 1000Hz 均为 1.000kHz),并做向下校正避免 0.001kHz。 二、选区截取面板 * 提示文本改为用户指定原文「启用双光标模式以进行选区截取」。 * EdgePanelWidth 计入该提示的实际文本宽度;绘制改用 UiTextClipped 不再省略。 * viewHeight 补回漏减的 bottomPad,修掉条目区与按钮之间的空白。 三、未展开的信号条目栏悬停不再变成手型光标。 四、开启「显示布局边界」后 GX 标签可正常拖拽排序 * 抽出 PaintLaneReorderOverlay(吸附槽位 + 被拖轮廓),贴图与整幅重绘共用。 * 整幅重绘在 LANE_REORDER 时按 LaneDragOffset 逐格平移重画(被拖网格最后画), 修掉调试模式下不跟随鼠标、无吸附提示、只能盲拖的问题。 五、信号右键新增「移动到…」二级菜单(位于「仅显示该信号」之上) * 条目为「分组 N(组名)」,当前组打勾;无标题行;底部「新建分组」。 * MenuItem::submenuMaxRows + PopupMenu 独立子菜单滚动(滚动条、滚轮、视口命中过滤), IsScrollable 计入子菜单可滚状态。 * 数据层 DataSet::MoveSignalToGroup/MoveSignal 让时间轴随信号搬运 (独占则移动、共享则复制);TabConfig::RemapAfterSignalMove 改写引用而非先删后插。 * Application::MoveSignalCore 统一入口,保证派生计算暂停、引用修正、选中迁移与同步一致。 六、信号列表支持按住拖拽排序(组内重排 / 跨组移动) * 4px 阈值进入拖拽;条目脱离原位跟随鼠标(阴影 + 强调色描边 + 插入指示线)。 * 其它信号行/分组行按插入位置做避让位移动画(RowRect 统一叠加插值位移, 宿主 kScrollTimerId 驱动 AdvanceSignalDragAnimation)。 * 数据层 DataSet::MoveSignal 支持任意位置;TabConfig::RemapAfterSignalReorder 线性重映射。 * 拖拽中禁用组名悬浮;双击改名与 InvalidateRows 均正确终止拖拽态。 验证:删 .o 全量重编译零 error / 零 warning;产物 873,984 B(< 1MB 预警阈值)。 本轮未使用鼠标/键盘/屏幕,未运行任何 GUI 探针,仅编译验证 + 源码级核对。
This commit is contained in:
1 parent
21a9865365
commit
97b361dedd
23 files changed
+1581
-74
No files matched your search
@@ -706,6 +706,47 @@ namespace jdi
|
||||
*/
|
||||
bool ForkAxisForSignal(int groupIndex, int signalIndex, std::size_t& axisIndex);
|
||||
|
||||
/**
|
||||
* @brief 把一条信号移动到**另一个分组**(追加到目标分组末尾)。
|
||||
*
|
||||
* 用于信号右键菜单的「移动到…」(用户 2026-11-03 要求):把信号从原分组摘出来、
|
||||
* 插入另一个分组(含「新建分组」)。
|
||||
*
|
||||
* ⚠️ **时间轴必须随信号一起走**:`SignalData::axisIndex` 指向**所属分组**的
|
||||
* `axes`,跨组移动后原下标毫无意义。这里的处理是:
|
||||
* * 该信号的轴**只被它自己使用** → 把轴 move 到目标分组(零拷贝,并从源分组删掉,
|
||||
* 源分组内其余信号的 `axisIndex` 若大于被删下标则整体前移一位);
|
||||
* * 与源分组内其它信号**共享** → 复制一条到目标分组(不能抽走共享轴)。
|
||||
*
|
||||
* ⚠️ 调用方必须紧接着对每个标签页调用
|
||||
* `TabConfig::RemapAfterSignalMove(from, to, visible)` 修正引用,
|
||||
* 并在之后 `SyncToDataSet`。源分组即便被移空也**不自动删除**(删除会让分组下标整体左移,
|
||||
* 牵动所有引用);需要的话由调用方另行处理。
|
||||
*
|
||||
* @param [IN] fromGroup 源分组下标。
|
||||
* @param [IN] fromSignal 源分组内的信号下标。
|
||||
* @param [IN] toGroup 目标分组下标(必须已存在,且与源分组不同)。
|
||||
* @param [OUT] newSignalIndex 移动后该信号在目标分组内的下标(= 移动前目标组信号数)。
|
||||
* @return 移动成功返回 true。
|
||||
*/
|
||||
bool MoveSignalToGroup(int fromGroup, int fromSignal, int toGroup, int& newSignalIndex);
|
||||
|
||||
/**
|
||||
* @brief 把一条信号移动到**任意分组内的任意位置**(组内重排 / 跨组移动的统一入口)。
|
||||
*
|
||||
* 组内重排(`fromGroup == toGroup`)只调整顺序,时间轴完全不动;
|
||||
* 跨组移动按 `MoveSignalToGroup` 的规则搬运时间轴。
|
||||
*
|
||||
* @param [IN] fromGroup 源分组下标。
|
||||
* @param [IN] fromSignal 源分组内的信号下标。
|
||||
* @param [IN] toGroup 目标分组下标。
|
||||
* @param [IN] toSignalIndex 移动后该信号在目标分组内的下标(负数或越界表示追加到末尾)。
|
||||
* @param [OUT] newSignalIndex 移动后的实际下标。
|
||||
* @return 移动成功返回 true。
|
||||
*/
|
||||
bool MoveSignal(int fromGroup, int fromSignal, int toGroup, int toSignalIndex,
|
||||
int& newSignalIndex);
|
||||
|
||||
/**
|
||||
* @brief 移除指定下标的分组,并立即释放其占用的全部采样内存。
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
@@ -1725,6 +1766,41 @@ namespace jdi
|
||||
*/
|
||||
void RemapAfterSignalInsert(int groupIndex, int insertPos);
|
||||
|
||||
/**
|
||||
* @brief 数据层把一条信号**跨分组移动**之后,修正本页的全部引用。
|
||||
*
|
||||
* 与 `RemapAfterSignalRemoval` + `RemapAfterSignalInsert` 组合不同:移动**不应**
|
||||
* 把信号从网格里删掉(用户期望曲线原地继续显示,只是归属分组变了),
|
||||
* 因此这里把「恰好等于源引用」的条目**改写**为目标引用,而不是先删后插。
|
||||
*
|
||||
* 处理顺序(不可调换):
|
||||
* 1. 精确等于 `from` 的引用 → 改写为 `to`(其下标已是移动后的最终值);
|
||||
* 2. 其余引用按「目标分组在 `to.signalIndex` 处插入」/「源分组删除 `from.signalIndex`」
|
||||
* 做 ±1 位移。
|
||||
* ⚠️ 若先位移再匹配,源分组里 `from.signalIndex + 1` 的引用会位移成 `from.signalIndex`
|
||||
* 而被误判成被移动的信号(曾经的 BUG)。
|
||||
*
|
||||
* 可见性表:目标分组在 `to.signalIndex` 处插入一列(取 `visible`),源分组删除一列。
|
||||
*
|
||||
* @param [IN] from 源引用(移动前)。
|
||||
* @param [IN] to 目标引用(移动后)。
|
||||
* @param [IN] visible 该信号在源分组里的可见性(迁移到目标分组后沿用)。
|
||||
*/
|
||||
void RemapAfterSignalMove(const SignalRef& from, const SignalRef& to, bool visible);
|
||||
|
||||
/**
|
||||
* @brief 数据层**组内重排**一条信号之后,修正本页在该分组内的全部引用下标。
|
||||
*
|
||||
* 只做线性下标映射(可见性表也按同一规则移动),不改变信号的分组归属:
|
||||
* `from` → `to`;`from < to` 时 `(from, to]` 区间内的下标各前移一位,
|
||||
* `from > to` 时 `[to, from)` 区间内的下标各后移一位。
|
||||
*
|
||||
* @param [IN] groupIndex 发生重排的分组下标。
|
||||
* @param [IN] from 移动前的下标。
|
||||
* @param [IN] to 移动后的下标。
|
||||
*/
|
||||
void RemapAfterSignalReorder(int groupIndex, int from, int to);
|
||||
|
||||
/**
|
||||
* @brief 把某信号所在的网格移动到另一信号所在网格的下方(紧邻其后)。
|
||||
*
|
||||
|
||||
@@ -653,6 +653,112 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DataSet::MoveSignal(int fromGroup, int fromSignal, int toGroup, int toSignalIndex,
|
||||
int& newSignalIndex)
|
||||
{
|
||||
newSignalIndex = -1;
|
||||
if (fromGroup < 0 || toGroup < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (fromGroup >= static_cast<int>(m_groups.size()) ||
|
||||
toGroup >= static_cast<int>(m_groups.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
SignalGroup& source = m_groups[static_cast<std::size_t>(fromGroup)];
|
||||
if (fromSignal < 0 || fromSignal >= static_cast<int>(source.signals.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- 组内重排:只改顺序,时间轴与 axisIndex 完全不动 ----
|
||||
if (fromGroup == toGroup)
|
||||
{
|
||||
std::vector<SignalData>& signals = source.signals;
|
||||
SignalData moved = std::move(signals[static_cast<std::size_t>(fromSignal)]);
|
||||
signals.erase(signals.begin() + fromSignal);
|
||||
int position = toSignalIndex;
|
||||
if (position < 0 || position > static_cast<int>(signals.size()))
|
||||
{
|
||||
position = static_cast<int>(signals.size());
|
||||
}
|
||||
signals.insert(signals.begin() + position, std::move(moved));
|
||||
newSignalIndex = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- 跨组移动 ----
|
||||
SignalGroup& target = m_groups[static_cast<std::size_t>(toGroup)];
|
||||
|
||||
// 1) 时间轴随信号一起走(详见 `MoveSignalToGroup` 的说明)。
|
||||
const std::size_t axisIndex = source.signals[static_cast<std::size_t>(fromSignal)].axisIndex;
|
||||
const std::size_t safeAxis = (axisIndex < source.axes.size()) ? axisIndex : 0;
|
||||
bool sharedWithOthers = false;
|
||||
for (std::size_t index = 0; index < source.signals.size(); ++index)
|
||||
{
|
||||
if (static_cast<int>(index) != fromSignal && source.signals[index].axisIndex == axisIndex)
|
||||
{
|
||||
sharedWithOthers = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::size_t newAxisIndex = 0;
|
||||
if (source.axes.empty())
|
||||
{
|
||||
target.axes.push_back(TimeAxis());
|
||||
newAxisIndex = target.axes.size() - 1;
|
||||
}
|
||||
else if (sharedWithOthers)
|
||||
{
|
||||
target.axes.push_back(source.axes[safeAxis]);
|
||||
newAxisIndex = target.axes.size() - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
target.axes.push_back(std::move(source.axes[safeAxis]));
|
||||
newAxisIndex = target.axes.size() - 1;
|
||||
source.axes.erase(source.axes.begin() + safeAxis);
|
||||
for (SignalData& other : source.signals)
|
||||
{
|
||||
if (other.axisIndex > safeAxis)
|
||||
{
|
||||
--other.axisIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 取出信号并插入目标位置的指定下标。
|
||||
SignalData moved = std::move(source.signals[static_cast<std::size_t>(fromSignal)]);
|
||||
moved.axisIndex = newAxisIndex;
|
||||
source.signals.erase(source.signals.begin() + fromSignal);
|
||||
int position = toSignalIndex;
|
||||
if (position < 0 || position > static_cast<int>(target.signals.size()))
|
||||
{
|
||||
position = static_cast<int>(target.signals.size());
|
||||
}
|
||||
target.signals.insert(target.signals.begin() + position, std::move(moved));
|
||||
newSignalIndex = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DataSet::MoveSignalToGroup(int fromGroup, int fromSignal, int toGroup, int& newSignalIndex)
|
||||
{
|
||||
if (fromGroup == toGroup)
|
||||
{
|
||||
newSignalIndex = -1;
|
||||
return false;
|
||||
}
|
||||
const SignalGroup* target = GroupAt(toGroup);
|
||||
if (target == nullptr)
|
||||
{
|
||||
newSignalIndex = -1;
|
||||
return false;
|
||||
}
|
||||
return MoveSignal(fromGroup, fromSignal, toGroup, static_cast<int>(target->signals.size()),
|
||||
newSignalIndex);
|
||||
}
|
||||
|
||||
bool DataSet::RemoveGroup(int groupIndex)
|
||||
{
|
||||
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
|
||||
|
||||
@@ -84,6 +84,140 @@ namespace jdi
|
||||
m_lanes.end());
|
||||
}
|
||||
|
||||
void TabConfig::RemapAfterSignalMove(const SignalRef& from, const SignalRef& to, bool visible)
|
||||
{
|
||||
if (from.groupIndex < 0 || to.groupIndex < 0 || from.groupIndex == to.groupIndex)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) 可见性表:目标分组在目标位置插入一列(沿用源分组里的可见性),源分组删除一列。
|
||||
const std::size_t toSlot = static_cast<std::size_t>(to.groupIndex);
|
||||
if (toSlot < m_visible.size() &&
|
||||
static_cast<std::size_t>(to.signalIndex) <= m_visible[toSlot].size())
|
||||
{
|
||||
m_visible[toSlot].insert(m_visible[toSlot].begin() + to.signalIndex, visible);
|
||||
}
|
||||
const std::size_t fromSlot = static_cast<std::size_t>(from.groupIndex);
|
||||
if (fromSlot < m_visible.size() &&
|
||||
static_cast<std::size_t>(from.signalIndex) < m_visible[fromSlot].size())
|
||||
{
|
||||
m_visible[fromSlot].erase(m_visible[fromSlot].begin() + from.signalIndex);
|
||||
}
|
||||
|
||||
// 2) 全部信号引用:先改写被移动的那一条,再做 ±1 位移(顺序不可调换,见头文件说明)。
|
||||
const auto adjust = [&from, &to](SignalRef& ref)
|
||||
{
|
||||
if (ref.groupIndex == from.groupIndex && ref.signalIndex == from.signalIndex)
|
||||
{
|
||||
ref = to;
|
||||
return;
|
||||
}
|
||||
if (ref.groupIndex == to.groupIndex && ref.signalIndex >= to.signalIndex)
|
||||
{
|
||||
++ref.signalIndex;
|
||||
}
|
||||
else if (ref.groupIndex == from.groupIndex && ref.signalIndex > from.signalIndex)
|
||||
{
|
||||
--ref.signalIndex;
|
||||
}
|
||||
};
|
||||
// 条目集合都是「带 `ref` 成员的结构体」,用泛型 lambda 统一处理(与
|
||||
// `RemapAfterSignalRemoval` 的 `purgeRefs` 同一手法)。
|
||||
const auto adjustItems = [&adjust](auto& items)
|
||||
{
|
||||
for (auto& item : items)
|
||||
{
|
||||
adjust(item.ref);
|
||||
}
|
||||
};
|
||||
|
||||
for (Lane& lane : m_lanes)
|
||||
{
|
||||
for (SignalRef& ref : lane.signals)
|
||||
{
|
||||
adjust(ref);
|
||||
}
|
||||
adjustItems(lane.meanFilterItems);
|
||||
adjustItems(lane.edgeCenterItems);
|
||||
adjustItems(lane.weakPeakItems);
|
||||
adjustItems(lane.resampleItems);
|
||||
adjustItems(lane.clipItems);
|
||||
adjustItems(lane.freeMarkerItems);
|
||||
}
|
||||
}
|
||||
|
||||
void TabConfig::RemapAfterSignalReorder(int groupIndex, int from, int to)
|
||||
{
|
||||
if (groupIndex < 0 || from < 0 || to < 0 || from == to)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 线性重排映射:被移动的那条改为 `to`,被它跨过的条目各让一位。
|
||||
const auto mapIndex = [from, to](int index)
|
||||
{
|
||||
if (index == from)
|
||||
{
|
||||
return to;
|
||||
}
|
||||
if (from < to)
|
||||
{
|
||||
if (index > from && index <= to)
|
||||
{
|
||||
return index - 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (index >= to && index < from)
|
||||
{
|
||||
return index + 1;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
};
|
||||
|
||||
// 可见性表整体重排(与信号顺序严格一致)。
|
||||
const std::size_t slot = static_cast<std::size_t>(groupIndex);
|
||||
if (slot < m_visible.size() && static_cast<std::size_t>(from) < m_visible[slot].size() &&
|
||||
static_cast<std::size_t>(to) < m_visible[slot].size())
|
||||
{
|
||||
std::vector<bool>& row = m_visible[slot];
|
||||
const bool moved = row[static_cast<std::size_t>(from)];
|
||||
row.erase(row.begin() + from);
|
||||
row.insert(row.begin() + to, moved);
|
||||
}
|
||||
|
||||
const auto adjust = [groupIndex, &mapIndex](SignalRef& ref)
|
||||
{
|
||||
if (ref.groupIndex == groupIndex)
|
||||
{
|
||||
ref.signalIndex = mapIndex(ref.signalIndex);
|
||||
}
|
||||
};
|
||||
const auto adjustItems = [&adjust](auto& items)
|
||||
{
|
||||
for (auto& item : items)
|
||||
{
|
||||
adjust(item.ref);
|
||||
}
|
||||
};
|
||||
|
||||
for (Lane& lane : m_lanes)
|
||||
{
|
||||
for (SignalRef& ref : lane.signals)
|
||||
{
|
||||
adjust(ref);
|
||||
}
|
||||
adjustItems(lane.meanFilterItems);
|
||||
adjustItems(lane.edgeCenterItems);
|
||||
adjustItems(lane.weakPeakItems);
|
||||
adjustItems(lane.resampleItems);
|
||||
adjustItems(lane.clipItems);
|
||||
adjustItems(lane.freeMarkerItems);
|
||||
}
|
||||
}
|
||||
|
||||
void TabConfig::RemapAfterSignalRemoval(int groupIndex, int signalIndex)
|
||||
{
|
||||
if (groupIndex < 0 || signalIndex < 0)
|
||||
|
||||
+123
-43
@@ -540,65 +540,131 @@ namespace jdi
|
||||
}
|
||||
|
||||
// ---- 顶级单位为 s:整数秒 + 可选 ms ----
|
||||
// ⚠️⚠️ 与下面的亚秒分支同一原则:**先量化到整数皮秒,再分解**。
|
||||
// 直接对 double 做 `floor` + `fraction * 1000` 会被浮点噪声坑到:
|
||||
// `0.9999999999s` 的余数四舍五入后是 `1000.000ms`(应进位成 `1s`),
|
||||
// 而 `5.0599999999999996s` 与 `5.06s` 会得到不同的尾数(用户 2026-11-03 报告)。
|
||||
if (value >= 1.0)
|
||||
{
|
||||
const double whole = std::floor(value);
|
||||
const double fraction = value - whole;
|
||||
std::wstring text = FormatDecimal(whole) + L"s";
|
||||
const double remainderMs = fraction * 1000.0;
|
||||
if (remainderMs >= 1.0e-6)
|
||||
const long long totalPs = static_cast<long long>(std::llround(value * 1.0e12));
|
||||
long long wholeSeconds = totalPs / 1000000000000LL;
|
||||
const long long restPs = totalPs % 1000000000000LL;
|
||||
// 次级(ms)以「千分之一 ms」为整数单位,保证舍入一次到位。
|
||||
long long milliMs = (restPs + 500000LL) / 1000000LL;
|
||||
if (milliMs >= 1000LL * 1000LL)
|
||||
{
|
||||
++wholeSeconds;
|
||||
milliMs = 0;
|
||||
}
|
||||
std::wstring text = FormatDecimal(static_cast<double>(wholeSeconds)) + L"s";
|
||||
if (milliMs > 0)
|
||||
{
|
||||
text += L" ";
|
||||
// ⚠️ 次级单位(ms)**最多保留三位小数**(用户 2026-11-03 要求:`3ms 696.631us`
|
||||
// 这类显示要能看到真实尾数);整数则不加小数点(`5.06s` → `5s 60ms`)。
|
||||
text += FormatSubUnit(remainderMs, kDurationSubUnitDecimals);
|
||||
text += L" ";
|
||||
text += FormatSubUnit(static_cast<double>(milliMs) / 1000.0, kDurationSubUnitDecimals);
|
||||
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"},
|
||||
// ⚠️⚠️ **必须以整数(皮秒)为唯一事实来源做分解**,不能对 double 反复 floor/除法。
|
||||
// 同一个物理间隔(例如 `0.001s`)在两次独立计算里可能分别是
|
||||
// `0.0010000000000000002` 与 `0.0009999999999999998`;任何"接近 1000 就进位"
|
||||
// 的浮点启发式都会在边界漏判,于是同一数值出现 `1ms` 与 `1000us` 两种显示,
|
||||
// 还会留下 `100us 0ns` 这种纯噪声尾数(用户 2026-11-03 报告)。
|
||||
// 量化到 ps 之后噪声被彻底消除:同一数值必然得到同一字符串。
|
||||
const long long totalPs = static_cast<long long>(std::llround(value * 1.0e12));
|
||||
if (totalPs <= 0)
|
||||
{
|
||||
// 小于 0.5ps:无法量化,保留高精度小数直接显示。
|
||||
return FormatDecimal(value / 1.0e-12) + L" ps";
|
||||
}
|
||||
|
||||
struct SubUnit
|
||||
{
|
||||
long long ps;
|
||||
const wchar_t* name;
|
||||
};
|
||||
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)
|
||||
const SubUnit subUnits[] = {
|
||||
{1000000000LL, L"ms"}, {1000000LL, L"us"}, {1000LL, L"ns"}, {1LL, L"ps"},
|
||||
};
|
||||
const int subCount = 4;
|
||||
|
||||
// 量化后达到 1s ⇒ 等价于上面的秒级分支(`999.9999999999s` → `1s`)。
|
||||
if (totalPs >= 1000000000000LL)
|
||||
{
|
||||
if (value >= subUnits[i].scale)
|
||||
long long wholeSeconds = totalPs / 1000000000000LL;
|
||||
const long long restPs = totalPs % 1000000000000LL;
|
||||
long long milliMs = (restPs + 500000LL) / 1000000LL;
|
||||
if (milliMs >= 1000LL * 1000LL)
|
||||
{
|
||||
index = i;
|
||||
break;
|
||||
++wholeSeconds;
|
||||
milliMs = 0;
|
||||
}
|
||||
std::wstring text = FormatDecimal(static_cast<double>(wholeSeconds)) + L"s";
|
||||
if (milliMs > 0)
|
||||
{
|
||||
text += L" ";
|
||||
text += FormatSubUnit(static_cast<double>(milliMs) / 1000.0, kDurationSubUnitDecimals);
|
||||
text += L"ms";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// 顶级单位为 ps:不再递进次级单位(保留小数)。
|
||||
if (index + 1 >= subCount)
|
||||
// 选主单位:第一个"能被 totalPs 容纳"的单位。
|
||||
int index = 0;
|
||||
while (index + 1 < subCount && totalPs < subUnits[index].ps)
|
||||
{
|
||||
return FormatDecimal(value / subUnits[index].scale) + L" " + subUnits[index].name;
|
||||
++index;
|
||||
}
|
||||
|
||||
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)
|
||||
// 把 totalPs 分解成 [主单位整数] + [次级单位(千分之一为单位的整数)]。
|
||||
// 抽成 lambda 是因为升级单位后需要用同一个 totalPs 重算,避免累积误差。
|
||||
long long whole = 0;
|
||||
long long milliNext = 0;
|
||||
const auto decompose = [&]()
|
||||
{
|
||||
whole += 1.0;
|
||||
fraction = 0.0;
|
||||
whole = totalPs / subUnits[index].ps;
|
||||
milliNext = 0;
|
||||
if (index + 1 >= subCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const long long restPs = totalPs % subUnits[index].ps;
|
||||
const long long nextPs = subUnits[index + 1].ps;
|
||||
// 次级值保留 3 位小数 ⇒ 用「千分之一个次级单位」作为整数量纲。
|
||||
milliNext = (restPs * 1000LL + nextPs / 2) / nextPs;
|
||||
// 次级舍入后达到 1000 个次级单位 ⇒ 进位到主单位。
|
||||
if (milliNext >= 1000LL * 1000LL)
|
||||
{
|
||||
++whole;
|
||||
milliNext = 0;
|
||||
}
|
||||
};
|
||||
decompose();
|
||||
// 主单位达到 1000 且没有次级尾数 ⇒ 升级到更高的主单位(`1000us` → `1ms`)。
|
||||
while (index > 0 && whole >= 1000LL && milliNext == 0)
|
||||
{
|
||||
--index;
|
||||
decompose();
|
||||
}
|
||||
|
||||
std::wstring text = FormatValue(whole);
|
||||
// 主单位已是 ms 且恰好进位到 1000ms ⇒ 直接显示为秒。
|
||||
if (index == 0 && milliNext == 0 && whole >= 1000LL)
|
||||
{
|
||||
return FormatDecimal(static_cast<double>(whole) / 1000.0) + L"s";
|
||||
}
|
||||
|
||||
std::wstring text = FormatDecimal(static_cast<double>(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)
|
||||
if (milliNext > 0)
|
||||
{
|
||||
text += L" ";
|
||||
// ⚠️ 次级单位**最多保留三位小数**(用户 2026-11-03 要求):
|
||||
// `0.003696631s` → 顶级 `3ms` + 次级 `696.631us`(而不是 `3ms 700us`)。
|
||||
text += FormatSubUnit(inNext, kDurationSubUnitDecimals);
|
||||
text += L" ";
|
||||
text += FormatSubUnit(static_cast<double>(milliNext) / 1000.0, kDurationSubUnitDecimals);
|
||||
text += subUnits[index + 1].name;
|
||||
}
|
||||
return text;
|
||||
@@ -620,23 +686,37 @@ namespace jdi
|
||||
{
|
||||
return L"—";
|
||||
}
|
||||
const wchar_t* unit = L"Hz";
|
||||
double scaled = hertz;
|
||||
if (hertz >= 1.0e9)
|
||||
// ⚠️⚠️ **单位选择必须按「舍入后的显示值」判定,否则同一物理量会出现两种显示**:
|
||||
// `999.9999999 Hz` 走 Hz 分支显示 `1000.000 Hz`,而 `1000.0000001 Hz` 走 kHz 分支
|
||||
// 显示 `1.000 kHz` —— 用户 2026-11-03 报告的 `1000.00 Hz` vs `1.000 kHz` 就是这个。
|
||||
// 因此:先取单位,再用**保留 3 位小数后的值**判断是否达到 1000,达到就升级。
|
||||
const wchar_t* units[] = {L"Hz", L"kHz", L"MHz", L"GHz"};
|
||||
const double scales[] = {1.0, 1.0e3, 1.0e6, 1.0e9};
|
||||
const int unitCount = 4;
|
||||
int unitIndex = 0;
|
||||
for (int i = unitCount - 1; i >= 0; --i)
|
||||
{
|
||||
unit = L"GHz";
|
||||
scaled = hertz / 1.0e9;
|
||||
if (hertz >= scales[i])
|
||||
{
|
||||
unitIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (hertz >= 1.0e6)
|
||||
// 向上校正:舍入到 3 位小数后若达到 1000 个当前单位,就升到上一级。
|
||||
const double epsilon = 0.5 * std::pow(10.0, -kFrequencyDecimals);
|
||||
while (unitIndex + 1 < unitCount &&
|
||||
(hertz / scales[unitIndex]) + epsilon >= 1000.0)
|
||||
{
|
||||
unit = L"MHz";
|
||||
scaled = hertz / 1.0e6;
|
||||
++unitIndex;
|
||||
}
|
||||
else if (hertz >= 1.0e3)
|
||||
// 向下校正:若当前单位下小于 1(极低频边界),降一级以免出现 `0.001 kHz` 这类。
|
||||
while (unitIndex > 0 && (hertz / scales[unitIndex]) < 1.0 - epsilon)
|
||||
{
|
||||
unit = L"kHz";
|
||||
scaled = hertz / 1.0e3;
|
||||
--unitIndex;
|
||||
}
|
||||
const wchar_t* unit = units[unitIndex];
|
||||
const double scaled = hertz / scales[unitIndex];
|
||||
|
||||
// ⚠️ 频率同样不得出现科学计数法(用户 2026-11-03 要求):
|
||||
// `FormatValue` 的 `%.6g` 在 1e-5 Hz 这类量级会输出 “1e-06”,改为十进制形式。
|
||||
// ⚠️ 并且**基于真实值保留三位小数**(用户 2026-11-03 要求:`270.517 Hz` 而不是
|
||||
|
||||
Reference in new issue
Block a user