1use std::rc::Rc;
62
63use geop_core_geometry::nurb_curve::NurbCurve2D;
64use geop_core_math::solvers::system::RELATIVE_TOLERANCE;
65use geop_core_math::{
66 geop_error::{GeopError, GeopResult},
67 scalars::Scalar,
68 vector::Vector2,
69};
70use serde::{Deserialize, Serialize};
71
72use crate::{
73 plain::{
74 self, P2, Plain, add, angle_between, bounds, cross, distance, dot, perp, scale,
75 segments_cross, sub, unit, wrap, xy,
76 },
77 profile::{Chain, ProfileEdge, basis_at, spline_nurbs},
78 sketch::{Constraint, ConstraintId, CurveId, CurveKind, PointId, Sketch},
79};
80
81const FIT_TOLERANCE: f64 = 1e-4;
85
86const MOST_FIT_POINTS: usize = 256;
88
89const SAMPLES: usize = 256;
93
94#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "snake_case")]
97pub enum Corners {
98 #[default]
100 Round,
101 Extend,
103}
104
105#[derive(Clone, Debug, PartialEq)]
108pub struct Offsetted {
109 pub curves: Vec<CurveId>,
110 pub distance: ConstraintId,
111}
112
113#[derive(Clone, Copy, Debug)]
116enum Of {
117 Piece(usize),
118 Round(usize),
119}
120
121#[derive(Clone, Debug)]
125struct Sub {
126 elem: usize,
127 shape: Shape,
128 from_start: bool,
129 to_end: bool,
130}
131
132#[derive(Debug)]
136struct SplineOffset {
137 spline: NurbCurve2D,
138 reversed: bool,
139 distance: f64,
140 samples: Vec<P2>,
142}
143
144impl SplineOffset {
145 fn new(spline: NurbCurve2D, reversed: bool, distance: f64) -> GeopResult<Self> {
146 let mut offset = SplineOffset {
147 spline,
148 reversed,
149 distance,
150 samples: Vec::new(),
151 };
152 offset.samples = (0..=SAMPLES)
153 .map(|k| offset.at(k as f64 / SAMPLES as f64))
154 .collect::<GeopResult<_>>()?;
155 Ok(offset)
156 }
157
158 fn t(&self, w: f64) -> f64 {
160 if self.reversed { 1.0 - w } else { w }
161 }
162
163 fn tangent(&self, w: f64) -> GeopResult<P2> {
165 let d = plain::of(&self.spline.tangent(Scalar::from_f64(self.t(w)))?);
166 let d = if self.reversed { scale(d, -1.0) } else { d };
167 unit(d).ok_or_else(|| GeopError::new("a spline with no tangent"))
168 }
169
170 fn at(&self, w: f64) -> GeopResult<P2> {
171 let p = plain::of(&self.spline.evaluate(Scalar::from_f64(self.t(w)))?);
172 Ok(add(p, scale(perp(self.tangent(w)?), self.distance)))
173 }
174
175 fn nearest(&self, q: P2, lo: f64, hi: f64) -> f64 {
178 let n = SAMPLES as f64;
179 let mut best = (f64::INFINITY, lo);
180 for k in 0..SAMPLES {
181 let (a, b) = (self.samples[k], self.samples[k + 1]);
182 let ab = sub(b, a);
183 let f = (dot(sub(q, a), ab) / dot(ab, ab).max(f64::MIN_POSITIVE)).clamp(0.0, 1.0);
184 let w = ((k as f64 + f) / n).clamp(lo, hi);
185 let d = distance(q, add(a, scale(ab, f)));
186 if d < best.0 {
187 best = (d, w);
188 }
189 }
190 let mut w = best.1;
191 let h = 1e-7;
192 for _ in 0..16 {
193 let (Ok(p), Ok(a), Ok(b)) = (self.at(w), self.at(w - h), self.at(w + h)) else {
194 break;
195 };
196 let v = scale(sub(b, a), 0.5 / h);
197 let step = dot(sub(q, p), v) / dot(v, v).max(f64::MIN_POSITIVE);
198 let next = (w + step).clamp(lo, hi);
199 if next == w {
200 break;
201 }
202 w = next;
203 }
204 w
205 }
206}
207
208#[derive(Clone, Debug)]
212enum Shape {
213 Plain(Plain),
214 Spline {
215 of: Rc<SplineOffset>,
216 from: f64,
217 to: f64,
218 },
219}
220
221impl Shape {
222 fn w(from: f64, to: f64, u: f64) -> f64 {
223 from + u * (to - from)
224 }
225
226 fn at(&self, u: f64) -> P2 {
228 match self {
229 Shape::Plain(p) => p.at(u),
230 Shape::Spline { of, from, to } => of
231 .at(Self::w(*from, *to, u))
232 .unwrap_or_else(|_| of.samples[0]),
233 }
234 }
235
236 fn tangent(&self, u: f64) -> P2 {
237 match self {
238 Shape::Plain(p) => p.tangent(u),
239 Shape::Spline { of, from, to } => {
240 let t = of.tangent(Self::w(*from, *to, u)).unwrap_or([1.0, 0.0]);
241 if to < from { scale(t, -1.0) } else { t }
242 }
243 }
244 }
245
246 fn param(&self, q: P2) -> f64 {
248 match self {
249 Shape::Plain(p) => p.param(q),
250 Shape::Spline { of, from, to } => (of.nearest(q, 0.0, 1.0) - from) / (to - from),
251 }
252 }
253
254 fn nearest(&self, q: P2) -> f64 {
256 match self {
257 Shape::Plain(p) => p.nearest(q),
258 Shape::Spline { of, from, to } => {
259 (of.nearest(q, from.min(*to), from.max(*to)) - from) / (to - from)
260 }
261 }
262 }
263
264 fn part(&self, u0: f64, u1: f64) -> Shape {
265 match self {
266 Shape::Plain(p) => Shape::Plain(p.part(u0, u1)),
267 Shape::Spline { of, from, to } => Shape::Spline {
268 of: of.clone(),
269 from: Self::w(*from, *to, u0),
270 to: Self::w(*from, *to, u1),
271 },
272 }
273 }
274
275 fn full(&self) -> Shape {
277 match self {
278 Shape::Plain(p) => Shape::Plain(p.full()),
279 Shape::Spline { of, .. } => Shape::Spline {
280 of: of.clone(),
281 from: 0.0,
282 to: 1.0,
283 },
284 }
285 }
286
287 fn polyline(&self) -> Vec<P2> {
290 match self {
291 Shape::Plain(Plain::Line {
292 a,
293 b,
294 infinite: true,
295 }) => {
296 let far = scale(sub(*b, *a), 1e6);
297 vec![sub(*a, far), add(*b, far)]
298 }
299 Shape::Plain(p) => p.polyline(0.0, 1.0),
300 Shape::Spline { of, from, to } => {
301 let (lo, hi) = (from.min(*to), from.max(*to));
302 let n = SAMPLES as f64;
303 let mut ws = vec![lo];
304 ws.extend(
305 ((lo * n).floor() as usize + 1..=(hi * n).ceil() as usize)
306 .map(|k| k as f64 / n)
307 .filter(|&w| w > lo && w < hi),
308 );
309 ws.push(hi);
310 ws.into_iter()
311 .map(|w| of.at(w).unwrap_or(of.samples[0]))
312 .collect()
313 }
314 }
315 }
316
317 fn crossings(&self, other: &Shape) -> Vec<P2> {
320 let (Shape::Plain(a), Shape::Plain(b)) = (self, other) else {
321 let (p, q) = (self.polyline(), other.polyline());
322 let mut found = Vec::new();
323 for s in p.windows(2) {
324 for t in q.windows(2) {
325 if let Some(x) = segments_cross(s[0], s[1], t[0], t[1]) {
326 found.push(refined(self, other, x));
327 }
328 }
329 }
330 return found;
331 };
332 a.crossings(b, false)
333 }
334}
335
336fn refined(a: &Shape, b: &Shape, x: P2) -> P2 {
339 let (mut u, mut v) = (a.param(x), b.param(x));
340 let h = 1e-7;
341 for _ in 0..16 {
342 let (p, q) = (a.at(u), b.at(v));
343 let da = scale(sub(a.at(u + h), a.at(u - h)), 0.5 / h);
344 let db = scale(sub(b.at(v + h), b.at(v - h)), 0.5 / h);
345 let r = sub(q, p);
347 let det = cross(da, db);
348 if det == 0.0 {
349 break;
350 }
351 let (du, dv) = (cross(r, db) / det, cross(r, da) / det);
352 if du == 0.0 && dv == 0.0 {
353 break;
354 }
355 (u, v) = (u + du, v + dv);
356 }
357 a.at(u)
358}
359
360fn oriented(sketch: &Sketch, curve: CurveId, reversed: bool, d: f64) -> GeopResult<Shape> {
363 let plain = match &sketch.curve(curve)?.kind {
364 CurveKind::Line { start, end } => {
365 let (a, b) = (xy(sketch, *start), xy(sketch, *end));
366 let (a, b) = if reversed { (b, a) } else { (a, b) };
367 Plain::Line {
368 a,
369 b,
370 infinite: false,
371 }
372 }
373 CurveKind::Arc { start, end, sweep } => {
374 let (s, e, sweep) = (xy(sketch, *start), xy(sketch, *end), sweep.to_f64());
375 let arc = if reversed {
376 Plain::arc(e, s, -sweep)
377 } else {
378 Plain::arc(s, e, sweep)
379 };
380 arc.ok_or_else(|| {
381 GeopError::new(format!("arc {curve} is too straight to have a center"))
382 })?
383 }
384 CurveKind::Circle { center, radius } => Plain::Round {
385 center: xy(sketch, *center),
386 radius: radius.to_f64(),
387 span: None,
388 },
389 CurveKind::Spline {
390 control_points,
391 shape,
392 } => {
393 if shape.as_ref().is_some_and(|s| s.degree > 3) {
394 return Err(GeopError::new(format!(
395 "spline {curve} is of degree {}: only splines of degree three or less can be offset",
396 shape.as_ref().map_or(0, |s| s.degree)
397 )));
398 }
399 let points: Vec<Vector2> = control_points
400 .iter()
401 .map(|p| sketch.points[p].xy())
402 .collect();
403 let spline = spline_nurbs(&points, shape.as_ref())?;
404 let of = SplineOffset::new(spline, reversed, d)?;
405 for k in 0..SAMPLES {
407 let w = (k as f64 + 0.5) / SAMPLES as f64;
408 let along = sub(of.samples[k + 1], of.samples[k]);
409 if dot(along, of.tangent(w)?) <= 0.0 {
410 return Err(GeopError::new(format!(
411 "spline {curve} cannot be offset by {d}: it bends tighter than that, and its offset would turn back on itself"
412 )));
413 }
414 }
415 return Ok(Shape::Spline {
416 of: Rc::new(of),
417 from: 0.0,
418 to: 1.0,
419 });
420 }
421 CurveKind::Text { .. } => {
422 return Err(GeopError::new(format!(
423 "text {curve} cannot be offset: its outlines are the font's, set again where it is placed"
424 )));
425 }
426 };
427 if d == 0.0 {
428 return Ok(Shape::Plain(plain));
429 }
430 offset_plain(plain, d).map(Shape::Plain).ok_or_else(|| {
431 GeopError::new(format!(
432 "{curve} cannot be offset by {d} towards its center: it is smaller than that"
433 ))
434 })
435}
436
437fn offset_plain(piece: Plain, d: f64) -> Option<Plain> {
440 Some(match piece {
441 Plain::Line { a, b, infinite } => {
442 let n = scale(perp(unit(sub(b, a))?), d);
443 Plain::Line {
444 a: add(a, n),
445 b: add(b, n),
446 infinite,
447 }
448 }
449 Plain::Round {
450 center,
451 radius,
452 span,
453 } => {
454 let turn = span.map_or(1.0, |(_, sweep)| sweep.signum());
456 let radius = radius - turn * d;
457 if radius <= 0.0 {
458 return None;
459 }
460 Plain::Round {
461 center,
462 radius,
463 span,
464 }
465 }
466 })
467}
468
469fn unwrapped(plain: &Plain, q: P2, from_start: bool) -> f64 {
473 match *plain {
474 Plain::Line { .. } => plain.param(q),
475 Plain::Round {
476 center,
477 span: Some((start, sweep)),
478 ..
479 } => {
480 let r = sub(q, center);
481 let at = r[1].atan2(r[0]);
482 let base = if from_start { start } else { start + sweep };
483 let off = wrap(at - base) * sweep.signum() / sweep.abs();
484 if from_start { off } else { 1.0 + off }
485 }
486 Plain::Round { span: None, .. } => 0.0,
487 }
488}
489
490type Fit = (Vec<P2>, Vec<(f64, f64)>);
493
494fn fitted(
500 spline: &NurbCurve2D,
501 d: f64,
502 (t0, t1): (f64, f64),
503 tolerance: f64,
504 first: usize,
505) -> GeopResult<Fit> {
506 let mut m = first.max(4);
507 loop {
508 let knots = cubic_knots(m)?;
509 let breaks: Vec<f64> = knots.iter().map(|k| k.to_f64()).collect();
510 let spline_breaks: Vec<f64> = spline.breakpoints().iter().map(|k| k.to_f64()).collect();
511 let mut at: Vec<(f64, f64)> = Vec::with_capacity(m);
515 for k in 0..m {
516 let mut s = (breaks[k + 1] + breaks[k + 2] + breaks[k + 3]) / 3.0;
517 if k > 0 && k + 1 < m {
518 let gap = 1.0 / (m - 3) as f64 / 8.0;
519 let on_knot = |s: f64| {
520 let t = t0 + s * (t1 - t0);
521 breaks.iter().any(|&b| (b - s).abs() < gap)
522 || spline_breaks
523 .iter()
524 .any(|&b| (b - t).abs() < gap * (t1 - t0).abs())
525 };
526 for nudge in [0.0, 1.0, -1.0, 2.0, -2.0] {
527 if !on_knot(s + nudge * gap) {
528 s += nudge * gap;
529 break;
530 }
531 }
532 }
533 at.push((s, t0 + s * (t1 - t0)));
534 }
535 let target = |t: f64| -> GeopResult<P2> {
536 let t = Scalar::from_f64(t);
537 let p = plain::of(&spline.evaluate(t)?);
538 let n = perp(
539 unit(plain::of(&spline.tangent(t)?))
540 .ok_or_else(|| GeopError::new("a spline with no tangent"))?,
541 );
542 Ok(add(p, scale(n, d)))
543 };
544 let mut rows: Vec<Vec<f64>> = Vec::with_capacity(m);
546 for &(s, _) in &at {
547 let mut row = vec![0.0; m];
548 for (j, value, _) in basis_at(m, None, Scalar::from_f64(s), false)? {
549 row[j] = value.to_f64();
550 }
551 rows.push(row);
552 }
553 let targets: Vec<P2> = at
554 .iter()
555 .map(|&(_, t)| target(t))
556 .collect::<GeopResult<_>>()?;
557 let control = solve_dense(rows, targets)?;
558 let fit = spline_nurbs(
559 &control
560 .iter()
561 .map(|p| Vector2::from_array(p.map(Scalar::from_f64)))
562 .collect::<Vec<_>>(),
563 None,
564 )?;
565 let miss = spline.offset_miss(
566 Scalar::from_f64(d),
567 (Scalar::from_f64(t0), Scalar::from_f64(t1)),
568 &fit,
569 Scalar::from_f64(tolerance),
570 )?;
571 if !miss.definitely_greater(Scalar::from_f64(tolerance)) {
572 return Ok((control, at));
573 }
574 if 2 * m > MOST_FIT_POINTS {
575 return Err(GeopError::new(format!(
576 "a spline's offset by {d} is not fitted within {tolerance} by {m} control points: it is {miss:?} off"
577 )));
578 }
579 m *= 2;
580 }
581}
582
583fn cubic_knots(m: usize) -> GeopResult<Vec<Scalar>> {
586 let spans = m - 3;
587 let mut knots = vec![Scalar::ZERO; 4];
588 for i in 1..spans {
589 knots.push(Scalar::from_ratio(i as i64, spans as i64)?);
590 }
591 knots.extend([Scalar::ONE; 4]);
592 Ok(knots)
593}
594
595fn solve_dense(mut rows: Vec<Vec<f64>>, mut targets: Vec<P2>) -> GeopResult<Vec<P2>> {
597 let m = rows.len();
598 for c in 0..m {
599 let pivot = (c..m)
600 .max_by(|&a, &b| rows[a][c].abs().total_cmp(&rows[b][c].abs()))
601 .expect("a row");
602 if rows[pivot][c] == 0.0 {
603 return Err(GeopError::new("the fit's collocation system is singular"));
604 }
605 rows.swap(c, pivot);
606 targets.swap(c, pivot);
607 for r in 0..m {
608 if r != c && rows[r][c] != 0.0 {
609 let f = rows[r][c] / rows[c][c];
610 let pivot_row = rows[c].clone();
611 for (x, p) in rows[r][c..].iter_mut().zip(&pivot_row[c..]) {
612 *x -= f * p;
613 }
614 targets[r] = sub(targets[r], scale(targets[c], f));
615 }
616 }
617 }
618 Ok((0..m)
619 .map(|c| scale(targets[c], 1.0 / rows[c][c]))
620 .collect())
621}
622
623fn sides(sketch: &Sketch, chains: &[Chain], d: f64) -> GeopResult<Vec<f64>> {
630 let mut polygons: Vec<Option<Vec<P2>>> = Vec::new();
632 for chain in chains {
633 if !chain.closed {
634 polygons.push(None);
635 continue;
636 }
637 let mut points = Vec::new();
638 for e in &chain.edges {
639 let piece = oriented(sketch, e.curve, e.reversed, 0.0)?;
640 for k in 0..32 {
641 points.push(piece.at(k as f64 / 32.0));
642 }
643 }
644 polygons.push(Some(points));
645 }
646 let area = |q: &[P2]| {
647 (0..q.len())
648 .map(|k| cross(q[k], q[(k + 1) % q.len()]))
649 .sum::<f64>()
650 };
651 let inside = |p: P2, q: &[P2]| {
652 let mut odd = false;
653 for k in 0..q.len() {
654 let (a, b) = (q[k], q[(k + 1) % q.len()]);
655 if (a[1] > p[1]) != (b[1] > p[1])
656 && p[0] < a[0] + (p[1] - a[1]) / (b[1] - a[1]) * (b[0] - a[0])
657 {
658 odd = !odd;
659 }
660 }
661 odd
662 };
663 let depth = |c: usize| -> usize {
665 let Some(q) = &polygons[c] else {
666 return 0;
667 };
668 (0..chains.len())
669 .filter(|&o| o != c)
670 .filter(|&o| polygons[o].as_ref().is_some_and(|r| inside(q[0], r)))
671 .count()
672 };
673 let (Some(first), base) = (&polygons[0], depth(0)) else {
674 return Ok(vec![d; chains.len()]);
675 };
676 let turn = area(first).signum();
677 Ok((0..chains.len())
678 .map(|c| match &polygons[c] {
679 Some(q) if c > 0 => {
680 let same = area(q).signum() == turn;
681 let nested = (depth(c) + base) % 2 == 1;
682 if same != nested { d } else { -d }
683 }
684 _ => d,
685 })
686 .collect())
687}
688
689impl Sketch {
690 pub fn chain_side(&self, chain: &Chain, p: P2) -> GeopResult<f64> {
692 let mut best: Option<(f64, f64)> = None;
693 for edge in &chain.edges {
694 let piece = oriented(self, edge.curve, edge.reversed, 0.0)?;
695 let t = piece.nearest(p);
696 let q = piece.at(t);
697 let d = distance(p, q);
698 if best.is_none_or(|(b, _)| d < b) {
699 let side = cross(piece.tangent(t), sub(p, q)).signum();
700 best = Some((d, side * d));
701 }
702 }
703 Ok(best.map_or(0.0, |(_, signed)| signed))
704 }
705
706 pub fn offset(
713 &mut self,
714 curves: &[CurveId],
715 distance: Scalar,
716 corners: Corners,
717 ) -> GeopResult<Offsetted> {
718 let chains = self.chains(curves)?;
719 let d = distance.to_f64();
720 if d == 0.0 || !d.is_finite() {
721 return Err(GeopError::new(format!("cannot offset by {d}")));
722 }
723 let value = distance.abs();
724
725 if let [chain] = &chains[..]
727 && let [edge] = &chain.edges[..]
728 && let CurveKind::Circle { center, radius } = self.curves[&edge.curve].kind
729 {
730 let c = edge.curve;
731 let r = radius.to_f64() - d;
732 if r <= 0.0 {
733 return Err(GeopError::new(format!(
734 "{c} cannot be offset by {d} towards its center: it is smaller than that"
735 )));
736 }
737 let circle = self.add_circle(center, Scalar::from_f64(r));
738 let distance = self.constrain(Constraint::Offset {
739 a: c,
740 b: circle,
741 value,
742 });
743 return Ok(Offsetted {
744 curves: vec![circle],
745 distance,
746 });
747 }
748 let towards = sides(self, &chains, d)?;
751 let chains: Vec<Chain> = (chains.into_iter().zip(&towards))
755 .map(|(chain, &side)| {
756 if side.signum() == d.signum() {
757 return chain;
758 }
759 Chain {
760 edges: (chain.edges.into_iter().rev())
761 .map(|e| ProfileEdge {
762 reversed: !e.reversed,
763 ..e
764 })
765 .collect(),
766 closed: chain.closed,
767 }
768 })
769 .collect();
770 let towards = vec![d; chains.len()];
771 let (rings, chains): (Vec<_>, Vec<_>) = chains.into_iter().zip(towards).partition(
773 |(chain, _)| {
774 matches!(&chain.edges[..], [edge] if matches!(self.curves[&edge.curve].kind, CurveKind::Circle { .. }))
775 },
776 );
777 let mut pieces_of: Vec<(CurveId, bool)> = Vec::new();
778 let mut along: Vec<f64> = Vec::new();
779 let mut chain_of: Vec<(usize, usize, bool)> = Vec::new();
780 for (chain, d) in &chains {
781 let start = pieces_of.len();
782 for e in &chain.edges {
783 pieces_of.push((e.curve, e.reversed));
784 along.push(*d);
785 chain_of.push((start, chain.edges.len(), chain.closed));
786 }
787 }
788 let n = pieces_of.len();
789 let next_piece = |i: usize| -> Option<usize> {
791 let (start, len, closed) = chain_of[i];
792 if i + 1 < start + len {
793 Some(i + 1)
794 } else {
795 closed.then_some(start)
796 }
797 };
798 let previous_piece = |i: usize| -> Option<usize> {
799 let (start, len, closed) = chain_of[i];
800 if i > start {
801 Some(i - 1)
802 } else {
803 closed.then_some(start + len - 1)
804 }
805 };
806 let size = {
809 let size = bounds(self.points.keys().map(|&p| xy(self, p)))
810 .map_or(0.0, |(lo, hi)| (hi[0] - lo[0]).hypot(hi[1] - lo[1]));
811 if size > 0.0 { size } else { 1.0 }
813 };
814 let resolution = size / RELATIVE_TOLERANCE as f64;
815 let pieces: Vec<Shape> = pieces_of
816 .iter()
817 .map(|&(c, r)| oriented(self, c, r, 0.0))
818 .collect::<GeopResult<_>>()?;
819 let offsets: Vec<Shape> = (pieces_of.iter().zip(&along))
820 .map(|(&(c, r), &d)| oriented(self, c, r, d))
821 .collect::<GeopResult<_>>()?;
822 let mut circles: Vec<(CurveId, Shape, Shape)> = Vec::new();
824 for (chain, d) in &rings {
825 let c = chain.edges[0].curve;
828 let d = if chain.edges[0].reversed { -*d } else { *d };
829 circles.push((
830 c,
831 oriented(self, c, false, 0.0)?,
832 oriented(self, c, false, d)?,
833 ));
834 }
835 let spline = |i: usize| matches!(pieces[i], Shape::Spline { .. });
836 enum Joint {
839 Shared { at: P2, by: Held },
841 Round { from: P2, to: P2, sweep: f64 },
843 }
844 #[derive(PartialEq)]
846 enum Held {
847 Tangent,
849 Circle,
851 Square,
854 }
855 let mut joint_of: Vec<Option<Joint>> = Vec::new();
856 for i in 0..n {
857 let Some(j) = next_piece(i) else {
858 joint_of.push(None);
859 continue;
860 };
861 let d = along[i];
862 let (ci, cj) = (pieces_of[i].0, pieces_of[j].0);
863 let p = pieces[i].at(1.0);
864 let (ti, tj) = (pieces[i].tangent(1.0), pieces[j].tangent(0.0));
865 let normal = perp(ti);
866 let smooth = self.tangent(ci, cj);
867 if smooth && dot(ti, tj) < 0.0 {
868 return Err(GeopError::new(format!(
869 "{ci} and {cj} turn back on each other where they meet: their offsets would cross"
870 )));
871 }
872 let turn = cross(ti, tj);
873 let (from, to) = (add(p, scale(normal, d)), add(p, scale(perp(tj), d)));
878 let straight = plain::distance(from, to) <= resolution && dot(ti, tj) > 0.0;
879 let lines = matches!(
880 (&offsets[i], &offsets[j]),
881 (
882 Shape::Plain(Plain::Line { .. }),
883 Shape::Plain(Plain::Line { .. })
884 )
885 );
886 joint_of.push(Some(if smooth {
887 Joint::Shared {
888 at: from,
889 by: Held::Tangent,
890 }
891 } else if straight {
892 Joint::Shared {
893 at: from,
894 by: Held::Square,
895 }
896 } else if turn * d < 0.0 && corners == Corners::Round {
897 Joint::Round {
898 from,
899 to,
900 sweep: angle_between(ti, tj),
901 }
902 } else if lines {
903 if dot(ti, tj) <= -1.0 {
906 return Err(GeopError::new(format!(
907 "{ci} and {cj} fold back on each other where they meet: their offsets would cross"
908 )));
909 }
910 Joint::Shared {
911 at: add(p, scale(add(normal, perp(tj)), d / (1.0 + dot(ti, tj)))),
912 by: Held::Circle,
913 }
914 } else {
915 let near = scale(add(from, to), 0.5);
918 let crossings = offsets[i].full().crossings(&offsets[j].full());
919 let Some(at) = crossings
920 .into_iter()
921 .min_by(|a, b| plain::distance(*a, near).total_cmp(&plain::distance(*b, near)))
922 else {
923 return Err(GeopError::new(format!(
924 "the offsets of {ci} and {cj} by {d} do not meet: round their corner instead"
925 )));
926 };
927 Joint::Shared {
928 at,
929 by: Held::Circle,
930 }
931 }));
932 }
933 let crossed = |k: usize| {
936 matches!(
937 joint_of[k],
938 Some(Joint::Shared {
939 by: Held::Circle,
940 ..
941 })
942 )
943 };
944 let start_crossed = |i: usize| previous_piece(i).is_some_and(crossed);
945 let end_crossed = crossed;
946
947 let start_of = |i: usize| -> P2 {
949 match previous_piece(i).and_then(|k| joint_of[k].as_ref()) {
950 None => offsets[i].at(0.0),
951 Some(Joint::Shared { at, .. }) => *at,
952 Some(Joint::Round { to, .. }) => *to,
953 }
954 };
955 let end_of = |i: usize| -> P2 {
956 match &joint_of[i] {
957 None => offsets[i].at(1.0),
958 Some(Joint::Shared { at, .. }) => *at,
959 Some(Joint::Round { from, .. }) => *from,
960 }
961 };
962 let mut path: Vec<(Shape, Of)> = Vec::new();
967 let mut path_next: Vec<Option<usize>> = Vec::new();
969 let mut chain_start = 0;
970 for (i, offset) in offsets.iter().enumerate() {
971 if chain_of[i].0 == i {
972 chain_start = path.len();
973 }
974 let (s, e) = (start_of(i), end_of(i));
975 let piece = match offset {
976 Shape::Plain(Plain::Line { .. }) => {
977 (plain::distance(s, e) > resolution).then_some(Shape::Plain(Plain::Line {
978 a: s,
979 b: e,
980 infinite: false,
981 }))
982 }
983 Shape::Plain(
984 plain @ Plain::Round {
985 span: Some((_, sweep)),
986 ..
987 },
988 ) => {
989 let (u, v) = (unwrapped(plain, s, true), unwrapped(plain, e, false));
990 Plain::arc(s, e, sweep * (v - u)).map(Shape::Plain)
991 }
992 Shape::Spline { of, .. } => {
993 let from = if start_crossed(i) {
995 of.nearest(s, 0.0, 1.0)
996 } else {
997 0.0
998 };
999 let to = if end_crossed(i) {
1000 of.nearest(e, 0.0, 1.0)
1001 } else {
1002 1.0
1003 };
1004 (plain::distance(s, e) > resolution).then(|| Shape::Spline {
1005 of: of.clone(),
1006 from,
1007 to,
1008 })
1009 }
1010 _ => None,
1011 };
1012 if let Some(piece) = piece {
1015 path.push((piece, Of::Piece(i)));
1016 }
1017 if let Some(Joint::Round { from, to, sweep }) = &joint_of[i]
1018 && let Some(arc) = Plain::arc(*from, *to, *sweep)
1019 {
1020 path.push((Shape::Plain(arc), Of::Round(i)));
1021 }
1022 let (start, len, closed) = chain_of[i];
1023 if i + 1 == start + len {
1024 for k in chain_start..path.len() {
1025 path_next.push(if k + 1 < path.len() {
1026 Some(k + 1)
1027 } else {
1028 (closed && path.len() > chain_start).then_some(chain_start)
1029 });
1030 }
1031 }
1032 }
1033 let mut path_previous: Vec<Option<usize>> = vec![None; path.len()];
1034 for (k, next) in path_next.iter().enumerate() {
1035 if let Some(next) = *next {
1036 path_previous[next] = Some(k);
1037 }
1038 }
1039 for (k, (c, _, ring)) in circles.iter().enumerate() {
1042 let crossed = path
1043 .iter()
1044 .any(|(shape, _)| !shape.crossings(ring).is_empty())
1045 || (circles.iter().enumerate())
1046 .any(|(j, (_, _, other))| j != k && !ring.crossings(other).is_empty());
1047 if crossed {
1048 return Err(GeopError::new(format!(
1049 "the offset of circle {c} by {d} would cross another offset: cutting a circle's offset is not supported"
1050 )));
1051 }
1052 }
1053
1054 let near_end = |shape: &Shape, q: P2| {
1058 plain::distance(shape.at(0.0), q) <= resolution
1059 || plain::distance(shape.at(1.0), q) <= resolution
1060 };
1061 let to_chain = |q: P2| {
1062 (pieces.iter().chain(circles.iter().map(|(_, c, _)| c)))
1063 .map(|p| plain::distance(q, p.at(p.nearest(q))))
1064 .fold(f64::INFINITY, f64::min)
1065 };
1066 let mut subs: Vec<Sub> = Vec::new();
1067 for (k, (shape, _)) in path.iter().enumerate() {
1068 let mut cuts: Vec<f64> = Vec::new();
1069 for (j, (other, _)) in path.iter().enumerate() {
1070 if j != k {
1071 cuts.extend(
1072 shape
1073 .crossings(other)
1074 .into_iter()
1075 .filter(|&q| !near_end(shape, q))
1076 .map(|q| shape.param(q))
1077 .filter(|u| (0.0..=1.0).contains(u)),
1078 );
1079 }
1080 }
1081 cuts.sort_by(f64::total_cmp);
1082 cuts.dedup_by(|a, b| plain::distance(shape.at(*a), shape.at(*b)) <= resolution);
1083 let ts: Vec<f64> = std::iter::once(0.0)
1084 .chain(cuts)
1085 .chain(std::iter::once(1.0))
1086 .collect();
1087 for w in ts.windows(2) {
1088 let part = shape.part(w[0], w[1]);
1089 if to_chain(part.at(0.5)) > d.abs() - resolution {
1090 subs.push(Sub {
1091 elem: k,
1092 shape: part,
1093 from_start: w[0] == 0.0,
1094 to_end: w[1] == 1.0,
1095 });
1096 }
1097 }
1098 }
1099 if subs.is_empty() && circles.is_empty() {
1100 return Err(GeopError::new(format!(
1101 "the offset by {d} leaves nothing: the chain is too short or narrow for it"
1102 )));
1103 }
1104
1105 let mut next: Vec<Option<usize>> = vec![None; subs.len()];
1109 for a in 0..subs.len() {
1110 let end = subs[a].shape.at(1.0);
1111 let open_end = subs[a].to_end && path_next[subs[a].elem].is_none();
1112 let found: Vec<usize> = (0..subs.len())
1113 .filter(|&b| {
1114 let joined = if subs[a].to_end {
1115 subs[b].from_start && Some(subs[b].elem) == path_next[subs[a].elem]
1116 } else {
1117 !subs[b].from_start
1118 };
1119 b != a && joined && plain::distance(subs[b].shape.at(0.0), end) <= resolution
1120 })
1121 .collect();
1122 match (found.as_slice(), open_end) {
1123 (&[b], false) => next[a] = Some(b),
1124 ([], true) => {}
1125 _ => {
1126 return Err(GeopError::new(format!(
1127 "the offset by {d}, trimmed where it crosses itself, does not run on from {end:?} as one curve"
1128 )));
1129 }
1130 }
1131 }
1132 let mut previous: Vec<Option<usize>> = vec![None; subs.len()];
1133 for (a, b) in next.iter().enumerate() {
1134 if let Some(b) = *b {
1135 if previous[b].is_some() {
1136 return Err(GeopError::new(format!(
1137 "the offset by {d}, trimmed where it crosses itself, runs into itself at {:?}",
1138 subs[b].shape.at(0.0)
1139 )));
1140 }
1141 previous[b] = Some(a);
1142 }
1143 }
1144 let joint_after = |a: usize| -> Option<usize> {
1147 if !subs[a].to_end {
1148 return None;
1149 }
1150 match path[subs[a].elem].1 {
1151 Of::Piece(i) | Of::Round(i) => Some(i),
1152 }
1153 };
1154 let of_spline = |a: usize| matches!(path[subs[a].elem].1, Of::Piece(i) if spline(i));
1156
1157 let mut start_point: Vec<Option<PointId>> = vec![None; subs.len()];
1160 let mut end_point: Vec<Option<PointId>> = vec![None; subs.len()];
1161 for a in 0..subs.len() {
1162 let p = self.new_point_at(subs[a].shape.at(1.0));
1163 end_point[a] = Some(p);
1164 if let Some(b) = next[a] {
1165 start_point[b] = Some(p);
1166 }
1167 }
1168 for a in 0..subs.len() {
1169 if start_point[a].is_none() {
1170 start_point[a] = Some(self.new_point_at(subs[a].shape.at(0.0)));
1171 }
1172 }
1173 let corner_of = |sketch: &Sketch, i: usize| -> PointId {
1174 let (source, reversed) = pieces_of[i];
1175 let (s, e) = sketch.curves[&source].endpoints().expect("an open curve");
1176 if reversed { s } else { e }
1177 };
1178 let first = (0..subs.len()).find(|&a| matches!(path[subs[a].elem].1, Of::Piece(_)));
1179 if first.is_none() && !subs.is_empty() {
1180 return Err(GeopError::new(format!(
1181 "the offset by {d} leaves only arcs round corners: the chain is too short for it"
1182 )));
1183 }
1184 let first_piece = first.map(|first| match path[subs[first].elem].1 {
1185 Of::Piece(i) | Of::Round(i) => i,
1186 });
1187 let mut reach: Option<CurveId> = None;
1191 if let Some(first_piece) = first_piece
1192 && (0..subs.len()).any(of_spline)
1193 {
1194 let circle = self.add_circle(corner_of(self, first_piece), value);
1195 self.set_construction(circle, true);
1196 reach = Some(circle);
1197 }
1198 let mut curve_of: Vec<CurveId> = Vec::with_capacity(subs.len());
1199 let mut made = Vec::new();
1200 let mut distance = None;
1201 for a in 0..subs.len() {
1202 let (s, e) = (start_point[a].unwrap(), end_point[a].unwrap());
1203 let curve = match path[subs[a].elem].1 {
1204 Of::Piece(i) => {
1205 let (source, reversed) = pieces_of[i];
1206 let (s, e) = if reversed { (e, s) } else { (s, e) };
1208 let curve = match (&self.curves[&source].kind, &subs[a].shape) {
1209 (CurveKind::Line { .. }, _) => self.add_line(s, e),
1210 (
1211 CurveKind::Arc { .. },
1212 Shape::Plain(Plain::Round {
1213 span: Some((_, sweep)),
1214 ..
1215 }),
1216 ) => self.add_arc(
1217 s,
1218 e,
1219 Scalar::from_f64(if reversed { -sweep } else { *sweep }),
1220 ),
1221 (CurveKind::Spline { .. }, Shape::Spline { of, from, to }) => {
1222 let (ta, tb) = if reversed {
1226 (of.t(*to), of.t(*from))
1227 } else {
1228 (of.t(*from), of.t(*to))
1229 };
1230 let ends_pinned = [
1231 subs[a].from_start && !start_crossed(i),
1232 subs[a].to_end && !end_crossed(i),
1233 ];
1234 let ends_pinned = if reversed {
1235 [ends_pinned[1], ends_pinned[0]]
1236 } else {
1237 ends_pinned
1238 };
1239 let dist = if reversed { -along[i] } else { along[i] };
1240 let tolerance = FIT_TOLERANCE * size;
1241 let (control, at) = fitted(
1242 &of.spline,
1243 dist,
1244 (ta, tb),
1245 tolerance,
1246 4 * of.spline.control_points.len(),
1247 )?;
1248 let last = control.len() - 1;
1249 let mut points = vec![s];
1250 for p in &control[1..last] {
1251 points.push(self.new_point_at(*p));
1252 }
1253 points.push(e);
1254 let fit = self.add_spline(points);
1255 let by = reach.expect("made for a spline's offset");
1256 for (k, &(on, at)) in at.iter().enumerate() {
1257 let across =
1258 (k == 0 && !ends_pinned[0]) || (k == last && !ends_pinned[1]);
1259 self.constrain(Constraint::OffsetAt {
1260 a: source,
1261 at: Scalar::from_f64(at),
1262 b: fit,
1263 on: Scalar::from_f64(on),
1264 by,
1265 across,
1266 });
1267 }
1268 fit
1269 }
1270 (kind, _) => unreachable!("chained and trimmed as it is: {kind:?}"),
1271 };
1272 if distance.is_none() {
1273 distance = Some(match self.curves[&source].kind {
1274 CurveKind::Spline { .. } => self.constrain(Constraint::Radius {
1275 curve: reach.expect("made for a spline's offset"),
1276 value,
1277 }),
1278 _ => self.constrain(Constraint::Offset {
1279 a: source,
1280 b: curve,
1281 value,
1282 }),
1283 });
1284 } else {
1285 match self.curves[&source].kind {
1286 CurveKind::Line { .. } => {
1287 self.constrain(Constraint::Parallel {
1288 a: source,
1289 b: curve,
1290 });
1291 }
1292 CurveKind::Spline { .. } => {}
1293 _ => {
1294 self.constrain(Constraint::Concentric {
1295 a: source,
1296 b: curve,
1297 });
1298 }
1299 }
1300 }
1301 curve
1302 }
1303 Of::Round(i) => {
1304 let Shape::Plain(Plain::Round {
1305 span: Some((_, sweep)),
1306 ..
1307 }) = subs[a].shape
1308 else {
1309 unreachable!("an arc round a corner");
1310 };
1311 let arc = self.add_arc(s, e, Scalar::from_f64(sweep));
1312 let corner = corner_of(self, i);
1313 self.constrain(Constraint::Center {
1314 point: corner,
1315 curve: arc,
1316 });
1317 arc
1318 }
1319 };
1320 curve_of.push(curve);
1321 made.push(curve);
1322 }
1323 if let (Some(circle), Some(first)) = (reach, first)
1326 && !of_spline(first)
1327 {
1328 self.constrain(Constraint::Tangent {
1329 a: curve_of[first],
1330 b: circle,
1331 });
1332 }
1333 let mut held: Vec<bool> = (0..subs.len()).map(of_spline).collect();
1339 if let Some(first) = first {
1340 held[first] = true;
1341 }
1342 let mut joined: Vec<(usize, usize)> = Vec::new();
1343 for a in 0..subs.len() {
1344 let (Some(b), Some(i)) = (next[a], joint_after(a)) else {
1345 continue;
1346 };
1347 if !subs[b].from_start {
1348 continue;
1349 }
1350 joined.push((a, b));
1351 let (oa, ob) = (curve_of[a], curve_of[b]);
1352 let splines = [of_spline(a), of_spline(b)];
1353 let joint = joint_of[i]
1354 .as_ref()
1355 .expect("a joint after a piece with one next");
1356 match (path[subs[a].elem].1, path[subs[b].elem].1, joint) {
1357 (Of::Piece(_), Of::Piece(_), Joint::Shared { by, .. }) => match by {
1358 _ if splines == [true, true] => {}
1359 Held::Circle if splines[0] || splines[1] => {
1360 let other = if splines[0] { ob } else { oa };
1361 let corner = corner_of(self, i);
1362 let circle = self.add_circle(corner, value);
1363 self.set_construction(circle, true);
1364 self.constrain(Constraint::Tangent {
1365 a: other,
1366 b: circle,
1367 });
1368 self.constrain(Constraint::Equal {
1369 a: circle,
1370 b: reach.expect("made for a spline's offset"),
1371 });
1372 }
1373 _ if splines[0] || splines[1] => {}
1374 Held::Tangent => {
1375 self.constrain(Constraint::Tangent { a: oa, b: ob });
1376 }
1377 Held::Square => {
1378 let corner = corner_of(self, i);
1379 self.square_end(pieces_of[i].0, corner, end_point[a].unwrap());
1380 }
1381 Held::Circle => {
1382 let corner = corner_of(self, i);
1383 let circle = self.add_circle(corner, value);
1384 self.set_construction(circle, true);
1385 self.constrain(Constraint::Tangent { a: oa, b: circle });
1386 self.constrain(Constraint::Tangent { a: ob, b: circle });
1387 }
1388 },
1389 _ if splines[0] || splines[1] => {}
1390 _ => {
1391 self.constrain(Constraint::Tangent { a: oa, b: ob });
1392 }
1393 }
1394 }
1395 loop {
1396 let mut grew = false;
1397 for &(a, b) in &joined {
1398 if held[a] != held[b] {
1399 held[a] = true;
1400 held[b] = true;
1401 grew = true;
1402 }
1403 }
1404 if !grew {
1405 break;
1406 }
1407 }
1408 for a in 0..subs.len() {
1412 if held[a] {
1413 continue;
1414 }
1415 let (Some(first), Some(first_piece)) = (first, first_piece) else {
1416 unreachable!("a part of a piece is kept");
1417 };
1418 let reference = *reach.get_or_insert_with(|| {
1419 let circle = self.add_circle(corner_of(self, first_piece), value);
1420 self.set_construction(circle, true);
1421 self.constrain(Constraint::Tangent {
1422 a: curve_of[first],
1423 b: circle,
1424 });
1425 circle
1426 });
1427 match path[subs[a].elem].1 {
1428 Of::Piece(i) => {
1429 let circle = self.add_circle(corner_of(self, i), value);
1430 self.set_construction(circle, true);
1431 self.constrain(Constraint::Tangent {
1432 a: curve_of[a],
1433 b: circle,
1434 });
1435 self.constrain(Constraint::Equal {
1436 a: circle,
1437 b: reference,
1438 });
1439 }
1440 Of::Round(_) => {
1441 self.constrain(Constraint::Equal {
1442 a: curve_of[a],
1443 b: reference,
1444 });
1445 }
1446 }
1447 }
1448 {
1451 for a in 0..subs.len() {
1452 let k = subs[a].elem;
1453 let at_start =
1454 if subs[a].from_start && path_previous[k].is_none() && previous[a].is_none() {
1455 true
1456 } else if subs[a].to_end && path_next[k].is_none() && next[a].is_none() {
1457 false
1458 } else {
1459 continue;
1460 };
1461 let Of::Piece(i) = path[k].1 else {
1462 continue;
1463 };
1464 if spline(i) {
1465 continue;
1466 }
1467 let (source, reversed) = pieces_of[i];
1468 let (s, e) = self.curves[&source].endpoints().expect("a line or arc");
1469 let end = if at_start != reversed { s } else { e };
1470 let image = if at_start {
1471 start_point[a].unwrap()
1472 } else {
1473 end_point[a].unwrap()
1474 };
1475 self.square_end(source, end, image);
1476 }
1477 }
1478 for (c, _, ring) in &circles {
1483 let Shape::Plain(Plain::Round { center, radius, .. }) = ring else {
1484 unreachable!("a circle's offset is a circle");
1485 };
1486 let CurveKind::Circle {
1487 center: middle,
1488 radius: source,
1489 } = self.curves[c].kind
1490 else {
1491 unreachable!("a circle");
1492 };
1493 let circle = self.add_circle(middle, Scalar::from_f64(*radius));
1494 made.push(circle);
1495 if distance.is_none() {
1496 distance = Some(self.constrain(Constraint::Offset {
1497 a: *c,
1498 b: circle,
1499 value,
1500 }));
1501 continue;
1502 }
1503 let on = self.new_point_at(add(*center, [source.to_f64(), 0.0]));
1504 self.constrain(Constraint::PointOnCurve {
1505 point: on,
1506 curve: *c,
1507 });
1508 let level = self.add_line(middle, on);
1509 self.set_construction(level, true);
1510 self.constrain(Constraint::Horizontal { line: level });
1511 let touching = self.add_circle(on, value);
1512 self.set_construction(touching, true);
1513 self.constrain(Constraint::Tangent {
1514 a: circle,
1515 b: touching,
1516 });
1517 let reference = match reach {
1518 Some(reach) => reach,
1519 None => {
1520 let first = first.expect("a part kept, its distance given");
1521 let first_piece = first_piece.expect("a part kept");
1522 let circle = self.add_circle(corner_of(self, first_piece), value);
1523 self.set_construction(circle, true);
1524 self.constrain(Constraint::Tangent {
1525 a: curve_of[first],
1526 b: circle,
1527 });
1528 reach = Some(circle);
1529 circle
1530 }
1531 };
1532 self.constrain(Constraint::Equal {
1533 a: touching,
1534 b: reference,
1535 });
1536 }
1537 Ok(Offsetted {
1538 curves: made,
1539 distance: distance.expect("a part of a piece is kept"),
1540 })
1541 }
1542
1543 pub fn chains(&self, curves: &[CurveId]) -> GeopResult<Vec<Chain>> {
1546 let class = self.point_classes();
1547 let ends = |c: CurveId| -> GeopResult<Vec<PointId>> {
1548 Ok(match self.curve(c)?.endpoints() {
1549 Some((s, e)) => vec![class[&s], class[&e]],
1550 None => Vec::new(),
1551 })
1552 };
1553 let mut left: Vec<CurveId> = Vec::new();
1554 for &c in curves {
1555 if !left.contains(&c) {
1556 left.push(c);
1557 }
1558 }
1559 let mut chains = Vec::new();
1560 while let Some(first) = left.first().copied() {
1561 let mut group = vec![first];
1562 let mut points = ends(first)?;
1563 left.remove(0);
1564 loop {
1565 let mut joined = None;
1566 for (k, &c) in left.iter().enumerate() {
1567 if ends(c)?.iter().any(|p| points.contains(p)) {
1568 joined = Some(k);
1569 break;
1570 }
1571 }
1572 let Some(k) = joined else {
1573 break;
1574 };
1575 let c = left.remove(k);
1576 points.extend(ends(c)?);
1577 group.push(c);
1578 }
1579 chains.push(self.chain(&group)?);
1580 }
1581 if chains.is_empty() {
1582 return Err(GeopError::new("no curves to make a chain of"));
1583 }
1584 Ok(chains)
1585 }
1586
1587 fn square_end(&mut self, source: CurveId, end: PointId, image: PointId) {
1591 match self.curves[&source].kind {
1592 CurveKind::Line { .. } => {
1593 let across = self.add_line(end, image);
1594 self.set_construction(across, true);
1595 self.constrain(Constraint::Perpendicular {
1596 a: source,
1597 b: across,
1598 });
1599 }
1600 _ => {
1601 let Some(Plain::Round { center, .. }) = Plain::of(self, source, false) else {
1602 return;
1603 };
1604 let m = self.new_point_at(center);
1605 self.constrain(Constraint::Center {
1606 point: m,
1607 curve: source,
1608 });
1609 let radial = self.add_line(m, end);
1610 self.set_construction(radial, true);
1611 self.constrain(Constraint::PointOnCurve {
1612 point: image,
1613 curve: radial,
1614 });
1615 }
1616 }
1617 }
1618}
1619
1620#[cfg(test)]
1621mod tests {
1622 use super::*;
1623 use std::f64::consts::PI;
1624
1625 fn n(x: f64) -> Scalar {
1626 Scalar::from_f64(x)
1627 }
1628
1629 fn rectangle(s: &mut Sketch, w: f64, h: f64) -> (Vec<CurveId>, ConstraintId) {
1632 let p = [[0.0, 0.0], [w, 0.0], [w, h], [0.0, h]].map(|q| s.add_point(n(q[0]), n(q[1])));
1633 let l: Vec<CurveId> = (0..4).map(|i| s.add_line(p[i], p[(i + 1) % 4])).collect();
1634 s.constrain(Constraint::Fix {
1635 point: p[0],
1636 x: n(0.0),
1637 y: n(0.0),
1638 });
1639 s.constrain(Constraint::Horizontal { line: l[0] });
1640 s.constrain(Constraint::Vertical { line: l[1] });
1641 s.constrain(Constraint::Horizontal { line: l[2] });
1642 s.constrain(Constraint::Vertical { line: l[3] });
1643 let width = s.constrain(Constraint::Length {
1644 curve: l[0],
1645 value: n(w),
1646 });
1647 s.constrain(Constraint::Length {
1648 curve: l[1],
1649 value: n(h),
1650 });
1651 (l, width)
1652 }
1653
1654 fn bounds(s: &Sketch, curves: &[CurveId]) -> [f64; 4] {
1656 let points = curves.iter().flat_map(|c| s.curves[c].points());
1657 let (lo, hi) = plain::bounds(points.map(|p| xy(s, p))).unwrap();
1658 [lo[0], lo[1], hi[0], hi[1]]
1659 }
1660
1661 fn near(a: [f64; 4], b: [f64; 4]) -> bool {
1662 a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-7)
1663 }
1664
1665 fn set(s: &mut Sketch, k: ConstraintId, v: f64) {
1666 match s.constraints.get_mut(&k) {
1667 Some(Constraint::Offset { value, .. } | Constraint::Length { value, .. }) => {
1668 *value = n(v)
1669 }
1670 other => panic!("{other:?}"),
1671 }
1672 }
1673
1674 fn lines(s: &Sketch, curves: &[CurveId]) -> Vec<CurveId> {
1675 curves
1676 .iter()
1677 .copied()
1678 .filter(|c| matches!(s.curves[c].kind, CurveKind::Line { .. }))
1679 .collect()
1680 }
1681
1682 #[test]
1686 fn a_rectangle_offset_outwards_rounds_its_corners() {
1687 let mut s = Sketch::new();
1688 let (rect, width) = rectangle(&mut s, 2.0, 1.0);
1689 let made = s.offset(&rect, n(-0.25), Corners::Round).unwrap();
1691 assert_eq!(made.curves.len(), 8);
1692 let report = s.solve().unwrap();
1693 assert!(report.converged && report.dof == 0, "{report:?}");
1694 assert_eq!(s.regions().unwrap().len(), 1);
1696 let sides = lines(&s, &made.curves);
1697 assert!(
1698 near(bounds(&s, &sides), [-0.25, -0.25, 2.25, 1.25]),
1699 "{:?}",
1700 bounds(&s, &sides)
1701 );
1702 set(&mut s, made.distance, 0.5);
1703 set(&mut s, width, 3.0);
1704 let report = s.solve().unwrap();
1705 assert!(report.converged, "{report:?}");
1706 assert!(
1707 near(bounds(&s, &sides), [-0.5, -0.5, 3.5, 1.5]),
1708 "{:?}",
1709 bounds(&s, &sides)
1710 );
1711 s.enclose().unwrap();
1712 }
1713
1714 #[test]
1717 fn offsets_meet_inwards_and_extended() {
1718 for (d, corners, inner, then) in [
1719 (
1720 0.25,
1721 Corners::Round,
1722 [0.25, 0.25, 1.75, 0.75],
1723 [0.125, 0.125, 1.875, 0.875],
1724 ),
1725 (
1726 -0.25,
1727 Corners::Extend,
1728 [-0.25, -0.25, 2.25, 1.25],
1729 [-0.125, -0.125, 2.125, 1.125],
1730 ),
1731 ] {
1732 let mut s = Sketch::new();
1733 let (rect, _) = rectangle(&mut s, 2.0, 1.0);
1734 let made = s.offset(&rect, n(d), corners).unwrap();
1735 assert_eq!(made.curves.len(), 4);
1736 let report = s.solve().unwrap();
1737 assert!(report.converged && report.dof == 0, "{d}: {report:?}");
1738 assert!(
1739 near(bounds(&s, &made.curves), inner),
1740 "{:?}",
1741 bounds(&s, &made.curves)
1742 );
1743 set(&mut s, made.distance, 0.125);
1744 let report = s.solve().unwrap();
1745 assert!(report.converged, "{report:?}");
1746 assert!(
1747 near(bounds(&s, &made.curves), then),
1748 "{:?}",
1749 bounds(&s, &made.curves)
1750 );
1751 assert_eq!(s.regions().unwrap().len(), 1);
1753 s.enclose().unwrap();
1754 }
1755 }
1756
1757 #[test]
1759 fn a_slot_offsets_into_a_slot() {
1760 let mut s = Sketch::new();
1761 let p =
1762 [[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [0.0, 1.0]].map(|q| s.add_point(n(q[0]), n(q[1])));
1763 let bottom = s.add_line(p[0], p[1]);
1764 let right = s.add_arc(p[1], p[2], n(PI));
1765 let top = s.add_line(p[2], p[3]);
1766 let left = s.add_arc(p[3], p[0], n(PI));
1767 for (a, b) in [(bottom, right), (right, top), (top, left), (left, bottom)] {
1768 s.constrain(Constraint::Tangent { a, b });
1769 }
1770 s.constrain(Constraint::Fix {
1771 point: p[0],
1772 x: n(0.0),
1773 y: n(0.0),
1774 });
1775 s.constrain(Constraint::Horizontal { line: bottom });
1776 s.constrain(Constraint::Length {
1777 curve: bottom,
1778 value: n(2.0),
1779 });
1780 s.constrain(Constraint::Radius {
1781 curve: right,
1782 value: n(0.5),
1783 });
1784 s.constrain(Constraint::Equal { a: left, b: right });
1785 assert!(s.solve().unwrap().converged);
1786 let made = s
1787 .offset(&[top, left, bottom, right], n(-0.25), Corners::Round)
1788 .unwrap();
1789 assert_eq!(made.curves.len(), 4);
1790 let report = s.solve().unwrap();
1791 assert!(report.converged && report.dof == 0, "{report:?}");
1792 assert!(
1793 near(bounds(&s, &made.curves), [0.0, -0.25, 2.0, 1.25]),
1794 "{:?}",
1795 bounds(&s, &made.curves)
1796 );
1797 s.enclose().unwrap();
1798 }
1799
1800 #[test]
1802 fn open_chains_end_square() {
1803 let mut s = Sketch::new();
1804 let p = [[0.0, 0.0], [2.0, 0.0], [3.0, 1.0]].map(|q| s.add_point(n(q[0]), n(q[1])));
1805 let a = s.add_line(p[0], p[1]);
1806 let b = s.add_arc(p[1], p[2], n(PI / 2.0));
1807 s.constrain(Constraint::Tangent { a, b });
1808 for (q, at) in [(p[0], [0.0, 0.0]), (p[1], [2.0, 0.0]), (p[2], [3.0, 1.0])] {
1809 s.constrain(Constraint::Fix {
1810 point: q,
1811 x: n(at[0]),
1812 y: n(at[1]),
1813 });
1814 }
1815 let made = s.offset(&[b, a], n(0.5), Corners::Round).unwrap();
1817 let report = s.solve().unwrap();
1818 assert!(report.converged && report.dof == 0, "{report:?}");
1819 assert!(
1820 near(bounds(&s, &made.curves), [0.0, 0.5, 2.5, 1.0]),
1821 "{:?}",
1822 bounds(&s, &made.curves)
1823 );
1824 s.enclose().unwrap();
1825 }
1826
1827 #[test]
1829 fn circles_offset_about_their_center() {
1830 let mut s = Sketch::new();
1831 let c = s.add_point(n(1.0), n(1.0));
1832 let circle = s.add_circle(c, n(1.0));
1833 let made = s.offset(&[circle], n(-0.5), Corners::Round).unwrap();
1834 assert!(matches!(
1835 s.curves[&made.curves[0]].kind,
1836 CurveKind::Circle { center, radius } if center == c && radius.to_f64() == 1.5
1837 ));
1838 assert!(s.solve().unwrap().converged);
1839 }
1840
1841 fn spread(s: &Sketch, source: CurveId, fit: CurveId) -> (f64, f64) {
1845 let shape = |c: CurveId| oriented(s, c, false, 0.0).unwrap();
1846 let (source, fit) = (shape(source), shape(fit));
1847 let mut range = (f64::INFINITY, 0.0f64);
1848 for k in 0..=200 {
1849 let p = fit.at(k as f64 / 200.0);
1850 let q = source.at(source.nearest(p));
1851 let d = plain::distance(p, q);
1852 range = (range.0.min(d), range.1.max(d));
1853 }
1854 range
1855 }
1856
1857 #[test]
1861 fn a_spline_offsets_into_a_fitted_spline() {
1862 let mut s = Sketch::new();
1863 let q = [[0.0, 0.0], [1.0, 0.6], [2.0, -0.3], [3.0, 0.4], [4.0, 0.0]]
1864 .map(|q| s.add_point(n(q[0]), n(q[1])));
1865 let spline = s.add_spline(q.to_vec());
1866 let free = s.solve().unwrap().dof;
1867 let made = s.offset(&[spline], n(0.2), Corners::Round).unwrap();
1868 let [fit] = made.curves[..] else {
1869 panic!("{made:?}");
1870 };
1871 assert!(matches!(s.curves[&fit].kind, CurveKind::Spline { .. }));
1872 let report = s.solve().unwrap();
1873 assert!(report.converged && report.dof == free, "{report:?}");
1874 let (lo, hi) = spread(&s, spline, fit);
1875 assert!(lo > 0.2 - 1e-3 && hi < 0.2 + 1e-3, "{lo} {hi}");
1876 match s.constraints.get_mut(&made.distance) {
1878 Some(Constraint::Radius { value, .. }) => *value = n(0.3),
1879 other => panic!("{other:?}"),
1880 }
1881 s.constrain(Constraint::Fix {
1882 point: q[4],
1883 x: n(4.0),
1884 y: n(0.4),
1885 });
1886 let report = s.solve().unwrap();
1887 assert!(report.converged, "{report:?}");
1888 let (lo, hi) = spread(&s, spline, fit);
1889 assert!(lo > 0.3 - 1e-3 && hi < 0.3 + 1e-3, "{lo} {hi}");
1890 s.enclose().unwrap();
1891 }
1892
1893 #[test]
1899 fn a_line_and_a_spline_offset_in_and_out() {
1900 for (d, corners) in [
1901 (0.1, Corners::Round),
1902 (0.1, Corners::Extend),
1903 (-0.1, Corners::Round),
1904 ] {
1905 let mut s = Sketch::new();
1906 let p = [[0.0, 0.0], [2.0, 0.0], [2.5, 1.0], [1.0, 2.0], [-0.5, 1.0]]
1907 .map(|q| s.add_point(n(q[0]), n(q[1])));
1908 let base = s.add_line(p[0], p[1]);
1909 let arch = s.add_spline(vec![p[1], p[2], p[3], p[4], p[0]]);
1910 let free = s.solve().unwrap().dof;
1911 let made = s
1912 .offset(&[base, arch], n(d), corners)
1913 .unwrap_or_else(|e| panic!("{d} {corners:?}: {e:?}"));
1914 let report = s.solve().unwrap();
1915 assert!(
1916 report.converged && report.dof == free,
1917 "{d} {corners:?}: {report:?}"
1918 );
1919 assert_eq!(s.regions().unwrap().len(), 1, "{d} {corners:?}");
1920 let fit = *made
1921 .curves
1922 .iter()
1923 .find(|c| matches!(s.curves[c].kind, CurveKind::Spline { .. }))
1924 .unwrap();
1925 let (lo, hi) = spread(&s, arch, fit);
1926 assert!(
1927 lo > 0.1 - 1e-4 && hi < 0.1 + 1e-4,
1928 "{d} {corners:?}: {lo} {hi}"
1929 );
1930 }
1931 let mut s = Sketch::new();
1932 let p = [[0.0, 0.0], [2.0, 0.0], [2.5, 1.0], [1.0, 2.0], [-0.5, 1.0]]
1933 .map(|q| s.add_point(n(q[0]), n(q[1])));
1934 let base = s.add_line(p[0], p[1]);
1935 let arch = s.add_spline(vec![p[1], p[2], p[3], p[4], p[0]]);
1936 let e = s
1937 .offset(&[base, arch], n(-0.1), Corners::Extend)
1938 .unwrap_err();
1939 assert!(e.root_message().contains("do not meet"), "{e}");
1940 }
1941
1942 #[test]
1944 fn offsets_that_would_cross_themselves_are_refused() {
1945 let mut s = Sketch::new();
1946 let (rect, _) = rectangle(&mut s, 2.0, 1.0);
1947 let e = s.offset(&rect, n(0.6), Corners::Round).unwrap_err();
1948 assert!(e.root_message().contains("too short"), "{e}");
1949 let c = s.add_point(n(5.0), n(0.0));
1950 let circle = s.add_circle(c, n(0.5));
1951 let e = s.offset(&[circle], n(0.5), Corners::Round).unwrap_err();
1952 assert!(e.root_message().contains("smaller"), "{e}");
1953 let q = [[0.0, 3.0], [1.0, 4.0], [2.0, 3.0]].map(|q| s.add_point(n(q[0]), n(q[1])));
1954 let spline = s.add_spline(q.to_vec());
1957 let e = s.offset(&[spline], n(-1.5), Corners::Round).unwrap_err();
1958 assert!(e.root_message().contains("bends tighter"), "{e}");
1959 }
1960
1961 fn fixed_square(s: &mut Sketch, corner: [f64; 2], side: f64) -> Vec<CurveId> {
1964 let q = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]
1965 .map(|q| [corner[0] + side * q[0], corner[1] + side * q[1]]);
1966 let p = q.map(|q| s.add_point(n(q[0]), n(q[1])));
1967 for (&point, q) in p.iter().zip(q) {
1968 s.constrain(Constraint::Fix {
1969 point,
1970 x: n(q[0]),
1971 y: n(q[1]),
1972 });
1973 }
1974 (0..4).map(|i| s.add_line(p[i], p[(i + 1) % 4])).collect()
1975 }
1976
1977 #[test]
1981 fn an_outline_and_its_holes_offset_into_the_material() {
1982 let mut s = Sketch::new();
1983 let outline = fixed_square(&mut s, [0.0, 0.0], 4.0);
1984 let hole = fixed_square(&mut s, [1.0, 1.0], 1.0);
1985 let middle = s.add_point(n(3.0), n(3.0));
1986 s.constrain(Constraint::Fix {
1987 point: middle,
1988 x: n(3.0),
1989 y: n(3.0),
1990 });
1991 let round = s.add_circle(middle, n(0.4));
1992 s.constrain(Constraint::Radius {
1993 curve: round,
1994 value: n(0.4),
1995 });
1996 let all: Vec<CurveId> = outline
1997 .iter()
1998 .chain(&hole)
1999 .copied()
2000 .chain([round])
2001 .collect();
2002 let made = s.offset(&all, n(0.2), Corners::Round).unwrap();
2003 let report = s.solve().unwrap();
2004 assert!(report.converged && report.dof == 0, "{report:?}");
2005 let sides: Vec<CurveId> = lines(&s, &made.curves);
2006 assert_eq!(sides.len(), 8, "{made:?}");
2007 let (inner, outer): (Vec<CurveId>, Vec<CurveId>) = sides
2008 .iter()
2009 .partition(|&&c| bounds(&s, &[c])[0] > 0.5 && bounds(&s, &[c])[2] < 3.5);
2010 assert!(
2011 near(bounds(&s, &outer), [0.2, 0.2, 3.8, 3.8]),
2012 "{:?}",
2013 bounds(&s, &outer)
2014 );
2015 assert!(
2016 near(bounds(&s, &inner), [0.8, 0.8, 2.2, 2.2]),
2017 "{:?}",
2018 bounds(&s, &inner)
2019 );
2020 let circle = made
2021 .curves
2022 .iter()
2023 .find(|c| {
2024 matches!(s.curves[c].kind, CurveKind::Circle { .. }) && !s.curves[c].construction
2025 })
2026 .unwrap();
2027 let CurveKind::Circle { radius, .. } = s.curves[circle].kind else {
2028 unreachable!()
2029 };
2030 assert!((radius.to_f64() - 0.6).abs() < 1e-7, "{radius:?}");
2031 set(&mut s, made.distance, 0.1);
2032 assert!(s.solve().unwrap().converged);
2033 assert!(
2034 near(bounds(&s, &outer), [0.1, 0.1, 3.9, 3.9]),
2035 "{:?}",
2036 bounds(&s, &outer)
2037 );
2038 s.enclose().unwrap();
2039 }
2040
2041 #[test]
2045 fn a_hole_near_the_outline_offsets_into_one_curve() {
2046 let mut s = Sketch::new();
2047 let outline = fixed_square(&mut s, [0.0, 0.0], 4.0);
2048 let hole = fixed_square(&mut s, [2.8, 1.5], 1.0);
2049 let all: Vec<CurveId> = outline.iter().chain(&hole).copied().collect();
2050 let made = s.offset(&all, n(0.3), Corners::Round).unwrap();
2051 let report = s.solve().unwrap();
2052 assert!(report.converged && report.dof == 0, "{report:?}");
2053 let loops = s.chain(
2056 &made
2057 .curves
2058 .iter()
2059 .copied()
2060 .filter(|c| !s.curves[c].construction)
2061 .collect::<Vec<_>>(),
2062 );
2063 assert!(loops.is_ok_and(|chain| chain.closed), "{made:?}");
2064 s.enclose().unwrap();
2065 }
2066
2067 #[test]
2069 fn chains_through_a_curve() {
2070 let mut s = Sketch::new();
2071 let (rect, _) = rectangle(&mut s, 2.0, 1.0);
2072 let mut chain = s.chain_through(rect[2]).unwrap();
2073 chain.sort();
2074 assert_eq!(chain, rect);
2075 let chain = s.chain(&rect).unwrap();
2076 assert!(chain.closed);
2077 assert!((s.chain_side(&chain, [1.0, 0.25]).unwrap() - 0.25).abs() < 1e-12);
2079 assert!((s.chain_side(&chain, [1.0, -0.5]).unwrap() + 0.5).abs() < 1e-12);
2080 }
2081
2082 #[test]
2086 fn lines_in_line_offset_straight_on() {
2087 let mut s = Sketch::new();
2088 let p = [[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]].map(|q| s.add_point(n(q[0]), n(q[1])));
2089 let a = s.add_line(p[0], p[1]);
2090 let b = s.add_line(p[1], p[2]);
2091 for (q, at) in [(p[0], 0.0), (p[1], 1.0), (p[2], 2.0)] {
2092 s.constrain(Constraint::Fix {
2093 point: q,
2094 x: n(at),
2095 y: n(0.0),
2096 });
2097 }
2098 for corners in [Corners::Round, Corners::Extend] {
2099 for d in [0.25, -0.25] {
2100 let mut s = s.clone();
2101 let made = s.offset(&[a, b], n(d), corners).unwrap();
2102 assert_eq!(made.curves.len(), 2, "{corners:?} {d}");
2103 let report = s.solve().unwrap();
2104 assert!(report.converged && report.dof == 0, "{report:?}");
2105 assert!(
2106 near(bounds(&s, &made.curves), [0.0, d, 2.0, d]),
2107 "{:?}",
2108 bounds(&s, &made.curves)
2109 );
2110 s.enclose().unwrap();
2111 }
2112 }
2113 }
2114
2115 fn rounded(s: &mut Sketch, w: f64, h: f64, r: f64) -> Vec<CurveId> {
2118 let q = |x: f64, y: f64| [x, y];
2119 let corners = [
2120 (q(r, 0.0), q(w - r, 0.0)),
2121 (q(w, r), q(w, h - r)),
2122 (q(w - r, h), q(r, h)),
2123 (q(0.0, h - r), q(0.0, r)),
2124 ];
2125 let mut points = Vec::new();
2126 for (a, b) in corners {
2127 points.push(s.add_point(n(a[0]), n(a[1])));
2128 points.push(s.add_point(n(b[0]), n(b[1])));
2129 }
2130 let mut curves = Vec::new();
2131 for k in 0..4 {
2132 let line = s.add_line(points[2 * k], points[2 * k + 1]);
2133 let arc = s.add_arc(points[2 * k + 1], points[(2 * k + 2) % 8], n(PI / 2.0));
2134 curves.extend([line, arc]);
2135 }
2136 for k in 0..8 {
2137 s.constrain(Constraint::Tangent {
2138 a: curves[k],
2139 b: curves[(k + 1) % 8],
2140 });
2141 }
2142 for (k, at) in points.iter().zip([
2143 q(r, 0.0),
2144 q(w - r, 0.0),
2145 q(w, r),
2146 q(w, h - r),
2147 q(w - r, h),
2148 q(r, h),
2149 q(0.0, h - r),
2150 q(0.0, r),
2151 ]) {
2152 s.constrain(Constraint::Fix {
2153 point: *k,
2154 x: n(at[0]),
2155 y: n(at[1]),
2156 });
2157 }
2158 curves
2159 }
2160
2161 #[test]
2165 fn closed_offsets_are_proven() {
2166 let mut failures = Vec::new();
2167 for (w, h) in [(2.0, 1.0), (1.0, 1.0), (3.0, 0.5), (1.3, 0.7)] {
2168 for d in [0.1, 0.2, -0.1, -0.3] {
2169 for corners in [Corners::Round, Corners::Extend] {
2170 for shape in ["rectangle", "rounded"] {
2171 let mut s = Sketch::new();
2172 let curves = match shape {
2173 "rectangle" => rectangle(&mut s, w, h).0,
2174 _ => rounded(&mut s, w, h, 0.2),
2175 };
2176 assert!(s.solve().unwrap().converged);
2177 let case = format!("{shape} {w}x{h} by {d}, {corners:?}");
2178 let Ok(_) = s.offset(&curves, n(d), corners) else {
2179 continue;
2180 };
2181 let report = s.solve().unwrap();
2182 if !report.converged || report.dof != 0 {
2183 failures.push(format!("{case}: {report:?}"));
2184 } else if let Err(e) = s.enclose() {
2185 failures.push(format!("{case}: {}", e.root_message()));
2186 }
2187 }
2188 }
2189 }
2190 }
2191 assert!(failures.is_empty(), "{}", failures.join("\n"));
2192 }
2193
2194 #[test]
2201 fn closed_offsets_of_a_crooked_outline_are_proven() {
2202 let corners = [[0.0, 0.0], [2.1, 0.3], [2.6, 1.7], [1.2, 2.4], [-0.3, 1.3]];
2203 let mut failures = Vec::new();
2204 for d in [0.15, -0.15, 0.4] {
2205 for corners_as in [Corners::Round, Corners::Extend] {
2206 let mut s = Sketch::new();
2207 let p = corners.map(|q| s.add_point(n(q[0]), n(q[1])));
2208 let sides: Vec<CurveId> =
2209 (0..5).map(|i| s.add_line(p[i], p[(i + 1) % 5])).collect();
2210 for (&point, q) in p.iter().zip(corners) {
2211 s.constrain(Constraint::Fix {
2212 point,
2213 x: n(q[0]),
2214 y: n(q[1]),
2215 });
2216 }
2217 let case = format!("by {d}, {corners_as:?}");
2218 if let Err(e) = s.offset(&sides, n(d), corners_as) {
2219 failures.push(format!("{case}: {}", e.root_message()));
2220 continue;
2221 }
2222 let report = s.solve().unwrap();
2223 if !report.converged || report.dof != 0 {
2224 failures.push(format!("{case}: {report:?}"));
2225 } else if let Err(e) = s.enclose() {
2226 failures.push(format!("{case}: {}", e.root_message()));
2227 }
2228 }
2229 }
2230 assert!(failures.is_empty(), "{}", failures.join("\n"));
2231 }
2232
2233 #[test]
2239 fn an_offset_over_a_notch_narrower_than_twice_it_is_trimmed() {
2240 let corners = [
2241 [0.0, 0.0],
2242 [4.0, 0.0],
2243 [4.0, 4.0],
2244 [2.5, 4.0],
2245 [2.5, 2.0],
2246 [1.5, 2.0],
2247 [1.5, 4.0],
2248 [0.0, 4.0],
2249 ];
2250 let mut s = Sketch::new();
2251 let p = corners.map(|q| s.add_point(n(q[0]), n(q[1])));
2252 let sides: Vec<CurveId> = (0..8).map(|i| s.add_line(p[i], p[(i + 1) % 8])).collect();
2253 for (&point, q) in p.iter().zip(corners) {
2254 s.constrain(Constraint::Fix {
2255 point,
2256 x: n(q[0]),
2257 y: n(q[1]),
2258 });
2259 }
2260 let made = s.offset(&sides, n(-0.6), Corners::Round).unwrap();
2261 let report = s.solve().unwrap();
2262 assert!(report.converged && report.dof == 0, "{report:?}");
2263 assert!(
2264 near(bounds(&s, &made.curves), [-0.6, -0.6, 4.6, 4.6]),
2265 "{:?}",
2266 bounds(&s, &made.curves)
2267 );
2268 let dip = |s: &Sketch, r: f64| {
2271 let q = [2.0, 4.0 + (r * r - 0.25f64).sqrt()];
2272 made.curves
2273 .iter()
2274 .flat_map(|c| s.curves[c].points())
2275 .any(|p| plain::distance(xy(s, p), q) < 1e-7)
2276 };
2277 assert!(dip(&s, 0.6));
2278 assert_eq!(lines(&s, &made.curves).len(), 5);
2279 assert_eq!(made.curves.len(), 5 + 4 + 2);
2280 s.enclose().unwrap();
2281 }
2282
2283 #[test]
2290 fn a_trimmed_offset_follows_its_dimension() {
2291 let mut s = Sketch::new();
2292 let corners = [
2293 [0.0, 0.0],
2294 [4.0, 0.0],
2295 [4.0, 4.0],
2296 [2.5, 4.0],
2297 [2.5, 2.0],
2298 [1.5, 2.0],
2299 [1.5, 4.0],
2300 [0.0, 4.0],
2301 ];
2302 let p = corners.map(|q| s.add_point(n(q[0]), n(q[1])));
2303 let sides: Vec<CurveId> = (0..8).map(|i| s.add_line(p[i], p[(i + 1) % 8])).collect();
2304 for (&point, q) in p.iter().zip(corners) {
2305 s.constrain(Constraint::Fix {
2306 point,
2307 x: n(q[0]),
2308 y: n(q[1]),
2309 });
2310 }
2311 let made = s.offset(&sides, n(-0.6), Corners::Round).unwrap();
2312 s.solve().unwrap();
2313 set(&mut s, made.distance, 0.7);
2314 let report = s.solve().unwrap();
2315 assert!(report.converged, "{report:?}");
2316 assert!(
2317 near(bounds(&s, &made.curves), [-0.7, -0.7, 4.7, 4.7]),
2318 "{:?}",
2319 bounds(&s, &made.curves)
2320 );
2321 }
2322}