三维编辑功能实现

三维编辑功能实现

摘要

本文从一款采用 Qt 作为人机界面框架、OpenSceneGraph(OSG)作为三维场景与事件管线、自研渲染引擎封装 Viewer 与命令管理 的桌面软件出发,选取三类典型交互:点云「放大/缩小」(实质为点大小的视觉尺度调节)、点云编辑(拾取与可撤销修改)、面积测量(多边形顶点拾取与度量回调) 。分别从为何采用某种调用结构 、从界面操作到底层实现的分步说明 、以及涉及的 C++ / Qt / STL / 设计模式与惯用法 三个维度展开。核心结论可以概括为:
同一套 Viewer 上并列存在多种「输入语义」------无模式浏览操作用「操作表 + OperationBase」短路径;有状态的编辑操作用「命令模式 + CommandManager + 可逆数据修改」;测量类只读结果用「ParameterTransferCallBack + std::function」隔离业务与引擎。 三类需求对状态持久性、撤销语义、与 Qt 的耦合程度要求不同,因此分层形态必然不同,并非随意堆砌。


第一章 软件分层与公共基础

1.1 表示层与场景层的边界

本类软件在工程结构上通常分为:
(1)主窗口 / Dock / 工具栏 (Qt Widgets):负责菜单、快捷键绑定、属性面板与工程级状态。
(2)三维子窗口 (如 Window3D):内嵌 OSG 的 GraphicsWindow 或等价封装,鼠标键盘事件首先进入 OSG 的 GUIEventAdapter 管线 ,而不是先经过 Qt 的某个「坐标发送槽」。
(3)渲染总线 (Qt QObject 子类):把「图层操作」「测量回调」「信号到属性 Dock」黏合在一起。
(4)RenderEngineViewerCommandManagerGUIEventHandler 等):三维拾取、射线求交、模式化事件分发。
(5)数据与几何 (点云块、瓦片模型、MeasurementToolsAlgorithms):真实修改或度量发生的位置。

理解 「Qt 信号槽不跑在 OSG 事件最前线」 是读懂后面三条链的前提:用户在 3D 视图里的操作,绝大部分是 OSG → 引擎 C++ 回调 → 必要时再 emit 到 Qt ;而菜单切换模式、工程保存等才是 Qt → 调用 Viewer API

1.2 三条链共用的「齿轮」

几乎所有三维交互都会碰到以下机械结构:

  • CommandManager::handle(const osgGA::GUIEventAdapter&, ...)

    继承 osgGA::GUIEventHandler,挂到 ViewerFrame 上。每当系统投递 MOVE / PUSH / RELEASE / SCROLL 等事件,OSG 会调用其 handle。内部顺序遍历 若干 GUIEventHandlerBase 子类(以及可选的 button 专用数组),将事件交给当前鼠标模式 对应的那一个 Handler。这是典型的 责任链 的变体(短路求值:一旦某 handler 宣告已处理,可停止向后传递,具体实现依项目而定)。

  • Viewer::setMousePickMode(EnPickModeType)

    根据枚举查找工厂函数表 (如 GetAreaMeasurementEventHandler),替换 CommandManager 里保存的当前唯一主 HandlersetEventHandle 会先清理再注册)。从而同一条 OSG 事件流 在不同模式下进入不同类,实现 策略模式

  • enableParameterTransferCallBack / getParameterTransferCallBack

    在 Viewer 内部维护一张 std::map<ParameterTransferCallBackType, CallBackAndState> ,用于与鼠标事件并行存在的、按需触发的业务回调 ------例如测量完成时调用面积回调。这与 CommandManager 管的「逐事件输入」是正交的第二条附着机制。

这三者在本工程中分工明确CommandManager 解决 「事件从哪进、按模式交给谁」ParameterTransferCallBack 解决 「异步或阶段结束时如何把结构化结果交给上层」Qt 解决 「如何把结构化结果变成界面状态」


第二章 点云「放大 / 缩小」:从滚轮到点片元尺寸

2.1 产品语义与实现语义的差别

