feat/vtk-merged-dataset-column #10

Merged
gaozheng merged 40 commits from feat/vtk-merged-dataset-column into main 2026-07-01 14:48:38 +08:00
7 changed files with 87 additions and 4 deletions
Showing only changes of commit dd0205919d - Show all commits

View File

@ -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;

View File

@ -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.2T2───────────────────────────────────────────
// 选中某 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:
// 当前坐标轴 proprender 可能多次调用 rebuildAxesrebuild 末尾 + 异步回灌),
// 持引用以便重建前移除旧 prop避免叠加评审 HIGH
vtkSmartPointer<vtkCubeAxesActor> currentAxes_;
// 贴合轴态T2true=坐标轴按 fittedBounds_选中子树盒非全场景数据包围盒选中时冻结该盒
// 取消/清场复位为 false走全场景 computeDataBounds
bool useFittedAxes_ = false;
double fittedBounds_[6] = {0, 0, 0, 0, 0, 0};
// 当前体素 image + 色阶P3 切片附着源);无体素时为空。「当前」=最后添加/活动的体(保存切片/
// 创建异常的默认归属)。多体并发下各体 image 另存 volumes_。

View File

@ -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<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();
});
// 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();
}
};

View File

@ -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);

View File

@ -30,6 +30,8 @@ public:
void setStructure(const std::vector<geopro::data::StructNode>& 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 切换

View File

@ -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<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 {
if (!list_) return false;
// 数据行 = 非 container 的可勾选行;只有容器节点(分组)不算「有数据」。

View File

@ -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: