Centralize trimesh import

This commit is contained in:
Richard Ward
2025-07-17 01:18:30 +01:00
parent 817f5a868b
commit e8fcc4fd5e
13 changed files with 62 additions and 48 deletions

View File

@@ -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}")

View File

@@ -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"]

15
parametric_cad/core.py Normal file
View File

@@ -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"]

View File

@@ -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)

View File

@@ -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}")

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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