Merge pull request #25783 from Connor9220/AddUnitsPerToolbit

CAM: Add Units (Metric/Imperial) property to ToolBit
This commit is contained in:
sliptonic
2025-12-12 10:46:42 -06:00
committed by GitHub
5 changed files with 168 additions and 20 deletions

View File

@@ -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:

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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)