Skip to main content

geop_lib_stdlib/
drive.rs

1//! The drive train: involute spur gears and their rack, GT2 timing
2//! pulleys, shaft collars and flange couplings. Each turns around the `z`
3//! axis — the rack runs along it — its datum `axis`.
4//!
5//! A gear's teeth are what is left of a disc when one gap, patterned round
6//! it, is cut away: a boolean per tooth, the price of a tooth count that is
7//! a parameter like any other — building a gear takes seconds per tooth.
8//! Every gap reaches past the disc on all sides it leaves by, so that no
9//! face of it lies on one of the disc's. A rack and a pulley, whose teeth
10//! would be cut along one long edge or past two flanges, are each drawn in
11//! one profile instead: a rack of at most [`RACK_TEETH`] teeth cut to
12//! length, and a pulley family per tooth count.
13//!
14//! # Meshing
15//!
16//! Gears of one module mesh with their pitch circles touching: centres
17//! `m · (z1 + z2) / 2` apart. Each gear's datum `axis` is a joint's
18//! connector; a revolute joint on each, and a `Transmission::Gear`
19//! coupling of the two joints with `value = z2 / z1`, the ratio — the
20//! first joint's gear turns that many times per turn of the second's — and
21//! `reverse`, since external gears turn opposite ways, keeps them in mesh.
22//! A rack slides by the arc its pinion's pitch circle rolls off: a
23//! `Transmission::RackPinion` coupling of `value = m · z / 2`, the
24//! pinion's pitch radius, which its sketch `pitch` draws. Two GT2 pulleys on one belt turn the same way, at the
25//! ratio of their tooth counts: a gear coupling without `reverse`.
26//!
27//! A gear has a tooth centred on `+x`. Two gears whose centres lie along
28//! `x` mesh when the second has a gap facing the first: turned half a
29//! tooth, `180° / z2`, if `z2` is even, and not at all if it is odd.
30
31use geop_core_math::{geop_error::GeopResult, primitives::FrameAxis};
32use geop_ops_extrude_revolve::{Extent, Extents};
33use geop_ops_model::Combine;
34use geop_ops_model::datums::{origin_axis, origin_plane};
35use geop_ops_model::parameters::{Parameter, ParameterKind, Row};
36use geop_ops_model::spacing::Spacing;
37use geop_ops_pattern::{CircularPatternArgs, Seeds, TurnAxis, body};
38
39use super::{
40    StandardPart,
41    drawing::Drawing,
42    involute::{self, DEGREE, FEWEST_TEETH},
43    steps::{
44        Around, axis_datum, col, datum_plane, extrude, family, offset_datum, outline_plane,
45        plane_datum, profile_plane, revolve, size,
46    },
47    tables, titled,
48};
49
50/// The table parameter a gear's tooth count is chosen by.
51pub const TEETH: &str = "teeth";
52
53/// The most teeth a gear of the library has.
54pub const MOST_TEETH: usize = 120;
55
56/// The columns of the [`TEETH`] table: `z`, then the lower side of the gap
57/// between two teeth, of module 1 (see [`involute::Gap`]): its foot `fx`,
58/// `fy`, and the control points of its flank `c0x`, `c0y`, ...
59const GAP_COLUMNS: [&str; 3 + 2 * (DEGREE + 1)] = [
60    "z", "fx", "fy", "c0x", "c0y", "c1x", "c1y", "c2x", "c2y", "c3x", "c3y", "c4x", "c4y", "c5x",
61    "c5y", "c6x", "c6y", "c7x", "c7y", "c8x", "c8y", "c9x", "c9y",
62];
63
64/// Every tooth count from [`FEWEST_TEETH`] to [`MOST_TEETH`], as the
65/// table parameter [`TEETH`]: row `z20` the gap of a gear of 20 teeth.
66fn teeth() -> GeopResult<Parameter> {
67    let rows = (FEWEST_TEETH..=MOST_TEETH)
68        .map(|z| {
69            let gap = involute::gap(z)?;
70            let mut values = vec![z as f64];
71            values.extend(gap.foot);
72            values.extend(gap.flank.iter().flatten());
73            Ok(Row {
74                name: format!("z{z}"),
75                values,
76            })
77        })
78        .collect::<GeopResult<_>>()?;
79    Ok(Parameter {
80        name: TEETH.into(),
81        kind: ParameterKind::Table {
82            columns: GAP_COLUMNS.iter().map(|c| c.to_string()).collect(),
83            rows,
84            selected: "z20".into(),
85        },
86    })
87}
88
89/// An involute spur gear, 20° pressure angle, of module `size.m` and
90/// [`TEETH`]`.z` teeth, `width` wide up `z` from its side on the `xy`
91/// plane, bored `bore` through: a disc of the tip diameter, the gap of
92/// [`involute::gap`] patterned round it and cut away. Its datums: `axis`,
93/// `side`, and the sketch `pitch`, its pitch circle on the side.
94pub fn spur_gear(file: &'static str) -> GeopResult<StandardPart> {
95    let mut program = family(
96        "Steel",
97        Some("#9aa0a8"),
98        vec![
99            size(tables::gear_modules()),
100            teeth()?,
101            Parameter::number("width", &format!("8 * {}", col("m")), 1.0, 100.0),
102            Parameter::number(
103                "bore",
104                &format!("max(2, round({} * {TEETH}.z / 4))", col("m")),
105                1.0,
106                100.0,
107            ),
108        ],
109    );
110    let m = col("m");
111    let z = format!("{TEETH}.z");
112    // The disc's rim, a circle of the tip diameter: its quarters end on
113    // multiples of 90°, tooth centres or at least 0.015 of a module from
114    // where any gap of up to 120 teeth crosses the rim.
115    let mut blank = Drawing::new(&program)?;
116    let centre = blank.origin();
117    blank.circle(centre, &format!("{m} * ({z} + 2)"))?;
118    blank.circle(centre, "bore")?;
119    extrude(
120        &mut program,
121        "blank",
122        "blank_outline",
123        (blank, outline_plane()),
124        Extents::blind("width"),
125        Combine::NewBody,
126    );
127
128    // One gap, round from its foot on the root circle: up its lower side,
129    // over a point on `x` past the tip circle, down its upper side — the
130    // lower one's mirror image — and back across the root. Extruded both
131    // ways from the gear's middle, past both its sides.
132    //
133    // Only lines and the flanks, every point placed by a formula: a sketch
134    // the solver places in one step for any size. With arcs, whose sweeps
135    // it has to find, a gear far from the one drawn sent it off to
136    // infinity. The root is so a chord of the root circle, at most
137    // `r (1 - cos(π / 2z))` below it — 0.04 of a module for 12 teeth.
138    let mut gap = Drawing::new(&program)?;
139    let at = |column: &str, sign: &str| format!("{sign}{m} * {TEETH}.{column}");
140    let lower =
141        |d: &mut Drawing, k: &str| d.point(at(&format!("{k}x"), ""), at(&format!("{k}y"), ""));
142    let upper =
143        |d: &mut Drawing, k: &str| d.point(at(&format!("{k}x"), ""), at(&format!("{k}y"), "-"));
144    let foot = lower(&mut gap, "f")?;
145    let flank = (0..=DEGREE)
146        .map(|k| lower(&mut gap, &format!("c{k}")))
147        .collect::<GeopResult<Vec<_>>>()?;
148    let upper_flank = (0..=DEGREE)
149        .rev()
150        .map(|k| upper(&mut gap, &format!("c{k}")))
151        .collect::<GeopResult<Vec<_>>>()?;
152    let upper_foot = upper(&mut gap, "f")?;
153    gap.line(foot, flank[0]);
154    gap.bezier(flank.clone());
155    let past = gap.point(format!("{m} * ({z} / 2 + 2.5)"), "0")?;
156    gap.line(flank[DEGREE], past);
157    gap.line(past, upper_flank[0]);
158    gap.bezier(upper_flank.clone());
159    gap.line(upper_flank[DEGREE], upper_foot);
160    gap.line(upper_foot, foot);
161    plane_datum(&mut program, "middle", "width / 2");
162    extrude(
163        &mut program,
164        "gap",
165        "gap_outline",
166        (gap, datum_plane("middle")),
167        Extents {
168            symmetric: true,
169            ..Extents::blind(format!("width + 2 * {m}"))
170        },
171        Combine::NewBody,
172    );
173    program.push(
174        "teeth",
175        CircularPatternArgs {
176            seeds: Seeds::bodies(vec![body("extrude(gap)")], Combine::cut("extrude(blank)")),
177            axis: Some(TurnAxis::Datum(origin_axis(FrameAxis::Z))),
178            reversed: false,
179            count: z.as_str().into(),
180            angle: Spacing::extent(360.0),
181        },
182    );
183
184    let mut pitch = Drawing::new(&program)?;
185    let centre = pitch.origin();
186    pitch.circle(centre, &format!("{m} * {z}"))?;
187    program.push("pitch", pitch.on(outline_plane()));
188    axis_datum(&mut program);
189    plane_datum(&mut program, "side", "0");
190    Ok(StandardPart {
191        file,
192        base: "side",
193        program: titled(
194            program,
195            "Involute spur gear, 20° pressure angle",
196            "Spur gear",
197        ),
198        threaded: Vec::new(),
199    })
200}
201
202/// The most teeth a rack of the library has.
203pub const RACK_TEETH: usize = 30;
204
205/// A gear rack of module `size.m`, `size.b` wide, its pitch line `size.h`
206/// above its back, with `teeth` teeth — at most [`RACK_TEETH`] — running
207/// `teeth · π m` along `z` from the `xy` plane: its teeth face `+y`, its
208/// pitch plane is the `xz` plane, and it is centred on it across `x`. The
209/// teeth are straight-sided, 20° to their centre lines, `π m / 2` thick on
210/// the pitch line, the first centred half a pitch up from `z = 0`.
211///
212/// One profile of all [`RACK_TEETH`] teeth and half a gap at either end,
213/// extruded across, then cut to length — through the middle of a gap — by
214/// a box: a single boolean, where cutting a gap per tooth would split the
215/// rack's long edges dozens of times over. Its datums: `axis`, the `z` axis
216/// on the pitch plane — a slider joint's connector — `pitch`, `back`, and
217/// `end`.
218pub fn rack(file: &'static str) -> GeopResult<StandardPart> {
219    let mut program = family(
220        "Steel",
221        Some("#9aa0a8"),
222        vec![
223            size(tables::racks()),
224            Parameter::number("teeth", "20", 1.0, RACK_TEETH as f64),
225        ],
226    );
227    let (m, b, h) = (col("m"), col("b"), col("h"));
228    // In the profile plane: `x` up the teeth, along `y`; `y` along `z`.
229    let half = |height: &str| format!("pi * {m} / 4 - {height} * tan(20)");
230    let (root, tip) = (format!("-1.25 * {m}"), m.clone());
231    let along = |k: usize, sign: &str, height: &str| {
232        format!("{}.5 * pi * {m} {sign} ({})", k, half(height))
233    };
234    let mut corners = vec![
235        [format!("-{h}"), "0".to_string()],
236        [root.clone(), "0".into()],
237    ];
238    for k in 0..=RACK_TEETH {
239        corners.push([root.clone(), along(k, "-", &root)]);
240        corners.push([tip.clone(), along(k, "-", &tip)]);
241        corners.push([tip.clone(), along(k, "+", &tip)]);
242        corners.push([root.clone(), along(k, "+", &root)]);
243    }
244    let end = format!("{} * pi * {m}", RACK_TEETH + 1);
245    corners.push([root.clone(), end.clone()]);
246    corners.push([format!("-{h}"), end]);
247    let mut profile = Drawing::new(&program)?;
248    profile.polygon(&corners)?;
249    let across = |length: String| Extents {
250        symmetric: true,
251        ..Extents::blind(length)
252    };
253    extrude(
254        &mut program,
255        "teeth",
256        "profile",
257        (profile, profile_plane()),
258        across(b.clone()),
259        Combine::NewBody,
260    );
261    let mut length = Drawing::new(&program)?;
262    length.polygon(&[
263        [format!("-{h} - {m}"), format!("-{m}")],
264        [format!("2 * {m}"), format!("-{m}")],
265        [format!("2 * {m}"), format!("teeth * pi * {m}")],
266        [format!("-{h} - {m}"), format!("teeth * pi * {m}")],
267    ])?;
268    extrude(
269        &mut program,
270        "rack",
271        "length",
272        (length, profile_plane()),
273        across(format!("{b} + 2 * {m}")),
274        Combine::intersect("extrude(teeth)"),
275    );
276    axis_datum(&mut program);
277    let xz = origin_plane(FrameAxis::Y);
278    offset_datum(&mut program, "pitch", xz.name(), "0");
279    offset_datum(&mut program, "back", xz.name(), &format!("-{h}"));
280    plane_datum(&mut program, "end", "0");
281    Ok(StandardPart {
282        file,
283        base: "back",
284        program: titled(program, "Gear rack, 20° pressure angle", "Gear rack"),
285        threaded: Vec::new(),
286    })
287}
288
289/// The radius of a GT2 pulley's round groove, one per tooth of the belt.
290const GT2_GROOVE: f64 = 0.555;
291
292/// How deep a GT2 pulley's groove is, from its outside diameter.
293const GT2_DEPTH: f64 = 0.76;
294
295/// A GT2 timing pulley of `z` teeth for a 6 mm belt: a hub `dh` across and
296/// `lh` long from its end on the `xy` plane, then the teeth `w` wide
297/// between two flanges `df` across and `t` thick, bored `d` through — `d`,
298/// `dh` and `lh` from its table, by bore.
299///
300/// The teeth are simplified: the outside diameter is the pitch diameter
301/// `2z / π` less twice the belt's pitch line offset, 0.254, and a round
302/// groove [`GT2_GROOVE`] in radius and [`GT2_DEPTH`] deep is cut for each
303/// tooth of the belt. Their count fixes the outline, so each count is a
304/// family: a ring of that outline, drawn in one sketch, joined to the hub
305/// and flanges turned in one profile — one boolean. Its datums: `axis`,
306/// `end`, and the plane `belt` through the middle of the belt, on which
307/// the sketch `pitch` draws its pitch circle.
308fn gt2_pulley(z: usize, file: &'static str) -> GeopResult<StandardPart> {
309    let mut program = family(
310        "Aluminium 6061",
311        Some("#c8ccd2"),
312        vec![size(tables::gt2_pulleys(z))],
313    );
314    let (d, dh, lh) = (col("d"), col("dh"), col("lh"));
315    let (t, w) = (1.0, 7.0);
316    let pitch = 2.0 * z as f64 / std::f64::consts::PI;
317    let outside = (pitch - 0.508) / 2.0;
318    let flange = format!("{}", outside + 2.0);
319    let core = format!("{}", outside - 1.0);
320    let (bore, hub) = (format!("{d} / 2"), format!("{dh} / 2"));
321    let (inner_face, outer_face, end) = (
322        format!("{lh} + {t}"),
323        format!("{lh} + {}", t + w),
324        format!("{lh} + {}", 2.0 * t + w),
325    );
326    let zero = || "0".to_string();
327    let mut profile = Drawing::new(&program)?;
328    profile.polygon(&[
329        [bore.clone(), zero()],
330        [hub.clone(), zero()],
331        [hub, lh.clone()],
332        [flange.clone(), lh.clone()],
333        [flange.clone(), inner_face.clone()],
334        [core.clone(), inner_face],
335        [core, outer_face.clone()],
336        [flange.clone(), outer_face],
337        [flange, end.clone()],
338        [bore, end],
339    ])?;
340    revolve(
341        &mut program,
342        "body",
343        "profile",
344        profile,
345        Around::ZAxis,
346        Combine::NewBody,
347    );
348
349    // The ring runs from the middle of one flange to the middle of the
350    // other, so that none of its faces lies on one of theirs: round, per
351    // tooth, a land on the outside circle and a groove, drawn as two arcs
352    // meeting at its bottom so that each is less than half a circle.
353    plane_datum(&mut program, "ring_floor", &format!("{lh} + {}", t / 2.0));
354    let centre = outside - GT2_DEPTH + GT2_GROOVE;
355    let half =
356        ((outside.powi(2) + centre.powi(2) - GT2_GROOVE.powi(2)) / (2.0 * outside * centre)).acos();
357    let mut ring = Drawing::new(&program)?;
358    let at = |r: f64, a: f64| [format!("{}", r * a.cos()), format!("{}", r * a.sin())];
359    let mut groove_ends = Vec::new();
360    for k in 0..z {
361        // Half a tooth round from `x`: no groove meets the seams of the
362        // turned flanks, which lie on the axes.
363        let a = 2.0 * std::f64::consts::PI * (k as f64 + 0.5) / z as f64;
364        let [x0, y0] = at(outside, a - half);
365        let [xb, yb] = at(outside - GT2_DEPTH, a);
366        let [x1, y1] = at(outside, a + half);
367        groove_ends.push([
368            ring.point(x0, y0)?,
369            ring.point(xb, yb)?,
370            ring.point(x1, y1)?,
371        ]);
372    }
373    let (groove, land) = (format!("{GT2_GROOVE}"), format!("{outside}"));
374    for k in 0..z {
375        let [start, bottom, end] = groove_ends[k];
376        ring.arc(start, bottom, &groove, false)?;
377        ring.arc(bottom, end, &groove, false)?;
378        ring.arc(end, groove_ends[(k + 1) % z][0], &land, true)?;
379    }
380    let centre_point = ring.origin();
381    ring.circle(centre_point, &format!("{}", 2.0 * (outside - 1.5)))?;
382    extrude(
383        &mut program,
384        "ring",
385        "ring_outline",
386        (ring, datum_plane("ring_floor")),
387        Extents::blind(t + w),
388        Combine::join("revolve(body)"),
389    );
390
391    axis_datum(&mut program);
392    plane_datum(&mut program, "end", "0");
393    plane_datum(&mut program, "belt", &format!("{lh} + {}", t + w / 2.0));
394    let mut circle = Drawing::new(&program)?;
395    let centre_point = circle.origin();
396    circle.circle(centre_point, &format!("{pitch}"))?;
397    program.push("pitch", circle.on(datum_plane("belt")));
398    Ok(StandardPart {
399        file,
400        base: "end",
401        program: titled(program, "GT2 timing pulley, 6 mm belt", "GT2 pulley"),
402        threaded: Vec::new(),
403    })
404}
405
406pub fn gt2_pulley_16(file: &'static str) -> GeopResult<StandardPart> {
407    gt2_pulley(16, file)
408}
409
410pub fn gt2_pulley_20(file: &'static str) -> GeopResult<StandardPart> {
411    gt2_pulley(20, file)
412}
413
414pub fn gt2_pulley_36(file: &'static str) -> GeopResult<StandardPart> {
415    gt2_pulley(36, file)
416}
417
418/// A shaft collar after DIN 705 A: a ring bored `d`, `D` across and `b`
419/// wide, standing on the `xy` plane, with a set screw hole `ds` across
420/// through its wall along `+x`, half way up: the thread's nominal
421/// diameter. The boolean cutting it names the hole's faces after what it
422/// crossed, so they are not listed as threaded.
423pub fn shaft_collar(file: &'static str) -> GeopResult<StandardPart> {
424    let mut program = family(
425        "Steel",
426        Some("#8e949c"),
427        vec![size(tables::shaft_collars())],
428    );
429    let (d, big, b) = (col("d"), col("D"), col("b"));
430    let mut profile = Drawing::new(&program)?;
431    profile.polygon(&[
432        [format!("{d} / 2"), "0".into()],
433        [format!("{big} / 2"), "0".into()],
434        [format!("{big} / 2"), b.clone()],
435        [format!("{d} / 2"), b.clone()],
436    ])?;
437    revolve(
438        &mut program,
439        "ring",
440        "profile",
441        profile,
442        Around::ZAxis,
443        Combine::NewBody,
444    );
445    // Drawn in the profile plane, through the axis, and extruded out along
446    // `+x` from inside the bore.
447    let mut hole = Drawing::new(&program)?;
448    let centre = hole.point("0", format!("{b} / 2"))?;
449    hole.circle(centre, &col("ds"))?;
450    extrude(
451        &mut program,
452        "set_screw",
453        "set_screw_hole",
454        (hole, profile_plane()),
455        Extents {
456            side1: Extent::ThroughAll,
457            symmetric: false,
458            side2: None,
459            reversed: false,
460        },
461        Combine::cut("revolve(ring)"),
462    );
463    axis_datum(&mut program);
464    plane_datum(&mut program, "base", "0");
465    Ok(StandardPart {
466        file,
467        base: "base",
468        program: titled(
469            program,
470            "Shaft collar with set screw, DIN 705 A",
471            "DIN 705 A",
472        ),
473        threaded: Vec::new(),
474    })
475}
476
477/// A rigid flange coupling as an envelope, joining a shaft `d1` across to
478/// one `d2` across: two hubs `D` across, `L` long in all from its `d1` end
479/// on the `xy` plane, and between them, in the middle, the bolted flanges
480/// `F` across and `f` thick together. Its datums: `axis`, `end` — the `d1`
481/// end — and `far_end`.
482pub fn flange_coupling(file: &'static str) -> GeopResult<StandardPart> {
483    let mut program = family(
484        "Aluminium 6061",
485        Some("#c8ccd2"),
486        vec![size(tables::flange_couplings())],
487    );
488    let (d1, d2, big, flange, l, f) =
489        (col("d1"), col("d2"), col("D"), col("F"), col("L"), col("f"));
490    let (hub, rim) = (format!("{big} / 2"), format!("{flange} / 2"));
491    let (low, mid, high) = (
492        format!("({l} - {f}) / 2"),
493        format!("{l} / 2"),
494        format!("({l} + {f}) / 2"),
495    );
496    let mut profile = Drawing::new(&program)?;
497    profile.polygon(&[
498        [format!("{d1} / 2"), "0".into()],
499        [hub.clone(), "0".into()],
500        [hub.clone(), low.clone()],
501        [rim.clone(), low],
502        [rim, high.clone()],
503        [hub.clone(), high],
504        [hub, l.clone()],
505        [format!("{d2} / 2"), l.clone()],
506        [format!("{d2} / 2"), mid.clone()],
507        [format!("{d1} / 2"), mid],
508    ])?;
509    revolve(
510        &mut program,
511        "coupling",
512        "profile",
513        profile,
514        Around::ZAxis,
515        Combine::NewBody,
516    );
517    axis_datum(&mut program);
518    plane_datum(&mut program, "end", "0");
519    plane_datum(&mut program, "far_end", &l);
520    Ok(StandardPart {
521        file,
522        base: "end",
523        program: titled(program, "Rigid flange shaft coupling", "Flange coupling"),
524        threaded: Vec::new(),
525    })
526}