geopro/src/app/VtkSceneView.cpp

853 lines
42 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 <array>
#include <cmath>
#include <memory>
#include <utility>
#include <QDebug>
#include <QString>
#include <vtkActor.h>
#include <vtkBillboardTextActor3D.h>
#include <vtkCallbackCommand.h>
#include <vtkCamera.h>
#include <vtkCommand.h>
#include <vtkProperty.h>
#include <vtkBoundingBox.h>
#include <vtkCubeAxesActor.h>
#include <vtkLineSource.h>
#include <vtkNew.h>
#include <vtkPolyDataMapper.h>
#include <vtkProp.h>
#include <vtkPropPicker.h>
#include <vtkTextProperty.h>
#include <vtkPiecewiseFunction.h>
#include <vtkColorTransferFunction.h>
#include <vtkGPUVolumeRayCastMapper.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkSphereSource.h>
#include <vtkVolume.h>
#include <vtkVolumeProperty.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);
ensureGnomon(); // 交互器若已就绪即装配角落方向标(否则首帧 render 时补装)
}
VtkSceneView::~VtkSceneView() {
// 摘除左键/相机观察者clientData=this本对象析构后若留存会悬垂+ 移除叠加渲染器。
// 渲染窗口/交互器可能已在 Qt 拆台中先行析构,全程判空。
if (renderWindow_) {
if (auto* iren = renderWindow_->GetInteractor()) {
if (gnomonClickTag_ != 0) iren->RemoveObserver(gnomonClickTag_);
if (gnomonHoverTag_ != 0) iren->RemoveObserver(gnomonHoverTag_);
}
}
gnomonClickTag_ = 0;
gnomonHoverTag_ = 0;
// 主相机由 scene_ 渲染器持有、生命周期覆盖本对象(构造契约),析构时仍在 → 可安全摘观察者。
if (gnomonObservedCam_ && gnomonCamTag_ != 0) gnomonObservedCam_->RemoveObserver(gnomonCamTag_);
gnomonCamTag_ = 0;
gnomonObservedCam_ = nullptr;
if (renderWindow_ && gnomonRenderer_) renderWindow_->RemoveRenderer(gnomonRenderer_);
}
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; // 新一轮选择重新按其首个真实剖面重锚原点
useFittedAxes_ = false; // 清场:贴合轴复位为全场景轴(选中随数据一并失效,防残留旧盒)
if (onVolumeChanged) onVolumeChanged();
}
void VtkSceneView::setVerticalExaggeration(double ve) { verticalExaggeration_ = ve; }
void VtkSceneView::setVolumeOpacity(double /*maxOpacity*/) {
// 已退役:体不透明度统一由【色阶「不透明度」】控制(每单位 = 单色alpha × 色阶不透明度100%=实心)。
// 旧工具条「透明度」滑块移除;保留空实现仅为满足接口(无调用方)。
}
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_); // 纵向夸张成墙
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) {
// 首次建体时一次性探测 GPU 体绘制支持(此刻 widget 已显示、GL 上下文就绪):不支持则全局回退
// SmartVolumeMapper(CPU),避免无独显/软件 GL/远程桌面上整个体渲不出(空值仍靠传函透明)。
static bool gpuProbed = false;
if (!gpuProbed && renderWindow_) {
gpuProbed = true;
// 关键addVolume 在普通 Qt 槽里跑GL 上下文未必 current → 先 MakeCurrent否则 IsRenderSupported
// 误判为不支持、把有独显的机器错误回退到 CPU体变稠密/分层)。再给真实传函属性供其判定。
renderWindow_->MakeCurrent();
vtkNew<vtkGPUVolumeRayCastMapper> probe;
vtkNew<vtkVolumeProperty> prop;
vtkNew<vtkColorTransferFunction> ctf;
ctf->AddRGBPoint(0.0, 1, 1, 1);
ctf->AddRGBPoint(1.0, 1, 1, 1);
vtkNew<vtkPiecewiseFunction> otf;
otf->AddPoint(0.0, 0.0);
otf->AddPoint(1.0, 1.0);
prop->SetColor(ctf);
prop->SetScalarOpacity(otf);
const bool ok = probe->IsRenderSupported(renderWindow_, prop) != 0;
geopro::render::setVolumeGpuSupported(ok);
qInfo().noquote() << "[volrender] GPU volume ray cast supported=" << ok
<< (ok ? "(GPU+mask 干净白化)" : "(回退 CPU SmartVolumeMapper边缘有细渗色)");
}
// 纵向夸张烤进 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();
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, volume}; // 多体并发:登记本体 image+actor
// G3 等值面:在值域高段(0.7)抽不透明实心异常体(参考图红块)——【反演专属】。
// 雷达体(registerRadarDataset 产的 "radar-" id)跳过:振幅体的 0.7 阈值面=强反射层,
// 既无地球物理含义、又是 SetOpacity(1.0) 实色 actor【不受体不透明度控制】(用户实测:
// 体不透明度调 0 仍见灰色实面=就是它)。"radar-" 是该体唯一生产者指定的稳定 id。
// 注impulse-GPR("vol-")同为振幅体、亦不该有等值面,但 "vol-" 与反演共用前缀,
// 待 ddCode 贯通 addVolume 后再统一按类型门控(见 spec §11)。
const bool isRadarVolume = dsId.rfind("radar-", 0) == 0;
if (!isRadarVolume) {
const double isoVal = vol.vmin + 0.7 * (vol.vmax - vol.vmin);
auto iso = geopro::render::buildIsosurface(image, cs, vol.vmin, vol.vmax, isoVal);
if (iso) {
iso->PickableOff(); // 不参与拾取(同体 actor避免串选
scene_.addActor(iso);
dsProps_[dsId].push_back(iso);
}
}
if (onVolumeChanged) onVolumeChanged();
}
}
bool VtkSceneView::updateVolumeColorInPlace(const std::string& dsId,
const geopro::core::ColorScale& cs) {
auto it = volumes_.find(dsId);
if (it == volumes_.end() || !it->second.volume) return false; // 未渲染 → 调用方回退 remove+add
// 仅换传函image 不变)→ 切片基底保持有效、不被关闭。等值面随阈值色变化较小,暂不重抽。
geopro::render::updateVolumeColors(it->second.volume, cs, it->second.vmin, it->second.vmax);
it->second.cs = cs;
currentColorScale_ = cs;
// onVolumeChanged → InteractionManager.setVolumeImage(同 image, 新 cs):检测 image 未变 → 不关切片,
// 仅更新体色阶并让该体下未保存切片跟随改色(见 InteractionManager::setVolumeImage
if (onVolumeChanged) onVolumeChanged();
if (renderWindow_) renderWindow_->Render();
return true;
}
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()));
// 折线几何建于 Z=0平面高程 worldZ 经 actor SetPosition 施加 → 后续拖 z 值滑块只改 position 即直接平移,
// 无需移除+异步重载几何setMapLinesZ 走此)。首勾/后续勾选在当前平面 z 加入者立即摆到该 z。
auto actor = geopro::render::buildMapLine(line.lat, line.lon, 0.0, *frame_);
if (actor) {
actor->SetPosition(0.0, 0.0, worldZ);
scene_.addActor(actor);
dsProps_[dsId].push_back(actor);
mapLineDs_.insert(dsId); // 记录此 ds 为 2D 足迹(供足迹归属识别)
}
}
void VtkSceneView::setMapLinesZ(const std::vector<std::string>& dsIds, double z) {
// 直接平移足迹:仅对属于足迹的 dsId 改其 actor 的 SetPosition(0,0,z),即时渲染,无移除+重载。
for (const auto& dsId : dsIds) {
if (!mapLineDs_.count(dsId)) continue;
auto it = dsProps_.find(dsId);
if (it == dsProps_.end()) continue;
for (auto& prop : it->second)
if (auto* a = vtkActor::SafeDownCast(prop)) a->SetPosition(0.0, 0.0, z);
}
if (renderWindow_) renderWindow_->Render();
}
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::focusAlongLongAxis(double t, double windowFrac) {
double b[6];
if (!computeDataBounds(b) || !scene_.renderer()) return;
const double ex = b[1] - b[0], ey = b[3] - b[2], ez = b[5] - b[4];
const int ax = (ex >= ey && ex >= ez) ? 0 : (ey >= ez ? 1 : 2); // 最长轴
const double lo = b[2 * ax], hi = b[2 * ax + 1], len = hi - lo;
if (len <= 0.0) return;
if (t < 0.0) t = 0.0;
if (t > 1.0) t = 1.0;
if (windowFrac <= 0.0) windowFrac = 0.12;
const double half = 0.5 * windowFrac * len;
const double center = lo + t * len;
double sub[6] = {b[0], b[1], b[2], b[3], b[4], b[5]}; // 短轴满幅
sub[2 * ax] = (center - half < lo) ? lo : center - half; // 长轴只取窗口段
sub[2 * ax + 1] = (center + half > hi) ? hi : center + half;
scene_.renderer()->ResetCamera(sub); // 保持朝向,仅重定位+缩放到该窗口
scene_.renderer()->ResetCameraClippingRange();
if (renderWindow_) renderWindow_->Render();
if (onCameraChanged) onCameraChanged(); // 底图随新视锥重算
}
double VtkSceneView::longAxisElongation() const {
double b[6];
if (!computeDataBounds(b)) return 0.0;
const double ex = std::abs(b[1] - b[0]), ey = std::abs(b[3] - b[2]), ez = std::abs(b[5] - b[4]);
double mx = ex, mn = ex;
if (ey > mx) mx = ey;
if (ez > mx) mx = ez;
if (ey < mn) mn = ey;
if (ez < mn) mn = ez;
return (mn > 0.0) ? mx / mn : 0.0;
}
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(); // 取景后 → 底图按新视锥重算覆盖(治首帧部分瓦片不出)
}
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::orbitToCurrentPivot(geopro::controller::ViewDir dir) {
// 支点 = 当前坐标轴盒中心(决策 5有选中(贴合轴)→选中子树盒 fittedBounds_否则全场景数据盒。
double b[6];
if (useFittedAxes_) {
for (int i = 0; i < 6; ++i) b[i] = fittedBounds_[i];
} else if (!computeDataBounds(b)) {
return; // 无有效数据包围盒 → 无支点可绕,静默不动
}
const double pivot[3] = {0.5 * (b[0] + b[1]), 0.5 * (b[2] + b[3]), 0.5 * (b[4] + b[5])};
orbitToAxis(dir, pivot); // 复用 T1绕 pivot 转到 dir 轴、保留当前缩放
}
void VtkSceneView::ensureGnomon() {
// 幂等装配:交互器就绪后建一次。用【专用叠加渲染器】(非 vtkOrientationMarkerWidget)图层1、固定
// 右下角视口、InteractiveOff、透明背景 → 无 widget 外框、不可拖动/缩放;相机由 syncGnomonCamera
// 镜像主相机朝向 → gizmo 随场景旋转同步转。三轴线 + 6 方向球(仅球可拾取) + 正向 XYZ 标签。
if (gnomonReady_ || !renderWindow_) return;
auto* iren = renderWindow_->GetInteractor();
if (!iren) return; // QVTK 尚未提供交互器 → 下一帧 render 再补装
// 叠加渲染器图层1 固定右下角(见下 SetViewport) —— 避开底部满宽沿线滑块条(仅雷达体时显示、
// 约占底 46px)。透明背景只显 gizmo 图元FXAA + MSAA 抗锯齿使边缘平滑;非交互不响应任何输入。
renderWindow_->SetNumberOfLayers(2);
gnomonRenderer_ = vtkSmartPointer<vtkRenderer>::New();
gnomonRenderer_->SetLayer(1);
gnomonRenderer_->InteractiveOff();
// 右下角、上抬避开底部满宽「沿线位置」滑块条(约占底 46px)y 从 0.10 抬到 0.15;靠右留 ~1% 边距。
gnomonRenderer_->SetViewport(0.855, 0.15, 0.995, 0.35);
gnomonRenderer_->SetBackgroundAlpha(0.0); // 透明合成到主场景之上,无背景块
gnomonRenderer_->SetUseFXAA(true); // FXAA + 窗口 MSAA 双重:轴线/球/字形边缘平滑
renderWindow_->AddRenderer(gnomonRenderer_);
// 抗锯齿(spec §7):整窗多重采样一次性开(仅当尚未开,不覆盖既有设置) → 平涂盘/白字边缘平滑。
if (renderWindow_->GetMultiSamples() == 0) renderWindow_->SetMultiSamples(8);
const double L = 1.0; // 球心到原点距离(= 轴线长度)
// 业界柔和轴色(非纯 RGB)X 红 / Y 绿 / Z 蓝spec §2。正向球=本色,负向球=本色×0.42(更暗)。
const std::array<double, 3> kColX = {0.90, 0.30, 0.36};
const std::array<double, 3> kColY = {0.55, 0.78, 0.33};
const std::array<double, 3> kColZ = {0.28, 0.45, 0.90};
// 三根过原点的轴线仅连到【正向】球X=红 / Y=绿 / Z=蓝平涂纯色、细、不可拾取spec §5
struct AxisLine { double to[3]; std::array<double, 3> col; };
const AxisLine lines[3] = {
{{L, 0, 0}, kColX}, // X 红
{{0, L, 0}, kColY}, // Y 绿
{{0, 0, L}, kColZ}, // Z 蓝
};
for (const auto& ln : lines) {
vtkNew<vtkLineSource> src;
src->SetPoint1(0.0, 0.0, 0.0);
src->SetPoint2(ln.to[0], ln.to[1], ln.to[2]);
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(src->GetOutputPort());
vtkNew<vtkActor> a;
a->SetMapper(mapper);
a->GetProperty()->SetColor(ln.col[0], ln.col[1], ln.col[2]);
a->GetProperty()->SetLineWidth(1.8f); // 细轴线
a->GetProperty()->LightingOff(); // 平涂纯色,无高光
a->SetPickable(0); // 轴线不参与拾取(仅方向球有方向语义)
gnomonRenderer_->AddViewProp(a);
}
// 6 个方向球(平涂实心盘,非高光 3D 球):正向亮盘 + 白色 XYZ 字标、稍大;负向同色更暗、更小、无字标。
// 方向 → ViewDir 与 CameraPreset 语义一致:+Z=Top、Z=Bottom、+Y=Back、Y=Front、+X=Right、X=Left。
struct DirSpec {
geopro::controller::ViewDir dir;
double pos[3];
std::array<double, 3> base; // 该轴柔和本色
bool positive;
const char* label; // 正向字标;负向 nullptr
};
const DirSpec specs[6] = {
{geopro::controller::ViewDir::Right, {L, 0, 0}, kColX, true, "X"}, // +X
{geopro::controller::ViewDir::Left, {-L, 0, 0}, kColX, false, nullptr}, // X
{geopro::controller::ViewDir::Back, {0, L, 0}, kColY, true, "Y"}, // +Y
{geopro::controller::ViewDir::Front, {0, -L, 0}, kColY, false, nullptr}, // Y
{geopro::controller::ViewDir::Top, {0, 0, L}, kColZ, true, "Z"}, // +Z
{geopro::controller::ViewDir::Bottom, {0, 0, -L}, kColZ, false, nullptr}, // Z
};
gnomonDirs_.clear();
gnomonBaseColor_.clear();
gnomonLabels_.clear();
for (const auto& s : specs) {
// 正向盘 r=0.32(稍大);负向盘 r=0.20(更小、更暗 → 呈"淡环/凹陷"观感,仍可拾取)。
const double radius = s.positive ? 0.32 : 0.20;
const std::array<double, 3> col =
s.positive ? s.base
: std::array<double, 3>{s.base[0] * 0.42, s.base[1] * 0.42, s.base[2] * 0.42};
vtkNew<vtkSphereSource> sphere;
sphere->SetRadius(radius);
sphere->SetThetaResolution(48); // 高分辨率 → 轮廓平滑(平涂下尤重要)
sphere->SetPhiResolution(48);
sphere->SetCenter(s.pos[0], s.pos[1], s.pos[2]);
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(sphere->GetOutputPort());
auto actor = vtkSmartPointer<vtkActor>::New();
actor->SetMapper(mapper);
auto* prop = actor->GetProperty();
prop->SetColor(col[0], col[1], col[2]);
prop->LightingOff(); // 关键(spec §1):平涂实心盘,无镜面/渐变 → 干净的 Blender 式 gizmo
actor->SetOrigin(s.pos[0], s.pos[1], s.pos[2]); // 缩放原点=球心 → hover 放大就地不位移
actor->SetPickable(1); // 6 球均可拾取(负向点击仍 orbit 到对侧)
gnomonRenderer_->AddViewProp(actor);
gnomonDirs_[actor.Get()] = s.dir; // 渲染器持 actor 保活;此处仅记裸指针→方向
gnomonBaseColor_[actor.Get()] = col; // 记本色 → hover 提亮后可复原
if (s.positive && s.label) { // 正向轴字标:始终朝相机的公告板文字,白色粗体、居中于球心
auto lbl = vtkSmartPointer<vtkBillboardTextActor3D>::New();
lbl->SetInput(s.label);
lbl->SetPosition(s.pos[0], s.pos[1], s.pos[2]);
auto* tp = lbl->GetTextProperty();
tp->SetFontSize(20);
tp->SetBold(true);
tp->SetColor(1.0, 1.0, 1.0); // 白字
tp->SetJustificationToCentered();
tp->SetVerticalJustificationToCentered();
lbl->SetPickable(0);
gnomonRenderer_->AddViewProp(lbl);
gnomonLabels_.push_back({lbl.Get(),
{s.pos[0], s.pos[1], s.pos[2]}}); // syncGnomonCamera 推到球前避遮挡
}
}
gnomonPicker_ = vtkSmartPointer<vtkPropPicker>::New();
// 左键高优先级(1.0)观察者:先于交互样式(0.0),命中方向球 → orbit + abort 消费(阻止相机旋转/拾取)。
gnomonClickCmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
gnomonClickCmd_->SetClientData(this);
gnomonClickCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
static_cast<VtkSceneView*>(client)->handleGnomonClick();
});
gnomonClickTag_ = iren->AddObserver(vtkCommand::LeftButtonPressEvent, gnomonClickCmd_, 1.0);
// 鼠标移动高优先级观察者:仅角落内拾取做 hover 高亮,永不 abort → 不阻塞场景旋转/平移/切片交互。
gnomonHoverCmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
gnomonHoverCmd_->SetClientData(this);
gnomonHoverCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
static_cast<VtkSceneView*>(client)->handleGnomonHover();
});
gnomonHoverTag_ = iren->AddObserver(vtkCommand::MouseMoveEvent, gnomonHoverCmd_, 1.0);
// 相机同步:观察主相机 ModifiedEvent每次朝向变化把 gizmo 相机镜像到同朝向 → gizmo 随场景转。
gnomonCamCmd_ = vtkSmartPointer<vtkCallbackCommand>::New();
gnomonCamCmd_->SetClientData(this);
gnomonCamCmd_->SetCallback([](vtkObject*, unsigned long, void* client, void*) {
static_cast<VtkSceneView*>(client)->syncGnomonCamera();
});
if (auto* mainCam = scene_.renderer() ? scene_.renderer()->GetActiveCamera() : nullptr) {
gnomonObservedCam_ = mainCam;
gnomonCamTag_ = mainCam->AddObserver(vtkCommand::ModifiedEvent, gnomonCamCmd_);
}
syncGnomonCamera(); // 初始一次:装配即与当前朝向对齐
gnomonReady_ = true;
}
void VtkSceneView::syncGnomonCamera() {
if (!gnomonRenderer_ || !scene_.renderer()) return;
auto* mainCam = scene_.renderer()->GetActiveCamera();
auto* gcam = gnomonRenderer_->GetActiveCamera();
if (!mainCam || !gcam) return;
// 复制主相机投影方向 + view-upgizmo 相机置于 -dir*dist、焦点在原点 → 与主相机同朝向看向 gizmo。
double dir[3];
mainCam->GetDirectionOfProjection(dir); // 已归一化(FP),指向场景内(背离相机)
double up[3];
mainCam->GetViewUp(up);
const double dist = 10.0;
gcam->SetParallelProjection(1); // 正交投影gizmo 无透视畸变(业界标准)
// 按视口像素长宽比自适应取景半高balls 到 ±(L+r)≈1.32 + 白字留边 → halfExtent=1.5。
// parallelScale = 视口世界半高;水平可见半宽 = scale×aspect。取 scale=halfExtent/min(1,aspect)
// 保证长/宽两向都容得下所有球 → 任意窗口长宽比不裁切(视口归一化随窗拉伸也不失效)。
const double halfExtent = 1.5;
double aspect = 1.0;
const int* wsz = renderWindow_->GetSize();
const double* vp = gnomonRenderer_->GetViewport();
if (wsz && wsz[0] > 0 && wsz[1] > 0) {
const double vpH = (vp[3] - vp[1]) * wsz[1];
if (vpH > 0.0) aspect = (vp[2] - vp[0]) * wsz[0] / vpH;
}
gcam->SetParallelScale(halfExtent / std::min(1.0, aspect));
gcam->SetFocalPoint(0.0, 0.0, 0.0);
gcam->SetPosition(-dir[0] * dist, -dir[1] * dist, -dir[2] * dist);
gcam->SetViewUp(up[0], up[1], up[2]);
gnomonRenderer_->ResetCameraClippingRange();
// 正向白字标签推到球【前】(朝相机 = dir 方向、偏移略大于球半径)billboard 恒面相机,位于球前
// → 不被球面前半遮挡,读数清晰。相机每次转动都随之更新前向偏移。
const double front = 0.40; // > 正向球半径 0.32 → 字浮于球前表面之外
for (const auto& lp : gnomonLabels_) {
if (!lp.first) continue;
lp.first->SetPosition(lp.second[0] - dir[0] * front, lp.second[1] - dir[1] * front,
lp.second[2] - dir[2] * front);
}
}
void VtkSceneView::handleGnomonClick() {
if (!gnomonRenderer_ || !gnomonPicker_ || !renderWindow_) return;
auto* iren = renderWindow_->GetInteractor();
if (!iren) return;
const int ex = iren->GetEventPosition()[0];
const int ey = iren->GetEventPosition()[1];
// 仅当点击落在 gnomon 角落视口矩形内才拾取(否则放行正常场景交互,且省去全场景每次左键的硬件拾取)。
const double* vp = gnomonRenderer_->GetViewport(); // 归一化 [xmin,ymin,xmax,ymax]
const int* sz = renderWindow_->GetSize();
if (sz[0] <= 0 || sz[1] <= 0) return;
const double fx = static_cast<double>(ex) / sz[0];
const double fy = static_cast<double>(ey) / sz[1];
if (fx < vp[0] || fx > vp[2] || fy < vp[1] || fy > vp[3]) return; // 不在角落 → 不 abort放行
// 角落内硬件拾取(仅方向球可拾):命中某方向球 → 取其 ViewDir → 绕当前轴盒中心转到该轴(保留缩放)。
if (gnomonPicker_->PickProp(ex, ey, gnomonRenderer_)) {
vtkProp* leaf = gnomonPicker_->GetViewProp(); // 叠加渲染器内为裸 actor直取即方向球
auto it = gnomonDirs_.find(leaf);
if (it != gnomonDirs_.end()) {
orbitToCurrentPivot(it->second);
if (gnomonClickCmd_) gnomonClickCmd_->SetAbortFlag(1); // 命中才消费:不触发相机旋转/场景拾取
}
}
// 未命中球 → 不 abort左键继续走正常交互旋转/平移/缩放/切片/拾取),保证非干扰。
}
void VtkSceneView::handleGnomonHover() {
// 非阻塞 hover 高亮:绝不 SetAbortFlag → 鼠标移动照常传给交互样式(旋转/平移/切片)。
if (!gnomonRenderer_ || !gnomonPicker_ || !renderWindow_) return;
auto* iren = renderWindow_->GetInteractor();
if (!iren) return;
// 提亮本色(向白混 0.42);复原用记录的本色。二者共用,避免重复。
auto applyColor = [](vtkProp* p, const std::array<double, 3>& c, bool highlight, double scale) {
auto* a = vtkActor::SafeDownCast(p);
if (!a) return;
if (highlight) {
a->GetProperty()->SetColor(c[0] * 0.58 + 0.42, c[1] * 0.58 + 0.42, c[2] * 0.58 + 0.42);
a->SetScale(scale); // 就地放大(origin=球心)
} else {
a->GetProperty()->SetColor(c[0], c[1], c[2]);
a->SetScale(1.0);
}
};
auto restore = [&]() {
if (!gnomonHovered_) return;
auto it = gnomonBaseColor_.find(gnomonHovered_);
if (it != gnomonBaseColor_.end()) applyColor(gnomonHovered_, it->second, false, 1.0);
gnomonHovered_ = nullptr;
};
const int ex = iren->GetEventPosition()[0];
const int ey = iren->GetEventPosition()[1];
const double* vp = gnomonRenderer_->GetViewport(); // 归一化 [xmin,ymin,xmax,ymax]
const int* sz = renderWindow_->GetSize();
if (sz[0] <= 0 || sz[1] <= 0) { restore(); return; }
const double fx = static_cast<double>(ex) / sz[0];
const double fy = static_cast<double>(ey) / sz[1];
if (fx < vp[0] || fx > vp[2] || fy < vp[1] || fy > vp[3]) { // 光标离开角落 → 复原并跳过拾取(廉价)
if (gnomonHovered_) { restore(); renderWindow_->Render(); }
return;
}
// 角落内才做硬件拾取:命中方向球 → 高亮该球、复原旧的;未命中 → 复原。
vtkProp* hit = nullptr;
if (gnomonPicker_->PickProp(ex, ey, gnomonRenderer_)) {
vtkProp* leaf = gnomonPicker_->GetViewProp();
if (gnomonBaseColor_.find(leaf) != gnomonBaseColor_.end()) hit = leaf;
}
if (hit == gnomonHovered_) return; // 无变化 → 不重绘
restore();
if (hit) {
auto it = gnomonBaseColor_.find(hit);
if (it != gnomonBaseColor_.end()) applyColor(hit, it->second, true, 1.18);
gnomonHovered_ = hit;
}
renderWindow_->Render(); // 高亮/复原变更 → 立即刷新
}
void VtkSceneView::rebuildAxes() {
// 先移除上一次的坐标轴 proprender 可能在一次 rebuild 内多次调用(末尾统一 render +
// 异步回灌 render不先移除会叠加坐标轴评审 HIGH。移除后再算 bounds仅数据图元
if (currentAxes_) {
scene_.renderer()->RemoveViewProp(currentAxes_);
currentAxes_ = nullptr;
}
// 坐标轴随数据包围盒重建:仅按数据图元算 bounds(不含底图,否则被~公里级底图撑大)
// 再造 vtkCubeAxesActor 入场。None 模式或无数据 → buildAxes 返回 nullptr场景无坐标轴。
// 贴合态(useFittedAxes_):改用选中子树盒 fittedBounds_只框该子树而非全场景spec §3.2)。
double bounds[6];
if (useFittedAxes_) {
for (int i = 0; i < 6; ++i) bounds[i] = fittedBounds_[i];
} else 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::showFittedAxes(const double b[6]) {
// 选中子树盒 → 冻结为贴合轴 bounds隐去全场景轴rebuildAxes 会先移除旧轴再按 fittedBounds_ 重建)。
useFittedAxes_ = true;
for (int i = 0; i < 6; ++i) fittedBounds_[i] = b[i];
rebuildAxes();
if (renderWindow_) renderWindow_->Render();
}
void VtkSceneView::showSceneAxes() {
// 取消选中 → 复位为全场景总览轴(现状默认)。清掉贴合态后 rebuildAxes 走 computeDataBounds。
useFittedAxes_ = false;
rebuildAxes();
if (renderWindow_) renderWindow_->Render();
}
void VtkSceneView::render(bool is2D, bool resetCamera) {
ensureGnomon(); // 构造时交互器未就绪则于此补装(幂等)
// 视图区背景永远深色(规范 §0.5:不随明暗切换),让色阶数据更突出。
double bgR, bgG, bgB;
geopro::app::vtkBackground(bgR, bgG, bgB);
scene_.renderer()->SetBackground(bgR, bgG, bgB);
// 坐标轴仅三维视图显示2D 俯视测线不需要立体坐标轴)。
if (!is2D) rebuildAxes();
// 相机预设(朝向)只在取景时应用——保留相机的重建(改放大系数)不重设朝向,否则也会跳视角。
// 朝向按 is2D俯视(Map2D)/三维自由透视。
if (resetCamera) {
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() {
ensureGnomon(); // 幂等:交互器就绪后补装角落方向标
// 增量渲染:仅按新包围盒重建坐标轴并提交,不动相机(勾选/取消时视角不跳)。
rebuildAxes();
scene_.renderer()->ResetCameraClippingRange(); // 数据/底图变化后扩裁剪面,防被切
if (renderWindow_) renderWindow_->Render();
}
} // namespace geopro::app