diff --git a/src/app/VtkSceneView.cpp b/src/app/VtkSceneView.cpp index b0631ad..dd3aa68 100644 --- a/src/app/VtkSceneView.cpp +++ b/src/app/VtkSceneView.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -424,6 +425,44 @@ void VtkSceneView::fitView() { if (onCameraChanged) onCameraChanged(); // 取景后 → 底图按新视锥重算覆盖(治首帧部分瓦片不出) } +bool VtkSceneView::datasetBounds(const std::vector& dsIds, double outB[6]) const { + // computeDataBounds 的按 dsId 版:只并集给定 dsIds 的已渲染 actor 包围盒(同样仅计可见 prop)。 + vtkBoundingBox bb; + for (const auto& id : dsIds) { + auto it = dsProps_.find(id); + if (it == dsProps_.end()) continue; + for (const auto& p : it->second) + if (p && p->GetVisibility()) { if (double* b = p->GetBounds()) bb.AddBounds(b); } + } + if (!bb.IsValid()) return false; + bb.GetBounds(outB); + return true; +} + +void VtkSceneView::fitToBounds(const double b[6]) { + if (!scene_.renderer()) return; + scene_.renderer()->ResetCamera(b); // 保持朝向,仅重定位+缩放到该盒(区别于 fitView 的全场景) + scene_.renderer()->ResetCameraClippingRange(); // 裁剪面含底图 → 不被"蒙版"切掉 + if (renderWindow_) renderWindow_->Render(); + if (onCameraChanged) onCameraChanged(); // 相机变了 → 底图按新视锥重算覆盖 +} + +void VtkSceneView::orbitToAxis(geopro::controller::ViewDir dir, const double pivot[3]) { + auto* r = scene_.renderer(); + if (!r) return; + auto* c = r->GetActiveCamera(); + // 保留当前缩放:取现距离 d=|cam−focal|,绕 pivot 转到 dir 轴(focal=pivot、pos=pivot+off*d)。 + const double d = c->GetDistance(); + const auto pose = geopro::render::orbitPose(toRenderViewDir(dir), pivot, d); + c->SetFocalPoint(pose.focal[0], pose.focal[1], pose.focal[2]); + c->SetPosition(pose.pos[0], pose.pos[1], pose.pos[2]); + c->SetViewUp(pose.up[0], pose.up[1], pose.up[2]); + c->OrthogonalizeViewUp(); + r->ResetCameraClippingRange(); // 只转向不改距离 → 不 ResetCamera;仅扩裁剪面 + if (renderWindow_) renderWindow_->Render(); + if (onCameraChanged) onCameraChanged(); // 相机变了 → 底图按新视锥重算覆盖 +} + void VtkSceneView::rebuildAxes() { // 先移除上一次的坐标轴 prop:render 可能在一次 rebuild 内多次调用(末尾统一 render + // 异步回灌 render),不先移除会叠加坐标轴(评审 HIGH)。移除后再算 bounds(仅数据图元)。 diff --git a/src/app/VtkSceneView.hpp b/src/app/VtkSceneView.hpp index 686814d..f1ad857 100644 --- a/src/app/VtkSceneView.hpp +++ b/src/app/VtkSceneView.hpp @@ -62,6 +62,14 @@ public: void applyCameraView(geopro::controller::ViewDir dir) override; void zoom(double factor) override; void fitView() override; + + // ── 视图导航基元(spec §3.1;T1)────────────────────────────────────────────── + // 给定 dsIds 的已渲染 actor 世界包围盒并集;无有效返回 false,否则填 out=[xmin,xmax,…,zmax]。 + bool datasetBounds(const std::vector& dsIds, double outB[6]) const; + // 相机适配到指定包围盒,保持当前朝向(ResetCamera(b)),用于双击适配/贴合。 + void fitToBounds(const double b[6]); + // 绕 pivot 转到沿 dir 轴看向 pivot,保留当前 focal-to-camera 距离(缩放不变)。 + void orbitToAxis(geopro::controller::ViewDir dir, const double pivot[3]); void render(bool is2D, bool resetCamera = true) override; void renderIncremental() override; diff --git a/src/render/CameraPreset.cpp b/src/render/CameraPreset.cpp index 3101e24..c772c64 100644 --- a/src/render/CameraPreset.cpp +++ b/src/render/CameraPreset.cpp @@ -90,6 +90,29 @@ void applyView(vtkRenderer* r, ViewDir dir) r->ResetCamera(); } +CameraPose orbitPose(ViewDir dir, const double pivot[3], double distance) +{ + // 方向偏移(pos = pivot + offset*distance)与 up 约定须与 applyView 完全一致: + // Top +Z/up+Y、Bottom -Z/up+Y、Front -Y/up+Z、Back +Y/up+Z、Left -X/up+Z、Right +X/up+Z。 + double off[3] = {0, 0, 0}; + double up[3] = {0, 0, 1}; + switch (dir) { + case ViewDir::Top: off[2] = 1; up[0] = 0; up[1] = 1; up[2] = 0; break; + case ViewDir::Bottom: off[2] = -1; up[0] = 0; up[1] = 1; up[2] = 0; break; + case ViewDir::Front: off[1] = -1; break; // 从 -Y 看 +Y,up=+Z + case ViewDir::Back: off[1] = 1; break; + case ViewDir::Left: off[0] = -1; break; // 从 -X 看 +X,up=+Z + case ViewDir::Right: off[0] = 1; break; + } + CameraPose pose; + for (int i = 0; i < 3; ++i) { + pose.focal[i] = pivot[i]; + pose.pos[i] = pivot[i] + off[i] * distance; + pose.up[i] = up[i]; + } + return pose; +} + void zoomBy(vtkRenderer* r, double factor) { if (!r || factor <= 0.0) return; diff --git a/src/render/CameraPreset.hpp b/src/render/CameraPreset.hpp index 5061074..0c33bb3 100644 --- a/src/render/CameraPreset.hpp +++ b/src/render/CameraPreset.hpp @@ -22,6 +22,16 @@ enum class ViewDir { Front, Back, Left, Right, Top, Bottom }; // 应用 6 向正交快捷视图:设 position/focalPoint/viewUp 后 ResetCamera。 void applyView(vtkRenderer* r, ViewDir dir); +// 绕支点转到某轴的相机位姿(纯数学,可单测):focal=pivot,pos=pivot+dir_offset*distance, +// up 按 dir 预设。方向偏移/up 约定与 applyView 完全一致(Top=+Z 看下、+Y 朝上;Front 从 -Y +// 看 +Y、+Z 朝上;…)。用于 orbitToAxis:保留当前缩放距离、只改朝向绕 pivot 转。 +struct CameraPose { + double pos[3]; + double focal[3]; + double up[3]; +}; +CameraPose orbitPose(ViewDir dir, const double pivot[3], double distance); + // 相机缩放:factor>1 拉近(放大),factor<1 推远(缩小)。透视下改距离、正交下改 parallelScale。 void zoomBy(vtkRenderer* r, double factor); diff --git a/tests/render/test_camera_preset.cpp b/tests/render/test_camera_preset.cpp index c563ee8..a123e30 100644 --- a/tests/render/test_camera_preset.cpp +++ b/tests/render/test_camera_preset.cpp @@ -139,6 +139,73 @@ TEST(CameraPreset, ZoomInOrthoReducesParallelScale) { EXPECT_LT(c->GetParallelScale(), before); } +// ── orbitPose(纯数学,供 orbitToAxis 用)────────────────────────────────────── +// 各方向:focal==pivot、|pos-pivot|==distance、pos 沿正确轴偏移、up 与 applyView 约定一致。 +namespace { +constexpr double kPivot[3] = {10.0, -20.0, 5.0}; +constexpr double kDist = 7.0; + +double dist3(const double a[3], const double b[3]) { + const double dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2]; + return std::sqrt(dx * dx + dy * dy + dz * dz); +} +} // namespace + +// Top:pos 在 pivot 的 +Z、距离保持,up=+Y(对齐 applyView Top)。 +TEST(OrbitPose, TopOffsetsPlusZUpY) { + auto pose = orbitPose(ViewDir::Top, kPivot, kDist); + EXPECT_NEAR(pose.focal[0], kPivot[0], 1e-9); + EXPECT_NEAR(pose.focal[1], kPivot[1], 1e-9); + EXPECT_NEAR(pose.focal[2], kPivot[2], 1e-9); + EXPECT_NEAR(dist3(pose.pos, kPivot), kDist, 1e-9); + EXPECT_NEAR(pose.pos[0], kPivot[0], 1e-9); + EXPECT_NEAR(pose.pos[1], kPivot[1], 1e-9); + EXPECT_NEAR(pose.pos[2], kPivot[2] + kDist, 1e-9); // +Z + EXPECT_NEAR(pose.up[0], 0.0, 1e-9); + EXPECT_NEAR(pose.up[1], 1.0, 1e-9); + EXPECT_NEAR(pose.up[2], 0.0, 1e-9); +} + +// Bottom:pos 在 -Z,up=+Y。 +TEST(OrbitPose, BottomOffsetsMinusZUpY) { + auto pose = orbitPose(ViewDir::Bottom, kPivot, kDist); + EXPECT_NEAR(dist3(pose.pos, kPivot), kDist, 1e-9); + EXPECT_NEAR(pose.pos[2], kPivot[2] - kDist, 1e-9); // -Z + EXPECT_NEAR(pose.up[1], 1.0, 1e-9); +} + +// Front:从 -Y 看 +Y → pos 在 -Y,up=+Z。 +TEST(OrbitPose, FrontOffsetsMinusYUpZ) { + auto pose = orbitPose(ViewDir::Front, kPivot, kDist); + EXPECT_NEAR(dist3(pose.pos, kPivot), kDist, 1e-9); + EXPECT_NEAR(pose.pos[1], kPivot[1] - kDist, 1e-9); // -Y + EXPECT_NEAR(pose.up[2], 1.0, 1e-9); +} + +// Back:pos 在 +Y,up=+Z。 +TEST(OrbitPose, BackOffsetsPlusYUpZ) { + auto pose = orbitPose(ViewDir::Back, kPivot, kDist); + EXPECT_NEAR(dist3(pose.pos, kPivot), kDist, 1e-9); + EXPECT_NEAR(pose.pos[1], kPivot[1] + kDist, 1e-9); // +Y + EXPECT_NEAR(pose.up[2], 1.0, 1e-9); +} + +// Left:从 -X 看 +X → pos 在 -X,up=+Z。 +TEST(OrbitPose, LeftOffsetsMinusXUpZ) { + auto pose = orbitPose(ViewDir::Left, kPivot, kDist); + EXPECT_NEAR(dist3(pose.pos, kPivot), kDist, 1e-9); + EXPECT_NEAR(pose.pos[0], kPivot[0] - kDist, 1e-9); // -X + EXPECT_NEAR(pose.up[2], 1.0, 1e-9); +} + +// Right:pos 在 +X,up=+Z。 +TEST(OrbitPose, RightOffsetsPlusXUpZ) { + auto pose = orbitPose(ViewDir::Right, kPivot, kDist); + EXPECT_NEAR(dist3(pose.pos, kPivot), kDist, 1e-9); + EXPECT_NEAR(pose.pos[0], kPivot[0] + kDist, 1e-9); // +X + EXPECT_NEAR(pose.up[2], 1.0, 1e-9); +} + // 空指针/非法 factor 安全。 TEST(CameraPreset, NullAndInvalidAreSafe) { applyView(nullptr, ViewDir::Top);