853 lines
42 KiB
C++
853 lines
42 KiB
C++
#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=|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::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-up:gizmo 相机置于 -dir*dist、焦点在原点 → 与主相机同朝向看向 gizmo。
|
||
double dir[3];
|
||
mainCam->GetDirectionOfProjection(dir); // 已归一化(F−P),指向场景内(背离相机)
|
||
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() {
|
||
// 先移除上一次的坐标轴 prop:render 可能在一次 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
|