diff --git a/pkg/freecad/silo_commands.py b/pkg/freecad/silo_commands.py index f46c73b..cfc5c70 100644 --- a/pkg/freecad/silo_commands.py +++ b/pkg/freecad/silo_commands.py @@ -1048,6 +1048,59 @@ def get_tracked_object(doc): return None +def _pull_dependencies(part_number: str) -> None: + """Download BOM children so PropertyXLink references resolve on open. + + For each child in the BOM that is not already present locally, fetch + the item metadata (for canonical description) and download the latest + file revision to the canonical path. + """ + try: + bom = _client.get_bom(part_number) + except Exception as e: + FreeCAD.Console.PrintWarning(f"Could not fetch BOM for {part_number}: {e}\n") + return + + if not bom: + return + + for entry in bom: + child_pn = entry.get("child_part_number", "") + if not child_pn: + continue + + # Check if we already have the file locally + existing = find_file_by_part_number(child_pn) + if existing and existing.exists(): + continue + + # Get canonical item description for the filename + try: + child_item = _client.get_item(child_pn) + child_desc = child_item.get("description", "") + except Exception: + child_desc = entry.get("child_description", "") + + dest_path = get_cad_file_path(child_pn, child_desc) + dest_path.parent.mkdir(parents=True, exist_ok=True) + + # Find latest revision with a file + rev = _client.latest_file_revision(child_pn) + if not rev: + FreeCAD.Console.PrintWarning(f"No file revision for dependency {child_pn}, skipping.\n") + continue + + rev_num = rev["revision_number"] + try: + ok = _client._download_file(child_pn, rev_num, str(dest_path)) + if ok: + FreeCAD.Console.PrintMessage(f"Pulled dependency {child_pn} rev {rev_num}\n") + else: + FreeCAD.Console.PrintWarning(f"Failed to download dependency {child_pn}\n") + except Exception as e: + FreeCAD.Console.PrintWarning(f"Error downloading dependency {child_pn}: {e}\n") + + class SiloSync: """Handles synchronization between FreeCAD and Silo.""" @@ -1165,14 +1218,31 @@ class SiloSync: return doc def open_item(self, part_number: str): - """Open or create item document.""" + """Open or create item document. + + For assemblies, BOM dependencies are pulled first so that + PropertyXLink references resolve when the document loads. + """ + # Fetch item metadata to check item_type + try: + item = self.client.get_item(part_number) + except Exception as e: + FreeCAD.Console.PrintError(f"Failed to fetch item: {e}\n") + item = {} + + # Pull assembly dependencies before opening so links resolve + if item.get("item_type") == "assembly": + _pull_dependencies(part_number) + existing_path = find_file_by_part_number(part_number) if existing_path and existing_path.exists(): return FreeCAD.openDocument(str(existing_path)) + if not item: + return None + try: - item = self.client.get_item(part_number) return self.create_document_for_item(item, save=True) except Exception as e: FreeCAD.Console.PrintError(f"Failed to open: {e}\n") @@ -1365,10 +1435,10 @@ class Silo_Open: # inside the dialog's nested event loop, which can cause crashes. data = _open_after_close[0] if data is not None: - if data.get("path"): - FreeCAD.openDocument(data["path"]) - else: + if data.get("part_number"): _sync.open_item(data["part_number"]) + elif data.get("path"): + FreeCAD.openDocument(data["path"]) def IsActive(self): return True @@ -1908,6 +1978,18 @@ class Silo_Pull: FreeCAD.Console.PrintMessage(f"Pulled revision {rev_num} of {part_number}\n") + # Pull assembly dependencies before reopening + if item.get("item_type") == "assembly": + dep_progress = QtGui.QProgressDialog( + f"Pulling dependencies for {part_number}...", None, 0, 0 + ) + dep_progress.setWindowModality(2) # Qt.WindowModal + dep_progress.setMinimumDuration(0) + dep_progress.setValue(0) + QtGui.QApplication.processEvents() + _pull_dependencies(part_number) + dep_progress.close() + # Close existing document if open, then reopen if doc and doc.FileName == str(dest_path): FreeCAD.closeDocument(doc.Name)