diff --git a/src/Mod/CAM/CAMTests/TestPathPropertyBag.py b/src/Mod/CAM/CAMTests/TestPathPropertyBag.py index 0cf14f129a..3cff57c1e3 100644 --- a/src/Mod/CAM/CAMTests/TestPathPropertyBag.py +++ b/src/Mod/CAM/CAMTests/TestPathPropertyBag.py @@ -25,6 +25,20 @@ import Path.Base.PropertyBag as PathPropertyBag import CAMTests.PathTestUtils as PathTestUtils +def as_group_list(groups): + """Normalize CustomPropertyGroups to a list of strings.""" + if groups is None: + return [] + if isinstance(groups, (list, tuple)): + return list(groups) + if isinstance(groups, str): + return [groups] + try: + return list(groups) + except Exception: + return [str(groups)] + + class TestPathPropertyBag(PathTestUtils.PathTestBase): def setUp(self): self.doc = FreeCAD.newDocument("test-property-bag") @@ -37,7 +51,7 @@ class TestPathPropertyBag(PathTestUtils.PathTestBase): bag = PathPropertyBag.Create() self.assertTrue(hasattr(bag, "Proxy")) self.assertEqual(bag.Proxy.getCustomProperties(), []) - self.assertEqual(bag.CustomPropertyGroups, []) + self.assertEqual(as_group_list(bag.CustomPropertyGroups), []) def test01(self): """adding properties to a PropertyBag is tracked properly""" @@ -48,7 +62,7 @@ class TestPathPropertyBag(PathTestUtils.PathTestBase): bag.Title = "Madame" self.assertEqual(bag.Title, "Madame") self.assertEqual(bag.Proxy.getCustomProperties(), ["Title"]) - self.assertEqual(bag.CustomPropertyGroups, ["Address"]) + self.assertEqual(as_group_list(bag.CustomPropertyGroups), ["Address"]) def test02(self): """refreshCustomPropertyGroups deletes empty groups""" @@ -59,7 +73,7 @@ class TestPathPropertyBag(PathTestUtils.PathTestBase): bag.removeProperty("Title") proxy.refreshCustomPropertyGroups() self.assertEqual(bag.Proxy.getCustomProperties(), []) - self.assertEqual(bag.CustomPropertyGroups, []) + self.assertEqual(as_group_list(bag.CustomPropertyGroups), []) def test03(self): """refreshCustomPropertyGroups does not delete non-empty groups""" @@ -72,4 +86,4 @@ class TestPathPropertyBag(PathTestUtils.PathTestBase): bag.removeProperty("Gender") proxy.refreshCustomPropertyGroups() self.assertEqual(bag.Proxy.getCustomProperties(), ["Title"]) - self.assertEqual(bag.CustomPropertyGroups, ["Address"]) + self.assertEqual(as_group_list(bag.CustomPropertyGroups), ["Address"]) diff --git a/src/Mod/CAM/CAMTests/TestPathToolBitListWidget.py b/src/Mod/CAM/CAMTests/TestPathToolBitListWidget.py index 2c233856ca..182695b51f 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolBitListWidget.py +++ b/src/Mod/CAM/CAMTests/TestPathToolBitListWidget.py @@ -55,7 +55,7 @@ class TestToolBitListWidget(PathTestWithAssets): self.assertEqual(cell_widget.tool_no, str(tool_no)) self.assertEqual(cell_widget.upper_text, toolbit.label) # Assuming the 5mm_Endmill asset has a shape named 'Endmill' - self.assertEqual(cell_widget.lower_text, "5 mm 4-flute endmill, 30 mm cutting edge") + self.assertEqual(cell_widget.lower_text, "5.00 mm 4-flute endmill, 30.00 mm cutting edge") # Verify URI is stored in item data stored_uri = item.data(ToolBitUriRole) diff --git a/src/Mod/CAM/CAMTests/TestPathToolBitSerializer.py b/src/Mod/CAM/CAMTests/TestPathToolBitSerializer.py index 7a9ea41995..fbc8cf16fa 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolBitSerializer.py +++ b/src/Mod/CAM/CAMTests/TestPathToolBitSerializer.py @@ -149,7 +149,7 @@ class TestYamlToolBitSerializer(_BaseToolBitSerializerTestCase): self.assertEqual(data.get("shape"), "endmill.fcstd") self.assertEqual(data.get("shape-type"), "Endmill") self.assertEqual(data.get("parameter", {}).get("Diameter"), "4.12 mm") - self.assertEqual(data.get("parameter", {}).get("Length"), "15.0 mm") + self.assertEqual(data.get("parameter", {}).get("Length"), "15.00 mm") def test_extract_dependencies(self): """Test dependency extraction for YAML.""" diff --git a/src/Mod/CAM/CAMTests/TestPathToolLibrarySerializer.py b/src/Mod/CAM/CAMTests/TestPathToolLibrarySerializer.py index ce578ddd27..597175c117 100644 --- a/src/Mod/CAM/CAMTests/TestPathToolLibrarySerializer.py +++ b/src/Mod/CAM/CAMTests/TestPathToolLibrarySerializer.py @@ -144,13 +144,13 @@ class TestLinuxCNCLibrarySerializer(TestPathToolLibrarySerializerBase): # Verify the content format (basic check) lines = serialized_data.decode("ascii", "ignore").strip().split("\n") self.assertEqual(len(lines), 3) - self.assertEqual(lines[0], "T1 P0 D6.000 ;Endmill 6mm") - self.assertEqual(lines[1], "T2 P0 D3.000 ;Endmill 3mm") - self.assertEqual(lines[2], "T3 P0 D5.000 ;Ballend 5mm") + self.assertEqual(lines[0], "T1 P0 X0 Y0 Z0 A0 B0 C0 U0 V0 W0 D6.00 I0 J0 Q0 ;Endmill 6mm") + self.assertEqual(lines[1], "T2 P0 X0 Y0 Z0 A0 B0 C0 U0 V0 W0 D3.00 I0 J0 Q0 ;Endmill 3mm") + self.assertEqual(lines[2], "T3 P0 X0 Y0 Z0 A0 B0 C0 U0 V0 W0 D5.00 I0 J0 Q0 ;Ballend 5mm") def test_linuxcnc_deserialize_not_implemented(self): serializer = LinuxCNCSerializer - dummy_data = b"T1 D6.0 ;Endmill 6mm\n" + dummy_data = b"T1 P0 X0 Y0 Z0 A0 B0 C0 U0 V0 W0 D6.00 I0 J0 Q0 ;Endmill 6mm\n" with self.assertRaises(NotImplementedError): serializer.deserialize(dummy_data, "dummy_id", {}) diff --git a/src/Mod/CAM/CMakeLists.txt b/src/Mod/CAM/CMakeLists.txt index 88a6f3643e..5cc41ed1b3 100644 --- a/src/Mod/CAM/CMakeLists.txt +++ b/src/Mod/CAM/CMakeLists.txt @@ -183,7 +183,7 @@ SET(PathPythonToolsToolBitModels_SRCS Path/Tool/toolbit/models/dovetail.py Path/Tool/toolbit/models/drill.py Path/Tool/toolbit/models/endmill.py - Path/Tool/toolbit/models/fillet.py + Path/Tool/toolbit/models/radius.py Path/Tool/toolbit/models/probe.py Path/Tool/toolbit/models/reamer.py Path/Tool/toolbit/models/slittingsaw.py @@ -264,7 +264,7 @@ SET(PathPythonToolsShapeModels_SRCS Path/Tool/shape/models/dovetail.py Path/Tool/shape/models/drill.py Path/Tool/shape/models/endmill.py - Path/Tool/shape/models/fillet.py + Path/Tool/shape/models/radius.py Path/Tool/shape/models/icon.py Path/Tool/shape/models/probe.py Path/Tool/shape/models/reamer.py @@ -455,8 +455,8 @@ SET(Tools_Shape_SRCS Tools/Shape/drill.svg Tools/Shape/endmill.fcstd Tools/Shape/endmill.svg - Tools/Shape/fillet.fcstd - Tools/Shape/fillet.svg + Tools/Shape/radius.fcstd + Tools/Shape/radius.svg Tools/Shape/probe.fcstd Tools/Shape/probe.svg Tools/Shape/reamer.fcstd diff --git a/src/Mod/CAM/Gui/Resources/panels/ShapeSelector.ui b/src/Mod/CAM/Gui/Resources/panels/ShapeSelector.ui index da9f2dbc17..86103f5d06 100644 --- a/src/Mod/CAM/Gui/Resources/panels/ShapeSelector.ui +++ b/src/Mod/CAM/Gui/Resources/panels/ShapeSelector.ui @@ -17,11 +17,11 @@ - - - 1 + + + true - + 0 @@ -30,22 +30,6 @@ 487 - - Standard tools - - - - - - 0 - 0 - 880 - 487 - - - - My tools - diff --git a/src/Mod/CAM/Path/Base/Gui/PropertyBag.py b/src/Mod/CAM/Path/Base/Gui/PropertyBag.py index f6bd417eae..f25b11e03a 100644 --- a/src/Mod/CAM/Path/Base/Gui/PropertyBag.py +++ b/src/Mod/CAM/Path/Base/Gui/PropertyBag.py @@ -150,7 +150,6 @@ class PropertyCreate(object): self.form.propertyEnum.textChanged.connect(self.updateUI) def updateUI(self): - typeSet = True if self.propertyIsEnumeration(): self.form.labelEnum.setEnabled(True) @@ -239,7 +238,17 @@ class TaskPanel(object): pass def _setupProperty(self, i, name): - typ = PathPropertyBag.getPropertyTypeName(self.obj.getTypeIdOfProperty(name)) + if name not in self.obj.PropertiesList: + Path.Log.warning(f"Property '{name}' not found in object {self.obj.Name}") + return + prop_type_id = self.obj.getTypeIdOfProperty(name) + try: + typ = PathPropertyBag.getPropertyTypeName(prop_type_id) + except IndexError: + Path.Log.error( + f"Unknown property type id '{prop_type_id}' for property '{name}' in object {self.obj.Name}" + ) + return val = PathUtil.getPropertyValueString(self.obj, name) info = self.obj.getDocumentationOfProperty(name) diff --git a/src/Mod/CAM/Path/Base/PropertyBag.py b/src/Mod/CAM/Path/Base/PropertyBag.py index e5f2657112..2b0fd946ab 100644 --- a/src/Mod/CAM/Path/Base/PropertyBag.py +++ b/src/Mod/CAM/Path/Base/PropertyBag.py @@ -68,12 +68,14 @@ class PropertyBag(object): CustomPropertyGroupDefault = "User" def __init__(self, obj): - obj.addProperty( - "App::PropertyStringList", - self.CustomPropertyGroups, - "Base", - QT_TRANSLATE_NOOP("App::Property", "List of custom property groups"), - ) + # Always add as enumeration + if not hasattr(obj, self.CustomPropertyGroups): + obj.addProperty( + "App::PropertyEnumeration", + self.CustomPropertyGroups, + "Base", + QT_TRANSLATE_NOOP("App::Property", "List of custom property groups"), + ) self.onDocumentRestored(obj) def dumps(self): @@ -96,15 +98,39 @@ class PropertyBag(object): def onDocumentRestored(self, obj): self.obj = obj - obj.setEditorMode(self.CustomPropertyGroups, 2) # hide + cpg = getattr(obj, self.CustomPropertyGroups, None) + # If it's a string list, convert to enum + if isinstance(cpg, list): + vals = cpg + try: + obj.removeProperty(self.CustomPropertyGroups) + except Exception: + pass + obj.addProperty( + "App::PropertyEnumeration", + self.CustomPropertyGroups, + "Base", + QT_TRANSLATE_NOOP("App::Property", "List of custom property groups"), + ) + if hasattr(obj, "setEnumerationsOfProperty"): + obj.setEnumerationsOfProperty(self.CustomPropertyGroups, vals) + else: + # Fallback: set the property value directly (may not work in all FreeCAD versions) + setattr(obj, self.CustomPropertyGroups, vals) + if hasattr(obj, "setEditorMode"): + obj.setEditorMode(self.CustomPropertyGroups, 2) # hide + elif hasattr(obj, "getEnumerationsOfProperty"): + if hasattr(obj, "setEditorMode"): + obj.setEditorMode(self.CustomPropertyGroups, 2) # hide def getCustomProperties(self): - """getCustomProperties() ... Return a list of all custom properties created in this container.""" - return [ - p - for p in self.obj.PropertiesList - if self.obj.getGroupOfProperty(p) in self.obj.CustomPropertyGroups - ] + """Return a list of all custom properties created in this container.""" + groups = [] + if hasattr(self.obj, "getEnumerationsOfProperty"): + groups = list(self.obj.getEnumerationsOfProperty(self.CustomPropertyGroups)) + else: + groups = list(getattr(self.obj, self.CustomPropertyGroups, [])) + return [p for p in self.obj.PropertiesList if self.obj.getGroupOfProperty(p) in groups] def addCustomProperty(self, propertyType, name, group=None, desc=None): """addCustomProperty(propertyType, name, group=None, desc=None) ... adds a custom property and tracks its group.""" @@ -112,15 +138,23 @@ class PropertyBag(object): desc = "" if group is None: group = self.CustomPropertyGroupDefault - groups = self.obj.CustomPropertyGroups + + # Always use enum for groups + if hasattr(self.obj, "getEnumerationsOfProperty"): + groups = list(self.obj.getEnumerationsOfProperty(self.CustomPropertyGroups)) + else: + groups = list(getattr(self.obj, self.CustomPropertyGroups, [])) name = self.__sanitizePropertyName(name) if not re.match("^[A-Za-z0-9_]*$", name): raise ValueError("Property Name can only contain letters and numbers") - if not group in groups: + if group not in groups: groups.append(group) - self.obj.CustomPropertyGroups = groups + if hasattr(self.obj, "setEnumerationsOfProperty"): + self.obj.setEnumerationsOfProperty(self.CustomPropertyGroups, groups) + else: + setattr(self.obj, self.CustomPropertyGroups, groups) self.obj.addProperty(propertyType, name, group, desc) return name @@ -129,9 +163,16 @@ class PropertyBag(object): customGroups = [] for p in self.obj.PropertiesList: group = self.obj.getGroupOfProperty(p) - if group in self.obj.CustomPropertyGroups and not group in customGroups: + if hasattr(self.obj, "getEnumerationsOfProperty"): + groups = list(self.obj.getEnumerationsOfProperty(self.CustomPropertyGroups)) + else: + groups = list(getattr(self.obj, self.CustomPropertyGroups, [])) + if group in groups and group not in customGroups: customGroups.append(group) - self.obj.CustomPropertyGroups = customGroups + if hasattr(self.obj, "setEnumerationsOfProperty"): + self.obj.setEnumerationsOfProperty(self.CustomPropertyGroups, customGroups) + else: + setattr(self.obj, self.CustomPropertyGroups, customGroups) def Create(name="PropertyBag"): diff --git a/src/Mod/CAM/Path/Base/Util.py b/src/Mod/CAM/Path/Base/Util.py index 33beb423e1..4905b8abda 100644 --- a/src/Mod/CAM/Path/Base/Util.py +++ b/src/Mod/CAM/Path/Base/Util.py @@ -78,10 +78,10 @@ def setProperty(obj, prop, value): """setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name.""" o, attr, name = _getProperty(obj, prop) if attr is not None and isinstance(value, str): - if isinstance(attr, int): - value = int(value, 0) - elif isinstance(attr, bool): + if isinstance(attr, bool): value = value.lower() in ["true", "1", "yes", "ok"] + elif isinstance(attr, int): + value = int(value, 0) if o and name: setattr(o, name, value) diff --git a/src/Mod/CAM/Path/Tool/library/serializers/linuxcnc.py b/src/Mod/CAM/Path/Tool/library/serializers/linuxcnc.py index 992dfe6760..1b599917fa 100644 --- a/src/Mod/CAM/Path/Tool/library/serializers/linuxcnc.py +++ b/src/Mod/CAM/Path/Tool/library/serializers/linuxcnc.py @@ -50,18 +50,29 @@ class LinuxCNCSerializer(AssetSerializer): output = io.BytesIO() for bit_no, bit in sorted(asset._bit_nos.items()): - assert isinstance(bit, ToolBit) - if not isinstance(bit, RotaryToolBitMixin): - Path.Log.warning( - f"Skipping too {bit.label} (bit.id) because it is not a rotary tool" - ) - continue - diameter = bit.get_diameter() + # Connor: assert isinstance(bit, ToolBit) + # if not isinstance(bit, RotaryToolBitMixin): + # Path.Log.warning( + # f"Skipping too {bit.label} (bit.id) because it is not a rotary tool" + # ) + # continue + # Commenting this out. Why did we skip because it is not a rotary tool? + diameter = bit.get_diameter().getUserPreferred()[0] pocket = "P0" # TODO: is there a better way? - - # Format diameter to one decimal place and remove units - diameter_value = diameter.Value if hasattr(diameter, "Value") else diameter - line = f"T{bit_no} {pocket} D{diameter_value:.3f} ;{bit.label}\n" + # TODO: Strip units by splitting at the first space if diameter is a string + # This is where we need a machine definition so we can export these out correctly + # for a metric or imperial machine + # Using user preferred for now + if hasattr(diameter, "Value"): + diameter_value = diameter.Value + elif isinstance(diameter, str): + diameter_value = diameter.split(" ")[0] + else: + diameter_value = diameter + line = ( + f"T{bit_no} {pocket} X0 Y0 Z0 A0 B0 C0 U0 V0 W0 " + f"D{diameter_value} I0 J0 Q0 ;{bit.label}\n" + ) output.write(line.encode("utf-8")) return output.getvalue() diff --git a/src/Mod/CAM/Path/Tool/library/ui/browser.py b/src/Mod/CAM/Path/Tool/library/ui/browser.py index c5527772a1..ff1f5f15ce 100644 --- a/src/Mod/CAM/Path/Tool/library/ui/browser.py +++ b/src/Mod/CAM/Path/Tool/library/ui/browser.py @@ -65,8 +65,17 @@ class LibraryBrowserWidget(ToolBitBrowserWidget): compact=compact, ) self.current_library: Optional[Library] = None + self._selected_tool_type: Optional[str] = None self.layout().setContentsMargins(0, 0, 0, 0) + # Add tool type filter combo box to the base widget + self._tool_type_combo = QtGui.QComboBox() + self._tool_type_combo.setSizePolicy( + QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred + ) + self._top_layout.insertWidget(0, self._tool_type_combo, 1) + self._tool_type_combo.currentTextChanged.connect(self._on_tool_type_combo_changed) + self.restore_last_sort_order() self.load_last_library() @@ -177,6 +186,35 @@ class LibraryBrowserWidget(ToolBitBrowserWidget): if library: Path.Preferences.setLastToolLibrary(str(library.get_uri())) + def _get_available_tool_types(self): + """Get all available tool types from the current assets.""" + tool_types = set() + # Make sure we have assets to work with + if not hasattr(self, "_all_assets") or not self._all_assets: + return [] + + for asset in self._all_assets: + # Use get_shape_name() method to get the tool type + if hasattr(asset, "get_shape_name"): + tool_type = asset.get_shape_name() + if tool_type: + tool_types.add(tool_type) + + return sorted(tool_types) + + def _get_filtered_assets(self): + """Get assets filtered by tool type if a specific type is selected.""" + if not self._selected_tool_type or self._selected_tool_type == "All Tool Types": + return self._all_assets + + filtered_assets = [] + for asset in self._all_assets: + if hasattr(asset, "get_shape_name"): + tool_type = asset.get_shape_name() + if tool_type == self._selected_tool_type: + filtered_assets.append(asset) + return filtered_assets + def _update_tool_list(self): """Updates the tool list based on the current library.""" if self.current_library: @@ -187,8 +225,34 @@ class LibraryBrowserWidget(ToolBitBrowserWidget): self._all_assets = cast(List[ToolBit], all_toolbits) self._sort_assets() self._tool_list_widget.clear_list() + # Update tool type combo after assets are loaded + if hasattr(self, "_tool_type_combo"): + self._update_tool_type_combo() self._update_list() + def _update_list(self): + """Updates the list widget with filtered assets.""" + self._tool_list_widget.clear_list() + filtered_assets = self._get_filtered_assets() + + # Apply search filter if there is one + search_term = self._search_edit.text().lower() + if search_term: + search_filtered = [] + for asset in filtered_assets: + if search_term in asset.label.lower(): + search_filtered.append(asset) + continue + # Also search in tool type + if hasattr(asset, "get_shape_name"): + tool_type = asset.get_shape_name() + if tool_type and search_term in tool_type.lower(): + search_filtered.append(asset) + filtered_assets = search_filtered + + for asset in filtered_assets: + self._tool_list_widget.add_toolbit(asset) + def _add_shortcuts(self): """Adds keyboard shortcuts for common actions.""" Path.Log.debug("LibraryBrowserWidget._add_shortcuts: Called.") @@ -476,6 +540,32 @@ class LibraryBrowserWidget(ToolBitBrowserWidget): self._asset_manager.add(library) self.refresh() + def _update_tool_type_combo(self): + """Update the tool type combo box with available types.""" + current_selection = self._tool_type_combo.currentText() + self._tool_type_combo.blockSignals(True) + try: + self._tool_type_combo.clear() + self._tool_type_combo.addItem("All Tool Types") + + for tool_type in self._get_available_tool_types(): + self._tool_type_combo.addItem(tool_type) + + # Restore selection if it still exists + index = self._tool_type_combo.findText(current_selection) + if index >= 0: + self._tool_type_combo.setCurrentIndex(index) + else: + self._tool_type_combo.setCurrentIndex(0) + self._selected_tool_type = "All Tool Types" + finally: + self._tool_type_combo.blockSignals(False) + + def _on_tool_type_combo_changed(self, tool_type): + """Handle tool type filter selection change.""" + self._selected_tool_type = tool_type + self._update_list() + class LibraryBrowserWithCombo(LibraryBrowserWidget): """ @@ -502,10 +592,15 @@ class LibraryBrowserWithCombo(LibraryBrowserWidget): self._top_layout.removeWidget(self._search_edit) layout.insertWidget(1, self._search_edit, 20) + # Library selection combo box self._library_combo = QtGui.QComboBox() self._library_combo.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) self._top_layout.insertWidget(0, self._library_combo, 1) self._library_combo.currentIndexChanged.connect(self._on_library_combo_changed) + + self._top_layout.removeWidget(self._tool_type_combo) + self._top_layout.insertWidget(1, self._tool_type_combo, 1) + self.current_library_changed.connect(self._on_current_library_changed) self._in_refresh = False @@ -554,6 +649,11 @@ class LibraryBrowserWithCombo(LibraryBrowserWidget): if not libraries: return if not self.current_library: + first_library = self._library_combo.itemData(0) + if first_library: + uri = first_library.get_uri() + library = self._asset_manager.get(uri, store=self._store_name, depth=1) + self.set_current_library(library) self._library_combo.setCurrentIndex(0) return diff --git a/src/Mod/CAM/Path/Tool/library/ui/dock.py b/src/Mod/CAM/Path/Tool/library/ui/dock.py index 215444ebf7..ff92275ed2 100644 --- a/src/Mod/CAM/Path/Tool/library/ui/dock.py +++ b/src/Mod/CAM/Path/Tool/library/ui/dock.py @@ -36,7 +36,6 @@ from ...toolbit import ToolBit from .editor import LibraryEditor from .browser import LibraryBrowserWithCombo - if False: Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) @@ -66,7 +65,7 @@ class ToolBitLibraryDock(object): self.form_layout.setSpacing(4) # Create the browser widget - self.browser_widget = LibraryBrowserWidget(asset_manager=cam_assets) + self.browser_widget = LibraryBrowserWithCombo(asset_manager=cam_assets) self._setup_ui() @@ -80,8 +79,6 @@ class ToolBitLibraryDock(object): main_layout.setContentsMargins(4, 4, 4, 4) main_layout.setSpacing(4) - # Create the browser widget - self.browser_widget = LibraryBrowserWithCombo(asset_manager=cam_assets) main_layout.addWidget(self.browser_widget) # Create buttons @@ -89,11 +86,19 @@ class ToolBitLibraryDock(object): translate("CAM_ToolBit", "Open Library Editor") ) self.addToolControllerButton = QtGui.QPushButton(translate("CAM_ToolBit", "Add to Job")) + self.closeButton = QtGui.QPushButton(translate("CAM_ToolBit", "Close")) - # Add buttons to a horizontal layout + button_width = 120 + self.libraryEditorOpenButton.setMinimumWidth(button_width) + self.addToolControllerButton.setMinimumWidth(button_width) + self.closeButton.setMinimumWidth(button_width) + + # Add buttons to a horizontal layout, right-align Close button_layout = QtGui.QHBoxLayout() button_layout.addWidget(self.libraryEditorOpenButton) button_layout.addWidget(self.addToolControllerButton) + button_layout.addStretch(1) + button_layout.addWidget(self.closeButton) # Add the button layout to the main layout main_layout.addLayout(button_layout) @@ -106,6 +111,7 @@ class ToolBitLibraryDock(object): self.browser_widget.itemDoubleClicked.connect(self._on_doubleclick) self.libraryEditorOpenButton.clicked.connect(self._open_editor) self.addToolControllerButton.clicked.connect(self._add_tool_controller_to_doc) + self.closeButton.clicked.connect(self.form.reject) # Update the initial state of the UI self._update_state() diff --git a/src/Mod/CAM/Path/Tool/shape/__init__.py b/src/Mod/CAM/Path/Tool/shape/__init__.py index 70aa088931..1a19b62139 100644 --- a/src/Mod/CAM/Path/Tool/shape/__init__.py +++ b/src/Mod/CAM/Path/Tool/shape/__init__.py @@ -10,7 +10,7 @@ from .models.custom import ToolBitShapeCustom from .models.dovetail import ToolBitShapeDovetail from .models.drill import ToolBitShapeDrill from .models.endmill import ToolBitShapeEndmill -from .models.fillet import ToolBitShapeFillet +from .models.radius import ToolBitShapeRadius from .models.probe import ToolBitShapeProbe from .models.reamer import ToolBitShapeReamer from .models.slittingsaw import ToolBitShapeSlittingSaw @@ -36,7 +36,7 @@ __all__ = [ "ToolBitShapeDovetail", "ToolBitShapeDrill", "ToolBitShapeEndmill", - "ToolBitShapeFillet", + "ToolBitShapeRadius", "ToolBitShapeProbe", "ToolBitShapeReamer", "ToolBitShapeSlittingSaw", diff --git a/src/Mod/CAM/Path/Tool/shape/models/custom.py b/src/Mod/CAM/Path/Tool/shape/models/custom.py index bdd1c12e8b..a64069068d 100644 --- a/src/Mod/CAM/Path/Tool/shape/models/custom.py +++ b/src/Mod/CAM/Path/Tool/shape/models/custom.py @@ -34,9 +34,31 @@ class ToolBitShapeCustom(ToolBitShape): name: str = "Custom" aliases = ("custom",) + # Connor: We're going to treat custom tools as normal endmills @classmethod def schema(cls) -> Mapping[str, Tuple[str, str]]: - return {} + return { + "CuttingEdgeHeight": ( + FreeCAD.Qt.translate("ToolBitShape", "Cutting edge height"), + "App::PropertyLength", + ), + "Diameter": ( + FreeCAD.Qt.translate("ToolBitShape", "Diameter"), + "App::PropertyLength", + ), + "Flutes": ( + FreeCAD.Qt.translate("ToolBitShape", "Flutes"), + "App::PropertyInteger", + ), + "Length": ( + FreeCAD.Qt.translate("ToolBitShape", "Overall tool length"), + "App::PropertyLength", + ), + "ShankDiameter": ( + FreeCAD.Qt.translate("ToolBitToolBitShapeShapeEndMill", "Shank diameter"), + "App::PropertyLength", + ), + } @property def label(self) -> str: diff --git a/src/Mod/CAM/Path/Tool/shape/models/fillet.py b/src/Mod/CAM/Path/Tool/shape/models/radius.py similarity index 86% rename from src/Mod/CAM/Path/Tool/shape/models/fillet.py rename to src/Mod/CAM/Path/Tool/shape/models/radius.py index 0156b910ff..be99fa6024 100644 --- a/src/Mod/CAM/Path/Tool/shape/models/fillet.py +++ b/src/Mod/CAM/Path/Tool/shape/models/radius.py @@ -25,23 +25,26 @@ from typing import Tuple, Mapping from .base import ToolBitShape -class ToolBitShapeFillet(ToolBitShape): - name = "Fillet" - aliases = ("fillet",) +class ToolBitShapeRadius(ToolBitShape): + name = "Radius" + aliases = ( + "radius", + "fillet", + ) @classmethod def schema(cls) -> Mapping[str, Tuple[str, str]]: return { - "CrownHeight": ( - FreeCAD.Qt.translate("ToolBitShape", "Crown height"), + "CuttingEdgeHeight": ( + FreeCAD.Qt.translate("ToolBitShape", "Cutting edge height"), "App::PropertyLength", ), "Diameter": ( FreeCAD.Qt.translate("ToolBitShape", "Diameter"), "App::PropertyLength", ), - "FilletRadius": ( - FreeCAD.Qt.translate("ToolBitShape", "Fillet radius"), + "CuttingRadius": ( + FreeCAD.Qt.translate("ToolBitShape", "Cutting radius"), "App::PropertyLength", ), "Flutes": ( @@ -60,4 +63,4 @@ class ToolBitShapeFillet(ToolBitShape): @property def label(self) -> str: - return FreeCAD.Qt.translate("ToolBitShape", "Filleted Chamfer") + return FreeCAD.Qt.translate("ToolBitShape", "Radius Mill") diff --git a/src/Mod/CAM/Path/Tool/shape/ui/shapeselector.py b/src/Mod/CAM/Path/Tool/shape/ui/shapeselector.py index 5e4e7ce8a6..542c847bad 100644 --- a/src/Mod/CAM/Path/Tool/shape/ui/shapeselector.py +++ b/src/Mod/CAM/Path/Tool/shape/ui/shapeselector.py @@ -39,7 +39,6 @@ class ShapeSelector: self.flows = {} self.update_shapes() - self.form.toolBox.setCurrentIndex(0) def _add_shape_group(self, toolbox): if toolbox in self.flows: @@ -70,8 +69,10 @@ class ShapeSelector: custom = cam_assets.fetch(asset_type="toolbitshape", store="local") for shape in custom: builtin.pop(shape.id, None) - self._add_shapes(self.form.standardTools, builtin.values()) - self._add_shapes(self.form.customTools, custom) + + # Combine all shapes into a single list + all_shapes = list(builtin.values()) + list(custom) + self._add_shapes(self.form.toolsContainer, all_shapes) def on_shape_button_clicked(self, shape): self.shape = shape diff --git a/src/Mod/CAM/Path/Tool/toolbit/__init__.py b/src/Mod/CAM/Path/Tool/toolbit/__init__.py index 9ae1e129ad..37fa8aa694 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/__init__.py +++ b/src/Mod/CAM/Path/Tool/toolbit/__init__.py @@ -10,7 +10,7 @@ from .models.custom import ToolBitCustom from .models.dovetail import ToolBitDovetail from .models.drill import ToolBitDrill from .models.endmill import ToolBitEndmill -from .models.fillet import ToolBitFillet +from .models.radius import ToolBitRadius from .models.probe import ToolBitProbe from .models.reamer import ToolBitReamer from .models.slittingsaw import ToolBitSlittingSaw @@ -28,7 +28,7 @@ __all__ = [ "ToolBitDovetail", "ToolBitDrill", "ToolBitEndmill", - "ToolBitFillet", + "ToolBitRadius", "ToolBitProbe", "ToolBitReamer", "ToolBitSlittingSaw", diff --git a/src/Mod/CAM/Path/Tool/toolbit/models/base.py b/src/Mod/CAM/Path/Tool/toolbit/models/base.py index 692ebe71e2..9cfff72974 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/models/base.py +++ b/src/Mod/CAM/Path/Tool/toolbit/models/base.py @@ -513,6 +513,7 @@ class ToolBit(Asset, ABC): self._create_base_properties() # Transfer property values from the detached object to the real object + self._suppress_visual_update = True temp_obj.copy_to(self.obj) # Ensure label is set @@ -520,6 +521,7 @@ class ToolBit(Asset, ABC): # Update the visual representation now that it's attached self._update_tool_properties() + self._suppress_visual_update = False self._update_visual_representation() def onChanged(self, obj, prop): @@ -528,6 +530,9 @@ class ToolBit(Asset, ABC): if "Restore" in obj.State: return + if getattr(self, "_suppress_visual_update", False): + return + if hasattr(self, "_in_update") and self._in_update: Path.Log.debug(f"Skipping onChanged for {obj.Label} due to active update.") return diff --git a/src/Mod/CAM/Path/Tool/toolbit/models/custom.py b/src/Mod/CAM/Path/Tool/toolbit/models/custom.py index b32004a796..b969e515e0 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/models/custom.py +++ b/src/Mod/CAM/Path/Tool/toolbit/models/custom.py @@ -35,3 +35,32 @@ class ToolBitCustom(ToolBit): @property def summary(self) -> str: return FreeCAD.Qt.translate("CAM", "Unknown custom toolbit type") + + # Connor: Adding in getters and setters for diameter and length + def get_diameter(self) -> FreeCAD.Units.Quantity: + """ + Get the diameter of the rotary tool bit from the shape. + """ + return self.obj.Diameter + + def set_diameter(self, diameter: FreeCAD.Units.Quantity): + """ + Set the diameter of the rotary tool bit on the shape. + """ + if not isinstance(diameter, FreeCAD.Units.Quantity): + raise ValueError("Diameter must be a FreeCAD Units.Quantity") + self.obj.Diameter = diameter + + def get_length(self) -> FreeCAD.Units.Quantity: + """ + Get the length of the rotary tool bit from the shape. + """ + return self.obj.Length + + def set_length(self, length: FreeCAD.Units.Quantity): + """ + Set the length of the rotary tool bit on the shape. + """ + if not isinstance(length, FreeCAD.Units.Quantity): + raise ValueError("Length must be a FreeCAD Units.Quantity") + self.obj.Length = length diff --git a/src/Mod/CAM/Path/Tool/toolbit/models/probe.py b/src/Mod/CAM/Path/Tool/toolbit/models/probe.py index 838667ea28..c07c6b879b 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/models/probe.py +++ b/src/Mod/CAM/Path/Tool/toolbit/models/probe.py @@ -46,3 +46,32 @@ class ToolBitProbe(ToolBit): def can_rotate(self) -> bool: return False + + # Connor: Add getters and setters for Diameter and Length + def get_diameter(self) -> FreeCAD.Units.Quantity: + """ + Get the diameter of the rotary tool bit from the shape. + """ + return self.obj.Diameter + + def set_diameter(self, diameter: FreeCAD.Units.Quantity): + """ + Set the diameter of the rotary tool bit on the shape. + """ + if not isinstance(diameter, FreeCAD.Units.Quantity): + raise ValueError("Diameter must be a FreeCAD Units.Quantity") + self.obj.Diameter = diameter + + def get_length(self) -> FreeCAD.Units.Quantity: + """ + Get the length of the rotary tool bit from the shape. + """ + return self.obj.Length + + def set_length(self, length: FreeCAD.Units.Quantity): + """ + Set the length of the rotary tool bit on the shape. + """ + if not isinstance(length, FreeCAD.Units.Quantity): + raise ValueError("Length must be a FreeCAD Units.Quantity") + self.obj.Length = length diff --git a/src/Mod/CAM/Path/Tool/toolbit/models/fillet.py b/src/Mod/CAM/Path/Tool/toolbit/models/radius.py similarity index 82% rename from src/Mod/CAM/Path/Tool/toolbit/models/fillet.py rename to src/Mod/CAM/Path/Tool/toolbit/models/radius.py index 05063a710c..b289e2bad5 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/models/fillet.py +++ b/src/Mod/CAM/Path/Tool/toolbit/models/radius.py @@ -21,25 +21,25 @@ # *************************************************************************** import FreeCAD import Path -from ...shape import ToolBitShapeFillet +from ...shape import ToolBitShapeRadius from ..mixins import RotaryToolBitMixin, CuttingToolMixin from .base import ToolBit -class ToolBitFillet(ToolBit, CuttingToolMixin, RotaryToolBitMixin): - SHAPE_CLASS = ToolBitShapeFillet +class ToolBitRadius(ToolBit, CuttingToolMixin, RotaryToolBitMixin): + SHAPE_CLASS = ToolBitShapeRadius - def __init__(self, shape: ToolBitShapeFillet, id: str | None = None): - Path.Log.track(f"ToolBitFillet __init__ called with shape: {shape}, id: {id}") + def __init__(self, shape: ToolBitShapeRadius, id: str | None = None): + Path.Log.track(f"ToolBitRadius __init__ called with shape: {shape}, id: {id}") super().__init__(shape, id=id) CuttingToolMixin.__init__(self, self.obj) @property def summary(self) -> str: - radius = self.get_property_str("FilletRadius", "?", precision=3) + radius = self.get_property_str("CuttingRadius", "?", precision=3) flutes = self.get_property("Flutes") diameter = self.get_property_str("ShankDiameter", "?", precision=3) return FreeCAD.Qt.translate( - "CAM", f"R{radius} fillet bit, {diameter} shank, {flutes}-flute" + "CAM", f"R{radius} radius mill, {diameter} shank, {flutes}-flute" ) diff --git a/src/Mod/CAM/Path/Tool/toolbit/ui/tablecell.py b/src/Mod/CAM/Path/Tool/toolbit/ui/tablecell.py index 7248bc56c6..e0bed94b43 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/ui/tablecell.py +++ b/src/Mod/CAM/Path/Tool/toolbit/ui/tablecell.py @@ -66,24 +66,20 @@ class TwoLineTableCell(QtGui.QWidget): self.vbox.addWidget(self.label_upper) self.vbox.addWidget(self.label_lower) - style = "color: {}".format(fg_color.name()) self.label_left = QtGui.QLabel() self.label_left.setMinimumWidth(40) self.label_left.setTextFormat(QtCore.Qt.RichText) self.label_left.setAlignment(QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter) - self.label_left.setStyleSheet(style) self.label_left.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) ratio = self.devicePixelRatioF() self.icon_size = QtCore.QSize(50 * ratio, 60 * ratio) self.icon_widget = QtGui.QLabel() - style = "color: {}".format(fg_color.name()) self.label_right = QtGui.QLabel() self.label_right.setMinimumWidth(40) self.label_right.setTextFormat(QtCore.Qt.RichText) self.label_right.setAlignment(QtCore.Qt.AlignCenter) - self.label_right.setStyleSheet(style) self.label_right.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) self.hbox = QtGui.QHBoxLayout() diff --git a/src/Mod/CAM/Path/Tool/toolbit/util.py b/src/Mod/CAM/Path/Tool/toolbit/util.py index 705ef05783..bc21722814 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/util.py +++ b/src/Mod/CAM/Path/Tool/toolbit/util.py @@ -34,9 +34,16 @@ def format_value(value: FreeCAD.Units.Quantity | int | float | None, precision: return None elif isinstance(value, FreeCAD.Units.Quantity): if precision is not None: + user_val, _, user_unit = value.getUserPreferred() + if user_unit in ("deg", "°", "degree", "degrees"): + # Remove the last character (degree symbol) and convert to float + try: + deg_val = float(str(user_val)[:-1]) + except Exception: + return value.getUserPreferred()[0] + formatted_value = f"{deg_val:.1f}".rstrip("0").rstrip(".") + return f"{formatted_value}°" # Format the value with the specified number of precision and strip trailing zeros - formatted_value = f"{value.Value:.{precision}f}".rstrip("0").rstrip(".") - unit = value.getUserPreferred()[2] - return f"{formatted_value} {unit}" + return value.getUserPreferred()[0] return value.UserString return str(value) diff --git a/src/Mod/CAM/Tools/Shape/fillet.fcstd b/src/Mod/CAM/Tools/Shape/fillet.fcstd deleted file mode 100644 index 536a57f848..0000000000 Binary files a/src/Mod/CAM/Tools/Shape/fillet.fcstd and /dev/null differ diff --git a/src/Mod/CAM/Tools/Shape/radius.fcstd b/src/Mod/CAM/Tools/Shape/radius.fcstd new file mode 100644 index 0000000000..b5276922fd Binary files /dev/null and b/src/Mod/CAM/Tools/Shape/radius.fcstd differ diff --git a/src/Mod/CAM/Tools/Shape/fillet.svg b/src/Mod/CAM/Tools/Shape/radius.svg similarity index 82% rename from src/Mod/CAM/Tools/Shape/fillet.svg rename to src/Mod/CAM/Tools/Shape/radius.svg index 03caccb09a..bb3f8e7e11 100644 --- a/src/Mod/CAM/Tools/Shape/fillet.svg +++ b/src/Mod/CAM/Tools/Shape/radius.svg @@ -5,9 +5,9 @@ viewBox="0 0 210 297" height="297mm" width="210mm" - sodipodi:docname="fillet.svg" + sodipodi:docname="radius.svg" xml:space="preserve" - inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)" + inkscape:version="1.3.2 (091e20e, 2023-11-25)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" @@ -26,13 +26,13 @@ inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" showgrid="false" - inkscape:zoom="0.50398562" - inkscape:cx="712.32191" - inkscape:cy="915.70073" - inkscape:window-width="2311" - inkscape:window-height="1509" - inkscape:window-x="1529" - inkscape:window-y="377" + inkscape:zoom="0.41628253" + inkscape:cx="136.92624" + inkscape:cy="599.35256" + inkscape:window-width="1512" + inkscape:window-height="916" + inkscape:window-x="0" + inkscape:window-y="38" inkscape:window-maximized="0" inkscape:current-layer="svg8" />image/svg+xmlhr + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:28.2222px;font-family:'URW Bookman L';-inkscape-font-specification:'URW Bookman L, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal">rd diff --git a/src/Mod/CAM/Tools/Shape/thread-mill.fcstd b/src/Mod/CAM/Tools/Shape/thread-mill.fcstd index e0eb96c9ea..575076c4f2 100644 Binary files a/src/Mod/CAM/Tools/Shape/thread-mill.fcstd and b/src/Mod/CAM/Tools/Shape/thread-mill.fcstd differ diff --git a/src/Mod/CAM/Tools/Shape/v-bit.fcstd b/src/Mod/CAM/Tools/Shape/v-bit.fcstd index 16cd0e630a..be25111e2e 100644 Binary files a/src/Mod/CAM/Tools/Shape/v-bit.fcstd and b/src/Mod/CAM/Tools/Shape/v-bit.fcstd differ