Skip to main content

geop_core_sketch/
sketch.rs

1//! Sketch entities and constraints: design data, in any [`Scalar`] — the
2//! values a file holds, read as sharp scalars and written as their
3//! midpoints.
4//!
5//! Points are the only entities with positions of their own; lines, arcs and
6//! splines reference points by [`PointId`], so two curves that share an
7//! endpoint share the *same* point and stay connected by construction.
8//! Separately drawn points are joined with [`Constraint::Coincident`], which
9//! the solver treats the same way (the two points become one set of
10//! variables), so connectivity is always structural — never inferred from
11//! two positions happening to be close.
12//!
13//! A point or curve can be *fixed*: given from outside the sketch rather
14//! than solved for — the sketch's own origin and axes, or an edge of the
15//! part projected into its plane. The solver never moves it, and every
16//! constraint on it is measured against it as it is.
17
18use std::collections::{BTreeMap, BTreeSet};
19
20use geop_core_math::{
21    geop_error::{GeopError, GeopResult},
22    scalars::{Scalar, as_f64},
23    union_find::UnionFind,
24    vector::Vector2,
25};
26
27use serde::{Deserialize, Serialize};
28
29macro_rules! define_ids {
30    ($($(#[$doc:meta])* $name:ident => $prefix:literal),* $(,)?) => {
31        $(
32            $(#[$doc])*
33            #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
34            #[serde(transparent)]
35            pub struct $name(pub u64);
36
37            /// The id as it appears in a topological name, e.g. `p3`.
38            impl std::fmt::Display for $name {
39                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40                    write!(f, concat!($prefix, "{}"), self.0)
41                }
42            }
43
44            /// The id as [`std::fmt::Display`] writes it, `p3`, read back.
45            impl std::str::FromStr for $name {
46                type Err = GeopError;
47
48                fn from_str(text: &str) -> GeopResult<Self> {
49                    (text.strip_prefix($prefix))
50                        .and_then(|id| id.parse().ok())
51                        .map($name)
52                        .ok_or_else(|| {
53                            GeopError::new(format!(concat!("{:?} is no id `", $prefix, "<n>`"), text))
54                        })
55                }
56            }
57
58            impl<'de> Deserialize<'de> for $name {
59                fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
60                    d.deserialize_any(IdVisitor).map($name)
61                }
62            }
63        )*
64    };
65}
66
67/// Reads an id from a number, or from the string a JSON object key holds it
68/// as. Serde's derived `u64` accepts the key string only when it knows the
69/// target type up front, not when the map was first buffered — as it is
70/// inside a `#[serde(flatten)]`ed or internally tagged container, where a
71/// sketch naturally ends up.
72struct IdVisitor;
73
74impl serde::de::Visitor<'_> for IdVisitor {
75    type Value = u64;
76
77    fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
78        f.write_str("a non-negative integer id, as a number or a string")
79    }
80
81    fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<u64, E> {
82        Ok(v)
83    }
84
85    fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<u64, E> {
86        u64::try_from(v).map_err(|_| E::custom(format!("id {v} is negative")))
87    }
88
89    fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<u64, E> {
90        v.parse()
91            .map_err(|_| E::custom(format!("{v:?} is not an integer id")))
92    }
93}
94
95define_ids!(
96    /// Key of [`Sketch::points`].
97    PointId => "p",
98    /// Key of [`Sketch::curves`].
99    CurveId => "c",
100    /// Key of [`Sketch::constraints`].
101    ConstraintId => "k",
102);
103
104pub(crate) fn is_false(b: &bool) -> bool {
105    !*b
106}
107
108/// What a curve or a constraint of a sketch — planar, or a 3-D
109/// [`crate::space::Sketch3d`] — refers to.
110pub(crate) trait Uses {
111    /// The points it refers to directly.
112    fn points(&self) -> Vec<PointId>;
113    /// The curves it refers to.
114    fn curves(&self) -> Vec<CurveId> {
115        Vec::new()
116    }
117    /// The two points it makes one, if it is a coincidence.
118    fn coincident(&self) -> Option<(PointId, PointId)> {
119        None
120    }
121}
122
123/// Removes `points`, `curves` and `constraints` from a sketch whose
124/// entities are `all_points`, `all_curves` and `all_constraints`, and
125/// everything that depends on them: a curve on a removed point, a
126/// constraint on any removed entity, and a point nothing uses any more
127/// because of it — a lone point drawn on purpose stays. Everything else
128/// keeps its id. Returns the curves removed.
129pub(crate) fn remove<P, C: Uses, K: Uses>(
130    all_points: &mut BTreeMap<PointId, P>,
131    all_curves: &mut BTreeMap<CurveId, C>,
132    all_constraints: &mut BTreeMap<ConstraintId, K>,
133    points: &[PointId],
134    curves: &[CurveId],
135    constraints: &[ConstraintId],
136) -> BTreeSet<CurveId> {
137    let dead_curves: BTreeSet<CurveId> = all_curves
138        .iter()
139        .filter(|(id, c)| curves.contains(id) || c.points().iter().any(|p| points.contains(p)))
140        .map(|(&id, _)| id)
141        .collect();
142    let dead_constraints: BTreeSet<ConstraintId> = all_constraints
143        .iter()
144        .filter(|(id, c)| {
145            constraints.contains(id)
146                || c.points().iter().any(|p| points.contains(p))
147                || c.curves().iter().any(|k| dead_curves.contains(k))
148        })
149        .map(|(&id, _)| id)
150        .collect();
151    let mut used_before = BTreeSet::new();
152    let mut used_after = BTreeSet::new();
153    for (id, c) in all_curves.iter() {
154        for p in c.points() {
155            used_before.insert(p);
156            if !dead_curves.contains(id) {
157                used_after.insert(p);
158            }
159        }
160    }
161    for (id, c) in all_constraints.iter() {
162        for p in c.points() {
163            used_before.insert(p);
164            if !dead_constraints.contains(id) {
165                used_after.insert(p);
166            }
167        }
168    }
169    all_points.retain(|p, _| {
170        let only_used_before = used_before.contains(p) && !used_after.contains(p);
171        !(points.contains(p) || only_used_before)
172    });
173    all_curves.retain(|id, _| !dead_curves.contains(id));
174    all_constraints.retain(|id, _| !dead_constraints.contains(id));
175    dead_curves
176}
177
178/// Union-find representative of each of `points` under the coincidences
179/// of `constraints`: points with the same representative are one point.
180/// The lowest id represents its class, so the representative does not
181/// depend on constraint order.
182pub(crate) fn point_classes<P, K: Uses>(
183    points: &BTreeMap<PointId, P>,
184    constraints: &BTreeMap<ConstraintId, K>,
185) -> BTreeMap<PointId, PointId> {
186    let ids: Vec<PointId> = points.keys().copied().collect();
187    let index: BTreeMap<PointId, usize> = ids.iter().enumerate().map(|(i, &id)| (id, i)).collect();
188    // The lowest index represents a class, and the ids are in order: so
189    // does the lowest id.
190    let mut classes = UnionFind::new(ids.len());
191    for (a, b) in constraints.values().filter_map(Uses::coincident) {
192        classes.union(index[&a], index[&b]);
193    }
194    (ids.iter().enumerate())
195        .map(|(i, &id)| (id, ids[classes.find(i)]))
196        .collect()
197}
198
199#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
200pub struct Point {
201    #[serde(with = "as_f64")]
202    pub x: Scalar,
203    #[serde(with = "as_f64")]
204    pub y: Scalar,
205    /// Given rather than solved for: the solver never moves it (see the
206    /// module docs).
207    #[serde(default, skip_serializing_if = "is_false")]
208    pub fixed: bool,
209}
210
211impl Point {
212    pub fn xy(&self) -> Vector2 {
213        Vector2::from_array([self.x, self.y])
214    }
215}
216
217#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
218#[serde(tag = "type")]
219pub enum CurveKind {
220    Line {
221        start: PointId,
222        end: PointId,
223    },
224    /// A circular arc from `start` to `end`, turning counter-clockwise by
225    /// `sweep` radians (clockwise if negative, `|sweep| < 2π`). Equivalent to
226    /// giving its curvature `2 sin(sweep / 2) / |end - start|` — see
227    /// [`crate::geometry`] for why the sweep is what is stored and solved.
228    Arc {
229        start: PointId,
230        end: PointId,
231        #[serde(with = "as_f64")]
232        sweep: Scalar,
233    },
234    Circle {
235        center: PointId,
236        #[serde(with = "as_f64")]
237        radius: Scalar,
238    },
239    /// A clamped B-spline through its first and last control points: as
240    /// drawn, uniform and non-rational of degree
241    /// `min(3, control_points.len() - 1)`; taken from elsewhere — an edge of
242    /// the part projected into the plane — any NURBS (see [`SplineShape`]).
243    Spline {
244        control_points: Vec<PointId>,
245        #[serde(default, skip_serializing_if = "Option::is_none")]
246        shape: Option<SplineShape>,
247    },
248    /// The outlines of `text`'s glyphs in the sketch font (see
249    /// [`crate::text`]): its first baseline from `anchor` on, its capitals
250    /// `height` tall, turned counter-clockwise by `angle` radians. Each of
251    /// a glyph's closed contours is a loop of the profile — an `o` an
252    /// outline and a hole in it. Only its anchor is solved for: a text is
253    /// placed by constraining that point, and takes no constraint itself.
254    Text {
255        anchor: PointId,
256        text: String,
257        #[serde(with = "as_f64")]
258        height: Scalar,
259        #[serde(with = "as_f64")]
260        angle: Scalar,
261    },
262}
263
264/// The shape of a [`CurveKind::Spline`] beyond its control points, for one
265/// that is not the uniform non-rational spline a drawn one is: its degree,
266/// its clamped knot vector (`control_points + degree + 1` knots) and one
267/// weight per control point.
268#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
269pub struct SplineShape {
270    pub degree: usize,
271    #[serde(with = "as_f64::vec")]
272    pub knots: Vec<Scalar>,
273    #[serde(with = "as_f64::vec")]
274    pub weights: Vec<Scalar>,
275}
276
277#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
278pub struct Curve {
279    #[serde(flatten)]
280    pub kind: CurveKind,
281    /// Construction geometry takes part in constraints but not in profiles
282    /// (e.g. a revolve axis or a symmetry line).
283    #[serde(default)]
284    pub construction: bool,
285    /// Given rather than solved for, like its points, which are all fixed:
286    /// an arc's sweep or a circle's radius too (see the module docs).
287    #[serde(default, skip_serializing_if = "is_false")]
288    pub fixed: bool,
289}
290
291impl Uses for Curve {
292    fn points(&self) -> Vec<PointId> {
293        Curve::points(self)
294    }
295}
296
297impl Curve {
298    /// The points this curve is defined by.
299    pub fn points(&self) -> Vec<PointId> {
300        match &self.kind {
301            CurveKind::Line { start, end } | CurveKind::Arc { start, end, .. } => {
302                vec![*start, *end]
303            }
304            CurveKind::Circle { center, .. } => vec![*center],
305            CurveKind::Spline { control_points, .. } => control_points.clone(),
306            CurveKind::Text { anchor, .. } => vec![*anchor],
307        }
308    }
309
310    /// `(start, end)` for an open curve, `None` for a circle or a text.
311    pub fn endpoints(&self) -> Option<(PointId, PointId)> {
312        match &self.kind {
313            CurveKind::Line { start, end } | CurveKind::Arc { start, end, .. } => {
314                Some((*start, *end))
315            }
316            CurveKind::Circle { .. } | CurveKind::Text { .. } => None,
317            CurveKind::Spline { control_points, .. } => {
318                Some((control_points[0], *control_points.last()?))
319            }
320        }
321    }
322}
323
324/// The typical CAD sketch constraints. Distances and lengths are in sketch
325/// units, angles in radians.
326#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
327#[serde(tag = "type")]
328pub enum Constraint {
329    /// `a` and `b` are the same point.
330    Coincident {
331        a: PointId,
332        b: PointId,
333    },
334    /// `point` lies on `curve` (a line's infinite extension, an arc's full
335    /// circle, or a circle).
336    PointOnCurve {
337        point: PointId,
338        curve: CurveId,
339    },
340    Horizontal {
341        line: CurveId,
342    },
343    Vertical {
344        line: CurveId,
345    },
346    Parallel {
347        a: CurveId,
348        b: CurveId,
349    },
350    Perpendicular {
351        a: CurveId,
352        b: CurveId,
353    },
354    /// Two lines on one infinite line.
355    Collinear {
356        a: CurveId,
357        b: CurveId,
358    },
359    /// Two curves meet tangentially: at a shared endpoint if they have one
360    /// (lines, arcs and splines), else a line touching a circle/arc, or two
361    /// circles/arcs touching each other.
362    Tangent {
363        a: CurveId,
364        b: CurveId,
365    },
366    /// Equal length (two lines) or equal radius (two circles/arcs).
367    Equal {
368        a: CurveId,
369        b: CurveId,
370    },
371    /// Two circles/arcs share a center.
372    Concentric {
373        a: CurveId,
374        b: CurveId,
375    },
376    /// `point` is the center of a circle or arc.
377    Center {
378        point: PointId,
379        curve: CurveId,
380    },
381    /// `point` is the midpoint of a line or arc.
382    Midpoint {
383        point: PointId,
384        curve: CurveId,
385    },
386    /// `a` and `b` are mirror images across `line`.
387    Symmetric {
388        a: PointId,
389        b: PointId,
390        line: CurveId,
391    },
392    /// `point` stays at `(x, y)`.
393    Fix {
394        point: PointId,
395        #[serde(with = "as_f64")]
396        x: Scalar,
397        #[serde(with = "as_f64")]
398        y: Scalar,
399    },
400    Distance {
401        a: PointId,
402        b: PointId,
403        #[serde(with = "as_f64")]
404        value: Scalar,
405    },
406    /// `b.x - a.x = value`.
407    DistanceX {
408        a: PointId,
409        b: PointId,
410        #[serde(with = "as_f64")]
411        value: Scalar,
412    },
413    /// `b.y - a.y = value`.
414    DistanceY {
415        a: PointId,
416        b: PointId,
417        #[serde(with = "as_f64")]
418        value: Scalar,
419    },
420    PointLineDistance {
421        point: PointId,
422        line: CurveId,
423        #[serde(with = "as_f64")]
424        value: Scalar,
425    },
426    /// Length of a line or arc.
427    Length {
428        curve: CurveId,
429        #[serde(with = "as_f64")]
430        value: Scalar,
431    },
432    /// Radius of a circle or arc.
433    Radius {
434        curve: CurveId,
435        #[serde(with = "as_f64")]
436        value: Scalar,
437    },
438    /// Diameter of a circle or arc.
439    Diameter {
440        curve: CurveId,
441        #[serde(with = "as_f64")]
442        value: Scalar,
443    },
444    /// The counter-clockwise angle from line `a`'s direction to line `b`'s.
445    Angle {
446        a: CurveId,
447        b: CurveId,
448        #[serde(with = "as_f64")]
449        value: Scalar,
450    },
451    /// `b` is `a` moved the way `by` carries its start onto its end: along
452    /// a line, shifted by it; round an arc, turned about the arc's center
453    /// by its sweep. How a pattern's copies follow what they copy (see
454    /// [`crate::copies`]).
455    Moved {
456        a: PointId,
457        b: PointId,
458        by: CurveId,
459    },
460    /// Two arcs turn by the same angle: an arc whose ends are the images of
461    /// another's, under a motion or a reflection, is then its image.
462    EqualSweep {
463        a: CurveId,
464        b: CurveId,
465    },
466    /// `b` runs `value` from `a`: two parallel lines `value` apart, or two
467    /// concentric circles/arcs whose radii differ by `value` (see
468    /// [`crate::offset`]).
469    Offset {
470        a: CurveId,
471        b: CurveId,
472        #[serde(with = "as_f64")]
473        value: Scalar,
474    },
475    /// The point `on` along the spline `b` lies the radius of the circle
476    /// `by` from the point `at` along the spline `a`, along `a`'s normal
477    /// there — or, `across`, only that far across it: anywhere on the line
478    /// `a`'s offset runs along there. Each as a fraction of the spline's
479    /// domain. The offset of a spline is no spline: a spline fitted through
480    /// such points is held to it (see [`crate::offset`]).
481    OffsetAt {
482        a: CurveId,
483        #[serde(with = "as_f64")]
484        at: Scalar,
485        b: CurveId,
486        #[serde(with = "as_f64")]
487        on: Scalar,
488        by: CurveId,
489        #[serde(default, skip_serializing_if = "is_false")]
490        across: bool,
491    },
492}
493
494impl Uses for Constraint {
495    fn points(&self) -> Vec<PointId> {
496        Constraint::points(self)
497    }
498
499    fn curves(&self) -> Vec<CurveId> {
500        Constraint::curves(self)
501    }
502
503    fn coincident(&self) -> Option<(PointId, PointId)> {
504        match *self {
505            Constraint::Coincident { a, b } => Some((a, b)),
506            _ => None,
507        }
508    }
509}
510
511impl Constraint {
512    /// The dimension it holds, if it holds one — an angle in radians: what
513    /// a dialog edits.
514    pub fn value(&self) -> Option<Scalar> {
515        use Constraint::*;
516        match *self {
517            Distance { value, .. }
518            | DistanceX { value, .. }
519            | DistanceY { value, .. }
520            | PointLineDistance { value, .. }
521            | Length { value, .. }
522            | Radius { value, .. }
523            | Diameter { value, .. }
524            | Offset { value, .. }
525            | Angle { value, .. } => Some(value),
526            _ => None,
527        }
528    }
529
530    /// Gives its dimension the value `v` — an angle in radians; nothing for
531    /// one without.
532    pub fn set_value(&mut self, v: Scalar) {
533        use Constraint::*;
534        match self {
535            Distance { value, .. }
536            | DistanceX { value, .. }
537            | DistanceY { value, .. }
538            | PointLineDistance { value, .. }
539            | Length { value, .. }
540            | Radius { value, .. }
541            | Diameter { value, .. }
542            | Offset { value, .. }
543            | Angle { value, .. } => *value = v,
544            _ => {}
545        }
546    }
547
548    /// The points this constraint refers to directly (curves aside).
549    pub fn points(&self) -> Vec<PointId> {
550        use Constraint::*;
551        match *self {
552            Coincident { a, b }
553            | Distance { a, b, .. }
554            | DistanceX { a, b, .. }
555            | DistanceY { a, b, .. }
556            | Symmetric { a, b, .. }
557            | Moved { a, b, .. } => vec![a, b],
558            PointOnCurve { point, .. }
559            | Midpoint { point, .. }
560            | Center { point, .. }
561            | Fix { point, .. }
562            | PointLineDistance { point, .. } => vec![point],
563            _ => Vec::new(),
564        }
565    }
566
567    /// The curves this constraint refers to.
568    pub fn curves(&self) -> Vec<CurveId> {
569        use Constraint::*;
570        match *self {
571            PointOnCurve { curve, .. }
572            | Midpoint { curve, .. }
573            | Center { curve, .. }
574            | Length { curve, .. }
575            | Radius { curve, .. }
576            | Diameter { curve, .. } => vec![curve],
577            Horizontal { line }
578            | Vertical { line }
579            | PointLineDistance { line, .. }
580            | Symmetric { line, .. }
581            | Moved { by: line, .. } => vec![line],
582            Parallel { a, b }
583            | Perpendicular { a, b }
584            | Collinear { a, b }
585            | Tangent { a, b }
586            | Equal { a, b }
587            | Concentric { a, b }
588            | Angle { a, b, .. }
589            | EqualSweep { a, b }
590            | Offset { a, b, .. } => vec![a, b],
591            OffsetAt { a, b, by, .. } => vec![a, b, by],
592            _ => Vec::new(),
593        }
594    }
595}
596
597/// Every point's `[x, y]`, by [`PointId`]: the sketch's own positions, as
598/// drawn (see [`Sketch::positions`]).
599pub type Positions = BTreeMap<PointId, Vector2>;
600
601/// A sketch's geometry as the kernel builds on it: every point, and every
602/// arc's sweep and circle's radius. Solved (see [`Sketch::enclose`]), each
603/// encloses the exact solution of the sketch's constraints; as drawn (see
604/// [`Enclosure::as_drawn`]), each is the sketch's own value, sharp. A rigid
605/// motion of either is one too (see [`crate::ProfileLoop::to_nurbs`]).
606#[derive(Clone, Debug, PartialEq)]
607pub struct Enclosure {
608    pub points: BTreeMap<PointId, Vector2>,
609    /// An arc's sweep, a circle's radius.
610    pub params: BTreeMap<CurveId, Scalar>,
611}
612
613impl Enclosure {
614    /// `sketch`'s geometry exactly as drawn: the sketch's own values.
615    pub fn as_drawn(sketch: &Sketch) -> Self {
616        Enclosure {
617            points: sketch.points.iter().map(|(&id, p)| (id, p.xy())).collect(),
618            params: sketch
619                .curves
620                .iter()
621                .filter_map(|(&id, c)| match c.kind {
622                    CurveKind::Arc { sweep, .. } => Some((id, sweep)),
623                    CurveKind::Circle { radius, .. } => Some((id, radius)),
624                    _ => None,
625                })
626                .collect(),
627        }
628    }
629}
630
631/// A constraint sketch.
632///
633/// Every entity is keyed by a stable id rather than stored by position: an id
634/// is handed out once, by the `add_*` methods, and never reused — not even
635/// after the entity is removed. That is what lets anything outside the sketch
636/// (a constraint, a profile, a topological name of the solid extruded from
637/// it) keep referring to "that line" while the sketch is edited around it.
638/// The maps are ordered, so a serialized sketch is deterministic and diffs
639/// line by line.
640#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
641pub struct Sketch {
642    pub points: BTreeMap<PointId, Point>,
643    pub curves: BTreeMap<CurveId, Curve>,
644    pub constraints: BTreeMap<ConstraintId, Constraint>,
645    /// The id the next added entity gets; greater than every id in use. One
646    /// counter for all three kinds, so an id `add_*` hands out is unique
647    /// across the sketch.
648    pub next_id: u64,
649}
650
651impl Sketch {
652    pub fn new() -> Self {
653        Self::default()
654    }
655
656    pub fn point(&self, id: PointId) -> GeopResult<&Point> {
657        self.points
658            .get(&id)
659            .ok_or_else(|| GeopError::new(format!("sketch has no point {id}")))
660    }
661
662    pub fn curve(&self, id: CurveId) -> GeopResult<&Curve> {
663        self.curves
664            .get(&id)
665            .ok_or_else(|| GeopError::new(format!("sketch has no curve {id}")))
666    }
667
668    /// Every point's `[x, y]`.
669    pub fn positions(&self) -> Positions {
670        self.points.iter().map(|(&id, p)| (id, p.xy())).collect()
671    }
672
673    fn fresh_id(&mut self) -> u64 {
674        let id = self.next_id;
675        self.next_id += 1;
676        id
677    }
678
679    pub fn add_point(&mut self, x: Scalar, y: Scalar) -> PointId {
680        let id = PointId(self.fresh_id());
681        self.points.insert(id, Point { x, y, fixed: false });
682        id
683    }
684
685    /// A point at `at`, as drawn.
686    pub fn new_point_at(&mut self, at: [f64; 2]) -> PointId {
687        self.add_point(Scalar::from_f64(at[0]), Scalar::from_f64(at[1]))
688    }
689
690    /// A point given from outside the sketch, at `(x, y)` (see the module
691    /// docs).
692    pub fn add_fixed_point(&mut self, x: Scalar, y: Scalar) -> PointId {
693        let id = PointId(self.fresh_id());
694        self.points.insert(id, Point { x, y, fixed: true });
695        id
696    }
697
698    /// Adds a curve of `kind`, profile geometry, solved for.
699    pub fn add_curve(&mut self, kind: CurveKind) -> CurveId {
700        let id = CurveId(self.fresh_id());
701        self.curves.insert(
702            id,
703            Curve {
704                kind,
705                construction: false,
706                fixed: false,
707            },
708        );
709        id
710    }
711
712    pub fn add_line(&mut self, start: PointId, end: PointId) -> CurveId {
713        self.add_curve(CurveKind::Line { start, end })
714    }
715
716    /// An arc from `start` to `end`, turning counter-clockwise by `sweep`
717    /// radians (clockwise if negative).
718    pub fn add_arc(&mut self, start: PointId, end: PointId, sweep: Scalar) -> CurveId {
719        self.add_curve(CurveKind::Arc { start, end, sweep })
720    }
721
722    pub fn add_circle(&mut self, center: PointId, radius: Scalar) -> CurveId {
723        self.add_curve(CurveKind::Circle { center, radius })
724    }
725
726    pub fn add_spline(&mut self, control_points: Vec<PointId>) -> CurveId {
727        self.add_curve(CurveKind::Spline {
728            control_points,
729            shape: None,
730        })
731    }
732
733    /// A text: `text`, its baseline from `anchor`, its capitals `height`
734    /// tall, turned by `angle` radians (see [`CurveKind::Text`]).
735    pub fn add_text(
736        &mut self,
737        anchor: PointId,
738        text: impl Into<String>,
739        height: Scalar,
740        angle: Scalar,
741    ) -> CurveId {
742        self.add_curve(CurveKind::Text {
743            anchor,
744            text: text.into(),
745            height,
746            angle,
747        })
748    }
749
750    pub fn set_construction(&mut self, curve: CurveId, construction: bool) {
751        if let Some(c) = self.curves.get_mut(&curve) {
752            c.construction = construction;
753        }
754    }
755
756    pub fn constrain(&mut self, constraint: Constraint) -> ConstraintId {
757        let id = ConstraintId(self.fresh_id());
758        self.constraints.insert(id, constraint);
759        id
760    }
761
762    /// Removes `points`, `curves` and `constraints`, and everything that
763    /// depends on them (see [`remove`]).
764    pub fn remove(&mut self, points: &[PointId], curves: &[CurveId], constraints: &[ConstraintId]) {
765        let (all_points, all_curves) = (&mut self.points, &mut self.curves);
766        remove(
767            all_points,
768            all_curves,
769            &mut self.constraints,
770            points,
771            curves,
772            constraints,
773        );
774    }
775
776    /// Check every reference and every constraint's operand kinds, so the
777    /// solver and profile code can rely on them.
778    pub fn validate(&self) -> GeopResult<()> {
779        let max_id = [
780            self.points.keys().last().map(|id| id.0),
781            self.curves.keys().last().map(|id| id.0),
782            self.constraints.keys().last().map(|id| id.0),
783        ];
784        if let Some(max_id) = max_id.into_iter().flatten().max()
785            && max_id >= self.next_id
786        {
787            return Err(GeopError::new(format!(
788                "sketch uses id {max_id}, but its next_id is only {}: new entities would reuse ids",
789                self.next_id
790            )));
791        }
792        for (&i, curve) in &self.curves {
793            for p in curve.points() {
794                let point = self
795                    .point(p)
796                    .map_err(|e| e.with_context(format!("curve {i}")))?;
797                if curve.fixed && !point.fixed {
798                    return Err(GeopError::new(format!(
799                        "curve {i} is fixed, but its point {p} is not"
800                    )));
801                }
802            }
803            match &curve.kind {
804                CurveKind::Line { start, end } | CurveKind::Arc { start, end, .. }
805                    if start == end =>
806                {
807                    return Err(GeopError::new(format!(
808                        "curve {i} starts and ends at the same point"
809                    )));
810                }
811                CurveKind::Arc { sweep, .. }
812                    if !sweep.is_finite()
813                        || !sweep.abs().definitely_less(Scalar::TWO.mul(Scalar::PI)) =>
814                {
815                    return Err(GeopError::new(format!(
816                        "arc {i} has sweep {sweep:?}, which is not definitely within (-2π, 2π)"
817                    )));
818                }
819                CurveKind::Spline { control_points, .. } if control_points.len() < 2 => {
820                    return Err(GeopError::new(format!(
821                        "spline {i} needs at least 2 control points"
822                    )));
823                }
824                CurveKind::Spline {
825                    control_points,
826                    shape: Some(shape),
827                } => {
828                    let n = control_points.len();
829                    if shape.degree == 0
830                        || shape.degree >= n
831                        || shape.knots.len() != n + shape.degree + 1
832                        || shape.weights.len() != n
833                        || !shape
834                            .weights
835                            .iter()
836                            .all(|w| w.definitely_greater(Scalar::ZERO))
837                    {
838                        return Err(GeopError::new(format!(
839                            "spline {i} of {n} control points has an inconsistent shape: {shape:?}"
840                        )));
841                    }
842                }
843                CurveKind::Text { height, angle, .. }
844                    if !height.is_finite()
845                        || !height.definitely_greater(Scalar::ZERO)
846                        || !angle.is_finite() =>
847                {
848                    return Err(GeopError::new(format!(
849                        "text {i} is {height:?} tall, turned by {angle:?}: a text is definitely taller than nothing, and turned by a finite angle"
850                    )));
851                }
852                _ => {}
853            }
854        }
855        for (&i, c) in &self.constraints {
856            self.validate_constraint(c)
857                .map_err(|e| e.with_context(format!("constraint {i} = {c:?}")))?;
858        }
859        Ok(())
860    }
861
862    /// Whether `c` could be a constraint of the sketch: its points and
863    /// curves are there, and of the kinds it constrains.
864    pub fn validate_constraint(&self, c: &Constraint) -> GeopResult<()> {
865        use Constraint::*;
866        for p in c.points() {
867            self.point(p)?;
868        }
869        let kind = |id: CurveId| self.curve(id).map(|c| &c.kind);
870        for curve in c.curves() {
871            if matches!(kind(curve)?, CurveKind::Text { .. }) {
872                return Err(GeopError::new(format!(
873                    "curve {curve} is a text, which takes no constraint: constrain the point it is placed at"
874                )));
875            }
876        }
877        let is_line = |id| Ok::<_, GeopError>(matches!(kind(id)?, CurveKind::Line { .. }));
878        let is_round = |id| {
879            Ok::<_, GeopError>(matches!(
880                kind(id)?,
881                CurveKind::Arc { .. } | CurveKind::Circle { .. }
882            ))
883        };
884        let need = |ok: bool, what: &str| {
885            if ok {
886                Ok(())
887            } else {
888                Err(GeopError::new(format!("operands must be {what}")))
889            }
890        };
891        match c {
892            Coincident { .. }
893            | Distance { .. }
894            | DistanceX { .. }
895            | DistanceY { .. }
896            | Fix { .. } => Ok(()),
897            PointOnCurve { curve, .. } => need(
898                !matches!(kind(*curve)?, CurveKind::Spline { .. }),
899                "a point and a line, arc or circle",
900            ),
901            Horizontal { line } | Vertical { line } => need(is_line(*line)?, "a line"),
902            Parallel { a, b }
903            | Perpendicular { a, b }
904            | Collinear { a, b }
905            | Angle { a, b, .. } => need(is_line(*a)? && is_line(*b)?, "two lines"),
906            Tangent { a, b } => {
907                let shared = self.shared_endpoint(*a, *b)?.is_some();
908                let two_lines = is_line(*a)? && is_line(*b)?;
909                let a_circle = matches!(kind(*a)?, CurveKind::Circle { .. });
910                let b_circle = matches!(kind(*b)?, CurveKind::Circle { .. });
911                let ok = shared && !(two_lines || a_circle || b_circle)
912                    || !shared
913                        && (is_round(*a)? && (is_round(*b)? || is_line(*b)?)
914                            || is_line(*a)? && is_round(*b)?);
915                need(
916                    ok,
917                    "curves sharing an endpoint (not two lines), a line and a circle/arc, or two circles/arcs",
918                )
919            }
920            Equal { a, b } | Offset { a, b, .. } => need(
921                is_line(*a)? && is_line(*b)? || is_round(*a)? && is_round(*b)?,
922                "two lines or two circles/arcs",
923            ),
924            Concentric { a, b } => need(is_round(*a)? && is_round(*b)?, "two circles/arcs"),
925            Center { curve, .. } => need(is_round(*curve)?, "a point and a circle or arc"),
926            Midpoint { curve, .. } => need(
927                matches!(
928                    kind(*curve)?,
929                    CurveKind::Line { .. } | CurveKind::Arc { .. }
930                ),
931                "a point and a line or arc",
932            ),
933            Symmetric { line, .. } => need(is_line(*line)?, "two points and a line"),
934            PointLineDistance { line, .. } => need(is_line(*line)?, "a point and a line"),
935            Length { curve, .. } => need(
936                matches!(
937                    kind(*curve)?,
938                    CurveKind::Line { .. } | CurveKind::Arc { .. }
939                ),
940                "a line or arc",
941            ),
942            Radius { curve, .. } | Diameter { curve, .. } => {
943                need(is_round(*curve)?, "a circle or arc")
944            }
945            Moved { by, .. } => need(
946                matches!(kind(*by)?, CurveKind::Line { .. } | CurveKind::Arc { .. }),
947                "two points and a line or arc",
948            ),
949            EqualSweep { a, b } => need(
950                matches!(kind(*a)?, CurveKind::Arc { .. })
951                    && matches!(kind(*b)?, CurveKind::Arc { .. }),
952                "two arcs",
953            ),
954            OffsetAt { a, b, by, .. } => need(
955                matches!(kind(*a)?, CurveKind::Spline { .. })
956                    && matches!(kind(*b)?, CurveKind::Spline { .. })
957                    && matches!(kind(*by)?, CurveKind::Circle { .. }),
958                "two splines and a circle",
959            ),
960        }
961    }
962
963    /// Union-find representative of every point under
964    /// [`Constraint::Coincident`]: points with the same representative are
965    /// one point.
966    pub fn point_classes(&self) -> BTreeMap<PointId, PointId> {
967        point_classes(&self.points, &self.constraints)
968    }
969
970    /// Every point the constraints put on the infinite line through the line
971    /// `line`: its endpoints, points constrained onto it
972    /// ([`Constraint::PointOnCurve`], a [`Constraint::Midpoint`] of it, an
973    /// endpoint of a [`Constraint::Collinear`] partner), and any point
974    /// coincident with one of those.
975    ///
976    /// Structural, like all connectivity here: a point that merely happens
977    /// to lie on the line is not reported.
978    pub fn on_line(&self, line: CurveId) -> GeopResult<BTreeSet<PointId>> {
979        let CurveKind::Line { start, end } = self.curve(line)?.kind else {
980            return Err(GeopError::new(format!("curve {line} is not a line")));
981        };
982        let class = self.point_classes();
983        let mut on = BTreeSet::new();
984        let mut mark = |p: PointId| {
985            on.insert(class[&p]);
986        };
987        mark(start);
988        mark(end);
989        for c in self.constraints.values() {
990            match *c {
991                Constraint::PointOnCurve { point, curve }
992                | Constraint::Midpoint { point, curve }
993                    if curve == line =>
994                {
995                    mark(point)
996                }
997                Constraint::Collinear { a, b } if a == line || b == line => {
998                    let other = if a == line { b } else { a };
999                    for p in self.curve(other)?.points() {
1000                        mark(p);
1001                    }
1002                }
1003                _ => {}
1004            }
1005        }
1006        Ok(class
1007            .iter()
1008            .filter(|(_, rep)| on.contains(rep))
1009            .map(|(&p, _)| p)
1010            .collect())
1011    }
1012
1013    /// Whether a [`Constraint::Tangent`] ties the curves `a` and `b`.
1014    pub fn tangent(&self, a: CurveId, b: CurveId) -> bool {
1015        self.constraints.values().any(|c| {
1016            matches!(*c, Constraint::Tangent { a: x, b: y } if (x, y) == (a, b) || (x, y) == (b, a))
1017        })
1018    }
1019
1020    /// Which ends `(a_at_end, b_at_end)` of open curves `a` and `b` are the
1021    /// same point, if any (`false` = start, `true` = end).
1022    pub fn shared_endpoint(&self, a: CurveId, b: CurveId) -> GeopResult<Option<(bool, bool)>> {
1023        let (Some((a0, a1)), Some((b0, b1))) =
1024            (self.curve(a)?.endpoints(), self.curve(b)?.endpoints())
1025        else {
1026            return Ok(None);
1027        };
1028        let class = self.point_classes();
1029        let same = |p: PointId, q: PointId| class[&p] == class[&q];
1030        Ok([(false, false), (false, true), (true, false), (true, true)]
1031            .into_iter()
1032            .find(|&(ea, eb)| same(if ea { a1 } else { a0 }, if eb { b1 } else { b0 })))
1033    }
1034}