brep_kernel/construction/sweep_topology/
twist.rs1use super::*;
2
3pub const SWEEP_TWIST_CLOSURE_REFUSAL: &str =
6 "sweepSolid: a requested twist does not close this closed path";
7
8#[derive(Debug, Clone, Copy, PartialEq)]
15pub(super) struct ClosingTwist {
16 pub twist: f64,
17 pub shift: usize,
18}
19
20pub(super) fn closing_twist(
45 section: &[NurbsCurve],
46 axis_point: Vec3,
47 axis: Vec3,
48 requested: f64,
49 floor: f64,
50) -> Result<ClosingTwist, String> {
51 use std::f64::consts::TAU;
52 if requested == 0.0 {
53 return Ok(ClosingTwist {
54 twist: 0.0,
55 shift: 0,
56 });
57 }
58 if !requested.is_finite() {
59 return Err("sweepSolid: twist angle must be finite".into());
60 }
61 let symmetry = section_symmetry(section, axis_point, axis, floor)?;
62 let order = symmetry.len();
63 let step = TAU / order as f64;
64 let turns = (requested / step).round();
65 let common = {
70 let (mut a, mut b) = ((turns as i64).unsigned_abs(), order as u64);
71 while b != 0 {
72 (a, b) = (b, a % b);
73 }
74 a.max(1) as f64
75 };
76 let snapped = TAU * (turns / common) / (order as f64 / common);
77 if (requested - snapped).abs() <= 64.0 * f64::EPSILON * requested.abs().max(1.0) {
78 let index = (turns as i64).rem_euclid(order as i64) as usize;
79 return Ok(ClosingTwist {
80 twist: snapped,
81 shift: symmetry[index],
82 });
83 }
84 let below = TAU * (requested / step).floor() / order as f64;
85 let above = TAU * (requested / step).ceil() / order as f64;
86 let reading = if order == 1 {
87 "this section has NO rotational symmetry about the path, so only a whole number of \
88 turns carries it onto itself"
89 .to_string()
90 } else {
91 format!(
92 "this section maps onto itself under {order} rotations about the path, so a twist \
93 closes only in steps of {:.6}°",
94 step.to_degrees()
95 )
96 };
97 Err(format!(
98 "{SWEEP_TWIST_CLOSURE_REFUSAL}: the section comes back from one lap rolled about the path \
99 by the requested {:.6}° (the frame's own roll, its holonomy, is closed separately by the \
100 counter-twist), and a roll that does not carry the section onto itself leaves the last \
101 wall meeting the first one turned. The symmetry is read off the section's boundary \
102 curves as drawn, each curve landing on another end for end, about the path's own axis \
103 (a section drawn off the path turns about the path, not about its centroid): {reading}. \
104 The nearest twists that close are {:.6}° and {:.6}°",
105 requested.to_degrees(),
106 below.to_degrees(),
107 above.to_degrees(),
108 ))
109}
110
111fn section_symmetry(
123 section: &[NurbsCurve],
124 axis_point: Vec3,
125 axis: Vec3,
126 floor: f64,
127) -> Result<Vec<usize>, String> {
128 use std::f64::consts::TAU;
129 let count = section.len();
130 let axis = axis
131 .normalized()
132 .map_err(|_| "sweepSolid: the path tangent is degenerate at its start".to_string())?;
133 let radial = |point: Vec3| {
134 let offset = point.sub(axis_point);
135 offset.sub(axis.scale(offset.dot(axis)))
136 };
137 let mut reach = 0.0_f64;
139 let mut farthest = (0, 0);
140 for (curve_index, curve) in section.iter().enumerate() {
141 for (control_index, control) in curve.control_points.iter().enumerate() {
142 let distance = radial(control.point()?).length();
143 if distance > reach {
144 reach = distance;
145 farthest = (curve_index, control_index);
146 }
147 }
148 }
149 let mut found: Vec<(usize, f64)> = vec![(0, 0.0)];
150 if reach <= floor {
151 return Ok(vec![0]);
152 }
153 let rotate = |point: Vec3, angle: f64| {
154 let offset = point.sub(axis_point);
155 let (sin, cos) = angle.sin_cos();
156 axis_point.add(
157 offset
158 .scale(cos)
159 .add(axis.cross(offset).scale(sin))
160 .add(axis.scale(axis.dot(offset) * (1.0 - cos))),
161 )
162 };
163 'shift: for shift in 1..count {
164 for index in 0..count {
165 let (a, b) = (§ion[index], §ion[(index + shift) % count]);
166 if a.degree != b.degree
167 || a.control_points.len() != b.control_points.len()
168 || a.knots.len() != b.knots.len()
169 || a.knots
170 .iter()
171 .zip(&b.knots)
172 .any(|(x, y)| (x - y).abs() > 1e-12 * (1.0 + x.abs()))
173 || a.control_points
174 .iter()
175 .zip(&b.control_points)
176 .any(|(x, y)| (x.w - y.w).abs() > 1e-12 * x.w.abs().max(1.0))
177 {
178 continue 'shift;
179 }
180 }
181 let from = radial(section[farthest.0].control_points[farthest.1].point()?);
182 let to = radial(section[(farthest.0 + shift) % count].control_points[farthest.1].point()?);
183 let angle = axis.dot(from.cross(to)).atan2(from.dot(to)).rem_euclid(TAU);
184 for index in 0..count {
185 let (a, b) = (§ion[index], §ion[(index + shift) % count]);
186 for (x, y) in a.control_points.iter().zip(&b.control_points) {
187 if rotate(x.point()?, angle).sub(y.point()?).length() > floor {
188 continue 'shift;
189 }
190 }
191 }
192 found.push((shift, angle));
193 }
194 let order = found.len();
197 let mut shifts = vec![None; order];
198 for &(shift, angle) in &found {
199 let step = (angle * order as f64 / TAU).round();
200 let miss = (angle - TAU * step / order as f64).abs() * reach;
201 let index = (step as i64).rem_euclid(order as i64) as usize;
202 if miss > floor || shifts[index].is_some() {
203 return Ok(vec![0]);
204 }
205 shifts[index] = Some(shift);
206 }
207 Ok(shifts.into_iter().map(|shift| shift.unwrap_or(0)).collect())
208}