feat(vtk): Plane2D 策略装入平面 z 生命周期(首勾定z/全消消失)+单元测试

This commit is contained in:
gaozheng 2026-06-30 23:21:38 +08:00
parent 24b53f5e0c
commit 989158427e
7 changed files with 115 additions and 9 deletions

View File

@ -16,9 +16,21 @@ void CurtainRenderStrategy::add(const std::string& /*typeId*/, const std::string
}
void CurtainRenderStrategy::remove(const std::string& dsId) { ctrl_.view_.removeDataset(dsId); }
void Plane2DRenderStrategy::add(const std::string& /*typeId*/, const std::string& dsId) {
ctrl_.add2DDatasetAsync(dsId, ctrl_.rebuildGeneration_);
void Plane2DRenderStrategy::add(const std::string& typeId, const std::string& dsId) {
// 该类型平面 z首勾以 dsZ(场景地表基准 zRefElev)定平面,后续勾选返回既有平面 z(投影)。
const double dsZ = ctrl_.view_.zRefElev();
const double z = planeReg_.onChecked(typeId, dsId, dsZ);
dsToType_[dsId] = typeId; // remove 仅得 dsId记下归属类型以回收平面成员
ctrl_.add2DDatasetAsync(dsId, ctrl_.rebuildGeneration_, z);
}
void Plane2DRenderStrategy::remove(const std::string& dsId) { ctrl_.view_.removeDataset(dsId); }
void Plane2DRenderStrategy::remove(const std::string& dsId) {
ctrl_.view_.removeDataset(dsId);
auto it = dsToType_.find(dsId);
if (it == dsToType_.end()) return;
planeReg_.onUnchecked(it->second, dsId); // 该类型成员集空时平面自动消失
dsToType_.erase(it);
}
// 该类型全消(控制器活跃计数归零):此时 planeReg_.hasPlane==false。Phase F2 在此销毁平面底图。
void Plane2DRenderStrategy::onTypeDeactivated(const std::string& /*typeId*/) {}
} // namespace geopro::controller

View File

@ -3,6 +3,8 @@
#include <memory>
#include <string>
#include "controller/PlaneZRegistry.hpp"
namespace geopro::controller {
class VtkSceneController; // 策略委托回控制器既有渲染路径add→addDatasetAsync/add2DDatasetAsyncremove→view.removeDataset
@ -50,13 +52,18 @@ private:
VtkSceneController& ctrl_;
};
// 每个 2D 类型(段)的勾选足迹落到该类型「同一平面」上:首勾的 ds 定平面 z后续投影到该 z
// 全消则平面消失(z 遗忘)。平面 z 生命周期由 PlaneZRegistry 维护;足迹摆放经 add2DDatasetAsync(z)。
class Plane2DRenderStrategy : public IDatasetRenderStrategy {
public:
explicit Plane2DRenderStrategy(VtkSceneController& ctrl) : ctrl_(ctrl) {}
void add(const std::string& typeId, const std::string& dsId) override;
void remove(const std::string& dsId) override;
void onTypeDeactivated(const std::string& typeId) override;
private:
VtkSceneController& ctrl_;
PlaneZRegistry planeReg_; // 按类型的平面 z 生命周期
std::map<std::string, std::string> dsToType_; // dsId→typeId(remove 只得 dsId需自存反查)
};
} // namespace geopro::controller

View File

@ -0,0 +1,45 @@
#pragma once
#include <map>
#include <set>
#include <string>
namespace geopro::controller {
// 纯逻辑:按 2D 类型管理「平面 z + 成员集」。首勾定 z(之后固定); 全消则平面消失。见 spec §8.2。
// 无 Qt/VTK 依赖,便于纯逻辑单测。被 Plane2DRenderStrategy 持有以摆放足迹。
class PlaneZRegistry {
public:
// 某类型某 ds 勾选:首勾(成员集空)记录平面 z=dsZ返回该类型当前平面 z(后续勾选投影到此)。
double onChecked(const std::string& typeId, const std::string& dsId, double dsZ) {
auto& p = planes_[typeId];
if (p.members.empty()) p.z = dsZ; // 首勾定 z
p.members.insert(dsId);
return p.z;
}
// 取消勾选:移出成员;该类型成员集空时清除条目(平面消失z 遗忘)。
void onUnchecked(const std::string& typeId, const std::string& dsId) {
auto it = planes_.find(typeId);
if (it == planes_.end()) return;
it->second.members.erase(dsId);
if (it->second.members.empty()) planes_.erase(it); // 全消 → 平面消失
}
bool hasPlane(const std::string& typeId) const { return planes_.count(typeId) > 0; }
double planeZ(const std::string& typeId) const {
auto it = planes_.find(typeId);
return it == planes_.end() ? 0.0 : it->second.z;
}
// 滑块整体调:移动该类型既有平面 z(类型不存在则无操作)。
void setPlaneZ(const std::string& typeId, double z) {
auto it = planes_.find(typeId);
if (it != planes_.end()) it->second.z = z;
}
private:
struct Plane {
double z = 0.0;
std::set<std::string> members;
};
std::map<std::string, Plane> planes_;
};
} // namespace geopro::controller

View File

