From 52830bbcb07ac07880478b155d0899c93655f73b Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 24 Jun 2026 20:37:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20Anomaly=20volumeDsId=E2=86=92rema?= =?UTF-8?q?rkSourceId(=E6=8C=82=E4=BD=93/=E5=88=87=E7=89=87)+resolveAnomal?= =?UTF-8?q?yMount+=E4=BF=AE=E6=AD=A3spec/plan=E5=91=BD=E5=90=8D(Task11=20S?= =?UTF-8?q?tep1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-06-24-vtk-category-view-refactor.md | 8 +++----- .../2026-06-24-vtk-category-view-refactor-design.md | 6 +++--- src/app/AnomalyPropertiesDialog.cpp | 2 +- src/app/main.cpp | 2 +- src/core/model/Anomaly.hpp | 11 ++++++++++- src/data/api/Api3dRepository.cpp | 6 +++--- src/data/api/Api3dRepository.hpp | 2 +- tests/core/test_model_data.cpp | 12 ++++++++++++ 8 files changed, 34 insertions(+), 15 deletions(-) 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 index d236053..e93fbf5 100644 --- a/docs/superpowers/plans/2026-06-24-vtk-category-view-refactor.md +++ b/docs/superpowers/plans/2026-06-24-vtk-category-view-refactor.md @@ -1040,11 +1040,9 @@ git commit -m "feat(data): createVolume/createSlice 扩参+请求体DTO组装(mo - Test: `tests/...`(remarkSource 归属判定纯函数 + Api3d 异常 mock 存查) **Interfaces / 数据模型:** -- `enum class RemarkSourceType { Volume = 1, Slice = 2 };`(对齐后端 remark 概念) -- `Anomaly`:删 `volumeDsId`,加 `std::string remarkSourceId;`(体 dsId 或 切片 dsId)+ `RemarkSourceType remarkSourceType;`。 -- 纯函数(可单测):`struct AnomalyMount { std::string remarkSourceId; RemarkSourceType type; };` - `AnomalyMount resolveAnomalyMount(bool sliceIsSaved, const std::string& savedSliceDsId, const std::string& volumeDsId);` - —— 已保存切片→{sliceDsId, Slice};否则→{volumeDsId, Volume}。 +- `Anomaly`:`volumeDsId` 改名为 `std::string remarkSourceId;`(挂载实体 dsId = 体 or 切片)。**不加** type 字段——挂体/挂切片由 `remarkSourceId` 查 `isVolume/isSlice` 区分,展示树按 `parentId=remarkSourceId` 自动挂载。(⚠️ 后端 `remarkSourceType` 是标注几何形态 1-4 = `markType`,勿混。) +- 纯函数(可单测):`std::string resolveAnomalyMount(bool sliceIsSaved, const std::string& savedSliceDsId, const std::string& volumeDsId);` + —— 已保存切片→`savedSliceDsId`;否则→`volumeDsId`。返回挂载实体 dsId(= remarkSourceId)。 - [ ] **Step 1(逻辑层,可单测): Anomaly 模型 + resolveAnomalyMount** 改 `Anomaly`(remarkSourceId/Type,全量改其引用点:VtkSceneView addAnomaly/removeAnomaly 按 id 跟踪不受影响,仅 main 创建处赋值变);写 `resolveAnomalyMount` 纯函数 + 单测(已保存切片挂切片 / 临时切片挂体 两例)。**build test 绿**。 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 70fb1f8..8b5d7b7 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 @@ -140,9 +140,9 @@ checkedSourcesChanged - **三维体段**:列已生成的体(客户端 mock + 后端 `dd_voxel`),按归属(项目/GS/TM)分组。(**「正在生成…」状态**:现 `createVolume` 同步登记、首次 `loadVolume` 惰性插值,本期**不引入异步生成态机**、体即时出行。)体节点下挂:① 基于该体生成的**切片**子节点;② **直接挂体的异常**(见归属规则)。多体可同时勾选渲染(`dsProps_` 按 dsId 各存 actor),切片/异常操作针对「当前激活体」`volumeOwnerDs_`(=切片源体 `currentVolumeImage_`)。 - **异常归属(核心规则)**:异常**必基于切片**(在某切片平面上画),切片**必基于体**(`SliceSpec.volumeDsId`)。查找链 `异常 → 所在切片 → 切片所属体`。挂载目标按**该切片是否已保存成 `dd_slice`** 决定: - - 切片**已保存**(是 `dd_slice` 实体)→ 异常挂**该切片**(`remarkSourceType=切片`,`remarkSourceId=切片dsId`)。 - - 切片**未保存**(临时圈定平面)→ 异常挂**切片所属体**(`remarkSourceType=体`,`remarkSourceId=体dsId=volumeOwnerDs_`)。 - - 数据模型:`Anomaly` 由原写死的 `volumeDsId` 改为 `remarkSourceId` + `remarkSourceType`(体/切片二选一),对齐后端 remark 概念。仍 mock 存储。 + - 切片**已保存**(是 `dd_slice` 实体)→ 异常挂**该切片**(`remarkSourceId=切片dsId`)。 + - 切片**未保存**(临时圈定平面)→ 异常挂**切片所属体**(`remarkSourceId=体dsId=volumeOwnerDs_`)。 + - 数据模型:`Anomaly` 的 `volumeDsId` 改名为 `remarkSourceId`(= 挂载实体 dsId,体 or 切片;对齐后端 `remarkSourceId=dsObjectId`)。挂体/挂切片由 `remarkSourceId` 指向的实体类型区分(查 `isVolumeDataset`/`isSliceDataset`),展示树按 `parentId=remarkSourceId` 自动挂到对应节点——**不引入新 type 字段**。⚠️ 后端 `remarkSourceType` 已是**标注几何形态**(1点/2线/3面/4文字 = `Anomaly.markType`),勿与"挂体/挂切片"混淆。仍 mock 存储。 - **切片段**:列已保存切片(`dd_slice`),按父体分组(`parentId` = 所属体)。与三维体段内的切片子节点同源(同一批 `sliceRows`)。 体素 / 切片 / 异常的渲染、生成、保存路径不变(`VtkSceneController` / `InteractionManager` / `Api3dRepository`),只改列表的承载位置。 diff --git a/src/app/AnomalyPropertiesDialog.cpp b/src/app/AnomalyPropertiesDialog.cpp index 54622f5..ccfafa2 100644 --- a/src/app/AnomalyPropertiesDialog.cpp +++ b/src/app/AnomalyPropertiesDialog.cpp @@ -33,7 +33,7 @@ AnomalyPropertiesDialog::AnomalyPropertiesDialog(const geopro::core::Anomaly& a, .row(QStringLiteral("名称"), orDash(a.name)) .row(QStringLiteral("类型"), orDash(a.typeName)) .row(QStringLiteral("标记类型"), markTypeLabel(a.markType)) - .row(QStringLiteral("归属三维体"), orDash(a.volumeDsId)) + .row(QStringLiteral("归属"), orDash(a.remarkSourceId)) .row(QStringLiteral("异常体"), a.consortiumId.empty() ? QStringLiteral("(未分组)") : QString::fromStdString(a.consortiumId)); diff --git a/src/app/main.cpp b/src/app/main.cpp index 5df0741..2d316a4 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -507,7 +507,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 草稿异常:先临时渲染(让用户在对话框前看到所画,且截图含异常)。 geopro::core::Anomaly a; a.markType = geopro::core::AnomalyMarkType::Polygon; - a.volumeDsId = volId; + a.remarkSourceId = volId; // Step1 暂挂体;Step3 按所在切片是否已保存改 resolveAnomalyMount a.lineColor = "#ff3030"; a.lineWidth = 2.0; a.dashed = false; diff --git a/src/core/model/Anomaly.hpp b/src/core/model/Anomaly.hpp index 6f2e8d7..9790c53 100644 --- a/src/core/model/Anomaly.hpp +++ b/src/core/model/Anomaly.hpp @@ -10,7 +10,9 @@ struct Vec3 { double x, y, z; }; struct Anomaly { std::string id; // 持久化 id(VTK 三维按 id 跟踪 actor 显隐/选中;2D 详情可空) - std::string volumeDsId; // 归属三维体 ds id(= remarkSourceId;异常挂三维体,非切片) + std::string remarkSourceId; // 挂载实体 dsId(体 or 切片;= 后端 remarkSourceId=dsObjectId)。 + // 挂体/挂切片由该 id 查 isVolume/isSlice 区分(spec §8); + // 注:后端 remarkSourceType 是标注几何形态(1-4)=markType,与此无关。 std::string consortiumId; // 异常体分组 id(空 = 未分组/loose) std::string name; std::string typeName; // exceptionTypeName @@ -42,4 +44,11 @@ struct Anomaly { double textOpacity = 1.0; // customLegend.opacity(0–1) }; +// 异常挂载实体解析(spec §8):在切片平面画异常时—— +// 切片已保存成 dd_slice → 挂该切片;临时未保存切片 → 挂切片所属体。返回挂载实体 dsId(= remarkSourceId)。 +inline std::string resolveAnomalyMount(bool sliceIsSaved, const std::string& savedSliceDsId, + const std::string& volumeDsId) { + return (sliceIsSaved && !savedSliceDsId.empty()) ? savedSliceDsId : volumeDsId; +} + } // namespace geopro::core diff --git a/src/data/api/Api3dRepository.cpp b/src/data/api/Api3dRepository.cpp index 64a90f7..152d73e 100644 --- a/src/data/api/Api3dRepository.cpp +++ b/src/data/api/Api3dRepository.cpp @@ -335,13 +335,13 @@ void Api3dRepository::deleteSlice(const std::string& dsId, std::function // ── 异常 / 异常体(后端真实端点存在,但异常挂三维体、三维体仍 mock → 异常暂内存 mock; // 挂载结构按"异常→三维体",整链端点就绪后切真实,见记忆 vtk-3d-persistence-structure)── -void Api3dRepository::loadAnomalyTree(const std::string& volumeDsId, +void Api3dRepository::loadAnomalyTree(const std::string& remarkSourceId, std::function onOk, OnError /*onErr*/) { - // 按归属三维体过滤;按 consortiumId 分组(异常体),空 consortiumId → loose(未分组)。 + // 按归属实体(体/切片)过滤;按 consortiumId 分组(异常体),空 consortiumId → loose(未分组)。 AnomalyTree tree; std::map bodyIndex; // consortiumId → tree.bodies 下标 for (const auto& [id, sa] : anomalies_) { - if (!volumeDsId.empty() && sa.a.volumeDsId != volumeDsId) continue; + if (!remarkSourceId.empty() && sa.a.remarkSourceId != remarkSourceId) continue; if (sa.a.consortiumId.empty()) { tree.loose.push_back(sa.a); continue; diff --git a/src/data/api/Api3dRepository.hpp b/src/data/api/Api3dRepository.hpp index 58b5e81..8a69048 100644 --- a/src/data/api/Api3dRepository.hpp +++ b/src/data/api/Api3dRepository.hpp @@ -134,7 +134,7 @@ private: std::map slices_; // dsId → 切片 int sliceCounter_ = 0; - // 内存态异常存储(mock;挂三维体 = a.volumeDsId)。异常体(consortium)分组用 a.consortiumId。 + // 内存态异常存储(mock;挂载实体 = a.remarkSourceId,体 or 切片)。异常体(consortium)分组用 a.consortiumId。 struct StoredAnomaly { geopro::core::Anomaly a; std::string screenshotPath; diff --git a/tests/core/test_model_data.cpp b/tests/core/test_model_data.cpp index 51e99d7..2dff372 100644 --- a/tests/core/test_model_data.cpp +++ b/tests/core/test_model_data.cpp @@ -24,6 +24,18 @@ TEST(DataModel, AnomalyHolds) { EXPECT_EQ(a.localPts.size(), 2u); } +// 异常挂载实体解析(spec §8):切片已保存挂切片、临时切片挂体。 +TEST(ResolveAnomalyMount, SavedSliceMountsOnSlice) { + EXPECT_EQ(resolveAnomalyMount(true, "slice-1", "vol-1"), "slice-1"); +} +TEST(ResolveAnomalyMount, UnsavedSliceMountsOnVolume) { + EXPECT_EQ(resolveAnomalyMount(false, "", "vol-1"), "vol-1"); +} +TEST(ResolveAnomalyMount, SavedFlagButEmptySliceIdFallsBackToVolume) { + // 防御:标记已保存但切片 id 缺失 → 退回挂体(不产出空 remarkSourceId)。 + EXPECT_EQ(resolveAnomalyMount(true, "", "vol-1"), "vol-1"); +} + #include TEST(GridNaN, HasValueReflectsNaN) { geopro::core::Grid g(2, 2);