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, KernelRefusal> {
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(KernelRefusal::input(
60 KernelStage::Collect,
61 "twist_finite",
62 "sweepSolid: twist angle must be finite",
63 ));
64 }
65 let symmetry = section_symmetry(section, axis_point, axis, floor)?;
66 let order = symmetry.len();
67 let step = TAU / order as f64;
68 let turns = (requested / step).round();
69 let common = {
74 let (mut a, mut b) = ((turns as i64).unsigned_abs(), order as u64);
75 while b != 0 {
76 (a, b) = (b, a % b);
77 }
78 a.max(1) as f64
79 };
80 let snapped = TAU * (turns / common) / (order as f64 / common);
81 if (requested - snapped).abs() <= 64.0 * f64::EPSILON * requested.abs().max(1.0) {
82 let index = (turns as i64).rem_euclid(order as i64) as usize;
83 return Ok(ClosingTwist {
84 twist: snapped,
85 shift: symmetry[index],
86 });
87 }
88 let below = TAU * (requested / step).floor() / order as f64;
89 let above = TAU * (requested / step).ceil() / order as f64;
90 let reading = if order == 1 {
91 "this section has NO rotational symmetry about the path, so only a whole number of \
92 turns carries it onto itself"
93 .to_string()
94 } else {
95 format!(
96 "this section maps onto itself under {order} rotations about the path, so a twist \
97 closes only in steps of {:.6}°",
98 step.to_degrees()
99 )
100 };
101 Err(KernelRefusal::input(
102 KernelStage::Classify,
103 "twist_closure",
104 format!(
105 "{SWEEP_TWIST_CLOSURE_REFUSAL}: the section comes back from one lap rolled about the path \
106 by the requested {:.6}° (the frame's own roll, its holonomy, is closed separately by the \
107 counter-twist), and a roll that does not carry the section onto itself leaves the last \
108 wall meeting the first one turned. The symmetry is read off the section's boundary \
109 curves as drawn, each curve landing on another end for end, about the path's own axis \
110 (a section drawn off the path turns about the path, not about its centroid): {reading}. \
111 The nearest twists that close are {:.6}° and {:.6}°",
112 requested.to_degrees(),
113 below.to_degrees(),
114 above.to_degrees(),
115 ),
116 ))
117}
118
119fn section_symmetry(
131 section: &[NurbsCurve],
132 axis_point: Vec3,
133 axis: Vec3,
134 floor: f64,
135) -> Result<Vec<usize>, KernelRefusal> {
136 use std::f64::consts::TAU;
137 let count = section.len();
138 let axis = axis.normalized().map_err(|_| {
139 KernelRefusal::internal(
140 KernelStage::Classify,
141 "symmetry_axis",
142 "sweepSolid: the path tangent is degenerate at its start",
143 )
144 })?;
145 let radial = |point: Vec3| {
146 let offset = point.sub(axis_point);
147 offset.sub(axis.scale(offset.dot(axis)))
148 };
149 let mut reach = 0.0_f64;
151 let mut farthest = (0, 0);
152 for (curve_index, curve) in section.iter().enumerate() {
153 for (control_index, control) in curve.control_points.iter().enumerate() {
154 let distance = radial(control.point().or_refuse(KernelStage::Classify, "control_point")?).length();
155 if distance > reach {
156 reach = distance;
157 farthest = (curve_index, control_index);
158 }
159 }
160 }
161 let mut found: Vec<(usize, f64)> = vec![(0, 0.0)];
162 if reach <= floor {
163 return Ok(vec![0]);
164 }
165 let rotate = |point: Vec3, angle: f64| {
166 axis_point.add(point.sub(axis_point).rotate_about(axis, angle))
167 };
168 'shift: for shift in 1..count {
169 for index in 0..count {
170 let (a, b) = (§ion[index], §ion[(index + shift) % count]);
171 if a.degree != b.degree
172 || a.control_points.len() != b.control_points.len()
173 || a.knots.len() != b.knots.len()
174 || a.knots
175 .iter()
176 .zip(&b.knots)
177 .any(|(x, y)| (x - y).abs() > 1e-12 * (1.0 + x.abs()))
178 || a.control_points
179 .iter()
180 .zip(&b.control_points)
181 .any(|(x, y)| (x.w - y.w).abs() > 1e-12 * x.w.abs().max(1.0))
182 {
183 continue 'shift;
184 }
185 }
186 let from = radial(section[farthest.0].control_points[farthest.1].point().or_refuse(KernelStage::Classify, "control_point")?);
187 let to = radial(section[(farthest.0 + shift) % count].control_points[farthest.1].point().or_refuse(KernelStage::Classify, "control_point")?);
188 let angle = axis.dot(from.cross(to)).atan2(from.dot(to)).rem_euclid(TAU);
189 for index in 0..count {
190 let (a, b) = (§ion[index], §ion[(index + shift) % count]);
191 for (x, y) in a.control_points.iter().zip(&b.control_points) {
192 if rotate(x.point().or_refuse(KernelStage::Classify, "control_point")?, angle).sub(y.point().or_refuse(KernelStage::Classify, "control_point")?).length() > floor {
193 continue 'shift;
194 }
195 }
196 }
197 found.push((shift, angle));
198 }
199 let order = found.len();
202 let mut shifts = vec![None; order];
203 for &(shift, angle) in &found {
204 let step = (angle * order as f64 / TAU).round();
205 let miss = (angle - TAU * step / order as f64).abs() * reach;
206 let index = (step as i64).rem_euclid(order as i64) as usize;
207 if miss > floor || shifts[index].is_some() {
208 return Ok(vec![0]);
209 }
210 shifts[index] = Some(shift);
211 }
212 Ok(shifts.into_iter().map(|shift| shift.unwrap_or(0)).collect())
213}