点云场景树架构-详细设计

Ubuntu中进行激光雷达点云处理和多设备标定时,总是需要写大量测试脚本处理和查看点云,有时还需要借助第三方工具,总觉得不是很方便。总想写一个专门搞这些事情的工具,但是总嫌麻烦进展太慢,看看claude code能否帮我完成这个任务,先和它沟通了一版,考考它。

1. 需求概述

当前工程通过硬编码按钮加载和操作点云,存在以下问题:

  • 每个点云需手动创建按钮和槽函数,扩展性差
  • 中间处理结果无统一存储,需单独编写保存逻辑
  • 联合计算(如 ICP 匹配)需手动编写胶水代码
  • 坐标系变换结果无处存放,无法便捷复现

目标:参考 CloudCompare 的设计,建立**场景树(Scene Tree)**管理体系,将点云及其处理结果、参考元素组织为树形层级结构,实现灵活的可视化、操作和计算。

2. 核心概念

概念 说明
场景树节点 (SceneNode) 树中的通用节点,包含元数据、显示属性、类型标识
点云节点 (CloudNode) 继承 SceneNode,封装 pcl::PointCloud 数据
参考元素 (GeoRef) 挂载在点云节点上的几何参考对象(点、线、面)
操作链 从父节点到子节点的变换历史,子节点可追溯到根节点形成完整操作链
当前选中 选中一个节点,主界面的操作按钮作用于该节点。按住 Ctrl 支持多选,但多选时不提供裁剪和 ICP 功能
变换注册表 集中存储坐标系变换参数,支持保存/加载/快速调用

3. 场景树结构

