From bb9ae3540ec33800764d93d63c38728717025e78 Mon Sep 17 00:00:00 2001 From: David Kaufman Date: Wed, 5 Nov 2025 13:28:54 -0500 Subject: [PATCH 1/5] [CAM] bugfix tools not imported properly during library import --- src/Mod/CAM/Path/Tool/assets/ui/filedialog.py | 2 ++ src/Mod/CAM/Path/Tool/toolbit/models/base.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py b/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py index 1e49be1461..9308b894a2 100644 --- a/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py +++ b/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py @@ -23,6 +23,7 @@ import pathlib from typing import Optional, Tuple, Type, Iterable from PySide.QtWidgets import QFileDialog, QMessageBox +from ...camassets import cam_assets from ..manager import AssetManager from ..serializer import AssetSerializer, Asset from .util import ( @@ -214,6 +215,7 @@ class AssetOpenDialog(QFileDialog): toolbit.id = dependency_uri.asset_id # Import the toolbit into local store + cam_assets.add(toolbit) imported_count += 1 except Exception as e: diff --git a/src/Mod/CAM/Path/Tool/toolbit/models/base.py b/src/Mod/CAM/Path/Tool/toolbit/models/base.py index 7214c92072..62ec4b2ac1 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/models/base.py +++ b/src/Mod/CAM/Path/Tool/toolbit/models/base.py @@ -103,6 +103,8 @@ class ToolBit(Asset, ABC): raise ValueError("ToolBit dictionary is missing 'shape' key") # Try to find the shape type. Default to Unknown if necessary. + if "shape" in attrs and "shape-type" not in attrs: + attrs["shape-type"] = attrs["shape"] shape_type = attrs.get("shape-type") shape_class = ToolBitShape.get_shape_class_from_id(shape_id, shape_type) if not shape_class: From 21319f95d2892f2dbba3bb2bf6f6fcb4f9e8df47 Mon Sep 17 00:00:00 2001 From: David Kaufman Date: Wed, 5 Nov 2025 15:18:43 -0500 Subject: [PATCH 2/5] automatically offer migration for tool libraries in custom working dir --- src/Mod/CAM/Path/Tool/assets/ui/filedialog.py | 4 +- src/Mod/CAM/Path/Tool/migration/migration.py | 90 ++++++++++++++++--- 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py b/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py index 9308b894a2..f1f106d7a4 100644 --- a/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py +++ b/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py @@ -57,7 +57,7 @@ class AssetOpenDialog(QFileDialog): if filters: self.selectNameFilter(filters[0]) # Default to "All supported files" - def _deserialize_selected_file(self, file_path: pathlib.Path) -> Optional[Asset]: + def deserialize_file(self, file_path: pathlib.Path) -> Optional[Asset]: """Deserialize the selected file using the appropriate serializer.""" # Find the correct serializer for the file. file_extension = file_path.suffix.lower() @@ -161,7 +161,7 @@ class AssetOpenDialog(QFileDialog): filenames = self.selectedFiles() if filenames: file_path = pathlib.Path(filenames[0]) - asset = self._deserialize_selected_file(file_path) + asset = self.deserialize_file(file_path) if asset: return file_path, asset return None diff --git a/src/Mod/CAM/Path/Tool/migration/migration.py b/src/Mod/CAM/Path/Tool/migration/migration.py index 049f92cea1..984e7438d9 100644 --- a/src/Mod/CAM/Path/Tool/migration/migration.py +++ b/src/Mod/CAM/Path/Tool/migration/migration.py @@ -30,6 +30,12 @@ import FreeCAD import Path import Path.Preferences import pathlib +import os +import glob +from ..assets.ui import AssetOpenDialog +from ..camassets import cam_assets, ensure_assets_initialized +from ..library.serializers import all_serializers as library_serializers +from ..library.models import Library # Logging setup - same pattern as Job.py if False: @@ -58,17 +64,18 @@ class CAMAssetMigrator: self.pref_group_path = "User parameter:BaseApp/Preferences/Mod/CAM/Migration" def check_migration_needed(self): + self.check_asset_location() + self.check_tool_library_workdir() + + def check_asset_location(self): """ Check if CAM asset migration is needed for version upgrade. This method determines if the current CAM assets are stored in a custom location outside the default user data directory and if migration has not been offered for the current FreeCAD version. - - Returns: - bool: True if migration should be offered, False otherwise """ - Path.Log.info("Starting CAM asset migration check") + Path.Log.debug("Starting CAM asset migration check") try: # Get current directories @@ -100,8 +107,8 @@ class CAMAssetMigrator: Path.Log.debug("Already using current version, no migration needed") return - Path.Log.info("Migration is needed and should be offered") - if self._offer_migration_to_user(): + Path.Log.info("Asset relocation is needed and should be offered") + if self._offer_asset_relocation(): self._migrate_assets(str(current_asset_path)) return @@ -109,15 +116,43 @@ class CAMAssetMigrator: Path.Log.error(f"Error checking CAM asset migration: {e}") import traceback - Path.Log.debug(f"Full traceback: {traceback.format_exc()}") - return False + Path.Log.info(f"Full traceback: {traceback.format_exc()}") + return - def _offer_migration_to_user(self): + def check_tool_library_workdir(self): + workdir_str = "LastPathToolLibrary" + migrated_str = "Migrated" + workdir_str + workdir = Path.Preferences.preferences().GetString(workdir_str) + migrated_dir = Path.Preferences.preferences().GetString(migrated_str) + Path.Log.debug(f"workdir: {workdir}, migrated: {migrated_dir}") + if workdir and workdir != migrated_dir: + # Look for tool libraries to import + if os.path.isdir(workdir): + libraries = [f for f in glob.glob(workdir + os.path.sep + "*.fctl")] + libraries.sort() + if len(libraries): + # Offer library import + if self._offer_tool_library_import(workdir, libraries): + import_dialog = AssetOpenDialog( + cam_assets, + asset_class=Library, + serializers=library_serializers, + parent=None, + ) + for library in libraries: + asset = import_dialog.deserialize_file(pathlib.Path(library)) + if asset: + cam_assets.add(asset) + + # Mark directory as migrated + Path.Preferences.preferences().SetString(migrated_str, workdir) + + def _offer_asset_relocation(self): """ - Present migration dialog to user. + Present asset relocation dialog to user. Returns: - bool: True if user accepted migration, False otherwise + bool: True if user accepted relocation, False otherwise """ # Get current version info major = int(FreeCAD.ConfigGet("BuildVersionMajor")) @@ -127,7 +162,7 @@ class CAMAssetMigrator: # Get current asset path for display current_asset_path = Path.Preferences.getAssetPath() - Path.Log.debug(f"Offering migration to user for version {current_version}") + Path.Log.debug(f"Offering asset relocation to user for version {current_version}") if not FreeCAD.GuiUp: Path.Log.debug("GUI not available, skipping migration offer") @@ -141,7 +176,7 @@ class CAMAssetMigrator: "This will copy your assets to a new directory." ) - Path.Log.debug("Showing migration dialog to user") + Path.Log.debug("Showing asset relocation dialog to user") reply = QMessageBox.question( None, "CAM Asset Migration", msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes @@ -162,6 +197,35 @@ class CAMAssetMigrator: Path.Log.info("User declined migration") return False + def _offer_tool_library_import(self, workdir, libraries): + if not FreeCAD.GuiUp: + Path.Log.debug("GUI not available, skipping tool library import offer") + return False + + library_names = "\n".join([f"• {os.path.basename(library)}" for library in libraries]) + + msg = ( + f"FreeCAD now keeps its tool libraries inside its assets directory." + f"You have {len(libraries)} libraries stored in another working " + f"directory {workdir}. They need to be copied to the assets directory " + f"to continue using them.\n\n" + f"{library_names}\n\n" + f"Would you like to import these libraries?" + ) + + Path.Log.debug("Showing tool library import dialog to user") + + reply = QMessageBox.question( + None, "CAM Tool Library Import", msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes + ) + + if reply == QMessageBox.Yes: + Path.Log.debug("User requested tool library import") + return True + else: + Path.Log.debug("User declined tool library import") + return False + def _migrate_assets(self, source_path): """ Perform actual directory copying and preference updates. From e2b4ea58b821d0c618a88adc1106c07f79831b22 Mon Sep 17 00:00:00 2001 From: David Kaufman Date: Fri, 7 Nov 2025 14:46:02 -0500 Subject: [PATCH 3/5] remove unused import --- src/Mod/CAM/Path/Tool/migration/migration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/CAM/Path/Tool/migration/migration.py b/src/Mod/CAM/Path/Tool/migration/migration.py index 984e7438d9..4c1faab496 100644 --- a/src/Mod/CAM/Path/Tool/migration/migration.py +++ b/src/Mod/CAM/Path/Tool/migration/migration.py @@ -33,7 +33,7 @@ import pathlib import os import glob from ..assets.ui import AssetOpenDialog -from ..camassets import cam_assets, ensure_assets_initialized +from ..camassets import cam_assets from ..library.serializers import all_serializers as library_serializers from ..library.models import Library From f22d0e2292cf08972cf5434f7cf289ef7e3a6a27 Mon Sep 17 00:00:00 2001 From: David Kaufman Date: Fri, 14 Nov 2025 14:37:05 -0500 Subject: [PATCH 4/5] migrate silently, no prompts --- src/Mod/CAM/Path/Tool/assets/ui/filedialog.py | 69 ++++++++++++------- src/Mod/CAM/Path/Tool/migration/migration.py | 44 +++--------- 2 files changed, 52 insertions(+), 61 deletions(-) diff --git a/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py b/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py index f1f106d7a4..1b417e02e6 100644 --- a/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py +++ b/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py @@ -31,6 +31,7 @@ from .util import ( make_export_filters, get_serializer_from_extension, ) +import Path import Path.Preferences as Preferences @@ -57,7 +58,7 @@ class AssetOpenDialog(QFileDialog): if filters: self.selectNameFilter(filters[0]) # Default to "All supported files" - def deserialize_file(self, file_path: pathlib.Path) -> Optional[Asset]: + def deserialize_file(self, file_path: pathlib.Path, quiet=False) -> Optional[Asset]: """Deserialize the selected file using the appropriate serializer.""" # Find the correct serializer for the file. file_extension = file_path.suffix.lower() @@ -65,11 +66,15 @@ class AssetOpenDialog(QFileDialog): self.serializers, file_extension, for_import=True ) if not serializer_class: - QMessageBox.critical( - self, - "Error", - f"No supported serializer found for file extension '{file_extension}'", - ) + message = f"No supported serializer found for file extension '{file_extension}'" + if quiet: + Path.Log.error(message) + else: + QMessageBox.critical( + self, + "Error", + message, + ) return None # Check whether all dependencies for importing the file exist. @@ -102,11 +107,15 @@ class AssetOpenDialog(QFileDialog): break if not dependency_found: - QMessageBox.critical( - self, - "Error", - f"Failed to import {file_path}: required dependency {dependency_uri} not found in stores or in parallel Bit directory", - ) + message = f"Failed to import {file_path}: required dependency {dependency_uri} not found in stores or in parallel Bit directory" + if quiet: + Path.Log.error(message) + else: + QMessageBox.critical( + self, + "Error", + message, + ) return None # If we found external toolbits, ask user if they want to import them @@ -114,20 +123,24 @@ class AssetOpenDialog(QFileDialog): from PySide.QtGui import QMessageBox toolbit_names = [uri.asset_id for uri, _ in external_toolbits] - reply = QMessageBox.question( - self, - "Import External Toolbits", - f"This library references {len(external_toolbits)} toolbit(s) that are not in your local store:\n\n" - + "\n".join(f"• {name}" for name in toolbit_names) - + f"\n\nWould you like to import these toolbits into your local store?\n" - + "This will make them permanently available for use in other libraries.", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.Yes, - ) + if quiet: + Path.Log.info("Importing tool bits for the library") + reply = QMessageBox.Yes + else: + reply = QMessageBox.question( + self, + "Import External Toolbits", + f"This library references {len(external_toolbits)} toolbit(s) that are not in your local store:\n\n" + + "\n".join(f"• {name}" for name in toolbit_names) + + f"\n\nWould you like to import these toolbits into your local store?\n" + + "This will make them permanently available for use in other libraries.", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes, + ) if reply == QMessageBox.Yes: # Import the external toolbits into local store - self._import_external_toolbits(external_toolbits) + self._import_external_toolbits(external_toolbits, quiet=quiet) # After importing, use regular deserialization since toolbits are now in local store else: # User declined import, use context-aware deserialization for external loading @@ -184,7 +197,7 @@ class AssetOpenDialog(QFileDialog): # Fallback to home directory if anything goes wrong return pathlib.Path.home() - def _import_external_toolbits(self, external_toolbits): + def _import_external_toolbits(self, external_toolbits, quiet=False): """Import external toolbits into the local asset store.""" from ...toolbit.serializers import all_serializers as toolbit_serializers from .util import get_serializer_from_extension @@ -228,12 +241,18 @@ class AssetOpenDialog(QFileDialog): message += f"\n\nFailed to import {len(failed_imports)} toolbit(s):\n" + "\n".join( failed_imports ) - QMessageBox.information(self, "Import Results", message) + if quiet: + Path.Log.info(message) + else: + QMessageBox.information(self, "Import Results", message) elif failed_imports: message = f"Failed to import all {len(failed_imports)} toolbit(s):\n" + "\n".join( failed_imports ) - QMessageBox.warning(self, "Import Failed", message) + if quiet: + Path.Log.error(message) + else: + QMessageBox.warning(self, "Import Failed", message) class AssetSaveDialog(QFileDialog): diff --git a/src/Mod/CAM/Path/Tool/migration/migration.py b/src/Mod/CAM/Path/Tool/migration/migration.py index 4c1faab496..40f29c3144 100644 --- a/src/Mod/CAM/Path/Tool/migration/migration.py +++ b/src/Mod/CAM/Path/Tool/migration/migration.py @@ -125,24 +125,25 @@ class CAMAssetMigrator: workdir = Path.Preferences.preferences().GetString(workdir_str) migrated_dir = Path.Preferences.preferences().GetString(migrated_str) Path.Log.debug(f"workdir: {workdir}, migrated: {migrated_dir}") - if workdir and workdir != migrated_dir: + if workdir and not migrated_dir: # Look for tool libraries to import if os.path.isdir(workdir): libraries = [f for f in glob.glob(workdir + os.path.sep + "*.fctl")] libraries.sort() if len(libraries): - # Offer library import - if self._offer_tool_library_import(workdir, libraries): + # Migrate libraries, automatically and silently + Path.Log.info("Migrating tool libraries into CAM assets") + for library in libraries: + Path.Log.info("Migrating " + library) import_dialog = AssetOpenDialog( cam_assets, asset_class=Library, serializers=library_serializers, parent=None, ) - for library in libraries: - asset = import_dialog.deserialize_file(pathlib.Path(library)) - if asset: - cam_assets.add(asset) + asset = import_dialog.deserialize_file(pathlib.Path(library), quiet=True) + if asset: + cam_assets.add(asset) # Mark directory as migrated Path.Preferences.preferences().SetString(migrated_str, workdir) @@ -197,35 +198,6 @@ class CAMAssetMigrator: Path.Log.info("User declined migration") return False - def _offer_tool_library_import(self, workdir, libraries): - if not FreeCAD.GuiUp: - Path.Log.debug("GUI not available, skipping tool library import offer") - return False - - library_names = "\n".join([f"• {os.path.basename(library)}" for library in libraries]) - - msg = ( - f"FreeCAD now keeps its tool libraries inside its assets directory." - f"You have {len(libraries)} libraries stored in another working " - f"directory {workdir}. They need to be copied to the assets directory " - f"to continue using them.\n\n" - f"{library_names}\n\n" - f"Would you like to import these libraries?" - ) - - Path.Log.debug("Showing tool library import dialog to user") - - reply = QMessageBox.question( - None, "CAM Tool Library Import", msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes - ) - - if reply == QMessageBox.Yes: - Path.Log.debug("User requested tool library import") - return True - else: - Path.Log.debug("User declined tool library import") - return False - def _migrate_assets(self, source_path): """ Perform actual directory copying and preference updates. From 767ee484baacd461cddd15bc74eb8f754221ec49 Mon Sep 17 00:00:00 2001 From: David Kaufman Date: Mon, 17 Nov 2025 09:18:30 -0500 Subject: [PATCH 5/5] Hopefully placate CodeQL --- src/Mod/CAM/Path/Tool/assets/ui/filedialog.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py b/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py index 1b417e02e6..884f429389 100644 --- a/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py +++ b/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py @@ -120,8 +120,6 @@ class AssetOpenDialog(QFileDialog): # If we found external toolbits, ask user if they want to import them if external_toolbits: - from PySide.QtGui import QMessageBox - toolbit_names = [uri.asset_id for uri, _ in external_toolbits] if quiet: Path.Log.info("Importing tool bits for the library")