From e8fcc4fd5e7704152e82aa4da5b8709b94610f77 Mon Sep 17 00:00:00 2001 From: Richard Ward Date: Thu, 17 Jul 2025 01:18:30 +0100 Subject: [PATCH] Centralize trimesh import --- configure_trimesh_scad.py | 20 ++++++++++---------- parametric_cad/__init__.py | 3 ++- parametric_cad/core.py | 15 +++++++++++++++ parametric_cad/examples/hollow_box.py | 1 - parametric_cad/export/stl.py | 9 ++++----- parametric_cad/mechanisms/butthinge.py | 14 +++++++------- parametric_cad/primitives/box.py | 4 ++-- parametric_cad/primitives/cylinder.py | 6 +++--- parametric_cad/primitives/gear.py | 12 ++++++------ parametric_cad/primitives/sphere.py | 6 +++--- tests/test_export.py | 10 +++++----- tests/test_mechanisms.py | 4 ++-- tests/test_primitives.py | 6 +++--- 13 files changed, 62 insertions(+), 48 deletions(-) create mode 100644 parametric_cad/core.py diff --git a/configure_trimesh_scad.py b/configure_trimesh_scad.py index 1b4c1cb..7e85e3c 100644 --- a/configure_trimesh_scad.py +++ b/configure_trimesh_scad.py @@ -1,6 +1,6 @@ import os import sys -import trimesh +from parametric_cad.core import tm import logging logging.basicConfig(filename='install_debug.log', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') @@ -15,15 +15,15 @@ def configure_trimesh_scad(): print("Configuring trimesh to use OpenSCAD engine...") logging.info("Checking trimesh module availability") - # Check if trimesh is available - try: - import trimesh - logging.info("trimesh module found") - print("trimesh module found.") - except ImportError: - logging.error("trimesh module not found") - print("trimesh module not found! Please ensure it is installed via 'pip install trimesh'.") - sys.exit(1) + # Check if trimesh (the mesh backend) is available + try: + _ = tm.__version__ + logging.info("trimesh module found") + print("trimesh module found.") + except Exception: + logging.error("trimesh module not found") + print("trimesh module not found! Please ensure it is installed via 'pip install trimesh'.") + sys.exit(1) # Check if OpenSCAD executable exists logging.debug(f"Verifying OpenSCAD existence at {openscad_path}") diff --git a/parametric_cad/__init__.py b/parametric_cad/__init__.py index 325ff26..0085201 100644 --- a/parametric_cad/__init__.py +++ b/parametric_cad/__init__.py @@ -1,8 +1,9 @@ """Consolidated parametric_cad package.""" +from .core import tm from .primitives.box import Box from .primitives.gear import SpurGear from .mechanisms.butthinge import ButtHinge from .export.stl import STLExporter -__all__ = ["Box", "SpurGear", "ButtHinge", "STLExporter"] +__all__ = ["tm", "Box", "SpurGear", "ButtHinge", "STLExporter"] diff --git a/parametric_cad/core.py b/parametric_cad/core.py new file mode 100644 index 0000000..92f0204 --- /dev/null +++ b/parametric_cad/core.py @@ -0,0 +1,15 @@ +"""Backend access for parametric_cad. + +This module centralizes interaction with the mesh backend so other +parts of the package do not need to import the backend directly. +Currently :mod:`trimesh` provides all geometry functionality, but this +wrapper allows the backend to be swapped or mocked easily. +""" + +import trimesh as _trimesh + +# Public alias so that other modules can use the backend without +# importing ``trimesh`` themselves. +tm = _trimesh + +__all__ = ["tm"] diff --git a/parametric_cad/examples/hollow_box.py b/parametric_cad/examples/hollow_box.py index 1f5b3af..8ca7535 100644 --- a/parametric_cad/examples/hollow_box.py +++ b/parametric_cad/examples/hollow_box.py @@ -1,6 +1,5 @@ from parametric_cad.primitives.box import Box from parametric_cad.export.stl import STLExporter -import trimesh outer = Box(100, 60, 40).at(0, 0, 0) inner = Box(90, 50, 30).at(5, 5, 5) diff --git a/parametric_cad/export/stl.py b/parametric_cad/export/stl.py index 1cbc290..c36890d 100644 --- a/parametric_cad/export/stl.py +++ b/parametric_cad/export/stl.py @@ -1,5 +1,4 @@ -import trimesh -import trimesh.exchange.stl +from parametric_cad.core import tm import os import logging import numpy as np @@ -18,7 +17,7 @@ class STLExporter: os.makedirs(self.output_dir, exist_ok=True) def _ensure_mesh(self, obj): - if isinstance(obj, trimesh.Trimesh): + if isinstance(obj, tm.Trimesh): return obj if hasattr(obj, "mesh"): m = obj.mesh @@ -31,7 +30,7 @@ class STLExporter: def export_meshes(self, objs, base_filename, timestamp=False, preview=True): meshes = [self._ensure_mesh(o) for o in objs] - combined = trimesh.util.concatenate(meshes) + combined = tm.util.concatenate(meshes) if not combined.is_watertight or combined.vertices.shape[0] == 0: repaired = combined.fill_holes() if repaired is not False: @@ -47,7 +46,7 @@ class STLExporter: if self.binary: combined.export(path, file_type="stl") else: - text = trimesh.exchange.stl.export_stl_ascii(combined) + text = tm.exchange.stl.export_stl_ascii(combined) with open(path, "w", encoding="utf-8") as f: f.write(text) logging.info(f"Exported STL to {path}") diff --git a/parametric_cad/mechanisms/butthinge.py b/parametric_cad/mechanisms/butthinge.py index d3cf3c4..748cd29 100644 --- a/parametric_cad/mechanisms/butthinge.py +++ b/parametric_cad/mechanisms/butthinge.py @@ -1,6 +1,6 @@ -import trimesh -import numpy as np -from parametric_cad.export.stl import STLExporter +from parametric_cad.core import tm +import numpy as np +from parametric_cad.export.stl import STLExporter class ButtHinge: def __init__(self, leaf_length=50.0, leaf_width=25.0, leaf_thickness=2.0, knuckles=5, pin_diameter=3.0): @@ -23,7 +23,7 @@ class ButtHinge: self.exporter = STLExporter() def _create_hinge(self): - """Create a trimesh object representing the butt hinge.""" + """Create a trimesh object representing the butt hinge.""" # Create two leaves leaf_vertices = np.array([ [0, 0, 0], [self.leaf_length, 0, 0], [self.leaf_length, self.leaf_width, 0], @@ -35,7 +35,7 @@ class ButtHinge: [0, 1, 5], [0, 5, 4], [2, 3, 7], [2, 7, 6], [0, 3, 7], [0, 7, 4], [1, 2, 6], [1, 6, 5] ]) - leaf1 = trimesh.Trimesh(vertices=leaf_vertices, faces=leaf_faces, process=False) + leaf1 = tm.Trimesh(vertices=leaf_vertices, faces=leaf_faces, process=False) # Second leaf, offset for hinge alignment leaf2 = leaf1.copy() @@ -46,7 +46,7 @@ class ButtHinge: knuckle_spacing = self.leaf_length / (self.knuckles + 1) for i in range(self.knuckles): z_pos = knuckle_spacing * (i + 1) - knuckle = trimesh.creation.cylinder( + knuckle = tm.creation.cylinder( radius=self.pin_diameter / 2, height=self.leaf_thickness + 0.1, # Slight overlap for union sections=16 @@ -55,7 +55,7 @@ class ButtHinge: knuckle_meshes.append(knuckle) # Combine leaves and knuckles - hinge = trimesh.util.concatenate([leaf1, leaf2] + knuckle_meshes) + hinge = tm.util.concatenate([leaf1, leaf2] + knuckle_meshes) return hinge def mesh(self): diff --git a/parametric_cad/primitives/box.py b/parametric_cad/primitives/box.py index 4ad299b..641cd5e 100644 --- a/parametric_cad/primitives/box.py +++ b/parametric_cad/primitives/box.py @@ -1,4 +1,4 @@ -import trimesh +from parametric_cad.core import tm class Box: def __init__(self, width, depth, height): @@ -12,6 +12,6 @@ class Box: return self def mesh(self): - box = trimesh.creation.box(extents=(self.width, self.depth, self.height)) + box = tm.creation.box(extents=(self.width, self.depth, self.height)) box.apply_translation(self._position) return box diff --git a/parametric_cad/primitives/cylinder.py b/parametric_cad/primitives/cylinder.py index 4731cd9..6203f78 100644 --- a/parametric_cad/primitives/cylinder.py +++ b/parametric_cad/primitives/cylinder.py @@ -1,4 +1,4 @@ -import trimesh +from parametric_cad.core import tm class Cylinder: def __init__(self, radius, height, sections=32): @@ -12,7 +12,7 @@ class Cylinder: return self def mesh(self): - cyl = trimesh.creation.cylinder(radius=self.radius, height=self.height, - sections=self.sections) + cyl = tm.creation.cylinder(radius=self.radius, height=self.height, + sections=self.sections) cyl.apply_translation(self._position) return cyl diff --git a/parametric_cad/primitives/gear.py b/parametric_cad/primitives/gear.py index ea1d90b..45e46ad 100644 --- a/parametric_cad/primitives/gear.py +++ b/parametric_cad/primitives/gear.py @@ -1,5 +1,5 @@ import numpy as np -import trimesh +from parametric_cad.core import tm from shapely.geometry import Polygon from math import pi, sin, cos, tan import logging @@ -75,21 +75,21 @@ class SpurGear: polygon = polygon.buffer(0) logging.warning("Tooth polygon was invalid, repaired with buffer") - tooth_mesh = trimesh.creation.extrude_polygon(polygon, self.width, engine='triangle') + tooth_mesh = tm.creation.extrude_polygon(polygon, self.width, engine='triangle') logging.debug(f"Extruded tooth mesh with {len(tooth_mesh.vertices)} vertices") all_teeth = [] for i in range(self.teeth): angle = 2 * pi * i / self.teeth - rot = trimesh.transformations.rotation_matrix(angle, [0, 0, 1]) + rot = tm.transformations.rotation_matrix(angle, [0, 0, 1]) rotated_tooth = tooth_mesh.copy().apply_transform(rot) all_teeth.append(rotated_tooth) logging.debug(f"Added tooth {i+1}/{self.teeth}") - gear_body = trimesh.util.concatenate(all_teeth) + gear_body = tm.util.concatenate(all_teeth) logging.debug(f"Combined {self.teeth} teeth into gear body with {len(gear_body.vertices)} vertices") - bore = trimesh.creation.cylinder(radius=self.bore_diameter / 2, height=self.width + 0.1) + 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') @@ -107,7 +107,7 @@ class SpurGear: angle = 2 * pi * i / self.hole_count x = cos(angle) * self.hole_radius y = sin(angle) * self.hole_radius - hole = trimesh.creation.cylinder(radius=self.hole_diameter / 2, height=self.width + 0.1) + hole = tm.creation.cylinder(radius=self.hole_diameter / 2, height=self.width + 0.1) hole.apply_translation([x, y, self.width / 2]) if not hole.is_volume: hole = hole.convex_hull diff --git a/parametric_cad/primitives/sphere.py b/parametric_cad/primitives/sphere.py index 96b73bc..672d500 100644 --- a/parametric_cad/primitives/sphere.py +++ b/parametric_cad/primitives/sphere.py @@ -1,4 +1,4 @@ -import trimesh +from parametric_cad.core import tm class Sphere: def __init__(self, radius, subdivisions=3): @@ -11,7 +11,7 @@ class Sphere: return self def mesh(self): - sph = trimesh.creation.icosphere(subdivisions=self.subdivisions, - radius=self.radius) + sph = tm.creation.icosphere(subdivisions=self.subdivisions, + radius=self.radius) sph.apply_translation(self._position) return sph diff --git a/tests/test_export.py b/tests/test_export.py index d8ae109..c0a669a 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -1,5 +1,5 @@ import os -import trimesh +from parametric_cad.core import tm from parametric_cad.primitives.box import Box from parametric_cad.primitives.cylinder import Cylinder from parametric_cad.primitives.sphere import Sphere @@ -14,14 +14,14 @@ def test_stl_exporter(tmp_path): assert path == str(tmp_path / "test_box.stl") with open(path, "r", encoding="utf-8") as f: contents = f.read() - mesh = trimesh.util.concatenate([box.mesh()]) + mesh = tm.util.concatenate([box.mesh()]) if not mesh.is_watertight or mesh.vertices.shape[0] == 0: repaired = mesh.fill_holes() if repaired is not False: mesh = repaired else: mesh = mesh.convex_hull - expected = trimesh.exchange.stl.export_stl_ascii(mesh) + expected = tm.exchange.stl.export_stl_ascii(mesh) assert contents == expected @@ -35,12 +35,12 @@ def test_ascii_stl_multiple_objects(tmp_path): assert path == str(tmp_path / "combo.stl") with open(path, "r", encoding="utf-8") as f: contents = f.read() - mesh = trimesh.util.concatenate([box.mesh(), cyl.mesh(), sph.mesh()]) + mesh = tm.util.concatenate([box.mesh(), cyl.mesh(), sph.mesh()]) if not mesh.is_watertight or mesh.vertices.shape[0] == 0: repaired = mesh.fill_holes() if repaired is not False: mesh = repaired else: mesh = mesh.convex_hull - expected = trimesh.exchange.stl.export_stl_ascii(mesh) + expected = tm.exchange.stl.export_stl_ascii(mesh) assert contents == expected diff --git a/tests/test_mechanisms.py b/tests/test_mechanisms.py index e9e6752..f6b808c 100644 --- a/tests/test_mechanisms.py +++ b/tests/test_mechanisms.py @@ -1,6 +1,6 @@ import numpy as np import pytest -import trimesh +from parametric_cad.core import tm from parametric_cad.mechanisms.butthinge import ButtHinge @@ -8,7 +8,7 @@ from parametric_cad.mechanisms.butthinge import ButtHinge def test_butthinge_mesh_and_translation(): hinge = ButtHinge() mesh = hinge.mesh() - assert isinstance(mesh, trimesh.Trimesh) + assert isinstance(mesh, tm.Trimesh) original_centroid = mesh.centroid.copy() hinge.at(1.0, 2.0, 3.0) translated_centroid = hinge.mesh().centroid diff --git a/tests/test_primitives.py b/tests/test_primitives.py index 20e8cd7..66af709 100644 --- a/tests/test_primitives.py +++ b/tests/test_primitives.py @@ -1,6 +1,6 @@ import pytest import numpy as np -import trimesh +from parametric_cad.core import tm from math import cos, pi from parametric_cad.primitives.box import Box @@ -30,7 +30,7 @@ def test_cylinder_and_sphere_meshes(): sph = Sphere(radius=1.0).at(-0.5, -0.5, 0) cyl_mesh = cyl.mesh() sph_mesh = sph.mesh() - assert isinstance(cyl_mesh, trimesh.Trimesh) - assert isinstance(sph_mesh, trimesh.Trimesh) + assert isinstance(cyl_mesh, tm.Trimesh) + assert isinstance(sph_mesh, tm.Trimesh) assert cyl_mesh.is_watertight assert sph_mesh.is_watertight