Skip to main content

brepkit_sketch/gcs/
system.rs

1//! GCS system: CRUD operations, parameter management, and solve orchestration.
2
3use std::collections::HashMap;
4
5use crate::SketchError;
6
7use super::constraint::{
8    Constraint, ConstraintEntry, ConstraintId, EntitySnapshot, JacobianWriter, eval_jacobian,
9    eval_residuals, residual_count,
10};
11use super::dof::{self, DofAnalysis};
12use super::entity::{
13    ArcData, ArcId, CircleData, CircleId, GenArena, LineData, LineId, ParamRef, PointData, PointId,
14};
15use super::solver::{self, SolveResult};
16
17/// The geometric constraint system.
18///
19/// Owns all entities (points, lines, circles) and constraints.
20/// Provides CRUD operations and orchestrates the solver.
21pub struct GcsSystem {
22    points: GenArena<PointData>,
23    lines: GenArena<LineData>,
24    circles: GenArena<CircleData>,
25    arcs: GenArena<ArcData>,
26    constraints: GenArena<ConstraintEntry>,
27    /// Internal constraints auto-added by `add_arc` (center–end distance).
28    /// Keyed by `ArcId` so they can be removed with the arc.
29    arc_internal_constraints: HashMap<ArcId, ConstraintId>,
30    /// Cached parameter map (rebuilt when dirty).
31    param_map: Vec<ParamRef>,
32    /// Map from `ParamRef` to index in param_map.
33    param_index: HashMap<ParamRef, usize>,
34    /// Whether the param map needs rebuilding.
35    dirty: bool,
36}
37
38impl Clone for GcsSystem {
39    fn clone(&self) -> Self {
40        Self {
41            points: self.points.clone(),
42            lines: self.lines.clone(),
43            circles: self.circles.clone(),
44            arcs: self.arcs.clone(),
45            constraints: self.constraints.clone(),
46            arc_internal_constraints: self.arc_internal_constraints.clone(),
47            param_map: self.param_map.clone(),
48            param_index: self.param_index.clone(),
49            dirty: self.dirty,
50        }
51    }
52}
53
54impl Default for GcsSystem {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60impl GcsSystem {
61    /// Create a new empty GCS.
62    #[must_use]
63    pub fn new() -> Self {
64        Self {
65            points: GenArena::new(),
66            lines: GenArena::new(),
67            circles: GenArena::new(),
68            arcs: GenArena::new(),
69            constraints: GenArena::new(),
70            arc_internal_constraints: HashMap::new(),
71            param_map: Vec::new(),
72            param_index: HashMap::new(),
73            dirty: false,
74        }
75    }
76
77    /// Add a point. Returns its handle.
78    pub fn add_point(&mut self, data: PointData) -> PointId {
79        self.dirty = true;
80        self.points.insert(data)
81    }
82
83    /// Get a point by handle.
84    #[must_use]
85    pub fn point(&self, id: PointId) -> Option<&PointData> {
86        self.points.get(id)
87    }
88
89    /// Get a mutable reference to a point.
90    pub fn point_mut(&mut self, id: PointId) -> Option<&mut PointData> {
91        self.points.get_mut(id)
92    }
93
94    /// Remove a point. Fails if referenced by any line, circle, or constraint.
95    ///
96    /// # Errors
97    ///
98    /// Returns `SketchError::EntityInUse` if the point is referenced by a line,
99    /// circle, or constraint. Returns `SketchError::InvalidHandle` if the handle
100    /// is stale or invalid.
101    pub fn remove_point(&mut self, id: PointId) -> Result<PointData, SketchError> {
102        for (_, line) in self.lines.iter() {
103            if line.p1 == id || line.p2 == id {
104                return Err(SketchError::EntityInUse);
105            }
106        }
107        for (_, circle) in self.circles.iter() {
108            if circle.center == id {
109                return Err(SketchError::EntityInUse);
110            }
111        }
112        for (_, arc) in self.arcs.iter() {
113            if arc.center == id || arc.start == id || arc.end == id {
114                return Err(SketchError::EntityInUse);
115            }
116        }
117        for (_, entry) in self.constraints.iter() {
118            if constraint_references_point(&entry.constraint, id) {
119                return Err(SketchError::EntityInUse);
120            }
121        }
122        self.dirty = true;
123        self.points.remove(id).ok_or(SketchError::InvalidHandle)
124    }
125
126    /// Add a line between two existing points.
127    ///
128    /// # Errors
129    ///
130    /// Returns `SketchError::InvalidHandle` if either point handle is invalid.
131    pub fn add_line(&mut self, p1: PointId, p2: PointId) -> Result<LineId, SketchError> {
132        if !self.points.contains(p1) || !self.points.contains(p2) {
133            return Err(SketchError::InvalidHandle);
134        }
135        Ok(self.lines.insert(LineData { p1, p2 }))
136    }
137
138    /// Get a line by handle.
139    #[must_use]
140    pub fn line(&self, id: LineId) -> Option<&LineData> {
141        self.lines.get(id)
142    }
143
144    /// Remove a line. Fails if referenced by any constraint.
145    ///
146    /// # Errors
147    ///
148    /// Returns `SketchError::EntityInUse` if the line is referenced by a constraint.
149    /// Returns `SketchError::InvalidHandle` if the handle is stale or invalid.
150    pub fn remove_line(&mut self, id: LineId) -> Result<LineData, SketchError> {
151        for (_, entry) in self.constraints.iter() {
152            if constraint_references_line(&entry.constraint, id) {
153                return Err(SketchError::EntityInUse);
154            }
155        }
156        self.lines.remove(id).ok_or(SketchError::InvalidHandle)
157    }
158
159    /// Add a circle with a center point and radius.
160    ///
161    /// # Errors
162    ///
163    /// Returns `SketchError::InvalidHandle` if the center point handle is invalid.
164    pub fn add_circle(&mut self, center: PointId, radius: f64) -> Result<CircleId, SketchError> {
165        if !self.points.contains(center) {
166            return Err(SketchError::InvalidHandle);
167        }
168        self.dirty = true;
169        Ok(self.circles.insert(CircleData { center, radius }))
170    }
171
172    /// Get a circle by handle.
173    #[must_use]
174    pub fn circle(&self, id: CircleId) -> Option<&CircleData> {
175        self.circles.get(id)
176    }
177
178    /// Remove a circle. Fails if referenced by any constraint.
179    ///
180    /// # Errors
181    ///
182    /// Returns `SketchError::EntityInUse` if the circle is referenced by a constraint.
183    /// Returns `SketchError::InvalidHandle` if the handle is stale or invalid.
184    pub fn remove_circle(&mut self, id: CircleId) -> Result<CircleData, SketchError> {
185        for (_, entry) in self.constraints.iter() {
186            if constraint_references_circle(&entry.constraint, id) {
187                return Err(SketchError::EntityInUse);
188            }
189        }
190        self.dirty = true;
191        self.circles.remove(id).ok_or(SketchError::InvalidHandle)
192    }
193
194    /// Add an arc defined by center, start, and end points.
195    ///
196    /// Auto-adds an internal `PointOnArc(end, arc)` constraint so that
197    /// `dist(center, end) == dist(center, start)` is maintained dynamically
198    /// as the start point moves.
199    ///
200    /// # Errors
201    ///
202    /// Returns `SketchError::InvalidHandle` if any point handle is invalid.
203    pub fn add_arc(
204        &mut self,
205        center: PointId,
206        start: PointId,
207        end: PointId,
208    ) -> Result<ArcId, SketchError> {
209        self.check_point(center)?;
210        self.check_point(start)?;
211        self.check_point(end)?;
212
213        let arc_id = self.arcs.insert(ArcData { center, start, end });
214
215        // Internal constraint: end point must lie on the arc's circle
216        // (dynamically tracks dist(center, start) rather than a frozen radius)
217        let cid = self.constraints.insert(ConstraintEntry {
218            constraint: Constraint::PointOnArc(end, arc_id),
219        });
220        self.arc_internal_constraints.insert(arc_id, cid);
221
222        self.dirty = true;
223        Ok(arc_id)
224    }
225
226    /// Get an arc by handle.
227    #[must_use]
228    pub fn arc(&self, id: ArcId) -> Option<&ArcData> {
229        self.arcs.get(id)
230    }
231
232    /// Get a mutable reference to an arc.
233    pub fn arc_mut(&mut self, id: ArcId) -> Option<&mut ArcData> {
234        self.arcs.get_mut(id)
235    }
236
237    /// Remove an arc. Fails if referenced by any user constraint.
238    ///
239    /// Also removes the internal distance constraint that was auto-added
240    /// by [`add_arc`](Self::add_arc).
241    ///
242    /// # Errors
243    ///
244    /// Returns `SketchError::EntityInUse` if the arc is referenced by a constraint.
245    /// Returns `SketchError::InvalidHandle` if the handle is stale or invalid.
246    pub fn remove_arc(&mut self, id: ArcId) -> Result<ArcData, SketchError> {
247        for (cid, entry) in self.constraints.iter() {
248            if self.arc_internal_constraints.get(&id) == Some(&cid) {
249                continue;
250            }
251            if constraint_references_arc(&entry.constraint, id) {
252                return Err(SketchError::EntityInUse);
253            }
254        }
255
256        if let Some(cid) = self.arc_internal_constraints.remove(&id) {
257            self.constraints.remove(cid);
258        }
259
260        self.dirty = true;
261        self.arcs.remove(id).ok_or(SketchError::InvalidHandle)
262    }
263
264    /// Number of arcs.
265    #[must_use]
266    pub fn arc_count(&self) -> usize {
267        self.arcs.len()
268    }
269
270    /// Iterate over all arcs.
271    pub fn arcs(&self) -> impl Iterator<Item = (ArcId, &ArcData)> {
272        self.arcs.iter()
273    }
274
275    /// Add a constraint. Validates that all referenced entities exist.
276    ///
277    /// # Errors
278    ///
279    /// Returns `SketchError::InvalidHandle` if any entity referenced by the
280    /// constraint does not exist.
281    pub fn add_constraint(&mut self, constraint: Constraint) -> Result<ConstraintId, SketchError> {
282        self.validate_constraint(&constraint)?;
283        self.dirty = true;
284        Ok(self.constraints.insert(ConstraintEntry { constraint }))
285    }
286
287    /// Remove a constraint by handle.
288    ///
289    /// # Errors
290    ///
291    /// Returns `SketchError::InvalidHandle` if the handle is stale or invalid.
292    pub fn remove_constraint(&mut self, id: ConstraintId) -> Result<(), SketchError> {
293        self.constraints
294            .remove(id)
295            .map(|_| {
296                self.dirty = true;
297            })
298            .ok_or(SketchError::InvalidHandle)
299    }
300
301    /// Get a constraint by handle.
302    #[must_use]
303    pub fn constraint(&self, id: ConstraintId) -> Option<&Constraint> {
304        self.constraints.get(id).map(|e| &e.constraint)
305    }
306
307    /// Number of constraints (includes internal arc constraints).
308    #[must_use]
309    pub fn constraint_count(&self) -> usize {
310        self.constraints.len()
311    }
312
313    /// Number of points.
314    #[must_use]
315    pub fn point_count(&self) -> usize {
316        self.points.len()
317    }
318
319    /// Number of lines.
320    #[must_use]
321    pub fn line_count(&self) -> usize {
322        self.lines.len()
323    }
324
325    /// Number of circles.
326    #[must_use]
327    pub fn circle_count(&self) -> usize {
328        self.circles.len()
329    }
330
331    /// Solve the constraint system.
332    ///
333    /// Modifies entity positions in-place to satisfy all constraints.
334    ///
335    /// # Errors
336    ///
337    /// Returns `SketchError` if the system parameters are in an invalid state.
338    /// The `Result` wrapper is retained for future error paths (e.g. singular
339    /// Jacobian detection).
340    #[allow(clippy::unnecessary_wraps)]
341    pub fn solve(
342        &mut self,
343        max_iterations: usize,
344        tolerance: f64,
345    ) -> Result<SolveResult, SketchError> {
346        self.rebuild_if_dirty();
347
348        let n = self.param_map.len();
349        let m: usize = self
350            .constraints
351            .iter()
352            .map(|(_, e)| residual_count(&e.constraint))
353            .sum();
354
355        if n == 0 {
356            // No free params — just check residuals
357            let snap = self.build_snapshot();
358            let mut residuals = Vec::with_capacity(m);
359            for (_, entry) in self.constraints.iter() {
360                eval_residuals(&entry.constraint, &snap, &mut residuals);
361            }
362            let max_r = residuals.iter().fold(0.0_f64, |a, &b| a.max(b.abs()));
363            return Ok(SolveResult {
364                converged: max_r < tolerance,
365                iterations: 0,
366                max_residual: max_r,
367            });
368        }
369
370        let mut params = self.extract_params();
371        let param_index = self.param_index.clone();
372        let param_map = self.param_map.clone();
373
374        let constraints: Vec<Constraint> = self
375            .constraints
376            .iter()
377            .map(|(_, e)| e.constraint.clone())
378            .collect();
379
380        let residual_fn = |p: &[f64]| -> Vec<f64> {
381            let snap = build_snapshot_from_params(p, &param_map, &param_index, self);
382            let mut r = Vec::with_capacity(m);
383            for c in &constraints {
384                eval_residuals(c, &snap, &mut r);
385            }
386            r
387        };
388
389        let jacobian_fn = |p: &[f64]| -> Vec<f64> {
390            let snap = build_snapshot_from_params(p, &param_map, &param_index, self);
391            let mut jac = vec![0.0; m * n];
392            let mut row = 0;
393            {
394                let mut jw = JacobianWriter {
395                    data: &mut jac,
396                    ncols: n,
397                    param_index: &param_index,
398                };
399                for c in &constraints {
400                    eval_jacobian(c, &snap, &mut jw, row);
401                    row += residual_count(c);
402                }
403            }
404            jac
405        };
406
407        let result = solver::solve_dogleg(
408            &mut params,
409            &residual_fn,
410            &jacobian_fn,
411            m,
412            max_iterations,
413            tolerance,
414        );
415
416        self.write_params(&params);
417
418        Ok(result)
419    }
420
421    /// Analyze degrees of freedom in the current system.
422    pub fn dof(&mut self) -> DofAnalysis {
423        self.rebuild_if_dirty();
424
425        let n = self.param_map.len();
426        let m: usize = self
427            .constraints
428            .iter()
429            .map(|(_, e)| residual_count(&e.constraint))
430            .sum();
431
432        if n == 0 || m == 0 {
433            return DofAnalysis {
434                dof: n,
435                rank: 0,
436                num_params: n,
437                num_equations: m,
438            };
439        }
440
441        let params = self.extract_params();
442        let snap = self.build_snapshot();
443        let mut jac = vec![0.0; m * n];
444        let mut row = 0;
445        {
446            let mut jw = JacobianWriter {
447                data: &mut jac,
448                ncols: n,
449                param_index: &self.param_index,
450            };
451            for (_, entry) in self.constraints.iter() {
452                eval_jacobian(&entry.constraint, &snap, &mut jw, row);
453                row += residual_count(&entry.constraint);
454            }
455        }
456        let _ = params; // params were needed to build snapshot
457
458        dof::analyze(&jac, m, n)
459    }
460
461    /// Iterate over all points.
462    pub fn points(&self) -> impl Iterator<Item = (PointId, &PointData)> {
463        self.points.iter()
464    }
465
466    /// Iterate over all lines.
467    pub fn lines(&self) -> impl Iterator<Item = (LineId, &LineData)> {
468        self.lines.iter()
469    }
470
471    /// Iterate over all circles.
472    pub fn circles(&self) -> impl Iterator<Item = (CircleId, &CircleData)> {
473        self.circles.iter()
474    }
475
476    /// Rebuild parameter map if dirty.
477    fn rebuild_if_dirty(&mut self) {
478        if !self.dirty {
479            return;
480        }
481        self.param_map.clear();
482        self.param_index.clear();
483
484        for (id, data) in self.points.iter() {
485            if !data.fixed {
486                let idx = self.param_map.len();
487                self.param_map.push(ParamRef::PointX(id));
488                self.param_index.insert(ParamRef::PointX(id), idx);
489                let idx = self.param_map.len();
490                self.param_map.push(ParamRef::PointY(id));
491                self.param_index.insert(ParamRef::PointY(id), idx);
492            }
493        }
494
495        for (id, _) in self.circles.iter() {
496            let idx = self.param_map.len();
497            self.param_map.push(ParamRef::CircleRadius(id));
498            self.param_index.insert(ParamRef::CircleRadius(id), idx);
499        }
500
501        self.dirty = false;
502    }
503
504    /// Extract parameter values from entities.
505    fn extract_params(&self) -> Vec<f64> {
506        self.param_map
507            .iter()
508            .map(|pr| match pr {
509                ParamRef::PointX(id) => self.points.get(*id).map_or(0.0, |p| p.x),
510                ParamRef::PointY(id) => self.points.get(*id).map_or(0.0, |p| p.y),
511                ParamRef::CircleRadius(id) => self.circles.get(*id).map_or(0.0, |c| c.radius),
512            })
513            .collect()
514    }
515
516    /// Write parameter values back to entities.
517    fn write_params(&mut self, params: &[f64]) {
518        for (i, pr) in self.param_map.iter().enumerate() {
519            match pr {
520                ParamRef::PointX(id) => {
521                    if let Some(p) = self.points.get_mut(*id) {
522                        p.x = params[i];
523                    }
524                }
525                ParamRef::PointY(id) => {
526                    if let Some(p) = self.points.get_mut(*id) {
527                        p.y = params[i];
528                    }
529                }
530                ParamRef::CircleRadius(id) => {
531                    if let Some(c) = self.circles.get_mut(*id) {
532                        c.radius = params[i];
533                    }
534                }
535            }
536        }
537    }
538
539    /// Build an entity snapshot for residual/Jacobian evaluation.
540    fn build_snapshot(&self) -> EntitySnapshot {
541        EntitySnapshot {
542            points: self.points.iter().map(|(id, d)| (id, (d.x, d.y))).collect(),
543            lines: self
544                .lines
545                .iter()
546                .map(|(id, d)| (id, (d.p1, d.p2)))
547                .collect(),
548            circles: self
549                .circles
550                .iter()
551                .map(|(id, d)| (id, (d.center, d.radius)))
552                .collect(),
553            arcs: self
554                .arcs
555                .iter()
556                .map(|(id, d)| (id, (d.center, d.start, d.end)))
557                .collect(),
558        }
559    }
560
561    /// Validate all entity references in a constraint.
562    fn validate_constraint(&self, c: &Constraint) -> Result<(), SketchError> {
563        match c {
564            Constraint::Coincident(p1, p2) | Constraint::Distance(p1, p2, _) => {
565                self.check_point(*p1)?;
566                self.check_point(*p2)?;
567            }
568            Constraint::PointLineDistance(pt, line, _) => {
569                self.check_point(*pt)?;
570                self.check_line(*line)?;
571            }
572            Constraint::FixX(p, _) | Constraint::FixY(p, _) => {
573                self.check_point(*p)?;
574            }
575            Constraint::Horizontal(line) | Constraint::Vertical(line) => {
576                self.check_line(*line)?;
577            }
578            Constraint::Angle(l1, l2, _)
579            | Constraint::Perpendicular(l1, l2)
580            | Constraint::Parallel(l1, l2) => {
581                self.check_line(*l1)?;
582                self.check_line(*l2)?;
583            }
584            Constraint::PointOnCircle(pt, circ) => {
585                self.check_point(*pt)?;
586                self.check_circle(*circ)?;
587            }
588            Constraint::PointOnArc(pt, arc) => {
589                self.check_point(*pt)?;
590                self.check_arc(*arc)?;
591            }
592            Constraint::TangentLineArc(line, arc, shared) => {
593                self.check_line(*line)?;
594                self.check_arc(*arc)?;
595                self.check_point(*shared)?;
596            }
597            Constraint::TangentArcArc(arc1, arc2, shared) => {
598                self.check_arc(*arc1)?;
599                self.check_arc(*arc2)?;
600                self.check_point(*shared)?;
601            }
602            Constraint::EqualRadiusArcArc(arc1, arc2) => {
603                self.check_arc(*arc1)?;
604                self.check_arc(*arc2)?;
605            }
606            Constraint::EqualRadiusArcCircle(arc, circ) => {
607                self.check_arc(*arc)?;
608                self.check_circle(*circ)?;
609            }
610            Constraint::ArcLength(arc, _) => {
611                self.check_arc(*arc)?;
612            }
613            Constraint::ConcentricArcArc(arc1, arc2) => {
614                self.check_arc(*arc1)?;
615                self.check_arc(*arc2)?;
616            }
617            Constraint::ConcentricArcCircle(arc, circ) => {
618                self.check_arc(*arc)?;
619                self.check_circle(*circ)?;
620            }
621        }
622        Ok(())
623    }
624
625    fn check_point(&self, id: PointId) -> Result<(), SketchError> {
626        if self.points.contains(id) {
627            Ok(())
628        } else {
629            Err(SketchError::InvalidHandle)
630        }
631    }
632
633    fn check_line(&self, id: LineId) -> Result<(), SketchError> {
634        if self.lines.contains(id) {
635            Ok(())
636        } else {
637            Err(SketchError::InvalidHandle)
638        }
639    }
640
641    fn check_circle(&self, id: CircleId) -> Result<(), SketchError> {
642        if self.circles.contains(id) {
643            Ok(())
644        } else {
645            Err(SketchError::InvalidHandle)
646        }
647    }
648
649    fn check_arc(&self, id: ArcId) -> Result<(), SketchError> {
650        if self.arcs.contains(id) {
651            Ok(())
652        } else {
653            Err(SketchError::InvalidHandle)
654        }
655    }
656}
657
658/// Build a snapshot from parameter values (used in solver closures).
659fn build_snapshot_from_params(
660    params: &[f64],
661    _param_map: &[ParamRef],
662    param_index: &HashMap<ParamRef, usize>,
663    sys: &GcsSystem,
664) -> EntitySnapshot {
665    let points = sys
666        .points
667        .iter()
668        .map(|(id, data)| {
669            let x = param_index
670                .get(&ParamRef::PointX(id))
671                .map_or(data.x, |&i| params[i]);
672            let y = param_index
673                .get(&ParamRef::PointY(id))
674                .map_or(data.y, |&i| params[i]);
675            (id, (x, y))
676        })
677        .collect();
678
679    let lines = sys.lines.iter().map(|(id, d)| (id, (d.p1, d.p2))).collect();
680
681    let circles = sys
682        .circles
683        .iter()
684        .map(|(id, data)| {
685            let r = param_index
686                .get(&ParamRef::CircleRadius(id))
687                .map_or(data.radius, |&i| params[i]);
688            (id, (data.center, r))
689        })
690        .collect();
691
692    let arcs = sys
693        .arcs
694        .iter()
695        .map(|(id, d)| (id, (d.center, d.start, d.end)))
696        .collect();
697
698    EntitySnapshot {
699        points,
700        lines,
701        circles,
702        arcs,
703    }
704}
705
706/// Check if a constraint references a specific point.
707fn constraint_references_point(c: &Constraint, id: PointId) -> bool {
708    match c {
709        Constraint::Coincident(p1, p2) | Constraint::Distance(p1, p2, _) => *p1 == id || *p2 == id,
710        Constraint::PointLineDistance(pt, _, _)
711        | Constraint::PointOnCircle(pt, _)
712        | Constraint::PointOnArc(pt, _) => *pt == id,
713        Constraint::FixX(p, _) | Constraint::FixY(p, _) => *p == id,
714        Constraint::TangentLineArc(_, _, shared) | Constraint::TangentArcArc(_, _, shared) => {
715            *shared == id
716        }
717        Constraint::Horizontal(_)
718        | Constraint::Vertical(_)
719        | Constraint::Angle(_, _, _)
720        | Constraint::Perpendicular(_, _)
721        | Constraint::Parallel(_, _)
722        | Constraint::EqualRadiusArcArc(_, _)
723        | Constraint::EqualRadiusArcCircle(_, _)
724        | Constraint::ArcLength(_, _)
725        | Constraint::ConcentricArcArc(_, _)
726        | Constraint::ConcentricArcCircle(_, _) => false,
727    }
728}
729
730/// Check if a constraint references a specific line.
731fn constraint_references_line(c: &Constraint, id: LineId) -> bool {
732    match c {
733        Constraint::Horizontal(l) | Constraint::Vertical(l) => *l == id,
734        Constraint::PointLineDistance(_, l, _) => *l == id,
735        Constraint::TangentLineArc(l, _, _) => *l == id,
736        Constraint::Angle(l1, l2, _)
737        | Constraint::Perpendicular(l1, l2)
738        | Constraint::Parallel(l1, l2) => *l1 == id || *l2 == id,
739        Constraint::Coincident(_, _)
740        | Constraint::Distance(_, _, _)
741        | Constraint::FixX(_, _)
742        | Constraint::FixY(_, _)
743        | Constraint::PointOnCircle(_, _)
744        | Constraint::PointOnArc(_, _)
745        | Constraint::TangentArcArc(_, _, _)
746        | Constraint::EqualRadiusArcArc(_, _)
747        | Constraint::EqualRadiusArcCircle(_, _)
748        | Constraint::ArcLength(_, _)
749        | Constraint::ConcentricArcArc(_, _)
750        | Constraint::ConcentricArcCircle(_, _) => false,
751    }
752}
753
754/// Check if a constraint references a specific circle.
755fn constraint_references_circle(c: &Constraint, id: CircleId) -> bool {
756    match c {
757        Constraint::PointOnCircle(_, circ) => *circ == id,
758        Constraint::EqualRadiusArcCircle(_, circ) | Constraint::ConcentricArcCircle(_, circ) => {
759            *circ == id
760        }
761        Constraint::Coincident(_, _)
762        | Constraint::Distance(_, _, _)
763        | Constraint::PointLineDistance(_, _, _)
764        | Constraint::FixX(_, _)
765        | Constraint::FixY(_, _)
766        | Constraint::Horizontal(_)
767        | Constraint::Vertical(_)
768        | Constraint::Angle(_, _, _)
769        | Constraint::Perpendicular(_, _)
770        | Constraint::Parallel(_, _)
771        | Constraint::PointOnArc(_, _)
772        | Constraint::TangentLineArc(_, _, _)
773        | Constraint::TangentArcArc(_, _, _)
774        | Constraint::EqualRadiusArcArc(_, _)
775        | Constraint::ArcLength(_, _)
776        | Constraint::ConcentricArcArc(_, _) => false,
777    }
778}
779
780/// Check if a constraint references a specific arc.
781fn constraint_references_arc(c: &Constraint, id: ArcId) -> bool {
782    match c {
783        Constraint::PointOnArc(_, arc) | Constraint::ArcLength(arc, _) => *arc == id,
784        Constraint::TangentLineArc(_, arc, _) => *arc == id,
785        Constraint::TangentArcArc(a1, a2, _)
786        | Constraint::EqualRadiusArcArc(a1, a2)
787        | Constraint::ConcentricArcArc(a1, a2) => *a1 == id || *a2 == id,
788        Constraint::EqualRadiusArcCircle(arc, _) | Constraint::ConcentricArcCircle(arc, _) => {
789            *arc == id
790        }
791        Constraint::Coincident(_, _)
792        | Constraint::Distance(_, _, _)
793        | Constraint::PointLineDistance(_, _, _)
794        | Constraint::FixX(_, _)
795        | Constraint::FixY(_, _)
796        | Constraint::Horizontal(_)
797        | Constraint::Vertical(_)
798        | Constraint::Angle(_, _, _)
799        | Constraint::Perpendicular(_, _)
800        | Constraint::Parallel(_, _)
801        | Constraint::PointOnCircle(_, _) => false,
802    }
803}
804
805#[cfg(test)]
806#[allow(clippy::unwrap_used, clippy::expect_used)]
807mod tests;