From bdebe54859fd5bca6dbc01695b4fa9da6392353b Mon Sep 17 00:00:00 2001 From: gaozheng Date: Fri, 26 Jun 2026 21:56:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(3d-view):=20=E4=BA=8C=E7=BB=B4=E5=88=86?= =?UTF-8?q?=E6=9E=90B=E6=9C=9F(=E8=B6=B3=E8=BF=B9=E9=AB=98=E7=A8=8BZ?= =?UTF-8?q?=E6=8B=96=E5=8A=A8)+=E9=80=89=E6=8B=A9=E8=81=94=E5=8A=A8/?= =?UTF-8?q?=E6=BB=9A=E8=BD=AE=E5=8D=87=E9=99=8D/=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=9D=A1=E7=A6=81=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - B期:二维分析里选中足迹(单击/Ctrl 多选)→ 竖向拖动只改世界 Z(锁 XY)、 顶部实时高程读数浮层;Z 偏移按 dsId 持久(切走再回/全量重建保留)。 VtkSceneView 加 pickMapLineAt/nudgeSelectedMapLinesZ/selectedMapLineZ(vtkCellPicker +PickFromList 只拾可见足迹、选中黄高亮加粗、mapLineZOffset_ 持久);PickInteractorStyle lock2D 下命中足迹→Z 拖动(onPick2D/onDrag2D/onDrag2DEnd + worldPerPixelZ 像素→世界Z); InteractionManager::pickStyle() 暴露样式;main.cpp 接回调 + 读数浮层。 - 列表↔VTK 双向选择联动:Column2DDataset 多选行 + selectedDatasetsChanged/setSelectedDsIds; VtkSceneView onMapLineSelectionChanged/setSelectedMapLines;两向各自断环。 - 滚轮升降:onWheel2D——有选中足迹时滚轮改其 Z(一格≈拖动24px)、消费滚轮,否则缩放; 读数浮层滚轮后 1.2s 自动隐藏。 - 工具条:二维分析激活禁用 6 向快捷视图(会改朝向破坏近俯视锁定),切回三维恢复。 --- ...DOFF-2026-06-26-vtk-anomaly-2d-analysis.md | 7 +- src/app/VtkSceneView.cpp | 117 +++++++++++++++++- src/app/VtkSceneView.hpp | 21 ++++ src/app/VtkViewToolbar.cpp | 13 +- src/app/VtkViewToolbar.hpp | 11 ++ src/app/main.cpp | 60 ++++++++- src/app/panels/columns/Column2DDataset.cpp | 15 +++ src/app/panels/columns/Column2DDataset.hpp | 5 +- src/render/interact/InteractionManager.cpp | 2 + src/render/interact/InteractionManager.hpp | 4 + src/render/interact/PickInteractorStyle.cpp | 62 +++++++++- src/render/interact/PickInteractorStyle.hpp | 19 +++ 12 files changed, 326 insertions(+), 10 deletions(-) diff --git a/docs/superpowers/HANDOFF-2026-06-26-vtk-anomaly-2d-analysis.md b/docs/superpowers/HANDOFF-2026-06-26-vtk-anomaly-2d-analysis.md index 9a3792a..4aeddbf 100644 --- a/docs/superpowers/HANDOFF-2026-06-26-vtk-anomaly-2d-analysis.md +++ b/docs/superpowers/HANDOFF-2026-06-26-vtk-anomaly-2d-analysis.md @@ -9,9 +9,10 @@ - **A(已实现 ✅,build+439测试全绿,未提交)**:一场景两相机。切「二维分析」tab → 近俯视(下压12°≈78°俯角)+禁旋转(左键改平移、仅平移/缩放);按维度翻 actor `SetVisibility`(轨迹↔体/帘面/异常,**不清空**);切片 `SetEnabled` 显隐(不销毁);地形+底图常驻;切回三维还原相机快照。**待用户实跑**:①近俯视角度是否合适②切换是否瞬时③左键平移手感④切回三维视角还原是否自然。 - 改动文件:`CameraPreset.{hpp,cpp}`(applyNearTop2D)、`PickInteractorStyle.{hpp,cpp}`(setLock2D)、`SliceTool.{hpp,cpp}`(setVisible)、`InteractionManager.{hpp,cpp}`(setMode2D)、`VtkSceneView.{hpp,cpp}`(setAnalysisMode2D+mapLineDs_+相机快照)、`ColumnDrawer.{hpp,cpp}`(analysisModeChanged 信号)、`main.cpp`(接信号)。 - 已知小风险:2D 取景 `computeDataBounds` 含隐藏的 3D 体包围盒(地形主导,影响小);切片 `SetEnabled` 显隐属 GUI 不可自测项。 -- **B(下一步)**:二维里选中 2D 内容(单/多选)→ 竖向拖动只改**高程 Z**、锁 XY、实时高程读数。锚点:新增 2D 拾取-拖动交互(仅 Z 平移),可参考切片 widget;用 `PickInteractorStyle` 在 lock2D 下保留拾取(A 期为简化已禁拾取,B 期需放开 2D 内容拾取)。 - - **注意**:A 期 lock2D 下 `OnLeftButtonDown` 直接 StartPan、跳过拾取。B 期要支持选中 2D 内容拖动,需改为「命中 2D 足迹→进入 Z 拖动;否则平移」。 -- **C**:dd_raster 纳入 2D 过滤 + 按 ddCode 分派渲染 + 栅格地理配准贴地形。**阻塞:dd_raster 数据端点未确认**。 +- **B(已实现 ✅,build+441测试全绿,未提交,待实跑)**:二维里选中足迹(单/Ctrl 多选)→ 竖向拖动只改**高程 Z**、锁 XY、顶部实时高程读数浮层;Z 偏移按 dsId 持久(切走再回/全量重建保留)。手势:单击足迹=选中、Ctrl+单击=多选切换、点空白=取消+平移、(多)选后竖向拖动=整体改 Z。 + - 实现:`VtkSceneView` 加 `pickMapLineAt/nudgeSelectedMapLinesZ/selectedMapLineZ/clearMapLineSelection`(vtkCellPicker+PickFromList 只拾可见足迹、选中高亮黄加粗、`mapLineZOffset_` 持久);`PickInteractorStyle` lock2D 下命中足迹→Z 拖动(`onPick2D/onDrag2D/onDrag2DEnd`+`worldPerPixelZ` 像素→世界Z)、否则平移;`InteractionManager::pickStyle()` 暴露样式;`main.cpp` 接回调 + 高程读数浮层(复用提示样式)。 + - **待用户实跑**:①拾取灵敏度(tol 0.012)②拖动 Z 灵敏度/方向(上移=抬高)③多选拖动④读数是否合理(现为 actor 包围盒中心世界 Z,含 placement+偏移,未除 VE)。 +- **C(下一步)**:dd_raster 纳入 2D 过滤 + 按 ddCode 分派渲染 + 栅格地理配准贴地形。**阻塞:dd_raster 数据端点未确认**(需后端给「像素 + 四至/投影」端点)。 --- diff --git a/src/app/VtkSceneView.cpp b/src/app/VtkSceneView.cpp index 0204fe5..e7f26e7 100644 --- a/src/app/VtkSceneView.cpp +++ b/src/app/VtkSceneView.cpp @@ -11,7 +11,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -104,7 +106,8 @@ void VtkSceneView::clear() { // 只移除数据 prop(按 ds 跟踪)+ 杂项(地形/测线)+ 坐标轴;不动底图(TileBasemap 自管)→ 重建不丢图。 for (auto& kv : dsProps_) removeProps(kv.second); dsProps_.clear(); - mapLineDs_.clear(); // 2D 足迹维度记录随数据图元一并清(模式标志/相机快照保留) + mapLineDs_.clear(); // 2D 足迹维度记录随数据图元一并清(模式标志保留) + selectedMapLines_.clear(); // 选中态随图元清(actor 已销毁);Z 偏移 mapLineZOffset_ 保留→重建后复位高度 removeProps(miscProps_); clearAnomalies(); // 异常 actor 随清场一并移除 if (currentAxes_) { @@ -193,6 +196,8 @@ void VtkSceneView::addMapLine(const std::string& dsId, const geopro::data::MapLi auto actor = geopro::render::buildMapLine(line.lat, line.lon, worldZ, *frame_); if (actor) { actor->SetVisibility(analysisMode2D_ ? 1 : 0); // 足迹=2D内容:仅二维分析下显示 + auto off = mapLineZOffset_.find(dsId); // B 期:复用持久 Z 偏移(全量重建后仍在该高度) + if (off != mapLineZOffset_.end()) actor->AddPosition(0.0, 0.0, off->second); scene_.addActor(actor); dsProps_[dsId].push_back(actor); mapLineDs_.insert(dsId); // 记录此 ds 为 2D 足迹(切 tab 按维度翻可见) @@ -317,6 +322,7 @@ void VtkSceneView::fitView() { void VtkSceneView::setAnalysisMode2D(bool is2D) { if (is2D == analysisMode2D_) return; // 幂等:同模式重复切不做事 analysisMode2D_ = is2D; + if (!is2D) clearMapLineSelection(); // 离开二维分析:清足迹选中(三维下不可拖 Z);Z 偏移仍持久 // ① 按维度翻可见标志(不清空、不重建→切换瞬时):2D 足迹↔3D 帘面/体;异常属 3D。 // 地形/测线(miscProps_)与底图(TileBasemap 自管)两边常驻、不动。 @@ -335,6 +341,115 @@ void VtkSceneView::setAnalysisMode2D(bool is2D) { render(/*is2D ViewMode=*/false, /*resetCamera=*/true); } +// ── 二维分析改造 B 期:选中 2D 足迹沿高程 Z 拖动 ─────────────────────────────────── +void VtkSceneView::applyMapLineSelectionVisual() { + for (auto& kv : dsProps_) { + if (!mapLineDs_.count(kv.first)) continue; + const bool sel = selectedMapLines_.count(kv.first) > 0; + for (auto& p : kv.second) { + auto* a = vtkActor::SafeDownCast(p); + if (!a) continue; + if (sel) { // 选中:黄高亮 + 加粗 + a->GetProperty()->SetColor(1.0, 0.85, 0.2); + a->GetProperty()->SetLineWidth(6.0); + } else { // 未选:复原 buildMapLine 默认(橙 3.0) + a->GetProperty()->SetColor(0.95, 0.55, 0.10); + a->GetProperty()->SetLineWidth(3.0); + } + } + } +} + +void VtkSceneView::clearMapLineSelection() { + if (selectedMapLines_.empty()) return; + selectedMapLines_.clear(); + applyMapLineSelectionVisual(); + if (renderWindow_) renderWindow_->Render(); + if (onMapLineSelectionChanged) onMapLineSelectionChanged(); // VTK→列表:同步清空 +} + +std::vector VtkSceneView::selectedMapLines() const { + return std::vector(selectedMapLines_.begin(), selectedMapLines_.end()); +} + +void VtkSceneView::setSelectedMapLines(const std::vector& dsIds) { + // 列表→VTK:按 dsId 设选中(仅已渲染足迹),高亮+渲染;不回调 onMapLineSelectionChanged(防回环)。 + selectedMapLines_.clear(); + for (const auto& id : dsIds) + if (mapLineDs_.count(id)) selectedMapLines_.insert(id); + applyMapLineSelectionVisual(); + if (renderWindow_) renderWindow_->Render(); +} + +bool VtkSceneView::pickMapLineAt(int screenX, int screenY, bool additive) { + auto* ren = scene_.renderer(); + if (!ren) return false; + // 只在"可见足迹"中拾取(PickFromList):避免地形/底图/隐藏的 3D 体抢命中。 + vtkNew picker; + picker->SetTolerance(0.012); + picker->PickFromListOn(); + bool any = false; + for (auto& kv : dsProps_) { + if (!mapLineDs_.count(kv.first)) continue; + for (auto& p : kv.second) + if (p && p->GetVisibility()) { picker->AddPickList(p); any = true; } + } + if (!any) return false; // 无可见足迹 → 不拦截(交由平移) + if (!picker->Pick(screenX, screenY, 0.0, ren)) { + if (!additive) clearMapLineSelection(); // 点空白(非多选)→ 取消选中 + return false; + } + vtkProp* hit = picker->GetViewProp(); + std::string hitDs; + for (auto& kv : dsProps_) { + if (!mapLineDs_.count(kv.first)) continue; + for (auto& p : kv.second) + if (p.Get() == hit) { hitDs = kv.first; break; } + if (!hitDs.empty()) break; + } + if (hitDs.empty()) { + if (!additive) clearMapLineSelection(); + return false; + } + if (additive) { // Ctrl 多选:切换该足迹 + if (selectedMapLines_.count(hitDs)) selectedMapLines_.erase(hitDs); + else selectedMapLines_.insert(hitDs); + } else if (!selectedMapLines_.count(hitDs)) { // 单击未选中的线 → 替换为它 + selectedMapLines_.clear(); + selectedMapLines_.insert(hitDs); + } + // 单击已选中的线(可能为多选之一):保持当前选中集 → 起手即可整体拖动,不塌缩为单选。 + applyMapLineSelectionVisual(); + if (renderWindow_) renderWindow_->Render(); + if (onMapLineSelectionChanged) onMapLineSelectionChanged(); // VTK→列表:同步选中 + return !selectedMapLines_.empty(); // 有选中 → 交互样式进入 Z 拖动 +} + +void VtkSceneView::nudgeSelectedMapLinesZ(double worldDz) { + if (selectedMapLines_.empty() || worldDz == 0.0) return; + for (const auto& dsId : selectedMapLines_) { + mapLineZOffset_[dsId] += worldDz; // 持久累计(全量重建后 addMapLine 复用) + auto it = dsProps_.find(dsId); + if (it == dsProps_.end()) continue; + for (auto& p : it->second) { + auto* a = vtkActor::SafeDownCast(p); + if (a) a->AddPosition(0.0, 0.0, worldDz); // 仅改 Z,锁 XY + } + } + if (scene_.renderer()) scene_.renderer()->ResetCameraClippingRange(); // Z 抬升后防被裁剪面切 + if (renderWindow_) renderWindow_->Render(); +} + +double VtkSceneView::selectedMapLineZ() const { + if (selectedMapLines_.empty()) return 0.0; + // 代表性 Z = 任一选中足迹 actor 的包围盒中心 Z(含 placement worldZ + 已累计偏移)。 + auto it = dsProps_.find(*selectedMapLines_.begin()); + if (it == dsProps_.end()) return 0.0; + for (const auto& p : it->second) + if (p) { if (double* b = p->GetBounds()) return 0.5 * (b[4] + b[5]); } + return 0.0; +} + void VtkSceneView::rebuildAxes() { // 先移除上一次的坐标轴 prop:render 可能在一次 rebuild 内多次调用(末尾统一 render + // 异步回灌 render),不先移除会叠加坐标轴(评审 HIGH)。移除后再算 bounds(仅数据图元)。 diff --git a/src/app/VtkSceneView.hpp b/src/app/VtkSceneView.hpp index 38124d1..1b2eaba 100644 --- a/src/app/VtkSceneView.hpp +++ b/src/app/VtkSceneView.hpp @@ -90,6 +90,22 @@ public: void setAnalysisMode2D(bool is2D); bool isAnalysisMode2D() const { return analysisMode2D_; } + // ── 二维分析改造 B 期:选中 2D 足迹沿高程 Z 拖动 ─────────────────────────────── + // 仅二维分析下用。pickMapLineAt:在屏幕(x,y)拾取足迹(只考虑可见足迹,不被地形/底图干扰);命中则 + // 选中(additive=Ctrl 多选切换,否则单选替换)并高亮,返回是否有选中(交互样式据此决定 Z 拖动/平移)。 + // nudgeSelectedMapLinesZ:选中足迹世界 Z += worldDz(锁 XY);偏移按 dsId 持久(切走再回/全量重建保留)。 + // selectedMapLineZ:代表性当前世界 Z(高程读数浮层用);无选中返回 0。 + bool pickMapLineAt(int screenX, int screenY, bool additive); + void clearMapLineSelection(); + bool hasMapLineSelection() const { return !selectedMapLines_.empty(); } + void nudgeSelectedMapLinesZ(double worldDz); + double selectedMapLineZ() const; + // 双向选择联动:列表↔VTK。selectedMapLines 取当前选中 dsId;setSelectedMapLines 由列表设置选中 + // (高亮,不回调,避免环)。VTK 内拾取改变选中时触发 onMapLineSelectionChanged → 上层同步列表。 + std::vector selectedMapLines() const; + void setSelectedMapLines(const std::vector& dsIds); + std::function onMapLineSelectionChanged; + private: // 首个带经纬数据(剖面/足迹)到达时把共享 frame 重锚到其 lat/lon 包围盒中心:使数据落在世界原点近旁 // (否则样本默认原点可能离真实数据数百公里→图元在视锥外、移动视角也找不到)。已锚或无经纬则跳过。 @@ -159,6 +175,11 @@ private: // 哪些 dsProps_ 条目是 2D 足迹(addMapLine):切 tab 按此区分维度翻可见(其余 dsProps_=帘面/体=3D)。 std::set mapLineDs_; bool analysisMode2D_ = false; // 当前是否处二维分析(默认三维:启动在「三维分析」tab) + + // B 期:选中的足迹 dsId(Z 拖动目标) + 各足迹累计 Z 偏移(持久,全量重建后 addMapLine 复用)。 + std::set selectedMapLines_; + std::map mapLineZOffset_; + void applyMapLineSelectionVisual(); // 选中足迹加粗变亮、其余复原(橙 3.0) }; } // namespace geopro::app diff --git a/src/app/VtkViewToolbar.cpp b/src/app/VtkViewToolbar.cpp index 65636cb..36f5124 100644 --- a/src/app/VtkViewToolbar.cpp +++ b/src/app/VtkViewToolbar.cpp @@ -61,8 +61,9 @@ VtkViewToolbar::VtkViewToolbar(QWidget* parent) : QWidget(parent) { {"下", ViewDir::Bottom}, {"左", ViewDir::Left}, {"右", ViewDir::Right}}; for (const V& v : views) { const ViewDir d = v.d; - connect(textBtn(QString::fromUtf8(v.t)), &QToolButton::clicked, this, - [this, d] { emit viewRequested(d); }); + auto* b = textBtn(QString::fromUtf8(v.t)); + connect(b, &QToolButton::clicked, this, [this, d] { emit viewRequested(d); }); + viewDirButtons_.push_back(b); // 二维分析下禁用(会改朝向、破坏近俯视锁定) } sep(); // ── 段3:缩放 / 复位 ── @@ -84,4 +85,12 @@ VtkViewToolbar::VtkViewToolbar(QWidget* parent) : QWidget(parent) { adjustSize(); } +void VtkViewToolbar::setAnalysisMode2D(bool is2D) { + for (auto* b : viewDirButtons_) { + if (!b) continue; + b->setEnabled(!is2D); + b->setToolTip(is2D ? QStringLiteral("二维分析下不可用(已锁定近俯视)") : QString()); + } +} + } // namespace geopro::app diff --git a/src/app/VtkViewToolbar.hpp b/src/app/VtkViewToolbar.hpp index 271c000..05e02d6 100644 --- a/src/app/VtkViewToolbar.hpp +++ b/src/app/VtkViewToolbar.hpp @@ -1,7 +1,10 @@ #pragma once #include +#include #include "I3dSceneView.hpp" // geopro::controller::ViewDir +class QToolButton; + namespace geopro::app { // VTK 画布竖排工具条(spec §9):全局视图控制——设置(坐标轴)/前后上下左右/放大缩小复位。 @@ -11,12 +14,20 @@ class VtkViewToolbar : public QWidget { public: explicit VtkViewToolbar(QWidget* parent = nullptr); +public slots: + // 二维分析激活时禁用不适用的工具:6 向快捷视图会改相机朝向→破坏二维近俯视锁定,故二维下禁用; + // 缩放/适配/坐标轴设置(含 VE)仍可用。切回三维恢复。 + void setAnalysisMode2D(bool is2D); + signals: void axesSettingsRequested(); // 设置 → 弹 AxesSettingsDialog void viewRequested(geopro::controller::ViewDir dir); // 前/后/上/下/左/右 void zoomInRequested(); void zoomOutRequested(); void fitRequested(); // 复位=适配 + +private: + std::vector viewDirButtons_; // 6 向快捷视图按钮:二维分析下禁用 }; } // namespace geopro::app diff --git a/src/app/main.cpp b/src/app/main.cpp index 9718785..a9d1cdc 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -152,6 +152,7 @@ #include "Scene.hpp" #include "VoxelFromScatters.hpp" #include "interact/InteractionManager.hpp" +#include "interact/PickInteractorStyle.hpp" #include "interact/SlicePlaneMath.hpp" #include "actors/AnomalyActor.hpp" #include "actors/CurtainActor.hpp" @@ -410,6 +411,62 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re "border:1px solid {{accent/primary}};padding:8px 12px;}")); anomalyHint->hide(); + // ── 二维分析 B 期:高程 Z 拖动读数浮层(顶部居中)+ 足迹拾取/拖动回调注入交互样式 ────────── + // 拖动选中足迹时显示其当前世界 Z,松开隐藏;不挡画布鼠标。深底方角(同异常提示坑规避)。 + auto* elevHint = new QLabel(vtkWidget); + elevHint->setObjectName(QStringLiteral("elevHint")); + elevHint->setAttribute(Qt::WA_TransparentForMouseEvents); + geopro::app::applyTokenizedStyleSheet( + elevHint, QStringLiteral("QLabel#elevHint{background:#0E1A2D;color:#E6ECF5;" + "border:1px solid {{accent/primary}};padding:6px 12px;}")); + elevHint->hide(); + // 滚轮升降时读数浮层 1.2s 后自动隐藏(拖动则在松开时隐藏)。 + auto* zHideTimer = new QTimer(vtkWidget); + zHideTimer->setSingleShot(true); + QObject::connect(zHideTimer, &QTimer::timeout, elevHint, [elevHint]() { elevHint->hide(); }); + auto showZReadout = std::make_shared>([sceneView, elevHint, vtkWidget]() { + elevHint->setText( + QStringLiteral("高程 Z:%1 m").arg(sceneView->selectedMapLineZ(), 0, 'f', 1)); + elevHint->adjustSize(); + elevHint->move((vtkWidget->width() - elevHint->width()) / 2, 12); // 顶部居中 + elevHint->show(); + elevHint->raise(); + }); + if (auto* style = interactionMgr->pickStyle()) { + // 命中可见足迹→选中(Ctrl 多选)并返回是否进入 Z 拖动;未命中(返回 false)→交互样式回退平移。 + style->onPick2D = [sceneView](int x, int y, bool additive) { + return sceneView->pickMapLineAt(x, y, additive); + }; + // 拖动中:施加世界 Z 增量(仅改 Z),并把选中足迹当前高程显示在顶部读数浮层。 + style->onDrag2D = [sceneView, showZReadout](double worldDz) { + sceneView->nudgeSelectedMapLinesZ(worldDz); + (*showZReadout)(); + }; + style->onDrag2DEnd = [elevHint]() { elevHint->hide(); }; + // 滚轮升降:有选中足迹则施加 Z 增量并显示读数(1.2s 后自动隐藏),返回 true 消费滚轮;否则缩放。 + style->onWheel2D = [sceneView, showZReadout, zHideTimer](double worldDz) { + if (!sceneView->hasMapLineSelection()) return false; + sceneView->nudgeSelectedMapLinesZ(worldDz); + (*showZReadout)(); + zHideTimer->start(1200); + return true; + }; + } + // 双向选择联动:列表行选中 ↔ VTK 足迹高亮。两向各自屏蔽回环(setSelectedMapLines 不回调、 + // setSelectedDsIds 屏蔽信号),故无需额外守卫。 + QObject::connect(drawer->col2D(), &geopro::app::Column2DDataset::selectedDatasetsChanged, &window, + [sceneView](const QStringList& ids) { + std::vector v; + for (const QString& s : ids) v.push_back(s.toStdString()); + sceneView->setSelectedMapLines(v); + }); + sceneView->onMapLineSelectionChanged = [sceneView, drawer]() { + QStringList ids; + for (const std::string& s : sceneView->selectedMapLines()) + ids << QString::fromStdString(s); + drawer->col2D()->setSelectedDsIds(ids); + }; + // 坐标轴设置抽屉面板:叠加 vtkWidget、工具条右侧滑出,默认隐藏(点设置 toggle)。 auto* axesPanel = new geopro::app::AxesSettingsPanel(vtkWidget); axesPanel->hide(); @@ -1084,10 +1141,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 渲染 [VtkSceneView]。顺序:先 ①②(都不渲染),最后 ③ 收尾统一渲染。只翻可见标志、不清空/重建 → // 切换瞬时;地形+底图常驻。 QObject::connect(drawer, &geopro::app::ColumnDrawer::analysisModeChanged, &window, - [interactionMgr, sceneCtrl, sceneView](bool is2D) { + [interactionMgr, sceneCtrl, sceneView, viewToolbar](bool is2D) { interactionMgr->setMode2D(is2D); sceneCtrl->onAnalysisModeChanged(is2D); sceneView->setAnalysisMode2D(is2D); + viewToolbar->setAnalysisMode2D(is2D); // 二维下禁用 6 向快捷视图 }); // 首个真实剖面到达 → frame 重锚到数据 lat/lon 后,把选中的底图加载到数据所在位置 diff --git a/src/app/panels/columns/Column2DDataset.cpp b/src/app/panels/columns/Column2DDataset.cpp index 4ee40f4..a8da596 100644 --- a/src/app/panels/columns/Column2DDataset.cpp +++ b/src/app/panels/columns/Column2DDataset.cpp @@ -72,6 +72,7 @@ Column2DDataset::Column2DDataset(QWidget* parent) : QWidget(parent) { list_ = new QTreeWidget(); list_->setHeaderHidden(true); list_->setRootIsDecorated(true); + list_->setSelectionMode(QAbstractItemView::ExtendedSelection); // 多选行(与 VTK 多选拖动联动) applyDatasetCardDelegate(list_); connect(list_, &QTreeWidget::itemChanged, this, [this](QTreeWidgetItem*, int) { QStringList ids; @@ -81,6 +82,13 @@ Column2DDataset::Column2DDataset(QWidget* parent) : QWidget(parent) { } emit checkedDatasetsChanged(ids); }); + // 行选中变化 → 上抛选中 dsId(高亮联动 VTK;与勾选/渲染独立)。 + connect(list_, &QTreeWidget::itemSelectionChanged, this, [this]() { + QStringList ids; + for (QTreeWidgetItem* it : list_->selectedItems()) + ids << it->data(0, kDsIdRole).toString(); + emit selectedDatasetsChanged(ids); + }); root->addWidget(list_, 1); } @@ -111,4 +119,11 @@ void Column2DDataset::setDatasets(const std::vector& rows) emit checkedDatasetsChanged(ids); } +void Column2DDataset::setSelectedDsIds(const QStringList& dsIds) { + QSignalBlocker blocker(list_); // 防回环:VTK→列表 设置选中不再上抛 selectedDatasetsChanged + list_->clearSelection(); + for (QTreeWidgetItemIterator it(list_); *it; ++it) + if (dsIds.contains((*it)->data(0, kDsIdRole).toString())) (*it)->setSelected(true); +} + } // namespace geopro::app diff --git a/src/app/panels/columns/Column2DDataset.hpp b/src/app/panels/columns/Column2DDataset.hpp index 2e5d979..5003510 100644 --- a/src/app/panels/columns/Column2DDataset.hpp +++ b/src/app/panels/columns/Column2DDataset.hpp @@ -14,12 +14,15 @@ class Column2DDataset : public QWidget { public: explicit Column2DDataset(QWidget* parent = nullptr); void setDatasets(const std::vector& rows); + // VTK→列表 选择联动:按 dsId 选中对应行(高亮),内部屏蔽信号避免回环。 + void setSelectedDsIds(const QStringList& dsIds); signals: void basemapChanged(int index); // 0 天地图 / 1 Google / 2 隐藏 void view2DModeChanged(int index); // 0 关闭 /1 Z=0 /2 顶部 /3 底部 /4 自定义 void customZChanged(double z); // 世界绝对高程(米),向上为正 - void checkedDatasetsChanged(const QStringList& dsIds); + void checkedDatasetsChanged(const QStringList& dsIds); // 勾选(渲染开关)变化 + void selectedDatasetsChanged(const QStringList& dsIds); // 行选中(高亮联动)变化,非勾选 private: QTreeWidget* list_ = nullptr; diff --git a/src/render/interact/InteractionManager.cpp b/src/render/interact/InteractionManager.cpp index 4bb8a47..61c74fe 100644 --- a/src/render/interact/InteractionManager.cpp +++ b/src/render/interact/InteractionManager.cpp @@ -260,6 +260,8 @@ void InteractionManager::closeAll() { safeRender(); } +PickInteractorStyle* InteractionManager::pickStyle() const { return style_; } + void InteractionManager::setMode2D(bool is2D) { // 切片属三维内容:二维分析隐藏(不销毁→切回零重建)、三维分析显示。 for (auto& s : slices_) diff --git a/src/render/interact/InteractionManager.hpp b/src/render/interact/InteractionManager.hpp index 21243c8..af14f27 100644 --- a/src/render/interact/InteractionManager.hpp +++ b/src/render/interact/InteractionManager.hpp @@ -112,6 +112,10 @@ public: void installStyle(); void uninstallStyle(); + // 暴露交互样式:供 app 层注入二维分析 B 期的足迹拾取/Z 拖动回调(onPick2D/onDrag2D/onDrag2DEnd)。 + // 定义在 .cpp(此处 PickInteractorStyle 仅前置声明,vtkSmartPointer→裸指针下转需完整类型)。 + PickInteractorStyle* pickStyle() const; + private: // 拾取回调实现(PickInteractorStyle 注入)。 void onPicked(const Vec3& worldPoint); // 选中所在切片 + 焦点 diff --git a/src/render/interact/PickInteractorStyle.cpp b/src/render/interact/PickInteractorStyle.cpp index 69e8f22..533576a 100644 --- a/src/render/interact/PickInteractorStyle.cpp +++ b/src/render/interact/PickInteractorStyle.cpp @@ -1,6 +1,7 @@ #include "interact/PickInteractorStyle.hpp" #include +#include #include #include @@ -48,12 +49,19 @@ bool PickInteractorStyle::pickWorld(Vec3& out) { void PickInteractorStyle::OnLeftButtonDown() { auto* iren = this->GetInteractor(); - // 二维分析:左键拖动=平移(等同中键),不拾取/不旋转 → 仅平移+缩放。抬键由基类按 State 收尾。 + // 二维分析:左键命中足迹→进入高程 Z 拖动(B 期);否则=平移(等同中键),禁旋转。抬键由 OnLeftButtonUp 收尾。 if (lock2D_) { const int* p = iren ? iren->GetEventPosition() : nullptr; if (p) this->FindPokedRenderer(p[0], p[1]); if (!this->CurrentRenderer) return; - this->GrabFocus(this->EventCallbackCommand); + const bool additive = iren && iren->GetControlKey(); // Ctrl=多选 + if (onPick2D && p && onPick2D(p[0], p[1], additive)) { // 命中足迹 → Z 拖动 + dragging2D_ = true; + lastDragY_ = p[1]; + this->GrabFocus(this->EventCallbackCommand); + return; + } + this->GrabFocus(this->EventCallbackCommand); // 未命中 → 平移 this->StartPan(); return; } @@ -137,12 +145,62 @@ void PickInteractorStyle::Rotate() { rwi->Render(); } +double PickInteractorStyle::worldPerPixelZ() const { + if (!this->CurrentRenderer) return 1.0; + auto* cam = this->CurrentRenderer->GetActiveCamera(); + auto* rw = this->CurrentRenderer->GetRenderWindow(); + if (!cam || !rw) return 1.0; + const int* sz = rw->GetSize(); + const double h = (sz && sz[1] > 0) ? static_cast(sz[1]) : 800.0; + if (cam->GetParallelProjection()) + return 2.0 * cam->GetParallelScale() / h; // 平行投影:可见世界高度=2*parallelScale + // 透视:可见世界高度 = 2*d*tan(viewAngle/2),d=相机到焦点距离。 + double pos[3], fp[3]; + cam->GetPosition(pos); + cam->GetFocalPoint(fp); + const double dx = pos[0] - fp[0], dy = pos[1] - fp[1], dz = pos[2] - fp[2]; + const double d = std::sqrt(dx * dx + dy * dy + dz * dz); + const double va = vtkMath::RadiansFromDegrees(cam->GetViewAngle()); + return 2.0 * d * std::tan(va * 0.5) / h; +} + +void PickInteractorStyle::OnMouseMove() { + if (dragging2D_) { // B 期:竖向拖动 → 选中足迹 Z 增量(仅改 Z)。鼠标上移(y 增)→ 抬高。 + auto* rwi = this->Interactor; + if (rwi) { + const int y = rwi->GetEventPosition()[1]; + const int dyPix = y - lastDragY_; + lastDragY_ = y; + if (dyPix != 0 && onDrag2D) onDrag2D(worldPerPixelZ() * dyPix); + } + return; // 不走基类(不平移/不旋转) + } + Superclass::OnMouseMove(); +} + +void PickInteractorStyle::OnLeftButtonUp() { + if (dragging2D_) { // 结束 Z 拖动 + dragging2D_ = false; + if (this->Interactor) this->ReleaseFocus(); + if (onDrag2DEnd) onDrag2DEnd(); + return; + } + Superclass::OnLeftButtonUp(); // 平移/旋转/缩放等由基类按 State 收尾 +} + +namespace { +constexpr double kWheelStepPx = 24.0; // 滚轮一格升降 ≈ 拖动 24 像素的世界 Z 量(与拖动手感一致) +} + void PickInteractorStyle::OnMouseWheelForward() { + // 二维分析有选中足迹 → 滚轮抬升其高程(消费滚轮);否则按切片推进 / 默认缩放。 + if (lock2D_ && onWheel2D && onWheel2D(worldPerPixelZ() * kWheelStepPx)) return; if (onWheelStep && onWheelStep(+1)) return; // 有选中切片 → 推进,消费滚轮 Superclass::OnMouseWheelForward(); // 否则默认缩放 } void PickInteractorStyle::OnMouseWheelBackward() { + if (lock2D_ && onWheel2D && onWheel2D(-worldPerPixelZ() * kWheelStepPx)) return; if (onWheelStep && onWheelStep(-1)) return; Superclass::OnMouseWheelBackward(); } diff --git a/src/render/interact/PickInteractorStyle.hpp b/src/render/interact/PickInteractorStyle.hpp index ae31c4e..5c9b41f 100644 --- a/src/render/interact/PickInteractorStyle.hpp +++ b/src/render/interact/PickInteractorStyle.hpp @@ -35,6 +35,19 @@ public: void setLock2D(bool on) { lock2D_ = on; } bool isLock2D() const { return lock2D_; } + // ── 二维分析 B 期:选中足迹沿高程 Z 拖动 ──(仅 lock2D 下生效;回调由 app 层注入) + // onPick2D:左键按下时在(x,y)拾取足迹(additive=Ctrl 多选),返回是否有选中→有则进入 Z 拖动、否则平移。 + // onDrag2D:拖动中把竖向像素换算成的世界 Z 增量(本类按相机算)交给 app 施加到选中足迹(仅改 Z)。 + // onDrag2DEnd:松开结束拖动(供 app 收起高程读数浮层)。 + std::function onPick2D; + std::function onDrag2D; + std::function onDrag2DEnd; + // 滚轮升降:有选中足迹时滚轮改其高程 Z(本类按相机算 worldDz);app 施加并返回是否消费(无选中→false→默认缩放)。 + std::function onWheel2D; + + void OnMouseMove() override; + void OnLeftButtonUp() override; + void OnLeftButtonDown() override; void OnMouseWheelForward() override; void OnMouseWheelBackward() override; @@ -48,6 +61,8 @@ protected: private: // 在当前鼠标位置拾取世界点;命中返回 true 并填 out。 bool pickWorld(Vec3& out); + // 当前相机下:竖向一屏幕像素对应的世界 Z(米/像素),用于把拖动像素换算成 Z 增量。 + double worldPerPixelZ() const; // 手动双击判定:QVTK+Windows 下 vtkRenderWindowInteractor::GetRepeatCount() 不可靠(评审 M5)。 // 记上次左键按下时刻+屏幕位置,两次按下间隔 < kDoubleClickMs 且位置相近视为双击。 @@ -56,6 +71,10 @@ private: // 二维分析模式:左键=平移、禁旋转(仅平移+缩放)。由 InteractionManager 在切 tab 时设。 bool lock2D_ = false; + + // B 期足迹 Z 拖动状态:左键命中足迹时进入,记上次鼠标 y 以算增量。 + bool dragging2D_ = false; + int lastDragY_ = 0; }; } // namespace geopro::render::interact