Merge pull request #25783 from Connor9220/AddUnitsPerToolbit
CAM: Add Units (Metric/Imperial) property to ToolBit
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user