diff --git a/src/Mod/CAM/Path/Tool/library/ui/browser.py b/src/Mod/CAM/Path/Tool/library/ui/browser.py index c9c1df2394..1be151b7ad 100644 --- a/src/Mod/CAM/Path/Tool/library/ui/browser.py +++ b/src/Mod/CAM/Path/Tool/library/ui/browser.py @@ -347,6 +347,7 @@ class LibraryBrowserWidget(ToolBitBrowserWidget): # If the editor was closed with "OK", save the changes self._asset_manager.add(toolbit) Path.Log.info(f"Toolbit {toolbit.get_id()} saved.") + editor._restore_original_schema() # Also save the library because the tool number may have changed. if self.current_library and tool_no != editor.tool_no: diff --git a/src/Mod/CAM/Path/Tool/library/ui/editor.py b/src/Mod/CAM/Path/Tool/library/ui/editor.py index 205a7a4be9..dc62205b22 100644 --- a/src/Mod/CAM/Path/Tool/library/ui/editor.py +++ b/src/Mod/CAM/Path/Tool/library/ui/editor.py @@ -46,6 +46,7 @@ from ...toolbit.serializers import all_serializers as toolbit_serializers from ...toolbit.ui import ToolBitEditor from ...toolbit.ui.toollist import ToolBitUriListMimeType from ...toolbit.ui.util import natural_sort_key +from ...toolbit.util import setToolBitSchema from ..serializers import all_serializers as library_serializers from ..models import Library from .browser import LibraryBrowserWidget @@ -531,6 +532,7 @@ class LibraryEditor(QWidget): ) raise + setToolBitSchema() # Ensure correct schema is set for the new toolbit self.browser.refresh() self.browser.select_by_uri([str(new_toolbit.get_uri())]) self._update_button_states() diff --git a/src/Mod/CAM/Path/Tool/toolbit/models/base.py b/src/Mod/CAM/Path/Tool/toolbit/models/base.py index 5ff7a713a7..422672541f 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/models/base.py +++ b/src/Mod/CAM/Path/Tool/toolbit/models/base.py @@ -307,6 +307,16 @@ class ToolBit(Asset, ABC): self.obj.setEditorMode("Shape", 2) # Create the ToolBit properties that are shared by all tool bits + + if not hasattr(self.obj, "Units"): + self.obj.addProperty( + "App::PropertyEnumeration", + "Units", + "Attributes", + QT_TRANSLATE_NOOP("App::Property", "Measurement units for the tool bit"), + ) + self.obj.Units = ["Metric", "Imperial"] + self.obj.Units = "Metric" # Default value if not hasattr(self.obj, "SpindleDirection"): self.obj.addProperty( "App::PropertyEnumeration", @@ -670,7 +680,7 @@ class ToolBit(Asset, ABC): self, name: str, default: str | None = None, precision: int | None = None ) -> str | None: value = self.get_property(name) - return format_value(value, precision=precision) if value else default + return format_value(value, precision=precision, units=self.obj.Units) if value else default def set_property(self, name: str, value: Any): return self.obj.setPropertyByName(name, value) @@ -781,7 +791,23 @@ class ToolBit(Asset, ABC): PathUtil.setProperty(self.obj, name, value) self.obj.setEditorMode(name, 0) - # 3. Ensure SpindleDirection property exists and is set + # 3. Ensure Units property exists and is set + if not hasattr(self.obj, "Units"): + print("Adding Units property") + self.obj.addProperty( + "App::PropertyEnumeration", + "Units", + "Attributes", + QT_TRANSLATE_NOOP("App::Property", "Measurement units for the tool bit"), + ) + self.obj.Units = ["Metric", "Imperial"] + self.obj.Units = "Metric" # Default value + + units_value = self._tool_bit_shape.get_parameters().get("Units") + if units_value in ("Metric", "Imperial") and self.obj.Units != units_value: + PathUtil.setProperty(self.obj, "Units", units_value) + + # 4. Ensure SpindleDirection property exists and is set # Maybe this could be done with a global schema or added to each # shape schema? if not hasattr(self.obj, "SpindleDirection"): @@ -802,7 +828,7 @@ class ToolBit(Asset, ABC): # self.obj.SpindleDirection = spindle_value PathUtil.setProperty(self.obj, "SpindleDirection", spindle_value) - # 4. Ensure Material property exists and is set + # 5. Ensure Material property exists and is set if not hasattr(self.obj, "Material"): self.obj.addProperty( "App::PropertyEnumeration", @@ -959,6 +985,14 @@ class ToolBit(Asset, ABC): return state def get_spindle_direction(self) -> toolchange.SpindleDirection: + """ + Returns the spindle direction for this toolbit. + The direction is determined by the ToolBit's properties and safety rules: + - Returns SpindleDirection.OFF if the tool cannot rotate (e.g., a probe). + - Returns SpindleDirection.CW for clockwise or 'forward' spindle direction. + - Returns SpindleDirection.CCW for counterclockwise or any other value. + - Defaults to SpindleDirection.OFF if not specified. + """ # To be safe, never allow non-rotatable shapes (such as probes) to rotate. if not self.can_rotate(): return toolchange.SpindleDirection.OFF diff --git a/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py b/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py index fff49e6eb7..11ed760bf1 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py +++ b/src/Mod/CAM/Path/Tool/toolbit/ui/editor.py @@ -30,6 +30,7 @@ import FreeCADGui from ...shape.ui.shapewidget import ShapeWidget from ...docobject.ui import DocumentObjectEditorWidget from ..models.base import ToolBit +from ..util import setToolBitSchema translate = FreeCAD.Qt.translate @@ -54,16 +55,7 @@ class ToolBitPropertiesWidget(QtGui.QWidget): self._toolbit = None self._show_shape = icon self._tool_no = tool_no - - # Set schema to user preference if no document is open - # TODO: Add a preference for toolbit unit schema. - # We probably want to look at making it possible to have a toolbit be metric - # or imperial regardless of document settings / or user preferences, but for now this is sufficient. - if FreeCAD.ActiveDocument is None: - pref_schema = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Units").GetInt( - "UserSchema", 0 - ) - FreeCAD.Units.setSchema(pref_schema) + setToolBitSchema() # UI Elements self._label_edit = QtGui.QLineEdit() @@ -82,7 +74,7 @@ class ToolBitPropertiesWidget(QtGui.QWidget): toolbit_group_box = QtGui.QGroupBox(translate("CAM", "Toolbit")) form_layout = QtGui.QFormLayout(toolbit_group_box) form_layout.addRow(translate("CAM", "Label:"), self._label_edit) - form_layout.addRow(translate("CAM", "ID:"), self._id_label) + # form_layout.addRow(translate("CAM", "ID:"), self._id_label) # Optional tool number edit field. self._tool_no_edit = QtGui.QSpinBox() @@ -143,6 +135,18 @@ class ToolBitPropertiesWidget(QtGui.QWidget): def load_toolbit(self, toolbit: ToolBit): """Load a ToolBit object into the editor.""" + # Set schema based on the toolbit's Units property if available + toolbit_units = None + if toolbit and hasattr(toolbit.obj, "Units"): + toolbit_units = getattr(toolbit.obj, "Units", None) + # If Units is an enumeration, get the value + if isinstance(toolbit_units, (list, tuple)) and len(toolbit_units) > 0: + toolbit_units = toolbit_units[0] + if toolbit_units in ("Metric", "Imperial"): + setToolBitSchema(toolbit_units) + elif FreeCAD.ActiveDocument is None: + setToolBitSchema() + self._toolbit = toolbit if not self._toolbit or not self._toolbit.obj: # Clear or disable fields if toolbit is invalid @@ -266,16 +270,24 @@ class ToolBitEditor(QtGui.QWidget): self.tool_no = tool_no self.default_title = self.form.windowTitle() + # Store the original schema to restore on close + self._original_schema = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Units").GetInt( + "UserSchema", 6 + ) + self._tab_closed = False + # Get first tab from the form, add the shape widget at the top. tool_tab_layout = self.form.toolTabLayout widget = ShapeWidget(toolbit._tool_bit_shape) tool_tab_layout.addWidget(widget) # Add tool properties editor to the same tab. - props = ToolBitPropertiesWidget(toolbit, tool_no, self, icon=icon) - props.toolBitChanged.connect(self._update) - props.toolNoChanged.connect(self._on_tool_no_changed) - tool_tab_layout.addWidget(props) + self._props = ToolBitPropertiesWidget(toolbit, tool_no, self, icon=icon) + self._last_units_value = self._get_units_value(self._props) + self._props.toolBitChanged.connect(self._on_toolbit_changed) + self._props.toolBitChanged.connect(self._update) + self._props.toolNoChanged.connect(self._on_tool_no_changed) + tool_tab_layout.addWidget(self._props) self.form.tabWidget.setCurrentIndex(0) self.form.tabWidget.currentChanged.connect(self._on_tab_switched) @@ -307,6 +319,58 @@ class ToolBitEditor(QtGui.QWidget): self.form.plainTextEditNotes.textChanged.connect(self._on_notes_changed) """ + self._update() + + def _get_units_value(self, props): + """ + Helper to extract the Units value from the toolbit properties. + """ + if props and hasattr(props._toolbit.obj, "Units"): + units_value = getattr(props._toolbit.obj, "Units", None) + if isinstance(units_value, (list, tuple)) and len(units_value) > 0: + units_value = units_value[0] + return units_value + return None + + def _on_toolbit_changed(self): + """ + Slot called when the toolbit is changed. If the Units value has changed, + refreshes the property editor widget to update the schema and UI. + """ + units_value = self._get_units_value(self._props) + if units_value in ("Metric", "Imperial") and units_value != self._last_units_value: + self._refresh_property_editor() + self._last_units_value = units_value + + def _refresh_property_editor(self): + """ + Refreshes the property editor widget in the tab. + Removes the current ToolBitPropertiesWidget, restores the original units schema, + recreates the widget, and reconnects all signals. This ensures the UI and schema + are in sync with the current toolbit's units, and user changes are preserved + because the ToolBit object is always up to date. + """ + # Remove the current property editor widget + tool_tab_layout = self.form.toolTabLayout + tool_tab_layout.removeWidget(self._props) + self._props.deleteLater() + # Restore the original schema + FreeCAD.Units.setSchema(self._original_schema) + # Recreate the property editor with the current toolbit + self._props = ToolBitPropertiesWidget(self.toolbit, self.tool_no, self, icon=False) + self._last_units_value = self._get_units_value(self._props) + self._props.toolBitChanged.connect(self._on_toolbit_changed) + self._props.toolBitChanged.connect(self._update) + self._props.toolNoChanged.connect(self._on_tool_no_changed) + tool_tab_layout.addWidget(self._props) + self.form.tabWidget.setCurrentIndex(0) + + def _restore_original_schema(self): + """ + Restores the original units schema that was active before the ToolBit editor was opened. + """ + FreeCAD.Units.setSchema(self._original_schema) + def _update(self): title = self.default_title tool_name = self.toolbit.label diff --git a/src/Mod/CAM/Path/Tool/toolbit/util.py b/src/Mod/CAM/Path/Tool/toolbit/util.py index 280d36bd75..3274275857 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/util.py +++ b/src/Mod/CAM/Path/Tool/toolbit/util.py @@ -30,7 +30,24 @@ def to_json(value): return value -def format_value(value: FreeCAD.Units.Quantity | int | float | None, precision: int | None = None): +def format_value( + value: FreeCAD.Units.Quantity | int | float | None, + precision: int | None = None, + units: str | None = None, +) -> str | None: + """ + Format a numeric value as a string, optionally appending a unit and controlling precision. + + This function uses the ToolBitSchema (via setToolBitSchema) to ensure that units are formatted according to the correct schema (Metric or Imperial) when a FreeCAD.Units.Quantity is provided. The schema is temporarily set for formatting and then restored. + + Args: + value: The numeric value to format. + unit: (Optional) The unit string to append (e.g., 'mm', 'in'). + precision: (Optional) Number of decimal places (default: 3). + + Returns: + str: The formatted value as a string, with unit if provided. + """ if value is None: return None elif isinstance(value, FreeCAD.Units.Quantity): @@ -45,7 +62,10 @@ def format_value(value: FreeCAD.Units.Quantity | int | float | None, precision: 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 - return value.getUserPreferred()[0] + setToolBitSchema(units) + _value = value.getUserPreferred()[0] + setToolBitSchema() + return _value return value.UserString return str(value) @@ -78,3 +98,30 @@ def is_imperial_pitch(pitch_mm, tol=1e-6): if two_dec_clean and not is_whole_tpi: return False # metric return True # imperial + + +def setToolBitSchema(schema=None): + """ + Set the FreeCAD units schema. If passed 'Metric' or 'Imperial', set accordingly (case-insensitive). + Otherwise, if a document is open, set to its schema. If no document, fallback to user preference or provided schema. + """ + units_schema_map = { + "metric": 6, # 6 = Metric schema in FreeCAD + "imperial": 3, # 3 = Imperial schema in FreeCAD + } + if isinstance(schema, str) and schema.lower() in units_schema_map: + FreeCAD.Units.setSchema(units_schema_map[schema.lower()]) + return + if FreeCAD.ActiveDocument is not None: + try: + doc_schema = FreeCAD.ActiveDocument.getSchema() + FreeCAD.Units.setSchema(doc_schema) + return + except Exception: + pass + # Fallback to user preference or provided schema + if schema is None: + schema = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Units").GetInt( + "UserSchema", 6 + ) + FreeCAD.Units.setSchema(schema)