feature/设备模板端子制作面板-zwl-0520

dev
Zhaowenlong 6 days ago
parent 9bd8e15f31
commit 70b0534964

@ -283,6 +283,31 @@ A 方案要求 `D:\code\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. 电气柜与设备装配
除了端子与接线,电气柜场景通常还会遇到另一个基础问题:

@ -182,6 +182,26 @@ FCStd 设备模板用于解决“这个模型本身就带端子语义”的问
4. 保存为 `.FCStd` 设备模板。
5. 后续 LightWork3D 工程引用该 `.FCStd` 后,自动识别模板端子并生成工程端子对象。
### 3.2.1 方案 2设备模板端子制作面板
当前 Python 控制台方式只适合开发验证,不适合 CAD 工作人员。下一步开发目标改为在 FreeCAD 右侧任务区提供“设备模板端子制作”面板。
第一版面板能力:
- 显示当前文档中的模板端子列表。
- 输入端子名,例如 `P1`、`P2`。
- 用户选择模型上的孔、点或对象后,点击“添加端子”。
- 点击“校验端子”显示总数、有效数和警告。
- 点击“保存为 FCStd”选择路径并保存模板。
该面板只包装现有 Python 能力,不改 FreeCAD C/C++ 源码:
- `TemplateAuthoring.create_template_terminal`
- `TemplateAuthoring.validate_template_terminals`
- `TemplateAuthoring.save_template_as_fcstd`
目标是让 CAD 工作人员只通过鼠标选择和按钮点击完成模板制作,不再直接输入 Python 代码。
### 3.3 A 方案资产流转约定
A 方案下,`.FCStd` 是正式可复用设备资产STEP / STP / STE 只作为模板制作的原始几何输入。
@ -297,6 +317,7 @@ FreeCADExchange/
DeviceImport.py # 设备导入,已存在
TemplateSemantics.py # 新增:读取 FCStd LCS 或 STEP sidecar 端子槽位
TemplateAuthoring.py # 计划新增:把 STEP/STP/STE 制作为带端子语义的 FCStd 模板
TemplateAuthoringPanel.py # 新增CAD 人员使用的端子制作任务面板
TerminalImport.py # 新增:根据 terminals 创建/更新端子对象
TerminalObjects.py # 新增:端子对象属性、查找、校验工具
ManualWiring.py # 新增:端子选择、折线路径创建、连线对象属性
@ -575,4 +596,6 @@ ManualWiring.py
- 2026-05-20补充 A 方案资产流转设计,明确 `.FCStd` 为正式设备资产zwl/QET 只负责选择、保存、导出 `.FCStd` 路径FreeCADExchange 负责读取 LCS 端子语义并生成工程端子。
- 2026-05-20补上 `QET_Template_SaveAsFCStd` 模板保存命令,保存前会校验至少存在一个有效模板端子,并自动补 `.FCStd` 后缀;已用单元测试验证保存路径和端子校验结果。
- 2026-05-20修复 `TemplateAuthoring.py``FreeCADCmd.exe` 命令行模式下导入时误注册 GUI 命令的问题;已在运行目录验证创建 `P1` 模板端子、保存 `.FCStd`、重新打开后端子语义仍可识别。
- 2026-05-20确定方案 2 开发目标:新增“设备模板端子制作”任务面板,让 CAD 工作人员通过输入端子名、选择模型位置、点击按钮完成添加端子、校验端子和保存 FCStd不再依赖 Python 控制台。
- 2026-05-20新增 `TemplateAuthoringPanel.py`,提供“设备模板端子制作”任务面板和 `QET_Template_OpenAuthoringPanel` 命令;面板支持输入端子名、添加端子、校验端子、保存 FCStd并已同步到运行目录验证模块可导入。
```

