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