diff --git a/docs/api/vtk-3d-openapi.json b/docs/api/vtk-3d-openapi.json index 0cb77f3..36f7fd0 100644 --- a/docs/api/vtk-3d-openapi.json +++ b/docs/api/vtk-3d-openapi.json @@ -2,8 +2,8 @@ "openapi": "3.0.3", "info": { "title": "Geopro3 三维视图 API(三维体 / 切片 / 异常 三件套)", - "version": "0.5.0-draft", - "description": "VTK 三维视图后端接口。归属结构(2026-06-24 修订):**GS/项目根 → 三维体(dd_voxel) → 切片(dd_slice)**(三维体由空间容器节点生成,非 TM;TM 单线不成体),异常挂在三维体上(remarkSourceId=三维体 dsObjectId)。\n\n**总原则:实体无关的契约一律复用存量;只为各自特有、存量装不下的部分扩展。**\n- 三维体/切片对后端 = 纯元数据 dsObject:增删改查/属性复用存量 dsObject 面,各加 1 个登记端点;体素字节/切面数据全在客户端(算+存+取+渲染),后端零数据端点。\n- 异常复用整套存量 /business/exception 端点(端点不限实体类型,三维体 id 直接塞 remarkSourceId);**异常体(consortium)分组也是存量已有**(consortiumId/Name/Type)。3D 仅扩展两处:location 加 worldPts+plane(三维几何)、加截图(R88)。\n\n响应统一信封 `{ code:int, msg:string, data:object|array }`,code==200 成功;列表/集合放 data.value。\n\n依赖前提:异常 remarkSourceId 指向三维体,须等三维体登记出真 dsObjectId 后,3D 异常才能接真端点。" + "version": "0.6.0-draft", + "description": "VTK 三维视图后端接口。归属结构(2026-06-24 修订):**GS/项目根/TM → 三维体(dd_voxel) → 切片(dd_slice)**(三维体生成位置由用户在生成对话框选择,默认单GS挂该GS/跨GS挂项目根,可改为项目内任意 GS/TM;源数据集与归属解耦),异常挂在三维体上(remarkSourceId=三维体 dsObjectId)。\n\n**总原则:实体无关的契约一律复用存量;只为各自特有、存量装不下的部分扩展。**\n- 三维体/切片对后端 = 纯元数据 dsObject:增删改查/属性复用存量 dsObject 面,各加 1 个登记端点;体素字节/切面数据全在客户端(算+存+取+渲染),后端零数据端点。\n- 异常复用整套存量 /business/exception 端点(端点不限实体类型,三维体 id 直接塞 remarkSourceId);**异常体(consortium)分组也是存量已有**(consortiumId/Name/Type)。3D 仅扩展两处:location 加 worldPts+plane(三维几何)、加截图(R88)。\n\n响应统一信封 `{ code:int, msg:string, data:object|array }`,code==200 成功;列表/集合放 data.value。\n\n依赖前提:异常 remarkSourceId 指向三维体,须等三维体登记出真 dsObjectId 后,3D 异常才能接真端点。" }, "servers": [ { "url": "/", "description": "业务网关根(各路径已含 /business 前缀)" } @@ -34,7 +34,7 @@ "post": { "tags": ["dsObject-reuse"], "summary": "[复用] 分页查询某父节点下的数据集行", - "description": "存量端点(loadRowsAsync)。查三维体:structParentId=容器节点 id(GS/项目根)、structParentConfType=1;查某三维体下切片:structParentId=该三维体 dsObjectId。返回行 ddCode=dd_voxel / dd_slice。", + "description": "存量端点(loadRowsAsync)。查三维体:structParentId=生成位置节点 id(GS/项目根/TM)、structParentConfType=1或2;查某三维体下切片:structParentId=该三维体 dsObjectId。返回行 ddCode=dd_voxel / dd_slice。", "operationId": "dsObjectDataPage", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DsPageRequest" } } } }, "responses": { @@ -99,7 +99,7 @@ "post": { "tags": ["voxel-new"], "summary": "[新增] 登记三维体记录", - "description": "在容器节点(GS/项目根)下登记一条 dd_voxel dsObject(名称 + 构建参数写入 attachedParameters.voxelParams),返回新 dsObjectId。只建记录、不触发后端计算——体素插值/落盘/渲染全在客户端。", + "description": "在生成位置节点(GS/项目根/TM)下登记一条 dd_voxel dsObject(名称 + 构建参数写入 attachedParameters.voxelParams),返回新 dsObjectId。只建记录、不触发后端计算——体素插值/落盘/渲染全在客户端。", "operationId": "registerVoxel", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VoxelGenerateRequest" } } } }, "responses": { @@ -303,8 +303,8 @@ "required": ["projectId", "structParentId", "structParentConfType", "classifyTypeList", "pageNo", "pageSize"], "properties": { "projectId": { "type": "string" }, - "structParentId": { "type": "string", "description": "查三维体填容器节点 id(GS/项目根);查切片填所属三维体 dsObjectId" }, - "structParentConfType": { "type": "integer", "description": "父节点配置类型:GS/项目根=1(三维体场景);查切片时=三维体所在层级" }, + "structParentId": { "type": "string", "description": "查三维体填生成位置节点 id(GS/项目根/TM);查切片填所属三维体 dsObjectId" }, + "structParentConfType": { "type": "integer", "description": "父节点配置类型:1=GS/项目根 2=TM(三维体生成位置);查切片时=三维体所在层级" }, "classifyTypeList": { "type": "array", "items": { "type": "integer" }, "description": "数据类别过滤(dd_voxel/dd_slice 的 classify code 由后端定义)" }, "pageNo": { "type": "integer", "default": 1 }, "pageSize": { "type": "integer", "default": 20 } @@ -321,7 +321,7 @@ "id": { "type": "string" }, "dsName": { "type": "string" }, "ddCode": { "type": "string", "enum": ["dd_voxel", "dd_slice"] }, "typeName": { "type": "string", "example": "三维体" }, - "parentId": { "type": "string", "nullable": true, "description": "三维体=容器节点 id(GS/项目根);切片=所属三维体 dsObjectId" } + "parentId": { "type": "string", "nullable": true, "description": "三维体=生成位置节点 id(GS/项目根/TM);切片=所属三维体 dsObjectId" } } }, "DsObjectDetail": { @@ -385,8 +385,8 @@ "type": "object", "required": ["projectId", "structParentId", "structParentConfType", "name", "sourceDatasetIds"], "properties": { "projectId": { "type": "string" }, - "structParentId": { "type": "string", "description": "归属容器节点 id —— 三维体挂在 GS/项目根下" }, - "structParentConfType": { "type": "integer", "default": 1, "description": "父节点配置类型:GS/项目根=1(项目根本质亦为 GS)" }, + "structParentId": { "type": "string", "description": "生成位置节点 id —— 三维体挂在所选 GS/项目根/TM 下" }, + "structParentConfType": { "type": "integer", "default": 1, "description": "1=GS/项目根 2=TM;默认单GS挂该GS、跨GS挂项目根,用户可在生成对话框改为任意 GS/TM" }, "name": { "type": "string" }, "sourceDatasetIds": { "type": "array", "items": { "type": "string" }, "minItems": 1 }, "interpModel": { "$ref": "#/components/schemas/InterpModel" }, diff --git a/docs/superpowers/plans/2026-06-24-vtk-category-view-refactor.md b/docs/superpowers/plans/2026-06-24-vtk-category-view-refactor.md new file mode 100644 index 0000000..8d64cce --- /dev/null +++ b/docs/superpowers/plans/2026-06-24-vtk-category-view-refactor.md @@ -0,0 +1,1116 @@ +# VTK 三维分类视图重构 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 把 VTK 视图左侧从「三维数据集/二维数据集/三维分析」三 tab 重构为「按数据类型大类分组」的两 tab 视图,并改造对象树联动、装置类型筛选、VTK 画布工具条、请求体 DTO 组装。 + +**Architecture:** 自底向上分层:先建纯逻辑/服务层(DsRow 扩展、分类映射表 splitByCategory、请求体 DTO、装置字典服务、对象树三态状态机),全部 GoogleTest 单测;再建 UI 层(CategorySection 段组件、QScrollArea 容器、VtkViewToolbar 工具条),靠 cmake build + 手动验证;最后在 main.cpp 总成接线。沿用现有信号槽 + 仓储抽象 + DatasetListPanel 复用,退役 Column3DDataset/Column3DAnalysis。 + +**Tech Stack:** C++17 / Qt6(Widgets)/ VTK / CMake + CTest + GoogleTest。 + +设计依据:`docs/superpowers/specs/2026-06-24-vtk-category-view-refactor-design.md`(spec 各节在任务中以「spec §N」引用)。 + +## Global Constraints + +- C++17;类型 PascalCase、方法 camelCase、成员 `snake_case_` 尾下划线(随现有 `src/` 风格)。 +- 不可变优先;UI 组件单一职责、文件 <800 行。 +- 所有新增逻辑/服务必须有 GoogleTest 单测(`tests/` 下,CMake 注册);UI 组件以「cmake build 通过 + 现有 ctest 全绿 + 手动验证清单」为验收。 +- 后端基址 `http://tenant.geomative.cn/pop-api`;响应信封 `{code,msg,data}`,`code==200` 成功,列表在 `data.value`/`data.list`。 +- 大类分类键:电阻率=`ERT platform inversion data`、视电阻率=`visual resistivity data`、瞬变电磁=`DD TRANSIENT ELECTROMAGNETIC INVERSION`(dsTypeCode,三者 ddCode 同为 `dd_inversion_data`);三维体=`dd_voxel`、切片=`dd_slice`(ddCode)。 +- 层级 `structParentConfType`:1=GS/项目根,2=TM。 +- 装置类型只电阻率/视电阻率段有,瞬变电磁/三维体/切片无。 +- 三维体/切片/异常仍走 `Api3dRepository` mock(不切真实后端)。 +- 提交信息用 conventional commits(feat/refactor/test/docs);署名全局已禁用,勿加。 +- 构建/测试命令(Windows):配置 `cmake --preset ` 后,`cmake --build build` 构建、`ctest --test-dir build --output-on-failure -R ` 跑指定测试。若仓库用 `build.bat`,按其 rebuild。先确认 `tests/` 现有用例如何注册(参考 `tests/data/test_nav_dto.cpp` 与对应 `CMakeLists.txt`)。 + +--- + +## 文件结构(决策锁定) + +**新建:** +- `src/data/repo/CategoryConfig.hpp` — 大类映射表 + 段元数据(纯数据/纯函数)。 +- `src/app/DatasetCategory.hpp` / `.cpp` — `splitByCategory`(替代 `DatasetDimension` 的 `splitByDimension`)。 +- `src/data/dto/Vtk3dRequests.hpp` / `.cpp` — `VoxelGenerateRequest` / `SliceGenerateRequest` + `toJson`。 +- `src/data/repo/DatasetFieldDictionary.hpp` / `.cpp` — 按 dsType 缓存 `dynamicForm` 的 confFieldId↔fieldCode 映射 + 装置类型 value→中文字典。 +- `src/app/panels/columns/CategorySection.hpp` / `.cpp` — 单个类型段(段头筛选/操作 + 段体树)。 +- `src/app/panels/columns/CategoryAnalysisTab.hpp` / `.cpp` — 「三维分析」tab 容器(QScrollArea 堆叠 5 段)。 +- `src/app/VtkViewToolbar.hpp` / `.cpp` — VTK 画布竖排工具条。 +- `src/app/AxesSettingsDialog.hpp` / `.cpp` — 坐标轴设置对话框。 +- 测试:`tests/data/test_dataset_category.cpp`、`tests/data/test_vtk3d_requests.cpp`、`tests/data/test_dataset_field_dictionary.cpp`;扩 `tests/data/test_nav_dto.cpp`。 + +**修改:** +- `src/data/repo/RepoTypes.hpp` — `DsRow` 加 `dsTypeCode` + `properties`(原始 KV)。 +- `src/data/dto/NavDto.cpp` — `parseDsRows` 解析新字段。 +- `src/app/panels/ObjectTreePanel.{hpp,cpp}` — GS 三态状态机(停 AutoTristate)+ 右键 ds/tm + `checkedSourcesChanged` 信号。 +- `src/data/repo/RepoTypes.hpp` 或新文件 — `DataSource{id,confType}` 类型。 +- `src/app/panels/columns/ColumnDrawer.{hpp,cpp}` — 三 tab → 两 tab。 +- `src/data/api/Api3dRepository.{hpp,cpp}` — `createVolume/createSlice` 扩参 + DTO 组装。 +- `src/app/main.cpp` — 数据流接线(分流拉取/分类分发/勾选并集/生成入口归属)。 +- 相关 `CMakeLists.txt`(app 与 tests)。 + +**退役(功能迁出后删除引用):** `Column3DDataset`、`Column3DAnalysis`(拆分到 `CategorySection` / `VtkViewToolbar` / `AxesSettingsDialog` / 三维体段)。 + +**依赖顺序:** Phase 1(数据模型+分类)→ Phase 2(DTO)→ Phase 3(对象树)→ Phase 4(字典服务)→ Phase 5(段+容器)→ Phase 6(工具条)→ Phase 7(Api3d 扩参+段重组)→ Phase 8(main 接线总成)。 + +--- + +## Phase 1 — 数据模型与分类层 + +### Task 1: DsRow 扩展 + parseDsRows 解析 + +**Files:** +- Modify: `src/data/repo/RepoTypes.hpp`(`DsRow` 结构) +- Modify: `src/data/dto/NavDto.cpp:116-137`(`parseDsRows`) +- Test: `tests/data/test_nav_dto.cpp`(新增用例) + +**Interfaces:** +- Produces: + - `struct DsPropKV { std::string confFieldId, value; };` + - `DsRow` 新成员:`std::string dsTypeCode;`、`std::vector properties;` + - `parseDsRows` 填充上述字段(其余字段不变)。 + +- [ ] **Step 1: 写失败测试** — 在 `tests/data/test_nav_dto.cpp` 增用例(喂带 `dsTypeCode` + `properties` 数组的 ds 行 JSON): + +```cpp +TEST(NavDtoTest, ParseDsRowsExtractsTypeCodeAndProperties) { + const QString json = R"({"list":[{ + "id":"d1","dsName":"ERT1-WS","name":"电阻率数据", + "ddCode":"dd_inversion_data","dsTypeCode":"ERT platform inversion data", + "createTime":"2026-03-25 16:48:57","structParentId":"tm1","structParentConfType":2, + "properties":[ + {"confFieldId":"1450495001706500","value":"1429468249448449"}, + {"confFieldId":"1455083478786048","value":"2026-03-25 16:48:57"} + ] + }]})"; + const QJsonObject data = QJsonDocument::fromJson(json.toUtf8()).object(); + const auto page = geopro::data::dto::parseDsPage(data); + ASSERT_EQ(page.rows.size(), 1u); + const auto& r = page.rows[0]; + EXPECT_EQ(r.dsTypeCode, "ERT platform inversion data"); + EXPECT_EQ(r.structParentId, "tm1"); + EXPECT_EQ(r.structParentConfType, 2); + EXPECT_EQ(r.ddCode, "dd_inversion_data"); + ASSERT_EQ(r.properties.size(), 2u); + EXPECT_EQ(r.properties[0].confFieldId, "1450495001706500"); + EXPECT_EQ(r.properties[0].value, "1429468249448449"); +} +``` + +- [ ] **Step 2: 跑测试确认失败** + +Run: `ctest --test-dir build --output-on-failure -R NavDtoTest` +Expected: 编译失败(`dsTypeCode`/`properties` 成员不存在)。 + +- [ ] **Step 3: 扩 DsRow** — `src/data/repo/RepoTypes.hpp` 在 `DsRow` 上方加 KV 结构、`DsRow` 内加两成员: + +```cpp +// ds 属性键值(data/page 的 properties[] 项:confFieldId→value 原始对)。 +struct DsPropKV { std::string confFieldId, value; }; + +struct DsRow { + std::string id, dsName, typeName, ddCode, createTime; + std::string parentId; + std::string fileName, fileUrl; + long long fileSize = 0; + std::string dsTypeCode; // 大类分类主键(spec §5;ddCode 粒度不足以区分电阻率/视电阻率) + std::vector properties; // 原始 confFieldId→value;装置类型/采集时间经 DatasetFieldDictionary 解析 + std::string structParentId; // 上级节点 id(段体容器分组 + 生成三维体归属用) + int structParentConfType = 0; // 1=GS/项目根 2=TM +}; +``` + +- [ ] **Step 4: 扩 parseDsRows** — `src/data/dto/NavDto.cpp` 在 `d.ddCode = str(o, "ddCode");` 后加: + +```cpp + d.dsTypeCode = str(o, "dsTypeCode"); + d.structParentId = str(o, "structParentId"); + d.structParentConfType = o.value(QStringLiteral("structParentConfType")).toInt(); + const QJsonArray props = o.value(QStringLiteral("properties")).toArray(); + d.properties.reserve(static_cast(props.size())); + for (const QJsonValue& pv : props) { + const QJsonObject po = pv.toObject(); + d.properties.push_back( + {str(po, "confFieldId"), po.value(QStringLiteral("value")).toVariant().toString().toStdString()}); + } +``` + +> 注:`value` 用 `toVariant().toString()` 兼容字符串/数值/时间(与 `parseDynamicForm:208` 同口径)。`properties` 也可能是对象(接地电阻/文件型 ds,§3.2 实测),此处 `.toArray()` 对非数组安全返回空——分类不依赖这些类型的 properties,可接受。 + +- [ ] **Step 5: 跑测试确认通过** + +Run: `ctest --test-dir build --output-on-failure -R NavDtoTest` +Expected: PASS(全部 NavDtoTest 用例)。 + +- [ ] **Step 6: 提交** + +```bash +git add src/data/repo/RepoTypes.hpp src/data/dto/NavDto.cpp tests/data/test_nav_dto.cpp +git commit -m "feat(data): DsRow 加 dsTypeCode/properties + parseDsRows 解析" +``` + +--- + +### Task 2: CategoryConfig 映射表 + splitByCategory + +**Files:** +- Create: `src/data/repo/CategoryConfig.hpp` +- Create: `src/app/DatasetCategory.hpp` / `src/app/DatasetCategory.cpp` +- Test: `tests/data/test_dataset_category.cpp` +- Modify: `src/app/CMakeLists.txt`(加 DatasetCategory.cpp)、`tests/.../CMakeLists.txt`(注册测试) + +**Interfaces:** +- Consumes: `DsRow`(Task 1)。 +- Produces: + - `struct CategorySpec { std::string id, title, dsTypeCode, ddCode; bool canGenerateVolume; bool hasArrayTypeFilter; };` + - `const std::vector& categoryConfigs();`(5 段有序) + - `struct CategoryBuckets { std::vector> segments; };`(与 configs 同序、同长) + - `CategoryBuckets splitByCategory(const std::vector& rows);` + +- [ ] **Step 1: 写失败测试** — `tests/data/test_dataset_category.cpp`: + +```cpp +#include +#include "DatasetCategory.hpp" +using geopro::data::DsRow; +using namespace geopro::app; + +static DsRow row(const std::string& id, const std::string& ddCode, const std::string& dsTypeCode) { + DsRow r; r.id = id; r.ddCode = ddCode; r.dsTypeCode = dsTypeCode; return r; +} + +TEST(SplitByCategory, RoutesByDsTypeCodeAndDdCode) { + std::vector rows = { + row("a", "dd_inversion_data", "ERT platform inversion data"), // 电阻率 + row("b", "dd_inversion_data", "visual resistivity data"), // 视电阻率 + row("c", "dd_inversion_data", "DD TRANSIENT ELECTROMAGNETIC INVERSION"), // 瞬变 + row("v", "dd_voxel", ""), // 三维体(按 ddCode) + row("s", "dd_slice", ""), // 切片(按 ddCode) + row("x", "dd_ert_measurement_gr_data", "earth resistance"), // 接地电阻 → 丢弃 + }; + const CategoryBuckets b = splitByCategory(rows); + ASSERT_EQ(b.segments.size(), categoryConfigs().size()); + EXPECT_EQ(b.segments[0].size(), 1u); EXPECT_EQ(b.segments[0][0].id, "a"); + EXPECT_EQ(b.segments[1].size(), 1u); EXPECT_EQ(b.segments[1][0].id, "b"); + EXPECT_EQ(b.segments[2].size(), 1u); EXPECT_EQ(b.segments[2][0].id, "c"); + EXPECT_EQ(b.segments[3].size(), 1u); EXPECT_EQ(b.segments[3][0].id, "v"); + EXPECT_EQ(b.segments[4].size(), 1u); EXPECT_EQ(b.segments[4][0].id, "s"); + // 接地电阻不进任何段 + std::size_t total = 0; for (auto& s : b.segments) total += s.size(); + EXPECT_EQ(total, 5u); +} +``` + +- [ ] **Step 2: 跑测试确认失败** + +Run: `ctest --test-dir build --output-on-failure -R SplitByCategory` +Expected: 编译失败(头文件/符号缺失)。 + +- [ ] **Step 3: 写 CategoryConfig.hpp** + +```cpp +#pragma once +#include +#include + +namespace geopro::app { + +// 一个数据类型大类段的配置(spec §5)。识别键二选一:dsTypeCode 优先;ddCode 用于三维体/切片。 +struct CategorySpec { + std::string id; // 段稳定 id + std::string title; // 段标题(UI 显示) + std::string dsTypeCode; // 主识别键(空=不按 dsTypeCode) + std::string ddCode; // 次识别键(dd_voxel/dd_slice;空=不按 ddCode) + bool canGenerateVolume; // 段内 GS/项目根是否提供「生成三维体」 + bool hasArrayTypeFilter; // 段头是否显示装置类型筛选(仅 ERT 类) +}; + +// 5 段固定有序(spec §5 表)。 +inline const std::vector& categoryConfigs() { + static const std::vector kCfg = { + {"resistivity", "电阻率数据", "ERT platform inversion data", "", true, true}, + {"apparent", "视电阻率数据", "visual resistivity data", "", true, true}, + {"transient", "瞬变电磁数据", "DD TRANSIENT ELECTROMAGNETIC INVERSION", "", true, false}, + {"voxel", "三维体", "", "dd_voxel", false, false}, + {"slice", "切片", "", "dd_slice", false, false}, + }; + return kCfg; +} + +} // namespace geopro::app +``` + +- [ ] **Step 4: 写 DatasetCategory.hpp / .cpp** + +`DatasetCategory.hpp`: +```cpp +#pragma once +#include +#include "repo/RepoTypes.hpp" +#include "CategoryConfig.hpp" + +namespace geopro::app { + +struct CategoryBuckets { + std::vector> segments; // 与 categoryConfigs() 同序同长 +}; + +// 按 CategoryConfig 把 ds 分入大类段:先判 ddCode 白名单(三维体/切片),否则按 dsTypeCode 匹配; +// 不在表内的丢弃(接地电阻/原始数据/白化/坐标等)。保留原顺序。 +CategoryBuckets splitByCategory(const std::vector& rows); + +} // namespace geopro::app +``` + +`DatasetCategory.cpp`: +```cpp +#include "DatasetCategory.hpp" + +namespace geopro::app { + +CategoryBuckets splitByCategory(const std::vector& rows) { + const auto& cfg = categoryConfigs(); + CategoryBuckets b; + b.segments.resize(cfg.size()); + for (const auto& r : rows) { + int hit = -1; + // 先按 ddCode(三维体/切片)——它们无 dsTypeCode(来自 Api3dRepository mock 行)。 + for (std::size_t i = 0; i < cfg.size() && hit < 0; ++i) + if (!cfg[i].ddCode.empty() && r.ddCode == cfg[i].ddCode) hit = static_cast(i); + // 再按 dsTypeCode。 + for (std::size_t i = 0; i < cfg.size() && hit < 0; ++i) + if (!cfg[i].dsTypeCode.empty() && r.dsTypeCode == cfg[i].dsTypeCode) hit = static_cast(i); + if (hit >= 0) b.segments[static_cast(hit)].push_back(r); + } + return b; +} + +} // namespace geopro::app +``` + +- [ ] **Step 5: 注册到 CMake** — 把 `src/app/DatasetCategory.cpp` 加入 app 目标源;把 `tests/data/test_dataset_category.cpp` 按 `test_dataset_dimension`/`test_nav_dto` 同样式注册(确认现有测试在哪个 CMakeLists、用何宏注册,照抄)。 + +- [ ] **Step 6: 跑测试确认通过** + +Run: `cmake --build build && ctest --test-dir build --output-on-failure -R SplitByCategory` +Expected: PASS。 + +- [ ] **Step 7: 提交** + +```bash +git add src/data/repo/CategoryConfig.hpp src/app/DatasetCategory.hpp src/app/DatasetCategory.cpp tests/data/test_dataset_category.cpp src/app/CMakeLists.txt tests/data/CMakeLists.txt +git commit -m "feat(app): CategoryConfig 映射表 + splitByCategory 按 dsTypeCode 分大类" +``` + +--- + +## Phase 2 — 请求体 DTO + +### Task 3: VoxelGenerateRequest / SliceGenerateRequest + toJson + +**Files:** +- Create: `src/data/dto/Vtk3dRequests.hpp` / `.cpp` +- Test: `tests/data/test_vtk3d_requests.cpp` +- Modify: `src/data/CMakeLists.txt`(或 app)、tests CMake + +**Interfaces:** +- Produces(对齐 `docs/api/vtk-3d-openapi.json` schema): + - `struct VoxelGenerateRequest { std::string projectId, structParentId; int structParentConfType=1; std::string name; std::vector sourceDatasetIds; std::string interpModel="Idw"; double cellXY=1.0,cellZ=0.5,power=2.0,maxDist=4.0; std::string colorScaleId; QJsonObject toJson() const; };` + - `struct SliceGenerateRequest { std::string projectId, volumeDsId, name; int axis=3; std::array origin{},point1{},point2{}; std::string colorScaleId; QJsonObject toJson() const; };` + +- [ ] **Step 1: 写失败测试** — `tests/data/test_vtk3d_requests.cpp`: + +```cpp +#include +#include +#include "dto/Vtk3dRequests.hpp" +using namespace geopro::data; + +TEST(Vtk3dRequests, VoxelToJsonMatchesContract) { + VoxelGenerateRequest q; + q.projectId = "p1"; q.structParentId = "g1"; q.structParentConfType = 1; + q.name = "体A"; q.sourceDatasetIds = {"d1", "d2"}; + const QJsonObject j = q.toJson(); + EXPECT_EQ(j["projectId"].toString(), "p1"); + EXPECT_EQ(j["structParentId"].toString(), "g1"); + EXPECT_EQ(j["structParentConfType"].toInt(), 1); + EXPECT_EQ(j["name"].toString(), "体A"); + ASSERT_TRUE(j["sourceDatasetIds"].isArray()); + EXPECT_EQ(j["sourceDatasetIds"].toArray().size(), 2); + EXPECT_EQ(j["interpModel"].toString(), "Idw"); + EXPECT_DOUBLE_EQ(j["cellXY"].toDouble(), 1.0); +} + +TEST(Vtk3dRequests, SliceToJsonMatchesContract) { + SliceGenerateRequest q; + q.projectId = "p1"; q.volumeDsId = "v1"; q.name = "切片1"; q.axis = 3; + q.origin = {0, 0, -10}; q.point1 = {100, 0, -10}; q.point2 = {0, 50, -10}; + const QJsonObject j = q.toJson(); + EXPECT_EQ(j["volumeDsId"].toString(), "v1"); + EXPECT_EQ(j["axis"].toInt(), 3); + ASSERT_TRUE(j["origin"].isArray()); + EXPECT_EQ(j["origin"].toArray().size(), 3); + EXPECT_DOUBLE_EQ(j["point1"].toArray()[0].toDouble(), 100.0); +} +``` + +- [ ] **Step 2: 跑测试确认失败** + +Run: `ctest --test-dir build --output-on-failure -R Vtk3dRequests` +Expected: 编译失败(头缺失)。 + +- [ ] **Step 3: 写 Vtk3dRequests.hpp** + +```cpp +#pragma once +#include +#include +#include +#include + +namespace geopro::data { + +// 对齐 docs/api/vtk-3d-openapi.json VoxelGenerateRequest(spec §8 请求体组装)。 +struct VoxelGenerateRequest { + std::string projectId; + std::string structParentId; // GS/项目根容器节点 id + int structParentConfType = 1; // 1=GS/项目根 + std::string name; + std::vector sourceDatasetIds; + std::string interpModel = "Idw"; // Idw|Kriging + double cellXY = 1.0, cellZ = 0.5, power = 2.0, maxDist = 4.0; + std::string colorScaleId; // 空=取首源色阶 + QJsonObject toJson() const; +}; + +// 对齐 SliceGenerateRequest。 +struct SliceGenerateRequest { + std::string projectId; + std::string volumeDsId; // 所属三维体 dsObjectId + std::string name; + int axis = 3; // 0上下/1前后/2左右/3任意 + std::array origin{{0, 0, 0}}; + std::array point1{{0, 0, 0}}; + std::array point2{{0, 0, 0}}; + std::string colorScaleId; + QJsonObject toJson() const; +}; + +} // namespace geopro::data +``` + +- [ ] **Step 4: 写 Vtk3dRequests.cpp** + +```cpp +#include "dto/Vtk3dRequests.hpp" +#include +#include + +namespace geopro::data { + +namespace { +QJsonArray vec3(const std::array& v) { + return QJsonArray{v[0], v[1], v[2]}; +} +QString qs(const std::string& s) { return QString::fromStdString(s); } +} // namespace + +QJsonObject VoxelGenerateRequest::toJson() const { + QJsonArray ids; + for (const auto& s : sourceDatasetIds) ids.append(qs(s)); + QJsonObject o{ + {"projectId", qs(projectId)}, + {"structParentId", qs(structParentId)}, + {"structParentConfType", structParentConfType}, + {"name", qs(name)}, + {"sourceDatasetIds", ids}, + {"interpModel", qs(interpModel)}, + {"cellXY", cellXY}, {"cellZ", cellZ}, {"power", power}, {"maxDist", maxDist}, + }; + if (!colorScaleId.empty()) o.insert("colorScaleId", qs(colorScaleId)); + return o; +} + +QJsonObject SliceGenerateRequest::toJson() const { + QJsonObject o{ + {"projectId", qs(projectId)}, + {"volumeDsId", qs(volumeDsId)}, + {"name", qs(name)}, + {"axis", axis}, + {"origin", vec3(origin)}, {"point1", vec3(point1)}, {"point2", vec3(point2)}, + }; + if (!colorScaleId.empty()) o.insert("colorScaleId", qs(colorScaleId)); + return o; +} + +} // namespace geopro::data +``` + +- [ ] **Step 5: 注册 CMake + 跑测试** + +Run: `cmake --build build && ctest --test-dir build --output-on-failure -R Vtk3dRequests` +Expected: PASS。 + +- [ ] **Step 6: 提交** + +```bash +git add src/data/dto/Vtk3dRequests.hpp src/data/dto/Vtk3dRequests.cpp tests/data/test_vtk3d_requests.cpp src/data/CMakeLists.txt tests/data/CMakeLists.txt +git commit -m "feat(data): VoxelGenerateRequest/SliceGenerateRequest DTO + toJson" +``` + +--- + +## Phase 3 — 对象树联动改造 + +### Task 4: GS 三态状态机(停 AutoTristate)+ 右键 ds/tm + +**Files:** +- Create: `src/app/panels/ObjectTreeSelection.hpp`(纯逻辑,可单测) +- Modify: `src/app/panels/ObjectTreePanel.cpp`(:123 停 AutoTristate、:174-191 itemChanged、:207-242 右键菜单)、`.hpp` +- Test: `tests/app/test_object_tree_selection.cpp` +- Modify: app/tests CMake + +**Interfaces:** +- Produces: + - `enum class GsCheck { Unchecked, Partial, Checked };` + - `GsCheck aggregateGsState(bool dsOn, int checkedTmCount, int totalTmCount);` + - ObjectTreePanel 新增 UserRole:`kRoleGsDsOn`(GS 自身 ds 开关 bool)。 + +- [ ] **Step 1: 写失败测试** — `tests/app/test_object_tree_selection.cpp`: + +```cpp +#include +#include "panels/ObjectTreeSelection.hpp" +using namespace geopro::app; + +TEST(AggregateGsState, AllOnIsChecked) { + EXPECT_EQ(aggregateGsState(true, 3, 3), GsCheck::Checked); +} +TEST(AggregateGsState, AllOffIsUnchecked) { + EXPECT_EQ(aggregateGsState(false, 0, 3), GsCheck::Unchecked); +} +TEST(AggregateGsState, DsOnTmNoneIsPartial) { + EXPECT_EQ(aggregateGsState(true, 0, 3), GsCheck::Partial); // 只 GS 自身 ds +} +TEST(AggregateGsState, DsOffSomeTmIsPartial) { + EXPECT_EQ(aggregateGsState(false, 1, 3), GsCheck::Partial); // 部分子 TM +} +TEST(AggregateGsState, NoTmFallsBackToDsOnly) { + EXPECT_EQ(aggregateGsState(true, 0, 0), GsCheck::Checked); // 无子 TM → 仅看 ds 开关 + EXPECT_EQ(aggregateGsState(false, 0, 0), GsCheck::Unchecked); +} +``` + +- [ ] **Step 2: 跑测试确认失败** + +Run: `ctest --test-dir build --output-on-failure -R AggregateGsState` +Expected: 编译失败。 + +- [ ] **Step 3: 写 ObjectTreeSelection.hpp** + +```cpp +#pragma once +namespace geopro::app { + +enum class GsCheck { Unchecked, Partial, Checked }; + +// GS 复选框三态 = [自身 ds 开关] ∨ [子 TM 勾选] 的聚合(spec §6)。 +// 无子 TM(totalTmCount==0)时退化为仅看 dsOn。 +inline GsCheck aggregateGsState(bool dsOn, int checkedTmCount, int totalTmCount) { + const bool anyOn = dsOn || checkedTmCount > 0; + if (!anyOn) return GsCheck::Unchecked; + const bool tmAll = (totalTmCount == 0) || (checkedTmCount == totalTmCount); + if (dsOn && tmAll) return GsCheck::Checked; + return GsCheck::Partial; +} + +} // namespace geopro::app +``` + +- [ ] **Step 4: 跑测试确认通过** + +Run: `cmake --build build && ctest --test-dir build --output-on-failure -R AggregateGsState` +Expected: PASS。 + +- [ ] **Step 5: 改 ObjectTreePanel —— 停用 AutoTristate + 手动三态** + +`ObjectTreePanel.cpp` 顶部常量区加 UserRole: +```cpp +constexpr int kRoleGsDsOn = Qt::UserRole + 7; // GS 自身 ds 开关(bool) +``` +`addNodes` 里 GS 分支(现 :122-125,`else { ... ItemIsAutoTristate ... }`)改为**不设 AutoTristate**、初始化 ds 开关: +```cpp +} else { + item->setData(0, kRoleConfType, kConfTypeGs); // GS + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); // 去掉 ItemIsAutoTristate + item->setData(0, kRoleGsDsOn, false); + item->setCheckState(0, Qt::Unchecked); + item->setIcon(0, makeGlyph(Glyph::WorkArea, iconColor, iconPx)); +} +``` +新增私有方法 `void recomputeGsState(QTreeWidgetItem* gs)`(`.hpp` 声明,`.cpp` 实现): +```cpp +void ObjectTreePanel::recomputeGsState(QTreeWidgetItem* gs) { + if (!gs || gs->data(0, kRoleConfType).toInt() != kConfTypeGs) return; + const bool dsOn = gs->data(0, kRoleGsDsOn).toBool(); + int total = 0, checked = 0; + for (int i = 0; i < gs->childCount(); ++i) { + QTreeWidgetItem* c = gs->child(i); + if (c->data(0, kRoleConfType).toInt() != kConfTypeTm) continue; + ++total; + if (c->checkState(0) == Qt::Checked) ++checked; + } + const GsCheck s = aggregateGsState(dsOn, checked, total); + const QSignalBlocker block(tree_); // 不再触发 itemChanged 递归 + gs->setCheckState(0, s == GsCheck::Checked ? Qt::Checked + : s == GsCheck::Partial ? Qt::PartiallyChecked : Qt::Unchecked); +} +``` +在现有 `itemChanged` 合并回调(:174-191)内,遍历收集后**对每个 GS 调 `recomputeGsState`**,再发 `checkedSourcesChanged`(见 Task 5)。点 GS 复选框的「任一开→全关 / 全关→全开」在 `itemClicked`(或复选框命中分支)里处理:读当前 `aggregateGsState`,若非 Unchecked → 置 dsOn=false + 子 TM 全 Unchecked;若 Unchecked → dsOn=true + 子 TM 全 Checked,然后 `recomputeGsState`。 + +- [ ] **Step 6: 右键 ds/tm 菜单** — 在右键菜单 GS 分支(现 :230-234 `if (isGs)`)加: +```cpp +if (isGs) { + QMenu* sel = menu.addMenu(QStringLiteral("选择")); + const bool dsOn = item->data(0, kRoleGsDsOn).toBool(); + bool hasOwnDs = /* 该 GS 是否有直挂 ds:由上层 setStructure 时标记,或暂以 true */ true; + int tmCount = 0; + for (int i = 0; i < item->childCount(); ++i) + if (item->child(i)->data(0, kRoleConfType).toInt() == kConfTypeTm) ++tmCount; + QAction* dsAct = sel->addAction(QStringLiteral("ds")); + dsAct->setCheckable(true); dsAct->setChecked(dsOn); dsAct->setEnabled(hasOwnDs); + QObject::connect(dsAct, &QAction::triggered, this, [this, item](bool on) { + item->setData(0, kRoleGsDsOn, on); recomputeGsState(item); emitCheckedSources(); + }); + QAction* tmAct = sel->addAction(QStringLiteral("tm")); + tmAct->setCheckable(true); + tmAct->setChecked(tmCount > 0 && allTmChecked(item)); // allTmChecked: 私有辅助 + tmAct->setEnabled(tmCount > 0); + QObject::connect(tmAct, &QAction::triggered, this, [this, item](bool on) { + setAllChildTmChecked(item, on); recomputeGsState(item); emitCheckedSources(); + }); + // 保留原「新建检测对象 / 新建方法对象」 + add(QStringLiteral("新建检测对象"), QStringLiteral("newGs")); + add(QStringLiteral("新建方法对象"), QStringLiteral("newTm")); +} +``` +(`allTmChecked` / `setAllChildTmChecked` / `emitCheckedSources` 为私有辅助,签名在 `.hpp` 声明;`emitCheckedSources` 见 Task 5。`hasOwnDs` 的真值来源:setStructure 时若 StructNode 标记了直挂 ds 则置 true,无标记暂保守 true——不影响正确性,仅菜单项是否灰显。) + +- [ ] **Step 7: build + 手动验证** + +Run: `cmake --build build` +手动清单(启动 app,进 VTK 视图,选有 GS 层级的项目): +1. 勾 GS 复选框 → 变全黑、其下 TM 全勾。 +2. 再点 GS → 变空、TM 全不勾。 +3. 右键 GS→选择▸ds 打勾 → GS 变灰(仅自身 ds)。 +4. 右键 GS→选择▸tm 打勾 → 子 TM 全勾、GS 若 ds 也开则变黑、否则灰。 +5. 单独勾一个子 TM → GS 变灰、tm 菜单对号消失。 +6. 无子 TM 的 GS:ds 开/关 → GS 黑/空(不出现灰)。 + +- [ ] **Step 8: 提交** + +```bash +git add src/app/panels/ObjectTreeSelection.hpp src/app/panels/ObjectTreePanel.hpp src/app/panels/ObjectTreePanel.cpp tests/app/test_object_tree_selection.cpp src/app/CMakeLists.txt tests/app/CMakeLists.txt +git commit -m "feat(tree): GS 三态状态机(停 AutoTristate)+右键 ds/tm 选择" +``` + +--- + +### Task 5: DataSource 去重 + checkedSourcesChanged 信号 + +**Files:** +- Modify: `src/data/repo/RepoTypes.hpp`(`DataSource`) +- Create: `src/app/panels/ObjectTreeSelection.hpp`(追加 `dedupeSources`) +- Modify: `src/app/panels/ObjectTreePanel.{hpp,cpp}`(`emitCheckedSources` + 信号) +- Test: `tests/app/test_object_tree_selection.cpp`(追加) + +**Interfaces:** +- Produces: + - `struct DataSource { std::string id; int confType; };`(confType: 1=GS/项目, 2=TM) + - `std::vector dedupeSources(std::vector in);`(按 {id,confType} 去重保序) + - `ObjectTreePanel` 信号 `void checkedSourcesChanged(const QList& sources);` + - 私有 `void emitCheckedSources();` + +- [ ] **Step 1: 写失败测试** — 追加到 `test_object_tree_selection.cpp`: + +```cpp +#include "repo/RepoTypes.hpp" +using geopro::data::DataSource; + +TEST(DedupeSources, RemovesDuplicateByIdAndConfType) { + std::vector in = {{"t1",2},{"g1",1},{"t1",2},{"g1",1},{"t2",2}}; + const auto out = geopro::app::dedupeSources(in); + ASSERT_EQ(out.size(), 3u); + EXPECT_EQ(out[0].id, "t1"); EXPECT_EQ(out[0].confType, 2); + EXPECT_EQ(out[1].id, "g1"); EXPECT_EQ(out[1].confType, 1); + EXPECT_EQ(out[2].id, "t2"); +} +TEST(DedupeSources, SameIdDifferentConfTypeKept) { + std::vector in = {{"x",1},{"x",2}}; + EXPECT_EQ(geopro::app::dedupeSources(in).size(), 2u); +} +``` + +- [ ] **Step 2: 跑确认失败** — `ctest -R DedupeSources` → 编译失败。 + +- [ ] **Step 3: 加 DataSource** — `RepoTypes.hpp`: +```cpp +// 对象树勾选产出的数据源(spec §6)。confType: 1=GS/项目根, 2=TM。 +struct DataSource { std::string id; int confType = 0; }; +``` +(若放 `QList` 过信号,需 `Q_DECLARE_METATYPE(geopro::data::DataSource)` + 注册,照 `DsPage` 现有 metatype 注册方式。) + +- [ ] **Step 4: 加 dedupeSources** — `ObjectTreeSelection.hpp` 追加: +```cpp +#include +#include "repo/RepoTypes.hpp" +namespace geopro::app { +inline std::vector dedupeSources(std::vector in) { + std::vector out; + for (const auto& s : in) { + bool dup = false; + for (const auto& o : out) if (o.id == s.id && o.confType == s.confType) { dup = true; break; } + if (!dup) out.push_back(s); + } + return out; +} +} // namespace geopro::app +``` + +- [ ] **Step 5: 跑确认通过** — `cmake --build build && ctest -R DedupeSources` → PASS。 + +- [ ] **Step 6: emitCheckedSources** — `ObjectTreePanel.cpp` 实现(替代旧 `checkedTmsChanged` 收集): +```cpp +void ObjectTreePanel::emitCheckedSources() { + std::vector src; + std::function walk = [&](QTreeWidgetItem* node) { + for (int i = 0; i < node->childCount(); ++i) { + QTreeWidgetItem* c = node->child(i); + const int ct = c->data(0, kRoleConfType).toInt(); + if (ct == kConfTypeTm && c->checkState(0) == Qt::Checked) + src.push_back({c->data(0, kRoleObjId).toString().toStdString(), 2}); + if (ct == kConfTypeGs && c->data(0, kRoleGsDsOn).toBool()) + src.push_back({c->data(0, kRoleObjId).toString().toStdString(), 1}); + walk(c); + } + }; + walk(tree_->invisibleRootItem()); + // 项目根直挂 ds 固定加入(根节点 id,confType=1)。 + if (tree_->topLevelItemCount() > 0) { + QTreeWidgetItem* root = tree_->topLevelItem(0); + if (root->data(0, kRoleIsRoot).toBool()) + src.push_back({root->data(0, kRoleObjId).toString().toStdString(), 1}); + } + const auto deduped = geopro::app::dedupeSources(std::move(src)); + QList list; + for (const auto& s : deduped) list.push_back(s); + emit checkedSourcesChanged(list); +} +``` +把现有 `itemChanged` 0ms 合并回调(:177-190)末尾的 `emit checkedTmsChanged(...)` 改为 `emitCheckedSources()`,并对涉及 GS 先 `recomputeGsState`。`setAllTmsChecked`/`invertTmChecks` 末尾同样改调 `emitCheckedSources`。`checkedTmsChanged` 信号删除(无其它消费者后;main.cpp 接线在 Task 12 改)。 + +- [ ] **Step 7: build + 提交** + +Run: `cmake --build build`(此时 main.cpp 仍连旧信号会编译错——可在 Task 12 一起绿;若分阶段,本 task 暂保留 `checkedTmsChanged` 与新信号并存,Task 12 删旧)。 + +> 决策:为保持每 task 可编译,**本 task 新增 `checkedSourcesChanged` 并存、不删 `checkedTmsChanged`**;Task 12 接线切换后再删旧信号。 + +```bash +git add src/data/repo/RepoTypes.hpp src/app/panels/ObjectTreeSelection.hpp src/app/panels/ObjectTreePanel.hpp src/app/panels/ObjectTreePanel.cpp tests/app/test_object_tree_selection.cpp +git commit -m "feat(tree): checkedSourcesChanged 带 confType 源集合(去重并集)" +``` + +--- + +## Phase 4 — 装置类型 / 采集时间字典服务 + +### Task 6: DatasetFieldDictionary + +**Files:** +- Create: `src/data/repo/DatasetFieldDictionary.hpp` / `.cpp` +- Test: `tests/data/test_dataset_field_dictionary.cpp` +- Modify: data/tests CMake + +**Interfaces:** +- Produces: + - `struct DsTypeFields { std::string arrayTypeConfFieldId, collectTimeConfFieldId; std::map arrayTypeLabels; };` + - `DsTypeFields parseFieldMapping(const QJsonObject& dynamicFormData);`(纯函数,解析 formList) + - class `DatasetFieldDictionary`:`void ensureFor(dsTypeCode, sampleDsId, cb)`(异步拉 dynamicForm 缓存)、`const DsTypeFields* fields(dsTypeCode) const`、`std::string arrayValueOf(const DsRow&) const`(从 properties 取 arrayType 值)、`std::string arrayLabel(dsTypeCode, value) const`(value→中文,缺失回退原值)。 + +- [ ] **Step 1: 写失败测试** — `tests/data/test_dataset_field_dictionary.cpp`(喂真实 dynamicForm 结构): + +```cpp +#include +#include +#include "repo/DatasetFieldDictionary.hpp" +using namespace geopro::data; + +TEST(ParseFieldMapping, ExtractsArrayTypeAndCollectTimeConfFieldIds) { + const QString js = R"({"formList":[{"groupName":"基本信息","values":[ + {"confFieldId":"f_ct","fieldCode":"collectTime","fieldName":"采集时间","optionsObject":null}, + {"confFieldId":"f_at","fieldCode":"arrayType","fieldName":"装置类型","optionsObject":[ + {"label":"温纳-施伦贝尔排列","value":"v1"},{"label":"全梯度","value":"v2"}]} + ]}]})"; + const QJsonObject data = QJsonDocument::fromJson(js.toUtf8()).object(); + const DsTypeFields f = parseFieldMapping(data); + EXPECT_EQ(f.arrayTypeConfFieldId, "f_at"); + EXPECT_EQ(f.collectTimeConfFieldId, "f_ct"); + ASSERT_EQ(f.arrayTypeLabels.count("v1"), 1u); + EXPECT_EQ(f.arrayTypeLabels.at("v1"), "温纳-施伦贝尔排列"); +} +``` + +- [ ] **Step 2: 跑确认失败** — `ctest -R ParseFieldMapping` → 编译失败。 + +- [ ] **Step 3: 写 DatasetFieldDictionary.hpp** + +```cpp +#pragma once +#include +#include +#include +#include +#include "repo/RepoTypes.hpp" + +namespace geopro::data { + +// 某 dsType 的字段映射(spec §10)。 +struct DsTypeFields { + std::string arrayTypeConfFieldId; // ds 行 properties 里装置类型项的 confFieldId + std::string collectTimeConfFieldId; // 采集时间项的 confFieldId + std::map arrayTypeLabels; // value→中文(来自 optionsObject) +}; + +// 纯函数:从 dsObject/dynamicForm 的 data 解析字段映射(formList → fieldCode==arrayType/collectTime)。 +DsTypeFields parseFieldMapping(const QJsonObject& dynamicFormData); + +// ds 行的装置类型原始值:properties 中 confFieldId==arrayTypeConfFieldId 的 value(缺=空)。 +std::string arrayValueOf(const DsRow& row, const DsTypeFields& f); +// ds 行的采集时间:properties 中 confFieldId==collectTimeConfFieldId 的 value(缺=空)。 +std::string collectTimeOf(const DsRow& row, const DsTypeFields& f); + +} // namespace geopro::data +``` + +- [ ] **Step 4: 写 DatasetFieldDictionary.cpp**(纯解析部分) + +```cpp +#include "repo/DatasetFieldDictionary.hpp" +#include + +namespace geopro::data { + +DsTypeFields parseFieldMapping(const QJsonObject& d) { + DsTypeFields f; + for (const QJsonValue& gv : d.value(QStringLiteral("formList")).toArray()) { + for (const QJsonValue& vv : gv.toObject().value(QStringLiteral("values")).toArray()) { + const QJsonObject fo = vv.toObject(); + const QString code = fo.value(QStringLiteral("fieldCode")).toString(); + const std::string cfid = fo.value(QStringLiteral("confFieldId")).toString().toStdString(); + if (code == QStringLiteral("arrayType")) { + f.arrayTypeConfFieldId = cfid; + for (const QJsonValue& ov : fo.value(QStringLiteral("optionsObject")).toArray()) { + const QJsonObject oo = ov.toObject(); + f.arrayTypeLabels[oo.value(QStringLiteral("value")).toString().toStdString()] = + oo.value(QStringLiteral("label")).toString().toStdString(); + } + } else if (code == QStringLiteral("collectTime")) { + f.collectTimeConfFieldId = cfid; + } + } + } + return f; +} + +static std::string propValue(const DsRow& row, const std::string& cfid) { + if (cfid.empty()) return {}; + for (const auto& kv : row.properties) if (kv.confFieldId == cfid) return kv.value; + return {}; +} +std::string arrayValueOf(const DsRow& row, const DsTypeFields& f) { return propValue(row, f.arrayTypeConfFieldId); } +std::string collectTimeOf(const DsRow& row, const DsTypeFields& f) { return propValue(row, f.collectTimeConfFieldId); } + +} // namespace geopro::data +``` + +> 装置 value→中文:本 task 用 `optionsObject` 建 `arrayTypeLabels`。**已知风险(spec §11)**:实测某些 ds 的 arrayType 原始值不在 optionsObject 里——此时 `arrayLabel` 回退显示原值,待坐实正确字典源后只换 `arrayTypeLabels` 的数据来源、接口不变。 + +- [ ] **Step 5: 跑确认通过** — `cmake --build build && ctest -R ParseFieldMapping` → PASS。 + +- [ ] **Step 6: 异步缓存壳**(无独立单测,随 Task 7 集成)— 在同文件加按 dsTypeCode 缓存的 `DatasetFieldDictionary` 类:构造收 `IAsyncProjectRepository&`(或现有 `loadDatasetFormAsync` 提供者);`ensureFor(dsTypeCode, sampleDsId, cb)` 若未缓存则 `loadDatasetFormAsync(sampleDsId)` → `parseFieldMapping` → 存 `std::map` → cb。`fields(dsTypeCode)` 返回缓存指针或 nullptr。 + +- [ ] **Step 7: 提交** + +```bash +git add src/data/repo/DatasetFieldDictionary.hpp src/data/repo/DatasetFieldDictionary.cpp tests/data/test_dataset_field_dictionary.cpp src/data/CMakeLists.txt tests/data/CMakeLists.txt +git commit -m "feat(data): DatasetFieldDictionary 解析 arrayType/collectTime 映射+装置字典" +``` + +--- + +## Phase 5 — 类型段组件与容器 + +### Task 7: CategorySection(段头筛选 + 段体树 + 生成入口) + +**Files:** +- Create: `src/app/panels/columns/CategorySection.hpp` / `.cpp` +- Modify: app CMake + +**Interfaces:** +- Consumes: `CategorySpec`(Task 2)、`splitByCategory` 桶里的 `std::vector`、`DatasetListPanel::populateDatasetList`/`applyDatasetFilter`、`DatasetFieldDictionary`(Task 6)。 +- Produces: +```cpp +class CategorySection : public QWidget { + Q_OBJECT +public: + CategorySection(const geopro::app::CategorySpec& spec, + geopro::data::DatasetFieldDictionary* dict, QWidget* parent=nullptr); + void setDatasets(const std::vector& rows); +signals: + void checkedDatasetsChanged(const QStringList& dsIds); // 数据行勾选=渲染 + void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds); // 段头「+新增三维体」→带勾选源 + void detailRequested(const QString& dsId, const QString& ddCode, const QString& name); +}; +``` + +**实现要点(`.cpp`,参照现有 `Column3DDataset.cpp` 的列表+右键 pattern):** +- 段头:标题 `QLabel`(`spec.title`)+ 折叠箭头;`spec.hasArrayTypeFilter` 为 true 才加装置类型 `QComboBox`;日期范围两个 `QDateEdit`。 +- 段体:`QTreeWidget` + `applyDatasetCardDelegate`;`setDatasets` 调 `populateDatasetList(tree_, rows, false)` + 让数据行可勾选(同现 Column3DDataset.cpp:170-185)。 +- 勾选 → `checkedDatasetsChanged`(同 Column3DDataset.cpp:128-135)。 +- 筛选:日期/装置类型变化 → 调 `applyDatasetFilter`(日期比较改 collectTime——经 dict 取值;装置类型按 `arrayValueOf` 过滤)。装置类型下拉项 = 当前数据经 `dict->arrayLabel` 去重集合。 +- 生成入口(仅 `spec.canGenerateVolume`):**段头**「+新增三维体」按钮;点击 → 收集本段当前勾选的源 ds → `emit generateVolumeRequested(spec.dsTypeCode, checkedSourceDsIds)`(归属「生成位置」与插值参数都在 main 的 `VolumeParamsDialog` 里选,见 Task 12)。 +- **段体树(核心实现)**:段体呈现「项目根 / GS / TM 容器节点 → ds 行」层级(spec §4 图)。`CategorySection` 加 `void setStructure(const std::vector& nodes)`(对象树同源的扁平 GS/TM 节点;main.cpp 传入、Task 8 经 CategoryAnalysisTab 转发、Task 12 接线)。`setDatasets` 据 structure 先建容器节点(项目根/GS;TM 作为 GS 子节点),再把每个 ds 按其 `structParentId`/`structParentConfType`(Task 1 已解析)挂到对应容器下;容器内 ds 间再按 `ds.parentId` 派生建树(复用 `populateDatasetList` 逻辑)。(生成三维体不在段体右键——改段头按钮,见上「生成入口」;归属由对话框「生成位置」选,与段体容器节点无关。) + +- [ ] **Step 1: 写 CategorySection.hpp**(按上 Interfaces 完整声明) +- [ ] **Step 2: 写 CategorySection.cpp**(按实现要点;段头/段体/筛选/右键) +- [ ] **Step 3: 注册 CMake** +- [ ] **Step 4: build** + +Run: `cmake --build build` +Expected: 编译通过(CategorySection 暂未接入 ColumnDrawer,仅编译)。 + +- [ ] **Step 5: 提交** + +```bash +git add src/app/panels/columns/CategorySection.hpp src/app/panels/columns/CategorySection.cpp src/data/repo/RepoTypes.hpp src/data/dto/NavDto.cpp src/app/CMakeLists.txt +git commit -m "feat(ui): CategorySection 类型段组件(段头筛选+段体树+生成入口)" +``` + +--- + +### Task 8: CategoryAnalysisTab 容器 + ColumnDrawer 两 tab + +**Files:** +- Create: `src/app/panels/columns/CategoryAnalysisTab.hpp` / `.cpp` +- Modify: `src/app/panels/columns/ColumnDrawer.{hpp,cpp}` +- Modify: app CMake + +**Interfaces:** +- Produces: +```cpp +class CategoryAnalysisTab : public QWidget { // QScrollArea 堆叠 5 段 + Q_OBJECT +public: + CategoryAnalysisTab(geopro::data::DatasetFieldDictionary* dict, QWidget* parent=nullptr); + void setBuckets(const geopro::app::CategoryBuckets& b); // 分发到 5 段 + CategorySection* section(const std::string& id) const; // 按 CategorySpec.id 取段 +signals: + void checkedDatasetsChanged(const QStringList& dsIds); // 5 段勾选合并(并集)上抛 + void generateVolumeRequested(const QString& dsTypeCode, const QStringList& sourceDsIds); + void detailRequested(const QString&, const QString&, const QString&); +}; +``` +- `ColumnDrawer`:`col3D_/colAnalysis_` 两个旧成员替换为 `CategoryAnalysisTab* analysisTab_`;tabs 改为 `addTab(analysisTab_, "三维分析")` + `addTab(col2D_, "二维分析")`。`col3D()/colAnalysis()` 访问器替换为 `analysisTab()`。 + +**实现要点:** `CategoryAnalysisTab` 内 `QScrollArea` + `QVBoxLayout`,遍历 `categoryConfigs()` 建 5 个 `CategorySection`(存 `std::map`);`setBuckets` 把 `b.segments[i]` 给第 i 段;把各段 `checkedDatasetsChanged` 收集成并集再上抛(每段维护各自勾选集,合并)。 + +- [ ] **Step 1: 写 CategoryAnalysisTab.hpp/.cpp** +- [ ] **Step 2: 改 ColumnDrawer**(两 tab;删 col3D_/colAnalysis_,留 col2D_) +- [ ] **Step 3: 注册 CMake + build** + +Run: `cmake --build build` +Expected: ColumnDrawer 编译通过;main.cpp 仍引用旧 `col3D()/colAnalysis()` 会编译错——Task 12 切换。**为保持可编译**:本 task 暂保留旧 `col3D()/colAnalysis()` 返回 nullptr 或保留旧成员并存,Task 12 删。决策:保留 `col2D()` + 新增 `analysisTab()`,旧 `col3D()/colAnalysis()` 暂留空实现直到 Task 12。 + +- [ ] **Step 4: 提交** + +```bash +git add src/app/panels/columns/CategoryAnalysisTab.hpp src/app/panels/columns/CategoryAnalysisTab.cpp src/app/panels/columns/ColumnDrawer.hpp src/app/panels/columns/ColumnDrawer.cpp src/app/CMakeLists.txt +git commit -m "feat(ui): CategoryAnalysisTab(QScrollArea 5段)+ColumnDrawer 两tab" +``` + +--- + +## Phase 6 — VTK 画布工具条 + +### Task 9: VtkViewToolbar + AxesSettingsDialog + +**Files:** +- Create: `src/app/VtkViewToolbar.hpp` / `.cpp`、`src/app/AxesSettingsDialog.hpp` / `.cpp` +- Modify: app CMake + +**Interfaces:** +- Produces: +```cpp +class VtkViewToolbar : public QWidget { // 竖排:设置/前后上下左右/放大缩小复位 + Q_OBJECT +public: + explicit VtkViewToolbar(QWidget* parent=nullptr); +signals: + void axesSettingsRequested(); // 设置→弹 AxesSettingsDialog + void viewRequested(geopro::controller::ViewDir dir); // 前后上下左右 + void zoomInRequested(); void zoomOutRequested(); void fitRequested(); // 复位=适配 +}; +struct AxisRange { bool show=true; double min=-500, max=500; }; +class AxesSettingsDialog : public QDialog { + Q_OBJECT +public: + AxesSettingsDialog(AxisRange x, AxisRange y, AxisRange z, QWidget* parent=nullptr); + AxisRange x() const; AxisRange y() const; AxisRange z() const; // 应用后读取 +}; +``` + +**实现要点:** 工具条三组按钮(QToolButton 竖排),图标用现有 `makeGlyph`/`Glyph`;信号直接转发。`AxesSettingsDialog`:三组(X/Y/深度)各 `QCheckBox 显示` + 两 `QDoubleSpinBox min/max`,底部 取消/应用。视图方向按钮文字「前/后/上/下/左/右」对应 `ViewDir::Front/Back/Top/Bottom/Left/Right`。 + +- [ ] **Step 1: 写 AxesSettingsDialog.hpp/.cpp** +- [ ] **Step 2: 写 VtkViewToolbar.hpp/.cpp** +- [ ] **Step 3: 注册 CMake + build** + +Run: `cmake --build build` +Expected: 编译通过(暂未接入中央视图)。 + +- [ ] **Step 4: 提交** + +```bash +git add src/app/VtkViewToolbar.hpp src/app/VtkViewToolbar.cpp src/app/AxesSettingsDialog.hpp src/app/AxesSettingsDialog.cpp src/app/CMakeLists.txt +git commit -m "feat(ui): VtkViewToolbar 画布工具条 + AxesSettingsDialog" +``` + +--- + +## Phase 7 — Api3dRepository 扩参 + 段重组 + +### Task 10: createVolume/createSlice 扩参 + 请求体 DTO 组装 + +**Files:** +- Modify: `src/data/api/Api3dRepository.{hpp,cpp}` +- Test: `tests/data/test_api3d_requests.cpp`(新增;验证组装的请求体) + +**Interfaces:** +- Changed: + - `std::string createVolume(const VoxelGenerateRequest& req);`(替代旧 `(VolumeBuildParams,name)`;内部 `StoredVolume` 同时存 `req` 与从 req 派生的 `VolumeBuildParams`) + - `void createSlice(const SliceSpec& spec, const std::string& name, const std::string& projectId, OnOk, OnErr);`(加 projectId) + - `const VoxelGenerateRequest* lastVoxelRequest(const std::string& dsId) const;`(测试/调试取回组装请求体) + +- [ ] **Step 1: 写失败测试** — `tests/data/test_api3d_requests.cpp`:构造 `Api3dRepository`(喂 stub dsRepo + frame),调 `createVolume(req)`,取回 `lastVoxelRequest(id)->toJson()` 断言含 structParentId/structParentConfType/sourceDatasetIds。 +(若 `Api3dRepository` 构造依赖重,改为只测「VolumeBuildParams ← VoxelGenerateRequest 的派生函数」纯函数 + toJson;把派生逻辑抽 `VolumeBuildParams fromRequest(const VoxelGenerateRequest&)` 纯函数单测。) + +```cpp +TEST(Api3dRequestAssembly, VolumeBuildParamsFromRequest) { + geopro::data::VoxelGenerateRequest q; + q.sourceDatasetIds = {"d1"}; q.cellXY = 2.0; q.power = 3.0; q.colorScaleId = "cs1"; + const auto p = geopro::data::fromRequest(q); + EXPECT_EQ(p.sourceDatasetIds.size(), 1u); + EXPECT_DOUBLE_EQ(p.cellXY, 2.0); + EXPECT_DOUBLE_EQ(p.power, 3.0); + EXPECT_EQ(p.colorScaleId, "cs1"); +} +``` + +- [ ] **Step 2: 跑确认失败** — `ctest -R Api3dRequestAssembly` → 失败。 +- [ ] **Step 3: 加 `fromRequest`**(`Vtk3dRequests.{hpp,cpp}` 或 Api3d):`VolumeBuildParams fromRequest(const VoxelGenerateRequest&)` 映射字段(interpModel 字符串→enum)。 +- [ ] **Step 4: 改 createVolume/createSlice 签名** — `createVolume(const VoxelGenerateRequest& req)`:`StoredVolume` 存 `req` + `fromRequest(req)`;返回 mock id。`createSlice` 加 projectId,内部组装 `SliceGenerateRequest`(`qDebug()` 打印 `toJson()`)。 +- [ ] **Step 5: 跑确认通过 + build** — `cmake --build build && ctest -R Api3dRequestAssembly` → PASS。 +- [ ] **Step 6: 提交** + +```bash +git add src/data/api/Api3dRepository.hpp src/data/api/Api3dRepository.cpp src/data/dto/Vtk3dRequests.hpp src/data/dto/Vtk3dRequests.cpp tests/data/test_api3d_requests.cpp tests/data/CMakeLists.txt +git commit -m "feat(data): createVolume/createSlice 扩参+请求体DTO组装(mock)" +``` + +--- + +### Task 11: 三维体/切片/异常段呈现(异常迁三维体段) + +**Files:** +- Modify: `src/app/panels/columns/CategorySection.cpp`(三维体段:异常子区)、`CategoryAnalysisTab.cpp` +- Modify: `src/app/main.cpp`(refreshAnalysis 改注入三维体段/切片段) + +**实现要点:** 三维体段(`spec.id=="voxel"`)的段体下,复用现 `Column3DAnalysis` 的 3D 异常列表/显示过滤档位/单条显隐控件(迁入本段,随当前活动体展示,逻辑沿用 main.cpp:394-427 refreshAnomalies);切片段(`id=="slice"`)按 `parentId`=父体分组。三维体「正在生成」本期不做异步态(spec §8)。 + +- [ ] **Step 1: 三维体段加异常子区**(迁 Column3DAnalysis 异常控件) +- [ ] **Step 2: 切片段按父体分组**(populateDatasetList 的 parentId 建树天然支持) +- [ ] **Step 3: build + 手动验证**(生成体→出现在三维体段;保存切片→切片段挂父体下;异常→三维体段异常子区) +- [ ] **Step 4: 提交** + +```bash +git add src/app/panels/columns/CategorySection.cpp src/app/panels/columns/CategoryAnalysisTab.cpp +git commit -m "feat(ui): 三维体/切片/异常段呈现(异常迁三维体段)" +``` + +--- + +## Phase 8 — main.cpp 接线总成 + +### Task 12: 数据流接线 + 退役旧栏 + +**Files:** +- Modify: `src/app/main.cpp`(:388-460 异常/勾选聚合、:1113-1225 对象树→分类分发) +- Modify: `src/app/panels/ObjectTreePanel.{hpp,cpp}`(删旧 `checkedTmsChanged`) +- Modify: `src/app/panels/columns/ColumnDrawer.{hpp,cpp}`(删旧 `col3D()/colAnalysis()`) +- Delete refs: `Column3DDataset` / `Column3DAnalysis`(源文件可留待后续清理,先解除 main 引用) + +**接线改动:** +1. 对象树勾选:`objectTree::checkedTmsChanged` → `checkedSourcesChanged(QList)`。回调内对每源 `loadRowsAsync(projId, src.id, src.confType, /*classify*/3, 1, 100000)`(第3参 `src.confType` 取代字面量 `2`,见 ApiProjectRepository.cpp:81-89 透传),汇总 `DsRow[]`(保留现有 `generation` 防陈旧 + 多源计数 finish)。 +2. 分类分发:`finish` 内 `splitByCategory(*acc)` → `drawer->analysisTab()->setBuckets(b)`(取代旧 `col3D/col2D/colAnalysis setDatasets` + `splitByDimension`)。二维数据(dd_trajectory_data 等)仍走 `drawer->col2D()->setDatasets(...)`——**注意**:splitByCategory 只产 5 个 3D 段,二维数据需单独分出:保留一个 `dim2D` 过滤(trajectory 类)喂 col2D。 +3. 渲染勾选并集:`analysisTab::checkedDatasetsChanged`(5 段并集,含帘面源=电阻率/视电阻率/瞬变 + 体素/切片)→ 并入 `checkedProfiles`/`checkedAnalysis` → `pushChecked()`(沿用现 :457-462 并集模型)。 +4. 生成三维体:`analysisTab::generateVolumeRequested(dsTypeCode, sourceDsIds)` → 弹 `VolumeParamsDialog`(**左侧**勾选源 ds 树·按 GS 分组·可二次增删确认;**右侧**参数含「生成位置」下拉 = 项目内 GS/TM 列表,**默认**源单 GS→该 GS、跨 GS→项目根)→ 用户确认 → 组装 `VoxelGenerateRequest{projectId, structParentId=所选生成位置, structParentConfType=1或2, name, sourceDatasetIds=对话框最终勾选, params}` → `scene3dRepo->createVolume(req)` → `refreshAnalysis()`。`VolumeParamsDialog` 需扩:左侧源列表(含二次增删)+ 「生成位置」下拉(项目 GS/TM 列表来自对象树结构,按默认规则预选)。 +5. 切片保存调用点补 `nav.currentProjectId()`(:581/716+ createSlice 调用)。 +6. 工具条接入中央视图:实例化 `VtkViewToolbar` 叠加在 QVTK 上,信号接 `sceneCtrl`(axesSettingsRequested→弹 AxesSettingsDialog→应用到坐标轴;viewRequested/zoom*/fit 接现有槽,迁自 Column3DDataset 接线 :898 等);`setVerticalExaggeration` 默认回灌迁到工具条对应控件。 +7. 删旧信号/访问器/`splitByDimension` 引用。 + +- [ ] **Step 1: 改对象树勾选接线**(confType 分流拉取 + splitByCategory 分发) +- [ ] **Step 2: 改渲染勾选并集**(analysisTab 并集→pushChecked) +- [ ] **Step 3: 改生成三维体接线**(组装 VoxelGenerateRequest) +- [ ] **Step 4: 接入工具条 + 坐标轴对话框** +- [ ] **Step 5: 切片保存补 projectId** +- [ ] **Step 6: 删旧信号/访问器/Column3D* 引用 + splitByDimension** +- [ ] **Step 7: build + 全量手动回归** + +Run: `cmake --build build && ctest --test-dir build --output-on-failure` +Expected: 全绿 + 编译通过。手动回归清单: +1. 勾对象树 GS/TM/项目根 → 对应数据进 5 个大类段(电阻率/视电阻率/瞬变/三维体/切片各就各位)。 +2. 勾电阻率某行 → 中央出帘面(原渲染不丢)。 +3. GS 节点右键「生成三维体」→ 弹参数对话框 → 生成体进三维体段 → 勾选出体素。 +4. 工具条:设置弹坐标轴对话框、前后上下左右切视图、放大/缩小/复位生效。 +5. 日期/装置类型筛选(电阻率段)生效;瞬变段无装置类型下拉。 +6. 二维分析 tab 不变(足迹照常)。 +7. 控制台可见 createVolume/createSlice 打印的真实请求体 JSON。 + +- [ ] **Step 8: 提交** + +```bash +git add src/app/main.cpp src/app/panels/ObjectTreePanel.hpp src/app/panels/ObjectTreePanel.cpp src/app/panels/columns/ColumnDrawer.hpp src/app/panels/columns/ColumnDrawer.cpp +git commit -m "refactor(app): main 接线总成-分类分发/勾选并集/生成归属/工具条+退役旧栏" +``` + +--- + +## 自审(spec 覆盖核对) + +| spec 节 | 覆盖任务 | +|---|---| +| §4 整体架构(两 tab/5 段/工具条) | Task 8, 9, 12 | +| §5 DsRow 扩展 + CategoryConfig + splitByCategory | Task 1, 2 | +| §6 对象树 GS 三态 + 信号 + 数据流 | Task 4, 5, 12 | +| §7 CategorySection(段头/段体/生成入口/勾选承接/双重过滤) | Task 7, 12 | +| §8 三维体/切片/异常段 + 请求体组装 | Task 10, 11, 12 | +| §9 VtkViewToolbar + AxesSettingsDialog + setVE 回灌 | Task 9, 12 | +| §10 DatasetFieldDictionary + collectTime 筛选 | Task 6, 7 | +| §11 装置 value→中文待坐实 | Task 6(回退原值 + 注明) | +| §12 组件/文件边界 | 全部 | + +**待坐实(不阻塞实施,实现中坐实):** 装置类型 value→中文 字典源(Task 6 注明,待属性面板截图)。 + +**Phase 间编译连续性:** Task 5/8 采用「新旧并存」过渡(保留旧信号/访问器),Task 12 统一切换并删旧——保证每次提交可编译。 + +--- + diff --git a/docs/superpowers/specs/2026-06-24-vtk-category-view-refactor-design.md b/docs/superpowers/specs/2026-06-24-vtk-category-view-refactor-design.md index de9f37f..8877251 100644 --- a/docs/superpowers/specs/2026-06-24-vtk-category-view-refactor-design.md +++ b/docs/superpowers/specs/2026-06-24-vtk-category-view-refactor-design.md @@ -93,8 +93,8 @@ | 节点 | 复选框 | 右键新增项 | |---|---|---| -| 项目根 | 无(不可勾,直挂 ds 固定显示) | 生成三维体 | -| 非根 GS | 三态 | 选择 ▸ ds / tm(带对号)、生成三维体 | +| 项目根 | 无(不可勾,直挂 ds 固定显示) | —(生成三维体改在段头,见 §7) | +| 非根 GS | 三态 | 选择 ▸ ds / tm(带对号) | | TM 叶子 | 普通二态 | — | **GS 三态语义**(标准 tristate 聚合):GS 复选框 = `[GS 自身 ds]` + `[所有子 TM 勾选]` 的聚合;都有=Checked,都无=Unchecked,部分=PartiallyChecked。 @@ -121,7 +121,7 @@ checkedSourcesChanged - **段头**:标题 + 折叠开关 + 日期范围筛选 + 装置类型下拉(仅 `装置类型筛选=✓` 的段显示)。 - **段体**:项目根 / GS / TM 树 + 数据行,复用 `DatasetListPanel::populateDatasetList`(按 parentId 建树)与卡片委托;数据行可勾选 = 渲染。 - **渲染勾选链承接(必须)**:CategorySection 暴露 `checkedDatasetsChanged`,**接管退役的 `Column3DDataset` 原有「剖面勾选→帘面渲染」主链**——main.cpp 把 5 段(电阻率/视电阻率/瞬变=帘面,三维体/切片=体素/切片)的勾选并集后下发 `pushChecked`(沿用现有 `checkedProfiles`/`checkedAnalysis` 并集模型),否则帘面渲染整体失联。 -- **生成三维体入口**:仅 `可生成三维体=✓` 的段(电阻率/视电阻率/瞬变电磁),在段内**项目根 / GS 节点**右键提供「生成三维体」——用该容器下本段类型的剖面生成体,类型由段决定、空间范围由容器节点决定。TM 节点不提供(单线不成体)。生成的 `sourceDatasetIds` 须按「**本段 dsTypeCode ∩ 本容器归属**」双重过滤,杜绝跨物理量插值(现 `Column3DDataset::showListContextMenu` 仅按 ddCode 白名单收集、不分 dsTypeCode,会把电阻率+瞬变混入同体;重构后入口移入段内天然隔离,但仍须按段类型+容器双重圈定源)。 +- **生成三维体入口**:仅 `可生成三维体=✓` 的段(电阻率/视电阻率/瞬变电磁)**段头**有「+新增三维体」按钮。**源数据集 = 三维分析中当前勾选的同类型 ds**(本段类型,天然按段隔离、可跨 GS)。点击 → `VolumeParamsDialog`(页面中心弹窗):**左侧·数据列表** 树状(按 GS 分组)展示这些已勾选源,每项可勾选/取消供**确认或二次修改**;**右侧·插值参数**:名称、**生成位置**(下拉)、插值模型、水平/竖向间距、IDW 幂次、最大影响距离。**生成位置(归属)规则**:默认 = 源同属单 GS→该 GS、源跨 GS→项目根;用户可改为**项目内任意 GS / TM**(故归属 `structParentConfType` 可为 1 或 2,与源数据解耦)。提交 → 组装 `VoxelGenerateRequest` → `createVolume`。 - **筛选**:复用并扩展 `applyDatasetFilter`,日期比较字段由 `createTime` 改为 `collectTime`(§10)。 「三维分析」tab 容器(替代原 Column3DDataset/Column3DAnalysis 在 tab 中的位置):`QScrollArea` + 垂直布局,按配置表实例化 5 个 `CategorySection`。 @@ -130,13 +130,13 @@ checkedSourcesChanged 复用现有 `Api3dRepository`(mock)与 `refreshAnalysis` 合并注入机制,仅重新组织到段: -> 三维体归属 **GS / 项目根**(非 TM)。后端契约 `docs/api/vtk-3d-openapi.json` 已同步至 v0.5-draft(`structParentConfType=1`);客户端 `createVolume` 接真实端点时需补 `structParentId/structParentConfType`,并新增按三维体 id 查异常的 `queryException/{remarkSourceId}` 调用。 -- **三维体段**:列已生成的体(客户端 mock + 后端 `dd_voxel`),按归属(项目/GS)分组。(**「正在生成…」状态**:现 `createVolume` 同步登记、首次 `loadVolume` 惰性插值,本期**不引入异步生成态机**、体即时出行;如需占位仅作纯展示文案、不做进度。)异常挂三维体(记忆 `vtk-3d-persistence-structure`):现 `Column3DAnalysis` 的 3D 异常列表 / 显示过滤档位 / 单条显隐,迁入三维体段内(随当前活动体展示)。 +> 三维体归属由「生成位置」选择决定:默认单 GS→该 GS、跨 GS→项目根,用户可改为项目内任意 **GS / TM**(`structParentConfType` 可 1 或 2)。后端契约 `docs/api/vtk-3d-openapi.json` 已同步至 v0.6-draft(`structParentConfType` 放开 1/2 + 默认规则);客户端 `createVolume` 接真实端点时需补 `structParentId/structParentConfType`,并新增按三维体 id 查异常的 `queryException/{remarkSourceId}` 调用。 +- **三维体段**:列已生成的体(客户端 mock + 后端 `dd_voxel`),按归属(项目/GS/TM)分组。(**「正在生成…」状态**:现 `createVolume` 同步登记、首次 `loadVolume` 惰性插值,本期**不引入异步生成态机**、体即时出行;如需占位仅作纯展示文案、不做进度。)异常挂三维体(记忆 `vtk-3d-persistence-structure`):现 `Column3DAnalysis` 的 3D 异常列表 / 显示过滤档位 / 单条显隐,迁入三维体段内(随当前活动体展示)。 - **切片段**:列已保存切片(`dd_slice`),按父体分组(`parentId` = 所属体)。 体素 / 切片 / 异常的渲染、生成、保存路径不变(`VtkSceneController` / `InteractionManager` / `Api3dRepository`),只改列表的承载位置。 -**请求体组装(本次新增)**:`createVolume` 扩参为 `(projectId, structParentId, structParentConfType, params, name)`——归属来自生成入口的 GS/项目根节点;`createSlice` 补 `projectId`。两者内部按新增客户端 DTO `VoxelGenerateRequest`/`SliceGenerateRequest`(对齐 `docs/api/vtk-3d-openapi.json` schema)组装出**完整请求体**并提供 `toJson` 序列化;mock 路径存内存 +(debug)打印请求体,将来切真实端点只把「存」改「发」、组装逻辑原样复用。如此重构一落地即可从 UI 真实产出请求体(值为 mock、结构与字段为真)。`createSlice` 补 `projectId` 会波及 main.cpp 切片保存调用点(切片右键/列表保存路径),须一并传 `nav.currentProjectId()`。 +**请求体组装(本次新增)**:`createVolume` 扩参为 `(projectId, structParentId, structParentConfType, params, name)`——归属来自对话框「生成位置」选择(GS/项目根/TM,默认单GS→该GS、跨GS→项目根);`createSlice` 补 `projectId`。两者内部按新增客户端 DTO `VoxelGenerateRequest`/`SliceGenerateRequest`(对齐 `docs/api/vtk-3d-openapi.json` schema)组装出**完整请求体**并提供 `toJson` 序列化;mock 路径存内存 +(debug)打印请求体,将来切真实端点只把「存」改「发」、组装逻辑原样复用。如此重构一落地即可从 UI 真实产出请求体(值为 mock、结构与字段为真)。`createSlice` 补 `projectId` 会波及 main.cpp 切片保存调用点(切片右键/列表保存路径),须一并传 `nav.currentProjectId()`。 ## 9. VTK 画布工具条 `VtkViewToolbar`