geopro/docs/superpowers/plans/2026-06-29-3d-radar-volume-...

1053 lines
49 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 三维雷达体接入规范化格式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消 DRYImpulse 与规范化两路同调。
**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-bit32-bit 抛 `not implemented`)。`ENDIAN_TYPE` 1=小端/2=大端。
- 通道插值复用纯函数 `geopro::io::gpr::planChannelInterpolation(offsets, targetDy)`(默认 `targetDy=0.025`),通道横向偏移取自 `.head``CH_X_OFFSETS`**绝不跨线**。
- 内存app 体是 `std::vector<double>`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<double> chXOffsets; // 每通道横向偏移(米); size==channels 时启用通道插值
double dxBase = 1.0; // 道距(米) → dx = dxBase*stride
double dyWhenNotInterpolated = 1.0;// 未插值时 Y 间距(米)
double dz = 1.0; // 深度采样间距(米)
};
using CubeSampler = std::function<double(int c, int t, int s)>;
// 扫值域→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 <gtest/gtest.h>
#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=24 道 → nxOut=2dx×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 <functional>
#include <vector>
#include "core/algo/GprVolumeBuilder.hpp"
namespace geopro::io::gpr {
struct RadarCubeDesc {
int channels = 0; int traces = 0; int samples = 0;
std::vector<double> chXOffsets;
double dxBase = 1.0; double dyWhenNotInterpolated = 1.0; double dz = 1.0;
};
using CubeSampler = std::function<double(int c, int t, int s)>;
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 <cmath>
#include <limits>
#include <stdexcept>
#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<ChannelInterpRow> rows;
bool interpolated = false;
if (static_cast<int>(d.chXOffsets.size()) == d.channels && targetDy > 0.0) {
rows = planChannelInterpolation(d.chXOffsets, targetDy);
interpolated = (static_cast<int>(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<int>(rows.size());
// 扫值域 → Quant(中点 offset, 64000 裕度)。
double vmin = std::numeric_limits<double>::infinity();
double vmax = -std::numeric_limits<double>::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): 抽出共享 assembleRadarVolumeImpulse 路改调(消填体 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<double> 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 <gtest/gtest.h>
#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 <string>`/`<vector>``#ifndef` 守卫。
`src/io/gpr/NormalizedRadarReader.cpp`(解析 KEY:VALUE
```cpp
#include "io/gpr/NormalizedRadarReader.hpp"
#include <cmath>
#include <map>
#include <sstream>
#include <stdexcept>
namespace geopro::io::gpr {
namespace {
std::map<std::string, std::string> parseKv(const std::string& text) {
std::map<std::string, std::string> 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<std::string, std::string>& 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<std::string, std::string>& 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<int>(h.lastTrace / h.channels);
h.bits = static_cast<int>(optD(kv, "BITS", 16));
h.endianType = static_cast<int>(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-major16-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<std::int16_t> readRadarDataCube(const std::string& dataPath,
const RadarHeader& h);
```
- Consumes: `RadarHeader`Task 2
- [ ] **Step 1: 写失败测试**
加到 `test_normalized_radar_reader.cpp`
```cpp
#include <cstdint>
#include <filesystem>
#include <fstream>
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<std::int16_t>(100 * t + 10 * c + s);
f.write(reinterpret_cast<const char*>(&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 <cstdint>`)。`.cpp` 加:
```cpp
#include <fstream>
#include <filesystem>
// ...
std::vector<std::int16_t> 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<std::size_t>(h.lastTrace) * h.samples;
const std::uintmax_t expect = static_cast<std::uintmax_t>(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<std::int16_t> cube(n);
std::ifstream f(dataPath, std::ios::binary);
if (!f) throw std::runtime_error("打开 .data 失败: " + dataPath);
f.read(reinterpret_cast<char*>(cube.data()), static_cast<std::streamsize>(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<std::uint16_t>(v);
v = static_cast<std::int16_t>((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<TracePos> 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<TracePos> parseRadarCor(const std::string& corText) {
std::vector<TracePos> 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 <gtest/gtest.h>
#include <cstdint>
#include <filesystem>
#include <fstream>
#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<std::int16_t>(t * 10 + c * 100 + s);
f.write(reinterpret_cast<const char*>(&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<std::int16_t> 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<double>(cube[(static_cast<std::size_t>(t) * M + c) * N + s]);
};
return assembleRadarVolume(d, sample, coarse, targetDy);
}
} // namespace geopro::io::gpr
```
`.cpp` 顶部 `#include <fstream>`/`<sstream>`/`<stdexcept>`。)加 `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<std::int16_t>(t * 10 + c * 100 + s);
f.write(reinterpret_cast<const char*>(&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<VolumeGrid> 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<VolumeGrid> g; std::string err;
try { g = std::make_shared<VolumeGrid>(
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 `<QMenu>`/`<QMenuBar>`/`<QFileDialog>`/`<QInputDialog>`(缺则补)。
- [ ] **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 渲染内核、切片/异常工具、三级树(下游零改动达成)。