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)