点云放大缩小在实现上往往不是相机拉近(那是 TrackBall / 操作器的事情) ,而是调节 OpenGL GL_POINT 的像素大小 或着色器里等价的 point size ,使点看起来更粗或更细。本代码路径中体现为 PointCloudRender::mpr_pointCloudSize1~10 范围内递增递减,并调用 PointCloudLayer::setPontSize 。因此这是显示参数调节

2.2 调用链(浏览模式 + 快捷键 / 滚轮)

(1)输入: 用户在 3D 视图内 按住 Shift 并滚动滚轮 (浏览模式 PMT_None 下由 BrowseEventHandler 处理 SCROLL 事件)。引擎内调用 ViewerImp::doOperation(OT_LargerPointCloudSize)OT_SmallerPointCloudSize

(2)操作注册表: ViewerImp::addOperation操作类型枚举 映射到 OperationBase* ,存储在 mpr_OperationMapdoOperation 根据枚举 find 到对应项,执行 op->second->doWork() 。这是 命令对象 的极简版(GoF Command 的「无 undo」变体):枚举 = 键,Operation = 可调对象

(3)进入 Qt / 应用层:Window3D.cpp 中,keyLargerPointCloudSizeOperation::doWorkkeySmallerPointCloudSizeOperation::doWork 显式调用:

  • getRenderManager()->getPointCloudLayerOperation()->slotLarger() / slotSmaller()
  • 以及对 块点云图层 getBlockPointCloudLayerOperation() 的同名槽。

此处出现第一次 「引擎事件线程路径 → Qt 侧图层 Operation」 的跳转:
OSG Handler 在同一进程、同一线程(主线程)里直接调用 Qt 侧的 slot 方法 (未经过 QMetaObject::invokeMethod 时即是同步调用)。只要该调用发生在主线程(GUI 线程),在 Qt5/6 中通常是安全的;若未来把 OSG 嵌到独立线程,则需改为 QueuedConnection

(4)图层与渲染: PointCloudLayerOperation::slotLarger 调用 PointCloudRender::largerPointCloudScale(SPARSE_POINT_CLOUD_NAME)FILTER_POINT_CLOUD_NAME 两遍,因为稀疏层与滤波层在业务上是两个逻辑图层名称;内部对 map 查层、限制大小在 1--10 ,然后 layerTemp->setPontSize ,最后 viewer->activeRendering() 确保按需渲染模式下刷新。BlockPointCloudLayerOperation 同理服务块显示。

2.3 为何这条链「长成这样」

  • 无持久编辑语义 :点大小是视图参数 ,用户心理预期是「立刻生效、可随时再调」,与「删点了要能撤销」不同,故 不需要 CommandManager::createCommand + undo 栈
  • 滚轮事件已在 BrowseHandler 里消费 :浏览模式不能简单把事件扔给测量 Handler,所以用 doOperation 统一出口 ,把「滚轮 + 修饰键」翻译成应用层语义操作,避免在 OSG 里写死业务名。
  • OperationBase + map<EnOperationType, Operation*> :用 多态virtual void doWork())和 表驱动 (枚举查表)把「按键/手势」与「业务动作」解耦。新增一种全局快捷键时,只需注册新的 Operation ,无需改 BrowseEventHandler 里一长串 switch

2.4 本条链涉及的「语法与知识点」

  • STL 关联容器std::map / operator[] 插入或替换。
  • 枚举作策略键EnOperationType 与 Handler、与 UI 快捷键配置对应。
  • Qt :图层 Operation 类往往继承 QObject,槽函数可被直接调或 emit 触发;此处是直接同步调用槽。
  • OSGGUIEventAdapter::SCROLL、修饰键 getModKeyMask()

第三章 点云编辑:命令模式、辅助数据与可逆修改

3.1 功能

实现框选 / 点选稀疏点云子集并改色、删除点、批量操作 等。
(1)操作会改变数据或 GPU 缓冲区的内容;
(2)期望 撤销 / 重做
(3) 有时要和其他子系统(要素点、密集点云)共享同一套命令管理器的扩展点

因此不能沿用第二章的轻量 OperationBase 路径,而采用 CommandBase + CommandManager 的 undo/redo 链表

3.2 调用链概览(从拾取到命令提交)

