diff --git a/src/data/api/Api3dRepository.cpp b/src/data/api/Api3dRepository.cpp index 5e16bac..ba19ad1 100644 --- a/src/data/api/Api3dRepository.cpp +++ b/src/data/api/Api3dRepository.cpp @@ -148,6 +148,25 @@ std::string Api3dRepository::createGprVolume(const std::string& lineDir, return id; } +std::string Api3dRepository::registerRadarDataset(const std::string& lineDir, + const std::string& linePrefix, + const std::string& name, + const std::string& structParentId, int coarse) { + // 只存元数据、不建体(懒建在首次 loadVolume 后台线程做并缓存)→ DS 优先、勾选才付出建体成本。 + const std::string id = "radar-" + std::to_string(++volumeCounter_); + StoredVolume sv; + sv.name = name; + sv.ddCode = "dd_radar_3d"; + sv.lineDir = lineDir; + sv.linePrefix = linePrefix; + sv.coarse = coarse; + sv.structParentId = structParentId; + sv.createTime = + QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm")).toStdString(); + volumes_[id] = std::move(sv); // 不预填 cachedGrid → 懒建 + return id; +} + const VoxelGenerateRequest* Api3dRepository::lastVoxelRequest(const std::string& dsId) const { const auto it = volumes_.find(dsId); return (it != volumes_.end() && it->second.request) ? &*it->second.request : nullptr; @@ -167,9 +186,10 @@ std::vector Api3dRepository::volumeRows() const { DsRow r; r.id = id; r.dsName = sv.name; - r.ddCode = "dd_voxel"; + r.ddCode = sv.ddCode; // 雷达体="dd_radar_3d",其余 dd_voxel r.typeName = "三维体"; - r.structParentId = sv.request ? sv.request->structParentId : std::string(); // 结构归属(生成位置) + // 结构归属(生成位置):mock 请求体路径取 request->structParentId,雷达体路径取 sv.structParentId。 + r.structParentId = sv.request ? sv.request->structParentId : sv.structParentId; r.createTime = sv.createTime; rows.push_back(std::move(r)); } @@ -348,6 +368,53 @@ void Api3dRepository::loadVolume(const std::string& dsId, onOk(*sv.cachedGrid, sv.cachedScale); return; } + + if (!sv.linePrefix.empty()) { // 雷达体 DS:后台建体,避免阻塞 UI(与 finalizeVolume 同范式) + const std::string lineDir = sv.lineDir, linePrefix = sv.linePrefix; + const int coarse = sv.coarse; + auto deliver = [this, dsId, onOk, onErr](std::shared_ptr g, std::string err) { + if (!g) { + onErr("Api3dRepository::loadVolume(radar): " + err); + return; + } + core::ColorScale scale; // 简易灰度色阶(负→暗、零→灰、正→亮),使体素渲染可见。 + const double mid = 0.5 * (g->vmin + g->vmax); + scale.addStop(g->vmin, core::Rgba{20, 24, 40, 255}); + scale.addStop(mid, core::Rgba{140, 140, 150, 255}); + scale.addStop(g->vmax, core::Rgba{235, 232, 220, 255}); + if (auto it2 = volumes_.find(dsId); it2 != volumes_.end()) { + it2->second.cachedGrid = *g; // 缓存 → 下次命中直渲 + it2->second.cachedScale = scale; + } + onOk(*g, scale); + }; + auto compute = [lineDir, linePrefix, coarse]() { + std::shared_ptr g; + std::string err; + try { + g = std::make_shared( + geopro::data::createRadarVolumeGrid(lineDir, linePrefix, coarse)); + } catch (const std::exception& e) { + err = e.what(); + } + return std::make_tuple(g, err); + }; + if (!QCoreApplication::instance()) { // headless/单测 → 同步交付 + auto r = compute(); + deliver(std::get<0>(r), std::get<1>(r)); + return; + } + std::thread([compute, deliver]() mutable { + auto r = compute(); + auto g = std::get<0>(r); // 具名变量(非结构化绑定)→ C++17 可被 lambda 捕获 + auto err = std::get<1>(r); + QMetaObject::invokeMethod( + qApp, [deliver, g, err]() mutable { deliver(std::move(g), std::move(err)); }, + Qt::QueuedConnection); + }).detach(); + return; + } + const VolumeBuildParams params = sv.params; // 拷贝:异步回调期间存储可能变动 if (params.sourceDatasetIds.empty()) { onErr("Api3dRepository::loadVolume: 三维体无源数据集"); diff --git a/src/data/api/Api3dRepository.hpp b/src/data/api/Api3dRepository.hpp index 6fe5416..929aef5 100644 --- a/src/data/api/Api3dRepository.hpp +++ b/src/data/api/Api3dRepository.hpp @@ -48,6 +48,12 @@ public: // 返回新 dsId;失败抛 std::runtime_error(加载/立方体空,由 io::gpr 链透传)。 std::string createGprVolume(const std::string& lineDir, const std::string& linePrefix, const std::string& name, int coarse = 8); + // 登记一条规范化测线为 dd_radar_3d 体 DS:只存元数据(lineDir/prefix/coarse),【不立即建体】。 + // 首次 loadVolume 时在后台线程惰性建体并缓存(仿 finalizeVolume,避免阻塞 UI)。 + // id="radar-N";structParentId=结构归属(生成位置)。返回新 dsId。 + std::string registerRadarDataset(const std::string& lineDir, const std::string& linePrefix, + const std::string& name, const std::string& structParentId, + int coarse = 4); // 取回某三维体组装的请求体(测试/联调);非本 repo 创建或无 request 时返回 nullptr。 const VoxelGenerateRequest* lastVoxelRequest(const std::string& dsId) const; // 清空内存态三维体/切片/异常(切换项目时调;否则上个项目的体/切片/异常残留在新项目列表)。 @@ -144,6 +150,11 @@ private: std::optional pointCount; // 聚合散点数(finalizeVolume 时持久化,详情统计用) std::optional request; // 组装的真实请求体(createVolume(req) 路径填充) std::string createTime; // 创建时刻(mock,列表副标题/详情用) + // 雷达体 DS(registerRadarDataset 路径):只存元数据,loadVolume 懒建。 + std::string lineDir, linePrefix; // 规范化测线 .head/.data 所在目录 + 前缀 + std::string ddCode = "dd_voxel"; // 数据字典码(雷达体="dd_radar_3d",其余默认 dd_voxel) + std::string structParentId; // 结构归属(生成位置);雷达体无 request 时由此提供 + int coarse = 4; // 沿测线下采样因子(控内存) }; std::map volumes_; // dsId → 体 int volumeCounter_ = 0; diff --git a/tests/data/test_3d_repo.cpp b/tests/data/test_3d_repo.cpp index b6955f3..32d68c1 100644 --- a/tests/data/test_3d_repo.cpp +++ b/tests/data/test_3d_repo.cpp @@ -1,7 +1,14 @@ #include +#include +#include + +#include +#include +#include #include #include +#include #include "api/Api3dRepository.hpp" #include "geo/GeoLocalFrame.hpp" @@ -190,3 +197,82 @@ TEST(Api3dRepo, AnomalyRowsCarryMountAsParent) { EXPECT_EQ(rs->parentId, "slice-9"); // 挂切片 EXPECT_EQ(rs->typeName, "异常"); // typeName 空 → 回退"异常" } + +namespace { +// 写一条规范化测线合成数据(.head + .data);同 Task 6 createRadarVolumeGrid 用例口径: +// SAMPLES=3 / NUMBER_OF_CH=2 / LAST_TRACE=8 → X=道(4 段)、Y=通道(2)、Z=采样(3)。 +void writeSyntheticRadarLine(const std::filesystem::path& dir) { + namespace fs = std::filesystem; + fs::create_directories(dir); + { + std::ofstream f(dir / "L.head"); + f << "SAMPLES:3\nNUMBER_OF_CH:2\nLAST_TRACE:8\nBITS:16\nENDIAN_TYPE:1\n" + "DISTANCE_INTERVAL:0.1\nTIMEWINDOW:30\nDIELECTRIC:9\n"; + } + { + std::ofstream f(dir / "L.data", std::ios::binary); + for (int t = 0; t < 4; ++t) + for (int c = 0; c < 2; ++c) + for (int s = 0; s < 3; ++s) { + std::int16_t v = static_cast(t * 10 + c * 100 + s); + f.write(reinterpret_cast(&v), 2); + } + } +} +} // namespace + +// registerRadarDataset:登记为 dd_radar_3d 体 DS(只存元数据、不建体)→ 仍被认作三维体, +// volumeRows 输出 ddCode="dd_radar_3d" + structParentId。 +TEST(Api3dRepo, RegisterRadarDatasetRoutesAsDdRadar3d) { + const auto dir = std::filesystem::temp_directory_path() / "api3d_radar_register"; + std::error_code ec; + std::filesystem::remove_all(dir, ec); + writeSyntheticRadarLine(dir); + + StubAsyncRepo dsRepo; + auto frame = std::make_shared(22.0, 114.0); + Api3dRepository repo(dsRepo, frame); + + const std::string id = repo.registerRadarDataset(dir.string(), "L", "测线L", + /*structParentId=*/"tm-1", /*coarse=*/1); + EXPECT_FALSE(id.empty()); + EXPECT_TRUE(repo.isVolumeDataset(id)); // 运行期按 volumes_ 成员判体 → 真(即便未建体) + const auto rows = repo.volumeRows(); + ASSERT_FALSE(rows.empty()); + EXPECT_EQ(rows.back().ddCode, "dd_radar_3d"); // 不是 dd_voxel + EXPECT_EQ(rows.back().structParentId, "tm-1"); + + std::filesystem::remove_all(dir, ec); +} + +// loadVolume:首次勾选时懒建雷达体(无 QCoreApplication → 同步交付;全量测试中若其它用例已建 +// QCoreApplication 单例则走异步,processEvents 排空队列交付)。回调收到有效 VolumeGrid。 +TEST(Api3dRepo, LoadVolumeBuildsRadarLazily) { + const auto dir = std::filesystem::temp_directory_path() / "api3d_radar_lazy"; + std::error_code ec; + std::filesystem::remove_all(dir, ec); + writeSyntheticRadarLine(dir); + + StubAsyncRepo dsRepo; + auto frame = std::make_shared(22.0, 114.0); + Api3dRepository repo(dsRepo, frame); + + const std::string id = repo.registerRadarDataset(dir.string(), "L", "测线L", "", 1); + bool got = false; + repo.loadVolume( + id, + [&](VolumeGrid g, geopro::core::ColorScale) { + got = true; + EXPECT_GT(g.vol.nx(), 0); + EXPECT_GT(g.vol.ny(), 0); + EXPECT_GT(g.vol.nz(), 0); + }, + [&](const std::string& e) { FAIL() << e; }); + // 全量测试单进程中其它用例(test_async_repo_dispatch/test_auth)会建持久 QCoreApplication 单例 → + // radar 分支走异步(std::thread + queued 交付),需驱动事件循环排空;无 app 时同步交付 got 已真。 + for (int i = 0; i < 200 && !got && QCoreApplication::instance(); ++i) + QCoreApplication::processEvents(QEventLoop::AllEvents, 10); + EXPECT_TRUE(got); + + std::filesystem::remove_all(dir, ec); +}