406 lines
19 KiB
C++
406 lines
19 KiB
C++
#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() {
|
||
// 先移除上一次的坐标轴 prop:render 可能在一次 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
|