以稀疏点云为例(具体类名因版本略有差异,逻辑一致):

(1)模式切换: 主界面调用 setMousePickMode(PMT_PointCloudSelection)(或矩形选点等),CommandManager::setEventHandle 安装 PointCloudSelectionEventHandler (或 RectSelection 系列)。

(2)事件: 用户在 3D 视图点击,CommandManager::handle 把事件交给当前 Handler。Handler 内通过 viewer->pickPointCloudgetParameterTransferCallBack(PTCBT_PickPointCloud) 等机制,将拾取结果(点 ID 列表、SceneObject*)写入 AuxiliaryData 或临时结构。

(3)命令创建: 当用户触发「应用编辑」(例如确认删除、改色),ViewerImp 或业务层调用 CommandManager::createCommand(PCT_..., ...) 。工厂内部 switch (paInType) new 出具体 CommandBase 子类(如 SPCCDeletePointsCommand),携带 SPCDDeletePointsData 等数据结构。

(4)执行与记录: CommandManager::addCommandexecute() ,成功则压入 mpr_undoList ,并清空 mpr_redoListundoComand / redoComand 反向或正向调用 unexecute() / execute()

(5)与 Qt 的衔接: 编辑结果若需反映到属性面板,往往通过 RenderManager 的信号 、或 Dock 主动拉取 getModification ;这条线与 OSG 事件仍然是分开触发的。

3.3 为何必须是命令模式而不是简单回调

  • 可逆性 :删除点是不可逆破坏性行为,**必须在内存中记录「删了哪些索引」**才能 unexecute。回调函数若没有数据外壳,无法重做。
  • 批处理与合并 :未来若有「一次编辑多图层」,命令对象是唯一自然的事务边界
  • 与其它业务统一CommandManager 还负责密集点云框选、要素点等,getModificationData 从 undo 链收集删除索引------这是横切功能。

3.4 本条链涉及的「语法与知识点」

  • GoF Command 模式CommandBase 接口,execute/unexecute
  • 工厂方法createCommand 巨型工厂(switch + new);可扩展为注册表以削弱编译期依赖。
  • 双端链表 / list<Ref_Ptr<CommandBase>>:undo/redo。
  • 辅助数据对象 AuxiliaryData:会话级拾取状态,避免把 OSG 细节泄漏到 Qt。
  • Ref_Ptr / 引用计数:引擎内对场景对象与命令的共享所有权风格。
  • 策略 + 责任链 :不同 EnPickModeType 对应不同 GUIEventHandlerBase

第四章 面积测量:模式 Handler + 参数传递回调 + Qt 信号

4.1 功能

在模型或点云表面上逐点点击 形成多边形,双击闭合 ,系统计算 周长面积 ,并在属性页展示。该过程是纯读取几何 + 回调数值,不默认产生可撤销命令。

4.2 调用链(与第二章、第三章对照)

(1)模式与测量使能:

界面调用 changeToAreaSelectionMode

  • setMousePickMode(PMT_AreaSelection)AreaMeasurementEventHandler
  • StartAreaMeasurementEnableFunenableParameterTransferCallBack(PTCBT_CalculateArea, new CalculateAreaCallBack(mpr_AreaMeasurementFunc))

(2)初始化时的函数对象:
RenderManager 构造里 SetAreaMeasurementFun(std::bind(&RenderManager::ActiveAreaFun, this, _1)) ,把成员函数绑定为 std::function<void(btVector<double>)>

(3)交互事件: 用户在 3D 视图操作 → CommandManager::handleAreaMeasurementEventHandler::handle

释放左键且判定为单击时,CalModelIntersectionPoints(winX, winY, ...)屏幕坐标射线模型图层 / 临时模型 求交,得到 Vec3d,压入 mpr_linesVertexs 并绘制辅助线;移动鼠标时更新预览。双击CalPolygonPerimeter / CalPolygonArea ,组装 AreaMeasurementCallbackParameters::AreaValue(两项:周长、面积)

(4)参数回调: getParameterTransferCallBack(PTCBT_CalculateArea) 取到 CalculateAreaCallBack* ,调用 operator()ActiveAreaFun

