feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
8 changed files with 34 additions and 15 deletions
Showing only changes of commit 52830bbcb0 - Show all commits

View File

@ -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 绿**。

View File

@ -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`),只改列表的承载位置。

View File

@ -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));

View File

@ -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;

View File

@ -10,7 +10,9 @@ struct Vec3 { double x, y, z; };
struct Anomaly {
std::string id; // 持久化 idVTK 三维按 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.opacity01
};
// 异常挂载实体解析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

View File

@ -335,13 +335,13 @@ void Api3dRepository::deleteSlice(const std::string& dsId, std::function<void()>
// ── 异常 / 异常体(后端真实端点存在,但异常挂三维体、三维体仍 mock → 异常暂内存 mock
// 挂载结构按"异常→三维体",整链端点就绪后切真实,见记忆 vtk-3d-persistence-structure──
void Api3dRepository::loadAnomalyTree(const std::string& volumeDsId,
void Api3dRepository::loadAnomalyTree(const std::string& remarkSourceId,
std::function<void(AnomalyTree)> onOk, OnError /*onErr*/) {
// 按归属三维体过滤;按 consortiumId 分组(异常体),空 consortiumId → loose(未分组)。
// 按归属实体(体/切片)过滤;按 consortiumId 分组(异常体),空 consortiumId → loose(未分组)。
AnomalyTree tree;
std::map<std::string, std::size_t> 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;

View File

@ -134,7 +134,7 @@ private:
std::map<std::string, StoredSlice> 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;

View File

@ -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 <cmath>
TEST(GridNaN, HasValueReflectsNaN) {
geopro::core::Grid g(2, 2);