@ -8,6 +8,7 @@ set(FreeCADExchange_Scripts
TerminalObjects.py
TemplateSemantics.py
TemplateAuthoring.py
TemplateAuthoringPanel.py
TerminalImport.py
ExchangeWriteBack.py
ManualWiring.py

@ -15,6 +15,7 @@ import ExchangeBootstrap
import ExchangeWriteBack
import ManualWiring
import TemplateAuthoring
import TemplateAuthoringPanel
def _append_init_log(message):
@ -53,5 +54,10 @@ try:
except Exception:
pass
try:
TemplateAuthoringPanel.register_commands()
except Exception:
pass
QtCore.QTimer.singleShot(0, ExchangeBootstrap.bootstrap_if_requested)

@ -0,0 +1,260 @@
# FreeCADExchange GUI panel for FCStd equipment template authoring.
import FreeCAD as App
try:
import FreeCADGui as Gui
except ImportError:
Gui = None
try:
from PySide6 import QtGui, QtWidgets
except ImportError:
try:
from PySide2 import QtGui, QtWidgets
except ImportError:
try:
from PySide import QtGui
from PySide import QtGui as QtWidgets
except ImportError:
QtGui = None
QtWidgets = None
import TemplateAuthoring
COMMAND_NAME = "QET_Template_OpenAuthoringPanel"
MENU_ACTION_OBJECT_NAME = "QET_Template_OpenAuthoringPanel_MenuAction"
def next_slot_name(report):
terminals = list((report or {}).get("terminals", []) or [])
return "T{0}".format(len(terminals) + 1)
def terminal_list_text(report):
rows = []
for item in list((report or {}).get("terminals", []) or []):
slot_name = (item.get("slot_name", "") or "").strip() or "(unnamed)"
object_name = (item.get("name", "") or "").strip() or "(object)"
suffix = "" if item.get("can_wire", False) and slot_name != "(unnamed)" else " [invalid]"
rows.append("{0} - {1}{2}".format(slot_name, object_name, suffix))
return rows
class TemplateAuthoringTaskPanel:
def __init__(self):
if QtWidgets is None:
raise TemplateAuthoring.TemplateAuthoringError("Qt widgets are not available.")
self.form = QtWidgets.QWidget()
self.form.setWindowTitle("设备模板端子制作")
layout = QtWidgets.QVBoxLayout(self.form)
name_row = QtWidgets.QHBoxLayout()
name_row.addWidget(QtWidgets.QLabel("端子名"))
self.slot_name_edit = QtWidgets.QLineEdit()
self.slot_name_edit.setPlaceholderText("P1")
name_row.addWidget(self.slot_name_edit)
layout.addLayout(name_row)
type_row = QtWidgets.QHBoxLayout()
type_row.addWidget(QtWidgets.QLabel("端子类型"))
self.terminal_type_combo = QtWidgets.QComboBox()
self.terminal_type_combo.addItems(["generic", "primary", "power", "control"])
type_row.addWidget(self.terminal_type_combo)
layout.addLayout(type_row)
self.add_button = QtWidgets.QPushButton("添加端子")
self.validate_button = QtWidgets.QPushButton("校验端子")
self.save_button = QtWidgets.QPushButton("保存为 FCStd")
self.refresh_button = QtWidgets.QPushButton("刷新列表")
layout.addWidget(self.add_button)
layout.addWidget(self.validate_button)
layout.addWidget(self.save_button)
layout.addWidget(self.refresh_button)
self.terminal_list = QtWidgets.QListWidget()
layout.addWidget(self.terminal_list)
self.status_label = QtWidgets.QLabel("")
self.status_label.setWordWrap(True)
layout.addWidget(self.status_label)
self.add_button.clicked.connect(self.add_terminal)
self.validate_button.clicked.connect(self.validate_terminals)
self.save_button.clicked.connect(self.save_template)
self.refresh_button.clicked.connect(self.refresh)
self.refresh()
def _document(self):
doc = getattr(App, "ActiveDocument", None)
if doc is None:
raise TemplateAuthoring.TemplateAuthoringError("请先打开或创建一个 FreeCAD 文档。")
return doc
def _set_status(self, message):
self.status_label.setText(message)
try:
App.Console.PrintMessage("[FreeCADExchange] {0}\n".format(message))
except Exception:
pass
def _set_error(self, message):
self.status_label.setText(message)
try:
App.Console.PrintError("[FreeCADExchange] {0}\n".format(message))
except Exception:
pass
def refresh(self):
try:
report = TemplateAuthoring.validate_template_terminals(getattr(App, "ActiveDocument", None))
except Exception as exc:
self._set_error(str(exc))
return
self.terminal_list.clear()
for row in terminal_list_text(report):
self.terminal_list.addItem(row)
if not self.slot_name_edit.text().strip():
self.slot_name_edit.setText(next_slot_name(report))
self.status_label.setText(
"端子:{0} 个,有效:{1}".format(
report.get("total_terminals", 0),
report.get("valid_terminals", 0),
)
)
def add_terminal(self):
try:
doc = self._document()
slot_name = self.slot_name_edit.text().strip()
position = TemplateAuthoring._selection_position()
if position is None:
raise TemplateAuthoring.TemplateAuthoringError("请先在模型上选择端子位置。")
terminal_type = self.terminal_type_combo.currentText().strip() or "generic"
TemplateAuthoring.create_template_terminal(
doc,
slot_name,
position,
terminal_type=terminal_type,
)
self.slot_name_edit.clear()
self.refresh()
self._set_status("已添加端子:{0}".format(slot_name))
except Exception as exc:
self._set_error(str(exc))
def validate_terminals(self):
try:
report = TemplateAuthoring.validate_template_terminals(self._document())
self.refresh()
if report.get("warnings"):
self._set_error("校验发现问题:{0}".format("; ".join(report["warnings"])))
else:
self._set_status(
"校验通过:{0}/{1} 个端子有效".format(
report.get("valid_terminals", 0),
report.get("total_terminals", 0),
)
)
except Exception as exc:
self._set_error(str(exc))
def save_template(self):
try:
doc = self._document()
file_path, _selected_filter = QtWidgets.QFileDialog.getSaveFileName(
self.form,
"保存 FCStd 设备模板",
"",
"FreeCAD template (*.FCStd *.fcstd);;All files (*.*)",
)
if not file_path:
return
report = TemplateAuthoring.save_template_as_fcstd(doc, file_path)
self.refresh()
self._set_status("已保存模板:{0}".format(report["path"]))
except Exception as exc:
self._set_error(str(exc))
def accept(self):
return True
def reject(self):
return True
class CommandOpenTemplateAuthoringPanel:
def GetResources(self):
return {
"MenuText": "设备模板端子制作",
"ToolTip": "打开 FCStd 设备模板端子制作面板",
}
def IsActive(self):
return getattr(App, "ActiveDocument", None) is not None and Gui is not None
def Activated(self):
if Gui is None or not hasattr(Gui, "Control"):
return
if hasattr(Gui.Control, "activeDialog") and Gui.Control.activeDialog():
Gui.Control.closeDialog()
Gui.Control.showDialog(TemplateAuthoringTaskPanel())
_COMMANDS_REGISTERED = False
_MENU_ACTION_INSTALLED = False
def _tools_menu():
if Gui is None or QtWidgets is None or QtGui is None:
return None
if not hasattr(Gui, "getMainWindow"):
return None
main_window = Gui.getMainWindow()
if main_window is None:
return None
menu_bar = main_window.menuBar()
for action in menu_bar.actions():
menu = action.menu()
if menu is not None and action.text().replace("&", "") in {"工具", "Tools"}:
return menu
return menu_bar.addMenu("工具")
def install_menu_action():
global _MENU_ACTION_INSTALLED
if _MENU_ACTION_INSTALLED:
return
menu = _tools_menu()
if menu is None or QtGui is None:
return
for action in menu.actions():
if action.objectName() == MENU_ACTION_OBJECT_NAME:
_MENU_ACTION_INSTALLED = True
return
action = QtGui.QAction("设备模板端子制作", menu)
action.setObjectName(MENU_ACTION_OBJECT_NAME)
action.triggered.connect(lambda: Gui.runCommand(COMMAND_NAME))
menu.addAction(action)
_MENU_ACTION_INSTALLED = True
def register_commands():
global _COMMANDS_REGISTERED
if Gui is None or not hasattr(Gui, "addCommand"):
return
if not _COMMANDS_REGISTERED:
Gui.addCommand(COMMAND_NAME, CommandOpenTemplateAuthoringPanel())
_COMMANDS_REGISTERED = True
install_menu_action()
register_commands()

@ -0,0 +1,85 @@
import importlib
import sys
import types
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
MODULE_DIR = REPO_ROOT / "src" / "Mod" / "FreeCADExchange"
if str(MODULE_DIR) not in sys.path:
sys.path.insert(0, str(MODULE_DIR))
def _install_fake_modules():
fake_freecad = types.ModuleType("FreeCAD")
fake_freecad.ActiveDocument = None
fake_freecad.Console = types.SimpleNamespace(
PrintMessage=lambda *args, **kwargs: None,
PrintWarning=lambda *args, **kwargs: None,
PrintError=lambda *args, **kwargs: None,
)
sys.modules["FreeCAD"] = fake_freecad
fake_freecadgui = types.ModuleType("FreeCADGui")
fake_freecadgui.addCommand = lambda *args, **kwargs: None
fake_freecadgui.Control = types.SimpleNamespace(
activeDialog=lambda: False,
showDialog=lambda panel: panel,
closeDialog=lambda: None,
)
sys.modules["FreeCADGui"] = fake_freecadgui
fake_template_authoring = types.ModuleType("TemplateAuthoring")
fake_template_authoring.validate_template_terminals = lambda doc: {
"terminals": [],
"total_terminals": 0,
"valid_terminals": 0,
"warnings": [],
}
fake_template_authoring._selection_position = lambda: None
fake_template_authoring.create_template_terminal = lambda *args, **kwargs: None
fake_template_authoring.save_template_as_fcstd = lambda *args, **kwargs: {}
sys.modules["TemplateAuthoring"] = fake_template_authoring
def _reload_panel_module():
sys.modules.pop("TemplateAuthoringPanel", None)
return importlib.import_module("TemplateAuthoringPanel")
class TemplateAuthoringPanelTest(unittest.TestCase):
def test_next_slot_name_uses_next_terminal_number(self):
_install_fake_modules()
panel_module = _reload_panel_module()
self.assertEqual(
"T3",
panel_module.next_slot_name(
{
"terminals": [
{"slot_name": "P1"},
{"slot_name": "P2"},
]
}
),
)
def test_terminal_list_text_marks_invalid_terminal(self):
_install_fake_modules()
panel_module = _reload_panel_module()
rows = panel_module.terminal_list_text(
{
"terminals": [
{"name": "Terminal_P1", "slot_name": "P1", "can_wire": True},
{"name": "Broken", "slot_name": "", "can_wire": False},
]
}
)
self.assertEqual(["P1 - Terminal_P1", "(unnamed) - Broken [invalid]"], rows)
if __name__ == "__main__":
unittest.main()
Loading…
Cancel
Save