Add chain sprocket primitive and example

This commit is contained in:
Richard Ward
2025-07-17 01:42:53 +01:00
parent e393502dad
commit 8cbe9fb9b0
6 changed files with 94 additions and 7 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
__pycache__/
*.pyc
output/
*.log

View File

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

View File

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

View File

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

View File

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

View File

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