Compare commits

..

No commits in common. 'csm' and 'main' have entirely different histories.
csm ... main

1
.gitignore vendored

@ -48,7 +48,6 @@ install_manifest.txt
/Mod/ /Mod/
/ZERO_CHECK.dir/ /ZERO_CHECK.dir/
/build/ /build/
/bulid/
/build-*/ /build-*/
/cmake-build*/ /cmake-build*/
/src/Tools/offlinedoc/localwiki/ /src/Tools/offlinedoc/localwiki/

@ -1,39 +0,0 @@
Rules:
- 涉及 QET <-> FreeCAD 协同开发前,先阅读 `D:\project\LightWork3D\FreeCAD\docs\数据库设计.md`
- 第一版 2D/3D 协同只允许使用这两张绑定表:
- `project_2d3d_symbol_binding`
- `project_2d3d_terminal_binding`
- 第一版禁止依赖这些 Legacy 表:
- `project_3d_scene_instance`
- `project_3d_space_object`
- `project_2d3d_link`
- `start_end_terminal_matches`
- 第一版禁止假定 3D 位姿保存在数据库中3D 位姿和装配状态以 FreeCAD 文档为准
- `device_attribute` 只作为设备类型主数据表使用,不承担 2D/3D 实例绑定职责
- 3D 模型资源绑定优先使用 `device_3d_asset``device_attribute.parts_3d` 仅允许作为兼容/回退字段
- `project_2d3d_symbol_binding` 第一版只允许依赖:
- `project_uuid`
- `element_uuid`
- `instance_id`
- `project_2d3d_terminal_binding` 第一版只允许依赖:
- `project_uuid`
- `terminal_uuid`
- `instance_id`
- 第一版禁止继续依赖这些字段:
- `source_diagram_uuid`
- `scene_diagram_uuid`
- `device_id`
- `display_tag`
- `asset_uri`
- `host_binding_mode`
- `host_object_id`
- `host_object_type`
- `extra_json`
- `binding_key`
- `symbol_terminal`
- `terminal_key`
- `connection_point_key`
- `wire_label`
- `net_id`
- `conductor_uuid`
- 第一版 3D 端子绑定唯一依据是 `terminal_uuid`

@ -1,454 +0,0 @@
# 2D / 3D 交换协议(第一版)
本文档只描述 **QET 与 FreeCAD 之间的 JSON 交换格式**
不负责描述:
- 数据库建表
- 数据库存储约束
- 旧表兼容策略
这些内容统一放在:
- [数据库设计.md](D:\project\LightWork3D\FreeCAD\docs\数据库设计.md)
---
## 1. 协议目标
第一版协议只解决下面这条最小闭环:
1. QET 能把当前项目中的设备实例和端子实例导出给 FreeCAD
2. FreeCAD 能根据导出结果创建或更新 3D 设备实例
3. FreeCAD 能根据导出结果创建或更新 3D 端子对象
4. FreeCAD 保存后,能把最小回写结果再交还给 QET
当前版本明确采用:
- **文件交换**
- **JSON 格式**
当前版本明确不采用:
- 实时 RPC
- HTTP API
- 直接双向数据库同步
---
## 2. 交换文件位置建议
建议所有交换文件都放到项目目录下:
```text
<ProjectRoot>/.qet_freecad/
2d_to_3d.json
3d_to_2d.json
scene.FCStd
logs/
```
含义:
- `2d_to_3d.json`QET 导出给 FreeCAD 的输入快照
- `3d_to_2d.json`FreeCAD 回写给 QET 的结果快照
- `scene.FCStd`:该项目对应的 FreeCAD 3D 工程
---
## 3. 第一版 `2d_to_3d.json` 设计原则
### 3.1 最小主键集
第一版最小主键集只认:
- `project_uuid`
- `element_uuid`
- `terminal_uuid`
- `instance_id`
### 3.2 数据来源
`2d_to_3d.json` 不是数据库整表导出,而是:
> 从 QET 当前项目状态中整理出的、面向 FreeCAD 使用的最小交换快照。
也就是说:
- 它可以来自数据库
- 也可以来自当前内存状态
- 但最终输出是给 FreeCAD 消费的统一协议格式
### 3.3 协议可以比数据库稍微丰富
数据库设计要求尽量去冗余。
但 JSON 交换协议可以为了:
- 降低 FreeCAD 读取复杂度
- 提升可调试性
- 避免 FreeCAD 再回查 QET 内部数据库
而适当带上一些“已解析数据”。
这不等于数据库必须保留这些字段。
---
## 4. 第一版 `2d_to_3d.json` 顶层结构
推荐结构:
```json
{
"schema_version": "1.1",
"project_uuid": "string",
"generated_at": "2026-05-18T10:30:00+08:00",
"source": {
"app": "QET",
"version": "string"
},
"cabinet": {},
"devices": [],
"terminals": [],
"device_models": []
}
```
说明:
- `schema_version`:协议版本,便于后续兼容升级
- `project_uuid`:项目主键
- `generated_at`:导出时间
- `source`:导出来源信息
- `cabinet`:当前图纸属性中绑定的机柜信息
- `devices`:设备实例绑定
- `terminals`:端子实例绑定
- `device_models`:设备 3D 模型解析结果
---
## 5. `cabinet` 结构
### 5.1 作用
当前版本仍然以 **图纸** 作为交换单位。
但图纸属性本身已经绑定了一个机柜,因此 `2d_to_3d.json` 顶层会额外带上:
> 当前图纸绑定的机柜信息
这样 FreeCAD 在保持“按图纸导入设备/端子”的同时,也能知道:
- 这批设备属于哪个机柜
- 这个机柜当前绑定了哪个 3D 相对路径
### 5.2 推荐字段
```json
{
"location_id": 12,
"label": "C",
"name": "电容柜",
"display_text": "C - 电容柜",
"associated_fileset": "电容柜文件集",
"three_d_relative_path": "3D/Cabinets/C.FCStd",
"resolved_scene_path": "C:/Users/Admin/Documents/MingTuProject/xxx/3D/Cabinets/C.FCStd"
}
```
### 5.3 字段说明
| 字段 | 中文 | 必需 | 说明 |
| --- | --- | --- | --- |
| `location_id` | 机柜位置ID | 否 | 当前图纸属性绑定的机柜位置 ID |
| `label` | 机柜标注 | 否 | 用于 3D 侧快速识别机柜 |
| `name` | 机柜名称 | 否 | 机柜名称 |
| `display_text` | 机柜显示文本 | 否 | 等价于 QET 图纸属性界面中看到的机柜显示文本 |
| `associated_fileset` | 关联文件集 | 否 | 当前机柜位置绑定的文件集信息 |
| `three_d_relative_path` | 3D 相对路径 | 否 | 相对于工程目录保存的 3D 机柜路径 |
| `resolved_scene_path` | 已解析机柜场景路径 | 否 | QET 已解析出的本地场景文件路径,便于 FreeCAD 直接使用 |
### 5.4 说明
- `cabinet` 是一个 **图纸级上下文对象**
- 它不是设备绑定表或端子绑定表的扩张
- 它只描述“当前图纸绑定了哪个机柜,以及机柜当前对应哪个 3D 文件”
---
## 6. `devices` 结构
### 6.1 作用
`devices` 负责表达:
> 一个 2D 设备实例,对应哪个 3D 设备实例。
### 6.2 第一版字段
```json
{
"element_uuid": "string",
"instance_id": "string",
"display_tag": "string"
}
```
### 6.3 字段说明
| 字段 | 中文 | 必需 | 说明 |
| --- | --- | --- | --- |
| `element_uuid` | 2D设备实例UUID | 是 | QET 图纸中的设备实例主键 |
| `instance_id` | 3D实例ID | 是 | FreeCAD 侧设备实例主键 |
| `display_tag` | 2D设备实例标注 | 否 | JSON 显示辅助字段,优先使用 2D 中设备标注作为 FreeCAD 树标签;为空时再退回 `instance_id` / `element_uuid` |
### 6.4 说明
- 如果第一次进入 3D 时还没有 `instance_id`,允许先导出空字符串或缺省值
- FreeCAD 创建 3D 实例后,再在回写阶段补齐
- `display_tag` 不进入第一版数据库最小字段集,它只存在于交换 JSON 中,用来让 3D 树视图与 2D 标注更容易对上
---
## 7. `terminals` 结构
### 7.1 作用
`terminals` 负责表达:
> 一个 2D 端子实例,属于哪个 3D 设备实例。
### 7.2 第一版字段
```json
{
"terminal_uuid": "string",
"instance_id": "string",
"element_uuid": "string"
}
```
### 7.3 字段说明
| 字段 | 中文 | 必需 | 说明 |
| --- | --- | --- | --- |
| `terminal_uuid` | 2D端子UUID | 是 | QET 端子实例主键 |
| `instance_id` | 3D实例ID | 是 | 该端子所属的 3D 设备实例 |
| `element_uuid` | 2D设备实例UUID | 否 | JSON 导入辅助字段,帮助 FreeCAD 在首次没有 `instance_id` 时仍能知道端子属于哪个设备 |
### 7.4 为什么这里允许带 `element_uuid`
注意:
- `element_uuid` **不是**第一版端子绑定表的数据库字段扩张
- 它只是交换 JSON 中的上下文辅助字段
原因:
- 当某些设备第一次进入 3D、暂时还没有 `instance_id`
- FreeCAD 仍需要知道该端子属于哪个 2D 设备实例
所以这里允许 JSON 比数据库稍微丰富一些。
### 7.5 为什么第一版不带更多字段
第一版先不强制包含:
- `terminal_key`
- `connection_point_key`
- `symbol_terminal`
- `wire_label`
- `net_id`
原因:
- 这些字段当前都不是 FreeCAD 第一版创建端子对象的硬前提
- 端子语义可以通过 `terminal_uuid` 回查 QET
- 先把绑定关系打通,比先堆字段更重要
---
## 8. `device_models` 结构
### 8.1 作用
`device_models` 不是绑定表本身,而是:
> QET 在导出时,顺手把设备对应 3D 模型资源解析出来,减少 FreeCAD 再回查 QET 内部数据库的复杂度。
### 8.2 第二步设备导入推荐字段
```json
{
"element_uuid": "string",
"device_id": 123,
"parts_3d": "string",
"resolved_model_path": "string"
}
```
### 8.3 字段说明
| 字段 | 中文 | 必需 | 说明 |
| --- | --- | --- | --- |
| `element_uuid` | 2D设备实例UUID | 是 | 与 `devices` 关联 |
| `device_id` | 设备类型ID | 否 | QET 设备主数据 ID |
| `parts_3d` | 3D模型资源URI | 否 | 原始资源引用,来自 `device_3d_asset.uri``device_attribute.parts_3d` |
| `resolved_model_path` | 已解析模型路径 | 是 | QET 已经解析好的本地模型文件路径FreeCAD 第二步直接用它导入 STEP / FCStd |
### 8.4 为什么 `resolved_model_path` 是第二步关键字段
第二步开始FreeCAD 不再只做 JSON 校验,而要真正导入设备模型。
如果没有 `resolved_model_path`FreeCAD 就必须自己理解和回查:
1. `element_uuid`
2. `device_id`
3. `device_3d_asset`
4. `device_attribute.parts_3d`
5. 本地路径解析规则
这会让 FreeCAD 过度耦合 QET 内部数据库结构。
所以第二步推荐由 QET 在导出时直接给出:
- `resolved_model_path`
让 FreeCAD 只负责消费结果。
### 7.4 为什么这里允许同时带 `parts_3d`
注意:
- **数据库设计**里我们已经决定不在绑定表冗余保存 `asset_uri`
- 但**交换协议**里允许带 `parts_3d`
原因是:
- JSON 是导出快照
- 不是绑定数据库本身
- 这样 FreeCAD 读取时更简单
也就是说:
> 数据库去冗余
> JSON 可适度带已解析结果
---
## 8. 第一版 `2d_to_3d.json` 完整样例
```json
{
"schema_version": "1.0",
"project_uuid": "proj-001",
"generated_at": "2026-05-18T10:30:00+08:00",
"source": {
"app": "QET",
"version": "1.0"
},
"devices": [
{
"element_uuid": "elem-1001",
"instance_id": "fc-inst-0001"
}
],
"terminals": [
{
"terminal_uuid": "term-2001",
"instance_id": "fc-inst-0001",
"element_uuid": "elem-1001"
},
{
"terminal_uuid": "term-2002",
"instance_id": "fc-inst-0001",
"element_uuid": "elem-1001"
}
],
"device_models": [
{
"element_uuid": "elem-1001",
"device_id": 123,
"parts_3d": "models/mccb/model.step",
"resolved_model_path": "C:/Users/Admin/Documents/MingTuProject/models/mccb/model.step"
}
]
}
```
---
## 9. 第一版 `3d_to_2d.json` 建议
第一版回写建议同样保持最小化。
推荐结构:
```json
{
"schema_version": "1.0",
"project_uuid": "string",
"generated_at": "2026-05-18T11:00:00+08:00",
"instances": [],
"terminals": []
}
```
### 9.1 `instances`
```json
{
"element_uuid": "string",
"instance_id": "string"
}
```
### 9.2 `terminals`
```json
{
"terminal_uuid": "string",
"instance_id": "string"
}
```
### 9.3 说明
第一版不回写:
- 3D 位姿
- 装配层级
- 几何路径
- 线槽信息
这些仍以 FreeCAD 文档为准。
---
## 10. 第一版推荐交互流程
1. 用户在 QET 中点击 `3D视图`
2. QET 生成 `2d_to_3d.json`
3. QET 打开 FreeCAD并打开 `scene.FCStd`
4. FreeCAD 读取 `2d_to_3d.json`
5. FreeCAD 创建或更新:
- 3D 设备实例
- 3D 端子对象
6. 用户在 FreeCAD 中完成装配和接线
7. 用户保存 FreeCAD 工程
8. FreeCAD 生成 `3d_to_2d.json`
9. QET 在后续时机读取 `3d_to_2d.json`
---
## 11. 当前推荐结论
第一版协议建议明确分层:
- **数据库设计**:尽量去冗余
- **JSON 协议**:允许带少量已解析结果,方便 FreeCAD 使用
一句话总结:
> 第一版先把 `2d_to_3d.json` 做成“面向 FreeCAD 的最小项目快照”,而不是数据库整表镜像。

