From 11d5707141b394c2eb5b9ea021ac8331bd183bba Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Sun, 29 Jun 2025 01:30:06 +0200 Subject: [PATCH 01/13] CAM: Fix: custom shape attributes not showing in toolbit editor --- src/Mod/CAM/CAMTests/TestPathToolShapeDoc.py | 17 ++++++- src/Mod/CAM/Path/Tool/shape/doc.py | 21 +++++--- src/Mod/CAM/Path/Tool/shape/models/base.py | 53 ++++++++++++++++++-- src/Mod/CAM/Path/Tool/toolbit/models/base.py | 23 +++++---- src/Mod/CAM/Path/Tool/toolbit/ui/editor.py | 2 +- 5 files changed, 90 insertions(+), 26 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestPathToolShapeDoc.py b/src/Mod/CAM/CAMTests/TestPathToolShapeDoc.py index 26ac88230f..e73674b388 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolShapeDoc.py +++ b/src/Mod/CAM/CAMTests/TestPathToolShapeDoc.py @@ -34,6 +34,7 @@ class TestPathToolShapeDoc(unittest.TestCase): mock_doc.Objects = [mock_obj] mock_obj.Label = "MockObjectLabel" mock_obj.Name = "MockObjectName" + mock_obj.getTypeIdOfProperty = MagicMock(return_value="App::PropertyString") # Ensure mock_doc also has a Name attribute used in tests/code mock_doc.Name = "Document_Mock" # Used in closeDocument calls @@ -91,7 +92,13 @@ class TestPathToolShapeDoc(unittest.TestCase): setattr(mock_obj, "Length", "50 mm") params = doc.get_object_properties(mock_obj, ["Diameter", "Length"]) # Expecting just the values, not tuples - self.assertEqual(params, {"Diameter": "10 mm", "Length": "50 mm"}) + self.assertEqual( + params, + { + "Diameter": ("10 mm", "App::PropertyString"), + "Length": ("50 mm", "App::PropertyString"), + }, + ) mock_freecad.Console.PrintWarning.assert_not_called() def test_doc_get_object_properties_missing(self): @@ -105,7 +112,13 @@ class TestPathToolShapeDoc(unittest.TestCase): delattr(mock_obj, "Height") params = doc_patched.get_object_properties(mock_obj, ["Diameter", "Height"]) # Expecting just the values, not tuples - self.assertEqual(params, {"Diameter": "10 mm", "Height": None}) # Height is missing + self.assertEqual( + params, + { + "Diameter": ("10 mm", "App::PropertyString"), + "Height": (None, "App::PropertyString"), + }, + ) # Height is missing @patch("FreeCAD.openDocument") @patch("FreeCAD.getDocument") diff --git a/src/Mod/CAM/Path/Tool/shape/doc.py b/src/Mod/CAM/Path/Tool/shape/doc.py index 28387dc3c2..a442a1a5d4 100644 --- a/src/Mod/CAM/Path/Tool/shape/doc.py +++ b/src/Mod/CAM/Path/Tool/shape/doc.py @@ -23,7 +23,7 @@ import FreeCAD import Path import Path.Base.Util as PathUtil -from typing import Dict, List, Any, Optional +from typing import Dict, List, Any, Optional, Tuple import tempfile import os @@ -69,33 +69,38 @@ def get_object_properties( obj: "FreeCAD.DocumentObject", props: List[str] | None = None, group: Optional[str] = None, -) -> Dict[str, Any]: +) -> Dict[str, Tuple[Any, str]]: """ - Extract properties matching expected_params from a FreeCAD PropertyBag. + Extract properties from a FreeCAD PropertyBag, including their types. Issues warnings for missing parameters but does not raise an error. Args: obj: The PropertyBag to extract properties from. - expected_params (List[str]): A list of property names to look for. + props (List[str], optional): A list of property names to look for. + If None, all properties in obj.PropertiesList are considered. + group (str, optional): If provided, only properties belonging to this group are extracted. Returns: - Dict[str, Any]: A dictionary mapping property names to their values. - Values are FreeCAD native types. + Dict[str, Tuple[Any, str]]: A dictionary mapping property names to a tuple + (value, type_id). Values are FreeCAD native types. + If a property is missing, its value will be None. """ properties = {} for name in props or obj.PropertiesList: if group and not obj.getGroupOfProperty(name) == group: continue if hasattr(obj, name): - properties[name] = getattr(obj, name) + value = getattr(obj, name) + type_id = obj.getTypeIdOfProperty(name) + properties[name] = value, type_id else: # Log a warning if a parameter expected by the shape class is missing Path.Log.debug( f"Parameter '{name}' not found on object '{obj.Label}' " f"({obj.Name}). Default value will be used by the shape class." ) - properties[name] = None # Indicate missing value + properties[name] = None, "App::PropertyString" return properties diff --git a/src/Mod/CAM/Path/Tool/shape/models/base.py b/src/Mod/CAM/Path/Tool/shape/models/base.py index de7c42c210..2e06f5887f 100644 --- a/src/Mod/CAM/Path/Tool/shape/models/base.py +++ b/src/Mod/CAM/Path/Tool/shape/models/base.py @@ -41,6 +41,13 @@ from ..doc import ( from .icon import ToolBitShapeIcon +if False: + Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) + Path.Log.trackModule(Path.Log.thisModule()) +else: + Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) + + class ToolBitShape(Asset): """Abstract base class for tool bit shapes.""" @@ -75,6 +82,9 @@ class ToolBitShape(Asset): # Stores default parameter values loaded from the FCStd file self._defaults: Dict[str, Any] = {} + # Stores the FreeCAD property types for each parameter + self._param_types: Dict[str, str] = {} + # Keeps the loaded FreeCAD document content for this instance self._data: Optional[bytes] = None @@ -321,6 +331,7 @@ class ToolBitShape(Asset): Exception: For other potential FreeCAD errors during loading. """ assert serializer == DummyAssetSerializer, "ToolBitShape supports only native import" + Path.Log.debug(f"{id}: ToolBitShape.from_bytes called with {len(data)} bytes") # Open the shape data temporarily to get the Body label and parameters with ShapeDocFromBytes(data) as temp_doc: @@ -335,12 +346,25 @@ class ToolBitShape(Asset): except Exception as e: Path.Log.debug(f"{id}: Failed to determine shape class from bytes: {e}") shape_class = ToolBitShape.get_shape_class_from_id("Custom") + if shape_class is None: + # This should ideally not happen due to get_shape_class_from_bytes fallback + # but added for linter satisfaction. + raise ValueError("Shape class could not be determined.") # Load properties from the temporary document props_obj = ToolBitShape._find_property_object(temp_doc) if not props_obj: raise ValueError("No 'Attributes' PropertyBag object found in document bytes") - loaded_params = get_object_properties(props_obj, group="Shape") + + # loaded_raw_params will now be Dict[str, Tuple[Any, str]] + loaded_raw_params = get_object_properties(props_obj, group="Shape") + + # Separate values and types, and populate _param_types + loaded_params = {} + loaded_param_types = {} + for name, (value, type_id) in loaded_raw_params.items(): + loaded_params[name] = value + loaded_param_types[name] = type_id # For now, we log missing parameters, but do not raise an error. # This allows for more flexible shape files that may not have all @@ -362,11 +386,14 @@ class ToolBitShape(Asset): for param in missing_params: param_type = shape_class.get_parameter_property_type(param) loaded_params[param] = get_unset_value_for(param_type) + loaded_param_types[param] = param_type # Store the type for missing params # Instantiate the specific subclass with the provided ID instance = shape_class(id=id) instance._data = data # Keep the byte content instance._defaults = loaded_params + instance._param_types = loaded_param_types + Path.Log.debug(f"Params: {instance._params} {instance._defaults}") instance._params = instance._defaults | instance._params if dependencies: # dependencies is None = shallow load @@ -442,6 +469,7 @@ class ToolBitShape(Asset): """ if not filepath.exists(): raise FileNotFoundError(f"Shape file not found: {filepath}") + Path.Log.debug(f"{id}: ToolBitShape.from_file called with {filepath}") try: data = filepath.read_bytes() @@ -664,6 +692,13 @@ class ToolBitShape(Asset): Retrieves the thumbnail data for the tool bit shape in PNG format. """ + def get_parameter_type(self, name: str) -> str: + """ + Get the FreeCAD property type string for a given parameter name, + as loaded from the FCStd file. + """ + return self._param_types.get(name, "App::PropertyString") + def get_icon(self) -> Optional[ToolBitShapeIcon]: """ Get the associated ToolBitShapeIcon instance. Tries to load one from @@ -676,18 +711,26 @@ class ToolBitShape(Asset): return self.icon # Try to get a matching SVG from the asset manager. - self.icon = cam_assets.get_or_none(f"toolbitshapesvg://{self.id}.svg") + self.icon = cast( + ToolBitShapeIcon, cam_assets.get_or_none(f"toolbitshapesvg://{self.id}.svg") + ) if self.icon: return self.icon - self.icon = cam_assets.get_or_none(f"toolbitshapesvg://{self.name.lower()}.svg") + self.icon = cast( + ToolBitShapeIcon, cam_assets.get_or_none(f"toolbitshapesvg://{self.name.lower()}.svg") + ) if self.icon: return self.icon # Try to get a matching PNG from the asset manager. - self.icon = cam_assets.get_or_none(f"toolbitshapepng://{self.id}.png") + self.icon = cast( + ToolBitShapeIcon, cam_assets.get_or_none(f"toolbitshapepng://{self.id}.png") + ) if self.icon: return self.icon - self.icon = cam_assets.get_or_none(f"toolbitshapepng://{self.name.lower()}.png") + self.icon = cast( + ToolBitShapeIcon, cam_assets.get_or_none(f"toolbitshapepng://{self.name.lower()}.png") + ) if self.icon: return self.icon return None diff --git a/src/Mod/CAM/Path/Tool/toolbit/models/base.py b/src/Mod/CAM/Path/Tool/toolbit/models/base.py index 6a8d85bd98..e6670537ba 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/models/base.py +++ b/src/Mod/CAM/Path/Tool/toolbit/models/base.py @@ -674,16 +674,19 @@ class ToolBit(Asset, ABC): if value is not None and getattr(self.obj, name) != value: setattr(self.obj, name, value) - # 2. Remove obsolete shape properties - # These are properties currently listed AND in the Shape group, - # but not required by the new shape. - 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 - 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) + # 2. Add additional properties that are part of the shape, + # but not part of the schema. + schema_prop_names = set(self._tool_bit_shape.schema().keys()) + for name, value in self._tool_bit_shape.get_parameters().items(): + if name in schema_prop_names: + continue + prop_type = self._tool_bit_shape.get_parameter_type(name) + docstring = QT_TRANSLATE_NOOP("App::Property", f"Custom property from shape: {name}") + 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})") + PathUtil.setProperty(self.obj, name, value) + self.obj.setEditorMode(name, 0) def _update_visual_representation(self): """ diff --git a/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py b/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py index 09c310e5c1..27bfd9440c 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py +++ b/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py @@ -22,9 +22,9 @@ """Widget for editing a ToolBit object.""" +from PySide import QtGui, QtCore import FreeCAD import FreeCADGui -from PySide import QtGui, QtCore from ...shape.ui.shapewidget import ShapeWidget from ...docobject.ui import DocumentObjectEditorWidget from ..models.base import ToolBit From 33935cf73e90b832a577fb67ee7443770faae85e Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Sun, 29 Jun 2025 11:52:07 +0200 Subject: [PATCH 02/13] CAM: store tool quantities in user units, not metric --- src/Mod/CAM/CAMTests/TestPathToolBitSerializer.py | 2 +- src/Mod/CAM/Path/Tool/toolbit/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestPathToolBitSerializer.py b/src/Mod/CAM/CAMTests/TestPathToolBitSerializer.py index 773641c33e..b2d36cb7ca 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolBitSerializer.py +++ b/src/Mod/CAM/CAMTests/TestPathToolBitSerializer.py @@ -96,7 +96,7 @@ class TestFCTBSerializer(_BaseToolBitSerializerTestCase): self.assertEqual(data.get("name"), "Test Tool") self.assertEqual(data.get("shape"), "endmill.fcstd") self.assertEqual(data.get("parameter", {}).get("Diameter"), "4.12 mm") - self.assertEqual(data.get("parameter", {}).get("Length"), "15.0 mm", data) + self.assertEqual(data.get("parameter", {}).get("Length"), "15.00 mm", data) def test_extract_dependencies(self): """Test dependency extraction for FCTB.""" diff --git a/src/Mod/CAM/Path/Tool/toolbit/util.py b/src/Mod/CAM/Path/Tool/toolbit/util.py index 3a81493ee2..03976184de 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/util.py +++ b/src/Mod/CAM/Path/Tool/toolbit/util.py @@ -25,7 +25,7 @@ import FreeCAD def to_json(value): """Convert a value to JSON format.""" if isinstance(value, FreeCAD.Units.Quantity): - return str(value) + return value.UserString return value From 2f051136fedeab37ec78a9a54498744d812f071a Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Sun, 29 Jun 2025 11:52:22 +0200 Subject: [PATCH 03/13] CAM: Fix: Chipload is not written to fctb file --- src/Mod/CAM/Path/Tool/toolbit/mixins/cutting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/CAM/Path/Tool/toolbit/mixins/cutting.py b/src/Mod/CAM/Path/Tool/toolbit/mixins/cutting.py index 73eb9cec37..086b91a7aa 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/mixins/cutting.py +++ b/src/Mod/CAM/Path/Tool/toolbit/mixins/cutting.py @@ -34,7 +34,7 @@ class CuttingToolMixin: obj.addProperty( "App::PropertyLength", "Chipload", - "Base", + "Attributes", QT_TRANSLATE_NOOP("App::Property", "Chipload per tooth"), ) obj.Chipload = FreeCAD.Units.Quantity("0.0 mm") From 0a7cf449bc78e4c32eb086b2362d645a7d1b5303 Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Sun, 29 Jun 2025 12:57:32 +0200 Subject: [PATCH 04/13] CAM: Fix: Tools without icon now display the thumbnail from the FCStd file --- src/Mod/CAM/Path/Tool/shape/models/base.py | 10 ------- src/Mod/CAM/Path/Tool/shape/ui/shapebutton.py | 23 ++------------- src/Mod/CAM/Path/Tool/shape/ui/shapewidget.py | 29 +++++++++++++++++-- 3 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/Mod/CAM/Path/Tool/shape/models/base.py b/src/Mod/CAM/Path/Tool/shape/models/base.py index 2e06f5887f..7e8ab2bdc8 100644 --- a/src/Mod/CAM/Path/Tool/shape/models/base.py +++ b/src/Mod/CAM/Path/Tool/shape/models/base.py @@ -714,11 +714,6 @@ class ToolBitShape(Asset): self.icon = cast( ToolBitShapeIcon, cam_assets.get_or_none(f"toolbitshapesvg://{self.id}.svg") ) - if self.icon: - return self.icon - self.icon = cast( - ToolBitShapeIcon, cam_assets.get_or_none(f"toolbitshapesvg://{self.name.lower()}.svg") - ) if self.icon: return self.icon @@ -726,11 +721,6 @@ class ToolBitShape(Asset): self.icon = cast( ToolBitShapeIcon, cam_assets.get_or_none(f"toolbitshapepng://{self.id}.png") ) - if self.icon: - return self.icon - self.icon = cast( - ToolBitShapeIcon, cam_assets.get_or_none(f"toolbitshapepng://{self.name.lower()}.png") - ) if self.icon: return self.icon return None diff --git a/src/Mod/CAM/Path/Tool/shape/ui/shapebutton.py b/src/Mod/CAM/Path/Tool/shape/ui/shapebutton.py index 1b5979ded2..78e9591c0f 100644 --- a/src/Mod/CAM/Path/Tool/shape/ui/shapebutton.py +++ b/src/Mod/CAM/Path/Tool/shape/ui/shapebutton.py @@ -20,6 +20,7 @@ # * * # *************************************************************************** from PySide import QtGui, QtCore +from .shapewidget import ShapeWidget class ShapeButton(QtGui.QToolButton): @@ -27,10 +28,6 @@ class ShapeButton(QtGui.QToolButton): super(ShapeButton, self).__init__(parent) self.shape = shape - # Remove default text handling and use a custom layout - # self.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) - # self.setText(f"{shape.label}\n{shape.id}") - self.vbox = QtGui.QVBoxLayout(self) self.vbox.setAlignment( QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter @@ -38,8 +35,7 @@ class ShapeButton(QtGui.QToolButton): self.vbox.setContentsMargins(0, 0, 0, 0) self.vbox.setSpacing(0) - self.icon_widget = QtGui.QLabel() - self.icon_widget.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) + self.icon_widget = ShapeWidget(self.shape, QtCore.QSize(71, 70)) self.label_widget = QtGui.QLabel(shape.label) self.label_widget.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) @@ -60,22 +56,7 @@ class ShapeButton(QtGui.QToolButton): self.setFixedSize(128, 128) self.setBaseSize(128, 128) - # Adjust icon size to make space for text. - # Total height is 128. Let's allocate 70 for icon, 25 for label, 25 for ID. - self.icon_size = QtCore.QSize(71, 70) - # self.setIconSize(self.icon_size) # Removed as custom layout handles sizing - - self._update_icon() def set_text(self, text): # Update the text of the label widget self.label_widget.setText(text) - - def _update_icon(self): - icon = self.shape.get_icon() - if icon: - # Set the pixmap on the icon_widget QLabel - pixmap = icon.get_qpixmap(self.icon_size) - self.icon_widget.setPixmap(pixmap) - else: - self.icon_widget.clear() # Clear pixmap if no icon diff --git a/src/Mod/CAM/Path/Tool/shape/ui/shapewidget.py b/src/Mod/CAM/Path/Tool/shape/ui/shapewidget.py index 2bea5969f2..0829d44ce5 100644 --- a/src/Mod/CAM/Path/Tool/shape/ui/shapewidget.py +++ b/src/Mod/CAM/Path/Tool/shape/ui/shapewidget.py @@ -19,18 +19,32 @@ # * USA * # * * # *************************************************************************** +from typing import Optional from PySide import QtGui, QtCore +def _png2qpixmap(data, icon_size): + pixmap = QtGui.QPixmap() + pixmap.loadFromData(data, "PNG") + # Scale the pixmap if the requested size is different + if pixmap.size() != icon_size: + pixmap = pixmap.scaled( + icon_size, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation, + ) + return pixmap + + class ShapeWidget(QtGui.QWidget): - def __init__(self, shape, parent=None): + def __init__(self, shape, icon_size: Optional[QtCore.QSize] = None, parent=None): super(ShapeWidget, self).__init__(parent) self.layout = QtGui.QVBoxLayout(self) self.layout.setAlignment(QtCore.Qt.AlignHCenter) self.shape = shape ratio = self.devicePixelRatioF() - self.icon_size = QtCore.QSize(200 * ratio, 235 * ratio) + self.icon_size = icon_size or QtCore.QSize(200 * ratio, 235 * ratio) self.icon_widget = QtGui.QLabel() self.layout.addWidget(self.icon_widget) @@ -41,3 +55,14 @@ class ShapeWidget(QtGui.QWidget): if icon: pixmap = icon.get_qpixmap(self.icon_size) self.icon_widget.setPixmap(pixmap) + return + + thumbnail = self.shape.get_thumbnail() + if thumbnail: + ratio = self.devicePixelRatioF() + size = self.icon_size * ratio + pixmap = _png2qpixmap(thumbnail, size) + self.icon_widget.setPixmap(pixmap) + return + + self.icon_widget.clear() # Clear pixmap if no icon From 3521ae91ccff8e0918e226d7f1eddaeb522d21d9 Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Mon, 9 Jun 2025 11:47:58 +0200 Subject: [PATCH 05/13] CAM: reduce default size of toolbit editor dialog --- src/Mod/CAM/Gui/Resources/panels/ToolBitEditor.ui | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Mod/CAM/Gui/Resources/panels/ToolBitEditor.ui b/src/Mod/CAM/Gui/Resources/panels/ToolBitEditor.ui index ebb60688a5..8153ae64fe 100644 --- a/src/Mod/CAM/Gui/Resources/panels/ToolBitEditor.ui +++ b/src/Mod/CAM/Gui/Resources/panels/ToolBitEditor.ui @@ -6,8 +6,8 @@ 0 0 - 1000 - 900 + 750 + 800 @@ -41,14 +41,14 @@ - 1000 - 1000 + 650 + 850 - 1000 - 1000 + 650 + 850 From 788560d709ad8b86f4cd2ffc41fb26a2b38e3da4 Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Mon, 9 Jun 2025 11:48:47 +0200 Subject: [PATCH 06/13] CAM: fix toolbit editor shape resolution for high DPI displays --- src/Mod/CAM/Path/Tool/shape/ui/shapewidget.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Mod/CAM/Path/Tool/shape/ui/shapewidget.py b/src/Mod/CAM/Path/Tool/shape/ui/shapewidget.py index 0829d44ce5..bcbc505178 100644 --- a/src/Mod/CAM/Path/Tool/shape/ui/shapewidget.py +++ b/src/Mod/CAM/Path/Tool/shape/ui/shapewidget.py @@ -21,6 +21,7 @@ # *************************************************************************** from typing import Optional from PySide import QtGui, QtCore +from ..models.base import ToolBitShape def _png2qpixmap(data, icon_size): @@ -37,30 +38,29 @@ def _png2qpixmap(data, icon_size): class ShapeWidget(QtGui.QWidget): - def __init__(self, shape, icon_size: Optional[QtCore.QSize] = None, parent=None): + def __init__(self, shape: ToolBitShape, icon_size: Optional[QtCore.QSize] = None, parent=None): super(ShapeWidget, self).__init__(parent) self.layout = QtGui.QVBoxLayout(self) self.layout.setAlignment(QtCore.Qt.AlignHCenter) self.shape = shape - ratio = self.devicePixelRatioF() - self.icon_size = icon_size or QtCore.QSize(200 * ratio, 235 * ratio) + self.icon_size = icon_size or QtCore.QSize(200, 235) self.icon_widget = QtGui.QLabel() self.layout.addWidget(self.icon_widget) self._update_icon() def _update_icon(self): + ratio = self.devicePixelRatioF() + size = self.icon_size * ratio icon = self.shape.get_icon() if icon: - pixmap = icon.get_qpixmap(self.icon_size) + pixmap = icon.get_qpixmap(size) self.icon_widget.setPixmap(pixmap) return thumbnail = self.shape.get_thumbnail() if thumbnail: - ratio = self.devicePixelRatioF() - size = self.icon_size * ratio pixmap = _png2qpixmap(thumbnail, size) self.icon_widget.setPixmap(pixmap) return From da8eec5744685e1b347035e59eb1d014f6a23ab1 Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Sun, 29 Jun 2025 21:34:06 +0200 Subject: [PATCH 07/13] CAM: Fix: ToolBitShapeCustom properties not editable if they had no type specified in the schema --- .../Path/Tool/docobject/models/docobject.py | 12 ++++ src/Mod/CAM/Path/Tool/shape/doc.py | 2 +- src/Mod/CAM/Path/Tool/shape/models/base.py | 64 ++++++++++++------- src/Mod/CAM/Path/Tool/toolbit/models/base.py | 39 ++++++----- src/Mod/CAM/Path/Tool/toolbit/ui/editor.py | 3 +- 5 files changed, 81 insertions(+), 39 deletions(-) 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 From 2d1746d04f5d40c7dad40bac5c672ede531003ff Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Sun, 29 Jun 2025 21:39:04 +0200 Subject: [PATCH 08/13] CAM: gracefully handle tool numbers that are strings in the tool library JSON --- src/Mod/CAM/Path/Tool/library/serializers/fctl.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Mod/CAM/Path/Tool/library/serializers/fctl.py b/src/Mod/CAM/Path/Tool/library/serializers/fctl.py index c4de58afc8..8b6737bddb 100644 --- a/src/Mod/CAM/Path/Tool/library/serializers/fctl.py +++ b/src/Mod/CAM/Path/Tool/library/serializers/fctl.py @@ -81,7 +81,11 @@ class FCTLSerializer(AssetSerializer): tools_list = data_dict.get("tools", []) for tool_data in tools_list: - tool_no = tool_data["nr"] + try: + tool_no = int(tool_data["nr"]) + except ValueError: + Path.Log.warning(f"Invalid tool ID in tool data: {tool_data}. Skipping.") + continue tool_id = pathlib.Path(tool_data["path"]).stem # Extract tool ID tool_uri = AssetUri(f"toolbit://{tool_id}") tool = dependencies.get(tool_uri) From 3829e1db762bd1cdbb1be9e7420de92c0898c2ed Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Mon, 30 Jun 2025 20:26:51 +0200 Subject: [PATCH 09/13] CAM: Fix: Remove invalid normalization of shape name --- src/Mod/CAM/CAMTests/TestPathToolShapeClasses.py | 12 +++++------- src/Mod/CAM/Path/Tool/shape/models/base.py | 10 ++-------- src/Mod/CAM/Path/Tool/toolbit/serializers/fctb.py | 11 +++++++++++ 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestPathToolShapeClasses.py b/src/Mod/CAM/CAMTests/TestPathToolShapeClasses.py index aa34fd4f66..98f73f4b89 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolShapeClasses.py +++ b/src/Mod/CAM/CAMTests/TestPathToolShapeClasses.py @@ -151,13 +151,11 @@ class TestPathToolShapeClasses(PathTestWithAssets): def test_base_resolve_name(self): """Test resolving shape aliases to canonical names.""" self.assertEqual(ToolBitShape.resolve_name("ballend").asset_id, "ballend") - self.assertEqual(ToolBitShape.resolve_name("Ballend").asset_id, "ballend") - self.assertEqual(ToolBitShape.resolve_name("v-bit").asset_id, "vbit") - self.assertEqual(ToolBitShape.resolve_name("VBit").asset_id, "vbit") - self.assertEqual(ToolBitShape.resolve_name("torus").asset_id, "bullnose") - self.assertEqual(ToolBitShape.resolve_name("bullnose").asset_id, "bullnose") - self.assertEqual(ToolBitShape.resolve_name("slitting-saw").asset_id, "slittingsaw") - self.assertEqual(ToolBitShape.resolve_name("SlittingSaw").asset_id, "slittingsaw") + self.assertEqual(ToolBitShape.resolve_name("v-bit").asset_id, "v-bit") + self.assertEqual(ToolBitShape.resolve_name("vbit").asset_id, "vbit") + self.assertEqual(ToolBitShape.resolve_name("torus").asset_id, "torus") + self.assertEqual(ToolBitShape.resolve_name("torus.fcstd").asset_id, "torus") + self.assertEqual(ToolBitShape.resolve_name("SlittingSaw").asset_id, "SlittingSaw") # Test unknown name - should return the input name self.assertEqual(ToolBitShape.resolve_name("nonexistent").asset_id, "nonexistent") self.assertEqual(ToolBitShape.resolve_name("UnknownShape").asset_id, "UnknownShape") diff --git a/src/Mod/CAM/Path/Tool/shape/models/base.py b/src/Mod/CAM/Path/Tool/shape/models/base.py index 77984dc9bf..f182c38fd6 100644 --- a/src/Mod/CAM/Path/Tool/shape/models/base.py +++ b/src/Mod/CAM/Path/Tool/shape/models/base.py @@ -523,7 +523,7 @@ class ToolBitShape(Asset): @classmethod def resolve_name(cls, identifier: str) -> AssetUri: """ - Resolves an identifier (alias, name, filename, or URI) to a Uri object. + Resolves an identifier (name, filename, or URI) to a Uri object. """ # 1. If the input is a url string, return the AssetUri for it. if AssetUri.is_uri(identifier): @@ -535,13 +535,7 @@ class ToolBitShape(Asset): if pathlib.Path(identifier).suffix.lower() == ".fcstd": asset_name = os.path.splitext(os.path.basename(identifier))[0] - # 3. Use get_subclass_by_name to try to resolve alias to a class. - # if one is found, use the class.name. - shape_class = cls.get_subclass_by_name(asset_name.lower()) - if shape_class: - asset_name = shape_class.name.lower() - - # 4. Construct the Uri using AssetUri.build() and return it + # 3. Construct the Uri using AssetUri.build() and return it return AssetUri.build( asset_type="toolbitshape", asset_id=asset_name, diff --git a/src/Mod/CAM/Path/Tool/toolbit/serializers/fctb.py b/src/Mod/CAM/Path/Tool/toolbit/serializers/fctb.py index 67d28eff74..90cf28aa0c 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/serializers/fctb.py +++ b/src/Mod/CAM/Path/Tool/toolbit/serializers/fctb.py @@ -28,6 +28,13 @@ from ...shape import ToolBitShape from ..models.base import ToolBit +if False: + Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) + Path.Log.trackModule(Path.Log.thisModule()) +else: + Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) + + class FCTBSerializer(AssetSerializer): for_class = ToolBit mime_type = "application/x-freecad-toolbit" @@ -70,6 +77,7 @@ class FCTBSerializer(AssetSerializer): if dependencies is None: # Shallow load: dependencies are not resolved. # Delegate to from_dict with shallow=True. + Path.Log.debug(f"FCTBSerializer.deserialize: shallow. id = {id!r}, attrs = {attrs!r}") return ToolBit.from_dict(attrs, shallow=True) # Full load: dependencies are resolved. @@ -93,6 +101,9 @@ class FCTBSerializer(AssetSerializer): ) # Find the correct ToolBit subclass for the shape + Path.Log.debug( + f"FCTBSerializer.deserialize: shape = {shape!r}, id = {id!r}, params = {shape.get_parameters()}" + ) return ToolBit.from_shape(shape, attrs, id) @classmethod From 7f7f5ba2bb754201fe098378be739d0ae2718f11 Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Mon, 30 Jun 2025 20:27:26 +0200 Subject: [PATCH 10/13] CAM: make ToolBitShape more robust against invalidly typed parameters in toolbit files (e.g. Flutes with string value) --- src/Mod/CAM/Path/Tool/shape/models/base.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Mod/CAM/Path/Tool/shape/models/base.py b/src/Mod/CAM/Path/Tool/shape/models/base.py index f182c38fd6..6a2122bb33 100644 --- a/src/Mod/CAM/Path/Tool/shape/models/base.py +++ b/src/Mod/CAM/Path/Tool/shape/models/base.py @@ -607,7 +607,17 @@ class ToolBitShape(Asset): 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 + elif prop_type == "App::PropertyInteger": + return int(value) + elif prop_type == "App::PropertyFloat": + return float(value) + elif prop_type == "App::PropertyBool": + if value in ("True", "true", "1"): + return True + elif value in ("False", "false", "0"): + return False + return bool(value) + return str(value) def get_parameters(self) -> Dict[str, Any]: """ From b5e4b400ad7033f608503cdd363d097ad4f8f53a Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Mon, 30 Jun 2025 20:27:54 +0200 Subject: [PATCH 11/13] CAM: Fix: Tool editor ignored shape attributes if they were not in the Shape group. Now it reads all except Base --- src/Mod/CAM/Path/Tool/shape/doc.py | 3 +++ src/Mod/CAM/Path/Tool/shape/models/base.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Mod/CAM/Path/Tool/shape/doc.py b/src/Mod/CAM/Path/Tool/shape/doc.py index da1bb79421..d1c5c463df 100644 --- a/src/Mod/CAM/Path/Tool/shape/doc.py +++ b/src/Mod/CAM/Path/Tool/shape/doc.py @@ -69,6 +69,7 @@ def get_object_properties( obj: "FreeCAD.DocumentObject", props: Optional[List[str]] = None, group: Optional[str] = None, + exclude_groups: Optional[List[str]] = None, ) -> Dict[str, Tuple[Any, str]]: """ Extract properties from a FreeCAD PropertyBag, including their types. @@ -90,6 +91,8 @@ def get_object_properties( for name in props or obj.PropertiesList: if group and not obj.getGroupOfProperty(name) == group: continue + if exclude_groups and obj.getGroupOfProperty(name) in exclude_groups: + continue if hasattr(obj, name): value = getattr(obj, name) type_id = obj.getTypeIdOfProperty(name) diff --git a/src/Mod/CAM/Path/Tool/shape/models/base.py b/src/Mod/CAM/Path/Tool/shape/models/base.py index 6a2122bb33..312109318b 100644 --- a/src/Mod/CAM/Path/Tool/shape/models/base.py +++ b/src/Mod/CAM/Path/Tool/shape/models/base.py @@ -357,7 +357,7 @@ class ToolBitShape(Asset): raise ValueError("No 'Attributes' PropertyBag object found in document bytes") # loaded_raw_params will now be Dict[str, Tuple[Any, str]] - loaded_raw_params = get_object_properties(props_obj, group="Shape") + loaded_raw_params = get_object_properties(props_obj, exclude_groups=["", "Base"]) # Separate values and types, and populate _param_types loaded_params = {} From 16d90cfbb9ddc2402ce2764f01052dd83d066073 Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Mon, 30 Jun 2025 21:48:21 +0200 Subject: [PATCH 12/13] CAM: Fix: shape parameters coming from the attributes section in the fctb file were not normalized --- src/Mod/CAM/Path/Tool/toolbit/models/base.py | 6 +++++- src/Mod/CAM/Path/Tool/toolbit/serializers/fctb.py | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Mod/CAM/Path/Tool/toolbit/models/base.py b/src/Mod/CAM/Path/Tool/toolbit/models/base.py index 7f0139db57..3f3c7155a4 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/models/base.py +++ b/src/Mod/CAM/Path/Tool/toolbit/models/base.py @@ -154,8 +154,12 @@ class ToolBit(Asset, ABC): for param_name, param_value in params.items(): tool_bit_shape.set_parameter(param_name, param_value) - # Update attributes; these are stored in the document model object. + # Update attributes; the separation between parameters and attributes + # is currently not well defined, so for now we add them to the + # ToolBitShape and the DocumentObject. + # Discussion: https://github.com/FreeCAD/FreeCAD/issues/21722 for attr_name, attr_value in attr.items(): + tool_bit_shape.set_parameter(attr_name, attr_value) if hasattr(toolbit.obj, attr_name): PathUtil.setProperty(toolbit.obj, attr_name, attr_value) else: diff --git a/src/Mod/CAM/Path/Tool/toolbit/serializers/fctb.py b/src/Mod/CAM/Path/Tool/toolbit/serializers/fctb.py index 90cf28aa0c..0722d770a2 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/serializers/fctb.py +++ b/src/Mod/CAM/Path/Tool/toolbit/serializers/fctb.py @@ -47,7 +47,7 @@ class FCTBSerializer(AssetSerializer): @classmethod def extract_dependencies(cls, data: bytes) -> List[AssetUri]: """Extracts URIs of dependencies from serialized data.""" - Path.Log.info(f"FCTBSerializer.extract_dependencies: raw data = {data!r}") + Path.Log.debug(f"FCTBSerializer.extract_dependencies: raw data = {data!r}") data_dict = json.loads(data.decode("utf-8")) shape = data_dict["shape"] return [ToolBitShape.resolve_name(shape)] @@ -102,7 +102,8 @@ class FCTBSerializer(AssetSerializer): # Find the correct ToolBit subclass for the shape Path.Log.debug( - f"FCTBSerializer.deserialize: shape = {shape!r}, id = {id!r}, params = {shape.get_parameters()}" + f"FCTBSerializer.deserialize: shape = {shape!r}, id = {id!r}," + f" params = {shape.get_parameters()}, attrs = {attrs!r}" ) return ToolBit.from_shape(shape, attrs, id) From 173ee65b8ba6b531da244f0d2f189380c0b511ca Mon Sep 17 00:00:00 2001 From: Samuel Abels Date: Mon, 30 Jun 2025 22:59:14 +0200 Subject: [PATCH 13/13] CAM: Fix: rename vbit and threadmill back to v-bit and thread-mill to avoid backward compatibility issues --- src/Mod/CAM/CAMTests/TestPathToolShapeClasses.py | 8 ++++---- src/Mod/CAM/CMakeLists.txt | 8 ++++---- src/Mod/CAM/Path/Op/SurfaceSupport.py | 1 + .../CAM/Path/Tool/toolbit/serializers/camotics.py | 1 + .../Shape/{threadmill.fcstd => thread-mill.fcstd} | Bin .../Tools/Shape/{threadmill.svg => thread-mill.svg} | 2 +- src/Mod/CAM/Tools/Shape/{vbit.fcstd => v-bit.fcstd} | Bin src/Mod/CAM/Tools/Shape/{vbit.svg => v-bit.svg} | 2 +- 8 files changed, 12 insertions(+), 10 deletions(-) rename src/Mod/CAM/Tools/Shape/{threadmill.fcstd => thread-mill.fcstd} (100%) rename src/Mod/CAM/Tools/Shape/{threadmill.svg => thread-mill.svg} (99%) rename src/Mod/CAM/Tools/Shape/{vbit.fcstd => v-bit.fcstd} (100%) rename src/Mod/CAM/Tools/Shape/{vbit.svg => v-bit.svg} (99%) diff --git a/src/Mod/CAM/CAMTests/TestPathToolShapeClasses.py b/src/Mod/CAM/CAMTests/TestPathToolShapeClasses.py index 98f73f4b89..dc09140ef7 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolShapeClasses.py +++ b/src/Mod/CAM/CAMTests/TestPathToolShapeClasses.py @@ -320,13 +320,13 @@ class TestPathToolShapeClasses(PathTestWithAssets): def test_toolbitshapethreadmill_defaults(self): """Test ToolBitShapeThreadMill default parameters and labels.""" # Provide a dummy filepath for instantiation. - shape = self._test_shape_common("threadmill") + shape = self._test_shape_common("thread-mill") self.assertEqual(shape["Diameter"].Value, 5.0) self.assertEqual(unit(shape["Diameter"]), "mm") self.assertEqual(shape["cuttingAngle"].Value, 60.0) self.assertEqual(unit(shape["cuttingAngle"]), "°") # Need an instance to get parameter labels, get it from the asset manager - uri = ToolBitShape.resolve_name("threadmill") + uri = ToolBitShape.resolve_name("thread-mill") instance = self.assets.get(uri) self.assertEqual(instance.get_parameter_label("cuttingAngle"), "Cutting angle") @@ -346,7 +346,7 @@ class TestPathToolShapeClasses(PathTestWithAssets): def test_toolbitshapevbit_defaults(self): """Test ToolBitShapeVBit default parameters and labels.""" # Provide a dummy filepath for instantiation. - shape = self._test_shape_common("vbit") + shape = self._test_shape_common("v-bit") self.assertEqual(shape["Diameter"].Value, 10.0) self.assertEqual(unit(shape["Diameter"]), "mm") self.assertEqual(shape["CuttingEdgeAngle"].Value, 90.0) @@ -354,7 +354,7 @@ class TestPathToolShapeClasses(PathTestWithAssets): self.assertEqual(shape["TipDiameter"].Value, 1.0) self.assertEqual(unit(shape["TipDiameter"]), "mm") # Need an instance to get parameter labels, get it from the asset manager - uri = ToolBitShape.resolve_name("vbit") + uri = ToolBitShape.resolve_name("v-bit") instance = self.assets.get(uri) self.assertEqual(instance.get_parameter_label("CuttingEdgeAngle"), "Cutting edge angle") diff --git a/src/Mod/CAM/CMakeLists.txt b/src/Mod/CAM/CMakeLists.txt index 6ba07c39af..b2e1e55ad7 100644 --- a/src/Mod/CAM/CMakeLists.txt +++ b/src/Mod/CAM/CMakeLists.txt @@ -462,10 +462,10 @@ SET(Tools_Shape_SRCS Tools/Shape/slittingsaw.svg Tools/Shape/tap.fcstd Tools/Shape/tap.svg - Tools/Shape/threadmill.fcstd - Tools/Shape/threadmill.svg - Tools/Shape/vbit.fcstd - Tools/Shape/vbit.svg + Tools/Shape/thread-mill.fcstd + Tools/Shape/thread-mill.svg + Tools/Shape/v-bit.fcstd + Tools/Shape/v-bit.svg ) SET(Tests_SRCS diff --git a/src/Mod/CAM/Path/Op/SurfaceSupport.py b/src/Mod/CAM/Path/Op/SurfaceSupport.py index 9962dcdf02..55bce6c161 100644 --- a/src/Mod/CAM/Path/Op/SurfaceSupport.py +++ b/src/Mod/CAM/Path/Op/SurfaceSupport.py @@ -2652,6 +2652,7 @@ class OCL_Tool: "drill": "ConeCutter", "engraver": "ConeCutter", "v_bit": "ConeCutter", + "v-bit": "ConeCutter", "vbit": "ConeCutter", "chamfer": "None", } diff --git a/src/Mod/CAM/Path/Tool/toolbit/serializers/camotics.py b/src/Mod/CAM/Path/Tool/toolbit/serializers/camotics.py index aea9787a19..f5c8f10d15 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/serializers/camotics.py +++ b/src/Mod/CAM/Path/Tool/toolbit/serializers/camotics.py @@ -33,6 +33,7 @@ from ...assets.asset import Asset SHAPEMAP = { "ballend": "Ballnose", "endmill": "Cylindrical", + "v-bit": "Conical", "vbit": "Conical", "chamfer": "Snubnose", } diff --git a/src/Mod/CAM/Tools/Shape/threadmill.fcstd b/src/Mod/CAM/Tools/Shape/thread-mill.fcstd similarity index 100% rename from src/Mod/CAM/Tools/Shape/threadmill.fcstd rename to src/Mod/CAM/Tools/Shape/thread-mill.fcstd diff --git a/src/Mod/CAM/Tools/Shape/threadmill.svg b/src/Mod/CAM/Tools/Shape/thread-mill.svg similarity index 99% rename from src/Mod/CAM/Tools/Shape/threadmill.svg rename to src/Mod/CAM/Tools/Shape/thread-mill.svg index aa811b634c..904d1f3aa5 100644 --- a/src/Mod/CAM/Tools/Shape/threadmill.svg +++ b/src/Mod/CAM/Tools/Shape/thread-mill.svg @@ -5,7 +5,7 @@ viewBox="0 0 210 297" height="297mm" width="210mm" - sodipodi:docname="threadmill.svg" + sodipodi:docname="thread-mill.svg" xml:space="preserve" inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" diff --git a/src/Mod/CAM/Tools/Shape/vbit.fcstd b/src/Mod/CAM/Tools/Shape/v-bit.fcstd similarity index 100% rename from src/Mod/CAM/Tools/Shape/vbit.fcstd rename to src/Mod/CAM/Tools/Shape/v-bit.fcstd diff --git a/src/Mod/CAM/Tools/Shape/vbit.svg b/src/Mod/CAM/Tools/Shape/v-bit.svg similarity index 99% rename from src/Mod/CAM/Tools/Shape/vbit.svg rename to src/Mod/CAM/Tools/Shape/v-bit.svg index 975022b615..e6e327b813 100644 --- a/src/Mod/CAM/Tools/Shape/vbit.svg +++ b/src/Mod/CAM/Tools/Shape/v-bit.svg @@ -5,7 +5,7 @@ viewBox="0 0 210 297" height="297mm" width="210mm" - sodipodi:docname="vbit.svg" + sodipodi:docname="v-bit.svg" xml:space="preserve" inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"