feat/vtk-3d-view #7
|
|
@ -0,0 +1,173 @@
|
||||||
|
# Task 12d 收尾探针报告 —— 视觉调优 + fps 预算 + 可交互开窗
|
||||||
|
|
||||||
|
实测环境: 本机 RTX 3060 / VTK 9.6 / MSVC+Ninja。store: `tmp/store_lod_001`
|
||||||
|
(level0 = 44476×29×162, 4 层金字塔, brick=64, 2.09 亿体素)。
|
||||||
|
|
||||||
|
所有数字为真实离屏实测, 双闸(纹理错捕获 + 回读非空像素)防假帧率。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 状态
|
||||||
|
|
||||||
|
完成。三件事全部落地、编译通过、离屏实测出数:
|
||||||
|
- ① `tune` 视觉调优: 出 `lod-tuned-local.png` / `lod-tuned-overview.png`, 打印调优前后 fps 对照。
|
||||||
|
- ② `fps-budget`: 递增全分辨率窗口 fps 表 + 每帧体素预算结论。
|
||||||
|
- ③ `view`: 真窗口 + interactor + 缩放切 LOD + 屏幕 fps 文本; 离屏 `--smoke` 通过不崩。
|
||||||
|
|
||||||
|
改动文件: `tools/gpr_poc/main.cpp` (新增 3 个子命令 + 视觉调优共享构件), 新增两张调优截图,
|
||||||
|
追加写 `docs/superpowers/plans/poc-results-C.md`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ① 视觉调优: 调优前后 fps 对照(证实视觉调优 fps 近乎中性)
|
||||||
|
|
||||||
|
`gpr_poc tune <store> --opacity 0.7 --exagg 8 --localBricks 4` (level0 256×29×162 局部段):
|
||||||
|
|
||||||
|
| 配置 | 色阶 | 不透明度 | 垂向夸张 | 局部 fps |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 调优前(基线) | 蓝-白-红线性单斜坡 | 0.15 | 1× | 323.3 |
|
||||||
|
| 调优后 | 结构色阶(深蓝→青→白→黄→红) + 双端斜坡 | 0.7 | 8× | 349.2 |
|
||||||
|
|
||||||
|
**fps 变化 = −8.0%(即调优后反而更快)**。完全证实探针认知:
|
||||||
|
|
||||||
|
- 隔离实验(`--exagg 1`): 不透明度 0.15→0.5/0.6、换结构色阶, fps −5.5%(更快)。
|
||||||
|
→ **配色/不透明度对 fps 近乎中性, 调高不透明度甚至更快(光线提前终止)。**
|
||||||
|
- 隔离实验(`--opacity 0.9 --exagg 10`): fps 反而 +49%(更快)。
|
||||||
|
双端斜坡把占多数的近零背景设透明, 不透明片段少 + 提前终止, 抵消了夸张放大的屏占。
|
||||||
|
- 早先一版"线性单斜坡 + exagg 8"曾掉 34%, 经排查 **掉帧全部来自垂向夸张(8× 放大薄轴
|
||||||
|
→ 屏占变大 → ray-cast 片段变多), 与不透明度/配色无关**。改用双端斜坡(背景透明)后
|
||||||
|
即转为净加速。
|
||||||
|
|
||||||
|
**关键视觉修复**: GPR/地震体值集中在零附近(背景), 强反射在正负两端。原线性单斜坡让
|
||||||
|
近零背景填满体、遮住结构(实测渲出一块均匀蓝板, 无结构)。改为**双端斜坡(中段透明 +
|
||||||
|
正负两端不透明)** 后, 截面的层状反射(地层条带)清晰可辨。
|
||||||
|
|
||||||
|
调优截图:
|
||||||
|
- `docs/superpowers/plans/poc-lod-shots/lod-tuned-local.png`
|
||||||
|
—— 全分辨率局部段, 可见多条水平层状反射条带(地层结构)+ 一处相干蓝色异常体。
|
||||||
|
- `docs/superpowers/plans/poc-lod-shots/lod-tuned-overview.png`
|
||||||
|
—— 粗层(level2)概览。物理真实: 整线 2.2km×1.5m×8m 极扁, 概览就是一条细带(可接受)。
|
||||||
|
|
||||||
|
> 诚实说明: 体物理纵横比极端(X≈2.2km vs Y≈1.5m / Z≈8m), 即便取局部段 + 8× 夸张,
|
||||||
|
> 单帧里结构仍偏小、偏一隅, 背景大片黑。结构确实可辨(层状条带 + 异常体), 但"一眼炸裂"
|
||||||
|
> 受物理形态限制——这正是 brief 预期的"细带本质"。production 可配可调色阶/取景控件让
|
||||||
|
> 用户交互找最佳视角(即 ③ view)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ② fps 预算: 递增全分辨率(level0)窗口 → 每帧体素预算
|
||||||
|
|
||||||
|
`gpr_poc fps-budget <store> --bricks 4,16,64,128,256,512,695 --frames 90`
|
||||||
|
(沿线中段递增 brick 列, 单 image 整段体绘制, 双闸):
|
||||||
|
|
||||||
|
| brick 段 | 维度 | 体素数 | 体绘制 fps | ≥30 | 备注 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 4 | 256×29×162 | 1,202,688 | 218.3 | 是 | |
|
||||||
|
| 16 | 1024×29×162 | 4,810,752 | 155.7 | 是 | |
|
||||||
|
| 64 | 4096×29×162 | 19,243,008 | 240.9 | 是 | |
|
||||||
|
| 128 | 8192×29×162 | 38,486,016 | 305.8 | 是 | |
|
||||||
|
| 256 | 16384×29×162 | 76,972,032 | 329.7 | 是 | 触达 GL_MAX_3D_TEXTURE_SIZE=16384 |
|
||||||
|
| 512 | 32768×29×162 | 153,944,064 | INVALID | 否 | X=32768>16384, 纹理墙, 双闸标 INVALID |
|
||||||
|
| 695 | 44476×29×162 | 208,948,248 | INVALID | 否 | 同上 |
|
||||||
|
|
||||||
|
### 每帧体素预算结论(重要, 与 brief 框架略有出入但更真实)
|
||||||
|
|
||||||
|
- **fps 在所有可上传测点(≤16384 单轴)始终 ≫ 30(218~330fps), 全程没跌破 30。** fps 不随
|
||||||
|
体素数单调下降(甚至上升), 因 ray-cast 成本主要由屏占 × 采样步长决定, 而薄维度(Y29/Z162)
|
||||||
|
使光线路径短, 单 3D 纹理上传成功后体素总数不是瓶颈。
|
||||||
|
- **真正的硬墙是 GL_MAX_3D_TEXTURE_SIZE = 16384**: 单轴超 16384 → 整段无法成单张 3D 纹理
|
||||||
|
(512/695 行双闸正确判 INVALID, 绝不当真上报)。
|
||||||
|
- 因此本数据集上, **"单张 3D 纹理的每帧体素预算" = 单轴 ≤16384 → ≈ 7700 万体素(256 brick 列)**
|
||||||
|
跑 ~330fps 仍极宽裕; **限制 production LOD 每帧块数的不是 30fps 阈值, 而是 16384 纹理墙——
|
||||||
|
超墙必须切块(MultiBlock / SetPartitions / 本机核外 OutOfCoreSource)。**
|
||||||
|
- fps 驱动的体素预算(跌破 30)只会在远更大/更稠密体或多块叠加渲染时出现; 本数据集薄维度下
|
||||||
|
GPU 余量充足, 未触达。
|
||||||
|
|
||||||
|
> 这与 brief"找 fps<30 阈值"的设想不同, 但是实测真相: **本数据集的命门是纹理尺寸墙,
|
||||||
|
> 不是帧率墙**。如实记录。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ③ `gpr_poc view <store>` —— 真窗口可交互(给用户肉眼测 + 最低配机跑)
|
||||||
|
|
||||||
|
实现要点:
|
||||||
|
- 真 `vtkRenderWindow` + `vtkRenderWindowInteractor`(`vtkInteractorStyleTrackballCamera`),
|
||||||
|
挂 `OutOfCoreSource`(核外 LOD + 视野选块, budget 限驻留, 内存恒定)。
|
||||||
|
- 相机变化(`EndInteractionEvent`)→ `source.update(camera)` 重选 LOD/视野块 → 重建 MultiBlock
|
||||||
|
→ 重渲。**缩放跨越距离/对角线档位时 LOD 真切换**(离屏 smoke 实测 level 1↔0 切换)。
|
||||||
|
- 屏幕左上角 `vtkTextActor` 实时显示 `fps | LOD level | blocks | exagg`, 每帧更新。
|
||||||
|
- 默认结构色阶 + 双端斜坡不透明度 + 垂向夸张(同 ①)。
|
||||||
|
- 参数: `--exagg N --opacity F --budget K`(K=每帧最大全分辨率块数, 接 ② 预算)。
|
||||||
|
|
||||||
|
离屏 smoke(`view --smoke`)实测:
|
||||||
|
```
|
||||||
|
预热: level=1 视野块=696/696 驻留=64 渲染块=64
|
||||||
|
近观 level=1 → 拉远 level=1 → 再拉近 level=0
|
||||||
|
LOD 随缩放切换 : 是 ✔
|
||||||
|
纹理维度错误 : 否
|
||||||
|
渲出非空像素 : 是 (近=1024000 远拉近=1024000)
|
||||||
|
smoke 结果 : OK ✔ 不崩
|
||||||
|
```
|
||||||
|
|
||||||
|
### view 命令用法
|
||||||
|
|
||||||
|
```
|
||||||
|
gpr_poc view <storeDir> [--exagg 8] [--opacity 0.6] [--budget 64] [--smoke]
|
||||||
|
```
|
||||||
|
- 不带 `--smoke` = 开真窗口可交互(留给用户跑)。
|
||||||
|
- 带 `--smoke` = 离屏建管线 + 模拟缩放验 LOD 切换 + 验不崩(CI/无显示环境用)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 给用户的肉眼测试说明(请转达用户)
|
||||||
|
|
||||||
|
**启动命令**(在已构建的仓库根目录):
|
||||||
|
```
|
||||||
|
build\release\tools\gpr_poc\gpr_poc.exe view tmp\store_lod_001 --exagg 8 --opacity 0.6 --budget 64
|
||||||
|
```
|
||||||
|
- DLL/PATH: 无需手设。CMake 已把 VTK/Qt 等运行时 DLL 拷到 exe 旁(`gpr_poc.exe` 同目录),
|
||||||
|
直接双击/命令行运行即可。
|
||||||
|
- 若换其它 store, 把 `tmp\store_lod_001` 换成你的金字塔 store 目录(需先 `gpr_poc build ... --levels 3`)。
|
||||||
|
|
||||||
|
**操作:**
|
||||||
|
- **滚轮**: 向前滚拉近 → 应看到全分辨率结构(屏幕 `LOD level` 数字变小, 0=最细);
|
||||||
|
向后滚拉远 → 变粗层概览(level 数字变大, 体变糊)。
|
||||||
|
- **左键拖动**: 旋转视角(TrackballCamera)。
|
||||||
|
- **q 键 / 关窗**: 退出。
|
||||||
|
|
||||||
|
**判断点(可接受标准):**
|
||||||
|
1. **拉近后能否看清地质结构**: 局部段应呈现水平层状反射条带(地层)+ 可辨的相干异常体。
|
||||||
|
能看出层次即可接受(受物理细带形态限制, 不会像规则立方体那样饱满)。
|
||||||
|
2. **概览(细带)可不可接受**: 拉远后是一条细长带(整线 2.2km×1.5m×8m 物理真实), 接受它是细带。
|
||||||
|
3. **拉近/拉远切 LOD 时卡不卡、糊→清过渡能不能接受**: 切换应顺滑, 无明显卡死/长 stall
|
||||||
|
(本机切换 ~5-9ms, 远小于 1 个 60Hz 帧 16.7ms, 不可感)。
|
||||||
|
4. **屏幕 fps 是否 ≥30**: 屏幕左上角实时 fps。本机(RTX 3060)远超 30(数百 fps);
|
||||||
|
**最低配机重点看这条**——拉到最细 LOD、最大夸张时 fps 是否仍 ≥30。
|
||||||
|
|
||||||
|
**最低配怎么跑:**
|
||||||
|
- 把整个 `build\release\tools\gpr_poc\` 目录(含所有 DLL)+ 一个 store 目录拷到目标机,
|
||||||
|
跑上面的 `view` 命令, 肉眼看屏幕 fps 与交互流畅度。
|
||||||
|
- 或无显示/批处理场景跑 `gpr_poc fps-budget tmp\store_lod_001` 出该机的体素-fps 表对照。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最低配未验声明
|
||||||
|
|
||||||
|
本探针仅在本机 **RTX 3060** 跑出上限数字(数百 fps, 余量充足)。**最低配机器未验证**,
|
||||||
|
需用户拿目标机跑 `gpr_poc view <store>`(肉眼判 fps≥30 + 交互流畅)或 `gpr_poc fps-budget <store>`
|
||||||
|
(出该机体素-fps 表)。production 是否对最低配可用, 以目标机实测为准。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Concerns
|
||||||
|
|
||||||
|
1. **视觉天花板受物理形态限制**: 体极扁(2.2km×1.5m×8m), 单帧结构偏小偏一隅。这是数据物理
|
||||||
|
真实, 非 bug; production 应给用户交互色阶/取景/裁剪控件(view 已具备旋转缩放, 色阶可参数化)。
|
||||||
|
2. **fps 不是本数据集的瓶颈, 纹理尺寸墙(16384)才是**: 与 brief"找 fps<30 阈值"设想不同。
|
||||||
|
每帧体素预算结论是"单轴 ≤16384 即可单纹理上传, fps 仍 ≫30", 超墙必须切块。如实记录。
|
||||||
|
3. **view 的 LOD 阈值按未夸张几何标定**: `pickLevel` 用 level0 原始对角线算距离比, 而 actor
|
||||||
|
已 `SetScale(1,exagg,exagg)`。夸张会轻微平移"缩放-LOD 映射"档位, 但切换仍正常触发
|
||||||
|
(smoke 实测 level 1↔0)。若用户觉得切档时机别扭, 后续可让 pickLevel 感知夸张系数。
|
||||||
|
4. **view 连续拖动 fps 文本基于上一帧耗时估算**(单帧 wall-clock 倒数), 非滑动平均, 数字会抖;
|
||||||
|
足够给用户感知量级(几十/几百 fps), 非精密基准(精密基准走 fps-budget/renderLOD 离屏)。
|
||||||
|
5. `last-metrics.txt`(repo 根, 探针追加输出)未纳入提交——它从未被 git 跟踪, 是瞬时产物。
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
|
|
@ -47,3 +47,26 @@
|
||||||
粗层概览 + 全分辨率局部【都达交互级】且切换【无不可接受卡顿】→ LOD-based C 路线钉死可行。
|
粗层概览 + 全分辨率局部【都达交互级】且切换【无不可接受卡顿】→ LOD-based C 路线钉死可行。
|
||||||
|
|
||||||
**最低配未验声明**:本探针仅在本机(RTX 3060)跑得上限数字,最低配机器未验证,需用户在目标机跑或提供型号。
|
**最低配未验声明**:本探针仅在本机(RTX 3060)跑得上限数字,最低配机器未验证,需用户在目标机跑或提供型号。
|
||||||
|
|
||||||
|
|
||||||
|
# POC-C fps 预算探针结果(Task 12d ②)
|
||||||
|
|
||||||
|
金字塔 store: tmp/store_lod_001(level0=44476x29x162,brick=64)
|
||||||
|
|
||||||
|
递增 level0 局部窗口(沿线中段 brick 列)体绘制 fps:
|
||||||
|
|
||||||
|
| brick段 | 体素数 | 体绘制 fps | ≥30fps |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 4 | 1202688 | 218.251659 | 是 |
|
||||||
|
| 16 | 4810752 | 155.708373 | 是 |
|
||||||
|
| 64 | 19243008 | 240.948244 | 是 |
|
||||||
|
| 128 | 38486016 | 305.837001 | 是 |
|
||||||
|
| 256 | 76972032 | 329.654511 | 是 |
|
||||||
|
| 512 | 153944064 | INVALID | 否 |
|
||||||
|
| 695 | 208948248 | INVALID | 否 |
|
||||||
|
|
||||||
|
- **每帧体素预算(fps≥30 上限)**: 76972032 体素(256 brick 列)
|
||||||
|
- 首个跌破 30 的窗口: 无(需更大 --bricks 段触达天花板)
|
||||||
|
- 双闸:纹理维度错误=是;每段均按非空像素校验。
|
||||||
|
- production LOD 应把【每帧渲染的全分辨率块】卡在此预算以内。
|
||||||
|
- **本机 RTX 3060 上限数;最低配需用户在目标机跑 fps-budget/view。**
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ AutoAnnotationDialog::AutoAnnotationDialog(geopro::data::IDatasetCommandReposito
|
||||||
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
|
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
|
||||||
auto* execBtn = new QPushButton(QStringLiteral("执行自动标注"), this);
|
auto* execBtn = new QPushButton(QStringLiteral("执行自动标注"), this);
|
||||||
saveBtn_ = new QPushButton(QStringLiteral("确认保存"), this);
|
saveBtn_ = new QPushButton(QStringLiteral("确认保存"), this);
|
||||||
|
saveBtn_->setDefault(true); // 区域唯一主操作(规范 §6.7 primary);执行/取消为次按钮
|
||||||
saveBtn_->setEnabled(false); // 必须先执行得到预览才能保存
|
saveBtn_->setEnabled(false); // 必须先执行得到预览才能保存
|
||||||
btnLay->addWidget(cancelBtn);
|
btnLay->addWidget(cancelBtn);
|
||||||
btnLay->addWidget(execBtn);
|
btnLay->addWidget(execBtn);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
|
||||||
#include "EmptyAwareComboBox.hpp"
|
#include "EmptyAwareComboBox.hpp"
|
||||||
|
#include <QDialogButtonBox>
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
|
|
@ -22,6 +23,7 @@
|
||||||
#include <QTreeWidget>
|
#include <QTreeWidget>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "FormKit.hpp" // addDialogButtons / addSection / editLabel
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
#include "panels/chart/InversionProcessOps.hpp" // buildFilterApplyBody / buildNewFilterBody
|
#include "panels/chart/InversionProcessOps.hpp" // buildFilterApplyBody / buildNewFilterBody
|
||||||
#include "repo/IDatasetCommandRepository.hpp"
|
#include "repo/IDatasetCommandRepository.hpp"
|
||||||
|
|
@ -32,7 +34,6 @@ namespace {
|
||||||
constexpr int kDialogW = 900; // 原版弹窗宽 900px
|
constexpr int kDialogW = 900; // 原版弹窗宽 900px
|
||||||
constexpr int kMatrixMin = 1, kMatrixMax = 21; // 矩阵行列范围(对照原版 1~21)
|
constexpr int kMatrixMin = 1, kMatrixMax = 21; // 矩阵行列范围(对照原版 1~21)
|
||||||
constexpr int kDefaultDim = 3;
|
constexpr int kDefaultDim = 3;
|
||||||
constexpr int kSettingLabelW = 80; // 原版 .setting-label width:80px
|
|
||||||
const char kDefaultCustomKey[] = "default-custom-filter"; // 默认自定义滤波器(不可删)
|
const char kDefaultCustomKey[] = "default-custom-filter"; // 默认自定义滤波器(不可删)
|
||||||
const char kCustomGroupName[] = "自定义滤波器";
|
const char kCustomGroupName[] = "自定义滤波器";
|
||||||
|
|
||||||
|
|
@ -44,17 +45,9 @@ double cellValue(const QTableWidgetItem* it) {
|
||||||
return ok ? v : 0.0;
|
return ok ? v : 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 原版分组小标题(14px 半粗 + 标题下 1px divider)。
|
// 分组小标题:走 §7.0.10 唯一实现 formkit::addSection(heading 半粗 + 标题下 1px divider)。
|
||||||
void addSpecTitle(QVBoxLayout* into, const QString& title, QWidget* parent) {
|
void addSpecTitle(QVBoxLayout* into, const QString& title, QWidget* parent) {
|
||||||
auto* lbl = new QLabel(title, parent);
|
formkit::addSection(into, title, parent, /*topGap=*/false);
|
||||||
auto f = lbl->font();
|
|
||||||
f.setBold(true);
|
|
||||||
lbl->setFont(f);
|
|
||||||
into->addWidget(lbl);
|
|
||||||
auto* line = new QFrame(parent);
|
|
||||||
line->setFrameShape(QFrame::HLine);
|
|
||||||
line->setFrameShadow(QFrame::Plain);
|
|
||||||
into->addWidget(line);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 原版带边框卡片(1px 边框 + 圆角 + 内距)。
|
// 原版带边框卡片(1px 边框 + 圆角 + 内距)。
|
||||||
|
|
@ -83,25 +76,18 @@ FilterDialog::FilterDialog(geopro::data::IDatasetCommandRepository* repo, QStrin
|
||||||
buildLeft(body);
|
buildLeft(body);
|
||||||
buildRight(body);
|
buildRight(body);
|
||||||
|
|
||||||
// 底部按钮(原版三按钮各 ~30%:保存设置(主,左)/确认(主,中)/取消(右))。
|
// 规范 §7.5 底部操作栏:右对齐 取消(次) + 确认(主);「保存设置」为次按钮
|
||||||
auto* btnLay = new QHBoxLayout();
|
// 经 ActionRole 落在左侧(QDialogButtonBox 自动把 ActionRole 排到主操作左边),不抢 primary。
|
||||||
btnLay->setSpacing(geopro::app::space::kMd);
|
auto* box = formkit::addDialogButtons(root, this, QStringLiteral("确认"), QStringLiteral("取消"));
|
||||||
auto* saveSettingBtn = new QPushButton(QStringLiteral("保存设置"), this);
|
okBtn_ = box->button(QDialogButtonBox::Ok);
|
||||||
okBtn_ = new QPushButton(QStringLiteral("确认"), this);
|
auto* saveSettingBtn = box->addButton(QStringLiteral("保存设置"), QDialogButtonBox::ActionRole);
|
||||||
okBtn_->setDefault(true);
|
// 确认需异步 applyFilter 成功才关闭 → 断开默认 accept,改接 onConfirm。
|
||||||
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
|
QObject::disconnect(box, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
btnLay->addWidget(saveSettingBtn, 30);
|
connect(okBtn_, &QPushButton::clicked, this, &FilterDialog::onConfirm);
|
||||||
btnLay->addStretch(2);
|
|
||||||
btnLay->addWidget(okBtn_, 30);
|
|
||||||
btnLay->addStretch(2);
|
|
||||||
btnLay->addWidget(cancelBtn, 30);
|
|
||||||
root->addLayout(btnLay);
|
|
||||||
|
|
||||||
resizeMatrix(); // 默认 3x3 中心 1
|
resizeMatrix(); // 默认 3x3 中心 1
|
||||||
if (auto* c = matrix_->item(1, 1)) c->setText(QStringLiteral("1"));
|
if (auto* c = matrix_->item(1, 1)) c->setText(QStringLiteral("1"));
|
||||||
|
|
||||||
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
|
|
||||||
connect(okBtn_, &QPushButton::clicked, this, &FilterDialog::onConfirm);
|
|
||||||
connect(saveSettingBtn, &QPushButton::clicked, this, &FilterDialog::saveCustomFilter);
|
connect(saveSettingBtn, &QPushButton::clicked, this, &FilterDialog::saveCustomFilter);
|
||||||
connect(tree_, &QTreeWidget::itemSelectionChanged, this,
|
connect(tree_, &QTreeWidget::itemSelectionChanged, this,
|
||||||
&FilterDialog::onTreeSelectionChanged);
|
&FilterDialog::onTreeSelectionChanged);
|
||||||
|
|
@ -213,14 +199,12 @@ void FilterDialog::buildRight(QHBoxLayout* body) {
|
||||||
body->addWidget(card, 6); // 右 ~60%
|
body->addWidget(card, 6); // 右 ~60%
|
||||||
}
|
}
|
||||||
|
|
||||||
// 原版 .setting-row:label(80px) + 主控件(30%) [+ 右侧 label「值:」+ 值框(30%)]。
|
// 设置行:定宽右标签列(§7.0.2 editLabel)+ 主控件 [+ 右侧「值:」标签 + 值框]。
|
||||||
QHBoxLayout* FilterDialog::settingRow(const QString& label, QWidget* main, const QString& valLabel,
|
QHBoxLayout* FilterDialog::settingRow(const QString& label, QWidget* main, const QString& valLabel,
|
||||||
QWidget* valField, QWidget* parent) {
|
QWidget* valField, QWidget* parent) {
|
||||||
auto* row = new QHBoxLayout();
|
auto* row = new QHBoxLayout();
|
||||||
row->setSpacing(geopro::app::space::kMd);
|
row->setSpacing(geopro::app::space::kMd);
|
||||||
auto* lbl = new QLabel(label, parent);
|
row->addWidget(formkit::editLabel(label, parent));
|
||||||
lbl->setMinimumWidth(kSettingLabelW);
|
|
||||||
row->addWidget(lbl);
|
|
||||||
row->addWidget(main, 3);
|
row->addWidget(main, 3);
|
||||||
if (valField) {
|
if (valField) {
|
||||||
row->addSpacing(geopro::app::space::kLg);
|
row->addSpacing(geopro::app::space::kLg);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
#include "EmptyAwareComboBox.hpp"
|
#include "EmptyAwareComboBox.hpp"
|
||||||
#include <QDoubleSpinBox>
|
#include <QDoubleSpinBox>
|
||||||
#include <QFrame>
|
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
|
@ -19,6 +18,7 @@
|
||||||
#include <QStackedWidget>
|
#include <QStackedWidget>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "FormKit.hpp" // addSection / editLabel
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
#include "panels/chart/InversionProcessOps.hpp" // buildGridToBody
|
#include "panels/chart/InversionProcessOps.hpp" // buildGridToBody
|
||||||
#include "repo/IDatasetCommandRepository.hpp"
|
#include "repo/IDatasetCommandRepository.hpp"
|
||||||
|
|
@ -42,17 +42,9 @@ QDoubleSpinBox* makeCoordSpin(QWidget* parent) {
|
||||||
return sp;
|
return sp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分组卡片标题(原版 .section-title:14px 半粗 + 标题下 1px divider)。
|
// 分组标题:走 §7.0.10 唯一实现 formkit::addSection(heading 半粗 + 标题下 1px divider)。
|
||||||
void addSectionTitle(QVBoxLayout* into, const QString& title, QWidget* parent) {
|
void addSectionTitle(QVBoxLayout* into, const QString& title, QWidget* parent) {
|
||||||
auto* lbl = new QLabel(title, parent);
|
formkit::addSection(into, title, parent, /*topGap=*/false);
|
||||||
auto f = lbl->font();
|
|
||||||
f.setBold(true);
|
|
||||||
lbl->setFont(f);
|
|
||||||
into->addWidget(lbl);
|
|
||||||
auto* line = new QFrame(parent);
|
|
||||||
line->setFrameShape(QFrame::HLine);
|
|
||||||
line->setFrameShadow(QFrame::Plain);
|
|
||||||
into->addWidget(line);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 原版 .param-group:定宽右标签 + 紧随输入框,多个并排成一行栅格。
|
// 原版 .param-group:定宽右标签 + 紧随输入框,多个并排成一行栅格。
|
||||||
|
|
@ -86,19 +78,20 @@ GridWizardDialog::GridWizardDialog(geopro::data::IDatasetCommandRepository* repo
|
||||||
buildStep1();
|
buildStep1();
|
||||||
buildStep2();
|
buildStep2();
|
||||||
|
|
||||||
// ── 底部按钮(上一步 / 确认(主) / 取消,原版步骤 2 三按钮)────────────
|
// ── 底部操作栏(规范 §7.5 右对齐):上一步(次按钮) 左;取消(次) + 下一步/确认(主) 右。──
|
||||||
auto* btnLay = new QHBoxLayout();
|
auto* btnLay = new QHBoxLayout();
|
||||||
btnLay->setSpacing(geopro::app::space::kMd);
|
btnLay->setSpacing(geopro::app::space::kMd);
|
||||||
btnLay->addStretch();
|
prevBtn_ = new QPushButton(QStringLiteral("上一步"), this); // 次按钮(描边),左侧
|
||||||
prevBtn_ = new QPushButton(QStringLiteral("上一步"), this);
|
|
||||||
okBtn_ = new QPushButton(QStringLiteral("确认"), this);
|
|
||||||
okBtn_->setDefault(true);
|
|
||||||
nextBtn_ = new QPushButton(QStringLiteral("下一步"), this);
|
|
||||||
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
|
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
|
||||||
|
nextBtn_ = new QPushButton(QStringLiteral("下一步"), this);
|
||||||
|
okBtn_ = new QPushButton(QStringLiteral("确认"), this);
|
||||||
|
nextBtn_->setDefault(true); // 步骤 1 主操作
|
||||||
|
okBtn_->setDefault(true); // 步骤 2 主操作(每屏仅一个可见,故无双 primary)
|
||||||
btnLay->addWidget(prevBtn_);
|
btnLay->addWidget(prevBtn_);
|
||||||
btnLay->addWidget(okBtn_);
|
btnLay->addStretch();
|
||||||
btnLay->addWidget(nextBtn_);
|
|
||||||
btnLay->addWidget(cancelBtn);
|
btnLay->addWidget(cancelBtn);
|
||||||
|
btnLay->addWidget(nextBtn_);
|
||||||
|
btnLay->addWidget(okBtn_);
|
||||||
root->addLayout(btnLay);
|
root->addLayout(btnLay);
|
||||||
prevBtn_->setVisible(false);
|
prevBtn_->setVisible(false);
|
||||||
okBtn_->setVisible(false);
|
okBtn_->setVisible(false);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include <QButtonGroup>
|
#include <QButtonGroup>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFormLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
|
|
@ -12,6 +14,7 @@
|
||||||
#include <QRadioButton>
|
#include <QRadioButton>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "FormKit.hpp" // makeEditForm / editLabel / capField / addDialogButtons
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
#include "ToastOverlay.hpp" // showToast:统一成功轻提示(规范 §7.7)
|
#include "ToastOverlay.hpp" // showToast:统一成功轻提示(规范 §7.7)
|
||||||
#include "panels/chart/ScatterDataOps.hpp" // buildSaveRawDataBody(纯组装,便于单测)
|
#include "panels/chart/ScatterDataOps.hpp" // buildSaveRawDataBody(纯组装,便于单测)
|
||||||
|
|
@ -20,9 +23,8 @@
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int kInversionW = 400; // 原版 inversion 另存为弹窗宽 400px
|
constexpr int kInversionW = 420; // 规范 §7.5 小号对话框宽
|
||||||
constexpr int kRawDataW = 280; // 原版 RawData「数据另存为」弹窗宽 280px
|
constexpr int kRawDataW = 420; // 同上(窄内容仍取小号标准宽,避免局促)
|
||||||
constexpr int kLabelW = 60; // 原版 .label width:60px
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
SaveAsDialog::SaveAsDialog(Mode mode, geopro::data::IDatasetCommandRepository* repo, QString dsId,
|
SaveAsDialog::SaveAsDialog(Mode mode, geopro::data::IDatasetCommandRepository* repo, QString dsId,
|
||||||
|
|
@ -30,34 +32,32 @@ SaveAsDialog::SaveAsDialog(Mode mode, geopro::data::IDatasetCommandRepository* r
|
||||||
: QDialog(parent), mode_(mode), repo_(repo), dsId_(std::move(dsId)) {
|
: QDialog(parent), mode_(mode), repo_(repo), dsId_(std::move(dsId)) {
|
||||||
setModal(true);
|
setModal(true);
|
||||||
|
|
||||||
auto* root = new QVBoxLayout(this);
|
// 规范 §7.5 对话框外壳 + §7.0.10 唯一表单实现(makeEditForm)。
|
||||||
root->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kLg,
|
auto* root = formkit::dialogRoot(this);
|
||||||
geopro::app::space::kLg, geopro::app::space::kLg);
|
auto* form = formkit::makeEditForm();
|
||||||
root->setSpacing(geopro::app::space::kMd);
|
|
||||||
|
|
||||||
if (mode_ == Mode::Inversion) {
|
if (mode_ == Mode::Inversion) {
|
||||||
// ── inversion:原版「另存为新的网格数据」400px,仅名称行 ──
|
// ── inversion:原版「另存为新的网格数据」,仅名称行 ──
|
||||||
setWindowTitle(QStringLiteral("另存为新的网格数据"));
|
setWindowTitle(QStringLiteral("另存为新的网格数据"));
|
||||||
setFixedWidth(kInversionW);
|
setFixedWidth(scaledPx(kInversionW));
|
||||||
|
|
||||||
auto* nameRow = new QHBoxLayout();
|
nameLabel_ = formkit::editLabel(QStringLiteral("名称"), this); // 原版 label「名称」
|
||||||
nameRow->setSpacing(geopro::app::space::kMd);
|
|
||||||
nameLabel_ = new QLabel(QStringLiteral("名称:"), this); // 原版 label「名称:」
|
|
||||||
nameLabel_->setMinimumWidth(kLabelW);
|
|
||||||
nameEdit_ = new QLineEdit(this);
|
nameEdit_ = new QLineEdit(this);
|
||||||
nameEdit_->setPlaceholderText(QStringLiteral("请输入名称"));
|
nameEdit_->setPlaceholderText(QStringLiteral("请输入名称"));
|
||||||
nameEdit_->setText(QStringLiteral("网格数据1")); // 原版默认值
|
nameEdit_->setText(QStringLiteral("网格数据1")); // 原版默认值
|
||||||
nameRow->addWidget(nameLabel_);
|
formkit::capField(nameEdit_);
|
||||||
nameRow->addWidget(nameEdit_, 1);
|
form->addRow(nameLabel_, nameEdit_);
|
||||||
root->addLayout(nameRow);
|
root->addLayout(form);
|
||||||
} else {
|
} else {
|
||||||
// ── RawData(measurement):新增/覆盖 + 名称(对照原版「数据另存为」280px)──
|
// ── RawData(measurement):新增/覆盖 + 名称(对照原版「数据另存为」)──
|
||||||
setWindowTitle(QStringLiteral("数据另存为"));
|
setWindowTitle(QStringLiteral("数据另存为"));
|
||||||
setFixedWidth(kRawDataW);
|
setFixedWidth(scaledPx(kRawDataW));
|
||||||
|
|
||||||
auto* opLay = new QHBoxLayout();
|
auto* opWrap = new QWidget(this);
|
||||||
auto* rbNew = new QRadioButton(QStringLiteral("新增"), this);
|
auto* opLay = new QHBoxLayout(opWrap);
|
||||||
auto* rbOverwrite = new QRadioButton(QStringLiteral("覆盖"), this);
|
opLay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
auto* rbNew = new QRadioButton(QStringLiteral("新增"), opWrap);
|
||||||
|
auto* rbOverwrite = new QRadioButton(QStringLiteral("覆盖"), opWrap);
|
||||||
opGroup_ = new QButtonGroup(this);
|
opGroup_ = new QButtonGroup(this);
|
||||||
opGroup_->addButton(rbNew, 1);
|
opGroup_->addButton(rbNew, 1);
|
||||||
opGroup_->addButton(rbOverwrite, 0);
|
opGroup_->addButton(rbOverwrite, 0);
|
||||||
|
|
@ -65,16 +65,13 @@ SaveAsDialog::SaveAsDialog(Mode mode, geopro::data::IDatasetCommandRepository* r
|
||||||
opLay->addWidget(rbNew);
|
opLay->addWidget(rbNew);
|
||||||
opLay->addWidget(rbOverwrite);
|
opLay->addWidget(rbOverwrite);
|
||||||
opLay->addStretch();
|
opLay->addStretch();
|
||||||
root->addLayout(opLay);
|
form->addRow(formkit::editLabel(QStringLiteral("操作"), this), opWrap);
|
||||||
|
|
||||||
auto* nameRow = new QHBoxLayout();
|
nameLabel_ = formkit::editLabel(QStringLiteral("数据名称"), this);
|
||||||
nameRow->setSpacing(geopro::app::space::kMd);
|
|
||||||
nameLabel_ = new QLabel(QStringLiteral("数据名称"), this);
|
|
||||||
nameLabel_->setMinimumWidth(kLabelW);
|
|
||||||
nameEdit_ = new QLineEdit(this);
|
nameEdit_ = new QLineEdit(this);
|
||||||
nameRow->addWidget(nameLabel_);
|
formkit::capField(nameEdit_);
|
||||||
nameRow->addWidget(nameEdit_, 1);
|
form->addRow(nameLabel_, nameEdit_);
|
||||||
root->addLayout(nameRow);
|
root->addLayout(form);
|
||||||
|
|
||||||
// 切到覆盖隐藏名称框,切回新增显示。
|
// 切到覆盖隐藏名称框,切回新增显示。
|
||||||
connect(opGroup_, QOverload<int>::of(&QButtonGroup::idClicked), this, [this](int id) {
|
connect(opGroup_, QOverload<int>::of(&QButtonGroup::idClicked), this, [this](int id) {
|
||||||
|
|
@ -84,18 +81,10 @@ SaveAsDialog::SaveAsDialog(Mode mode, geopro::data::IDatasetCommandRepository* r
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 底部按钮:原版右对齐,确认(主,左)/取消(右)。
|
// 规范 §7.5 底部操作栏:右对齐 取消(次) + 确认(主);确认需异步保存成功才关闭。
|
||||||
auto* btnLay = new QHBoxLayout();
|
auto* box = formkit::addDialogButtons(root, this, QStringLiteral("确认"), QStringLiteral("取消"));
|
||||||
btnLay->setSpacing(geopro::app::space::kMd);
|
okBtn_ = box->button(QDialogButtonBox::Ok);
|
||||||
btnLay->addStretch();
|
QObject::disconnect(box, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
okBtn_ = new QPushButton(QStringLiteral("确认"), this);
|
|
||||||
okBtn_->setDefault(true);
|
|
||||||
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
|
|
||||||
btnLay->addWidget(okBtn_);
|
|
||||||
btnLay->addWidget(cancelBtn);
|
|
||||||
root->addLayout(btnLay);
|
|
||||||
|
|
||||||
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
|
|
||||||
connect(okBtn_, &QPushButton::clicked, this, &SaveAsDialog::onConfirm);
|
connect(okBtn_, &QPushButton::clicked, this, &SaveAsDialog::onConfirm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
#include <QSignalBlocker>
|
#include <QSignalBlocker>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "FormKit.hpp" // makeEditForm / editLabel / capField
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
#include "ToastOverlay.hpp" // showToast:成功轻提示
|
#include "ToastOverlay.hpp" // showToast:成功轻提示
|
||||||
#include "panels/chart/RangeSlider.hpp"
|
#include "panels/chart/RangeSlider.hpp"
|
||||||
|
|
@ -97,16 +98,16 @@ ScatterFilterDialog::ScatterFilterDialog(geopro::data::IDatasetCommandRepository
|
||||||
currentPtsLbl_->setStyleSheet(QStringLiteral("color:%1;").arg(kHighlight)); // 橙色高亮
|
currentPtsLbl_->setStyleSheet(QStringLiteral("color:%1;").arg(kHighlight)); // 橙色高亮
|
||||||
infoLay->addLayout(statForm);
|
infoLay->addLayout(statForm);
|
||||||
|
|
||||||
// 最大值在上、最小值在下(对照原版输入框顺序)。
|
// 最大值在上、最小值在下(对照原版输入框顺序)。可编辑表单走 §7.0.10 唯一实现。
|
||||||
auto* inputForm = new QFormLayout();
|
auto* inputForm = formkit::makeEditForm();
|
||||||
maxSpin_ = new QDoubleSpinBox(this);
|
maxSpin_ = new QDoubleSpinBox(this);
|
||||||
maxSpin_->setRange(-kSpinRange, kSpinRange);
|
maxSpin_->setRange(-kSpinRange, kSpinRange);
|
||||||
maxSpin_->setDecimals(2);
|
maxSpin_->setDecimals(2);
|
||||||
minSpin_ = new QDoubleSpinBox(this);
|
minSpin_ = new QDoubleSpinBox(this);
|
||||||
minSpin_->setRange(-kSpinRange, kSpinRange);
|
minSpin_->setRange(-kSpinRange, kSpinRange);
|
||||||
minSpin_->setDecimals(2);
|
minSpin_->setDecimals(2);
|
||||||
inputForm->addRow(new QLabel(QStringLiteral("最大值:")), maxSpin_);
|
inputForm->addRow(formkit::editLabel(QStringLiteral("最大值"), this), maxSpin_);
|
||||||
inputForm->addRow(new QLabel(QStringLiteral("最小值:")), minSpin_);
|
inputForm->addRow(formkit::editLabel(QStringLiteral("最小值"), this), minSpin_);
|
||||||
infoLay->addLayout(inputForm);
|
infoLay->addLayout(inputForm);
|
||||||
|
|
||||||
// 计算分布 / 重置(信息区中部,对照原版 .filter-actions)。
|
// 计算分布 / 重置(信息区中部,对照原版 .filter-actions)。
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
|
||||||
#include "EmptyAwareComboBox.hpp"
|
#include "EmptyAwareComboBox.hpp"
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFormLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
|
|
@ -16,7 +17,7 @@
|
||||||
#include <QStackedWidget>
|
#include <QStackedWidget>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "FormKit.hpp" // formkit::comboBox(空态感知下拉)
|
#include "FormKit.hpp" // formkit::comboBox / makeEditForm / editLabel / capField / addDialogButtons
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
#include "panels/chart/InversionProcessOps.hpp" // buildWhitenBody
|
#include "panels/chart/InversionProcessOps.hpp" // buildWhitenBody
|
||||||
#include "repo/IDatasetCommandRepository.hpp"
|
#include "repo/IDatasetCommandRepository.hpp"
|
||||||
|
|
@ -24,26 +25,12 @@
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int kDialogW = 550; // 原版弹窗宽 550px
|
constexpr int kDialogW = 560; // 规范 §7.5 中号对话框宽
|
||||||
constexpr int kLabelMinW = 120; // 原版 .field-label min-width:120px 右对齐
|
|
||||||
constexpr double kCtrlRatio = 0.6; // 原版控件宽 60%
|
|
||||||
|
|
||||||
// 原版 .field-label:定宽右对齐标签。
|
// 把「标签 + 控件」按 §7.0 度量加入表单(右对齐定宽标签列 + 字段宽上限)。
|
||||||
QLabel* fieldLabel(const QString& text, QWidget* parent) {
|
void addFormRow(QFormLayout* form, const QString& label, QWidget* ctrl, QWidget* parent) {
|
||||||
auto* lbl = new QLabel(text, parent);
|
formkit::capField(ctrl);
|
||||||
lbl->setMinimumWidth(kLabelMinW);
|
form->addRow(formkit::editLabel(label, parent), ctrl);
|
||||||
lbl->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
|
||||||
return lbl;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 原版 .option-item:flex 行(标签右对齐 + 控件占 60%)。控件外裹一层以 60/40 拉伸。
|
|
||||||
QHBoxLayout* optionRow(const QString& label, QWidget* ctrl, QWidget* parent) {
|
|
||||||
auto* row = new QHBoxLayout();
|
|
||||||
row->setSpacing(geopro::app::space::kLg);
|
|
||||||
row->addWidget(fieldLabel(label, parent));
|
|
||||||
row->addWidget(ctrl, 6); // 控件 60%
|
|
||||||
row->addStretch(4); // 余 40% 留白
|
|
||||||
return row;
|
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
@ -56,30 +43,30 @@ WhiteningDialog::WhiteningDialog(geopro::data::IDatasetCommandRepository* repo,
|
||||||
tmObjectId_(std::move(tmObjectId)) {
|
tmObjectId_(std::move(tmObjectId)) {
|
||||||
setWindowTitle(QStringLiteral("白化配置")); // 原版 whiteningSetting
|
setWindowTitle(QStringLiteral("白化配置")); // 原版 whiteningSetting
|
||||||
setModal(true);
|
setModal(true);
|
||||||
setFixedWidth(kDialogW);
|
setFixedWidth(scaledPx(kDialogW));
|
||||||
|
|
||||||
auto* root = new QVBoxLayout(this);
|
// 规范 §7.5 对话框外壳:统一边距 + 行距(dialogRoot)。
|
||||||
root->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kLg,
|
auto* root = formkit::dialogRoot(this);
|
||||||
geopro::app::space::kLg, geopro::app::space::kLg);
|
|
||||||
root->setSpacing(geopro::app::space::kMd);
|
|
||||||
|
|
||||||
// 白化方式下拉(原版 3 项,数值对照 whiteningMethod 1/2/3)。
|
// 白化方式下拉(原版 3 项,数值对照 whiteningMethod 1/2/3)。
|
||||||
methodCombo_ = new EmptyAwareComboBox(this);
|
methodCombo_ = new EmptyAwareComboBox(this);
|
||||||
methodCombo_->addItem(QStringLiteral("数据边界自动白化"), 1);
|
methodCombo_->addItem(QStringLiteral("数据边界自动白化"), 1);
|
||||||
methodCombo_->addItem(QStringLiteral("白化文件"), 2);
|
methodCombo_->addItem(QStringLiteral("白化文件"), 2);
|
||||||
methodCombo_->addItem(QStringLiteral("模型白化"), 3);
|
methodCombo_->addItem(QStringLiteral("模型白化"), 3);
|
||||||
root->addLayout(optionRow(QStringLiteral("白化方式"), methodCombo_, this));
|
// §7.0.10 唯一实现:makeEditForm + editLabel(右对齐定宽标签列)。
|
||||||
|
auto* methodForm = formkit::makeEditForm();
|
||||||
|
addFormRow(methodForm, QStringLiteral("白化方式"), methodCombo_, this);
|
||||||
|
root->addLayout(methodForm);
|
||||||
|
|
||||||
stack_ = new QStackedWidget(this);
|
stack_ = new QStackedWidget(this);
|
||||||
root->addWidget(stack_);
|
root->addWidget(stack_);
|
||||||
|
|
||||||
// ── 方式 1:数据边界自动白化(边界扩展文本框 + 内/外白化单选)────────────
|
// ── 方式 1:数据边界自动白化(边界扩展文本框 + 内/外白化单选)────────────
|
||||||
auto* page1 = new QWidget(this);
|
auto* page1 = new QWidget(this);
|
||||||
auto* p1 = new QVBoxLayout(page1);
|
auto* p1 = formkit::makeEditForm();
|
||||||
p1->setContentsMargins(0, 0, 0, 0);
|
page1->setLayout(p1);
|
||||||
p1->setSpacing(geopro::app::space::kMd);
|
|
||||||
extension_ = new QLineEdit(QStringLiteral("0"), page1); // 原版 AInput,默认 "0"
|
extension_ = new QLineEdit(QStringLiteral("0"), page1); // 原版 AInput,默认 "0"
|
||||||
p1->addLayout(optionRow(QStringLiteral("白化边界扩展"), extension_, page1));
|
addFormRow(p1, QStringLiteral("白化边界扩展"), extension_, page1);
|
||||||
auto* typeWrap = new QWidget(page1);
|
auto* typeWrap = new QWidget(page1);
|
||||||
auto* typeRow = new QHBoxLayout(typeWrap);
|
auto* typeRow = new QHBoxLayout(typeWrap);
|
||||||
typeRow->setContentsMargins(0, 0, 0, 0);
|
typeRow->setContentsMargins(0, 0, 0, 0);
|
||||||
|
|
@ -92,22 +79,22 @@ WhiteningDialog::WhiteningDialog(geopro::data::IDatasetCommandRepository* repo,
|
||||||
typeRow->addWidget(rbOuter);
|
typeRow->addWidget(rbOuter);
|
||||||
typeRow->addWidget(rbInner);
|
typeRow->addWidget(rbInner);
|
||||||
typeRow->addStretch();
|
typeRow->addStretch();
|
||||||
p1->addLayout(optionRow(QStringLiteral("白化"), typeWrap, page1));
|
p1->addRow(formkit::editLabel(QStringLiteral("白化"), page1), typeWrap);
|
||||||
stack_->addWidget(page1);
|
stack_->addWidget(page1);
|
||||||
|
|
||||||
// ── 方式 2:白化文件(选文件)──────────────────────────────────────
|
// ── 方式 2:白化文件(选文件)──────────────────────────────────────
|
||||||
auto* page2 = new QWidget(this);
|
auto* page2 = new QWidget(this);
|
||||||
auto* p2 = new QVBoxLayout(page2);
|
auto* p2 = formkit::makeEditForm();
|
||||||
p2->setContentsMargins(0, 0, 0, 0);
|
page2->setLayout(p2);
|
||||||
// 空态感知下拉:白化文件异步加载(listWhitenedData),未选显占位、无文件弹「暂无数据」。
|
// 空态感知下拉:白化文件异步加载(listWhitenedData),未选显占位、无文件弹「暂无数据」。
|
||||||
fileCombo_ = formkit::comboBox(QStringLiteral("请选择白化文件"), page2);
|
fileCombo_ = formkit::comboBox(QStringLiteral("请选择白化文件"), page2);
|
||||||
p2->addLayout(optionRow(QStringLiteral("选择白化文件"), fileCombo_, page2));
|
addFormRow(p2, QStringLiteral("选择白化文件"), fileCombo_, page2);
|
||||||
stack_->addWidget(page2);
|
stack_->addWidget(page2);
|
||||||
|
|
||||||
// ── 方式 3:模型白化(梯形/矩形)───────────────────────────────────
|
// ── 方式 3:模型白化(梯形/矩形)───────────────────────────────────
|
||||||
auto* page3 = new QWidget(this);
|
auto* page3 = new QWidget(this);
|
||||||
auto* p3 = new QVBoxLayout(page3);
|
auto* p3 = formkit::makeEditForm();
|
||||||
p3->setContentsMargins(0, 0, 0, 0);
|
page3->setLayout(p3);
|
||||||
auto* subWrap = new QWidget(page3);
|
auto* subWrap = new QWidget(page3);
|
||||||
auto* subRow = new QHBoxLayout(subWrap);
|
auto* subRow = new QHBoxLayout(subWrap);
|
||||||
subRow->setContentsMargins(0, 0, 0, 0);
|
subRow->setContentsMargins(0, 0, 0, 0);
|
||||||
|
|
@ -120,23 +107,14 @@ WhiteningDialog::WhiteningDialog(geopro::data::IDatasetCommandRepository* repo,
|
||||||
subRow->addWidget(rbTrap);
|
subRow->addWidget(rbTrap);
|
||||||
subRow->addWidget(rbRect);
|
subRow->addWidget(rbRect);
|
||||||
subRow->addStretch();
|
subRow->addStretch();
|
||||||
p3->addLayout(optionRow(QStringLiteral("白化"), subWrap, page3));
|
p3->addRow(formkit::editLabel(QStringLiteral("白化"), page3), subWrap);
|
||||||
stack_->addWidget(page3);
|
stack_->addWidget(page3);
|
||||||
|
|
||||||
Q_UNUSED(kCtrlRatio);
|
// 规范 §7.5 底部操作栏:右对齐,取消(次) 左 + 确认(主) 右。
|
||||||
|
// 确认需先异步 whitenData 成功才关闭 → 断开 Ok 默认 accept,改接 onConfirm。
|
||||||
// 底部按钮:原版 justify-content:space-between,确认(主,左)/取消(右) 各 45%。
|
auto* box = formkit::addDialogButtons(root, this, QStringLiteral("确认"), QStringLiteral("取消"));
|
||||||
auto* btnLay = new QHBoxLayout();
|
okBtn_ = box->button(QDialogButtonBox::Ok);
|
||||||
btnLay->setSpacing(geopro::app::space::kMd);
|
QObject::disconnect(box, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
okBtn_ = new QPushButton(QStringLiteral("确认"), this);
|
|
||||||
okBtn_->setDefault(true);
|
|
||||||
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
|
|
||||||
btnLay->addWidget(okBtn_, 45);
|
|
||||||
btnLay->addStretch(10);
|
|
||||||
btnLay->addWidget(cancelBtn, 45);
|
|
||||||
root->addLayout(btnLay);
|
|
||||||
|
|
||||||
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
|
|
||||||
connect(okBtn_, &QPushButton::clicked, this, &WhiteningDialog::onConfirm);
|
connect(okBtn_, &QPushButton::clicked, this, &WhiteningDialog::onConfirm);
|
||||||
connect(methodCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
connect(methodCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||||
[this](int) { onMethodChanged(methodCombo_->currentData().toInt()); });
|
[this](int) { onMethodChanged(methodCombo_->currentData().toInt()); });
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,11 @@
|
||||||
#include <vtkPolyDataMapper.h>
|
#include <vtkPolyDataMapper.h>
|
||||||
#include <vtkProperty.h>
|
#include <vtkProperty.h>
|
||||||
#include <vtkRenderWindow.h>
|
#include <vtkRenderWindow.h>
|
||||||
|
#include <vtkRenderWindowInteractor.h>
|
||||||
|
#include <vtkInteractorStyleTrackballCamera.h>
|
||||||
|
#include <vtkCallbackCommand.h>
|
||||||
|
#include <vtkTextActor.h>
|
||||||
|
#include <vtkTextProperty.h>
|
||||||
#include <vtkRenderer.h>
|
#include <vtkRenderer.h>
|
||||||
#include <vtkShortArray.h>
|
#include <vtkShortArray.h>
|
||||||
#include <vtkSmartPointer.h>
|
#include <vtkSmartPointer.h>
|
||||||
|
|
@ -653,6 +658,98 @@ geopro::core::ColorScale makeColorScale(double vmin, double vmax) {
|
||||||
return cs;
|
return cs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 视觉调优共享构件(Task 12d ①)
|
||||||
|
// ============================================================================
|
||||||
|
//
|
||||||
|
// 结构化配色:地震/雷达体常用的「结构色阶」——深蓝(强负)→青→白(零)→黄→红(强正),
|
||||||
|
// 比单纯蓝-白-红更易拉开正负反射层次。值域用数据 vmin/vmax,无需手调控制点。
|
||||||
|
geopro::core::ColorScale makeStructuralColorScale(double vmin, double vmax) {
|
||||||
|
geopro::core::ColorScale cs;
|
||||||
|
const double span = (vmax > vmin) ? (vmax - vmin) : 1.0;
|
||||||
|
auto at = [&](double t) { return vmin + span * t; };
|
||||||
|
cs.addStop(at(0.00), geopro::core::Rgba{0, 0, 140, 255}); // 深蓝
|
||||||
|
cs.addStop(at(0.25), geopro::core::Rgba{0, 160, 220, 255}); // 青
|
||||||
|
cs.addStop(at(0.50), geopro::core::Rgba{245, 245, 245, 255}); // 白(零附近)
|
||||||
|
cs.addStop(at(0.75), geopro::core::Rgba{250, 190, 30, 255}); // 黄
|
||||||
|
cs.addStop(at(1.00), geopro::core::Rgba{170, 0, 0, 255}); // 暗红
|
||||||
|
return cs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参数化量化域传函:与 makeI16VolumeProperty 同逻辑,但 kMaxOpacity 可由 --opacity 控。
|
||||||
|
// 不透明度调高时光线提前终止,fps 近乎中性甚至更快(探针认知,报告打印前后对照证实)。
|
||||||
|
vtkSmartPointer<vtkVolumeProperty> makeTunedVolumeProperty(
|
||||||
|
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
|
||||||
|
double vminPhys, double vmaxPhys, double maxOpacity,
|
||||||
|
bool structuralOpacity = true) {
|
||||||
|
constexpr int kTransferSamples = 64;
|
||||||
|
if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0;
|
||||||
|
const double qminD = static_cast<double>(q.toQ(vminPhys));
|
||||||
|
const double qmaxD = static_cast<double>(q.toQ(vmaxPhys));
|
||||||
|
|
||||||
|
vtkNew<vtkColorTransferFunction> color;
|
||||||
|
for (int t = 0; t < kTransferSamples; ++t) {
|
||||||
|
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
|
||||||
|
const auto qvLevel = static_cast<std::int16_t>(std::lround(qd));
|
||||||
|
const double phys = q.toPhys(qvLevel);
|
||||||
|
const auto c = cs.colorAt(phys);
|
||||||
|
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不透明度:
|
||||||
|
// - 原始(structuralOpacity=false):线性单斜坡 [qmin,qmax]→[0,maxOpacity],
|
||||||
|
// 与 VoxelActor 默认一致,作调优前对照基线。
|
||||||
|
// - 调优(structuralOpacity=true):双端斜坡。GPR/地震体值多集中在零附近(背景),
|
||||||
|
// 强反射在正负两端;线性单斜坡会让占多数的近零背景填满体、遮住结构。改为
|
||||||
|
// 「中段(零附近)透明 + 正负两端不透明」——抑制背景、凸显强反射层,截面结构才看得出。
|
||||||
|
vtkNew<vtkPiecewiseFunction> opacity;
|
||||||
|
opacity->AddPoint(
|
||||||
|
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
|
||||||
|
if (structuralOpacity) {
|
||||||
|
const double qmid = 0.5 * (qminD + qmaxD);
|
||||||
|
const double half = 0.5 * (qmaxD - qminD);
|
||||||
|
opacity->AddPoint(qminD, maxOpacity); // 强负反射:不透明
|
||||||
|
opacity->AddPoint(qmid - 0.30 * half, 0.0); // 近零背景:透明
|
||||||
|
opacity->AddPoint(qmid + 0.30 * half, 0.0);
|
||||||
|
opacity->AddPoint(qmaxD, maxOpacity); // 强正反射:不透明
|
||||||
|
} else {
|
||||||
|
opacity->AddPoint(qminD, 0.0);
|
||||||
|
opacity->AddPoint(qmaxD, maxOpacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
|
||||||
|
prop->SetColor(color);
|
||||||
|
prop->SetScalarOpacity(opacity);
|
||||||
|
prop->SetInterpolationTypeToLinear();
|
||||||
|
prop->ShadeOff();
|
||||||
|
return prop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 由预构建 VTK_SHORT 图像建一个「视觉调优」体:自定义不透明度 + 垂向夸张。
|
||||||
|
// 垂向夸张用 vtkVolume::SetScale(1, exagg, exagg) 缩放跨通道(Y)与深度(Z)两薄轴,
|
||||||
|
// 不改图像数据;体物理极扁(X≈2.2km vs Y≈1.5m/Z≈8m),放大薄轴截面结构才看得出。
|
||||||
|
vtkSmartPointer<vtkVolume> buildTunedVolume(vtkImageData* shortImg,
|
||||||
|
const geopro::core::Quant& q,
|
||||||
|
const geopro::core::ColorScale& cs,
|
||||||
|
double vminPhys, double vmaxPhys,
|
||||||
|
double maxOpacity, double exagg,
|
||||||
|
bool structuralOpacity = true) {
|
||||||
|
vtkNew<vtkSmartVolumeMapper> mapper;
|
||||||
|
mapper->SetInputData(shortImg);
|
||||||
|
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||||||
|
mapper->SetAutoAdjustSampleDistances(0);
|
||||||
|
mapper->SetInteractiveAdjustSampleDistances(0);
|
||||||
|
|
||||||
|
auto prop = makeTunedVolumeProperty(q, cs, vminPhys, vmaxPhys, maxOpacity,
|
||||||
|
structuralOpacity);
|
||||||
|
|
||||||
|
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||||||
|
volume->SetMapper(mapper);
|
||||||
|
volume->SetProperty(prop);
|
||||||
|
volume->SetScale(1.0, exagg, exagg); // 垂向夸张:放大 Y/Z 薄轴
|
||||||
|
return volume;
|
||||||
|
}
|
||||||
|
|
||||||
int cmdRenderB(int argc, char** argv) {
|
int cmdRenderB(int argc, char** argv) {
|
||||||
const Args a = parseArgs(argc, argv, 2);
|
const Args a = parseArgs(argc, argv, 2);
|
||||||
if (a.positional.empty()) {
|
if (a.positional.empty()) {
|
||||||
|
|
@ -1761,6 +1858,557 @@ int cmdRenderLOD(int argc, char** argv) {
|
||||||
return valid ? 0 : 1;
|
return valid ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ① 视觉调优:出一帧能看结构的图 + 调优前后 fps 对照(Task 12d)
|
||||||
|
// ============================================================================
|
||||||
|
//
|
||||||
|
// 在【真实金字塔 store】上对局部段(level0 一段 brick 列)与粗层概览(level2 整卷)
|
||||||
|
// 各跑两遍体绘制 fps:调优前(默认色阶 0.15 不透明度 无夸张) vs 调优后(结构色阶 +
|
||||||
|
// --opacity + --exagg 垂向夸张),离屏存 lod-tuned-local.png / lod-tuned-overview.png,
|
||||||
|
// 并打印前后 fps 对照——证实「视觉调优对 fps 近乎中性」这一探针认知。双闸防假帧率。
|
||||||
|
int cmdTune(int argc, char** argv) {
|
||||||
|
const Args a = parseArgs(argc, argv, 2);
|
||||||
|
if (a.positional.empty()) {
|
||||||
|
std::cerr << "用法: gpr_poc tune <storeDir> [--opacity 0.5] [--exagg 8] "
|
||||||
|
"[--frames 120] [--localBricks 4]\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
const std::string dir = a.positional[0];
|
||||||
|
const double opacity = std::stod(a.get("opacity", "0.5"));
|
||||||
|
const double exagg = std::stod(a.get("exagg", "8"));
|
||||||
|
const int frames = std::stoi(a.get("frames", "120"));
|
||||||
|
const int localBricks = std::stoi(a.get("localBricks", "4"));
|
||||||
|
std::cout << "[tune] storeDir=" << dir << " opacity=" << opacity
|
||||||
|
<< " exagg=" << exagg << " frames=" << frames << "\n";
|
||||||
|
|
||||||
|
std::cout << "[tune] 离屏闸门复检...\n";
|
||||||
|
if (cmdOffscreenSmoke() != 0) {
|
||||||
|
std::cout << "[tune] 闸门失败,中止。\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
geopro::data::ChunkedVolumeStore store(dir);
|
||||||
|
const geopro::data::StoreMeta& m = store.meta();
|
||||||
|
const int totLevels = store.levels();
|
||||||
|
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||||||
|
|
||||||
|
const geopro::core::ColorScale csPlain = makeColorScale(vmin, vmax);
|
||||||
|
const geopro::core::ColorScale csTuned = makeStructuralColorScale(vmin, vmax);
|
||||||
|
|
||||||
|
const fs::path shotDir =
|
||||||
|
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
|
||||||
|
fs::create_directories(shotDir);
|
||||||
|
const int winW = 1024, winH = 768;
|
||||||
|
|
||||||
|
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||||||
|
vtkOutputWindow::SetInstance(capWin);
|
||||||
|
|
||||||
|
// ---- 局部段:level0 一段 brick 列(沿线中段)----
|
||||||
|
const int totBx = store.bricksX(0);
|
||||||
|
const int localBx = std::min(localBricks, totBx);
|
||||||
|
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
|
||||||
|
vtkSmartPointer<vtkImageData> locImg =
|
||||||
|
buildLocalLevel0Image(store, m, bx0, localBx);
|
||||||
|
int locDims[3];
|
||||||
|
locImg->GetDimensions(locDims);
|
||||||
|
|
||||||
|
// 调优前局部 fps(默认色阶 0.15 无夸张)。
|
||||||
|
auto rwA = makeOffscreenWindow(winW, winH);
|
||||||
|
vtkNew<vtkRenderer> renA;
|
||||||
|
renA->SetBackground(0.0, 0.0, 0.0);
|
||||||
|
rwA->AddRenderer(renA);
|
||||||
|
vtkSmartPointer<vtkVolume> volA =
|
||||||
|
buildTunedVolume(locImg.Get(), m.quant, csPlain, vmin, vmax, 0.15, 1.0,
|
||||||
|
/*structuralOpacity=*/false); // 原始线性单斜坡基线
|
||||||
|
renA->AddVolume(volA);
|
||||||
|
const double locFpsBefore = benchVolumeFps(rwA.Get(), renA, frames);
|
||||||
|
|
||||||
|
// 调优后局部 fps(结构色阶 + opacity + exagg)。
|
||||||
|
auto rwB = makeOffscreenWindow(winW, winH);
|
||||||
|
vtkNew<vtkRenderer> renB;
|
||||||
|
renB->SetBackground(0.04, 0.04, 0.08); // 深蓝灰背景,衬托体
|
||||||
|
rwB->AddRenderer(renB);
|
||||||
|
vtkSmartPointer<vtkVolume> volB =
|
||||||
|
buildTunedVolume(locImg.Get(), m.quant, csTuned, vmin, vmax, opacity,
|
||||||
|
exagg);
|
||||||
|
renB->AddVolume(volB);
|
||||||
|
const double locFpsAfter = benchVolumeFps(rwB.Get(), renB, frames);
|
||||||
|
// 调优后取景:夸张后块更"立体",斜俯视呈现截面层次;Zoom 拉近填满画面。
|
||||||
|
renB->ResetCamera();
|
||||||
|
renB->GetActiveCamera()->Elevation(28.0);
|
||||||
|
renB->GetActiveCamera()->Azimuth(30.0);
|
||||||
|
renB->GetActiveCamera()->Zoom(1.7);
|
||||||
|
renB->ResetCameraClippingRange();
|
||||||
|
rwB->Render();
|
||||||
|
const vtkIdType locNonBlack = countNonBlackPixels(rwB.Get(), winW, winH);
|
||||||
|
savePng(rwB.Get(), (shotDir / "lod-tuned-local.png").string());
|
||||||
|
|
||||||
|
// ---- 概览:level2 整卷(接受它就是细带)----
|
||||||
|
const int ovLevel = std::min(2, totLevels - 1);
|
||||||
|
vtkSmartPointer<vtkImageData> ovImg = buildLevelImage(store, ovLevel, m);
|
||||||
|
auto rwO = makeOffscreenWindow(winW, winH);
|
||||||
|
vtkNew<vtkRenderer> renO;
|
||||||
|
renO->SetBackground(0.04, 0.04, 0.08);
|
||||||
|
rwO->AddRenderer(renO);
|
||||||
|
vtkSmartPointer<vtkVolume> volO =
|
||||||
|
buildTunedVolume(ovImg.Get(), m.quant, csTuned, vmin, vmax, opacity,
|
||||||
|
exagg);
|
||||||
|
renO->AddVolume(volO);
|
||||||
|
const double ovFpsAfter = benchVolumeFps(rwO.Get(), renO, frames);
|
||||||
|
renO->ResetCamera();
|
||||||
|
renO->GetActiveCamera()->Elevation(50.0);
|
||||||
|
renO->GetActiveCamera()->Azimuth(20.0);
|
||||||
|
renO->ResetCameraClippingRange();
|
||||||
|
rwO->Render();
|
||||||
|
const vtkIdType ovNonBlack = countNonBlackPixels(rwO.Get(), winW, winH);
|
||||||
|
savePng(rwO.Get(), (shotDir / "lod-tuned-overview.png").string());
|
||||||
|
|
||||||
|
const bool textureErr = capWin->textureError();
|
||||||
|
vtkOutputWindow::SetInstance(nullptr);
|
||||||
|
const bool valid =
|
||||||
|
!textureErr && locNonBlack > 0 && ovNonBlack > 0;
|
||||||
|
|
||||||
|
const double dropPct =
|
||||||
|
locFpsBefore > 0 ? (locFpsBefore - locFpsAfter) / locFpsBefore * 100.0
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
std::cout << "\n=== tune 视觉调优指标 ===\n";
|
||||||
|
std::cout << "局部段维度 : " << locDims[0] << "x" << locDims[1] << "x"
|
||||||
|
<< locDims[2] << " (level0)\n";
|
||||||
|
std::cout << "调优前局部 fps : "
|
||||||
|
<< (valid ? std::to_string(locFpsBefore) : "INVALID")
|
||||||
|
<< " (默认蓝白红, 不透明度 0.15, 无夸张)\n";
|
||||||
|
std::cout << "调优后局部 fps : "
|
||||||
|
<< (valid ? std::to_string(locFpsAfter) : "INVALID")
|
||||||
|
<< " (结构色阶, 不透明度 " << opacity << ", 夸张 " << exagg
|
||||||
|
<< "x)\n";
|
||||||
|
std::cout << "fps 变化 : " << dropPct
|
||||||
|
<< "% (正=变慢/负=变快; 探针预期近乎中性)\n";
|
||||||
|
std::cout << "调优后概览 fps : "
|
||||||
|
<< (valid ? std::to_string(ovFpsAfter) : "INVALID") << " (level"
|
||||||
|
<< ovLevel << ")\n";
|
||||||
|
std::cout << "双闸 : 纹理错=" << (textureErr ? "是" : "否")
|
||||||
|
<< " 局部非空=" << locNonBlack << " 概览非空=" << ovNonBlack
|
||||||
|
<< " → " << (valid ? "可信" : "INVALID") << "\n";
|
||||||
|
std::cout << "截图 : " << shotDir.string()
|
||||||
|
<< " (lod-tuned-local.png / lod-tuned-overview.png)\n";
|
||||||
|
|
||||||
|
writeMetricLine(
|
||||||
|
"tune,dir=" + dir + ",opacity=" + std::to_string(opacity) +
|
||||||
|
",exagg=" + std::to_string(exagg) +
|
||||||
|
",locDims=" + std::to_string(locDims[0]) + "x" +
|
||||||
|
std::to_string(locDims[1]) + "x" + std::to_string(locDims[2]) +
|
||||||
|
",locFpsBefore=" + (valid ? std::to_string(locFpsBefore) : "INVALID") +
|
||||||
|
",locFpsAfter=" + (valid ? std::to_string(locFpsAfter) : "INVALID") +
|
||||||
|
",dropPct=" + std::to_string(dropPct) +
|
||||||
|
",ovFpsAfter=" + (valid ? std::to_string(ovFpsAfter) : "INVALID") +
|
||||||
|
",locNonBlack=" + std::to_string(locNonBlack) +
|
||||||
|
",ovNonBlack=" + std::to_string(ovNonBlack) +
|
||||||
|
",valid=" + std::to_string(valid ? 1 : 0));
|
||||||
|
return valid ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ② fps 预算:递增全分辨率(level0)窗口找「每帧体素预算」(Task 12d)
|
||||||
|
// ============================================================================
|
||||||
|
//
|
||||||
|
// 对递增的 level0 brick 列段(4,16,64,128,256 brick,可 --bricks 覆盖)各重组成
|
||||||
|
// 局部整卷 image 跑体绘制 fps,输出表 brick数/体素数/fps,找出 fps 跌破 30 的体素
|
||||||
|
// 阈值 = production LOD 每帧渲染的全分辨率块数上限。双闸防假帧率。
|
||||||
|
int cmdFpsBudget(int argc, char** argv) {
|
||||||
|
const Args a = parseArgs(argc, argv, 2);
|
||||||
|
if (a.positional.empty()) {
|
||||||
|
std::cerr << "用法: gpr_poc fps-budget <storeDir> [--frames 90] "
|
||||||
|
"[--bricks 4,16,64,128,256]\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
const std::string dir = a.positional[0];
|
||||||
|
const int frames = std::stoi(a.get("frames", "90"));
|
||||||
|
const double opacity = std::stod(a.get("opacity", "0.5"));
|
||||||
|
const double exagg = std::stod(a.get("exagg", "8"));
|
||||||
|
|
||||||
|
// 解析 brick 段列表(逗号分隔)。
|
||||||
|
std::vector<int> brickSteps;
|
||||||
|
{
|
||||||
|
const std::string raw = a.get("bricks", "4,16,64,128,256");
|
||||||
|
std::string cur;
|
||||||
|
for (char ch : raw) {
|
||||||
|
if (ch == ',') {
|
||||||
|
if (!cur.empty()) brickSteps.push_back(std::stoi(cur));
|
||||||
|
cur.clear();
|
||||||
|
} else {
|
||||||
|
cur.push_back(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!cur.empty()) brickSteps.push_back(std::stoi(cur));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "[fps-budget] storeDir=" << dir << " frames=" << frames << "\n";
|
||||||
|
std::cout << "[fps-budget] 离屏闸门复检...\n";
|
||||||
|
if (cmdOffscreenSmoke() != 0) {
|
||||||
|
std::cout << "[fps-budget] 闸门失败,中止,不产出 fps。\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
geopro::data::ChunkedVolumeStore store(dir);
|
||||||
|
const geopro::data::StoreMeta& m = store.meta();
|
||||||
|
const int totBx = store.bricksX(0);
|
||||||
|
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||||||
|
const geopro::core::ColorScale cs = makeStructuralColorScale(vmin, vmax);
|
||||||
|
|
||||||
|
std::cout << "[fps-budget] level0=" << m.nx << "x" << m.ny << "x" << m.nz
|
||||||
|
<< " 总 brick列=" << totBx << " brick=" << m.brick << "\n";
|
||||||
|
|
||||||
|
struct Row {
|
||||||
|
int bricks;
|
||||||
|
long long voxels;
|
||||||
|
double fps;
|
||||||
|
bool valid;
|
||||||
|
};
|
||||||
|
std::vector<Row> rows;
|
||||||
|
|
||||||
|
constexpr double kTargetFps = 30.0;
|
||||||
|
long long budgetVoxels = -1; // fps 跌破 30 前的最大体素数
|
||||||
|
int budgetBricks = -1;
|
||||||
|
long long firstBelowVoxels = -1;
|
||||||
|
int firstBelowBricks = -1;
|
||||||
|
|
||||||
|
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||||||
|
vtkOutputWindow::SetInstance(capWin);
|
||||||
|
|
||||||
|
for (int nb : brickSteps) {
|
||||||
|
const int localBx = std::min(nb, totBx);
|
||||||
|
if (localBx <= 0) continue;
|
||||||
|
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
|
||||||
|
vtkSmartPointer<vtkImageData> img =
|
||||||
|
buildLocalLevel0Image(store, m, bx0, localBx);
|
||||||
|
int d[3];
|
||||||
|
img->GetDimensions(d);
|
||||||
|
const long long voxels =
|
||||||
|
static_cast<long long>(d[0]) * d[1] * d[2];
|
||||||
|
|
||||||
|
auto rw = makeOffscreenWindow(1024, 768);
|
||||||
|
vtkNew<vtkRenderer> ren;
|
||||||
|
ren->SetBackground(0.0, 0.0, 0.0);
|
||||||
|
rw->AddRenderer(ren);
|
||||||
|
vtkSmartPointer<vtkVolume> vol =
|
||||||
|
buildTunedVolume(img.Get(), m.quant, cs, vmin, vmax, opacity, exagg);
|
||||||
|
ren->AddVolume(vol);
|
||||||
|
|
||||||
|
const double fps = benchVolumeFps(rw.Get(), ren, frames);
|
||||||
|
// 双闸:纹理无错 + 该段渲出非空像素。
|
||||||
|
ren->ResetCamera();
|
||||||
|
rw->Render();
|
||||||
|
const vtkIdType nonBlack = countNonBlackPixels(rw.Get(), 1024, 768);
|
||||||
|
const bool valid = !capWin->textureError() && nonBlack > 0;
|
||||||
|
|
||||||
|
rows.push_back({localBx, voxels, fps, valid});
|
||||||
|
std::cout << "[fps-budget] brick=" << localBx << " (" << d[0] << "x" << d[1]
|
||||||
|
<< "x" << d[2] << ") 体素=" << voxels << " fps="
|
||||||
|
<< (valid ? std::to_string(fps) : "INVALID")
|
||||||
|
<< " 非空=" << nonBlack << "\n";
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
if (fps >= kTargetFps) {
|
||||||
|
if (voxels > budgetVoxels) {
|
||||||
|
budgetVoxels = voxels;
|
||||||
|
budgetBricks = localBx;
|
||||||
|
}
|
||||||
|
} else if (firstBelowVoxels < 0) {
|
||||||
|
firstBelowVoxels = voxels;
|
||||||
|
firstBelowBricks = localBx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool textureErr = capWin->textureError();
|
||||||
|
vtkOutputWindow::SetInstance(nullptr);
|
||||||
|
const double peak = Probe::peakMemMB();
|
||||||
|
|
||||||
|
std::cout << "\n=== fps-budget 每帧体素预算表 ===\n";
|
||||||
|
std::cout << "| brick段 | 维度体素数 | 体绘制 fps | ≥30 |\n";
|
||||||
|
std::cout << "|---|---|---|---|\n";
|
||||||
|
for (const auto& r : rows) {
|
||||||
|
std::cout << "| " << r.bricks << " | " << r.voxels << " | "
|
||||||
|
<< (r.valid ? std::to_string(r.fps) : std::string("INVALID"))
|
||||||
|
<< " | " << (r.valid && r.fps >= kTargetFps ? "是" : "否")
|
||||||
|
<< " |\n";
|
||||||
|
}
|
||||||
|
std::cout << "\n每帧体素预算(fps≥30 上限) : "
|
||||||
|
<< (budgetVoxels >= 0 ? std::to_string(budgetVoxels) +
|
||||||
|
" 体素 (" + std::to_string(budgetBricks) +
|
||||||
|
" brick列)"
|
||||||
|
: std::string("未触达(所有测点均 ≥30)"))
|
||||||
|
<< "\n";
|
||||||
|
std::cout << "首个跌破 30 的窗口 : "
|
||||||
|
<< (firstBelowVoxels >= 0
|
||||||
|
? std::to_string(firstBelowVoxels) + " 体素 (" +
|
||||||
|
std::to_string(firstBelowBricks) + " brick列)"
|
||||||
|
: std::string("无(测点未跌破; 需更大 --bricks)"))
|
||||||
|
<< "\n";
|
||||||
|
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否")
|
||||||
|
<< "\n";
|
||||||
|
std::cout << "进程峰值内存(MB) : " << peak << "\n";
|
||||||
|
|
||||||
|
// 落 last-metrics + 追加写 poc-results-C.md。
|
||||||
|
for (const auto& r : rows) {
|
||||||
|
writeMetricLine(
|
||||||
|
"fps-budget,dir=" + dir + ",bricks=" + std::to_string(r.bricks) +
|
||||||
|
",voxels=" + std::to_string(r.voxels) +
|
||||||
|
",fps=" + (r.valid ? std::to_string(r.fps) : "INVALID") +
|
||||||
|
",valid=" + std::to_string(r.valid ? 1 : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const fs::path repo =
|
||||||
|
fs::path("docs") / "superpowers" / "plans" / "poc-results-C.md";
|
||||||
|
fs::create_directories(repo.parent_path());
|
||||||
|
std::ofstream rf(repo.string(), std::ios::app);
|
||||||
|
if (rf) {
|
||||||
|
rf << "\n\n# POC-C fps 预算探针结果(Task 12d ②)\n\n";
|
||||||
|
rf << "金字塔 store: " << dir << "(level0=" << m.nx << "x" << m.ny << "x"
|
||||||
|
<< m.nz << ",brick=" << m.brick << ")\n\n";
|
||||||
|
rf << "递增 level0 局部窗口(沿线中段 brick 列)体绘制 fps:\n\n";
|
||||||
|
rf << "| brick段 | 体素数 | 体绘制 fps | ≥30fps |\n|---|---|---|---|\n";
|
||||||
|
for (const auto& r : rows) {
|
||||||
|
rf << "| " << r.bricks << " | " << r.voxels << " | "
|
||||||
|
<< (r.valid ? std::to_string(r.fps) : "INVALID") << " | "
|
||||||
|
<< (r.valid && r.fps >= kTargetFps ? "是" : "否") << " |\n";
|
||||||
|
}
|
||||||
|
rf << "\n- **每帧体素预算(fps≥30 上限)**: "
|
||||||
|
<< (budgetVoxels >= 0
|
||||||
|
? std::to_string(budgetVoxels) + " 体素(" +
|
||||||
|
std::to_string(budgetBricks) + " brick 列)"
|
||||||
|
: "未触达,所有测点 ≥30fps")
|
||||||
|
<< "\n";
|
||||||
|
rf << "- 首个跌破 30 的窗口: "
|
||||||
|
<< (firstBelowVoxels >= 0
|
||||||
|
? std::to_string(firstBelowVoxels) + " 体素(" +
|
||||||
|
std::to_string(firstBelowBricks) + " brick 列)"
|
||||||
|
: "无(需更大 --bricks 段触达天花板)")
|
||||||
|
<< "\n";
|
||||||
|
rf << "- 双闸:纹理维度错误=" << (textureErr ? "是" : "否")
|
||||||
|
<< ";每段均按非空像素校验。\n";
|
||||||
|
rf << "- production LOD 应把【每帧渲染的全分辨率块】卡在此预算以内。\n";
|
||||||
|
rf << "- **本机 RTX 3060 上限数;最低配需用户在目标机跑 fps-budget/view。**\n";
|
||||||
|
}
|
||||||
|
std::cout << "[fps-budget] 报告追加写入 " << repo.string() << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return textureErr ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ③ view:真窗口可交互(给用户肉眼测 + 最低配机跑)(Task 12d)
|
||||||
|
// ============================================================================
|
||||||
|
//
|
||||||
|
// 真 vtkRenderWindow + vtkRenderWindowInteractor(TrackballCamera),挂
|
||||||
|
// OutOfCoreSource:相机变化时 source.update(camera) 重选 LOD/视野块再渲(确保
|
||||||
|
// 拖动/缩放时 LOD 真切换);屏幕左上角 vtkTextActor 实时显示 fps + 当前 level。
|
||||||
|
// 默认取景对准局部段 + 默认垂向夸张/不透明度(同 ①)。
|
||||||
|
//
|
||||||
|
// 离屏 smoke:--smoke 时不开真窗口,只离屏建管线 + 渲一帧 + 验非空像素,确保不崩。
|
||||||
|
|
||||||
|
// view 的每帧回调共享状态(挂到 interactor 的 EndInteraction/Timer/Render 上)。
|
||||||
|
struct ViewState {
|
||||||
|
geopro::render::OutOfCoreSource* src = nullptr;
|
||||||
|
vtkMultiBlockVolumeMapper* mapper = nullptr;
|
||||||
|
vtkCamera* cam = nullptr;
|
||||||
|
vtkTextActor* fpsText = nullptr;
|
||||||
|
vtkRenderWindow* rw = nullptr;
|
||||||
|
Stopwatch frameTimer;
|
||||||
|
double exagg = 8.0;
|
||||||
|
int lastLevel = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 用 source 当前工作集刷新 mapper 输入(每块成 MultiBlock)。返回块数。
|
||||||
|
std::size_t viewRefreshBlocks(ViewState* st) {
|
||||||
|
st->src->update(st->cam);
|
||||||
|
auto imgs = st->src->currentImages();
|
||||||
|
auto mb = makeMultiBlock(imgs);
|
||||||
|
st->mapper->SetInputDataObject(mb);
|
||||||
|
st->mapper->Update();
|
||||||
|
return imgs.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// interactor 回调:每次交互(旋转/缩放)结束后重选 LOD + 刷新 fps 文本。
|
||||||
|
void viewOnInteract(vtkObject*, unsigned long, void* clientData, void*) {
|
||||||
|
auto* st = static_cast<ViewState*>(clientData);
|
||||||
|
const double frameMs = st->frameTimer.elapsedMs();
|
||||||
|
const std::size_t blocks = viewRefreshBlocks(st);
|
||||||
|
const int lvl = st->src->lastLevel();
|
||||||
|
const double fps = frameMs > 0 ? 1000.0 / frameMs : 0.0;
|
||||||
|
|
||||||
|
char buf[256];
|
||||||
|
std::snprintf(buf, sizeof(buf),
|
||||||
|
"fps: %.1f | LOD level: %d | blocks: %zu | exagg: %.0fx",
|
||||||
|
fps, lvl, blocks, st->exagg);
|
||||||
|
st->fpsText->SetInput(buf);
|
||||||
|
st->lastLevel = lvl;
|
||||||
|
st->rw->Render();
|
||||||
|
st->frameTimer.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
int cmdView(int argc, char** argv) {
|
||||||
|
const Args a = parseArgs(argc, argv, 2);
|
||||||
|
if (a.positional.empty()) {
|
||||||
|
std::cerr << "用法: gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
|
||||||
|
"[--budget 64] [--smoke]\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
const std::string dir = a.positional[0];
|
||||||
|
const double exagg = std::stod(a.get("exagg", "8"));
|
||||||
|
const double opacity = std::stod(a.get("opacity", "0.5"));
|
||||||
|
const std::size_t budget =
|
||||||
|
static_cast<std::size_t>(std::stoul(a.get("budget", "64")));
|
||||||
|
const bool smoke = a.kv.count("smoke") > 0 ||
|
||||||
|
std::find(a.positional.begin(), a.positional.end(),
|
||||||
|
"--smoke") != a.positional.end();
|
||||||
|
std::cout << "[view] storeDir=" << dir << " exagg=" << exagg
|
||||||
|
<< " opacity=" << opacity << " budget=" << budget
|
||||||
|
<< (smoke ? " [SMOKE 离屏]" : " [真窗口交互]") << "\n";
|
||||||
|
|
||||||
|
const int winW = 1280, winH = 800;
|
||||||
|
|
||||||
|
// 核外源(读 meta + 建 pager,不载整卷)。
|
||||||
|
geopro::render::OutOfCoreSource src(dir, budget);
|
||||||
|
const auto& m = src.meta();
|
||||||
|
src.setAspect(static_cast<double>(winW) / winH);
|
||||||
|
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
|
||||||
|
const geopro::core::ColorScale cs = makeStructuralColorScale(vmin, vmax);
|
||||||
|
vtkSmartPointer<vtkVolumeProperty> prop =
|
||||||
|
makeTunedVolumeProperty(m.quant, cs, vmin, vmax, opacity);
|
||||||
|
|
||||||
|
// 渲染窗口:smoke 走离屏,否则真窗口。
|
||||||
|
vtkSmartPointer<vtkRenderWindow> rw;
|
||||||
|
if (smoke) {
|
||||||
|
rw = makeOffscreenWindow(winW, winH);
|
||||||
|
} else {
|
||||||
|
rw = vtkSmartPointer<vtkRenderWindow>::New();
|
||||||
|
rw->SetSize(winW, winH);
|
||||||
|
rw->SetWindowName("gpr_poc view —— 核外 LOD 体绘制 (滚轮缩放切 LOD, 左键旋转)");
|
||||||
|
}
|
||||||
|
|
||||||
|
vtkNew<vtkRenderer> ren;
|
||||||
|
ren->SetBackground(0.04, 0.04, 0.08);
|
||||||
|
rw->AddRenderer(ren);
|
||||||
|
|
||||||
|
vtkNew<vtkMultiBlockVolumeMapper> mapper;
|
||||||
|
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
|
||||||
|
|
||||||
|
auto volume = vtkSmartPointer<vtkVolume>::New();
|
||||||
|
volume->SetMapper(mapper);
|
||||||
|
volume->SetProperty(prop);
|
||||||
|
volume->SetScale(1.0, exagg, exagg); // 垂向夸张(同 ①)
|
||||||
|
ren->AddVolume(volume);
|
||||||
|
|
||||||
|
// 屏幕左上角实时 fps 文本。
|
||||||
|
vtkNew<vtkTextActor> fpsText;
|
||||||
|
fpsText->SetInput("fps: -- | LOD level: --");
|
||||||
|
fpsText->GetTextProperty()->SetFontSize(20);
|
||||||
|
fpsText->GetTextProperty()->SetColor(1.0, 1.0, 0.4);
|
||||||
|
fpsText->SetDisplayPosition(12, winH - 30);
|
||||||
|
ren->AddViewProp(fpsText);
|
||||||
|
|
||||||
|
// 捕获式 OutputWindow(拦截块上传纹理错)。
|
||||||
|
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
|
||||||
|
vtkOutputWindow::SetInstance(capWin);
|
||||||
|
|
||||||
|
ViewState st;
|
||||||
|
st.src = &src;
|
||||||
|
st.mapper = mapper.Get();
|
||||||
|
st.fpsText = fpsText.Get();
|
||||||
|
st.rw = rw.Get();
|
||||||
|
st.exagg = exagg;
|
||||||
|
|
||||||
|
// 相机初始定向:先框整体选出工作集,再 ResetCamera 到工作集包围盒(同 renderC)。
|
||||||
|
ren->ResetCamera(m.origin[0], m.origin[0] + m.nx * m.spacing[0],
|
||||||
|
m.origin[1], m.origin[1] + m.ny * m.spacing[1] * exagg,
|
||||||
|
m.origin[2], m.origin[2] + m.nz * m.spacing[2] * exagg);
|
||||||
|
st.cam = ren->GetActiveCamera();
|
||||||
|
|
||||||
|
const std::size_t warm = viewRefreshBlocks(&st);
|
||||||
|
{
|
||||||
|
double b[6];
|
||||||
|
mapper->GetBounds(b);
|
||||||
|
if (b[0] <= b[1]) {
|
||||||
|
// 工作集包围盒需按 exagg 缩放后再框(actor 已 SetScale)。
|
||||||
|
ren->ResetCamera();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
st.cam->Elevation(25.0);
|
||||||
|
st.cam->Azimuth(25.0);
|
||||||
|
ren->ResetCameraClippingRange();
|
||||||
|
rw->Render();
|
||||||
|
|
||||||
|
std::cout << "[view] 预热: level=" << src.lastLevel() << " 视野块="
|
||||||
|
<< src.lastVisibleCount() << "/" << src.lastLevelBrickTotal()
|
||||||
|
<< " 驻留=" << src.residentCount() << " 渲染块=" << warm << "\n";
|
||||||
|
|
||||||
|
const vtkIdType nonBlack = countNonBlackPixels(rw.Get(), winW, winH);
|
||||||
|
const bool textureErr = capWin->textureError();
|
||||||
|
const bool renderedOk = !textureErr && nonBlack > 0;
|
||||||
|
|
||||||
|
if (smoke) {
|
||||||
|
// 离屏 smoke:模拟一次缩放 → 验 LOD 切换 + 不崩。
|
||||||
|
const int lvlNear = src.lastLevel();
|
||||||
|
st.cam->Dolly(0.2); // 拉远 → 期望切粗 LOD
|
||||||
|
ren->ResetCameraClippingRange();
|
||||||
|
const std::size_t blocksFar = viewRefreshBlocks(&st);
|
||||||
|
const int lvlFar = src.lastLevel();
|
||||||
|
rw->Render();
|
||||||
|
st.cam->Dolly(8.0); // 拉近 → 期望切细 LOD
|
||||||
|
ren->ResetCameraClippingRange();
|
||||||
|
viewRefreshBlocks(&st);
|
||||||
|
const int lvlNear2 = src.lastLevel();
|
||||||
|
rw->Render();
|
||||||
|
const vtkIdType nb2 = countNonBlackPixels(rw.Get(), winW, winH);
|
||||||
|
|
||||||
|
vtkOutputWindow::SetInstance(nullptr);
|
||||||
|
const bool lodSwitched = (lvlFar != lvlNear) || (lvlNear2 != lvlFar);
|
||||||
|
const bool ok = renderedOk && nb2 > 0 && !capWin->textureError();
|
||||||
|
std::cout << "\n=== view --smoke 离屏冒烟 ===\n";
|
||||||
|
std::cout << "近观 level=" << lvlNear << " → 拉远 level=" << lvlFar
|
||||||
|
<< " → 再拉近 level=" << lvlNear2 << "\n";
|
||||||
|
std::cout << "LOD 随缩放切换 : " << (lodSwitched ? "是 ✔" : "否(测点档位未跨界)")
|
||||||
|
<< " (blocksFar=" << blocksFar << ")\n";
|
||||||
|
std::cout << "纹理维度错误 : " << (textureErr ? "是(!!)" : "否") << "\n";
|
||||||
|
std::cout << "渲出非空像素 : " << (renderedOk ? "是" : "否(!!)")
|
||||||
|
<< " (近=" << nonBlack << " 远拉近=" << nb2 << ")\n";
|
||||||
|
std::cout << "smoke 结果 : " << (ok ? "OK ✔ 不崩" : "FAIL ✘") << "\n";
|
||||||
|
return ok ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
vtkOutputWindow::SetInstance(nullptr);
|
||||||
|
if (!renderedOk) {
|
||||||
|
std::cout << "[view] 警告: 首帧未渲出非空像素(纹理错=" << textureErr
|
||||||
|
<< ");窗口仍开,供人工排查。\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 真窗口交互:TrackballCamera + 每次交互结束重选 LOD + 刷 fps 文本。
|
||||||
|
vtkNew<vtkRenderWindowInteractor> iren;
|
||||||
|
iren->SetRenderWindow(rw);
|
||||||
|
vtkNew<vtkInteractorStyleTrackballCamera> style;
|
||||||
|
iren->SetInteractorStyle(style);
|
||||||
|
|
||||||
|
vtkNew<vtkCallbackCommand> cb;
|
||||||
|
cb->SetCallback(viewOnInteract);
|
||||||
|
cb->SetClientData(&st);
|
||||||
|
// EndInteraction:旋转/缩放松手后重选 LOD(保证 LOD 真切换 + fps 刷新)。
|
||||||
|
iren->AddObserver(vtkCommand::EndInteractionEvent, cb);
|
||||||
|
// 每帧 Render 后也更新一次 fps 文本(连续拖动时实时反馈)。
|
||||||
|
rw->AddObserver(vtkCommand::EndEvent, cb);
|
||||||
|
|
||||||
|
std::cout << "[view] 打开真窗口。左键旋转 / 滚轮缩放(切 LOD) / q 退出。\n";
|
||||||
|
st.frameTimer.reset();
|
||||||
|
iren->Initialize();
|
||||||
|
rw->Render();
|
||||||
|
iren->Start();
|
||||||
|
|
||||||
|
std::cout << "[view] 窗口关闭,退出。\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
void usage() {
|
void usage() {
|
||||||
std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n"
|
std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n"
|
||||||
" gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
|
" gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
|
||||||
|
|
@ -1771,7 +2419,13 @@ void usage() {
|
||||||
" gpr_poc renderB <storeDir> [--frames 120]\n"
|
" gpr_poc renderB <storeDir> [--frames 120]\n"
|
||||||
" gpr_poc renderC <storeDir> [--budget 64] [--frames 120]\n"
|
" gpr_poc renderC <storeDir> [--budget 64] [--frames 120]\n"
|
||||||
" gpr_poc renderC-partitioned <storeDir> [--frames 120]\n"
|
" gpr_poc renderC-partitioned <storeDir> [--frames 120]\n"
|
||||||
" gpr_poc renderLOD <storeDir> [--frames 120]\n";
|
" gpr_poc renderLOD <storeDir> [--frames 120]\n"
|
||||||
|
" gpr_poc tune <storeDir> [--opacity 0.5] [--exagg 8] "
|
||||||
|
"[--frames 120] [--localBricks 4]\n"
|
||||||
|
" gpr_poc fps-budget <storeDir> [--frames 90] "
|
||||||
|
"[--bricks 4,16,64,128,256]\n"
|
||||||
|
" gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
|
||||||
|
"[--budget 64] [--smoke]\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
@ -1792,6 +2446,9 @@ int main(int argc, char** argv) {
|
||||||
if (cmd == "renderC-partitioned")
|
if (cmd == "renderC-partitioned")
|
||||||
return cmdRenderCPartitioned(argc, argv);
|
return cmdRenderCPartitioned(argc, argv);
|
||||||
if (cmd == "renderLOD") return cmdRenderLOD(argc, argv);
|
if (cmd == "renderLOD") return cmdRenderLOD(argc, argv);
|
||||||
|
if (cmd == "tune") return cmdTune(argc, argv);
|
||||||
|
if (cmd == "fps-budget") return cmdFpsBudget(argc, argv);
|
||||||
|
if (cmd == "view") return cmdView(argc, argv);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cerr << "错误: " << e.what() << "\n";
|
std::cerr << "错误: " << e.what() << "\n";
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue