geopro/src/app/VtkSceneView.cpp

406 lines
19 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "VtkSceneView.hpp"
#include <algorithm>
#include <cmath>
#include <memory>
#include <utility>
#include <QDebug>
#include <QString>
#include <vtkActor.h>
#include <vtkProperty.h>
#include <vtkBoundingBox.h>
#include <vtkCubeAxesActor.h>
#include <vtkProp.h>
#include <vtkRenderWindow.h>
#include <vtkRenderer.h>
#include <vtkVolume.h>
#include "CameraPreset.hpp"
#include "Scene.hpp"
#include "Theme.hpp"
#include "actors/AnomalyActor.hpp"
#include "actors/AxesActor.hpp"
#include "actors/CurtainActor.hpp"
#include "actors/MapLineActor.hpp"
#include "actors/TerrainActor.hpp"
#include "actors/VoxelActor.hpp"
#include "geo/GeoLocalFrame.hpp"
namespace geopro::app {
namespace {
// 控制器层枚举 → render 层枚举(保持控制器不依赖 render
geopro::render::AxesMode toRenderMode(geopro::controller::AxesMode m) {
switch (m) {
case geopro::controller::AxesMode::Standard: return geopro::render::AxesMode::Standard;
case geopro::controller::AxesMode::Stereo: return geopro::render::AxesMode::Stereo;
case geopro::controller::AxesMode::None: return geopro::render::AxesMode::None;
}
return geopro::render::AxesMode::Standard;
}
geopro::render::AxesUnit toRenderUnit(geopro::controller::AxesUnit u) {
switch (u) {
case geopro::controller::AxesUnit::None: return geopro::render::AxesUnit::None;
case geopro::controller::AxesUnit::Meter: return geopro::render::AxesUnit::Meter;
case geopro::controller::AxesUnit::Feet: return geopro::render::AxesUnit::Feet;
case geopro::controller::AxesUnit::LatLon: return geopro::render::AxesUnit::LatLon;
}
return geopro::render::AxesUnit::Meter;
}
geopro::render::ViewDir toRenderViewDir(geopro::controller::ViewDir d) {
switch (d) {
case geopro::controller::ViewDir::Front: return geopro::render::ViewDir::Front;
case geopro::controller::ViewDir::Back: return geopro::render::ViewDir::Back;
case geopro::controller::ViewDir::Left: return geopro::render::ViewDir::Left;
case geopro::controller::ViewDir::Right: return geopro::render::ViewDir::Right;
case geopro::controller::ViewDir::Top: return geopro::render::ViewDir::Top;
case geopro::controller::ViewDir::Bottom: return geopro::render::ViewDir::Bottom;
}
return geopro::render::ViewDir::Front;
}
} // namespace
VtkSceneView::VtkSceneView(geopro::render::Scene& scene, vtkRenderWindow* renderWindow,
std::shared_ptr<geopro::core::GeoLocalFrame> frame, double zRefElev)
: scene_(scene),
renderWindow_(renderWindow),
frame_(std::move(frame)),
zRefElev_(zRefElev) {
// 近裁剪容差调小:场景含近处剖面 + 远处底图(几十km),默认容差会把近裁剪面随远面推出去、
// 切掉离相机近的剖面。调小后近面可贴近,剖面不被切(代价:远处深度精度略降,不可见层无所谓)。
scene_.renderer()->SetNearClippingPlaneTolerance(1e-5);
}
void VtkSceneView::removeProps(std::vector<vtkSmartPointer<vtkProp>>& props) {
for (auto& p : props)
if (p) scene_.renderer()->RemoveViewProp(p);
props.clear();
}
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 && p->GetVisibility()) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
for (const auto& p : miscProps_)
if (p && p->GetVisibility()) { if (double* b = p->GetBounds()) bb.AddBounds(b); }
if (!bb.IsValid()) return false;
bb.GetBounds(out);
return true;
}
double VtkSceneView::dataHorizontalRadius() const {
double b[6];
if (!computeDataBounds(b)) return 0.0;
const double dx = b[1] - b[0], dy = b[3] - b[2];
return 0.5 * std::sqrt(dx * dx + dy * dy); // 水平对角线半径
}
void VtkSceneView::clear() {
// 只移除数据 prop按 ds 跟踪)+ 杂项(地形/测线)+ 坐标轴;不动底图(TileBasemap 自管)→ 重建不丢图。
for (auto& kv : dsProps_) removeProps(kv.second);
dsProps_.clear();
mapLineDs_.clear(); // 2D 足迹维度记录随数据图元一并清(模式标志/相机快照保留)
removeProps(miscProps_);
clearAnomalies(); // 异常 actor 随清场一并移除
if (currentAxes_) {
scene_.renderer()->RemoveViewProp(currentAxes_);
currentAxes_ = nullptr;
}
// 体素 image 失效:置空并通知上层关闭切片(防切片附着到已移除的 image
currentVolumeImage_ = nullptr;
volumeOwnerDs_.clear();
volumes_.clear(); // 多体并发:清场移除所有体 image
frameAnchoredToData_ = false; // 新一轮选择重新按其首个真实剖面重锚原点
if (onVolumeChanged) onVolumeChanged();
}
void VtkSceneView::setVerticalExaggeration(double ve) { verticalExaggeration_ = ve; }
void VtkSceneView::addSurveyLine(const geopro::core::Grid& grid) {
auto line = geopro::render::buildSurveyLine(grid, *frame_);
if (line) {
scene_.addActor(line);
miscProps_.push_back(line);
}
}
void VtkSceneView::anchorFrameIfNeeded(const std::vector<double>& lat,
const std::vector<double>& lon, int n) {
// 首个带经纬数据到达 → 把 GeoLocalFrame 原点重锚到其 lat/lon 包围盒中心:使局部坐标从 0 附近起
// (轴刻度有意义),同一选择内多条剖面/足迹共用此原点 → 相互地理配准。已锚或无经纬则保持不动。
if (frameAnchoredToData_ || n < 1) return;
if (static_cast<int>(lat.size()) < n || static_cast<int>(lon.size()) < n) return;
double la0 = lat[0], la1 = lat[0], lo0 = lon[0], lo1 = lon[0];
for (int i = 1; i < n; ++i) {
la0 = std::min(la0, lat[i]); la1 = std::max(la1, lat[i]);
lo0 = std::min(lo0, lon[i]); lo1 = std::max(lo1, lon[i]);
}
// 就地重锚共享 frame不换对象→ 同持此 frame 的底图层等随即一致对齐。
frame_->reanchor((la0 + la1) / 2.0, (lo0 + lo1) / 2.0);
frameAnchoredToData_ = true;
if (onFrameReanchored) onFrameReanchored(); // 通知底图刷新到数据位置
}
void VtkSceneView::addCurtain(const std::string& dsId, const geopro::core::Grid& grid,
const geopro::core::ColorScale& cs) {
anchorFrameIfNeeded(grid.lat, grid.lon, grid.nx()); // 首个带经纬剖面 → 重锚原点
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);
}
}
void VtkSceneView::addVolume(const std::string& dsId, const geopro::data::VolumeGrid& vol,
const geopro::core::ColorScale& cs) {
// 纵向夸张烤进 image 的 z 原点/间距(与帘面 SetScale 同倍,保证纵向一致)。
// 用暴露 image 的 buildVoxel 重载:保留 currentVolumeImage_ 供 P3 切片附着(几何含 VE
vtkSmartPointer<vtkImageData> image;
auto volume = geopro::render::buildVoxel(
vol.vol, cs, vol.origin[0], vol.origin[1], vol.origin[2] * verticalExaggeration_,
vol.spacing[0], vol.spacing[1], vol.spacing[2] * verticalExaggeration_, vol.vmin, vol.vmax,
image);
if (volume) {
// 体 actor 不参与拾取切片选中靠点中切片平面widget 交互/拾取)。否则点击落到体内部时
// picker 命中体、worldPoint 落体内 → nearestSlice 按平面距离选错切片(用户 ④ 串选)。
volume->PickableOff();
volume->SetVisibility(analysisMode2D_ ? 0 : 1); // 体=3D内容二维分析下隐藏
scene_.addViewProp(volume);
dsProps_[dsId].push_back(volume);
currentVolumeImage_ = image;
currentColorScale_ = cs;
currentVmin_ = vol.vmin;
currentVmax_ = vol.vmax;
volumeOwnerDs_ = dsId;
volumes_[dsId] = VolumeRec{image, cs, vol.vmin, vol.vmax}; // 多体并发:登记本体 image
if (onVolumeChanged) onVolumeChanged();
}
}
void VtkSceneView::addMapLine(const std::string& dsId, const geopro::data::MapLine& line,
double worldZ) {
// 2D 足迹:经共享 frame 投影到世界 XY、Z=worldZ。按 dsId 跟踪(与帘面同 dsProps_ → removeDataset 复用)。
// worldZ 已是最终世界高程(含摆放语义),不再施加 VE足迹是水平线非随深度的竖直图元
// 足迹可能是首个(且唯一)带经纬的数据 → 与帘面同样重锚原点,否则按样本默认原点投到数百公里外不可见。
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 按维度翻可见)
}
}
void VtkSceneView::addTerrain(const geopro::data::TerrainPaths& paths) {
auto terrain = geopro::render::buildTerrain(paths.demPath, paths.imagePath, *frame_, zRefElev_,
verticalExaggeration_);
if (terrain) {
scene_.addActor(terrain);
miscProps_.push_back(terrain);
}
}
void VtkSceneView::removeDataset(const std::string& dsId) {
auto it = dsProps_.find(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()) {
const auto& last = *volumes_.rbegin();
volumeOwnerDs_ = last.first;
currentVolumeImage_ = last.second.image;
currentColorScale_ = last.second.cs;
currentVmin_ = last.second.vmin;
currentVmax_ = last.second.vmax;
} else {
currentVolumeImage_ = nullptr;
volumeOwnerDs_.clear();
}
}
if (wasVolume && onVolumeChanged) onVolumeChanged(); // 任一体移除 → 上层多体同步切片
}
void VtkSceneView::addAnomaly(const geopro::core::Anomaly& a) {
if (a.id.empty()) return;
removeAnomaly(a.id); // 幂等:同 id 先移除旧 actor避免重复
auto actor = geopro::render::buildAnomaly3D(a);
if (!actor) return;
scene_.addActor(actor); // worldPts 已是世界系(含 VE),不再 SetScale
anomalyProps_[a.id] = actor;
}
void VtkSceneView::removeAnomaly(const std::string& anomalyId) {
auto it = anomalyProps_.find(anomalyId);
if (it == anomalyProps_.end()) return;
if (it->second) scene_.renderer()->RemoveViewProp(it->second);
anomalyProps_.erase(it);
}
void VtkSceneView::clearAnomalies() {
for (auto& kv : anomalyProps_)
if (kv.second) scene_.renderer()->RemoveViewProp(kv.second);
anomalyProps_.clear();
}
void VtkSceneView::setAnomalyVisible(const std::string& anomalyId, bool visible) {
auto it = anomalyProps_.find(anomalyId);
if (it != anomalyProps_.end() && it->second) it->second->SetVisibility(visible ? 1 : 0);
}
void VtkSceneView::setSelectedAnomaly(const std::string& anomalyId) {
// 选中者加粗高亮、其余恢复常态列表↔VTK 联动 R84
for (auto& kv : anomalyProps_) {
if (!kv.second) continue;
const bool sel = (kv.first == anomalyId);
kv.second->GetProperty()->SetLineWidth(sel ? 5.0 : 2.0);
kv.second->GetProperty()->SetPointSize(sel ? 12.0 : 8.0);
}
if (renderWindow_) renderWindow_->Render();
}
void VtkSceneView::setAxes(geopro::controller::AxesMode mode, geopro::controller::AxesUnit unit,
int fontSize) {
axesMode_ = mode;
axesUnit_ = unit;
axesFontSize_ = fontSize;
}
void VtkSceneView::setAxesRanges(const geopro::controller::AxisRangeCfg& x,
const geopro::controller::AxisRangeCfg& y,
const geopro::controller::AxisRangeCfg& z) {
axisX_ = x;
axisY_ = y;
axisZ_ = z;
}
void VtkSceneView::applyCameraView(geopro::controller::ViewDir dir) {
geopro::render::applyView(scene_.renderer(), toRenderViewDir(dir)); // 设朝向(内部 ResetCamera 含底图)
double bounds[6];
if (computeDataBounds(bounds))
scene_.renderer()->ResetCamera(bounds); // 重新取景到数据(否则被~公里级底图推到超远)
scene_.renderer()->ResetCameraClippingRange(); // 裁剪面含底图
if (renderWindow_) renderWindow_->Render();
if (onCameraChanged) onCameraChanged(); // 相机变了 → 底图按新视锥重算覆盖
}
void VtkSceneView::zoom(double factor) {
geopro::render::zoomBy(scene_.renderer(), factor);
if (renderWindow_) renderWindow_->Render();
if (onCameraChanged) onCameraChanged();
}
void VtkSceneView::fitView() {
double bounds[6];
if (computeDataBounds(bounds))
scene_.renderer()->ResetCamera(bounds); // 取景到数据(不含底图)
else
geopro::render::fitView(scene_.renderer());
scene_.renderer()->ResetCameraClippingRange(); // 裁剪面含底图 → 不被"蒙版"切掉
if (renderWindow_) renderWindow_->Render();
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() {
// 先移除上一次的坐标轴 proprender 可能在一次 rebuild 内多次调用(末尾统一 render +
// 异步回灌 render不先移除会叠加坐标轴评审 HIGH。移除后再算 bounds仅数据图元
if (currentAxes_) {
scene_.renderer()->RemoveViewProp(currentAxes_);
currentAxes_ = nullptr;
}
// 二维分析无立体坐标轴:任何渲染路径(全量/增量/切模式)走到此都不重建坐标轴,
// 保证切到二维分析后坐标轴消失、且后续增量渲染不把它带回来。
if (analysisMode2D_) return;
// 坐标轴随数据包围盒重建:仅按数据图元算 bounds(不含底图,否则被~公里级底图撑大)
// 再造 vtkCubeAxesActor 入场。None 模式或无数据 → buildAxes 返回 nullptr场景无坐标轴。
double bounds[6];
if (!computeDataBounds(bounds)) return; // 无数据 → 不建坐标轴
geopro::render::AxesOptions opts;
opts.mode = toRenderMode(axesMode_);
opts.unit = toRenderUnit(axesUnit_);
opts.fontSize = axesFontSize_;
opts.frame = frame_.get();
auto toDisp = [](const geopro::controller::AxisRangeCfg& c) {
return geopro::render::AxisDisplay{c.visible, c.customRange, c.min, c.max};
};
opts.x = toDisp(axisX_);
opts.y = toDisp(axisY_);
opts.z = toDisp(axisZ_);
auto axes = geopro::render::buildAxes(bounds, opts, scene_.renderer());
if (axes) {
scene_.addViewProp(axes);
currentAxes_ = axes;
}
}
void VtkSceneView::render(bool is2D, bool resetCamera) {
// 视图区背景永远深色(规范 §0.5:不随明暗切换),让色阶数据更突出。
double bgR, bgG, bgB;
geopro::app::vtkBackground(bgR, bgG, bgB);
scene_.renderer()->SetBackground(bgR, bgG, bgB);
// 坐标轴仅三维视图显示2D 俯视测线不需要立体坐标轴)。
if (!is2D) rebuildAxes();
// 相机预设(朝向)只在取景时应用——保留相机的重建(改放大系数)不重设朝向,否则也会跳视角。
// 朝向优先看二维分析模式(本期 A):处二维分析→近俯视;否则按 ViewMode(旧 Map2D 正俯视/三维自由)。
// 这样 VE 改/项目切等全量重建在二维分析下仍保持近俯视,不跳回三维自由视角。
if (resetCamera) {
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); // 取景到"可见"数据(不含底图,否则数据缩成小点)
else
scene_.renderer()->ResetCamera();
}
scene_.renderer()->ResetCameraClippingRange(); // 裁剪面含底图 → 不被"蒙版"切掉
if (renderWindow_) renderWindow_->Render();
if (onCameraChanged) onCameraChanged(); // 相机/数据变了 → 底图按新视锥重算覆盖
}
void VtkSceneView::renderIncremental() {
// 增量渲染:仅按新包围盒重建坐标轴并提交,不动相机(勾选/取消时视角不跳)。
rebuildAxes();
scene_.renderer()->ResetCameraClippingRange(); // 数据/底图变化后扩裁剪面,防被切
if (renderWindow_) renderWindow_->Render();
}
} // namespace geopro::app