You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

242 lines
6.9 KiB
Python

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)