(5)Qt: ActiveAreaFun 填充 AreaMeasurementPropertyInfoemit sigShowMeasurementAttr ;主窗口 connect(..., m_pAttributeDock, SLOT(slotBaseInfoChanged)) 更新 UI。

4.3 面积的几何实现(三角剖分 + 海伦公式)

CalPolygonArea :对 3D 顶点序列 调用 doDelaunayTriangulation (来自 Geometry/Algorithms),得到三角索引;对每个三角形的三顶点调用 CalTriangleArea ------用 三维欧氏边长 + 海伦公式 求面积,再 累加

这不是「经纬度平面投影面积」的严格测绘公式,而是引擎内几何近似 ,若需工程计量意义上的面积,通常要在 GIS 层再做一次投影变换

4.4 为何这条链同时需要 CommandManagerParameterTransferCallBack

  • 鼠标事件 必须走 GUIEventHandler,否则无法在每一帧/每次 MOVE 更新预览。
  • 测量结果阶段性产物 ,若塞进事件 handle 的返回值,无法自然表达「结构化参数」;用 回调参数对象 AreaMeasurementCallbackParameters 更干净。
  • std::function + std::bindRenderManager::ActiveAreaFun 与引擎层 AreaMeasurementCallback 接口衔接,编译期接口匹配、运行期多态 ,避免 RenderEngine 直接 #include 具体业务类。

4.5 本条链涉及的「语法与知识点」

  • std::function / std::bind / std::placeholders:成员函数作回调。
  • 类型擦除 :引擎只认 AreaMeasurementCallback*,应用层填入子类实例。
  • std::map 型 callback 注册表(Viewer 内部)。
  • Qt signals/slotsemit:线程安全与队列连接若后续引入多线程需重温 Qt 文档。
  • 计算几何 :Delaunay、海伦公式、射线与模型求交(osgUtil::IntersectionVisitor 一类)。

第六章 贯穿全工程的 C++ 与现代惯用法小结

面向对象: 大量 继承 + 虚函数GUIEventHandlerBaseOperationBaseCommandBase)。
STL: vectormaplistfunctionbind、迭代器与算法。
RAII 与智能指针: OSG ref_ptrRef_Ptr 风格降低裸 delete 风险。
设计模式: 策略 (模式切换)、命令 (编辑)、责任链 (Handler 序列)、工厂createCommand)、观察者 (Qt 信号槽、ParameterTransferCallBack)。
互操作: Qt 与 OSG 共享 OpenGL 上下文与主线程事件循环 是常见集成方式;本工程通过 在同一主线程同步调用槽 简化模型,代价是长时间计算必须自行切片或使用后台线程,否则阻塞 UI。


结语

点云尺度调节 是无状态显示参数,用 doOperation 表驱动 足够;点云编辑 涉及数据变更与撤销,必须用 命令对象 + undo 栈面积测量 需要 持续的鼠标事件处理阶段性数值回调 解耦,故 OSG Handler + std::function + Qt 信号 各司其职。「轻交互、无副作用」走短路径;「有副作用、要撤销」走命令;「要结构化输出、少耦合」走回调表 + std::function + Qt 信号。


相关推荐
平行侠1 小时前
026FFT快速乘法 - 从信号处理到大数计算的革命
数据结构·算法·信号处理
gqk011 小时前
C++ / MFC / Qt / C# 核心知识点汇总笔记
c++·qt·mfc
Controller-Inversion1 小时前
240. 搜索二维矩阵 II
线性代数·算法·矩阵
计算机安禾1 小时前
【c++面向对象编程】第4篇:类与对象(三):拷贝构造函数与深浅拷贝问题
开发语言·c++·算法
C雨后彩虹1 小时前
猴子爬山问题
java·数据结构·算法·华为·面试
y = xⁿ1 小时前
20天速通LeetCodeday13:关于回溯
算法
计算机安禾1 小时前
【c++面向对象编程】第1篇:从C到C++:面向对象编程思想入门
c语言·c++·算法
Master_oid1 小时前
机器学习41:利用KNN算法实现手写数字识别
深度学习·算法·机器学习
leo__5201 小时前
基于 OpenCV + Qt 的水果智能识别分类系统
qt·opencv·分类