三维编辑功能实现摘要本文从一款采用Qt 作为人机界面框架、OpenSceneGraphOSG作为三维场景与事件管线、自研渲染引擎封装 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渲染总线QtQObject子类把「图层操作」「测量回调」「信号到属性 Dock」黏合在一起。4RenderEngineViewer、CommandManager、GUIEventHandler等三维拾取、射线求交、模式化事件分发。5数据与几何点云块、瓦片模型、MeasurementTools、Algorithms真实修改或度量发生的位置。理解「Qt 信号槽不跑在 OSG 事件最前线」是读懂后面三条链的前提用户在 3D 视图里的操作绝大部分是OSG → 引擎 C 回调 → 必要时再 emit 到 Qt而菜单切换模式、工程保存等才是Qt → 调用 Viewer API。1.2 三条链共用的「齿轮」几乎所有三维交互都会碰到以下机械结构CommandManager::handle(const osgGA::GUIEventAdapter, …)继承osgGA::GUIEventHandler挂到Viewer的Frame上。每当系统投递MOVE / PUSH / RELEASE / SCROLL等事件OSG 会调用其handle。内部顺序遍历若干GUIEventHandlerBase子类以及可选的 button 专用数组将事件交给当前鼠标模式对应的那一个 Handler。这是典型的责任链的变体短路求值一旦某 handler 宣告已处理可停止向后传递具体实现依项目而定。Viewer::setMousePickMode(EnPickModeType)根据枚举查找工厂函数表如GetAreaMeasurementEventHandler替换CommandManager里保存的当前唯一主 HandlersetEventHandle会先清理再注册。从而同一条 OSG 事件流在不同模式下进入不同类实现策略模式。enableParameterTransferCallBack/getParameterTransferCallBack在 Viewer 内部维护一张std::mapParameterTransferCallBackType, CallBackAndState用于与鼠标事件并行存在的、按需触发的业务回调——例如测量完成时调用面积回调。这与CommandManager管的「逐事件输入」是正交的第二条附着机制。这三者在本工程中分工明确CommandManager解决「事件从哪进、按模式交给谁」ParameterTransferCallBack解决「异步或阶段结束时如何把结构化结果交给上层」Qt解决「如何把结构化结果变成界面状态」。第二章 点云「放大 / 缩小」从滚轮到点片元尺寸2.1 产品语义与实现语义的差别点云放大缩小在实现上往往不是相机拉近那是 TrackBall / 操作器的事情而是调节OpenGLGL_POINT的像素大小或着色器里等价的point size使点看起来更粗或更细。本代码路径中体现为PointCloudRender::mpr_pointCloudSize在110范围内递增递减并调用PointCloudLayer::setPontSize。因此这是显示参数调节。2.2 调用链浏览模式 快捷键 / 滚轮1输入用户在 3D 视图内按住 Shift 并滚动滚轮浏览模式PMT_None下由BrowseEventHandler处理 SCROLL 事件。引擎内调用ViewerImp::doOperation(OT_LargerPointCloudSize)或OT_SmallerPointCloudSize。2操作注册表ViewerImp::addOperation将操作类型枚举映射到OperationBase*存储在mpr_OperationMap。doOperation根据枚举find到对应项执行op-second-doWork()。这是命令对象的极简版GoF Command 的「无 undo」变体枚举 键Operation 可调对象。3进入 Qt / 应用层在Window3D.cpp中keyLargerPointCloudSizeOperation::doWork与keySmallerPointCloudSizeOperation::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 里写死业务名。OperationBasemapEnOperationType, 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路径而采用CommandBaseCommandManager的 undo/redo 链表。3.2 调用链概览从拾取到命令提交以稀疏点云为例具体类名因版本略有差异逻辑一致1模式切换主界面调用setMousePickMode(PMT_PointCloudSelection)或矩形选点等CommandManager::setEventHandle安装PointCloudSelectionEventHandler或RectSelection系列。2事件用户在 3D 视图点击CommandManager::handle把事件交给当前 Handler。Handler 内通过viewer-pickPointCloud、getParameterTransferCallBack(PTCBT_PickPointCloud)等机制将拾取结果点 ID 列表、SceneObject*写入AuxiliaryData或临时结构。3命令创建当用户触发「应用编辑」例如确认删除、改色ViewerImp或业务层调用CommandManager::createCommand(PCT_..., ...)。工厂内部switch (paInType)new出具体CommandBase子类如SPCCDeletePointsCommand携带SPCDDeletePointsData等数据结构。4执行与记录CommandManager::addCommand先execute()成功则压入mpr_undoList并清空mpr_redoList。undoComand/redoComand反向或正向调用unexecute()/execute()。5与 Qt 的衔接编辑结果若需反映到属性面板往往通过RenderManager的信号、或Dock 主动拉取getModification这条线与 OSG 事件仍然是分开触发的。3.3 为何必须是命令模式而不是简单回调可逆性删除点是不可逆破坏性行为**必须在内存中记录「删了哪些索引」**才能unexecute。回调函数若没有数据外壳无法重做。批处理与合并未来若有「一次编辑多图层」命令对象是唯一自然的事务边界。与其它业务统一CommandManager还负责密集点云框选、要素点等getModificationData从 undo 链收集删除索引——这是横切功能。3.4 本条链涉及的「语法与知识点」GoF Command 模式CommandBase接口execute/unexecute。工厂方法createCommand巨型工厂switchnew可扩展为注册表以削弱编译期依赖。双端链表 /listRef_PtrCommandBaseundo/redo。辅助数据对象AuxiliaryData会话级拾取状态避免把 OSG 细节泄漏到 Qt。Ref_Ptr/ 引用计数引擎内对场景对象与命令的共享所有权风格。策略 责任链不同EnPickModeType对应不同GUIEventHandlerBase。第四章 面积测量模式 Handler 参数传递回调 Qt 信号4.1 功能在模型或点云表面上逐点点击形成多边形双击闭合系统计算周长与面积并在属性页展示。该过程是纯读取几何 回调数值不默认产生可撤销命令。4.2 调用链与第二章、第三章对照1模式与测量使能界面调用changeToAreaSelectionModesetMousePickMode(PMT_AreaSelection)→AreaMeasurementEventHandlerStartAreaMeasurementEnableFun→enableParameterTransferCallBack(PTCBT_CalculateArea, new CalculateAreaCallBack(mpr_AreaMeasurementFunc))。2初始化时的函数对象RenderManager构造里SetAreaMeasurementFun(std::bind(RenderManager::ActiveAreaFun, this, _1))把成员函数绑定为std::functionvoid(btVectordouble)。3交互事件用户在 3D 视图操作 →CommandManager::handle→AreaMeasurementEventHandler::handle。释放左键且判定为单击时CalModelIntersectionPoints(winX, winY, ...)将屏幕坐标射线与模型图层 / 临时模型求交得到Vec3d压入mpr_linesVertexs并绘制辅助线移动鼠标时更新预览。双击后CalPolygonPerimeter/CalPolygonArea组装AreaMeasurementCallbackParameters::AreaValue两项周长、面积。4参数回调getParameterTransferCallBack(PTCBT_CalculateArea)取到CalculateAreaCallBack*调用operator()→ActiveAreaFun。5QtActiveAreaFun填充AreaMeasurementPropertyInfoemit sigShowMeasurementAttr主窗口connect(..., m_pAttributeDock, SLOT(slotBaseInfoChanged))更新 UI。4.3 面积的几何实现三角剖分 海伦公式CalPolygonArea对3D 顶点序列调用doDelaunayTriangulation来自Geometry/Algorithms得到三角索引对每个三角形的三顶点调用CalTriangleArea——用三维欧氏边长 海伦公式求面积再累加。这不是「经纬度平面投影面积」的严格测绘公式而是引擎内几何近似若需工程计量意义上的面积通常要在GIS 层再做一次投影变换。4.4 为何这条链同时需要CommandManager与ParameterTransferCallBack鼠标事件必须走GUIEventHandler否则无法在每一帧/每次 MOVE 更新预览。测量结果是阶段性产物若塞进事件handle的返回值无法自然表达「结构化参数」用回调参数对象AreaMeasurementCallbackParameters更干净。std::functionstd::bind把RenderManager::ActiveAreaFun与引擎层AreaMeasurementCallback接口衔接编译期接口匹配、运行期多态避免 RenderEngine 直接#include具体业务类。4.5 本条链涉及的「语法与知识点」std::function/std::bind/std::placeholders成员函数作回调。类型擦除引擎只认AreaMeasurementCallback*应用层填入子类实例。std::map型 callback 注册表Viewer 内部。Qtsignals/slots与emit线程安全与队列连接若后续引入多线程需重温 Qt 文档。计算几何Delaunay、海伦公式、射线与模型求交osgUtil::IntersectionVisitor一类。第六章 贯穿全工程的 C 与现代惯用法小结面向对象大量继承 虚函数GUIEventHandlerBase、OperationBase、CommandBase。STLvector、map、list、function、bind、迭代器与算法。RAII 与智能指针OSGref_ptr、Ref_Ptr风格降低裸delete风险。设计模式策略模式切换、命令编辑、责任链Handler 序列、工厂createCommand、观察者Qt 信号槽、ParameterTransferCallBack。互操作Qt 与 OSG共享 OpenGL 上下文与主线程事件循环是常见集成方式本工程通过在同一主线程同步调用槽简化模型代价是长时间计算必须自行切片或使用后台线程否则阻塞 UI。结语点云尺度调节是无状态显示参数用doOperation表驱动足够点云编辑涉及数据变更与撤销必须用命令对象 undo 栈面积测量需要持续的鼠标事件处理与阶段性数值回调解耦故OSG Handler std::function Qt 信号各司其职。「轻交互、无副作用」走短路径「有副作用、要撤销」走命令「要结构化输出、少耦合」走回调表 std::function Qt 信号。