@ -100,21 +100,22 @@ void VtkSceneController::setCheckedDatasets(
view_.renderIncremental(); // 立即反映移除 / 触发坐标轴重算
}
void VtkSceneController::add2DDatasetAsync(const std::string& dsId, unsigned long long gen) {
void VtkSceneController::add2DDatasetAsync(const std::string& dsId, unsigned long long gen,
double z) {
if (loadingDs_.count(dsId)) return; // 已在加载(重复勾选竞态)→ 不重复请求
loadingDs_.insert(dsId);
QPointer<VtkSceneController> self(this);
sceneRepo_.loadMapLine(
dsId,
[self, gen, dsId](data::MapLine line) {
[self, gen, dsId, z](data::MapLine line) {
if (!self) return;
self->loadingDs_.erase(dsId);
// gen 作废 / 已取消勾选 → 丢弃迟到回调。
if (gen != self->rebuildGeneration_ || !self->is2DChecked(dsId)) {
return;
}
// 单一自由场景:足迹统一摆到 Z=0按类型平面 z 由 Phase E2 接入)。
self->view_.addMapLine(dsId, line, 0.0);
// 足迹摆到所属 2D 类型的平面 z首勾定、后续投影由 Plane2DRenderStrategy 决定)。
self->view_.addMapLine(dsId, line, z);
self->onDatasetArrived();
},
[self, gen, dsId](const std::string& m) {

View File

@ -96,8 +96,9 @@ private:
void recolorDataset(const QString& dsId);
// 增量加入单个 ds帘面/体素,按图层开关);回调按 gen + 仍勾选 守护,落地后增量渲染。
void addDatasetAsync(const std::string& dsId, unsigned long long gen);
// 增量加入单个 2D 足迹(异步 loadMapLine → addMapLine at 当前摆放 Z回调按 gen + 仍勾选 守护。
void add2DDatasetAsync(const std::string& dsId, unsigned long long gen);
// 增量加入单个 2D 足迹(异步 loadMapLine → addMapLine at 摆放 z回调按 gen + 仍勾选 守护。
// z 为该 ds 所属 2D 类型的平面高程(由 Plane2DRenderStrategy 经 PlaneZRegistry 决定§E2
void add2DDatasetAsync(const std::string& dsId, unsigned long long gen, double z);
void onDatasetArrived(); // 单个 ds 落地后:增量渲染 + 首批数据自动取景
bool isChecked(const std::string& dsId) const;
bool is2DChecked(const std::string& dsId) const;

View File

@ -230,6 +230,8 @@ target_sources(geopro_tests PRIVATE controller/test_workbench_nav_controller.cpp
target_sources(geopro_tests PRIVATE controller/test_vtk_scene_controller.cpp)
# RenderStrategyRegistryregister/getFakeStrategy
target_sources(geopro_tests PRIVATE controller/test_render_strategy_registry.cpp)
# PlaneZRegistry 2D z z / / setPlaneZ
target_sources(geopro_tests PRIVATE controller/test_plane_z_registry.cpp)
target_link_libraries(geopro_tests PRIVATE geopro_controller Qt6::Test)
# io/gpr .iprh + .iprb B-scan C++17 Qt/VTK

View File

@ -0,0 +1,38 @@
#include <gtest/gtest.h>
#include "controller/PlaneZRegistry.hpp"
using geopro::controller::PlaneZRegistry;
TEST(PlaneZRegistry, FirstCheckSetsPlaneZ) {
PlaneZRegistry r;
EXPECT_DOUBLE_EQ(r.onChecked("trajectory", "a", 12.0), 12.0);
EXPECT_TRUE(r.hasPlane("trajectory"));
EXPECT_DOUBLE_EQ(r.planeZ("trajectory"), 12.0);
}
TEST(PlaneZRegistry, SecondCheckKeepsFirstZ) {
PlaneZRegistry r;
r.onChecked("trajectory", "a", 12.0);
EXPECT_DOUBLE_EQ(r.onChecked("trajectory", "b", 99.0), 12.0); // 投影到首个 ds 的平面
}
TEST(PlaneZRegistry, PlaneDisappearsWhenAllUnchecked) {
PlaneZRegistry r;
r.onChecked("trajectory", "a", 12.0);
r.onChecked("trajectory", "b", 99.0);
r.onUnchecked("trajectory", "a");
EXPECT_TRUE(r.hasPlane("trajectory")); // 还有 b
r.onUnchecked("trajectory", "b");
EXPECT_FALSE(r.hasPlane("trajectory")); // 全消 → 平面消失
}
TEST(PlaneZRegistry, RecheckAfterEmptyResetsZ) {
PlaneZRegistry r;
r.onChecked("trajectory", "a", 12.0);
r.onUnchecked("trajectory", "a");
EXPECT_DOUBLE_EQ(r.onChecked("trajectory", "c", 7.0), 7.0); // 重新首勾 → 新 z
}
TEST(PlaneZRegistry, SetPlaneZMovesPlane) {
PlaneZRegistry r;
r.onChecked("trajectory", "a", 12.0);
r.setPlaneZ("trajectory", 30.0);
EXPECT_DOUBLE_EQ(r.planeZ("trajectory"), 30.0);
}