Skip to main content

kcl_ezpz/textual/
executor.rs

1use std::collections::HashMap;
2
3use indexmap::IndexMap;
4
5use crate::Analysis;
6use crate::Config;
7use crate::Constraint;
8use crate::ConstraintRequest;
9use crate::FailureOutcome;
10use crate::FreedomAnalysis;
11use crate::IdGenerator;
12use crate::NoAnalysis;
13use crate::SolveOutcome;
14use crate::SolveOutcomeAnalysis;
15use crate::Warning;
16use crate::datatypes;
17use crate::datatypes::AngleKind;
18use crate::datatypes::inputs::DatumCircularArc;
19use crate::datatypes::inputs::DatumDistance;
20use crate::datatypes::inputs::DatumLineSegment;
21use crate::datatypes::inputs::DatumPoint;
22use crate::datatypes::outputs::Arc;
23use crate::datatypes::outputs::{Circle, Component, Point};
24use crate::error::TextualError;
25use crate::textual::Label;
26use crate::textual::geometry_variables::DoneState;
27use crate::textual::geometry_variables::GeometryVariables;
28use crate::textual::geometry_variables::PointsState;
29use crate::textual::geometry_variables::VARS_PER_ARC;
30use crate::textual::instruction::*;
31
32use super::Instruction;
33use super::Problem;
34
35impl Problem {
36    /// Build a [`ConstraintSystem`] which models the system in this problem.
37    /// Error means this problem was not properly specified, e.g. it could be
38    /// missing a variable used in a constraint.
39    pub fn to_constraint_system(&self) -> Result<ConstraintSystem<'_>, TextualError> {
40        let mut id_generator = IdGenerator::default();
41        // First, construct the list of initial guesses,
42        // and assign them to solver variables.
43        let mut initial_guesses: GeometryVariables<PointsState> = Default::default();
44        // Maps labels to points
45        let mut guessmap_points = HashMap::new();
46        guessmap_points.extend(
47            self.point_guesses
48                .iter()
49                .map(|pg| (pg.point.0.clone(), pg.guess)),
50        );
51        for point in &self.inner_points {
52            let Some(guess) = guessmap_points.remove(&point.0) else {
53                return Err(TextualError::MissingGuess {
54                    label: point.0.clone(),
55                });
56            };
57            initial_guesses.push_point(&mut id_generator, guess.x, guess.y);
58        }
59        let mut guessmap_scalars = HashMap::new();
60        guessmap_scalars.extend(
61            self.scalar_guesses
62                .iter()
63                .map(|sg| (sg.scalar.0.clone(), sg.guess)),
64        );
65        let mut initial_guesses = initial_guesses.done();
66        for circle in &self.inner_circles {
67            // Each circle should have a guess for its center and radius.
68            // First, find the guess for its center:
69            let center_label = format!("{}.center", circle.0);
70            let Some(center_guess) = guessmap_points.remove(&center_label) else {
71                return Err(TextualError::MissingGuess {
72                    label: center_label,
73                });
74            };
75            // Now, find the guess for its radius.
76            let radius_label = format!("{}.radius", circle.0);
77            let Some(radius_guess) = guessmap_scalars.remove(&radius_label) else {
78                return Err(TextualError::MissingGuess {
79                    label: radius_label,
80                });
81            };
82            initial_guesses.push_circle(
83                &mut id_generator,
84                center_guess.x,
85                center_guess.y,
86                radius_guess,
87            );
88        }
89        let mut initial_guesses = initial_guesses.done();
90        for arc in &self.inner_arcs {
91            // Each arc should have a guess for its 3 points (p, q, and center).
92            let center_label = format!("{}.center", arc.0);
93            let Some(center_guess) = guessmap_points.remove(&center_label) else {
94                return Err(TextualError::MissingGuess {
95                    label: center_label,
96                });
97            };
98            let a_label = format!("{}.a", arc.0);
99            let Some(a_guess) = guessmap_points.remove(&a_label) else {
100                return Err(TextualError::MissingGuess { label: a_label });
101            };
102            let b_label = format!("{}.b", arc.0);
103            let Some(b_guess) = guessmap_points.remove(&b_label) else {
104                return Err(TextualError::MissingGuess { label: b_label });
105            };
106            initial_guesses.push_arc(&mut id_generator, a_guess, b_guess, center_guess);
107        }
108        if !guessmap_points.is_empty() {
109            let labels: Vec<String> = guessmap_points.keys().cloned().collect();
110            return Err(TextualError::UnusedGuesses { labels });
111        }
112        if !guessmap_scalars.is_empty() {
113            let labels: Vec<String> = guessmap_scalars.keys().cloned().collect();
114            return Err(TextualError::UnusedGuesses { labels });
115        }
116
117        // Good. Now we can define all the constraints, referencing the solver variables that
118        // were defined in the previous step.
119        let mut constraints = Vec::new();
120        let datum_point_for_label = |label: &Label| -> Result<DatumPoint, TextualError> {
121            // Is the point a single geometric point?
122            if let Some(point_id) = self.inner_points.iter().position(|p| p == &label.0) {
123                let ids = initial_guesses.point_ids(point_id);
124                return Ok(DatumPoint {
125                    x_id: ids.x,
126                    y_id: ids.y,
127                });
128            }
129            // Maybe it's a point in a circle?
130            if let Some(circle_id) = self
131                .inner_circles
132                .iter()
133                .position(|circ| format!("{}.center", circ.0) == label.0.as_str())
134            {
135                let center = initial_guesses.circle_ids(circle_id).center;
136                return Ok(DatumPoint {
137                    x_id: center.x,
138                    y_id: center.y,
139                });
140            }
141            // Maybe it's a point in an arc?
142            // Is it an arc's center?
143            if let Some(arc_id) = self
144                .inner_arcs
145                .iter()
146                .position(|arc| format!("{}.center", arc.0) == label.0.as_str())
147            {
148                let center = initial_guesses.arc_ids(arc_id).center;
149                return Ok(center.into());
150            }
151            // Is it an arc's start point (labeled as `.a` in textual format)?
152            if let Some(arc_id) = self
153                .inner_arcs
154                .iter()
155                .position(|arc| format!("{}.a", arc.0) == label.0.as_str())
156            {
157                let start = initial_guesses.arc_ids(arc_id).start;
158                return Ok(start.into());
159            }
160            // Is it an arc's end point (labeled as `.b` in textual format)?
161            if let Some(arc_id) = self
162                .inner_arcs
163                .iter()
164                .position(|arc| format!("{}.b", arc.0) == label.0.as_str())
165            {
166                let end = initial_guesses.arc_ids(arc_id).end;
167                return Ok(end.into());
168            }
169            // Well, it wasn't any of the geometries we recognize.
170            Err(TextualError::UndefinedPoint {
171                label: label.0.clone(),
172            })
173        };
174        let datum_distance_for_label = |label: &Label| -> Result<DatumDistance, TextualError> {
175            if let Some(circle_id) = self
176                .inner_circles
177                .iter()
178                .position(|circ| format!("{}.radius", circ.0) == label.0.as_str())
179            {
180                let ids = initial_guesses.circle_ids(circle_id);
181                return Ok(DatumDistance { id: ids.radius });
182            }
183            Err(TextualError::UndefinedPoint {
184                label: label.0.clone(),
185            })
186        };
187
188        for instr in &self.instructions {
189            match instr {
190                Instruction::DeclarePoint(_) => {}
191                Instruction::DeclareCircle(_) => {}
192                Instruction::DeclareArc(_) => {}
193                Instruction::Line(_) => {}
194                Instruction::CircleRadius(CircleRadius { circle, radius }) => {
195                    let circ = &circle.0;
196                    let center_id = datum_point_for_label(&Label(format!("{circ}.center")))?;
197                    let radius_id = datum_distance_for_label(&Label(format!("{circ}.radius")))?;
198                    constraints.push(Constraint::CircleRadius(
199                        datatypes::inputs::DatumCircle {
200                            center: center_id,
201                            radius: radius_id,
202                        },
203                        *radius,
204                    ));
205                }
206                Instruction::ArcRadius(ArcRadius { arc_label, radius }) => {
207                    let arc_label = &arc_label.0;
208                    let circular_arc = DatumCircularArc {
209                        center: datum_point_for_label(&Label(format!("{arc_label}.center")))?,
210                        start: datum_point_for_label(&Label(format!("{arc_label}.a")))?,
211                        end: datum_point_for_label(&Label(format!("{arc_label}.b")))?,
212                    };
213                    constraints.push(Constraint::ArcRadius(circular_arc, *radius));
214                }
215                Instruction::IsArc(IsArc { arc_label }) => {
216                    let arc_label = &arc_label.0;
217                    let circular_arc = DatumCircularArc {
218                        center: datum_point_for_label(&Label(format!("{arc_label}.center")))?,
219                        start: datum_point_for_label(&Label(format!("{arc_label}.a")))?,
220                        end: datum_point_for_label(&Label(format!("{arc_label}.b")))?,
221                    };
222                    constraints.push(Constraint::Arc(circular_arc));
223                }
224                Instruction::PointLineDistance(PointLineDistance {
225                    point,
226                    line_p0,
227                    line_p1,
228                    distance,
229                }) => {
230                    let line = DatumLineSegment {
231                        p0: datum_point_for_label(line_p0)?,
232                        p1: datum_point_for_label(line_p1)?,
233                    };
234                    let p = datum_point_for_label(point)?;
235                    constraints.push(Constraint::PointLineDistance(p, line, *distance));
236                }
237                Instruction::Tangent(Tangent {
238                    circle,
239                    line_p0,
240                    line_p1,
241                }) => {
242                    let circ = &circle.0;
243                    let center_id = datum_point_for_label(&Label(format!("{circ}.center")))?;
244                    let radius_id = datum_distance_for_label(&Label(format!("{circ}.radius")))?;
245                    let line = DatumLineSegment {
246                        p0: datum_point_for_label(line_p0)?,
247                        p1: datum_point_for_label(line_p1)?,
248                    };
249                    constraints.push(Constraint::LineTangentToCircle(
250                        line,
251                        datatypes::inputs::DatumCircle {
252                            center: center_id,
253                            radius: radius_id,
254                        },
255                    ));
256                }
257                Instruction::FixPointComponent(FixPointComponent {
258                    point,
259                    component,
260                    value,
261                }) => {
262                    if let Some(point_id) =
263                        self.inner_points.iter().position(|label| label == point)
264                    {
265                        let ids = initial_guesses.point_ids(point_id);
266                        let id = match component {
267                            Component::X => ids.x,
268                            Component::Y => ids.y,
269                        };
270                        constraints.push(Constraint::Fixed(id, *value));
271                    } else if let Some(circle_label) = point.0.strip_suffix(".center") {
272                        if let Some(circle_id) =
273                            self.inner_circles.iter().position(|p| p.0 == circle_label)
274                        {
275                            let center = initial_guesses.circle_ids(circle_id).center;
276                            let id = match component {
277                                Component::X => center.x,
278                                Component::Y => center.y,
279                            };
280                            constraints.push(Constraint::Fixed(id, *value));
281                        }
282                    } else {
283                        return Err(TextualError::UndefinedPoint {
284                            label: point.0.clone(),
285                        });
286                    }
287                }
288                Instruction::FixCenterPointComponent(FixCenterPointComponent {
289                    object,
290                    center_component,
291                    value,
292                }) => {
293                    // Is this center talking about a circle object?
294                    if let Some(circle_id) =
295                        self.inner_circles.iter().position(|label| label == object)
296                    {
297                        let center = initial_guesses.circle_ids(circle_id).center;
298                        let id = match center_component {
299                            Component::X => center.x,
300                            Component::Y => center.y,
301                        };
302                        constraints.push(Constraint::Fixed(id, *value));
303                    // Is this center talking about an arc object?
304                    } else if let Some(arc_id) =
305                        self.inner_arcs.iter().position(|label| label == object)
306                    {
307                        let center = initial_guesses.arc_ids(arc_id).center;
308                        let id = match center_component {
309                            Component::X => center.x,
310                            Component::Y => center.y,
311                        };
312                        constraints.push(Constraint::Fixed(id, *value));
313                    } else {
314                        return Err(TextualError::UndefinedPoint {
315                            label: object.0.clone(),
316                        });
317                    }
318                }
319                Instruction::Vertical(Vertical { label }) => {
320                    let p0 = datum_point_for_label(&label.0)?;
321                    let p1 = datum_point_for_label(&label.1)?;
322                    constraints.push(Constraint::Vertical(DatumLineSegment { p0, p1 }));
323                }
324                Instruction::PointsCoincident(PointsCoincident { point0, point1 }) => {
325                    let p0 = datum_point_for_label(point0)?;
326                    let p1 = datum_point_for_label(point1)?;
327                    constraints.push(Constraint::PointsCoincident(p0, p1));
328                }
329                Instruction::PointArcCoincident(PointArcCoincident { point, arc }) => {
330                    let p = datum_point_for_label(point)?;
331                    let arc_label = &arc.0;
332                    let datum_arc = DatumCircularArc {
333                        center: datum_point_for_label(&Label(format!("{arc_label}.center")))?,
334                        start: datum_point_for_label(&Label(format!("{arc_label}.a")))?,
335                        end: datum_point_for_label(&Label(format!("{arc_label}.b")))?,
336                    };
337                    constraints.push(Constraint::PointArcCoincident(datum_arc, p));
338                }
339                Instruction::Midpoint(Midpoint { point0, point1, mp }) => {
340                    let p0 = datum_point_for_label(point0)?;
341                    let p1 = datum_point_for_label(point1)?;
342                    let mp = datum_point_for_label(mp)?;
343                    constraints.push(Constraint::Midpoint(DatumLineSegment { p0, p1 }, mp));
344                }
345                Instruction::Symmetric(Symmetric { p0, p1, line }) => {
346                    let p0 = datum_point_for_label(p0)?;
347                    let p1 = datum_point_for_label(p1)?;
348                    let line = (
349                        datum_point_for_label(&line.0)?,
350                        datum_point_for_label(&line.1)?,
351                    );
352                    let line = DatumLineSegment {
353                        p0: line.0,
354                        p1: line.1,
355                    };
356                    constraints.push(Constraint::Symmetric(line, p0, p1));
357                }
358                Instruction::Horizontal(Horizontal { label }) => {
359                    let p0 = datum_point_for_label(&label.0)?;
360                    let p1 = datum_point_for_label(&label.1)?;
361                    constraints.push(Constraint::Horizontal(DatumLineSegment { p0, p1 }));
362                }
363                Instruction::Distance(Distance { label, distance }) => {
364                    let p0 = datum_point_for_label(&label.0)?;
365                    let p1 = datum_point_for_label(&label.1)?;
366                    constraints.push(Constraint::Distance(p0, p1, *distance));
367                }
368                Instruction::Parallel(Parallel { line0, line1 }) => {
369                    let p0 = datum_point_for_label(&line0.0)?;
370                    let p1 = datum_point_for_label(&line0.1)?;
371                    let p2 = datum_point_for_label(&line1.0)?;
372                    let p3 = datum_point_for_label(&line1.1)?;
373                    constraints.push(Constraint::lines_parallel([
374                        DatumLineSegment { p0, p1 },
375                        DatumLineSegment { p0: p2, p1: p3 },
376                    ]));
377                }
378                Instruction::LinesEqualLength(LinesEqualLength { line0, line1 }) => {
379                    let p0 = datum_point_for_label(&line0.0)?;
380                    let p1 = datum_point_for_label(&line0.1)?;
381                    let p2 = datum_point_for_label(&line1.0)?;
382                    let p3 = datum_point_for_label(&line1.1)?;
383                    constraints.push(Constraint::LinesEqualLength(
384                        DatumLineSegment { p0, p1 },
385                        DatumLineSegment { p0: p2, p1: p3 },
386                    ));
387                }
388                Instruction::Perpendicular(Perpendicular { line0, line1 }) => {
389                    let p0 = datum_point_for_label(&line0.0)?;
390                    let p1 = datum_point_for_label(&line0.1)?;
391                    let p2 = datum_point_for_label(&line1.0)?;
392                    let p3 = datum_point_for_label(&line1.1)?;
393                    constraints.push(Constraint::lines_perpendicular([
394                        DatumLineSegment { p0, p1 },
395                        DatumLineSegment { p0: p2, p1: p3 },
396                    ]));
397                }
398                Instruction::AngleLine(AngleLine {
399                    line0,
400                    line1,
401                    angle,
402                }) => {
403                    let p0 = datum_point_for_label(&line0.0)?;
404                    let p1 = datum_point_for_label(&line0.1)?;
405                    let p2 = datum_point_for_label(&line1.0)?;
406                    let p3 = datum_point_for_label(&line1.1)?;
407                    constraints.push(Constraint::LinesAtAngle(
408                        DatumLineSegment { p0, p1 },
409                        DatumLineSegment { p0: p2, p1: p3 },
410                        AngleKind::Other(*angle),
411                    ));
412                }
413                Instruction::ArcLength(arc_length) => {
414                    let arc_label = &arc_length.arc.0;
415                    let length = arc_length.distance;
416                    let circular_arc = DatumCircularArc {
417                        center: datum_point_for_label(&Label(format!("{arc_label}.center")))?,
418                        start: datum_point_for_label(&Label(format!("{arc_label}.a")))?,
419                        end: datum_point_for_label(&Label(format!("{arc_label}.b")))?,
420                    };
421                    constraints.push(Constraint::ArcLength(circular_arc, length));
422                }
423            }
424        }
425        let initial_guesses = initial_guesses.done();
426
427        // At some point, the textual format should support setting priority.
428        // For now, set it to max priority.
429        let priority = 0;
430        let constraints = constraints
431            .into_iter()
432            .map(|c| ConstraintRequest::new(c, priority))
433            .collect();
434
435        Ok(ConstraintSystem {
436            constraints,
437            initial_guesses,
438            inner_points: &self.inner_points,
439            inner_circles: &self.inner_circles,
440            inner_arcs: &self.inner_arcs,
441            inner_lines: &self.inner_lines,
442        })
443    }
444}
445
446/// A constraint system that ezpz could solve,
447/// built from the ezpz text format.
448#[derive(Clone)]
449pub struct ConstraintSystem<'a> {
450    /// Constraints from the text input.
451    pub constraints: Vec<ConstraintRequest>,
452    initial_guesses: GeometryVariables<DoneState>,
453    inner_points: &'a [Label],
454    inner_circles: &'a [Label],
455    inner_arcs: &'a [Label],
456    inner_lines: &'a [(Label, Label)],
457}
458
459impl ConstraintSystem<'_> {
460    /// Solve, without carrying through metadata about the solve.
461    pub fn solve_no_metadata(&self, config: Config) -> Result<SolveOutcome, FailureOutcome> {
462        crate::solve(&self.constraints, self.initial_guesses.variables(), config)
463    }
464
465    fn solve_no_metadata_inner<A: Analysis>(
466        &self,
467        config: Config,
468    ) -> Result<SolveOutcomeAnalysis<A>, FailureOutcome> {
469        crate::solve_with_priority_inner(
470            &self.constraints,
471            self.initial_guesses.variables(),
472            config,
473        )
474    }
475
476    /// Solve, with metadata about the solve.
477    pub fn solve(&self) -> Result<Outcome, FailureOutcome> {
478        self.solve_with_config(Default::default())
479    }
480
481    /// Solve, and analyze the degrees of freedom.
482    pub fn solve_with_config_analysis(
483        &self,
484        config: Config,
485    ) -> Result<OutcomeAnalysis, FailureOutcome> {
486        let (analysis, outcome) = self.solve_with_config_inner::<FreedomAnalysis>(config)?;
487        Ok(OutcomeAnalysis { analysis, outcome })
488    }
489
490    /// Solve, but give a non-default config.
491    pub fn solve_with_config(&self, config: Config) -> Result<Outcome, FailureOutcome> {
492        let (NoAnalysis, outcome) = self.solve_with_config_inner::<NoAnalysis>(config)?;
493        Ok(outcome)
494    }
495
496    fn solve_with_config_inner<A: Analysis>(
497        &self,
498        config: Config,
499    ) -> Result<(A, Outcome), FailureOutcome> {
500        let num_vars = self.initial_guesses.len();
501        let num_eqs = self
502            .constraints
503            .iter()
504            .map(|c| c.constraint().residual_dim())
505            .sum();
506        // Pass into the solver.
507        let SolveOutcomeAnalysis {
508            analysis,
509            outcome:
510                SolveOutcome {
511                    iterations,
512                    warnings,
513                    final_values,
514                    unsatisfied,
515                    priority_solved,
516                },
517        } = self.solve_no_metadata_inner::<A>(config)?;
518        let num_points = self.inner_points.len();
519        let num_circles = self.inner_circles.len();
520        let num_arcs = self.inner_arcs.len();
521
522        let mut final_points = IndexMap::with_capacity(num_points);
523        for (i, point) in self.inner_points.iter().enumerate() {
524            let x_id = 2 * i;
525            let y_id = 2 * i + 1;
526            let p = Point {
527                x: final_values[x_id],
528                y: final_values[y_id],
529            };
530            final_points.insert(point.0.clone(), p);
531        }
532        let start_of_circles = 2 * self.inner_points.len();
533        let mut final_circles = IndexMap::with_capacity(num_circles);
534        for (i, circle_label) in self.inner_circles.iter().enumerate() {
535            let cx = final_values[start_of_circles + 3 * i]; // center x
536            let cy = final_values[start_of_circles + 3 * i + 1]; // center y
537            let rd = final_values[start_of_circles + 3 * i + 2]; // radius
538            final_circles.insert(
539                circle_label.0.clone(),
540                Circle {
541                    radius: rd,
542                    center: Point { x: cx, y: cy },
543                },
544            );
545        }
546        let start_of_arcs = start_of_circles + 3 * self.inner_circles.len();
547        let mut final_arcs = IndexMap::with_capacity(num_arcs);
548        for (i, arc_label) in self.inner_arcs.iter().enumerate() {
549            let ax = final_values[start_of_arcs + VARS_PER_ARC * i];
550            let ay = final_values[start_of_arcs + VARS_PER_ARC * i + 1];
551            let bx = final_values[start_of_arcs + VARS_PER_ARC * i + 2];
552            let by = final_values[start_of_arcs + VARS_PER_ARC * i + 3];
553            let cx = final_values[start_of_arcs + VARS_PER_ARC * i + 4];
554            let cy = final_values[start_of_arcs + VARS_PER_ARC * i + 5];
555            final_arcs.insert(
556                arc_label.0.clone(),
557                Arc {
558                    center: Point { x: cx, y: cy },
559                    a: Point { x: ax, y: ay },
560                    b: Point { x: bx, y: by },
561                },
562            );
563        }
564        Ok((
565            analysis,
566            Outcome {
567                priority_solved,
568                unsatisfied,
569                iterations,
570                warnings,
571                points: final_points,
572                circles: final_circles,
573                arcs: final_arcs,
574                num_vars,
575                lines: self.inner_lines.to_vec(),
576                num_eqs,
577            },
578        ))
579    }
580}
581
582/// Outcome of successfully solving a constraint system.
583#[derive(Debug)]
584pub struct Outcome {
585    /// All constraint IDs which couldn't be satisfied.
586    pub unsatisfied: Vec<usize>,
587    /// How many iterations of the core Newton-Gauss loop this system required.
588    pub iterations: usize,
589    /// Anything bad that users should know about.
590    pub warnings: Vec<Warning>,
591    /// Points the user defined, with their final solved values.
592    pub points: IndexMap<String, Point>,
593    /// Circles the user defined, with their final solved values.
594    pub circles: IndexMap<String, Circle>,
595    /// Arcs the user defined, with their final solved values.
596    pub arcs: IndexMap<String, Arc>,
597    /// Lines the user defined, with labels for their two points.
598    pub lines: Vec<(Label, Label)>,
599    /// Size of the constraint system. Number of variables being solved for.
600    pub num_vars: usize,
601    /// Size of the constraint system. Number of residual equations.
602    pub num_eqs: usize,
603    /// The lowest priority solved before the constraint solver stopped.
604    /// The constraint solver stops when it cannot solve any more constraints, i.e.
605    /// got an error.
606    pub priority_solved: u32,
607}
608
609/// Outcome of solving an ezpz system, and degrees-of-freedom analysis.
610#[derive(Debug)]
611pub struct OutcomeAnalysis {
612    /// Degrees of freedom analysis
613    pub analysis: FreedomAnalysis,
614    /// Outcome of solving the constraint system.
615    pub outcome: Outcome,
616}
617
618impl Outcome {
619    /// Look up a point by its label.
620    pub fn get_point(&self, label: &str) -> Option<Point> {
621        self.points.get(label).copied()
622    }
623
624    /// Look up a circle by its label.
625    pub fn get_circle(&self, label: &str) -> Option<Circle> {
626        self.circles.get(label).copied()
627    }
628
629    /// Look up an arc by its label.
630    pub fn get_arc(&self, label: &str) -> Option<Arc> {
631        self.arcs.get(label).copied()
632    }
633}
634
635impl OutcomeAnalysis {
636    /// Look up a point by its label.
637    #[cfg(test)]
638    pub fn get_point(&self, label: &str) -> Option<Point> {
639        self.outcome.get_point(label)
640    }
641
642    /// Look up a circle by its label.
643    #[cfg(test)]
644    pub fn get_circle(&self, label: &str) -> Option<Circle> {
645        self.outcome.get_circle(label)
646    }
647
648    /// Look up an arc by its label.
649    #[cfg(test)]
650    pub fn get_arc(&self, label: &str) -> Option<Arc> {
651        self.outcome.get_arc(label)
652    }
653
654    /// Are all constraints satisfied?
655    #[cfg(test)]
656    pub fn is_satisfied(&self) -> bool {
657        !self.is_unsatisfied()
658    }
659
660    /// Are any constraints not satisfied?
661    #[cfg(test)]
662    pub fn is_unsatisfied(&self) -> bool {
663        !self.outcome.unsatisfied.is_empty()
664    }
665}
666
667#[cfg(test)]
668mod tests {
669    use super::*;
670    use crate::textual::{PointGuess, instruction::FixPointComponent};
671
672    fn empty_problem() -> Problem {
673        Problem {
674            instructions: Vec::new(),
675            inner_points: Vec::new(),
676            inner_circles: Vec::new(),
677            inner_arcs: Vec::new(),
678            inner_lines: Vec::new(),
679            point_guesses: Vec::new(),
680            scalar_guesses: Vec::new(),
681        }
682    }
683
684    #[test]
685    fn missing_guess_is_reported() {
686        let mut problem = empty_problem();
687        problem.inner_points.push(Label::from("p"));
688        let err = problem
689            .to_constraint_system()
690            .err()
691            .expect("expected missing guess");
692        assert!(matches!(err, TextualError::MissingGuess { label } if label == "p"));
693    }
694
695    #[test]
696    fn unused_guesses_are_detected() {
697        let mut problem = empty_problem();
698        problem.point_guesses.push(PointGuess {
699            point: Label::from("ghost"),
700            guess: Point { x: 0.0, y: 0.0 },
701        });
702
703        let err = problem
704            .to_constraint_system()
705            .err()
706            .expect("expected unused guess error");
707        match err {
708            TextualError::UnusedGuesses { labels } => {
709                assert_eq!(labels.len(), 1);
710                assert_eq!(labels[0], "ghost");
711            }
712            other => panic!("unexpected error: {other:?}"),
713        }
714    }
715
716    #[test]
717    fn undefined_point_in_instruction_errors() {
718        let mut problem = empty_problem();
719        problem.inner_points.push(Label::from("p"));
720        problem.point_guesses.push(PointGuess {
721            point: Label::from("p"),
722            guess: Point { x: 0.0, y: 0.0 },
723        });
724        problem
725            .instructions
726            .push(Instruction::FixPointComponent(FixPointComponent {
727                point: Label::from("missing"),
728                component: Component::X,
729                value: 2.5,
730            }));
731
732        let err = problem
733            .to_constraint_system()
734            .err()
735            .expect("expected undefined point error");
736        assert!(matches!(err, TextualError::UndefinedPoint { label } if label == "missing"));
737    }
738}