@ -1,522 +0,0 @@
# FreeCAD Windows 编译运行傻瓜指南
这份文档面向第一次在 Windows 上编译 FreeCAD 的同学。
目标是:
- 不改源码
- 用 Visual Studio 2022 成功编译
- 能在本机直接运行 FreeCAD
- 能在 VS 里按 `F5` 调试
本文档基于下面这套已经验证成功的组合:
- 源码分支:`releases/FreeCAD-1-1`
- 源码版本:`1.1.1-28-g94f4cb77f6`
- 编译器Visual Studio 2022
- 构建配置:`RelWithDebInfo | x64`
- LibPack`LibPack-1.1.0-v3.1.1.3-Release`
## 1. 先理解三个目录
编译 FreeCAD 时,最好把“源码目录”“构建目录”“运行目录”分开。
建议按下面这样放:
```text
D:\project\LightWork3D\FreeCAD 源码目录
E:\fc\LibPack-1.1.0-v3.1.1.3-Release 依赖目录
E:\fc\build-relwithdebinfo-libpack3113 构建目录
E:\fc\run-FreeCAD-1.1.1 安装后的运行目录
```
不要把所有东西都堆在一个目录里。
## 2. 需要安装的软件
至少准备好这些:
1. `Visual Studio 2022`
2. `Desktop development with C++` 工作负载
3. `CMake`
4. `Git`
建议:
- 用 64 位系统
- `E:` 盘最好预留 100GB 以上
- `C:``D:` 至少各留 30GB 可用空间
## 3. 下载源码
把 FreeCAD 源码放到:
```text
D:\project\LightWork3D\FreeCAD
```
如果你已经有源码,确认当前分支是:
```powershell
git -C D:\project\LightWork3D\FreeCAD rev-parse --abbrev-ref HEAD
```
应该看到:
```text
releases/FreeCAD-1-1
```
## 4. 下载完全匹配的 LibPack
这一步非常重要。
对于这份源码,不要随便换别的 LibPack。已经验证成功的是
[LibPack-1.1.0-v3.1.1.3-Release.7z](https://github.com/FreeCAD/FreeCAD-LibPack/releases/download/3.1.1.3/LibPack-1.1.0-v3.1.1.3-Release.7z)
下载后解压到:
```text
E:\fc\LibPack-1.1.0-v3.1.1.3-Release
```
不要用 `3.1.1.2`
这份源码配 `3.1.1.2` 时,已经实测会在链接阶段报 `boost::program_options ... contains` 相关错误。
## 5. 清理磁盘空间
如果你之前编过很多次,建议先清理旧构建目录。
重点清理这些“可再生目录”:
```text
旧 build 目录
旧 LibPack 目录
下载缓存
C:\Users\<你的用户名>\AppData\Local\Temp
C:\Windows\Temp
```
如果你在编译中看到下面这些错误:
```text
No space left on device
磁盘空间不足
无法创建目录
```
那不是源码问题,就是磁盘满了。
## 6. 新建构建目录
`E:` 盘新建目录:
```text
E:\fc\build-relwithdebinfo-libpack3113
```
## 7. 用 CMake 生成 VS 工程
打开 PowerShell执行
```powershell
$cmake = 'C:\CMake\bin\cmake.exe'
$src = 'D:\project\LightWork3D\FreeCAD'
$build = 'E:\fc\build-relwithdebinfo-libpack3113'
$libpack = 'E:\fc\LibPack-1.1.0-v3.1.1.3-Release'
$env:Path = @(
'C:\CMake\bin',
'D:\VisualStudio\MSBuild\Current\Bin',
'D:\VisualStudio\Common7\IDE',
'C:\Git\cmd',
'C:\Windows\System32',
'C:\Windows',
'C:\Windows\System32\Wbem',
'C:\Windows\System32\WindowsPowerShell\v1.0',
"$libpack\bin",
"$libpack\lib"
) -join ';'
& $cmake `
-S $src `
-B $build `
-G 'Visual Studio 17 2022' `
-A x64 `
-D FREECAD_LIBPACK_USE=ON `
-D FREECAD_LIBPACK_DIR=$libpack `
-D CMAKE_CONFIGURATION_TYPES=RelWithDebInfo
```
成功后会生成:
```text
E:\fc\build-relwithdebinfo-libpack3113\FreeCAD.sln
```
## 8. 编译前先记住一个坑
这套环境里,`TechDraw` 模块会触发 MSVC 的编译器堆空间不足:
```text
error C1060: 编译器的堆空间不足
```
这不是源码坏了。
解决办法是不改源码,只在编译时强制:
```text
/MP1
```
也就是让 `cl.exe` 单进程编译重模块。
## 9. 编译整个工程
继续在 PowerShell 里执行:
```powershell
$cmake = 'C:\CMake\bin\cmake.exe'
$build = 'E:\fc\build-relwithdebinfo-libpack3113'
$libpack = 'E:\fc\LibPack-1.1.0-v3.1.1.3-Release'
$env:Path = @(
'C:\CMake\bin',
'D:\VisualStudio\MSBuild\Current\Bin',
'D:\VisualStudio\Common7\IDE',
'C:\Git\cmd',
'C:\Windows\System32',
'C:\Windows',
'C:\Windows\System32\Wbem',
'C:\Windows\System32\WindowsPowerShell\v1.0',
"$libpack\bin",
"$libpack\lib"
) -join ';'
$env:_CL_ = '/MP1'
& $cmake --build $build --config RelWithDebInfo --target ALL_BUILD --parallel 1
```
说明:
- `--parallel 1` 是 MSBuild 外层单并发
- `_CL_=/MP1` 是 C/C++ 编译器内层单并发
这两个都保留,最稳。
## 10. 不要直接运行 build 目录里的 FreeCAD.exe
很多人编译成功后,第一反应是去点:
```text
E:\fc\build-relwithdebinfo-libpack3113\bin\RelWithDebInfo\FreeCAD.exe
```
这通常会报各种缺 DLL比如
- `xerces-c_3_2.dll`
- `icuuc74.dll`
- `boost_program_options-vc143-mt-x64-1_87.dll`
- `pyside6.abi3.dll`
- `shiboken6.abi3.dll`
原因不是没编好,而是它只是构建产物,不是整理好的运行目录。
## 11. 生成真正可运行的安装目录
编译完成后执行:
```powershell
$cmake = 'C:\CMake\bin\cmake.exe'
$build = 'E:\fc\build-relwithdebinfo-libpack3113'
$prefix = 'E:\fc\run-FreeCAD-1.1.1'
& $cmake --install $build --config RelWithDebInfo --prefix $prefix
```
执行完以后,真正建议运行的是:
```text
E:\fc\run-FreeCAD-1.1.1\bin\FreeCAD.exe
```
## 12. 推荐运行方式
推荐做法不是双击某个 `bat`,而是:
1. 先执行 `INSTALL`
2. 然后在 Visual Studio 里直接按 `F5`
这样有几个好处:
- 断点能直接生效
- 环境变量配置固定在 VS 里,不容易忘
- 每次改完代码后可以继续用同一套调试入口
运行时真正使用的程序是:
```text
E:\fc\run-FreeCAD-1.1.1\bin\FreeCAD.exe
```
## 13. 在 VS 中打开哪个目录
你在 Visual Studio 里要打开的是:
```text
E:\fc\build-relwithdebinfo-libpack3113
```
准确地说,是打开这个文件:
[E:\fc\build-relwithdebinfo-libpack3113\FreeCAD.sln](E:\fc\build-relwithdebinfo-libpack3113\FreeCAD.sln)
不要打开:
- `D:\project\LightWork3D\FreeCAD` 作为“编译入口”
- `E:\fc\run-FreeCAD-1.1.1` 作为“解决方案目录”
这两个都不是 VS 编译入口。
## 14. 在 VS 中怎样直接 F5 运行
### 14.1 启动工程
打开解决方案后:
1. 配置切到 `RelWithDebInfo | x64`
2. 右键 `FreeCADMain`
3. 选择“设为启动项目”
### 14.2 调试配置
打开:
`FreeCADMain -> 属性 -> 配置属性 -> 调试`
填写:
`命令`
```text
E:\fc\run-FreeCAD-1.1.1\bin\FreeCAD.exe
```
`工作目录`
```text
E:\fc\run-FreeCAD-1.1.1\bin
```
`环境`
```text
FREECAD_LIBPACK_BIN=E:\fc\LibPack-1.1.0-v3.1.1.3-Release\bin
PATH=E:\fc\LibPack-1.1.0-v3.1.1.3-Release\bin;E:\fc\LibPack-1.1.0-v3.1.1.3-Release\lib;E:\fc\LibPack-1.1.0-v3.1.1.3-Release\bin\Lib\site-packages\PySide6;E:\fc\LibPack-1.1.0-v3.1.1.3-Release\bin\Lib\site-packages\shiboken6;E:\fc\run-FreeCAD-1.1.1\bin;%PATH%
QT_PLUGIN_PATH=E:\fc\LibPack-1.1.0-v3.1.1.3-Release\plugins
QML2_IMPORT_PATH=E:\fc\LibPack-1.1.0-v3.1.1.3-Release\qml
```
配完以后,直接按 `F5`
注意:
- `FREECAD_LIBPACK_BIN` 这一行不要漏
- 它不是可有可无的变量
- 在 Windows + Python 3.12 下,很多模块会靠它触发 `os.add_dll_directory(...)`
- 如果漏掉它,程序本体可能能启动,但打开带 `Part`、`Measure`、`TechDraw`、`PartDesignGui` 的工程时会报 `DLL load failed while importing ...`
### 14.3 如果打开自带工程时报模块导入失败
如果你打开 FreeCAD 自带工程后,消息面板里出现类似这些错误:
```text
DLL load failed while importing Part
DLL load failed while importing Measure
DLL load failed while importing TechDraw
DLL load failed while importing PartDesignGui
Cannot create object 'Page'
Cannot create object 'Template'
Cannot create object 'Hatch'
```
先不要怀疑源码,也不要先改代码。
先检查下面 4 件事:
1. 你运行的是不是 `E:\fc\run-FreeCAD-1.1.1\bin\FreeCAD.exe`
2. 你是不是已经执行过 `INSTALL`
3. VS 调试环境里有没有 `FREECAD_LIBPACK_BIN=E:\fc\LibPack-1.1.0-v3.1.1.3-Release\bin`
4. `PATH` 里有没有 LibPack 的 `bin/lib`、PySide6、shiboken6、运行目录的 `bin`
这类报错在当前这套环境下绝大多数都属于“VS 调试环境没配完整”,不是源码本身坏了。
## 15. 每次改完代码后该怎么做
推荐顺序:
1. 在 VS 里编译你改动的项目
2. 再编一次 `ALL_BUILD`
3. 再执行一次 `INSTALL`
4. 在 VS 里按 `F5`
如果你不执行 `INSTALL`,运行目录里的文件可能不是最新的。
## 16. 常见错误和对应处理
### 错误 1`python312_d.lib` 找不到
原因:
- 你在编 `Debug`
- 但 LibPack 是 release 版
解决:
- 不要用 `Debug`
- 用 `RelWithDebInfo``Release`
### 错误 2`boost::program_options ... contains` 链接失败
原因:
- LibPack 版本不匹配
解决:
- 改用 `LibPack-1.1.0-v3.1.1.3-Release`
### 错误 3`No space left on device` / `磁盘空间不足`
原因:
- 盘满了
解决:
- 清理旧 build
- 清理旧 LibPack
- 清理 Temp
- 把构建目录移到 `E:`
### 错误 4`C1060 编译器的堆空间不足`
原因:
- `TechDraw` 太重
- 编译器内部并发太高
解决:
```powershell
$env:_CL_='/MP1'
cmake --build E:\fc\build-relwithdebinfo-libpack3113 --config RelWithDebInfo --target ALL_BUILD --parallel 1
```
### 错误 5直接双击 exe 提示缺 DLL
原因:
- 你点的是构建产物目录里的裸 `exe`
解决:
- 不要直接点 `build\bin\RelWithDebInfo\FreeCAD.exe`
- 先执行 `INSTALL`
- 然后在 VS 中把启动命令设为 `E:\fc\run-FreeCAD-1.1.1\bin\FreeCAD.exe`
- 并在 VS 调试配置里补好 `PATH`、`QT_PLUGIN_PATH`、`QML2_IMPORT_PATH`
### 错误 6打开自带工程时报 `DLL load failed while importing Part/Measure/TechDraw/PartDesignGui`
原因:
- 运行时不是源码坏了
- 而是 VS 的调试环境缺少关键变量
- 最常见的是漏掉 `FREECAD_LIBPACK_BIN`
解决:
- 确认已经执行过 `INSTALL`
- 在 VS 的调试环境中加入:
```text
FREECAD_LIBPACK_BIN=E:\fc\LibPack-1.1.0-v3.1.1.3-Release\bin
```
- 同时保留:
```text
PATH=E:\fc\LibPack-1.1.0-v3.1.1.3-Release\bin;E:\fc\LibPack-1.1.0-v3.1.1.3-Release\lib;E:\fc\LibPack-1.1.0-v3.1.1.3-Release\bin\Lib\site-packages\PySide6;E:\fc\LibPack-1.1.0-v3.1.1.3-Release\bin\Lib\site-packages\shiboken6;E:\fc\run-FreeCAD-1.1.1\bin;%PATH%
QT_PLUGIN_PATH=E:\fc\LibPack-1.1.0-v3.1.1.3-Release\plugins
QML2_IMPORT_PATH=E:\fc\LibPack-1.1.0-v3.1.1.3-Release\qml
```
说明:
- 这类问题通常可以通过环境配置解决
- 不需要先修改任何源码
## 17. 一套最终可复用的命令
### 生成工程
```powershell
$cmake = 'C:\CMake\bin\cmake.exe'
$src = 'D:\project\LightWork3D\FreeCAD'
$build = 'E:\fc\build-relwithdebinfo-libpack3113'
$libpack = 'E:\fc\LibPack-1.1.0-v3.1.1.3-Release'
$env:Path = @(
'C:\CMake\bin',
'D:\VisualStudio\MSBuild\Current\Bin',
'D:\VisualStudio\Common7\IDE',
'C:\Git\cmd',
'C:\Windows\System32',
'C:\Windows',
'C:\Windows\System32\Wbem',
'C:\Windows\System32\WindowsPowerShell\v1.0',
"$libpack\bin",
"$libpack\lib"
) -join ';'
& $cmake -S $src -B $build -G 'Visual Studio 17 2022' -A x64 -D FREECAD_LIBPACK_USE=ON -D FREECAD_LIBPACK_DIR=$libpack -D CMAKE_CONFIGURATION_TYPES=RelWithDebInfo
```
### 编译
```powershell
$env:_CL_='/MP1'
cmake --build E:\fc\build-relwithdebinfo-libpack3113 --config RelWithDebInfo --target ALL_BUILD --parallel 1
```
### 安装到运行目录
```powershell
cmake --install E:\fc\build-relwithdebinfo-libpack3113 --config RelWithDebInfo --prefix E:\fc\run-FreeCAD-1.1.1
```
### 运行
打开:
[E:\fc\build-relwithdebinfo-libpack3113\FreeCAD.sln](E:\fc\build-relwithdebinfo-libpack3113\FreeCAD.sln)
确认:
- 启动项目是 `FreeCADMain`
- 配置是 `RelWithDebInfo | x64`
- 已执行过 `INSTALL`
- 调试命令指向 `E:\fc\run-FreeCAD-1.1.1\bin\FreeCAD.exe`
然后按:
```text
F5
```
---
如果你严格按这份文档走,基于当前这份源码和这套 LibPack已经实测可以成功编译并运行。
如果后面换了源码分支,第一件事先确认:**LibPack 版本是不是还匹配。**

File diff suppressed because it is too large Load Diff

@ -1,10 +0,0 @@
@echo off
setlocal
set "FC_LIBPACK=E:\fc\LibPack-1.1.0-v3.1.1.3-Release"
set "FC_RUN=E:\fc\run-FreeCAD-1.1.1"
set "FC_PYSIDE=%FC_LIBPACK%\bin\Lib\site-packages\PySide6"
set "FC_SHIBOKEN=%FC_LIBPACK%\bin\Lib\site-packages\shiboken6"
set "QT_PLUGIN_PATH=%FC_LIBPACK%\plugins"
set "QML2_IMPORT_PATH=%FC_LIBPACK%\qml"
set "PATH=%FC_LIBPACK%\bin;%FC_LIBPACK%\lib;%FC_PYSIDE%;%FC_SHIBOKEN%;%FC_RUN%\bin;%PATH%"
start "" /D "%FC_RUN%\bin" "%FC_RUN%\bin\FreeCAD.exe"

@ -1,481 +0,0 @@
# 2D / 3D 协同数据库设计(第一版最小集)
本文档只保留当前阶段真正必需的字段。
目标不是一次把未来所有能力都放进表里,而是先支持下面这条最小闭环:
1. 2D 里有设备实例
2. 2D 里有端子实例
3. FreeCAD 里创建对应的 3D 设备实例
4. FreeCAD 里创建对应的 3D 端子对象
5. 2D 与 3D 可以靠稳定主键互相找到对方
当前设计严格遵守两条原则:
- 电气语义以 2D 为准
- 空间位姿以 3D 为准
另外再补一条当前版本明确采用的约定:
- 3D 设备资源不在绑定表中重复保存,统一通过 `element_uuid -> device_id -> parts_3d` 回查
因此,第一版不重复存这些信息:
- `display_tag`
- `terminal_key`
- `connection_point_key`
- `host_binding_mode`
- `host_object_id`
- `host_object_type`
- `extra_json`
- `scene_diagram_uuid`
- `source_diagram_uuid`
原因很简单:
- 这些字段当前要么能通过 2D 主键回查
- 要么属于未来扩展
- 要么当前语义还不够稳定
## 1. 统一主键约定
第一版只认下面这几个核心标识:
- `project_uuid`
- `element_uuid`
- `terminal_uuid`
- `instance_id`
含义:
- `project_uuid`:项目唯一标识
- `element_uuid`2D 设备实例唯一标识
- `terminal_uuid`2D 端子实例唯一标识
- `instance_id`3D 设备实例唯一标识,由 FreeCAD 生成
## 2. 需要保留的最小表
第一版只建议保留 2 张表:
1. `project_2d3d_symbol_binding`
2. `project_2d3d_terminal_binding`
---
## 3. 设备绑定表
表名:
`project_2d3d_symbol_binding`
作用:
> 记录“一个 2D 设备实例,对应哪个 3D 设备实例,以及它使用哪个 3D 资源”
但“它使用哪个 3D 资源”这一点,第一版不在本表中直接存储,而是通过 2D 侧已有关系回查:
`element_uuid -> device_id -> parts_3d`
### 3.1 保留字段
| 字段名 | 中文 | 责任方 | 说明 |
| --- | --- | --- | --- |
| `project_uuid` | 项目UUID | 2D | 项目范围主键 |
| `element_uuid` | 设备实例UUID | 2D | 2D 设备实例唯一标识 |
| `instance_id` | 3D实例ID | 3D | FreeCAD 生成的 3D 设备实例唯一标识 |
### 3.2 推荐约束
- 主唯一键:`(project_uuid, element_uuid)`
- 3D 实例唯一键:`(project_uuid, instance_id)`
### 3.3 推荐 SQLite 结构
```sql
CREATE TABLE IF NOT EXISTS project_2d3d_symbol_binding (
project_uuid TEXT NOT NULL,
element_uuid TEXT NOT NULL,
instance_id TEXT NOT NULL,
PRIMARY KEY (project_uuid, element_uuid)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_symbol_binding_instance
ON project_2d3d_symbol_binding(project_uuid, instance_id);
```
### 3.4 为什么只留这 3 个字段
因为第一版要解决的问题只有:
- 2D 设备是谁:`element_uuid`
- 3D 设备是谁:`instance_id`
- 属于哪个项目:`project_uuid`
而 3D 模型资源本身可以通过:
- `element_uuid`
- 回查到对应设备实例
- 再回查 `device_id`
- 最后拿到 `parts_3d`
所以第一版不重复存 `asset_uri`
---
## 4. 端子绑定表
表名:
`project_2d3d_terminal_binding`
作用:
> 记录“一个 2D 端子实例,对应哪个 3D 设备实例上的端子对象”
### 4.1 保留字段
| 字段名 | 中文 | 责任方 | 说明 |
| --- | --- | --- | --- |
| `project_uuid` | 项目UUID | 2D | 项目范围主键 |
| `terminal_uuid` | 端子UUID | 2D | 2D 端子实例唯一标识 |
| `instance_id` | 3D实例ID | 3D | 该端子属于哪个 3D 设备实例 |
### 4.2 推荐约束
- 主唯一键:`(project_uuid, terminal_uuid)`
- 查询索引:`(project_uuid, instance_id)`
### 4.3 推荐 SQLite 结构
```sql
CREATE TABLE IF NOT EXISTS project_2d3d_terminal_binding (
project_uuid TEXT NOT NULL,
terminal_uuid TEXT NOT NULL,
instance_id TEXT NOT NULL,
PRIMARY KEY (project_uuid, terminal_uuid)
);
CREATE INDEX IF NOT EXISTS idx_terminal_binding_instance
ON project_2d3d_terminal_binding(project_uuid, instance_id);
```
### 4.4 为什么不保留 `terminal_key``connection_point_key`
当前第一版设计里:
- 3D 端子本身就是 2D 端子的空间映射
- 所有端子语义都可以通过 `terminal_uuid` 从 2D 回查
所以目前不需要额外再保存:
- `terminal_key`
- `symbol_terminal`
- `connection_point_key`
- `wire_label`
- `net_id`
这些信息如果后续 3D 侧需要显示或校验,可以通过 `terminal_uuid` 回查 2D。
---
## 5. 现阶段明确删掉的字段
下面这些字段不是说永远没用,而是 **第一版先删掉,不进入最小表结构**。
### 5.1 从设备绑定表中删掉
- `source_diagram_uuid`
- `scene_diagram_uuid`
- `device_id`
- `display_tag`
- `asset_uri`
- `host_binding_mode`
- `host_object_id`
- `host_object_type`
- `extra_json`
### 5.2 从端子绑定表中删掉
- `element_uuid`
- `binding_key`
- `symbol_terminal`
- `terminal_key`
- `connection_point_key`
- `wire_label`
- `net_id`
- `conductor_uuid`
- `extra_json`
### 5.3 从 3D 实例表中删掉
- `project_3d_scene_instance` 整张表
- 所有位姿字段:`tx / ty / tz / rx / ry / rz`
- 所有缩放字段:`sx / sy / sz`
- 所有 3D 场景附加字段:`diagram_uuid / asset_id / extra_json`
---
## 6. 第一版数据流
### 6.1 2D -> 3D
2D 提供:
- `project_uuid`
- `element_uuid`
- `terminal_uuid`
3D 在导入和实例化时生成:
- `instance_id`
然后写入:
- `project_2d3d_symbol_binding`
- `project_2d3d_terminal_binding`
### 6.2 3D -> 2D
3D 第一版只回写:
- `instance_id`
当前不回写位姿。
原因是:
- 3D 场景真相源已经切换为 FreeCAD
- 2D 不再承担 3D 场景保存职责
- 避免 FreeCAD 文档和数据库各存一份位姿造成冲突
---
## 7. 这版设计的核心思想
第一版故意把设计收得很紧,只保留:
- 一个 2D 设备实例如何找到 3D 设备实例
- 一个 2D 端子实例如何找到 3D 端子对象
也就是说,当前数据库设计只服务于:
> “2D 语义 + 3D 空间映射”这个最小闭环
而且这里的“空间映射”只表示:
- 2D 对象知道自己对应哪个 3D 实例
不表示数据库要保存完整 3D 位姿。
---
## 8. 后续扩展时再加的字段
只有当下面这些需求真的出现时,再补字段:
- 多 3D 场景:补 `scene_diagram_uuid`
- 设备挂载规则:补 `host_object_id / host_object_type`
- 3D 资源缓存:补 `asset_uri`
- 多连接点/复杂端子:补 `connection_point_key`
- 3D 端子独立语义缓存:补 `terminal_key / symbol_terminal`
- 导线级 3D 校验:补 `wire_label / net_id / conductor_uuid`
- 2D 侧需要读取 3D 位姿:再恢复 3D 场景实例表或等价位姿存储
- 临时扩展:最后才考虑 `extra_json`
---
## 9. FreeCAD 与数据库的职责边界
继续收缩后的职责建议如下:
### 9.1 数据库负责
- 2D 设备实例和 3D 设备实例的绑定
- 2D 端子实例和 3D 端子对象的绑定
### 9.2 FreeCAD 负责
- 3D 设备实例实际创建
- 设备位姿
- 导轨/安装板/柜体装配关系
- 端子在空间中的实际几何位置
- 3D 接线几何
也就是说,第一版开始就建议明确:
> 3D 位姿和装配关系只保存在 FreeCAD 文档里,不在数据库中重复保存。
---
## 10. 当前推荐结论
第一版数据库设计建议就收成这三张表:
1. `project_2d3d_symbol_binding`
2. `project_2d3d_terminal_binding`
并且只保留最小核心字段:
- 设备绑定只保留:`project_uuid / element_uuid / instance_id`
- 端子绑定只保留:`project_uuid / terminal_uuid / instance_id`
一句话总结:
> 第一版先让 2D 能找到 3D至于 3D 怎么摆、摆在哪、怎么装配,全部交给 FreeCAD 自己管理。
---
## 11. 第一版交换形式
第一版 2D / 3D 协同,交换形式先统一为:
- **JSON 文件交换**
当前不建议第一版直接做:
- 实时数据库双向同步
- 进程间 RPC
- HTTP API
- FreeCAD 保存后直接写回 QET 运行库
原因:
- JSON 最容易调试
- 字段改动时最容易观察
- 不会把 QET 和 FreeCAD 的运行时强耦合在一起
- 更适合当前仍在收缩字段和表结构的阶段
---
## 12. 第一版工具入口
QET 侧建议保留并改造一个工具项:
- `3D视图`
这个工具项的职责不再是走 QET 旧的 3D 模块逻辑,而是:
1. 从 QET 当前项目和当前 2D 图纸中整理 2D -> 3D 最小绑定数据
2. 导出 `2d_to_3d.json`
3. 启动 FreeCAD或打开已有的 FreeCAD 工程文件
也就是说:
> 第一版 `3D视图` 本质上是“导出 2D 快照并切换到 FreeCAD”的入口。
---
## 13. 第一版交换目录建议
建议在项目目录下固定一套交换目录:
```text
<ProjectRoot>/.qet_freecad/
2d_to_3d.json
3d_to_2d.json
scene.FCStd
logs/
```
含义:
- `2d_to_3d.json`QET 导出给 FreeCAD 的输入
- `3d_to_2d.json`FreeCAD 回写给 QET 的输出
- `scene.FCStd`:该项目对应的 FreeCAD 3D 工程
- `logs/`:调试与排障日志
---
## 14. 第一版时机约定
### 14.1 QET -> FreeCAD
触发时机:
- 用户点击 QET 的 `3D视图`
动作:
1. 更新并确认当前 2D 设备实例 / 端子实例绑定
2. 生成 `2d_to_3d.json`
3. 打开 FreeCAD
4. 打开或创建 `scene.FCStd`
### 14.2 FreeCAD -> QET
第一版不建议:
- FreeCAD 保存时直接写数据库
第一版建议:
- FreeCAD 保存时输出 `3d_to_2d.json`
然后由 QET 在后续时机主动读取:
- 手动刷新
- 再次点击 `3D视图`
- 或后续增加单独的“从 3D 刷新”命令
这样做的目的,是先把同步时机收成“文件交换”,避免两边运行时直接打架。
---
## 15. 第一版最小交换内容
### 15.1 `2d_to_3d.json`
第一版只要求包含最小绑定信息:
- 设备绑定:
- `project_uuid`
- `element_uuid`
- `instance_id`
- 端子绑定:
- `project_uuid`
- `terminal_uuid`
- `instance_id`
说明:
- `instance_id` 在第一版中由 FreeCAD 侧生成更合理
- 如果首次进入 3D 时尚未生成 `instance_id`,可以先导出为空,再由 FreeCAD 创建后回写
### 15.2 `3d_to_2d.json`
第一版只建议回写:
- `project_uuid`
- `element_uuid`
- `instance_id`
- `terminal_uuid`
当前不要求回写:
- 3D 位姿
- 装配结构
- 导线几何路径
这些仍以 FreeCAD 文档为准。
---
## 16. 第一版推荐交互流程
建议按下面这条闭环实现:
1. 用户在 QET 中点击 `3D视图`
2. QET 生成 `2d_to_3d.json`
3. QET 打开 FreeCAD并打开 `scene.FCStd`
4. FreeCAD 读取 `2d_to_3d.json`
5. FreeCAD 创建或更新:
- 3D 设备实例
- 3D 端子对象
6. 用户在 FreeCAD 中完成装配、摆放、接线
7. 用户保存 FreeCAD 工程
8. FreeCAD 生成 `3d_to_2d.json`
9. QET 在后续时机读取 `3d_to_2d.json`
一句话总结:
> 第一版先做“QET 导出 JSON + 打开 FreeCADFreeCAD 保存时回写 JSON”的文件交换闭环不做强耦合实时同步。

@ -351,7 +351,7 @@ void StdCmdRestartInSafeMode::activated(int iMsg)
QMessageBox restartBox(Gui::getMainWindow()); QMessageBox restartBox(Gui::getMainWindow());
restartBox.setIcon(QMessageBox::Warning); restartBox.setIcon(QMessageBox::Warning);
restartBox.setWindowTitle(QObject::tr("Restart in Safe Mode")); restartBox.setWindowTitle(QObject::tr("Restart in Safe Mode"));
restartBox.setText(QObject::tr("Restart Light works 3D and enter safe mode?")); restartBox.setText(QObject::tr("Restart FreeCAD and enter safe mode?"));
restartBox.setInformativeText( restartBox.setInformativeText(
QObject::tr("Safe mode temporarily disables the configuration and addons.") QObject::tr("Safe mode temporarily disables the configuration and addons.")
); );

@ -268,12 +268,12 @@
&lt;html&gt;&lt;head&gt;&lt;meta name="qrichtext" content="1" /&gt;&lt;style type="text/css"&gt; &lt;html&gt;&lt;head&gt;&lt;meta name="qrichtext" content="1" /&gt;&lt;style type="text/css"&gt;
p, li { white-space: pre-wrap; } p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=" font-family:'Fira Sans'; font-size:9pt; font-weight:400; font-style:normal;"&gt; &lt;/style&gt;&lt;/head&gt;&lt;body style=" font-family:'Fira Sans'; font-size:9pt; font-weight:400; font-style:normal;"&gt;
&lt;p style=" margin-top:16px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"&gt;&lt;span style=" font-size:x-large; font-weight:600;"&gt;Light works 3D license &lt;/span&gt;&lt;/p&gt; &lt;p style=" margin-top:16px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"&gt;&lt;span style=" font-size:x-large; font-weight:600;"&gt;FreeCAD license &lt;/span&gt;&lt;/p&gt;
&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"&gt;The Light works 3D application is licensed under the terms of the LGPL2+ license, as stated below.&lt;/p&gt; &lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"&gt;The FreeCAD application is licensed under the terms of the LGPL2+ license, as stated below.&lt;/p&gt;
&lt;p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"&gt;&lt;br /&gt;&lt;/p&gt; &lt;p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"&gt;&lt;span style=" font-size:14pt; font-weight:600;"&gt;Third-party libraries licenses&lt;/span&gt;&lt;/p&gt; &lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"&gt;&lt;span style=" font-size:14pt; font-weight:600;"&gt;Third-party libraries licenses&lt;/span&gt;&lt;/p&gt;
&lt;p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:14pt; font-weight:600;"&gt;&lt;br /&gt;&lt;/p&gt; &lt;p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:14pt; font-weight:600;"&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"&gt;The different libraries used in Light works 3D and their respective licenses are described in the documentation.&lt;/p&gt; &lt;p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"&gt;The different libraries used in FreeCAD and their respective licenses are described on the&lt;a href="https://www.freecad.org/wiki/Third_Party_Libraries"&gt;&lt;span style=" text-decoration: underline; color:#0000ff;"&gt;Third Party Libraries wiki page&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt; &lt;hr /&gt;
&lt;hr /&gt; &lt;hr /&gt;
&lt;hr /&gt; &lt;hr /&gt;

@ -289,7 +289,7 @@ void AboutDialog::setupLabels()
} }
if (url == QStringLiteral("Unknown")) { if (url == QStringLiteral("Unknown")) {
url = QStringLiteral("https://github.com/LightWorks/LightWorks3D"); // Just take a guess url = QStringLiteral("https://github.com/FreeCAD/FreeCAD"); // Just take a guess
} }
// This may only create valid URLs for Github, but some other hosts use the same format // This may only create valid URLs for Github, but some other hosts use the same format
@ -328,7 +328,7 @@ void AboutDialog::showCredits()
QString creditsHTML QString creditsHTML
= QStringLiteral("<html><body><h1>%1</h1><p>%2</p><h2>%3</h2><ul>") = QStringLiteral("<html><body><h1>%1</h1><p>%2</p><h2>%3</h2><ul>")
.arg(tr("Credits", "Header for the Credits tab of the About screen")) .arg(tr("Credits", "Header for the Credits tab of the About screen"))
.arg(tr("Light works 3D would not be possible without the contributions of:")) .arg(tr("FreeCAD would not be possible without the contributions of:"))
.arg(tr("Individuals", "Header for the list of individual people in the Credits list.")); .arg(tr("Individuals", "Header for the list of individual people in the Credits list."));
QTextStream stream(&creditsFile); QTextStream stream(&creditsFile);

@ -176,7 +176,7 @@ QByteArray PythonOnlineHelp::fileNotFound() const
"<table width=\"100%\" cellspacing=0 cellpadding=2 border=0 summary=\"heading\">" "<table width=\"100%\" cellspacing=0 cellpadding=2 border=0 summary=\"heading\">"
"<tr bgcolor=\"#7799ee\">" "<tr bgcolor=\"#7799ee\">"
"<td valign=bottom>&nbsp;<br>" "<td valign=bottom>&nbsp;<br>"
"<font color=\"#ffffff\" face=\"helvetica, arial\">&nbsp;<br><big><big><strong>Light works 3D " "<font color=\"#ffffff\" face=\"helvetica, arial\">&nbsp;<br><big><big><strong>FreeCAD "
"Documentation</strong></big></big></font></td>" "Documentation</strong></big></big></font></td>"
"<td align=right valign=bottom>" "<td align=right valign=bottom>"
"<font color=\"#ffffff\" face=\"helvetica, arial\">&nbsp;</font></td></tr></table>" "<font color=\"#ffffff\" face=\"helvetica, arial\">&nbsp;</font></td></tr></table>"
@ -211,7 +211,7 @@ QByteArray PythonOnlineHelp::loadFailed(const QString& error) const
"<tr bgcolor=\"#7799ee\">" "<tr bgcolor=\"#7799ee\">"
"<td valign=bottom>&nbsp;<br>" "<td valign=bottom>&nbsp;<br>"
"<font color=\"#ffffff\" face=\"helvetica, " "<font color=\"#ffffff\" face=\"helvetica, "
"arial\">&nbsp;<br><big><big><strong>Light works 3D " "arial\">&nbsp;<br><big><big><strong>FreeCAD "
"Documentation</strong></big></big></font></td>" "Documentation</strong></big></big></font></td>"
"<td align=right valign=bottom>" "<td align=right valign=bottom>"
"<font color=\"#ffffff\" face=\"helvetica, arial\">&nbsp;</font></td></tr></table>" "<font color=\"#ffffff\" face=\"helvetica, arial\">&nbsp;</font></td></tr></table>"

@ -815,6 +815,13 @@ MenuItem* StdWorkbench::setupMenuBar() const
// Help // Help
auto help = new MenuItem(menuBar); auto help = new MenuItem(menuBar);
help->setCommand("&Help"); help->setCommand("&Help");
*help << "Std_WhatsThis"
<< "Separator"
// Start page and additional separator are dynamically inserted here
<< "Std_FreeCADUserHub" << "Std_FreeCADForum" << "Std_ReportBug" << "Separator"
<< "Std_RestartInSafeMode" << "Separator"
<< "Std_DevHandbook" << "Std_PythonHelp" << "Separator"
<< "Std_FreeCADWebsite" << "Std_FreeCADDonation" << "Std_About";
return menuBar; return menuBar;
} }
@ -872,6 +879,11 @@ ToolBarItem* StdWorkbench::setupToolBars() const
structure->setCommand("Structure"); structure->setCommand("Structure");
*structure << "Std_Part" << "Std_Group" << "Std_LinkActions" << "Std_VarSet"; *structure << "Std_Part" << "Std_Group" << "Std_LinkActions" << "Std_VarSet";
// Help
auto help = new ToolBarItem(root);
help->setCommand("Help");
*help << "Std_WhatsThis";
return root; return root;
} }
@ -1015,6 +1027,7 @@ MenuItem* NoneWorkbench::setupMenuBar() const
// Help // Help
auto help = new MenuItem(menuBar); auto help = new MenuItem(menuBar);
help->setCommand("&Help"); help->setCommand("&Help");
*help << "Std_OnlineHelp" << "Std_About";
return menuBar; return menuBar;
} }
@ -1054,9 +1067,7 @@ MenuItem* TestWorkbench::setupMenuBar() const
MenuItem* menuBar = StdWorkbench::setupMenuBar(); MenuItem* menuBar = StdWorkbench::setupMenuBar();
MenuItem* item = menuBar->findItem("&Help"); MenuItem* item = menuBar->findItem("&Help");
if (auto whatsThis = item->findItem("Std_WhatsThis")) { item->removeItem(item->findItem("Std_WhatsThis"));
item->removeItem(whatsThis);
}
// Test commands // Test commands
auto test = new MenuItem; auto test = new MenuItem;

@ -55,8 +55,8 @@
void PrintInitHelp(); void PrintInitHelp();
const auto sBanner = fmt::format( const auto sBanner = fmt::format(
"(C) 2001-{} LightWorks contributors\n" "(C) 2001-{} FreeCAD contributors\n"
"Light works 3D is free and open-source software licensed under the terms of LGPL2+ license.\n\n", "FreeCAD is free and open-source software licensed under the terms of LGPL2+ license.\n\n",
FCCopyrightYear FCCopyrightYear
); );
@ -188,10 +188,10 @@ int main(int argc, char** argv)
#endif #endif
// Name and Version of the Application // Name and Version of the Application
App::Application::Config()["ExeName"] = "Light works 3D"; App::Application::Config()["ExeName"] = "FreeCAD";
App::Application::Config()["ExeVendor"] = "LightWorks"; App::Application::Config()["ExeVendor"] = "FreeCAD";
App::Application::Config()["AppDataSkipVendor"] = "true"; App::Application::Config()["AppDataSkipVendor"] = "true";
App::Application::Config()["MaintainerUrl"] = ""; App::Application::Config()["MaintainerUrl"] = "https://freecad.org";
// set the banner (for logging and console) // set the banner (for logging and console)
App::Application::Config()["CopyrightInfo"] = sBanner; App::Application::Config()["CopyrightInfo"] = sBanner;
@ -207,7 +207,7 @@ int main(int argc, char** argv)
App::Application::Config()["SplashWarningColor"] = "#CA333B"; App::Application::Config()["SplashWarningColor"] = "#CA333B";
App::Application::Config()["SplashInfoColor"] = "#000000"; App::Application::Config()["SplashInfoColor"] = "#000000";
App::Application::Config()["SplashInfoPosition"] = "6,75"; App::Application::Config()["SplashInfoPosition"] = "6,75";
App::Application::Config()["DesktopFileName"] = "com.lightworks.LightWorks3D"; App::Application::Config()["DesktopFileName"] = "org.freecad.FreeCAD";
try { try {
// Init phase =========================================================== // Init phase ===========================================================

@ -22,8 +22,6 @@ if(BUILD_DRAFT)
add_subdirectory(Draft) add_subdirectory(Draft)
endif(BUILD_DRAFT) endif(BUILD_DRAFT)
add_subdirectory(FreeCADExchange)
if(BUILD_FEM) if(BUILD_FEM)
add_subdirectory(Fem) add_subdirectory(Fem)
endif(BUILD_FEM) endif(BUILD_FEM)

@ -1,25 +0,0 @@
set(FreeCADExchange_Scripts
__init__.py
Init.py
InitGui.py
ExchangeBootstrap.py
DeviceImport.py
DevicePreview.py
)
add_custom_target(FreeCADExchangeScripts ALL
SOURCES ${FreeCADExchange_Scripts}
)
fc_target_copy_resource(FreeCADExchangeScripts
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_BINARY_DIR}/Mod/FreeCADExchange
${FreeCADExchange_Scripts}
)
install(
FILES
${FreeCADExchange_Scripts}
DESTINATION
Mod/FreeCADExchange
)

@ -1,517 +0,0 @@
import os
from pathlib import Path
import FreeCAD as App
import FreeCADGui as Gui
import ImportGui
import DevicePreview
ROOT_GROUP_NAME = "QETExchangeDevices"
ROOT_GROUP_LABEL = "QET Exchange Devices"
CABINET_MODEL_GROUP_NAME = "QETCabinetModel"
DEVICE_GROUP_PREFIX = "QETDevice_"
class DeviceImportError(RuntimeError):
pass
def _debug_log_path():
local_app_data = os.environ.get("LOCALAPPDATA", "").strip()
if local_app_data:
return os.path.join(local_app_data, "QETDeps", "freecad_exchange_bootstrap.log")
return os.path.join(str(Path.home()), "AppData", "Local", "QETDeps", "freecad_exchange_bootstrap.log")
def _append_debug_log(message):
try:
log_path = _debug_log_path()
os.makedirs(os.path.dirname(log_path), exist_ok=True)
with open(log_path, "a", encoding="utf-8") as handle:
handle.write(message + "\n")
except Exception:
pass
def _safe_token(value):
text = (value or "").strip()
if not text:
return "unknown"
chars = []
for ch in text:
if ch.isalnum():
chars.append(ch)
else:
chars.append("_")
return "".join(chars)
def _native_path(value):
text = (value or "").strip()
if not text:
return ""
return os.path.normpath(os.path.expandvars(os.path.expanduser(text)))
def _existing_object_names(doc):
return {obj.Name for obj in doc.Objects}
def _new_objects_since(doc, before_names):
return [obj for obj in doc.Objects if obj.Name not in before_names]
def _top_level_imported_objects(imported_objects):
imported_by_name = {obj.Name: obj for obj in imported_objects}
child_names = set()
for obj in imported_objects:
for parent in list(getattr(obj, "InList", []) or []):
if getattr(parent, "Name", None) in imported_by_name:
child_names.add(obj.Name)
for child in list(getattr(obj, "Group", []) or []):
if getattr(child, "Name", None) in imported_by_name:
child_names.add(child.Name)
return [obj for obj in imported_objects if obj.Name not in child_names]
def _ensure_string_property(obj, prop_name, group_name, description, value):
if prop_name not in getattr(obj, "PropertiesList", []):
obj.addProperty("App::PropertyString", prop_name, group_name, description)
setattr(obj, prop_name, value or "")
def _ensure_bool_property(obj, prop_name, group_name, description, value):
if prop_name not in getattr(obj, "PropertiesList", []):
obj.addProperty("App::PropertyBool", prop_name, group_name, description)
setattr(obj, prop_name, bool(value))
def _ensure_document(scene_path):
preferred_name = _safe_token(Path(scene_path).stem if scene_path else "QETScene")[:48] or "QETScene"
existing_doc = DevicePreview.find_main_exchange_document(preferred_name)
if existing_doc is not None:
return existing_doc
return App.newDocument(preferred_name)
def _cabinet_label_text(cabinet):
if not isinstance(cabinet, dict):
return "QET Cabinet"
label = (cabinet.get("display_text") or "").strip()
if label:
return label
label = (cabinet.get("label") or "").strip()
if label:
return label
label = (cabinet.get("name") or "").strip()
if label:
return label
return "QET Cabinet"
def _ensure_root_group(doc, cabinet=None):
root = doc.getObject(ROOT_GROUP_NAME)
if root is None:
root = doc.addObject("App::DocumentObjectGroup", ROOT_GROUP_NAME)
if isinstance(cabinet, dict):
root.Label = _cabinet_label_text(cabinet)
else:
root.Label = ROOT_GROUP_LABEL
_ensure_string_property(
root,
"QetCabinetLabel",
"QET Exchange",
"Cabinet label from QET exchange",
cabinet.get("label", "") if isinstance(cabinet, dict) else "",
)
_ensure_string_property(
root,
"QetCabinetName",
"QET Exchange",
"Cabinet name from QET exchange",
cabinet.get("name", "") if isinstance(cabinet, dict) else "",
)
_ensure_string_property(
root,
"QetCabinetDisplayText",
"QET Exchange",
"Cabinet display text from QET exchange",
cabinet.get("display_text", "") if isinstance(cabinet, dict) else "",
)
_ensure_string_property(
root,
"QetCabinetFileSet",
"QET Exchange",
"Associated fileset from QET exchange",
cabinet.get("associated_fileset", "") if isinstance(cabinet, dict) else "",
)
_ensure_string_property(
root,
"QetCabinetRelativePath",
"QET Exchange",
"Relative 3D cabinet path from QET exchange",
cabinet.get("three_d_relative_path", "") if isinstance(cabinet, dict) else "",
)
_ensure_string_property(
root,
"QetCabinetResolvedScenePath",
"QET Exchange",
"Resolved local cabinet scene path from QET exchange",
cabinet.get("resolved_scene_path", "") if isinstance(cabinet, dict) else "",
)
_ensure_string_property(
root,
"QetCabinetLocationId",
"QET Exchange",
"Cabinet location id from QET exchange",
str(cabinet.get("location_id") or "") if isinstance(cabinet, dict) else "",
)
return root
def _ensure_cabinet_model_group(doc, root_group):
group = doc.getObject(CABINET_MODEL_GROUP_NAME)
if group is None:
group = doc.addObject("App::DocumentObjectGroup", CABINET_MODEL_GROUP_NAME)
group.Label = "3D机柜"
if group not in getattr(root_group, "Group", []):
root_group.addObject(group)
return group
def _find_device_group(doc, element_uuid):
target_uuid = (element_uuid or "").strip()
if not target_uuid:
return None
preferred_name = DEVICE_GROUP_PREFIX + _safe_token(target_uuid)
obj = doc.getObject(preferred_name)
if obj is not None:
return obj
for candidate in doc.Objects:
if "QetElementUuid" in getattr(candidate, "PropertiesList", []):
if getattr(candidate, "QetElementUuid", "").strip() == target_uuid:
return candidate
return None
def _device_label_text(display_tag, instance_id, element_uuid):
label = (display_tag or "").strip()
if label:
return label
fallback = (instance_id or "").strip() or (element_uuid or "").strip()
if fallback:
return fallback
return "QET Device"
def _ensure_device_group(doc, root_group, element_uuid, instance_id, model_path, display_tag, layout_index):
created_now = False
device_group = _find_device_group(doc, element_uuid)
if device_group is not None and getattr(device_group, "TypeId", "") != "App::Part":
_remove_object_tree(doc, device_group)
device_group = None
if device_group is None:
device_group = doc.addObject(
"App::Part",
DEVICE_GROUP_PREFIX + _safe_token(element_uuid),
)
created_now = True
if device_group not in getattr(root_group, "Group", []):
root_group.addObject(device_group)
device_group.Label = _device_label_text(display_tag, instance_id, element_uuid)
_ensure_string_property(
device_group,
"QetElementUuid",
"QET Exchange",
"2D element UUID from QET",
element_uuid,
)
_ensure_string_property(
device_group,
"QetInstanceId",
"QET Exchange",
"3D instance id from QET/FreeCAD exchange",
instance_id,
)
_ensure_string_property(
device_group,
"QetResolvedModelPath",
"QET Exchange",
"Resolved local model path from QET exchange",
model_path,
)
_ensure_string_property(
device_group,
"QetDisplayTag",
"QET Exchange",
"2D display tag from QET exchange",
display_tag,
)
_ensure_bool_property(
device_group,
"QetAutoPlaced",
"QET Exchange",
"Whether the device has been placed by the QET auto layout.",
created_now,
)
if created_now:
device_group.Placement = App.Placement()
return device_group, created_now
def _remove_object_tree(doc, obj):
if obj is None:
return
children = list(getattr(obj, "Group", []) or [])
for child in children:
_remove_object_tree(doc, child)
if doc.getObject(obj.Name) is not None:
doc.removeObject(obj.Name)
def _clear_group_contents(doc, group):
for child in list(getattr(group, "Group", []) or []):
_remove_object_tree(doc, child)
def _supported_for_import(model_path):
suffix = Path(model_path).suffix.lower()
return suffix in {
".step",
".stp",
".iges",
".igs",
".brep",
".brp",
".fcstd",
}
def _import_model_into_group(doc, device_group, model_path, merge=False, use_link_group=True):
before_names = _existing_object_names(doc)
try:
ImportGui.insert(
name=model_path,
docName=doc.Name,
merge=bool(merge),
useLinkGroup=bool(use_link_group),
)
except Exception:
for obj in _new_objects_since(doc, before_names):
_remove_object_tree(doc, obj)
raise
imported_objects = _new_objects_since(doc, before_names)
top_level_objects = _top_level_imported_objects(imported_objects)
for obj in top_level_objects:
if obj not in getattr(device_group, "Group", []):
device_group.addObject(obj)
return top_level_objects
def _model_index(payload):
index = {}
for item in payload.get("device_models", []):
element_uuid = item.get("element_uuid", "").strip()
if element_uuid and element_uuid not in index:
index[element_uuid] = item
return index
def _import_cabinet_model(doc, root_group, cabinet, report):
if not isinstance(cabinet, dict):
return
resolved_scene_path = _native_path(cabinet.get("resolved_scene_path", ""))
_append_debug_log(
"DeviceImport cabinet resolved_scene_path={0}".format(resolved_scene_path)
)
if not resolved_scene_path:
report["cabinet_skipped_missing_model"] += 1
return
if not os.path.isfile(resolved_scene_path):
report["cabinet_skipped_missing_file"] += 1
report["warnings"].append(
"机柜 3D 文件不存在:{0}".format(resolved_scene_path)
)
return
if not _supported_for_import(resolved_scene_path):
report["cabinet_skipped_unsupported_format"] += 1
report["warnings"].append(
"机柜 3D 文件格式暂不支持:{0}".format(resolved_scene_path)
)
return
cabinet_group = _ensure_cabinet_model_group(doc, root_group)
_clear_group_contents(doc, cabinet_group)
_ensure_string_property(
cabinet_group,
"QetCabinetResolvedScenePath",
"QET Exchange",
"Resolved local cabinet scene path from QET exchange",
resolved_scene_path,
)
try:
_append_debug_log(
"DeviceImport importing cabinet model: {0}".format(
resolved_scene_path
)
)
_import_model_into_group(
doc,
cabinet_group,
resolved_scene_path,
merge=False,
use_link_group=True,
)
report["cabinet_imported"] += 1
_append_debug_log("DeviceImport cabinet import succeeded")
except Exception as exc:
report["cabinet_skipped_import_error"] += 1
report["warnings"].append(
"机柜 3D 导入失败:{0}".format(exc)
)
_append_debug_log(
"DeviceImport cabinet import failed: {0}".format(exc)
)
def import_devices_from_payload(payload, scene_path=""):
_append_debug_log("DeviceImport.import_devices_from_payload entered")
doc = _ensure_document(scene_path)
cabinet = payload.get("cabinet")
root_group = _ensure_root_group(doc, cabinet)
models_by_element = _model_index(payload)
report = {
"document_name": doc.Name,
"scene_path": scene_path or "",
"total_devices": 0,
"imported_devices": 0,
"updated_devices": 0,
"imported_without_instance_id": 0,
"skipped_missing_model": 0,
"skipped_missing_file": 0,
"skipped_unsupported_format": 0,
"skipped_import_error": 0,
"cabinet_imported": 0,
"cabinet_skipped_missing_model": 0,
"cabinet_skipped_missing_file": 0,
"cabinet_skipped_unsupported_format": 0,
"cabinet_skipped_import_error": 0,
"warnings": [],
}
_import_cabinet_model(doc, root_group, cabinet, report)
for index, device in enumerate(payload.get("devices", [])):
report["total_devices"] += 1
element_uuid = device.get("element_uuid", "").strip()
instance_id = (device.get("instance_id") or "").strip()
display_tag = (device.get("display_tag") or "").strip()
model_info = models_by_element.get(element_uuid, {})
resolved_model_path = _native_path(model_info.get("resolved_model_path", ""))
_append_debug_log(
"DeviceImport device element_uuid={0}, instance_id={1}, display_tag={2}, resolved_model_path={3}".format(
element_uuid, instance_id, display_tag, resolved_model_path
)
)
if not resolved_model_path:
report["skipped_missing_model"] += 1
report["warnings"].append(
"设备 {0} 缺少 resolved_model_path已跳过。".format(element_uuid)
)
continue
if not os.path.isfile(resolved_model_path):
report["skipped_missing_file"] += 1
report["warnings"].append(
"设备 {0} 的模型文件不存在:{1}".format(element_uuid, resolved_model_path)
)
continue
if not _supported_for_import(resolved_model_path):
report["skipped_unsupported_format"] += 1
report["warnings"].append(
"设备 {0} 的模型格式暂不支持:{1}".format(element_uuid, resolved_model_path)
)
continue
existing_group = _find_device_group(doc, element_uuid)
device_group, created_now = _ensure_device_group(
doc,
root_group,
element_uuid,
instance_id,
resolved_model_path,
display_tag,
index,
)
_clear_group_contents(doc, device_group)
try:
_append_debug_log(
"DeviceImport importing model for element_uuid={0}: {1}".format(
element_uuid, resolved_model_path
)
)
_import_model_into_group(doc, device_group, resolved_model_path)
_append_debug_log(
"DeviceImport import succeeded for element_uuid={0}".format(element_uuid)
)
except Exception as exc:
report["skipped_import_error"] += 1
report["warnings"].append(
"设备 {0} 导入失败:{1}".format(element_uuid, exc)
)
_append_debug_log(
"DeviceImport import failed for element_uuid={0}: {1}".format(
element_uuid, exc
)
)
continue
if created_now or existing_group is None:
report["imported_devices"] += 1
else:
report["updated_devices"] += 1
if not instance_id:
report["imported_without_instance_id"] += 1
doc.recompute()
try:
Gui.SendMsgToActiveView("ViewFit")
except Exception:
pass
_append_debug_log(
"DeviceImport finished: cabinet_imported={0}, imported={1}, updated={2}, skipped_missing_model={3}, skipped_missing_file={4}, skipped_import_error={5}".format(
report["cabinet_imported"],
report["imported_devices"],
report["updated_devices"],
report["skipped_missing_model"],
report["skipped_missing_file"],
report["skipped_import_error"],
)
)
return report

@ -1,241 +0,0 @@
import os
from pathlib import Path
import FreeCAD as App
import FreeCADGui as Gui
DEVICE_GROUP_PREFIX = "QETDevice_"
PREVIEW_DOC_PREFIX = "QETPreview_"
ROOT_GROUP_NAME = "QETExchangeDevices"
def _debug_log_path():
local_app_data = os.environ.get("LOCALAPPDATA", "").strip()
if local_app_data:
return os.path.join(local_app_data, "QETDeps", "freecad_exchange_bootstrap.log")
return os.path.join(str(Path.home()), "AppData", "Local", "QETDeps", "freecad_exchange_bootstrap.log")
def _append_debug_log(message):
try:
log_path = _debug_log_path()
os.makedirs(os.path.dirname(log_path), exist_ok=True)
with open(log_path, "a", encoding="utf-8") as handle:
handle.write(message + "\n")
except Exception:
pass
def _safe_token(value):
text = (value or "").strip()
if not text:
return "unknown"
chars = []
for ch in text:
if ch.isalnum():
chars.append(ch)
else:
chars.append("_")
return "".join(chars)
def is_preview_document_name(doc_name):
return str(doc_name or "").startswith(PREVIEW_DOC_PREFIX)
def _get_document_if_exists(doc_name):
try:
return App.getDocument(doc_name)
except Exception:
return None
def find_main_exchange_document(preferred_name=""):
preferred_name = (preferred_name or "").strip()
if preferred_name:
preferred_doc = _get_document_if_exists(preferred_name)
if preferred_doc is not None and not is_preview_document_name(preferred_doc.Name):
return preferred_doc
active_doc = App.ActiveDocument
if (
active_doc is not None
and not is_preview_document_name(active_doc.Name)
and active_doc.getObject(ROOT_GROUP_NAME) is not None
):
return active_doc
for candidate in App.listDocuments().values():
if (
candidate is not None
and not is_preview_document_name(candidate.Name)
and candidate.getObject(ROOT_GROUP_NAME) is not None
):
return candidate
if active_doc is not None and not is_preview_document_name(active_doc.Name):
return active_doc
return None
def is_qet_device_object(obj):
if obj is None:
return False
if getattr(obj, "TypeId", "") != "App::Part":
return False
if not getattr(obj, "Name", "").startswith(DEVICE_GROUP_PREFIX):
return False
return "QetElementUuid" in getattr(obj, "PropertiesList", [])
def find_parent_qet_device_object(obj):
"""
从当前选中对象向上查找所属的 QETDevice_xxx 设备组
作用
1. 双击 QETDevice_xxx 本身可以预览
2. 双击 QETDevice_xxx 下面的子实体也可以自动找到父设备并预览
"""
if obj is None:
_append_debug_log("DevicePreview resolve parent: selected object is None")
return None
if is_qet_device_object(obj):
_append_debug_log(
"DevicePreview resolve parent: selected object is direct device {0}".format(
getattr(obj, "Name", "")
)
)
return obj
visited = set()
stack = list(getattr(obj, "InList", []) or [])
while stack:
parent = stack.pop()
if parent is None:
continue
parent_name = getattr(parent, "Name", "")
if parent_name in visited:
continue
visited.add(parent_name)
if is_qet_device_object(parent):
_append_debug_log(
"DevicePreview resolve parent: found parent device {0} from child {1}".format(
getattr(parent, "Name", ""),
getattr(obj, "Name", ""),
)
)
return parent
stack.extend(list(getattr(parent, "InList", []) or []))
_append_debug_log(
"DevicePreview resolve parent: no device parent found for {0}".format(
getattr(obj, "Name", "")
)
)
return None
def _preview_document_name(device_group):
display_tag = getattr(device_group, "QetDisplayTag", "")
element_uuid = getattr(device_group, "QetElementUuid", "")
display_token = _safe_token(display_tag)[:24]
uuid_token = _safe_token(element_uuid)[:8] or _safe_token(device_group.Name)[-8:]
if display_token:
return "{0}{1}_{2}".format(PREVIEW_DOC_PREFIX, display_token, uuid_token)
return "{0}{1}".format(PREVIEW_DOC_PREFIX, uuid_token)
def _clear_document_objects(doc):
while getattr(doc, "Objects", []):
doc.removeObject(doc.Objects[-1].Name)
def _activate_document(doc):
App.setActiveDocument(doc.Name)
App.ActiveDocument = doc
try:
Gui.ActiveDocument = Gui.getDocument(doc.Name)
except Exception:
pass
def _fit_active_view():
try:
active_view = Gui.activeDocument().activeView()
active_view.viewIsometric()
Gui.SendMsgToActiveView("ViewFit")
except Exception:
pass
def _set_visibility_recursive(obj, visible):
if obj is None:
return
try:
view_object = getattr(obj, "ViewObject", None)
if view_object is not None and hasattr(view_object, "Visibility"):
view_object.Visibility = bool(visible)
except Exception:
pass
for child in list(getattr(obj, "Group", []) or []):
_set_visibility_recursive(child, visible)
def _open_device_preview(device_group):
doc_name = _preview_document_name(device_group)
_append_debug_log(
"DevicePreview opening preview doc={0} for device={1}".format(
doc_name,
getattr(device_group, "Name", ""),
)
)
preview_doc = _get_document_if_exists(doc_name)
if preview_doc is None:
preview_doc = App.newDocument(doc_name)
_append_debug_log("DevicePreview created new preview document")
else:
_append_debug_log("DevicePreview reusing existing preview document")
_clear_document_objects(preview_doc)
copied_device = preview_doc.copyObject(device_group, True)
copied_device.Label = device_group.Label
if hasattr(copied_device, "Placement"):
copied_device.Placement = App.Placement()
_set_visibility_recursive(copied_device, True)
preview_doc.recompute()
_activate_document(preview_doc)
_fit_active_view()
_append_debug_log(
"DevicePreview preview ready: doc={0}, copied_name={1}, copied_label={2}".format(
preview_doc.Name,
getattr(copied_device, "Name", ""),
getattr(copied_device, "Label", ""),
)
)
return preview_doc
def open_preview_for_device_object(obj):
if not is_qet_device_object(obj):
return None
if is_preview_document_name(getattr(getattr(obj, "Document", None), "Name", "")):
return None
_append_debug_log(
"DevicePreview open requested element_uuid={0}, label={1}".format(
getattr(obj, "QetElementUuid", ""),
getattr(obj, "Label", ""),
)
)
return _open_device_preview(obj)

@ -1,749 +0,0 @@
import json
import traceback
import os
from pathlib import Path
import FreeCAD as App
import FreeCADGui as Gui
import DeviceImport
import DevicePreview
try:
from PySide6 import QtCore, QtWidgets
except ImportError:
try:
from PySide2 import QtCore, QtWidgets
except ImportError:
from PySide import QtCore
from PySide import QtGui as QtWidgets
ENV_JSON_PATH = "QET_2D_TO_3D_JSON"
ENV_SCENE_PATH = "QET_FREECAD_SCENE_FILE"
STATE_FLAG = "_qet_exchange_bootstrapped"
STATE_PAYLOAD = "_qet_exchange_payload"
STATE_SUMMARY = "_qet_exchange_summary"
STATE_IMPORT_REPORT = "_qet_exchange_import_report"
STATE_IMPORT_SCHEDULED = "_qet_exchange_import_scheduled"
STATE_TREE_FILTER = "_qet_exchange_tree_filter"
STATE_TREE_SIGNAL_CONNECTIONS = "_qet_exchange_tree_signal_connections"
TREE_FILTER_MARKER = "_qet_exchange_tree_filter_installed"
TREE_SIGNAL_MARKER = "_qet_exchange_tree_signal_installed"
IMPORT_READY_DELAY_MS = 1500
IMPORT_READY_RETRY_DELAY_MS = 1000
IMPORT_READY_MAX_RETRIES = 10
class ExchangeValidationError(RuntimeError):
pass
def _debug_log_path():
local_app_data = os.environ.get("LOCALAPPDATA", "").strip()
if local_app_data:
return os.path.join(local_app_data, "QETDeps", "freecad_exchange_bootstrap.log")
return os.path.join(str(Path.home()), "AppData", "Local", "QETDeps", "freecad_exchange_bootstrap.log")
def _append_debug_log(message):
try:
log_path = _debug_log_path()
os.makedirs(os.path.dirname(log_path), exist_ok=True)
with open(log_path, "a", encoding="utf-8") as handle:
handle.write(message + "\n")
except Exception:
pass
def _reset_debug_log():
try:
log_path = _debug_log_path()
os.makedirs(os.path.dirname(log_path), exist_ok=True)
with open(log_path, "w", encoding="utf-8") as handle:
handle.write("[QET Exchange] new debug session\n")
except Exception:
pass
def _get_main_window():
try:
return Gui.getMainWindow()
except Exception:
return None
def _show_info(title, message):
QtWidgets.QMessageBox.information(_get_main_window(), title, message)
def _show_error(title, message):
QtWidgets.QMessageBox.critical(_get_main_window(), title, message)
def _qt_class_name(widget):
try:
return widget.metaObject().className()
except Exception:
return ""
def _has_tree_widget_parent(widget):
current = widget
while current is not None:
class_name = _qt_class_name(current)
if "TreeWidget" in class_name or class_name.endswith("QTreeWidget") or class_name.endswith("QTreeView"):
return True
current = current.parent()
return False
class _DeviceTreeDoubleClickFilter(QtCore.QObject):
def eventFilter(self, watched, event):
try:
if event.type() != QtCore.QEvent.MouseButtonDblClick:
return False
_append_debug_log(
"tree double click captured: watched_class={0}".format(
_qt_class_name(watched)
)
)
QtCore.QTimer.singleShot(0, _open_selected_device_preview_if_needed)
except Exception as exc:
_append_debug_log(
"tree double click filter failed: {0}".format(exc)
)
return False
class _DeviceTreeSignalBridge(QtCore.QObject):
def on_item_double_clicked(self, *args):
_append_debug_log("tree double click signal received: itemDoubleClicked")
QtCore.QTimer.singleShot(0, _open_selected_device_preview_if_needed)
def on_index_double_clicked(self, *args):
_append_debug_log("tree double click signal received: doubleClicked")
QtCore.QTimer.singleShot(0, _open_selected_device_preview_if_needed)
def _install_tree_double_click_filter():
main_window = _get_main_window()
if main_window is None:
return
if getattr(Gui, STATE_TREE_FILTER, None) is not None:
tree_filter = getattr(Gui, STATE_TREE_FILTER)
else:
tree_filter = _DeviceTreeDoubleClickFilter(main_window)
setattr(Gui, STATE_TREE_FILTER, tree_filter)
if getattr(Gui, STATE_TREE_SIGNAL_CONNECTIONS, None) is not None:
signal_bridge = getattr(Gui, STATE_TREE_SIGNAL_CONNECTIONS)
else:
signal_bridge = _DeviceTreeSignalBridge(main_window)
setattr(Gui, STATE_TREE_SIGNAL_CONNECTIONS, signal_bridge)
installed_count = 0
signal_count = 0
for widget in main_window.findChildren(QtWidgets.QWidget):
class_name = _qt_class_name(widget)
if "TreeWidget" not in class_name and not class_name.endswith("QTreeWidget") and not class_name.endswith("QTreeView"):
continue
targets = [widget]
viewport = getattr(widget, "viewport", lambda: None)()
if viewport is not None:
targets.append(viewport)
for target in targets:
if target is None:
continue
if bool(target.property(TREE_FILTER_MARKER)):
continue
target.installEventFilter(tree_filter)
target.setProperty(TREE_FILTER_MARKER, True)
installed_count += 1
_append_debug_log(
"tree double click filter attached: class={0}".format(
_qt_class_name(target)
)
)
if not bool(widget.property(TREE_SIGNAL_MARKER)):
connected = False
item_double_clicked = getattr(widget, "itemDoubleClicked", None)
if item_double_clicked is not None:
try:
item_double_clicked.connect(signal_bridge.on_item_double_clicked)
connected = True
signal_count += 1
_append_debug_log(
"tree double click signal connected: class={0}, signal=itemDoubleClicked".format(
class_name
)
)
except Exception as exc:
_append_debug_log(
"tree double click signal connect failed: class={0}, signal=itemDoubleClicked, error={1}".format(
class_name, exc
)
)
view_double_clicked = getattr(widget, "doubleClicked", None)
if view_double_clicked is not None:
try:
view_double_clicked.connect(signal_bridge.on_index_double_clicked)
connected = True
signal_count += 1
_append_debug_log(
"tree double click signal connected: class={0}, signal=doubleClicked".format(
class_name
)
)
except Exception as exc:
_append_debug_log(
"tree double click signal connect failed: class={0}, signal=doubleClicked, error={1}".format(
class_name, exc
)
)
if connected:
widget.setProperty(TREE_SIGNAL_MARKER, True)
_append_debug_log(
"tree double click filter install pass complete: attached={0}, signal_connections={1}".format(
installed_count,
signal_count,
)
)
def _open_selected_device_preview_if_needed():
try:
selection = Gui.Selection.getSelection()
_append_debug_log(
"tree double click selection count={0}".format(len(selection))
)
if len(selection) != 1:
return
obj = selection[0]
_append_debug_log(
"tree double click selected object: name={0}, label={1}, type={2}, doc={3}".format(
getattr(obj, "Name", ""),
getattr(obj, "Label", ""),
getattr(obj, "TypeId", ""),
getattr(getattr(obj, "Document", None), "Name", ""),
)
)
device_obj = DevicePreview.find_parent_qet_device_object(obj)
_append_debug_log(
"tree double click resolved device object: name={0}, label={1}, doc={2}".format(
getattr(device_obj, "Name", "") if device_obj else "",
getattr(device_obj, "Label", "") if device_obj else "",
getattr(getattr(device_obj, "Document", None), "Name", "") if device_obj else "",
)
)
if device_obj is None:
return
if DevicePreview.is_preview_document_name(
getattr(getattr(device_obj, "Document", None), "Name", "")
):
_append_debug_log("tree double click ignored inside preview document")
return
DevicePreview.open_preview_for_device_object(device_obj)
_append_debug_log("tree double click preview open requested")
except Exception as exc:
_append_debug_log(
"open selected device preview failed: {0}".format(exc)
)
def _is_gui_ready():
main_window = _get_main_window()
if main_window is None:
return False
try:
return bool(main_window.isVisible())
except Exception:
return False
def _require_string(payload, field_name):
value = payload.get(field_name)
if not isinstance(value, str) or not value.strip():
raise ExchangeValidationError(
"Field '{0}' must be a non-empty string.".format(field_name)
)
return value.strip()
def _normalize_instance_id(item):
value = item.get("instance_id", "")
if value is None:
return ""
if not isinstance(value, str):
raise ExchangeValidationError(
"Field 'instance_id' must be a string when present."
)
return value.strip()
def _normalize_devices(payload):
devices = payload.get("devices", [])
if not isinstance(devices, list):
raise ExchangeValidationError("Field 'devices' must be a list.")
normalized = []
for index, item in enumerate(devices):
if not isinstance(item, dict):
raise ExchangeValidationError(
"Device entry #{0} must be an object.".format(index)
)
element_uuid = _require_string(item, "element_uuid")
display_tag = item.get("display_tag", "")
if display_tag and not isinstance(display_tag, str):
raise ExchangeValidationError(
"Field 'display_tag' in device entry #{0} must be a string.".format(
index
)
)
normalized.append(
{
"element_uuid": element_uuid,
"instance_id": _normalize_instance_id(item),
"display_tag": display_tag.strip() if isinstance(display_tag, str) else "",
}
)
return normalized
def _normalize_terminals(payload):
terminals = payload.get("terminals", [])
if not isinstance(terminals, list):
raise ExchangeValidationError("Field 'terminals' must be a list.")
normalized = []
for index, item in enumerate(terminals):
if not isinstance(item, dict):
raise ExchangeValidationError(
"Terminal entry #{0} must be an object.".format(index)
)
terminal_uuid = _require_string(item, "terminal_uuid")
element_uuid = item.get("element_uuid", "")
if element_uuid and not isinstance(element_uuid, str):
raise ExchangeValidationError(
"Field 'element_uuid' in terminal entry #{0} must be a string.".format(
index
)
)
normalized.append(
{
"terminal_uuid": terminal_uuid,
"instance_id": _normalize_instance_id(item),
"element_uuid": element_uuid.strip() if isinstance(element_uuid, str) else "",
}
)
return normalized
def _normalize_device_models(payload):
models = payload.get("device_models", [])
if not isinstance(models, list):
raise ExchangeValidationError("Field 'device_models' must be a list.")
normalized = []
for index, item in enumerate(models):
if not isinstance(item, dict):
raise ExchangeValidationError(
"Device model entry #{0} must be an object.".format(index)
)
element_uuid = _require_string(item, "element_uuid")
parts_3d = item.get("parts_3d", "")
if parts_3d and not isinstance(parts_3d, str):
raise ExchangeValidationError(
"Field 'parts_3d' in device model entry #{0} must be a string.".format(
index
)
)
resolved_model_path = item.get("resolved_model_path", "")
if resolved_model_path and not isinstance(resolved_model_path, str):
raise ExchangeValidationError(
"Field 'resolved_model_path' in device model entry #{0} must be a string.".format(
index
)
)
device_id = item.get("device_id")
if device_id is not None and not isinstance(device_id, int):
raise ExchangeValidationError(
"Field 'device_id' in device model entry #{0} must be an integer.".format(
index
)
)
normalized.append(
{
"element_uuid": element_uuid,
"device_id": device_id,
"parts_3d": parts_3d.strip() if isinstance(parts_3d, str) else "",
"resolved_model_path": (
resolved_model_path.strip()
if isinstance(resolved_model_path, str)
else ""
),
}
)
return normalized
def _normalize_cabinet(payload):
cabinet = payload.get("cabinet")
if cabinet is None:
return None
if not isinstance(cabinet, dict):
raise ExchangeValidationError("Field 'cabinet' must be an object when present.")
location_id = cabinet.get("location_id")
if location_id is not None and not isinstance(location_id, int):
raise ExchangeValidationError("Field 'location_id' in cabinet must be an integer.")
normalized = {
"location_id": location_id,
"label": "",
"name": "",
"display_text": "",
"associated_fileset": "",
"three_d_relative_path": "",
"resolved_scene_path": "",
}
for field_name in (
"label",
"name",
"display_text",
"associated_fileset",
"three_d_relative_path",
"resolved_scene_path",
):
value = cabinet.get(field_name, "")
if value and not isinstance(value, str):
raise ExchangeValidationError(
"Field '{0}' in cabinet must be a string.".format(field_name)
)
normalized[field_name] = value.strip() if isinstance(value, str) else ""
return normalized
def load_exchange_payload(json_path):
try:
raw_text = Path(json_path).read_text(encoding="utf-8")
except OSError as exc:
raise ExchangeValidationError(
"Failed to read exchange file:\n{0}".format(exc)
) from exc
try:
payload = json.loads(raw_text)
except json.JSONDecodeError as exc:
raise ExchangeValidationError(
"Exchange JSON is invalid:\n{0}".format(exc)
) from exc
if not isinstance(payload, dict):
raise ExchangeValidationError("Exchange JSON root must be an object.")
project_uuid = _require_string(payload, "project_uuid")
schema_version = payload.get("schema_version", "1.0")
if not isinstance(schema_version, str) or not schema_version.strip():
raise ExchangeValidationError("Field 'schema_version' must be a string.")
normalized = {
"schema_version": schema_version.strip(),
"project_uuid": project_uuid,
"generated_at": payload.get("generated_at", ""),
"source": payload.get("source", {}),
"cabinet": _normalize_cabinet(payload),
"devices": _normalize_devices(payload),
"terminals": _normalize_terminals(payload),
"device_models": _normalize_device_models(payload),
}
return normalized
def _build_summary(payload, json_path):
devices = payload["devices"]
terminals = payload["terminals"]
device_models = payload["device_models"]
cabinet = payload.get("cabinet")
missing_device_instances = sum(1 for item in devices if not item["instance_id"])
missing_terminal_instances = sum(
1 for item in terminals if not item["instance_id"]
)
with_model_paths = sum(
1 for item in device_models if item["resolved_model_path"] or item["parts_3d"]
)
return {
"json_path": json_path,
"project_uuid": payload["project_uuid"],
"device_count": len(devices),
"terminal_count": len(terminals),
"device_model_count": len(device_models),
"device_models_with_parts": with_model_paths,
"missing_device_instances": missing_device_instances,
"missing_terminal_instances": missing_terminal_instances,
"cabinet": cabinet,
"scene_path": os.environ.get(ENV_SCENE_PATH, "").strip(),
}
def _summary_message(summary, import_report=None):
lines = [
"QET exchange file loaded successfully.",
"",
"Project UUID: {0}".format(summary["project_uuid"]),
"Exchange file: {0}".format(summary["json_path"]),
"Devices: {0}".format(summary["device_count"]),
"Terminals: {0}".format(summary["terminal_count"]),
"Device models: {0}".format(summary["device_model_count"]),
"Resolved model paths: {0}".format(summary["device_models_with_parts"]),
]
cabinet = summary.get("cabinet")
if isinstance(cabinet, dict):
cabinet_name = cabinet.get("display_text") or cabinet.get("label") or cabinet.get("name")
if cabinet_name:
lines.append("Cabinet: {0}".format(cabinet_name))
if cabinet.get("associated_fileset"):
lines.append("Cabinet fileset: {0}".format(cabinet["associated_fileset"]))
if cabinet.get("three_d_relative_path"):
lines.append(
"Cabinet 3D relative path: {0}".format(
cabinet["three_d_relative_path"]
)
)
if cabinet.get("resolved_scene_path"):
lines.append(
"Cabinet resolved scene path: {0}".format(
cabinet["resolved_scene_path"]
)
)
if summary["missing_device_instances"]:
lines.append(
"Devices without instance_id yet: {0}".format(
summary["missing_device_instances"]
)
)
if summary["missing_terminal_instances"]:
lines.append(
"Terminals without instance_id yet: {0}".format(
summary["missing_terminal_instances"]
)
)
if summary["scene_path"]:
lines.append("Scene file: {0}".format(summary["scene_path"]))
if import_report:
lines.extend(
[
"",
"3D device import summary:",
"Target document: {0}".format(import_report["document_name"]),
"Imported cabinet models: {0}".format(import_report["cabinet_imported"]),
"Imported devices: {0}".format(import_report["imported_devices"]),
"Updated devices: {0}".format(import_report["updated_devices"]),
]
)
if import_report["imported_without_instance_id"]:
lines.append(
"Imported without instance_id yet: {0}".format(
import_report["imported_without_instance_id"]
)
)
if import_report["skipped_missing_model"]:
lines.append(
"Skipped without resolved model path: {0}".format(
import_report["skipped_missing_model"]
)
)
if import_report["skipped_missing_file"]:
lines.append(
"Skipped missing model file: {0}".format(
import_report["skipped_missing_file"]
)
)
if import_report["skipped_unsupported_format"]:
lines.append(
"Skipped unsupported model format: {0}".format(
import_report["skipped_unsupported_format"]
)
)
if import_report["skipped_import_error"]:
lines.append(
"Skipped after import errors: {0}".format(
import_report["skipped_import_error"]
)
)
warnings = import_report.get("warnings", [])
if warnings:
lines.append("")
lines.append("Warnings:")
lines.extend("- {0}".format(item) for item in warnings[:10])
if len(warnings) > 10:
lines.append("- ... ({0} more)".format(len(warnings) - 10))
lines.append("")
lines.append("This step validates the exchange payload and imports devices with valid resolved model paths.")
lines.append("3D terminal creation is not running yet.")
return "\n".join(lines)
def _run_scheduled_device_import(attempt=0):
_append_debug_log(
"scheduled device import invoked: attempt={0}".format(attempt)
)
if not _is_gui_ready():
if attempt < IMPORT_READY_MAX_RETRIES:
_append_debug_log(
"scheduled device import postponed: gui not ready, retrying"
)
QtCore.QTimer.singleShot(
IMPORT_READY_RETRY_DELAY_MS,
lambda: _run_scheduled_device_import(attempt + 1),
)
return
_append_debug_log("scheduled device import aborted: gui never became ready")
_show_error(
"QET Exchange",
"FreeCAD main window did not finish initializing before device import.",
)
return
payload = getattr(App, STATE_PAYLOAD, None)
summary = getattr(App, STATE_SUMMARY, None)
if not isinstance(payload, dict) or not isinstance(summary, dict):
_append_debug_log(
"scheduled device import aborted: cached payload/summary missing"
)
return
scene_path = os.environ.get(ENV_SCENE_PATH, "").strip()
_append_debug_log(
"scheduled device import starting with scene_path={0}".format(scene_path)
)
try:
import_report = DeviceImport.import_devices_from_payload(payload, scene_path)
except DeviceImport.DeviceImportError as exc:
_append_debug_log("device import failed: {0}".format(exc))
_show_error("QET Exchange", str(exc))
App.Console.PrintError("[FreeCADExchange] {0}\n".format(exc))
return
except Exception as exc:
_append_debug_log("unexpected device import exception: {0}".format(exc))
_append_debug_log(traceback.format_exc())
_show_error("QET Exchange", "Failed to import 3D devices:\n{0}".format(exc))
App.Console.PrintError(
"[FreeCADExchange] Failed to import devices: {0}\n".format(exc)
)
return
setattr(App, STATE_IMPORT_REPORT, import_report)
_append_debug_log(
"device import summary: imported={0}, updated={1}, skipped_missing_model={2}, skipped_missing_file={3}, skipped_import_error={4}".format(
import_report["imported_devices"],
import_report["updated_devices"],
import_report["skipped_missing_model"],
import_report["skipped_missing_file"],
import_report["skipped_import_error"],
)
)
App.Console.PrintMessage(
"[FreeCADExchange] Loaded exchange payload from {0}\n".format(
summary["json_path"]
)
)
App.Console.PrintMessage(
"[FreeCADExchange] Devices: {0}, Terminals: {1}, Device models: {2}\n".format(
summary["device_count"],
summary["terminal_count"],
summary["device_model_count"],
)
)
App.Console.PrintMessage(
"[FreeCADExchange] Imported devices: {0}, updated: {1}, skipped without model: {2}\n".format(
import_report["imported_devices"],
import_report["updated_devices"],
import_report["skipped_missing_model"],
)
)
_show_info("QET Exchange", _summary_message(summary, import_report))
_append_debug_log("summary dialog shown")
def bootstrap_if_requested():
if not getattr(App, STATE_FLAG, False):
_reset_debug_log()
_append_debug_log("bootstrap_if_requested entered")
_install_tree_double_click_filter()
if getattr(App, STATE_FLAG, False):
_append_debug_log("bootstrap_if_requested skipped: already bootstrapped")
return
json_path = os.environ.get(ENV_JSON_PATH, "").strip()
_append_debug_log("ENV QET_2D_TO_3D_JSON={0}".format(json_path))
_append_debug_log("ENV QET_FREECAD_SCENE_FILE={0}".format(os.environ.get(ENV_SCENE_PATH, "").strip()))
if not json_path:
_append_debug_log("bootstrap_if_requested skipped: env missing")
return
setattr(App, STATE_FLAG, True)
_append_debug_log("STATE_FLAG set")
if not os.path.isfile(json_path):
_append_debug_log("exchange file missing: {0}".format(json_path))
_show_error(
"QET Exchange",
"Environment variable {0} points to a missing file:\n{1}".format(
ENV_JSON_PATH, json_path
),
)
return
try:
payload = load_exchange_payload(json_path)
except ExchangeValidationError as exc:
_append_debug_log("payload validation failed: {0}".format(exc))
_show_error("QET Exchange", str(exc))
App.Console.PrintError("[FreeCADExchange] {0}\n".format(exc))
return
except Exception as exc:
_append_debug_log("unexpected payload exception: {0}".format(exc))
_append_debug_log(traceback.format_exc())
raise
summary = _build_summary(payload, json_path)
_append_debug_log(
"payload loaded: devices={0}, terminals={1}, models={2}, cabinet={3}".format(
summary["device_count"],
summary["terminal_count"],
summary["device_model_count"],
"yes" if summary.get("cabinet") else "no",
)
)
setattr(App, STATE_PAYLOAD, payload)
setattr(App, STATE_SUMMARY, summary)
if not getattr(App, STATE_IMPORT_SCHEDULED, False):
setattr(App, STATE_IMPORT_SCHEDULED, True)
_append_debug_log(
"device import scheduled after startup delay: {0} ms".format(
IMPORT_READY_DELAY_MS
)
)
QtCore.QTimer.singleShot(
IMPORT_READY_DELAY_MS, lambda: _run_scheduled_device_import(0)
)

@ -1 +0,0 @@
# FreeCADExchange init module.

@ -1,34 +0,0 @@
# FreeCADExchange gui init module.
import os
from pathlib import Path
try:
from PySide6 import QtCore
except ImportError:
try:
from PySide2 import QtCore
except ImportError:
from PySide import QtCore
import ExchangeBootstrap
def _append_init_log(message):
try:
local_app_data = os.environ.get("LOCALAPPDATA", "").strip()
if local_app_data:
log_path = os.path.join(local_app_data, "QETDeps", "freecad_exchange_bootstrap.log")
else:
log_path = os.path.join(str(Path.home()), "AppData", "Local", "QETDeps", "freecad_exchange_bootstrap.log")
os.makedirs(os.path.dirname(log_path), exist_ok=True)
with open(log_path, "a", encoding="utf-8") as handle:
handle.write(message + "\n")
except Exception:
pass
_append_init_log("InitGui imported")
QtCore.QTimer.singleShot(0, ExchangeBootstrap.bootstrap_if_requested)

@ -1 +0,0 @@
# FreeCADExchange Python module.

@ -72,16 +72,11 @@ void StartGui::Manipulator::modifyMenuBar(Gui::MenuItem* menuBar)
} }
Gui::MenuItem* helpMenu = menuBar->findItem("&Help"); Gui::MenuItem* helpMenu = menuBar->findItem("&Help");
if (!helpMenu) {
return;
}
Gui::MenuItem* loadStart = new Gui::MenuItem(); Gui::MenuItem* loadStart = new Gui::MenuItem();
Gui::MenuItem* loadSeparator = new Gui::MenuItem(); Gui::MenuItem* loadSeparator = new Gui::MenuItem();
loadStart->setCommand("Start_Start"); loadStart->setCommand("Start_Start");
loadSeparator->setCommand("Separator"); loadSeparator->setCommand("Separator");
Gui::MenuItem* firstItem = helpMenu->findItem("Std_FreeCADUserHub"); Gui::MenuItem* firstItem = helpMenu->findItem("Std_FreeCADUserHub");
if (firstItem) { helpMenu->insertItem(firstItem, loadStart);
helpMenu->insertItem(firstItem, loadStart); helpMenu->insertItem(firstItem, loadSeparator);
helpMenu->insertItem(firstItem, loadSeparator);
}
} }

@ -1,250 +0,0 @@
param(
[string]$RunRoot = "",
[string]$LibPackRoot = "",
[string]$OcctRoot = "",
[string]$RuntimeRoot = "",
[switch]$SkipRuntimeJson
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Resolve-NormalizedPath {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
return [System.IO.Path]::GetFullPath($Path)
}
function Resolve-ConfiguredPath {
param(
[string]$ConfiguredPath,
[string[]]$EnvironmentVariableNames
)
if (-not [string]::IsNullOrWhiteSpace($ConfiguredPath)) {
return Resolve-NormalizedPath -Path $ConfiguredPath
}
foreach ($variableName in $EnvironmentVariableNames) {
$value = [Environment]::GetEnvironmentVariable($variableName)
if (-not [string]::IsNullOrWhiteSpace($value)) {
return Resolve-NormalizedPath -Path $value
}
}
return ""
}
function Ensure-Directory {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
if (-not (Test-Path -LiteralPath $Path)) {
New-Item -ItemType Directory -Path $Path | Out-Null
}
}
function Copy-MatchingFiles {
param(
[Parameter(Mandatory = $true)]
[string]$SourceDir,
[Parameter(Mandatory = $true)]
[string[]]$Patterns,
[Parameter(Mandatory = $true)]
[string]$DestinationDir
)
if (-not (Test-Path -LiteralPath $SourceDir)) {
return
}
foreach ($pattern in $Patterns) {
Get-ChildItem -LiteralPath $SourceDir -Filter $pattern -File -ErrorAction SilentlyContinue | ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $DestinationDir $_.Name) -Force
}
}
}
function Copy-PluginDirectory {
param(
[Parameter(Mandatory = $true)]
[string]$SourceDir,
[Parameter(Mandatory = $true)]
[string]$DestinationDir
)
if (-not (Test-Path -LiteralPath $SourceDir)) {
return
}
Ensure-Directory -Path $DestinationDir
Get-ChildItem -LiteralPath $SourceDir -Force | ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination $DestinationDir -Recurse -Force
}
}
function Resolve-RuntimeRoot {
param([string]$ConfiguredRoot)
if (-not [string]::IsNullOrWhiteSpace($ConfiguredRoot)) {
return Resolve-NormalizedPath -Path $ConfiguredRoot
}
$localAppData = [Environment]::GetEnvironmentVariable("LOCALAPPDATA")
if ([string]::IsNullOrWhiteSpace($localAppData)) {
throw "LOCALAPPDATA is not available."
}
return Resolve-NormalizedPath -Path (Join-Path $localAppData "QETDeps")
}
function Read-ExistingRuntimeJson {
param([string]$RuntimeConfigPath)
if (-not (Test-Path -LiteralPath $RuntimeConfigPath)) {
return $null
}
return Get-Content -LiteralPath $RuntimeConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
}
function Resolve-OcctRoot {
param(
[string]$ConfiguredRoot,
$ExistingRuntime
)
$resolvedConfiguredRoot = Resolve-ConfiguredPath -ConfiguredPath $ConfiguredRoot -EnvironmentVariableNames @(
"FREECAD_OCCT_ROOT",
"QET_OCCT_ROOT"
)
if (-not [string]::IsNullOrWhiteSpace($resolvedConfiguredRoot)) {
return $resolvedConfiguredRoot
}
if ($null -ne $ExistingRuntime -and $ExistingRuntime.PSObject.Properties.Name -contains "occt_root") {
$existingOcctRoot = [string]$ExistingRuntime.occt_root
if (-not [string]::IsNullOrWhiteSpace($existingOcctRoot) -and (Test-Path -LiteralPath $existingOcctRoot)) {
return Resolve-NormalizedPath -Path $existingOcctRoot
}
}
return ""
}
$resolvedRunRoot = Resolve-ConfiguredPath -ConfiguredPath $RunRoot -EnvironmentVariableNames @(
"FREECAD_RUN_ROOT",
"QET_FREECAD_RUN_ROOT"
)
if ([string]::IsNullOrWhiteSpace($resolvedRunRoot)) {
throw "RunRoot is required. Pass -RunRoot or set FREECAD_RUN_ROOT / QET_FREECAD_RUN_ROOT."
}
$resolvedLibPackRoot = Resolve-ConfiguredPath -ConfiguredPath $LibPackRoot -EnvironmentVariableNames @(
"FREECAD_LIBPACK_ROOT",
"QET_FREECAD_LIBPACK_ROOT"
)
if ([string]::IsNullOrWhiteSpace($resolvedLibPackRoot)) {
throw "LibPackRoot is required. Pass -LibPackRoot or set FREECAD_LIBPACK_ROOT / QET_FREECAD_LIBPACK_ROOT."
}
$runBinDir = Join-Path $resolvedRunRoot "bin"
if (-not (Test-Path -LiteralPath $runBinDir)) {
throw "Run directory bin folder was not found: $runBinDir"
}
if (-not (Test-Path -LiteralPath $resolvedLibPackRoot)) {
throw "LibPack root was not found: $resolvedLibPackRoot"
}
$copySpecs = @(
@{ Source = (Join-Path $resolvedLibPackRoot "bin"); Patterns = @("*.dll") },
@{ Source = (Join-Path $resolvedLibPackRoot "lib"); Patterns = @("*.dll") },
@{ Source = (Join-Path $resolvedLibPackRoot "bin"); Patterns = @("python.exe", "pythonw.exe", "py.exe", "python*.zip") },
@{ Source = (Join-Path $resolvedLibPackRoot "bin\Lib\site-packages\shiboken6"); Patterns = @("*.dll", "*.pyd") },
@{ Source = (Join-Path $resolvedLibPackRoot "bin\Lib\site-packages\PySide6"); Patterns = @("*.dll", "*.pyd") }
)
foreach ($copySpec in $copySpecs) {
Copy-MatchingFiles -SourceDir $copySpec.Source -Patterns $copySpec.Patterns -DestinationDir $runBinDir
}
$pluginRoot = Join-Path $resolvedLibPackRoot "bin\Lib\site-packages\PySide6\plugins"
$pluginDirs = @(
"platforms",
"imageformats",
"iconengines",
"platformthemes",
"styles"
)
foreach ($pluginDir in $pluginDirs) {
Copy-PluginDirectory `
-SourceDir (Join-Path $pluginRoot $pluginDir) `
-DestinationDir (Join-Path $runBinDir $pluginDir)
}
Copy-PluginDirectory `
-SourceDir (Join-Path $resolvedLibPackRoot "bin\Lib") `
-DestinationDir (Join-Path $runBinDir "Lib")
Copy-PluginDirectory `
-SourceDir (Join-Path $resolvedLibPackRoot "bin\DLLs") `
-DestinationDir (Join-Path $runBinDir "DLLs")
if (-not $SkipRuntimeJson) {
$resolvedRuntimeRoot = Resolve-RuntimeRoot -ConfiguredRoot $RuntimeRoot
Ensure-Directory -Path $resolvedRuntimeRoot
$runtimeConfigPath = Join-Path $resolvedRuntimeRoot "runtime.json"
$diagnosticLogPath = Join-Path $resolvedRuntimeRoot "bootstrap.log"
$existingRuntime = Read-ExistingRuntimeJson -RuntimeConfigPath $runtimeConfigPath
$resolvedOcctRoot = Resolve-OcctRoot -ConfiguredRoot $OcctRoot -ExistingRuntime $existingRuntime
$resolvedFreeCadPython = ""
$pythonCandidate = Join-Path $runBinDir "python.exe"
if (Test-Path -LiteralPath $pythonCandidate) {
$resolvedFreeCadPython = $pythonCandidate
}
$resolvedFreeCadCmd = ""
$cmdCandidate = Join-Path $runBinDir "FreeCADCmd.exe"
if (Test-Path -LiteralPath $cmdCandidate) {
$resolvedFreeCadCmd = $cmdCandidate
}
$qet3dPython = if (-not [string]::IsNullOrWhiteSpace($resolvedFreeCadPython)) {
$resolvedFreeCadPython
} else {
$resolvedFreeCadCmd
}
$runtimeObject = [ordered]@{
schema_version = 1
runtime_root = $resolvedRuntimeRoot
runtime_config = $runtimeConfigPath
diagnostic_log = $diagnosticLogPath
occt_root = $resolvedOcctRoot
freecad_root = $resolvedRunRoot
freecad_lib = $runBinDir
freecad_python = $resolvedFreeCadPython
freecad_cmd = $resolvedFreeCadCmd
qet_3d_python = $qet3dPython
path_prefix = @($runBinDir)
}
($runtimeObject | ConvertTo-Json -Depth 5) | Set-Content -LiteralPath $runtimeConfigPath -Encoding UTF8
}
Write-Host "FreeCAD runtime deployment completed."
Write-Host (" Run root: {0}" -f $resolvedRunRoot)
Write-Host (" LibPack root: {0}" -f $resolvedLibPackRoot)
if (-not $SkipRuntimeJson) {
Write-Host (" runtime.json: {0}" -f (Join-Path (Resolve-RuntimeRoot -ConfiguredRoot $RuntimeRoot) "runtime.json"))
}
Loading…
Cancel
Save