CAM: Fix: ToolBitShapeCustom properties not editable if they had no type specified in the schema
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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]]:
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user