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

49 KiB
Raw Blame History

三维雷达体接入规范化格式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"产出 BuiltI16builtI16ToVolumeGridcreateRadarVolumeGrid;上层走 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::Quantscale=(vmax-vmin)/64000offset=中点GPR 体稠密无空洞 → 不置 kBlank;不特判 -32768(饱和真实值,经 toQ 落普通区间)。
  • BITS 真源 = .headBITS 字段P0 仅支持 16-bit32-bit 抛 not implemented)。ENDIAN_TYPE 1=小端/2=大端。
  • 通道插值复用纯函数 geopro::io::gpr::planChannelInterpolation(offsets, targetDy)(默认 targetDy=0.025),通道横向偏移取自 .headCH_X_OFFSETS绝不跨线
  • 内存app 体是 std::vector<double>int16 的 4×单线默认 coarse=4(峰值 <0.5GB)。多线/全分辨率不在本计划(走 P2 核外)。
  • 构建:build.bat appapp/ cmake --build build/release --target geopro_tests(测试);重链前关掉在跑的 app/exeLNK1104别删 build/release
  • 提交Conventional Commitsfeat:/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-197dims 之后改为组 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:

    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

#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

#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)

#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);,再回填 metricsOutnx/ny/nz 取 built.vol.*()dx/dy/dz 取 built.spacing[*]vmin/vmax 取 built.vminPhys/vmaxPhysbefore/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
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:

    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

#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

#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.txtgpr/NormalizedRadarReader.cpp。加 tests/CMakeLists.txtio/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
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:

    // 读规范化 .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: RadarHeaderTask 2

  • Step 1: 写失败测试

加到 test_normalized_radar_reader.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 加:

#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
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:

    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: 写失败测试

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: 写实现

.hppTracePos/parseRadarCor.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
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:

    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/depthSpacingZTask 2-3assembleRadarVolumeTask 1

  • Step 1: 写失败测试

tests/io/gpr/test_normalized_radar_bridge.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

#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.txtgpr/NormalizedRadarVolumeBridge.cpp;加 tests/CMakeLists.txtio/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
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:

    // 规范化测线 → BuiltI16(buildLineVolumeFromNormalized) → 反量化 VolumeGrid。
    VolumeGrid createRadarVolumeGrid(const std::string& lineDir, const std::string& linePrefix,
                                     int coarse = 4, double targetDy = 0.025);
    
  • Consumes: buildLineVolumeFromNormalizedTask 5builtI16ToVolumeGrid(现成)。

  • Step 1: 写失败测试

加到 tests/data/test_gpr_volume_repository.cpp(仿现有 createGprVolumeGrid 测试,用 Task 5 同款合成 .head/.data

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.hppcreateGprVolumeGrid 声明后加 createRadarVolumeGrid(注释说明走规范化链)。.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
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_.countloadVolume 按 dsId 查、addDatasetAsyncisVolumeDataset 分流),故 isVolumeDataset/addDatasetAsync 零改动。

Files:

  • Modify: src/data/api/Api3dRepository.hppStoredVolume 加字段 + 声明 registerRadarDataset
  • Modify: src/data/api/Api3dRepository.cppregisterRadarDataset + loadVolume radar 分支 + volumeRows() 输出 sv.ddCode
  • Test: tests/data/test_3d_repo.cpp(加用例:登记为 dd_radar_3d + loadVolume 同步退化路径产有效体)

Interfaces:

  • Produces:

    // 登记一条 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: createRadarVolumeGridTask 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 无 QCoreApplicationloadVolume 走同步退化路径,回调即时触发):

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.hppStoredVolume(:139 附近) 加 std::string lineDir, linePrefix, ddCode = "dd_voxel", structParentId; int coarse = 4;;在 createGprVolume 声明后加 registerRadarDataset 声明。 .cppregisterRadarDataset(只存元数据,不建体

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;
}

loadVolumeApi3dRepository.cpp:347if (sv.cachedGrid) 块之后、IDW 路径 :351 之前)插入 radar 分支(仿 finalizeVolume:268-335 后台线程范式,#include "data/GprVolumeRepository.hpp" 已在):

  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
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->registerRadarDatasetTask 7refreshAnalysisanalysisTabwindow

  • Step 1: 加导入菜单动作

main.cpprefreshAnalysis 已定义、analysisTab 已存在处(紧接 generateVolumeRequested 连接块之后,约 :982 后)插入。登记瞬时、不建体——勾选后由 Task 7 的 loadVolume 后台建体,列表项 spinner 显进度,失败由 controller loadFailed→toast 兜底(main.cpp:995

// 本地导入三维雷达测线目录(后端未就绪的过渡入口):选目录+前缀 → 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) 同在 :397if (ddCode == "dd_voxel") 块内只改 :397 这一处判定即同时放开二者(无独立 :403 gate

  • :397 if (ddCode == QStringLiteral("dd_voxel"))if (ddCode == QStringLiteral("dd_voxel") || ddCode == QStringLiteral("dd_radar_3d"))

(这是 UI 唯一切片创建入口 sliceRequestedmain.cpp:1061interactionMgr->addSlice;不改则 radar 体右键无切片/色阶。addSlice 依赖的 volume image 由勾选→后台建体→addVolume 附着,与"先勾选渲染再切片"次序一致。)

  • Step 3: dd_radar_3d 双击详情路由

main.cppanalysisTab 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 → 三维体段出现 南同大道_000ddCode=dd_radar_3d)行、自动勾选、列表项转 spinner、后台建体完成后渲染出体spinner 撤)。验:① 体可见;② **右键该行有「生成切片」**→切 updown/leftright/frontback 三向;③ 画点/线/面异常并保存、挂在体下;④ 双击该行弹体属性。日志看 geopro_*.log 无异常。

  • Step 6: Commit
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.cppTask 8 的 radarMenu 加第二个动作)

  • Step 1: 加 Impulse 导入动作

在 Task 8 的 radarMenu 内追加:

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
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 渲染内核、切片/异常工具、三级树(下游零改动达成)。