
本文从业务分析与逻辑推理出发,旨在研究FreeCAD中Selection Model的相关实现原理。注1:限于研究水平,分析难免不当,欢迎批评指正。注2:文章内容会不定期更新。一、概述在图形交互系统中,“选择”通常是用户意图进入系统内部处理链路的第一个明确动作。对于 FreeCAD 这样的参数化 CAD 平台来说,选择不仅决定“当前操作对象是谁”,还决定后续约束建立、特征编辑、视图同步以及脚本自动化的作用范围。因此,在讨论具体实现之前,先从应用场景、业务逻辑和经典模型三个层面梳理选择系统的共性问题,有助于后续理解 FreeCAD 这套实现为何会采用当前的结构。1.1 应用场景在 CAD/CAE/CAM 系统里,“选择”不是一个简单的界面动作,而是几乎所有交互命令的起点。用户无论是查看模型、编辑草图、修改特征、装配定位、创建约束、导出几何,还是在 Python 中做自动化操作,第一步通常都是先明确“操作对象是谁”。因此,一个成熟的选择系统通常需要同时解决五类问题:对象定位:用户点击、框选、悬停时,系统如何识别目标对象。语义归一:用户选中的可能是整个对象,也可能是面、边、点、链接路径中的子对象,系统要把这些输入统一为稳定的内部语义。选择约束:系统需要根据当前编辑模式、过滤器、任务上下文限制可选对象集合,避免非法选择进入后续流程。状态共享:3D 视图、树视图、属性面板、任务面板、脚本接口要共享同一份选择状态。交互反馈:一旦选择状态变化,界面必须同步完成高亮、列表刷新、属性刷新、命令启停等响应。FreeCAD 的业务场景比普通二维 GUI 更复杂,原因在于它的选择对象不仅有“文档对象”一级,还存在:子元素选择,例如Face1、Edge3、Vertex2装配/链接路径选择,例如通过 Link、Body、Part 容器逐级路由到实际对象多视图同步选择,例如 Tree View、3D View、Selection View、Python Console 同时参与模式化选择,例如任务面板或编辑状态只允许选择某类对象或某类子元素这意味着 FreeCAD 的选择功能不能只做成“点击一个对象,把 selected=true”,而必须演化为一个独立的 GUI 状态系统。在 FreeCAD 中,这种“选择约束”能力并不是附属逻辑,而是主链路的一部分,典型实现就是SelectionGate、SelectionFilter以及不同编辑上下文下对可选对象范围的限制。本文聚焦 FreeCAD GUI 层的主选择链路,重点分析Gui::Selection相关组件如何在Tree View、3D View与Python三类入口之间完成选择解析、状态维护和消息分发。文中会覆盖预选、正式选择、选择过滤、picked list、选择历史栈等核心机制,但不展开各个具体工作台命令内部的选择后处理逻辑。1.2 业务分析与逻辑链路从业务逻辑角度看,选择功能可以抽象为一条完整的交互闭环。前述五类问题描述的是系统目标,下面这条链路描述的是系统运行过程,二者是交叉映射关系,而不是一一对应关系:选择入口 - 对象定位与语义归一 - 选择约束与路由 - 中心状态更新 - 状态共享与选择分发 - 交互反馈它不是单一步骤,而是一条带有语义加工和状态传播的流水线。1.2.1 选择入口选择入口是“用户意图被系统感知”的地方。在 FreeCAD 中主要有三类:3D View:鼠标移动产生预选,鼠标点击产生正式选择Tree View:树节点点击、扩展选择、批量选择、拖拽后的重选中Python Console / 宏命令:脚本直接调用Gui.Selection接口入口层的职责不是保存状态,而是把原始交互事件翻译为“文档名 + 对象名 + 子路径 + 坐标”等选择请求。1.2.2 对象定位与语义归一对象定位与语义归一阶段负责回答“用户到底选中了什么”。在 CAD 系统里,原始点击点并不直接等于业务对象:点击命中的是渲染路径,不是文档对象用户看到的是子形状,业务上可能要映射到DocumentObject或链接后的真实对象同一点击位置可能命中多个候选,系统要决定优先级因此,这一阶段一般包括:几何拾取子元素命名解析链接/容器路径解析多命中候选筛选重复选择判定在 FreeCAD 中,这一步主要由SoFCUnifiedSelection的拾取逻辑和SelectionSingleton::checkSelection()的规范化逻辑共同完成。1.2.3 选择约束与路由所谓“选择约束与路由”,是指系统先判断这次选择是否被允许,再把合法的选择请求送到正确的选择上下文和正确的消费者。在普通桌面应用里,选择约束与路由通常都比较简单,因为只有一个窗口、一个列表、一个对象集合。但在 FreeCAD 中,这一阶段至少要考虑:文档维度:选择属于哪个App::Document对象维度:是顶层对象,还是解析后的子对象语义维度:是预选,还是正式选择视图维度:消息要发给 3D View、Tree View、Selection View、PropertyView 等不同响应者过滤维度:当前是否存在SelectionGate限制因此,FreeCAD 的选择系统本质上是“带约束的上下文路由系统”,而不是简单的全局数组。1.2.4 中心状态更新中心状态更新阶段负责把一次合法的选择请求写入中心状态。理论上常见有两种方式:把选择状态存进对象本身把选择状态集中存放在独立的状态容器中FreeCAD 选择了后者:选择状态集中保存在Gui::SelectionSingleton中,并进一步按文档切分为SelectionInfo上下文。这样做的结果是:App::DocumentObject不承担 GUI 会话状态选择可以天然支持多文档同一份选择状态可以同时服务于 C++、Qt UI、3D 场景和 Python1.2.5 状态共享与选择分发状态共享与选择分发阶段负责把中心状态变化广播给所有观察者。理论上,这属于典型的发布-订阅模型(Publish-Subscribe)或观察者模型(Observer Pattern)。FreeCAD 在这一层做了两级分发:先通知对象对应的ViewProvider再通过SelectionObserver及信号系统分发给其他 UI 组件这使得“对象自己的视觉表现”和“外围界面响应”能够同时更新。1.2.6 交互反馈交互反馈阶段是选择业务逻辑真正对用户可见的部分,常见表现包括:3D 视图高亮或取消高亮Tree View 同步勾选或展开节点Selection View 刷新列表属性面板刷新当前对象状态栏显示预选对象与点坐标命令可用性变化,例如按钮启用/禁用因此,选择系统从业务上看并不是一个“输入子系统”,而是一个“输入识别 + 约束判断 + 中心状态 + 广播共享 + 交互反馈”的综合交互子系统。1.2.7 FreeCAD 中的业务链路映射运行阶段理论职责FreeCAD 主要落点选择入口捕获用户选择意图SoFCUnifiedSelection、TreeWidget、Python API对象定位与语义归一几何拾取、对象语义化getPickedList()、SelectionPickPolicy、checkSelection()选择约束与路由选择合法性判断,并按文档/消息类型/解析模式投递getSelectionContext()、ResolveMode、SelectionGate中心状态更新维护当前选择与预选状态selList、pickedList、CurrentPreselection状态共享与选择分发广播给观察者与视图提供者notify()、signalSelectionChanged*、SelectionObserver交互反馈完成高亮、列表刷新、命令联动ViewProvider::onSelectionChanged()、TreeWidget、SelectionView1.3 经典模型从软件架构角度,交互式系统的选择功能大致有几种经典实现模型。1.3.1 对象内嵌状态模型最直接的做法是:每个可选对象自己维护一个selected状态位,点击时直接修改对象,再由对象自己刷新显示。优点:实现简单单视图场景容易落地缺点:选择状态污染业务模型很难处理多视图同步很难表达预选、历史栈、过滤器等高级机制这类模型适合简单图形编辑器,不适合 FreeCAD 这种多文档、多视图、子对象密集的 CAD 系统。1.3.2 集中式选择集模型更常见的做法是把当前选择存放在一个独立的“选择集合”里,由系统统一管理,视图只负责展示与订阅。优点:选择状态与业务对象解耦易于做多视图同步易于做脚本接口、历史栈和命令判断这实际上就是 FreeCAD 的基本模型,即以SelectionSingleton作为中心选择仓库。1.3.3 场景图选择模型在 3D 系统里,很多框架会把选择直接绑定到场景图节点上,通过拾取路径来确定命中对象,并就地完成高亮。优点:拾取效率高与渲染系统耦合紧密,容易做即时高亮缺点:语义层常常不够稳定,拾取结果更多是“渲染节点”而不是“业务对象”很难直接满足链接、装配路径、脚本接口等高级需求FreeCAD 在 3D 层使用了这种模型的一部分,即SoFCSelection/SoFCUnifiedSelection负责场景拾取和局部高亮,但不会把它作为最终业务状态源。1.3.4 MVC/观察者模型在经典 GUI 理论中,选择通常被视为 Model 的一个派生状态,View 通过观察者机制响应变化。优点:解耦清晰易于组合多个视图缺点:如果没有额外的语义解析层,无法独立解决 CAD 场景中的子元素、链接和多命中问题FreeCAD 明显采用了这个方向:SelectionSingleton作为中心状态,SelectionObserver作为订阅基类,Tree、Selection View、Property View 等作为响应者。1.3.5 命令/事件总线模型现代交互系统还常把选择动作视为一种命令或事件,通过事件总线统一处理。这种做法有利于:录制宏命令支持撤销/重放让脚本接口与 GUI 走同一套行为路径FreeCAD 虽然没有把选择彻底做成独立命令总线,但已经具备很强的事件化特征:SelectionChanges作为消息体notify()作为中心分发点Python API 和宏录制共享相同的选择中心1.3.6 FreeCAD 所采用的混合模型综合来看,FreeCAD 的选择模型并不是单一范式,而是一种混合架构:在 3D 入口上,采用“场景图拾取模型”在状态管理上,采用“集中式选择集模型”在界面联动上,采用“MVC/观察者模型”在脚本与扩展上,具备“事件/命令化接口”的特征再加上一层 CAD 特有的“子元素路径解析模型”,构成了 FreeCAD 选择系统的核心特色。二、主要组件下面结合代码,抽取 FreeCAD 选择系统中的主要组件。组件代码位置核心职责说明SelectionSingletonsrc/Gui/Selection/Selection.h/.cpp中心状态管理、增删查改、预选、分发整个选择系统的核心单例SelectionChangessrc/Gui/Selection/Selection.h选择事件消息模型描述 Add/Rmv/Clr/Preselect 等变化SelectionDescriptionsrc/Gui/Selection/Selection.cpp内部选择记录保存对象、解析对象、元素名、坐标等完整信息SelectionInfosrc/Gui/Selection/Selection.h每文档选择上下文保存selList、pickedList、过滤器、历史栈、风格SelectionObjectsrc/Gui/Selection/SelectionObject.h对外暴露的选择结果对象给 C++/Python 提供聚合后的选择对象SelectionObserversrc/Gui/Selection/Selection.h/.cpp观察者基类Tree、SelectionView、PropertyView 等都靠它订阅选择变化SelectionGatesrc/Gui/Selection/Selection.h选择过滤策略接口控制什么能被选中SelectionFilter/SelectionFilterGatesrc/Gui/Selection/SelectionFilter.h/.cpp基于语法规则的过滤器例如限制只能选择 Edge、Face 等SoFCUnifiedSelectionsrc/Gui/Selection/SoFCUnifiedSelection.h/.cpp新 3D 选择入口负责拾取、多命中筛选、预选、高亮、正式选择提交SoFCSelectionsrc/Gui/Selection/SoFCSelection.h/.cpp旧 3D 选择节点兼容旧模型SelectionPickPolicysrc/Gui/Selection/SoFCUnifiedSelection.*多命中优选策略在多个拾取候选里决定最终目标TreeWidgetsrc/Gui/Tree.cppTree View 入口与响应者把树操作映射为选择请求,并订阅选择变化SelectionViewsrc/Gui/Selection/SelectionView.cpp选择列表视图展示当前选择,支持反向驱动选择SelectionObserverPythonsrc/Gui/Selection/SelectionObserverPython.*Python 观察者桥接让 Python 对象订阅选择消息SelectionSingleton::Methods[]src/Gui/Selection/Selection.cppPython API 出口暴露Gui.Selection系列接口从结构上看,这些组件可以进一步分成四层。2.1 入口层组件负责接受外部交互事件:SoFCUnifiedSelectionSoFCSelectionTreeWidgetGui.SelectionPython API2.2 核心状态层组件负责保存和规范化选择状态:SelectionSingletonSelectionInfoSelectionDescriptionSelectionObject2.3 路由与约束层组件负责控制选择语义、过滤规则和解析策略:ResolveModeSelectionGateSelectionFilterSelectionPickPolicyApp::GeoFeature::resolveElement()2.4 响应层组件负责订阅和展示选择变化:ViewProvider::onSelectionChanged()SelectionObserverTreeWidgetSelectionViewPropertyViewPython 观察者这说明 FreeCAD 的选择系统基本符合“输入-状态-消息-响应”四层组织方式。三、关键流程从实现上看,虽然Tree View、3D View、Python Console三类入口的上游事件完全不同,但一旦进入Gui::Selection()主链路,最终都会收敛到同一套状态更新与广播机制。可以先抽象出一张统一主干时序图:Tree/View/Property/PythonSelectionObserverViewProviderSelectionSingleton解析/约束层选择入口Tree/View/Property/PythonSelectionObserverViewProviderSelectionSingleton解析/约束层选择入口