07_sweep/07_sweep.rs
1//! Sweep showcase: M2 screw (helix spine) + U-shaped pipe (line+arc+line spine)
2//! + twisted ribbon (`Auxiliary` aux-spine mode).
3//!
4//! `ProfileOrient` controls how the profile is oriented as it travels along the spine:
5//!
6//! - `Fixed`: profile is parallel-transported without rotating. Cross-sections
7//! stay parallel to the starting orientation. Suited for straight extrusions;
8//! on a curved spine the profile drifts off the tangent and the result breaks.
9//! - `Torsion`: profile follows the spine's principal normal (raw Frenet–Serret
10//! frame). Suited for constant-curvature/torsion curves like helices and for
11//! 3D free curves where the natural twist should carry into the profile.
12//! Fails near inflection points where the principal normal flips.
13//! - `Up(axis)`: profile keeps `axis` as its binormal — at every point the
14//! profile is rotated around the tangent so one in-plane axis stays in the
15//! tangent–`axis` plane. Suited for roads/rails/pipes that must preserve a
16//! gravity direction. On a helix, `Up(helix_axis)` is equivalent to `Torsion`.
17//! Fails when the tangent becomes parallel to `axis`.
18//! - `Auxiliary(aux_spine)`: profile's tracked axis points from the main spine
19//! toward a parallel auxiliary spine. Arbitrary twist control — e.g. a
20//! helical `aux_spine` on a straight `spine` produces a twisted ribbon.
21
22use cadrum::{Compound, DVec3, Edge, Error, ProfileOrient, Solid, Wire};
23
24// ==================== Component 1: M2 ISO screw ====================
25
26fn build_m2_screw() -> Result<Vec<Solid>, Error> {
27 let r = 1.0;
28 let h_pitch = 0.4;
29 let h_thread = 6.0;
30 let r_head = 1.75;
31 let h_head = 1.3;
32 // ISO M thread fundamental triangle height: H = √3/2 · P (sharp 60° triangle).
33 let r_delta = 3f64.sqrt() / 2.0 * h_pitch;
34
35 // Helix spine at the root radius. x_ref=+X anchors the start at (r-r_delta, 0, 0).
36 let helix = Edge::helix(r - r_delta, h_pitch, h_thread, DVec3::Z, DVec3::X)?;
37
38 // Closed triangular profile in local coords (x: radial, y: along helix tangent).
39 let profile = Edge::polygon(&[DVec3::new(0.0, -h_pitch / 2.0, 0.0), DVec3::new(r_delta, 0.0, 0.0), DVec3::new(0.0, h_pitch / 2.0, 0.0)])?;
40
41 // Align profile +Z with the helix start tangent, then translate to the start point.
42 let profile = profile.align_z(helix.start_tangent(), helix.start_point()).translate(helix.start_point());
43
44 // Sweep along the helix. Up(+Z) ≡ Torsion for a helix and yields a correct thread.
45 let thread = Solid::sweep(&profile, &[helix], ProfileOrient::Up(DVec3::Z))?;
46
47 // Reconstruct the ISO 68-1 basic profile (trapezoid) from the sharp triangle:
48 // union(shaft) fills the bottom H/4 → P/4-wide flat at the root
49 // intersect(crest) trims the top H/8 → P/8-wide flat at the crest
50 let shaft = Solid::cylinder(r - r_delta * 6.0 / 8.0, DVec3::Z, h_thread);
51 let crest = Solid::cylinder(r - r_delta / 8.0, DVec3::Z, h_thread);
52 let thread_shaft = thread.union([&shaft])?.intersect([&crest])?;
53
54 // Stack the flat head on top. Screw ends up centered on the origin.
55 let head = Solid::cylinder(r_head, DVec3::Z, h_head).translate(DVec3::Z * h_thread);
56 Ok(thread_shaft.union([&head])?.color("red"))
57}
58
59// ==================== Component 2: U-shaped pipe ====================
60
61fn build_u_pipe() -> Result<Vec<Solid>, Error> {
62 let pipe_radius = 0.4;
63 let leg_length = 6.0;
64 let gap = 3.0;
65 let half_gap = gap / 2.0;
66 let bend_radius = half_gap;
67
68 // U-shaped path in the XZ plane, centered on origin in X: A↑B ⌒ C↓D.
69 let a = DVec3::new(-half_gap, 0.0, 0.0);
70 let b = DVec3::new(-half_gap, 0.0, leg_length);
71 let arc_mid = DVec3::new(0.0, 0.0, leg_length + bend_radius);
72 let c = DVec3::new(half_gap, 0.0, leg_length);
73 let d = DVec3::new(half_gap, 0.0, 0.0);
74
75 // Spine wire: line → semicircle → line.
76 let up_leg = Edge::line(a, b)?;
77 let bend = Edge::arc_3pts(b, arc_mid, c)?;
78 let down_leg = Edge::line(c, d)?;
79
80 // Circular profile in XY (normal +Z) translated to the spine start `a`.
81 // Spine tangent at `a` is +Z, so the XY-plane circle is already aligned.
82 let profile = Edge::circle(pipe_radius, DVec3::Z)?.translate(a);
83
84 // Up(+Y) fixes the binormal to the path-plane normal, avoiding Frenet
85 // degeneracy on the straight segments.
86 let pipe = Solid::sweep(&[profile], &[up_leg, bend, down_leg], ProfileOrient::Up(DVec3::Y))?;
87 Ok(vec![pipe].translate(DVec3::X * 6.0).color("blue"))
88}
89
90// ==================== Component 3: Auxiliary-spine twisted ribbon ====================
91
92// Sweeping a straight spine with `Auxiliary(&[helix])` rotates the tracked
93// axis of the profile at each point to face the corresponding helix point.
94// A pitch=h helix makes exactly one 360° turn over [0, h], so a flat
95// rectangular profile becomes a ribbon twisted once. With `Fixed` or
96// `Torsion` the profile wouldn't rotate along a straight spine — visible
97// twist is therefore proof that Auxiliary is in effect.
98fn build_twisted_ribbon() -> Result<Vec<Solid>, Error> {
99 let h = 8.0;
100 let aux_r = 3.0;
101
102 let spine = Edge::line(DVec3::ZERO, DVec3::Z * h)?;
103 let aux = Edge::helix(aux_r, h, h, DVec3::Z, DVec3::X)?;
104
105 // Flat rectangle (10:1 aspect) — circles or squares wouldn't reveal any twist.
106 let profile = Edge::polygon(&[DVec3::new(-2.0, -0.2, 0.0), DVec3::new(2.0, -0.2, 0.0), DVec3::new(2.0, 0.2, 0.0), DVec3::new(-2.0, 0.2, 0.0)])?;
107
108 let ribbon = Solid::sweep(&profile, &[spine], ProfileOrient::Auxiliary(&[aux]))?;
109 Ok(vec![ribbon].translate(DVec3::X * 12.0).color("green"))
110}
111
112// ==================== main: side-by-side layout ====================
113//
114// Each builder places its component at its final world position (screw at
115// origin, U-pipe at x=6, ribbon at x=12) and applies its color, so main
116// just concatenates them.
117
118fn main() -> Result<(), Error> {
119 let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
120 let all: Vec<Solid> = [build_m2_screw()?, build_u_pipe()?, build_twisted_ribbon()?].concat();
121
122 let mut f = std::fs::File::create(format!("{example_name}.step")).expect("failed to create STEP file");
123 cadrum::write_step(&all, &mut f)?;
124 let mut f_svg = std::fs::File::create(format!("{example_name}.svg")).expect("failed to create SVG file");
125 // Helical threads have dense hidden lines that clutter the SVG; disable them.
126 cadrum::mesh(&all, 0.5)?.write_svg(DVec3::new(1.0, 1.0, -1.0), DVec3::Z, false, false, &mut f_svg)?;
127 println!("wrote {example_name}.step / {example_name}.svg ({} solids)", all.len());
128 Ok(())
129}