diff --git a/src/Mod/CAM/Path/Tool/docobject/models/docobject.py b/src/Mod/CAM/Path/Tool/docobject/models/docobject.py index 1218fa84fa..8fb3cdd4be 100644 --- a/src/Mod/CAM/Path/Tool/docobject/models/docobject.py +++ b/src/Mod/CAM/Path/Tool/docobject/models/docobject.py @@ -68,6 +68,18 @@ class DetachedDocumentObject: # Initialize Quantity properties with a default value self._properties[name] = FreeCAD.Units.Quantity(0.0) + def removeProperty(self, name: str) -> None: + """Removes a property from the detached object.""" + if name in self._properties: + if name in self.PropertiesList: + self.PropertiesList.remove(name) + del self._properties[name] + self._property_groups.pop(name, None) + self._property_types.pop(name, None) + self._property_docs.pop(name, None) + self._editor_modes.pop(name, None) + self._property_enums.pop(name, None) + def getPropertyByName(self, name: str) -> Any: """Mimics FreeCAD DocumentObject.getPropertyByName.""" return self._properties.get(name) diff --git a/src/Mod/CAM/Path/Tool/shape/doc.py b/src/Mod/CAM/Path/Tool/shape/doc.py index a442a1a5d4..da1bb79421 100644 --- a/src/Mod/CAM/Path/Tool/shape/doc.py +++ b/src/Mod/CAM/Path/Tool/shape/doc.py @@ -67,7 +67,7 @@ def get_unset_value_for(attribute_type: str): def get_object_properties( obj: "FreeCAD.DocumentObject", - props: List[str] | None = None, + props: Optional[List[str]] = None, group: Optional[str] = None, ) -> Dict[str, Tuple[Any, str]]: """ diff --git a/src/Mod/CAM/Path/Tool/shape/models/base.py b/src/Mod/CAM/Path/Tool/shape/models/base.py index 7e8ab2bdc8..77984dc9bf 100644 --- a/src/Mod/CAM/Path/Tool/shape/models/base.py +++ b/src/Mod/CAM/Path/Tool/shape/models/base.py @@ -133,15 +133,15 @@ class ToolBitShape(Asset): shape_classes = {c.name: c for c in ToolBitShape.__subclasses__()} shape_class = shape_classes.get(body_obj.Label) if not shape_class: - return ToolBitShape.get_subclass_by_id("Custom") + return ToolBitShape.get_subclass_by_name("Custom") return shape_class @classmethod def get_shape_class_from_id( cls, shape_id: str, - shape_type: str | None = None, - default: Type["ToolBitShape"] | None = None, + shape_type: Optional[str] = None, + default: Optional[Type["ToolBitShape"]] = None, ) -> Optional[Type["ToolBitShape"]]: """ Extracts the shape class from the given ID and shape_type, retrieving it @@ -228,7 +228,7 @@ class ToolBitShape(Asset): # Find the correct subclass based on the body label shape_class = cls.get_subclass_by_name(body_label) - return shape_class or ToolBitShape.get_subclass_by_id("Custom") + return shape_class or ToolBitShape.get_subclass_by_name("Custom") except zipfile.BadZipFile: raise ValueError("Invalid FCStd file data (not a valid zip archive)") @@ -384,7 +384,7 @@ class ToolBitShape(Asset): f" In future releases, these shapes will not load!" ) for param in missing_params: - param_type = shape_class.get_parameter_property_type(param) + param_type = shape_class.get_schema_property_type(param) loaded_params[param] = get_unset_value_for(param_type) loaded_param_types[param] = param_type # Store the type for missing params @@ -489,7 +489,7 @@ class ToolBitShape(Asset): @classmethod def get_subclass_by_name( - cls, name: str, default: Type["ToolBitShape"] | None = None + cls, name: str, default: Optional[Type["ToolBitShape"]] = None ) -> Optional[Type["ToolBitShape"]]: """ Retrieves a ToolBitShape class by its name or alias. @@ -506,7 +506,7 @@ class ToolBitShape(Asset): @classmethod def guess_subclass_from_name( - cls, name: str, default: Type["ToolBitShape"] | None = None + cls, name: str, default: Optional[Type["ToolBitShape"]] = None ) -> Optional[Type["ToolBitShape"]]: """ Retrieves a ToolBitShape class by its name or alias. @@ -578,12 +578,43 @@ class ToolBitShape(Asset): return entry[0] if entry else str_param_name @classmethod - def get_parameter_property_type(cls, param_name: str) -> str: + def get_schema_property_type(cls, param_name: str) -> str: """ Get the FreeCAD property type string for a given parameter name. """ return cls.schema()[param_name][1] + def get_parameter_property_type( + self, param_name: str, default: str = "App::PropertyString" + ) -> str: + """ + Get the FreeCAD property type string for a given parameter name. + """ + try: + return self.get_schema_property_type(param_name) + except KeyError: + try: + return self._param_types[param_name] + except KeyError: + return default + + def _normalize_value(self, name: str, value: Any) -> Any: + """ + Normalize the value for a parameter based on its expected type. + This is a placeholder for any type-specific normalization logic. + + Args: + name (str): The name of the parameter. + value: The value to normalize. + + Returns: + The normalized value, potentially converted to a FreeCAD.Units.Quantity. + """ + prop_type = self.get_parameter_property_type(name) + if prop_type in ("App::PropertyDistance", "App::PropertyLength", "App::PropertyAngle"): + return FreeCAD.Units.Quantity(value) + return value + def get_parameters(self) -> Dict[str, Any]: """ Get the dictionary of current parameters and their values. @@ -591,7 +622,7 @@ class ToolBitShape(Asset): Returns: dict: A dictionary mapping parameter names to their values. """ - return self._params + return {name: self._normalize_value(name, value) for name, value in self._params.items()} def get_parameter(self, name: str) -> Any: """ @@ -608,7 +639,7 @@ class ToolBitShape(Asset): """ if name not in self.schema(): raise KeyError(f"Shape '{self.name}' has no parameter '{name}'") - return self._params[name] + return self._normalize_value(name, self._params[name]) def set_parameter(self, name: str, value: Any): """ @@ -622,18 +653,7 @@ class ToolBitShape(Asset): Raises: KeyError: If the parameter name is not valid for this shape. """ - if name not in self.schema().keys(): - Path.Log.debug( - f"Shape '{self.name}' was given an invalid parameter '{name}'. Has {self._params}\n" - ) - # Log to confirm this path is taken when an invalid parameter is given - Path.Log.debug( - f"Invalid parameter '{name}' for shape " - f"'{self.name}', returning without raising KeyError." - ) - return - - self._params[name] = value + self._params[name] = self._normalize_value(name, value) def set_parameters(self, **kwargs): """ diff --git a/src/Mod/CAM/Path/Tool/toolbit/models/base.py b/src/Mod/CAM/Path/Tool/toolbit/models/base.py index e6670537ba..7f0139db57 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/models/base.py +++ b/src/Mod/CAM/Path/Tool/toolbit/models/base.py @@ -56,7 +56,7 @@ class ToolBit(Asset, ABC): asset_type: str = "toolbit" SHAPE_CLASS: Type[ToolBitShape] # Abstract class attribute - def __init__(self, tool_bit_shape: ToolBitShape, id: str | None = None): + def __init__(self, tool_bit_shape: ToolBitShape, id: Optional[str] = None): Path.Log.track("ToolBit __init__ called") self.id = id if id is not None else str(uuid.uuid4()) self.obj = DetachedDocumentObject() @@ -136,7 +136,12 @@ class ToolBit(Asset, ABC): return cls.from_shape(tool_bit_shape, attrs, id=attrs.get("id")) @classmethod - def from_shape(cls, tool_bit_shape: ToolBitShape, attrs: Mapping, id: str | None) -> "ToolBit": + def from_shape( + cls, + tool_bit_shape: ToolBitShape, + attrs: Mapping, + id: Optional[str] = None, + ) -> "ToolBit": selected_toolbit_subclass = cls._find_subclass_for_shape(tool_bit_shape) toolbit = selected_toolbit_subclass(tool_bit_shape, id=id) toolbit.label = attrs.get("name") or tool_bit_shape.label @@ -145,18 +150,11 @@ class ToolBit(Asset, ABC): params = attrs.get("parameter", {}) attr = attrs.get("attribute", {}) - # Update parameters; these are stored in the document model object. + # Update parameters. for param_name, param_value in params.items(): - if hasattr(toolbit.obj, param_name): - PathUtil.setProperty(toolbit.obj, param_name, param_value) - else: - 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." - ) + tool_bit_shape.set_parameter(param_name, param_value) - # Update parameters; these are stored in the document model object. + # Update attributes; these are stored in the document model object. for attr_name, attr_value in attr.items(): if hasattr(toolbit.obj, attr_name): PathUtil.setProperty(toolbit.obj, attr_name, attr_value) @@ -167,6 +165,7 @@ class ToolBit(Asset, ABC): f" '{toolbit.obj.Label}'. Skipping." ) + toolbit._update_tool_properties() return toolbit @classmethod @@ -586,7 +585,7 @@ class ToolBit(Asset, ABC): def get_property(self, name: str): return self.obj.getPropertyByName(name) - def get_property_str(self, name: str, default: str | None = None) -> str | None: + def get_property_str(self, name: str, default: Optional[str] = None) -> Optional[str]: value = self.get_property(name) return format_value(value) if value else default @@ -654,8 +653,6 @@ class ToolBit(Asset, ABC): ) continue - docstring = self._tool_bit_shape.get_parameter_label(name) - # Add new property if not hasattr(self.obj, name): self.obj.addProperty(prop_type, name, "Shape", docstring) @@ -682,9 +679,21 @@ class ToolBit(Asset, ABC): continue prop_type = self._tool_bit_shape.get_parameter_type(name) docstring = QT_TRANSLATE_NOOP("App::Property", f"Custom property from shape: {name}") + + # Skip existing properties if they have a different type + if hasattr(self.obj, name) and self.obj.getTypeIdOfProperty(name) != prop_type: + Path.Log.debug( + f"Skipping existing property '{name}' due to type mismatch." + f" has type {self.obj.getTypeIdOfProperty(name)}, expected {prop_type}" + ) + continue + + # Add the property if it does not exist if not hasattr(self.obj, name): self.obj.addProperty(prop_type, name, PropertyGroupShape, docstring) Path.Log.debug(f"Added custom shape property: {name} ({prop_type})") + + # Set the property value PathUtil.setProperty(self.obj, name, value) self.obj.setEditorMode(name, 0) diff --git a/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py b/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py index 27bfd9440c..f9c7c22847 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py +++ b/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py @@ -22,6 +22,7 @@ """Widget for editing a ToolBit object.""" +from typing import Optional from PySide import QtGui, QtCore import FreeCAD import FreeCADGui @@ -38,7 +39,7 @@ class ToolBitPropertiesWidget(QtGui.QWidget): # Signal emitted when the toolbit data has been modified toolBitChanged = QtCore.Signal() - def __init__(self, toolbit: ToolBit | None = None, parent=None, icon: bool = True): + def __init__(self, toolbit: Optional[ToolBit] = None, parent=None, icon: bool = True): super().__init__(parent) self._toolbit = None self._show_shape = icon