1use geop_core_geometry::nurb_curve::{NurbCurve2D, loop_signed_area};
16use geop_core_math::{
17 geop_error::{GeopError, GeopResult},
18 scalars::Scalar,
19 vector::Vector2,
20};
21use geop_ops_model::Profile;
22
23const FIT_TOLERANCE: f64 = 1e-6;
26
27const MOST_FIT_POINTS: usize = 1024;
29
30#[derive(Clone, Debug)]
33pub struct Piece {
34 pub curve: NurbCurve2D,
35 pub center: Option<Vector2>,
38 pub name: String,
39}
40
41#[derive(Clone, Debug)]
46pub struct Chain {
47 pub pieces: Vec<Piece>,
48 pub joints: Vec<String>,
49 pub smooth: Vec<bool>,
54 pub closed: bool,
55}
56
57impl Piece {
58 fn start(&self) -> GeopResult<Vector2> {
59 self.curve.start()
60 }
61
62 fn end(&self) -> GeopResult<Vector2> {
63 self.curve.end()
64 }
65
66 fn directions(&self) -> GeopResult<(Vector2, Vector2)> {
68 let (t0, t1) = self.curve.domain();
69 Ok((
70 self.curve.tangent(t0)?.normalize()?,
71 self.curve.tangent(t1)?.normalize()?,
72 ))
73 }
74
75 fn offset(&self, distance: Scalar) -> GeopResult<NurbCurve2D> {
78 let mut curve = self.curve.clone();
79 let (start, _) = self.directions()?;
80 match &self.center {
81 None if !self.is_straight() => return self.fitted_offset(distance),
82 None => {
83 let shift = start.perp().prod_scalar(distance);
84 for cp in &mut curve.control_points {
85 cp[0] = cp[0].add(shift[0].mul(cp[2]));
86 cp[1] = cp[1].add(shift[1].mul(cp[2]));
87 }
88 }
89 Some(center) => {
90 let from = self.start()?;
91 let radius = from.sub(center).norm();
92 let turning_left = start
95 .prod_cross(¢er.sub(&from))
96 .definitely_greater(Scalar::ZERO);
97 let new_radius = if turning_left {
98 radius.sub(distance)
99 } else {
100 radius.add(distance)
101 };
102 if !new_radius.definitely_greater(Scalar::ZERO) {
103 return Err(GeopError::new(format!(
104 "{} curves too tightly: its radius {:?} leaves no room for an offset of {:?}",
105 self.name,
106 radius.to_f64(),
107 distance.to_f64()
108 )));
109 }
110 let scale = new_radius.div(radius)?;
111 for cp in &mut curve.control_points {
112 for k in 0..2 {
113 let c = center[k].mul(cp[2]);
114 cp[k] = c.add(cp[k].sub(c).mul(scale));
115 }
116 }
117 }
118 }
119 curve.recompute_aabb();
120 Ok(curve)
121 }
122}
123
124impl Piece {
125 pub(crate) fn is_straight(&self) -> bool {
127 self.curve.degree == 1 && self.curve.control_points.len() == 2
128 }
129
130 fn fitted_offset(&self, distance: Scalar) -> GeopResult<NurbCurve2D> {
135 let ctx = |e: GeopError| {
136 e.with_context(format!(
137 "the offset of {} by {:?}",
138 self.name,
139 distance.to_f64()
140 ))
141 };
142 let curve = &self.curve;
143 let (t0, t1) = curve.domain();
144 let tolerance = distance.abs().mul(Scalar::from_f64(FIT_TOLERANCE));
145 let breaks = curve.breakpoints();
146 let spans = breaks.len() - 1;
147 let mut per_span = 4;
148 let mut last: Option<(Scalar, NurbCurve2D)> = None;
149 loop {
150 let mut ts = vec![t0];
152 for w in breaks.windows(2) {
153 for k in 1..=per_span {
154 let f = Scalar::from_ratio(k as i64, per_span as i64)?;
155 ts.push(w[0].add(w[1].sub(w[0]).mul(f)).sharpen());
156 }
157 }
158 *ts.last_mut().expect("a parameter") = t1;
159 let span = t1.sub(t0);
160 let mut params = Vec::with_capacity(ts.len());
161 let mut values = Vec::with_capacity(ts.len());
162 for (k, &t) in ts.iter().enumerate() {
163 params.push(match k {
164 0 => Scalar::ZERO,
165 k if k + 1 == ts.len() => Scalar::ONE,
166 _ => t.sub(t0).div(span).map_err(ctx)?.sharpen(),
167 });
168 let normal = curve
169 .tangent(t)
170 .map_err(ctx)?
171 .perp()
172 .normalize()
173 .map_err(ctx)?;
174 let p = curve
177 .evaluate(t)?
178 .add(&normal.prod_scalar(distance))
179 .sharpen();
180 values.push(p.homogeneous(Scalar::ONE));
181 }
182 let fit = NurbCurve2D::interpolate_homogeneous(&values, ¶ms, 3).map_err(ctx)?;
183 let miss = curve
184 .offset_miss(distance, (t0, t1), &fit, tolerance)
185 .map_err(ctx)?;
186 if !miss.definitely_greater(tolerance) {
187 return Ok(fit);
188 }
189 if let Some((before, coarser)) = last
192 && !before.definitely_greater(miss.mul(Scalar::TWO))
193 {
194 return Ok(coarser);
195 }
196 if 2 * per_span * spans > MOST_FIT_POINTS {
197 return Err(ctx(GeopError::new(format!(
198 "a cubic through {} points of it is still {miss:?} off",
199 per_span * spans + 1
200 ))));
201 }
202 per_span *= 2;
203 last = Some((miss, fit));
204 }
205 }
206}
207
208fn with_ends(curve: &NurbCurve2D, start: Vector2, end: Vector2) -> GeopResult<NurbCurve2D> {
211 if curve.degree == 1 && curve.control_points.len() == 2 {
212 return NurbCurve2D::line(start, end);
213 }
214 let mut curve = curve.clone();
215 let last = curve.control_points.len() - 1;
216 for (i, p) in [(0, start), (last, end)] {
217 curve.control_points[i] = p.homogeneous(curve.control_points[i][2]);
218 }
219 curve.recompute_aabb();
220 Ok(curve)
221}
222
223impl Chain {
224 pub fn offset(&self, distance: Scalar) -> GeopResult<Vec<NurbCurve2D>> {
227 let n = self.pieces.len();
228 let raw = self
229 .pieces
230 .iter()
231 .map(|p| p.offset(distance))
232 .collect::<GeopResult<Vec<_>>>()?;
233 let mut starts = Vec::with_capacity(n + 1);
236 for i in 0..n {
237 starts.push(if i == 0 && !self.closed {
238 raw[0].start()?
239 } else {
240 self.joint(i, distance, &raw)?
241 });
242 }
243 starts.push(if self.closed {
244 starts[0]
245 } else {
246 raw[n - 1].end()?
247 });
248 (0..n)
249 .map(|i| {
250 let curve = with_ends(&raw[i], starts[i], starts[i + 1])?;
251 if self.pieces[i].is_straight() {
253 let (old, new) = (
254 self.pieces[i].end()?.sub(&self.pieces[i].start()?),
255 starts[i + 1].sub(&starts[i]),
256 );
257 if !old.prod_dot(&new).definitely_greater(Scalar::ZERO) {
258 return Err(GeopError::new(format!(
259 "{} is too short for an offset of {:?}: it would turn around",
260 self.pieces[i].name,
261 distance.to_f64()
262 )));
263 }
264 }
265 Ok(curve)
266 })
267 .collect()
268 }
269
270 fn joint(&self, i: usize, distance: Scalar, raw: &[NurbCurve2D]) -> GeopResult<Vector2> {
272 let n = self.pieces.len();
273 let before = (i + n - 1) % n;
274 let (_, arriving) = self.pieces[before].directions()?;
275 let (leaving, _) = self.pieces[i].directions()?;
276 let at = self.pieces[i].start()?;
277 if self.smooth[i]
278 || (arriving.prod_cross(&leaving).could_be_equal(Scalar::ZERO)
279 && arriving.prod_dot(&leaving).definitely_greater(Scalar::ZERO))
280 {
281 let along = if self.pieces[before].is_straight() && !self.pieces[i].is_straight() {
283 arriving
284 } else {
285 leaving
286 };
287 return Ok(at.add(&along.perp().prod_scalar(distance)));
288 }
289 if !(self.pieces[before].is_straight() && self.pieces[i].is_straight()) {
290 return Err(GeopError::new(format!(
291 "{} and {} meet at a corner, at {}: an offset round a corner is only supported between straight pieces",
292 self.pieces[before].name, self.pieces[i].name, self.joints[i]
293 )));
294 }
295 let a = raw[before].end()?;
297 let b = raw[i].start()?;
298 let denominator = arriving.prod_cross(&leaving);
299 let s = b.sub(&a).prod_cross(&leaving).div(denominator)?;
300 Ok(a.add(&arriving.prod_scalar(s)))
301 }
302
303 pub fn band(&self, near: Scalar, far: Scalar) -> GeopResult<Vec<Profile>> {
309 let at = |distance: Scalar| -> GeopResult<Vec<NurbCurve2D>> {
310 if distance.could_be_equal(Scalar::ZERO) && distance.is_sharp() {
311 Ok(self.pieces.iter().map(|p| p.curve.clone()).collect())
313 } else {
314 self.offset(distance)
315 }
316 };
317 let (near_curves, far_curves) = (at(near)?, at(far)?);
318 let names: Vec<String> = self.pieces.iter().map(|p| p.name.clone()).collect();
319 let far_name = |name: &str| format!("{name},far");
320 let near_side = Profile {
321 name: String::new(),
322 curves: near_curves,
323 curve_names: names.clone(),
324 joint_names: self.joints.clone(),
325 };
326 let far_side = Profile {
327 name: String::new(),
328 curves: far_curves,
329 curve_names: names.iter().map(|n| far_name(n)).collect(),
330 joint_names: self.joints.iter().map(|n| far_name(n)).collect(),
331 };
332 if !self.closed {
333 let near_end = near_side.curves.last().unwrap().end()?;
336 let near_start = near_side.curves[0].start()?;
337 let far = far_side.reversed();
338 let far_start = far.curves[0].start()?;
339 let far_end = far.curves.last().unwrap().end()?;
340 let mut curves = near_side.curves;
341 curves.push(NurbCurve2D::line(near_end, far_start)?);
342 curves.extend(far.curves);
343 curves.push(NurbCurve2D::line(far_end, near_start)?);
344 let mut curve_names = near_side.curve_names;
345 curve_names.push("last".into());
346 curve_names.extend(far.curve_names);
347 curve_names.push("first".into());
348 let mut joint_names = near_side.joint_names;
349 joint_names.extend(far.joint_names);
350 return Ok(vec![Profile {
351 name: String::new(),
352 curves,
353 curve_names,
354 joint_names,
355 }]);
356 }
357 if self.winds_counter_clockwise()? {
360 Ok(vec![near_side, far_side.reversed()])
361 } else {
362 Ok(vec![far_side.reversed(), near_side])
363 }
364 }
365
366 fn winds_counter_clockwise(&self) -> GeopResult<bool> {
369 let area = loop_signed_area(self.pieces.iter().map(|p| &p.curve))?;
370 if area.definitely_greater(Scalar::ZERO) {
371 Ok(true)
372 } else if area.definitely_less(Scalar::ZERO) {
373 Ok(false)
374 } else {
375 Err(GeopError::new("cannot tell which way the chain winds"))
376 }
377 }
378}
379
380#[cfg(test)]
381mod tests;