use std::collections::HashMap;
use indexmap::IndexMap;
use crate::Analysis;
use crate::Config;
use crate::Constraint;
use crate::ConstraintRequest;
use crate::FailureOutcome;
use crate::FreedomAnalysis;
use crate::IdGenerator;
use crate::NoAnalysis;
use crate::SolveOutcome;
use crate::SolveOutcomeAnalysis;
use crate::Warning;
use crate::datatypes;
use crate::datatypes::AngleKind;
use crate::datatypes::inputs::DatumCircularArc;
use crate::datatypes::inputs::DatumDistance;
use crate::datatypes::inputs::DatumLineSegment;
use crate::datatypes::inputs::DatumPoint;
use crate::datatypes::outputs::Arc;
use crate::datatypes::outputs::{Circle, Component, Point};
use crate::error::TextualError;
use crate::textual::Label;
use crate::textual::geometry_variables::DoneState;
use crate::textual::geometry_variables::GeometryVariables;
use crate::textual::geometry_variables::PointsState;
use crate::textual::geometry_variables::VARS_PER_ARC;
use crate::textual::instruction::*;
use super::Instruction;
use super::Problem;
impl Problem {
pub fn to_constraint_system(&self) -> Result<ConstraintSystem<'_>, TextualError> {
let mut id_generator = IdGenerator::default();
let mut initial_guesses: GeometryVariables<PointsState> = Default::default();
let mut guessmap_points = HashMap::new();
guessmap_points.extend(
self.point_guesses
.iter()
.map(|pg| (pg.point.0.clone(), pg.guess)),
);
for point in &self.inner_points {
let Some(guess) = guessmap_points.remove(&point.0) else {
return Err(TextualError::MissingGuess {
label: point.0.clone(),
});
};
initial_guesses.push_point(&mut id_generator, guess.x, guess.y);
}
let mut guessmap_scalars = HashMap::new();
guessmap_scalars.extend(
self.scalar_guesses
.iter()
.map(|sg| (sg.scalar.0.clone(), sg.guess)),
);
let mut initial_guesses = initial_guesses.done();
for circle in &self.inner_circles {
let center_label = format!("{}.center", circle.0);
let Some(center_guess) = guessmap_points.remove(¢er_label) else {
return Err(TextualError::MissingGuess {
label: center_label,
});
};
let radius_label = format!("{}.radius", circle.0);
let Some(radius_guess) = guessmap_scalars.remove(&radius_label) else {
return Err(TextualError::MissingGuess {
label: radius_label,
});
};
initial_guesses.push_circle(
&mut id_generator,
center_guess.x,
center_guess.y,
radius_guess,
);
}
let mut initial_guesses = initial_guesses.done();
for arc in &self.inner_arcs {
let center_label = format!("{}.center", arc.0);
let Some(center_guess) = guessmap_points.remove(¢er_label) else {
return Err(TextualError::MissingGuess {
label: center_label,
});
};
let a_label = format!("{}.a", arc.0);
let Some(a_guess) = guessmap_points.remove(&a_label) else {
return Err(TextualError::MissingGuess { label: a_label });
};
let b_label = format!("{}.b", arc.0);
let Some(b_guess) = guessmap_points.remove(&b_label) else {
return Err(TextualError::MissingGuess { label: b_label });
};
initial_guesses.push_arc(&mut id_generator, a_guess, b_guess, center_guess);
}
if !guessmap_points.is_empty() {
let labels: Vec<String> = guessmap_points.keys().cloned().collect();
return Err(TextualError::UnusedGuesses { labels });
}
if !guessmap_scalars.is_empty() {
let labels: Vec<String> = guessmap_scalars.keys().cloned().collect();
return Err(TextualError::UnusedGuesses { labels });
}
let mut constraints = Vec::new();
let datum_point_for_label = |label: &Label| -> Result<DatumPoint, TextualError> {
if let Some(point_id) = self.inner_points.iter().position(|p| p == &label.0) {
let ids = initial_guesses.point_ids(point_id);
return Ok(DatumPoint {
x_id: ids.x,
y_id: ids.y,
});
}
if let Some(circle_id) = self
.inner_circles
.iter()
.position(|circ| format!("{}.center", circ.0) == label.0.as_str())
{
let center = initial_guesses.circle_ids(circle_id).center;
return Ok(DatumPoint {
x_id: center.x,
y_id: center.y,
});
}
if let Some(arc_id) = self
.inner_arcs
.iter()
.position(|arc| format!("{}.center", arc.0) == label.0.as_str())
{
let center = initial_guesses.arc_ids(arc_id).center;
return Ok(center.into());
}
if let Some(arc_id) = self
.inner_arcs
.iter()
.position(|arc| format!("{}.a", arc.0) == label.0.as_str())
{
let start = initial_guesses.arc_ids(arc_id).start;
return Ok(start.into());
}
if let Some(arc_id) = self
.inner_arcs
.iter()
.position(|arc| format!("{}.b", arc.0) == label.0.as_str())
{
let end = initial_guesses.arc_ids(arc_id).end;
return Ok(end.into());
}
Err(TextualError::UndefinedPoint {
label: label.0.clone(),
})
};
let datum_distance_for_label = |label: &Label| -> Result<DatumDistance, TextualError> {
if let Some(circle_id) = self
.inner_circles
.iter()
.position(|circ| format!("{}.radius", circ.0) == label.0.as_str())
{
let ids = initial_guesses.circle_ids(circle_id);
return Ok(DatumDistance { id: ids.radius });
}
Err(TextualError::UndefinedPoint {
label: label.0.clone(),
})
};
for instr in &self.instructions {
match instr {
Instruction::DeclarePoint(_) => {}
Instruction::DeclareCircle(_) => {}
Instruction::DeclareArc(_) => {}
Instruction::Line(_) => {}
Instruction::CircleRadius(CircleRadius { circle, radius }) => {
let circ = &circle.0;
let center_id = datum_point_for_label(&Label(format!("{circ}.center")))?;
let radius_id = datum_distance_for_label(&Label(format!("{circ}.radius")))?;
constraints.push(Constraint::CircleRadius(
datatypes::inputs::DatumCircle {
center: center_id,
radius: radius_id,
},
*radius,
));
}
Instruction::ArcRadius(ArcRadius { arc_label, radius }) => {
let arc_label = &arc_label.0;
let circular_arc = DatumCircularArc {
center: datum_point_for_label(&Label(format!("{arc_label}.center")))?,
start: datum_point_for_label(&Label(format!("{arc_label}.a")))?,
end: datum_point_for_label(&Label(format!("{arc_label}.b")))?,
};
constraints.push(Constraint::ArcRadius(circular_arc, *radius));
}
Instruction::IsArc(IsArc { arc_label }) => {
let arc_label = &arc_label.0;
let circular_arc = DatumCircularArc {
center: datum_point_for_label(&Label(format!("{arc_label}.center")))?,
start: datum_point_for_label(&Label(format!("{arc_label}.a")))?,
end: datum_point_for_label(&Label(format!("{arc_label}.b")))?,
};
constraints.push(Constraint::Arc(circular_arc));
}
Instruction::PointLineDistance(PointLineDistance {
point,
line_p0,
line_p1,
distance,
}) => {
let line = DatumLineSegment {
p0: datum_point_for_label(line_p0)?,
p1: datum_point_for_label(line_p1)?,
};
let p = datum_point_for_label(point)?;
constraints.push(Constraint::PointLineDistance(p, line, *distance));
}
Instruction::Tangent(Tangent {
circle,
line_p0,
line_p1,
}) => {
let circ = &circle.0;
let center_id = datum_point_for_label(&Label(format!("{circ}.center")))?;
let radius_id = datum_distance_for_label(&Label(format!("{circ}.radius")))?;
let line = DatumLineSegment {
p0: datum_point_for_label(line_p0)?,
p1: datum_point_for_label(line_p1)?,
};
constraints.push(Constraint::LineTangentToCircle(
line,
datatypes::inputs::DatumCircle {
center: center_id,
radius: radius_id,
},
));
}
Instruction::FixPointComponent(FixPointComponent {
point,
component,
value,
}) => {
if let Some(point_id) =
self.inner_points.iter().position(|label| label == point)
{
let ids = initial_guesses.point_ids(point_id);
let id = match component {
Component::X => ids.x,
Component::Y => ids.y,
};
constraints.push(Constraint::Fixed(id, *value));
} else if let Some(circle_label) = point.0.strip_suffix(".center") {
if let Some(circle_id) =
self.inner_circles.iter().position(|p| p.0 == circle_label)
{
let center = initial_guesses.circle_ids(circle_id).center;
let id = match component {
Component::X => center.x,
Component::Y => center.y,
};
constraints.push(Constraint::Fixed(id, *value));
}
} else {
return Err(TextualError::UndefinedPoint {
label: point.0.clone(),
});
}
}
Instruction::FixCenterPointComponent(FixCenterPointComponent {
object,
center_component,
value,
}) => {
if let Some(circle_id) =
self.inner_circles.iter().position(|label| label == object)
{
let center = initial_guesses.circle_ids(circle_id).center;
let id = match center_component {
Component::X => center.x,
Component::Y => center.y,
};
constraints.push(Constraint::Fixed(id, *value));
} else if let Some(arc_id) =
self.inner_arcs.iter().position(|label| label == object)
{
let center = initial_guesses.arc_ids(arc_id).center;
let id = match center_component {
Component::X => center.x,
Component::Y => center.y,
};
constraints.push(Constraint::Fixed(id, *value));
} else {
return Err(TextualError::UndefinedPoint {
label: object.0.clone(),
});
}
}
Instruction::Vertical(Vertical { label }) => {
let p0 = datum_point_for_label(&label.0)?;
let p1 = datum_point_for_label(&label.1)?;
constraints.push(Constraint::Vertical(DatumLineSegment { p0, p1 }));
}
Instruction::PointsCoincident(PointsCoincident { point0, point1 }) => {
let p0 = datum_point_for_label(point0)?;
let p1 = datum_point_for_label(point1)?;
constraints.push(Constraint::PointsCoincident(p0, p1));
}
Instruction::PointArcCoincident(PointArcCoincident { point, arc }) => {
let p = datum_point_for_label(point)?;
let arc_label = &arc.0;
let datum_arc = DatumCircularArc {
center: datum_point_for_label(&Label(format!("{arc_label}.center")))?,
start: datum_point_for_label(&Label(format!("{arc_label}.a")))?,
end: datum_point_for_label(&Label(format!("{arc_label}.b")))?,
};
constraints.push(Constraint::PointArcCoincident(datum_arc, p));
}
Instruction::Midpoint(Midpoint { point0, point1, mp }) => {
let p0 = datum_point_for_label(point0)?;
let p1 = datum_point_for_label(point1)?;
let mp = datum_point_for_label(mp)?;
constraints.push(Constraint::Midpoint(DatumLineSegment { p0, p1 }, mp));
}
Instruction::Symmetric(Symmetric { p0, p1, line }) => {
let p0 = datum_point_for_label(p0)?;
let p1 = datum_point_for_label(p1)?;
let line = (
datum_point_for_label(&line.0)?,
datum_point_for_label(&line.1)?,
);
let line = DatumLineSegment {
p0: line.0,
p1: line.1,
};
constraints.push(Constraint::Symmetric(line, p0, p1));
}
Instruction::Horizontal(Horizontal { label }) => {
let p0 = datum_point_for_label(&label.0)?;
let p1 = datum_point_for_label(&label.1)?;
constraints.push(Constraint::Horizontal(DatumLineSegment { p0, p1 }));
}
Instruction::Distance(Distance { label, distance }) => {
let p0 = datum_point_for_label(&label.0)?;
let p1 = datum_point_for_label(&label.1)?;
constraints.push(Constraint::Distance(p0, p1, *distance));
}
Instruction::Parallel(Parallel { line0, line1 }) => {
let p0 = datum_point_for_label(&line0.0)?;
let p1 = datum_point_for_label(&line0.1)?;
let p2 = datum_point_for_label(&line1.0)?;
let p3 = datum_point_for_label(&line1.1)?;
constraints.push(Constraint::lines_parallel([
DatumLineSegment { p0, p1 },
DatumLineSegment { p0: p2, p1: p3 },
]));
}
Instruction::LinesEqualLength(LinesEqualLength { line0, line1 }) => {
let p0 = datum_point_for_label(&line0.0)?;
let p1 = datum_point_for_label(&line0.1)?;
let p2 = datum_point_for_label(&line1.0)?;
let p3 = datum_point_for_label(&line1.1)?;
constraints.push(Constraint::LinesEqualLength(
DatumLineSegment { p0, p1 },
DatumLineSegment { p0: p2, p1: p3 },
));
}
Instruction::Perpendicular(Perpendicular { line0, line1 }) => {
let p0 = datum_point_for_label(&line0.0)?;
let p1 = datum_point_for_label(&line0.1)?;
let p2 = datum_point_for_label(&line1.0)?;
let p3 = datum_point_for_label(&line1.1)?;
constraints.push(Constraint::lines_perpendicular([
DatumLineSegment { p0, p1 },
DatumLineSegment { p0: p2, p1: p3 },
]));
}
Instruction::AngleLine(AngleLine {
line0,
line1,
angle,
}) => {
let p0 = datum_point_for_label(&line0.0)?;
let p1 = datum_point_for_label(&line0.1)?;
let p2 = datum_point_for_label(&line1.0)?;
let p3 = datum_point_for_label(&line1.1)?;
constraints.push(Constraint::LinesAtAngle(
DatumLineSegment { p0, p1 },
DatumLineSegment { p0: p2, p1: p3 },
AngleKind::Other(*angle),
));
}
Instruction::ArcLength(arc_length) => {
let arc_label = &arc_length.arc.0;
let length = arc_length.distance;
let circular_arc = DatumCircularArc {
center: datum_point_for_label(&Label(format!("{arc_label}.center")))?,
start: datum_point_for_label(&Label(format!("{arc_label}.a")))?,
end: datum_point_for_label(&Label(format!("{arc_label}.b")))?,
};
constraints.push(Constraint::ArcLength(circular_arc, length));
}
}
}
let initial_guesses = initial_guesses.done();
let priority = 0;
let constraints = constraints
.into_iter()
.map(|c| ConstraintRequest::new(c, priority))
.collect();
Ok(ConstraintSystem {
constraints,
initial_guesses,
inner_points: &self.inner_points,
inner_circles: &self.inner_circles,
inner_arcs: &self.inner_arcs,
inner_lines: &self.inner_lines,
})
}
}
#[derive(Clone)]
pub struct ConstraintSystem<'a> {
pub constraints: Vec<ConstraintRequest>,
initial_guesses: GeometryVariables<DoneState>,
inner_points: &'a [Label],
inner_circles: &'a [Label],
inner_arcs: &'a [Label],
inner_lines: &'a [(Label, Label)],
}
impl ConstraintSystem<'_> {
pub fn solve_no_metadata(&self, config: Config) -> Result<SolveOutcome, FailureOutcome> {
crate::solve(&self.constraints, self.initial_guesses.variables(), config)
}
fn solve_no_metadata_inner<A: Analysis>(
&self,
config: Config,
) -> Result<SolveOutcomeAnalysis<A>, FailureOutcome> {
crate::solve_with_priority_inner(
&self.constraints,
self.initial_guesses.variables(),
config,
)
}
pub fn solve(&self) -> Result<Outcome, FailureOutcome> {
self.solve_with_config(Default::default())
}
pub fn solve_with_config_analysis(
&self,
config: Config,
) -> Result<OutcomeAnalysis, FailureOutcome> {
let (analysis, outcome) = self.solve_with_config_inner::<FreedomAnalysis>(config)?;
Ok(OutcomeAnalysis { analysis, outcome })
}
pub fn solve_with_config(&self, config: Config) -> Result<Outcome, FailureOutcome> {
let (NoAnalysis, outcome) = self.solve_with_config_inner::<NoAnalysis>(config)?;
Ok(outcome)
}
fn solve_with_config_inner<A: Analysis>(
&self,
config: Config,
) -> Result<(A, Outcome), FailureOutcome> {
let num_vars = self.initial_guesses.len();
let num_eqs = self
.constraints
.iter()
.map(|c| c.constraint().residual_dim())
.sum();
let SolveOutcomeAnalysis {
analysis,
outcome:
SolveOutcome {
iterations,
warnings,
final_values,
unsatisfied,
priority_solved,
},
} = self.solve_no_metadata_inner::<A>(config)?;
let num_points = self.inner_points.len();
let num_circles = self.inner_circles.len();
let num_arcs = self.inner_arcs.len();
let mut final_points = IndexMap::with_capacity(num_points);
for (i, point) in self.inner_points.iter().enumerate() {
let x_id = 2 * i;
let y_id = 2 * i + 1;
let p = Point {
x: final_values[x_id],
y: final_values[y_id],
};
final_points.insert(point.0.clone(), p);
}
let start_of_circles = 2 * self.inner_points.len();
let mut final_circles = IndexMap::with_capacity(num_circles);
for (i, circle_label) in self.inner_circles.iter().enumerate() {
let cx = final_values[start_of_circles + 3 * i]; let cy = final_values[start_of_circles + 3 * i + 1]; let rd = final_values[start_of_circles + 3 * i + 2]; final_circles.insert(
circle_label.0.clone(),
Circle {
radius: rd,
center: Point { x: cx, y: cy },
},
);
}
let start_of_arcs = start_of_circles + 3 * self.inner_circles.len();
let mut final_arcs = IndexMap::with_capacity(num_arcs);
for (i, arc_label) in self.inner_arcs.iter().enumerate() {
let ax = final_values[start_of_arcs + VARS_PER_ARC * i];
let ay = final_values[start_of_arcs + VARS_PER_ARC * i + 1];
let bx = final_values[start_of_arcs + VARS_PER_ARC * i + 2];
let by = final_values[start_of_arcs + VARS_PER_ARC * i + 3];
let cx = final_values[start_of_arcs + VARS_PER_ARC * i + 4];
let cy = final_values[start_of_arcs + VARS_PER_ARC * i + 5];
final_arcs.insert(
arc_label.0.clone(),
Arc {
center: Point { x: cx, y: cy },
a: Point { x: ax, y: ay },
b: Point { x: bx, y: by },
},
);
}
Ok((
analysis,
Outcome {
priority_solved,
unsatisfied,
iterations,
warnings,
points: final_points,
circles: final_circles,
arcs: final_arcs,
num_vars,
lines: self.inner_lines.to_vec(),
num_eqs,
},
))
}
}
#[derive(Debug)]
pub struct Outcome {
pub unsatisfied: Vec<usize>,
pub iterations: usize,
pub warnings: Vec<Warning>,
pub points: IndexMap<String, Point>,
pub circles: IndexMap<String, Circle>,
pub arcs: IndexMap<String, Arc>,
pub lines: Vec<(Label, Label)>,
pub num_vars: usize,
pub num_eqs: usize,
pub priority_solved: u32,
}
#[derive(Debug)]
pub struct OutcomeAnalysis {
pub analysis: FreedomAnalysis,
pub outcome: Outcome,
}
impl Outcome {
pub fn get_point(&self, label: &str) -> Option<Point> {
self.points.get(label).copied()
}
pub fn get_circle(&self, label: &str) -> Option<Circle> {
self.circles.get(label).copied()
}
pub fn get_arc(&self, label: &str) -> Option<Arc> {
self.arcs.get(label).copied()
}
}
impl OutcomeAnalysis {
#[cfg(test)]
pub fn get_point(&self, label: &str) -> Option<Point> {
self.outcome.get_point(label)
}
#[cfg(test)]
pub fn get_circle(&self, label: &str) -> Option<Circle> {
self.outcome.get_circle(label)
}
#[cfg(test)]
pub fn get_arc(&self, label: &str) -> Option<Arc> {
self.outcome.get_arc(label)
}
#[cfg(test)]
pub fn is_satisfied(&self) -> bool {
!self.is_unsatisfied()
}
#[cfg(test)]
pub fn is_unsatisfied(&self) -> bool {
!self.outcome.unsatisfied.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::textual::{PointGuess, instruction::FixPointComponent};
fn empty_problem() -> Problem {
Problem {
instructions: Vec::new(),
inner_points: Vec::new(),
inner_circles: Vec::new(),
inner_arcs: Vec::new(),
inner_lines: Vec::new(),
point_guesses: Vec::new(),
scalar_guesses: Vec::new(),
}
}
#[test]
fn missing_guess_is_reported() {
let mut problem = empty_problem();
problem.inner_points.push(Label::from("p"));
let err = problem
.to_constraint_system()
.err()
.expect("expected missing guess");
assert!(matches!(err, TextualError::MissingGuess { label } if label == "p"));
}
#[test]
fn unused_guesses_are_detected() {
let mut problem = empty_problem();
problem.point_guesses.push(PointGuess {
point: Label::from("ghost"),
guess: Point { x: 0.0, y: 0.0 },
});
let err = problem
.to_constraint_system()
.err()
.expect("expected unused guess error");
match err {
TextualError::UnusedGuesses { labels } => {
assert_eq!(labels.len(), 1);
assert_eq!(labels[0], "ghost");
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn undefined_point_in_instruction_errors() {
let mut problem = empty_problem();
problem.inner_points.push(Label::from("p"));
problem.point_guesses.push(PointGuess {
point: Label::from("p"),
guess: Point { x: 0.0, y: 0.0 },
});
problem
.instructions
.push(Instruction::FixPointComponent(FixPointComponent {
point: Label::from("missing"),
component: Component::X,
value: 2.5,
}));
let err = problem
.to_constraint_system()
.err()
.expect("expected undefined point error");
assert!(matches!(err, TextualError::UndefinedPoint { label } if label == "missing"));
}
}