Compare commits

8 Commits
Author SHA1 Message Date
DeepSeek-V4.1-Flash c5d7a6fb91 docs(review): 新增 Code Review 最终报告
Documents/Code_Review_Report.md:逐项说明本轮改了什么、为什么、效果如何,
含体积节表前后对照(890,880 -> 794,112 B,-96,768 / -10.9%)、
Debug 构建缺陷(DEBUG_OPT 从未生效)、面板派发收敛与编译期保护、
ApplicationCommands 拆分、性能审查结论(含 3 个记录未做的优化点)、
注释/文档校正、验证清单、提交清单与后续建议。

同时把 Code_Review_Progress.md 补齐 P15/P16 与“搁置节点”的原因说明。
2026-11-04 03:08:45 +08:00
DeepSeek-V4.1-Flash f13944034a docs(review): 清理过时注释并校正项目记忆中的构建/目录说明
- PlotView.hpp:删除“已迁移到数据层的公式包络缓存”这个指向已删代码的注释块。
- agent_mem.md §2:修正 -Os -> -Oz;补充体积相关开关(NO_EXCEPTIONS / NO_RTTI /
  LTO / RELEASE_ONLY_OPT)与“改 Makefile 必须删 .o 全量重编”的提醒;
  链接库列表补 msimg32;忽略列表补 Temp/、.preflight/、Tools/。
- agent_mem.md §3:目录结构按现状重写(Shared/ 已承载通用控件与 PopupMenu/
  ColorPickerPopup;Utilities 现为数据与格式层:DataModel/DataLoader/CsvLoader/
  Mf4Loader/MatLoader/MatH5/Deflate/FormulaEval/Utf8Util/MemProbe;
  删掉早已不存在的 CsvTable/、DataModel/、PaintUtil/ 等旧条目),并写明依赖方向。
- agent_mem.md 末尾新增第 33 轮(本轮审查)记录。
- Code_Review_Progress.md 追加 P15(注释/文档校正)。
2026-11-04 03:07:48 +08:00
DeepSeek-V4.1-Flash 8437100586 refactor(application): 拆分 2165 行的 ApplicationCommands.cpp 为 4 个职责分片
原文件混杂了标签页、派生信号、菜单、工具栏、键盘鼠标、布局调试绘制六类职责,
是本项目最大的“大杂烩”单文件之一。按职责纯搬移(零逻辑改动):

- ApplicationTabs.cpp   (355 行): 标签页新建/关闭/切换/重排、视图同步、窗口标题、
                                   标签栏结果回调、标签页右键菜单、待提交改名。
- ApplicationDerive.cpp (786 行): 派生烘焙、重采样、选区截取(新建/就地)、克隆(含带调整)。
- ApplicationMenus.cpp  (409 行): 绘图区右键菜单、菜单数值回写、主题切换、设置菜单、
                                   菜单结果派发。
- ApplicationCommands.cpp (642 行): 分组/信号增删、移动与重排、文件对话框、工具栏动作、
                                   键盘鼠标分发、布局边界绘制。

Makefile 按目录通配收集,无需登记新文件。

验证:release 零 error 零 warning;产物 793,088 -> 794,112 B(+1,024,LTO 内联边界变化,
< 0.13%,属结构性拆分不可避免的噪声)。
2026-11-04 03:02:36 +08:00
DeepSeek-V4.1-Flash 7a3fefb9c2 refactor(plotview): 面板状态/滚动量收敛为唯一派发点,消除 20+ 处手写分支
问题:`Lane` 里 7 种实验性功能各自持有一份 `LanePanelState` 与滚动偏移(共 14 个字段)。
旧实现在每个需要“按种类取用”的地方各写一遍多层嵌套三元式或 switch:
 - EdgePanelLayoutFor 的 enabled / open / collapsed / storedX / storedY
   共 5 条 7 层嵌套三元式(历史上多次因多/少一个右括号编译失败);
 - 标题栏拖拽、✕ 关闭、滚轮翻页、滚动条拖动、错开序数统计各自再写一遍。
新增一种面板要改 20+ 处,漏一处就是「面板拖不动 / 关不掉 / 滚轮滚不到底」
(MARK、RESAMPLE、CLIP 三种面板都真实踩过)。

改法(PlotViewInternal.hpp 新增唯一派发点):
- LanePanelEnabled(lane, kind)
- LanePanelStateOf(lane, kind)    可写/只读
- LanePanelScrollOf(lane, kind)   可写/只读
这些 switch **穷举全部 7 种且不写 default**,因此 -Wswitch(属 -Wall)会在新增
EdgePanelKind 而漏登记时直接报警 —— 已用最小程序验证 -Wswitch 确实会报。

调用点改造:
- EdgePanelLayoutFor:5 条嵌套三元式 -> 3 行访问器调用;局部 kOrder[] -> kEdgePanelKinds。
- 标题栏拖拽:整个 7 分支 switch -> 一次访问器写入。
- 滚轮翻页:current 的三元式链 -> LanePanelScrollOf;switch 补全 MEAN/MARK 并去掉 default。
- 滚动条拖动:switch 补全 MEAN/MARK 并去掉 default。
- ✕ 关闭:去掉 `case COUNT: default:` 的兜底,改为穷举。
- SetLanePanelCollapsed / LanePanelCollapsed:两个 switch -> 一次访问器。
- 滚轮“是否可滚面板”的 kind 列举改为 `hoveredKind != MARK`。

验证:release / debug 全量编译零 warning;产物大小不变(793,088 B,逻辑等价)。
2026-11-04 02:59:57 +08:00
DeepSeek-V4.1-Flash 94fc1c45da perf(size): Release 关闭异步展开表 + 修正 Debug 构建参数
1. Release 专用 `-fno-asynchronous-unwind-tables`(新增 RELEASE_ONLY_OPT,只加在
   Release 的两条编译规则上)。-fno-exceptions 下绝大多数函数不再需要 .pdata/.xdata:
   实测 829,440 -> 793,088 B(-36,352);.pdata 30,420 -> 12,684、.xdata 28,904 -> 10,156。
   Release 不再生成可靠的栈回溯(崩溃诊断);Debug 保留完整展开表。

2. 修正真实缺陷:Makefile 的 DEBUG_OPT = -O0 从未被使用 —— Debug 两条编译规则误用了
   $(RELEASE_OPT)(即 -Oz),且全程没有 -g,因此所谓 Debug 构建其实只是"不 strip 的
   Release"。现在 Debug 规则改用 $(DEBUG_OPT) -g,链接也加 -g。
   代价:Debug 产物 1,734,578 -> 5,992,064 B(真实 -O0 -g,属预期)。

release(全量)790KB 零警告;debug(全量 -O0 -g)零警告。
2026-11-04 02:52:13 +08:00
DeepSeek-V4.1-Flash 5de3763e72 perf(size): 移除多余的 dynamic_cast 并省掉 56KB 的 C++ 名字反修饰器
两处独立收益:

1. Toolbar::BuildBoxes 里唯一的 dynamic_cast 是**多余强转**(Button::widget 本来就是
   UiButtonWidget*),却让全项目必须开 RTTI。改为直接遍历 m_buttons(与控件树同源)。
   最小复现程序实测:39,936 B -> 115,200 B(+75,264 B)。
2. RTTI 关掉后 cp-demangle.o 仍然在链接里 —— 真凶是 libstdc++ 的 vterminate.o
   (引用 __cxa_demangle)。本文件自行定义 __gnu_cxx::__verbose_terminate_handler(),
   让它不再被拉入(本应用启动后会换成自己的 terminate 处理,行为等效)。

Makefile 新增 NO_RTTI=1(默认开启)-> CXXFLAGS += -fno-rtti。

实测:Release 890,880 -> 829,440 B(-61,440);.text 753,136 -> 703,616(-49,520);
.rdata 36,936 -> 26,616(-10,320);Debug 1,801,617 -> 1,734,578(-67,039)。
release/debug 全量重建零警告。
2026-11-04 02:48:59 +08:00
DeepSeek-V4.1-Flash 86ff0291ba docs(review): 新增代码审查计划与进度文档(Code_Review 分支)
- 删除已完成使命的 UI_Refactor_Plan/Progress/Report(已在 dev 提交 764e5be)。
- 新增 Documents/Code_Review_Plan.md:结构 + 性能 + 体积的节点化计划,
  含已确认的问题事实(dynamic_cast 在最小复现里 +75,264 B、
  cp-demangle.o 56,320 B、每个 EdgePanelKind 出现 21-26 次等)。
- 新增 Documents/Code_Review_Progress.md:基线数字与节点记录。
2026-11-04 02:44:23 +08:00
DeepSeek-V4.1-Flash 764e5bee71 chore(docs): 删除过时的 UI 重构阶段文档
Documents/UI_Refactor_Plan.md、UI_Refactor_Progress.md、UI_Refactor_Report.md
描述的是 2026-10-28 那一阶段的计划与结论,已被后续多轮实现推翻/覆盖,
且内容与当前代码不一致,继续保留会误导接手者。

