feat(vtk): 异常3D基础(#4a)-Anomaly补3D几何+buildAnomaly3D+视图异常方法+Api mock持久化(挂三维体)
异常功能地基(对象→三维体→异常;异常挂三维体非切片,见记忆 vtk-3d-persistence-structure): - core::Anomaly 补 id/volumeDsId(=remarkSourceId)/consortiumId/worldPts(3D世界点)/planeNormal·planeOrigin, 保留 2D localPts(剖面详情兼容) - render::buildAnomaly3D(单异常→世界坐标 点/折线/闭合多边形 actor,不翻y/不压z;抽 buildActor 共享 2D/3D) - I3dSceneView + VtkSceneView:addAnomaly/removeAnomaly/clearAnomalies/setAnomalyVisible(按 id 跟踪 actor, worldPts 已含VE 故不再 SetScale;clear 一并清异常) - Api3dRepository:StoredAnomaly + saveAnomaly/loadAnomalyTree(按 volumeDsId 过滤+consortiumId 分组异常体)/ deleteAnomaly/deleteAnomalyGroup 内存 mock(取代 stub onErr) 同时修复测试漂移(此前 3a 加 isVolumeDataset 纯虚 + 3c 改 loadVolume 回调签名后,geopro_tests 一直未随之更新): - FakeView 补异常方法;FakeSceneRepo 补 isVolumeDataset(可配置 volumeIds)+修 loadVolume 签名 - test_3d_repo loadVolume 回调改 (VolumeGrid,ColorScale) - 控制器测试 View3DWithVoxelAddsVolume 按新"类型分流"语义重写为 View3DVolumeDatasetAddsVolume(体素XOR帘面) 编译全绿(build.bat all);228/228 单元测试通过。4a 为地基(无 UI 接线、尚不可见),圈定/保存见 4b。
This commit is contained in:
parent
0e7a5c1bf7
commit
4e1b8e7635
|
|
@ -0,0 +1,93 @@
|
|||
# 实现计划:VTK 三维异常(#4,全量含异常体/列表/过滤)
|
||||
|
||||
- 日期:2026-06-18
|
||||
- 分支:`feat/vtk-3d-view`
|
||||
- 上位设计:`docs/superpowers/specs/2026-06-17-vtk-3d-volume-slice-anomaly-design.md`;补充需求 R49-56(切片右键创建异常) + R58-65(三维体详情·异常) + R69-88(异常/异常体列表/属性/过滤)。
|
||||
- 关键决策(用户 2026-06-18 定):
|
||||
- **异常挂「三维体」**(`remarkSourceId`=三维体 ds id),不挂切片(切片是临时圈定载体)、不挂源 ds。见记忆 `vtk-3d-persistence-structure`。
|
||||
- **全做**:圈定 + 保存(含截图) + 3D 渲染 + 异常/异常体列表(对象→异常体→异常) + 选中联动 + 显示过滤 + 删除/删除分组。
|
||||
- **不参考 Geopro1.0**:按需求 + 行业最佳实践(标准多边形圈定)。
|
||||
- **持久化 mock**:三维体/切片/异常端点后端均未就绪 → 全 mock(内存)走 `I3dSceneRepository`,整链端点就绪再切真实。截图先存本地(R88 截图属性待后端新增)。
|
||||
|
||||
## 0. 现状(可复用 vs 新建,实证见探查)
|
||||
|
||||
| 资产 | 现状 |
|
||||
|---|---|
|
||||
| `core::Anomaly`(name/typeName/markType点线面/localPts Vec2/线样式) | ✅ 有,但**2D**(localPts=x距离·y深度),需补 3D 几何 |
|
||||
| `I3dSceneRepository` 异常接口(loadAnomalyTree/saveAnomaly/deleteAnomaly/deleteAnomalyGroup + AnomalyTree/AnomalyBody) | ✅ 接口齐,**实现是 stub**(Api 回 onErr/空树) |
|
||||
| `ObjectExceptionPanel`(对象→异常体→异常 树) | ✅ 只读树完整,**无勾选/选中/删除/过滤交互** |
|
||||
| `render::buildAnomalies`(点/线/面 vtkActor) | ✅ 有,但坐标=2D(x,−depth,0),需 3D(世界点) |
|
||||
| 异常 DTO(parseExceptions/groupByConsortium) + 真实读取(loadExceptionsByTmAsync) | ✅ 真实读取链路通(后端就绪后用) |
|
||||
| `I3dSceneView` 异常方法 / VtkSceneController 异常逻辑 / 3D 圈定工具 / 选中联动(3D) / 过滤 | ❌ 全无,需新建 |
|
||||
|
||||
## 1. 数据模型:core::Anomaly 补 3D 几何
|
||||
|
||||
`src/core/model/Anomaly.hpp` 增(保留现有 2D 字段,新增 3D):
|
||||
- `struct Vec3 { double x,y,z; };`
|
||||
- `std::vector<Vec3> worldPts;`:异常多边形/折线/点的**世界 3D 坐标**(落在所在切片平面上)。
|
||||
- `Vec3 planeNormal{0,0,1}, planeOrigin{};`:所在切片平面(法向+一点)——供重定位/正视,及与切片解耦后仍能定位。
|
||||
- 持久化补充字段(不入 core,入仓储存储或 Anomaly 扩展):`id`、`volumeDsId`(=remarkSourceId)、`exceptionTypeId`/`typeName`、`remark`、`screenshotPath`、`consortiumId`(异常体分组,空=未分组)。
|
||||
|
||||
> core::Anomaly 保持渲染/几何纯数据;id/归属/截图等持久化元数据放仓储的 StoredAnomaly 包装(同 StoredVolume/StoredSlice)。
|
||||
|
||||
## 2. 渲染:3D 异常 actor + I3dSceneView 接口
|
||||
|
||||
- `render::buildAnomalies3D(const std::vector<core::Anomaly>&)`(新增或改造 AnomalyActor):用 `worldPts` 直接建点/折线/闭合多边形 actor(世界坐标,不再 ×−1 深度);样式复用(lineColor/width/dashed);选中高亮(加粗/变色)。
|
||||
- `I3dSceneView` 新增:
|
||||
- `addAnomaly(const core::Anomaly&)` / `removeAnomaly(id)` / `clearAnomalies()`
|
||||
- `setAnomalyVisible(id, bool)` / `setAnomalySelected(id, bool)`(选中联动)
|
||||
- `pickedAnomalyId()` 或经回调 `onAnomalyPicked(id)`(VTK 点选异常→列表)
|
||||
- `VtkSceneView` 持 `map<id, actor>`,实现上述。
|
||||
|
||||
## 3. 圈定工具(切片平面上画多边形)
|
||||
|
||||
`src/render/interact/AnomalyDrawTool.{hpp,cpp}`(新):
|
||||
- 输入:当前选中切片的平面(origin/normal) + interactor + renderer。
|
||||
- 交互(行业标准):左键逐点加顶点(投影到切片平面);右键/双击/回车闭合;Esc 取消;实时预览折线。点类型=单击一点;面=多边形闭合;(线/文字按 markType)。
|
||||
- 产物:`worldPts`(平面上的世界点) + planeNormal/origin → 回调上层。
|
||||
- 入口:VTK 视图切片右键「创建异常」(已占位) → 启动本工具(以光标拾取点为起点,R49)。
|
||||
|
||||
## 4. 保存对话框 + 截图
|
||||
|
||||
`src/app/AnomalySaveDialog.{hpp,cpp}`(新,参考 VolumeParamsDialog 风格):
|
||||
- 字段:异常名称、异常类型(下拉,**mock 几个类型**;真实类型端点 `exceptionType/*` 只读、后续可接)、备注。
|
||||
- 截图(R50):圈定结束截当前 VTK 视图(或异常包络区) → 存本地文件 → 路径+大小入异常记录(`SliceExport` 同款 PNG 写)。
|
||||
- accept → 组装 `core::Anomaly`(markType/worldPts/plane/样式) + 元数据(name/typeId/remark/screenshot) → `saveAnomaly`。
|
||||
|
||||
## 5. 持久化 mock(Api3dRepository,挂三维体)
|
||||
|
||||
- `StoredAnomaly { core::Anomaly geom; id; volumeDsId; exceptionTypeId/typeName; remark; screenshotPath; consortiumId; }`;`map<id, StoredAnomaly> anomalies_`。
|
||||
- `saveAnomaly(a, screenshotPath, onOk(id), onErr)`:生成 `anomaly-N`,存,回 id。(接口已含 screenshotPath 参数)
|
||||
- `loadAnomalyTree(objectId, onOk(tree), onErr)`:按 objectId 下所有三维体聚合异常 → 组 `AnomalyTree`(bodies=异常体分组 + loose=未分组)。mock 阶段:以 volumeDsId 关联,未分组进 loose。
|
||||
- `deleteAnomaly(id)` / `deleteAnomalyGroup(bodyId)`:删/删组。
|
||||
- 异常体(consortium)分组:mock 内存(`map<bodyId, {name,typeName,memberIds}>`);真实端点 `exceptionConsortium/*` 后续接。
|
||||
- 接口签名不变;后端整链就绪仅换实现。
|
||||
|
||||
## 6. 列表面板(R69-88)+ 选中联动 + 过滤
|
||||
|
||||
扩展 `ObjectExceptionPanel`(或在三维分析视图侧新建异常面板,复用其树构建):
|
||||
- 树:对象 → 异常体 → 异常 + 未分组异常(R71-77)。
|
||||
- 勾选(显隐)、单选(选中) → 信号;选中 ↔ VTK 视图异常高亮**双向联动**(R84)。
|
||||
- 操作(R79):删除异常、删除分组(deleteAnomaly/deleteAnomalyGroup)。
|
||||
- 异常属性(R83):选中异常 → 详情(名称/类型/坐标/截图/备注)。
|
||||
- 显示过滤(R86-87):全部显示 / 随GS / 随数据集 / 全部隐藏 → 控制 VTK 异常可见性集合。
|
||||
- 异常属性·截图(R88):展示截图缩略 + "确定截图大小"。
|
||||
|
||||
## 7. main.cpp 编排
|
||||
|
||||
- 切片右键「创建异常」→ 启动 `AnomalyDrawTool`(用当前选中切片平面) → 圈定完成 → `AnomalySaveDialog` → `scene3dRepo->saveAnomaly` → 渲染(view addAnomaly) + 刷新异常面板。
|
||||
- 当前对象/三维体变化 → `loadAnomalyTree` → 填异常面板 + 渲染已存异常。
|
||||
- 面板选中/勾选/过滤/删除 → 驱动 view 的 setAnomalyVisible/Selected + 仓储删;VTK 点选异常 → 面板选中(联动)。
|
||||
|
||||
## 8. 阶段(每阶段编译绿 + 用户实测)
|
||||
|
||||
- **4a 基础**:§1 模型 + §2 渲染/接口 + §5 mock 持久化(saveAnomaly/loadAnomalyTree/delete) + main 加载已存异常渲染。可注入一两个测试异常验证 3D 渲染。无圈定/对话框。
|
||||
- **4b 圈定+保存**:§3 圈定工具 + §4 保存对话框(含截图) + 切片右键「创建异常」接通 → 闭环:画→存→显示→删。
|
||||
- **4c 列表/异常体/联动/过滤**:§6 面板交互(选中联动/过滤/删除分组) + 异常体分组 + 异常属性/截图展示。
|
||||
|
||||
## 9. 风险/待确认
|
||||
|
||||
- **core::Anomaly 改动影响 2D 路径**:补字段不动现有 2D 字段,2D 渲染(ContourPlotItem/buildAnomalies)不受影响;3D 走新 worldPts 路径。
|
||||
- **异常体(consortium)创建入口**:需求 R71 有异常体,但"如何把异常归入异常体"的 UI 入口需求未细化 → 4c 落地时按最佳实践补(多选异常→成组),或先只做 loose + 展示分组。
|
||||
- **截图属性后端缺**(R88 待新增):先本地存,后端加字段再上传。
|
||||
- **真实类型/异常体端点只读可接**:mock 阶段先 mock,降耦合;可选接真实只读。
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
#include "CameraPreset.hpp"
|
||||
#include "Scene.hpp"
|
||||
#include "Theme.hpp"
|
||||
#include "actors/AnomalyActor.hpp"
|
||||
#include "actors/AxesActor.hpp"
|
||||
#include "actors/CurtainActor.hpp"
|
||||
#include "actors/MapLineActor.hpp"
|
||||
|
|
@ -101,6 +102,7 @@ void VtkSceneView::clear() {
|
|||
for (auto& kv : dsProps_) removeProps(kv.second);
|
||||
dsProps_.clear();
|
||||
removeProps(miscProps_);
|
||||
clearAnomalies(); // 异常 actor 随清场一并移除
|
||||
if (currentAxes_) {
|
||||
scene_.renderer()->RemoveViewProp(currentAxes_);
|
||||
currentAxes_ = nullptr;
|
||||
|
|
@ -189,6 +191,33 @@ void VtkSceneView::removeDataset(const std::string& dsId) {
|
|||
}
|
||||
}
|
||||
|
||||
void VtkSceneView::addAnomaly(const geopro::core::Anomaly& a) {
|
||||
if (a.id.empty()) return;
|
||||
removeAnomaly(a.id); // 幂等:同 id 先移除旧 actor,避免重复
|
||||
auto actor = geopro::render::buildAnomaly3D(a);
|
||||
if (!actor) return;
|
||||
scene_.addActor(actor); // worldPts 已是世界系(含 VE),不再 SetScale
|
||||
anomalyProps_[a.id] = actor;
|
||||
}
|
||||
|
||||
void VtkSceneView::removeAnomaly(const std::string& anomalyId) {
|
||||
auto it = anomalyProps_.find(anomalyId);
|
||||
if (it == anomalyProps_.end()) return;
|
||||
if (it->second) scene_.renderer()->RemoveViewProp(it->second);
|
||||
anomalyProps_.erase(it);
|
||||
}
|
||||
|
||||
void VtkSceneView::clearAnomalies() {
|
||||
for (auto& kv : anomalyProps_)
|
||||
if (kv.second) scene_.renderer()->RemoveViewProp(kv.second);
|
||||
anomalyProps_.clear();
|
||||
}
|
||||
|
||||
void VtkSceneView::setAnomalyVisible(const std::string& anomalyId, bool visible) {
|
||||
auto it = anomalyProps_.find(anomalyId);
|
||||
if (it != anomalyProps_.end() && it->second) it->second->SetVisibility(visible ? 1 : 0);
|
||||
}
|
||||
|
||||
void VtkSceneView::setAxes(geopro::controller::AxesMode mode, geopro::controller::AxesUnit unit,
|
||||
int fontSize) {
|
||||
axesMode_ = mode;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ public:
|
|||
const geopro::core::ColorScale& cs) override;
|
||||
void addTerrain(const geopro::data::TerrainPaths& paths) override;
|
||||
void removeDataset(const std::string& dsId) override;
|
||||
void addAnomaly(const geopro::core::Anomaly& a) override;
|
||||
void removeAnomaly(const std::string& anomalyId) override;
|
||||
void clearAnomalies() override;
|
||||
void setAnomalyVisible(const std::string& anomalyId, bool visible) override;
|
||||
void setAxes(geopro::controller::AxesMode mode, geopro::controller::AxesUnit unit,
|
||||
int fontSize) override;
|
||||
void applyCameraView(geopro::controller::ViewDir dir) override;
|
||||
|
|
@ -107,6 +111,7 @@ private:
|
|||
std::map<std::string, std::vector<vtkSmartPointer<vtkProp>>> dsProps_;
|
||||
std::vector<vtkSmartPointer<vtkProp>> miscProps_;
|
||||
std::string volumeOwnerDs_; // 当前 currentVolumeImage_ 归属的 ds(其被移除时置空切片源)
|
||||
std::map<std::string, vtkSmartPointer<vtkProp>> anomalyProps_; // 异常 id → 3D actor
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -38,6 +38,14 @@ public:
|
|||
// 增量移除某数据集的全部图元(取消勾选时调,不影响其余 ds 与底图)。
|
||||
virtual void removeDataset(const std::string& dsId) = 0;
|
||||
|
||||
// ── 异常(#4):按 anomaly id 跟踪 3D actor,独立于数据集图元 ──────────────
|
||||
// addAnomaly:用 worldPts 建 3D 多边形/折线/点 actor 加入场景(id 已在 Anomaly 内)。
|
||||
// 坐标已是世界系(圈定时从切片平面取,含 VE),故不再额外施加 VE 缩放。
|
||||
virtual void addAnomaly(const geopro::core::Anomaly& a) = 0;
|
||||
virtual void removeAnomaly(const std::string& anomalyId) = 0;
|
||||
virtual void clearAnomalies() = 0;
|
||||
virtual void setAnomalyVisible(const std::string& anomalyId, bool visible) = 0;
|
||||
|
||||
// 坐标轴设置(P2):显示方式 + 刻度单位 + 字号。视图据当前场景包围盒重建坐标轴 prop。
|
||||
// None 模式 = 移除坐标轴;rebuild 时由控制器在 clear 后重新下发当前坐标轴设置。
|
||||
virtual void setAxes(AxesMode mode, AxesUnit unit, int fontSize) = 0;
|
||||
|
|
|
|||
|
|
@ -6,12 +6,21 @@ namespace geopro::core {
|
|||
enum class AnomalyMarkType { Point = 1, Polyline = 2, Polygon = 3 };
|
||||
|
||||
struct Vec2 { double x, y; };
|
||||
struct Vec3 { double x, y, z; };
|
||||
|
||||
struct Anomaly {
|
||||
std::string id; // 持久化 id(VTK 三维按 id 跟踪 actor 显隐/选中;2D 详情可空)
|
||||
std::string volumeDsId; // 归属三维体 ds id(= remarkSourceId;异常挂三维体,非切片)
|
||||
std::string consortiumId; // 异常体分组 id(空 = 未分组/loose)
|
||||
std::string name;
|
||||
std::string typeName; // exceptionTypeName
|
||||
AnomalyMarkType markType = AnomalyMarkType::Polyline;
|
||||
std::vector<Vec2> localPts; // location.coordinate(局部坐标)
|
||||
std::vector<Vec2> localPts; // 2D 局部坐标(剖面详情:x=距离, y=深度)
|
||||
// VTK 三维:异常多边形/折线/点的世界 3D 坐标(落在所在切片平面上)+ 平面(法向/一点),
|
||||
// 用于 3D 渲染与重定位/正视;与切片生命周期解耦(切片可删,异常按 worldPts/plane 仍可显示)。
|
||||
std::vector<Vec3> worldPts;
|
||||
Vec3 planeNormal{0.0, 0.0, 1.0};
|
||||
Vec3 planeOrigin{0.0, 0.0, 0.0};
|
||||
std::string lineColor = "#000000"; // legend.polylineColor
|
||||
double lineWidth = 1.0; // legend.polylineWidth
|
||||
bool dashed = true; // legend.polylineShape == "dash"
|
||||
|
|
|
|||
|
|
@ -252,27 +252,60 @@ void Api3dRepository::deleteSlice(const std::string& dsId, std::function<void()>
|
|||
onOk();
|
||||
}
|
||||
|
||||
// ── 异常 / 异常体(load 回空树避免 UI 崩;变更走 onErr)─────────────────────
|
||||
// ── 异常 / 异常体(后端真实端点存在,但异常挂三维体、三维体仍 mock → 异常暂内存 mock;
|
||||
// 挂载结构按"异常→三维体",整链端点就绪后切真实,见记忆 vtk-3d-persistence-structure)──
|
||||
|
||||
void Api3dRepository::loadAnomalyTree(const std::string& /*objectId*/,
|
||||
void Api3dRepository::loadAnomalyTree(const std::string& volumeDsId,
|
||||
std::function<void(AnomalyTree)> onOk, OnError /*onErr*/) {
|
||||
onOk(AnomalyTree{}); // 后端未就绪 → 空树
|
||||
// 按归属三维体过滤;按 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 (sa.a.consortiumId.empty()) {
|
||||
tree.loose.push_back(sa.a);
|
||||
continue;
|
||||
}
|
||||
auto it = bodyIndex.find(sa.a.consortiumId);
|
||||
if (it == bodyIndex.end()) {
|
||||
it = bodyIndex.emplace(sa.a.consortiumId, tree.bodies.size()).first;
|
||||
AnomalyBody body;
|
||||
body.id = sa.a.consortiumId;
|
||||
body.name = sa.a.consortiumId; // mock:名同 id(真实异常体有独立 name/typeName)
|
||||
tree.bodies.push_back(std::move(body));
|
||||
}
|
||||
tree.bodies[it->second].members.push_back(sa.a);
|
||||
}
|
||||
onOk(std::move(tree));
|
||||
}
|
||||
|
||||
void Api3dRepository::saveAnomaly(const geopro::core::Anomaly& /*a*/,
|
||||
const std::string& /*screenshotPngPath*/,
|
||||
std::function<void(std::string)> /*onOk*/, OnError onErr) {
|
||||
onErr(kNotReady);
|
||||
void Api3dRepository::saveAnomaly(const geopro::core::Anomaly& a,
|
||||
const std::string& screenshotPngPath,
|
||||
std::function<void(std::string)> onOk, OnError /*onErr*/) {
|
||||
std::string id = a.id;
|
||||
if (id.empty()) id = "anomaly-" + std::to_string(++anomalyCounter_); // 新建 → 生成 id
|
||||
geopro::core::Anomaly stored = a;
|
||||
stored.id = id;
|
||||
anomalies_[id] = StoredAnomaly{std::move(stored), screenshotPngPath};
|
||||
onOk(id);
|
||||
}
|
||||
|
||||
void Api3dRepository::deleteAnomaly(const std::string& /*anomalyId*/,
|
||||
std::function<void()> /*onOk*/, OnError onErr) {
|
||||
onErr(kNotReady);
|
||||
void Api3dRepository::deleteAnomaly(const std::string& anomalyId, std::function<void()> onOk,
|
||||
OnError /*onErr*/) {
|
||||
anomalies_.erase(anomalyId);
|
||||
onOk();
|
||||
}
|
||||
|
||||
void Api3dRepository::deleteAnomalyGroup(const std::string& /*bodyId*/,
|
||||
std::function<void()> /*onOk*/, OnError onErr) {
|
||||
onErr(kNotReady);
|
||||
void Api3dRepository::deleteAnomalyGroup(const std::string& bodyId, std::function<void()> onOk,
|
||||
OnError /*onErr*/) {
|
||||
// 删除该异常体分组下所有异常(mock:consortiumId == bodyId 的全删)。
|
||||
for (auto it = anomalies_.begin(); it != anomalies_.end();) {
|
||||
if (it->second.a.consortiumId == bodyId)
|
||||
it = anomalies_.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
onOk();
|
||||
}
|
||||
|
||||
// ── 任务管理(load 回空列表避免 UI 崩)──────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -109,6 +109,14 @@ private:
|
|||
};
|
||||
std::map<std::string, StoredSlice> slices_; // dsId → 切片
|
||||
int sliceCounter_ = 0;
|
||||
|
||||
// 内存态异常存储(mock;挂三维体 = a.volumeDsId)。异常体(consortium)分组用 a.consortiumId。
|
||||
struct StoredAnomaly {
|
||||
geopro::core::Anomaly a;
|
||||
std::string screenshotPath;
|
||||
};
|
||||
std::map<std::string, StoredAnomaly> anomalies_; // anomalyId → 异常
|
||||
int anomalyCounter_ = 0;
|
||||
};
|
||||
|
||||
} // namespace geopro::data
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ constexpr int kDashRepeat = 1;
|
|||
constexpr float kPointSize = 8.0F;
|
||||
|
||||
// 把一个异常的 localPts 灌入 points(x, -y, 0:深度取负,与 #18 同坐标系)。
|
||||
void fillPoints(vtkPoints* points, const geopro::core::Anomaly& a)
|
||||
void fillPoints2D(vtkPoints* points, const geopro::core::Anomaly& a)
|
||||
{
|
||||
points->SetNumberOfPoints(static_cast<vtkIdType>(a.localPts.size()));
|
||||
for (std::size_t i = 0; i < a.localPts.size(); ++i) {
|
||||
|
|
@ -31,6 +31,67 @@ void fillPoints(vtkPoints* points, const geopro::core::Anomaly& a)
|
|||
}
|
||||
}
|
||||
|
||||
// 把一个异常的 worldPts 灌入 points(世界 3D 坐标,直接用,不翻 y/不压 z)。
|
||||
void fillPoints3D(vtkPoints* points, const geopro::core::Anomaly& a)
|
||||
{
|
||||
points->SetNumberOfPoints(static_cast<vtkIdType>(a.worldPts.size()));
|
||||
for (std::size_t i = 0; i < a.worldPts.size(); ++i) {
|
||||
points->SetPoint(static_cast<vtkIdType>(i), a.worldPts[i].x, a.worldPts[i].y,
|
||||
a.worldPts[i].z);
|
||||
}
|
||||
}
|
||||
|
||||
// 由已灌点的 points + 异常样式/类型,构建单个 actor(点/折线/闭合多边形 + 颜色/线宽/虚线)。
|
||||
vtkSmartPointer<vtkActor> buildActor(vtkPoints* points, std::size_t n,
|
||||
const geopro::core::Anomaly& a)
|
||||
{
|
||||
vtkNew<vtkPolyData> poly;
|
||||
poly->SetPoints(points);
|
||||
|
||||
const bool asPoints = (a.markType == geopro::core::AnomalyMarkType::Point);
|
||||
if (asPoints) {
|
||||
vtkNew<vtkCellArray> verts;
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
const auto id = static_cast<vtkIdType>(i);
|
||||
verts->InsertNextCell(1, &id);
|
||||
}
|
||||
poly->SetVerts(verts);
|
||||
} else {
|
||||
// 线/面型:经各点的折线;面(Polygon)闭合(首尾相连)成轮廓。
|
||||
const bool closed = (a.markType == geopro::core::AnomalyMarkType::Polygon) && n >= 3;
|
||||
const vtkIdType count = static_cast<vtkIdType>(n) + (closed ? 1 : 0);
|
||||
vtkNew<vtkPolyLine> line;
|
||||
line->GetPointIds()->SetNumberOfIds(count);
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
line->GetPointIds()->SetId(static_cast<vtkIdType>(i), static_cast<vtkIdType>(i));
|
||||
}
|
||||
if (closed) line->GetPointIds()->SetId(static_cast<vtkIdType>(n), 0); // 回到起点
|
||||
vtkNew<vtkCellArray> cells;
|
||||
cells->InsertNextCell(line);
|
||||
poly->SetLines(cells);
|
||||
}
|
||||
|
||||
vtkNew<vtkPolyDataMapper> mapper;
|
||||
mapper->SetInputData(poly);
|
||||
mapper->ScalarVisibilityOff(); // 用 actor 单色,不用标量上色
|
||||
|
||||
auto actor = vtkSmartPointer<vtkActor>::New();
|
||||
actor->SetMapper(mapper);
|
||||
|
||||
const auto c = geopro::core::parseColor(a.lineColor, geopro::core::AlphaScale::Bit255);
|
||||
actor->GetProperty()->SetColor(c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||||
if (asPoints) {
|
||||
actor->GetProperty()->SetPointSize(kPointSize);
|
||||
} else {
|
||||
actor->GetProperty()->SetLineWidth(a.lineWidth > 0.0 ? a.lineWidth : 1.0);
|
||||
if (a.dashed) {
|
||||
actor->GetProperty()->SetLineStipplePattern(kDashPattern);
|
||||
actor->GetProperty()->SetLineStippleRepeatFactor(kDashRepeat);
|
||||
}
|
||||
}
|
||||
return actor;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<vtkSmartPointer<vtkActor>> buildAnomalies(
|
||||
|
|
@ -38,63 +99,23 @@ std::vector<vtkSmartPointer<vtkActor>> buildAnomalies(
|
|||
{
|
||||
std::vector<vtkSmartPointer<vtkActor>> out;
|
||||
out.reserve(anomalies.size());
|
||||
|
||||
for (const auto& a : anomalies) {
|
||||
const std::size_t n = a.localPts.size();
|
||||
if (n == 0) continue; // 无几何,跳过
|
||||
|
||||
vtkNew<vtkPoints> points;
|
||||
fillPoints(points, a);
|
||||
|
||||
vtkNew<vtkPolyData> poly;
|
||||
poly->SetPoints(points);
|
||||
|
||||
const bool asPoints = (a.markType == geopro::core::AnomalyMarkType::Point);
|
||||
if (asPoints) {
|
||||
// 点型:每点一个 vtkVertex。
|
||||
vtkNew<vtkCellArray> verts;
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
const auto id = static_cast<vtkIdType>(i);
|
||||
verts->InsertNextCell(1, &id);
|
||||
}
|
||||
poly->SetVerts(verts);
|
||||
} else {
|
||||
// 线/面型:经各点的折线;面(Polygon)闭合(首尾相连)成轮廓。
|
||||
const bool closed = (a.markType == geopro::core::AnomalyMarkType::Polygon) && n >= 3;
|
||||
const vtkIdType count = static_cast<vtkIdType>(n) + (closed ? 1 : 0);
|
||||
vtkNew<vtkPolyLine> line;
|
||||
line->GetPointIds()->SetNumberOfIds(count);
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
line->GetPointIds()->SetId(static_cast<vtkIdType>(i), static_cast<vtkIdType>(i));
|
||||
}
|
||||
if (closed) line->GetPointIds()->SetId(static_cast<vtkIdType>(n), 0); // 回到起点
|
||||
vtkNew<vtkCellArray> cells;
|
||||
cells->InsertNextCell(line);
|
||||
poly->SetLines(cells);
|
||||
}
|
||||
|
||||
vtkNew<vtkPolyDataMapper> mapper;
|
||||
mapper->SetInputData(poly);
|
||||
mapper->ScalarVisibilityOff(); // 用 actor 单色,不用标量上色
|
||||
|
||||
auto actor = vtkSmartPointer<vtkActor>::New();
|
||||
actor->SetMapper(mapper);
|
||||
|
||||
const auto c = geopro::core::parseColor(a.lineColor, geopro::core::AlphaScale::Bit255);
|
||||
actor->GetProperty()->SetColor(c.r / 255.0, c.g / 255.0, c.b / 255.0);
|
||||
if (asPoints) {
|
||||
actor->GetProperty()->SetPointSize(kPointSize);
|
||||
} else {
|
||||
actor->GetProperty()->SetLineWidth(a.lineWidth > 0.0 ? a.lineWidth : 1.0);
|
||||
if (a.dashed) {
|
||||
actor->GetProperty()->SetLineStipplePattern(kDashPattern);
|
||||
actor->GetProperty()->SetLineStippleRepeatFactor(kDashRepeat);
|
||||
}
|
||||
}
|
||||
out.push_back(actor);
|
||||
fillPoints2D(points, a);
|
||||
out.push_back(buildActor(points, n, a));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
vtkSmartPointer<vtkActor> buildAnomaly3D(const geopro::core::Anomaly& a)
|
||||
{
|
||||
const std::size_t n = a.worldPts.size();
|
||||
if (n == 0) return nullptr; // 无 3D 几何
|
||||
vtkNew<vtkPoints> points;
|
||||
fillPoints3D(points, a);
|
||||
return buildActor(points, n, a);
|
||||
}
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
|
|||
|
|
@ -18,4 +18,8 @@ namespace geopro::render {
|
|||
std::vector<vtkSmartPointer<vtkActor>> buildAnomalies(
|
||||
const std::vector<geopro::core::Anomaly>& anomalies);
|
||||
|
||||
// 单个异常 → 世界坐标 3D actor(VTK 三维视图):用 worldPts 直接建点/折线/闭合多边形(不翻 y、不压 z=0)。
|
||||
// 空几何(worldPts 为空)返回 nullptr。样式同 buildAnomalies(lineColor/width/dashed)。
|
||||
vtkSmartPointer<vtkActor> buildAnomaly3D(const geopro::core::Anomaly& anomaly);
|
||||
|
||||
} // namespace geopro::render
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
|
@ -78,6 +79,12 @@ struct FakeView : I3dSceneView {
|
|||
void render(bool is2D) override { ++renders; lastIs2D = is2D; }
|
||||
void renderIncremental() override { ++renders; }
|
||||
|
||||
// 异常(#4):测试不断言异常渲染,空实现满足接口。
|
||||
void addAnomaly(const core::Anomaly&) override {}
|
||||
void removeAnomaly(const std::string&) override {}
|
||||
void clearAnomalies() override {}
|
||||
void setAnomalyVisible(const std::string&, bool) override {}
|
||||
|
||||
int props() const { return surveyLines + curtains + volumes + terrains; }
|
||||
};
|
||||
|
||||
|
|
@ -106,13 +113,23 @@ struct FakeSceneRepo : data::I3dSceneRepository {
|
|||
data::DsDimension dimensionOf(const data::DsRow&) const override {
|
||||
return data::DsDimension::Dim3D;
|
||||
}
|
||||
void loadVolume(const std::string&, std::function<void(data::VolumeGrid)> onOk,
|
||||
// 按数据集类型分流(取代旧全局 showVoxel/showCurtain):volumeIds 内 → 体素,否则帘面。
|
||||
// 默认空 → 全走帘面(同旧默认行为);体素测试显式标记某 ds 为体素类型。
|
||||
std::set<std::string> volumeIds;
|
||||
bool isVolumeDataset(const std::string& dsId) const override {
|
||||
return volumeIds.count(dsId) > 0;
|
||||
}
|
||||
void loadVolume(const std::string&,
|
||||
std::function<void(data::VolumeGrid, core::ColorScale)> onOk,
|
||||
OnError) override {
|
||||
data::VolumeGrid g;
|
||||
g.vol = core::ScalarVolume(2, 2, 2);
|
||||
g.spacing = {{1.0, 1.0, 1.0}};
|
||||
g.vmin = 0.0; g.vmax = 1.0;
|
||||
onOk(std::move(g)); // 同步回调(异步壳)
|
||||
core::ColorScale cs;
|
||||
cs.addStop(0.0, core::Rgba{0, 0, 255, 255});
|
||||
cs.addStop(1.0, core::Rgba{255, 0, 0, 255});
|
||||
onOk(std::move(g), cs); // 同步回调(异步壳)
|
||||
}
|
||||
void loadSection(const std::string&, std::function<void(data::SectionData)> onOk,
|
||||
OnError) override {
|
||||
|
|
@ -177,16 +194,16 @@ TEST(VtkSceneController, View3DCurtainAddsCurtain) {
|
|||
EXPECT_FALSE(view.lastIs2D);
|
||||
}
|
||||
|
||||
// 3D + 帘面 + 体素 → 帘面 1 + 体素 1(体素经异步回调进场)。
|
||||
TEST(VtkSceneController, View3DWithVoxelAddsVolume) {
|
||||
// 3D + 体素类型数据集 → 体素 1、帘面 0(按类型分流:体素 XOR 帘面,一个 ds 只一种表示)。
|
||||
TEST(VtkSceneController, View3DVolumeDatasetAddsVolume) {
|
||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
||||
sc.volumeIds = {"ds1"}; // ds1 = 三维体类型 → 体素渲染路径
|
||||
VtkSceneController c(ds, sc, view);
|
||||
c.setViewMode(ViewMode::View3D);
|
||||
c.setLayer(SceneLayer::Voxel, true);
|
||||
c.setCheckedDatasets({"ds1"});
|
||||
|
||||
EXPECT_EQ(view.curtains, 1);
|
||||
EXPECT_EQ(view.volumes, 1);
|
||||
EXPECT_EQ(view.curtains, 0); // 体素数据集不再同时出帘面
|
||||
}
|
||||
|
||||
// 3D + 地形 → 地形 1(与勾选数据集无关,地形是场景图层)。
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ TEST(LocalSample3dRepo, LoadVolumeCallsBackWithValidGrid) {
|
|||
bool ok = false;
|
||||
std::string err;
|
||||
VolumeGrid got;
|
||||
repo.loadVolume("voxel1", [&](VolumeGrid g) { ok = true; got = std::move(g); },
|
||||
repo.loadVolume("voxel1",
|
||||
[&](VolumeGrid g, geopro::core::ColorScale) { ok = true; got = std::move(g); },
|
||||
[&](const std::string& m) { err = m; });
|
||||
|
||||
ASSERT_TRUE(ok) << "loadVolume onErr: " << err;
|
||||
|
|
|
|||
Loading…
Reference in New Issue