feat(vtk): 异常圈定+保存闭环(#4b)+多项交互修复
#4b 异常圈定→保存→渲染→持久 闭环(异常挂三维体,mock 持久化): - AnomalyDrawTool:切片平面上圈定多边形(屏幕射线-平面求交落点);左键加点(醒目黄圆点)、 鼠标移动虚线橡皮筋(末点→光标跟手)、双击/右键/回车闭合、Esc 取消、左上屏幕提示;高优先级观察者绘制期独占输入 - AnomalySaveDialog:名称/异常类型(mock)/备注 + 截图预览及尺寸(R50) - 切片右键「创建异常」接通:圈定→草稿渲染→截图→保存对话框→saveAnomaly(挂三维体)→红色多边形渲染 - onVolumeChanged→reloadAnomalies:体到场重载其异常(= 显示过滤"随数据集"默认档) - SliceExport 加 captureRenderWindowPng(异常截图,带尺寸);Anomaly 补 exceptionTypeId/remark 交互修复(用户实测确认): - 生成三维体**按勾选集合**(checkbox)收集源,不再受行高亮/右键项影响(撤销错误的多选子类) - buildVolume 网格**覆盖全程**:包络过大时放大间距(fitAxis)而非截断 → 跨 TM 多剖面不再丢远端剖面 - 滚轮回退为"推进当前选中切片";点击切片外**取消选中**(取消后滚轮恢复相机缩放) - 修圈定闭合误触切片:闭合事件提前 abort,避免漏给切片 widget 触发 slice-motion 文档:plans/2026-06-18-vtk-3d-anomaly.md §6 摆放决策(3D异常控制在三维分析区/右侧总表为全集)+§7/§8 状态; design spec 顶部更正块(异常挂三维体/remarkSourceType=标注形态/无截图字段/独立显隐);HANDOFF 更新到 4a/4b。 编译链接绿(build.bat app exit 0);4b 闭环 + 交互修复已用户实测通过。下一步 4c:三维分析区 3D 异常控制(树+过滤R86-87+VTK选中联动R84+显隐+删除)。
This commit is contained in:
parent
4e1b8e7635
commit
6210d615f3
|
|
@ -58,7 +58,10 @@
|
||||||
- **已保存切片重渲染**:分析栏勾选→`syncSlices`在当前活动体上还原(`showSavedSlice`),取消→移除;靠`onVolumeChanged→syncSlices`解决父体异步到场。dd_slice 不进控制器(避免 loadSection 失败),main 编排走 InteractionManager。
|
- **已保存切片重渲染**:分析栏勾选→`syncSlices`在当前活动体上还原(`showSavedSlice`),取消→移除;靠`onVolumeChanged→syncSlices`解决父体异步到场。dd_slice 不进控制器(避免 loadSection 失败),main 编排走 InteractionManager。
|
||||||
- **场景↔列表同步**:VTK「关闭」已保存切片→`onSliceClosed`→列表取消勾选。`Column3DAnalysis::setDatasets`按 dsId 保留勾选+仅勾选集变化才发信号(修"保存切片连带取消体勾选/列表重置")。
|
- **场景↔列表同步**:VTK「关闭」已保存切片→`onSliceClosed`→列表取消勾选。`Column3DAnalysis::setDatasets`按 dsId 保留勾选+仅勾选集变化才发信号(修"保存切片连带取消体勾选/列表重置")。
|
||||||
- 导出:`SliceExport.{hpp,cpp}`(图片=切片上采样2048上色 PNG;dat=重采样标量网格)。切片持久化=`Api3dRepository` createSlice/saveSlice/deleteSlice 内存 mock + sliceRows/isSliceDataset/sliceSpec。
|
- 导出:`SliceExport.{hpp,cpp}`(图片=切片上采样2048上色 PNG;dat=重采样标量网格)。切片持久化=`Api3dRepository` createSlice/saveSlice/deleteSlice 内存 mock + sliceRows/isSliceDataset/sliceSpec。
|
||||||
4. 异常:切片右键创建异常(圈定+保存对话框含截图)→ **接真实端点**。VTK 切片右键菜单的「创建异常」入口已占位(弹"开发中")。**下一主线**。
|
4. 异常(**进行中**,全量含异常体/列表/过滤,计划见 `plans/2026-06-18-vtk-3d-anomaly.md`)。**异常挂三维体**(非切片非源ds,见记忆 vtk-3d-persistence-structure);mock 持久化(三维体/切片端点未就绪)。
|
||||||
|
- **4a ✅ 已提交(4e1b8e7)**:`core::Anomaly` 补 3D(id/volumeDsId/consortiumId/worldPts/plane);`buildAnomaly3D`;`I3dSceneView`+`VtkSceneView` addAnomaly/removeAnomaly/clearAnomalies/setAnomalyVisible(按id跟踪actor);`Api3dRepository` 异常 mock(saveAnomaly/loadAnomalyTree按volumeDsId+consortiumId分组/delete)。**附带修复测试漂移→228/228 绿**。地基、尚不可见。
|
||||||
|
- **4b 进行中**:圈定工具(切片平面画多边形)+保存对话框(名称/类型/备注/截图)+切片右键「创建异常」接通 → 画→存→显示→删闭环。
|
||||||
|
- **4c 待做**:异常/异常体列表(R69-88,扩展 ObjectExceptionPanel)+选中双向联动+显示过滤+删除分组+属性/截图。
|
||||||
5. 分析栏右键接线:**已完成**(切片 保存/保存为/导出▸/删除 全接;体 切片▸/详情);`colorScaleRequested` 仍占位("色阶开发中")。已移除"显示/隐藏"(勾选即显隐)。
|
5. 分析栏右键接线:**已完成**(切片 保存/保存为/导出▸/删除 全接;体 切片▸/详情);`colorScaleRequested` 仍占位("色阶开发中")。已移除"显示/隐藏"(勾选即显隐)。
|
||||||
6. 三维体/切片/异常详情面板(源数据/插值参数/色阶/测量点数体积/异常列表)。
|
6. 三维体/切片/异常详情面板(源数据/插值参数/色阶/测量点数体积/异常列表)。
|
||||||
- **其它小欠项**:三维分析栏完整三级树"对象→三维体→切片"里"对象"根层未套(体目前是顶层);真实色阶编辑。
|
- **其它小欠项**:三维分析栏完整三级树"对象→三维体→切片"里"对象"根层未套(体目前是顶层);真实色阶编辑。
|
||||||
|
|
|
||||||
|
|
@ -63,27 +63,34 @@
|
||||||
- 异常体(consortium)分组:mock 内存(`map<bodyId, {name,typeName,memberIds}>`);真实端点 `exceptionConsortium/*` 后续接。
|
- 异常体(consortium)分组:mock 内存(`map<bodyId, {name,typeName,memberIds}>`);真实端点 `exceptionConsortium/*` 后续接。
|
||||||
- 接口签名不变;后端整链就绪仅换实现。
|
- 接口签名不变;后端整链就绪仅换实现。
|
||||||
|
|
||||||
## 6. 列表面板(R69-88)+ 选中联动 + 过滤
|
## 6. 异常展示与控制的摆放(用户 2026-06-18 定,需求实证 R28/R36/R58-88)
|
||||||
|
|
||||||
扩展 `ObjectExceptionPanel`(或在三维分析视图侧新建异常面板,复用其树构建):
|
需求结构:R58/R67/R69/R90 均为 C1 顶级分节 = **数据详情栏**的各类详情内容;R28/R36「数据详情 → 在数据详情栏显示」。R84 选中联动、R86-87 VTK 显示过滤 = **3D 场景操作**。结论(职责拆分,互补不重复):
|
||||||
- 树:对象 → 异常体 → 异常 + 未分组异常(R71-77)。
|
|
||||||
- 勾选(显隐)、单选(选中) → 信号;选中 ↔ VTK 视图异常高亮**双向联动**(R84)。
|
- **三维分析区 = 3D 异常的"场景控制"**(本期 4c 重点,3D 异常现为 mock):
|
||||||
- 操作(R79):删除异常、删除分组(deleteAnomaly/deleteAnomalyGroup)。
|
- 树:对象 → 三维体 → 异常(异常挂三维体,R61;非切片非源 ds,见记忆 `vtk-3d-persistence-structure`)。
|
||||||
- 异常属性(R83):选中异常 → 详情(名称/类型/坐标/截图/备注)。
|
- **显示过滤 4 档(R86-87)**:全部显示 / 随GS / 随数据集 / 全部隐藏 —— **独立于体勾选**控制 VTK 异常可见性(解决"异常被体勾选绑死")。
|
||||||
- 显示过滤(R86-87):全部显示 / 随GS / 随数据集 / 全部隐藏 → 控制 VTK 异常可见性集合。
|
- 每条异常**单独显隐**(复用 AnomalyListPanel 的"眼睛")。
|
||||||
- 异常属性·截图(R88):展示截图缩略 + "确定截图大小"。
|
- **VTK 选中双向联动(R84)**:列表选中 ↔ VTK 高亮。
|
||||||
|
- 删除异常 / 删除分组(R79-81, deleteAnomaly/deleteAnomalyGroup)。
|
||||||
|
- **右侧「对象异常」面板(现有 `ObjectExceptionPanel`) = 异常全集 master**:对象下所有异常总表。**本期保持不动**(仍连后端 2D 异常);后端三维体/切片/异常整链就绪后,3D 异常并入此处成全集。
|
||||||
|
- **三维体数据详情(R58-65)**:源数据/切片/**异常列表(R61,只读摘要)**/插值参数/色阶/测量——经右键「数据详情」打开。
|
||||||
|
|
||||||
|
> 不在右侧总表里塞 3D 场景控制(过滤/联动属 3D 操作,归三维分析区);不在三维分析区重复全集总表。
|
||||||
|
|
||||||
## 7. main.cpp 编排
|
## 7. main.cpp 编排
|
||||||
|
|
||||||
- 切片右键「创建异常」→ 启动 `AnomalyDrawTool`(用当前选中切片平面) → 圈定完成 → `AnomalySaveDialog` → `scene3dRepo->saveAnomaly` → 渲染(view addAnomaly) + 刷新异常面板。
|
- 切片右键「创建异常」→ 启动 `AnomalyDrawTool`(当前选中切片平面) → 圈定 → `AnomalySaveDialog` → `saveAnomaly` → 渲染(addAnomaly) + 刷新三维分析区异常列表。**[4b 已实现]**
|
||||||
- 当前对象/三维体变化 → `loadAnomalyTree` → 填异常面板 + 渲染已存异常。
|
- 体到场/移除(onVolumeChanged) → `loadAnomalyTree(volumeId)` → 渲染该体已存异常(reloadAnomalies)。**[4b 已实现,= "随数据集" 档默认]**
|
||||||
- 面板选中/勾选/过滤/删除 → 驱动 view 的 setAnomalyVisible/Selected + 仓储删;VTK 点选异常 → 面板选中(联动)。
|
- 三维分析区异常列表:选中/显隐/过滤/删除 → 驱动 view 的 setAnomalyVisible/Selected + 仓储删;VTK 点选异常 → 列表选中(联动)。**[4c]**
|
||||||
|
|
||||||
## 8. 阶段(每阶段编译绿 + 用户实测)
|
## 8. 阶段(每阶段编译绿 + 用户实测)
|
||||||
|
|
||||||
- **4a 基础**:§1 模型 + §2 渲染/接口 + §5 mock 持久化(saveAnomaly/loadAnomalyTree/delete) + main 加载已存异常渲染。可注入一两个测试异常验证 3D 渲染。无圈定/对话框。
|
- **4a 基础 ✅ 已提交(4e1b8e7)**:§1 模型 + §2 渲染/接口 + §5 mock 持久化 + 测试修复(228/228 绿)。
|
||||||
- **4b 圈定+保存**:§3 圈定工具 + §4 保存对话框(含截图) + 切片右键「创建异常」接通 → 闭环:画→存→显示→删。
|
- **4b 圈定+保存 ✅ 已实现(未提交,用户已测通)**:§3 `AnomalyDrawTool`(切片平面圈定,射线-平面求交,左键加点/双击·右键·回车闭合/Esc 取消/屏幕提示) + §4 `AnomalySaveDialog`(名称/类型 mock/备注/截图预览) + 切片右键「创建异常」接通 + onVolumeChanged→reloadAnomalies(随体重载渲染)。闭环:画→存→显示→跨重勾持久。
|
||||||
- **4c 列表/异常体/联动/过滤**:§6 面板交互(选中联动/过滤/删除分组) + 异常体分组 + 异常属性/截图展示。
|
- 同批交互修复(待提交):生成体**按勾选集合**(非行高亮/右键项)、buildVolume 网格**覆盖全程**(跨 TM 多剖面不截断)、滚轮推进选中切片(点切片外取消选中→恢复缩放)。
|
||||||
|
- **4c 三维分析区 3D 异常控制(下一步)**:§6 —— 三维分析区异常树(对象→三维体→异常) + **显示过滤 4 档(R86-87)** + **VTK 选中双向联动(R84)** + 每条显隐 + 删除/删组 + 异常属性(R83)。异常体分组 mock。右侧总表不动。
|
||||||
|
- **后续**:三维体/切片数据详情(R58-65/R67);真实端点整链就绪后切真实(异常并入右侧全集)。
|
||||||
|
|
||||||
## 9. 风险/待确认
|
## 9. 风险/待确认
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,13 @@
|
||||||
- 依据:①《Geopro3.0 需求表.xlsx》「补充需求」页(行号见引用);② 与产品方就 6 个设计问题的确认;③ 现有代码。
|
- 依据:①《Geopro3.0 需求表.xlsx》「补充需求」页(行号见引用);② 与产品方就 6 个设计问题的确认;③ 现有代码。
|
||||||
- 原则:缺后端端点的**先本地 mock**(保证功能可见可用),端点就绪后切真实;能纯客户端做的先做。
|
- 原则:缺后端端点的**先本地 mock**(保证功能可见可用),端点就绪后切真实;能纯客户端做的先做。
|
||||||
|
|
||||||
|
> **⚠ 更正(2026-06-18,本文档以下异常部分已被修订,以此为准)**——实现计划见 `plans/2026-06-18-vtk-3d-anomaly.md`,结构铁律见记忆 `vtk-3d-persistence-structure`:
|
||||||
|
> 1. **异常挂「三维体」**(`remarkSourceId` = 三维体 ds id),**不挂切片**(§1.3 的 `parentSliceId` 作废)——切片是临时圈定载体,业务语义上异常属于三维体(需求 R61)。
|
||||||
|
> 2. **`remarkSourceType` = 标注形态**(1点/2线/3面/4文字),**不是**"来源实体类型"(§3 原表述更正,实证 `commercial-admin/contourPage.vue:386`)。接口不限定挂载实体类型,`remarkSourceId` 放谁 id 挂谁。
|
||||||
|
> 3. 异常请求体**无截图字段**;补充需求 **R88「增加截图属性」**证实截图是待新增属性 → 现 mock 本地存。
|
||||||
|
> 4. **摆放**:3D 异常的"场景控制"(树+显示过滤 R86-87+VTK 选中联动 R84+显隐+删除)放**三维分析区**;右侧「对象异常」面板 = 异常全集 master(暂连后端 2D,整链就绪后并入 3D)。
|
||||||
|
> 5. 异常**独立显隐**靠 R86-87 过滤(全部显示/随GS/随数据集/全部隐藏),**不被三维体勾选绑死**。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 核心数据模型
|
## 1. 核心数据模型
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
#include "AnomalySaveDialog.hpp"
|
||||||
|
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFormLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QPlainTextEdit>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// 异常类型 mock 列表(label, id)。真实 exceptionType 端点只读、后续接。
|
||||||
|
struct TypeItem { const char* label; const char* id; };
|
||||||
|
const TypeItem kMockTypes[] = {
|
||||||
|
{"断层", "mock-fault"},
|
||||||
|
{"破碎带", "mock-fracture"},
|
||||||
|
{"含水构造", "mock-water"},
|
||||||
|
{"其它", "mock-other"},
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
AnomalySaveDialog::AnomalySaveDialog(const QString& screenshotPath, int shotW, int shotH,
|
||||||
|
QWidget* parent)
|
||||||
|
: QDialog(parent) {
|
||||||
|
setWindowTitle(QStringLiteral("保存异常"));
|
||||||
|
setModal(true);
|
||||||
|
|
||||||
|
auto* root = new QVBoxLayout(this);
|
||||||
|
|
||||||
|
auto* form = new QFormLayout();
|
||||||
|
name_ = new QLineEdit(QStringLiteral("异常"));
|
||||||
|
form->addRow(QStringLiteral("名称"), name_);
|
||||||
|
|
||||||
|
type_ = new QComboBox();
|
||||||
|
for (const auto& t : kMockTypes)
|
||||||
|
type_->addItem(QString::fromUtf8(t.label), QString::fromUtf8(t.id));
|
||||||
|
form->addRow(QStringLiteral("异常类型"), type_);
|
||||||
|
|
||||||
|
remark_ = new QPlainTextEdit();
|
||||||
|
remark_->setFixedHeight(60);
|
||||||
|
form->addRow(QStringLiteral("备注"), remark_);
|
||||||
|
root->addLayout(form);
|
||||||
|
|
||||||
|
// 截图预览 + 大小(R50「确定截图大小」)。
|
||||||
|
if (!screenshotPath.isEmpty()) {
|
||||||
|
root->addWidget(new QLabel(QStringLiteral("截图(%1 × %2)").arg(shotW).arg(shotH)));
|
||||||
|
QPixmap pm(screenshotPath);
|
||||||
|
if (!pm.isNull()) {
|
||||||
|
auto* img = new QLabel();
|
||||||
|
img->setPixmap(pm.scaledToWidth(320, Qt::SmoothTransformation));
|
||||||
|
root->addWidget(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||||
|
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
|
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
root->addWidget(buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AnomalySaveDialog::anomalyName() const {
|
||||||
|
const QString n = name_->text().trimmed();
|
||||||
|
return n.isEmpty() ? QStringLiteral("异常") : n;
|
||||||
|
}
|
||||||
|
QString AnomalySaveDialog::typeName() const { return type_->currentText(); }
|
||||||
|
QString AnomalySaveDialog::typeId() const { return type_->currentData().toString(); }
|
||||||
|
QString AnomalySaveDialog::remark() const { return remark_->toPlainText(); }
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
#pragma once
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class QLineEdit;
|
||||||
|
class QComboBox;
|
||||||
|
class QPlainTextEdit;
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
namespace geopro::app {
|
||||||
|
|
||||||
|
// 异常保存对话框(#4b,需求 R50):名称 + 异常类型 + 备注 + 截图预览/大小。
|
||||||
|
// 异常类型本期 mock 列表(真实 exceptionType 端点只读、后续可接)。accept 后取 name/typeName/typeId/remark。
|
||||||
|
class AnomalySaveDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
// screenshotPath:圈定结束截图的本地路径(为空则不显示预览);w/h:截图像素尺寸(R50「确定截图大小」)。
|
||||||
|
AnomalySaveDialog(const QString& screenshotPath, int shotW, int shotH, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
QString anomalyName() const;
|
||||||
|
QString typeName() const;
|
||||||
|
QString typeId() const;
|
||||||
|
QString remark() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLineEdit* name_ = nullptr;
|
||||||
|
QComboBox* type_ = nullptr;
|
||||||
|
QPlainTextEdit* remark_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::app
|
||||||
|
|
@ -64,6 +64,7 @@ add_executable(geopro_desktop WIN32
|
||||||
ObjectFormDialog.cpp
|
ObjectFormDialog.cpp
|
||||||
ImportDatasetDialog.cpp
|
ImportDatasetDialog.cpp
|
||||||
ExportDatasetDialog.cpp
|
ExportDatasetDialog.cpp
|
||||||
|
AnomalySaveDialog.cpp
|
||||||
SettingsDialog.cpp
|
SettingsDialog.cpp
|
||||||
SliceExport.cpp
|
SliceExport.cpp
|
||||||
VolumeParamsDialog.cpp
|
VolumeParamsDialog.cpp
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
#include <vtkNew.h>
|
#include <vtkNew.h>
|
||||||
#include <vtkPNGWriter.h>
|
#include <vtkPNGWriter.h>
|
||||||
#include <vtkPointData.h>
|
#include <vtkPointData.h>
|
||||||
|
#include <vtkRenderWindow.h>
|
||||||
|
#include <vtkWindowToImageFilter.h>
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
|
|
@ -19,6 +21,26 @@ bool exportSliceImagePng(vtkImageData* colorImage, const std::string& path) {
|
||||||
return writer->GetErrorCode() == 0;
|
return writer->GetErrorCode() == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool captureRenderWindowPng(vtkRenderWindow* win, const std::string& path, int& outW, int& outH) {
|
||||||
|
outW = outH = 0;
|
||||||
|
if (win == nullptr || path.empty()) return false;
|
||||||
|
vtkNew<vtkWindowToImageFilter> w2i;
|
||||||
|
w2i->SetInput(win);
|
||||||
|
w2i->ReadFrontBufferOff(); // 用后台缓冲,避免被遮挡污染
|
||||||
|
w2i->Update();
|
||||||
|
if (auto* img = w2i->GetOutput()) {
|
||||||
|
int dims[3];
|
||||||
|
img->GetDimensions(dims);
|
||||||
|
outW = dims[0];
|
||||||
|
outH = dims[1];
|
||||||
|
}
|
||||||
|
vtkNew<vtkPNGWriter> writer;
|
||||||
|
writer->SetFileName(path.c_str());
|
||||||
|
writer->SetInputConnection(w2i->GetOutputPort());
|
||||||
|
writer->Write();
|
||||||
|
return writer->GetErrorCode() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
bool exportSliceDat(vtkImageData* slice, const std::string& path) {
|
bool exportSliceDat(vtkImageData* slice, const std::string& path) {
|
||||||
if (slice == nullptr || path.empty()) return false;
|
if (slice == nullptr || path.empty()) return false;
|
||||||
vtkDataArray* arr = slice->GetPointData() ? slice->GetPointData()->GetScalars() : nullptr;
|
vtkDataArray* arr = slice->GetPointData() ? slice->GetPointData()->GetScalars() : nullptr;
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,16 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
class vtkImageData;
|
class vtkImageData;
|
||||||
|
class vtkRenderWindow;
|
||||||
|
|
||||||
namespace geopro::app {
|
namespace geopro::app {
|
||||||
|
|
||||||
// 把切片"上色后"的 2D RGB 影像写为 PNG(切片右键「导出为图片」= 导出切片本身,非整窗截图)。
|
// 把切片"上色后"的 2D RGB 影像写为 PNG(切片右键「导出为图片」= 导出切片本身,非整窗截图)。
|
||||||
bool exportSliceImagePng(vtkImageData* colorImage, const std::string& path);
|
bool exportSliceImagePng(vtkImageData* colorImage, const std::string& path);
|
||||||
|
|
||||||
|
// 截整个渲染窗口为 PNG(异常标识截图,需求 R88);成功返回 true,并填回截图像素宽高。
|
||||||
|
bool captureRenderWindowPng(vtkRenderWindow* win, const std::string& path, int& outW, int& outH);
|
||||||
|
|
||||||
// 把切片重采样 2D 标量影像写为 .dat 文本网格(行=j、列=i,空格分隔,每格取标量首分量);成功返回 true。
|
// 把切片重采样 2D 标量影像写为 .dat 文本网格(行=j、列=i,空格分隔,每格取标量首分量);成功返回 true。
|
||||||
bool exportSliceDat(vtkImageData* slice, const std::string& path);
|
bool exportSliceDat(vtkImageData* slice, const std::string& path);
|
||||||
|
|
||||||
|
|
|
||||||
100
src/app/main.cpp
100
src/app/main.cpp
|
|
@ -43,6 +43,7 @@
|
||||||
#include <QDate>
|
#include <QDate>
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
|
#include <QDir>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
|
@ -96,10 +97,12 @@
|
||||||
#include "Logging.hpp"
|
#include "Logging.hpp"
|
||||||
#include "PanelHeader.hpp"
|
#include "PanelHeader.hpp"
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
|
#include "AnomalySaveDialog.hpp"
|
||||||
#include "SettingsDialog.hpp"
|
#include "SettingsDialog.hpp"
|
||||||
#include "SliceExport.hpp"
|
#include "SliceExport.hpp"
|
||||||
#include "TopBar.hpp"
|
#include "TopBar.hpp"
|
||||||
#include "VolumeParamsDialog.hpp"
|
#include "VolumeParamsDialog.hpp"
|
||||||
|
#include "interact/AnomalyDrawTool.hpp"
|
||||||
#include "ProjectListDialog.hpp"
|
#include "ProjectListDialog.hpp"
|
||||||
#include "ObjectFormDialog.hpp"
|
#include "ObjectFormDialog.hpp"
|
||||||
#include "ImportDatasetDialog.hpp"
|
#include "ImportDatasetDialog.hpp"
|
||||||
|
|
@ -271,12 +274,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
// 安装自定义拾取样式 + 持活动切片。仅三维 + 有体素可用;切到二维 closeAll。
|
// 安装自定义拾取样式 + 持活动切片。仅三维 + 有体素可用;切到二维 closeAll。
|
||||||
auto* interactionMgr = new geopro::render::interact::InteractionManager(
|
auto* interactionMgr = new geopro::render::interact::InteractionManager(
|
||||||
renderWindowPtr->GetInteractor(), renderWindowPtr, scene->renderer());
|
renderWindowPtr->GetInteractor(), renderWindowPtr, scene->renderer());
|
||||||
|
// 异常圈定工具(#4b):在切片平面上画多边形(高优先级观察者,绘制期独占输入)。
|
||||||
|
auto* anomalyDrawTool = new geopro::render::interact::AnomalyDrawTool(
|
||||||
|
renderWindowPtr->GetInteractor(), scene->renderer());
|
||||||
// sceneView->onVolumeChanged 在三栏接线处设置(把体素 image 推给 InteractionManager,见下)。
|
// sceneView->onVolumeChanged 在三栏接线处设置(把体素 image 推给 InteractionManager,见下)。
|
||||||
// 非 QObject 堆对象统一在此清理,按构造逆序:
|
// 非 QObject 堆对象统一在此清理,按构造逆序(持 interactor 观察者者先析构,防悬挂崩溃):
|
||||||
// interactionMgr(持 interactor/切片观察者) → sceneView(持 scene&) → scene3dRepo → scene。
|
|
||||||
// interactionMgr 先析构:closeAll() 解绑所有切片观察者,再拆 scene/interactor,防悬挂崩溃。
|
|
||||||
QObject::connect(vtkWidget, &QObject::destroyed,
|
QObject::connect(vtkWidget, &QObject::destroyed,
|
||||||
[scene, scene3dRepo, sceneView, interactionMgr]() {
|
[scene, scene3dRepo, sceneView, interactionMgr, anomalyDrawTool]() {
|
||||||
|
delete anomalyDrawTool;
|
||||||
delete interactionMgr;
|
delete interactionMgr;
|
||||||
delete sceneView;
|
delete sceneView;
|
||||||
delete scene3dRepo;
|
delete scene3dRepo;
|
||||||
|
|
@ -381,15 +386,31 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 体素变化(重建/清场)后把体素 image 推给 InteractionManager(切片基底),并调和已保存切片。
|
// 当前活动体的异常重载渲染(#4b):体到场→载其异常 actor;体移除→清空。持久化跨重勾可见。
|
||||||
sceneView->onVolumeChanged = [interactionMgr, sceneView, syncSlices]() {
|
auto reloadAnomalies = [sceneView, scene3dRepo]() {
|
||||||
|
sceneView->clearAnomalies();
|
||||||
|
const std::string vol = sceneView->currentVolumeDsId();
|
||||||
|
if (vol.empty()) return;
|
||||||
|
scene3dRepo->loadAnomalyTree(
|
||||||
|
vol,
|
||||||
|
[sceneView](geopro::data::I3dSceneRepository::AnomalyTree tree) {
|
||||||
|
for (auto& b : tree.bodies)
|
||||||
|
for (auto& a : b.members) sceneView->addAnomaly(a);
|
||||||
|
for (auto& a : tree.loose) sceneView->addAnomaly(a);
|
||||||
|
},
|
||||||
|
[](const std::string&) {});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 体素变化(重建/清场)后把体素 image 推给 InteractionManager(切片基底),并调和已保存切片 + 异常。
|
||||||
|
sceneView->onVolumeChanged = [interactionMgr, sceneView, syncSlices, reloadAnomalies]() {
|
||||||
if (sceneView->hasVolume())
|
if (sceneView->hasVolume())
|
||||||
interactionMgr->setVolumeImage(sceneView->currentVolumeImage(),
|
interactionMgr->setVolumeImage(sceneView->currentVolumeImage(),
|
||||||
sceneView->currentColorScale(), sceneView->currentVmin(),
|
sceneView->currentColorScale(), sceneView->currentVmin(),
|
||||||
sceneView->currentVmax());
|
sceneView->currentVmax());
|
||||||
else
|
else
|
||||||
interactionMgr->setVolumeImage(nullptr, sceneView->currentColorScale(), 0.0, 0.0);
|
interactionMgr->setVolumeImage(nullptr, sceneView->currentColorScale(), 0.0, 0.0);
|
||||||
syncSlices(); // 体到场/移除后(setVolumeImage 已 closeAll)重建当前体下已勾选的切片
|
syncSlices(); // 体到场/移除后重建当前体下已勾选的切片
|
||||||
|
reloadAnomalies(); // 同步重载当前体的异常 actor
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── 三栏抽屉信号 → 控制器/交互(Task 7 接线)──────────────────────────────
|
// ── 三栏抽屉信号 → 控制器/交互(Task 7 接线)──────────────────────────────
|
||||||
|
|
@ -421,7 +442,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
// 保存=按"未保存/已保存"分派(新建+链接+自动勾选 / 覆盖位姿);导出统一为「导出▸图片·dat」;
|
// 保存=按"未保存/已保存"分派(新建+链接+自动勾选 / 覆盖位姿);导出统一为「导出▸图片·dat」;
|
||||||
// 正视/翻转/关闭=接现有交互(关闭已保存切片→onSliceClosed 取消列表勾选);创建异常=占位(#4)。
|
// 正视/翻转/关闭=接现有交互(关闭已保存切片→onSliceClosed 取消列表勾选);创建异常=占位(#4)。
|
||||||
interactionMgr->onSliceContextMenuRequested =
|
interactionMgr->onSliceContextMenuRequested =
|
||||||
[&window, interactionMgr, sceneView, scene3dRepo, refreshAnalysis, drawer]() {
|
[&window, interactionMgr, sceneView, scene3dRepo, refreshAnalysis, drawer, anomalyDrawTool,
|
||||||
|
renderWindowPtr]() {
|
||||||
QMenu menu(&window);
|
QMenu menu(&window);
|
||||||
QAction* aAnomaly = menu.addAction(QStringLiteral("创建异常"));
|
QAction* aAnomaly = menu.addAction(QStringLiteral("创建异常"));
|
||||||
QAction* aSave = menu.addAction(QStringLiteral("保存"));
|
QAction* aSave = menu.addAction(QStringLiteral("保存"));
|
||||||
|
|
@ -439,8 +461,66 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
||||||
if (chosen == aFlip) { interactionMgr->flipView(); return; }
|
if (chosen == aFlip) { interactionMgr->flipView(); return; }
|
||||||
if (chosen == aClose) { interactionMgr->closeSelected(); return; } // →onSliceClosed→取消列表勾选
|
if (chosen == aClose) { interactionMgr->closeSelected(); return; } // →onSliceClosed→取消列表勾选
|
||||||
if (chosen == aAnomaly) {
|
if (chosen == aAnomaly) {
|
||||||
QMessageBox::information(&window, QStringLiteral("创建异常"),
|
// 在选中切片平面上启动圈定(左键逐点、右键/回车闭合、Esc 取消)。
|
||||||
QStringLiteral("异常功能开发中(#4:切片圈定 + 接真实后端端点)。"));
|
namespace ri = geopro::render::interact;
|
||||||
|
int axis = 3;
|
||||||
|
ri::Vec3 o{}, p1{}, p2{};
|
||||||
|
if (!interactionMgr->selectedSlicePlane(axis, o, p1, p2)) return;
|
||||||
|
const ri::Vec3 e1{{p1[0] - o[0], p1[1] - o[1], p1[2] - o[2]}};
|
||||||
|
const ri::Vec3 e2{{p2[0] - o[0], p2[1] - o[1], p2[2] - o[2]}};
|
||||||
|
const ri::Vec3 normal = ri::normalize(ri::cross(e1, e2));
|
||||||
|
const std::string volId = sceneView->currentVolumeDsId();
|
||||||
|
anomalyDrawTool->start(
|
||||||
|
o, normal,
|
||||||
|
[&window, sceneView, scene3dRepo, renderWindowPtr, refreshAnalysis, volId,
|
||||||
|
normal, o](const std::vector<ri::Vec3>& worldPts) {
|
||||||
|
// 草稿异常:先临时渲染(让用户在对话框前看到所画,且截图含异常)。
|
||||||
|
geopro::core::Anomaly a;
|
||||||
|
a.markType = geopro::core::AnomalyMarkType::Polygon;
|
||||||
|
a.volumeDsId = volId;
|
||||||
|
a.lineColor = "#ff3030";
|
||||||
|
a.lineWidth = 2.0;
|
||||||
|
a.dashed = false;
|
||||||
|
a.planeNormal = {normal[0], normal[1], normal[2]};
|
||||||
|
a.planeOrigin = {o[0], o[1], o[2]};
|
||||||
|
for (const auto& p : worldPts) a.worldPts.push_back({p[0], p[1], p[2]});
|
||||||
|
const std::string draftId = "draft-anomaly";
|
||||||
|
a.id = draftId;
|
||||||
|
sceneView->addAnomaly(a);
|
||||||
|
renderWindowPtr->Render();
|
||||||
|
// 截图(含异常)→ 临时文件。
|
||||||
|
const QString shot =
|
||||||
|
QDir(QDir::tempPath()).filePath(QStringLiteral("geopro_anomaly_shot.png"));
|
||||||
|
int sw = 0, sh = 0;
|
||||||
|
geopro::app::captureRenderWindowPng(renderWindowPtr, shot.toStdString(), sw, sh);
|
||||||
|
geopro::app::AnomalySaveDialog dlg(shot, sw, sh, &window);
|
||||||
|
if (dlg.exec() != QDialog::Accepted) {
|
||||||
|
sceneView->removeAnomaly(draftId);
|
||||||
|
renderWindowPtr->Render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
a.id.clear(); // 让仓储生成真实 id
|
||||||
|
a.name = dlg.anomalyName().toStdString();
|
||||||
|
a.typeName = dlg.typeName().toStdString();
|
||||||
|
a.exceptionTypeId = dlg.typeId().toStdString();
|
||||||
|
a.remark = dlg.remark().toStdString();
|
||||||
|
geopro::core::Anomaly finalA = a;
|
||||||
|
scene3dRepo->saveAnomaly(
|
||||||
|
finalA, shot.toStdString(),
|
||||||
|
[sceneView, renderWindowPtr, refreshAnalysis, draftId,
|
||||||
|
finalA](std::string id) mutable {
|
||||||
|
sceneView->removeAnomaly(draftId); // 撤草稿,换真实 id 重渲染
|
||||||
|
finalA.id = id;
|
||||||
|
sceneView->addAnomaly(finalA);
|
||||||
|
renderWindowPtr->Render();
|
||||||
|
refreshAnalysis(); // 列表刷新(4c 异常面板接入后体现)
|
||||||
|
},
|
||||||
|
[&window](const std::string& m) {
|
||||||
|
QMessageBox::warning(&window, QStringLiteral("保存异常"),
|
||||||
|
QString::fromStdString(m));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[]() { /* onCancel:放弃,无需处理 */ });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (chosen == aSave) {
|
if (chosen == aSave) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include <QAbstractItemView>
|
#include <QAbstractItemView>
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
#include <QDebug>
|
||||||
#include <QFormLayout>
|
#include <QFormLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
|
@ -116,11 +117,10 @@ Column3DDataset::Column3DDataset(QWidget* parent) : QWidget(parent) {
|
||||||
root->addLayout(row);
|
root->addLayout(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 数据集列表(可勾选 = 渲染选择;多选高亮 + 右键 = 生成三维体的源选择,两者独立)
|
// 数据集列表(可勾选)。勾选 = 渲染为帘面,同时是「生成三维体」的源集合(右键菜单据勾选集生成)。
|
||||||
list_ = new QTreeWidget();
|
list_ = new QTreeWidget();
|
||||||
list_->setHeaderHidden(true);
|
list_->setHeaderHidden(true);
|
||||||
list_->setRootIsDecorated(true);
|
list_->setRootIsDecorated(true);
|
||||||
list_->setSelectionMode(QAbstractItemView::ExtendedSelection); // Ctrl/Shift 多选源剖面
|
|
||||||
list_->setContextMenuPolicy(Qt::CustomContextMenu);
|
list_->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
applyDatasetCardDelegate(list_);
|
applyDatasetCardDelegate(list_);
|
||||||
connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) {
|
connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) {
|
||||||
|
|
@ -137,15 +137,18 @@ Column3DDataset::Column3DDataset(QWidget* parent) : QWidget(parent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Column3DDataset::showListContextMenu(const QPoint& pos) {
|
void Column3DDataset::showListContextMenu(const QPoint& pos) {
|
||||||
// 收集选中项中"可作三维体源"的数据集(反演剖面类)。
|
// 按**勾选集合**收集"可作三维体源"的数据集(反演剖面类)——与右键点在哪一项无关。
|
||||||
static const QSet<QString> kSourceDdCodes = {QStringLiteral("dd_section"),
|
static const QSet<QString> kSourceDdCodes = {QStringLiteral("dd_section"),
|
||||||
QStringLiteral("dd_inversion_data")};
|
QStringLiteral("dd_inversion_data")};
|
||||||
QStringList sourceIds;
|
QStringList sourceIds;
|
||||||
for (QTreeWidgetItem* item : list_->selectedItems()) {
|
for (QTreeWidgetItemIterator it(list_); *it; ++it) {
|
||||||
const QString ddCode = item->data(0, kDsDdCodeRole).toString();
|
if ((*it)->checkState(0) != Qt::Checked) continue; // 仅勾选项
|
||||||
|
const QString ddCode = (*it)->data(0, kDsDdCodeRole).toString();
|
||||||
if (kSourceDdCodes.contains(ddCode))
|
if (kSourceDdCodes.contains(ddCode))
|
||||||
sourceIds << item->data(0, kDsIdRole).toString();
|
sourceIds << (*it)->data(0, kDsIdRole).toString();
|
||||||
}
|
}
|
||||||
|
qInfo().noquote() << "[volsrc] 按勾选收集源 ds 数 =" << sourceIds.size() << ":"
|
||||||
|
<< sourceIds.join(',');
|
||||||
|
|
||||||
QMenu menu(this);
|
QMenu menu(this);
|
||||||
QAction* gen = menu.addAction(QStringLiteral("生成三维体"));
|
QAction* gen = menu.addAction(QStringLiteral("生成三维体"));
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,18 @@
|
||||||
namespace geopro::core {
|
namespace geopro::core {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// ext(包络长度)/ cell(间距)→ 网格点数,限幅 [1, kMaxVolumeDim]。
|
// 某轴:优先用 cell 间距;若包络 ext 过大致格数超 kMaxVolumeDim,则**放大间距**使 maxDim 格跨满 ext
|
||||||
int clampDim(double ext, double cell) {
|
// (分辨率降低,但**不截断**——否则跨 TM 多剖面相距 > maxDim×cell 时,远端剖面落网格外、丢失)。
|
||||||
|
void fitAxis(double ext, double cell, double& outCell, int& outN) {
|
||||||
|
if (!(ext > 0.0) || !(cell > 0.0)) { outCell = (cell > 0.0 ? cell : 1.0); outN = 1; return; }
|
||||||
int n = static_cast<int>(ext / cell) + 1;
|
int n = static_cast<int>(ext / cell) + 1;
|
||||||
if (n < 1) n = 1;
|
if (n <= kMaxVolumeDim) {
|
||||||
if (n > kMaxVolumeDim) n = kMaxVolumeDim;
|
outCell = cell;
|
||||||
return n;
|
outN = (n < 1) ? 1 : n;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
outN = kMaxVolumeDim;
|
||||||
|
outCell = ext / static_cast<double>(kMaxVolumeDim - 1); // maxDim 格覆盖全 ext
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
@ -36,13 +42,13 @@ BuiltVolume buildVolume(const PointSet& pts, double cellXY, double cellZ,
|
||||||
minz = std::min(minz, pts.z[i]); maxz = std::max(maxz, pts.z[i]);
|
minz = std::min(minz, pts.z[i]); maxz = std::max(maxz, pts.z[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) GridSpec(角点对齐 = 原点取包络最小角)。
|
// 2) GridSpec(角点对齐 = 原点取包络最小角)。间距优先用 cell;包络过大时放大间距以覆盖全程
|
||||||
|
// (fitAxis),避免跨 TM 多剖面相距过远时远端被截断。
|
||||||
GridSpec spec{};
|
GridSpec spec{};
|
||||||
spec.ox = minx; spec.oy = miny; spec.oz = minz;
|
spec.ox = minx; spec.oy = miny; spec.oz = minz;
|
||||||
spec.dx = cellXY; spec.dy = cellXY; spec.dz = cellZ;
|
fitAxis(maxx - minx, cellXY, spec.dx, spec.nx);
|
||||||
spec.nx = clampDim(maxx - minx, cellXY);
|
fitAxis(maxy - miny, cellXY, spec.dy, spec.ny);
|
||||||
spec.ny = clampDim(maxy - miny, cellXY);
|
fitAxis(maxz - minz, cellZ, spec.dz, spec.nz);
|
||||||
spec.nz = clampDim(maxz - minz, cellZ);
|
|
||||||
spec.power = power;
|
spec.power = power;
|
||||||
spec.maxDist = maxDist;
|
spec.maxDist = maxDist;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ struct Anomaly {
|
||||||
std::string consortiumId; // 异常体分组 id(空 = 未分组/loose)
|
std::string consortiumId; // 异常体分组 id(空 = 未分组/loose)
|
||||||
std::string name;
|
std::string name;
|
||||||
std::string typeName; // exceptionTypeName
|
std::string typeName; // exceptionTypeName
|
||||||
|
std::string exceptionTypeId; // 异常类型 id(保存请求 exceptionTypeId)
|
||||||
|
std::string remark; // 备注
|
||||||
AnomalyMarkType markType = AnomalyMarkType::Polyline;
|
AnomalyMarkType markType = AnomalyMarkType::Polyline;
|
||||||
std::vector<Vec2> localPts; // 2D 局部坐标(剖面详情:x=距离, y=深度)
|
std::vector<Vec2> localPts; // 2D 局部坐标(剖面详情:x=距离, y=深度)
|
||||||
// VTK 三维:异常多边形/折线/点的世界 3D 坐标(落在所在切片平面上)+ 平面(法向/一点),
|
// VTK 三维:异常多边形/折线/点的世界 3D 坐标(落在所在切片平面上)+ 平面(法向/一点),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "api/Api3dRepository.hpp"
|
#include "api/Api3dRepository.hpp"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
@ -133,6 +134,10 @@ void Api3dRepository::finalizeVolume(const std::string& dsId, const core::PointS
|
||||||
vmin = stops.front();
|
vmin = stops.front();
|
||||||
vmax = stops.back();
|
vmax = stops.back();
|
||||||
}
|
}
|
||||||
|
qInfo().noquote() << "[volbuild] finalize pts=" << pts.v.size() << "grid"
|
||||||
|
<< bv.spec.nx << "x" << bv.spec.ny << "x" << bv.spec.nz
|
||||||
|
<< "origin" << bv.spec.ox << bv.spec.oy << bv.spec.oz << "spacing"
|
||||||
|
<< bv.spec.dx << bv.spec.dy << bv.spec.dz;
|
||||||
VolumeGrid out{std::move(bv.vol),
|
VolumeGrid out{std::move(bv.vol),
|
||||||
{{bv.spec.ox, bv.spec.oy, bv.spec.oz}},
|
{{bv.spec.ox, bv.spec.oy, bv.spec.oz}},
|
||||||
{{bv.spec.dx, bv.spec.dy, bv.spec.dz}},
|
{{bv.spec.dx, bv.spec.dy, bv.spec.dz}},
|
||||||
|
|
@ -182,9 +187,14 @@ void Api3dRepository::loadVolume(const std::string& dsId,
|
||||||
for (const std::string& srcId : params.sourceDatasetIds) {
|
for (const std::string& srcId : params.sourceDatasetIds) {
|
||||||
loadSection(
|
loadSection(
|
||||||
srcId,
|
srcId,
|
||||||
[this, dsId, params, agg, onOk, onErr](SectionData s) {
|
[this, dsId, srcId, params, agg, onOk, onErr](SectionData s) {
|
||||||
if (agg->failed) return;
|
if (agg->failed) return;
|
||||||
|
const std::size_t before = agg->pts.v.size();
|
||||||
appendGridPoints(s.grid, agg->pts);
|
appendGridPoints(s.grid, agg->pts);
|
||||||
|
qInfo().noquote() << "[volbuild] source" << QString::fromStdString(srcId)
|
||||||
|
<< "grid" << s.grid.nx() << "x" << s.grid.ny() << "-> +"
|
||||||
|
<< (agg->pts.v.size() - before) << "pts (total"
|
||||||
|
<< agg->pts.v.size() << ")";
|
||||||
if (!agg->haveScale) {
|
if (!agg->haveScale) {
|
||||||
agg->scale = s.scale;
|
agg->scale = s.scale;
|
||||||
agg->haveScale = true;
|
agg->haveScale = true;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry
|
||||||
find_package(GDAL CONFIG REQUIRED)
|
find_package(GDAL CONFIG REQUIRED)
|
||||||
add_library(geopro_render STATIC
|
add_library(geopro_render STATIC
|
||||||
Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp ContourBands.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp actors/TerrainActor.cpp actors/AxesActor.cpp
|
Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp ContourBands.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp actors/TerrainActor.cpp actors/AxesActor.cpp
|
||||||
interact/SlicePlaneMath.cpp interact/SliceTool.cpp interact/PickInteractorStyle.cpp interact/InteractionManager.cpp
|
interact/SlicePlaneMath.cpp interact/SliceTool.cpp interact/PickInteractorStyle.cpp interact/InteractionManager.cpp interact/AnomalyDrawTool.cpp
|
||||||
ground/TileMath.cpp)
|
ground/TileMath.cpp)
|
||||||
target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES} GDAL::GDAL)
|
target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES} GDAL::GDAL)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,276 @@
|
||||||
|
#include "interact/AnomalyDrawTool.hpp"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <vtkActor.h>
|
||||||
|
#include <vtkCallbackCommand.h>
|
||||||
|
#include <vtkCellArray.h>
|
||||||
|
#include <vtkCommand.h>
|
||||||
|
#include <vtkNew.h>
|
||||||
|
#include <vtkPoints.h>
|
||||||
|
#include <vtkPolyData.h>
|
||||||
|
#include <vtkPolyDataMapper.h>
|
||||||
|
#include <vtkPolyLine.h>
|
||||||
|
#include <vtkProperty.h>
|
||||||
|
#include <vtkRenderWindowInteractor.h>
|
||||||
|
#include <vtkRenderer.h>
|
||||||
|
#include <vtkTextActor.h>
|
||||||
|
#include <vtkTextProperty.h>
|
||||||
|
|
||||||
|
namespace geopro::render::interact {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr double kEps = 1e-9;
|
||||||
|
constexpr double kObserverPriority = 5.0; // 高于切片 widget 与右键菜单(1.0),绘制期独占
|
||||||
|
constexpr double kDoubleClickMs = 350.0; // 左键双击闭合阈值
|
||||||
|
constexpr int kClickSlopPx = 6; // 双击位置相近阈值(px)
|
||||||
|
|
||||||
|
double nowMs() {
|
||||||
|
return std::chrono::duration<double, std::milli>(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch())
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
AnomalyDrawTool::AnomalyDrawTool(vtkRenderWindowInteractor* interactor, vtkRenderer* renderer)
|
||||||
|
: interactor_(interactor), renderer_(renderer) {}
|
||||||
|
|
||||||
|
AnomalyDrawTool::~AnomalyDrawTool() { removeObservers(); }
|
||||||
|
|
||||||
|
void AnomalyDrawTool::start(const Vec3& planeOrigin, const Vec3& planeNormal,
|
||||||
|
std::function<void(const std::vector<Vec3>&)> onFinish,
|
||||||
|
std::function<void()> onCancel) {
|
||||||
|
if (active_) cancel();
|
||||||
|
origin_ = planeOrigin;
|
||||||
|
normal_ = normalize(planeNormal);
|
||||||
|
onFinish_ = std::move(onFinish);
|
||||||
|
onCancel_ = std::move(onCancel);
|
||||||
|
pts_.clear();
|
||||||
|
lastClickMs_ = -1.0;
|
||||||
|
hasCursor_ = false;
|
||||||
|
active_ = true;
|
||||||
|
installObservers();
|
||||||
|
|
||||||
|
// 屏幕操作提示(左上角),解决"不知如何闭合"。
|
||||||
|
if (renderer_) {
|
||||||
|
hint_ = vtkSmartPointer<vtkTextActor>::New();
|
||||||
|
hint_->SetInput("圈定异常:左键逐点 · 双击或右键完成 · Esc 取消");
|
||||||
|
hint_->GetTextProperty()->SetFontSize(16);
|
||||||
|
hint_->GetTextProperty()->SetColor(1.0, 0.9, 0.0);
|
||||||
|
hint_->GetPositionCoordinate()->SetCoordinateSystemToNormalizedViewport();
|
||||||
|
hint_->GetPositionCoordinate()->SetValue(0.02, 0.94);
|
||||||
|
renderer_->AddViewProp(hint_);
|
||||||
|
if (interactor_) interactor_->Render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnomalyDrawTool::cancel() {
|
||||||
|
if (!active_) return;
|
||||||
|
auto cb = onCancel_;
|
||||||
|
teardownActive(); // 先清理状态,回调里可能再 start
|
||||||
|
if (cb) cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取:清理活动态(移观察者/预览/置 inactive),不触发回调。
|
||||||
|
void AnomalyDrawTool::teardownActive() {
|
||||||
|
removeObservers();
|
||||||
|
if (renderer_) {
|
||||||
|
if (preview_) renderer_->RemoveViewProp(preview_);
|
||||||
|
if (rubber_) renderer_->RemoveViewProp(rubber_);
|
||||||
|
if (hint_) renderer_->RemoveViewProp(hint_);
|
||||||
|
}
|
||||||
|
preview_ = nullptr;
|
||||||
|
rubber_ = nullptr;
|
||||||
|
hint_ = nullptr;
|
||||||
|
active_ = false;
|
||||||
|
hasCursor_ = false;
|
||||||
|
pts_.clear();
|
||||||
|
if (interactor_) interactor_->Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 AnomalyDrawTool::pickOnPlane() const {
|
||||||
|
const int* pos = interactor_->GetEventPosition();
|
||||||
|
// 屏幕点 → 世界近/远点(齐次,需除 w)。
|
||||||
|
auto toWorld = [this](int x, int y, double z) -> Vec3 {
|
||||||
|
renderer_->SetDisplayPoint(static_cast<double>(x), static_cast<double>(y), z);
|
||||||
|
renderer_->DisplayToWorld();
|
||||||
|
double w[4];
|
||||||
|
renderer_->GetWorldPoint(w);
|
||||||
|
if (std::abs(w[3]) > kEps) {
|
||||||
|
w[0] /= w[3]; w[1] /= w[3]; w[2] /= w[3];
|
||||||
|
}
|
||||||
|
return Vec3{{w[0], w[1], w[2]}};
|
||||||
|
};
|
||||||
|
const Vec3 nearP = toWorld(pos[0], pos[1], 0.0);
|
||||||
|
const Vec3 farP = toWorld(pos[0], pos[1], 1.0);
|
||||||
|
const Vec3 dir{{farP[0] - nearP[0], farP[1] - nearP[1], farP[2] - nearP[2]}};
|
||||||
|
const double denom = dot(dir, normal_);
|
||||||
|
if (std::abs(denom) < kEps) return nearP; // 射线平行平面 → 退化用近点
|
||||||
|
// t = ((origin - near)·normal) / (dir·normal)
|
||||||
|
const Vec3 on{{origin_[0] - nearP[0], origin_[1] - nearP[1], origin_[2] - nearP[2]}};
|
||||||
|
const double t = dot(on, normal_) / denom;
|
||||||
|
return Vec3{{nearP[0] + t * dir[0], nearP[1] + t * dir[1], nearP[2] + t * dir[2]}};
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnomalyDrawTool::addVertex() {
|
||||||
|
pts_.push_back(pickOnPlane());
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnomalyDrawTool::updatePreview() {
|
||||||
|
if (!renderer_) return;
|
||||||
|
if (preview_) renderer_->RemoveViewProp(preview_);
|
||||||
|
preview_ = nullptr;
|
||||||
|
if (pts_.empty()) {
|
||||||
|
interactor_->Render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vtkNew<vtkPoints> points;
|
||||||
|
points->SetNumberOfPoints(static_cast<vtkIdType>(pts_.size()));
|
||||||
|
for (std::size_t i = 0; i < pts_.size(); ++i)
|
||||||
|
points->SetPoint(static_cast<vtkIdType>(i), pts_[i][0], pts_[i][1], pts_[i][2]);
|
||||||
|
vtkNew<vtkPolyData> poly;
|
||||||
|
poly->SetPoints(points);
|
||||||
|
// 顶点圆点:每点一个 vtkVertex → 单点也可见(解决"第一下看不到点在哪")。
|
||||||
|
vtkNew<vtkCellArray> verts;
|
||||||
|
for (std::size_t i = 0; i < pts_.size(); ++i) {
|
||||||
|
const auto id = static_cast<vtkIdType>(i);
|
||||||
|
verts->InsertNextCell(1, &id);
|
||||||
|
}
|
||||||
|
poly->SetVerts(verts);
|
||||||
|
// 实线折线(≥2 点)。
|
||||||
|
if (pts_.size() >= 2) {
|
||||||
|
vtkNew<vtkPolyLine> line;
|
||||||
|
line->GetPointIds()->SetNumberOfIds(static_cast<vtkIdType>(pts_.size()));
|
||||||
|
for (std::size_t i = 0; i < pts_.size(); ++i)
|
||||||
|
line->GetPointIds()->SetId(static_cast<vtkIdType>(i), static_cast<vtkIdType>(i));
|
||||||
|
vtkNew<vtkCellArray> cells;
|
||||||
|
cells->InsertNextCell(line);
|
||||||
|
poly->SetLines(cells);
|
||||||
|
}
|
||||||
|
vtkNew<vtkPolyDataMapper> mapper;
|
||||||
|
mapper->SetInputData(poly);
|
||||||
|
mapper->ScalarVisibilityOff();
|
||||||
|
preview_ = vtkSmartPointer<vtkActor>::New();
|
||||||
|
preview_->SetMapper(mapper);
|
||||||
|
preview_->GetProperty()->SetColor(1.0, 0.9, 0.0); // 亮黄
|
||||||
|
preview_->GetProperty()->SetLineWidth(2.0);
|
||||||
|
preview_->GetProperty()->SetPointSize(9.0); // 醒目圆点
|
||||||
|
renderer_->AddActor(preview_);
|
||||||
|
interactor_->Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnomalyDrawTool::updateRubber() {
|
||||||
|
if (!renderer_) return;
|
||||||
|
if (rubber_) renderer_->RemoveViewProp(rubber_);
|
||||||
|
rubber_ = nullptr;
|
||||||
|
if (pts_.empty() || !hasCursor_) {
|
||||||
|
if (interactor_) interactor_->Render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 末点 → 当前光标投影点 的虚线橡皮筋(跟手反馈)。
|
||||||
|
const Vec3& a = pts_.back();
|
||||||
|
vtkNew<vtkPoints> points;
|
||||||
|
points->SetNumberOfPoints(2);
|
||||||
|
points->SetPoint(0, a[0], a[1], a[2]);
|
||||||
|
points->SetPoint(1, cursorPt_[0], cursorPt_[1], cursorPt_[2]);
|
||||||
|
vtkNew<vtkPolyData> poly;
|
||||||
|
poly->SetPoints(points);
|
||||||
|
vtkNew<vtkPolyLine> line;
|
||||||
|
line->GetPointIds()->SetNumberOfIds(2);
|
||||||
|
line->GetPointIds()->SetId(0, 0);
|
||||||
|
line->GetPointIds()->SetId(1, 1);
|
||||||
|
vtkNew<vtkCellArray> cells;
|
||||||
|
cells->InsertNextCell(line);
|
||||||
|
poly->SetLines(cells);
|
||||||
|
vtkNew<vtkPolyDataMapper> mapper;
|
||||||
|
mapper->SetInputData(poly);
|
||||||
|
mapper->ScalarVisibilityOff();
|
||||||
|
rubber_ = vtkSmartPointer<vtkActor>::New();
|
||||||
|
rubber_->SetMapper(mapper);
|
||||||
|
rubber_->GetProperty()->SetColor(1.0, 0.9, 0.0);
|
||||||
|
rubber_->GetProperty()->SetLineWidth(1.5);
|
||||||
|
rubber_->GetProperty()->SetLineStipplePattern(0xF0F0); // 虚线
|
||||||
|
rubber_->GetProperty()->SetLineStippleRepeatFactor(1);
|
||||||
|
renderer_->AddActor(rubber_);
|
||||||
|
interactor_->Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnomalyDrawTool::finish() {
|
||||||
|
if (pts_.size() < 3) { // 不足以成面 → 取消
|
||||||
|
cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::vector<Vec3> result = pts_;
|
||||||
|
auto cb = onFinish_;
|
||||||
|
teardownActive();
|
||||||
|
if (cb) cb(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnomalyDrawTool::installObservers() {
|
||||||
|
if (!interactor_) return;
|
||||||
|
cmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
|
||||||
|
cmd_->SetClientData(this);
|
||||||
|
cmd_->SetCallback([](vtkObject*, unsigned long eid, void* client, void*) {
|
||||||
|
auto* self = static_cast<AnomalyDrawTool*>(client);
|
||||||
|
if (!self->active_) return;
|
||||||
|
if (eid == vtkCommand::MouseMoveEvent) {
|
||||||
|
// 鼠标移动:更新末点→光标的虚线橡皮筋(跟手反馈)。不 abort,不干扰其它悬停。
|
||||||
|
self->cursorPt_ = self->pickOnPlane();
|
||||||
|
self->hasCursor_ = true;
|
||||||
|
self->updateRubber();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 先消费事件(abort)再处理:finish()/cancel() 内 teardown 会置空 cmd_,若 abort 留到末尾会被跳过,
|
||||||
|
// 导致触发闭合的那次按键漏给切片 widget → widget 当左键按下开始 slice-motion(鼠标一动切片就动)。
|
||||||
|
if (self->cmd_) self->cmd_->SetAbortFlag(1);
|
||||||
|
switch (eid) {
|
||||||
|
case vtkCommand::LeftButtonPressEvent: {
|
||||||
|
// 左键双连击 = 闭合(标准多边形交互);否则加顶点。
|
||||||
|
const double now = nowMs();
|
||||||
|
const int* p = self->interactor_->GetEventPosition();
|
||||||
|
const bool dbl = self->lastClickMs_ >= 0.0 &&
|
||||||
|
(now - self->lastClickMs_) < kDoubleClickMs &&
|
||||||
|
std::abs(p[0] - self->lastClickX_) <= kClickSlopPx &&
|
||||||
|
std::abs(p[1] - self->lastClickY_) <= kClickSlopPx;
|
||||||
|
self->lastClickMs_ = now;
|
||||||
|
self->lastClickX_ = p[0];
|
||||||
|
self->lastClickY_ = p[1];
|
||||||
|
if (dbl)
|
||||||
|
self->finish();
|
||||||
|
else
|
||||||
|
self->addVertex();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case vtkCommand::RightButtonPressEvent: self->finish(); break;
|
||||||
|
case vtkCommand::KeyPressEvent: {
|
||||||
|
const char* key = self->interactor_->GetKeySym();
|
||||||
|
if (key && (std::string(key) == "Escape")) self->cancel();
|
||||||
|
else if (key && (std::string(key) == "Return")) self->finish();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tagLeft_ = interactor_->AddObserver(vtkCommand::LeftButtonPressEvent, cmd_, kObserverPriority);
|
||||||
|
tagRight_ = interactor_->AddObserver(vtkCommand::RightButtonPressEvent, cmd_, kObserverPriority);
|
||||||
|
tagKey_ = interactor_->AddObserver(vtkCommand::KeyPressEvent, cmd_, kObserverPriority);
|
||||||
|
tagMove_ = interactor_->AddObserver(vtkCommand::MouseMoveEvent, cmd_, kObserverPriority);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnomalyDrawTool::removeObservers() {
|
||||||
|
if (interactor_) {
|
||||||
|
if (tagLeft_) interactor_->RemoveObserver(tagLeft_);
|
||||||
|
if (tagRight_) interactor_->RemoveObserver(tagRight_);
|
||||||
|
if (tagKey_) interactor_->RemoveObserver(tagKey_);
|
||||||
|
if (tagMove_) interactor_->RemoveObserver(tagMove_);
|
||||||
|
if (tagDbl_) interactor_->RemoveObserver(tagDbl_);
|
||||||
|
}
|
||||||
|
tagLeft_ = tagRight_ = tagKey_ = tagMove_ = tagDbl_ = 0;
|
||||||
|
cmd_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::render::interact
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <vtkSmartPointer.h>
|
||||||
|
|
||||||
|
#include "interact/SlicePlaneMath.hpp"
|
||||||
|
|
||||||
|
class vtkRenderWindowInteractor;
|
||||||
|
class vtkRenderer;
|
||||||
|
class vtkActor;
|
||||||
|
class vtkTextActor;
|
||||||
|
class vtkCallbackCommand;
|
||||||
|
|
||||||
|
namespace geopro::render::interact {
|
||||||
|
|
||||||
|
// 异常圈定工具(#4b):在给定切片平面上交互式画多边形。
|
||||||
|
// 左键逐点加顶点(屏幕射线与平面求交,落在平面上);右键 / 双击 / 回车 闭合 → onFinish(worldPts);
|
||||||
|
// Esc / 不足 3 点闭合 → onCancel。绘制中实时预览折线。
|
||||||
|
// 高优先级(2.0)交互器观察者抢输入:先于切片 widget 与 InteractionManager 右键菜单,绘制期独占左右键。
|
||||||
|
// render 层:只碰 VTK,不认业务;产物(平面上的世界点)经回调交上层组装 core::Anomaly。
|
||||||
|
class AnomalyDrawTool {
|
||||||
|
public:
|
||||||
|
AnomalyDrawTool(vtkRenderWindowInteractor* interactor, vtkRenderer* renderer);
|
||||||
|
~AnomalyDrawTool();
|
||||||
|
|
||||||
|
AnomalyDrawTool(const AnomalyDrawTool&) = delete;
|
||||||
|
AnomalyDrawTool& operator=(const AnomalyDrawTool&) = delete;
|
||||||
|
|
||||||
|
// 开始在平面(origin/normal)上圈定。onFinish 收闭合多边形顶点(世界系);onCancel 取消。
|
||||||
|
void start(const Vec3& planeOrigin, const Vec3& planeNormal,
|
||||||
|
std::function<void(const std::vector<Vec3>&)> onFinish,
|
||||||
|
std::function<void()> onCancel);
|
||||||
|
bool active() const { return active_; }
|
||||||
|
void cancel(); // 外部强制取消(如切走视图)
|
||||||
|
|
||||||
|
private:
|
||||||
|
void addVertex(); // 左键:加顶点
|
||||||
|
void updatePreview(); // 重建已点几何(顶点圆点 + 实线折线;单点也可见)
|
||||||
|
void updateRubber(); // 鼠标移动:末点→光标的虚线橡皮筋
|
||||||
|
void finish(); // 右键/双击/回车:闭合
|
||||||
|
Vec3 pickOnPlane() const; // 当前鼠标屏幕点 → 射线与平面交点
|
||||||
|
|
||||||
|
void installObservers();
|
||||||
|
void removeObservers();
|
||||||
|
void teardownActive(); // 清理活动态(移观察者/预览/置inactive),不触发回调
|
||||||
|
|
||||||
|
vtkRenderWindowInteractor* interactor_;
|
||||||
|
vtkRenderer* renderer_;
|
||||||
|
|
||||||
|
bool active_ = false;
|
||||||
|
Vec3 origin_{{0, 0, 0}}, normal_{{0, 0, 1}};
|
||||||
|
std::vector<Vec3> pts_;
|
||||||
|
std::function<void(const std::vector<Vec3>&)> onFinish_;
|
||||||
|
std::function<void()> onCancel_;
|
||||||
|
|
||||||
|
vtkSmartPointer<vtkActor> preview_; // 已点几何(顶点圆点 + 实线折线)
|
||||||
|
vtkSmartPointer<vtkActor> rubber_; // 末点→光标 虚线橡皮筋
|
||||||
|
vtkSmartPointer<vtkTextActor> hint_; // 屏幕操作提示
|
||||||
|
Vec3 cursorPt_{{0, 0, 0}}; // 当前鼠标在切面上的投影点
|
||||||
|
bool hasCursor_ = false;
|
||||||
|
vtkSmartPointer<vtkCallbackCommand> cmd_;
|
||||||
|
unsigned long tagLeft_ = 0, tagMove_ = 0, tagRight_ = 0, tagKey_ = 0, tagDbl_ = 0;
|
||||||
|
// 双击闭合检测(左键两连击):记上次左键时刻 + 屏幕位置。
|
||||||
|
double lastClickMs_ = -1.0;
|
||||||
|
int lastClickX_ = 0, lastClickY_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::render::interact
|
||||||
|
|
@ -281,24 +281,25 @@ vtkSmartPointer<vtkImageData> InteractionManager::selectedSliceColorImage() cons
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InteractionManager::handleRightButton() {
|
int InteractionManager::pickSliceAtCursor() const {
|
||||||
// 高优先级右键观察者(先于 vtkImagePlaneWidget 消费右键)。
|
if (!interactor_ || slices_.empty()) return -1;
|
||||||
// 选中目标 = 拾取命中的切片;拾取没命中切片平面(实测常因拾到体/其它面而落在阈值外)则
|
|
||||||
// 回退到"当前选中切片"(左键交互/新建已选中)。有可操作切片 → abort 右键 + 弹菜单;否则放行。
|
|
||||||
if (!interactor_) return;
|
|
||||||
|
|
||||||
int idx = -1;
|
|
||||||
const int* pos = interactor_->GetEventPosition();
|
const int* pos = interactor_->GetEventPosition();
|
||||||
auto* ren = interactor_->FindPokedRenderer(pos[0], pos[1]);
|
auto* ren = interactor_->FindPokedRenderer(pos[0], pos[1]);
|
||||||
if (ren) {
|
if (!ren) return -1;
|
||||||
vtkNew<vtkCellPicker> picker;
|
vtkNew<vtkCellPicker> picker;
|
||||||
picker->SetTolerance(0.005);
|
picker->SetTolerance(0.005);
|
||||||
if (picker->Pick(pos[0], pos[1], 0.0, ren)) {
|
if (!picker->Pick(pos[0], pos[1], 0.0, ren)) return -1;
|
||||||
double w[3];
|
double w[3];
|
||||||
picker->GetPickPosition(w);
|
picker->GetPickPosition(w);
|
||||||
idx = nearestSlice({w[0], w[1], w[2]});
|
return nearestSlice({w[0], w[1], w[2]});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
void InteractionManager::handleRightButton() {
|
||||||
|
// 高优先级右键观察者(先于 vtkImagePlaneWidget 消费右键)。
|
||||||
|
// 选中目标 = 拾取命中的切片;拾取没命中(常因拾到体/其它面)则回退到"当前选中切片"。
|
||||||
|
// 有可操作切片 → abort 右键 + 弹菜单;否则放行默认右键。
|
||||||
|
if (!interactor_) return;
|
||||||
|
int idx = pickSliceAtCursor();
|
||||||
if (idx < 0) idx = selected_; // 回退到当前选中切片
|
if (idx < 0) idx = selected_; // 回退到当前选中切片
|
||||||
if (idx < 0 || idx >= static_cast<int>(slices_.size())) return; // 无切片可操作 → 放行默认右键
|
if (idx < 0 || idx >= static_cast<int>(slices_.size())) return; // 无切片可操作 → 放行默认右键
|
||||||
selected_ = idx;
|
selected_ = idx;
|
||||||
|
|
@ -329,14 +330,10 @@ int InteractionManager::nearestSlice(const Vec3& worldPoint) const {
|
||||||
}
|
}
|
||||||
|
|
||||||
void InteractionManager::onPicked(const Vec3& worldPoint) {
|
void InteractionManager::onPicked(const Vec3& worldPoint) {
|
||||||
// 单击 = 仅选中命中切片 + 高亮,**不动相机** → 切换切片永不跳。
|
// 单击 = 选中命中切片;点在切片外(如点到体/帘面)→ 取消选中(idx=-1)。**不动相机**。
|
||||||
// 拖动旋转交给默认 TrackballCamera(绕场景/体中心,稳定)。曾试"按切片中心移焦点"以实现
|
// 解决"选了切片无法取消":点击切片之外即清选中,滚轮恢复缩放(见 onWheel)。
|
||||||
// spec C38'以切片为中心',但切片中心≈体中心→与默认视觉等价、却引入切换跳动,得不偿失,故去除。
|
selected_ = nearestSlice(worldPoint);
|
||||||
const int idx = nearestSlice(worldPoint);
|
|
||||||
if (idx >= 0) {
|
|
||||||
selected_ = idx;
|
|
||||||
updateSelectionVisual();
|
updateSelectionVisual();
|
||||||
}
|
|
||||||
safeRender();
|
safeRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -367,11 +364,14 @@ void InteractionManager::faceSlice(int idx) {
|
||||||
|
|
||||||
|
|
||||||
bool InteractionManager::onWheel(int dir) {
|
bool InteractionManager::onWheel(int dir) {
|
||||||
|
// 滚轮推进**当前选中**的切片(需先显式选中);无选中 → 不消费 → 相机缩放。
|
||||||
|
// 配合 onPicked 的"点击切片外取消选中":取消后滚轮即恢复缩放,解决"选了切片无法缩放"。
|
||||||
|
// (不采用"悬停即推进":推进时鼠标难持续压在移动的切片上,且过敏感。)
|
||||||
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return false;
|
if (selected_ < 0 || selected_ >= static_cast<int>(slices_.size())) return false;
|
||||||
const double step = wheelStep(imageBounds(image_), dir);
|
const double step = wheelStep(imageBounds(image_), dir);
|
||||||
slices_[static_cast<std::size_t>(selected_)]->advance(step);
|
slices_[static_cast<std::size_t>(selected_)]->advance(step);
|
||||||
safeRender();
|
safeRender();
|
||||||
return true; // 消费滚轮(不缩放)
|
return true; // 消费滚轮(推进选中切片,不缩放)
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace geopro::render::interact
|
} // namespace geopro::render::interact
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,8 @@ private:
|
||||||
|
|
||||||
// 找离世界点最近的切片索引;无切片返回 -1。
|
// 找离世界点最近的切片索引;无切片返回 -1。
|
||||||
int nearestSlice(const Vec3& worldPoint) const;
|
int nearestSlice(const Vec3& worldPoint) const;
|
||||||
|
// 在当前鼠标屏幕位置拾取 → 命中的切片索引;未命中切片返回 -1。
|
||||||
|
int pickSliceAtCursor() const;
|
||||||
// 按 SliceTool 指针设为选中(widget 交互回调用:触碰即选中)。
|
// 按 SliceTool 指针设为选中(widget 交互回调用:触碰即选中)。
|
||||||
void selectByTool(const SliceTool* tool);
|
void selectByTool(const SliceTool* tool);
|
||||||
// 相机正视给定切面(focal=center, 沿 normal 退 dist)。
|
// 相机正视给定切面(focal=center, 沿 normal 退 dist)。
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue