diff --git a/src/Mod/CAM/Path/Tool/shape/models/base.py b/src/Mod/CAM/Path/Tool/shape/models/base.py index 301bb7ec30..9f08dac869 100644 --- a/src/Mod/CAM/Path/Tool/shape/models/base.py +++ b/src/Mod/CAM/Path/Tool/shape/models/base.py @@ -275,8 +275,27 @@ class ToolBitShape(Asset): # but keeping for clarity. raise ValueError("Failed to open shape document from bytes") - # Determine the specific subclass of ToolBitShape using the new method - shape_class = ToolBitShape.get_shape_class_from_bytes(data) + # Determine the specific subclass of ToolBitShape. + try: + shape_class = ToolBitShape.get_shape_class_from_bytes(data) + except Exception as e: + Path.Log.warning(f"{id}: Failed to determine shape class from bytes: {e}") + shape_types = [c.name for c in ToolBitShape.__subclasses__()] + shape_class = ToolBitShape.guess_subclass_from_name(id) + if shape_class: + Path.Log.warning( + f'{id}: failed to infer shape type from bytes,' + f' guessing "{shape_class.name}". To fix, name' + f' the body in the shape file to one of: {shape_types}' + ) + else: + Path.Log.warning( + f'{id}: failed to infer shape type from bytes,' + f' using "endmill". To fix, name' + f' the body in the shape file to one of: {shape_types}' + ) + from .endmill import ToolBitShapeEndmill + shape_class = ToolBitShapeEndmill # Load properties from the temporary document props_obj = ToolBitShape._find_property_object(temp_doc) @@ -293,10 +312,16 @@ class ToolBitShape(Asset): if name not in loaded_params or loaded_params[name] is None ] + # For now, we log missing parameters, but do not raise an error. + # This allows for more flexible shape files that may not have all + # parameters set, while still warning the user. + # In the future, we may want to raise an error if critical parameters + # are missing. if missing_params: - raise ValueError( - f"Validation error: Object '{props_obj.Label}' in document bytes " - + f"is missing parameters for {shape_class.__name__}: {', '.join(missing_params)}" + Path.Log.error( + f"Validation error: Object '{props_obj.Label}' in document {id} " + f"is missing parameters for {shape_class.__name__}: {', '.join(missing_params)}." + f" In future releases, these shapes will not load!" ) # Instantiate the specific subclass with the provided ID @@ -415,6 +440,25 @@ class ToolBitShape(Asset): return thecls return default + @classmethod + def guess_subclass_from_name( + cls, name: str, default: Type["ToolBitShape"] | None = None + ) -> Optional[Type["ToolBitShape"]]: + """ + Retrieves a ToolBitShape class by its name or alias. + """ + name = name.lower() + for thecls in cls.__subclasses__(): + if ( + thecls.name.lower() in name + or thecls.__name__.lower() in name + ): + return thecls + for alias in thecls.aliases: + if alias.lower() in name: + return thecls + return default + @classmethod def resolve_name(cls, identifier: str) -> AssetUri: """ diff --git a/src/Mod/CAM/Path/Tool/toolbit/docobject.py b/src/Mod/CAM/Path/Tool/toolbit/docobject.py index 5bc8f29662..5f8cd3521c 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/docobject.py +++ b/src/Mod/CAM/Path/Tool/toolbit/docobject.py @@ -58,6 +58,15 @@ class DetachedDocumentObject: self._property_groups[name] = group self._property_types[name] = thetype self._property_docs[name] = doc + if thetype in [ + "App::PropertyQuantity", + "App::PropertyLength", + "App::PropertyArea", + "App::PropertyVolume", + "App::PropertyAngle", + ]: + # Initialize Quantity properties with a default value + self._properties[name] = FreeCAD.Units.Quantity(0.0) def getPropertyByName(self, name: str) -> Any: """Mimics FreeCAD DocumentObject.getPropertyByName.""" diff --git a/src/Mod/CAM/Path/Tool/toolbit/models/base.py b/src/Mod/CAM/Path/Tool/toolbit/models/base.py index ff26bac41c..02bd587eb7 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/models/base.py +++ b/src/Mod/CAM/Path/Tool/toolbit/models/base.py @@ -32,7 +32,6 @@ from itertools import chain from lazy_loader.lazy_loader import LazyLoader from typing import Any, List, Optional, Tuple, Type, Union, Mapping, cast from PySide.QtCore import QT_TRANSLATE_NOOP -import Part from Path.Base.Generator import toolchange from ...assets import Asset from ...camassets import cam_assets @@ -103,13 +102,26 @@ class ToolBit(Asset, ABC): raise ValueError("ToolBit dictionary is missing 'shape' key") # Find the shape type. + shape_types = [c.name for c in ToolBitShape.__subclasses__()] shape_type = attrs.get("shape-type") shape_class = None if shape_type is None: shape_class = ToolBitShape.get_subclass_by_name(shape_id) if not shape_class: - Path.Log.error(f'failed to infer shape type from {shape_id}; using "endmill"') - shape_class = ToolBitShapeEndmill + shape_class = ToolBitShape.guess_subclass_from_name(shape_id) + if shape_class: + Path.Log.warning( + f'failed to infer shape type from {shape_id},' + f' guessing "{shape_class.name}". To fix, name' + f' the body in the shape file to one of: {shape_types}' + ) + else: + Path.Log.warning( + f'failed to infer shape type from {shape_id},' + f' using "endmill". To fix, name' + f' the body in the shape file to one of: {shape_types}' + ) + shape_class = ToolBitShapeEndmill shape_type = shape_class.name # Try to load the shape, if the asset exists. @@ -120,6 +132,7 @@ class ToolBit(Asset, ABC): tool_bit_shape = cast(ToolBitShape, cam_assets.get(shape_asset_uri)) except FileNotFoundError: pass # Rely on the fallback below + Path.Log.debug(f"ToolBit.from_dict: Shape asset {shape_asset_uri} not found.") # If it does not exist, create a new instance from scratch. params = attrs.get("parameter", {}) @@ -129,6 +142,10 @@ class ToolBit(Asset, ABC): if not shape_class: raise ValueError(f"failed to get shape class from {shape_id}") tool_bit_shape = shape_class(shape_id, **params) + Path.Log.debug( + f"ToolBit.from_dict: created shape instance {tool_bit_shape.name}" + f" from {shape_id}. Uri: {tool_bit_shape.get_uri()}" + ) # Now that we have a shape, create the toolbit instance. return cls.from_shape(tool_bit_shape, attrs, id=attrs.get("id")) @@ -148,7 +165,7 @@ class ToolBit(Asset, ABC): if hasattr(toolbit.obj, param_name): PathUtil.setProperty(toolbit.obj, param_name, param_value) else: - Path.Log.warning( + Path.Log.debug( f" ToolBit {id} Parameter '{param_name}' not found on" f" {selected_toolbit_subclass.__name__} ({tool_bit_shape})" f" '{toolbit.obj.Label}'. Skipping." @@ -159,7 +176,7 @@ class ToolBit(Asset, ABC): if hasattr(toolbit.obj, attr_name): PathUtil.setProperty(toolbit.obj, attr_name, attr_value) else: - Path.Log.warning( + Path.Log.debug( f"ToolBit {id} Attribute '{attr_name}' not found on" f" {selected_toolbit_subclass.__name__} ({tool_bit_shape})" f" '{toolbit.obj.Label}'. Skipping." @@ -667,7 +684,7 @@ class ToolBit(Asset, ABC): # Conditional to avoid unnecessary migration warning when called # from onDocumentRestored. - if getattr(self.obj, name) != value: + if value is not None and getattr(self.obj, name) != value: setattr(self.obj, name, value) # 2. Remove obsolete shape properties @@ -676,7 +693,12 @@ class ToolBit(Asset, ABC): current_shape_prop_names = set(self._get_props("Shape")) new_shape_param_names = self._tool_bit_shape.schema().keys() obsolete = current_shape_prop_names - new_shape_param_names - self._remove_properties("Shape", obsolete) + Path.Log.debug( + f"Removing obsolete shape properties: {obsolete} from {self.obj.Label}" + ) + # Gracefully skipping the deletion for now; + # in future releases we may handle schema violations more strictly + # self._remove_properties("Shape", obsolete) def _update_visual_representation(self): """