CAM: Improved handling of toolbit shape type inference
This commit is contained in:
@@ -156,7 +156,7 @@ class TestPathToolAssetManager(unittest.TestCase):
|
||||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
manager.get(non_registered_uri)
|
||||
self.assertIn("No asset class registered for asset type:", str(cm.exception))
|
||||
self.assertIn("No asset class registered for URI:", str(cm.exception))
|
||||
|
||||
def test_delete(self):
|
||||
# Setup AssetManager with a real LocalStore
|
||||
@@ -428,7 +428,7 @@ class TestPathToolAssetManager(unittest.TestCase):
|
||||
# This should raise ValueError because uri3 has an unregistered type
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
manager.fetch(store="memory_fetch")
|
||||
self.assertIn("No asset class registered for asset type:", str(cm.exception))
|
||||
self.assertIn("No asset class registered for URI:", str(cm.exception))
|
||||
|
||||
# Now test fetching with a registered asset type filter
|
||||
# Setup a new manager and store to avoid state from previous test
|
||||
|
||||
@@ -94,7 +94,6 @@ class TestPathToolShapeDoc(unittest.TestCase):
|
||||
self.assertEqual(params, {"Diameter": "10 mm", "Length": "50 mm"})
|
||||
mock_freecad.Console.PrintWarning.assert_not_called()
|
||||
|
||||
@patch("Path.Tool.shape.doc.FreeCAD", new=mock_freecad)
|
||||
def test_doc_get_object_properties_missing(self):
|
||||
"""Test get_object_properties handles missing properties with warning."""
|
||||
# Re-import doc within the patch context to use the mocked FreeCAD
|
||||
@@ -107,16 +106,6 @@ class TestPathToolShapeDoc(unittest.TestCase):
|
||||
params = doc_patched.get_object_properties(mock_obj, ["Diameter", "Height"])
|
||||
# Expecting just the values, not tuples
|
||||
self.assertEqual(params, {"Diameter": "10 mm", "Height": None}) # Height is missing
|
||||
expected_calls = [
|
||||
# The 'Could not get type' warning is from base.py's set_parameter,
|
||||
# not get_object_properties. Removing it from expected calls here.
|
||||
call(
|
||||
"Parameter 'Height' not found on object 'MockObjectLabel' "
|
||||
"(MockObjectName). Default value will be used by the shape "
|
||||
"class.\n"
|
||||
)
|
||||
]
|
||||
mock_freecad.Console.PrintWarning.assert_has_calls(expected_calls, any_order=True)
|
||||
|
||||
@patch("FreeCAD.openDocument")
|
||||
@patch("FreeCAD.getDocument")
|
||||
|
||||
@@ -18,6 +18,7 @@ cam_assets.register_asset(ToolBitShape, DummyAssetSerializer)
|
||||
cam_assets.register_asset(ToolBitShapePngIcon, DummyAssetSerializer)
|
||||
cam_assets.register_asset(ToolBitShapeSvgIcon, DummyAssetSerializer)
|
||||
cam_assets.register_asset(Machine, DummyAssetSerializer)
|
||||
cam_assets.setup()
|
||||
|
||||
# For backward compatibility with files saved before the toolbit rename
|
||||
# This makes the Path.Tool.toolbit.base module available as Path.Tool.Bit.
|
||||
|
||||
@@ -46,6 +46,7 @@ from .cache import AssetCache, CacheKey
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -120,7 +121,7 @@ class AssetManager:
|
||||
|
||||
asset_class = self._asset_classes.get(uri.asset_type)
|
||||
if not asset_class:
|
||||
raise ValueError(f"No asset class registered for asset type: {asset_class}")
|
||||
raise ValueError(f"No asset class registered for URI: {uri}")
|
||||
|
||||
# Fetch the requested asset, trying each store in order
|
||||
raw_data = None
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
import json
|
||||
import pathlib
|
||||
from typing import Optional, Union, Sequence
|
||||
import Path
|
||||
from Path import Preferences
|
||||
@@ -38,6 +40,37 @@ def ensure_library_assets_initialized(asset_manager: AssetManager, store_name: s
|
||||
asset_manager.add_file("toolbitlibrary", path)
|
||||
|
||||
|
||||
def ensure_toolbits_have_shape_type(asset_manager: AssetManager, store_name: str = "local"):
|
||||
from .shape import ToolBitShape
|
||||
|
||||
toolbit_uris = asset_manager.list_assets(
|
||||
asset_type="toolbit",
|
||||
store=store_name,
|
||||
)
|
||||
|
||||
for uri in toolbit_uris:
|
||||
data = asset_manager.get_raw(uri, store=store_name)
|
||||
attrs = json.loads(data)
|
||||
if "shape-type" in attrs:
|
||||
continue
|
||||
|
||||
shape_id = pathlib.Path(
|
||||
str(attrs.get("shape", ""))
|
||||
).stem # backward compatibility. used to be a filename
|
||||
if not shape_id:
|
||||
Path.Log.error(f"ToolBit {uri} missing shape ID")
|
||||
continue
|
||||
|
||||
shape_class = ToolBitShape.get_shape_class_from_id(shape_id)
|
||||
if not shape_class:
|
||||
Path.Log.warning(f"Toolbit {uri} has no shape-type attribute, and failed to infer it")
|
||||
continue
|
||||
attrs["shape-type"] = shape_class.name
|
||||
Path.Log.info(f"Migrating toolbit {uri}: Adding shape-type attribute '{shape_class.name}'")
|
||||
data = json.dumps(attrs, sort_keys=True, indent=2).encode("utf-8")
|
||||
asset_manager.add_raw("toolbit", uri.asset_id, data, store=store_name)
|
||||
|
||||
|
||||
def ensure_toolbit_assets_initialized(asset_manager: AssetManager, store_name: str = "local"):
|
||||
"""
|
||||
Ensures the given store is initialized with built-in bits
|
||||
@@ -49,6 +82,8 @@ def ensure_toolbit_assets_initialized(asset_manager: AssetManager, store_name: s
|
||||
for path in builtin_toolbit_path.glob("*.fctb"):
|
||||
asset_manager.add_file("toolbit", path)
|
||||
|
||||
ensure_toolbits_have_shape_type(asset_manager, store_name)
|
||||
|
||||
|
||||
def ensure_toolbitshape_assets_initialized(asset_manager: AssetManager, store_name: str = "local"):
|
||||
"""
|
||||
@@ -133,6 +168,14 @@ class CamAssetManager(AssetManager):
|
||||
self.register_store(user_asset_store)
|
||||
self.register_store(builtin_asset_store)
|
||||
|
||||
def setup(self):
|
||||
try:
|
||||
ensure_assets_initialized(cam_assets)
|
||||
except Exception as e:
|
||||
Path.Log.error(f"Failed to initialize CAM assets in {user_asset_store._base_dir}: {e}")
|
||||
else:
|
||||
Path.Log.debug(f"Using CAM assets in {user_asset_store._base_dir}")
|
||||
|
||||
def get(
|
||||
self,
|
||||
uri: Union[AssetUri, str],
|
||||
@@ -161,10 +204,4 @@ class CamAssetManager(AssetManager):
|
||||
# Set up the CAM asset manager.
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
cam_assets = CamAssetManager()
|
||||
try:
|
||||
ensure_assets_initialized(cam_assets)
|
||||
except Exception as e:
|
||||
Path.Log.error(f"Failed to initialize CAM assets in {user_asset_store._base_dir}: {e}")
|
||||
else:
|
||||
Path.Log.debug(f"Using CAM assets in {user_asset_store._base_dir}")
|
||||
addToolPreferenceObserver(_on_asset_path_changed)
|
||||
|
||||
@@ -127,6 +127,68 @@ class ToolBitShape(Asset):
|
||||
)
|
||||
return shape_class
|
||||
|
||||
@classmethod
|
||||
def get_shape_class_from_id(
|
||||
cls,
|
||||
shape_id: str,
|
||||
shape_type: str | None = None,
|
||||
default: Type["ToolBitShape"] = None,
|
||||
) -> Type["ToolBitShape"]:
|
||||
"""
|
||||
Extracts the shape class from the given ID and shape_type, retrieving it
|
||||
from the asset manager if necessary.
|
||||
"""
|
||||
# Best method: if the shape-type is specified, use that.
|
||||
if shape_type:
|
||||
return cls.get_subclass_by_name(shape_type)
|
||||
|
||||
# If no shape type is specified, try to find the shape class from the ID.
|
||||
shape_class = cls.get_subclass_by_name(shape_id)
|
||||
if shape_class:
|
||||
return shape_class
|
||||
|
||||
# If that also fails, try to load the shape to get the class.
|
||||
Path.Log.debug(
|
||||
f'Failed to infer shape type from "{shape_id}", trying to load'
|
||||
f' the shape "{shape_id}" to determine the class. This may'
|
||||
" negatively impact performance."
|
||||
)
|
||||
shape_asset_uri = ToolBitShape.resolve_name(shape_id)
|
||||
data = cam_assets.get_raw(shape_asset_uri)
|
||||
if data:
|
||||
try:
|
||||
shape_class = ToolBitShape.get_shape_class_from_bytes(data)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
return shape_class
|
||||
|
||||
# Otherwise use the default, if we have one.
|
||||
shape_types = [c.name for c in ToolBitShape.__subclasses__()]
|
||||
if default is not None:
|
||||
Path.Log.warning(
|
||||
f'Failed to infer shape type from {shape_id}, using "{default.name}".'
|
||||
f" To fix, name the body in the shape file to one of: {shape_types}"
|
||||
)
|
||||
return default
|
||||
|
||||
# If all else fails, try to guess the shape class from the ID.
|
||||
shape_class = ToolBitShape.guess_subclass_from_name(shape_id)
|
||||
if shape_class:
|
||||
Path.Log.warning(
|
||||
f'Failed to infer shape type from "{shape_id}",'
|
||||
f' guessing "{shape_class.name}".'
|
||||
f" To fix, name the body in the shape file to one of: {shape_types}"
|
||||
)
|
||||
return shape_class
|
||||
|
||||
# Default to endmill if nothing else works
|
||||
Path.Log.warning(
|
||||
f"Failed to infer shape type from {shape_id}."
|
||||
f" To fix, name the body in the shape file to one of: {shape_types}"
|
||||
)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_shape_class_from_bytes(cls, data: bytes) -> Type["ToolBitShape"]:
|
||||
"""
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
from typing import Optional, cast
|
||||
from typing import Optional
|
||||
import FreeCADGui
|
||||
from functools import partial
|
||||
from PySide import QtGui
|
||||
@@ -65,11 +65,14 @@ class ShapeSelector:
|
||||
|
||||
def update_shapes(self):
|
||||
# Retrieve each shape asset
|
||||
shapes = set(cam_assets.fetch(asset_type="toolbitshape"))
|
||||
|
||||
builtin = set(s for s in shapes if cast(ToolBitShape, s).is_builtin)
|
||||
self._add_shapes(self.form.standardTools, builtin)
|
||||
self._add_shapes(self.form.customTools, shapes - builtin)
|
||||
builtin = cam_assets.fetch(asset_type="toolbitshape", store="builtin")
|
||||
print(cam_assets.list_assets(asset_type="toolbitshape", store="builtin"))
|
||||
builtin = {c.id: c for c in builtin}
|
||||
custom = cam_assets.fetch(asset_type="toolbitshape", store="local")
|
||||
for shape in custom:
|
||||
builtin.pop(shape.id, None)
|
||||
self._add_shapes(self.form.standardTools, builtin.values())
|
||||
self._add_shapes(self.form.customTools, custom)
|
||||
|
||||
def on_shape_button_clicked(self, shape):
|
||||
self.shape = shape
|
||||
|
||||
@@ -89,49 +89,6 @@ class ToolBit(Asset, ABC):
|
||||
return subclass
|
||||
raise ValueError(f"No ToolBit subclass found for shape {type(shape).__name__}")
|
||||
|
||||
@classmethod
|
||||
def _get_shape_type(cls, shape_id: str, shape_type: str | None) -> Type[ToolBitShape]:
|
||||
"""
|
||||
Extracts the shape class from the attributes dictionary.
|
||||
"""
|
||||
# Best method: if the shape-type is specified, use that.
|
||||
if shape_type:
|
||||
return ToolBitShape.get_subclass_by_name(shape_type)
|
||||
|
||||
# If no shape type is specified, try to find the shape class from the ID.
|
||||
shape_class = ToolBitShape.get_subclass_by_name(shape_id)
|
||||
if shape_class:
|
||||
return shape_class
|
||||
|
||||
# If that also fails, try to load the shape to get the class.
|
||||
Path.Log.debug(
|
||||
f'Failed to infer shape type from "{shape_id}", trying to load'
|
||||
f' the shape "{shape_id}" to determine the class. This may'
|
||||
" negatively impact performance."
|
||||
)
|
||||
shape_asset_uri = ToolBitShape.resolve_name(shape_id)
|
||||
shape = cam_assets.get(shape_asset_uri, depth=0)
|
||||
if shape:
|
||||
return shape.__class__
|
||||
|
||||
# If all else fails, try to guess the shape class from the ID.
|
||||
shape_types = [c.name for c in ToolBitShape.__subclasses__()]
|
||||
shape_class = ToolBitShape.guess_subclass_from_name(shape_id)
|
||||
if shape_class:
|
||||
Path.Log.warning(
|
||||
f'Failed to infer shape type from "{shape_id}",'
|
||||
f' guessing "{shape_class.name}".'
|
||||
f" To fix, name the body in the shape file to one of: {shape_types}"
|
||||
)
|
||||
return shape_class
|
||||
|
||||
# Default to endmill if nothing else works
|
||||
Path.Log.warning(
|
||||
f'Failed to infer shape type from {shape_id}, using "endmill".'
|
||||
f" To fix, name the body in the shape file to one of: {shape_types}"
|
||||
)
|
||||
return ToolBitShapeEndmill
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, attrs: Mapping, shallow: bool = False) -> "ToolBit":
|
||||
"""
|
||||
@@ -144,7 +101,7 @@ class ToolBit(Asset, ABC):
|
||||
if not shape_id:
|
||||
raise ValueError("ToolBit dictionary is missing 'shape' key")
|
||||
|
||||
shape_class = cls._get_shape_type(shape_id, attrs.get("shape-type"))
|
||||
shape_class = ToolBitShape.get_shape_class_from_id(shape_id, attrs.get("shape-type"))
|
||||
|
||||
# Create a ToolBitShape instance.
|
||||
if not shallow: # Shallow means: skip loading of child assets
|
||||
|
||||
Reference in New Issue
Block a user