From 8cbe9fb9b05ca98504d4d389f45b134967f91ec1 Mon Sep 17 00:00:00 2001 From: Richard Ward Date: Thu, 17 Jul 2025 01:42:53 +0100 Subject: [PATCH] Add chain sprocket primitive and example --- .gitignore | 1 + parametric_cad/__init__.py | 11 +++- parametric_cad/examples/sprocket_example.py | 10 ++++ parametric_cad/primitives/sprocket.py | 56 +++++++++++++++++++++ run_examples.py | 11 ++-- tests/test_primitives.py | 12 ++++- 6 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 parametric_cad/examples/sprocket_example.py create mode 100644 parametric_cad/primitives/sprocket.py diff --git a/.gitignore b/.gitignore index f849cca..aab5279 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ __pycache__/ *.pyc output/ +*.log diff --git a/parametric_cad/__init__.py b/parametric_cad/__init__.py index 1404854..f633efd 100644 --- a/parametric_cad/__init__.py +++ b/parametric_cad/__init__.py @@ -3,7 +3,16 @@ from .core import tm, safe_difference from .primitives.box import Box from .primitives.gear import SpurGear +from .primitives.sprocket import ChainSprocket from .mechanisms.butthinge import ButtHinge from .export.stl import STLExporter -__all__ = ["tm", "safe_difference", "Box", "SpurGear", "ButtHinge", "STLExporter"] +__all__ = [ + "tm", + "safe_difference", + "Box", + "SpurGear", + "ChainSprocket", + "ButtHinge", + "STLExporter", +] diff --git a/parametric_cad/examples/sprocket_example.py b/parametric_cad/examples/sprocket_example.py new file mode 100644 index 0000000..70b7473 --- /dev/null +++ b/parametric_cad/examples/sprocket_example.py @@ -0,0 +1,10 @@ +from parametric_cad.primitives.sprocket import ChainSprocket +from parametric_cad.export.stl import STLExporter + +# Example sprocket for #420 chain (pitch 12.7 mm, roller dia ~7.75 mm) + +sprocket = ChainSprocket(pitch=12.7, roller_diameter=7.75, teeth=14, + thickness=6.0, bore_diameter=25.0) + +exporter = STLExporter(output_dir="output/sprocket_example_output") +exporter.export_mesh(sprocket.mesh(), "sprocket") diff --git a/parametric_cad/primitives/sprocket.py b/parametric_cad/primitives/sprocket.py new file mode 100644 index 0000000..cc49826 --- /dev/null +++ b/parametric_cad/primitives/sprocket.py @@ -0,0 +1,56 @@ +from math import cos, sin, pi +from parametric_cad.core import tm, safe_difference + +class ChainSprocket: + """Simple chain sprocket for roller chain.""" + + def __init__(self, pitch=12.7, roller_diameter=7.75, teeth=16, + thickness=5.0, bore_diameter=10.0, clearance=0.5): + self.pitch = float(pitch) + self.roller_diameter = float(roller_diameter) + self.teeth = int(teeth) + self.thickness = float(thickness) + self.bore_diameter = float(bore_diameter) + self.clearance = float(clearance) + + @property + def pitch_radius(self): + return self.pitch / (2 * sin(pi / self.teeth)) + + @property + def pitch_diameter(self): + return self.pitch_radius * 2 + + def mesh(self): + # Base disc sized so pockets can be subtracted + outer_radius = self.pitch_radius + self.roller_diameter / 2 + self.clearance + disc = tm.creation.cylinder(radius=outer_radius, height=self.thickness, + sections=self.teeth * 4) + + disc.apply_translation([0, 0, self.thickness / 2]) + + bore = tm.creation.cylinder(radius=self.bore_diameter / 2, + height=self.thickness + 0.1) + bore.apply_translation([0, 0, self.thickness / 2]) + sprocket = safe_difference(disc, bore) + + pocket_radius = self.roller_diameter / 2 + self.clearance + pockets = [] + for i in range(self.teeth): + angle = 2 * pi * i / self.teeth + x = cos(angle) * self.pitch_radius + y = sin(angle) * self.pitch_radius + pocket = tm.creation.cylinder(radius=pocket_radius, + height=self.thickness + 0.1, + sections=16) + pocket.apply_translation([x, y, self.thickness / 2]) + pockets.append(pocket) + + sprocket = safe_difference(sprocket, pockets) + if not sprocket.is_watertight: + repaired = sprocket.fill_holes() + if repaired is not False: + sprocket = repaired + else: + sprocket = sprocket.convex_hull + return sprocket diff --git a/run_examples.py b/run_examples.py index 33e4fc0..a2e49e4 100644 --- a/run_examples.py +++ b/run_examples.py @@ -49,11 +49,12 @@ if __name__ == "__main__": logging.debug("Starting run_examples.py execution") set_environment() - examples = [ - "box_with_door.py", - "hollow_box.py", - "spur_gear_example.py" - ] + examples = [ + "box_with_door.py", + "hollow_box.py", + "spur_gear_example.py", + "sprocket_example.py", + ] for example in examples: run_example_script(example) diff --git a/tests/test_primitives.py b/tests/test_primitives.py index e598d1e..a2c7b43 100644 --- a/tests/test_primitives.py +++ b/tests/test_primitives.py @@ -1,12 +1,13 @@ import pytest import numpy as np from parametric_cad.core import tm, safe_difference -from math import cos, pi +from math import cos, sin, pi from parametric_cad.primitives.box import Box from parametric_cad.primitives.gear import SpurGear from parametric_cad.primitives.cylinder import Cylinder from parametric_cad.primitives.sphere import Sphere +from parametric_cad.primitives.sprocket import ChainSprocket def test_box_mesh_extents_and_position(): @@ -41,3 +42,12 @@ def test_safe_difference_returns_mesh(): 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) + + +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)) + assert sprocket.pitch_diameter == pytest.approx(expected_pitch_dia) + mesh = sprocket.mesh() + assert isinstance(mesh, tm.Trimesh) + assert mesh.is_watertight