From 8e91351dabc987e68d15ef6df6a818b0e797bb90 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Mon, 22 Jun 2026 20:31:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(vtk):=20=E4=BA=8C=E7=BB=B4=E8=B6=B3?= =?UTF-8?q?=E8=BF=B9=E4=B8=8D=E5=8F=AF=E8=A7=81=20+=20=E5=8F=B0=E6=B9=BE?= =?UTF-8?q?=E5=8C=BA=E5=A4=A9=E5=9C=B0=E5=9B=BE=E5=BA=95=E5=9B=BE=E5=85=A8?= =?UTF-8?q?=E5=8D=A0=E4=BD=8D=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三处缺陷,均由「勾选二维数据集 → VTK 看不到渲染/底图」串起: 1) 摆放默认关闭致足迹静默丢弃 Column2DDataset「2D视图」下拉可见默认项为 Z=0(setCurrentIndex(1)), 但该初始信号在 connect 前发射、且组件早于 main.cpp 接线构造 → 永不送达控制器, 控制器 placement2dMode_ 仍为 0(关闭),勾选被记录却不入场(setChecked2DDatasets 守卫 placement!=0 不通过)。改:控制器默认 1、main.cpp view2dMode 默认 1, 与下拉可见默认项对齐,彻底摆脱对信号时序的依赖。 2) 足迹未重锚 frame → 投到数百公里外、移动视角也找不到 GeoLocalFrame 启动锚在样本 grid1 中位经纬;addCurtain 会重锚到剖面真实经纬, 但 addMapLine 未重锚 → 台湾足迹(经120.8/纬24.7)按样本锚点投到世界原点数十万米外。 改:抽出 anchorFrameIfNeeded(剖面/足迹共用),首个带经纬数据(无论帘面或足迹) 重锚原点;控制器 setChecked2DDatasets 在空场景首批足迹时取景(fitOnArrival)。 3) 台湾区天地图卫星只覆盖到 z16,z17/z18 返回固定「无影像」占位图 底图四叉树拉近时细分到 kMaxZoom=18 → 台湾中心瓦片全是占位图(实测 z17/z18 字节恒等 size=4769/MD5 c0edbdcb,z16 为真实影像;内地有 z18 故正常)。 改:TileBasemap 加自适应 satMaxZoom_,isTiandituNoImagery 按 大小+MD5 精确识别 占位图 → 学习把卫星上限降到 z-1 并重铺(台湾 18→16 收敛,用 z16 真实影像放大); refineTile 卫星层用学习上限,街道矢量仍到 z18;show()/换源复位、refresh 保留。 内地项目零影响(z18 有影像,永不触发降级)。 测试:253/253 通过;新增 TwoDDefaultPlacementRendersAtZeroOnCheck 回归, 原依赖「默认关闭」的两个用例改为显式 set2DPlacement(0)。 --- src/app/TileBasemap.cpp | 36 ++++++++++++++++--- src/app/TileBasemap.hpp | 4 +++ src/app/VtkSceneView.cpp | 35 ++++++++++-------- src/app/VtkSceneView.hpp | 3 ++ src/app/main.cpp | 4 ++- src/controller/VtkSceneController.cpp | 4 ++- src/controller/VtkSceneController.hpp | 5 ++- .../controller/test_vtk_scene_controller.cpp | 17 +++++++-- 8 files changed, 84 insertions(+), 24 deletions(-) diff --git a/src/app/TileBasemap.cpp b/src/app/TileBasemap.cpp index da7255b..14052b9 100644 --- a/src/app/TileBasemap.cpp +++ b/src/app/TileBasemap.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -94,6 +95,15 @@ vtkSmartPointer makeTexture(const QImage& img) { return tex; } +// 天地图「此级别下,该区域无影像」固定占位 JPEG:所有无影像瓦片字节完全一致(实测 size=4769、 +// MD5 固定)。按 大小+MD5 精确识别 → 仅命中该占位图,绝不误判真实影像瓦片。 +bool isTiandituNoImagery(const QByteArray& data) { + if (data.size() != 4769) return false; // 廉价预筛:仅对疑似占位大小算哈希 + static const QByteArray kNoImageMd5 = + QByteArray::fromHex("c0edbdcb2c8ddd3e6a5cf09348c0fcb4"); + return QCryptographicHash::hash(data, QCryptographicHash::Md5) == kNoImageMd5; +} + // Terrarium 像素解码高程:(fx,fy)∈[0,1],fy=0 北/顶行。 double demElev(const QImage& dem, double fx, double fy) { const int w = dem.width(), h = dem.height(); @@ -183,6 +193,7 @@ void TileBasemap::show(Kind kind) { desired_.clear(); // demCache_/texCache_ 跨隐藏-重选保留 → 重选地图秒出,不重拉。 terrainProbed_ = false; + satMaxZoom_ = kMaxZoom; // 新源/新区域:复位卫星层级上限,重新探测该区域影像覆盖深度 kind_ = kind; if (kind == Hidden) { requestRender(); @@ -236,7 +247,9 @@ void TileBasemap::refineTile(int z, int x, int y, std::set& out, int& } // 细分条件:屏幕上太大 → 细分(近细远粗);或瓦片本身比允许范围还大 → 也强制细分, // 否则拉到最远时一块巨瓦(如 78km)正好盖住数据中心、过不了距离剔除 → 覆盖超大面积。 - if ((screenPx > kTargetPx || g > maxTileDist_) && z < kMaxZoom) { + // 卫星层用「学习到的」上限 satMaxZoom_(无影像区域已降级),街道层仍到 kMaxZoom。 + const int maxZ = (kind_ == Satellite) ? satMaxZoom_ : kMaxZoom; + if ((screenPx > kTargetPx || g > maxTileDist_) && z < maxZ) { refineTile(z + 1, 2 * x, 2 * y, out, count); refineTile(z + 1, 2 * x + 1, 2 * y, out, count); refineTile(z + 1, 2 * x, 2 * y + 1, out, count); @@ -368,11 +381,26 @@ void TileBasemap::fetchTile(int z, int x, int y, long long key) { enqueueGet(url, [this, key, z, x, y, gen](QNetworkReply* reply) { reply->deleteLater(); // inFlight 保持到瓦片最终落地(起伏/平面),使旧层在新块就位前不被清理 → 无空白闪烁。 - QImage img; const bool stale = (gen != generation_) || kind_ == Hidden || desired_.find(key) == desired_.end() || placed_.count(key); - const bool ok = !stale && reply->error() == QNetworkReply::NoError && - img.loadFromData(reply->readAll()); + const QByteArray data = + (!stale && reply->error() == QNetworkReply::NoError) ? reply->readAll() : QByteArray(); + // 天地图无影像占位图:该区域此层级无卫星影像 → 学习把卫星上限降到 z-1 并重铺(改用父层真实 + // 影像放大覆盖),不缓存/不落地占位图。仅卫星层适用(街道矢量层全球到 z18 无此占位)。 + if (kind_ == Satellite && !data.isEmpty() && isTiandituNoImagery(data)) { + inFlight_.erase(key); + if (z - 1 < satMaxZoom_) { + satMaxZoom_ = z - 1; + purgeStale(); + refresh(); // 以新上限重铺该区域 + } else { + purgeStale(); + requestRender(); + } + return; + } + QImage img; + const bool ok = !data.isEmpty() && img.loadFromData(data); if (!ok) { inFlight_.erase(key); purgeStale(); diff --git a/src/app/TileBasemap.hpp b/src/app/TileBasemap.hpp index 128de87..cbfb792 100644 --- a/src/app/TileBasemap.hpp +++ b/src/app/TileBasemap.hpp @@ -78,6 +78,10 @@ private: double ve_ = 1.0; // 地形垂向夸张(与剖面 verticalExaggeration 一致才对齐) double maxTileDist_ = 2000.0; // 底图最大距离(米),每次刷新按剖面范围动态算 std::function dataRadiusProvider_; // 返回当前勾选剖面合并范围的半径 + // 卫星层「学习到的」最大可用层级:天地图卫星影像各区域覆盖深度不同(内地到z18, 台湾等仅到z16), + // 超出则回固定「此级别下无影像」占位图。检测到占位即把上限降到 z-1 并重铺(改用父层真实影像放大), + // 使该区域不再请求无影像层。show()/换源时复位为 kMaxZoom 以便新区域重新探测。 + int satMaxZoom_ = 18; // 四叉树当前帧相机参数(refresh 写, refineTile 读):相机位置 + 投影系数 + 视锥 6 面。 double camX_ = 0, camY_ = 0, camZ_ = 0; double projK_ = 1.0; diff --git a/src/app/VtkSceneView.cpp b/src/app/VtkSceneView.cpp index 68eae4b..b158762 100644 --- a/src/app/VtkSceneView.cpp +++ b/src/app/VtkSceneView.cpp @@ -125,23 +125,26 @@ void VtkSceneView::addSurveyLine(const geopro::core::Grid& grid) { } } +void VtkSceneView::anchorFrameIfNeeded(const std::vector& lat, + const std::vector& lon, int n) { + // 首个带经纬数据到达 → 把 GeoLocalFrame 原点重锚到其 lat/lon 包围盒中心:使局部坐标从 0 附近起 + // (轴刻度有意义),同一选择内多条剖面/足迹共用此原点 → 相互地理配准。已锚或无经纬则保持不动。 + if (frameAnchoredToData_ || n < 1) return; + if (static_cast(lat.size()) < n || static_cast(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) { - // 首个带经纬度的剖面到达 → 把 GeoLocalFrame 原点重锚到该剖面 lat/lon 中心:使局部坐标从 0 附近起 - // (轴刻度有意义),同一选择内多条剖面共用此原点 → 相互地理配准。无经纬剖面是平面、不受原点影响。 - const int nx = grid.nx(); - if (!frameAnchoredToData_ && nx > 0 && static_cast(grid.lat.size()) >= nx && - static_cast(grid.lon.size()) >= nx) { - double la0 = grid.lat[0], la1 = grid.lat[0], lo0 = grid.lon[0], lo1 = grid.lon[0]; - for (int i = 1; i < nx; ++i) { - la0 = std::min(la0, grid.lat[i]); la1 = std::max(la1, grid.lat[i]); - lo0 = std::min(lo0, grid.lon[i]); lo1 = std::max(lo1, grid.lon[i]); - } - // 就地重锚共享 frame(不换对象)→ 同持此 frame 的底图层等随即一致对齐。 - frame_->reanchor((la0 + la1) / 2.0, (lo0 + lo1) / 2.0); - frameAnchoredToData_ = true; - if (onFrameReanchored) onFrameReanchored(); // 通知底图刷新到数据位置 - } + anchorFrameIfNeeded(grid.lat, grid.lon, grid.nx()); // 首个带经纬剖面 → 重锚原点 auto curtain = geopro::render::buildCurtain(grid, cs, *frame_); if (curtain) { curtain->SetScale(1.0, 1.0, verticalExaggeration_); // 纵向夸张成墙 @@ -175,6 +178,8 @@ void VtkSceneView::addMapLine(const std::string& dsId, const geopro::data::MapLi double worldZ) { // 2D 足迹:经共享 frame 投影到世界 XY、Z=worldZ。按 dsId 跟踪(与帘面同 dsProps_ → removeDataset 复用)。 // worldZ 已是最终世界高程(含摆放语义),不再施加 VE(足迹是水平线,非随深度的竖直图元)。 + // 足迹可能是首个(且唯一)带经纬的数据 → 与帘面同样重锚原点,否则按样本默认原点投到数百公里外不可见。 + anchorFrameIfNeeded(line.lat, line.lon, static_cast(line.lat.size())); auto actor = geopro::render::buildMapLine(line.lat, line.lon, worldZ, *frame_); if (actor) { scene_.addActor(actor); diff --git a/src/app/VtkSceneView.hpp b/src/app/VtkSceneView.hpp index 02eb295..aecbb43 100644 --- a/src/app/VtkSceneView.hpp +++ b/src/app/VtkSceneView.hpp @@ -76,6 +76,9 @@ public: std::function onCameraChanged; private: + // 首个带经纬数据(剖面/足迹)到达时把共享 frame 重锚到其 lat/lon 包围盒中心:使数据落在世界原点近旁 + // (否则样本默认原点可能离真实数据数百公里→图元在视锥外、移动视角也找不到)。已锚或无经纬则跳过。 + void anchorFrameIfNeeded(const std::vector& lat, const std::vector& lon, int n); // 按当前坐标轴设置 + 场景包围盒重建坐标轴 prop(render 末尾调)。 void rebuildAxes(); void removeProps(std::vector>& props); // 从 renderer 移除并清空 diff --git a/src/app/main.cpp b/src/app/main.cpp index d7314fe..4d86eaf 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -861,7 +861,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re sceneCtrl, &geopro::controller::VtkSceneController::setChecked2DDatasets); // 2D视图下拉(0关闭/1 Z=0/2顶部/3底部/4自定义) + 自定义 Z → 控制器重摆已勾选足迹。 auto custom2dZ = std::make_shared(0.0); - auto view2dMode = std::make_shared(0); + // 默认 1(Z=0):与「2D视图」下拉可见默认项(setCurrentIndex(1))及控制器默认摆放一致—— + // 组合框初始 view2DModeChanged 因 connect 前发射而丢失,此处不可依赖其同步初值。 + auto view2dMode = std::make_shared(1); QObject::connect(drawer->col2D(), &geopro::app::Column2DDataset::view2DModeChanged, sceneCtrl, [sceneCtrl, custom2dZ, view2dMode](int mode) { *view2dMode = mode; diff --git a/src/controller/VtkSceneController.cpp b/src/controller/VtkSceneController.cpp index bbbca89..ee63e27 100644 --- a/src/controller/VtkSceneController.cpp +++ b/src/controller/VtkSceneController.cpp @@ -61,12 +61,14 @@ void VtkSceneController::setChecked2DDatasets(const QStringList& dsIds) { // 二维足迹始终画进 View3D 场景,且按 dsId 跟踪 → 一律增量 diff(不全量重建,不打断 3D 帘面/体)。 const std::set oldSet(checked2dDs_.begin(), checked2dDs_.end()); const std::set newSet(newDs.begin(), newDs.end()); + // 此前空场景(无 3D 数据且无 2D 足迹) → 首批足迹到场自动取景;否则增量追加保持相机不跳。 + const bool wasEmpty = checkedDs_.empty() && checked2dDs_.empty(); for (const auto& id : checked2dDs_) if (!newSet.count(id)) view_.removeDataset(id); // 取消勾选 → 移除该足迹图元 checked2dDs_ = std::move(newDs); - fitOnArrival_ = false; // 足迹增量追加:保持当前相机不跳 + fitOnArrival_ = wasEmpty; // 首批足迹(空场景)取景;否则保持当前相机不跳 // 足迹画进 View3D 场景;mode=0 关闭 → 仅记录勾选不渲染(见 set2DPlacement 切回时补画)。 if (placement2dMode_ != 0 && mode_ == ViewMode::View3D) { diff --git a/src/controller/VtkSceneController.hpp b/src/controller/VtkSceneController.hpp index b4d16d7..958c40a 100644 --- a/src/controller/VtkSceneController.hpp +++ b/src/controller/VtkSceneController.hpp @@ -81,7 +81,10 @@ private: // 二维足迹勾选集(与 checkedDs_ 独立;都画进 View3D 场景)。按 dsId 增量加/删。 std::vector checked2dDs_; // 二维足迹摆放:mode 0关闭/1 Z=0/2顶部/3底部/4自定义;customZ2d_ 仅 mode=4 用。 - int placement2dMode_ = 0; + // 默认 Z=0(1) 与 Column2DDataset「2D视图」下拉可见默认项一致——避免「下拉显示 Z=0 但 + // 控制器实为关闭」的初始信号丢失desync(组合框 setCurrentIndex 在 connect 前发射、且 + // 组件早于 main.cpp 接线构造,初始 view2DModeChanged 永不送达),致勾选足迹静默不渲染。 + int placement2dMode_ = 1; double customZ2d_ = 0.0; ViewMode mode_ = ViewMode::Map2D; bool showCurtain_ = true; diff --git a/tests/controller/test_vtk_scene_controller.cpp b/tests/controller/test_vtk_scene_controller.cpp index 95e2fae..d9443cc 100644 --- a/tests/controller/test_vtk_scene_controller.cpp +++ b/tests/controller/test_vtk_scene_controller.cpp @@ -406,15 +406,27 @@ TEST(VtkSceneController, ZoomAndFitForwarded) { // ── 二维数据集视图:足迹平铺进 View3D ── -// 默认摆放模式=关闭(0) → 勾选 2D 足迹不渲染(仅记录勾选)。 +// 显式关闭摆放(0) → 勾选 2D 足迹不渲染(仅记录勾选)。 TEST(VtkSceneController, TwoDPlacementOffDoesNotRender) { FakeDsRepo ds; FakeSceneRepo sc; FakeView view; VtkSceneController c(ds, sc, view); c.setViewMode(ViewMode::View3D); - c.setChecked2DDatasets({"traj1"}); // 摆放默认关闭 + c.set2DPlacement(0, 0.0); // 显式关闭 + c.setChecked2DDatasets({"traj1"}); EXPECT_EQ(view.mapLines, 0); } +// 回归(足迹默认不渲染 bug):默认摆放=Z=0(1),与 2D视图下拉可见默认项一致 → +// 仅勾选 2D 足迹(不手动调 set2DPlacement)即应在 View3D 渲染,worldZ=0。 +TEST(VtkSceneController, TwoDDefaultPlacementRendersAtZeroOnCheck) { + FakeDsRepo ds; FakeSceneRepo sc; FakeView view; + VtkSceneController c(ds, sc, view); + c.setViewMode(ViewMode::View3D); + c.setChecked2DDatasets({"traj1"}); // 不调 set2DPlacement,依赖默认摆放 + EXPECT_EQ(view.mapLines, 1); + EXPECT_DOUBLE_EQ(view.lastMapLineZ, 0.0); +} + // 摆放 Z=0(1) + 勾选足迹 → 1 条 mapLine,worldZ=0;不影响帘面/体素计数。 TEST(VtkSceneController, TwoDPlacementZeroAddsMapLine) { FakeDsRepo ds; FakeSceneRepo sc; FakeView view; @@ -494,6 +506,7 @@ TEST(VtkSceneController, TwoDPlacementOffToOnDrawsCheckedFootprint) { FakeDsRepo ds; FakeSceneRepo sc; FakeView view; VtkSceneController c(ds, sc, view); c.setViewMode(ViewMode::View3D); + c.set2DPlacement(0, 0.0); // 显式关闭(默认已是 Z=0) c.setChecked2DDatasets({"traj1"}); // 关闭态:仅记录 ASSERT_EQ(view.mapLines, 0);