You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
LightWork3D/docs/FreeCAD 二次开发说明.md

1321 lines
42 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# FreeCAD 二次开发笔记(中文)
本文档记录基于当前 FreeCAD 1.1.1 Windows 开发环境开展二次开发时,一些适合先固定下来的设计约定。目标不是一次把系统做满,而是先把“哪些对象能被识别、哪些对象能参与后续能力”这件事做稳。
## 1. 文档范围
当前先记录一个很具体、也很关键的方向:
- 在 FreeCAD 原生对象体系中,如何表示“端子/接线柱”
- 如何让系统先把端子识别出来
- 如何在后期再给端子绑定电气属性
- 如何约束“只有端子才能进行 3D 接线”
这份文档偏架构和对象设计,不要求现在就改源码实现全部功能。
## 2. 结论先说
如果目标是后续做“带电气语义的 3D 接线”,第一版最推荐的约定是:
- 端子使用 `Datum CoordinateSystem`LCS本地坐标系表示
- LCS 原点表示连接点位置
- LCS 某一根轴表示出线方向
- 只有被明确标记为端子的 LCS才允许参与 3D 接线
也就是说,第一版可以直接把:
> 端子 = 被标记过的 LCS
作为系统约定。
## 3. 为什么优先选 LCS而不是普通点或顶点
FreeCAD 里可以拿很多东西表示“一个点”,但如果后面要做真正可识别、可约束、可扩展的端子,`Datum CoordinateSystem` 比普通几何点更合适。
推荐 LCS 的原因:
- 有明确的位置
- 有明确的方向
- 可以附着到面、边、孔位或其他基准对象
- 更适合承载后续业务语义
- 更适合做 3D 接线的起点/终点
不推荐直接使用几何顶点作为端子,主要原因是:
- 拓扑命名不稳定,模型变化后引用容易失效
- 顶点只有位置,没有天然方向
- 顶点不适合作为长期挂载业务属性的对象
- 后期做规则校验和交互约束会比较难
如果只是做纯几何辅助点,`Datum Point` 也能用;但只要目标里包含“出线方向”或“只有端子才能接线”,优先选 LCS。
## 4. 原生 FreeCAD 能做什么,不能直接做什么
FreeCAD 原生能力足够支持下面这些事:
- 创建可定位、可附着的基准对象
- 给对象增加属性
- 建立对象之间的引用关系
- 用 Python/C++ 扩展命令、对象和工作流
但 FreeCAD 原生并没有现成的 ECAD 级对象体系,例如:
- 原生 `Terminal`
- 原生 `Pin`
- 原生 `Net`
- 原生“端子之间自动形成电气连接关系”的对象模型
所以比较现实的路线不是“找到现成电气端子对象”,而是:
1. 先选一个原生对象来承载几何定位
2. 再通过类型或属性,把它定义成“端子”
3. 最后在扩展层实现端子的电气语义和接线规则
## 5. 端子的识别策略
### 5.1 推荐的过渡方案
第一阶段最实用的做法是:
- 底层对象类型:`Datum CoordinateSystem`
- 业务标记属性:`Role = "Terminal"`
这样后续任何工具在扫描文档时,都可以用统一规则识别端子:
1. 对象必须是 LCS
2. 对象必须带有 `Role`
3. `Role` 的值必须是 `"Terminal"`
这个方案的好处是:
- 不需要一开始就引入新的底层对象类型
- 不需要修改 FreeCAD 原生数据结构
- 对后续升级成自定义对象也兼容
### 5.2 后续更干净的方案
如果后面需求稳定了,可以把过渡方案升级成真正的自定义对象类型,例如:
- `MyElectrical::Terminal`
到那时,工具就不必依赖字符串属性判断,而可以直接按对象类型判断。这个方案更干净,但适合在第一版规则已经验证稳定之后再做。
## 6. 推荐的最小属性集合
如果第一版就要为后续电气扩展留好口子,建议端子对象至少预留下面这些属性:
- `Role`:固定写 `"Terminal"`
- `TerminalId`:端子唯一标识
- `TerminalName`:端子显示名
- `CanWire`:是否允许参与 3D 接线
- `DirectionAxis`:约定使用哪根局部轴作为默认出线方向
如果准备继续往电气语义扩展,可以继续补:
- `NetName`
- `VoltageClass`
- `CurrentLimit`
- `SignalType`
- `ConnectorId`
- `GroupName`
第一版不一定要全部做完,但建议命名先统一,不然后面会很难兼容旧数据。
## 7. 推荐的对象分层
为了让系统后面好扩展,建议把端子拆成两层理解。
### 7.1 几何层
由 FreeCAD 原生对象负责:
- 位置
- 朝向
- 附着关系
- 在三维场景中的显示
这里推荐继续使用 LCS。
### 7.2 电气层
由二次开发逻辑负责:
- 端子编号
- 网络名
- 电压等级
- 电流等级
- 信号类型
- 可连接规则
- 当前连接到了哪些对象
这样做的好处是,几何层保持稳定,电气层可以渐进增强。
## 8. “只有端子才能 3D 接线”的实现约定
如果要做 3D 接线,建议从第一版开始就把选择规则收紧,而不是允许任意点都参与接线。
推荐规则:
- 3D 接线命令的起点必须是端子
- 3D 接线命令的终点必须是端子
- 普通顶点、普通草图点、普通边端点不允许直接接线
也就是:
> 接线工具只接受“被识别为 Terminal 的 LCS 对象”
这样做的收益很直接:
- 交互更稳定
- 业务语义更清楚
- 后期规则校验更容易加
- 不会把“几何上的点”和“电气上的连接点”混在一起
## 9. 推荐的第一版判断规则
后续插件或命令在判断一个对象能不能参与 3D 接线时,可以采用下面这组规则:
1. 对象类型是 `Datum CoordinateSystem`
2. 对象具有 `Role` 属性
3. `Role == "Terminal"`
4. `CanWire == true`(如果启用这个属性)
如果以上条件不满足,就不允许作为接线端点。
这套规则比“靠对象名称前缀判断”稳得多。名称前缀可以保留为辅助约定,但不建议把它作为唯一依据。
## 10. 是否需要专门的端子分组
可以有,但建议把它当辅助信息,不要当唯一判定条件。
例如可以建立一个组:
- `ElectricalTerminals`
然后把所有端子对象放进去。这样做有两个好处:
- 方便用户浏览和管理
- 方便批量检查端子
但真正的业务判断,仍建议以“对象类型 + 属性标记”为准。
## 11. 一个适合落地的第一版约定
如果现在就要开始做,并且希望后期演进成本低,建议我们先统一以下约定:
- 所有端子都使用 LCS 创建
- 所有端子都必须带 `Role = "Terminal"`
- 所有端子都应具有稳定的 `TerminalId`
- 如果对象没有被标记为端子,就不能参与 3D 接线
- 接线工具只能选择端子对象作为端点
这一版先解决“识别”和“约束”的问题,后面再逐步加:
- 电压/电流/信号属性
- 网络关系
- 自动路由
- 连接合法性检查
- 端子表和报表输出
## 12. 后续扩展建议
等第一版跑顺之后,可以继续往下面这些方向扩展:
- 自定义对象类型:把属性式端子升级为真正的 `Terminal` 对象
- 自定义视图提供器:让端子在三维视图中更醒目
- 连接关系对象:显式记录“哪个端子连到哪个端子”
- 规则检查器:检查悬空端子、重复连接、等级不匹配
- 线束/线缆语义:把单根连接升级为可管理的线束对象
## 13. 当前推荐结论
在当前 FreeCAD 二次开发阶段,最适合作为“电气端子”的原生对象是:
- `Datum CoordinateSystem`LCS
最适合作为第一版识别规则的是:
- `LCS + Role="Terminal"`
最适合作为 3D 接线准入规则的是:
- 只有被标记为端子的 LCS 才能作为接线端点
这个方案不要求现在就修改 FreeCAD 原生源码,但已经足够支撑后续做一个结构清晰、规则明确的电气端子与 3D 接线系统。
### 13.1 FCStd 设备模板作为正式资产
对于从厂家、网络或已有资源库拿到的 `.step`、`.stp`、`.ste` 模型,不建议把它们直接当作最终电气设备模板。原因是 STEP 系列文件主要表达几何,不能可靠保存 FreeCAD LCS、动态属性和二次开发语义。
当前推荐的正式资产流程是:
```text
STEP / STP / STE 几何模型
-> FreeCAD 模板制作
-> 添加 LCS 端子
-> 写入端子槽位语义
-> 保存为 FCStd 设备模板
```
这样得到的 `.FCStd` 才是后续工程复用、交付给其他人、放入设备资源库的主文件。
模板端子是“通用槽位”,不是“某个工程中的端子实例”。例如电流互感器模板中可以保存:
```text
Terminal_P1
Terminal_P2
```
这些对象表示模型上 P1/P2 的真实接线位置和方向,但不保存某个工程里的 `terminal_uuid`。项目导入时FreeCADExchange 再根据 `2d_to_3d.json` 把工程端子 UUID 绑定到模板槽位上。
### 13.2 A 方案下 zwl 与 FreeCAD 的文件职责
A 方案要求 `D:\code\zwl` 支持 `.FCStd` 作为设备 3D 资产格式,但不要求 zwl 解析 `.FCStd` 文件。
职责划分如下:
- zwl/QET选择 `.FCStd`、复制到工程资产目录、保存到 `device_3d_asset`、导出 `resolved_model_path`
- FreeCADExchange导入 `.FCStd`、扫描模板内 `Role="Terminal"` 的 LCS、创建工程端子对象、保存 3D 装配和手动连线。
也就是说,`.FCStd` 内部的端子、LCS、动态属性和装配结构都是 FreeCAD 侧语义zwl 只把 `.FCStd` 当作一种可流转的 3D 资源文件。
详细设计见:
- `docs/superpowers/specs/2026-05-20-freecad-fcstd-asset-flow-design.md`
### 13.3 面向 CAD 人员的模板制作交互
Python 控制台方式只作为开发验证手段,不能作为正式 CAD 工作流。A 方案第一版需要补一个 FreeCAD 任务面板,把端子制作流程封装成按钮和输入框。
推荐交互:
1. 用户打开 STEP / STP / STE 模型。
2. 打开“设备模板端子制作”面板。
3. 在端子名输入框填写 `P1`
4. 用鼠标选中模型上的端子位置。
5. 点击“添加端子”。
6. 重复添加 `P2` 等端子。
7. 点击“校验端子”。
8. 点击“保存为 FCStd”。
面板背后仍然使用 LCS 作为端子对象,仍然写入:
- `Role = "Terminal"`
- `CanWire = true`
- `QetTemplateSlotName`
- `QetTerminalLabel`
- `QetTerminalType`
这样既保持当前技术路线稳定,又让非开发人员可以直接使用。
## 14. 电气柜与设备装配
除了端子与接线,电气柜场景通常还会遇到另一个基础问题:
- 是否可以把多个设备模型装配到一个柜体模型中
对于这一点,当前 FreeCAD 原生能力是支持的。
### 14.1 原生是否支持多设备装配
支持。
当前 FreeCAD 已经提供原生 `Assembly` 工作台,可以用于:
- 创建装配
- 插入外部或已有零部件
- 建立装配关系
- 求解装配位置
- 管理装配层级
对于“电气柜 + 多个设备”的典型场景,原生 FreeCAD 可以承担:
- 柜体作为总装配基准
- 断路器、继电器、电源、端子排等设备作为子部件
- 将多个设备模型放入同一个柜体模型
- 调整设备的位置和姿态
- 维护装配结构
也就是说从几何装配角度看FreeCAD 原生已经具备“把很多设备装进一个电气柜”的能力。
### 14.2 原生能力更适合解决什么问题
在电气柜场景里,原生装配更适合负责下面这些内容:
- 设备模型复用
- 设备插入与摆放
- 设备姿态与空间位置控制
- 装配层级组织
- 柜体与设备之间的几何关系
它比较像一个可靠的三维装配底座。
### 14.3 原生不会直接替代的电气语义
虽然原生装配能做几何放置,但它并不会天然提供完整的电气柜业务语义,例如:
- 自动识别“这是导轨设备”或“这是面板设备”
- 自动判断安装孔位、导轨、槽位是否符合某类电器安装规则
- 自动完成电气设备布置规则
- 自动识别设备的电气连接关系
- 自动限制哪些点能接线、哪些设备能互连
因此,比较现实的路线依然是:
1. 使用原生装配能力处理几何放置
2. 在二次开发层补充设备、电气、安装规则等业务语义
### 14.4 推荐的装配思路
如果目标是后续做“电气柜设备布置 + 端子识别 + 3D 接线”,建议按下面这条路线走:
1. 柜体模型作为总装配对象
2. 各类设备模型作为子部件插入装配
3. 使用 `App::Link` 或装配中的链接对象复用设备模型
4. 使用 `Placement`、LCS 和装配约束管理设备位置
5. 在后续扩展层再给设备补安装语义和电气语义
这样做的优点是:
- 几何装配先跑通
- 设备复用能力强
- 不会把电气逻辑硬塞进几何底层
- 后续接线与设备属性扩展更容易接上
### 14.5 为什么推荐继续使用 LCS 作为安装基准
在装配场景里LCS 依然很重要。
推荐做法是:
- 柜体上定义安装基准 LCS
- 设备上定义安装基准 LCS
- 装配时优先让“设备安装 LCS”对齐“柜体安装 LCS”
这样做有几个明显好处:
- 设备摆放规则更清楚
- 安装方向更明确
- 后续端子、接线点、设备朝向可以共用同一套基准体系
- 从“设备装配”过渡到“设备接线”会自然很多
### 14.6 当前推荐结论
对于“电气柜中放置多个设备模型”这个需求,当前推荐结论如下:
- 原生 FreeCAD 可以做多设备装配
- 原生 `Assembly` 工作台足够承担柜体与设备的几何装配任务
- 原生装配适合作为电气柜二次开发的三维底座
- 电气设备类型、安装规则、接线规则仍建议放在二次开发层实现
如果后续要把这条路线继续收敛成统一约定,可以先把系统拆成下面两层:
- 装配层:负责柜体、设备、位置、方向、层级
- 电气层:负责设备属性、端子属性、连线规则、网络关系
这样从模型装配到电气语义的演进路径会比较清楚,也更适合逐步落地。
## 15. 2D 与 3D 协同的总体思路
当前系统并不是“从零做一个既懂 2D 又懂 3D 的全新电气 CAD”而是更适合拆成两个真相源
- 2D 软件(`D:\project\MingTuCAD\qelectrotech`)负责电气语义
- 3D 软件(`D:\project\LightWork3D\FreeCAD`)负责三维装配、空间位置与走线表现
推荐原则:
- 2D 继续作为设备、符号、端子、网络、导线关系的主数据源
- 3D 不重复发明电气语义,而是读取并绑定 2D 已有语义
- 2D 与 3D 通过稳定标识进行绑定,而不是靠显示名称猜测
一句话概括:
> 2D 是“电气真相源”3D 是“空间真相源”。
## 16. 为什么优先走数据库绑定,而不是重新解析 XML
从 2D 代码看,项目文件虽然是 XML 体系,但 2D/3D 协同所需的数据已经被整理到本地数据库和专用仓储层。
已经存在的关键入口包括:
- 项目运行库表结构
[projectlocaldatabase.cpp](D:/project/MingTuCAD/qelectrotech/sources/dataBase/projectlocaldatabase.cpp:364)
- 3D 绑定仓储
[ThreeDAssemblyRepository.cpp](D:/project/MingTuCAD/qelectrotech/sources/ThreeD/ThreeDAssemblyRepository.cpp:300)
- 3D 绑定同步服务
[ThreeDAssemblySyncService.cpp](D:/project/MingTuCAD/qelectrotech/sources/ThreeD/ThreeDAssemblySyncService.cpp:107)
因此当前推荐:
- 不把 XML 作为首选交换接口
- 优先复用 2D 现有 SQLite/运行库表和同步服务
这样做的好处:
- 标识字段已经存在
- 2D 端子与导线关系已经能稳定落库
- 3D 侧只需要读写明确的绑定表,不必重新推断图纸语义
## 17. 2D 侧已经具备的关键数据
### 17.1 设备级数据
设备元数据里已经存在与 3D 相关的字段:
- `parts_3d`
- `layout_2d_diagram`
对应代码:
- [DeviceMeta.h](D:/project/MingTuCAD/qelectrotech/sources/DeviceManager/DeviceMeta.h:32)
- [deviceeditor.cpp](D:/project/MingTuCAD/qelectrotech/sources/DeviceManager/deviceeditor.cpp:1484)
这意味着 2D 侧已经能表达:
- 设备使用哪个 3D 资产
- 设备关联哪个 2D 布局图
### 17.2 设备端子数据
设备端子并不是简单文本,而是已经有结构化表:
- `device_circuit_terminals`
表中已存字段包括:
- `tag`
- `direction`
- `type_id`
- `max_wire`
- `mnemonic`
- `usage`
- `max_sec`
- `min_sec`
对应代码:
- [deviceterminaleditor.cpp](D:/project/MingTuCAD/qelectrotech/sources/InsertFunction/deviceterminaleditor.cpp:94)
- [deviceterminaleditor.cpp](D:/project/MingTuCAD/qelectrotech/sources/InsertFunction/deviceterminaleditor.cpp:214)
这说明 2D 侧已经有“端子属性”基础,不需要在 3D 里重新手填。
### 17.3 2D/3D 绑定表
项目本地库已经定义了下面这些表:
- `project_3d_scene_instance`
- `project_3d_space_object`
- `project_2d3d_link`
- `project_2d3d_symbol_binding`
- `project_2d3d_terminal_binding`
- `start_end_terminal_matches`
对应代码:
- [projectlocaldatabase.cpp](D:/project/MingTuCAD/qelectrotech/sources/dataBase/projectlocaldatabase.cpp:364)
- [projectlocaldatabase.cpp](D:/project/MingTuCAD/qelectrotech/sources/dataBase/projectlocaldatabase.cpp:411)
- [projectlocaldatabase.cpp](D:/project/MingTuCAD/qelectrotech/sources/dataBase/projectlocaldatabase.cpp:425)
- [projectlocaldatabase.cpp](D:/project/MingTuCAD/qelectrotech/sources/dataBase/projectlocaldatabase.cpp:446)
- [projectlocaldatabase.cpp](D:/project/MingTuCAD/qelectrotech/sources/dataBase/projectlocaldatabase.cpp:469)
这些表已经非常接近最终所需接口。
### 17.4 2D 已经在生成端子绑定
`ThreeDAssemblySyncService` 已经会为单个 2D 元件生成端子绑定数据,核心字段包括:
- `elementUuid`
- `terminalUuid`
- `symbolTerminal`
- `instanceId`
- `terminalKey`
- `connectionPointKey`
- `wireLabel`
- `netId`
对应代码:
- [ThreeDAssemblySyncService.cpp](D:/project/MingTuCAD/qelectrotech/sources/ThreeD/ThreeDAssemblySyncService.cpp:125)
- [ThreeDAssemblySyncService.cpp](D:/project/MingTuCAD/qelectrotech/sources/ThreeD/ThreeDAssemblySyncService.cpp:159)
这意味着“2D 端子 -> 3D 端子”的映射字段体系,其实已经具备。
## 18. 推荐的数据传递方案
### 18.1 设备级绑定
设备级建议用 `project_2d3d_symbol_binding` 表表达:
- `project_uuid`
- `source_diagram_uuid`
- `scene_diagram_uuid`
- `element_uuid`
- `instance_id`
- `device_id`
- `display_tag`
- `asset_uri`
- `host_binding_mode`
- `host_object_id`
- `host_object_type`
- `extra_json`
这一层解决的问题是:
- 2D 中某个设备实例,对应 3D 中哪个设备实例
- 这个设备实例使用哪个 3D 资源
- 它被装配到哪个宿主对象上
### 18.2 端子级绑定
端子级建议用 `project_2d3d_terminal_binding` 表表达:
- `project_uuid`
- `source_diagram_uuid`
- `scene_diagram_uuid`
- `element_uuid`
- `terminal_uuid`
- `binding_key`
- `symbol_terminal`
- `instance_id`
- `terminal_key`
- `connection_point_key`
- `wire_label`
- `net_id`
- `conductor_uuid`
- `extra_json`
这一层解决的问题是:
- 2D 里的哪个端子,对应 3D 里的哪个连接点
- 当前端子属于哪个设备实例
- 当前端子所在网络、线号、导线标识是什么
### 18.3 设备与端子的主键约定
建议后续统一按下面这组键思考:
- 设备主键:`element_uuid + instance_id`
- 端子主键:`terminal_uuid` 或 `binding_key`
- 3D 连接点主键:`connection_point_key`
不要依赖:
- 显示名称
- 标签文字
- 树节点名称
## 19. 推荐的数据流
### 19.1 从 2D 到 3D
推荐主流程:
1. 2D 编辑设备、端子、导线和网络
2. 2D 把设备级绑定写入 `project_2d3d_symbol_binding`
3. 2D 把端子级绑定写入 `project_2d3d_terminal_binding`
4. FreeCAD 插件读取这些表
5. FreeCAD 创建/更新 3D 设备实例、3D 端子对象和连接点绑定
### 19.2 从 3D 回写到 2D
推荐回写内容:
1. 设备实例的三维位姿写入 `project_3d_scene_instance`
2. 宿主绑定信息写入 `project_2d3d_symbol_binding`
3. 端子实际落到哪个连接点,写入 `connection_point_key`
4. 后续若 3D 连线参与校验,可回写线束/路径附加信息到 `extra_json`
## 20. FreeCAD 侧的扩展基础
FreeCAD 这边不建议第一版就改 C++ 内核,先走 Python 工作台/插件路线更稳。
当前可直接复用的基础有:
- `FeaturePython`:可创建自定义对象
[FeaturePython.h](D:/project/LightWork3D/FreeCAD/src/App/FeaturePython.h:178)
- `App::LocalCoordinateSystem / Part::LocalCoordinateSystem`:适合作为连接点
[Datums.h](D:/project/LightWork3D/FreeCAD/src/App/Datums.h:46)
- 动态属性机制:可加 `PropertyString / PropertyBool / PropertyLink / PropertyLinkList`
参考 [ifctree.py](D:/project/LightWork3D/FreeCAD/src/Mod/BIM/utils/ifctree.py:100)
另外Assembly 对 LCS/Datum 本身也是认可的:
- [JointObject.py](D:/project/LightWork3D/FreeCAD/src/Mod/Assembly/JointObject.py:1449)
## 21. 3D 端子的推荐定义
当前推荐把 3D 端子定义成:
> 一个带 LCS 的语义对象,而不是普通点
建议最小结构:
- 一个 `FeaturePython` 端子对象,作为语义容器
- 一个 `Part::LocalCoordinateSystem`,作为几何连接点
- 一组绑定属性,映射到 2D 端子语义
### 21.1 端子对象最小属性建议
建议至少包含:
- `ProjectUuid`
- `SourceDiagramUuid`
- `ElementUuid`
- `TerminalUuid`
- `InstanceId`
- `TerminalKey`
- `ConnectionPointKey`
- `SymbolTerminal`
- `WireLabel`
- `NetId`
- `CanWire`
- `Status`
- `OwnerDevice``PropertyLink`
这样它就不再是“一个点”,而是一个:
- 有几何位置
- 有 2D 语义映射
- 能参与接线规则
的端子对象。
### 21.2 LCS 的职责
LCS 只负责:
- 连接点位置
- 连接点姿态
- 后续装配/连线的几何基准
LCS 不负责:
- 保存全部电气语义
- 保存网络拓扑
电气语义仍建议放在端子对象本身的属性里。
### 21.3 模板端子和工程端子的区别
后续 FreeCAD 二次开发中需要区分两类端子:
1. 模板端子
2. 工程端子
模板端子存在于设备模板 `.FCStd` 中,职责是描述“这个设备模型上哪里可以接线”。它只保存跨工程稳定的信息:
- `Role = "Terminal"`
- `CanWire = true`
- `QetTemplateSlotName`
- `QetTerminalLabel`
- `QetTerminalType`
- LCS 的位置和方向
模板端子不保存:
- `project_uuid`
- `element_uuid`
- `terminal_uuid`
- `instance_id`
- 数据库绑定字段
工程端子存在于具体项目的 `scene.FCStd` 中,职责是描述“当前工程里的哪个 2D 端子绑定到了哪个 3D 连接点”。它由 FreeCADExchange 根据 `2d_to_3d.json` 生成或更新,才会保存:
- `QetProjectUuid`
- `QetElementUuid`
- `QetTerminalUuid`
- `QetInstanceId`
- `CanWire`
因此FCStd 设备模板是可复用的电气几何资产,项目场景 FCStd 是某个工程的装配和接线结果。不要把两者的职责混在同一个对象里。
### 21.4 模板制作工具方向
为了让普通 STEP 模型变成可复用电气模板,建议在 `FreeCADExchange` Python 层增加模板制作工具,而不是修改 FreeCAD C++ 内核。
第一版工具目标:
1. 导入 STEP / STP / STE 几何模型。
2. 用户选择模型上的接线位置。
3. 输入端子槽位名,例如 `P1`、`P2`、`A1`、`A2`。
4. 自动创建 LCS。
5. 自动写入模板端子属性。
6. 保存为 `.FCStd`
第一版建议新增模块:
```text
src/Mod/FreeCADExchange/TemplateAuthoring.py
```
该模块只负责设备模板制作,不负责项目导入、手动连线或数据库回写。
## 22. 3D 端子二开实施步骤
### 阶段 1读绑定不做自动路由
先完成:
1. 在 FreeCAD 侧做一个独立工作台或插件
2. 读取 `project_2d3d_symbol_binding`
3. 读取 `project_2d3d_terminal_binding`
4. 为每个 2D 设备创建一个 3D 设备实例
5. 为每个 2D 端子创建一个 3D 端子对象 + LCS
这一阶段先验证:
- 2D 设备能不能在 3D 找到对应设备
- 2D 端子能不能在 3D 找到对应连接点
补充说明:
- 项目端子创建前,优先读取 FCStd 设备模板中的 `Role="Terminal"` LCS。
- 如果模板没有端子语义,才使用 sidecar 或 bbox fallback。
- 长期目标是让常用设备都具备 FCStd 模板端子,而不是长期依赖 fallback。
### 阶段 2让 3D 连线只认端子对象
下一步完成:
1. 3D 连线命令只允许选择端子对象
2. 端子对象必须带 `CanWire == true`
3. 连线命令优先读取:
- `TerminalUuid`
- `TerminalKey`
- `ConnectionPointKey`
- `NetId`
这一阶段的目标是:
- 把“3D 接线点”升级成“可识别的电气端子”
### 阶段 3回写绑定状态
再往后完成:
1. 将设备位姿写回 `project_3d_scene_instance`
2. 将端子落点写回 `project_2d3d_terminal_binding.connection_point_key`
3. 将宿主对象关系写回 `project_2d3d_symbol_binding.host_object_id / host_object_type`
这一阶段的目标是:
- 让 2D 和 3D 之间形成真正稳定的双向绑定
### 阶段 4再考虑自动路由
只有在前 3 个阶段稳定之后,才建议继续做:
- 沿机柜/线槽的自动路径
- 端子与线槽的吸附规则
- 空间避障
- 线束与报表联动
## 23. 当前推荐结论
对于当前这两套代码,最推荐的路线不是:
- 在 FreeCAD 里重新造一套电气数据库
- 或在 3D 端重复维护设备和端子语义
而是:
1. 继续让 QElectroTech 负责设备、端子、导线、网络等电气语义
2. 继续使用其现有 `project_2d3d_*` 表和 `ThreeDAssembly*` 服务作为绑定入口
3. 让 FreeCAD 读取绑定表,把端子做成“带 LCS 的语义对象”
4. 让 3D 接线、装配、空间走线全部围绕这些绑定对象展开
一句话总结:
> 3D 端子不应只是几何点,而应是“有位置的 2D 电气语义映射节点”。
## 24. 阶段 A统一标识和数据口径
阶段 A 的目标不是先把 2D/3D 联动界面做出来,而是先把“双方到底在说谁”这件事说清楚。
如果这一阶段没有先定死,后面最容易出现的问题是:
- 2D 里的设备实例和 3D 里的设备实例靠显示名硬猜
- 端子编号改一次,绑定关系全部漂移
- 同一个对象在数据库、3D 文档、界面显示里分别有三套名字
- 回写数据时不知道应该更新哪一条记录
所以阶段 A 的交付物应该是:
- 一份统一字段清单
- 一份主键和唯一性约定
- 一份“谁是主数据源”的边界说明
- 一份最小同步记录样例
### 24.1 阶段 A 的范围
阶段 A 先不做下面这些事:
- 不做 FreeCAD 端子对象代码
- 不做自动装配
- 不做自动路由
- 不做 2D/3D 双向实时联动
阶段 A 只回答四个问题:
1. 设备怎么唯一识别
2. 端子怎么唯一识别
3. 2D 和 3D 之间靠哪些字段绑定
4. 哪些字段由 2D 负责,哪些字段由 3D 负责
### 24.2 总体原则
建议在阶段 A 固定下面四条原则:
1. 电气语义真相源在 2D
2. 空间位姿真相源在 3D
3. 绑定靠稳定 ID不靠显示名称
4. 一个字段只定义一个主责任方,避免双写冲突
也就是说:
- 2D 负责定义“这是什么设备、这是什么端子、它属于哪个网络”
- 3D 负责定义“这个设备装在哪里、这个端子在空间里具体在哪”
### 24.3 阶段 A 推荐统一字段
建议第一版统一下面这组字段名。后续无论走 SQLite、JSON、还是接口对象都尽量保持同名。
#### 24.3.1 项目级字段
- `project_uuid`
- `project_name`
- `source_diagram_uuid`
- `scene_diagram_uuid`
含义建议:
- `project_uuid`:整个项目唯一标识
- `project_name`:项目显示名称,不参与绑定主键
- `source_diagram_uuid`2D 原理图/来源图纸唯一标识
- `scene_diagram_uuid`3D 场景或 3D 视图唯一标识
#### 24.3.2 设备级字段
- `element_uuid`
- `instance_id`
- `device_id`
- `display_tag`
- `asset_uri`
- `layout_2d_diagram`
- `host_object_id`
- `host_object_type`
含义建议:
- `element_uuid`2D 元件实例唯一标识
- `instance_id`3D 设备实例唯一标识
- `device_id`:设备类型或库元件标识
- `display_tag`:界面显示标签,例如 `QF1`
- `asset_uri`3D 资产路径或 3D 资源定位符
- `layout_2d_diagram`2D 布局图标识
- `host_object_id`3D 中的宿主对象标识,例如某导轨、安装板、柜体分区
- `host_object_type`:宿主对象类型,例如 `Rail`、`Panel`、`Cabinet`
#### 24.3.3 端子级字段
- `terminal_uuid`
- `terminal_key`
- `symbol_terminal`
- `connection_point_key`
- `terminal_direction`
- `terminal_type`
- `wire_label`
- `net_id`
含义建议:
- `terminal_uuid`2D 端子实例唯一标识
- `terminal_key`:设备内部端子业务键,例如 `QF1:13`
- `symbol_terminal`:图纸显示端子号,例如 `13`
- `connection_point_key`3D 连接点键,例如设备模型中的 `cp_13`
- `terminal_direction`:端子方向/进出方向语义
- `terminal_type`:端子类型语义
- `wire_label`:当前线号/导线标签
- `net_id`:网络唯一标识
### 24.4 推荐主键和唯一性约定
建议在阶段 A 先把“哪个字段必须稳定、哪个字段只是显示用途”彻底分开。
#### 24.4.1 只做显示用途,不可作为绑定主键
下面这些字段不建议直接作为绑定主键:
- `project_name`
- `display_tag`
- `symbol_terminal`
- 任何中文名称
- 任何模型树显示名称
原因很简单:
- 这些字段可能会被人工修改
- 它们有显示意义,但不一定有全局唯一性
- 一旦改名,绑定关系会断
#### 24.4.2 推荐的主键约定
建议按下面规则理解:
- 设备绑定主键:`project_uuid + element_uuid`
- 3D 设备实例主键:`project_uuid + instance_id`
- 端子绑定主键:`project_uuid + terminal_uuid`
- 3D 连接点主键:`project_uuid + instance_id + connection_point_key`
如果需要业务可读键,可以再保留:
- `terminal_key = <display_tag>:<symbol_terminal>`
但这个可读键建议作为辅助字段,不代替 `terminal_uuid`
### 24.5 一对一 / 一对多关系约定
阶段 A 建议先把关系收紧,不要一开始就支持太多复杂映射。
第一版建议:
- 一个 `element_uuid` 对应一个 3D `instance_id`
- 一个 `terminal_uuid` 对应一个 3D 端子对象
- 一个 3D 端子对象对应一个 `connection_point_key`
这意味着第一版先按最稳的模型来:
- 一个 2D 设备实例,只落成一个 3D 设备实例
- 一个 2D 端子,只绑定一个 3D 连接点
后续如果要支持:
- 一符号多模型
- 一端子多连接点
- 跳线桥接
再在阶段 B/C 之后扩展,不建议在阶段 A 就放开。
### 24.6 2D 与 3D 的职责边界
为了避免双向覆盖,建议把字段责任按下面方式固定。
#### 24.6.1 由 2D 负责的字段
- `project_uuid`
- `source_diagram_uuid`
- `element_uuid`
- `device_id`
- `display_tag`
- `terminal_uuid`
- `terminal_key`
- `symbol_terminal`
- `wire_label`
- `net_id`
- 端子类型、端子方向、电气属性
也就是说2D 负责:
- 设备是谁
- 端子是谁
- 设备和端子在电气上是什么意思
- 它们属于哪个网络
#### 24.6.2 由 3D 负责的字段
- `instance_id` 对应的空间位姿
- 宿主对象落点
- 3D 端子具体几何位置
- `connection_point_key` 对应的实际连接点落位
- 柜内装配关系
也就是说3D 负责:
- 它在柜体中装在哪里
- 它挂在哪条导轨、哪块安装板上
- 端子在三维空间里具体位于哪里
#### 24.6.3 需要双方共同遵守但不能双写冲突的字段
- `asset_uri`
- `instance_id`
- `connection_point_key`
- `host_object_id`
- `host_object_type`
建议策略:
- 第一版由 2D 提供 `asset_uri`
- 第一版由 3D 回写 `instance_id / connection_point_key / host_object_*`
- 如果 2D 后续能预先规划 3D 实例 ID也要保证生成规则稳定且不会和 3D 重复造号
### 24.7 阶段 A 的推荐记录样例
下面给出两个“建议形态”的样例,目的是帮助阶段 A 对齐字段语义,不代表必须先改成 JSON 存储。
#### 24.7.1 设备级绑定样例
```json
{
"project_uuid": "proj-001",
"source_diagram_uuid": "sch-001",
"element_uuid": "elem-a1b2c3",
"instance_id": "inst-qf1",
"device_id": "MCCB_1P_OF",
"display_tag": "QF1",
"asset_uri": "file:///assets/MCCB_1P_OF.step",
"layout_2d_diagram": "layout-main",
"host_object_id": "rail-01",
"host_object_type": "Rail"
}
```
#### 24.7.2 端子级绑定样例
```json
{
"project_uuid": "proj-001",
"source_diagram_uuid": "sch-001",
"element_uuid": "elem-a1b2c3",
"terminal_uuid": "term-13",
"instance_id": "inst-qf1",
"terminal_key": "QF1:13",
"symbol_terminal": "13",
"connection_point_key": "cp_13",
"terminal_direction": "bidirectional",
"terminal_type": "power",
"wire_label": "W-001",
"net_id": "net-l1"
}
```
### 24.8 阶段 A 与现有 2D 代码的对应关系
当前 2D 代码里,阶段 A 所需的大部分基础其实已经存在。
#### 24.8.1 设备 3D 资源字段
已有:
- `parts_3d`
- `layout_2d_diagram`
参考:
- [deviceeditor.cpp](D:/project/MingTuCAD/qelectrotech/sources/DeviceManager/deviceeditor.cpp:1484)
- [DeviceMeta.h](D:/project/MingTuCAD/qelectrotech/sources/DeviceManager/DeviceMeta.h:32)
#### 24.8.2 端子属性字段
已有:
- `tag`
- `direction`
- `type_id`
- `max_wire`
- `mnemonic`
- `usage`
- `max_sec`
- `min_sec`
参考:
- [deviceterminaleditor.cpp](D:/project/MingTuCAD/qelectrotech/sources/InsertFunction/deviceterminaleditor.cpp:94)
- [deviceterminaleditor.cpp](D:/project/MingTuCAD/qelectrotech/sources/InsertFunction/deviceterminaleditor.cpp:214)
#### 24.8.3 2D/3D 绑定表
已有:
- `project_3d_scene_instance`
- `project_3d_space_object`
- `project_2d3d_link`
- `project_2d3d_symbol_binding`
- `project_2d3d_terminal_binding`
- `start_end_terminal_matches`
参考:
- [projectlocaldatabase.cpp](D:/project/MingTuCAD/qelectrotech/sources/dataBase/projectlocaldatabase.cpp:364)
- [projectlocaldatabase.cpp](D:/project/MingTuCAD/qelectrotech/sources/dataBase/projectlocaldatabase.cpp:411)
- [projectlocaldatabase.cpp](D:/project/MingTuCAD/qelectrotech/sources/dataBase/projectlocaldatabase.cpp:425)
- [projectlocaldatabase.cpp](D:/project/MingTuCAD/qelectrotech/sources/dataBase/projectlocaldatabase.cpp:446)
- [projectlocaldatabase.cpp](D:/project/MingTuCAD/qelectrotech/sources/dataBase/projectlocaldatabase.cpp:469)
所以阶段 A 的重点并不是“重新设计一套完全新的数据结构”,而是:
- 统一字段命名
- 统一主键规则
- 明确哪些现有字段可以直接沿用
- 明确哪些字段需要补充或规范化
### 24.9 阶段 A 的文档交付清单
建议阶段 A 至少完成下面这些文档性交付物:
1. 《字段对照表》
2. 《主键与唯一性约定》
3. 《2D/3D 职责边界说明》
4. 《设备级绑定样例》
5. 《端子级绑定样例》
只有这几样都齐了,后面的代码实现才不容易边写边改字段语义。
### 24.10 阶段 A 的验收标准
阶段 A 完成的判断标准建议定成下面这些:
1. 团队内部能明确回答:
- 设备靠哪个字段唯一识别
- 端子靠哪个字段唯一识别
- 哪些字段只是显示用途
2. 能给出一条设备绑定记录样例
3. 能给出一条端子绑定记录样例
4. 能明确指出每个关键字段由 2D 还是 3D 负责
5. 不需要看代码猜字段含义,单看文档就能开始阶段 B
## 25. 2D -> 3D 字段对照表
下面这张表的目标不是一次覆盖所有未来字段,而是先把阶段 A 真正要对齐的核心字段固定下来。
建议使用原则:
- 左侧 `2D 字段` 表示 QElectroTech 侧已有字段或推荐输出字段
- 中间 `3D 字段` 表示 FreeCAD 侧对象属性、绑定对象属性或同步结构中的字段名
- `责任方` 表示该字段的主维护方
- `备注` 用于说明是否直接映射、是否仅显示、是否后续回写
### 25.1 项目级字段对照
| 2D 字段 | 3D 字段 | 责任方 | 备注 |
| --- | --- | --- | --- |
| `project_uuid` | `ProjectUuid` | 2D | 项目全局主键3D 只读并保存,不自行生成 |
| `project_name` | `ProjectName` | 2D | 仅显示用途,不作为绑定主键 |
| `source_diagram_uuid` | `SourceDiagramUuid` | 2D | 2D 来源图纸主键3D 端用于追溯来源 |
| `scene_diagram_uuid` | `SceneDiagramUuid` | 3D | 3D 场景主键,若 2D 侧已有对应字段可同步保存 |
### 25.2 设备级字段对照
| 2D 字段 | 3D 字段 | 责任方 | 备注 |
| --- | --- | --- | --- |
| `element_uuid` | `ElementUuid` | 2D | 2D 设备实例主键,设备绑定首要识别字段 |
| `instance_id` | `InstanceId` | 3D | 3D 设备实例主键,建议由 3D 创建后回写 2D |
| `device_id` | `DeviceId` | 2D | 设备类型标识,可映射到元件库类型 |
| `display_tag` | `DisplayTag` | 2D | 显示名,例如 `QF1`,不可替代主键 |
| `parts_3d` / `asset_uri` | `AssetUri` | 2D | 3D 资产路径来源字段,第一版由 2D 提供 |
| `layout_2d_diagram` | `Layout2DDiagram` | 2D | 2D 布局来源标识3D 仅保存引用 |
| `host_object_id` | `HostObjectId` | 3D | 设备在 3D 中挂载到哪个宿主对象,例如导轨或安装板 |
| `host_object_type` | `HostObjectType` | 3D | 宿主对象类型,例如 `Rail`、`Panel`、`Cabinet` |
| `pos_x / pos_y / pos_z`(若 2D 侧后续引入) | `Placement.Base` | 3D | 第一版建议位姿由 3D 主导2D 可只保存回写结果 |
| `rot_x / rot_y / rot_z`(若 2D 侧后续引入) | `Placement.Rotation` | 3D | 同上,作为 3D 位姿结果回写,不建议 2D 首发控制 |
### 25.3 端子级字段对照
| 2D 字段 | 3D 字段 | 责任方 | 备注 |
| --- | --- | --- | --- |
| `terminal_uuid` | `TerminalUuid` | 2D | 2D 端子实例主键,端子绑定核心标识 |
| `terminal_key` | `TerminalKey` | 2D | 业务可读键,例如 `QF1:13`,建议稳定生成 |
| `symbol_terminal` | `SymbolTerminal` | 2D | 图纸显示端子号,例如 `13`,仅显示和辅助检索 |
| `tag` | `TerminalTag` | 2D | 若 2D 端已有端子标签字段,可单独映射保留 |
| `direction` | `TerminalDirection` | 2D | 端子方向语义3D 用于规则判断,不直接决定整体路由 |
| `type_id` | `TerminalTypeId` | 2D | 端子类型枚举主键,便于后续规则扩展 |
| `mnemonic` | `Mnemonic` | 2D | 端子助记符,偏显示和检索用途 |
| `usage` | `Usage` | 2D | 端子用途说明,例如控制、电源、保护 |
| `max_wire` | `MaxWire` | 2D | 最大接线数,后续可用于 3D 规则检查 |
| `max_sec` | `MaxSection` | 2D | 最大线径/截面积,第一版可先只保存不执行校验 |
| `min_sec` | `MinSection` | 2D | 最小线径/截面积,第一版可先只保存不执行校验 |
| `connection_point_key` | `ConnectionPointKey` | 3D | 3D 连接点键,建议由 3D 端确定后回写 2D |
| `wire_label` | `WireLabel` | 2D | 线号/导线标签3D 只读并附着到端子或导线对象 |
| `net_id` | `NetId` | 2D | 网络主键3D 连线对象应直接读取并保存 |
| `can_wire`(若 2D 侧后续显式输出) | `CanWire` | 2D/3D 共同遵守 | 第一版默认可由 3D 端按端子对象类型推导,也可由 2D 显式下发 |
### 25.4 3D 端子与连接点扩展字段
这部分字段不一定在 2D 现有数据库里原生存在,但建议在 3D 侧先固定命名,便于后续回写和扩展。
| 2D 字段 | 3D 字段 | 责任方 | 备注 |
| --- | --- | --- | --- |
| - | `OwnerDevice` | 3D | FreeCAD 端子对象指向所属设备对象的链接属性 |
| - | `LcsName` | 3D | 端子几何基准的 LCS 名称,便于界面调试和对象检索 |
| - | `Status` | 3D | 3D 端子当前状态,例如 `Unbound`、`Bound`、`Connected` |
| `extra_json`(若已有或后续扩展) | `ExtraJson` | 双方约定 | 放临时扩展字段,但不建议承载主键语义 |
### 25.5 表级映射建议
为了让字段对照真正能落到代码里,建议阶段 A 同时固定“字段主要从哪张表来”。
| 2D 表/来源 | 对应 3D 对象/结构 | 责任方 | 备注 |
| --- | --- | --- | --- |
| `project_2d3d_symbol_binding` | 3D 设备实例绑定记录 | 2D 写入3D 读取/回写部分字段 | 设备级绑定主表 |
| `project_2d3d_terminal_binding` | 3D 端子对象绑定记录 | 2D 写入3D 回写 `connection_point_key` 等 | 端子级绑定主表 |
| `project_3d_scene_instance` | 3D 场景实例位姿记录 | 3D | 位姿与宿主落位建议由 3D 主导 |
| `device_circuit_terminals` | 3D 端子语义属性来源 | 2D | 类型、方向、线径、接线数等语义来源 |
| `parts_3d` / `layout_2d_diagram` | 3D 资产入口与布局引用 | 2D | 第一版不建议 3D 自行猜资产路径 |
### 25.6 推荐命名规则
为了减少后续字段漂移,建议阶段 A 先固定这些命名规则:
- 数据库存储字段保持 `snake_case`
- FreeCAD 对象属性使用 `PascalCase`
- 界面显示名允许中文,但不参与绑定
- `Uuid` 后缀只用于真正全局唯一标识
- `Key` 后缀用于业务键或连接点键
- `Id` 后缀用于类型标识、实例标识或宿主标识
示例:
- 数据库字段:`terminal_uuid`
- FreeCAD 属性:`TerminalUuid`
- 界面显示:`端子 13`
### 25.7 第一版建议严格区分的字段
下面这些字段看起来相近,但建议在阶段 A 就明确区分,避免后面混用:
#### 25.7.1 `terminal_uuid` 与 `terminal_key`
- `terminal_uuid`:机器主键,稳定唯一
- `terminal_key`:业务可读键,便于调试和跨界面识别
不建议只保留 `terminal_key`,因为显示编号或设备标签变化时它可能变化。
#### 25.7.2 `element_uuid` 与 `instance_id`
- `element_uuid`2D 设备实例身份
- `instance_id`3D 设备实例身份
这两个字段不要互相替代。第一版即使它们暂时值相同,也建议语义上保持区分。
#### 25.7.3 `symbol_terminal` 与 `connection_point_key`
- `symbol_terminal`2D 图纸上的端子显示号
- `connection_point_key`3D 模型内部连接点键
一个显示端子号理论上可以映射到一个具体连接点,但两者不是同一个概念。
### 25.8 字段对照表的使用方式
这张表建议在阶段 B/C 里直接作为开发输入使用:
1. 2D 侧先检查哪些字段已经稳定存在
2. 对不存在但必要的字段做补充或规范化
3. 3D 侧严格按对照表创建对象属性
4. 接口层不要临时发明新名字,优先复用这里的统一命名
也就是说,后续代码实现时应该尽量做到:
> 看字段对照表就能知道字段从哪来、落到哪去、该由谁维护。
### 24.11 阶段 A 结束后再进入代码
阶段 A 结束后,下一步再做代码时,建议顺序是:
1. 先在 2D 侧把设备级绑定输出稳定下来
2. 再在 3D 侧按 `instance_id` 落设备实例
3. 设备级稳定后,再进入端子级绑定
也就是说,阶段 A 的价值就在于:
> 先把绑定语言说清楚,再开始真正的数据读写和对象创建。