# 三维雷达体接入(规范化格式)Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 让一条"规范化 `.head/.data/.cor`"三维雷达测线在桌面 app 里**登记成 `dd_radar_3d` DS**并渲染成体,复用现成切片/异常工具。 **Architecture:** 在稳定的 `core::BuiltI16` 汇聚点接入新格式——新写"规范化 reader + bridge"产出 `BuiltI16` → `builtI16ToVolumeGrid` → `createRadarVolumeGrid`;上层走 **DS 优先**:`registerRadarDataset` 把体以 `ddCode=dd_radar_3d` 登记进 `volumes_`(运行期路由只认 `volumes_` 成员,故 `isVolumeDataset`/`loadVolume`/`addDatasetAsync` 零改动)→ 勾选 → 渲染。先把现有 `Gpr3dvVolumeBridge` 的"扫值域→量化→通道插值→填体→spacing"抽成共享 helper(消 DRY),Impulse 与规范化两路同调。 **Tech Stack:** C++17、Qt6、VTK 9.6.2、GoogleTest/CTest、CMake(Ninja)、vcpkg。 ## Global Constraints - C++17;命名空间 `geopro::io::gpr` / `geopro::data`;类型 `PascalCase`、函数 `camelCase`、成员 `snake_`、常量 `kPascalCase`。 - 体轴映射固定:**X=道(nx=K) / Y=通道(ny=M) / Z=采样(nz=N)**。 - 数据体磁盘主序固定:**position-major**,扁平索引 `((t*M + c)*N + s)`(t=道, c=通道, s=采样;采样最快、道最慢)。⚠️ 与体内存布局(`((k*ny+j)*nx+i)`,道最快)相反 → **逐体素散射填值,严禁 memcpy/整块拷贝**。 - 量化:复用 `geopro::core::Quant`(`scale=(vmax-vmin)/64000`、`offset=中点`);GPR 体稠密无空洞 → **不置 `kBlank`**;不特判 `-32768`(饱和真实值,经 `toQ` 落普通区间)。 - `BITS` 真源 = `.head` 的 `BITS` 字段(P0 仅支持 16-bit;32-bit 抛 `not implemented`)。`ENDIAN_TYPE` 1=小端/2=大端。 - 通道插值复用纯函数 `geopro::io::gpr::planChannelInterpolation(offsets, targetDy)`(默认 `targetDy=0.025`),通道横向偏移取自 `.head` 的 `CH_X_OFFSETS`,**绝不跨线**。 - 内存:app 体是 `std::vector`(int16 的 4×);单线默认 `coarse=4`(峰值 <0.5GB)。多线/全分辨率不在本计划(走 P2 核外)。 - 构建/测试:⚠️ **`cmake`/`ctest`/`cl` 不在 PATH** —— 必须经 `D:\Git\lanbingtech\geopro\build.bat`(自带 vcvars + VS cmake/ctest)。各 task 步骤里写的 `cmake --build ... && ctest -R ...` 是**意图**,实际跑:(1) **构建+全测**:PowerShell `D:\Git\lanbingtech\geopro\build.bat test`(构建 geopro_tests + ctest 全跑,~68s,**基线 444/444 全绿**——经 build.bat 时 proj.db 等都能找到);(2) **聚焦单测**(构建后快速复跑):`build/release/tests/geopro_tests.exe --gtest_filter=Pattern*`(雷达测试不依赖 proj.db,可直接跑);(3) **app**:`build.bat app`。**重链前关掉在跑的 app/exe**(LNK1104)。**别删** `build/release`。 - 提交:Conventional Commits(`feat:`/`refactor:`/`test:`),分支 `feat/3d-radar-volume-ingest`(已存在)。 - 失败排查:桌面日志 `%LOCALAPPDATA%/Geomative/Geopro3/logs/geopro_*.log`。 --- ### Task 1: 抽出共享建体 helper `assembleRadarVolume`(消 HIGH-1 DRY) 把 `Gpr3dvVolumeBridge.cpp:132-197` 的"扫值域→Quant→通道插值→逐体素填→spacing"抽成格式无关的共享函数,Impulse 路改为调用它,行为不变(现有 gtest 兜底)。 **Files:** - Create: `src/io/gpr/RadarVolumeAssembler.hpp` - Create: `src/io/gpr/RadarVolumeAssembler.cpp` - Modify: `src/io/gpr/Gpr3dvVolumeBridge.cpp:99-197`(dims 之后改为组 `RadarCubeDesc` + sampler 调 helper) - Modify: `src/io/CMakeLists.txt`(加 `RadarVolumeAssembler.cpp`) - Test: `tests/io/gpr/test_radar_volume_assembler.cpp` - Modify: `tests/CMakeLists.txt`(加新测试源) **Interfaces:** - Produces: ```cpp namespace geopro::io::gpr { struct RadarCubeDesc { int channels = 0; // M int traces = 0; // K(源道数, 下采样前) int samples = 0; // N std::vector chXOffsets; // 每通道横向偏移(米); size==channels 时启用通道插值 double dxBase = 1.0; // 道距(米) → dx = dxBase*stride double dyWhenNotInterpolated = 1.0;// 未插值时 Y 间距(米) double dz = 1.0; // 深度采样间距(米) }; using CubeSampler = std::function; // 扫值域→Quant→通道插值(planChannelInterpolation)→逐体素填→spacing。轴 X=道/Y=通道/Z=样本。 // coarse≥1 沿道下采样(nxOut=ceil(K/coarse), dx×coarse);targetDy>0 启用通道插值。GPR 稠密不置 kBlank。 geopro::core::BuiltI16 assembleRadarVolume(const RadarCubeDesc& desc, const CubeSampler& sample, int coarse, double targetDy); } ``` - Consumes: `geopro::core::{BuiltI16,Quant,ScalarVolumeI16}`、`planChannelInterpolation`。 - [ ] **Step 1: 写失败测试** `tests/io/gpr/test_radar_volume_assembler.cpp`: ```cpp #include #include "core/algo/GprVolumeBuilder.hpp" #include "io/gpr/RadarVolumeAssembler.hpp" using geopro::io::gpr::RadarCubeDesc; using geopro::io::gpr::assembleRadarVolume; // 2 道 × 3 通道 × 4 采样,值 = 100*c + 10*t + s。不插值(chXOffsets 空)、coarse=1。 TEST(RadarVolumeAssembler, AxisMapAndQuantRoundTrip) { RadarCubeDesc d; d.channels = 3; d.traces = 2; d.samples = 4; d.dxBase = 0.1; d.dyWhenNotInterpolated = 0.5; d.dz = 0.05; auto sampler = [](int c, int t, int s) { return 100.0 * c + 10.0 * t + s; }; const geopro::core::BuiltI16 b = assembleRadarVolume(d, sampler, /*coarse=*/1, /*targetDy=*/0.0); EXPECT_EQ(b.vol.nx(), 2); // 道 EXPECT_EQ(b.vol.ny(), 3); // 通道(未插值=原通道数) EXPECT_EQ(b.vol.nz(), 4); // 采样 EXPECT_DOUBLE_EQ(b.spacing[0], 0.1); EXPECT_DOUBLE_EQ(b.spacing[1], 0.5); EXPECT_DOUBLE_EQ(b.spacing[2], 0.05); EXPECT_NEAR(b.vminPhys, 0.0, 1e-9); // c0,t0,s0 EXPECT_NEAR(b.vmaxPhys, 213.0, 1e-9); // c2,t1,s3 = 200+10+3 // 反量化对位:体素(道 t=1, 通道 c=2, 采样 s=3) 应≈213(量化误差内)。 const double recon = b.quant.toPhys(b.vol.at(1, 2, 3)); EXPECT_NEAR(recon, 213.0, b.quant.scale); } // coarse=2:4 道 → nxOut=2,dx×2。 TEST(RadarVolumeAssembler, CoarseDownsamplesTracesAndScalesDx) { RadarCubeDesc d; d.channels = 1; d.traces = 4; d.samples = 2; d.dxBase = 0.1; auto sampler = [](int, int t, int s) { return 10.0 * t + s; }; const geopro::core::BuiltI16 b = assembleRadarVolume(d, sampler, /*coarse=*/2, 0.0); EXPECT_EQ(b.vol.nx(), 2); EXPECT_DOUBLE_EQ(b.spacing[0], 0.2); EXPECT_NEAR(b.quant.toPhys(b.vol.at(1, 0, 0)), 20.0, b.quant.scale); // 输出道1 = 源道2 } ``` - [ ] **Step 2: 跑测试确认失败** Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R RadarVolumeAssembler -V` Expected: 编译失败(`RadarVolumeAssembler.hpp` 不存在)。 - [ ] **Step 3: 写 helper 实现** `src/io/gpr/RadarVolumeAssembler.hpp`: ```cpp #ifndef GEOPRO_IO_GPR_RADARVOLUMEASSEMBLER_HPP #define GEOPRO_IO_GPR_RADARVOLUMEASSEMBLER_HPP #include #include #include "core/algo/GprVolumeBuilder.hpp" namespace geopro::io::gpr { struct RadarCubeDesc { int channels = 0; int traces = 0; int samples = 0; std::vector chXOffsets; double dxBase = 1.0; double dyWhenNotInterpolated = 1.0; double dz = 1.0; }; using CubeSampler = std::function; geopro::core::BuiltI16 assembleRadarVolume(const RadarCubeDesc& desc, const CubeSampler& sample, int coarse, double targetDy); } // namespace geopro::io::gpr #endif ``` `src/io/gpr/RadarVolumeAssembler.cpp`(搬 `Gpr3dvVolumeBridge.cpp:115-195` 的逻辑,数据访问换成 `sample(c,t,s)`): ```cpp #include "io/gpr/RadarVolumeAssembler.hpp" #include #include #include #include "core/model/ScalarVolumeI16.hpp" #include "io/gpr/GprGeometry.hpp" // planChannelInterpolation, ChannelInterpRow namespace geopro::io::gpr { geopro::core::BuiltI16 assembleRadarVolume(const RadarCubeDesc& d, const CubeSampler& sample, int coarse, double targetDy) { if (d.channels <= 0 || d.traces <= 0 || d.samples <= 0) throw std::runtime_error("assembleRadarVolume: 维度为空"); const int stride = coarse > 1 ? coarse : 1; const int nxOut = (d.traces + stride - 1) / stride; const int nz = d.samples; // 通道插值方案(读 chXOffsets 规则化到 targetDy);退路=逐通道 identity。 std::vector rows; bool interpolated = false; if (static_cast(d.chXOffsets.size()) == d.channels && targetDy > 0.0) { rows = planChannelInterpolation(d.chXOffsets, targetDy); interpolated = (static_cast(rows.size()) != d.channels); } if (rows.empty()) for (int c = 0; c < d.channels; ++c) rows.push_back({c, c, 0.0}); const int ny = static_cast(rows.size()); // 扫值域 → Quant(中点 offset, 64000 裕度)。 double vmin = std::numeric_limits::infinity(); double vmax = -std::numeric_limits::infinity(); for (int c = 0; c < d.channels; ++c) for (int t = 0; t < d.traces; ++t) for (int s = 0; s < d.samples; ++s) { const double v = sample(c, t, s); if (v < vmin) vmin = v; if (v > vmax) vmax = v; } if (!(vmin <= vmax)) { vmin = 0.0; vmax = 0.0; } geopro::core::Quant quant; quant.scale = (vmax > vmin) ? (vmax - vmin) / 64000.0 : 1.0; quant.offset = 0.5 * (vmin + vmax); // 逐(输出行 j, 输出道 to, 采样 s)填,散射写入(绝不 memcpy)。 geopro::core::BuiltI16 built; built.vol = geopro::core::ScalarVolumeI16(nxOut, ny, nz); for (int j = 0; j < ny; ++j) { const int a = rows[j].a, b = rows[j].b; const double wb = rows[j].wb, wa = 1.0 - wb; for (int to = 0; to < nxOut; ++to) { const int t = to * stride; for (int s = 0; s < nz; ++s) { const double va = sample(a, t, s); const double vb = (b == a) ? va : sample(b, t, s); built.vol.at(to, j, s) = quant.toQ(wa * va + wb * vb); } } } const double dy = interpolated ? targetDy : d.dyWhenNotInterpolated; built.quant = quant; built.origin = {0.0, 0.0, 0.0}; built.spacing = {d.dxBase * stride, dy, d.dz}; built.vminPhys = vmin; built.vmaxPhys = vmax; return built; } } // namespace geopro::io::gpr ``` 在 `src/io/CMakeLists.txt` 现有 gpr 源清单里加 `gpr/RadarVolumeAssembler.cpp`(紧挨 `gpr/Gpr3dvVolumeBridge.cpp`)。在 `tests/CMakeLists.txt` 现有 `geopro_tests` 源清单里加 `io/gpr/test_radar_volume_assembler.cpp`(紧挨 `io/gpr/test_gpr3dv_volume_bridge.cpp`)。 - [ ] **Step 4: 跑测试确认通过** Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R RadarVolumeAssembler -V` Expected: 2 个用例 PASS。 - [ ] **Step 5: Impulse 路改调 helper(保持现有 gtest 绿)** 把 `Gpr3dvVolumeBridge.cpp` 内联的"扫值域+Quant+填体+spacing"(约 :132-195)替换为:组 `RadarCubeDesc desc{channels, traces, samples, latOff, h.distanceInc>1e-9?h.distanceInc:1.0, channelSpacingY(h,channels), depthSpacingZ(h)}` + `CubeSampler sample = [&processed](int c,int t,int s){ const auto& ch=processed.volumeData[c]; if(t>=(int)ch.size()) return 0.0; const auto& tr=ch[t]; return s<(int)tr.size()? (double)tr[s] : 0.0; }`,调 `const auto built = assembleRadarVolume(desc, sample, coarse, targetDy);`,再回填 `metricsOut`(nx/ny/nz 取 `built.vol.*()`,dx/dy/dz 取 `built.spacing[*]`,vmin/vmax 取 `built.vminPhys/vmaxPhys`,before/after/load/pipeline 仍用原 Impulse 统计)。保留 `latOff`/`channelSpacingY`/`depthSpacingZ`/`meanAbsAmplitude` 不动。`#include "io/gpr/RadarVolumeAssembler.hpp"`。 - [ ] **Step 6: 跑现有 Impulse 测试确认无回归** Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R "Gpr3dv|RadarVolumeAssembler" -V` Expected: `test_gpr3dv_volume_bridge` 全部仍 PASS + 新测试 PASS(行为不变)。 - [ ] **Step 7: Commit** ```bash git add src/io/gpr/RadarVolumeAssembler.hpp src/io/gpr/RadarVolumeAssembler.cpp \ src/io/gpr/Gpr3dvVolumeBridge.cpp src/io/CMakeLists.txt \ tests/io/gpr/test_radar_volume_assembler.cpp tests/CMakeLists.txt git commit -m "refactor(gpr): 抽出共享 assembleRadarVolume,Impulse 路改调(消填体 DRY)" ``` --- ### Task 2: 规范化 `.head` 解析器 **Files:** - Create: `src/io/gpr/NormalizedRadarReader.hpp` - Create: `src/io/gpr/NormalizedRadarReader.cpp` - Modify: `src/io/CMakeLists.txt`(加 `NormalizedRadarReader.cpp`) - Test: `tests/io/gpr/test_normalized_radar_reader.cpp` - Modify: `tests/CMakeLists.txt`(加新测试源) **Interfaces:** - Produces: ```cpp namespace geopro::io::gpr { struct RadarHeader { int samples = 0; // N (SAMPLES) int channels = 0; // M (NUMBER_OF_CH) long lastTrace = 0; // 总扫描数=K*M (LAST_TRACE) int traces = 0; // K = lastTrace/channels int bits = 16; // 8/16/32 (BITS) int endianType = 1; // 1 小端/2 大端 (ENDIAN_TYPE) double distanceInterval = 1.0; // 道距 m (DISTANCE_INTERVAL) double timeWindowNs = 0.0; // 时窗 ns (TIMEWINDOW) double dielectric = 0.0; // 介电常数 (DIELECTRIC, 0=未知) std::vector chXOffsets; // 通道横向偏移 m (CH_X_OFFSETS) }; // 解析 KEY:VALUE 文本头。缺 SAMPLES/NUMBER_OF_CH/LAST_TRACE 任一 → std::runtime_error。 // traces = lastTrace/channels(不整除抛错)。 RadarHeader parseRadarHead(const std::string& headText); // 由 dielectric 求波速(m/ns): >0 时 0.2998/sqrt(eps),否则 0.1(默认)。 double waveVelocityMperNs(const RadarHeader& h); // 深度采样间距(米): timeWindowNs/(samples-1) × 波速/2。samples<=1 → 0。 double depthSpacingZ(const RadarHeader& h); } ``` - [ ] **Step 1: 写失败测试** `tests/io/gpr/test_normalized_radar_reader.cpp`: ```cpp #include #include "io/gpr/NormalizedRadarReader.hpp" using namespace geopro::io::gpr; TEST(NormalizedRadarHead, ParsesCoreFieldsAndDerivesTraces) { const std::string head = "SAMPLES:516\nNUMBER_OF_CH:16\nLAST_TRACE:60448\nBITS:16\nENDIAN_TYPE:1\n" "DISTANCE_INTERVAL:0.099194\nTIMEWINDOW:96.419553\nDIELECTRIC:\n" "CH_X_OFFSETS:0.080 0.160 0.240 0.320 0.400 0.480 0.560 0.640 0.720 0.800 " "0.880 0.960 1.040 1.120 1.200 1.280\n"; const RadarHeader h = parseRadarHead(head); EXPECT_EQ(h.samples, 516); EXPECT_EQ(h.channels, 16); EXPECT_EQ(h.lastTrace, 60448); EXPECT_EQ(h.traces, 3778); // 60448/16 EXPECT_EQ(h.bits, 16); EXPECT_EQ(h.endianType, 1); EXPECT_DOUBLE_EQ(h.distanceInterval, 0.099194); ASSERT_EQ(h.chXOffsets.size(), 16u); EXPECT_DOUBLE_EQ(h.chXOffsets.front(), 0.080); EXPECT_DOUBLE_EQ(h.chXOffsets.back(), 1.280); } TEST(NormalizedRadarHead, MissingRequiredFieldThrows) { EXPECT_THROW(parseRadarHead("SAMPLES:516\nNUMBER_OF_CH:16\n"), std::runtime_error); } TEST(NormalizedRadarHead, DepthSpacingUsesDefaultVelocityWhenNoDielectric) { const std::string head = "SAMPLES:516\nNUMBER_OF_CH:16\nLAST_TRACE:32\n" "TIMEWINDOW:96.419553\nDIELECTRIC:\n"; const RadarHeader h = parseRadarHead(head); EXPECT_NEAR(waveVelocityMperNs(h), 0.1, 1e-9); // 无介电 → 默认 0.1 const double dz = depthSpacingZ(h); EXPECT_NEAR(dz, (96.419553 / 515.0) * 0.1 / 2.0, 1e-9); } ``` - [ ] **Step 2: 跑测试确认失败** Run: `cmake --build build/release --target geopro_tests 2>&1 | head` Expected: 编译失败(头不存在)。 - [ ] **Step 3: 写实现** `src/io/gpr/NormalizedRadarReader.hpp`:放上面 Interfaces 的结构体与三个函数声明,加 `#include `/``,`#ifndef` 守卫。 `src/io/gpr/NormalizedRadarReader.cpp`(解析 KEY:VALUE): ```cpp #include "io/gpr/NormalizedRadarReader.hpp" #include #include #include #include namespace geopro::io::gpr { namespace { std::map parseKv(const std::string& text) { std::map kv; std::istringstream in(text); std::string line; while (std::getline(in, line)) { const auto pos = line.find(':'); if (pos == std::string::npos) continue; std::string k = line.substr(0, pos), v = line.substr(pos + 1); auto trim = [](std::string& s) { const auto a = s.find_first_not_of(" \t\r\n"); const auto b = s.find_last_not_of(" \t\r\n"); s = (a == std::string::npos) ? "" : s.substr(a, b - a + 1); }; trim(k); trim(v); kv[k] = v; } return kv; } int reqInt(const std::map& kv, const char* k) { auto it = kv.find(k); if (it == kv.end() || it->second.empty()) throw std::runtime_error(std::string("规范化 .head 缺字段: ") + k); return std::stoi(it->second); } double optD(const std::map& kv, const char* k, double dv) { auto it = kv.find(k); return (it == kv.end() || it->second.empty()) ? dv : std::stod(it->second); } } // namespace RadarHeader parseRadarHead(const std::string& headText) { const auto kv = parseKv(headText); RadarHeader h; h.samples = reqInt(kv, "SAMPLES"); h.channels = reqInt(kv, "NUMBER_OF_CH"); h.lastTrace = reqInt(kv, "LAST_TRACE"); if (h.channels <= 0 || h.lastTrace % h.channels != 0) throw std::runtime_error("LAST_TRACE 不能被 NUMBER_OF_CH 整除"); h.traces = static_cast(h.lastTrace / h.channels); h.bits = static_cast(optD(kv, "BITS", 16)); h.endianType = static_cast(optD(kv, "ENDIAN_TYPE", 1)); h.distanceInterval = optD(kv, "DISTANCE_INTERVAL", 1.0); h.timeWindowNs = optD(kv, "TIMEWINDOW", 0.0); h.dielectric = optD(kv, "DIELECTRIC", 0.0); auto it = kv.find("CH_X_OFFSETS"); if (it != kv.end() && !it->second.empty()) { std::istringstream os(it->second); double v; while (os >> v) h.chXOffsets.push_back(v); } return h; } double waveVelocityMperNs(const RadarHeader& h) { return h.dielectric > 0.0 ? 0.2998 / std::sqrt(h.dielectric) : 0.1; } double depthSpacingZ(const RadarHeader& h) { if (h.samples <= 1 || h.timeWindowNs <= 0.0) return 0.0; return (h.timeWindowNs / (h.samples - 1)) * waveVelocityMperNs(h) / 2.0; } } // namespace geopro::io::gpr ``` 加 `src/io/CMakeLists.txt`:`gpr/NormalizedRadarReader.cpp`。加 `tests/CMakeLists.txt`:`io/gpr/test_normalized_radar_reader.cpp`。 - [ ] **Step 4: 跑测试确认通过** Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R NormalizedRadarHead -V` Expected: 3 个用例 PASS。 - [ ] **Step 5: Commit** ```bash git add src/io/gpr/NormalizedRadarReader.hpp src/io/gpr/NormalizedRadarReader.cpp \ src/io/CMakeLists.txt tests/io/gpr/test_normalized_radar_reader.cpp tests/CMakeLists.txt git commit -m "feat(radar): 规范化 .head 解析(维度/字节序/通道偏移/深度间距)" ``` --- ### Task 3: 规范化 `.data` 立方体读取(position-major,16-bit) **Files:** - Modify: `src/io/gpr/NormalizedRadarReader.hpp`(加 `readRadarDataCube` 声明) - Modify: `src/io/gpr/NormalizedRadarReader.cpp`(加实现) - Test: `tests/io/gpr/test_normalized_radar_reader.cpp`(加用例) **Interfaces:** - Produces: ```cpp // 读规范化 .data → 扁平 int16 立方体,position-major 索引 ((t*M + c)*N + s)。 // 按 header.bits/endianType 解码;P0 仅 16-bit(32-bit 抛 not implemented)。 // 文件大小须 == lastTrace*samples*(bits/8),否则抛 std::runtime_error。 std::vector readRadarDataCube(const std::string& dataPath, const RadarHeader& h); ``` - Consumes: `RadarHeader`(Task 2)。 - [ ] **Step 1: 写失败测试** 加到 `test_normalized_radar_reader.cpp`: ```cpp #include #include #include namespace fs = std::filesystem; TEST(NormalizedRadarData, ReadsPositionMajorCubeLittleEndian) { // K=2 道, M=3 通道, N=2 采样; 值 v(t,c,s)=int16(100*t+10*c+s)。position-major 写。 fs::path dir = fs::temp_directory_path() / "radar_data_test"; fs::create_directories(dir); const fs::path dp = dir / "L.data"; { std::ofstream f(dp, std::ios::binary); for (int t = 0; t < 2; ++t) for (int c = 0; c < 3; ++c) for (int s = 0; s < 2; ++s) { std::int16_t v = static_cast(100 * t + 10 * c + s); f.write(reinterpret_cast(&v), sizeof(v)); // 小端(x86) } } geopro::io::gpr::RadarHeader h; h.samples = 2; h.channels = 3; h.lastTrace = 6; h.traces = 2; h.bits = 16; h.endianType = 1; const auto cube = geopro::io::gpr::readRadarDataCube(dp.string(), h); ASSERT_EQ(cube.size(), 2u * 3u * 2u); auto at = [&](int t, int c, int s) { return cube[(size_t(t) * 3 + c) * 2 + s]; }; EXPECT_EQ(at(0, 0, 0), 0); EXPECT_EQ(at(1, 2, 1), 121); // 100+20+1 EXPECT_EQ(at(0, 1, 0), 10); } TEST(NormalizedRadarData, WrongFileSizeThrows) { fs::path dir = fs::temp_directory_path() / "radar_data_test"; fs::create_directories(dir); const fs::path dp = dir / "bad.data"; { std::ofstream f(dp, std::ios::binary); std::int16_t v = 0; f.write((char*)&v, 2); } geopro::io::gpr::RadarHeader h; h.samples = 2; h.channels = 3; h.lastTrace = 6; h.traces = 2; h.bits = 16; EXPECT_THROW(geopro::io::gpr::readRadarDataCube(dp.string(), h), std::runtime_error); } ``` - [ ] **Step 2: 跑测试确认失败** Run: `cmake --build build/release --target geopro_tests 2>&1 | head` Expected: `readRadarDataCube` 未声明,编译失败。 - [ ] **Step 3: 写实现** `NormalizedRadarReader.hpp` 加声明(+`#include `)。`.cpp` 加: ```cpp #include #include // ... std::vector readRadarDataCube(const std::string& dataPath, const RadarHeader& h) { if (h.bits != 16) throw std::runtime_error("readRadarDataCube: 暂仅支持 16-bit(BITS=" + std::to_string(h.bits) + " 待实现)"); const std::size_t n = static_cast(h.lastTrace) * h.samples; const std::uintmax_t expect = static_cast(n) * 2; std::error_code ec; const auto fsize = std::filesystem::file_size(dataPath, ec); if (ec || fsize != expect) throw std::runtime_error("规范化 .data 大小不符: " + dataPath); std::vector cube(n); std::ifstream f(dataPath, std::ios::binary); if (!f) throw std::runtime_error("打开 .data 失败: " + dataPath); f.read(reinterpret_cast(cube.data()), static_cast(expect)); if (!f) throw std::runtime_error("读 .data 失败: " + dataPath); if (h.endianType == 2) // 大端 → 主机小端(x86),逐元素交换字节 for (auto& v : cube) { const std::uint16_t u = static_cast(v); v = static_cast((u >> 8) | (u << 8)); } return cube; } ``` - [ ] **Step 4: 跑测试确认通过** Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R NormalizedRadarData -V` Expected: 2 个用例 PASS。 - [ ] **Step 5: Commit** ```bash git add src/io/gpr/NormalizedRadarReader.hpp src/io/gpr/NormalizedRadarReader.cpp \ tests/io/gpr/test_normalized_radar_reader.cpp git commit -m "feat(radar): 规范化 .data 立方体读取(position-major/16bit/字节序)" ``` --- ### Task 4: 规范化 `.cor` 轨迹解析 本期只解析(单线渲染不依赖),为 P1 多线配准预留。 **Files:** - Modify: `src/io/gpr/NormalizedRadarReader.hpp`(加 `TracePos` + `parseRadarCor`) - Modify: `src/io/gpr/NormalizedRadarReader.cpp` - Test: `tests/io/gpr/test_normalized_radar_reader.cpp`(加用例) **Interfaces:** - Produces: ```cpp struct TracePos { int index = 0; double lat = 0, lon = 0, elev = 0; int solution = 0; }; // 解析 .cor:跳过 "VERSION:" 行,每行 [序号 纬度 N/S 经度 E/W 高程 M 解状态] (制表/空格分隔)。 std::vector parseRadarCor(const std::string& corText); ``` - [ ] **Step 1: 写失败测试** ```cpp TEST(NormalizedRadarCor, ParsesRowsSkippingVersion) { const std::string cor = "VERSION:1\n" "1\t317.179340\tN\t472.759046\tE\t49.980000\tM\t4\n" "12\t317.201303\tN\t472.700649\tE\t51.040000\tM\t4\n"; const auto pts = geopro::io::gpr::parseRadarCor(cor); ASSERT_EQ(pts.size(), 2u); EXPECT_EQ(pts[0].index, 1); EXPECT_DOUBLE_EQ(pts[0].lat, 317.179340); EXPECT_DOUBLE_EQ(pts[0].lon, 472.759046); EXPECT_DOUBLE_EQ(pts[1].elev, 51.040000); EXPECT_EQ(pts[1].solution, 4); } ``` - [ ] **Step 2: 跑测试确认失败** Run: `cmake --build build/release --target geopro_tests 2>&1 | head` Expected: `parseRadarCor` 未声明。 - [ ] **Step 3: 写实现** `.hpp` 加 `TracePos`/`parseRadarCor`。`.cpp` 加: ```cpp std::vector parseRadarCor(const std::string& corText) { std::vector out; std::istringstream in(corText); std::string line; while (std::getline(in, line)) { if (line.empty() || line.rfind("VERSION", 0) == 0) continue; std::istringstream ls(line); TracePos p; std::string ns, ew, m; if (ls >> p.index >> p.lat >> ns >> p.lon >> ew >> p.elev >> m >> p.solution) out.push_back(p); } return out; } ``` - [ ] **Step 4: 跑测试确认通过** Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R NormalizedRadarCor -V` Expected: PASS。 - [ ] **Step 5: Commit** ```bash git add src/io/gpr/NormalizedRadarReader.hpp src/io/gpr/NormalizedRadarReader.cpp \ tests/io/gpr/test_normalized_radar_reader.cpp git commit -m "feat(radar): 规范化 .cor 轨迹解析(P1 配准预留)" ``` --- ### Task 5: 规范化体 bridge `buildLineVolumeFromNormalized` 组合 reader + `assembleRadarVolume`,产出 `BuiltI16`。 **Files:** - Create: `src/io/gpr/NormalizedRadarVolumeBridge.hpp` - Create: `src/io/gpr/NormalizedRadarVolumeBridge.cpp` - Modify: `src/io/CMakeLists.txt` - Test: `tests/io/gpr/test_normalized_radar_bridge.cpp` - Modify: `tests/CMakeLists.txt` **Interfaces:** - Produces: ```cpp namespace geopro::io::gpr { // 读 {lineDir}/{prefix}.head + .data → assembleRadarVolume → BuiltI16(轴 X=道/Y=通道/Z=采样)。 // coarse 沿道下采样;targetDy 线内通道插值(读 .head CH_X_OFFSETS)。失败抛 std::runtime_error。 geopro::core::BuiltI16 buildLineVolumeFromNormalized(const std::string& lineDir, const std::string& linePrefix, int coarse = 4, double targetDy = 0.025); } ``` - Consumes: `parseRadarHead`/`readRadarDataCube`/`depthSpacingZ`(Task 2-3)、`assembleRadarVolume`(Task 1)。 - [ ] **Step 1: 写失败测试** `tests/io/gpr/test_normalized_radar_bridge.cpp`: ```cpp #include #include #include #include #include "core/algo/GprVolumeBuilder.hpp" #include "io/gpr/NormalizedRadarVolumeBridge.hpp" namespace fs = std::filesystem; TEST(NormalizedRadarBridge, BuildsVolumeWithExpectedAxes) { // K=4 道, M=2 通道, N=3 采样, 无通道偏移(不插值), coarse=1。 fs::path dir = fs::temp_directory_path() / "radar_bridge_test"; 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); } } const auto b = geopro::io::gpr::buildLineVolumeFromNormalized( (dir).string(), "L", /*coarse=*/1, /*targetDy=*/0.0); // targetDy=0 不插值 EXPECT_EQ(b.vol.nx(), 4); // 道 EXPECT_EQ(b.vol.ny(), 2); // 通道 EXPECT_EQ(b.vol.nz(), 3); // 采样 EXPECT_DOUBLE_EQ(b.spacing[0], 0.1); // dx=DISTANCE_INTERVAL EXPECT_GT(b.spacing[2], 0.0); // dz 由 timewindow/dielectric 求得 >0 EXPECT_NEAR(b.quant.toPhys(b.vol.at(3, 1, 2)), 132.0, b.quant.scale); // t3c1s2=30+100+2 } ``` - [ ] **Step 2: 跑测试确认失败** Run: `cmake --build build/release --target geopro_tests 2>&1 | head` Expected: 头不存在,编译失败。 - [ ] **Step 3: 写实现** `src/io/gpr/NormalizedRadarVolumeBridge.hpp`:守卫 + 声明。`.cpp`: ```cpp #include "io/gpr/NormalizedRadarVolumeBridge.hpp" #include "io/gpr/NormalizedRadarReader.hpp" #include "io/gpr/RadarVolumeAssembler.hpp" namespace geopro::io::gpr { geopro::core::BuiltI16 buildLineVolumeFromNormalized(const std::string& lineDir, const std::string& linePrefix, int coarse, double targetDy) { const std::string head = lineDir + "/" + linePrefix + ".head"; const std::string data = lineDir + "/" + linePrefix + ".data"; std::string headText; { std::ifstream f(head); if (!f) throw std::runtime_error("打开 .head 失败: " + head); std::stringstream ss; ss << f.rdbuf(); headText = ss.str(); } const RadarHeader h = parseRadarHead(headText); const std::vector cube = readRadarDataCube(data, h); const int M = h.channels, N = h.samples; RadarCubeDesc d; d.channels = M; d.traces = h.traces; d.samples = N; d.chXOffsets = h.chXOffsets; d.dxBase = h.distanceInterval > 1e-9 ? h.distanceInterval : 1.0; d.dyWhenNotInterpolated = (h.chXOffsets.size() >= 2) ? (h.chXOffsets.back() - h.chXOffsets.front()) / (M - 1) : 1.0; d.dz = depthSpacingZ(h) > 1e-12 ? depthSpacingZ(h) : 1.0; CubeSampler sample = [&cube, M, N](int c, int t, int s) { return static_cast(cube[(static_cast(t) * M + c) * N + s]); }; return assembleRadarVolume(d, sample, coarse, targetDy); } } // namespace geopro::io::gpr ``` (`.cpp` 顶部 `#include `/``/``。)加 `src/io/CMakeLists.txt`:`gpr/NormalizedRadarVolumeBridge.cpp`;加 `tests/CMakeLists.txt`:`io/gpr/test_normalized_radar_bridge.cpp`。 - [ ] **Step 4: 跑测试确认通过** Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R NormalizedRadarBridge -V` Expected: PASS。 - [ ] **Step 5: Commit** ```bash git add src/io/gpr/NormalizedRadarVolumeBridge.hpp src/io/gpr/NormalizedRadarVolumeBridge.cpp \ src/io/CMakeLists.txt tests/io/gpr/test_normalized_radar_bridge.cpp tests/CMakeLists.txt git commit -m "feat(radar): 规范化体 bridge buildLineVolumeFromNormalized" ``` --- ### Task 6: 数据层 `createRadarVolumeGrid` **Files:** - Modify: `src/data/GprVolumeRepository.hpp`(加声明) - Modify: `src/data/GprVolumeRepository.cpp`(加实现) - Test: `tests/data/test_gpr_volume_repository.cpp`(加用例) **Interfaces:** - Produces: ```cpp // 规范化测线 → BuiltI16(buildLineVolumeFromNormalized) → 反量化 VolumeGrid。 VolumeGrid createRadarVolumeGrid(const std::string& lineDir, const std::string& linePrefix, int coarse = 4, double targetDy = 0.025); ``` - Consumes: `buildLineVolumeFromNormalized`(Task 5)、`builtI16ToVolumeGrid`(现成)。 - [ ] **Step 1: 写失败测试** 加到 `tests/data/test_gpr_volume_repository.cpp`(仿现有 `createGprVolumeGrid` 测试,用 Task 5 同款合成 .head/.data): ```cpp TEST(GprVolumeRepository, CreateRadarVolumeGridFromNormalized) { fs::path dir = fs::temp_directory_path() / "radar_repo_test"; 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); } } const auto grid = geopro::data::createRadarVolumeGrid(dir.string(), "L", 1, 0.0); EXPECT_EQ(grid.vol.nx(), 4); EXPECT_EQ(grid.vol.ny(), 2); EXPECT_EQ(grid.vol.nz(), 3); EXPECT_GT(grid.vmax, grid.vmin); } ``` - [ ] **Step 2: 跑测试确认失败** Run: `cmake --build build/release --target geopro_tests 2>&1 | head` Expected: `createRadarVolumeGrid` 未声明。 - [ ] **Step 3: 写实现** `GprVolumeRepository.hpp` 在 `createGprVolumeGrid` 声明后加 `createRadarVolumeGrid`(注释说明走规范化链)。`.cpp` 加: ```cpp #include "io/gpr/NormalizedRadarVolumeBridge.hpp" // ... VolumeGrid createRadarVolumeGrid(const std::string& lineDir, const std::string& linePrefix, int coarse, double targetDy) { const geopro::core::BuiltI16 built = geopro::io::gpr::buildLineVolumeFromNormalized(lineDir, linePrefix, coarse, targetDy); return builtI16ToVolumeGrid(built); } ``` - [ ] **Step 4: 跑测试确认通过** Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R "GprVolumeRepository" -V` Expected: 新用例 + 现有用例全 PASS。 - [ ] **Step 5: Commit** ```bash git add src/data/GprVolumeRepository.hpp src/data/GprVolumeRepository.cpp \ tests/data/test_gpr_volume_repository.cpp git commit -m "feat(radar): 数据层 createRadarVolumeGrid(规范化→VolumeGrid)" ``` --- ### Task 7: DS 优先登记 `registerRadarDataset` + `loadVolume` 懒加载后台建体 把规范化测线登记成一条 **`dd_radar_3d`** DS(不是游离 `dd_voxel` vol-N),**只存元数据、不建体**;首次勾选时 `loadVolume` 在**后台线程**建体(仿 `finalizeVolume`,避免 UI 冻结 + spinner 卡死)。运行期路由只认 `volumes_` 成员(已验:`isVolumeDataset`=`volumes_.count`、`loadVolume` 按 dsId 查、`addDatasetAsync` 按 `isVolumeDataset` 分流),故 `isVolumeDataset/addDatasetAsync` 零改动。 **Files:** - Modify: `src/data/api/Api3dRepository.hpp`(`StoredVolume` 加字段 + 声明 `registerRadarDataset`) - Modify: `src/data/api/Api3dRepository.cpp`(`registerRadarDataset` + `loadVolume` radar 分支 + `volumeRows()` 输出 `sv.ddCode`) - Test: `tests/data/test_3d_repo.cpp`(加用例:登记为 `dd_radar_3d` + `loadVolume` 同步退化路径产有效体) **Interfaces:** - Produces: ```cpp // 登记一条 dd_radar_3d 体 DS:只存元数据(lineDir/prefix/coarse),不建体。id="radar-"+(++counter)。 std::string registerRadarDataset(const std::string& lineDir, const std::string& linePrefix, const std::string& name, const std::string& structParentId, int coarse = 4); ``` - Consumes: `createRadarVolumeGrid`(Task 6)。 - `StoredVolume`(`Api3dRepository.hpp:139`) 新增字段:`std::string lineDir, linePrefix, ddCode = "dd_voxel", structParentId; int coarse = 4;`。 - [ ] **Step 1: 写失败测试** 加到 `tests/data/test_3d_repo.cpp`(用 Task 6 同款合成目录;构造 repo 照本文件现有用例的构造方式。gtest 无 `QCoreApplication` → `loadVolume` 走同步退化路径,回调即时触发): ```cpp TEST(Api3dRepository, RegisterRadarDatasetRoutesAsDdRadar3d) { // ...写合成 dir/L.head + L.data(同 Task 6)... geopro::data::Api3dRepository repo(/*照本文件现有构造*/); 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"); } TEST(Api3dRepository, LoadVolumeBuildsRadarLazily) { // ...写合成 dir/L.head + L.data(同上)... geopro::data::Api3dRepository repo(/*照本文件现有构造*/); const std::string id = repo.registerRadarDataset(dir.string(), "L", "测线L", "", 1); bool got = false; repo.loadVolume(id, [&](geopro::data::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; }); EXPECT_TRUE(got); // 无 QCoreApplication → 同步交付 } ``` - [ ] **Step 2: 跑测试确认失败** Run: `cmake --build build/release --target geopro_tests 2>&1 | head` Expected: `registerRadarDataset` 未声明。 - [ ] **Step 3: 写实现** `Api3dRepository.hpp`:`StoredVolume`(:139 附近) 加 `std::string lineDir, linePrefix, ddCode = "dd_voxel", structParentId; int coarse = 4;`;在 `createGprVolume` 声明后加 `registerRadarDataset` 声明。 `.cpp` 加 `registerRadarDataset`(只存元数据,**不建体**): ```cpp std::string Api3dRepository::registerRadarDataset(const std::string& lineDir, const std::string& linePrefix, const std::string& name, const std::string& structParentId, int coarse) { 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; } ``` `loadVolume`(`Api3dRepository.cpp:347` 的 `if (sv.cachedGrid)` 块之后、IDW 路径 `:351` 之前)插入 radar 分支(仿 `finalizeVolume:268-335` 后台线程范式,`#include "data/GprVolumeRepository.hpp"` 已在): ```cpp if (!sv.linePrefix.empty()) { // radar 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); auto err = std::get<1>(r); QMetaObject::invokeMethod(qApp, [deliver, g, err]() mutable { deliver(std::move(g), std::move(err)); }, Qt::QueuedConnection); }).detach(); return; } ``` 改 `volumeRows()`(`Api3dRepository.cpp:170` 附近):`r.ddCode = "dd_voxel";` → `r.ddCode = sv.ddCode;`,且 `r.structParentId = sv.request ? sv.request->structParentId : sv.structParentId;`(radar 无 request,用 sv.structParentId)。 - [ ] **Step 4: 跑测试确认通过** Run: `cmake --build build/release --target geopro_tests && ctest --test-dir build/release -R "Api3dRepository" -V` Expected: 2 个新用例 + 现有用例全 PASS(现有体 `sv.ddCode` 默认 `"dd_voxel"`、`linePrefix` 空不走 radar 分支,行为不变)。 - [ ] **Step 5: Commit** ```bash git add src/data/api/Api3dRepository.hpp src/data/api/Api3dRepository.cpp tests/data/test_3d_repo.cpp git commit -m "feat(radar): registerRadarDataset 登记 dd_radar_3d DS + loadVolume 懒加载后台建体" ``` --- ### Task 8: 本地导入入口 + `dd_radar_3d` 三处 ddCode gate 放开 导入登记 DS(瞬时,建体走 Task 7 的懒加载后台路径),并放开三处硬编码 `dd_voxel` 的 ddCode 判断让 radar 体可切片/调色阶/看详情。**评审 CRITICAL:列表右键「生成切片」是 UI 唯一切片入口,不放开 `CategorySection.cpp:397` 则 radar 体无法切片、P0 验收②不可达。** **Files:** - Modify: `src/app/main.cpp`(导入菜单动作 + `detailRequested` gate) - Modify: `src/app/panels/columns/CategorySection.cpp:397`(右键「生成切片」+「色阶」同一 if 块的 gate) **Interfaces:** - Consumes: `scene3dRepo->registerRadarDataset`(Task 7)、`refreshAnalysis`、`analysisTab`、`window`。 - [ ] **Step 1: 加导入菜单动作** 在 `main.cpp` 中 `refreshAnalysis` 已定义、`analysisTab` 已存在处(紧接 `generateVolumeRequested` 连接块之后,约 :982 后)插入。**登记瞬时、不建体**——勾选后由 Task 7 的 `loadVolume` 后台建体,列表项 spinner 显进度,失败由 controller `loadFailed`→toast 兜底(`main.cpp:995`): ```cpp // 本地导入三维雷达测线目录(后端未就绪的过渡入口):选目录+前缀 → registerRadarDataset(登记dd_radar_3d DS) → 勾选→后台建体渲染。 { QMenu* radarMenu = window.menuBar()->addMenu(QStringLiteral("三维雷达")); QAction* importAct = radarMenu->addAction(QStringLiteral("导入测线目录(本地)…")); QObject::connect(importAct, &QAction::triggered, &window, [&window, scene3dRepo, refreshAnalysis, analysisTab]() { const QString dir = QFileDialog::getExistingDirectory( &window, QStringLiteral("选择规范化三维雷达测线目录(含 *.head/*.data)")); if (dir.isEmpty()) return; bool ok = false; const QString prefix = QInputDialog::getText( &window, QStringLiteral("测线前缀"), QStringLiteral("输入测线前缀(如 南同大道_000):"), QLineEdit::Normal, QString(), &ok); if (!ok || prefix.isEmpty()) return; // structParentId 暂空(P0 挂三维体段根;P1 接 TM 归属)。 const std::string newId = scene3dRepo->registerRadarDataset( dir.toLocal8Bit().toStdString(), prefix.toLocal8Bit().toStdString(), prefix.toStdString(), /*structParentId=*/std::string(), /*coarse=*/4); { const QSignalBlocker block(analysisTab); refreshAnalysis(); } // DS 进三维体段(不触发渲染) const QString qid = QString::fromStdString(newId); analysisTab->setItemChecked(qid, true); // 勾选 → addDatasetAsync → loadVolume 后台建体渲染 analysisTab->setItemBusy(qid, true); // spinner; 渲染完成由 datasetRendered 撤(main.cpp:987) analysisTab->scrollItemToTop(qid); }); } ``` 确认 `main.cpp` 顶部已 include ``/``/``/``(缺则补)。 - [ ] **Step 2: 放开列表右键「生成切片/色阶」gate(评审 CRITICAL)** `src/app/panels/columns/CategorySection.cpp`:右键菜单的「生成切片」(:398-402) 与「色阶」(:403) **同在 `:397` 的 `if (ddCode == "dd_voxel")` 块内** → **只改 :397 这一处**判定即同时放开二者(无独立 :403 gate): - `:397` `if (ddCode == QStringLiteral("dd_voxel"))` → `if (ddCode == QStringLiteral("dd_voxel") || ddCode == QStringLiteral("dd_radar_3d"))` (这是 UI 唯一切片创建入口 `sliceRequested`→`main.cpp:1061`→`interactionMgr->addSlice`;不改则 radar 体右键无切片/色阶。`addSlice` 依赖的 volume image 由勾选→后台建体→`addVolume` 附着,与"先勾选渲染再切片"次序一致。) - [ ] **Step 3: dd_radar_3d 双击详情路由** `main.cpp` 的 `analysisTab` `detailRequested` 处理器(约 :1003-1013)目前只认 `dd_slice`/`dd_voxel`。把 `dd_voxel` 分支 `if (ddCode == QStringLiteral("dd_voxel"))` 改为 `if (ddCode == QStringLiteral("dd_voxel") || ddCode == QStringLiteral("dd_radar_3d"))`(`volumeInfo(dsId)` 对 radar DS 有效——它在 `volumes_` 里、loadVolume 后 `cachedGrid` 就绪 loaded=true)。 > 注(MEDIUM 显示瑕疵,本期可不处理):体属性对话框对 radar 会显示 `StoredVolume.params` 的默认 IDW 参数(cellXY/power…)与 pointCount=0,因 radar 不走 IDW。如需净化,给 `VolumePropertiesDialog` 加"radar 体隐藏插值参数段"分支,登记到 backlog。 - [ ] **Step 4: 构建 app** Run: `build.bat app`(先关掉在跑的 app,避免 LNK1104) Expected: 链接通过,生成 exe。 - [ ] **Step 5: 人工联调验收** 启动 app → 登录 → 菜单「三维雷达 → 导入测线目录(本地)」→ 选 `samples/radar/malamira_南同大道`、前缀 `南同大道_000` → 三维体段出现 `南同大道_000`(**ddCode=dd_radar_3d**)行、自动勾选、列表项转 spinner、后台建体完成后渲染出体(spinner 撤)。验:① 体可见;② **右键该行有「生成切片」**→切 updown/leftright/frontback 三向;③ 画点/线/面异常并保存、挂在体下;④ 双击该行弹体属性。日志看 `geopro_*.log` 无异常。 - [ ] **Step 6: Commit** ```bash git add src/app/main.cpp src/app/panels/columns/CategorySection.cpp git commit -m "feat(radar): 导入入口 + dd_radar_3d 切片/色阶/详情 gate 放开" ``` --- ### Task 9: 双数据集互证(明星路 in-app 首次稠密渲染) 按评审 HIGH-2:明星路 in-app 渲染从未跑过,需接其导入入口(复用现成 `createGprVolume`,仍走 `.iprb` 路径、体 ddCode 仍为 `dd_voxel` vol-N——Impulse 归 `dd_radar_3d` 待其规范化插件就绪),与 Mala 同入口形态,证下游对 14ch/16ch 几何无关。**只验"管路通+几何无关",不验成像一致**(明星路处理后 vs Mala 原始)。 **Files:** - Modify: `src/app/main.cpp`(Task 8 的 `radarMenu` 加第二个动作) - [ ] **Step 1: 加 Impulse 导入动作** 在 Task 8 的 `radarMenu` 内追加: ```cpp QAction* importImpulseAct = radarMenu->addAction(QStringLiteral("导入Impulse测线目录(.iprb)…")); QObject::connect(importImpulseAct, &QAction::triggered, &window, [&window, scene3dRepo, refreshAnalysis, analysisTab, vtkLoading]() { const QString dir = QFileDialog::getExistingDirectory( &window, QStringLiteral("选择 Impulse 测线目录(含 *.iprb/*.ord)")); if (dir.isEmpty()) return; bool ok = false; const QString prefix = QInputDialog::getText( &window, QStringLiteral("测线前缀"), QStringLiteral("输入测线前缀(如 明星路_010):"), QLineEdit::Normal, QString(), &ok); if (!ok || prefix.isEmpty()) return; vtkLoading->showOver(QStringLiteral("正在建Impulse体…")); QTimer::singleShot(0, &window, [=]() { std::string newId; try { newId = scene3dRepo->createGprVolume(dir.toLocal8Bit().toStdString(), prefix.toLocal8Bit().toStdString(), prefix.toStdString(), /*coarse=*/8); } catch (const std::exception& e) { vtkLoading->hide(); geopro::app::showToast(&window, QStringLiteral("建体失败:%1").arg(QString::fromLocal8Bit(e.what()))); return; } { const QSignalBlocker block(analysisTab); refreshAnalysis(); } vtkLoading->hide(); const QString qid = QString::fromStdString(newId); // createGprVolume 预填 cachedGrid → setItemChecked 内 loadVolume 同步渲染、datasetRendered 自动撤 busy; // 故此处【不要】再 setItemBusy(true)(否则 spinner 永久转圈 —— 评审 MEDIUM)。 analysisTab->setItemChecked(qid, true); analysisTab->scrollItemToTop(qid); }); }); ``` > 注:明星路体经 `createGprVolume` 仍是 eager 同步建体(~74MB×14ch,建体期 UI 短冻,已用 `vtkLoading` 蒙版遮挡)。这是现有 `.iprb` 路径、本期不改;Mala 走 Task 7 懒加载后台路径不受影响。 - [ ] **Step 2: 构建 + 双数据集联调** Run: `build.bat app`(先关 app) 验收:分别导入 `D:/Downloads/明星路`(前缀 `明星路_001`) 与 `samples/radar/malamira_南同大道`(前缀 `南同大道_000`),两体都能渲染 + 切片 + 异常。以 `gpr_poc` CLI 的明星路成像为离线对照基线,确认 in-app 明星路体几何合理(不要求与 Mala 成像可比)。 - [ ] **Step 3: Commit** ```bash git add src/app/main.cpp git commit -m "feat(radar): Impulse 测线本地导入入口(双数据集互证下游)" ``` --- ## 完成判据 - `geopro_tests` 全绿(含新 reader/assembler/bridge/repo 用例 + 现有 Gpr3dv/Api3d 无回归)。 - app 内导入 Mala 规范化测线 → 渲染 + 三向切片 + 三类异常 OK。 - app 内导入明星路 Impulse 测线 → 渲染 OK(双数据集互证)。 - 未触碰 controller/view 渲染内核、切片/异常工具、三级树(下游零改动达成)。