diff --git a/src/app/VtkSceneView.cpp b/src/app/VtkSceneView.cpp index dd3aa68..b4b22d0 100644 --- a/src/app/VtkSceneView.cpp +++ b/src/app/VtkSceneView.cpp @@ -122,6 +122,7 @@ void VtkSceneView::clear() { volumeOwnerDs_.clear(); volumes_.clear(); // 多体并发:清场移除所有体 image frameAnchoredToData_ = false; // 新一轮选择重新按其首个真实剖面重锚原点 + useFittedAxes_ = false; // 清场:贴合轴复位为全场景轴(选中随数据一并失效,防残留旧盒) if (onVolumeChanged) onVolumeChanged(); } @@ -472,8 +473,13 @@ void VtkSceneView::rebuildAxes() { } // 坐标轴随数据包围盒重建:仅按数据图元算 bounds(不含底图,否则被~公里级底图撑大), // 再造 vtkCubeAxesActor 入场。None 模式或无数据 → buildAxes 返回 nullptr,场景无坐标轴。 + // 贴合态(useFittedAxes_):改用选中子树盒 fittedBounds_,只框该子树而非全场景(spec §3.2)。 double bounds[6]; - if (!computeDataBounds(bounds)) return; // 无数据 → 不建坐标轴 + if (useFittedAxes_) { + for (int i = 0; i < 6; ++i) bounds[i] = fittedBounds_[i]; + } else if (!computeDataBounds(bounds)) { + return; // 无数据 → 不建坐标轴 + } geopro::render::AxesOptions opts; opts.mode = toRenderMode(axesMode_); opts.unit = toRenderUnit(axesUnit_); @@ -492,6 +498,21 @@ void VtkSceneView::rebuildAxes() { } } +void VtkSceneView::showFittedAxes(const double b[6]) { + // 选中子树盒 → 冻结为贴合轴 bounds,隐去全场景轴(rebuildAxes 会先移除旧轴再按 fittedBounds_ 重建)。 + useFittedAxes_ = true; + for (int i = 0; i < 6; ++i) fittedBounds_[i] = b[i]; + rebuildAxes(); + if (renderWindow_) renderWindow_->Render(); +} + +void VtkSceneView::showSceneAxes() { + // 取消选中 → 复位为全场景总览轴(现状默认)。清掉贴合态后 rebuildAxes 走 computeDataBounds。 + useFittedAxes_ = false; + rebuildAxes(); + if (renderWindow_) renderWindow_->Render(); +} + void VtkSceneView::render(bool is2D, bool resetCamera) { // 视图区背景永远深色(规范 §0.5:不随明暗切换),让色阶数据更突出。 double bgR, bgG, bgB; diff --git a/src/app/VtkSceneView.hpp b/src/app/VtkSceneView.hpp index f1ad857..8871a0f 100644 --- a/src/app/VtkSceneView.hpp +++ b/src/app/VtkSceneView.hpp @@ -70,6 +70,11 @@ public: void fitToBounds(const double b[6]); // 绕 pivot 转到沿 dir 轴看向 pivot,保留当前 focal-to-camera 距离(缩放不变)。 void orbitToAxis(geopro::controller::ViewDir dir, const double pivot[3]); + // ── 贴合轴 / 全景轴(spec §3.2;T2)─────────────────────────────────────────── + // 选中某 ds → 用其子树盒 b 显示贴合 cube axes、隐去全场景总览轴(立即提交渲染)。 + void showFittedAxes(const double b[6]); + // 取消选中 → 恢复全场景总览轴(现状默认行为,立即提交渲染)。 + void showSceneAxes(); void render(bool is2D, bool resetCamera = true) override; void renderIncremental() override; @@ -136,6 +141,10 @@ private: // 当前坐标轴 prop:render 可能多次调用 rebuildAxes(rebuild 末尾 + 异步回灌), // 持引用以便重建前移除旧 prop,避免叠加(评审 HIGH)。 vtkSmartPointer currentAxes_; + // 贴合轴态(T2):true=坐标轴按 fittedBounds_(选中子树盒)建,非全场景数据包围盒;选中时冻结该盒, + // 取消/清场复位为 false(走全场景 computeDataBounds)。 + bool useFittedAxes_ = false; + double fittedBounds_[6] = {0, 0, 0, 0, 0, 0}; // 当前体素 image + 色阶(P3 切片附着源);无体素时为空。「当前」=最后添加/活动的体(保存切片/ // 创建异常的默认归属)。多体并发下各体 image 另存 volumes_。 diff --git a/src/app/main.cpp b/src/app/main.cpp index d015735..f7c433b 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1265,8 +1265,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re }); // 树选中切片/异常 → VTK 高亮联动(正向 list→VTK;反向 VTK→list 需拾取回调,见 OPT-002)。 QObject::connect(analysisTab, &geopro::app::CategoryAnalysisTab::datasetSelected, vtkWidget, - [sceneView, interactionMgr, renderWindowPtr](const QString& dsId, - const QString& ddCode) { + [sceneView, interactionMgr, renderWindowPtr, analysisTab](const QString& dsId, + const QString& ddCode) { const std::string id = dsId.toStdString(); // 选中项决定高亮:异常↔切片互斥,选其它对象两者都清(否则切到别的对象后切片/异常仍高亮)。 if (ddCode == QStringLiteral("dd_anomaly")) { @@ -1279,6 +1279,21 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re sceneView->setSelectedAnomaly(std::string{}); interactionMgr->deselectSlice(); } + // 贴合坐标轴(T2):选中 → 该 ds 子树盒贴合轴+隐全景;空选中(取消) → 恢复全景轴。 + // 子树未渲染/无盒 → 退回全景轴,避免留下无据的贴合框。 + if (dsId.isEmpty()) { + sceneView->showSceneAxes(); + } else { + const QStringList sub = analysisTab->subtreeDsIds(dsId); + std::vector ids; + ids.reserve(static_cast(sub.size())); + for (const QString& s : sub) ids.push_back(s.toStdString()); + double box[6]; + if (!ids.empty() && sceneView->datasetBounds(ids, box)) + sceneView->showFittedAxes(box); + else + sceneView->showSceneAxes(); + } renderWindowPtr->Render(); }); // 2D 段「z 值」滑块 → 整体升降该 2D 类型平面(Plane2DRenderStrategy 重摆其全部足迹)。 @@ -1297,6 +1312,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re sec->selectItem(QString::fromStdString(dsId)); if (dsId.empty()) { sceneView->setSelectedAnomaly(std::string{}); + sceneView->showSceneAxes(); // VTK 里点空白清选 → 一并恢复全景轴(selectItem 空被 blocker 拦,不走 datasetSelected) renderWindowPtr->Render(); } }; diff --git a/src/app/panels/columns/CategoryAnalysisTab.cpp b/src/app/panels/columns/CategoryAnalysisTab.cpp index 8e1c4d9..62fcbe0 100644 --- a/src/app/panels/columns/CategoryAnalysisTab.cpp +++ b/src/app/panels/columns/CategoryAnalysisTab.cpp @@ -144,6 +144,14 @@ CategorySection* CategoryAnalysisTab::section(const std::string& id) const { return it != sections_.end() ? it->second : nullptr; } +QStringList CategoryAnalysisTab::subtreeDsIds(const QString& dsId) const { + for (auto* sec : ordered_) { + const QStringList ids = sec->subtreeDsIds(dsId); + if (!ids.isEmpty()) return ids; // ds 归属唯一段 → 首个命中段即答案 + } + return {}; +} + // ds 归属唯一段未知 → 广播到各段,仅含该 ds 的段会命中生效(其余 no-op)。 void CategoryAnalysisTab::setItemChecked(const QString& dsId, bool on) { for (auto* sec : ordered_) sec->setChecked(dsId, on); diff --git a/src/app/panels/columns/CategoryAnalysisTab.hpp b/src/app/panels/columns/CategoryAnalysisTab.hpp index 7a939c3..fe67064 100644 --- a/src/app/panels/columns/CategoryAnalysisTab.hpp +++ b/src/app/panels/columns/CategoryAnalysisTab.hpp @@ -30,6 +30,8 @@ public: void setStructure(const std::vector& nodes); // 转发各段 void refreshArrayFilters(); // 装置枚举异步加载后,重填各段装置筛选下拉 CategorySection* section(const std::string& id) const; // 按 CategoryDescriptor.id 取段 + // 该 ds 所在段的层级子树 dsId 集(贴合坐标轴子树盒):遍历各段,返首个命中段的 subtreeDsIds。空=无段含该 ds。 + QStringList subtreeDsIds(const QString& dsId) const; // ── 三维体生成后:自动勾选触发渲染 + 标题前等待动画(spinner)反馈(转发到含该 ds 的段)── void setItemChecked(const QString& dsId, bool on); // 自动勾选(触发渲染) void setItemBusy(const QString& dsId, bool busy); // 复选框↔等待 spinner 切换 diff --git a/src/app/panels/columns/CategorySection.cpp b/src/app/panels/columns/CategorySection.cpp index 9ce60b8..f0fbb74 100644 --- a/src/app/panels/columns/CategorySection.cpp +++ b/src/app/panels/columns/CategorySection.cpp @@ -162,7 +162,8 @@ CategorySection::CategorySection(const geopro::data::CategoryDescriptor& desc, // 树选中切片/异常 → VTK 高亮联动(正向 list→VTK)。 connect(list_, &QTreeWidget::itemSelectionChanged, this, [this] { const auto items = list_->selectedItems(); - if (items.isEmpty()) return; + // 空选中(点树空白/清选)→ 发空 datasetSelected:上层据此恢复全景轴、清高亮(贴合轴取消,spec §3.2)。 + if (items.isEmpty()) { emit datasetSelected(QString(), QString()); return; } QTreeWidgetItem* it = items.first(); const QString id = it->data(0, kDsIdRole).toString(); const QString dd = it->data(0, kDsDdCodeRole).toString(); @@ -193,6 +194,28 @@ QTreeWidgetItem* CategorySection::itemFor(const QString& dsId) const { return nullptr; } +QStringList CategorySection::subtreeDsIds(const QString& dsId) const { + QTreeWidgetItem* item = itemFor(dsId); + if (!item) return {}; + // 归一到子树根:向上找到最高的非容器祖先(三维体行;其父为结构容器 TM)。选中体/切片/异常都收敛到该体。 + QTreeWidgetItem* root = item; + for (QTreeWidgetItem* p = root->parent(); p; p = p->parent()) { + if (p->data(0, kDsDdCodeRole).toString() == QStringLiteral("container")) break; + root = p; + } + // 自根向下遍历整棵子树,收集非空 dsId(跳过容器骨架节点)。 + QStringList ids; + std::vector stack{root}; + while (!stack.empty()) { + QTreeWidgetItem* it = stack.back(); + stack.pop_back(); + const QString id = it->data(0, kDsIdRole).toString(); + if (!id.isEmpty()) ids << id; + for (int i = 0; i < it->childCount(); ++i) stack.push_back(it->child(i)); + } + return ids; +} + bool CategorySection::hasRenderableRows() const { if (!list_) return false; // 数据行 = 非 container 的可勾选行;只有容器节点(分组)不算「有数据」。 diff --git a/src/app/panels/columns/CategorySection.hpp b/src/app/panels/columns/CategorySection.hpp index 089e038..38ea630 100644 --- a/src/app/panels/columns/CategorySection.hpp +++ b/src/app/panels/columns/CategorySection.hpp @@ -47,6 +47,10 @@ public: void ensureExpanded(); // 展开本段(折叠则展开):滚动定位前确保目标行可见 QTreeWidget* listWidget() const { return list_; } // 段体树(外层滚动定位用) QTreeWidgetItem* itemFor(const QString& dsId) const; // 按 dsId 取行(无则 nullptr) + // 该 ds 所在层级子树的全部 dsId(贴合坐标轴子树盒,spec §2/§3.2 决策 3):先归一到子树根 + // (向上找最高非容器祖先=三维体行),再自根向下收集整棵子树(体+切片+异常)的非空 dsId。 + // 故选中体/切片/异常都归到同一个「该体子树」盒。dsId 不在本段则返回空。 + QStringList subtreeDsIds(const QString& dsId) const; bool hasRenderableRows() const; // 段体是否含可渲染数据行(非 container 容器节点),供单列动态显隐 signals: