feat/vtk-3d-view #7
|
|
@ -5,12 +5,13 @@
|
|||
---
|
||||
|
||||
## 0. 立刻要做的事(下个会话从这里开始)
|
||||
**实现二维分析改造**,spec 已写好:`docs/superpowers/specs/2026-06-26-2d-analysis-topdown-elevation.md`(commit `227ee8f`)。
|
||||
- 设计已与用户**逐条确认**(见 §4 决策)。分期 A→B→C:
|
||||
- **A(先做)**:一个场景两相机——切「二维分析」tab → 相机锁定**近俯视(75–80°,禁旋转,仅平移/缩放)**;切 tab 翻另一方数据集 actor 的 `SetVisibility`(**不清空**);地形+底图常驻。
|
||||
- **B**:二维里选中 2D 内容(单/多选)→ 竖向拖动只改**高程 Z**、锁 XY、实时高程读数。
|
||||
- **C**:dd_raster 纳入 2D 过滤 + 按 ddCode 分派渲染 + 栅格地理配准贴地形。**阻塞:dd_raster 数据端点未确认**。
|
||||
- 实现锚点:`view2DModeChanged` 信号(`Column2DDataset`→`sceneCtrl`)已存在,在其上扩展相机/可见标志切换。
|
||||
**二维分析改造 A 期已实现**(未提交,下个会话需用户实跑反馈手感/角度)。spec:`docs/superpowers/specs/2026-06-26-2d-analysis-topdown-elevation.md`(commit `227ee8f`)。分期 A→B→C:
|
||||
- **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 数据端点未确认**。
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -232,9 +232,11 @@ void TileBasemap::refineTile(int z, int x, int y, std::set<long long>& out, int&
|
|||
const double cx = (sw.x + ne.x) * 0.5, cy = (sw.y + ne.y) * 0.5; // 瓦片中心(局部米)
|
||||
const double g = std::max(std::abs(ne.x - sw.x), std::abs(ne.y - sw.y)); // 瓦片地面尺寸(米)
|
||||
|
||||
// 距离上限(按剖面范围动态):数据中心在局部原点(0,0);瓦片离它太远则不加载——远裁剪面有界
|
||||
// (剖面不被近裁剪面切),也避免拉远无限铺。叶块本身可大于此距离(其近端仍在范围内即保留)。
|
||||
if (std::sqrt(cx * cx + cy * cy) - g * 0.5 > maxTileDist_) return;
|
||||
// 距离上限(按剖面范围动态):以覆盖中心(相机焦点 cenX_,cenY_)为心,瓦片离它太远则不加载——
|
||||
// 远裁剪面有界(剖面不被近裁剪面切),也避免拉远无限铺。叶块本身可大于此距离(近端仍在范围内即保留)。
|
||||
// 心改用焦点而非原点(0,0):否则 frame 锚在别处数据(如深圳)时,看台湾数据全被剔除→底图空。
|
||||
const double rx = cx - cenX_, ry = cy - cenY_;
|
||||
if (std::sqrt(rx * rx + ry * ry) - g * 0.5 > maxTileDist_) return;
|
||||
|
||||
// 该瓦片投影到屏幕的近似像素尺寸 > 阈值且未到最大层级 → 细分为 4 子块(近处更细)。
|
||||
double screenPx;
|
||||
|
|
@ -286,6 +288,10 @@ void TileBasemap::refresh() {
|
|||
if (pl[0] * fp[0] + pl[1] * fp[1] + pl[2] * fp[2] + pl[3] < 0.0)
|
||||
for (int k = 0; k < 4; ++k) pl[k] = -pl[k];
|
||||
}
|
||||
// 底图覆盖中心 = 相机焦点(用户正看处)的局部 XY,而非世界原点:frame 锚在首个数据集,看远处别处
|
||||
// 数据时原点离视野很远会把全部瓦片距离剔除→底图空。焦点为心则底图随视野走(同 frame 仍与数据对齐)。
|
||||
cenX_ = fp[0];
|
||||
cenY_ = fp[1];
|
||||
|
||||
// 底图最大距离按当前剖面合并范围动态定(随勾选增删自动伸缩);无数据用下限。
|
||||
maxTileDist_ = kRangeFloor;
|
||||
|
|
@ -294,10 +300,10 @@ void TileBasemap::refresh() {
|
|||
if (r > 0.0) maxTileDist_ = std::clamp(r * kRangeFactor, kRangeFloor, kRangeCeil);
|
||||
}
|
||||
|
||||
// 四叉树:从数据中心一圈粗根块出发,按屏幕误差细分 → 近细远粗、铺满视野,无单层级盲区。
|
||||
// 四叉树:从覆盖中心(相机焦点经纬)一圈粗根块出发,按屏幕误差细分 → 近细远粗、铺满视野,无盲区。
|
||||
desired_.clear();
|
||||
int count = 0;
|
||||
const auto c = frame_->toLatLon(0.0, 0.0); // 数据中心
|
||||
const auto c = frame_->toLatLon(cenX_, cenY_); // 覆盖中心 = 相机焦点(非世界原点)
|
||||
const geopro::render::TileXY root = geopro::render::lonLatToTile(c.lon, c.lat, kRootZoom);
|
||||
for (int dy = -1; dy <= 1; ++dy)
|
||||
for (int dx = -1; dx <= 1; ++dx)
|
||||
|
|
|
|||
|
|
@ -84,6 +84,10 @@ private:
|
|||
int satMaxZoom_ = 18;
|
||||
// 四叉树当前帧相机参数(refresh 写, refineTile 读):相机位置 + 投影系数 + 视锥 6 面。
|
||||
double camX_ = 0, camY_ = 0, camZ_ = 0;
|
||||
// 底图覆盖中心(相机焦点的局部 XY):四叉树根块取此处经纬、距离剔除以此为心。
|
||||
// 关键——不能用世界原点(0,0):frame 锚在首个数据集(如深圳),看远处别处数据(如台湾,相距数百公里)时
|
||||
// 原点离视野数百公里→全部瓦片被距离剔除→底图空。改用焦点→底图随视野走(瓦片与数据同 frame 仍对齐)。
|
||||
double cenX_ = 0, cenY_ = 0;
|
||||
double projK_ = 1.0;
|
||||
bool projParallel_ = false;
|
||||
double frustum_[24] = {0}; // 6 个视锥平面(内法向),AABB 全在某面外则剔除
|
||||
|
|
|
|||
|
|
@ -80,12 +80,14 @@ void VtkSceneView::removeProps(std::vector<vtkSmartPointer<vtkProp>>& props) {
|
|||
}
|
||||
|
||||
bool VtkSceneView::computeDataBounds(double out[6]) const {
|
||||
// 仅计「可见」prop:二维分析下 3D 体/帘面已隐藏,取景/坐标轴/底图范围都应只围当前可见维度,
|
||||
// 否则二维取景被隐藏的远处 3D 体撑歪、坐标轴框错维度。
|
||||
vtkBoundingBox bb;
|
||||
for (const auto& kv : dsProps_)
|
||||
for (const auto& p : kv.second)
|
||||
if (p) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
|
||||
if (p && p->GetVisibility()) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
|
||||
for (const auto& p : miscProps_)
|
||||
if (p) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
|
||||
if (p && p->GetVisibility()) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
|
||||
if (!bb.IsValid()) return false;
|
||||
bb.GetBounds(out);
|
||||
return true;
|
||||
|
|
@ -102,6 +104,7 @@ void VtkSceneView::clear() {
|
|||
// 只移除数据 prop(按 ds 跟踪)+ 杂项(地形/测线)+ 坐标轴;不动底图(TileBasemap 自管)→ 重建不丢图。
|
||||
for (auto& kv : dsProps_) removeProps(kv.second);
|
||||
dsProps_.clear();
|
||||
mapLineDs_.clear(); // 2D 足迹维度记录随数据图元一并清(模式标志/相机快照保留)
|
||||
removeProps(miscProps_);
|
||||
clearAnomalies(); // 异常 actor 随清场一并移除
|
||||
if (currentAxes_) {
|
||||
|
|
@ -149,6 +152,7 @@ void VtkSceneView::addCurtain(const std::string& dsId, const geopro::core::Grid&
|
|||
auto curtain = geopro::render::buildCurtain(grid, cs, *frame_);
|
||||
if (curtain) {
|
||||
curtain->SetScale(1.0, 1.0, verticalExaggeration_); // 纵向夸张成墙
|
||||
curtain->SetVisibility(analysisMode2D_ ? 0 : 1); // 帘面=3D内容:二维分析下隐藏
|
||||
scene_.addActor(curtain);
|
||||
dsProps_[dsId].push_back(curtain);
|
||||
}
|
||||
|
|
@ -167,6 +171,7 @@ void VtkSceneView::addVolume(const std::string& dsId, const geopro::data::Volume
|
|||
// 体 actor 不参与拾取:切片选中靠点中切片平面(widget 交互/拾取)。否则点击落到体内部时
|
||||
// picker 命中体、worldPoint 落体内 → nearestSlice 按平面距离选错切片(用户 ④ 串选)。
|
||||
volume->PickableOff();
|
||||
volume->SetVisibility(analysisMode2D_ ? 0 : 1); // 体=3D内容:二维分析下隐藏
|
||||
scene_.addViewProp(volume);
|
||||
dsProps_[dsId].push_back(volume);
|
||||
currentVolumeImage_ = image;
|
||||
|
|
@ -187,8 +192,10 @@ void VtkSceneView::addMapLine(const std::string& dsId, const geopro::data::MapLi
|
|||
anchorFrameIfNeeded(line.lat, line.lon, static_cast<int>(line.lat.size()));
|
||||
auto actor = geopro::render::buildMapLine(line.lat, line.lon, worldZ, *frame_);
|
||||
if (actor) {
|
||||
actor->SetVisibility(analysisMode2D_ ? 1 : 0); // 足迹=2D内容:仅二维分析下显示
|
||||
scene_.addActor(actor);
|
||||
dsProps_[dsId].push_back(actor);
|
||||
mapLineDs_.insert(dsId); // 记录此 ds 为 2D 足迹(切 tab 按维度翻可见)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -206,6 +213,10 @@ void VtkSceneView::removeDataset(const std::string& dsId) {
|
|||
if (it == dsProps_.end()) return;
|
||||
removeProps(it->second);
|
||||
dsProps_.erase(it);
|
||||
mapLineDs_.erase(dsId); // 若是 2D 足迹则同步去除维度记录
|
||||
// 场景已无任何数据图元 → 复位重锚标志:下个数据(可能在别处)重新把 frame 锚到它,底图随之归位。
|
||||
// 否则删到空再加远处新数据时,新数据按旧锚点投到偏远世界坐标、底图仍贴在旧位置 → 底图"消失"。
|
||||
if (dsProps_.empty()) frameAnchoredToData_ = false;
|
||||
const bool wasVolume = volumes_.erase(dsId) > 0;
|
||||
if (volumeOwnerDs_ == dsId) { // 移除的是"当前体" → currentImage 回退到剩余某体,无则置空
|
||||
if (!volumes_.empty()) {
|
||||
|
|
@ -303,6 +314,27 @@ void VtkSceneView::fitView() {
|
|||
if (onCameraChanged) onCameraChanged(); // 取景后 → 底图按新视锥重算覆盖(治首帧部分瓦片不出)
|
||||
}
|
||||
|
||||
void VtkSceneView::setAnalysisMode2D(bool is2D) {
|
||||
if (is2D == analysisMode2D_) return; // 幂等:同模式重复切不做事
|
||||
analysisMode2D_ = is2D;
|
||||
|
||||
// ① 按维度翻可见标志(不清空、不重建→切换瞬时):2D 足迹↔3D 帘面/体;异常属 3D。
|
||||
// 地形/测线(miscProps_)与底图(TileBasemap 自管)两边常驻、不动。
|
||||
for (auto& kv : dsProps_) {
|
||||
const bool is2dContent = mapLineDs_.count(kv.first) > 0;
|
||||
const bool vis = is2D ? is2dContent : !is2dContent;
|
||||
for (auto& p : kv.second)
|
||||
if (p) p->SetVisibility(vis ? 1 : 0);
|
||||
}
|
||||
for (auto& kv : anomalyProps_)
|
||||
if (kv.second) kv.second->SetVisibility(is2D ? 0 : 1); // 异常=3D内容
|
||||
|
||||
// ② 取景 + 坐标轴 + 渲染统一走 render():朝向按 analysisMode2D_(已设)选近俯视/自由透视;
|
||||
// ResetCamera 到"可见"数据包围盒(computeDataBounds 只计可见 prop);rebuildAxes 在二维下自移除;
|
||||
// 末尾 Render + onCameraChanged(底图按新视锥重算)。不再用相机快照(陈旧易错),每次按可见内容取景。
|
||||
render(/*is2D ViewMode=*/false, /*resetCamera=*/true);
|
||||
}
|
||||
|
||||
void VtkSceneView::rebuildAxes() {
|
||||
// 先移除上一次的坐标轴 prop:render 可能在一次 rebuild 内多次调用(末尾统一 render +
|
||||
// 异步回灌 render),不先移除会叠加坐标轴(评审 HIGH)。移除后再算 bounds(仅数据图元)。
|
||||
|
|
@ -310,6 +342,9 @@ void VtkSceneView::rebuildAxes() {
|
|||
scene_.renderer()->RemoveViewProp(currentAxes_);
|
||||
currentAxes_ = nullptr;
|
||||
}
|
||||
// 二维分析无立体坐标轴:任何渲染路径(全量/增量/切模式)走到此都不重建坐标轴,
|
||||
// 保证切到二维分析后坐标轴消失、且后续增量渲染不把它带回来。
|
||||
if (analysisMode2D_) return;
|
||||
// 坐标轴随数据包围盒重建:仅按数据图元算 bounds(不含底图,否则被~公里级底图撑大),
|
||||
// 再造 vtkCubeAxesActor 入场。None 模式或无数据 → buildAxes 返回 nullptr,场景无坐标轴。
|
||||
double bounds[6];
|
||||
|
|
@ -340,14 +375,18 @@ void VtkSceneView::render(bool is2D, bool resetCamera) {
|
|||
// 坐标轴仅三维视图显示(2D 俯视测线不需要立体坐标轴)。
|
||||
if (!is2D) rebuildAxes();
|
||||
// 相机预设(朝向)只在取景时应用——保留相机的重建(改放大系数)不重设朝向,否则也会跳视角。
|
||||
// 朝向优先看二维分析模式(本期 A):处二维分析→近俯视;否则按 ViewMode(旧 Map2D 正俯视/三维自由)。
|
||||
// 这样 VE 改/项目切等全量重建在二维分析下仍保持近俯视,不跳回三维自由视角。
|
||||
if (resetCamera) {
|
||||
if (is2D)
|
||||
if (analysisMode2D_)
|
||||
geopro::render::applyNearTop2D(scene_.renderer());
|
||||
else if (is2D)
|
||||
geopro::render::applyTop2D(scene_.renderer());
|
||||
else
|
||||
geopro::render::applyFree3D(scene_.renderer());
|
||||
double bounds[6];
|
||||
if (computeDataBounds(bounds))
|
||||
scene_.renderer()->ResetCamera(bounds); // 取景到数据(不含底图,否则数据缩成小点)
|
||||
scene_.renderer()->ResetCamera(bounds); // 取景到"可见"数据(不含底图,否则数据缩成小点)
|
||||
else
|
||||
scene_.renderer()->ResetCamera();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
|
@ -82,6 +83,13 @@ public:
|
|||
// 相机程序化变化(取景/预设/缩放)后回调,供底图按新视锥重算覆盖(否则首帧部分瓦片要手动微动才出)。
|
||||
std::function<void()> onCameraChanged;
|
||||
|
||||
// ── 二维分析改造 A 期:一场景两相机 ──────────────────────────────────────────
|
||||
// 切「二维分析」(is2D=true):相机锁近俯视、显 2D 足迹/隐 3D(体/帘面/异常);切回反之。
|
||||
// 只翻 actor 可见标志(不清空、不重建)→ 切换瞬时、零重插值。地形/底图常驻不动。
|
||||
// 切片显隐 + 交互锁由 InteractionManager::setMode2D 配合(上层在同一处调两者)。
|
||||
void setAnalysisMode2D(bool is2D);
|
||||
bool isAnalysisMode2D() const { return analysisMode2D_; }
|
||||
|
||||
private:
|
||||
// 首个带经纬数据(剖面/足迹)到达时把共享 frame 重锚到其 lat/lon 包围盒中心:使数据落在世界原点近旁
|
||||
// (否则样本默认原点可能离真实数据数百公里→图元在视锥外、移动视角也找不到)。已锚或无经纬则跳过。
|
||||
|
|
@ -146,6 +154,11 @@ private:
|
|||
std::vector<vtkSmartPointer<vtkProp>> miscProps_;
|
||||
std::string volumeOwnerDs_; // 当前 currentVolumeImage_ 归属的 ds(其被移除时置空切片源)
|
||||
std::map<std::string, vtkSmartPointer<vtkActor>> anomalyProps_; // 异常 id → 3D actor
|
||||
|
||||
// ── 二维分析改造 A 期 ──
|
||||
// 哪些 dsProps_ 条目是 2D 足迹(addMapLine):切 tab 按此区分维度翻可见(其余 dsProps_=帘面/体=3D)。
|
||||
std::set<std::string> mapLineDs_;
|
||||
bool analysisMode2D_ = false; // 当前是否处二维分析(默认三维:启动在「三维分析」tab)
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -1078,6 +1078,18 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
if (*view2dMode == 4) sceneCtrl->set2DPlacement(4, z);
|
||||
});
|
||||
|
||||
// ── 二维分析改造 A 期:切「三维分析/二维分析」tab → 一场景两相机 ──────────────────
|
||||
// 三处协作:①切片隐藏+交互锁(仅平移+缩放) [InteractionManager];②按目标维度重置取景基线
|
||||
// [VtkSceneController]——使切换后该维度首条数据自动取景;③维度显隐+近俯视/自由相机+取景+坐标轴+
|
||||
// 渲染 [VtkSceneView]。顺序:先 ①②(都不渲染),最后 ③ 收尾统一渲染。只翻可见标志、不清空/重建 →
|
||||
// 切换瞬时;地形+底图常驻。
|
||||
QObject::connect(drawer, &geopro::app::ColumnDrawer::analysisModeChanged, &window,
|
||||
[interactionMgr, sceneCtrl, sceneView](bool is2D) {
|
||||
interactionMgr->setMode2D(is2D);
|
||||
sceneCtrl->onAnalysisModeChanged(is2D);
|
||||
sceneView->setAnalysisMode2D(is2D);
|
||||
});
|
||||
|
||||
// 首个真实剖面到达 → frame 重锚到数据 lat/lon 后,把选中的底图加载到数据所在位置
|
||||
// (默认天地图即在此刻出现,避免启动时在样本原点拉无关瓦片)。
|
||||
sceneView->onFrameReanchored = [basemap, basemapKind]() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
#include "panels/columns/Column2DDataset.hpp"
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include <QComboBox>
|
||||
|
||||
#include "EmptyAwareComboBox.hpp"
|
||||
|
|
@ -82,15 +85,25 @@ Column2DDataset::Column2DDataset(QWidget* parent) : QWidget(parent) {
|
|||
}
|
||||
|
||||
void Column2DDataset::setDatasets(const std::vector<geopro::data::DsRow>& rows) {
|
||||
// 增量保留:记住当前已勾选的足迹 ds,重建后复原(仍存在的项保持勾选)。否则对象树每次增删勾选都触发
|
||||
// 本刷新 → 清空全部勾选 + 上抛空集 → 已渲染足迹被移除、列表选中丢失(用户反馈:必须增量更新,
|
||||
// 与三维分析段 CategorySection::rebuildList 同一处理)。
|
||||
std::set<std::string> wasChecked;
|
||||
for (QTreeWidgetItemIterator it(list_); *it; ++it)
|
||||
if ((*it)->checkState(0) == Qt::Checked)
|
||||
wasChecked.insert((*it)->data(0, kDsIdRole).toString().toStdString());
|
||||
|
||||
{
|
||||
QSignalBlocker blocker(list_);
|
||||
populateDatasetList(list_, rows, /*append=*/false);
|
||||
for (QTreeWidgetItemIterator it(list_); *it; ++it) {
|
||||
(*it)->setFlags((*it)->flags() | Qt::ItemIsUserCheckable);
|
||||
(*it)->setCheckState(0, Qt::Unchecked);
|
||||
const std::string id = (*it)->data(0, kDsIdRole).toString().toStdString();
|
||||
// 复原勾选:仍存在的曾勾选项保持勾选;新项默认不勾。
|
||||
(*it)->setCheckState(0, wasChecked.count(id) ? Qt::Checked : Qt::Unchecked);
|
||||
}
|
||||
} // blocker released here
|
||||
// 填充后统一发一次(新载入必为空选):清掉上一次的渲染勾选
|
||||
// 上抛复原后的勾选集(保持渲染,不再清空 → 控制器据 diff 增量保留已渲染足迹,集合不变则不增删)。
|
||||
QStringList ids;
|
||||
for (QTreeWidgetItemIterator it(list_); *it; ++it)
|
||||
if ((*it)->checkState(0) == Qt::Checked)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ ColumnDrawer::ColumnDrawer(QWidget* parent, geopro::data::DatasetFieldDictionary
|
|||
tabs->addTab(analysisTab_, QStringLiteral("三维分析"));
|
||||
tabs->addTab(col2D_, QStringLiteral("二维分析"));
|
||||
tabs->tabBar()->setUsesScrollButtons(false); // 永不出左右滚动箭头(两 tab 必能平铺)
|
||||
// 切 tab → 发 analysisModeChanged(is2D):以"当前 widget 是否 col2D"判定,不写死索引。
|
||||
connect(tabs, &QTabWidget::currentChanged, this, [this, tabs](int idx) {
|
||||
emit analysisModeChanged(tabs->widget(idx) == col2D_);
|
||||
});
|
||||
|
||||
// 折叠按钮:固定宽 18px,垂直拉伸。
|
||||
// 用 SVG 图标(makeGlyph)而非 ◀/▶ 文字——三角符(U+25C0/25B6)不在 YaHei,作按钮文字会触发
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ public:
|
|||
Column2DDataset* col2D() const { return col2D_; }
|
||||
CategoryAnalysisTab* analysisTab() const { return analysisTab_; }
|
||||
|
||||
signals:
|
||||
// 切换「三维分析 / 二维分析」tab:is2D=true 进入二维分析。上层据此切相机+翻维度可见标志。
|
||||
void analysisModeChanged(bool is2D);
|
||||
|
||||
public slots:
|
||||
void toggleCollapsed();
|
||||
void expand(); // 强制展开(进入全屏时确保三栏可见)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ void VtkSceneController::setCheckedDatasets(const QStringList& dsIds) {
|
|||
// 取景意图按「场景是否已有数据到场过」判定,而非 checkedDs_ 是否空——否则连续快速勾选第二个
|
||||
// ds 时 checkedDs_ 已非空但首批尚未到场,会被误清取景意图,相机不对准数据 → 看似不渲染。
|
||||
fitOnArrival_ = !hadArrivedData_;
|
||||
if (checkedDs_.empty()) hadArrivedData_ = false; // 全取消 → 下批到场重新取景
|
||||
// 仅当 3D 与 2D 都空才复位取景基线:否则取消全部 3D 但仍有 2D 足迹时,下个 3D 勾选会误跳取景。
|
||||
if (checkedDs_.empty() && checked2dDs_.empty()) hadArrivedData_ = false;
|
||||
|
||||
const unsigned long long gen = rebuildGeneration_; // 不自增:并发增量互不作废
|
||||
for (const auto& id : checkedDs_)
|
||||
|
|
@ -63,14 +64,16 @@ void VtkSceneController::setChecked2DDatasets(const QStringList& dsIds) {
|
|||
// 二维足迹始终画进 View3D 场景,且按 dsId 跟踪 → 一律增量 diff(不全量重建,不打断 3D 帘面/体)。
|
||||
const std::set<std::string> oldSet(checked2dDs_.begin(), checked2dDs_.end());
|
||||
const std::set<std::string> newSet(newDs.begin(), newDs.end());
|
||||
// 此前空场景(无 3D 数据且无 2D 足迹) → 首批足迹到场自动取景;否则增量追加保持相机不跳。
|
||||
const bool wasEmpty = checkedDs_.empty() && checked2dDs_.empty();
|
||||
|
||||
for (const auto& id : checked2dDs_)
|
||||
if (!newSet.count(id)) view_.removeDataset(id); // 取消勾选 → 移除该足迹图元
|
||||
|
||||
checked2dDs_ = std::move(newDs);
|
||||
fitOnArrival_ = wasEmpty; // 首批足迹(空场景)取景;否则保持当前相机不跳
|
||||
// 取景基线与 3D 路径统一用 hadArrivedData_(而非"两栏皆空"):否则二维分析下若已有隐藏的 3D 数据,
|
||||
// 勾选首条足迹会因 wasEmpty=false 而不取景 → 足迹落在视野外。切 tab 时 onAnalysisModeChanged 已按
|
||||
// 目标维度是否有数据重置该基线,故此处首条可见维度数据能正确取景。
|
||||
fitOnArrival_ = !hadArrivedData_;
|
||||
if (checkedDs_.empty() && checked2dDs_.empty()) hadArrivedData_ = false;
|
||||
|
||||
// 足迹画进 View3D 场景;mode=0 关闭 → 仅记录勾选不渲染(见 set2DPlacement 切回时补画)。
|
||||
if (placement2dMode_ != 0 && mode_ == ViewMode::View3D) {
|
||||
|
|
@ -210,6 +213,14 @@ void VtkSceneController::setViewMode(ViewMode mode) {
|
|||
rebuildInternal();
|
||||
}
|
||||
|
||||
void VtkSceneController::onAnalysisModeChanged(bool is2D) {
|
||||
// 切「三维分析/二维分析」tab:按目标维度是否已有数据重置取景基线。
|
||||
// 目标维度空 → hadArrivedData_=false:切换后该维度第一条数据自动取景(治"3D 数据不知生成到哪")。
|
||||
// 目标维度非空 → hadArrivedData_=true:视图切换时已 fit 到该维度,后续勾选不再跳(与三维一致)。
|
||||
// 显隐/相机/坐标轴由 VtkSceneView::setAnalysisMode2D 处理(上层在同一处调用);此处只管取景基线。
|
||||
hadArrivedData_ = is2D ? !checked2dDs_.empty() : !checkedDs_.empty();
|
||||
}
|
||||
|
||||
void VtkSceneController::setLayer(SceneLayer layer, bool on) {
|
||||
switch (layer) {
|
||||
case SceneLayer::Curtain: showCurtain_ = on; break;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ public slots:
|
|||
// 二维足迹摆放高度(mode:0关闭 /1 Z=0 /2 顶部 /3 底部 /4 自定义;customZ 仅 mode=4 用)。
|
||||
void set2DPlacement(int mode, double customZ);
|
||||
void setViewMode(ViewMode mode);
|
||||
// 切「三维分析/二维分析」tab(A 期):按目标维度是否已有数据重置取景基线,使切换后该维度第一条
|
||||
// 数据自动取景。显隐/相机/坐标轴由 VtkSceneView::setAnalysisMode2D 负责(上层在同一处一并调用)。
|
||||
void onAnalysisModeChanged(bool is2D);
|
||||
void setLayer(SceneLayer layer, bool on);
|
||||
void setVerticalExaggeration(double ve);
|
||||
void rebuild(); // 主题切换等外部触发的重渲染
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ namespace {
|
|||
// 三维斜视方位角 / 仰角。
|
||||
constexpr double kAzimuth = 30.0;
|
||||
constexpr double kElevation = 25.0;
|
||||
// 二维分析近俯视:自正俯视下压的角度(12°→俯角约78°)。留一点倾斜使高程差可辨。
|
||||
constexpr double kNearTopTilt = 12.0;
|
||||
} // namespace
|
||||
|
||||
void applyTop2D(vtkRenderer* r)
|
||||
|
|
@ -37,6 +39,20 @@ void applyFree3D(vtkRenderer* r)
|
|||
r->ResetCamera();
|
||||
}
|
||||
|
||||
void applyNearTop2D(vtkRenderer* r)
|
||||
{
|
||||
if (!r) return;
|
||||
auto* c = r->GetActiveCamera();
|
||||
c->ParallelProjectionOff(); // 透视:配合一点倾斜,使高程差可见(正交/正俯视下不可辨)
|
||||
// 自正俯视(+Z 向下看、北朝上)起,下压 kNearTopTilt → 俯角约 78°;方位不偏(正北俯视)。
|
||||
c->SetFocalPoint(0, 0, 0);
|
||||
c->SetPosition(0, 0, 1);
|
||||
c->SetViewUp(0, 1, 0);
|
||||
c->Elevation(kNearTopTilt);
|
||||
c->OrthogonalizeViewUp();
|
||||
r->ResetCamera();
|
||||
}
|
||||
|
||||
void applyView(vtkRenderer* r, ViewDir dir)
|
||||
{
|
||||
if (!r) return;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ void applyTop2D(vtkRenderer* r);
|
|||
// 自由三维:透视投影,斜视方位看到剖面立体。
|
||||
void applyFree3D(vtkRenderer* r);
|
||||
|
||||
// 二维分析近俯视:透视投影,自正俯视下压一点(约12°→约78°俯角)。留一点倾斜使高程差可见
|
||||
// (绝对正俯视下高程不可辨),仅平移+缩放(旋转由 interactor style 锁定)。
|
||||
void applyNearTop2D(vtkRenderer* r);
|
||||
|
||||
// 快捷视图方向(世界系 x=East,y=North,z=-depth)。
|
||||
// Top 俯视 (相机在 +Z 向下看)
|
||||
// Bottom 仰视 (相机在 -Z 向上看)
|
||||
|
|
|
|||
|
|
@ -260,6 +260,14 @@ void InteractionManager::closeAll() {
|
|||
safeRender();
|
||||
}
|
||||
|
||||
void InteractionManager::setMode2D(bool is2D) {
|
||||
// 切片属三维内容:二维分析隐藏(不销毁→切回零重建)、三维分析显示。
|
||||
for (auto& s : slices_)
|
||||
if (s) s->setVisible(!is2D);
|
||||
if (style_) style_->setLock2D(is2D); // 二维=禁旋转、左键平移(仅平移+缩放)
|
||||
// 不在此渲染:相机/地形/底图/维度显隐及统一 Render 由 VtkSceneView::setAnalysisMode2D 收尾。
|
||||
}
|
||||
|
||||
void InteractionManager::flipView() {
|
||||
if (!renderer_) return;
|
||||
auto* cam = renderer_->GetActiveCamera();
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ public:
|
|||
void closeSelected();
|
||||
// 关闭并释放所有切片(切到二维 / 清场 / 体素重建前调)。
|
||||
void closeAll();
|
||||
|
||||
// 切二维分析(is2D=true)/三维分析(false):翻所有切片显隐(不销毁,切回零重建) + 锁/解锁交互样式
|
||||
// (二维=仅平移+缩放、禁旋转)。地形/底图/相机由 VtkSceneView::setAnalysisMode2D 处理,此处不渲染。
|
||||
void setMode2D(bool is2D);
|
||||
// 关闭并释放某体下的所有切片(该体移除/重建时;不动其它体的切片)。
|
||||
void closeSlicesOfVolume(const std::string& volumeDsId);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <chrono>
|
||||
|
||||
#include <vtkCallbackCommand.h>
|
||||
#include <vtkCamera.h>
|
||||
#include <vtkCellPicker.h>
|
||||
#include <vtkMath.h>
|
||||
|
|
@ -47,6 +48,15 @@ bool PickInteractorStyle::pickWorld(Vec3& out) {
|
|||
|
||||
void PickInteractorStyle::OnLeftButtonDown() {
|
||||
auto* iren = this->GetInteractor();
|
||||
// 二维分析:左键拖动=平移(等同中键),不拾取/不旋转 → 仅平移+缩放。抬键由基类按 State 收尾。
|
||||
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);
|
||||
this->StartPan();
|
||||
return;
|
||||
}
|
||||
Vec3 world;
|
||||
const bool hit = pickWorld(world);
|
||||
|
||||
|
|
@ -82,6 +92,7 @@ void PickInteractorStyle::OnLeftButtonDown() {
|
|||
}
|
||||
|
||||
void PickInteractorStyle::Rotate() {
|
||||
if (lock2D_) return; // 二维分析禁旋转(仅平移+缩放)
|
||||
Vec3 c;
|
||||
if (!this->CurrentRenderer || !getRotateCenter || !getRotateCenter(c)) {
|
||||
Superclass::Rotate(); // 无选中物 → 默认绕焦点旋转
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ public:
|
|||
// 在"按下开始拖动"时调用一次,把焦点设到该中心(位置同步补偿,画面不变)→ 之后绕它旋转、不跳。
|
||||
std::function<bool(Vec3& center)> getRotateCenter;
|
||||
|
||||
// 二维分析锁:开 → 左键拖动改为平移、禁旋转(仅平移+缩放);关 → 恢复三维拾取/旋转交互。
|
||||
void setLock2D(bool on) { lock2D_ = on; }
|
||||
bool isLock2D() const { return lock2D_; }
|
||||
|
||||
void OnLeftButtonDown() override;
|
||||
void OnMouseWheelForward() override;
|
||||
void OnMouseWheelBackward() override;
|
||||
|
|
@ -49,6 +53,9 @@ private:
|
|||
// 记上次左键按下时刻+屏幕位置,两次按下间隔 < kDoubleClickMs 且位置相近视为双击。
|
||||
double lastDownTime_ = -1.0; // 单调时钟(毫秒),-1=无
|
||||
int lastDownPos_[2] = {0, 0};
|
||||
|
||||
// 二维分析模式:左键=平移、禁旋转(仅平移+缩放)。由 InteractionManager 在切 tab 时设。
|
||||
bool lock2D_ = false;
|
||||
};
|
||||
|
||||
} // namespace geopro::render::interact
|
||||
|
|
|
|||
|
|
@ -172,9 +172,16 @@ vtkImageData* SliceTool::reslicedOutput() const {
|
|||
}
|
||||
|
||||
void SliceTool::setInteractive(bool on) {
|
||||
interactive_ = on; // 记录锁定态:setVisible 重显时复原
|
||||
if (widget_) widget_->SetInteraction(on ? 1 : 0); // 关=锁移动/旋转/光标,纹理仍显示
|
||||
}
|
||||
|
||||
void SliceTool::setVisible(bool on) {
|
||||
if (!widget_) return;
|
||||
widget_->SetEnabled(on ? 1 : 0); // 翻显隐(不销毁):几何/纹理保留、切回零重建
|
||||
if (on) widget_->SetInteraction(interactive_ ? 1 : 0); // SetEnabled 可能重置交互→复原锁定态
|
||||
}
|
||||
|
||||
vtkSmartPointer<vtkImageData> SliceTool::coloredResliceImage() const {
|
||||
if (!widget_) return nullptr;
|
||||
vtkImageMapToColors* cm = widget_->GetColorMap(); // widget 内部把 reslice 经 LUT 上色 → 纹理
|
||||
|
|
|
|||
|
|
@ -84,11 +84,16 @@ public:
|
|||
// 拾取选中/右键菜单由 PickInteractorStyle 独立处理,不受此影响。
|
||||
void setInteractive(bool on);
|
||||
|
||||
// 显/隐切片(切到二维分析时隐藏,切回再显):SetEnabled 翻显隐而非销毁,几何/位置保留、
|
||||
// 切回零重建。重显时复原锁定态(SetEnabled 可能把交互重置为开)。
|
||||
void setVisible(bool on);
|
||||
|
||||
// 关闭:Off() 并解除 interactor 绑定(幂等)。
|
||||
void close();
|
||||
|
||||
private:
|
||||
SliceAxis axis_;
|
||||
bool interactive_ = true; // 当前是否允许交互(setInteractive 记录):重显(setVisible)时复原锁定态
|
||||
std::string dsId_; // 已保存切片归属标签(空=临时交互切片)
|
||||
std::string volumeDsId_; // 所属三维体 dsId(多体并发用)
|
||||
vtkImageData* image_; // 非拥有;生命周期由调用方(VtkSceneView 的 currentVolumeImage_)保证
|
||||
|
|
|
|||
|
|
@ -489,6 +489,40 @@ TEST(VtkSceneController, TwoDFootprintCoexistsWith3DCurtain) {
|
|||
EXPECT_EQ(view.curtains, 1);
|
||||
}
|
||||
|
||||
// 回归(BUG3:二维分析切回三维分析后,三维数据"不知生成到哪",要手动适配才定位):
|
||||
// 二维勾选足迹自动取景后 hadArrivedData_=true;切回三维前 onAnalysisModeChanged(false) 按"三维栏空"
|
||||
// 复位取景基线 → 勾选三维数据应自动取景(fitView),而非停在旧相机。
|
||||
TEST(VtkSceneController, ThreeDDataFitsAfterSwitchingBackFrom2D) {
|
||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
||||
VtkSceneController c(ds, sc, view);
|
||||
c.setViewMode(ViewMode::View3D);
|
||||
|
||||
c.onAnalysisModeChanged(true); // 切到二维(2D 栏空 → 基线允许取景)
|
||||
c.setChecked2DDatasets({"traj1"});
|
||||
ASSERT_EQ(view.mapLines, 1);
|
||||
const int fitsAfter2D = view.fitCalls;
|
||||
EXPECT_GE(fitsAfter2D, 1); // 足迹首次到场已取景
|
||||
|
||||
c.onAnalysisModeChanged(false); // 切回三维(3D 栏空 → 基线允许取景)
|
||||
c.setCheckedDatasets({"prof1"});
|
||||
EXPECT_EQ(view.curtains, 1);
|
||||
EXPECT_GT(view.fitCalls, fitsAfter2D); // 三维数据到场自动取景(修复前不取景)
|
||||
}
|
||||
|
||||
// 回归(二维分析下已有隐藏 3D 数据时,勾选首条足迹也应取景;旧 wasEmpty 逻辑因 3D 非空而漏取景):
|
||||
TEST(VtkSceneController, TwoDFootprintFitsEvenWhenHidden3DExists) {
|
||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
||||
VtkSceneController c(ds, sc, view);
|
||||
c.setViewMode(ViewMode::View3D);
|
||||
c.setCheckedDatasets({"prof1"}); // 三维数据(取景一次)
|
||||
const int fitsAfter3D = view.fitCalls;
|
||||
|
||||
c.onAnalysisModeChanged(true); // 切到二维(2D 栏空 → 基线允许取景)
|
||||
c.setChecked2DDatasets({"traj1"});
|
||||
EXPECT_EQ(view.mapLines, 1);
|
||||
EXPECT_GT(view.fitCalls, fitsAfter3D); // 首条足迹取景(旧逻辑因有隐藏 3D 而漏)
|
||||
}
|
||||
|
||||
// 自定义摆放(4) → worldZ=customZ;改摆放重摆已勾选足迹(移除旧 + 按新 Z 重加)。
|
||||
TEST(VtkSceneController, TwoDPlacementCustomZAndReplace) {
|
||||
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
|
||||
|
|
|
|||
Loading…
Reference in New Issue