diff --git a/parametric_cad/__init__.py b/parametric_cad/__init__.py index 0085201..1404854 100644 --- a/parametric_cad/__init__.py +++ b/parametric_cad/__init__.py @@ -1,9 +1,9 @@ """Consolidated parametric_cad package.""" -from .core import tm +from .core import tm, safe_difference from .primitives.box import Box from .primitives.gear import SpurGear from .mechanisms.butthinge import ButtHinge from .export.stl import STLExporter -__all__ = ["tm", "Box", "SpurGear", "ButtHinge", "STLExporter"] +__all__ = ["tm", "safe_difference", "Box", "SpurGear", "ButtHinge", "STLExporter"] diff --git a/parametric_cad/core.py b/parametric_cad/core.py index 92f0204..00404f8 100644 --- a/parametric_cad/core.py +++ b/parametric_cad/core.py @@ -8,8 +8,38 @@ wrapper allows the backend to be swapped or mocked easily. import trimesh as _trimesh + +def safe_difference(mesh, other, *, engine="scad"): + """Perform a boolean difference with graceful fallback. + + Parameters + ---------- + mesh : _trimesh.Trimesh + Base mesh to subtract from. + other : _trimesh.Trimesh or list + Mesh or list of meshes to subtract. + engine : str or None, optional + Preferred boolean engine. ``"scad"`` is tried by default. + + Returns + ------- + _trimesh.Trimesh + Resulting mesh if the operation succeeds, otherwise the original + ``mesh`` if all boolean attempts fail. + """ + + try: + if engine: + return mesh.difference(other, engine=engine) + return mesh.difference(other) + except Exception: + try: + return mesh.difference(other) + except Exception: + return mesh + # Public alias so that other modules can use the backend without # importing ``trimesh`` themselves. tm = _trimesh -__all__ = ["tm"] +__all__ = ["tm", "safe_difference"] diff --git a/parametric_cad/examples/hollow_box.py b/parametric_cad/examples/hollow_box.py index 8ca7535..73f2e83 100644 --- a/parametric_cad/examples/hollow_box.py +++ b/parametric_cad/examples/hollow_box.py @@ -1,5 +1,6 @@ from parametric_cad.primitives.box import Box from parametric_cad.export.stl import STLExporter +from parametric_cad.core import safe_difference outer = Box(100, 60, 40).at(0, 0, 0) inner = Box(90, 50, 30).at(5, 5, 5) @@ -7,13 +8,7 @@ inner = Box(90, 50, 30).at(5, 5, 5) # Create hollow box by subtracting inner from outer outer_mesh = outer.mesh() inner_mesh = inner.mesh() -try: - hollow_box = outer_mesh.difference(inner_mesh, engine='scad') -except Exception: - try: - hollow_box = outer_mesh.difference(inner_mesh) - except Exception: - hollow_box = outer_mesh +hollow_box = safe_difference(outer_mesh, inner_mesh) exporter = STLExporter(output_dir="output/hollow_box_output") exporter.export_mesh(hollow_box, "hollow_box") diff --git a/parametric_cad/primitives/gear.py b/parametric_cad/primitives/gear.py index 45e46ad..dad44ab 100644 --- a/parametric_cad/primitives/gear.py +++ b/parametric_cad/primitives/gear.py @@ -1,5 +1,5 @@ import numpy as np -from parametric_cad.core import tm +from parametric_cad.core import tm, safe_difference from shapely.geometry import Polygon from math import pi, sin, cos, tan import logging @@ -91,14 +91,7 @@ class SpurGear: bore = tm.creation.cylinder(radius=self.bore_diameter / 2, height=self.width + 0.1) bore.apply_translation([0, 0, self.width / 2]) - try: - gear = gear_body.difference(bore, engine='scad') - except Exception: - try: - gear = gear_body.difference(bore) - except Exception: - logging.warning("Boolean difference not available, skipping bore subtraction") - gear = gear_body + gear = safe_difference(gear_body, bore) logging.debug(f"Subtracted bore, resulting mesh has {len(gear.vertices)} vertices") if self.hole_count > 0: @@ -112,13 +105,7 @@ class SpurGear: if not hole.is_volume: hole = hole.convex_hull hole_cylinders.append(hole) - try: - gear = gear.difference(hole_cylinders, engine='scad') - except Exception: - try: - gear = gear.difference(hole_cylinders) - except Exception: - logging.warning("Boolean difference not available, skipping hole subtraction") + gear = safe_difference(gear, hole_cylinders) logging.debug(f"Subtracted {self.hole_count} holes, resulting mesh has {len(gear.vertices)} vertices") if not gear.is_watertight: diff --git a/tests/test_primitives.py b/tests/test_primitives.py index 66af709..e598d1e 100644 --- a/tests/test_primitives.py +++ b/tests/test_primitives.py @@ -1,6 +1,6 @@ import pytest import numpy as np -from parametric_cad.core import tm +from parametric_cad.core import tm, safe_difference from math import cos, pi from parametric_cad.primitives.box import Box @@ -34,3 +34,10 @@ def test_cylinder_and_sphere_meshes(): assert isinstance(sph_mesh, tm.Trimesh) assert cyl_mesh.is_watertight assert sph_mesh.is_watertight + + +def test_safe_difference_returns_mesh(): + outer = Box(1.0, 1.0, 1.0) + inner = Box(0.5, 0.5, 0.5).at(0.25, 0.25, 0.25) + result = safe_difference(outer.mesh(), inner.mesh(), engine="invalid") + assert isinstance(result, tm.Trimesh)