复制代码
[SceneRoot 场景根节点]
├── [点云A] ────────────────────── 顶层原始点云
│   ├── [直通滤波结果]            ← 子节点(处理结果)
│   │   ├── [欧氏聚类#0]          ← 子节点的子节点
│   │   └── [欧氏聚类#1]
│   ├── [旋转平移结果]
│   └── [Polygon裁剪结果]
│       ├── [参考点 p0, p1, p2]   ← 参考元素
│       ├── [参考线 L1]
│       └── [拟合平面 Plane_A]
├── [点云B] ────────────────────── 顶层原始点云
│   ├── [降采样结果]
│   │   └── [ICP配准(配准到点云A)] ← 多节点联合操作结果
│   └── [参考平面 Plane_B]
└── [点云C]
    └── ...

规则

  • 从外部加载的原始点云都放在顶层(SceneRoot 的直接子节点),平级
  • 对任何节点执行操作后,结果作为子节点挂载到该节点下
  • 选中某个节点,操作作用于该节点,结果挂在该节点下
  • 按住 Ctrl 多选,支持联合操作(联合标定/ICP),详见 5.3 节
  • 每个节点知道自己是如何生成的(父节点 + 操作类型 + 参数)

4. 数据结构设计

4.1 标量场(Scalar Field)

参考 CloudCompare 的标量场设计,支持为每个点云挂载多个 per-point 数值数组。

每个标量场有独立的名字、颜色标尺,可用于可视化(映射为颜色)、过滤、导出。

cpp 复制代码
// 颜色标尺:将标量值映射为 RGB
struct ColorScale {
    struct Stop {
        double value;     // 标量值
        QColor   color;   // 对应颜色
    };
    QVector<Stop> stops;  // 至少 2 个,如 blue(0) → red(100)

    QColor sample(double scalarValue) const;  // 线性插值
};

// 标量场:per-point 数值数组
class ScalarField {
public:
    ScalarField(const QString& name, int pointCount);

    void setValue(int index, double value);
    double value(int index) const;
    int count() const;

    // 统计信息(懒计算)
    double minVal() const;
    double maxVal() const;
    void recomputeBounds();

    // 名字与颜色标尺
    QString name() const;
    void setName(const QString& name);

    ColorScale colorScale() const;
    void setColorScale(const ColorScale& scale);

private:
    QString     m_name;
    QVector<double> m_data;       // per-point 值
    double      m_min = 0, m_max = 0;
    bool        m_boundsValid = false;
    ColorScale  m_colorScale;     // 默认蓝→红渐变
};

与点云的关系

cpp 复制代码
// 在 CloudNode 中管理标量场(见 4.7 节):
//   QList<ScalarField*> scalarFields() const;
//   void addScalarField(ScalarField* field);   // 取得所有权
//   void removeScalarField(int index);
//   void setActiveScalarField(int index);      // -1 表示无活动标量场
//   int  activeScalarFieldIndex() const;

渲染模式(三选一)

cpp 复制代码
enum class ColorMode {
    UniqueColor,     // 整个点云一个固定颜色(DisplayProperty::color)
    ScalarField,     // 根据 activeScalarField + 对应 ColorScale 着色
    PerPointColor,   // 每个点独立 RGBA 颜色(预留,后期实现)
};

4.2 显示属性

cpp 复制代码
struct DisplayProperty {
    bool        visible       = true;      // 是否在 VTK 中显示
    float       point_size    = 1.0f;      // 点渲染大小
    float       opacity       = 1.0f;      // 透明度 0~1
    QColor      color         = Qt::white; // 唯一颜色模式下的颜色
    ColorMode   colorMode     = ColorMode::UniqueColor;
    bool        showBBox      = true;      // 是否在 VTK 中显示 AABB 线框
};

4.3 操作类型枚举

两层设计:OpCategory 大类 + OpName 字符串。便于后续插件动态注册新操作类型。

cpp 复制代码
enum class OpCategory {
    None,          // 原始加载
    Filter,        // 滤波
    Crop,          // 裁剪
    Transform,     // 变换(旋转/平移/缩放)
    Cluster,       // 聚类
    Registration,  // 配准
    Fit,           // 几何拟合
};

struct OpInfo {
    OpCategory category  = OpCategory::None;
    QString    name;       // 操作名称(如 "体素下采样"、"直通滤波_X")
    QString    label;      // 显示名(如 "直通滤波_X_1~11")
    double     timestamp  = 0.0;
};

4.4 操作记录(操作链追溯)

cpp 复制代码
struct OpRecord {
    OpInfo      info;                          // 操作分类 + 名称 + 时间戳
    QMap<QString, QVariant> params;            // 操作参数 key-value
};

4.5 参考元素基类 + 子类

cpp 复制代码
enum class GeoType { Point, Line, Plane };

struct GeoRef {
    GeoType     type;
    QString     name;
    QColor      color;
    float       size;   // 点大小 / 线宽 / 面透明度

    // 参考点:多个 3D 坐标
    QVector<QVector3D> points;

    // 参考线:起点 + 方向(从多点拟合)
    QVector3D line_origin;
    QVector3D line_direction;

    // 参考面:ax+by+cz+d=0(从多点/区域拟合)
    float plane_a = 0, plane_b = 0, plane_c = 0, plane_d = 0;
};

4.7 变换注册表

cpp 复制代码
struct TransformEntry {
    QString     name;           // 变换名称,如 "Car_To_World"
    QString     source_cloud;   // 源点云名称(全路径)
    QString     target_cloud;   // 目标点云名称(全路径)
    Eigen::Matrix4f matrix;     // 4x4 变换矩阵
    double      timestamp;      // 记录时间
    QString     remark;         // 备注
};

class TransformRegistry {
public:
    static const int CURRENT_VERSION = 1;

    void add(const TransformEntry& entry);
    TransformEntry get(const QString& name) const;
    QList<TransformEntry> listAll() const;
    void remove(const QString& name);
    bool saveToJson(const QString& path) const;
    bool loadFromJson(const QString& path);
private:
    QMap<QString, TransformEntry> m_entries; // key = name
};

4.8 场景树节点

cpp 复制代码
enum class NodeType {
    Cloud,       // 点云节点
    Group,       // 分组/文件夹节点(无几何数据)
    Mesh,        // 网格节点(预留)
    Polyline,    // 曲线节点(预留)
    GeoRef,      // 参考元素节点(预留,Phase 4)
};

// 全局唯一 ID 生成器(线程安全)
class UniqueIdGenerator {
public:
    static quint64 next() { return m_counter.fetch_add(1) + 1; }
private:
    static QAtomicInteger<quint64> m_counter;
};

class SceneNode {
public:
    explicit SceneNode(NodeType type, SceneNode* parent = nullptr);
    virtual ~SceneNode();

    // 唯一标识
    quint64 id() const;                    // 构造时由 UniqueIdGenerator 分配

    // 层级关系
    SceneNode* parent() const;
    QList<SceneNode*> children() const;
    void addChild(SceneNode* child);       // 自动 re-parent
    void removeChild(SceneNode* child);
    int indexOf(SceneNode* child) const;
    int childCount() const;

    // 类型判断
    NodeType nodeType() const;
    virtual bool isCloudNode() const { return false; }
    virtual bool isGroup()     const { return false; }

    // 元数据
    QString name() const;
    void setName(const QString& name);
    QString path() const;                  // 全路径,如 /点云A/直通滤波/聚类#0
    OpInfo operationInfo() const;
    void setOperation(const OpRecord& op);
    OpRecord operationRecord() const;

    // 显示属性
    DisplayProperty display() const;
    void setDisplay(const DisplayProperty& prop);
    bool visible() const;
    void setVisible(bool v);
    float pointSize() const;
    void setPointSize(float s);

    // 参考元素(Phase 4 使用)
    QList<GeoRef> geoRefs() const;
    void addGeoRef(const GeoRef& ref);
    void removeGeoRef(int index);
    void clearGeoRefs();

    // 状态
    bool isProcessing() const;
    void setProcessing(bool flag);
    QString statusMessage() const;
    void setStatusMessage(const QString& msg);

    // 变换矩阵(预留,§13.2 #12)
    Eigen::Matrix4f storedTransform() const;
    void setStoredTransform(const Eigen::Matrix4f& t);

protected:
    quint64               m_id;
    NodeType              m_type;
    QString               m_name;
    SceneNode*            m_parent;
    QList<SceneNode*>     m_children;
    OpRecord              m_operation;
    DisplayProperty       m_display;
    QList<GeoRef>         m_geoRefs;
    bool                  m_processing = false;
    QString               m_statusMsg;
    Eigen::Matrix4f       m_storedTransform = Eigen::Matrix4f::Identity();
};

4.9 点云节点

cpp 复制代码
struct BoundingBox {
    float x_min, x_max;
    float y_min, y_max;
    float z_min, z_max;

    float width()  const { return x_max - x_min; }
    float depth()  const { return y_max - y_min; }
    float height() const { return z_max - z_min; }
    float volume() const { return width() * depth() * height(); }
};

class CloudNode : public SceneNode {
public:
    explicit CloudNode(SceneNode* parent = nullptr);
    ~CloudNode() override;

    NodeType nodeType() const override { return NodeType::Cloud; }
    bool isCloudNode() const override { return true; }

    // 点云数据(当前版本固定为 pcl::PointXYZ)
    // ★ 预留:后续改为模板或抽象接口,支持 XYZRGB、XYZINormal 等
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud() const;
    void setCloud(const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud);
    int pointCount() const;

    // AABB(调用 computeBounds() 后填充)
    BoundingBox bbox() const;
    void computeBounds();

    // 标量场管理(★ 新增,参考 CloudCompare)
    QList<ScalarField*> scalarFields() const;
    void addScalarField(ScalarField* field);       // 取得所有权
    void removeScalarField(int index);
    void setActiveScalarField(int index);          // -1 表示无活动标量场
    int  activeScalarFieldIndex() const;
    ScalarField* activeScalarField() const;

    // 大坐标精度(预留,§13.2 #8)
    Eigen::Vector3f globalShift() const;
    void setGlobalShift(const Eigen::Vector3f& shift);
    float globalScale() const;
    void setGlobalScale(float scale);

private:
    pcl::PointCloud<pcl::PointXYZ>::Ptr m_cloud;
    BoundingBox            m_bbox;
    bool                   m_bboxValid = false;
    QList<ScalarField*>    m_scalarFields;
    int                    m_activeScalarField = -1;
    Eigen::Vector3f        m_globalShift = Eigen::Vector3f::Zero();
    float                  m_globalScale = 1.0f;
};

5. UI 设计

5.1 整体布局

复制代码
┌─────────────────────────────────────────────────────────┐
│  Menu Bar (文件 编辑 视图 工具 ▾ 帮助)                   │
├───────────┬─────────────────────────────────────────────┤
│           │  VTK 渲染窗口 (PCLVisualizer)               │
│           │                                             │
│  场景树   │  ┌─ 坐标轴 ─┐  ┌─ 信息面板 ─┐              │
│  (QTree)  │  │  3D 视口  │  │ 点数量: xxx│              │
│           │  │          │  │ 边界: ...  │              │
│           │  │          │  │ 操作链: ...│              │
│           │  └──────────┘  └────────────┘              │
│           │                                             │
├───────────┤                                             │
│           │                                             │
│  属性面板  │                                             │
│  (选中节点 │                                             │
│   时显示)  │                                             │
│           │                                             │
├───────────┴─────────────────────────────────────────────┤
│  状态栏: 就绪 / 操作中: ICP 配准 进度 45% / 错误信息      │
└─────────────────────────────────────────────────────────┘

5.2 场景树 (QTreeWidget)

每个 TreeWidgetItem 显示

  • 图标:点云图标 / 处理结果图标 / 参考元素图标
  • 名称:节点名称
  • 第 2 列显示点数量(仅 CloudNode)

交互

操作 行为
左键单击 选中节点 → 更新 m_lastClicked,属性面板刷新
Ctrl + 单击 多选切换
Shift + 单击 范围选择
双击 居中视图到该点云
右键单击 弹出上下文菜单
复选框 (Qt::CheckStateRole) 控制显示/隐藏

5.3 上下文菜单

单选节点时(CloudNode):
复制代码
┌─────────────────────────────┐
│ ▶ 仅显示本节点               │
│ 👁 显示/隐藏                 │
├─────────────────────────────┤
│ 居中视图                    │
├─────────────────────────────┤
│  ── 滤波 ──                 │
│  ▸ 体素下采样               │
│  ▸ 统计滤波                 │
│  ▸ 直通滤波                 │
│  ▸ 最大连通区域             │
├─────────────────────────────┤
│  ── 裁剪 ──                 │
│  Polygon 裁剪               │
├─────────────────────────────┤
│  ── 变换 ──                 │
│  缩放(单位统一)            │
├─────────────────────────────┤
│  ── 聚类 ──                 │
│  欧几里得聚类               │
├─────────────────────────────┤
│  ── 配准 ──                 │
│  ICP 配准                   │
├─────────────────────────────┤
│  ── 参考元素 ──             │
│  添加参考点                 │
│  添加参考线                 │
│  添加参考面                 │
│  清理所有参考元素           │
├─────────────────────────────┤
│  导出点云 (.pcd)            │
│  删除节点                  │
└─────────────────────────────┘
多选节点时(Ctrl 多选):
复制代码
┌─────────────────────────────┐
│  同时显示选中节点             │
│  同时隐藏选中节点             │
├─────────────────────────────┤
│  联合标定                  │
├─────────────────────────────┤
│  删除所有选中节点            │
└─────────────────────────────┘

联合标定交互(核心多选功能):

复制代码
1. Ctrl 多选多个点云节点(需 ≥2 个)
2. 右键 → 联合标定
3. 弹出标定对话框:
   ┌─── 联合标定 ────────────────┐
   │ 设计坐标系(目标/参考):      │
   │ [▼ 选择 ▾]                  │
   │   点云A                     │
   │   点云B                    │
   │   点云A/降采样结果          │
   └─────────────────────────────┘
   │ 测量坐标系(源/待配准):      │
   │ ☑ 点云A/降采样结果          │
   │ ☑ 点云B                    │
   │ ☐ 点云A                    │
   └─────────────────────────────┘
   │ ICP 参数:                   │
   │ 最大对应距离: [5.0]         │
   │ 最大迭代次数: [50]          │
   │                              │
   │ [ 取消 ]  [ 开始标定 ]       │
   └──────────────────────────────┘
4. 用户选择设计坐标系(一个),勾选测量坐标系(一个或多个)
5. 点击"开始标定" → 后台依次将每个测量坐标系 ICP 配准到设计坐标系
6. 标定完成后显示结果表格:
   ┌─── 标定结果 ────────────────┐
   │ 源节点      │ 目标节点     │ RMS    │ 状态   │
   ├─────────────┼──────────────┼────────┼────────┤
   │ 点云B       │ 点云A        │ 0.003  │ 完成   │
   │ 点云A/降采样│ 点云A        │ 0.001  │ 完成   │
   └─────────────┴──────────────┴────────┴────────┘
   │ [ 保存变换 ]  [ 关闭 ]       │
   └──────────────────────────────┘
7. 每条结果行显示:源、目标、RMS 残差、配准状态
8. 点"保存变换"将所有结果存入 TransformRegistry

注意:多选时不提供 Polygon 裁剪,如需裁剪请逐次处理。

有 GeoRef 被选中时:
复制代码
┌─────────────────────────────┐
│  显示/隐藏                  │
├─────────────────────────────┤
│  修改颜色                   │
│  修改大小                   │
├─────────────────────────────┤
│  ── 计算 ──                │
│  ▸ 多点拟合直线             │
│  ▸ 多点拟合平面             │
│  ▸ 点线距离                 │
│  ▸ 点面距离                 │
├─────────────────────────────┤
│  删除                       │
└─────────────────────────────┘

5.4 坐标系处理工具(独立模块)

入口 :菜单栏 工具 ▸ 坐标系处理

功能:输入一组坐标变换参数,以不同表示方式实时预览,并在 VTK 视口中以坐标轴图标显示。

UI 布局

复制代码
┌─── 坐标系处理 ─────────────────┐
│ 变换表示(四选一):             │
│ ◉ 欧拉角 (XYZ)  ○ 轴角        │
│ ○ 四元数     ○ 旋转矩阵       │
├───────────────────────────────┤
│ 欧拉角:                       │
│ Rx: [  0.00  ] deg            │
│ Ry: [  0.00  ] deg            │
│ Rz: [  0.00  ] deg            │
├───────────────────────────────┤
│ 轴角:                         │
│ 轴: [0.00, 0.00, 1.00]        │
│ 角: [  0.00  ] deg            │
├───────────────────────────────┤
│ 四元数:                       │
│ w: [1.000]  x: [0.000]        │
│ y: [0.000]  z: [0.000]        │
├───────────────────────────────┤
│ 旋转矩阵:                     │
│ [ 1.000  0.000  0.000 ]      │
│ [ 0.000  1.000  0.000 ]      │
│ [ 0.000  0.000  1.000 ]      │
├───────────────────────────────┤
│ 平移:                         │
│ Tx: [ 0.000 ]  Ty: [ 0.000 ] │
│ Tz: [ 0.000 ]                 │
├───────────────────────────────┤
│ 显示选项:                     │
│ ☑ 在视口显示坐标系轴          │
│ 轴长: [ 10.0 ]                │
│ 原点: [ 0.0, 0.0, 0.0 ]       │
├───────────────────────────────┤
│ 4×4 齐次变换矩阵:             │
│ ┌───────────────────────┐    │
│ │ 1.000  0.000  0.000  0.000││
│ │ 0.000  1.000  0.000  0.000││
│ │ 0.000  0.000  1.000  0.000││
│ │ 0.000  0.000  0.000  1.000││
│ └───────────────────────┘    │
├───────────────────────────────┤
│ [ 应用到当前点云 ] [ 复制 ]   │
└───────────────────────────────┘

交互规则

  1. 切换表示方式时,自动同步更新其他表示方式的数值
  2. 欧拉角默认 XYZ 顺序(内置固定顺序,不改)
  3. 修改任意值 → 自动重算所有其他表示 + 4×4 矩阵预览
  4. 勾选"在视口显示坐标系轴" → VTK 中实时显示坐标系轴图标(红=X、绿=Y、蓝=Z)
  5. 点击"应用到当前点云" → 将当前变换作为 Transform 操作,挂载到当前场景树节点下(调用 OperationRouter::applyTransform
  6. 点击"复制" → 将 4×4 矩阵复制到系统剪贴板(制表符分隔,方便粘贴到 Excel/Matlab)

数据类型

cpp 复制代码
struct CoordTransform {
    // 平移
    Eigen::Vector3f translation;

    // 旋转(四表示法,内部只存一个,其余实时转换)
    Eigen::Vector3f eulerAngle;        // XYZ 欧拉角,弧度
    Eigen::Vector3f axisAngleAxis;     // 轴角 旋转轴(单位向量)
    float             axisAngleAngle;  // 轴角 角度(弧度)
    Eigen::Quaternionf quaternion;     // 四元数
    Eigen::Matrix3f   rotationMatrix;  // 3×3 旋转矩阵

    // 4×4 齐次变换
    Eigen::Matrix4f homogeneousMatrix() const;

    // 单位切换
    bool eulerInDegrees;   // 界面显示用角度,内部存弧度
};

5.5 属性面板

选中节点后显示:

复制代码
┌─── 节点属性 ───────────────┐
│ 名称: [直通滤波_X]         │
│ 类型: 处理结果             │
│ 操作: 体素下采样           │
│ 点数量: 125,430            │
│ 父节点: /点云A             │
├───────────────────────────┤
│ 显示属性                   │
│ 点大小: [====|====] 2.0   │
│ 透明度: [====|====] 0.8   │
│ 颜色: [色块]              │
│ 着色模式: [唯一颜色 ▾]    │
│ 显示 BBox: [✓]           │
├───────────────────────────┤
│ 标量场 (2)                │
│ [▼ height ▾]  min:0 max:5│
│ · distance               │
│ [+ 添加标量场]           │
├───────────────────────────┤
│ BBox 信息                 │
│ X: [1.000, 11.000] 宽 10.000 │
│ Y: [0.500,  4.000] 深  3.500 │
│ Z: [1.000,  5.000] 高  4.000 │
│ 体积: 140.000 m³          │
├───────────────────────────┤
│ 操作链                     │
│ /点云A                     │
│   └→ 直通滤波_X(1~11)     │
│      └→ 当前节点          │
├───────────────────────────┤
│ 参考元素 (3)               │
│ · 参考点 p0       (颜色)  │
│ · 参考线 L1       (颜色)  │
│ · 拟合平面 P1     (颜色)  │
└───────────────────────────┘

5.6 状态监控

操作状态显示在状态栏 ,使用 QProgressBar + QLabel

复制代码
[状态栏]
├─ QLabel: "操作中: ICP 配准  源:点云A/降采样 → 目标:点云B"
├─ QProgressBar: [████████░░░░░░░░░░░░] 40%
└─ QPushButton: "取消"

方案 :OperationRouter 通过信号 operationProgress 向主线程报告进度(0~100)。主线程的 SceneTreeManager 或状态栏组件监听此信号,更新 QProgressBar。后台操作不阻塞 UI,用户可继续浏览场景树。

6. 核心操作路由设计

6.1 选中节点管理

选择模型 :以 m_selectedNodes 为唯一数据源,单选是多选的特例(列表长度为 1):

复制代码
QTreeWidget 信号                    SceneTreeManager 状态
────────────────────────────────────────────────────────────────
左键单击节点 A          →    m_selectedNodes = {A}
                                m_lastClicked     = A
                                发射 selectionChanged(A)

Ctrl+单击节点 B         →    m_selectedNodes = {A, B}
                                m_lastClicked     = B
                                发射 selectionChanged(B)  // 属性面板刷新为最后点击的

Shift+单击节点 C        →    m_selectedNodes = {A, B, C}
                                m_lastClicked     = C
                                发射 multiSelectionChanged({A,B,C})

双击节点 A              →    m_selectedNodes = {A}
                                m_lastClicked     = A
                                相机居中到 A 的点云

无选中(取消所有选择)   →    m_selectedNodes = {}
                                m_lastClicked     = nullptr
                                属性面板显示"未选择"
cpp 复制代码
class SceneTreeManager {
public:
    static SceneTreeManager& instance();

    // 选中管理(以 m_selectedNodes 为唯一真相)
    SceneNode* currentNode() const;                    // = m_lastClicked(最后点击的)
    QList<SceneNode*> selectedNodes() const;           // = m_selectedNodes(所有选中的)
    bool isMultiSelected() const;                      // = m_selectedNodes.size() > 1
    SceneNode* firstSelected() const;                  // = m_selectedNodes.first()
    QList<CloudNode*> selectedClouds() const;          // 过滤出所有 CloudNode 类型

    void setCurrentNode(SceneNode* node);              // 单选:内部清空列表后 setMultiNodes({node})
    void setMultiNodes(const QList<SceneNode*>& nodes);// 多选:直接赋值
    void clearSelection();

    // 树操作
    CloudNode* addCloud(const QString& name,
                        const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud,
                        SceneNode* parent = nullptr);
    CloudNode* addResultNode(CloudNode* parent,
                             const QString& name,
                             const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud,
                             const OpRecord& op);
    bool removeNode(SceneNode* node);                  // 递归删除子节点

    // 节点查找
    CloudNode* findNodeByPath(const QString& path) const;

    // 变换管理
    TransformRegistry& transformRegistry();

    // VTK 渲染
    void refreshVTKView();                             // 根据可见节点更新 VTK
    void addCloudToVTK(CloudNode* node);
    void removeCloudFromVTK(CloudNode* node);
    void updateCloudInVTK(CloudNode* node);

signals:
    void selectionChanged(SceneNode* node);            // 选中变化(单选或多选都发此信号)
    void nodeAdded(SceneNode* parent, CloudNode* child);
    void nodeRemoved(SceneNode* node);
    void operationStarted(const QString& opName);
    void operationProgress(const QString& opName, int progress);
    void operationFinished(const QString& opName, bool success);

private:
    SceneNode*             m_root;
    SceneNode*             m_lastClicked;              // 最后点击的节点(用于单选和属性面板)
    QList<SceneNode*>      m_selectedNodes;            // 当前所有选中的节点(唯一数据源)
    TransformRegistry      m_transformRegistry;
};

QTreeWidget 信号对接SceneTreeWidget 内部实现):

cpp 复制代码
// 在 SceneTreeWidget 中:
connect(this, &QTreeWidget::itemSelectionChanged, this, [this]() {
    auto items = selectedItems();  // QTreeWidget 自己管理多选
    QList<SceneNode*> nodes;
    for (auto* item : items)
        nodes.append(static_cast<SceneNode*>(item->data(0, Qt::UserRole).value<quintptr>()));
    SceneTreeManager::instance().setMultiNodes(nodes);
});

设计要点

  1. QTreeWidget 负责管理"哪些 item 被勾选中"(Qt 内置多选逻辑)
  2. SceneTreeManager 是全局单例,存储当前选中状态,业务逻辑通过它获取节点
  3. m_lastClicked 只用于属性面板显示,不参与多选逻辑
  4. 需要 CloudNode 时调用 selectedClouds() 自动过滤,不维护单独的 QList<CloudNode*> 副本

6.2 操作路由逻辑

所有操作入口统一从场景树出发,不再绑定具体按钮到具体点云。

Axis 枚举(直通滤波用):

cpp 复制代码
enum class Axis { X, Y, Z };

CalibrationResult(独立结构体,不嵌套在任何类内部):

cpp 复制代码
// 定义在 operation_context.h 中,供 OperationRouter 和标定对话框共用
struct CalibrationResult {
    CloudNode* source;           // 测量节点
    CloudNode* target;           // 设计节点
    Eigen::Matrix4f matrix;      // 配准变换矩阵
    double rms;                  // RMS 残差
    bool success;                // 是否成功
    QString errorMsg;            // 失败原因
};

OperationRouter(QObject 基类,非全 static):

改为 QObject 继承,用信号槽驱动操作执行,便于后续扩展为插件机制和单元测试。

cpp 复制代码
class OperationRouter : public QObject {
    Q_OBJECT
public:
    explicit OperationRouter(QObject* parent = nullptr);

    // 滤波类操作:作用于选中单节点,结果挂为子节点
    void applyVoxelDownsample(CloudNode* target, double leafSize);
    void applyStatisticalFilter(CloudNode* target, int neighbors, double stdDev);
    void applyPassThrough(CloudNode* target, Axis axis, float min, float max);
    void applyMaxConnectedRegion(CloudNode* target);

    // 聚类操作:结果产生多个子节点
    void applyEuclideanCluster(CloudNode* target, double tolerance, int minPts, int maxPts);

    // 变换操作:作用于选中单节点
    void applyTransform(CloudNode* target, const Eigen::Matrix4f& transform);

    // 缩放操作:作用于选中单节点,按指定倍率缩放 XYZ 坐标
    void applyScale(CloudNode* target, const Eigen::Vector3f& scaleFactor);

    // Polygon 裁剪:在 VTK 视口上交互式绘制多边形,裁剪选中节点
    void applyPolygonCrop(CloudNode* target,
                          const QVector<QVector3D>& polygon,
                          bool keepInside);

    // ICP 单节点配准:将 source 点云配准到 target 点云,结果挂载到 source 下
    void applyICP(CloudNode* source, CloudNode* target,
                  double maxCorrDist, int maxIter);

    // 联合标定:将多个测量坐标系配准到一个设计坐标系,结果通过信号返回
    void applyJointCalibration(CloudNode* design,
                               const QList<CloudNode*>& measurements,
                               double maxCorrDist, int maxIter);

signals:
    // 操作生命周期信号
    void operationStarted(const QString& opName);
    void operationProgress(const QString& opName, int progress);
    void operationFinished(const QString& opName, bool success);
    void calibrationFinished(const QList<CalibrationResult>& results);

    // 数据变更信号(对话框监听后调用 SceneTreeManager 添加节点)
    void cloudCreated(CloudNode* parentNode,
                      const QString& childName,
                      const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud,
                      const OpRecord& op);
};

扩展性预留 :后续可通过 OperationRegistry::registerOperation(name, factory) 动态注册新操作,

而不需要修改 OperationRouter 本身。插件 DLL 只需注册自己的操作工厂函数。

操作执行流程(以直通滤波为例)

复制代码
用户右键 → 滤波 → X 向直通滤波 → 弹出参数对话框
→ 用户输入 min=1, max=11 → 确认
→ 获取 currentNode() → CloudNode A
→ 对话框调用 router.applyPassThrough(A, Axis::X, 1, 11)
→ 发射 signal: operationStarted("直通滤波_X_1~11")
→ 后台线程:
     1. 报告 progress(0)
     2. 调用 alg_cloud_common::passThrough(A->cloud(), axis, 1, 11)
     3. 报告 progress(100)
→ 发射 signal: operationFinished("直通滤波_X_1~11", true)
→ 发射 signal: cloudCreated(A, "直通滤波_X_1~11", result, {OpInfo{Filter, "直通滤波_X"}, {min:1, max:11}})
→ 对话框监听 cloudCreated → 调用 SceneTreeManager::addResultNode():
     1. 创建 CloudNode B,parent = A
     2. B->setCloud(result)
     3. 更新场景树 UI,展开 A,选中 B
     4. 刷新 VTK 渲染

信号流设计要点 :OperationRouter 不直接操作 SceneTreeManager,通过信号解耦。

对话框/主窗口监听 cloudCreated 信号,负责调用 SceneTreeManager::addResultNode() 完成节点挂载。

6.3 Polygon 裁剪交互(所见即所得)

核心理念:当前 VTK 视口看到什么视角,就在那个视角的 2D 投影面上画多边形裁剪。所见即所得,不指定固定 XY/XZ/ZY 投影面,而是直接用当前相机视角。

绘制方式(参考 CloudCompare):

复制代码
1. 用户右键 → 裁剪 → Polygon 裁剪
2. 进入 Polygon 绘制模式,VTK 窗口鼠标变为十字光标
3. 依次单击左键添加顶点 → 每点之间用红色线段连接,实时显示在 VTK 视口上
4. 双击左键闭合多边形 → 自动从最后一个点连线到第一个点
5. 弹出裁剪对话框:
   ┌─── Polygon 裁剪 ────────────┐
   │ 裁剪模式: ◉ 保留内部  ○ 外部 │
   │ 顶点数: 6                    │
   │ 当前视角: 俯视               │
   │                              │
   │ [ 取消 ]  [ 保存 ]           │
   └──────────────────────────────┘
6. 用户确认 → 执行裁剪 → 结果挂载为当前节点子节点

实现要点

  • 用户在 VTK 视口上点的是 2D 屏幕坐标(像素位置)
  • 将当前视口中可见的所有点云 3D 点 投影到当前相机的 2D 屏幕平面 (用 VTK 的 vtkRenderWindow::WorldToDisplay / vtkCamera 变换矩阵)
  • 在 2D 屏幕坐标系下用射线法(ray casting)判断每个投影点是否在 Polygon 内部
  • 保留内部(或外部)的点 → 这些点在 3D 空间的原坐标保留(不修改 3D 坐标,只是筛选)
  • Polygon 轮廓在 VTK 视口上实时绘制为红色虚线(用 vtkActor2DvtkPolyData
  • 绘制期间不影响相机旋转,用户可以重新调整视角后再画(但裁剪基于最终 Polygon 所在视角)

关键公式

复制代码
camera_3D_point → WorldToDisplay(camera_3D_point) → (screen_x, screen_y, depth)
for each cloud point P:
    (sx, sy, _) = WorldToDisplay(P)
    if ray_casting(polygon, (sx, sy)) == inside:
        根据 keepInside 决定保留或剔除

这样用户旋转到任意角度,画个框就能把"看到的那一面"的点筛出来,完全符合直觉。

6.4 ICP 配准交互

复制代码
1. 用户右键 → 配准 → ICP 配准
2. 弹出 ICP 对话框:
   ┌─── ICP 配准 ────────────────┐
   │ 源点云: [当前节点名称]       │
   │ 目标点云: [▼ 选择节点 ▾]    │
   │    ├─ 点云A                 │
   │    ├─ 点云B                 │
   │    ├─ 点云A/降采样结果      │
   │    └─ ...                   │
   │                              │
   │ 最大对应距离: [5.0]          │
   │ 最大迭代次数: [50]           │
   │ RMS 残差: [---]              │
   │                              │
   │ [ 取消 ]  [ 开始配准 ]       │
   └──────────────────────────────┘
3. 用户选择目标点云 → 点击"开始配准"
4. 后台执行 ICP(使用 PCL 自带 `pcl::IterativeClosestPoint`)→ 状态栏显示进度
5. 配准完成后:
   - 显示变换矩阵和 RMS 残差
   - 询问是否保存变换到 TransformRegistry(输入名称/备注)
   - 将配准后的点云作为子节点挂载到当前节点下

7. 坐标系变换管理

7.1 设计

变换信息统一由 TransformRegistry 管理,支持持久化:

json 复制代码
{
  "version": 1,
  "transforms": [
    {
      "name": "Car_To_World",
      "source": "/点云A/降采样",
      "target": "/点云B",
      "matrix": [
        [0.998, -0.062,  0.012,  1.5],
        [0.061,  0.997, -0.045,  0.3],
        [-0.014, 0.044,  0.999, -0.2],
        [0,      0,      0,      1]
      ],
      "timestamp": 1716528000.0,
      "remark": "ICP配准结果, 残差=0.003"
    }
  ]
}

7.2 交互

  • 保存变换:ICP 或手动配准完成后,弹出对话框输入名称和备注,存入注册表
  • 加载变换:属性面板有"变换"区域,下拉列表显示已存变换,选择后预览/应用
  • 快速应用:右键 → 坐标系变换 → 应用已保存变换 → 选择 → 点云自动变换

8. 参考元素系统

8.1 拾取方式

方式 操作
单击拾取点 进入拾取模式 → 双击 VTK 视口 → 记录 3D 坐标 → 创建 GeoRef(Point)
Polygon 拾取 进入 Polygon 绘制模式 → 在 VTK 视口上点击多个点 → 闭合多边形 → 投影到点云 → 记录范围内的所有点
手动输入 属性面板输入 XYZ 坐标 → 创建参考点

8.2 拟合计算

复制代码
选 2+ 个参考点 → 右键 → 计算 → 拟合直线
  → 最小二乘法拟合直线(方向 + 原点)
  → 创建 GeoRef(Line) 挂在当前节点上

选 3+ 个参考点 → 右键 → 计算 → 拟合平面
  → SVD 法拟合平面(法向 + d)
  → 创建 GeoRef(Plane) 挂在当前节点上

选 Polygon 区域内的点云 → 右键 → 计算 → 拟合平面
  → 提取区域内点 → RANSAC 拟合平面(抗离群点)
  → 创建 GeoRef(Plane) 挂在当前节点上

8.3 跨节点计算

复制代码
Ctrl 选 节点A 上的参考点 P1
Ctrl 选 节点B 上的参考点 P2
→ 右键 → 计算 → 点间距
  → 显示 |P1-P2| 距离

9. 文件与持久化

9.1 滤波参数配置文件(FilterConfig 类管理)

所有滤波/聚类操作的参数通过 FilterConfig 类读写 filter_params.ini,遵循"界面与文件双向同步"原则:

cpp 复制代码
class FilterConfig {
public:
    static FilterConfig& instance();

    // 通用接口:获取/设置参数
    double get(const QString& section, const QString& key, double defaultValue) const;
    void set(const QString& section, const QString& key, double value);
    QString getString(const QString& section, const QString& key, const QString& defaultValue) const;
    void setString(const QString& section, const QString& key, const QString& value);

    // 加载/保存
    void load();     // 程序启动时调用,从 filter_params.ini 加载
    void save();     // 用户点"保存为默认"时调用

private:
    QSettings m_settings;  // QSettings with INI format
};

INI 文件格式

ini 复制代码
; filter_params.ini
[general]
version = 1

[voxel]
leaf_size = 100.0

[statistical]
neighbors = 1000
std_dev = 1.0

[pass_through]
x_min = 1.0
x_max = 11.0
y_min = 0.5
y_max = 4.0
z_min = 1.0
z_max = 5.0

[euclidean_cluster]
tolerance = 0.5
min_pts = 100
max_pts = 25000

[scale]
x_factor = 1.0
y_factor = 1.0
z_factor = 1.0
preset = custom        # custom / m2cm / m2mm / cm2mm / mm2cm

[icp]
max_correspondence_distance = 5.0
max_iterations = 50
ransac_iterations = 0
ransac_outlier_reject = 0.05

[polygon]
keep_inside = true

同步机制

  • 启动加载 :程序启动时 FilterConfig::instance().load(),所有滤波对话框的默认值来自此文件
  • 对话框修改:用户在滤波对话框中修改参数 → 仅影响本次操作
  • 保存参数 :对话框有"保存为默认"复选框,勾选时将当前参数写回 filter_params.ini
  • 下次启动:自动加载上次保存的参数值

扩展性 :新增操作参数只需在 INI 文件中新增 section/key,无需修改 FilterConfig 类本身。

9.2 变换注册表持久化

ICP 配准等变换结果通过 TransformRegistry 管理,保存为 transforms.json(格式见 7.1 节)。

9.3 当前阶段不做

  • 磁盘缓存(点云数据量大时暂不缓存)
  • 撤销/重做(Ctrl+Z)
  • 项目文件序列化(场景树整体保存,Phase 4 考虑)

10. 迁移方案

10.1 从现有架构迁移

现有模块保留,新增以下模块:

新增文件 职责
scene_tree.h SceneNode, CloudNode, BoundingBox, ScalarField, ColorScale, GeoRef, NodeType 等数据结构定义
scene_tree_widget.h/.cpp 场景树 QTreeWidget 封装
scene_tree_manager.h/.cpp 场景树全局管理器
operation_router.h/.cpp OperationRouter(QObject 基类,信号驱动)
operation_context.h CalibrationResult 独立结构体定义
transform_registry.h/.cpp 变换注册表(含 JSON 序列化,version 字段)
property_panel.h/.cpp 属性面板 Widget
context_menu.h/.cpp 上下文菜单构建器
filter_config.h/.cpp FilterConfig 类(读写 filter_params.ini,含 version 字段)
polygon_crop_dialog.h/.cpp Polygon 裁剪交互(VTK 上绘制 + 对话框)
icp_dialog.h/.cpp ICP 配准对话框
joint_calibration_dialog.h/.cpp 联合标定对话框(选择设计/测量坐标系 + 结果表格)
coord_transform_dialog.h/.cpp 坐标系处理工具对话框(四表示法转换 + VTK 预览)

10.2 修改现有文件

文件 修改内容
cloud_anaylzer.h/.cpp 移除 m_cloud / m_transformed_cloud,改为通过 SceneTreeManager 管理;UI 加入 QTreeWidget + 属性面板
cloud_anaylzer.ui 添加场景树面板、属性面板
现有槽函数 改为调用 OperationRouter,如 applyPassThrough(Axis::X, 1, 11)
pcl_pointpicker.h/.cpp 改为向场景树添加 GeoRef 而非返回裸坐标

10.3 实施步骤

Phase 1 --- 基础框架(优先,当前版本核心)

  1. 实现 SceneNode + CloudNode 数据结构
  2. 实现 SceneTreeWidget(显示、选中、右键菜单)
  3. 实现 SceneTreeManager(添加/删除/选中管理)
  4. 改造主窗口布局:嵌入场景树
  5. 实现节点显示/隐藏联动 VTK 渲染
  6. 迁移点云加载 → 不再用 m_cloud,改为 addCloud()
  7. 实现右键菜单:显示/隐藏、删除、居中视图
  8. 坐标系处理工具:四表示法转换 + VTK 坐标系轴预览(独立于场景树,可直接开发)

Phase 2 --- 操作链 (当前版本核心)

  1. 改造现有操作函数 → OperationRouter(滤波、变换、聚类)

  2. 操作后结果作为子节点挂载

  3. 实现属性面板(显示节点信息 + 显示属性编辑)

  4. 实现状态栏操作状态提示

Phase 3 --- Polygon + 联合标定 (当前版本核心)

  1. Polygon 交互式绘制(VTK 点击拾取、实时预览、双击闭合)

  2. Polygon 裁剪对话框(内部/外部选择、保存按钮)

  3. 单节点 ICP 配准对话框(combobox 选目标、参数配置、结果展示)

  4. 联合标定 对话框:选择设计/测量坐标系,批量 ICP 配准

  5. 标定结果表格组件(QTableWidget),显示源/目标/RMS/状态

  6. 裁剪/ICP/标定结果挂载为子节点

Phase 4 --- 参考元素 (后续迭代)

  1. 参考元素拾取与可视化

  2. 几何拟合(平面、直线)

  3. TransformRegistry 持久化

11. 技术选型备注

决策 选择 理由
树控件 QTreeWidget QTreeView 更简单,当前项目已有 Qt 使用经验,节点数量不大时性能无压力
VTK 渲染 保持 PCLVisualizer 不改动 3D 引擎,只在可见性控制上对接场景树
进度报告 QTimer + std::atomic 轮询 简单可靠,不引入额外并发框架
变换存储 JSON 文件(含 version) 易读易写,Qt 内置 QJsonDocument 支持
滤波配置 FilterConfig 类 + INI(含 version) get/set 通用接口,新增参数无需改代码
编码 纯 ASCII 标识符 + 英文 UI 避免 GBK/UTF-8 编码问题,运行时通过 QObject::tr() 做中文本地化
缓存/撤销 暂不实现 先做一版能用的,后续按需添加
操作路由 QObject 基类 + 信号槽 便于扩展插件机制和单元测试

12. 已确认的技术决策

决策 结论
Polygon 投影方式 所见即所得:基于当前相机视角的 2D 屏幕投影,不指定固定平面
Polygon 裁剪交互 左键依次点顶点 → 双击闭合 → 弹出对话框选内/外 → 点"保存"确认
ICP 算法 PCL 自带 pcl::IterativeClosestPoint,暂不提取到算法库
滤波参数 FilterConfig 类管理 filter_params.ini(含 [general] version = 1),get/set 通用接口,界面修改后点"保存为默认"写回,启动时自动加载
缩放操作 支持 XYZ 三向独立缩放,预设常用单位转换(m→cm、m→mm、cm→mm、mm→cm)
BBox 选中节点自动在 VTK 视口显示 AABB 线框 + 属性面板实时显示 BBox 尺寸/体积;预留 globalShift/globalScale 处理大坐标精度
多选联合操作 联合标定:选一个设计坐标系,勾选多个测量坐标系 → 批量 ICP 配准 → 结果表格展示
多选 Polygon/ICP 不提供(裁剪仅限单选)
磁盘缓存/撤销 暂不实现,先做一版能用的
参考元素 Phase 4 才实现,Phase 1~3 先跳过
点云类型 当前版本固定 pcl::PointXYZ,CloudNode 注释标注预留抽象层(支持 XYZRGB 等)
着色机制 三模式:UniqueColor(固定色)/ ScalarField(标量场+颜色标尺)/ PerPointColor(预留),取代孤立的 color_by_height
操作类型 OpCategory 大类 + OpInfo(字符串名),便于插件动态注册
操作路由 OperationRouterQObject 基类,信号驱动,非全 static
节点唯一 ID SceneNode 构造时由 UniqueIdGeneratorQAtomicInteger)分配 quint64 id()
配置版本 filter_params.initransforms.json 均含 version 字段,支持跨版本兼容

13. 架构评审 --- 与 CloudCompare 对比发现的疏漏

本节基于 CloudCompare(开源点云处理标杆工具)的架构分析,列出当前设计中遗漏或不合理 的设计点,按重要性 排序。

所有"本期不做"的项标记为 ,会在后续 Phase 补充;"立即修复"的标记为 ,应在本期设计阶段就解决。

13.1 立即修复(设计阶段就应解决)✅ 已全部修正

# 问题 修正方案 修正位置
1 点云类型硬编码为 pcl::PointXYZ CloudNode 保留 PointXYZ,但注释标注预留抽象层;DisplayProperty 用 ColorMode 替代 color_by_height;属性面板支持 ScalarField 切换 4.9 CloudNode
2 缺少标量场(Scalar Field)机制 新增 §4.1 ScalarField + ColorScale 设计;CloudNode 增加标量场管理 API;属性面板增加标量场区域 4.1, 4.2, 4.9
3 CalibrationResult 嵌套在 OperationRouter 内部 移出到 operation_context.h 作为独立结构体;OperationRouter 通过信号 calibrationFinished 返回 6.2
4 INI/JSON 配置缺少版本字段 filter_params.ini 增加 [general] version = 1transforms.json 增加 "version": 1 9.1, 7.1
5 缺少唯一 ID 机制 SceneNode 增加 quint64 id() 成员 + UniqueIdGenerator 全局计数器(QAtomicInteger 线程安全) 4.8
6 操作路由用 static OperationRouter 改为 QObject 继承,用信号槽驱动操作执行;预留 OperationRegistry 插件注册机制 6.2

13.2 本期架构预留(接口设计时考虑,本期不实现)

# 预留 说明 如何预留
7 OBB(方向包围盒) 当前只有 AABB(轴对齐包围盒)。CloudCompare 对旋转后的点云可显示 OBB SceneNode 预留 OrientedBoundingBox 可选数据,DisplayProperty 预留 show_OBB
8 大坐标精度处理(Global Shift) 当点云坐标值很大时(如 UTM 坐标 X=600000),float 精度不够导致渲染抖动 CloudNode 预留 shift 机制接口,setCloud 时自动检测坐标范围
9 多格式导入/导出 当前只提到 .pcd。常见格式:.las.ply.e57.stl exportCloud 参数带格式枚举,不硬编码后缀
10 分组/文件夹节点 场景树只支持"点云 → 结果",缺少纯组织性质的分组节点 SceneNode 支持 isGroup() 类型,分组节点无几何数据但可包含子节点
11 网格/曲线实体 CloudCompare 支持 Mesh、Polyline、2D Entity 等多种实体 NodeType 已有 Mesh/Polyline 枚举值,后续扩展 MeshNode/PolylineNode 继承 SceneNode 即可
12 变换的"应用"与"存储"分离 当前 ICP 结果直接挂子节点,但存储的变换矩阵实际变换点云是两个概念 SceneNode 预留 storedTransform 属性(不修改点云数据,仅记录变换);右键"应用变换"才真正修改点云
13 命令历史栈(Undo/Redo) 操作链是只读的追溯,不是可逆的命令历史 OpRecord 预留 inverse() 接口,后续接 QUndoStack

13.3 需要补充的设计细节 ✅ 已补充

# 细节 修正方案 修正位置
14 节点类型系统 已定义 NodeType 枚举 + nodeType() + isCloudNode()/isGroup() 虚方法 4.6 SceneNode
15 VTK 可见性联动 定义 SceneTreeManager::refreshVTKView() 遍历所有节点,对 visible=true 的 CloudNode 调用 VTK SetVisibility(true),false 的调用 SetVisibility(false) 6.1 § 6
16 删除子节点处理 级联删除:删除父节点时递归删除所有子节点(与 CloudCompare 一致) 6.1 removeNode()
17 多节点 BBox 管理 各自独立显示 :每个可见 CloudNode 按 showBBox 独立控制是否显示其 BBox 线框 4.2 DisplayProperty::showBBox
18 点云数据所有权 setCloud() 接收 shared_ptr(浅拷贝引用计数),不复制底层点云数据;子节点结果通过 addResultNode 传递新分配的 Ptr 4.9 CloudNode::setCloud()
19 拖拽排序 暂不实现,Phase 4 考虑 标记为

13.4 设计决策修正建议 ✅ 已全部采纳

原设计 修正后 状态
OpType 枚举列具体操作 改为 OpCategory + OpInfo(字符串名),两层设计 ✅ 已改
OperationRouter 全部 static 改为 QObject 基类,信号驱动,预留插件注册 ✅ 已改
INI 参数硬编码解析 FilterConfig 类管理,get/set 通用接口 ✅ 已改
BoundingBox 只有 AABB 预留 globalShift/globalScale,OBB 在 §13.2 #7 预留 ✅ 已改
无节点唯一标识 SceneNode 增加 quint64 id() + UniqueIdGenerator ✅ 已改
相关推荐
一个数据大开发14 小时前
大模型时代的数据中台架构演进:从数据仓库到认知引擎
数据仓库·架构
小许同学记录成长14 小时前
QGC整体架构与代码目录解析
架构·无人机
Rain50914 小时前
架构解密:mini-cc 的核心设计思路
前端·架构·开源·node.js·ai编程
mohaoyuan14 小时前
软考架构师知识点汇总
开发语言·架构
GISer_Jing14 小时前
现代分布式系统架构全链路解析
后端·架构
mydeman14 小时前
智能体工程化演进:架构收敛、协议标准化与安全边界下沉
人工智能·架构·软件工程·ai编程
Maimai1080814 小时前
用 TanStack Table、React Query 和 shadcn/ui 搭一个可维护的数据表格架构
前端·javascript·react.js·ui·架构·前端框架·reactjs
song50115 小时前
多模态模型在昇腾上的部署架构
人工智能·分布式·深度学习·架构·transformer·交互
heimeiyingwang15 小时前
【架构实战】DevOps工程化:从需求到上线的完整闭环
运维·架构·devops