feat(vtk): 相机/包围盒基元 datasetBounds/fitToBounds/orbitToAxis(+orbit数学单测)

- CameraPreset: 纯数学 orbitPose(ViewDir,pivot,distance)->CameraPose,
  方向偏移/up 约定复用 applyView(Top+Z/up+Y, Front-Y/up+Z, …),6 向 TDD 单测。
- VtkSceneView: datasetBounds(按 dsId 并集已渲染 actor 包围盒)、
  fitToBounds(保朝向 ResetCamera(b))、orbitToAxis(保当前缩放距离绕 pivot 转)。
- spec §3.1 T1 基元;不含贴合轴/gnomon/双击(T2/T3/T4)。
This commit is contained in:
gaozheng 2026-07-01 10:02:08 +08:00
parent d991faa1ee
commit 8b85e1e514
5 changed files with 147 additions and 0 deletions

View File

@ -9,6 +9,7 @@
#include <QString>
#include <vtkActor.h>
#include <vtkCamera.h>
#include <vtkProperty.h>
#include <vtkBoundingBox.h>
#include <vtkCubeAxesActor.h>
@ -424,6 +425,44 @@ void VtkSceneView::fitView() {
if (onCameraChanged) onCameraChanged(); // 取景后 → 底图按新视锥重算覆盖(治首帧部分瓦片不出)
}
bool VtkSceneView::datasetBounds(const std::vector<std::string>& 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=|camfocal|,绕 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() {
// 先移除上一次的坐标轴 proprender 可能在一次 rebuild 内多次调用(末尾统一 render +
// 异步回灌 render不先移除会叠加坐标轴评审 HIGH。移除后再算 bounds仅数据图元

View File

@ -62,6 +62,14 @@ public:
void applyCameraView(geopro::controller::ViewDir dir) override;
void zoom(double factor) override;
void fitView() override;
// ── 视图导航基元spec §3.1T1──────────────────────────────────────────────
// 给定 dsIds 的已渲染 actor 世界包围盒并集;无有效返回 false否则填 out=[xmin,xmax,…,zmax]。
bool datasetBounds(const std::vector<std::string>& 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;

View File

@ -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 看 +Yup=+Z
case ViewDir::Back: off[1] = 1; break;
case ViewDir::Left: off[0] = -1; break; // 从 -X 看 +Xup=+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;

View File

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

View File

@ -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
// Toppos 在 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);
}
// Bottompos 在 -Zup=+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 在 -Yup=+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);
}
// Backpos 在 +Yup=+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 在 -Xup=+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);
}
// Rightpos 在 +Xup=+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);