Merge pull request #17 from Rich43/codex/refactor-right-angle-bracket-code-declaratively

Add declarative motor bracket example
This commit is contained in:
Richard Ward
2025-07-17 09:19:33 +01:00
committed by GitHub
5 changed files with 100 additions and 9 deletions

View File

@@ -1,7 +1,8 @@
"""Consolidated parametric_cad package."""
from .core import tm, safe_difference
from .core import tm, safe_difference, combine
from .primitives.box import Box
from .primitives.cylinder import Cylinder
from .primitives.gear import SpurGear
from .primitives.sprocket import ChainSprocket
from .mechanisms.butthinge import ButtHinge
@@ -10,7 +11,9 @@ from .export.stl import STLExporter
__all__ = [
"tm",
"safe_difference",
"combine",
"Box",
"Cylinder",
"SpurGear",
"ChainSprocket",
"ButtHinge",

View File

@@ -7,6 +7,7 @@ wrapper allows the backend to be swapped or mocked easily.
"""
import trimesh as _trimesh
from typing import Iterable, Any
def safe_difference(mesh, other, *, engine="scad"):
@@ -42,4 +43,21 @@ def safe_difference(mesh, other, *, engine="scad"):
# importing ``trimesh`` themselves.
tm = _trimesh
__all__ = ["tm", "safe_difference"]
def combine(objects: Iterable[Any]) -> _trimesh.Trimesh:
"""Return a union of ``objects``.
Each object may be a :class:`~trimesh.Trimesh` or have a ``mesh``
method returning one.
"""
meshes = []
for obj in objects:
if isinstance(obj, _trimesh.Trimesh):
meshes.append(obj)
elif hasattr(obj, "mesh"):
m = obj.mesh
meshes.append(m() if callable(m) else m)
else:
raise TypeError(f"Object {obj!r} cannot be converted to a mesh")
return _trimesh.util.concatenate(meshes)
__all__ = ["tm", "safe_difference", "combine"]

View File

@@ -0,0 +1,47 @@
from math import pi
from parametric_cad.primitives.box import Box
from parametric_cad.primitives.cylinder import Cylinder
from parametric_cad.core import combine, safe_difference
from parametric_cad.export.stl import STLExporter
# Basic dimensions for a 540/550 motor bracket
BASE_LENGTH = 50.0
BASE_WIDTH = 40.0
PLATE_HEIGHT = 40.0
THICKNESS = 3.0
MOTOR_HOLE_SPACING = 25.0
MOTOR_HOLE_DIAMETER = 3.2
SHAFT_CLEARANCE_DIAMETER = 10.0
MOTOR_MOUNT_HEIGHT = 20.0
# Create the two plates of the bracket
base = Box(BASE_LENGTH, BASE_WIDTH, THICKNESS).at(0, 0, 0)
plate = Box(BASE_LENGTH, THICKNESS, PLATE_HEIGHT).at(
0, BASE_WIDTH - THICKNESS, THICKNESS
)
# Union the plates into a single mesh
bracket = combine([base, plate])
# Define mounting and shaft clearance holes
hole_y = BASE_WIDTH - THICKNESS / 2
hole_z = THICKNESS + MOTOR_MOUNT_HEIGHT
holes = [
Cylinder(MOTOR_HOLE_DIAMETER / 2, THICKNESS + 0.2)
.rotate([1, 0, 0], pi / 2)
.at(BASE_LENGTH / 2 - MOTOR_HOLE_SPACING / 2, hole_y, hole_z),
Cylinder(MOTOR_HOLE_DIAMETER / 2, THICKNESS + 0.2)
.rotate([1, 0, 0], pi / 2)
.at(BASE_LENGTH / 2 + MOTOR_HOLE_SPACING / 2, hole_y, hole_z),
Cylinder(SHAFT_CLEARANCE_DIAMETER / 2, THICKNESS + 0.2)
.rotate([1, 0, 0], pi / 2)
.at(BASE_LENGTH / 2, hole_y, hole_z),
]
# Subtract holes from the bracket body
bracket = safe_difference(bracket, [h.mesh() for h in holes])
# Export result
exporter = STLExporter(output_dir="output/declarative_motor_bracket_output")
exporter.export_mesh(bracket, "declarative_motor_bracket")

View File

@@ -1,18 +1,32 @@
from parametric_cad.core import tm
from typing import Sequence, Optional
class Cylinder:
def __init__(self, radius, height, sections=32):
def __init__(self, radius: float, height: float, sections: int = 32):
self.radius = radius
self.height = height
self.sections = sections
self._position = (0, 0, 0)
self._position = (0.0, 0.0, 0.0)
self._rotation: Optional[Sequence[float]] = None
def at(self, x, y, z):
def at(self, x: float, y: float, z: float) -> "Cylinder":
self._position = (x, y, z)
return self
def mesh(self):
cyl = tm.creation.cylinder(radius=self.radius, height=self.height,
sections=self.sections)
def rotate(self, axis: Sequence[float], angle: float) -> "Cylinder":
"""Rotate the cylinder around ``axis`` by ``angle`` radians."""
self._rotation = (axis, angle)
return self
def mesh(self) -> tm.Trimesh:
cyl = tm.creation.cylinder(
radius=self.radius,
height=self.height,
sections=self.sections,
)
if self._rotation is not None:
axis, angle = self._rotation
rot = tm.transformations.rotation_matrix(angle, axis)
cyl.apply_transform(rot)
cyl.apply_translation(self._position)
return cyl

View File

@@ -1,6 +1,6 @@
import pytest
import numpy as np
from parametric_cad.core import tm, safe_difference
from parametric_cad.core import tm, safe_difference, combine
from math import cos, sin, pi
from parametric_cad.primitives.box import Box
@@ -44,6 +44,15 @@ def test_safe_difference_returns_mesh():
assert isinstance(result, tm.Trimesh)
def test_combine_and_rotation():
box = Box(1.0, 1.0, 1.0)
cyl = Cylinder(radius=0.5, height=2.0).rotate([1, 0, 0], pi / 2)
combined = combine([box, cyl])
assert isinstance(combined, tm.Trimesh)
# Cylinder rotated around X should extend its height along Y axis
assert combined.extents[1] >= 2.0
def test_chain_sprocket_properties_and_mesh():
sprocket = ChainSprocket(pitch=12.7, roller_diameter=7.75, teeth=10)
expected_pitch_dia = 2 * sprocket.pitch / (2 * sin(pi / sprocket.teeth))