删除后由 Documents/Code_Review_Plan.md / Code_Review_Progress.md /
Code_Review_Report.md 承接(下一分支 Code_Review)。
2026-11-04 02:41:53 +08:00
19 changed files with 2554 additions and 2264 deletions

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,789 @@
// Application 实现分片 —— **派生信号**:派生烘焙(均值滤波/中心轴/弱峰调整)、
// 重采样、选区截取(新建 / 就地破坏性截取)、克隆(普通 / 带调整)。
//
// 共用的常量与工具函数在 ApplicationInternal.hpp(命名空间 `appdetail`,全部 inline)。
// 本文件由 2026-11-04 代码审查从 `ApplicationCommands.cpp`(原 2,165 行)按职责拆出,
// **纯搬移、零逻辑改动**。分片划分见 `Documents/Code_Review_Plan.md` 的 P11。
#include "ApplicationInternal.hpp"
namespace jdi
{
bool Application::BuildDerivedSignal(const DerivedSignalRequest& request, bool makeVisible)
{
const SignalGroup* group = m_dataSet.GroupAt(request.groupIndex);
if (group == nullptr || request.signalIndex < 0 ||
request.signalIndex >= static_cast<int>(group->signals.size()))
{
return false;
}
const SignalData& source = group->signals[static_cast<std::size_t>(request.signalIndex)];
const std::wstring baseName = source.name;
// 0. 先把源信号的「调整」变换快照下来(下面 `InsertDerivedSignal` 会移动信号存储,
// 之后再取 `source` 会悬垂)。
//
// ⚠️ **变换要"烘进数据",而不是让新信号继承这套指纹**(用户明确要求):
// 网格里的预览曲线画的是 `变换(滤波结果)`。若新信号只存 `滤波结果` 再**继承变换**,
// 它的「调整」面板里就会莫名其妙带着一整套系数 / 镜像 / 偏移 —— 用户看到的是
// "新信号只是继承了原信号的指纹,数据本身没变"。现在改为:
// * 数值域变换(系数 → 绝对值 → 垂直镜像 → Y 偏移)**与**水平镜像(数据反序)
// 全部作用在派生采样值上;
// * 于是新信号**用默认外观**绘制出来就与预览曲线逐位一致。
// * ⚠️ **只有「数值域」的变换烘进采样值;时间轴偏移留在样式上**(见 3b)。
// 曾经把 `offsetTime` 也按 `time[i] - offsetTime` 重采样烘进采样值("新信号
// 不启用任何调整"),但那是**有损**的:平移出时间轴范围的那一段只能写成 NaN,
// 偏移稍大(≥ 数据跨度)就连一个有效采样都不剩 —— 用户点「构建为新信号」
// 得到的是一条**空信号**(2026-10-29 报告)。派生信号与源信号共用同一条文件
// 时间轴,无法用采样值表达超出轴范围的平移,因此时间偏移只能由样式表达。
// 预览曲线本来也是用源样式的 `offsetTime` 绘制的,两条路径**显示完全一致**。
SignalStyle sourceTransform;
bool hasSourceStyle = false;
{
SignalRef sourceRef;
sourceRef.groupIndex = request.groupIndex;
sourceRef.signalIndex = request.signalIndex;
if (const SignalStyle* sourceStyle = m_dataSet.Style(sourceRef))
{
sourceTransform = *sourceStyle;
hasSourceStyle = true;
}
}
// 1. 计算派生采样值(与网格内的预览曲线共用同一实现,因此逐位一致)。
// 多次迭代均值滤波形如「<原信号名称> (N点均值 M次迭代)」,
// 弱峰调整形如「<原信号名称> (弱峰调整 M% N次迭代)」。
std::vector<float> derived_values;
SignalData derived;
if (request.weakPeak)
{
ComputeWeakPeakAdjust(source.values, static_cast<double>(request.amountPercent) / 100.0,
request.iterations, derived_values);
derived.name = baseName + L" (弱峰调整 " + std::to_wstring(request.amountPercent) + L"% " +
std::to_wstring(request.iterations) + L"次迭代)";
}
else
{
ComputeMovingAverageIterated(source.values, request.points, request.iterations, derived_values);
derived.name = baseName + L" (" + std::to_wstring(request.points) + L"点均值 " +
std::to_wstring(request.iterations) + L"次迭代)";
}
// 2. 组装新信号(外观一律用默认值:变换已经烘进采样数据,不再是"继承的指纹")。
// ⚠️ 新信号**沿用源信号的时间轴**(`axisIndex`):派生只改采样值,不改时间基准。
derived.axisIndex = source.axisIndex;
derived.values.assign(derived_values.begin(), derived_values.end());
// 2b. 把源信号的「调整」变换**烘进派生采样值**:
// * 数值域(**公式映射** → 系数 → 绝对值 → 垂直镜像 → Y 偏移)逐点套用
// `ApplyDrawTransform`(它本来就是“公式最先结算”,与绘制路径同源);
// * 水平镜像 = 数据反序,等价于把采样数组整体反转。
// ⚠️ `ApplyDrawTransform` 是逐点函数,因此"先变换再反转"与"先反转再变换"等价。
// ⚠️⚠️ **公式映射必须算作“有变换”**(用户 2026-11-03):旧判定只列了系数/绝对值/
// 垂直镜像/Y 偏移,于是带公式的信号派生出来的新信号**丢掉了公式**(不烘也不继承)
// ⇒ 新信号曲线与预览曲线不一致。
if (hasSourceStyle)
{
const bool valueTransform =
sourceTransform.HasFormula() || sourceTransform.gain != 1.0f ||
sourceTransform.absolute || sourceTransform.mirrorVertical ||
sourceTransform.offsetY != 0.0f;
if (valueTransform)
{
for (double& value : derived.values)
{
value = ApplyDrawTransform(sourceTransform, value);
}
}
if (sourceTransform.mirrorHorizontal)
{
std::reverse(derived.values.begin(), derived.values.end());
}
}
// 2c.(已删除)时间轴偏移**不再**按 `time[i] - offsetTime` 重采样烘进采样值。
//
// 旧实现为了让"新信号的「调整」面板保持默认"而把平移烘进数据,代价是**有损**:
// 平移出时间轴范围的那一段只能写成 NaN,偏移 ≥ 数据跨度时**整条信号全变 NaN**
//(用户 2026-10-29 报告的"派生出来是一条空信号");即便偏移很小,也会在两端
// 留下与实际曲线不符的空洞。
// 派生信号与源信号共用同一条文件时间轴,无法用采样值表达超出轴范围的平移,
// 因此时间偏移改由**新信号样式**表达(见 3b)——预览曲线本来就用源样式的
// `offsetTime` 绘制,所以两条路径的**显示完全一致**,且数据一点都不会丢。
// 3. 插入到原信号的下方(数据层 + 各标签页引用重映射)。
int newIndex = -1;
if (!InsertSignalBelowSource(request.groupIndex, request.signalIndex, std::move(derived),
makeVisible, newIndex))
{
return false;
}
// 3b. 时间轴偏移:**只**写进新信号的样式(不再烘进采样值,见 2c 的说明)。
// 绘制路径本来就把采样画在 `t + offsetTime` 处,因此新信号的曲线与预览曲线
// 逐点重合,同时采样数据保持完整(不会因为平移越界而变成 NaN)。
if (hasSourceStyle && sourceTransform.offsetTime != 0.0)
{
if (SignalStyle* derivedStyle = m_dataSet.Style(SignalRef{request.groupIndex, newIndex}))
{
derivedStyle->offsetTime = sourceTransform.offsetTime;
}
}
// 4. 数据层结构变化:刷新列表、作废折线/派生缓存并重绘。
SyncTabsToDataSet();
return true;
}
bool Application::BuildResampledSignal(int groupIndex, int signalIndex, double interval)
{
const SignalGroup* group = m_dataSet.GroupAt(groupIndex);
if (group == nullptr || signalIndex < 0 ||
signalIndex >= static_cast<int>(group->signals.size()))
{
return false;
}
if (!std::isfinite(interval) || interval <= 0.0)
{
return false;
}
const SignalData& source = group->signals[static_cast<std::size_t>(signalIndex)];
const TimeAxis& axis = group->AxisOf(source);
const std::wstring baseName = source.name;
// 0. 快照源信号的「调整」变换(插入会移动信号存储,之后取 `source` 会悬垂)。
SignalStyle sourceStyle;
bool hasSourceStyle = false;
{
const SignalStyle* style = m_dataSet.Style(SignalRef{groupIndex, signalIndex});
if (style != nullptr)
{
sourceStyle = *style;
hasSourceStyle = true;
}
}
// 1. 收集「变换后」的采样点(升序)。变换与绘制一致:
// 公式映射 → 系数 → 绝对值 → 垂直镜像 → Y 偏移(时间轴偏移不烘值)。
// ⚠️ 判定统一走 `HasValueTransform()`(含公式映射),否则带公式的信号
// 重采样时会**丢掉公式**(用户 2026-11-03 报告)。
const std::size_t count = std::min(axis.time.size(), source.values.size());
std::vector<double> sourceTimes;
std::vector<double> sourceValues;
sourceTimes.reserve(count);
sourceValues.reserve(count);
bool monotonic = true;
double previous = 0.0;
for (std::size_t index = 0; index < count; ++index)
{
const double time = axis.time[index];
const double value = source.values[index];
if (!std::isfinite(time) || !std::isfinite(value))
{
continue;
}
if (!sourceTimes.empty())
{
if (time < previous)
{
monotonic = false;
break;
}
if (time == previous)
{
continue; // 重复时刻只保留第一个
}
}
previous = time;
sourceTimes.push_back(time);
sourceValues.push_back(hasSourceStyle ? ApplyDrawTransform(sourceStyle, value) : value);
}
if (!monotonic || sourceTimes.size() < 2)
{
m_plot.ShowPersistentHint(!monotonic ? L"重采样失败:该信号的时间轴不是单调递增的。"
: L"重采样失败:有效采样点不足两个。");
return false;
}
// 2. 生成目标时间刻度(等间隔)。
const double t0 = sourceTimes.front();
const double t1 = sourceTimes.back();
const double span = t1 - t0;
if (!(span > 0.0))
{
m_plot.ShowPersistentHint(L"重采样失败:该信号的时间跨度为 0。");
return false;
}
// 采样点数上限:防止用户把间隔输得太小导致内存爆炸。
constexpr double kMaxResamplePoints = 20000000.0;
const double stepsExact = std::floor(span / interval + 1.0e-9) + 1.0;
if (!(stepsExact >= 2.0))
{
m_plot.ShowPersistentHint(L"重采样失败:重采样间隔大于信号时间跨度。");
return false;
}
if (stepsExact > kMaxResamplePoints)
{
m_plot.ShowPersistentHint(L"重采样失败:目标采样点过多(超过 2000 万),请增大重采样间隔。");
return false;
}
const std::size_t targetCount = static_cast<std::size_t>(stepsExact);
TimeAxis newAxis;
newAxis.name = axis.name;
newAxis.time.resize(targetCount);
newAxis.monotonic = true;
std::vector<double> resampled(targetCount);
std::size_t cursor = 0;
const std::size_t last = sourceTimes.size() - 1;
for (std::size_t index = 0; index < targetCount; ++index)
{
const double time = std::min(t1, t0 + static_cast<double>(index) * interval);
newAxis.time[index] = time;
// 单调推进游标(O(n) 总代价)。
while (cursor < last && sourceTimes[cursor + 1] < time)
{
++cursor;
}
if (cursor >= last)
{
resampled[index] = sourceValues[last];
continue;
}
const double ta = sourceTimes[cursor];
const double tb = sourceTimes[cursor + 1];
if (time <= ta)
{
resampled[index] = sourceValues[cursor];
continue;
}
if (tb <= ta)
{
resampled[index] = sourceValues[cursor];
continue;
}
const double ratio = (time - ta) / (tb - ta);
const double a = sourceValues[cursor];
const double b = sourceValues[cursor + 1];
resampled[index] = a + (b - a) * ratio;
}
newAxis.minTime = t0;
newAxis.maxTime = newAxis.time.back();
newAxis.finiteCount = targetCount;
// 3. 追加新时间轴(重采样后的采样刻度无法复用源信号的时间轴)。
std::size_t axisIndex = 0;
if (!m_dataSet.AppendGroupAxis(groupIndex, std::move(newAxis), axisIndex))
{
return false;
}
// 4. 组装新信号并插入到源信号下方。
SignalData derived;
derived.name = baseName + L" (重采样 " + FormatFrequency(1.0 / interval) + L")";
derived.axisIndex = axisIndex;
derived.values = std::move(resampled);
int newIndex = -1;
if (!InsertSignalBelowSource(groupIndex, signalIndex, std::move(derived), true, newIndex))
{
return false;
}
// 5. 时间轴偏移**不烘值**:留在新信号的样式上(与其它派生信号同一规则)。
if (hasSourceStyle && sourceStyle.offsetTime != 0.0)
{
if (SignalStyle* derivedStyle = m_dataSet.Style(SignalRef{groupIndex, newIndex}))
{
derivedStyle->offsetTime = sourceStyle.offsetTime;
}
}
SyncTabsToDataSet();
return true;
}
// ---------------------------------------------------------------- 选区截取
bool Application::BuildClippedSignal(int groupIndex, int signalIndex, double begin, double end)
{
const SignalGroup* group = m_dataSet.GroupAt(groupIndex);
if (group == nullptr || signalIndex < 0 ||
signalIndex >= static_cast<int>(group->signals.size()))
{
return false;
}
if (!std::isfinite(begin) || !std::isfinite(end))
{
return false;
}
const double low = std::min(begin, end);
const double high = std::max(begin, end);
if (!(high > low))
{
m_plot.ShowPersistentHint(L"选区截取失败:双光标区间宽度为 0。");
return false;
}
const SignalData& source = group->signals[static_cast<std::size_t>(signalIndex)];
const TimeAxis& axis = group->AxisOf(source);
const std::wstring baseName = source.name;
// 源信号的整套「调整」快照(插入会移动信号存储,之后不能再解引用 `source`)。
SignalStyle sourceStyle;
bool hasSourceStyle = false;
{
const SignalStyle* style = m_dataSet.Style(SignalRef{groupIndex, signalIndex});
if (style != nullptr)
{
sourceStyle = *style;
hasSourceStyle = true;
}
}
// 1. 逐点收集**区间内**的原始采样(不烘焙任何调整)。
// ⚠️⚠️ 比较必须用**显示时刻**(`time + offsetTime`):双光标给出的是屏幕上看到的时刻,
// 而采样画在 `time + offsetTime` 处(`offsetTime` 不进采样值,只平移绘制位置)。
// 若直接拿 `time` 与选区比较,带时间轴偏移的信号会截错区间(用户 2026-11-03)。
const double timeShift = hasSourceStyle ? sourceStyle.offsetTime : 0.0;
const std::size_t count = std::min(axis.time.size(), source.values.size());
std::vector<double> keptTimes;
std::vector<double> keptValues;
keptTimes.reserve(count);
keptValues.reserve(count);
for (std::size_t index = 0; index < count; ++index)
{
const double time = axis.time[index];
if (!std::isfinite(time))
{
continue;
}
const double shown = time + timeShift;
if (shown < low || shown > high)
{
continue;
}
keptTimes.push_back(time);
keptValues.push_back(source.values[index]);
}
if (keptValues.size() < 2)
{
m_plot.ShowPersistentHint(L"选区截取失败:区间内的有效采样点不足两个。");
return false;
}
// 2. 名称尾缀:`(AAAA - BBBB)`,秒、保留三位小数(用户 2026-11-03 明确要求)。
// 数值取**显示时刻**(与双光标读数一致),因此加上 `timeShift`。
const std::wstring suffix =
L" (" + FormatDecimalFixed(keptTimes.front() + timeShift, 3) + L"s - " +
FormatDecimalFixed(keptTimes.back() + timeShift, 3) + L"s)";
// 3. 新建时间轴(截取后的采样刻度无法复用源信号的时间轴)。
TimeAxis newAxis;
newAxis.name = axis.name;
newAxis.time = std::move(keptTimes);
newAxis.monotonic = true;
newAxis.finiteCount = newAxis.time.size();
newAxis.minTime = newAxis.time.front();
newAxis.maxTime = newAxis.time.back();
std::size_t axisIndex = 0;
if (!m_dataSet.AppendGroupAxis(groupIndex, std::move(newAxis), axisIndex))
{
return false;
}
// 4. 组装新信号并插入到源信号下方。
SignalData derived;
derived.name = baseName + suffix;
derived.axisIndex = axisIndex;
derived.values = std::move(keptValues);
int newIndex = -1;
if (!InsertSignalBelowSource(groupIndex, signalIndex, std::move(derived), true, newIndex))
{
return false;
}
// 5. 继承源信号的**整套调整**(不烘值 ⇒ 新曲线与源信号在区间内逐像素重合)。
if (hasSourceStyle)
{
if (SignalStyle* derivedStyle = m_dataSet.Style(SignalRef{groupIndex, newIndex}))
{
derivedStyle->color = sourceStyle.color;
derivedStyle->customColor = sourceStyle.customColor;
derivedStyle->opacity = sourceStyle.opacity;
derivedStyle->lineWidth = sourceStyle.lineWidth;
derivedStyle->mode = sourceStyle.mode;
derivedStyle->formula = sourceStyle.formula;
derivedStyle->gain = sourceStyle.gain;
derivedStyle->absolute = sourceStyle.absolute;
derivedStyle->mirrorVertical = sourceStyle.mirrorVertical;
derivedStyle->mirrorHorizontal = sourceStyle.mirrorHorizontal;
derivedStyle->offsetY = sourceStyle.offsetY;
derivedStyle->offsetTime = sourceStyle.offsetTime;
}
}
SyncTabsToDataSet();
return true;
}
bool Application::ClipSignalInPlace(int groupIndex, int signalIndex, double begin, double end)
{
if (!std::isfinite(begin) || !std::isfinite(end))
{
return false;
}
const double low = std::min(begin, end);
const double high = std::max(begin, end);
if (!(high > low))
{
m_plot.ShowPersistentHint(L"选区截取失败:双光标区间宽度为 0。");
return false;
}
// 破坏性截取会移动/释放采样 ⇒ 先暂停后台派生计算,避免它读到已释放的数据。
m_plot.PauseDerivedAsync();
// ⚠️ 双光标给的是**显示时刻**,而采样时刻是 `time`;这里换算回采样时刻区间
// (`offsetTime` 不烘进采样值,只平移绘制位置)。
double timeShift = 0.0;
if (const SignalStyle* style = m_dataSet.Style(SignalRef{groupIndex, signalIndex}))
{
timeShift = style->offsetTime;
}
// ⚠️⚠️ **必须先让该信号独占时间轴**再裁剪:同组信号常常共享同一条时间轴,
// 直接裁剪会把其它信号的时间基准一起改短 ⇒ 它们的采样与时间错位
//(用户 2026-11-03 报告:"多个信号同时截取时截取出来的信号并不对")。
std::size_t ownAxis = 0;
if (!m_dataSet.ForkAxisForSignal(groupIndex, signalIndex, ownAxis))
{
m_plot.ShowPersistentHint(L"选区截取失败:无法为该信号创建独立时间轴。");
return false;
}
if (!m_dataSet.ClipSignalRange(groupIndex, signalIndex, low - timeShift, high - timeShift))
{
m_plot.ShowPersistentHint(L"选区截取失败:区间内的有效采样点不足两个。");
return false;
}
SyncTabsToDataSet();
return true;
}
bool Application::InsertSignalBelowSource(int groupIndex, int sourceIndex, SignalData&& signal,
bool makeVisible, int& newIndex)
{
newIndex = -1;
// 插入派生信号会移动该分组的信号存储:先暂停后台计算,避免其读取已移动的数据。
m_plot.PauseDerivedAsync();
if (!m_dataSet.InsertDerivedSignal(groupIndex, sourceIndex, std::move(signal), newIndex))
{
return false;
}
for (TabConfig& tab : m_tabs)
{
tab.RemapAfterSignalInsert(groupIndex, newIndex);
}
// 在当前标签页中勾选新信号,并把它所在的网格移到原信号网格的下方。
if (makeVisible && !m_tabs.empty())
{
const SignalRef insertedRef{groupIndex, newIndex};
ActiveTab().SetVisible(insertedRef, true);
const SignalRef sourceRef{groupIndex, sourceIndex};
ActiveTab().MoveLaneOfSignalBelow(insertedRef, sourceRef);
}
return true;
}
bool Application::CloneSignal(const SignalRef& sourceRef)
{
const SignalGroup* group = m_dataSet.GroupAt(sourceRef.groupIndex);
if (group == nullptr || sourceRef.signalIndex < 0 ||
sourceRef.signalIndex >= static_cast<int>(group->signals.size()))
{
return false;
}
const SignalData& source = group->signals[static_cast<std::size_t>(sourceRef.signalIndex)];
// 源信号的样式快照(插入会移动信号存储,之后不能再解引用 `source`)。
SignalStyle transform;
bool hasStyle = false;
if (const SignalStyle* style = m_dataSet.Style(sourceRef))
{
transform = *style;
hasStyle = true;
}
// ⚠️ 用户 2026-11-03 明确要求:「克隆该信号」**不做任何烘焙**。
// 新信号直接使用源信号的**原始采样数据**,配方上只沿用:
// * 外观(颜色 / 自定义色 / 透明度 / 线宽 / 绘制方式);
// * **时间轴偏移**(`offsetTime` 只能留在样式上 —— 共用同一条时间轴,
// 烘焙进采样值会在越界段写 NaN,见 `BuildDerivedSignal` 2c 的说明)。
// 公式映射 / 系数 / 绝对值 / 镜像 / Y 偏移一律**保持默认**(不烘也不应用)。
// 需要“连调整一起复制”请用「克隆该信号(带调整)」(`CloneSignalAdjusted`)。
SignalData clone;
clone.name = L"*" + source.name;
clone.axisIndex = source.axisIndex;
clone.values = source.values;
int newIndex = -1;
if (!InsertSignalBelowSource(sourceRef.groupIndex, sourceRef.signalIndex, std::move(clone),
true, newIndex))
{
return false;
}
// 外观:克隆要**看起来与原信号一样**,但所有数值调整保持默认值。
if (SignalStyle* cloned = m_dataSet.Style(SignalRef{sourceRef.groupIndex, newIndex}))
{
if (hasStyle)
{
cloned->color = transform.color;
cloned->customColor = transform.customColor;
cloned->opacity = transform.opacity;
cloned->lineWidth = transform.lineWidth;
cloned->mode = transform.mode;
// ⚠️ 只保留时间轴偏移,其余调整保持默认(用户 2026-11-03 明确要求)。
cloned->offsetTime = transform.offsetTime;
}
// ⚠️ 单点信号默认会改成「散点 + 最大线宽」(`InsertDerivedSignal` 里的规则),
// 克隆应该用**原信号的样式**,因此上面几行把它覆盖回去了。
}
SyncTabsToDataSet();
SetStatusMessage(L"已克隆信号:" + m_dataSet.GroupAt(sourceRef.groupIndex)
->signals[static_cast<std::size_t>(newIndex)]
.name,
false);
InvalidateRect(m_hwnd, nullptr, TRUE);
return true;
}
bool Application::CloneSignalAdjusted(const SignalRef& sourceRef)
{
const SignalGroup* group = m_dataSet.GroupAt(sourceRef.groupIndex);
if (group == nullptr || sourceRef.signalIndex < 0 ||
sourceRef.signalIndex >= static_cast<int>(group->signals.size()))
{
return false;
}
const SignalData& source = group->signals[static_cast<std::size_t>(sourceRef.signalIndex)];
SignalStyle transform;
bool hasStyle = false;
if (const SignalStyle* style = m_dataSet.Style(sourceRef))
{
transform = *style;
hasStyle = true;
}
// 「克隆该信号(带调整)」:同样是**原始采样数据**(不做烘焙),
// 区别只在样式——完整沿用源信号的整套调整参数(公式映射 / 系数 / Y 偏移 /
// 时间轴偏移 / 水平与垂直镜像 / 绝对值),因此两条曲线**逐像素重合**,
// 且新信号可以独立继续调参而不影响源信号(用户 2026-11-03 明确要求)。
SignalData clone;
clone.name = L"*" + source.name;
clone.axisIndex = source.axisIndex;
clone.values = source.values;
int newIndex = -1;
if (!InsertSignalBelowSource(sourceRef.groupIndex, sourceRef.signalIndex, std::move(clone),
true, newIndex))
{
return false;
}
if (SignalStyle* cloned = m_dataSet.Style(SignalRef{sourceRef.groupIndex, newIndex}))
{
if (hasStyle)
{
// 外观。
cloned->color = transform.color;
cloned->customColor = transform.customColor;
cloned->opacity = transform.opacity;
cloned->lineWidth = transform.lineWidth;
cloned->mode = transform.mode;
// 整套「调整」参数(与源信号完全一致)。
cloned->formula = transform.formula;
cloned->gain = transform.gain;
cloned->absolute = transform.absolute;
cloned->mirrorVertical = transform.mirrorVertical;
cloned->mirrorHorizontal = transform.mirrorHorizontal;
cloned->offsetY = transform.offsetY;
cloned->offsetTime = transform.offsetTime;
}
}
SyncTabsToDataSet();
SetStatusMessage(L"已克隆信号(带调整):" + m_dataSet.GroupAt(sourceRef.groupIndex)
->signals[static_cast<std::size_t>(newIndex)]
.name,
false);
InvalidateRect(m_hwnd, nullptr, TRUE);
return true;
}
bool Application::ApplyPendingDerivedBuild()
{
// 「重采样」的构建流程与均值/弱峰不同(要新建时间轴),单独处理。
{
int laneIndex = -1;
std::vector<PlotView::ResampleBuild> items;
if (m_plot.TakePendingResampleBuild(laneIndex, items) && !items.empty())
{
// **同一文件内必须按下标降序处理**(每次插入只让插入点之后的信号下标 +1)。
std::sort(items.begin(), items.end(),
[](const PlotView::ResampleBuild& a, const PlotView::ResampleBuild& b)
{
if (a.groupIndex != b.groupIndex)
{
return a.groupIndex < b.groupIndex;
}
return a.signalIndex > b.signalIndex;
});
int built = 0;
double lastInterval = 0.0;
for (const PlotView::ResampleBuild& item : items)
{
if (BuildResampledSignal(item.groupIndex, item.signalIndex, item.interval))
{
++built;
lastInterval = item.interval;
}
}
if (built > 0)
{
SetStatusMessage(L"已构建 " + std::to_wstring(built) + L" 条重采样信号(频率 " +
FormatFrequency(lastInterval > 0.0 ? 1.0 / lastInterval : 0.0) +
L")。",
false);
}
return built > 0;
}
}
// 「选区截取」:两个按钮(截取为新信号 / 原始信号截取)共用同一份区间。
{
int laneIndex = -1;
std::vector<PlotView::ClipBuild> items;
if (m_plot.TakePendingClipBuild(laneIndex, items) && !items.empty())
{
// 同一文件内必须按下标降序处理(插入会让后续下标 +1)。
std::sort(items.begin(), items.end(),
[](const PlotView::ClipBuild& a, const PlotView::ClipBuild& b)
{
if (a.groupIndex != b.groupIndex)
{
return a.groupIndex < b.groupIndex;
}
return a.signalIndex > b.signalIndex;
});
int built = 0;
for (const PlotView::ClipBuild& item : items)
{
if (item.destructive
? ClipSignalInPlace(item.groupIndex, item.signalIndex, item.begin, item.end)
: BuildClippedSignal(item.groupIndex, item.signalIndex, item.begin, item.end))
{
++built;
}
}
if (built > 0)
{
SetStatusMessage((items.front().destructive ? L"已截取 " : L"已截取为新信号 ") +
std::to_wstring(built) + L" 条。",
false);
}
return built > 0;
}
}
// 两类「构建为新信号」请求(均值滤波 / 弱峰调整)机制完全一致,
// 统一整理成同一份构建清单后按下标降序处理。
std::vector<DerivedSignalRequest> requests;
{
int laneIndex = -1;
std::vector<PlotView::MeanFilterBuild> items;
if (m_plot.TakePendingMeanFilterBuild(laneIndex, items))
{
for (const PlotView::MeanFilterBuild& item : items)
{
DerivedSignalRequest request;
request.groupIndex = item.groupIndex;
request.signalIndex = item.signalIndex;
request.points = item.points;
request.iterations = item.iterations;
request.weakPeak = false;
requests.push_back(request);
}
}
}
{
int laneIndex = -1;
std::vector<PlotView::WeakPeakBuild> items;
if (m_plot.TakePendingWeakPeakBuild(laneIndex, items))
{
for (const PlotView::WeakPeakBuild& item : items)
{
DerivedSignalRequest request;
request.groupIndex = item.groupIndex;
request.signalIndex = item.signalIndex;
request.amountPercent = item.amountPercent;
request.iterations = item.iterations;
request.weakPeak = true;
requests.push_back(request);
}
}
}
if (requests.empty())
{
return false;
}
if (m_tabs.empty())
{
return false;
}
// **同一文件内必须按下标降序处理**:每次插入只让插入点之后的信号下标 +1,
// 降序处理可保证尚未处理的信号下标始终有效(升序会把新插入的信号当成原信号)。
std::sort(requests.begin(), requests.end(),
[](const DerivedSignalRequest& a, const DerivedSignalRequest& b)
{
if (a.groupIndex != b.groupIndex)
{
return a.groupIndex < b.groupIndex;
}
return a.signalIndex > b.signalIndex;
});
int built = 0;
DerivedSignalRequest last;
for (const DerivedSignalRequest& request : requests)
{
if (BuildDerivedSignal(request, true))
{
++built;
last = request;
}
}
if (built > 0)
{
std::wstring text = L"已构建 " + std::to_wstring(built) + L" 条 ";
if (last.weakPeak)
{
text += L"弱峰调整 " + std::to_wstring(last.amountPercent) + L"% " +
std::to_wstring(last.iterations) + L"次迭代信号。";
}
else
{
text += std::to_wstring(last.points) + L"点均值 " +
std::to_wstring(last.iterations) + L"次迭代滤波信号。";
}
SetStatusMessage(text, false);
}
return built > 0;
}
} // namespace jdi
@@ -0,0 +1,409 @@
// Application 实现分片 —— **菜单**:绘图区右键菜单(含「移动到…」子菜单)、
// 菜单数值回写、主题切换、设置菜单,以及菜单结果的统一派发。
//
// 共用的常量与工具函数在 ApplicationInternal.hpp(命名空间 `appdetail`,全部 inline)。
// 本文件由 2026-11-04 代码审查从 `ApplicationCommands.cpp`(原 2,165 行)按职责拆出,
// **纯搬移、零逻辑改动**。分片划分见 `Documents/Code_Review_Plan.md` 的 P11。
#include "ApplicationInternal.hpp"
namespace jdi
{
void Application::ShowPlotContextMenu(int laneIndex, int x, int y)
{
if (laneIndex < 0)
{
return;
}
m_plotMenuLane = laneIndex;
std::vector<MenuItem> items;
MenuItem header;
header.kind = MenuItemKind::HEADER;
header.label = L"网格 " + std::to_wstring(laneIndex + 1);
items.push_back(header);
// 当前网格的 Y 轴上下限值直接在菜单内的输入框里显示,单击即可就地编辑(菜单不关闭)。
double laneYMin = 0.0;
double laneYMax = 1.0;
m_plot.LaneRange(laneIndex, laneYMin, laneYMax);
MenuItem maxItem;
maxItem.kind = MenuItemKind::EDIT;
maxItem.label = L"Y 轴最大值";
maxItem.id = PLOT_CMD_LANE_Y_MAX;
maxItem.value = FormatValue(laneYMax);
maxItem.filter = EditFilter::NUMBER;
items.push_back(maxItem);
MenuItem minItem;
minItem.kind = MenuItemKind::EDIT;
minItem.label = L"Y 轴最小值";
minItem.id = PLOT_CMD_LANE_Y_MIN;
minItem.value = FormatValue(laneYMin);
minItem.filter = EditFilter::NUMBER;
items.push_back(minItem);
// 选区自适应(切换项):勾选后每次本网格的时间轴选区放大都会自动匹配 Y 轴幅值。
MenuItem autoViewItem;
autoViewItem.label = L"选区 Y 轴自适应";
autoViewItem.id = PLOT_CMD_LANE_AUTO_VIEW;
autoViewItem.checked = m_plot.LaneAutoFitToSelection(laneIndex);
items.push_back(autoViewItem);
// 时间轴自适应到本网格信号范围(原先位于分隔线下方,现移到分隔线上方)。
MenuItem fitLaneTime;
fitLaneTime.label = L"按网格时间轴展开";
fitLaneTime.id = PLOT_CMD_LANE_FIT_TIME;
items.push_back(fitLaneTime);
// 「拆分当前网格」:把该网格内的信号按个拆成独立网格,插到其下方依次排列。
// 可点击时在后方追加「(N个信号)」说明将拆出的网格数;只有 1 条信号时置灰且不加尾缀。
MenuItem splitLane;
const std::size_t laneSignalCount = m_plot.LaneSignalCount(laneIndex);
splitLane.label = (laneSignalCount > 1)
? (L"拆分当前网格(" + std::to_wstring(laneSignalCount) + L"个信号)")
: L"拆分当前网格";
splitLane.id = PLOT_CMD_LANE_SPLIT;
splitLane.enabled = laneSignalCount > 1;
items.push_back(splitLane);
// 实验性功能(二级菜单):横线上方,鼠标悬停即向右展开。
MenuItem experimental; experimental.kind = MenuItemKind::SUBMENU;
experimental.label = L"实验性功能";
{
MenuItem edgeCount;
edgeCount.label = L"边沿计数";
edgeCount.id = PLOT_CMD_LANE_EDGE_COUNT;
edgeCount.checked = m_plot.LaneEdgeCount(laneIndex);
experimental.submenu.push_back(edgeCount);
MenuItem edgeCenter;
edgeCenter.label = L"边沿计数(中心轴)";
edgeCenter.id = PLOT_CMD_LANE_EDGE_CENTER;
edgeCenter.checked = m_plot.LaneEdgeCenterCount(laneIndex);
experimental.submenu.push_back(edgeCenter);
MenuItem meanFilter;
meanFilter.label = L"均值滤波";
meanFilter.id = PLOT_CMD_LANE_MEAN_FILTER;
meanFilter.checked = m_plot.LaneMeanFilter(laneIndex);
experimental.submenu.push_back(meanFilter);
MenuItem weakPeak;
weakPeak.label = L"弱峰调整";
weakPeak.id = PLOT_CMD_LANE_WEAK_PEAK;
weakPeak.checked = m_plot.LaneWeakPeak(laneIndex);
experimental.submenu.push_back(weakPeak);
MenuItem freeMarker;
freeMarker.label = L"自由标记";
freeMarker.id = PLOT_CMD_LANE_FREE_MARKER;
freeMarker.checked = m_plot.LaneFreeMarker(laneIndex);
// 「重采样」位于「自由标记」**上方**(用户 2026-11-02 要求)。
MenuItem resample;
resample.label = L"重采样";
resample.id = PLOT_CMD_LANE_RESAMPLE;
resample.checked = m_plot.LaneResample(laneIndex);
experimental.submenu.push_back(resample);
// 「选区截取」位于「重采样」下方、「自由标记」上方(用户 2026-11-03 要求)。
MenuItem clip;
clip.label = L"选区截取";
clip.id = PLOT_CMD_LANE_CLIP;
clip.checked = m_plot.LaneClip(laneIndex);
experimental.submenu.push_back(clip);
experimental.submenu.push_back(freeMarker);
}
items.push_back(experimental);
MenuItem separator;
separator.kind = MenuItemKind::SEPARATOR;
items.push_back(separator);
// 全局开关:一次性打开/关闭全部网格各自的「选区 Y 轴自适应」。
MenuItem autoFitAllItem;
autoFitAllItem.label = L"选区 Y 轴自适应(全局)";
autoFitAllItem.id = PLOT_CMD_AUTO_FIT_ALL;
autoFitAllItem.checked = m_plot.AllLanesAutoFitToSelection();
items.push_back(autoFitAllItem);
MenuItem fitItem;
fitItem.label = L"全部自适应";
fitItem.id = PLOT_CMD_FIT_ALL;
items.push_back(fitItem);
RECT client = {};
GetClientRect(m_hwnd, &client);
const gd::RectF bounds(0.0f, 0.0f, static_cast<float>(client.right - client.left),
static_cast<float>(client.bottom - client.top));
m_menu.Open(MenuScope::PLOT_VIEW, bounds, x, y, items);
}
void Application::RefreshPlotMenuValues(int laneIndex)
{
if (laneIndex < 0)
{
return;
}
double laneYMin = 0.0;
double laneYMax = 1.0;
m_plot.LaneRange(laneIndex, laneYMin, laneYMax);
m_menu.UpdateItemValue(PLOT_CMD_LANE_Y_MAX, FormatValue(laneYMax));
m_menu.UpdateItemValue(PLOT_CMD_LANE_Y_MIN, FormatValue(laneYMin));
// 「选区 Y 轴自适应」(本网格)与「(全局)」都是切换项,实时反映各自的勾选状态。
m_menu.UpdateItemLabel(PLOT_CMD_LANE_AUTO_VIEW, L"选区 Y 轴自适应",
m_plot.LaneAutoFitToSelection(laneIndex), true);
m_menu.UpdateItemLabel(PLOT_CMD_AUTO_FIT_ALL, L"选区 Y 轴自适应(全局)",
m_plot.AllLanesAutoFitToSelection(), true);
}
void Application::ApplyTheme(ThemeKind kind)
{
if (kind == m_themeKind && m_theme.windowBg == ThemeFor(kind).windowBg)
{
return; // 幂等:同一个主题重复点击不必作废缓存
}
m_themeKind = kind;
m_theme = ThemeFor(kind);
SetCurrentTheme(&m_theme);
// 主题色烘进了多处位图缓存,必须整体作废(只作废场景缓存是不够的:
// 宽基底图层、装饰层、浮动面板层、信号列表行位图里都存着旧配色的像素)。
m_plot.OnThemeChanged();
m_panel.OnThemeChanged();
// 装饰区(标签栏 / 工具栏 / 面板)全部重画,并整窗回送。
m_chromeDirty = true;
m_panelDirty = true;
InvalidateRect(m_hwnd, nullptr, TRUE);
}
void Application::OpenSettingsMenu()
{
// 锚点取齿轮按钮的**实际矩形**:按钮宽度由控件实测,宿主不应自己推算几何。
gd::RectF anchor;
if (!m_toolbar.ButtonRect(ToolbarAction::SETTINGS, anchor))
{
// 尚未布局(极端情形):退化为从菜单快捷键位置展开,而不是什么都不发生。
anchor = m_toolbarRect;
}
std::vector<MenuItem> items;
// ── 调试 ──────────────────────────────────────────────
MenuItem debugHeader;
debugHeader.kind = MenuItemKind::HEADER;
debugHeader.label = L"调试";
items.push_back(debugHeader);
MenuItem boundsItem;
boundsItem.label = L"显示布局边界";
boundsItem.id = kAppCmdShowLayoutBounds;
boundsItem.checked = m_showLayoutBounds;
// 切换项:点击后菜单保持打开,勾选态由宿主回写(与既有「镜像」等切换项一致)。
boundsItem.keepOpen = true;
items.push_back(boundsItem);
// ── 主题配色(二级菜单:鼠标悬停展开,带勾选态)──────────────
// 顺序固定为 Light / Solarised Light / Dark / Solarised Dark(用户 2026-10-29 指定)。
// `keepOpen` 让点击主题后菜单不关闭,勾选态由宿主回写 —— 便于连续预览几种配色。
MenuItem themeItem;
themeItem.kind = MenuItemKind::SUBMENU;
themeItem.label = L"主题配色";
for (int index = 0; index <= static_cast<int>(ThemeKind::SOLARIZED_DARK); ++index)
{
const ThemeKind kind = static_cast<ThemeKind>(index);
MenuItem option;
option.kind = MenuItemKind::NORMAL;
option.label = ThemeName(kind);
option.id = kAppCmdThemeBase + index;
option.checked = (m_themeKind == kind);
option.keepOpen = true;
themeItem.submenu.push_back(option);
}
items.push_back(themeItem);
RECT client = {};
GetClientRect(m_hwnd, &client);
const gd::RectF bounds(0.0f, 0.0f, static_cast<float>(client.right - client.left),
static_cast<float>(client.bottom - client.top));
// 右下展开,锚在按钮**下沿**(留 4 逻辑像素,视觉上不贴着按钮边框)。
const int x = static_cast<int>(anchor.X);
const int y = static_cast<int>(anchor.Y + anchor.Height + 4.0f * m_scale);
m_menu.Open(MenuScope::APP_SETTINGS, bounds, x, y, items);
}
void Application::DispatchMenuResult(const MenuResult& result)
{
if (!result.valid)
{
return;
}
switch (result.scope)
{
case MenuScope::APP_SETTINGS:
{
// 应用设置菜单:全部条目都是**带勾选态的切换项**,菜单保持打开,
// 勾选变化通过 `UpdateItemLabel` 立即回写(连续切换多项时不用反复打开菜单)。
if (result.id == kAppCmdShowLayoutBounds)
{
m_showLayoutBounds = !m_showLayoutBounds;
m_menu.UpdateItemLabel(kAppCmdShowLayoutBounds, L"显示布局边界", m_showLayoutBounds);
// 边界比组件矩形外扩 1px 且跨越所有装饰区 ⇒ 必须整帧重绘。
m_chromeDirty = true;
}
else if (result.id >= kAppCmdThemeBase &&
result.id <= kAppCmdThemeBase + static_cast<int>(ThemeKind::SOLARIZED_DARK))
{
const ThemeKind kind = static_cast<ThemeKind>(result.id - kAppCmdThemeBase);
ApplyTheme(kind);
// 回写勾选态:四项里只有当前主题打勾(子菜单项要用专用接口才会重建布局)。
for (int index = 0; index <= static_cast<int>(ThemeKind::SOLARIZED_DARK); ++index)
{
const ThemeKind option = static_cast<ThemeKind>(index);
m_menu.UpdateSubmenuItemLabel(kAppCmdThemeBase + index, ThemeName(option),
m_themeKind == option);
}
}
InvalidateRect(m_hwnd, nullptr, TRUE);
break;
}
case MenuScope::SIGNAL_PANEL:
{
const bool needRelayout = m_panel.OnMenuCommand(result);
// 「公式映射」等就地输入可能产生**非模态**警告(不使用 Windows 消息框),
// 由绘图区底部的自绘提示浮层显示(与加载失败提示同一套)。
{
std::wstring hint;
if (m_panel.TakePendingHint(hint))
{
m_plot.ShowPersistentHint(hint);
}
}
if (needRelayout)
{
Relayout();
}
InvalidateRect(m_hwnd, nullptr, FALSE);
break;
}
case MenuScope::PLOT_VIEW:
{
// 菜单内联输入框的提交:就地更新网格 Y 轴上下限,菜单保持打开,
// 并把夹取后的真实值回写到菜单上(输入值可能被合法区间修正)。
if (result.editCommit)
{
const int laneIndex = m_plotMenuLane;
if (laneIndex >= 0)
{
m_plot.ApplyLaneValueText(laneIndex, result.id == PLOT_CMD_LANE_Y_MAX, result.editText);
RefreshPlotMenuValues(laneIndex);
}
SetFocus(m_hwnd);
InvalidateRect(m_hwnd, nullptr, FALSE);
break;
}
const int laneIndex = m_plotMenuLane;
m_plotMenuLane = -1;
switch (result.id)
{
case PLOT_CMD_LANE_AUTO_VIEW:
// 切换本网格的「选区 Y 轴自适应」。
m_plot.ToggleLaneAutoFitToSelection(laneIndex);
break;
case PLOT_CMD_AUTO_FIT_ALL:
{
// 全局切换:一次打开/关闭全部网格各自的开关;打开时立即生效一次。
const bool enable = !m_plot.AllLanesAutoFitToSelection();
m_plot.SetAllLanesAutoFitToSelection(enable, true);
break;
}
case PLOT_CMD_LANE_FIT_TIME:
// 「按网格时间轴展开」:展开时间轴后同时把该网格的 Y 轴自适应一次。
m_plot.FitTimeToLane(laneIndex);
break;
case PLOT_CMD_LANE_EDGE_COUNT:
// 「实验性功能 → 边沿计数」:切换本网格的横向光标与统计面板。
m_plot.ToggleLaneEdgeCount(laneIndex);
break;
case PLOT_CMD_LANE_EDGE_CENTER:
// 「实验性功能 → 边沿计数(中心轴)」:切换中心平均值曲线与统计面板。
m_plot.ToggleLaneEdgeCenterCount(laneIndex);
break;
case PLOT_CMD_LANE_MEAN_FILTER:
// 「实验性功能 → 均值滤波」:切换均值滤波曲线与参数面板。
m_plot.ToggleLaneMeanFilter(laneIndex);
break;
case PLOT_CMD_LANE_WEAK_PEAK:
// 「实验性功能 → 弱峰调整」:切换弱峰调整曲线与参数面板。
m_plot.ToggleLaneWeakPeak(laneIndex);
break;
case PLOT_CMD_LANE_FREE_MARKER:
// 「实验性功能 → 自由标记」:切换网格内跟随鼠标的纵向光标与固定数据点面板。
m_plot.ToggleLaneFreeMarker(laneIndex);
break;
case PLOT_CMD_LANE_RESAMPLE:
// 「实验性功能 → 重采样」:切换重采样参数面板。
m_plot.ToggleLaneResample(laneIndex);
break;
case PLOT_CMD_LANE_CLIP:
// 「实验性功能 → 选区截取」:切换选区截取面板。
m_plot.ToggleLaneClip(laneIndex);
break;
case PLOT_CMD_LANE_SPLIT:
// 「拆分当前网格」:信号按个拆成独立网格,新网格顺次插入其下方。
m_plot.SplitLane(laneIndex);
break;
case PLOT_CMD_FIT_ALL:
m_plot.FitAll();
break;
default:
break;
}
SetFocus(m_hwnd);
InvalidateRect(m_hwnd, nullptr, TRUE);
break;
}
case MenuScope::TAB_BAR:
{
const int index = m_tabMenuIndex;
if (result.id == kMenuTabNew)
{
NewTab(true);
}
else if (result.id == kMenuTabClose)
{
CloseTab(index);
}
else if (result.id == kMenuTabCloseOthers)
{
if (index >= 0 && index < static_cast<int>(m_tabs.size()) && m_tabs.size() > 1)
{
const TabConfig keep = m_tabs[static_cast<std::size_t>(index)];
m_tabs.clear();
m_tabs.push_back(keep);
SetActiveTab(0);
}
}
m_tabMenuIndex = -1;
InvalidateRect(m_hwnd, nullptr, TRUE);
break;
}
case MenuScope::NONE:
default:
break;
}
}
} // namespace jdi
@@ -0,0 +1,360 @@
// Application 实现分片 —— **标签页**:新建/关闭/切换/重排、视图同步、窗口标题、
// 标签栏结果回调与标签页右键菜单。
//
// 共用的常量与工具函数在 ApplicationInternal.hpp(命名空间 `appdetail`,全部 inline)。
// 本文件由 2026-11-04 代码审查从 `ApplicationCommands.cpp`(原 2,165 行)按职责拆出,
// **纯搬移、零逻辑改动**。分片划分见 `Documents/Code_Review_Plan.md` 的 P11:
// 标签页 = 本文件;派生/重采样/截取/克隆 = `ApplicationDerive.cpp`;
// 上下文菜单与设置菜单 = `ApplicationMenus.cpp`;其余命令 = `ApplicationCommands.cpp`。
#include "ApplicationInternal.hpp"
namespace jdi
{
void Application::NewTab(bool collapseGroups)
{
TabConfig tab;
tab.SetName(NextTabName());
// 用户新建的标签页:信号分组**默认折起**(用户明确要求:列表默认紧凑;
// 之后在该标签页里新加载的分组也沿用这个默认值)。
tab.SetGroupsCollapsedByDefault(collapseGroups);
tab.SyncToDataSet(m_dataSet);
if (collapseGroups)
{
tab.SetAllCollapsed(true);
}
// ⚠️ **新标签页不存在任何被点选的信号**(用户 2026-11-02 要求):
// 选中状态是**全局共享**的(`DataSet::Selected()`),不清掉的话新建出来的标签页会
// 直接带着上一个标签页的选中信号 —— 样式编辑区于是有自己的“目标”,与新页“空白”
// 的预期不符。清空后样式区回到「未选择信号」状态,直到用户手动点选任意信号。
m_dataSet.SetSelected(SignalRef());
m_tabs.push_back(std::move(tab));
SetActiveTab(static_cast<int>(m_tabs.size()) - 1);
}
void Application::CloseTab(int index)
{
if (index < 0 || index >= static_cast<int>(m_tabs.size()))
{
return;
}
if (m_tabs.size() <= 1)
{
// ⚠️ **唯一标签页不弹警告**(用户 2026-11-02 要求):直接删掉它并新建一个默认的
// 「视图 1」—— 组全部折起、无选中信号、样式区无目标(由 `NewTab(true)` 保证)。
// ⚠️ `m_tabs` 清空后 `NextTabName()` 必然返回「视图 1」,编号自然从 1 重新开始。
// erase 与 `NewTab` 之间**不要**碰 `m_panel` / `m_plot`(它们的上下文仍指向
// 刚被释放的 `TabConfig`);`NewTab` 里的 `SetActiveTab` 会第一时间重新绑定。
m_tabs.erase(m_tabs.begin() + index);
m_activeTab = 0;
NewTab(true);
return;
}
m_tabs.erase(m_tabs.begin() + index);
int next = index;
if (next >= static_cast<int>(m_tabs.size()))
{
next = static_cast<int>(m_tabs.size()) - 1;
}
SetActiveTab(next);
}
void Application::SetActiveTab(int index)
{
if (index < 0 || index >= static_cast<int>(m_tabs.size()))
{
return;
}
m_activeTab = index;
// 重新绑定上下文:面板与绘图区均指向同一数据层与当前标签页配置。
m_panel.SetContext(&m_dataSet, &m_tabs[static_cast<std::size_t>(m_activeTab)]);
m_plot.SetContext(&m_dataSet, &m_tabs[static_cast<std::size_t>(m_activeTab)]);
RefreshTabBar();
Relayout();
// 切换标签页后把该页已选中的信号滚动到可见位置。
// 切换标签页后把共享的选中信号滚动到可见位置(选中状态跨页保持一致)。
m_panel.ScrollToSignal(m_dataSet.Selected());
SynchronizeView(true);
UpdateWindowTitle();
InvalidateRect(m_hwnd, nullptr, TRUE);
}
void Application::RefreshTabBar()
{
std::vector<std::wstring> names;
names.reserve(m_tabs.size());
for (const TabConfig& tab : m_tabs)
{
names.push_back(tab.Name());
}
m_tabBar.SetTabs(names);
m_tabBar.SetActiveIndex(m_activeTab);
}
void Application::SynchronizeView(bool force)
{
if (m_tabs.empty() || m_activeTab < 0)
{
return;
}
TabConfig& tab = m_tabs[static_cast<std::size_t>(m_activeTab)];
const int visibleCount = tab.VisibleCount();
const bool anyVisible = tab.AnyVisible();
const std::size_t laneCount = tab.Lanes().size();
const bool changed = (visibleCount != m_cachedVisibleCount) || (anyVisible != m_cachedAnyVisible) ||
(laneCount != m_cachedLaneCount);
if (!changed && !force)
{
return;
}
const std::size_t previousLaneCount = m_cachedLaneCount;
m_cachedVisibleCount = visibleCount;
m_cachedAnyVisible = anyVisible;
m_cachedLaneCount = laneCount;
// 标签页首次出现可见信号时,把时间轴对齐到可见数据的范围。
// 没有任何可见信号时(未勾选 / 全部取消勾选 / 清空网格)主动复位自适应标记,
// 这样用户下一次勾选信号时会重新做一次自适应,而不是停留在上次的视图范围。
if (!anyVisible)
{
tab.ViewInitialized() = false;
}
else if (!tab.ViewInitialized())
{
// 首次出现可见信号时做一次自适应。单点信号(时间/幅值范围退化成 0)由
// `FitAll` 内部按"单值散点"的规则处理(居中 + 一大格 1s),这里不再分开判断。
m_plot.FitAll();
// ⚠️ **网格纵向滚动也要回到最上方**(用户 2026-10-17 要求):
// “有过网格 → 没有信号显示 → 又加载了需要滚动条数量的网格”这一整类情形,
// 新出现的网格集必须从最上方开始看。旧实现保留了上一次的 `LaneScroll()`,
// 于是重新显示后会**停在与取消显示时相同的滚动位置**(用户报告)。
// 放在 `FitAll()` 之后:`FitAll` 本身不改变滚动量(§11.2 第 112 条),
// 这里显式归零即可;随后 `Relayout`/`RefreshLanes` 会重算几何。
tab.LaneScroll() = 0.0f;
// 用户 2026-10-17 要求:光标工具处于激活状态(单 / 双光标)而之前**没有任何信号绘制**时,
// 第一个信号出现后光标要按“光标出现的规则”重新出现一次,而不是停在旧的残留位置。
// 规则与 `CycleCursorMode()` 首次启用完全同源;光标工具未激活时本调用无副作用。
m_plot.ResetCursorPlacementOnFreshContent();
}
const bool laneCountChanged = (laneCount != previousLaneCount);
if (laneCountChanged || force)
{
// 网格数量变化会影响滚动条与绘图区宽度,必须重新布局。
Relayout();
}
else
{
m_plot.RefreshLanes();
}
}
void Application::SyncTabsToDataSet()
{
for (TabConfig& tab : m_tabs)
{
tab.SyncToDataSet(m_dataSet);
}
// 选中状态由数据层共享维护,数据变动后需校验其是否仍指向存在的信号。
m_dataSet.ValidateSelection();
m_panel.InvalidateRows();
// 分组/信号结构变化会让信号引用与分组地址改变,派生缓存必须整体作废。
m_plot.InvalidateDerivedCaches();
// 强制下一次同步重新计算视图状态。
m_cachedVisibleCount = -1;
m_cachedAnyVisible = false;
m_cachedLaneCount = 0;
if (m_hwnd != nullptr)
{
InvalidateRect(m_hwnd, nullptr, TRUE);
}
}
void Application::UpdateWindowTitle()
{
// 标题栏 = 应用名称 + 分隔竖线 + 内存占用(单位在 KB/MB/GB 间自适应,保留两位小数)。
// 除内存占用外不再附带标签页、文件数量等无关信息。
// ⚠️ 内存口径 = **私有工作集**(与任务管理器"内存"列一致),不是总工作集。
std::wstring title = kWindowTitle;
title += kTitleSeparator;
title += kRamTitlePrefix;
title += FormatByteSize(ProcessPrivateWorkingSetBytes());
// 数值未变化时不调用 SetWindowTextW,避免无谓地重绘标题栏。
if (title == m_windowTitleText)
{
return;
}
m_windowTitleText = title;
SetWindowTextW(m_hwnd, m_windowTitleText.c_str());
}
void Application::OnTabBarResult(const TabBarResult& result)
{
switch (result.type)
{
case TabBarResult::Type::ADD:
NewTab(true);
break;
case TabBarResult::Type::SELECT:
if (result.index != m_activeTab)
{
SetActiveTab(result.index);
}
break;
case TabBarResult::Type::CLOSE:
CloseTab(result.index);
break;
case TabBarResult::Type::CONTEXT:
ShowTabContextMenu(result);
break;
case TabBarResult::Type::REORDER:
MoveTab(result.index, result.targetIndex);
break;
case TabBarResult::Type::RENAME:
if (result.index >= 0 && result.index < static_cast<int>(m_tabs.size()) && !result.text.empty())
{
m_tabs[static_cast<std::size_t>(result.index)].SetName(result.text);
RefreshTabBar();
// SetTabs 会重置各标签的矩形,必须重新布局才能把它们算回来。
Relayout();
InvalidateRect(m_hwnd, nullptr, TRUE);
}
break;
case TabBarResult::Type::NONE:
default:
break;
}
}
void Application::MoveTab(int from, int target)
{
if (from < 0 || from >= static_cast<int>(m_tabs.size()))
{
return;
}
if (target < 0 || target > static_cast<int>(m_tabs.size()))
{
return;
}
// 拖拽语义:target 表示「插入到该下标之前」;向后拖时需扣除自身占位。
int destination = target;
if (destination > from)
{
--destination;
}
destination = std::max(0, std::min(destination, static_cast<int>(m_tabs.size()) - 1));
if (destination == from)
{
return;
}
// 保持当前激活的标签页在重排后仍处于激活状态。
TabConfig moved = m_tabs[static_cast<std::size_t>(from)];
const int activeIndex = m_activeTab;
m_tabs.erase(m_tabs.begin() + from);
m_tabs.insert(m_tabs.begin() + destination, std::move(moved));
int newActive = activeIndex;
if (activeIndex == from)
{
newActive = destination;
}
else if (from < activeIndex && destination >= activeIndex)
{
--newActive;
}
else if (from > activeIndex && destination <= activeIndex)
{
++newActive;
}
SetActiveTab(newActive);
InvalidateRect(m_hwnd, nullptr, TRUE);
}
void Application::ShowTabContextMenu(const TabBarResult& result)
{
if (result.index < 0 || result.index >= static_cast<int>(m_tabs.size()))
{
return;
}
m_tabMenuIndex = result.index;
std::vector<MenuItem> items;
MenuItem header;
header.kind = MenuItemKind::HEADER;
header.label = m_tabs[static_cast<std::size_t>(result.index)].Name();
items.push_back(header);
MenuItem create;
create.label = L"新建标签页";
create.id = kMenuTabNew;
items.push_back(create);
MenuItem close;
close.label = L"关闭该标签页";
close.id = kMenuTabClose;
close.enabled = (m_tabs.size() > 1);
items.push_back(close);
MenuItem closeOthers;
closeOthers.label = L"关闭其它标签页";
closeOthers.id = kMenuTabCloseOthers;
closeOthers.enabled = (m_tabs.size() > 1);
items.push_back(closeOthers);
// 菜单绘制在客户区内,因此把屏幕坐标换算为客户区坐标。
POINT anchor = {result.screenX, result.screenY};
ScreenToClient(m_hwnd, &anchor);
RECT client = {};
GetClientRect(m_hwnd, &client);
const gd::RectF bounds(0.0f, 0.0f, static_cast<float>(client.right - client.left),
static_cast<float>(client.bottom - client.top));
m_menu.Open(MenuScope::TAB_BAR, bounds, anchor.x, anchor.y, items);
}
bool Application::ApplyPendingRename()
{
SignalRef ref;
std::wstring name;
if (!m_plot.TakePendingRename(ref, name))
{
return false;
}
if (!m_dataSet.RenameSignal(ref.groupIndex, ref.signalIndex, name))
{
return false;
}
// 改名只影响显示,但需要刷新左侧列表与绘图区图例。
m_panel.InvalidateRows();
SetStatusMessage(L"信号已重命名为:" + name, false);
InvalidateRect(m_hwnd, nullptr, TRUE);
return true;
}
} // namespace jdi
-7
View File
@@ -3620,13 +3620,6 @@ namespace jdi
* 只在信号带公式时建立;条目键含公式文本与影响数值的样式字段,
* 公式 / 系数 / 镜像等变化时自然失效。用 `deque` 保证追加时已有引用不失效。
*/
/**
* @brief 「公式映射」信号的绘制值缓存(**唯一实现在数据层**)。
*
* 旧实现是 `mutable std::deque<FormulaEnvelopeEntry> m_formulaEnvelopes;` —— 与
* `TabConfig::ComputeLaneRange` 各扫一遍整条信号。现已合并进
* `DataSet::m_formulaTransforms`(见 `DataSet::FormulaTransformFor`)。
*/
/**
* @brief 折线顶点缓存的**容量上限**(条目数)。
*
@@ -1081,6 +1081,103 @@ namespace jdi
/** 实验性功能浮动面板的种类数(遍历与命中测试统一用它,避免新增功能时漏改)。 */
inline const int kEdgePanelKindCount = static_cast<int>(sizeof(kEdgePanelKinds) / sizeof(kEdgePanelKinds[0]));
// ---- 面板状态 / 滚动量的**唯一派发点** --------------------------------------
//
// ⚠️⚠️ 为什么必须有这一组函数:`Lane` 里每种实验性功能各有一份
// `LanePanelState` 与滚动偏移(7 种 × 2 = 14 个字段)。旧实现在**每个**需要
// 按种类取用这些字段的地方各写一遍多层三元式或 `switch`:
// * `EdgePanelLayoutFor` 的 `enabled / open / collapsed` 各一条 7 层嵌套三元式
// (历史多次因为“多/少一个右括号”编译失败);
// * 标题栏拖拽、✕ 关闭、滚轮滚动、折叠切换、缓存键……各自再写一遍。
// 新增一种面板要改 20+ 处,漏一处就是“面板拖不动 / 关不掉 / 滚轮滚不到底”
//(`MARK`、`RESAMPLE`、`CLIP` 都真实踩过)。
//
// 现在这些字段只在这一组函数里按种类分派;**调用方一律用它**。
// ⚠️ 这些 `switch` **不得写 `default:`** —— 让 `-Wswitch` 在新增
// `EdgePanelKind` 而漏登记时直接报警(编译期保护,0 运行时代价)。
/** @brief 取某种面板的 UI 状态(展开/折起/位置)——可写版本。 */
inline LanePanelState& LanePanelStateOf(Lane& lane, EdgePanelKind kind)
{
switch (kind)
{
case EdgePanelKind::COUNT:
return lane.edgePanel;
case EdgePanelKind::CENTER:
return lane.edgeCenterPanel;
case EdgePanelKind::MEAN:
return lane.meanFilterPanel;
case EdgePanelKind::PEAK:
return lane.weakPeakPanel;
case EdgePanelKind::MARK:
return lane.freeMarkerPanel;
case EdgePanelKind::RESAMPLE:
return lane.resamplePanel;
case EdgePanelKind::CLIP:
return lane.clipPanel;
}
// 枚举值越界(只可能来自非法强制转换):返回一个安全的占位对象。
return lane.edgePanel;
}
/** @brief 取某种面板的 UI 状态——只读版本。 */
inline const LanePanelState& LanePanelStateOf(const Lane& lane, EdgePanelKind kind)
{
return LanePanelStateOf(const_cast<Lane&>(lane), kind);
}
/** @brief 取某种面板的**内容滚动偏移**(客户区像素)——可写版本。 */
inline float& LanePanelScrollOf(Lane& lane, EdgePanelKind kind)
{
switch (kind)
{
case EdgePanelKind::COUNT:
return lane.edgeCountScroll;
case EdgePanelKind::CENTER:
return lane.edgeCenterScroll;
case EdgePanelKind::MEAN:
return lane.meanFilterScroll;
case EdgePanelKind::PEAK:
return lane.weakPeakScroll;
case EdgePanelKind::MARK:
return lane.freeMarkerScroll;
case EdgePanelKind::RESAMPLE:
return lane.resampleScroll;
case EdgePanelKind::CLIP:
return lane.clipScroll;
}
return lane.meanFilterScroll;
}
/** @brief 取某种面板的**内容滚动偏移**——只读版本。 */
inline const float& LanePanelScrollOf(const Lane& lane, EdgePanelKind kind)
{
return LanePanelScrollOf(const_cast<Lane&>(lane), kind);
}
/** @brief 该面板对应的功能是否已启用(右键菜单里的开关)。 */
inline bool LanePanelEnabled(const Lane& lane, EdgePanelKind kind)
{
switch (kind)
{
case EdgePanelKind::COUNT:
return lane.edgeCount;
case EdgePanelKind::CENTER:
return lane.edgeCenter;
case EdgePanelKind::MEAN:
return lane.meanFilter;
case EdgePanelKind::PEAK:
return lane.weakPeak;
case EdgePanelKind::MARK:
return lane.freeMarker;
case EdgePanelKind::RESAMPLE:
return lane.resample;
case EdgePanelKind::CLIP:
return lane.clip;
}
return false;
}
/**
* @brief 计算一条派生曲线在给定采样窗口内的取值(同步路径与后台线程共用)。
*
+5 -48
View File
@@ -2186,38 +2186,13 @@ namespace jdi
return false;
}
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
LanePanelState* state = nullptr;
switch (kind)
{
case EdgePanelKind::COUNT:
state = &lane.edgePanel;
break;
case EdgePanelKind::CENTER:
state = &lane.edgeCenterPanel;
break;
case EdgePanelKind::MEAN:
state = &lane.meanFilterPanel;
break;
case EdgePanelKind::PEAK:
state = &lane.weakPeakPanel;
break;
case EdgePanelKind::MARK:
state = &lane.freeMarkerPanel;
break;
case EdgePanelKind::RESAMPLE:
state = &lane.resamplePanel;
break;
case EdgePanelKind::CLIP:
state = &lane.clipPanel;
break;
default:
return false;
}
if (state->collapsed == collapsed)
// ⚠️ 面板状态字段的唯一派发点(见 `LanePanelStateOf` 的说明)。
LanePanelState& state = LanePanelStateOf(lane, kind);
if (state.collapsed == collapsed)
{
return false;
}
state->collapsed = collapsed;
state.collapsed = collapsed;
return true;
}
@@ -2228,25 +2203,7 @@ namespace jdi
return false;
}
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
switch (kind)
{
case EdgePanelKind::COUNT:
return lane.edgePanel.collapsed;
case EdgePanelKind::CENTER:
return lane.edgeCenterPanel.collapsed;
case EdgePanelKind::MEAN:
return lane.meanFilterPanel.collapsed;
case EdgePanelKind::PEAK:
return lane.weakPeakPanel.collapsed;
case EdgePanelKind::MARK:
return lane.freeMarkerPanel.collapsed;
case EdgePanelKind::RESAMPLE:
return lane.resamplePanel.collapsed;
case EdgePanelKind::CLIP:
return lane.clipPanel.collapsed;
default:
return false;
}
return LanePanelStateOf(lane, kind).collapsed;
}
bool PlotView::ToggleLanePanelCollapsed(int laneIndex, EdgePanelKind kind)
+32 -62
View File
@@ -1736,42 +1736,12 @@ namespace jdi
top = std::max(m_plotRect.Y + 2.0f * m_scale,
std::min(top, m_plotRect.Y + m_plotRect.Height - panel.Height - 2.0f * m_scale));
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(m_edgePanelDragLane)];
switch (m_edgePanelDragKind)
{
case EdgePanelKind::CENTER:
lane.edgeCenterPanel.x = left;
lane.edgeCenterPanel.y = top;
break;
case EdgePanelKind::MEAN:
lane.meanFilterPanel.x = left;
lane.meanFilterPanel.y = top;
break;
case EdgePanelKind::PEAK:
lane.weakPeakPanel.x = left;
lane.weakPeakPanel.y = top;
break;
case EdgePanelKind::MARK:
// ⚠️ 漏掉这一支会让「自由标记」面板**拖不动**(位置写进了 edgePanel,
// 下一帧又从 freeMarkerPanel 读到旧值 ⇒ 面板纹丝不动)。
lane.freeMarkerPanel.x = left;
lane.freeMarkerPanel.y = top;
break;
case EdgePanelKind::RESAMPLE:
// ⚠️ 同类缺陷:漏掉这一支会让「重采样」面板**拖不动**(用户 2026-11-03 报告)。
// 位置必须写进它自己的 `resamplePanel`,布局也只从那里读。
lane.resamplePanel.x = left;
lane.resamplePanel.y = top;
break;
case EdgePanelKind::CLIP:
lane.clipPanel.x = left;
lane.clipPanel.y = top;
break;
case EdgePanelKind::COUNT:
default:
lane.edgePanel.x = left;
lane.edgePanel.y = top;
break;
}
// ⚠️ 面板位置必须写进**它自己的** `LanePanelState`(见 `LanePanelStateOf` 的说明):
// 漏写会让面板“纹丝不动”(位置写进别的面板、布局又从自己那里读)。
// 历史上 `MARK` 与 `RESAMPLE` 都踩过这个坑,因此这里只保留唯一派发点。
LanePanelState& dragState = LanePanelStateOf(lane, m_edgePanelDragKind);
dragState.x = left;
dragState.y = top;
m_dragMoved = true;
return true;
}
@@ -1850,6 +1820,9 @@ namespace jdi
const float ratio = (thumbTop - layout.scrollTrack.Y) / travel;
const float target = ratio * maxScroll;
bool scrolled = false;
// ⚠️ 穷举所有面板种类且**不写 `default:`**:将来新增面板而忘登记时,
// `-Wswitch`(属 `-Wall`)会直接报错,而不是静默落到其它面板上。
// (「自由标记」在上面的独立分支处理,不会到达这里。)
switch (m_meanDragKind)
{
case EdgePanelKind::COUNT:
@@ -1858,18 +1831,20 @@ namespace jdi
case EdgePanelKind::CENTER:
scrolled = SetLaneEdgeCenterScroll(m_markerScrollDragLane, target);
break;
case EdgePanelKind::MEAN:
scrolled = SetLaneMeanFilterScroll(m_markerScrollDragLane, target);
break;
case EdgePanelKind::PEAK:
scrolled = SetLaneWeakPeakScroll(m_markerScrollDragLane, target);
break;
case EdgePanelKind::MARK:
break;
case EdgePanelKind::RESAMPLE:
scrolled = SetLaneResampleScroll(m_markerScrollDragLane, target);
break;
case EdgePanelKind::CLIP:
scrolled = SetLaneClipScroll(m_markerScrollDragLane, target);
break;
default:
scrolled = SetLaneMeanFilterScroll(m_markerScrollDragLane, target);
break;
}
if (scrolled)
{
@@ -3419,8 +3394,12 @@ namespace jdi
if (layout.close.Width > 0.0f &&
UiPointInRect(layout.close, static_cast<float>(x), static_cast<float>(y)))
{
// ⚠️ 穷举所有面板种类且**不写 `default:`**(理由同上:漏登记时编译器报警)。
switch (kind)
{
case EdgePanelKind::COUNT:
SetLaneEdgeCount(lane, false);
break;
case EdgePanelKind::CENTER:
SetLaneEdgeCenterCount(lane, false);
break;
@@ -3434,17 +3413,11 @@ namespace jdi
SetLaneFreeMarker(lane, false);
break;
case EdgePanelKind::RESAMPLE:
// ⚠️ 漏掉这一支时 ✕ 点了没反应(落进 `default` 关掉的是「边沿计数」),
// 用户只能改走右键菜单取消功能(用户 2026-11-03 报告)。
SetLaneResample(lane, false);
break;
case EdgePanelKind::CLIP:
SetLaneClip(lane, false);
break;
case EdgePanelKind::COUNT:
default:
SetLaneEdgeCount(lane, false);
break;
}
changed = true;
}
@@ -3569,11 +3542,11 @@ namespace jdi
panelLane, panelKind))
{
const EdgePanelKind hoveredKind = static_cast<EdgePanelKind>(panelKind);
// 「均值滤波 / 中心轴边沿计数 / 弱峰调整 / 边沿计数」面板:内容区超高时用滚轮翻页
//(上百个信号合并时必备)。
if (hoveredKind == EdgePanelKind::MEAN || hoveredKind == EdgePanelKind::CENTER ||
hoveredKind == EdgePanelKind::PEAK || hoveredKind == EdgePanelKind::COUNT ||
hoveredKind == EdgePanelKind::RESAMPLE || hoveredKind == EdgePanelKind::CLIP)
// 「均值滤波 / 中心轴边沿计数 / 弱峰调整 / 边沿计数 / 重采样 / 选区截取」面板:
// 内容区超高时用滚轮翻页(上百个信号合并时必备)。
// ⚠️ 除「自由标记」外的所有面板都走这一条(它有自己的独立分支,见下)——
// 写成“逐个列举 kind”会让新增面板默默失去滚轮(历史踩过)。
if (hoveredKind != EdgePanelKind::MARK)
{
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoveredKind);
if (layout.content.Width > 0.0f &&
@@ -3586,17 +3559,11 @@ namespace jdi
{
const Lane& wheelLane =
Tab().Lanes()[static_cast<std::size_t>(panelLane)];
const float current = (hoveredKind == EdgePanelKind::CENTER)
? wheelLane.edgeCenterScroll
: (hoveredKind == EdgePanelKind::PEAK)
? wheelLane.weakPeakScroll
: (hoveredKind == EdgePanelKind::COUNT)
? wheelLane.edgeCountScroll
: (hoveredKind == EdgePanelKind::RESAMPLE)
? wheelLane.resampleScroll
: (hoveredKind == EdgePanelKind::CLIP)
? wheelLane.clipScroll
: wheelLane.meanFilterScroll;
// ⚠️ 读取当前滚动量必须按**同一种分派规则**(`LanePanelScrollOf`):
// 旧实现这里是一条三元式链,漏掉新面板时会退化成
// `meanFilterScroll`(恒 0)⇒ 每次滚轮都从 0 开始算、
// 永远滚不到底(用户 2026-11-03 报告「选区截取」的正是这一处)。
const float current = LanePanelScrollOf(wheelLane, hoveredKind);
const float next = current - static_cast<float>(notches * 40.0 * m_scale);
const float target = std::max(0.0f, std::min(next, maxScroll));
switch (hoveredKind)
@@ -3616,9 +3583,12 @@ namespace jdi
case EdgePanelKind::CLIP:
SetLaneClipScroll(panelLane, target);
break;
default:
case EdgePanelKind::MEAN:
SetLaneMeanFilterScroll(panelLane, target);
break;
case EdgePanelKind::MARK:
// 「自由标记」由下面的独立分支处理(它的内容高度来自布局)。
break;
}
return true;
}
+14 -71
View File
@@ -343,38 +343,18 @@ namespace jdi
const bool listStyle = (meanFilter || centerAxis || weakPeak || resamplePanel || clipPanel);
const bool countTable = (kind == EdgePanelKind::COUNT);
const bool hasButton = (meanFilter || weakPeak || resamplePanel || clipPanel);
const bool enabled = meanFilter ? lane.meanFilter
: (weakPeak ? lane.weakPeak
: (freeMarker ? lane.freeMarker
: (resamplePanel ? lane.resample
: (clipPanel ? lane.clip
: (centerAxis ? lane.edgeCenter
: lane.edgeCount)))));
const bool open = meanFilter ? lane.meanFilterPanel.open
: (weakPeak ? lane.weakPeakPanel.open
: (freeMarker ? lane.freeMarkerPanel.open
: (resamplePanel
? lane.resamplePanel.open
: (clipPanel
? lane.clipPanel.open
: (centerAxis
? lane.edgeCenterPanel.open
: lane.edgePanel.open)))));
// ⚠️ 面板的「启用 / 展开 / 折起」状态一律通过 `LanePanelEnabled` /
// `LanePanelStateOf` 取值(见 `PlotViewInternal.hpp` 的说明):
// 旧实现是三条 7 层嵌套三元式,新增面板时极容易漏改或写错括号。
const bool enabled = LanePanelEnabled(lane, kind);
const LanePanelState& panelState = LanePanelStateOf(lane, kind);
const bool open = panelState.open;
if (!enabled || !open)
{
return layout;
}
// 面板的**折起**状态(只显示标题栏)。折起时仍然算宽度与位置,但内容区全部省略。
const bool collapsed = meanFilter ? lane.meanFilterPanel.collapsed
: (weakPeak ? lane.weakPeakPanel.collapsed
: (freeMarker ? lane.freeMarkerPanel.collapsed
: (resamplePanel
? lane.resamplePanel.collapsed
: (clipPanel
? lane.clipPanel.collapsed
: (centerAxis
? lane.edgeCenterPanel.collapsed
: lane.edgePanel.collapsed)))));
const bool collapsed = panelState.collapsed;
layout.collapsed = collapsed;
int rowCount = 0;
@@ -597,26 +577,8 @@ namespace jdi
//(用户 2026-10-17 报告:“在当前范围内的网格开启实验性功能,面板首次出现在右下角”)。
// 改用**可见序数**:它的量级只有一屏的网格数(约 20),再夹到“面板上沿不超过绘图区
// 中间偏下”以内,于是面板始终停**右上区域**,同时保留“依次错开”的观感。
const double storedX = meanFilter ? lane.meanFilterPanel.x
: (weakPeak ? lane.weakPeakPanel.x
: (freeMarker ? lane.freeMarkerPanel.x
: (resamplePanel
? lane.resamplePanel.x
: (clipPanel
? lane.clipPanel.x
: (centerAxis
? lane.edgeCenterPanel.x
: lane.edgePanel.x)))));
const double storedY = meanFilter ? lane.meanFilterPanel.y
: (weakPeak ? lane.weakPeakPanel.y
: (freeMarker ? lane.freeMarkerPanel.y
: (resamplePanel
? lane.resamplePanel.y
: (clipPanel
? lane.clipPanel.y
: (centerAxis
? lane.edgeCenterPanel.y
: lane.edgePanel.y)))));
const double storedX = panelState.x;
const double storedY = panelState.y;
int openPanelOrdinal = 0;
{
// ⚠️ 错开量用「**当前真正显示出来的面板**中排在本面板之前的个数」:
@@ -626,37 +588,18 @@ namespace jdi
// ⚠️ 不能用 `laneIndex`(上百个网格时会把面板推到右下角);
// ⚠️ 也不能只看 `*Panel.open`:该标志**默认就是 true**,必须同时看功能是否启用
// (与 `EdgePanelLayoutFor` 的 `enabled && open` 判定一致)。
const auto shown = [](const Lane& lane, EdgePanelKind panelKind) -> bool
const auto shown = [](const Lane& panelLane, EdgePanelKind panelKind) -> bool
{
switch (panelKind)
{
case EdgePanelKind::COUNT:
return lane.edgeCount && lane.edgePanel.open;
case EdgePanelKind::CENTER:
return lane.edgeCenter && lane.edgeCenterPanel.open;
case EdgePanelKind::MEAN:
return lane.meanFilter && lane.meanFilterPanel.open;
case EdgePanelKind::PEAK:
return lane.weakPeak && lane.weakPeakPanel.open;
case EdgePanelKind::MARK:
return lane.freeMarker && lane.freeMarkerPanel.open;
case EdgePanelKind::RESAMPLE:
return lane.resample && lane.resamplePanel.open;
case EdgePanelKind::CLIP:
return lane.clip && lane.clipPanel.open;
}
return false;
return LanePanelEnabled(panelLane, panelKind) &&
LanePanelStateOf(panelLane, panelKind).open;
};
static const EdgePanelKind kOrder[7] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
EdgePanelKind::MARK, EdgePanelKind::RESAMPLE,
EdgePanelKind::CLIP};
// ⚠️ 面板错开序数按 `kEdgePanelKinds` 的固定顺序统计(与绘制 / 命中同源)。
int openSeen = 0;
bool found = false;
for (std::size_t lane = 0; lane < m_lanes.size() && !found; ++lane)
{
const Lane& tabLane = Tab().Lanes()[lane];
for (const EdgePanelKind panelKind : kOrder)
for (const EdgePanelKind panelKind : kEdgePanelKinds)
{
if (!shown(tabLane, panelKind))
{
+10 -8
View File
@@ -359,19 +359,21 @@ namespace jdi
return;
}
// ⚠️ 跳过根控件:它的矩形与工具栏区域完全重合(已经被上一层登记过)。
for (const std::unique_ptr<UiWidget>& button : m_root->Children())
//
// ⚠️⚠️ **这里绝不能出现 `dynamic_cast`**:旧写法用它把 `UiWidget*` 转成
// `UiButtonWidget*`,但 `Button::widget` 本来就是 `UiButtonWidget*`。
// 一个多余的转换会链入 RTTI 元数据 + `cp-demangle.o`(Release map 里
// **56,320 B**;最小复现程序实测 **+75,264 B**),并逼得全项目无法用
// `-fno-rtti`。现在改为直接遍历 `m_buttons`(与控件树同源,且无需转换)。
for (const Button& spec : m_buttons)
{
if (button == nullptr)
if (spec.widget == nullptr)
{
continue;
}
boxes.Add(button->Bounds(), 1);
boxes.Add(spec.widget->Bounds(), 1);
// 按钮**内容**(文本或齿轮图标)单独上报:它是居中绘制的,与按钮矩形不同。
const UiButtonWidget* raw = dynamic_cast<const UiButtonWidget*>(button.get());
if (raw != nullptr)
{
boxes.Add(raw->ContentRect(), 2);
}
boxes.Add(spec.widget->ContentRect(), 2);
}
}
+176
View File
@@ -0,0 +1,176 @@
# Code Review 计划(分支 `Code_Review`)
> 目标:在不引入性能回归、不改变用户可见行为的前提下,对全项目做一次**结构 + 性能 + 体积**
> 的查漏补缺,并把「改一个功能要动 40 处」的屎山点收敛成表驱动/单一实现。
>
> 基线(本计划开始时):
> - 分支 `Code_Review`,起点 `764e5be`(dev 已删除过时的 `UI_Refactor_*` 三份文档)。
> - `make release` 产物 **890,880 B**(`.text` 753,136 B、`.rdata` 36,936 B、`.rsrc` 20,656 B、
> `.pdata` 30,420 B、`.xdata` 29,496 B、`.idata` 12,856 B)。
> - 源码约 **5.6 万行**;最大单文件 `PlotViewPaint.cpp` 6,561 行、`PlotView.hpp` 4,654 行。
>
> 约束:
> - 每完成一个节点**编译验证(release + debug 零警告)**并**提交**,同时更新
> `Documents/Code_Review_Progress.md`。
> - 性能相关节点必须给出**同一次运行内的相对量**(历史教训:绝对毫秒阈值不可靠)。
> - 不占用用户的鼠标/键盘/屏幕(只用纯控制台探针或源码级核对)。
---
## 一、已确认的问题(本计划的事实基础)
### A. 体积
| 项 | 实测 | 说明 |
| --- | --- | --- |
| `dynamic_cast`(`Toolbar.cpp:370`) | **+75,264 B** | 最小复现程序:39,936 B → 115,200 B。它会链入 RTTI 元数据 + `cp-demangle.o`(map 里 **56,320 B**)。而该处 `Button::widget` **本来就是 `UiButtonWidget*`** ⇒ 转换完全多余。 |
| `-fno-rtti` | 未启用 | 移除上一条后即可开启,进一步去掉 `type_info` 表。 |
| `.pdata` + `.xdata` | 60 KB | `-fno-exceptions` 已开;SEH 展开表仍存在。可尝试 `-fno-asynchronous-unwind-tables`(历史上链接失败,需复核)。 |
| `libmingwex` 的 `strtodg/gdtoa/mingw_wpformat` | 27.8 KB | 来自 `wcstod` / `wprintf` 类调用;可评估用自写解析替代 `wcstod` 的收益(公式/输入框解析)。 |
| `.rsrc` | 20.7 KB | 图标(PNG 帧)+ manifest + 版本信息,已优化过。 |
| 重复常量/重复实现 | 多处 | `kFormulaEnvelopeBuckets` 已清理;仍有 `ClampDouble` 两份实现等。 |
### B. 性能(结构性热点)
1. **面板开关/参数处理是 6 个 `EdgePanelKind` × ~20–26 处 switch 分支**(实测每个 kind 出现
21–26 次,全部由手写 `if/switch` 维护)。这是**新增功能必然漏改 2–3 处**的根本原因
(历史已发生 4 次:`MARK` 漏拖动、`RESAMPLE` 漏拖动+关闭、`CLIP` 漏滚轮+缓存键)。
2. **`SceneContentHash` 里 6 段几乎相同的逐面板混入循环**(`PlotViewCacheKey.cpp`)。
3. **`EdgePanelLayoutFor` 是巨型分支**,`EdgePanelLayout` 结构体带 20+ 个按面板类型区分的
矩形数组;高度算式与几何算式分开写(历史上多减一个 pad 造成空白空白 BUG)。
4. **`PlotView` 有 30+ 个缓存**,每个都手写「键 + DC + 位图 + 贴图 + 失效」五件套;
失效路径散落在 8 个文件里,`m_sceneValid=false` 出现 6 处。
5. `ApplicationCommands.cpp` **2,165 行**混杂:标签页、数据派生、菜单、工具栏、键盘、鼠标。
6. `PlotView.hpp` 4,654 行(声明 + 内联实现),单文件认知负担极大。
### C. 过时/错误注释与命名(抽样已确认)
- `agent_mem.md` §3「目录结构」仍写着已不存在的 `Utilities/CsvTable/`、`Utilities/DataModel/`
等目录;§2 写 `-Os` 而实际是 `-Oz`。
- 源码中留有历史轮次的「本轮」「上一轮」叙述(对当前读者无意义),以及被推翻方案的注释。
- `PlotViewInternal.hpp` 里 `kFormulaEnvelope*` 注释已随实现迁移,需同步。
---
## 二、节点计划
> 优先级:**S**(安全、收益明确、风险低)→ **A**(结构调整,中风险,需逐步验证)→
> **B**(深度重构,必须有可验证的前后一致性)。
### 阶段 0:建立基线(节点 P0)
- P0.1 记录基线:release/debug 体积、`.text/.rdata` 等节表、关键路径的可复跑测量程序
(纯控制台,链接 `Build/Release/*.o`,不入库)。**已完成基线的部分见下表**。
- P0.2 建立 `Documents/Code_Review_Progress.md`,每节点完成后追加一行结论 + 数字。
### 阶段 1:体积(节点 P1–P5,安全优先)
- **P1 移除 `dynamic_cast` + 开启 `-fno-rtti`**
- 把 `Toolbar::BuildBoxes` 的 `dynamic_cast<const UiButtonWidget*>` 改为直接用
`Button::widget`(已是该类型)或给 `UiWidget` 加虚函数 `ContentBounds()`。
- Makefile `CXXFLAGS += -fno-rtti`;全仓确认无 `typeid`/`dynamic_cast`/`type_info`。
- 预期 **−70~80 KB**。验收:产物下降 ≥60 KB;功能逐项核对(工具栏按钮、布局边界调试)。
- **P2 体积审计第二轮**
- 复核 `-fno-asynchronous-unwind-tables`(历史上链接失败,试 `-fno-unwind-tables` 组合)。
- 评估 `wcstod` 替身(自写定点/浮点解析,仅在公式与输入框使用)去掉
`strtodg/gdtoa/gethex/hexnan`(约 20 KB)。
- 评估 `--gc-sections` 在 LTO 下是否已生效(读 map 确认无死节)。
- 验收:逐项给出「测得的字节数 + 是否采纳」的结论,**不接受「感觉变小」**。
- **P3 重复实现归并**
- `ClampDouble` 两份(`PlotViewInternal.hpp` / `UiLayout.cpp`)→ 统一到 `Shared`。
- 扫描全部 `static`/匿名命名空间重复工具函数,合并为单一实现。
- 重复字符串/常量字面量、重复的三元式开关表。
- **P4 死代码与未使用成员清理**
- 用非 LTO 对象 + `nm` 反查「定义但从未被引用」的函数;删除确认无引用的实现。
- 清理 `PlotView.hpp` 中已无人读取的成员/字段。
- **P5 资源与启动数据**
- `.rsrc` 复核(图标帧数、manifest、版本字符串);`.idata` 导入表复核。
### 阶段 2:性能(节点 P6–P10)
- **P6 建立可复跑的渲染性能探针(纯控制台)**
- 目标:能在**不弹窗口**的情况下测量 `BuildLaneVisuals` / `SceneContentHash` /
`EdgePanelLayoutFor` / 面板绘制 / 信号面板绘制的耗时。
- 方式:链接 `Build/Release/*.o` + 手工构造 `DataSet`/`TabConfig`,只调用纯函数与
`PlotView` 的非窗口方法(避免 `Application::Initialize` 触发模态框,见历史教训)。
- 所有断言用**同一进程内的相对量**。
- **P7 面板元数据表驱动(核心)**
- 新增 `EdgePanelSpec`(每 kind 一行:命令 id、默认尺寸、是否列表型、footer 类型、
滚动字段偏移、面板状态字段偏移、绘制函数指针、命中函数指针)。
- 把 6×~20 处 `switch (kind)` 收敛为「查表 + 3 个通用实现(列表型 / 表格型 / 固定行型)」。
- 验收:**逐像素**对比改造前后面板(离屏渲染哈希);新增一个假 kind 只需加一行表项即可
被绘制/命中/拖动/关闭/滚动全覆盖(用编译期断言或探针验证)。
- **P8 `SceneContentHash` 混入收敛**
- 把 6 段重复循环改为「按面板表遍历 + 每面板一个 `MixContent()`」。
- 验收:改造前后哈希值**逐位相同**(同一状态)。
- **P9 `EdgePanelLayoutFor` 表驱动 + 高度/几何同源**
- 高度算式与内容几何共用同一份 `PanelMetrics` 计算(消除重复减 pad 类 BUG)。
- 验收:探针打印「内容区下边界 vs 按钮上边界」的像素差,与改造前一致。
- **P10 缓存五件套抽象(低风险版)**
- 抽出 `OffscreenCache`(DC/位图/键/贴图/失效),**只替换三处结构完全相同的**
(derived / decor / panel),保留 scene 与 lane layer 不动(热路径最复杂)。
- 验收:逐像素一致 + 帧时间不回退。
### 阶段 3:架构(节点 P11–P14)
- **P11 `ApplicationCommands.cpp` 拆分**
- → `ApplicationTabs.cpp`(标签页)/ `ApplicationDerive.cpp`(派生/重采样/截取/克隆)/
`ApplicationCommands.cpp`(命令分发)/ `ApplicationMenus.cpp`(上下文菜单与设置菜单)。
- 纯搬移,零行为变更;Makefile 无需改动(按目录通配收集)。
- **P12 `PlotView.hpp` 拆分**
- 把 `EdgePanelLayout`、派生曲线缓存条目、图层结构体等**纯数据结构**搬到
`PlotViewTypes.hpp`;把 `PlotView` 的内联小函数按关注点分块放到 `PlotViewInline.hpp`。
- 目标:`PlotView.hpp` ≤ 2,500 行。
- **P13 UI 基础层职责核对**
- 核对 `UiWidget` / `UiComponent` / `UiLayout` / `UiBox` / `UiSurface` / `UiButtonWidget` /
`UiControls` 的边界是否重叠;合并确认冗余的(只保留一套矩形/命中/布局原语)。
- 明确依赖方向:`Components/*` → `Components/Shared` → `Utilities`(校验无反向)。
- **P14 绘制/功能组件边界**
- `PlotViewPaint` / `Raster` / `Panels` / `Features` / `Interaction` 的职责再核对;
把「面板几何」与「面板绘制」的交叉依赖理顺(几何 → 共享 `PanelMetrics`)。
- 把散落在 `PlotView` 里的「颜色/线宽/胶囊样式」常量收敛到 `Theme`/`PaintUtil`。
### 阶段 4:注释与命名清理(节点 P15)
- **P15 注释/命名审计**
- 删除「本轮/上一轮」叙述、被推翻方案的描述、与实现不符的注释。
- 统一命名:`*Dc`/`*DC`、`Lane*` vs `Grid*`、`Edge*` vs `Panel*` 等不一致处。
- 确保所有注释与当前实现一致(重点:`PlotViewInternal.hpp`、`PlotViewCacheKey.cpp`)。
- 同步更新 `agent_mem.md` 的 §2/§3(已经不准确的目录结构与 `-Os`)。
### 阶段 5:验证与报告(节点 P16)
- **P16 最终验证与报告**
- release + debug 零警告;产物体积对比;关键路径探针全部通过。
- 生成 `Documents/Code_Review_Report.md`:逐节点写「改了什么 / 为什么 / 效果 /
未做的事与原因」,并给出体积与性能的前后对照表。
---
## 三、验收标准(硬性)
1. `make release` 与 `make debug` **零 error、零 warning**。
2. 产物体积**必须下降**(目标:从 890,880 B 降到 ≤ 780 KB;若某项无法达成,在报告里
说明实测字节数与原因)。
3. 不改变任何用户可见行为(面板几何、绘制像素、交互语义一致)。
4. 每条性能结论都必须有**同进程相对量**或**逐像素/逐位一致性**证据。
5. 每个节点单独提交,提交信息用中文、说明改动与验证方式。
---
## 四、风险与对策
| 风险 | 对策 |
| --- | --- |
| 面板表驱动改造引入像素差异 | 改造前后用离屏渲染哈希对比;先改 `CLIP`/`MARK` 两个最简单面板做样板,再推广 |
| `-fno-rtti` 破坏第三方/库代码 | 全仓 grep 确认;先单独提交,失败可独立回退 |
| 缓存抽象拖慢热路径 | 只抽象非热路径(derived/decor/panel),scene/lane 不动;每次改完测帧时间 |
| 大文件拆分时误改逻辑 | 纯搬移(不重写函数体),逐文件 diff 核对;先拆 `ApplicationCommands` |
| 探针触发模态「内存不足」框 | 绝不在探针里调 `Application::Initialize()`(历史教训);只构造数据层/纯函数 |
---
## 五、进度索引
进度与每节点的实测数字见 `Documents/Code_Review_Progress.md`。
最终结论见 `Documents/Code_Review_Report.md`。
+150
View File
@@ -0,0 +1,150 @@
# Code Review 进度(分支 `Code_Review`)
> 每个节点完成后追加:节点号、改动摘要、验证方式、实测数字。
> 计划见 `Documents/Code_Review_Plan.md`;最终报告见 `Documents/Code_Review_Report.md`。
## 基线(起点 `764e5be`)
| 项 | 数值 |
| --- | --- |
| Release 产物 | **890,880 B** |
| `.text` | 753,136 B |
| `.rdata` | 36,936 B |
| `.pdata` | 30,420 B |
| `.xdata` | 29,496 B |
| `.rsrc` | 20,656 B |
| `.idata` | 12,856 B |
| Debug 产物 | 1,801,617 B |
| 源码规模 | 约 5.6 万行 |
关键发现(详见计划 §一):
* `dynamic_cast`(`Toolbar.cpp:370`)使最小复现程序从 39,936 B 涨到 115,200 B
⇒ **+75,264 B**;map 中 `cp-demangle.o` 占 **56,320 B**。
* 每个 `EdgePanelKind` 在源码中出现 21–26 次(6 种面板 ⇒ 约 140 处手写分支)。
---
## 节点记录
(下面按完成顺序追加)
### P1 移除 `dynamic_cast` + 关闭 RTTI —— ✅ 完成(提交 `5de3763`)
**改动**
1. `Toolbar::BuildBoxes` 里唯一的 `dynamic_cast` 是**多余强转**(`Button::widget` 本来就是
`UiButtonWidget*`)。改为直接遍历 `m_buttons`(与 `m_root->Children()` 同源同序)。
2. 仅关 RTTI **不够**:`cp-demangle.o` 仍在链接里。真凶是 libstdc++ 的 `vterminate.o`
(引用 `__cxa_demangle`),它被 `eh_terminate.o` 的 `std::terminate` 拉入。
⇒ 在 `Utilities/Src/MemProbe.cpp` 里**自行定义** `__gnu_cxx::__verbose_terminate_handler()`,
归档成员 `vterminate.o` 不再被拉入,demangler 整体消失。
应用启动后本来就会 `std::set_terminate` 换成自己的处理函数,行为等效。
3. `Makefile` 新增 `NO_RTTI=1`(默认开)→ `CXXFLAGS += -fno-rtti`。
**实测**
| 指标 | 改前 | 改后 | 差 |
| --- | --- | --- | --- |
| Release | 890,880 B | **829,440 B** | **−61,440 B** |
| `.text` | 753,136 B | 703,616 B | −49,520 B |
| `.rdata` | 36,936 B | 26,616 B | −10,320 B |
| Debug | 1,801,617 B | 1,734,578 B | −67,039 B |
**验证**:release + debug **全量重建**(删 `*.o`/`*.d`)零 error 零 warning;
map 中 `cp-demangle.o` 已无 `.text` 段分配。
### P2 Release 关闭异步展开表 + 修正 Debug 构建参数 —— ✅ 完成(提交 `94fc1c4`)
**改动**
1. 新增 `RELEASE_ONLY_OPT = -fno-asynchronous-unwind-tables`,**只**加在 Release 的两条
编译规则上。`-fno-exceptions` 下绝大多数函数不再需要 `.pdata`/`.xdata`。
* 代价(已评估接受):Release 崩溃时 WER 拿不到可靠调用栈;不打印栈回溯的程序
功能不受影响,Debug 构建保留完整展开表。
* `-fno-unwind-tables`(全关)会链接失败,不采用。
2. **修正真实缺陷**:`DEBUG_OPT = -O0` 定义了却**从未被使用** —— Debug 的两条编译规则
误用 `$(RELEASE_OPT)`(`-Oz`),且全程没有 `-g`。所谓 "Debug 构建" 其实只是
"不 strip 的 Release",根本无法调试。现改用 `$(DEBUG_OPT) -g`,链接也加 `-g`。
**实测**
| 指标 | 改前 | 改后 |
| --- | --- | --- |
| Release | 829,440 B | **793,088 B**(−36,352) |
| `.pdata` | 30,420 B | 12,684 B |
| `.xdata` | 28,904 B | 10,156 B |
| Debug | 1,734,578 B | 5,992,064 B(真实 `-O0 -g`,属预期) |
**验证**:release 与 debug 均全量重建,零 error 零 warning(`-O0` 下也没有新警告)。
### P7 面板状态/滚动量收敛为唯一派发点 —— ✅ 完成(提交 `7a3fefb`)
**问题(屎山核心之一)**:`Lane` 里 7 种实验性功能各有 1 份 `LanePanelState` + 1 个滚动偏移
(14 个字段)。旧实现在**每一处**“按种类取用”的地方各写一遍嵌套三元式或 `switch`:
`EdgePanelLayoutFor` 5 条 7 层嵌套三元式;标题栏拖拽、✕ 关闭、滚轮、滚动条、错开序数各写一遍。
新增面板要改 20+ 处,漏一处就是「拖不动 / 关不掉 / 滚轮滚不到底」
(`MARK`、`RESAMPLE`、`CLIP` 均真实踩过)。
**改动**:`PlotViewInternal.hpp` 新增唯一派发点
`LanePanelEnabled` / `LanePanelStateOf` / `LanePanelScrollOf`(可写 + 只读)。
这些 `switch` **穷举 7 种且不写 `default:`** ⇒ `-Wswitch` 在漏登记时编译报警
(已用最小程序验证 `-Wswitch` 确实会报)。
调用点:布局 5 条三元式 → 3 行;标题栏拖拽 7 分支 switch → 1 次写入;
滚轮/滚动条/✕ 关闭/折叠切换全部改为穷举无 `default`。
**验证**:release + debug 零 warning;产物 **793,088 B 不变**(等价变换,说明只是结构收敛)。
**未做(受验证条件限制)**:尚未把 `EdgePanelLayoutFor` 的 957 行分支整体改成
“每面板一行元数据表”。原因:没有可用的离屏像素对比工具(`.preflight` 已删除,
且崩溃会弹模态框),纯逻辑重写无法自证“像素完全一致”。已通过上面的派发收敛
消除最容易出错的一类重复,剩余重复属于“几何算式本身”,风险高于收益。
### P11 拆分 `ApplicationCommands.cpp` —— ✅ 完成(提交 `8437100`)
原文件 **2,165 行**,混杂六类职责。按职责**纯搬移**(零逻辑改动):
| 新文件 | 行数 | 内容 |
| --- | --- | --- |
| `ApplicationTabs.cpp` | 355 | 标签页新建/关闭/切换/重排、视图同步、窗口标题、标签栏回调、标签页右键菜单 |
| `ApplicationDerive.cpp` | 786 | 派生烘焙、重采样、选区截取(新建/就地)、克隆(含带调整) |
| `ApplicationMenus.cpp` | 409 | 绘图区右键菜单、菜单数值回写、主题切换、设置菜单、菜单结果派发 |
| `ApplicationCommands.cpp` | 642 | 分组/信号增删、移动与重排、文件对话框、工具栏动作、键盘鼠标分发、布局边界绘制 |
Makefile 按目录通配收集源文件,**无需登记新文件**。
**验证**:release 零 error 零 warning;产物 793,088 → 794,112 B(+1,024,LTO 内联边界
变化,< 0.13%)。
### P15 注释/文档校正 —— ✅ 完成(提交 `f139440`)
* `PlotView.hpp`:删除指向已删除代码的 `FormulaEnvelopeEntry` 注释块。
* `agent_mem.md` §2:`-Os` → `-Oz`;补体积开关(`NO_EXCEPTIONS` / `NO_RTTI` / `LTO` /
`RELEASE_ONLY_OPT`)与「改 Makefile 必须删 `.o` 全量重编」提醒;补 `msimg32`;
忽略列表补 `Temp/`、`.preflight/`、`Tools/`。
* `agent_mem.md` §3:目录结构按现状重写(删除不存在的 `CsvTable/`、`DataModel/`、
`PaintUtil/` 等旧条目;写明 `Components/Shared` 与 `Utilities` 的真实职责和依赖方向)。
* `agent_mem.md` 末尾新增「第 33 轮」记录。
### P16 最终验证与报告 —— ✅ 完成
* release(全量)+ debug(全量 `-O0 -g`)**零 error 零 warning**。
* 最终产物 **794,112 B**(起点 890,880 B,−96,768,−10.9%)。
* `cp-demangle.o` 已无 `.text` 段分配;`__gnu_cxx::__verbose_terminate_handler`
定义在本产物内(`0x140057ed1`);导入 DLL 无新增。
* 报告:`Documents/Code_Review_Report.md`。
### 未完成/搁置的节点(均已在报告中说明原因)
* **P8** `SceneContentHash` 混入收敛:无逐像素验证手段,合并会改变哈希值,风险不划算。
* **P9** `EdgePanelLayoutFor` 表驱动化:同上(957 行几何分支,无法自证像素一致)。
* **P10** 缓存五件套抽象:同上,且热路径复杂。
* **P12** `PlotView.hpp` 拆分:嵌套类型外移需改 70+ 处引用,收益仅为可读性。
* **P13/P14** UI 基础层与绘制边界核对:已做**依赖方向与引用统计**,未发现重叠或反向依赖,
因此不再拆分。
* **性能项**:未发现新的可复现热点;已记录 3 个"还能再快一点"的点(见报告 §6.2)。
+325
View File
@@ -0,0 +1,325 @@
# Code Review 报告(2026-11-04,分支 `Code_Review`)
> 计划:`Documents/Code_Review_Plan.md` | 逐节点记录:`Documents/Code_Review_Progress.md`
>
> 起点 `764e5be`(dev,已删除过时的 `UI_Refactor_*` 三份文档),终点 `f139440`。
---
## 0. 一句话结论
**体积从 890,880 B 降到 794,112 B(−96,768 B,−10.9%)**,同时
① 修掉一个"Debug 构建从来不是 Debug"的真实缺陷;
② 把"新增实验性功能面板必须手改 20+ 处"的屎山点收敛成**唯一派发点 + 编译期强制检查**;
③ 把 2,165 行的 `ApplicationCommands.cpp` 拆成 4 个职责分片;
④ 校正了项目记忆里已经失真的目录结构与编译设置。
性能方面:本轮**没有发现新的、可复现的卡顿热点**(上一轮公式映射的 4 处卡死已根治);
审到 3 个"可以再快一点"的点,但都因为**缺少可自证的验证手段**而只记录、不动手(见 §6)。
---
## 1. 体积:890,880 → 794,112 B
### 1.1 节表前后对照
| 节 | 改前 | 改后 | 差 |
| --- | --- | --- | --- |
| `.text`(代码) | 753,136 | **703,616** | −49,520 |
| `.rdata`(只读数据) | 36,936 | **26,456** | −10,480 |
| `.pdata`(SEH 展开) | 30,420 | **12,684** | −17,736 |
| `.xdata`(SEH 展开) | 29,496 | **10,156** | −19,340 |
| `.data` | 1,680 | 2,704 | +1,024 |
| `.idata`(导入表) | 12,856 | 12,692 | −164 |
| `.rsrc`(图标/清单/版本) | 20,656 | 20,656 | 0 |
| **文件总计** | **890,880** | **794,112** | **−96,768(−10.9%)** |
### 1.2 逐项来源
#### (1)`dynamic_cast` 是一个 +75 KB 的"体积炸弹"——提交 `5de3763`
`Toolbar::BuildBoxes` 里唯一的一处:
```cpp
const UiButtonWidget* raw = dynamic_cast<const UiButtonWidget*>(button.get());
```
而 `Button::widget` **本来就是 `UiButtonWidget*`** —— 这个转换毫无作用。
用最小复现程序量化(`g++ -O2 -fno-exceptions -static -s`):
| 最小程序 | 体积 |
| --- | --- |
| 纯虚函数 + 虚析构(无 `dynamic_cast`) | 39,936 B |
| 多一个 `dynamic_cast` | **115,200 B** |
| 差 | **+75,264 B** |
原因:`dynamic_cast` 需要 RTTI 元数据,并牵出 libstdc++ 的
`__dynamic_cast` → `__cxa_bad_cast` → verbose terminate handler → **`cp-demangle`**
(Release map 里 `cp-demangle.o` 占 `.text` 49,600 + `.rdata` 6,720)。
改为直接遍历 `m_buttons`(与 `m_root->Children()` 同源同序),并开启 `-fno-rtti`。
#### (2)只关 RTTI 不够:demangler 仍被 `vterminate.o` 拉入——同一提交
关掉 RTTI 后 **`cp-demangle.o` 依然在链接里**(实测只省 2 KB)。查 map 找到真凶:
```
libstdc++.a(eh_terminate.o) (__gnu_cxx::__verbose_terminate_handler())
libstdc++.a(vterminate.o) (__cxa_demangle)
libstdc++.a(cp-demangle.o) ...
```
`std::terminate()`(`eh_terminate.o`)引用 `__gnu_cxx::__verbose_terminate_handler()`,
该符号由归档成员 `vterminate.o` 提供,而它调用 `__cxa_demangle`。
**修法**:在 `Utilities/Src/MemProbe.cpp` 里**自行定义**该函数
(`std::terminate` 的未定义引用被我们满足 ⇒ `vterminate.o` 不再被拉入 ⇒ demangler 消失)。
> ⚠️ 必须定义在**全局** `__gnu_cxx` 命名空间,不能嵌在 `jdi` 里。
> ⚠️ 行为等效:应用启动后本来就会 `std::set_terminate` 换成自己的处理函数,
> 这里的默认实现只在"启动早期崩溃"时生效,同样给出内存不足提示后退出。
#### (3)Release 关闭异步展开表——提交 `94fc1c4`
`RELEASE_ONLY_OPT = -fno-asynchronous-unwind-tables`,**只**加在 Release 的两条编译规则上。
`-fno-exceptions` 下绝大多数函数不再需要 `.pdata`/`.xdata`:−36,352 B。
**代价(已评估并接受)**:Release 崩溃时 WER 拿不到可靠调用栈。
本程序不打印栈回溯,功能不受影响;Debug 构建保留完整展开表。
(`-fno-unwind-tables` 全关会链接失败,历史上踩过,不采用。)
#### (4)`.rsrc` 20,656 B 无优化空间
图标 17,404 B(已是 PNG 承载的多尺寸帧)+ manifest 1,955 B。
再压只能减图标帧或降尺寸,会影响观感,**不动**。
### 1.3 未采纳的体积方案(实测否决)
| 方案 | 实测 | 结论 |
| --- | --- | --- |
| `-fno-asynchronous-unwind-tables`(Debug 也开) | — | 否决:Debug 需要展开表才能调试 |
| `-fno-unwind-tables`(全关) | 链接失败 | 否决(历史已踩) |
| 替换 `wcstod` 以去掉 `strtodg/gdtoa/gethex/hexnan` | ≈20 KB | 暂缓:公式/输入框的数值语义必须与现状逐位一致,收益不足以承担风险 |
| 图标继续瘦身 | — | 否决:观感优先 |
---
## 2. 构建系统:修掉一个"Debug 从来不是 Debug"的缺陷——提交 `94fc1c4`
审查 Makefile 时发现:
```make
DEBUG_OPT = -O0 # 定义了
...
# Debug 目标文件编译规则
$(CXX_COMPLIER) -c $(CXXFLAGS) $(RELEASE_OPT) ... # ← 用的是 -Oz!
```
**`DEBUG_OPT` 从未被任何规则引用**,且全流程没有 `-g`。
所谓 "Debug 构建" 实际上只是"不 strip 的 Release",**根本没有调试信息、也不是 `-O0`**。
现在 Debug 规则改用 `$(DEBUG_OPT) -g`,链接也加 `-g`。
| 构建 | 改前 | 改后 | 说明 |
| --- | --- | --- | --- |
| Release | 890,880 | **794,112** | 体积优化 |
| Debug | 1,801,617 | **5,994,805** | 变**大**是**正确**的:真实 `-O0 -g` |
> ⚠️ 这意味着"Debug 构建更慢、更大"从此名副其实;如果以后仍在用 `Build/Debug` 做
> 日常运行,请改用 Release。
---
## 3. 架构:面板分派收敛为唯一派发点——提交 `7a3fefb`
### 3.1 问题(屎山核心)
`Lane` 里 7 种实验性功能(边沿计数 / 中心轴 / 均值滤波 / 弱峰调整 / 自由标记 /
重采样 / 选区截取)各持有一份 `LanePanelState` 与一个滚动偏移,共 14 个字段。
旧实现在**每一处**"按种类取用"的地方各写一遍嵌套三元式或 `switch`:
| 位置 | 旧写法 |
| --- | --- |
| `EdgePanelLayoutFor` 的 `enabled` / `open` / `collapsed` / `storedX` / `storedY` | **5 条 7 层嵌套三元式** |
| 标题栏拖拽 | 7 分支 `switch`(漏 `MARK`/`RESAMPLE` 各一次 ⇒ 面板拖不动) |
| ✕ 关闭 | 7 分支 `switch` + `default`(漏 `RESAMPLE` ⇒ 关掉的是别的面板) |
| 滚轮翻页 | 三元式链 + `switch`(漏 `CLIP` ⇒ 永远滚不到底) |
| 滚动条拖动 | `switch` + `default` |
| 折叠状态读写 | 2 个 `switch` |
| 首现错开序数 | 局部 `kOrder[]` + `switch` |
新增一种面板要改 **20+ 处**,漏一处就是运行时 BUG —— 历史上 `MARK`、`RESAMPLE`、
`CLIP` **三种面板各自都真实踩过**。
### 3.2 修法
`PlotViewInternal.hpp` 新增唯一派发点(各带可写/只读重载):
```cpp
LanePanelEnabled(lane, kind); // 功能是否启用
LanePanelStateOf(lane, kind); // 展开/折起/位置
LanePanelScrollOf(lane, kind); // 内容滚动偏移
```
调用点全部改为访问器:布局里 5 条嵌套三元式 → 3 行;标题栏拖拽 7 分支 → 1 次写入;
滚轮/滚动条/✕/折叠全部改为穷举。
### 3.3 ⭐ 关键收益:编译期保护
这些 `switch` **穷举 7 种且一律不写 `default:`** ⇒ `-Wswitch`(包含在 `-Wall` 里)
在新增 `EdgePanelKind` 而漏登记时**直接编译报警**。
已用最小程序验证该机制确实生效:
```
warning: enumeration value 'C' not handled in switch [-Wswitch]
```
⇒ **今后新增实验性功能面板,忘记登记会编译失败,而不是运行时静默出错。**
(对比:旧代码用 `case COUNT: default:` 兜底,`-Wswitch` 永远不会报。)
### 3.4 验证
release + debug 零 warning;**产物体积不变(793,088 B)** —— 这正好说明本次是
纯粹的等价结构收敛,没有引入额外代码。
---
## 4. 架构:拆分 2,165 行的 `ApplicationCommands.cpp`——提交 `8437100`
原文件混杂六类职责。按职责**纯搬移**(零逻辑改动、函数体一字未改):
| 文件 | 行数 | 职责 |
| --- | --- | --- |
| `ApplicationTabs.cpp` | 355 | 标签页新建/关闭/切换/重排、视图同步、窗口标题、标签栏回调、标签页右键菜单 |
| `ApplicationDerive.cpp` | 786 | 派生烘焙、重采样、选区截取(新建 / 就地)、克隆(含带调整) |
| `ApplicationMenus.cpp` | 409 | 绘图区右键菜单、菜单数值回写、主题切换、设置菜单、菜单结果派发 |
| `ApplicationCommands.cpp` | 642 | 分组/信号增删、移动与重排、文件对话框、工具栏动作、键盘鼠标分发、布局边界绘制 |
Makefile 按目录通配收集源文件,**无需登记**。
体积 793,088 → 794,112 B(+1,024,LTO 内联边界变化,< 0.13%)。
---
## 5. 审查中"没有发现问题"的项(同样重要)
这些是**主动查了但结论是干净**的,避免后续接手者重复怀疑:
| 检查项 | 方法 | 结论 |
| --- | --- | --- |
| 死代码(未使用的静态函数) | `-Wall` 零警告(GCC 会对未使用的 `static` 函数报警) | 无 |
| 死字段(只声明不使用) | 对 `PlotView` / `SignalPanel` / `Application` / `TabBar` / `Toolbar` / `DataSet` 的全部 `m_*` 成员做全仓引用计数 | 233+ 个成员**无一**只声明不使用 |
| TODO / FIXME / 临时方案 | 全仓 grep | 无(源码里的"未完成"全是"边沿统计进行中"的业务语义) |
| 布局文本测量是否重复开销 | 读 `UiMeasureTextWidth` 实现 | 已记忆化(键 = 字体 + 文本),不是热点 |
| 页面上是否残留指向已删文件的注释 | grep `UI_Refactor_*` / `refactor_report` / `structcheck` / `.preflight` | 仅 1 处(`PlotView.hpp` 的 `FormulaEnvelopeEntry` 注释),已删 |
---
## 6. 性能审查结论
### 6.1 结论
**没有发现新的、可复现的卡顿热点。** 上一轮(第 32 轮)的四处卡死同源于
"公式信号幅值范围/包络各扫一遍整条信号",已通过 `DataSet::FormulaTransformFor`
(一次扫描 + 缓存,命中 0.0015 ms)根治。
逐一复核了下列曾出过问题的路径,均仍走快路径:
| 路径 | 现状 |
| --- | --- |
| `BuildLaneVisuals` 的幅值范围 | 指纹缓存(`m_laneRangeKey`),命中时 O(1) |
| 场景/装饰/面板/图层像素缓存 | 键齐全,未发现遗漏项 |
| 边沿统计 | 按帧预算 + 每状态配额推进,暂停条件齐备 |
| 图层预建 | `EnsureLaneLayersPrefetched`(不再整批重建) |
| 信号面板行集合自愈 | `RowsSignature` 每帧 O(组数),组数远小于信号数 |
### 6.2 记录但未实施的三处"还能再快一点"
| 位置 | 量级 | 为什么不改 |
| --- | --- | --- |
| `EdgeScanFor` 的累积状态查找是**线性扫描** `m_edgeScans`(上限 2×信号数+余量)。288 信号时每帧约 16 万次指针比较 | ~0.3 ms/帧 | 改哈希索引需要重做"淘汰最旧已完成状态"的逻辑;收益与风险不匹配,且该路径已被预算化 |
| `EdgePanelLayoutFor`(957 行分支)整体改为"每面板一行元数据表" | 可读性 + 少量代码体积 | **没有离屏像素对比工具**(`.preflight` 已删,崩溃会弹模态框),纯逻辑重写无法自证"像素完全一致" |
| `SceneContentHash` 里 6 段逐面板混入循环可合并 | 可读性 | 合并会改变哈希值;虽然"多加字段安全、少加字段危险",但在无法逐像素验证的前提下不值得冒险 |
### 6.3 关于"性能验证手段"的重要说明
本轮**没有**做任何 GUI 探针,原因(也是给后续接手的约束):
* 用户要求不占用其鼠标/键盘/屏幕;
* `.preflight` 测试框架已被用户删除,且已明确"不要重建";
* `-fno-exceptions` + `MemProbe::OnTerminate()` 会在探针崩溃时弹**模态"内存不足"框**,
反复运行就是反复弹窗(第 26 轮的真实教训)。
因此本轮的体积结论全部来自**可复现的静态测量**(`objdump -h` 节表 + `Makefile` 变量 +
非 LTO 目标文件尺寸),架构结论来自**编译期检查**(`-Wswitch`)与**等价性论证**
(体积逐字节不变)。
---
## 7. 注释与文档
### 7.1 删除
* `Documents/UI_Refactor_Plan.md`、`UI_Refactor_Progress.md`、`UI_Refactor_Report.md`
(描述 2026-10-28 那一阶段,已被后续多轮实现推翻,与当前代码不一致)。
### 7.2 校正(`agent_mem.md`)
| 位置 | 原内容(错) | 现内容 |
| --- | --- | --- |
| §2 | Release 用 `-Os` | `-Oz` + LTO + `-s` |
| §2 | 未提体积开关 | 补充 `NO_EXCEPTIONS` / `NO_RTTI` / `LTO` / `RELEASE_ONLY_OPT`,并警告"改 Makefile 必须删 `.o` 全量重编"(本轮实测:只改 Makefile 不会触发重编,会误以为"优化无效") |
| §2 | 链接库少 `msimg32` | 已补 |
| §3 | 目录里写着早已不存在的 `Utilities/CsvTable/`、`Utilities/DataModel/`、`Utilities/PaintUtil/`、`Utilities/PopupMenu/`… | 按现状重写(`Components/Shared` 承载通用控件与 `PopupMenu`/`ColorPickerPopup`;`Utilities` 为数据与格式层),并写明依赖方向 |
| 末尾 | — | 新增「第 33 轮」完整记录 |
### 7.3 源码注释
* `PlotView.hpp`:删除指向已删除代码的 `FormulaEnvelopeEntry` 注释块。
* 新增/改写的注释一律说明**为什么**(尤其是 `-fno-rtti`、`__verbose_terminate_handler`
覆盖、`-fno-asynchronous-unwind-tables` 的代价),避免后续被"好心"改回去。
---
## 8. 验证清单
| 项 | 结果 |
| --- | --- |
| `make release`(删 `*.o`/`*.d` 全量) | **零 error、零 warning** |
| `make debug`(真实 `-O0 -g`,全量) | **零 error、零 warning** |
| Release 产物 | 794,112 B(< 1 MB 红线) |
| `cp-demangle.o` 是否仍占 `.text` | 已无 `.text` 段分配 |
| `__gnu_cxx::__verbose_terminate_handler` | 定义在**本产物内**(`0x140057ed1`),非库导入 |
| 导入 DLL 列表 | `comdlg32 / GDI32 / gdiplus / KERNEL32 / MSIMG32 / SHELL32 / USER32` + UCRT,无新增依赖 |
| 工作区状态 | 干净;4 个节点提交 + 1 个文档提交 + 1 个 dev 提交 |
---
## 9. 提交清单
| 提交 | 分支 | 内容 |
| --- | --- | --- |
| `764e5be` | `dev` | 删除过时的 `UI_Refactor_*` 三份文档 |
| `86ff029` | `Code_Review` | 新增 `Code_Review_Plan.md` / `Code_Review_Progress.md` |
| `5de3763` | `Code_Review` | 移除多余 `dynamic_cast` + 关闭 RTTI + 覆盖 terminate handler(−61,440 B) |
| `94fc1c4` | `Code_Review` | Release 关闭异步展开表(−36,352 B)+ 修正 Debug 构建参数 |
| `7a3fefb` | `Code_Review` | 面板分派收敛为唯一派发点(编译期保护) |
| `8437100` | `Code_Review` | 拆分 `ApplicationCommands.cpp` → 4 个职责分片 |
| `f139440` | `Code_Review` | 清理过时注释 + 校正 `agent_mem.md` §2/§3 |
---
## 10. 后续建议(按性价比排序)
1. **重建离屏验证手段**(不弹窗口、不占用用户输入)。
没有它,"重构面板几何 / 缓存键"这类工作无法自证正确性 —— 这正是本轮 §6.2
三项被搁置的唯一原因。建议做纯控制台 + `SW_HIDE` + 逐像素哈希比对,并且
**绝不调用 `Application::Initialize()`**(会 `LoadCommandLineFiles` → 失败 → 模态框)。
2. **`EdgeScanFor` 状态查找哈希化**(0.3 ms/帧 @288 信号)。
3. **`EdgePanelLayoutFor` 表驱动化**(需第 1 项就绪)。
4. 若要继续压体积:`wcstod` 自写替身(≈20 KB,需逐位一致性验证)、
图标帧裁剪(观感优先,不建议)。
-153
View File
@@ -1,153 +0,0 @@
# Just DataInspector UI 重构 —— 计划
> 本文档只描述**要做什么、为什么这么做、怎么验收**。
> 已完成情况见 `UI_Refactor_Progress.md`,结论与遗留见 `UI_Refactor_Report.md`。
> 最后更新:2026-10-29
---
## 1. 目标与非目标
### 1.1 目标
| # | 目标 | 判据 |
| --- | --- | --- |
| G1 | 收敛"宿主点名每个组件"的散乱结构 | 宿主只遍历一张组件表,不再 `if (component == plot)` 式分支 |
| G2 | 几何只有**一处**来源 | "画出来的形状"与"能点到的区域"由同一批函数给出,不再各算一遍 |
| G3 | 源码按**职责**分片,单片可读 | 单个 `.cpp` 不超过约 1200 行;文件名与内容一致 |
| G4 | 减小产物体积,且**仍然只有一个 `.exe`** | 体积比基线小,无新增依赖 DLL |
| G5 | 关闭 C++ 异常后**不牺牲健壮性** | 所有大块分配前有预检验;OOM 有可见提示 |
| G6 | 重构**不得改变任何绘制像素** | 8 个场景的逐像素哈希与基线逐一相同 |
### 1.2 非目标(明确不做)
* **不把编译产物拆成 DLL**:本项目的定位是"单文件、拷过去就能跑的轻量工具"。
任何以拆 DLL 换体积的方案一律否决。
* **不改优化等级**(`-Oz` 保持不变)。
* **不把"波形"与"网格线"拆成两层位图**:像素级平移不变量会被破坏。
* **不为体积牺牲可维护性**(例如放弃分片、放弃几何同源)。
---
## 2. 硬约束
1. **单一可执行文件**:不得引入额外的 `.dll` / 运行时依赖。
2. **体积预警阈值 = 1MB(1,048,576 B)**。这不是硬上限:超过仍可发布,
但必须主动提醒用户,并说明"超在哪一段(`.text` / `.rdata` / `.xdata`+`.pdata` / `.rsrc`)、
由哪个改动贡献"。旧的 900KB / 800KB 红线**已作废**。
3. **异常处理用预检验,不用 try/catch**:`-fno-exceptions` 下 `try/catch` 不会被编译,
因此所有"按数据规模预留大块内存"的地方必须先问 `MemReserveIfRoom()`。
4. **像素级零回归**:验收以 `Tools/rendercheck` 的 8 个场景哈希为准。
5. **中文注释按 UTF-8 无 BOM 保存**;构建参数用 `-finput-charset=UTF-8 -fexec-charset=GBK`。
---
## 3. 分层架构(L0–L3)
```
L3 体积与异常策略 -fno-exceptions / MemProbe / LTO / gc-sections
▲
L2 重绘范围控制 UiDamage(脏矩形) ← 本轮评估后**缓做**,见 §5
▲
L1 几何产物层 UiBox / UiBoxTable / UiComponent::BuildBoxes
▲
L0 组件协议 UiComponent + 组件表 + UiWidget 控件树
```
### L0 组件协议(前几轮已完成)
* `UiComponent`:`SetScale / Layout / Area / Paint / HitTest / OnMouseLeave / IsEditing / QueryCursor`。
* 宿主持有一张**有序组件表**,逐个遍历;新增组件不必改宿主的 5 处消息分支。
* `UiWidget` 与 `UiComponent` 是**兄弟**而非父子:前者管"一块区域内部的布局/命中/绘制",
后者管"自己这一整块地盘:区域、缓存、局部重绘策略"。
`Toolbar` / `TabBar` 同时是两者。
### L1 几何产物层(本轮完成)
* 新文件 `Components/Shared/Inc/UiBox.hpp` + `Src/UiBox.cpp`。
* `struct UiBox { rect, clip, depth, layer }`:**纯值类型**,不继承任何东西。
* `class UiBoxTable`:几何收集器,只维护"矩形 / 层级 / 绘制层 / 裁剪 / 遮挡"这些**几何事实**,
**不含任何绘制代码**。
* `UiComponent::BuildBoxes(UiBoxTable&)`:组件上报自己的几何产物(默认只登记 `Area()`)。
* `UiDebugBounds` 退化为**消费者**:`UiDebugPaintBoxList(g, table)` 把几何画成彩色包角线。
⇒ 组件不再需要知道"调试"这个概念存在。
### L2 重绘范围控制(缓做 —— 见 §5)
* 设计意图:`UiDamage`(最多 12 个矩形的脏区集合,超限退化为包围盒)替代
"全窗重绘 / 绘图区重绘"的二值分支;`PlotView::Paint(g, UiDamage&)` 逐路径累加脏区。
* 收益:去掉 `SetForceFullRepaint`(布局边界开关打开时的整幅重绘)。
### L3 体积与异常策略(本轮完成)
见 `UI_Refactor_Progress.md` 的实测表。
---
## 4. 阶段与验收标准
| 阶段 | 内容 | 验收 |
| --- | --- | --- |
| P1–P6 | 文件按职责拆分、`UiComponent` 协议、组件表、布局边界功能 | 编译 + `uicheck` |
| P7 | 布局边界"几何同源"(`RowRect` 前缀和、文本墨迹矩形、浮层层级/遮挡/裁剪) | `lbcovr` / `lbzorder` / `lbresid` / `lbpanel` |
| P8 | 体积:`-fno-exceptions` + `MemProbe` + `-ffunction-sections` + `--gc-sections` | 体积下降且 8 哈希不变 |
| P9 | 体积:LTO(含 `-flto-partition=one` 稳定性约束) | 同上,且可重复构建 |
| P10 | L1 几何产物层(`UiBox` / `UiBoxTable` / `BuildBoxes`) | 同上,且源码零残留旧名 |
| P11 | 代码审计(过时注释 / 名实不符 / 死代码) | 零 warning + 全量回归 |
| P12 | 文档重建 + 本地提交 | `git status` 干净 |
**统一验收命令**
```powershell
make clean
make release TARGET_FILE_NAME=JustDataInspector -j16 # 期望零 error / 零 warning
make debug TARGET_FILE_NAME=JustDataInspectorD -j16 # 期望零 warning
objdump -h Build\Release\JustDataInspector.exe # 段构成,用于体积溯源
Tools\uicheck\uicheck.exe # 69/69
.preflight\lbcheck\lbcheck.exe # 21/21
.preflight\lbresid\lbresid.exe # 5/5
.preflight\lbzorder\lbzorder.exe # 4/4
.preflight\lbcovr\lbcovr.exe # 7/7
.preflight\lbpanel\lbpanel.exe # 10/10
Tools\rendercheck\rendercheck.exe # 8 哈希不变 + FUNC 7/7
```
---
## 5. 明确缓做的部分及理由
### L2 `UiDamage` + 独立叠加层(S4)
**为什么缓做**
1. 受益方只有"**调试开关打开时**的帧率"。`SetForceFullRepaint` 只在
「显示布局边界」开启时被置位(`Application::Render` 里 `m_plot.SetForceFullRepaint(m_showLayoutBounds)`),
普通用户路径上**本来就是快路径**。
2. 正确实现无法只改 `PlotView` 内部:宿主用的是**持久后备缓冲区**,
上一帧叠加层画上去的边框像素必须在下一帧被"底层内容重画"覆盖掉。
若改成"独立叠加位图 + 只混合脏区",就必须把叠加层的旧矩形**注入**到底层组件的重绘范围里 ——
这要穿透 `PlotView` 的全部四条快路径(静态场景贴图 / 宽基底贴图 / 样式补丁 / 网格层重建)
与两层缓存。
3. 风险与收益不成比例:用户已在 2026-10-28 反复报告过"拖拽错位 / 贴图残留"一类事故,
而这类事故正是从"某条快路径漏了某块像素"开始的。
**保留了什么**:`SetForceFullRepaint` 及其四处守卫、`UiBoxTable` 的几何产物。
将来若真要做,只需把 `UiBoxTable` 的 `rect` 直接喂给 `UiDamage`,不需要重新收集几何。
---
## 6. 交付物清单
```
Components/Shared/Inc/UiBox.hpp 新增(几何产物层)
Components/Shared/Src/UiBox.cpp 新增
Components/Shared/Inc/UiDebugBounds.hpp 精简为"只画"(消费者)
Components/Shared/Src/UiDebugBounds.cpp 精简为"只画"
Components/Shared/Inc/UiComponent.hpp CollectDebugBoxes → BuildBoxes
Utilities/Inc/MemProbe.hpp 新增(分配预检验 + terminate 兜底)
Utilities/Src/MemProbe.cpp 新增
Makefile LAYOUT_BOUNDS / NO_EXCEPTIONS / LTO 三个开关
Documents/UI_Refactor_{Plan,Progress,Report}.md 重建
```
-202
View File
@@ -1,202 +0,0 @@
# Just DataInspector UI 重构 —— 当前进度
> 计划见 `UI_Refactor_Plan.md`,结论与遗留见 `UI_Refactor_Report.md`。
> 最后更新:2026-10-29(本轮:体积 L3 落地 + L1 几何产物层 + 审计)
---
## 1. 总览
| 层 | 内容 | 状态 |
| --- | --- | --- |
| L0 | `UiComponent` 协议 + 有序组件表 + `UiWidget` 控件树 | ✅ 已完成(前几轮) |
| L1 | `UiBox` / `UiBoxTable` / `UiComponent::BuildBoxes` | ✅ **本轮完成** |
| L2 | `UiDamage` 脏矩形 + S4 独立叠加层 | ⏸ **评估后缓做**(理由见 Plan §5) |
| L3 | `-fno-exceptions` + `MemProbe` + `-ffunction-sections` + `--gc-sections` + LTO | ✅ **本轮完成** |
---
## 2. L3 体积与异常策略
### 2.1 Makefile 开关
| 开关 | 默认 | 作用 | 备注 |
| --- | --- | --- | --- |
| `LAYOUT_BOUNDS` | `1` | 是否编入「显示布局边界」调试叠加层 | `make release LAYOUT_BOUNDS=0` 可剔除 |
| `NO_EXCEPTIONS` | `1` | 追加 `-fno-exceptions` | 必须配合 `MemProbe` 的预检验 |
| `LTO` | `1` | 追加 `-flto`(编译 + 链接) | **切换需 `make clean`** |
派生变量:
| 变量 | `LTO=1` | `LTO=0` |
| --- | --- | --- |
| `FUNC_SECTIONS` | 空 | `-ffunction-sections -fdata-sections` |
| `ASM_LISTING` / `ASM_LISTING_C` | 空 | `-Wa,-a,-ad,-ahlms=$(@:%.o=%.lst)` |
| `GC_SECTIONS` | `-Wl,--gc-sections` | 同左 |
> `GC_SECTIONS` / `FUNC_SECTIONS` / `ASM_LISTING*` 都定义在 `LDFLAGS` 之后。
> 它们是递归变量(`=` / `+=`),make 在展开 recipe 时才求值,所以顺序不影响生效;
> 之所以还是放在后面,是为了让"三个开关"集中在同一处,不散落在两个位置。
### 2.2 两个已复现的坑
**坑 1:切换 LTO 必须 `make clean`。**
LTO 与非 LTO 目标文件混链会让 GCC 15.2.0 在 `binds_to_current_def_p (symtab.cc:2598)` 处 ICE。
**坑 2:`-flto` 与 `-ffunction-sections -fdata-sections` 同时打开也会触发同一个 ICE。**
即使全部干净重编也必崩(`Utilities/Inc/DataModel.hpp:393` 的 `struct SignalData` 被指为出错点,
但那只是 COMDAT 解析的落点,不是原因)。
**解决办法**:LTO 下 `FUNC_SECTIONS` 置空(LTO 自己就做跨单元分区与去重),
并追加 **`-flto-partition=one`**:
默认多分区 LTRANS(本项目 11 个分区)稳定复现,单分区从未复现。
代价是 LTRANS 串行,全量构建慢约一倍。
> 结论:**`-flto-partition=one` 不是性能选项,是稳定性选项**,不要删。
### 2.3 实测体积
| 阶段 | 产物 | 体积 | 相对基线 |
| --- | --- | --- | --- |
| 基线 | `BaseCheck.exe` | 909,824 B | — |
| `-fno-exceptions` + `--gc-sections` + `MemProbe` | `Step1.exe` | 882,688 B | **−27,136 B** |
| 再叠加 LTO | `JustDataInspector.exe` | **788,992 B** | **−120,832 B(−13.3%)** |
段构成(`objdump -h`,最终构建):
| 段 | 基线 | `-fno-exc` + `gc-sections` | 最终(LTO) |
| --- | ---: | ---: | ---: |
| `.text` | 735,600 | 720,528 | **657,168** |
| `.rdata` | 49,184 | 45,072 | **35,016** |
| `.xdata` | 48,132 | 34,152 | **27,512** |
| `.pdata` | 35,424 | 34,524 | **28,524** |
| `.rsrc` | 20,656 | 20,656 | 20,656 |
| `.data` | 2,272 | 9,312 | 1,952 |
| `.bss` | 10,224 | 3,088 | 10,000 |
| `.idata` | 12,440 | 12,440 | 12,440 |
| `.tls` | 16 | 16 | 16 |
| `.reloc` | — | — | 1,700 |
**体积溯源(超阈值时必须能回答这个问题)**:
* `.text` 减少最多(−78 KB / −10.7%)—— 主要来自 LTO 的跨单元内联与重复代码合并。
* `.xdata`+`.pdata`(异常展开表 + 函数表)减少 27.5 KB —— 来自 `-fno-exceptions`
与 `--gc-sections` 丢弃未引用的函数。
* `.rdata` 减少 14 KB —— 来自"未引用字符串/常量被 gc 掉"。
* `.rsrc` 完全不变(图标/清单)。
> 预警阈值 1MB(1,048,576 B):当前 788,992 B,**余量 259,584 B(约 253 KiB)**。
### 2.4 `MemProbe`(预检验 + 兜底)
| 函数 | 作用 |
| --- | --- |
| `MemAvailablePhysical()` | `GlobalMemoryStatusEx` + **200ms 缓存**(绘制路径每帧可能问多次) |
| `MemCanAllocate(bytes)` | 留出 1/8 可用内存作 headroom 后判断是否安全 |
| `MemReserveIfRoom(container, n)` | 仅供"按数据规模预留":含 `capacity()` 早退与乘法溢出保护 |
| `MemInstallTerminateHandler()` | `std::set_terminate` 装一次性模态提示后 `_Exit` |
改动点:
* `Utilities/Src/DataModel.cpp`:原先**唯一**的 `try/catch`(`extremes.reserve(count / 8)`
捕 `std::bad_alloc`)→ `MemReserveIfRoom(extremes, count / 8)`。
* `Components/PlotView/Src/PlotView.cpp`:`PlotView::AvailablePhysicalBytes()` 原有**一份完整的
`GlobalMemoryStatusEx` + 200ms 缓存实现**(与 `MemProbe` 重复)→ 改为转发 `MemAvailablePhysical()`。
* `Components/Application/Src/Application.cpp`:`Initialize()` 开头 `MemInstallTerminateHandler();`。
> 全仓已确认**无 `try/catch` 残留**(除 `MemProbe` 注释中对它的说明)。
---
## 3. L1 几何产物层
### 3.1 结构
```
UiBox.hpp struct UiBox { rect, clip, depth, layer }; ← 纯值,无行为
class UiBoxTable : UiTextRectSink ← 只累积几何 + 裁剪/层/遮挡
UiDebugBounds.hpp UiDebugBoundsColor / UiDebugPaintBoundsClipped / UiDebugPaintBoxList
↑ 唯一的消费者(调试描边),与几何解耦
```
`UiComponent::BuildBoxes(UiBoxTable&)` 默认实现:
```cpp
void UiComponent::BuildBoxes(UiBoxTable& boxes) { boxes.Add(Area(), 0); }
```
各组件覆盖情况:
| 组件 | 上报内容 |
| --- | --- |
| `SignalPanel` | 表头 / 筛选框 / 列表 / 样式区 / 每个可见行 / 行内元素(复选框、色块、名称、GX 徽标、✕、折起三角、组计数)/ 滚动轨道与滑块 / 编辑框 |
| `PlotView` | 网格、GX 徽标、图例条目、**5 类浮动面板**(含逐字段)+ 面板遮挡与裁剪 |
| `Toolbar` | 区域、每个按钮、按钮内容(文本/图标) |
| `TabBar` | 区域、每个标签条、新建按钮、标签 ✕、控件树 |
| `PopupMenu` | 条目(裁到 `m_scrollEnabled ? m_scrollViewport : m_rect`)、可视区、轨道、滑块、二级子菜单 |
| `ColorPickerPopup` | 各控件 |
### 3.2 与 `UiDebugBounds` 的边界
* 几何(矩形 / 层级 / 层号 / 裁剪 / 遮挡)→ `UiBox.hpp`
* 观感(配色 / 包角粗细 / 图元数量 / 抗锯齿开关)→ `UiDebugBounds.hpp`
这样 `SignalPanel.cpp` / `PlotViewPaint.cpp` / `PopupMenu.cpp` 等**只包含 `UiBox.hpp`**,
不再把"调试描边"这个依赖拉进自己的编译单元;只有宿主 `ApplicationCommands.cpp` 同时包含两者。
### 3.3 零像素回归的证明
L1 落地前后 `JustDataInspector.exe` **字节数完全相同(788,992 B)**,
`rendercheck` 8 个场景哈希逐一相同 —— 因为这只是"把几何从调试绘制里搬出来",
没有动任何绘制顺序或图元。
---
## 4. 回归结果(最终构建上跑)
| 探针 | 结果 |
| --- | --- |
| `Tools/uicheck` | **69 passed, 0 failed** |
| `.preflight/lbcheck` | **21 passed, 0 failed** |
| `.preflight/lbresid` | **5 passed, 0 failed** |
| `.preflight/lbzorder` | **4 passed, 0 failed** |
| `.preflight/lbcovr` | **7 passed, 0 failed** |
| `.preflight/lbpanel` | **10 passed, 0 failed** |
`Tools/rendercheck`(逐像素哈希,必须与基线逐一相同):
```
A FAED59DF284153DB B E6443BFE4E3955F8 C 96A0AA8C402F1729 D E3D5DBD998E43720
E DA0583CB13C26A6F F FFB822159F604D1D G 19F8DE8FE3A8DF18 H DBBDDE4CB2C97C92
FUNC 7/7 PASS, exit=0
```
构建:`make release` 零 error / 零 warning;`make debug` 零 warning。
> ⚠️ 探针 `build.ps1` 链接的是 `Build/Release/*.o`。**LTO 下这些是 LTO 字节码**,
> 因此 7 个探针脚本的链接参数里都补了 `-flto -flto-partition=one`。
> 若把 `LTO` 关掉,这两个参数无害(可与普通目标文件混链)。
---
## 5. 本轮审计清单
| 项 | 处理 |
| --- | --- |
| `MemProbe.hpp` / `.cpp` 里"省 40~60KB"的估算 | 改为实测值 27,136 B(909,824 → 882,688) |
| `MemProbe.hpp` 的 `#include <new>` | 未使用,删除 |
| `MemProbe.cpp` 的 `#include <cstdio>` | 未使用,删除 |
| `UiDebugPaintBounds(g, rect, depth)` | **零调用者**(唯一调用者是它自己),删除;只保留带 `clip` 的版本 |
| `TabBar::BuildBoxes` / `Toolbar::BuildBoxes` / `PlotView::BuildBoxes` / `SignalPanel::BuildBoxes` 的"(调试用)" | 改为"几何 box"(层级已变,它们不再关心调试) |
| `PlotView::SetForceFullRepaint` 文档 | 补写"为什么保留而不优化"(L2 缓做的理由) |
| 全仓旧名 `UiDebugBoxList` / `UiDebugBox` / `CollectDebugBoxes` | 源码零残留(仅 `Documents/agent_mem.md` 历史记录保留原样) |
---
## 6. 已知问题 / 未做
1. **L2 未做**(见 Plan §5)。`SetForceFullRepaint` 仍在,布局边界打开时 `PlotView` 走整幅重绘。
2. `Tools/` 与 `.preflight/` 下的探针脚本未被 git 跟踪(本地验证工具),
本轮给它们补的 `-flto` 参数**不会进版本库**。若 LTO 长期默认开启,
建议把探针脚本纳入版本管理,否则下次 `make clean` 后重建探针需要手工补参数。
3. `.lst` 汇编清单在 `LTO=1` 时不再生成(`lto-wrapper` 会丢弃 `-Wa` 参数)。
需要看汇编时用 `make release LTO=0`。
-157
View File
@@ -1,157 +0,0 @@
# Just DataInspector UI 重构 —— 最终报告
> 计划见 `UI_Refactor_Plan.md`,过程与实测数据见 `UI_Refactor_Progress.md`。
> 日期:2026-10-29
---
## 1. 一句话结论
在**不拆 DLL、不改优化等级、不改变任何绘制像素**的前提下,
把界面从"宿主点名各组件 + 各算一遍几何"改成"组件协议 + 几何产物层",
同时把产物从 **909,824 B 降到 788,992 B(−120,832 B,−13.3%)**,
并用 6 组、共 116 条断言 + 8 个逐像素哈希证明零回归。
---
## 2. 收益
### 2.1 结构
| 之前 | 之后 |
| --- | --- |
| 宿主 `if` 链逐个点名 4 个大组件,同一件事要写 5 遍 | 一张有序组件表,新增组件不改宿主 |
| 几何分散在 `Paint*` / `HitTest` / `CollectDebugBoxes` 三处,**三份实现** | `BuildBoxes()` 一处产出,绘制/命中/调试同源 |
| "调试"概念渗透进每个组件 | 组件只吐几何;"怎么画"收敛在 `UiDebugBounds` |
| 单个 `.cpp` 数千行 | 按职责分片(PlotView 5 片、SignalPanel 3 片…) |
### 2.2 体积
| 项 | 数值 |
| --- | ---: |
| 基线 | 909,824 B |
| 最终 | **788,992 B** |
| 减少 | **120,832 B(−13.3%)** |
| 相对 1MB 预警阈值的余量 | 259,584 B(约 253 KiB) |
构成变化的解释见 `UI_Refactor_Progress.md` §2.3(`.text` −78 KB 来自 LTO,
`.xdata`+`.pdata` −27.5 KB 来自关异常 + gc,`.rdata` −14 KB 来自 gc 掉未引用常量)。
### 2.3 健壮性
* `-fno-exceptions` 之后没有"静默消失":OOM 走 `MemInstallTerminateHandler()`,
弹一次带建议的模态提示再干净退出。
* 唯一一处 `try/catch` 改成**预检验**(`MemReserveIfRoom`),语义等价且不依赖异常。
* `PlotView` 里重复实现的"可用物理内存查询"被去重到 `MemProbe`(单一实现、单一缓存)。
---
## 3. 关键决策与依据
### 3.1 拒绝"拆 DLL 换体积"
用户的定位是**单文件轻量工具**。拆 DLL 只把字节从 `.exe` 挪到旁边的 `.dll`,
总交付物更大、部署更脆(版本错配、路径缺失),且 `-static` 换来的"拷一个文件就能跑"
这一核心体验会丢掉。**因此体积优化的边界是"编译期与链接期手段",不含产物拆分。**
### 3.2 关异常 → 必须预检验,而不是"少写 try/catch"
`-fno-exceptions` 的真实代价不是"少了个 try",而是**分配失败从"可恢复"变成"进程终止"**。
所以必须成对交付:
1. 所有"按数据规模预留"的路径先问内存(`MemReserveIfRoom`);
2. 遗漏路径有兜底提示(terminate handler)。
只做第 1 步是不够的 —— 事后新增的代码很容易忘记检查。
### 3.3 LTO 用 `-flto-partition=one` 换稳定性
`-flto` 单独用会在 COMDAT 解析处 ICE(`binds_to_current_def_p, symtab.cc:2598`),
根因是多分区 LTRANS;单分区稳定(实测 `--gc-sections` 开/关都得到 791,552 B,
说明这一档体积收益来自 LTO 本身而不是分区数)。
代价是 LTRANS 串行 —— 这是**用构建时间换"构建能过"**,值得。
同时确认:`-flto` 与 `-ffunction-sections -fdata-sections` 不能同时开(同一个 ICE),
LTO 下把 `FUNC_SECTIONS` 置空。
### 3.4 L1 而不是"给所有元素加基类"
`PlotView` 有 4 层自持位图缓存、`SignalPanel` 有行位图缓存,它们要求"像素由组件自己负责"。
把几何塞进继承体系会和缓存失效体系打架(历史上"后台渲染覆盖用户平移""命中错位"
就是这类冲突)。`UiBox` 是**纯值类型**,因此可以被绘制、命中、将来的脏矩形共用,
却不需要虚函数与生命周期管理。
### 3.5 L2 缓做
见 `UI_Refactor_Plan.md` §5。一句话:**受益方只有调试开关打开时的帧率,
而正确实现要穿透 `PlotView` 全部四条快路径与两层缓存。**
在"用户反复报告过拖拽错位/贴图残留"的背景下,这笔风险不值得为一个调试特性去承担。
---
## 4. 验证方法与结果
### 4.1 为什么"逐像素哈希"是唯一可信的验收
"看起来一样"不可作为判据:这类重构最容易出的问题是**1px 偏移**与**边界条件**
(例如某个元素在某个 DPI 下多画了一条线)。`Tools/rendercheck` 对 8 个典型场景
(空白 / 单网格 / 多网格 / 分组 / 面板展开 / 菜单 / 拾色器 / 样式区)渲染到内存位图后
取 FNV 哈希,**与基线逐一比对**,因此任何像素级差异都会暴露。
结果:8 个哈希与改造前**逐一相同**,且 `JustDataInspector.exe` 在 L1 前后**字节数完全相同**。
### 4.2 断言矩阵
| 探针 | 断言 | 覆盖 |
| --- | ---: | --- |
| `uicheck` | 69 | 协议行为、命中、几何同源、浮层表面 |
| `lbcheck` | 21 | 边界收集 + **关闭后逐像素还原** |
| `lbresid` | 5 | 同一状态连续两帧逐像素相同(残留判据) |
| `lbzorder` | 4 | 浮层压住本体 |
| `lbcovr` | 7 | 覆盖完整性(时间轴/滚动条/胶囊/文本框/缓存行) |
| `lbpanel` | 10 | 面板遮挡、面板内容裁剪、滚动不闪烁、菜单条目裁剪 |
| 合计 | **116** | — |
### 4.3 方法论教训(已写入 `agent_mem.md`)
* **体积结论必须区分稳态与瞬时**;改编译开关后**必须 `make clean`**(混链会 ICE)。
* **判断"残留"不能用"拖拽帧 vs 松开后一帧"**(预热进度不同会产生几十万像素合法差异),
正确判据是**同一状态连续两帧逐像素相同**。
* **`--gc-sections` 已经承担了内联去重**(`UiPointInRect` 等自有符号重复组数为 0),
不必为了体积手工把 inline 实现搬进 `.cpp`。
* **探针判"盒子越界"必须按绘制层过滤**,否则会把更上层浮层的合法越界算成错误。
---
## 5. 风险与缓解
| 风险 | 缓解 |
| --- | --- |
| LTO 构建偶发 ICE(编译器缺陷,非代码缺陷) | `-flto-partition=one`;`LTO=0` 可一键回退(体积回到 882,688 B,仍远低于阈值) |
| `-fno-exceptions` 下遗漏一处分配检查 | terminate handler 兜底 + 全仓已无 `try/catch` + `MemReserveIfRoom` 是唯一预留入口 |
| LTO 构建时间翻倍 | 日常开发可用 `make release LTO=0`;提交前用 `LTO=1` 校验体积 |
| 探针脚本未纳入版本库,重建时缺 `-flto` | 已写入 Progress §6 待办;脚本本身只是本地验证工具 |
| L2 未做,布局边界打开时帧率下降 | 该开关是调试特性,默认关闭;文档已记录取舍 |
---
## 6. 遗留
1. **L2 `UiDamage` + S4 叠加层**:设计已定型(`kMaxRects=12`、超限退化为包围盒、
整数像素外扩 1px、只增不减;`PlotView::Paint(g, UiDamage&)` 契约;6 条路径各自 `damage.Add`),
但未实施。`UiBoxTable` 的几何可以直接喂给它,不需要重新收集。
2. **探针脚本入版本库**(`Tools/*/build.ps1`、`.preflight/*/build.ps1`)。
3. **`.lst` 汇编清单在 LTO 下不可用**:需要时用 `make release LTO=0`,这一点已在 Makefile 注释中写明。
---
## 7. 交付确认
```powershell
git status --short # 干净
make clean; make release TARGET_FILE_NAME=JustDataInspector -j16
# → 零 error / 零 warning,788,992 B
```
唯一产物:`Build/Release/JustDataInspector.exe`(含图标、清单,`-static` 链 CRT)。
**无任何新增 DLL 依赖。**
+104 -19
View File
@@ -20,13 +20,20 @@
- 工具链:**MinGW-w64**(`x86_64-win32-seh-rev0`, MinGW-Builds 16.1.0),可直接用 `make`。
- 构建入口:根目录 `Makefile`(C / C++ / windres 混合构建)。
- 常用命令:
- `make release` → `Build/Release/Target.exe`(`-Os`,strip:`-s`)。
- `make debug` → `Build/Debug/Target.exe`(`-O0 -g`)。
- `make release` → `Build/Release/Target.exe`(`-Oz` + LTO + `-s`,strip)。
- `make debug` → `Build/Debug/Target.exe`(`-O0 -g`,**不** strip)。
- `make` → 同时构建 Release 与 Debug。
- `make clean` → 删除 `Build/`。
- 关键编译设置:`-std=c++17`;`-finput-charset=UTF-8 -fexec-charset=GBK -fwide-exec-charset=UTF-16LE`;`-static`;`-mwindows`(GUI);链接 `gdi32 gdiplus comdlg32 d3d11 dwmapi d3dcompiler`。
- **新增组件/工具目录后,必须在 `Makefile` 的 `CXX_INCLUDES_PATHS` 与 `CXX_SOURCES_PATHS` 中登记**(源文件按目录通配自动收集)。
- 构建产物统一在 `Build/`,已由 `.gitignore` 忽略;同时忽略 `*.o/*.d/*.lst/*.map/*.exe` 与 `TestData/`。
- 关键编译设置:`-std=c++17`;`-finput-charset=UTF-8 -fexec-charset=GBK -fwide-exec-charset=UTF-16LE`;
`-static`;`-mwindows`(GUI);链接 `gdi32 gdiplus msimg32 comdlg32 d3d11 dwmapi d3dcompiler`。
- 体积相关开关(Makefile 顶部,**改这些必须删 `*.o` 全量重编,Makefile 变更不会自动触发重编**):
- `NO_EXCEPTIONS = 1` → `-fno-exceptions`。
- `NO_RTTI = 1` → `-fno-rtti`(全仓无 `dynamic_cast`/`typeid`;见 `MemProbe.cpp` 里对
`__gnu_cxx::__verbose_terminate_handler` 的覆盖,它同时省掉 56 KB 的 `cp-demangle`)。
- `LTO = 1` → `-flto -flto-partition=one`。
- `RELEASE_ONLY_OPT` = `-fno-asynchronous-unwind-tables`(**只**加在 Release 规则上)。
- **新增组件/工具目录后,必须在 `Makefile` 的 `CXX_INCLUDES_PATHS` 与 `CXX_SOURCES_PATHS` 中登记**(源文件按目录通配自动收集;只加 `.cpp` 文件到已登记目录时无需改 Makefile)。
- 构建产物统一在 `Build/`,已由 `.gitignore` 忽略;同时忽略 `*.o/*.d/*.lst/*.map/*.exe`、`TestData/`、`Temp/`、`.preflight/`、`Tools/`。
---
@@ -34,27 +41,42 @@
```
Components/ 功能组件块(每块 Inc/ 放 .hpp,Src/ 放 .cpp)
Application/ 应用主框架:窗口、布局、消息分发、标签页、绘制合成
Application/ 应用主框架:窗口、布局、消息分发、绘制合成
(分片:Application.cpp / ApplicationTabs.cpp / ApplicationDerive.cpp /
ApplicationMenus.cpp / ApplicationCommands.cpp / ApplicationLoading.cpp)
Main/ 程序入口(WinMain → Application::Run)
Shared/ 与业务无关的通用层:UiComponent(顶层组件协议)、UiWidget(控件树)、
UiLayout / UiBox / UiSurface / UiButtonWidget / UiControls、
PaintUtil(低层绘制原语)、Theme、TextEdit、PopupMenu、ColorPickerPopup、
UiDebugBounds(「显示布局边界」唯一消费者)
TabBar/ 顶部多标签栏
Toolbar/ 顶部工具栏(导入文件 / 适应视图 / 时间轴自适应 / Y 轴自适应 / 选区放大 / 光标 / 合并·拆分·清空网格)
SignalPanel/ 左侧信号面板:按文件分组的信号树、可见性勾选、样式编辑
Toolbar/ 顶部工具栏(设置 / 导入文件 / 适应视图 / Y 轴自适应 / 选区放大 / 光标 /
合并·拆分·清空网格)
SignalPanel/ 左侧信号面板:按分组信号树、可见性勾选、样式与公式编辑、拖拽排序
PlotView/ 波形绘图区:网格、时间轴、光标、缩放、拖拽、实验性功能面板
Utilities/ 工具与辅助模块(Inc/ + Src/)
Application/ ... (见上)
CsvTable/ CSV 流式解析(列优先)
DataModel/ 数据层:DataSet / DataFile / SignalData / 显示配置
PaintUtil/ GDI+ 绘制与文本/控件辅助(含 UiPointInRect、UiMeasureTextWidth 等)
PopupMenu/ 主题化自绘弹出菜单(含二级子菜单、内联编辑)
ColorPickerPopup/ 浮动拾色器
TextEdit/ 内联文本编辑模型
Theme/ 主题色板(Solarised Light)与波形默认配色
Utf8Util/ 编码转换(UTF-8 ↔ UTF-16 等)
(分片:PlotView.cpp / PlotViewPaint.cpp / PlotViewRaster.cpp /
PlotViewPanels.cpp / PlotViewCacheKey.cpp / PlotViewFeatures.cpp /
PlotViewInteraction.cpp)
Utilities/ 与 UI 无关的数据与格式层(Inc/ + Src/)
DataModel.* 数据层:DataSet / SignalGroup / TimeAxis / SignalData / SignalStyle / TabConfig
DataModelTab.* TabConfig(每标签页的视图配置)实现
DataTable.hpp 加载层中间表(TableBlock)
DataLoader.* 加载入口(按扩展名分发)
CsvLoader.* CSV(多 time 列 = 多时间轴)
Mf4Loader.* ASAM MDF v4(.mf4/.mdf)
MatLoader.* MATLAB .mat(v5 / v7.3 HDF5)
MatH5.* HDF5 只读子集
Deflate.* HDF5 分块解压(内置 inflate,不依赖 zlib)
FormulaEval.* 「公式映射」表达式编译与求值
Utf8Util.* 编码转换 + 数值/时长/频率格式化
MemProbe.* 内存预检验与 terminate 兜底
Resources/ 图标、清单(.manifest)、版本信息(.rc)、ResourceIds.h
TestData/ 本地测试 CSV(**不纳入版本控制**)
Documents/ 项目文档(含本文件)
Documents/ 项目文档(含本文件、Code_Review_* 系列)
```
- **依赖方向固定**:`Components/*` → `Components/Shared` → `Utilities`(不得反向)。
`PopupMenu` / `ColorPickerPopup` 历史上放在 `Utilities`,现已在 `Components/Shared`。
- **命名硬性约束**:源文件/头文件**不得含空格**,只能字母与下划线组合。
- **头文件后缀**:C 用 `.h`,C++ 用 `.hpp`;C 源 `.c`,C++ 源 `.cpp`。
- **防重复包含**:每个头文件必须 `#ifndef __XXX_XX_HPP__ / #define __XXX_XX_HPP__ / ... / #endif`;宏名 = 文件名的双边双下划线 + 全大写。
@@ -10283,3 +10305,66 @@ release/debug **零警告**;Release `TargetCheck.exe` **890,880 B**(< 1 MB
* 首次 379ms 仍是同步阻塞。若要彻底无感需把公式缓存改成后台线程计算 +
未就绪时回退到保序端点映射;当前收益不足以承担该复杂度。
---
## 2026-11-04 · 第 33 轮(`Code_Review` 分支):全项目结构/性能/体积审查
> 完整节点计划见 `Documents/Code_Review_Plan.md`,逐节点记录见
> `Documents/Code_Review_Progress.md`,结论见 `Documents/Code_Review_Report.md`。
> 本轮同时删除了过时的 `Documents/UI_Refactor_{Plan,Progress,Report}.md`。
### 1. 体积:890,880 B → **794,112 B**(−96,768,−10.9%)
* **`dynamic_cast` 是体积炸弹**:`Toolbar::BuildBoxes` 里唯一的
`dynamic_cast<const UiButtonWidget*>` 是**多余强转**(`Button::widget` 本来就是该类型)。
最小复现程序实测 39,936 → 115,200 B(+75,264)。
* **光关 RTTI 不够**:`cp-demangle.o` 仍在链接里 —— 真凶是 libstdc++ 的 `vterminate.o`
(被 `eh_terminate.o` 的 `std::terminate` 拉入,它引用 `__cxa_demangle`)。
在 `MemProbe.cpp` 里**自行定义** `__gnu_cxx::__verbose_terminate_handler()`
⇒ `vterminate.o` 不再被拉入 ⇒ 整个 demangler 消失(`.text` −49,520、`.rdata` −10,320)。
⚠️ 该定义**必须**在全局 `__gnu_cxx` 命名空间,不能嵌在 `jdi` 里。
* **Release 关闭异步展开表**(`RELEASE_ONLY_OPT = -fno-asynchronous-unwind-tables`):
`-fno-exceptions` 下 `.pdata` 30,420 → 12,684、`.xdata` 28,904 → 10,156(**−36,352**)。
代价:Release 崩溃时 WER 拿不到可靠调用栈(不打印栈回溯的程序功能不受影响)。
### 2. ⚠️ 修掉一个真实构建缺陷:`DEBUG_OPT` 从未生效
`DEBUG_OPT = -O0` 定义了却**没有任何规则使用它** —— Debug 的两条编译规则误用
`$(RELEASE_OPT)`(`-Oz`),且全程没有 `-g`。所谓 "Debug 构建" 其实只是
"不 strip 的 Release",**根本无法调试**。现已改用 `$(DEBUG_OPT) -g`(链接也加 `-g`)。
⚠️ 因此 Debug 产物从 1.73 MB 变成 6.0 MB,属预期(真实 `-O0 -g`)。
### 3. 架构:面板"按种类分派"收敛为唯一派发点
`Lane` 里 7 种实验性功能各有 1 份 `LanePanelState` + 1 个滚动偏移。旧实现在
**每一处**按种类取用的地方各写一遍嵌套三元式/`switch`(`EdgePanelLayoutFor` 里就有
5 条 7 层嵌套三元式),新增一种面板要改 20+ 处,漏一处就是「拖不动 / 关不掉 /
滚轮滚不到底」——`MARK`、`RESAMPLE`、`CLIP` 三种都真实踩过。
现在唯一派发点是 `PlotViewInternal.hpp` 的
`LanePanelEnabled` / `LanePanelStateOf` / `LanePanelScrollOf`。
⚠️ **这些 `switch` 一律不写 `default:`**,让 `-Wswitch`(属 `-Wall`)在新增
`EdgePanelKind` 而漏登记时直接**编译报错**(已用最小程序验证 `-Wswitch` 确实会报)。
⇒ **今后新增实验性功能面板,忘记登记会编译失败,而不是运行时静默出 BUG。**
### 4. 架构:`ApplicationCommands.cpp`(2,165 行)拆成 4 个分片
`ApplicationTabs.cpp`(355)/ `ApplicationDerive.cpp`(786)/ `ApplicationMenus.cpp`(409)/
`ApplicationCommands.cpp`(642)。纯搬移、零逻辑改动;Makefile 按目录通配,无需登记。
### 5. 审查结论中的"没有发现问题"项(同样重要)
* **无死代码**:`-Wall` 零警告(未使用的 `static` 函数会报警);对
`PlotView` / `SignalPanel` / `Application` / `TabBar` / `Toolbar` / `DataSet`
的 233+ 个 `m_*` 成员做全仓引用统计,**没有"只声明不使用"的字段**。
* **无 TODO/FIXME/未完成的临时方案**(源码里的"未完成"全是边沿统计进行中的业务语义)。
* **`UiMeasureTextWidth` 有记忆化**(键 = 字体 + 文本),因此布局层的重复测量不是热点。
### 6. 未做(诚实记录)
* `EdgePanelLayoutFor`(957 行分支)未改成"每面板一行元数据表":
没有离屏像素对比工具(`.preflight` 已删,崩溃会弹模态框),无法自证像素一致。
* `EdgeScanFor` 的累积状态查找是**线性扫描** `m_edgeScans`(上限 2×信号数+余量)。
288 信号时每帧约 16 万次比较(~0.3ms 量级)。可改哈希索引,但收益有限、风险不小。
* `SceneContentHash` 里 6 段逐面板混入循环未合并(同上:无法验证"仅失效条件不变")。
+24 -5
View File
@@ -192,6 +192,23 @@ ifeq ($(strip $(NO_EXCEPTIONS)), 1)
CXXFLAGS += -fno-exceptions
endif
# ---- RTTI:全项目不使用 `dynamic_cast` / `typeid` ----
# 2026-11-04 审查:唯一的 `dynamic_cast`(Toolbar::BuildBoxes)是多余的强转,
# 已改为直接使用已有的 `UiButtonWidget*`。RTTI 会链入 `cp-demangle.o`
# (Release map 实测 56,320 B)+ `type_info` 表,最小复现程序实测 +75,264 B。
NO_RTTI = 1
ifeq ($(strip $(NO_RTTI)), 1)
CXXFLAGS += -fno-rtti
endif
# ---- SEH 展开表:只对 Release 关闭 `-fasynchronous-unwind-tables` ----
# 2026-11-04 审查实测:**只对 Release** 关闭后产物从 829,440 → 793,088 B(**−36,352 B**),
# 因为 `-fno-exceptions` 下绝大多数函数不再需要 `.pdata`/`.xdata`。
# * `.pdata` 只用于栈回溯(崩溃诊断);本程序不打印栈回溯,功能不受影响。
# * ⚠️ 代价:Release 崩溃时 WER 拿不到可靠调用栈(Debug 构建保留完整展开表)。
# * ⚠️ 全关(`-fno-unwind-tables`)会链接失败(历史踩坑),**不要**打开。
RELEASE_ONLY_OPT = -fno-asynchronous-unwind-tables
# 丢弃未被引用的函数/数据(非 LTO 时还需要配合 -ffunction-sections 才有粒度)。
GC_SECTIONS = -Wl,--gc-sections
@@ -276,18 +293,18 @@ $(BUILD_DIR)/$(SUB_DIR_RELEASE)/$(TARGET_FILE_NAME).exe: $(RELEASE_OBJECTS)
# Debug 最终可执行文件编译任务
$(BUILD_DIR)/$(SUB_DIR_DEBUG)/$(TARGET_FILE_NAME).exe: $(DEBUG_OBJECTS)
@echo ====== [Debug] All File Compiled. Now Linking... ======
$(CXX_COMPLIER) $(DEBUG_OBJECTS) -static -o $@ $(LIB_LINK) $(LDFLAGS)
$(CXX_COMPLIER) $(DEBUG_OBJECTS) -static -g -o $@ $(LIB_LINK) $(LDFLAGS)
@echo ====== [Debug] Program Link Finished ======
# C Release 目标文件编译
$(BUILD_DIR)/$(SUB_DIR_RELEASE)/%.o: %.c Makefile | $(BUILD_DIR)/$(SUB_DIR_RELEASE)
@echo ====== [Release] C Source File "$<" Compiling... ======
$(C_COMPLIER) -c $(CFLAGS) $(RELEASE_OPT) $(ASM_LISTING_C) $< -o $@
$(C_COMPLIER) -c $(CFLAGS) $(RELEASE_OPT) $(RELEASE_ONLY_OPT) $(ASM_LISTING_C) $< -o $@
# C++ Release 目标文件编译
$(BUILD_DIR)/$(SUB_DIR_RELEASE)/%.o: %.cpp Makefile | $(BUILD_DIR)/$(SUB_DIR_RELEASE)
@echo ====== [Release] C++ Source File "$<" Compiling... ======
$(CXX_COMPLIER) -c $(CXXFLAGS) $(RELEASE_OPT) $(ASM_LISTING) $< -o $@
$(CXX_COMPLIER) -c $(CXXFLAGS) $(RELEASE_OPT) $(RELEASE_ONLY_OPT) $(ASM_LISTING) $< -o $@
# Release 资源脚本文件编译
$(BUILD_DIR)/$(SUB_DIR_RELEASE)/%.o: %.rc Makefile | $(BUILD_DIR)/$(SUB_DIR_RELEASE)
@@ -295,14 +312,16 @@ $(BUILD_DIR)/$(SUB_DIR_RELEASE)/%.o: %.rc Makefile | $(BUILD_DIR)/$(SUB_DIR_RELE
$(WIN_RES_COMPLIER) -c $(WIN_RESOURCE_CODEPAGE) $(foreach path,$(WIN_RESOURCE_INCLUDES_PATHS),$(addprefix -I,$(path))) $< -o $@
# C Debug 目标文件编译
# ⚠️ 2026-11-04 审查修正:原来这里误用了 `$(RELEASE_OPT)`,导致 `DEBUG_OPT = -O0`
# **从未生效**(Debug 实际上也是 -Oz,且不带 `-g`,根本无法调试)。
$(BUILD_DIR)/$(SUB_DIR_DEBUG)/%.o: %.c Makefile | $(BUILD_DIR)/$(SUB_DIR_DEBUG)
@echo ====== [Debug] C Source File "$<" Compiling... ======
$(C_COMPLIER) -c $(CFLAGS) $(RELEASE_OPT) $(ASM_LISTING_C) $< -o $@
$(C_COMPLIER) -c $(CFLAGS) $(DEBUG_OPT) -g $(ASM_LISTING_C) $< -o $@
# C++ Debug 目标文件编译
$(BUILD_DIR)/$(SUB_DIR_DEBUG)/%.o: %.cpp Makefile | $(BUILD_DIR)/$(SUB_DIR_DEBUG)
@echo ====== [Debug] C++ Source File "$<" Compiling... ======
$(CXX_COMPLIER) -c $(CXXFLAGS) $(RELEASE_OPT) $(ASM_LISTING) $< -o $@
$(CXX_COMPLIER) -c $(CXXFLAGS) $(DEBUG_OPT) -g $(ASM_LISTING) $< -o $@
# Debug 资源脚本文件编译
$(BUILD_DIR)/$(SUB_DIR_DEBUG)/%.o: %.rc Makefile | $(BUILD_DIR)/$(SUB_DIR_DEBUG)
+54 -8
View File
@@ -12,6 +12,35 @@
#include <cstdlib>
#include <exception>
namespace
{
/**
* @brief 「未捕获异常 / 分配失败」的可见提示(`std::terminate` 的兜底路径)。
*
* ⚠️ **为什么放在全局匿名命名空间**:下面要在 `namespace __gnu_cxx` 里覆盖
* libstdc++ 的默认终止处理器,而 `__gnu_cxx` 必须是**全局**命名空间
* (不能嵌套在本项目自己的 `jdi` 里)。
*
* ⚠️ 本来这个函数由 libstdc++ 的 `__gnu_cxx::__verbose_terminate_handler()` 提供,
* 但它所在的 `vterminate.o` 会链入 `cp-demangle.o`
* (Release map 实测 `.text` 49,600 + `.rdata` 6,720 ≈ **56 KB** 的 C++ 名字反修饰器,
* 对一个不打印栈回溯的 GUI 程序毫无用处)。
* 本文件自行定义该函数 ⇒ 归档成员 `vterminate.o` 不再被拉入 ⇒ demangler 消失
* (2026-11-04 代码审查,实测 −55 KB 量级)。
*
* ⚠️ 这里**不能**用任何会再次分配内存的机制(此刻堆可能已不可用):
* 只用静态字面量与 Win32 API。
*/
void MemTerminateFallbackImpl()
{
MessageBoxW(nullptr,
L"内存不足:本次操作需要的内存超过系统可用量。\n\n"
L"建议:减少同时显示的网格数或信号数,或关闭占用内存较大的程序后重试。",
L"Just DataInspector", MB_ICONERROR | MB_OK | MB_SETFOREGROUND);
std::_Exit(EXIT_FAILURE);
}
} // namespace
namespace jdi
{
@@ -32,18 +61,13 @@ namespace jdi
std::atomic<std::size_t> g_memCachedBytes{0};
/**
* @brief terminate 处理:给出可见提示后退出。
* @brief 应用安装的 terminate 处理(`MemInstallTerminateHandler` 注册)。
*
* ⚠️ 这里**不能**用 MessageBoxW 之外的任何复杂机制:此刻堆可能已不可用,
* 连 `std::wstring` 都不该构造(字面量是静态数据,安全)。
* ⚠️ 与文件末尾的 `__gnu_cxx::__verbose_terminate_handler()` 覆盖共用同一条提示路径。
*/
void OnTerminate()
{
MessageBoxW(nullptr,
L"内存不足:本次操作需要的内存超过系统可用量。\n\n"
L"建议:减少同时显示的网格数或信号数,或关闭占用内存较大的程序后重试。",
L"Just DataInspector", MB_ICONERROR | MB_OK | MB_SETFOREGROUND);
std::_Exit(EXIT_FAILURE);
MemTerminateFallbackImpl();
}
} // namespace
@@ -99,3 +123,25 @@ namespace jdi
}
} // namespace jdi
/**
* @brief **覆盖** libstdc++ 的默认终止处理器(省掉 56 KB 的 C++ 名字反修饰器)。
*
* `std::terminate()` 定义在 `eh_terminate.o` 里,它引用 `__gnu_cxx::__verbose_terminate_handler()`。
* 该符号原本由归档成员 `vterminate.o` 提供,而 `vterminate.o` 又引用 `__cxa_demangle`
* ⇒ 链接器被迫拉入 `cp-demangle.o`(Release map 实测 `.text` 49,600 B + `.rdata` 6,720 B)。
* 我们在自己的目标文件里提供同名定义,`eh_terminate.o` 的未定义引用就被满足,
* `vterminate.o` 与整个 demangler 都不会进入最终产物。
*
* ⚠️ 必须定义在**全局** `__gnu_cxx` 命名空间(不能嵌套在 `jdi` 内)。
* ⚠️ 应用启动后会通过 `jdi::MemInstallTerminateHandler()` 换上自己的处理函数,
* 因此这个默认实现只在「启动早期崩溃」时生效 —— 行为与 libstdc++ 的版本等效
* (libstdc++ 版会打印反修饰后的异常类型名,我们只给内存提示)。
*/
namespace __gnu_cxx
{
void __verbose_terminate_handler()
{
MemTerminateFallbackImpl();
}
} // namespace __gnu_cxx