CAM: Add migration for Toolbit Units property

This PR adds migration for toolbit units by automatically infering the
Units (Metric/Imperial) from toolbit parameter strings and sets the
Units property if missing. It adds a utility function to detect units
from JSON. This is done at the JSON level during migration to ensure
that all toolbits have the correct Units property set.

src/Mod/CAM/Path/Tool/toolbit/migration.py:
- Infer Units from parameter strings if not set during migration
- Set Units property and log inference
- Refactor migration logic for clarity and reliability

src/Mod/CAM/Path/Tool/toolbit/models/base.py:
- Use Path.Log.debug instead of print when adding Units property

src/Mod/CAM/Path/Tool/toolbit/util.py:
- Add units_from_json() to infer Metric/Imperial from parameter strings
This commit is contained in:
Billy Huddleston
2025-12-22 20:48:03 -05:00
parent d2daf0fc20
commit 88a28f64c5
3 changed files with 71 additions and 12 deletions

View File

@@ -25,6 +25,7 @@
import FreeCAD
import Path
from typing import Dict, Any, Optional, Union
from .util import units_from_json
class ParameterAccessor:
@@ -74,9 +75,9 @@ class ParameterAccessor:
else:
self.target.setEditorMode(key, mode)
def label(self):
def name(self):
if self.is_dict:
return "toolbit"
return self.target.get("name", "toolbit")
else:
return getattr(self.target, "Label", "unknown toolbit")
@@ -103,6 +104,7 @@ def migrate_parameters(accessor: ParameterAccessor) -> bool:
Currently handles:
- TorusRadius → CornerRadius
- FlatRadius/Diameter → CornerRadius
- Infers Units from parameter strings if not set
Args:
accessor: ParameterAccessor instance wrapping dict or FreeCAD object
@@ -110,13 +112,28 @@ def migrate_parameters(accessor: ParameterAccessor) -> bool:
Returns:
True if migration occurred, False otherwise
"""
migrated = False
has_torus = accessor.has("TorusRadius")
has_flat = accessor.has("FlatRadius")
has_diam = accessor.has("Diameter")
has_corner = accessor.has("CornerRadius")
label = accessor.label()
has_units = accessor.has("Units")
name = accessor.name()
shape_type = accessor.get_shape_type()
# Infer Units from parameter strings if not set
if not has_units:
# Gather all parameters to check for units
params = {}
if accessor.is_dict:
params = accessor.target.get("parameter", {})
inferred_units = units_from_json(params)
if inferred_units:
accessor.set("Units", inferred_units)
Path.Log.info(f"Adding Units as '{inferred_units}' for {name}")
migrated = True
# Only run migration logic if shape type == 'Bullnose'
if shape_type and str(shape_type).lower() == "bullnose":
# Case 1: TorusRadius exists, copy to CornerRadius
@@ -130,11 +147,11 @@ def migrate_parameters(accessor: ParameterAccessor) -> bool:
)
accessor.set_editor_mode("CornerRadius", 0)
accessor.set("CornerRadius", value)
Path.Log.info(f"Copied TorusRadius to CornerRadius={value} for {label}")
return True
Path.Log.info(f"Copied TorusRadius to CornerRadius={value} for {name}")
migrated = True
# Case 2: FlatRadius and Diameter exist, calculate CornerRadius
if has_flat and has_diam and not has_corner:
if has_flat and has_diam and not has_corner and not has_torus:
try:
diam_raw = accessor.get("Diameter")
flat_raw = accessor.get("FlatRadius")
@@ -161,10 +178,9 @@ def migrate_parameters(accessor: ParameterAccessor) -> bool:
)
accessor.set_editor_mode("CornerRadius", 0)
accessor.set("CornerRadius", value)
Path.Log.info(f"Migrated FlatRadius/Diameter to CornerRadius={value} for {label}")
return True
Path.Log.info(f"Migrated FlatRadius/Diameter to CornerRadius={value} for {name}")
migrated = True
except Exception as e:
Path.Log.error(f"Failed to migrate FlatRadius for toolbit {label}: {e}")
return False
Path.Log.error(f"Failed to migrate FlatRadius for toolbit {name}: {e}")
return False
return migrated

View File

@@ -793,7 +793,7 @@ class ToolBit(Asset, ABC):
# 3. Ensure Units property exists and is set
if not hasattr(self.obj, "Units"):
print("Adding Units property")
Path.Log.debug("Adding Units property")
self.obj.addProperty(
"App::PropertyEnumeration",
"Units",

View File

@@ -30,6 +30,49 @@ def to_json(value):
return value
def units_from_json(params):
"""
Infer Units (Metric/Imperial) from JSON parameter strings.
For JSON files from disk, values are stored as strings like "3.175 in" or "6 mm".
This function examines common dimensional parameters (Diameter, Length, CuttingEdgeHeight, etc.)
to determine if the toolbit uses metric or imperial units.
Args:
params: Dictionary of parameters from JSON (before conversion to FreeCAD.Units.Quantity)
Returns:
str: "Metric" or "Imperial", or None if units cannot be determined
"""
if not isinstance(params, dict):
return None
imperial_count = 0
metric_count = 0
for param_name in ("Diameter", "ShankDiameter", "Length", "CuttingEdgeLength"):
value = params.get(param_name)
if value is not None:
# Check if it's a string with unit suffix
if isinstance(value, str):
value_lower = value.lower().strip()
# Check for imperial units
if any(unit in value_lower for unit in ["in", "inch", '"', "thou"]):
imperial_count += 1
# Check for metric units
elif any(unit in value_lower for unit in ["mm", "cm", "m "]):
metric_count += 1
# Make a decision based on counts
if imperial_count > metric_count:
return "Imperial"
elif metric_count > imperial_count:
return "Metric"
return "Metric" # Default to Metric if uncertain
def format_value(
value: FreeCAD.Units.Quantity | int | float | None,
precision: int | None = None,