From 94153a6a7f1fb649d54f086e4b9b8b828b25c0d7 Mon Sep 17 00:00:00 2001 From: Billy Huddleston Date: Mon, 22 Dec 2025 20:48:03 -0500 Subject: [PATCH] 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 --- src/Mod/CAM/Path/Tool/toolbit/migration.py | 38 ++++++++++++----- src/Mod/CAM/Path/Tool/toolbit/models/base.py | 2 +- src/Mod/CAM/Path/Tool/toolbit/util.py | 43 ++++++++++++++++++++ 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/src/Mod/CAM/Path/Tool/toolbit/migration.py b/src/Mod/CAM/Path/Tool/toolbit/migration.py index d8597bc113..96f3f1fcc6 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/migration.py +++ b/src/Mod/CAM/Path/Tool/toolbit/migration.py @@ -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 diff --git a/src/Mod/CAM/Path/Tool/toolbit/models/base.py b/src/Mod/CAM/Path/Tool/toolbit/models/base.py index 422672541f..42a940aa1c 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/models/base.py +++ b/src/Mod/CAM/Path/Tool/toolbit/models/base.py @@ -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", diff --git a/src/Mod/CAM/Path/Tool/toolbit/util.py b/src/Mod/CAM/Path/Tool/toolbit/util.py index 3274275857..9bb51f16ac 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/util.py +++ b/src/Mod/CAM/Path/Tool/toolbit/util.py @@ -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,