diff --git a/src/Mod/FreeCADExchange/DeviceImport.py b/src/Mod/FreeCADExchange/DeviceImport.py index 2e7c151..f229188 100644 --- a/src/Mod/FreeCADExchange/DeviceImport.py +++ b/src/Mod/FreeCADExchange/DeviceImport.py @@ -81,6 +81,10 @@ def _top_level_imported_objects(imported_objects): return [obj for obj in imported_objects if obj.Name not in child_names] +def _top_level_document_objects(doc): + return _top_level_imported_objects(list(getattr(doc, "Objects", []) or [])) + + 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) @@ -419,6 +423,9 @@ def _supported_for_import(model_path): def _import_model_into_group(doc, device_group, model_path, merge=False, use_link_group=True): + if Path(model_path).suffix.lower() == ".fcstd": + return _import_fcstd_into_group(doc, device_group, model_path) + before_names = _existing_object_names(doc) try: ImportGui.insert( @@ -440,6 +447,41 @@ def _import_model_into_group(doc, device_group, model_path, merge=False, use_lin return top_level_objects +def _open_fcstd_source_document(model_path): + normalized_target = os.path.normcase(os.path.normpath(model_path)) + for candidate in App.listDocuments().values(): + candidate_path = getattr(candidate, "FileName", "") or "" + if candidate_path and os.path.normcase(os.path.normpath(candidate_path)) == normalized_target: + return candidate, False + source_doc = App.openDocument(model_path, hidden=True, temporary=True) + return source_doc, True + + +def _import_fcstd_into_group(doc, device_group, model_path): + source_doc = None + should_close = False + try: + source_doc, should_close = _open_fcstd_source_document(model_path) + if source_doc is None: + raise DeviceImportError("Cannot open FCStd file") + + top_level_objects = _top_level_document_objects(source_doc) + copied_objects = [] + for source_obj in top_level_objects: + copied_obj = doc.copyObject(source_obj, True) + if copied_obj not in getattr(device_group, "Group", []): + device_group.addObject(copied_obj) + copied_objects.append(copied_obj) + + return copied_objects + finally: + if should_close and source_doc is not None: + try: + App.closeDocument(source_doc.Name) + except Exception: + pass + + def _model_index(payload): index = {} for item in payload.get("device_models", []): diff --git a/src/Mod/FreeCADExchange/ExchangeBootstrap.py b/src/Mod/FreeCADExchange/ExchangeBootstrap.py index e087051..79494e0 100644 --- a/src/Mod/FreeCADExchange/ExchangeBootstrap.py +++ b/src/Mod/FreeCADExchange/ExchangeBootstrap.py @@ -7,8 +7,14 @@ import FreeCAD as App import FreeCADGui as Gui import DeviceImport import DevicePreview -import ExchangeWriteBack -import TerminalImport +try: + import ExchangeWriteBack +except Exception: + ExchangeWriteBack = None +try: + import TerminalImport +except Exception: + TerminalImport = None try: from PySide6 import QtCore, QtWidgets @@ -84,6 +90,16 @@ def _show_error(title, message): QtWidgets.QMessageBox.critical(_get_main_window(), title, message) +def _terminal_report_not_available(): + return { + "imported_terminals": 0, + "updated_terminals": 0, + "removed_terminals": 0, + "skipped_terminals": 0, + "warnings": ["3D terminal import module is not available."], + } + + def _qt_class_name(widget): try: return widget.metaObject().className() @@ -721,34 +737,42 @@ def _run_scheduled_device_import(attempt=0): ) ) - try: - terminal_report = TerminalImport.import_terminals_from_payload(payload, scene_path) - except TerminalImport.TerminalImportError as exc: - _append_debug_log("terminal 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 terminal import exception: {0}".format(exc)) - _append_debug_log(traceback.format_exc()) - _show_error("QET Exchange", "Failed to import 3D terminals:\n{0}".format(exc)) - App.Console.PrintError( - "[FreeCADExchange] Failed to import terminals: {0}\n".format(exc) - ) - return + if TerminalImport is None: + _append_debug_log("terminal import skipped: TerminalImport module unavailable") + terminal_report = _terminal_report_not_available() + else: + try: + terminal_report = TerminalImport.import_terminals_from_payload(payload, scene_path) + except TerminalImport.TerminalImportError as exc: + _append_debug_log("terminal 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 terminal import exception: {0}".format(exc)) + _append_debug_log(traceback.format_exc()) + _show_error("QET Exchange", "Failed to import 3D terminals:\n{0}".format(exc)) + App.Console.PrintError( + "[FreeCADExchange] Failed to import terminals: {0}\n".format(exc) + ) + return setattr(App, STATE_TERMINAL_IMPORT_REPORT, terminal_report) - try: - writeback_report = ExchangeWriteBack.write_back_document( - App.ActiveDocument, scene_path=scene_path, payload=payload - ) - except Exception as exc: - _append_debug_log("write-back failed after import: {0}".format(exc)) - _append_debug_log(traceback.format_exc()) + if ExchangeWriteBack is None: + _append_debug_log("write-back skipped: ExchangeWriteBack module unavailable") writeback_report = None else: - setattr(App, STATE_WRITEBACK_REPORT, writeback_report) + try: + writeback_report = ExchangeWriteBack.write_back_document( + App.ActiveDocument, scene_path=scene_path, payload=payload + ) + except Exception as exc: + _append_debug_log("write-back failed after import: {0}".format(exc)) + _append_debug_log(traceback.format_exc()) + writeback_report = None + else: + setattr(App, STATE_WRITEBACK_REPORT, writeback_report) App.Console.PrintMessage( "[FreeCADExchange] Imported terminals: {0}, updated: {1}, removed: {2}\n".format( diff --git a/src/Mod/FreeCADExchange/TerminalImport.py b/src/Mod/FreeCADExchange/TerminalImport.py index 1778a1c..12a1db8 100644 --- a/src/Mod/FreeCADExchange/TerminalImport.py +++ b/src/Mod/FreeCADExchange/TerminalImport.py @@ -43,6 +43,39 @@ def _normalize_terminal_entry(item, index): } +def _payload_device_lookup(payload): + by_element_uuid = set() + by_instance_id = set() + + for item in payload.get("devices", []) or []: + if not isinstance(item, dict): + continue + + element_uuid = (item.get("element_uuid") or "").strip() + instance_id = (item.get("instance_id") or "").strip() + + if element_uuid: + by_element_uuid.add(element_uuid) + if instance_id: + by_instance_id.add(instance_id) + + return { + "element_uuids": by_element_uuid, + "instance_ids": by_instance_id, + } + + +def _terminal_belongs_to_payload_devices(entry, device_lookup): + instance_id = entry["instance_id"] + element_uuid = entry["element_uuid"] + + if instance_id and instance_id in device_lookup["instance_ids"]: + return True + if element_uuid and element_uuid in device_lookup["element_uuids"]: + return True + return False + + def _ensure_visible(obj): try: if getattr(obj, "ViewObject", None) is not None: @@ -179,6 +212,8 @@ def import_terminals_from_payload(payload, scene_path=""): if not isinstance(terminal_entries, list): raise TerminalImportError("Field 'terminals' must be a list.") + device_lookup = _payload_device_lookup(payload) + report = { "document_name": doc.Name, "scene_path": scene_path or "", @@ -190,6 +225,7 @@ def import_terminals_from_payload(payload, scene_path=""): "reused_template_hints": 0, "skipped_missing_device": 0, "skipped_invalid_entry": 0, + "skipped_unmatched_parent": 0, "warnings": [], } @@ -203,6 +239,10 @@ def import_terminals_from_payload(payload, scene_path=""): report["warnings"].append(str(exc)) continue + if not _terminal_belongs_to_payload_devices(entry, device_lookup): + report["skipped_unmatched_parent"] += 1 + continue + device_group = _locate_device_group(doc, entry) if device_group is None: report["skipped_missing_device"] += 1 @@ -316,10 +356,11 @@ def import_terminals_from_payload(payload, scene_path=""): pass _append_debug_log( - "TerminalImport finished: imported={0}, updated={1}, removed={2}".format( + "TerminalImport finished: imported={0}, updated={1}, removed={2}, skipped_unmatched_parent={3}".format( report["imported_terminals"], report["updated_terminals"], report["removed_terminals"], + report["skipped_unmatched_parent"], ) ) return report