use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::sync::Arc;
use std::time::Instant;

use bone_solver::{
    ConstraintSystem, CurveRadius3, DofConfig, DofReport, Ellipse3Handles, Line3Handle,
    NewtonConfig, Point3Handle, Residual, SolverError, analyze_dof, analyze_dof_at, decompose,
    dedup_preserving_order, minimal_unsatisfiable_subset, solve_newton_decomposed,
};
use bone_types::{
    Angle, Axis3, BudgetCeiling, DegreesOfFreedom, FeatureId, Length, Parameter, ParameterIndex,
    Plane3, Point3, ResidualIndex, Sketch3EntityRef, SketchEntityId, SketchItemId,
    SketchPlaneBasis, SketchRelationId, Tolerance, UnitVec3, dimensioned_serde, millimeter, radian,
};

use crate::sketch::{DimensionValue, EditOutcome, Sketch, SketchEdit, SketchEntity};
use crate::solve::{DragError, SketchStatusReport, status_from_solve};
use serde::{Deserialize, Serialize};
use slotmap::SlotMap;

const BASIS_TOLERANCE: Tolerance = Tolerance::new(1e-12);

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Point3Data {
    at: Point3,
}

impl Point3Data {
    #[must_use]
    pub const fn new(at: Point3) -> Self {
        Self { at }
    }

    #[must_use]
    pub const fn at(self) -> Point3 {
        self.at
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Line3Data {
    a: SketchEntityId,
    b: SketchEntityId,
    for_construction: bool,
}

impl Line3Data {
    #[must_use]
    pub const fn new(a: SketchEntityId, b: SketchEntityId, for_construction: bool) -> Self {
        Self {
            a,
            b,
            for_construction,
        }
    }

    #[must_use]
    pub const fn a(self) -> SketchEntityId {
        self.a
    }

    #[must_use]
    pub const fn b(self) -> SketchEntityId {
        self.b
    }

    #[must_use]
    pub const fn for_construction(self) -> bool {
        self.for_construction
    }

    #[must_use]
    pub const fn with_construction(self, for_construction: bool) -> Self {
        Self {
            for_construction,
            ..self
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Circle3Data {
    center: SketchEntityId,
    #[serde(with = "dimensioned_serde::length_si")]
    radius: Length,
    normal: UnitVec3,
    for_construction: bool,
}

impl Circle3Data {
    #[must_use]
    pub const fn new(
        center: SketchEntityId,
        radius: Length,
        normal: UnitVec3,
        for_construction: bool,
    ) -> Self {
        Self {
            center,
            radius,
            normal,
            for_construction,
        }
    }

    #[must_use]
    pub const fn center(self) -> SketchEntityId {
        self.center
    }

    #[must_use]
    pub const fn radius(self) -> Length {
        self.radius
    }

    #[must_use]
    pub const fn with_radius(self, radius: Length) -> Self {
        Self { radius, ..self }
    }

    #[must_use]
    pub const fn normal(self) -> UnitVec3 {
        self.normal
    }

    #[must_use]
    pub const fn for_construction(self) -> bool {
        self.for_construction
    }

    #[must_use]
    pub const fn with_construction(self, for_construction: bool) -> Self {
        Self {
            for_construction,
            ..self
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Arc3Data {
    center: SketchEntityId,
    start: SketchEntityId,
    end: SketchEntityId,
    normal: UnitVec3,
    for_construction: bool,
}

impl Arc3Data {
    #[must_use]
    pub const fn new(
        center: SketchEntityId,
        start: SketchEntityId,
        end: SketchEntityId,
        normal: UnitVec3,
        for_construction: bool,
    ) -> Self {
        Self {
            center,
            start,
            end,
            normal,
            for_construction,
        }
    }

    #[must_use]
    pub const fn center(self) -> SketchEntityId {
        self.center
    }

    #[must_use]
    pub const fn start(self) -> SketchEntityId {
        self.start
    }

    #[must_use]
    pub const fn end(self) -> SketchEntityId {
        self.end
    }

    #[must_use]
    pub const fn normal(self) -> UnitVec3 {
        self.normal
    }

    #[must_use]
    pub const fn for_construction(self) -> bool {
        self.for_construction
    }

    #[must_use]
    pub const fn with_construction(self, for_construction: bool) -> Self {
        Self {
            for_construction,
            ..self
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Ellipse3Data {
    center: SketchEntityId,
    major: SketchEntityId,
    minor: SketchEntityId,
    normal: UnitVec3,
    for_construction: bool,
}

impl Ellipse3Data {
    #[must_use]
    pub const fn new(
        center: SketchEntityId,
        major: SketchEntityId,
        minor: SketchEntityId,
        normal: UnitVec3,
        for_construction: bool,
    ) -> Self {
        Self {
            center,
            major,
            minor,
            normal,
            for_construction,
        }
    }

    #[must_use]
    pub const fn center(self) -> SketchEntityId {
        self.center
    }

    #[must_use]
    pub const fn major(self) -> SketchEntityId {
        self.major
    }

    #[must_use]
    pub const fn minor(self) -> SketchEntityId {
        self.minor
    }

    #[must_use]
    pub const fn normal(self) -> UnitVec3 {
        self.normal
    }

    #[must_use]
    pub const fn for_construction(self) -> bool {
        self.for_construction
    }

    #[must_use]
    pub const fn with_construction(self, for_construction: bool) -> Self {
        Self {
            for_construction,
            ..self
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Spline3Data {
    control: Vec<SketchEntityId>,
    #[serde(default)]
    closed: bool,
    #[serde(default)]
    for_construction: bool,
}

impl Spline3Data {
    #[must_use]
    pub fn new(control: Vec<SketchEntityId>, closed: bool, for_construction: bool) -> Self {
        Self {
            control,
            closed,
            for_construction,
        }
    }

    #[must_use]
    pub fn control(&self) -> &[SketchEntityId] {
        &self.control
    }

    #[must_use]
    pub const fn closed(&self) -> bool {
        self.closed
    }

    #[must_use]
    pub const fn for_construction(&self) -> bool {
        self.for_construction
    }

    #[must_use]
    pub fn with_construction(&self, for_construction: bool) -> Self {
        Self {
            control: self.control.clone(),
            closed: self.closed,
            for_construction,
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Sketch3EntityKind {
    Point,
    Line,
    Circle,
    Arc,
    Ellipse,
    Spline,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Sketch3Entity {
    Point(Point3Data),
    Line(Line3Data),
    Circle(Circle3Data),
    Arc(Arc3Data),
    Ellipse(Ellipse3Data),
    Spline(Spline3Data),
}

impl Sketch3Entity {
    #[must_use]
    pub const fn point(at: Point3) -> Self {
        Self::Point(Point3Data::new(at))
    }

    #[must_use]
    pub const fn line(a: SketchEntityId, b: SketchEntityId, for_construction: bool) -> Self {
        Self::Line(Line3Data::new(a, b, for_construction))
    }

    #[must_use]
    pub const fn circle(
        center: SketchEntityId,
        radius: Length,
        normal: UnitVec3,
        for_construction: bool,
    ) -> Self {
        Self::Circle(Circle3Data::new(center, radius, normal, for_construction))
    }

    #[must_use]
    pub const fn arc(
        center: SketchEntityId,
        start: SketchEntityId,
        end: SketchEntityId,
        normal: UnitVec3,
        for_construction: bool,
    ) -> Self {
        Self::Arc(Arc3Data::new(center, start, end, normal, for_construction))
    }

    #[must_use]
    pub const fn ellipse(
        center: SketchEntityId,
        major: SketchEntityId,
        minor: SketchEntityId,
        normal: UnitVec3,
        for_construction: bool,
    ) -> Self {
        Self::Ellipse(Ellipse3Data::new(
            center,
            major,
            minor,
            normal,
            for_construction,
        ))
    }

    #[must_use]
    pub fn spline(control: Vec<SketchEntityId>, closed: bool, for_construction: bool) -> Self {
        Self::Spline(Spline3Data::new(control, closed, for_construction))
    }

    #[must_use]
    pub const fn is_point(&self) -> bool {
        matches!(self, Self::Point(_))
    }

    #[must_use]
    pub fn for_construction(&self) -> bool {
        match self {
            Self::Point(_) => false,
            Self::Line(l) => l.for_construction(),
            Self::Circle(c) => c.for_construction(),
            Self::Arc(a) => a.for_construction(),
            Self::Ellipse(e) => e.for_construction(),
            Self::Spline(s) => s.for_construction(),
        }
    }

    #[must_use]
    pub const fn kind(&self) -> Sketch3EntityKind {
        match self {
            Self::Point(_) => Sketch3EntityKind::Point,
            Self::Line(_) => Sketch3EntityKind::Line,
            Self::Circle(_) => Sketch3EntityKind::Circle,
            Self::Arc(_) => Sketch3EntityKind::Arc,
            Self::Ellipse(_) => Sketch3EntityKind::Ellipse,
            Self::Spline(_) => Sketch3EntityKind::Spline,
        }
    }

    #[must_use]
    pub fn references(&self) -> Entity3Refs {
        Entity3Refs(match self {
            Self::Point(_) => Vec::new(),
            Self::Line(l) => vec![l.a(), l.b()],
            Self::Circle(c) => vec![c.center()],
            Self::Arc(a) => vec![a.center(), a.start(), a.end()],
            Self::Ellipse(e) => vec![e.center(), e.major(), e.minor()],
            Self::Spline(s) => s.control().to_vec(),
        })
    }

    #[must_use]
    pub fn with_construction(self, for_construction: bool) -> Option<Self> {
        match self {
            Self::Point(_) => None,
            Self::Line(l) => Some(Self::Line(l.with_construction(for_construction))),
            Self::Circle(c) => Some(Self::Circle(c.with_construction(for_construction))),
            Self::Arc(a) => Some(Self::Arc(a.with_construction(for_construction))),
            Self::Ellipse(e) => Some(Self::Ellipse(e.with_construction(for_construction))),
            Self::Spline(s) => Some(Self::Spline(s.with_construction(for_construction))),
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct Entity3Refs(Vec<SketchEntityId>);

impl IntoIterator for Entity3Refs {
    type Item = SketchEntityId;
    type IntoIter = std::vec::IntoIter<SketchEntityId>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Sketch3Relation {
    Coincident(SketchEntityId, SketchEntityId),
    Distance {
        a: SketchEntityId,
        b: SketchEntityId,
        #[serde(with = "dimensioned_serde::length_si")]
        value: Length,
        #[serde(default)]
        reference: bool,
    },
    OnPlane {
        point: SketchEntityId,
        normal: UnitVec3,
        #[serde(with = "dimensioned_serde::length_si")]
        offset: Length,
    },
    AlongAxis {
        line: SketchEntityId,
        axis: UnitVec3,
    },
    Horizontal(SketchEntityId),
    Vertical(SketchEntityId),
    Perpendicular(SketchEntityId, SketchEntityId),
    Midpoint {
        point: SketchEntityId,
        line: SketchEntityId,
    },
    PointOnLine {
        point: SketchEntityId,
        line: SketchEntityId,
    },
    Angle {
        a: SketchEntityId,
        b: SketchEntityId,
        #[serde(with = "dimensioned_serde::angle_si")]
        value: Angle,
        #[serde(default)]
        reference: bool,
    },
    Parallel(SketchEntityId, SketchEntityId),
    Tangent(SketchEntityId, SketchEntityId),
    Concentric(SketchEntityId, SketchEntityId),
    Equal(SketchEntityId, SketchEntityId),
    Symmetric {
        a: SketchEntityId,
        b: SketchEntityId,
        axis: SketchEntityId,
    },
    Radius {
        target: SketchEntityId,
        #[serde(with = "dimensioned_serde::length_si")]
        value: Length,
        #[serde(default)]
        reference: bool,
    },
    Diameter {
        target: SketchEntityId,
        #[serde(with = "dimensioned_serde::length_si")]
        value: Length,
        #[serde(default)]
        reference: bool,
    },
    Fix(SketchEntityId),
    Pierce {
        point: SketchEntityId,
        curve: Sketch3EntityRef,
    },
}

impl Sketch3Relation {
    #[must_use]
    pub const fn references(&self) -> Relation3Refs {
        match *self {
            Self::Coincident(a, b)
            | Self::Distance { a, b, .. }
            | Self::Perpendicular(a, b)
            | Self::Angle { a, b, .. }
            | Self::Parallel(a, b)
            | Self::Tangent(a, b)
            | Self::Concentric(a, b)
            | Self::Equal(a, b) => Relation3Refs([Some(a), Some(b), None]),
            Self::Symmetric { a, b, axis } => Relation3Refs([Some(a), Some(b), Some(axis)]),
            Self::Midpoint { point, line } | Self::PointOnLine { point, line } => {
                Relation3Refs([Some(point), Some(line), None])
            }
            Self::OnPlane { point, .. } | Self::Pierce { point, .. } => {
                Relation3Refs([Some(point), None, None])
            }
            Self::AlongAxis { line, .. } | Self::Horizontal(line) | Self::Vertical(line) => {
                Relation3Refs([Some(line), None, None])
            }
            Self::Radius { target, .. } | Self::Diameter { target, .. } => {
                Relation3Refs([Some(target), None, None])
            }
            Self::Fix(a) => Relation3Refs([Some(a), None, None]),
        }
    }

    #[must_use]
    pub const fn is_reference(self) -> bool {
        matches!(
            self,
            Self::Distance {
                reference: true,
                ..
            } | Self::Angle {
                reference: true,
                ..
            } | Self::Radius {
                reference: true,
                ..
            } | Self::Diameter {
                reference: true,
                ..
            }
        )
    }

    #[must_use]
    pub const fn as_reference(self) -> Self {
        match self {
            Self::Distance { a, b, value, .. } => Self::Distance {
                a,
                b,
                value,
                reference: true,
            },
            Self::Angle { a, b, value, .. } => Self::Angle {
                a,
                b,
                value,
                reference: true,
            },
            Self::Radius { target, value, .. } => Self::Radius {
                target,
                value,
                reference: true,
            },
            Self::Diameter { target, value, .. } => Self::Diameter {
                target,
                value,
                reference: true,
            },
            other => other,
        }
    }

    #[must_use]
    pub const fn as_driving(self) -> Self {
        match self {
            Self::Distance { a, b, value, .. } => Self::Distance {
                a,
                b,
                value,
                reference: false,
            },
            Self::Angle { a, b, value, .. } => Self::Angle {
                a,
                b,
                value,
                reference: false,
            },
            Self::Radius { target, value, .. } => Self::Radius {
                target,
                value,
                reference: false,
            },
            Self::Diameter { target, value, .. } => Self::Diameter {
                target,
                value,
                reference: false,
            },
            other => other,
        }
    }

    #[must_use]
    pub fn with_value(self, value: DimensionValue) -> Option<Self> {
        match (self, value) {
            (
                Self::Distance {
                    a, b, reference, ..
                },
                DimensionValue::Length(value),
            ) => Some(Self::Distance {
                a,
                b,
                value,
                reference,
            }),
            (
                Self::Radius {
                    target, reference, ..
                },
                DimensionValue::Length(value),
            ) => Some(Self::Radius {
                target,
                value,
                reference,
            }),
            (
                Self::Diameter {
                    target, reference, ..
                },
                DimensionValue::Length(value),
            ) => Some(Self::Diameter {
                target,
                value,
                reference,
            }),
            (
                Self::Angle {
                    a, b, reference, ..
                },
                DimensionValue::Angle(value),
            ) => Some(Self::Angle {
                a,
                b,
                value,
                reference,
            }),
            _ => None,
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Relation3Refs([Option<SketchEntityId>; 3]);

impl IntoIterator for Relation3Refs {
    type Item = SketchEntityId;
    type IntoIter = core::iter::Flatten<core::array::IntoIter<Option<SketchEntityId>, 3>>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter().flatten()
    }
}

#[derive(Clone, Debug, PartialEq)]
pub enum Sketch3Edit {
    AddEntity(Sketch3Entity),
    AddRelation(Sketch3Relation),
    DeleteEntity(SketchEntityId),
    DeleteRelation(SketchRelationId),
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Edit3Outcome {
    Entity(SketchEntityId),
    Relation(SketchRelationId),
    None,
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Sketch3Version(u64);

impl Sketch3Version {
    #[must_use]
    pub const fn value(self) -> u64 {
        self.0
    }

    #[must_use]
    fn bump(self) -> Self {
        Self(self.0.wrapping_add(1))
    }
}

type Entity3Map = SlotMap<SketchEntityId, Sketch3Entity>;
type Relation3Map = SlotMap<SketchRelationId, Sketch3Relation>;

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Sketch3 {
    #[serde(default = "SketchPlaneBasis::xy")]
    plane: SketchPlaneBasis,
    entities: Arc<Entity3Map>,
    entity_order: Arc<Vec<SketchEntityId>>,
    relations: Arc<Relation3Map>,
    relation_order: Arc<Vec<SketchRelationId>>,
    #[serde(skip)]
    version: Sketch3Version,
}

impl Default for Sketch3 {
    fn default() -> Self {
        Self::new()
    }
}

impl Sketch3 {
    #[must_use]
    pub fn new() -> Self {
        Self::on_plane(SketchPlaneBasis::xy())
    }

    #[must_use]
    pub fn on_plane(plane: SketchPlaneBasis) -> Self {
        Self {
            plane,
            entities: Arc::new(SlotMap::with_key()),
            entity_order: Arc::new(Vec::new()),
            relations: Arc::new(SlotMap::with_key()),
            relation_order: Arc::new(Vec::new()),
            version: Sketch3Version::default(),
        }
    }

    #[must_use]
    pub fn plane(&self) -> SketchPlaneBasis {
        self.plane
    }

    #[must_use]
    pub fn with_plane(&self, plane: SketchPlaneBasis) -> Self {
        Self {
            plane,
            entities: self.entities.clone(),
            entity_order: self.entity_order.clone(),
            relations: self.relations.clone(),
            relation_order: self.relation_order.clone(),
            version: self.version.bump(),
        }
    }

    #[must_use]
    pub fn entities(&self) -> &Entity3Map {
        &self.entities
    }

    #[must_use]
    pub fn relations(&self) -> &Relation3Map {
        &self.relations
    }

    pub fn pierce_curve_sketches(&self) -> impl Iterator<Item = FeatureId> + '_ {
        self.relation_order
            .iter()
            .filter_map(move |rid| match self.relations.get(*rid)? {
                Sketch3Relation::Pierce { curve, .. } => Some(curve.sketch),
                _ => None,
            })
    }

    #[must_use]
    pub fn entity_order(&self) -> &[SketchEntityId] {
        &self.entity_order
    }

    #[must_use]
    pub fn relation_order(&self) -> &[SketchRelationId] {
        &self.relation_order
    }

    #[must_use]
    pub fn version(&self) -> Sketch3Version {
        self.version
    }

    #[must_use]
    pub fn with_entity(&self, entity: Sketch3Entity) -> (Self, SketchEntityId) {
        let (id, entities) = insert(self.entities.clone(), entity);
        let entity_order = push(self.entity_order.clone(), id);
        let next = Self {
            plane: self.plane,
            entities,
            entity_order,
            relations: self.relations.clone(),
            relation_order: self.relation_order.clone(),
            version: self.version.bump(),
        };
        (next, id)
    }

    #[must_use]
    pub fn with_relation(&self, relation: Sketch3Relation) -> (Self, SketchRelationId) {
        let (id, relations) = insert(self.relations.clone(), relation);
        let relation_order = push(self.relation_order.clone(), id);
        let next = Self {
            plane: self.plane,
            entities: self.entities.clone(),
            entity_order: self.entity_order.clone(),
            relations,
            relation_order,
            version: self.version.bump(),
        };
        (next, id)
    }

    #[must_use]
    pub fn apply(&self, edit: Sketch3Edit) -> (Self, Edit3Outcome) {
        match edit {
            Sketch3Edit::AddEntity(entity) => {
                let (next, id) = self.with_entity(entity);
                (next, Edit3Outcome::Entity(id))
            }
            Sketch3Edit::AddRelation(relation) => {
                let (next, id) = self.with_relation(relation);
                (next, Edit3Outcome::Relation(id))
            }
            Sketch3Edit::DeleteEntity(id) => (self.without_entity(id), Edit3Outcome::None),
            Sketch3Edit::DeleteRelation(id) => (self.without_relation(id), Edit3Outcome::None),
        }
    }

    #[must_use]
    pub fn apply_all<I>(&self, edits: I) -> (Self, Vec<Edit3Outcome>)
    where
        I: IntoIterator<Item = Sketch3Edit>,
    {
        edits.into_iter().fold(
            (self.clone(), Vec::new()),
            |(sketch, mut outcomes), edit| {
                let (next, outcome) = sketch.apply(edit);
                outcomes.push(outcome);
                (next, outcomes)
            },
        )
    }

    #[must_use]
    pub fn lower(&self) -> (ConstraintSystem, Mapping3) {
        let (parameters, points, radii) = self.entity_order.iter().fold(
            (
                Vec::<Parameter>::new(),
                BTreeMap::<SketchEntityId, Point3Handle>::new(),
                BTreeMap::<SketchEntityId, ParameterIndex>::new(),
            ),
            |(mut parameters, mut points, mut radii), id| {
                match self.entities.get(*id) {
                    Some(Sketch3Entity::Point(p)) => {
                        let (x, y, z) = p.at().coords_mm();
                        let handle = Point3Handle {
                            x: push_parameter(&mut parameters, x),
                            y: push_parameter(&mut parameters, y),
                            z: push_parameter(&mut parameters, z),
                        };
                        points.insert(*id, handle);
                    }
                    Some(Sketch3Entity::Circle(c)) => {
                        let idx = push_parameter(&mut parameters, c.radius().get::<millimeter>());
                        radii.insert(*id, idx);
                    }
                    _ => {}
                }
                (parameters, points, radii)
            },
        );

        let intrinsic = self.entity_order.iter().flat_map(|eid| {
            lower_intrinsic(self.entities.get(*eid), &points)
                .into_iter()
                .map(move |r| (r, Origin3::Intrinsic(*eid)))
        });
        let relations = self.relation_order.iter().flat_map(|rid| {
            lower_relation(self, self.relations[*rid], &points, &radii, &parameters)
                .into_iter()
                .map(move |r| (r, Origin3::Relation(*rid)))
        });
        let rows: Vec<(Residual, Origin3)> = intrinsic.chain(relations).collect();

        let residual_origin: Vec<Origin3> = rows
            .iter()
            .flat_map(|(r, o)| std::iter::repeat_n(*o, r.rows()))
            .collect();
        let residuals: Vec<Residual> = rows.into_iter().map(|(r, _)| r).collect();
        let system = ConstraintSystem::new(parameters, residuals);
        let mapping = Mapping3 {
            points,
            radii,
            residual_origin,
        };
        (system, mapping)
    }

    pub fn solve(&self) -> Result<Sketch3, SolverError> {
        let (system, mapping) = self.lower();
        let decomposition = decompose(&system);
        match solve_newton_decomposed(&system, &decomposition, NewtonConfig::DEFAULT) {
            Ok(parameters) => {
                let report = analyze_dof_at(&system, &parameters, DofConfig::DEFAULT);
                if report.over_constrained().is_empty() {
                    Ok(self
                        .clone()
                        .apply_solution(&parameters, &mapping)
                        .remeasure_references())
                } else {
                    Err(narrow_over_defined(
                        &system,
                        &mapping,
                        report.over_constrained(),
                    ))
                }
            }
            Err(SolverError::NoSolutionFound { last }) => {
                let report = analyze_dof(&system, DofConfig::DEFAULT);
                if report.over_constrained().is_empty() {
                    Err(SolverError::NoSolutionFound { last })
                } else {
                    Err(narrow_over_defined(
                        &system,
                        &mapping,
                        report.over_constrained(),
                    ))
                }
            }
            Err(other) => Err(other),
        }
    }

    pub fn solve_with_drag_pins(
        &self,
        pins: &[(SketchEntityId, Point3)],
        budget: BudgetCeiling,
    ) -> Result<Sketch3, DragError> {
        let started = Instant::now();
        pins.iter()
            .try_for_each(|(id, _)| match self.entities.get(*id) {
                Some(Sketch3Entity::Point(_)) => Ok(()),
                Some(_) => Err(DragError::NotAPoint(*id)),
                None => Err(DragError::NotFound(*id)),
            })?;
        let fixed_points: BTreeSet<SketchEntityId> = self
            .relations
            .values()
            .filter_map(|rel| match *rel {
                Sketch3Relation::Fix(id) => Some(id),
                _ => None,
            })
            .collect();
        let deduped: Vec<(SketchEntityId, Point3)> = pins
            .iter()
            .copied()
            .enumerate()
            .filter(|(i, (id, _))| pins[..*i].iter().all(|(prior, _)| prior != id))
            .filter(|(_, (id, _))| !fixed_points.contains(id))
            .map(|(_, pair)| pair)
            .collect();
        let warm = deduped.iter().fold(self.clone(), |acc, (id, target)| {
            acc.with_point_at(*id, *target)
        });
        let (system, mapping) = warm.lower();
        let extra: Vec<Residual> = deduped
            .iter()
            .flat_map(|(id, target)| {
                let Some(handle) = mapping.point(*id) else {
                    unreachable!("Point entity must lower to a Point3Handle in the mapping")
                };
                let (tx, ty, tz) = target.coords_mm();
                [
                    Residual::Pin {
                        param: handle.x,
                        target: tx,
                    },
                    Residual::Pin {
                        param: handle.y,
                        target: ty,
                    },
                    Residual::Pin {
                        param: handle.z,
                        target: tz,
                    },
                ]
            })
            .collect();
        let system = system.with_extra_residuals(extra);
        let decomposition = decompose(&system);
        let remaining = BudgetCeiling::new(budget.duration().saturating_sub(started.elapsed()));
        let cfg = NewtonConfig {
            budget: Some(remaining),
            ..NewtonConfig::DEFAULT
        };
        let parameters = solve_newton_decomposed(&system, &decomposition, cfg)?;
        Ok(warm
            .apply_solution(&parameters, &mapping)
            .remeasure_references())
    }

    #[must_use]
    pub fn analyze_dof(&self) -> Sketch3DofReport {
        let (system, mapping) = self.lower();
        let decomposition = decompose(&system);
        let report = match solve_newton_decomposed(&system, &decomposition, NewtonConfig::DEFAULT) {
            Ok(parameters) => analyze_dof_at(&system, &parameters, DofConfig::DEFAULT),
            Err(_) => analyze_dof(&system, DofConfig::DEFAULT),
        };
        Sketch3DofReport::from_solver_report(&report, &mapping)
    }

    #[must_use]
    pub fn status(&self) -> SketchStatusReport {
        let dangling = collect_dangling3(self);
        if !dangling.is_empty() {
            return SketchStatusReport::dangling(dangling);
        }
        let (system, mapping) = self.lower();
        status_from_solve(
            &system,
            |rows| mapping.items(rows.iter().copied()),
            |redundant_rows| mapping.redundant_relations(redundant_rows),
        )
    }

    #[must_use]
    pub fn apply_solution(self, parameters: &[Parameter], mapping: &Mapping3) -> Self {
        let entities = mapping
            .points
            .iter()
            .fold(unwrap_arc(self.entities), |mut acc, (id, h)| {
                if let Some(slot @ Sketch3Entity::Point(_)) = acc.get_mut(*id) {
                    *slot = Sketch3Entity::Point(Point3Data::new(Point3::from_mm(
                        parameters[h.x.as_usize()].value(),
                        parameters[h.y.as_usize()].value(),
                        parameters[h.z.as_usize()].value(),
                    )));
                }
                acc
            });
        let entities = mapping.radii.iter().fold(entities, |mut acc, (id, idx)| {
            if let Some(slot @ Sketch3Entity::Circle(_)) = acc.get_mut(*id) {
                let Sketch3Entity::Circle(c) = *slot else {
                    unreachable!("guarded by the slot pattern");
                };
                *slot = Sketch3Entity::Circle(c.with_radius(Length::new::<millimeter>(
                    parameters[idx.as_usize()].value(),
                )));
            }
            acc
        });
        Self {
            plane: self.plane,
            entities: Arc::new(entities),
            entity_order: self.entity_order,
            relations: self.relations,
            relation_order: self.relation_order,
            version: self.version.bump(),
        }
    }

    #[must_use]
    pub fn point_position(&self, id: SketchEntityId) -> Option<Point3> {
        match self.entities.get(id)? {
            Sketch3Entity::Point(p) => Some(p.at()),
            _ => None,
        }
    }

    #[must_use]
    pub fn remeasure_references(self) -> Self {
        let updates: Vec<(SketchRelationId, DimensionValue)> = self
            .relation_order
            .iter()
            .filter_map(|rid| {
                let relation = *self.relations.get(*rid)?;
                relation
                    .is_reference()
                    .then(|| self.measure_reference(relation).map(|value| (*rid, value)))
                    .flatten()
            })
            .collect();
        updates.into_iter().fold(self, |sketch, (rid, value)| {
            sketch.with_relation_value(rid, value)
        })
    }

    fn measure_reference(&self, relation: Sketch3Relation) -> Option<DimensionValue> {
        match relation {
            Sketch3Relation::Distance { a, b, .. } => {
                Some(DimensionValue::Length(Length::new::<millimeter>(
                    distance_mm(self.point_position(a)?, self.point_position(b)?),
                )))
            }
            Sketch3Relation::Radius { target, .. } => self
                .curve_radius_mm(target)
                .map(|r| DimensionValue::Length(Length::new::<millimeter>(r))),
            Sketch3Relation::Diameter { target, .. } => self
                .curve_radius_mm(target)
                .map(|r| DimensionValue::Length(Length::new::<millimeter>(r * 2.0))),
            Sketch3Relation::Angle { a, b, .. } => {
                let da = self.line_direction3(a)?;
                let db = self.line_direction3(b)?;
                Some(DimensionValue::Angle(Angle::new::<radian>(angle_between3(
                    da, db,
                ))))
            }
            _ => None,
        }
    }

    fn curve_radius_mm(&self, id: SketchEntityId) -> Option<f64> {
        match self.entities.get(id)? {
            Sketch3Entity::Circle(c) => Some(c.radius().get::<millimeter>()),
            Sketch3Entity::Arc(a) => Some(distance_mm(
                self.point_position(a.center())?,
                self.point_position(a.start())?,
            )),
            _ => None,
        }
    }

    fn line_direction3(&self, id: SketchEntityId) -> Option<UnitVec3> {
        match self.entities.get(id)? {
            Sketch3Entity::Line(l) => {
                let (ax, ay, az) = self.point_position(l.a())?.coords_mm();
                let (bx, by, bz) = self.point_position(l.b())?.coords_mm();
                UnitVec3::try_from_components(bx - ax, by - ay, bz - az, BASIS_TOLERANCE).ok()
            }
            _ => None,
        }
    }

    #[must_use]
    pub fn rebased_onto(&self, plane: SketchPlaneBasis) -> Self {
        if plane == self.plane {
            return self.clone();
        }
        let old = Plane3::from(self.plane);
        let new = Plane3::from(plane);
        let tx_point = |p: Point3| -> Point3 {
            let (u, v) = old.project(p).coords_mm();
            let (ox, oy, oz) = old.origin().coords_mm();
            let (px, py, pz) = p.coords_mm();
            let (nx, ny, nz) = old.normal().components();
            let w = (px - ox).mul_add(nx, (py - oy).mul_add(ny, (pz - oz) * nz));
            new.point_at_local(u, v, w)
        };
        let tx_dir = |d: UnitVec3| -> UnitVec3 {
            let (dx, dy, dz) = d.components();
            let coeff = |axis: UnitVec3| {
                let (ax, ay, az) = axis.components();
                dx.mul_add(ax, dy.mul_add(ay, dz * az))
            };
            let (a, b, c) = (
                coeff(old.x_axis()),
                coeff(old.y_axis()),
                coeff(old.normal()),
            );
            let (nxx, nxy, nxz) = new.x_axis().components();
            let (nyx, nyy, nyz) = new.y_axis().components();
            let (nnx, nny, nnz) = new.normal().components();
            UnitVec3::try_from_components(
                a.mul_add(nxx, b.mul_add(nyx, c * nnx)),
                a.mul_add(nxy, b.mul_add(nyy, c * nny)),
                a.mul_add(nxz, b.mul_add(nyz, c * nnz)),
                BASIS_TOLERANCE,
            )
            .unwrap_or(d)
        };
        let entities =
            self.entity_order
                .iter()
                .fold(unwrap_arc(self.entities.clone()), |mut acc, id| {
                    if let Some(slot) = acc.get_mut(*id) {
                        *slot = rebase_entity(slot.clone(), &tx_point, &tx_dir);
                    }
                    acc
                });
        let relations =
            self.relation_order
                .iter()
                .fold(unwrap_arc(self.relations.clone()), |mut acc, id| {
                    if let Some(slot) = acc.get_mut(*id) {
                        *slot = rebase_relation(*slot, &tx_point, &tx_dir);
                    }
                    acc
                });
        Self {
            plane,
            entities: Arc::new(entities),
            entity_order: self.entity_order.clone(),
            relations: Arc::new(relations),
            relation_order: self.relation_order.clone(),
            version: self.version.bump(),
        }
    }

    pub fn resolved_pierces(
        &self,
        mut axis_of: impl FnMut(Sketch3EntityRef) -> Option<Axis3>,
    ) -> Result<Self, PierceUnresolved> {
        let plane = Plane3::from(self.plane);
        let anchors: Vec<(SketchRelationId, SketchEntityId, Point3)> = self
            .relation_order
            .iter()
            .filter_map(|rid| match *self.relations.get(*rid)? {
                Sketch3Relation::Pierce { point, curve } => Some((*rid, point, curve)),
                _ => None,
            })
            .map(|(rid, point, curve)| {
                let anchor = axis_of(curve)
                    .and_then(|axis| plane_line_intersection(plane, axis))
                    .ok_or(PierceUnresolved { curve })?;
                Ok((rid, point, anchor))
            })
            .collect::<Result<Vec<_>, PierceUnresolved>>()?;
        if anchors.is_empty() {
            return Ok(self.clone());
        }
        let entities = anchors.iter().fold(
            unwrap_arc(self.entities.clone()),
            |mut acc, (_, point, anchor)| {
                if let Some(slot) = acc.get_mut(*point) {
                    *slot = Sketch3Entity::point(*anchor);
                }
                acc
            },
        );
        let relations = anchors.iter().fold(
            unwrap_arc(self.relations.clone()),
            |mut acc, (rid, point, _)| {
                if let Some(slot) = acc.get_mut(*rid) {
                    *slot = Sketch3Relation::Fix(*point);
                }
                acc
            },
        );
        Ok(Self {
            plane: self.plane,
            entities: Arc::new(entities),
            entity_order: self.entity_order.clone(),
            relations: Arc::new(relations),
            relation_order: self.relation_order.clone(),
            version: self.version.bump(),
        })
    }

    #[must_use]
    pub fn project(&self) -> ProjectedSketch {
        self.project_onto(self.plane)
    }

    #[must_use]
    pub fn profile_plane(&self) -> Option<SketchPlaneBasis> {
        let points: Vec<Point3> = self
            .entity_order
            .iter()
            .filter_map(|id| self.point_position(*id))
            .collect();
        let circles: Vec<(Point3, UnitVec3)> = self
            .entity_order
            .iter()
            .filter_map(|id| match self.entities.get(*id) {
                Some(Sketch3Entity::Circle(c)) => {
                    self.point_position(c.center()).map(|p| (p, c.normal()))
                }
                _ => None,
            })
            .collect();
        let candidate = if points.len() >= 3 {
            if coplanar_with(&points, self.plane) {
                self.plane
            } else {
                fit_plane(&points)?
            }
        } else if let [(center, normal)] = circles.as_slice() {
            plane_from_point_normal(*center, *normal)?
        } else {
            self.plane
        };
        let planar = coplanar_with(&points, candidate)
            && circles.iter().all(|(center, normal)| {
                coplanar_with(std::slice::from_ref(center), candidate)
                    && parallel_unit(*normal, candidate.normal())
            });
        planar.then_some(candidate)
    }

    #[must_use]
    pub fn project_onto(&self, plane: SketchPlaneBasis) -> ProjectedSketch {
        let frame = Plane3::from(plane);
        let seed = ProjectedSketch {
            sketch: Sketch::new(plane),
            to_3d: HashMap::new(),
            to_2d: HashMap::new(),
        };
        let with_points =
            self.entity_order
                .iter()
                .fold(seed, |acc, id3| match self.point_position(*id3) {
                    Some(at) => acc.add(*id3, SketchEntity::point(frame.project(at))),
                    None => acc,
                });
        self.entity_order.iter().fold(with_points, |acc, id3| {
            match project_entity(self.entities.get(*id3), &acc.to_2d) {
                Some(entity) => acc.add(*id3, entity),
                None => acc,
            }
        })
    }

    #[must_use]
    pub fn with_construction(&self, id: SketchEntityId, for_construction: bool) -> Self {
        let mut entities = unwrap_arc(self.entities.clone());
        if let Some(slot) = entities.get_mut(id)
            && let Some(updated) = slot.clone().with_construction(for_construction)
        {
            *slot = updated;
        }
        Self {
            plane: self.plane,
            entities: Arc::new(entities),
            entity_order: self.entity_order.clone(),
            relations: self.relations.clone(),
            relation_order: self.relation_order.clone(),
            version: self.version.bump(),
        }
    }

    #[must_use]
    pub fn without_entity(&self, id: SketchEntityId) -> Self {
        if !self.entities.contains_key(id) {
            return self.clone();
        }
        let closure = crate::closure::entity_closure(&self.entities, id, Sketch3Entity::references);
        let entities = {
            let mut owned = unwrap_arc(self.entities.clone());
            closure.iter().for_each(|dead| {
                owned.remove(*dead);
            });
            owned
        };
        let entity_order = self
            .entity_order
            .iter()
            .copied()
            .filter(|eid| !closure.contains(eid))
            .collect();
        let touches_closure =
            |rel: &Sketch3Relation| rel.references().into_iter().any(|e| closure.contains(&e));
        let dropped: BTreeSet<SketchRelationId> = self
            .relations
            .iter()
            .filter_map(|(rid, rel)| touches_closure(rel).then_some(rid))
            .collect();
        let relations = {
            let mut owned = unwrap_arc(self.relations.clone());
            dropped.iter().for_each(|rid| {
                owned.remove(*rid);
            });
            owned
        };
        let relation_order = self
            .relation_order
            .iter()
            .copied()
            .filter(|rid| !dropped.contains(rid))
            .collect();
        Self {
            plane: self.plane,
            entities: Arc::new(entities),
            entity_order: Arc::new(entity_order),
            relations: Arc::new(relations),
            relation_order: Arc::new(relation_order),
            version: self.version.bump(),
        }
    }

    #[must_use]
    pub fn without_relation(&self, id: SketchRelationId) -> Self {
        if !self.relations.contains_key(id) {
            return self.clone();
        }
        let relations = {
            let mut owned = unwrap_arc(self.relations.clone());
            owned.remove(id);
            owned
        };
        let relation_order = self
            .relation_order
            .iter()
            .copied()
            .filter(|rid| *rid != id)
            .collect();
        Self {
            plane: self.plane,
            entities: self.entities.clone(),
            entity_order: self.entity_order.clone(),
            relations: Arc::new(relations),
            relation_order: Arc::new(relation_order),
            version: self.version.bump(),
        }
    }

    #[must_use]
    pub fn with_point_at(&self, id: SketchEntityId, at: Point3) -> Self {
        let mut entities = unwrap_arc(self.entities.clone());
        if let Some(slot @ Sketch3Entity::Point(_)) = entities.get_mut(id) {
            *slot = Sketch3Entity::point(at);
        }
        Self {
            plane: self.plane,
            entities: Arc::new(entities),
            entity_order: self.entity_order.clone(),
            relations: self.relations.clone(),
            relation_order: self.relation_order.clone(),
            version: self.version.bump(),
        }
    }

    #[must_use]
    pub fn with_reference(&self, id: SketchRelationId, reference: bool) -> Self {
        let mut relations = unwrap_arc(self.relations.clone());
        if let Some(slot) = relations.get_mut(id) {
            *slot = if reference {
                slot.as_reference()
            } else {
                slot.as_driving()
            };
        }
        Self {
            plane: self.plane,
            entities: self.entities.clone(),
            entity_order: self.entity_order.clone(),
            relations: Arc::new(relations),
            relation_order: self.relation_order.clone(),
            version: self.version.bump(),
        }
    }

    #[must_use]
    pub fn with_relation_value(&self, id: SketchRelationId, value: DimensionValue) -> Self {
        let mut relations = unwrap_arc(self.relations.clone());
        if let Some(slot) = relations.get_mut(id)
            && let Some(updated) = slot.with_value(value)
        {
            *slot = updated;
        }
        Self {
            plane: self.plane,
            entities: self.entities.clone(),
            entity_order: self.entity_order.clone(),
            relations: Arc::new(relations),
            relation_order: self.relation_order.clone(),
            version: self.version.bump(),
        }
    }

    #[must_use]
    pub fn axis_candidate(&self) -> Option<SketchEntityId> {
        self.unique_construction_line()
            .or_else(|| self.unique_standalone_line())
    }

    fn unique_construction_line(&self) -> Option<SketchEntityId> {
        let mut lines = self.entity_order.iter().copied().filter(|id| {
            matches!(
                self.entities.get(*id),
                Some(Sketch3Entity::Line(line)) if line.for_construction()
            )
        });
        let only = lines.next()?;
        lines.next().is_none().then_some(only)
    }

    fn unique_standalone_line(&self) -> Option<SketchEntityId> {
        let mut endpoint_uses: HashMap<SketchEntityId, usize> = HashMap::new();
        self.entity_order
            .iter()
            .filter_map(|id| self.segment_endpoints(*id))
            .flatten()
            .for_each(|point| *endpoint_uses.entry(point).or_default() += 1);
        let mut standalone = self.entity_order.iter().copied().filter(|id| {
            matches!(self.entities.get(*id), Some(Sketch3Entity::Line(_)))
                && self.segment_endpoints(*id).is_some_and(|[a, b]| {
                    endpoint_uses.get(&a) == Some(&1) && endpoint_uses.get(&b) == Some(&1)
                })
        });
        let only = standalone.next()?;
        standalone.next().is_none().then_some(only)
    }

    fn segment_endpoints(&self, id: SketchEntityId) -> Option<[SketchEntityId; 2]> {
        match self.entities.get(id)? {
            Sketch3Entity::Line(line) => Some([line.a(), line.b()]),
            Sketch3Entity::Arc(arc) => Some([arc.start(), arc.end()]),
            _ => None,
        }
    }

    #[must_use]
    pub fn line_axis(&self, line: SketchEntityId) -> Option<Axis3> {
        let Sketch3Entity::Line(l) = self.entities.get(line)? else {
            return None;
        };
        let from = self.point_position(l.a())?;
        let to = self.point_position(l.b())?;
        Axis3::through(from, to, BASIS_TOLERANCE).ok()
    }

    pub fn validate(&self) -> Result<(), Sketch3IntegrityError> {
        if let Some(id) = self
            .entity_order
            .iter()
            .find(|id| !self.entities.contains_key(**id))
        {
            return Err(Sketch3IntegrityError::DanglingEntity(*id));
        }
        let ordered: BTreeSet<SketchEntityId> = self.entity_order.iter().copied().collect();
        if let Some(id) = self.entities.keys().find(|id| !ordered.contains(id)) {
            return Err(Sketch3IntegrityError::UnorderedEntity(id));
        }
        if let Some(id) = self
            .relation_order
            .iter()
            .find(|id| !self.relations.contains_key(**id))
        {
            return Err(Sketch3IntegrityError::DanglingRelation(*id));
        }
        let ordered_relations: BTreeSet<SketchRelationId> =
            self.relation_order.iter().copied().collect();
        if let Some(id) = self
            .relations
            .keys()
            .find(|id| !ordered_relations.contains(id))
        {
            return Err(Sketch3IntegrityError::UnorderedRelation(id));
        }
        self.entity_order
            .iter()
            .copied()
            .try_for_each(|id| self.validate_entity(id))?;
        self.relation_order
            .iter()
            .copied()
            .try_for_each(|id| self.validate_relation(self.relations[id]))
    }

    fn validate_entity(&self, id: SketchEntityId) -> Result<(), Sketch3IntegrityError> {
        self.entities[id]
            .references()
            .into_iter()
            .try_for_each(|reference| self.require_point(reference))?;
        self.require_nondegenerate(id)
    }

    fn require_nondegenerate(&self, id: SketchEntityId) -> Result<(), Sketch3IntegrityError> {
        let ok = match &self.entities[id] {
            Sketch3Entity::Point(_) => true,
            Sketch3Entity::Line(l) => l.a() != l.b(),
            Sketch3Entity::Arc(a) => {
                a.center() != a.start() && a.start() != a.end() && a.center() != a.end()
            }
            Sketch3Entity::Circle(c) => c.radius() > Length::default(),
            Sketch3Entity::Ellipse(e) => {
                e.center() != e.major() && e.center() != e.minor() && e.major() != e.minor()
            }
            Sketch3Entity::Spline(s) => {
                let control = s.control();
                let minimum = if s.closed() { 3 } else { 2 };
                let positions: Option<Vec<Point3>> =
                    control.iter().map(|id| self.point_position(*id)).collect();
                let distinct = positions.as_deref().is_some_and(|p| {
                    let distinct_consecutive = p.windows(2).all(|w| w[0] != w[1]);
                    let distinct_wrap = !s.closed()
                        || match (p.first(), p.last()) {
                            (Some(first), Some(last)) => first != last,
                            _ => true,
                        };
                    distinct_consecutive && distinct_wrap
                });
                control.len() >= minimum && distinct
            }
        };
        if ok {
            Ok(())
        } else {
            Err(Sketch3IntegrityError::DegenerateEntity(id))
        }
    }

    pub fn validate_relation(
        &self,
        relation: Sketch3Relation,
    ) -> Result<(), Sketch3IntegrityError> {
        match relation {
            Sketch3Relation::Coincident(a, b) => {
                match (self.require_point(a), self.require_point(b)) {
                    (Ok(()), Ok(())) => Ok(()),
                    (Ok(()), Err(_)) => self.require_curve(b),
                    (Err(_), Ok(())) => self.require_curve(a),
                    (Err(e), Err(_)) => Err(e),
                }
            }
            Sketch3Relation::Distance { a, b, .. } => {
                self.require_point(a).and(self.require_point(b))
            }
            Sketch3Relation::OnPlane { point, .. } | Sketch3Relation::Fix(point) => {
                self.require_point(point)
            }
            Sketch3Relation::AlongAxis { line, .. }
            | Sketch3Relation::Horizontal(line)
            | Sketch3Relation::Vertical(line) => self.require_line(line),
            Sketch3Relation::Perpendicular(a, b) | Sketch3Relation::Angle { a, b, .. } => {
                self.require_line(a).and(self.require_line(b))
            }
            Sketch3Relation::PointOnLine { point, line } => {
                self.require_point(point).and(self.require_line(line))
            }
            Sketch3Relation::Midpoint { point, line } => self
                .require_point(point)
                .and(self.require_line(line))
                .and_then(|()| match self.entities.get(line) {
                    Some(Sketch3Entity::Line(l)) if point != l.a() && point != l.b() => Ok(()),
                    _ => Err(Sketch3IntegrityError::IncompatibleOperands(point, line)),
                }),
            Sketch3Relation::Parallel(a, b) => self.require_line(a).and(self.require_line(b)),
            Sketch3Relation::Equal(a, b) => {
                let (ka, kb) = (self.measurable_kind(a)?, self.measurable_kind(b)?);
                if ka == kb {
                    Ok(())
                } else {
                    Err(Sketch3IntegrityError::IncompatibleOperands(a, b))
                }
            }
            Sketch3Relation::Tangent(a, b) => {
                match (self.measurable_kind(a)?, self.measurable_kind(b)?) {
                    (MeasurableKind::Line, MeasurableKind::Line) => {
                        Err(Sketch3IntegrityError::IncompatibleOperands(a, b))
                    }
                    _ => Ok(()),
                }
            }
            Sketch3Relation::Concentric(a, b) => {
                self.require_centered(a).and(self.require_centered(b))
            }
            Sketch3Relation::Symmetric { a, b, .. } if a == b => {
                Err(Sketch3IntegrityError::IncompatibleOperands(a, b))
            }
            Sketch3Relation::Symmetric { a, b, axis } => self
                .require_point(a)
                .and(self.require_point(b))
                .and(self.require_line(axis)),
            Sketch3Relation::Radius { target, .. } | Sketch3Relation::Diameter { target, .. } => {
                self.require_curve(target)
            }
            Sketch3Relation::Pierce { point, .. } => self.require_point(point),
        }
    }

    fn require_point(&self, id: SketchEntityId) -> Result<(), Sketch3IntegrityError> {
        match self.entities.get(id) {
            Some(Sketch3Entity::Point(_)) => Ok(()),
            Some(_) => Err(Sketch3IntegrityError::NotAPoint(id)),
            None => Err(Sketch3IntegrityError::MissingReference(id)),
        }
    }

    fn require_line(&self, id: SketchEntityId) -> Result<(), Sketch3IntegrityError> {
        match self.entities.get(id) {
            Some(Sketch3Entity::Line(_)) => Ok(()),
            Some(_) => Err(Sketch3IntegrityError::NotALine(id)),
            None => Err(Sketch3IntegrityError::MissingReference(id)),
        }
    }

    fn require_curve(&self, id: SketchEntityId) -> Result<(), Sketch3IntegrityError> {
        match self.entities.get(id) {
            Some(Sketch3Entity::Circle(_) | Sketch3Entity::Arc(_) | Sketch3Entity::Ellipse(_)) => {
                Ok(())
            }
            Some(_) => Err(Sketch3IntegrityError::NotACurve(id)),
            None => Err(Sketch3IntegrityError::MissingReference(id)),
        }
    }

    fn require_centered(&self, id: SketchEntityId) -> Result<(), Sketch3IntegrityError> {
        match self.entities.get(id) {
            Some(Sketch3Entity::Circle(_) | Sketch3Entity::Arc(_) | Sketch3Entity::Ellipse(_)) => {
                Ok(())
            }
            Some(_) => Err(Sketch3IntegrityError::NotACurve(id)),
            None => Err(Sketch3IntegrityError::MissingReference(id)),
        }
    }

    fn measurable_kind(&self, id: SketchEntityId) -> Result<MeasurableKind, Sketch3IntegrityError> {
        match self.entities.get(id) {
            Some(Sketch3Entity::Line(_)) => Ok(MeasurableKind::Line),
            Some(Sketch3Entity::Circle(_) | Sketch3Entity::Arc(_)) => Ok(MeasurableKind::Radial),
            Some(_) => Err(Sketch3IntegrityError::NotMeasurable(id)),
            None => Err(Sketch3IntegrityError::MissingReference(id)),
        }
    }
}

#[derive(Copy, Clone, PartialEq, Eq)]
enum MeasurableKind {
    Line,
    Radial,
}

pub struct ProjectedSketch {
    sketch: Sketch,
    to_3d: HashMap<SketchEntityId, SketchEntityId>,
    to_2d: HashMap<SketchEntityId, SketchEntityId>,
}

impl ProjectedSketch {
    #[must_use]
    pub fn sketch(&self) -> &Sketch {
        &self.sketch
    }

    #[must_use]
    pub fn into_sketch(self) -> Sketch {
        self.sketch
    }

    #[must_use]
    pub fn to_3d(&self, projected: SketchEntityId) -> Option<SketchEntityId> {
        self.to_3d.get(&projected).copied()
    }

    #[must_use]
    pub fn to_2d(&self, three: SketchEntityId) -> Option<SketchEntityId> {
        self.to_2d.get(&three).copied()
    }

    #[must_use]
    fn add(self, id3: SketchEntityId, entity: SketchEntity) -> Self {
        let Self {
            sketch,
            mut to_3d,
            mut to_2d,
        } = self;
        let fallback = sketch.clone();
        match sketch.apply(SketchEdit::AddEntity(entity)) {
            Ok((sketch, EditOutcome::Entity(id2))) => {
                to_3d.insert(id2, id3);
                to_2d.insert(id3, id2);
                Self {
                    sketch,
                    to_3d,
                    to_2d,
                }
            }
            Ok((sketch, _)) => Self {
                sketch,
                to_3d,
                to_2d,
            },
            Err(_) => Self {
                sketch: fallback,
                to_3d,
                to_2d,
            },
        }
    }
}

fn rebase_entity(
    entity: Sketch3Entity,
    tx_point: &impl Fn(Point3) -> Point3,
    tx_dir: &impl Fn(UnitVec3) -> UnitVec3,
) -> Sketch3Entity {
    match entity {
        Sketch3Entity::Point(p) => Sketch3Entity::point(tx_point(p.at())),
        Sketch3Entity::Circle(c) => Sketch3Entity::Circle(Circle3Data::new(
            c.center(),
            c.radius(),
            tx_dir(c.normal()),
            c.for_construction(),
        )),
        Sketch3Entity::Arc(a) => Sketch3Entity::Arc(Arc3Data::new(
            a.center(),
            a.start(),
            a.end(),
            tx_dir(a.normal()),
            a.for_construction(),
        )),
        Sketch3Entity::Ellipse(e) => Sketch3Entity::Ellipse(Ellipse3Data::new(
            e.center(),
            e.major(),
            e.minor(),
            tx_dir(e.normal()),
            e.for_construction(),
        )),
        Sketch3Entity::Line(_) | Sketch3Entity::Spline(_) => entity,
    }
}

fn rebase_relation(
    relation: Sketch3Relation,
    tx_point: &impl Fn(Point3) -> Point3,
    tx_dir: &impl Fn(UnitVec3) -> UnitVec3,
) -> Sketch3Relation {
    match relation {
        Sketch3Relation::OnPlane {
            point,
            normal,
            offset,
        } => {
            let normal = tx_dir(normal);
            let (nx, ny, nz) = normal.components();
            let o = offset.get::<millimeter>();
            let (tx, ty, tz) = tx_point(Point3::from_mm(nx * o, ny * o, nz * o)).coords_mm();
            Sketch3Relation::OnPlane {
                point,
                normal,
                offset: Length::new::<millimeter>(nx.mul_add(tx, ny.mul_add(ty, nz * tz))),
            }
        }
        Sketch3Relation::AlongAxis { line, axis } => Sketch3Relation::AlongAxis {
            line,
            axis: tx_dir(axis),
        },
        other => other,
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct PierceUnresolved {
    pub curve: Sketch3EntityRef,
}

const LINE_PLANE_TOLERANCE: f64 = 1.0e-9;

fn plane_line_intersection(plane: Plane3, axis: Axis3) -> Option<Point3> {
    let (nx, ny, nz) = plane.normal().components();
    let (dx, dy, dz) = axis.direction().components();
    let denom = dx.mul_add(nx, dy.mul_add(ny, dz * nz));
    if denom.abs() < LINE_PLANE_TOLERANCE {
        return None;
    }
    let (ox, oy, oz) = axis.origin().coords_mm();
    let (cx, cy, cz) = plane.origin().coords_mm();
    let numerator = (cx - ox).mul_add(nx, (cy - oy).mul_add(ny, (cz - oz) * nz));
    let t = numerator / denom;
    Some(Point3::from_mm(
        t.mul_add(dx, ox),
        t.mul_add(dy, oy),
        t.mul_add(dz, oz),
    ))
}

const PROFILE_PLANE_TOLERANCE_MM: f64 = 1.0e-6;
const PROFILE_PLANE_RELATIVE: f64 = 1.0e-6;
const NEWELL_RELATIVE: f64 = 1.0e-9;
const NEWELL_FLOOR: f64 = 1.0e-12;
const PARALLEL_TOLERANCE: f64 = 1.0e-9;

fn coplanar_with(points: &[Point3], plane: SketchPlaneBasis) -> bool {
    let (nx, ny, nz) = plane.normal().components();
    let (ox, oy, oz) = plane.origin().coords_mm();
    let signed = |p: &Point3| {
        let (px, py, pz) = p.coords_mm();
        (px - ox, py - oy, pz - oz)
    };
    let span = points
        .iter()
        .map(|p| {
            let (dx, dy, dz) = signed(p);
            dx.mul_add(dx, dy.mul_add(dy, dz * dz)).sqrt()
        })
        .fold(0.0_f64, f64::max);
    let tolerance = PROFILE_PLANE_TOLERANCE_MM.max(span * PROFILE_PLANE_RELATIVE);
    points.iter().all(|p| {
        let (dx, dy, dz) = signed(p);
        dx.mul_add(nx, dy.mul_add(ny, dz * nz)).abs() <= tolerance
    })
}

fn parallel_unit(a: UnitVec3, b: UnitVec3) -> bool {
    let (ax, ay, az) = a.components();
    let (bx, by, bz) = b.components();
    (ax.mul_add(bx, ay.mul_add(by, az * bz)).abs() - 1.0).abs() <= PARALLEL_TOLERANCE
}

fn plane_from_point_normal(origin: Point3, normal: UnitVec3) -> Option<SketchPlaneBasis> {
    let (x_axis, y_axis) = normal.orthonormal_basis();
    SketchPlaneBasis::new(origin, x_axis, y_axis, BASIS_TOLERANCE).ok()
}

fn fit_plane(points: &[Point3]) -> Option<SketchPlaneBasis> {
    let n = points.len();
    if n < 3 {
        return None;
    }
    let (nx, ny, nz) = (0..n).fold((0.0, 0.0, 0.0), |(nx, ny, nz), i| {
        let (x0, y0, z0) = points[i].coords_mm();
        let (x1, y1, z1) = points[(i + 1) % n].coords_mm();
        (
            (y0 - y1).mul_add(z0 + z1, nx),
            (z0 - z1).mul_add(x0 + x1, ny),
            (x0 - x1).mul_add(y0 + y1, nz),
        )
    });
    let (rx, ry, rz) = points[0].coords_mm();
    let scale_sq = points
        .iter()
        .map(|p| {
            let (px, py, pz) = p.coords_mm();
            (px - rx).mul_add(px - rx, (py - ry).mul_add(py - ry, (pz - rz) * (pz - rz)))
        })
        .fold(0.0_f64, f64::max);
    let newell_floor = (scale_sq * NEWELL_RELATIVE).max(NEWELL_FLOOR);
    let normal = UnitVec3::try_from_components(nx, ny, nz, Tolerance::new(newell_floor)).ok()?;
    let (x_axis, y_axis) = normal.orthonormal_basis();
    SketchPlaneBasis::new(points[0], x_axis, y_axis, BASIS_TOLERANCE).ok()
}

fn project_entity(
    entity: Option<&Sketch3Entity>,
    to_2d: &HashMap<SketchEntityId, SketchEntityId>,
) -> Option<SketchEntity> {
    let map = |id| to_2d.get(&id).copied();
    match entity? {
        Sketch3Entity::Point(_) => None,
        Sketch3Entity::Line(l) => Some(SketchEntity::line(
            map(l.a())?,
            map(l.b())?,
            l.for_construction(),
        )),
        Sketch3Entity::Circle(c) => Some(SketchEntity::circle(
            map(c.center())?,
            c.radius(),
            c.for_construction(),
        )),
        Sketch3Entity::Arc(a) => Some(SketchEntity::arc(
            map(a.center())?,
            map(a.start())?,
            map(a.end())?,
            a.for_construction(),
        )),
        Sketch3Entity::Ellipse(e) => Some(SketchEntity::ellipse(
            map(e.center())?,
            map(e.major())?,
            map(e.minor())?,
            e.for_construction(),
        )),
        Sketch3Entity::Spline(s) => {
            let control = s
                .control()
                .iter()
                .map(|id| map(*id))
                .collect::<Option<Vec<_>>>()?;
            Some(SketchEntity::spline(
                control,
                s.closed(),
                s.for_construction(),
            ))
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum Sketch3IntegrityError {
    #[error("entity {0:?} is indexed in order but absent from storage")]
    DanglingEntity(SketchEntityId),
    #[error("entity {0:?} is stored but absent from the order index")]
    UnorderedEntity(SketchEntityId),
    #[error("relation {0:?} is indexed in order but absent from storage")]
    DanglingRelation(SketchRelationId),
    #[error("relation {0:?} is stored but absent from the order index")]
    UnorderedRelation(SketchRelationId),
    #[error("reference {0:?} does not exist")]
    MissingReference(SketchEntityId),
    #[error("reference {0:?} must be a point")]
    NotAPoint(SketchEntityId),
    #[error("reference {0:?} must be a line")]
    NotALine(SketchEntityId),
    #[error("reference {0:?} must be a curve")]
    NotACurve(SketchEntityId),
    #[error("reference {0:?} must be a line, circle, or arc")]
    NotMeasurable(SketchEntityId),
    #[error("references {0:?} and {1:?} are not compatible operands for this relation")]
    IncompatibleOperands(SketchEntityId, SketchEntityId),
    #[error("entity {0:?} is degenerate")]
    DegenerateEntity(SketchEntityId),
}

impl PartialEq for Sketch3 {
    fn eq(&self, other: &Self) -> bool {
        self.plane == other.plane
            && *self.entity_order == *other.entity_order
            && entries_equal(&self.entity_order, &self.entities, &other.entities)
            && *self.relation_order == *other.relation_order
            && entries_equal(&self.relation_order, &self.relations, &other.relations)
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
enum Origin3 {
    Relation(SketchRelationId),
    Intrinsic(SketchEntityId),
}

#[derive(Clone, Debug)]
pub struct Mapping3 {
    points: BTreeMap<SketchEntityId, Point3Handle>,
    radii: BTreeMap<SketchEntityId, ParameterIndex>,
    residual_origin: Vec<Origin3>,
}

impl Mapping3 {
    #[must_use]
    pub fn point(&self, id: SketchEntityId) -> Option<Point3Handle> {
        self.points.get(&id).copied()
    }

    #[must_use]
    pub fn radius(&self, id: SketchEntityId) -> Option<ParameterIndex> {
        self.radii.get(&id).copied()
    }

    #[must_use]
    pub fn entity_of_parameter(&self, idx: ParameterIndex) -> Option<SketchEntityId> {
        self.points
            .iter()
            .find_map(|(id, h)| (h.x == idx || h.y == idx || h.z == idx).then_some(*id))
            .or_else(|| {
                self.radii
                    .iter()
                    .find_map(|(id, p)| (*p == idx).then_some(*id))
            })
    }

    #[must_use]
    pub fn item_of_residual(&self, idx: ResidualIndex) -> Option<SketchItemId> {
        self.residual_origin
            .get(idx.as_usize())
            .map(|origin| match origin {
                Origin3::Relation(id) => SketchItemId::Relation(*id),
                Origin3::Intrinsic(id) => SketchItemId::Entity(*id),
            })
    }

    #[must_use]
    pub fn redundant_relations(
        &self,
        redundant_rows: &BTreeSet<ResidualIndex>,
    ) -> Vec<SketchItemId> {
        let counts = self.residual_origin.iter().enumerate().fold(
            BTreeMap::<SketchRelationId, (usize, usize)>::new(),
            |mut acc, (row, origin)| {
                if let Origin3::Relation(id) = origin {
                    let is_redundant = u32::try_from(row)
                        .ok()
                        .is_some_and(|r| redundant_rows.contains(&ResidualIndex::new(r)));
                    let entry = acc.entry(*id).or_insert((0, 0));
                    entry.0 += 1;
                    entry.1 += usize::from(is_redundant);
                }
                acc
            },
        );
        counts
            .into_iter()
            .filter_map(|(id, (total, redundant))| {
                (total == redundant).then_some(SketchItemId::Relation(id))
            })
            .collect()
    }

    #[must_use]
    pub fn items(&self, indices: impl IntoIterator<Item = ResidualIndex>) -> Vec<SketchItemId> {
        indices
            .into_iter()
            .filter_map(|i| self.item_of_residual(i))
            .collect()
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct Sketch3DofReport {
    dof: DegreesOfFreedom,
    under_constrained: Vec<SketchEntityId>,
    over_constrained: Vec<SketchItemId>,
}

impl Sketch3DofReport {
    #[must_use]
    pub fn dof(&self) -> DegreesOfFreedom {
        self.dof
    }

    #[must_use]
    pub fn under_constrained(&self) -> &[SketchEntityId] {
        &self.under_constrained
    }

    #[must_use]
    pub fn over_constrained(&self) -> &[SketchItemId] {
        &self.over_constrained
    }

    fn from_solver_report(report: &DofReport, mapping: &Mapping3) -> Self {
        Self {
            dof: report.dof(),
            under_constrained: dedup_preserving_order(
                report
                    .under_constrained()
                    .iter()
                    .filter_map(|p| mapping.entity_of_parameter(*p)),
            ),
            over_constrained: dedup_preserving_order(
                mapping.items(report.over_constrained().iter().copied()),
            ),
        }
    }
}

fn collect_dangling3(sketch: &Sketch3) -> Vec<SketchItemId> {
    let exists = |id: SketchEntityId| sketch.entities.contains_key(id);
    let dangling_entities = sketch
        .entities
        .iter()
        .filter(|(_, entity)| !entity.references().into_iter().all(exists))
        .map(|(eid, _)| SketchItemId::Entity(eid));
    let dangling_relations = sketch
        .relations
        .iter()
        .filter(|(_, relation)| !relation.references().into_iter().all(exists))
        .map(|(rid, _)| SketchItemId::Relation(rid));
    dangling_entities.chain(dangling_relations).collect()
}

fn narrow_over_defined(
    system: &ConstraintSystem,
    mapping: &Mapping3,
    over_flagged: &[ResidualIndex],
) -> SolverError {
    let mus = minimal_unsatisfiable_subset(system, over_flagged, DofConfig::DEFAULT);
    let rows = if mus.is_empty() { over_flagged } else { &mus };
    SolverError::OverDefined {
        conflicts: dedup_preserving_order(mapping.items(rows.iter().copied())),
    }
}

fn lower_intrinsic(
    entity: Option<&Sketch3Entity>,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
) -> Vec<Residual> {
    match entity {
        Some(Sketch3Entity::Arc(arc)) => {
            match (
                points.get(&arc.center()),
                points.get(&arc.start()),
                points.get(&arc.end()),
            ) {
                (Some(center), Some(a), Some(b)) => {
                    let normal = normal_components(arc.normal());
                    vec![
                        Residual::EqualDistance3 {
                            center: *center,
                            a: *a,
                            b: *b,
                        },
                        Residual::InPlane3 {
                            point: *a,
                            origin: *center,
                            normal,
                        },
                        Residual::InPlane3 {
                            point: *b,
                            origin: *center,
                            normal,
                        },
                    ]
                }
                _ => Vec::new(),
            }
        }
        Some(Sketch3Entity::Ellipse(ellipse)) => {
            match (
                points.get(&ellipse.center()),
                points.get(&ellipse.major()),
                points.get(&ellipse.minor()),
            ) {
                (Some(center), Some(a), Some(b)) => {
                    let normal = normal_components(ellipse.normal());
                    vec![
                        Residual::OrthogonalSpokes3 {
                            center: *center,
                            a: *a,
                            b: *b,
                        },
                        Residual::InPlane3 {
                            point: *a,
                            origin: *center,
                            normal,
                        },
                        Residual::InPlane3 {
                            point: *b,
                            origin: *center,
                            normal,
                        },
                    ]
                }
                _ => Vec::new(),
            }
        }
        _ => Vec::new(),
    }
}

fn lower_coincident3(
    sketch: &Sketch3,
    a: SketchEntityId,
    b: SketchEntityId,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
    radii: &BTreeMap<SketchEntityId, ParameterIndex>,
) -> Vec<Residual> {
    match (points.get(&a), points.get(&b)) {
        (Some(pa), Some(pb)) => vec![Residual::Coincident3(*pa, *pb)],
        (Some(p), None) => point_on_curve3(sketch, *p, b, points, radii),
        (None, Some(p)) => point_on_curve3(sketch, *p, a, points, radii),
        (None, None) => Vec::new(),
    }
}

fn point_on_curve3(
    sketch: &Sketch3,
    point: Point3Handle,
    curve_id: SketchEntityId,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
    radii: &BTreeMap<SketchEntityId, ParameterIndex>,
) -> Vec<Residual> {
    let core = match sketch.entities().get(curve_id) {
        Some(Sketch3Entity::Ellipse(e)) => match (
            points.get(&e.center()),
            points.get(&e.major()),
            points.get(&e.minor()),
        ) {
            (Some(center), Some(major), Some(minor)) => {
                Some(Residual::CoincidentPointEllipse3(Ellipse3Handles {
                    point,
                    center: *center,
                    major: *major,
                    minor: *minor,
                }))
            }
            _ => None,
        },
        _ => curve_radius3(sketch, curve_id, points, radii)
            .map(|curve| Residual::CoincidentPointCurve3 { point, curve }),
    };
    let Some(core) = core else {
        return Vec::new();
    };
    let in_plane =
        curve_plane3(sketch, curve_id, points).map(|(origin, normal)| Residual::InPlane3 {
            point,
            origin,
            normal,
        });
    std::iter::once(core).chain(in_plane).collect()
}

fn lower_on_plane3(
    point: SketchEntityId,
    normal: UnitVec3,
    offset: Length,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
) -> Vec<Residual> {
    points
        .get(&point)
        .map(|p| {
            let (nx, ny, nz) = normal.components();
            vec![Residual::OnPlane {
                point: *p,
                normal: [nx, ny, nz],
                offset_mm: offset.get::<millimeter>(),
            }]
        })
        .unwrap_or_default()
}

fn lower_along_axis3(
    sketch: &Sketch3,
    line: SketchEntityId,
    axis: UnitVec3,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
) -> Vec<Residual> {
    line3_handle(sketch, line, points)
        .map(|handle| {
            vec![Residual::AlongAxis {
                line: handle,
                basis: perpendicular_basis(axis),
            }]
        })
        .unwrap_or_default()
}

fn lower_relation(
    sketch: &Sketch3,
    relation: Sketch3Relation,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
    radii: &BTreeMap<SketchEntityId, ParameterIndex>,
    parameters: &[Parameter],
) -> Vec<Residual> {
    if relation.is_reference() {
        return Vec::new();
    }
    match relation {
        Sketch3Relation::Coincident(a, b) => lower_coincident3(sketch, a, b, points, radii),
        Sketch3Relation::Distance { a, b, value, .. } => match (points.get(&a), points.get(&b)) {
            (Some(pa), Some(pb)) => vec![Residual::Distance3 {
                a: *pa,
                b: *pb,
                value_mm: value.get::<millimeter>(),
            }],
            _ => Vec::new(),
        },
        Sketch3Relation::OnPlane {
            point,
            normal,
            offset,
        } => lower_on_plane3(point, normal, offset, points),
        Sketch3Relation::AlongAxis { line, axis } => lower_along_axis3(sketch, line, axis, points),
        Sketch3Relation::Horizontal(line) => {
            lower_along_axis3(sketch, line, sketch.plane.x_axis(), points)
        }
        Sketch3Relation::Vertical(line) => {
            lower_along_axis3(sketch, line, sketch.plane.y_axis(), points)
        }
        Sketch3Relation::Perpendicular(la, lb) => match (
            line3_handle(sketch, la, points),
            line3_handle(sketch, lb, points),
        ) {
            (Some(a), Some(b)) => vec![Residual::Perpendicular3(a, b)],
            _ => Vec::new(),
        },
        Sketch3Relation::Midpoint { point, line } => {
            match (points.get(&point), line3_handle(sketch, line, points)) {
                (Some(p), Some(handle)) => vec![Residual::Midpoint3 {
                    point: *p,
                    line: handle,
                }],
                _ => Vec::new(),
            }
        }
        Sketch3Relation::PointOnLine { point, line } => {
            match (points.get(&point), line3_handle(sketch, line, points)) {
                (Some(p), Some(handle)) => vec![Residual::PointOnLine3 {
                    point: *p,
                    line: handle,
                }],
                _ => Vec::new(),
            }
        }
        Sketch3Relation::Angle { a, b, value, .. } => match (
            line3_handle(sketch, a, points),
            line3_handle(sketch, b, points),
        ) {
            (Some(la), Some(lb)) => vec![Residual::AngularBetweenLines3 {
                a: la,
                b: lb,
                angle_rad: value.get::<radian>(),
            }],
            _ => Vec::new(),
        },
        Sketch3Relation::Parallel(a, b) => lower_parallel3(sketch, a, b, points),
        Sketch3Relation::Tangent(a, b) => lower_tangent3(sketch, a, b, points, radii, parameters),
        Sketch3Relation::Concentric(a, b) => lower_concentric3(sketch, a, b, points),
        Sketch3Relation::Equal(a, b) => lower_equal3(sketch, a, b, points, radii),
        Sketch3Relation::Symmetric { a, b, axis } => lower_symmetric3(sketch, a, b, axis, points),
        Sketch3Relation::Radius { target, value, .. } => {
            lower_radius3(sketch, target, value.get::<millimeter>(), points, radii)
        }
        Sketch3Relation::Diameter { target, value, .. } => lower_radius3(
            sketch,
            target,
            value.get::<millimeter>() * 0.5,
            points,
            radii,
        ),
        Sketch3Relation::Fix(id) => lower_fix(id, points, parameters),
        Sketch3Relation::Pierce { .. } => Vec::new(),
    }
}

fn lower_parallel3(
    sketch: &Sketch3,
    a: SketchEntityId,
    b: SketchEntityId,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
) -> Vec<Residual> {
    match (
        line3_handle(sketch, a, points),
        line3_handle(sketch, b, points),
    ) {
        (Some(la), Some(lb)) => vec![Residual::Parallel3(la, lb)],
        _ => Vec::new(),
    }
}

fn lower_concentric3(
    sketch: &Sketch3,
    a: SketchEntityId,
    b: SketchEntityId,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
) -> Vec<Residual> {
    match (
        curve_center3(sketch, a).and_then(|c| points.get(&c).copied()),
        curve_center3(sketch, b).and_then(|c| points.get(&c).copied()),
    ) {
        (Some(p), Some(q)) => vec![Residual::Coincident3(p, q)],
        _ => Vec::new(),
    }
}

fn lower_radius3(
    sketch: &Sketch3,
    target: SketchEntityId,
    value_mm: f64,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
    radii: &BTreeMap<SketchEntityId, ParameterIndex>,
) -> Vec<Residual> {
    curve_radius3(sketch, target, points, radii)
        .map(|curve| vec![Residual::RadiusCurve3 { curve, value_mm }])
        .unwrap_or_default()
}

fn lower_tangent3(
    sketch: &Sketch3,
    a: SketchEntityId,
    b: SketchEntityId,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
    radii: &BTreeMap<SketchEntityId, ParameterIndex>,
    parameters: &[Parameter],
) -> Vec<Residual> {
    let line_id = [a, b]
        .into_iter()
        .find(|id| matches!(sketch.entities.get(*id), Some(Sketch3Entity::Line(_))));
    if let Some(lid) = line_id {
        let curve_id = if lid == a { b } else { a };
        return match (
            line3_handle(sketch, lid, points),
            curve_radius3(sketch, curve_id, points, radii),
        ) {
            (Some(line), Some(curve)) => vec![Residual::TangentLineCurve3 { line, curve }],
            _ => Vec::new(),
        };
    }
    match (
        curve_radius3(sketch, a, points, radii),
        curve_radius3(sketch, b, points, radii),
    ) {
        (Some(ca), Some(cb)) => vec![Residual::TangentCurveCurve3 {
            a: ca,
            b: cb,
            internal: tangent_is_internal(sketch, a, b, points, radii, parameters),
        }],
        _ => Vec::new(),
    }
}

fn tangent_is_internal(
    sketch: &Sketch3,
    a: SketchEntityId,
    b: SketchEntityId,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
    radii: &BTreeMap<SketchEntityId, ParameterIndex>,
    parameters: &[Parameter],
) -> bool {
    let (Some((ca, ra)), Some((cb, rb))) = (
        curve_metrics3(sketch, a, points, radii, parameters),
        curve_metrics3(sketch, b, points, radii, parameters),
    ) else {
        return false;
    };
    let gap = ((ca.0 - cb.0).powi(2) + (ca.1 - cb.1).powi(2) + (ca.2 - cb.2).powi(2)).sqrt();
    (gap - (ra - rb).abs()).abs() < (gap - (ra + rb)).abs()
}

fn curve_metrics3(
    sketch: &Sketch3,
    id: SketchEntityId,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
    radii: &BTreeMap<SketchEntityId, ParameterIndex>,
    parameters: &[Parameter],
) -> Option<((f64, f64, f64), f64)> {
    let curve = curve_radius3(sketch, id, points, radii)?;
    let center = point_value3(parameters, curve.center());
    let radius = match curve {
        CurveRadius3::Explicit { radius, .. } => parameters[radius.as_usize()].value(),
        CurveRadius3::FromSpoke { center: c, spoke } => {
            let cp = point_value3(parameters, c);
            let sp = point_value3(parameters, spoke);
            ((sp.0 - cp.0).powi(2) + (sp.1 - cp.1).powi(2) + (sp.2 - cp.2).powi(2)).sqrt()
        }
    };
    Some((center, radius))
}

fn point_value3(parameters: &[Parameter], handle: Point3Handle) -> (f64, f64, f64) {
    (
        parameters[handle.x.as_usize()].value(),
        parameters[handle.y.as_usize()].value(),
        parameters[handle.z.as_usize()].value(),
    )
}

fn curve_plane3(
    sketch: &Sketch3,
    id: SketchEntityId,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
) -> Option<(Point3Handle, [f64; 3])> {
    let (center, normal) = match sketch.entities.get(id)? {
        Sketch3Entity::Circle(c) => (c.center(), c.normal()),
        Sketch3Entity::Arc(arc) => (arc.center(), arc.normal()),
        Sketch3Entity::Ellipse(e) => (e.center(), e.normal()),
        _ => return None,
    };
    Some((*points.get(&center)?, normal_components(normal)))
}

fn normal_components(normal: UnitVec3) -> [f64; 3] {
    let (x, y, z) = normal.components();
    [x, y, z]
}

fn lower_equal3(
    sketch: &Sketch3,
    a: SketchEntityId,
    b: SketchEntityId,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
    radii: &BTreeMap<SketchEntityId, ParameterIndex>,
) -> Vec<Residual> {
    match (
        line3_handle(sketch, a, points),
        line3_handle(sketch, b, points),
    ) {
        (Some(la), Some(lb)) => vec![Residual::EqualLength3(la, lb)],
        _ => match (
            curve_radius3(sketch, a, points, radii),
            curve_radius3(sketch, b, points, radii),
        ) {
            (Some(ca), Some(cb)) => vec![Residual::EqualRadius3(ca, cb)],
            _ => Vec::new(),
        },
    }
}

fn lower_symmetric3(
    sketch: &Sketch3,
    a: SketchEntityId,
    b: SketchEntityId,
    axis: SketchEntityId,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
) -> Vec<Residual> {
    match (
        points.get(&a).copied(),
        points.get(&b).copied(),
        line3_handle(sketch, axis, points),
    ) {
        (Some(pa), Some(pb), Some(axis_line)) => vec![Residual::Symmetric3 {
            a: pa,
            b: pb,
            axis: axis_line,
        }],
        _ => Vec::new(),
    }
}

fn curve_radius3(
    sketch: &Sketch3,
    id: SketchEntityId,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
    radii: &BTreeMap<SketchEntityId, ParameterIndex>,
) -> Option<CurveRadius3> {
    match sketch.entities.get(id)? {
        Sketch3Entity::Circle(c) => Some(CurveRadius3::Explicit {
            center: *points.get(&c.center())?,
            radius: *radii.get(&id)?,
        }),
        Sketch3Entity::Arc(arc) => Some(CurveRadius3::FromSpoke {
            center: *points.get(&arc.center())?,
            spoke: *points.get(&arc.start())?,
        }),
        _ => None,
    }
}

fn distance_mm(a: Point3, b: Point3) -> f64 {
    let (ax, ay, az) = a.coords_mm();
    let (bx, by, bz) = b.coords_mm();
    let (dx, dy, dz) = (bx - ax, by - ay, bz - az);
    dx.mul_add(dx, dy.mul_add(dy, dz * dz)).sqrt()
}

fn angle_between3(a: UnitVec3, b: UnitVec3) -> f64 {
    let (ax, ay, az) = a.components();
    let (bx, by, bz) = b.components();
    ax.mul_add(bx, ay.mul_add(by, az * bz))
        .clamp(-1.0, 1.0)
        .acos()
}

fn curve_center3(sketch: &Sketch3, id: SketchEntityId) -> Option<SketchEntityId> {
    match sketch.entities.get(id)? {
        Sketch3Entity::Circle(c) => Some(c.center()),
        Sketch3Entity::Arc(a) => Some(a.center()),
        Sketch3Entity::Ellipse(e) => Some(e.center()),
        _ => None,
    }
}

fn lower_fix(
    id: SketchEntityId,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
    parameters: &[Parameter],
) -> Vec<Residual> {
    points
        .get(&id)
        .map(|h| {
            [h.x, h.y, h.z]
                .into_iter()
                .map(|param| Residual::Pin {
                    param,
                    target: parameters[param.as_usize()].value(),
                })
                .collect()
        })
        .unwrap_or_default()
}

fn line3_handle(
    sketch: &Sketch3,
    id: SketchEntityId,
    points: &BTreeMap<SketchEntityId, Point3Handle>,
) -> Option<Line3Handle> {
    let Sketch3Entity::Line(l) = sketch.entities.get(id)? else {
        return None;
    };
    Some(Line3Handle {
        a: *points.get(&l.a())?,
        b: *points.get(&l.b())?,
    })
}

fn perpendicular_basis(axis: UnitVec3) -> [[f64; 3]; 2] {
    let (u, v) = axis.orthonormal_basis();
    let (ux, uy, uz) = u.components();
    let (vx, vy, vz) = v.components();
    [[ux, uy, uz], [vx, vy, vz]]
}

fn push_parameter(out: &mut Vec<Parameter>, value: f64) -> ParameterIndex {
    let idx = ParameterIndex::new(u32::try_from(out.len()).unwrap_or(u32::MAX));
    out.push(Parameter::new(value));
    idx
}

fn insert<K, V>(map: Arc<SlotMap<K, V>>, value: V) -> (K, Arc<SlotMap<K, V>>)
where
    K: slotmap::Key,
    V: Clone,
{
    let mut owned = unwrap_arc(map);
    let id = owned.insert(value);
    (id, Arc::new(owned))
}

fn push<T: Clone>(list: Arc<Vec<T>>, value: T) -> Arc<Vec<T>> {
    let mut owned = unwrap_arc(list);
    owned.push(value);
    Arc::new(owned)
}

fn unwrap_arc<T: Clone>(arc: Arc<T>) -> T {
    Arc::try_unwrap(arc).unwrap_or_else(|shared| (*shared).clone())
}

fn entries_equal<K, V>(order: &[K], lhs: &SlotMap<K, V>, rhs: &SlotMap<K, V>) -> bool
where
    K: slotmap::Key,
    V: PartialEq,
{
    order.iter().all(|id| lhs.get(*id) == rhs.get(*id))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn point(sketch: &Sketch3, x: f64, y: f64, z: f64) -> (Sketch3, SketchEntityId) {
        let (next, outcome) = sketch.apply(Sketch3Edit::AddEntity(Sketch3Entity::point(
            Point3::from_mm(x, y, z),
        )));
        let Edit3Outcome::Entity(id) = outcome else {
            panic!("expected entity outcome");
        };
        (next, id)
    }

    fn relation(sketch: &Sketch3, rel: Sketch3Relation) -> Sketch3 {
        let (next, outcome) = sketch.apply(Sketch3Edit::AddRelation(rel));
        let Edit3Outcome::Relation(_) = outcome else {
            panic!("expected relation outcome");
        };
        next
    }

    fn line(sketch: &Sketch3, a: SketchEntityId, b: SketchEntityId) -> (Sketch3, SketchEntityId) {
        let (next, outcome) =
            sketch.apply(Sketch3Edit::AddEntity(Sketch3Entity::line(a, b, false)));
        let Edit3Outcome::Entity(id) = outcome else {
            panic!("expected entity outcome");
        };
        (next, id)
    }

    fn coords(sketch: &Sketch3, id: SketchEntityId) -> (f64, f64, f64) {
        let Some(p) = sketch.point_position(id) else {
            panic!("expected a point");
        };
        p.coords_mm()
    }

    #[test]
    fn spline_references_every_control_point_and_validates() {
        let (sketch, ids) = (0..5).fold((Sketch3::new(), Vec::new()), |(sketch, mut ids), i| {
            let (next, id) = point(&sketch, f64::from(i), 0.0, 0.0);
            ids.push(id);
            (next, ids)
        });
        let (sketch, spline) = sketch.with_entity(Sketch3Entity::spline(ids.clone(), false, false));
        let Some(stored) = sketch.entities().get(spline) else {
            panic!("spline is stored");
        };
        let refs: Vec<_> = stored.references().into_iter().collect();
        assert_eq!(
            refs, ids,
            "references list every control point, not just three"
        );
        assert!(sketch.validate().is_ok(), "a five-point spline validates");

        let (closed_two, ids) =
            (0..2).fold((Sketch3::new(), Vec::new()), |(sketch, mut ids), i| {
                let (next, id) = point(&sketch, f64::from(i), 0.0, 0.0);
                ids.push(id);
                (next, ids)
            });
        let (closed_two, _) = closed_two.with_entity(Sketch3Entity::spline(ids, true, false));
        assert!(
            closed_two.validate().is_err(),
            "a closed spline needs at least three control points",
        );
    }

    #[test]
    fn profile_plane_accepts_coplanar_and_refuses_skew() {
        let (flat, _a) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (flat, _b) = point(&flat, 10.0, 0.0, 0.0);
        let (flat, _c) = point(&flat, 10.0, 10.0, 0.0);
        let (flat, _d) = point(&flat, 0.0, 10.0, 0.0);
        assert!(
            flat.profile_plane().is_some(),
            "a coplanar 3D sketch resolves a profile plane",
        );
        let (skew, _e) = point(&flat, 5.0, 5.0, 8.0);
        assert!(
            skew.profile_plane().is_none(),
            "a point off the plane makes the profile non-planar",
        );
    }

    #[test]
    fn profile_plane_uses_a_lone_off_plane_circle_normal() {
        let Ok(normal) = UnitVec3::try_from_components(0.0, 1.0, 1.0, Tolerance::new(1.0e-9))
        else {
            panic!("a nonzero normal");
        };
        let (sketch, center) = point(&Sketch3::new(), 0.0, 5.0, 5.0);
        let (sketch, _circle) = sketch.with_entity(Sketch3Entity::circle(
            center,
            Length::new::<millimeter>(3.0),
            normal,
            false,
        ));
        let Some(plane) = sketch.profile_plane() else {
            panic!("a lone off-plane circle resolves its own plane");
        };
        assert!(
            parallel_unit(plane.normal(), normal),
            "the profile plane adopts the circle's normal instead of flattening onto the stored plane",
        );
    }

    #[test]
    fn point_coincident_on_ellipse_solves_onto_the_rim() {
        let (sketch, center) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, major) = point(&sketch, 4.0, 0.0, 0.0);
        let (sketch, minor) = point(&sketch, 0.0, 2.0, 0.0);
        let (sketch, ellipse) = sketch.with_entity(Sketch3Entity::ellipse(
            center,
            major,
            minor,
            UnitVec3::z_axis(),
            false,
        ));
        let (sketch, free) = point(&sketch, 3.0, 3.0, 1.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(center));
        let sketch = relation(&sketch, Sketch3Relation::Fix(major));
        let sketch = relation(&sketch, Sketch3Relation::Fix(minor));
        let sketch = relation(&sketch, Sketch3Relation::Coincident(free, ellipse));
        let Ok(solved) = sketch.solve() else {
            panic!("a point-on-ellipse coincidence converges");
        };
        let (px, py, pz) = coords(&solved, free);
        let on_curve = (px / 4.0).powi(2) + (py / 2.0).powi(2) - 1.0;
        assert!(
            on_curve.abs() < 1.0e-6,
            "point lands on the ellipse: {on_curve}"
        );
        assert!(pz.abs() < 1.0e-6, "point lands in the ellipse plane: {pz}");
    }

    #[test]
    fn driven_dimension_remeasures_after_solve() {
        let (sketch, a) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, b) = point(&sketch, 5.0, 0.0, 0.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(a));
        let sketch = relation(&sketch, Sketch3Relation::Fix(b));
        let sketch = relation(
            &sketch,
            Sketch3Relation::Distance {
                a,
                b,
                value: Length::new::<millimeter>(10.0),
                reference: true,
            },
        );
        let Ok(solved) = sketch.solve() else {
            panic!("a reference dimension never over-constrains");
        };
        let measured = solved.relations().values().find_map(|r| match r {
            Sketch3Relation::Distance {
                value,
                reference: true,
                ..
            } => Some(value.get::<millimeter>()),
            _ => None,
        });
        let Some(measured) = measured else {
            panic!("the driven distance survives the solve");
        };
        assert!(
            (measured - 5.0).abs() < 1.0e-6,
            "the driven dimension re-measures the actual 5mm gap, not its stale 10mm seed: {measured}",
        );
    }

    #[test]
    fn driven_radius_and_diameter_remeasure_after_solve() {
        let (sketch, center) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, c) = circle(&sketch, center, 3.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(center));
        let sketch = relation(
            &sketch,
            Sketch3Relation::Radius {
                target: c,
                value: Length::new::<millimeter>(10.0),
                reference: true,
            },
        );
        let sketch = relation(
            &sketch,
            Sketch3Relation::Diameter {
                target: c,
                value: Length::new::<millimeter>(99.0),
                reference: true,
            },
        );
        let Ok(solved) = sketch.solve() else {
            panic!("two reference dimensions on a circle never over-constrain");
        };
        let radius = solved.relations().values().find_map(|r| match r {
            Sketch3Relation::Radius {
                value,
                reference: true,
                ..
            } => Some(value.get::<millimeter>()),
            _ => None,
        });
        let diameter = solved.relations().values().find_map(|r| match r {
            Sketch3Relation::Diameter {
                value,
                reference: true,
                ..
            } => Some(value.get::<millimeter>()),
            _ => None,
        });
        assert!(
            radius.is_some_and(|v| (v - 3.0).abs() < 1.0e-6),
            "the driven radius reads the 3mm circle, not its 10mm seed: {radius:?}",
        );
        assert!(
            diameter.is_some_and(|v| (v - 6.0).abs() < 1.0e-6),
            "the driven diameter reads twice the radius (6mm), not its 99mm seed: {diameter:?}",
        );
    }

    #[test]
    fn driven_angle_remeasures_after_solve() {
        let (sketch, origin) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, end_a) = point(&sketch, 5.0, 0.0, 0.0);
        let (sketch, end_b) = point(&sketch, 0.0, 5.0, 0.0);
        let (sketch, line_a) = line(&sketch, origin, end_a);
        let (sketch, line_b) = line(&sketch, origin, end_b);
        let sketch = relation(&sketch, Sketch3Relation::Fix(origin));
        let sketch = relation(&sketch, Sketch3Relation::Fix(end_a));
        let sketch = relation(&sketch, Sketch3Relation::Fix(end_b));
        let sketch = relation(
            &sketch,
            Sketch3Relation::Angle {
                a: line_a,
                b: line_b,
                value: Angle::new::<radian>(0.5),
                reference: true,
            },
        );
        let Ok(solved) = sketch.solve() else {
            panic!("a reference angle never over-constrains");
        };
        let measured = solved.relations().values().find_map(|r| match r {
            Sketch3Relation::Angle {
                value,
                reference: true,
                ..
            } => Some(value.get::<radian>()),
            _ => None,
        });
        let Some(measured) = measured else {
            panic!("the driven angle survives the solve");
        };
        assert!(
            (measured - core::f64::consts::FRAC_PI_2).abs() < 1.0e-6,
            "the driven angle re-measures the real 90 degrees, not its 0.5 radian seed: {measured}",
        );
    }

    #[test]
    fn profile_plane_tolerance_scales_with_sketch_size() {
        let big = 1000.0;
        let (flat, _a) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (flat, _b) = point(&flat, big, 0.0, 0.0);
        let (flat, _c) = point(&flat, big, big, 1.0e-5);
        let (flat, _d) = point(&flat, 0.0, big, 0.0);
        assert!(
            flat.profile_plane().is_some(),
            "a 1m sketch tolerates sub-micron coplanarity noise instead of a fixed 1nm floor",
        );

        let (bent, _a) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (bent, _b) = point(&bent, big, 0.0, 0.0);
        let (bent, _c) = point(&bent, big, big, 1.0);
        let (bent, _d) = point(&bent, 0.0, big, 0.0);
        assert!(
            bent.profile_plane().is_none(),
            "a 1mm bend across a 1m sketch is still non-planar",
        );
    }

    #[test]
    fn status_reports_fully_defined_for_a_fixed_point() {
        let (sketch, p) = point(&Sketch3::new(), 1.0, 2.0, 3.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(p));
        let report = sketch.status();
        assert_eq!(report.status(), bone_types::SketchStatus::FullyDefined);
        assert!(report.offending().is_empty());
    }

    #[test]
    fn status_reports_under_defined_for_a_free_point() {
        let (sketch, _p) = point(&Sketch3::new(), 1.0, 2.0, 3.0);
        let report = sketch.status();
        assert_eq!(report.status(), bone_types::SketchStatus::UnderDefined);
        assert!(report.dof().value() > 0);
    }

    #[test]
    fn status_flags_the_conflict_for_two_driving_distances() {
        let (sketch, a) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, b) = point(&sketch, 5.0, 0.0, 0.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(a));
        let sketch = relation(
            &sketch,
            Sketch3Relation::Distance {
                a,
                b,
                value: Length::new::<millimeter>(5.0),
                reference: false,
            },
        );
        let sketch = relation(
            &sketch,
            Sketch3Relation::Distance {
                a,
                b,
                value: Length::new::<millimeter>(7.0),
                reference: false,
            },
        );
        let report = sketch.status();
        assert_eq!(report.status(), bone_types::SketchStatus::OverDefined);
        assert!(
            !report.offending().is_empty(),
            "over-defined status names a conflict"
        );
    }

    #[test]
    fn status_flags_a_dangling_reference() {
        let (sketch, a) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, b) = point(&sketch, 5.0, 0.0, 0.0);
        let (mut sketch, edge) = line(&sketch, a, b);
        assert!(Arc::make_mut(&mut sketch.entities).remove(b).is_some());
        let report = sketch.status();
        assert_eq!(report.status(), bone_types::SketchStatus::Dangling);
        assert!(
            report.offending().contains(&SketchItemId::Entity(edge)),
            "the line whose endpoint vanished is flagged"
        );
    }

    #[test]
    fn fixed_point_solves_to_its_target() {
        let (sketch, p) = point(&Sketch3::new(), 3.0, -4.0, 5.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(p));
        let Ok(solved) = sketch.solve() else {
            panic!("a pinned point is linear and must converge");
        };
        let (x, y, z) = coords(&solved, p);
        assert!((x - 3.0).abs() < 1e-6, "x {x}");
        assert!((y - -4.0).abs() < 1e-6, "y {y}");
        assert!((z - 5.0).abs() < 1e-6, "z {z}");
    }

    #[test]
    fn coincident_second_point_converges_onto_pinned_first() {
        let (sketch, first) = point(&Sketch3::new(), 1.0, -2.0, 3.5);
        let (sketch, second) = point(&sketch, 9.0, 9.0, 9.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(first));
        let sketch = relation(&sketch, Sketch3Relation::Coincident(second, first));
        let Ok(solved) = sketch.solve() else {
            panic!("coincident over a pinned point is linear and must converge");
        };
        let (x, y, z) = coords(&solved, second);
        assert!((x - 1.0).abs() < 1e-6, "x {x}");
        assert!((y - -2.0).abs() < 1e-6, "y {y}");
        assert!((z - 3.5).abs() < 1e-6, "z {z}");
    }

    #[test]
    fn along_x_axis_with_distance_lays_line_along_x() {
        let (sketch, start) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, end) = point(&sketch, 5.0, 2.7, -1.3);
        let (sketch, segment) = line(&sketch, start, end);
        let sketch = relation(&sketch, Sketch3Relation::Fix(start));
        let sketch = relation(
            &sketch,
            Sketch3Relation::AlongAxis {
                line: segment,
                axis: UnitVec3::x_axis(),
            },
        );
        let sketch = relation(
            &sketch,
            Sketch3Relation::Distance {
                a: start,
                b: end,
                value: Length::new::<millimeter>(7.0),
                reference: false,
            },
        );
        let Ok(solved) = sketch.solve() else {
            panic!("along-x with a distance must converge");
        };
        let (sx, sy, sz) = coords(&solved, start);
        let (ex, ey, ez) = coords(&solved, end);
        assert!((ey - sy).abs() < 1e-6, "y should match start: {ey} vs {sy}");
        assert!((ez - sz).abs() < 1e-6, "z should match start: {ez} vs {sz}");
        assert!(
            ((ex - sx).abs() - 7.0).abs() < 1e-6,
            "x must differ by the distance: {ex} vs {sx}"
        );
        let Some(axis) = solved.line_axis(segment) else {
            panic!("solved line has a direction");
        };
        let (dx, dy, dz) = axis.direction().components();
        assert!(dx.abs() > 1.0 - 1e-6, "axis points along x: {dx}");
        assert!(dy.abs() < 1e-6, "axis y component {dy}");
        assert!(dz.abs() < 1e-6, "axis z component {dz}");
    }

    #[test]
    fn reference_dimension_measures_without_over_constraining() {
        let (sketch, a) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, b) = point(&sketch, 1.0, 0.0, 0.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(a));
        let driving = Sketch3Relation::Distance {
            a,
            b,
            value: Length::new::<millimeter>(5.0),
            reference: false,
        };
        let second = Sketch3Relation::Distance {
            a,
            b,
            value: Length::new::<millimeter>(10.0),
            reference: false,
        };
        let constrained = relation(&sketch, driving);

        let Err(SolverError::OverDefined { conflicts }) = relation(&constrained, second).solve()
        else {
            panic!("two driving distances over one segment must surface OverDefined");
        };
        assert!(
            !conflicts.is_empty(),
            "over-defined solve narrows to a non-empty conflict set",
        );

        let reference = second.as_reference();
        assert!(reference.is_reference());
        let Ok(solved) = relation(&constrained, reference).solve() else {
            panic!("a reference distance adds no residual, so the segment stays well-posed");
        };
        let (ax, ay, az) = coords(&solved, a);
        let (bx, by, bz) = coords(&solved, b);
        let length = (bx - ax)
            .mul_add(bx - ax, (by - ay).mul_add(by - ay, (bz - az) * (bz - az)))
            .sqrt();
        assert!(
            (length - 5.0).abs() < 1e-6,
            "geometry follows the driving 5mm, not the reference 10mm: {length}"
        );
    }

    #[test]
    fn with_relation_value_retargets_a_distance_dimension() {
        let (sketch, a) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, b) = point(&sketch, 5.0, 0.0, 0.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(a));
        let (sketch, dim) = sketch.with_relation(Sketch3Relation::Distance {
            a,
            b,
            value: Length::new::<millimeter>(5.0),
            reference: false,
        });
        let retargeted = sketch
            .with_relation_value(dim, DimensionValue::Length(Length::new::<millimeter>(12.0)));
        let Ok(solved) = retargeted.solve() else {
            panic!("the retargeted distance stays well-posed and must converge");
        };
        let (ax, ay, az) = coords(&solved, a);
        let (bx, by, bz) = coords(&solved, b);
        let length = (bx - ax)
            .mul_add(bx - ax, (by - ay).mul_add(by - ay, (bz - az) * (bz - az)))
            .sqrt();
        assert!(
            (length - 12.0).abs() < 1e-6,
            "distance follows the edited 12mm value: {length}"
        );
    }

    #[test]
    fn coincident_pulls_a_point_onto_a_circle() {
        let (sketch, center) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, free) = point(&sketch, 10.0, 0.0, 0.0);
        let (sketch, circle) = {
            let (next, outcome) = sketch.apply(Sketch3Edit::AddEntity(Sketch3Entity::circle(
                center,
                Length::new::<millimeter>(4.0),
                UnitVec3::z_axis(),
                false,
            )));
            let Edit3Outcome::Entity(id) = outcome else {
                panic!("expected a circle entity");
            };
            (next, id)
        };
        let sketch = relation(&sketch, Sketch3Relation::Fix(center));
        let sketch = relation(
            &sketch,
            Sketch3Relation::Radius {
                target: circle,
                value: Length::new::<millimeter>(4.0),
                reference: false,
            },
        );
        let sketch = relation(&sketch, Sketch3Relation::Coincident(free, circle));
        let Ok(solved) = sketch.solve() else {
            panic!("point-on-circle over a fixed radius is well-posed and must converge");
        };
        let (cx, cy, cz) = coords(&solved, center);
        let (fx, fy, fz) = coords(&solved, free);
        let dist = (fx - cx)
            .mul_add(fx - cx, (fy - cy).mul_add(fy - cy, (fz - cz) * (fz - cz)))
            .sqrt();
        assert!(
            (dist - 4.0).abs() < 1e-6,
            "free point lands on the radius-4 circle: {dist}"
        );
    }

    #[test]
    fn solve_with_drag_pins_holds_the_dragged_point_at_its_target() {
        let (sketch, a) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, b) = point(&sketch, 5.0, 0.0, 0.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(a));
        let target = Point3::from_mm(2.0, 3.0, -1.0);
        let Ok(dragged) = sketch.solve_with_drag_pins(&[(b, target)], BudgetCeiling::FRAME_16MS)
        else {
            panic!("a free point pins exactly onto its drag target");
        };
        let (bx, by, bz) = coords(&dragged, b);
        assert!(
            (bx - 2.0).abs() < 1e-9 && (by - 3.0).abs() < 1e-9 && (bz - -1.0).abs() < 1e-9,
            "dragged point lands on target: {bx} {by} {bz}"
        );
    }

    #[test]
    fn on_plane_z_zero_pulls_point_into_the_plane() {
        let (sketch, p) = point(&Sketch3::new(), 2.0, -3.0, 4.0);
        let sketch = relation(
            &sketch,
            Sketch3Relation::OnPlane {
                point: p,
                normal: UnitVec3::z_axis(),
                offset: Length::new::<millimeter>(0.0),
            },
        );
        let Ok(solved) = sketch.solve() else {
            panic!("on-plane drives z to the offset and must converge");
        };
        let (x, y, z) = coords(&solved, p);
        assert!(z.abs() < 1e-6, "point lands on z=0: {z}");
        assert!((x - 2.0).abs() < 1e-6, "free x stays put: {x}");
        assert!((y - -3.0).abs() < 1e-6, "free y stays put: {y}");
    }

    #[test]
    fn midpoint_pulls_point_to_the_line_center() {
        let (sketch, start) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, end) = point(&sketch, 6.0, 2.0, -4.0);
        let (sketch, segment) = line(&sketch, start, end);
        let (sketch, mid) = point(&sketch, 9.0, 9.0, 9.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(start));
        let sketch = relation(&sketch, Sketch3Relation::Fix(end));
        let sketch = relation(
            &sketch,
            Sketch3Relation::Midpoint {
                point: mid,
                line: segment,
            },
        );
        let Ok(solved) = sketch.solve() else {
            panic!("midpoint over a fixed line is linear and must converge");
        };
        let (x, y, z) = coords(&solved, mid);
        assert!((x - 3.0).abs() < 1e-6, "x {x}");
        assert!((y - 1.0).abs() < 1e-6, "y {y}");
        assert!((z - -2.0).abs() < 1e-6, "z {z}");
    }

    #[test]
    fn with_point_at_moves_only_the_named_point() {
        let (sketch, a) = point(&Sketch3::new(), 1.0, 2.0, 3.0);
        let (sketch, b) = point(&sketch, 4.0, 5.0, 6.0);
        let moved = sketch.with_point_at(a, Point3::from_mm(-7.0, -8.0, -9.0));
        assert_eq!(coords(&moved, a), (-7.0, -8.0, -9.0));
        assert_eq!(coords(&moved, b), (4.0, 5.0, 6.0));
        assert_ne!(moved.version(), sketch.version());
    }

    #[test]
    fn point_on_line_pulls_free_point_onto_the_segment() {
        let (sketch, start) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, end) = point(&sketch, 6.0, 0.0, 0.0);
        let (sketch, segment) = line(&sketch, start, end);
        let (sketch, free) = point(&sketch, 2.0, 3.0, -4.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(start));
        let sketch = relation(&sketch, Sketch3Relation::Fix(end));
        let sketch = relation(
            &sketch,
            Sketch3Relation::PointOnLine {
                point: free,
                line: segment,
            },
        );
        let Ok(solved) = sketch.solve() else {
            panic!("point-on-line over a fixed segment is linear and must converge");
        };
        let (x, y, z) = coords(&solved, free);
        assert!(y.abs() < 1e-6, "point pulled to the line in y: {y}");
        assert!(z.abs() < 1e-6, "point pulled to the line in z: {z}");
        assert!((x - 2.0).abs() < 1e-6, "free slide stays put: {x}");
    }

    #[test]
    fn angle_relation_opens_two_lines_to_the_target() {
        let (sketch, origin) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, end_a) = point(&sketch, 5.0, 0.0, 0.0);
        let (sketch, end_b) = point(&sketch, 5.0, 0.1, 0.0);
        let (sketch, line_a) = line(&sketch, origin, end_a);
        let (sketch, line_b) = line(&sketch, origin, end_b);
        let sketch = relation(&sketch, Sketch3Relation::Fix(origin));
        let sketch = relation(&sketch, Sketch3Relation::Fix(end_a));
        let sketch = relation(
            &sketch,
            Sketch3Relation::OnPlane {
                point: end_b,
                normal: UnitVec3::z_axis(),
                offset: Length::new::<millimeter>(0.0),
            },
        );
        let sketch = relation(
            &sketch,
            Sketch3Relation::Distance {
                a: origin,
                b: end_b,
                value: Length::new::<millimeter>(5.0),
                reference: false,
            },
        );
        let sketch = relation(
            &sketch,
            Sketch3Relation::Angle {
                a: line_a,
                b: line_b,
                value: Angle::new::<radian>(core::f64::consts::FRAC_PI_3),
                reference: false,
            },
        );
        let Ok(solved) = sketch.solve() else {
            panic!("a single angle over a free second line is well-posed");
        };
        let (ax, ay, az) = coords(&solved, end_a);
        let (bx, by, bz) = coords(&solved, end_b);
        let dot = ax * bx + ay * by + az * bz;
        let na = (ax * ax + ay * ay + az * az).sqrt();
        let nb = (bx * bx + by * by + bz * bz).sqrt();
        let theta = (dot / (na * nb)).clamp(-1.0, 1.0).acos();
        assert!(
            (theta - core::f64::consts::FRAC_PI_3).abs() < 1e-6,
            "lines open to 60 degrees: {theta}",
        );
    }

    #[test]
    fn arc_intrinsic_equalizes_its_two_spoke_radii() {
        let (sketch, center) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, start) = point(&sketch, 4.0, 0.0, 0.0);
        let (sketch, end) = point(&sketch, 0.0, 9.0, 0.0);
        let (sketch, _arc) = {
            let (next, outcome) = sketch.apply(Sketch3Edit::AddEntity(Sketch3Entity::arc(
                center,
                start,
                end,
                UnitVec3::z_axis(),
                false,
            )));
            let Edit3Outcome::Entity(id) = outcome else {
                panic!("expected an entity outcome");
            };
            (next, id)
        };
        let sketch = relation(&sketch, Sketch3Relation::Fix(center));
        let sketch = relation(&sketch, Sketch3Relation::Fix(start));
        let sketch = relation(
            &sketch,
            Sketch3Relation::OnPlane {
                point: end,
                normal: UnitVec3::x_axis(),
                offset: Length::new::<millimeter>(0.0),
            },
        );
        let Ok(solved) = sketch.solve() else {
            panic!("arc equal-radius over a fixed spoke is well-posed");
        };
        let (ex, ey, ez) = coords(&solved, end);
        let radius_end = (ex * ex + ey * ey + ez * ez).sqrt();
        assert!(
            (radius_end - 4.0).abs() < 1e-6,
            "the arc end is pulled to the start radius: {radius_end}",
        );
    }

    #[test]
    fn deleting_an_endpoint_cascades_to_its_line_and_relations() {
        let (sketch, start) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, end) = point(&sketch, 5.0, 0.0, 0.0);
        let (sketch, segment) = line(&sketch, start, end);
        let sketch = relation(&sketch, Sketch3Relation::Fix(start));
        let sketch = relation(
            &sketch,
            Sketch3Relation::AlongAxis {
                line: segment,
                axis: UnitVec3::x_axis(),
            },
        );
        let pruned = sketch.without_entity(start);
        assert!(pruned.entities().get(start).is_none(), "seed point is gone");
        assert!(
            pruned.entities().get(segment).is_none(),
            "the dependent line cascades away",
        );
        assert!(
            pruned.entities().get(end).is_some(),
            "the unrelated endpoint survives",
        );
        assert!(
            pruned.relation_order().is_empty(),
            "relations touching the closure are dropped",
        );
        assert!(pruned.validate().is_ok(), "no dangling references remain");
    }

    #[test]
    fn deleting_a_line_keeps_its_endpoints() {
        let (sketch, start) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, end) = point(&sketch, 5.0, 0.0, 0.0);
        let (sketch, segment) = line(&sketch, start, end);
        let pruned = sketch.without_entity(segment);
        assert!(pruned.entities().get(segment).is_none());
        assert!(pruned.entities().get(start).is_some());
        assert!(pruned.entities().get(end).is_some());
        assert!(pruned.validate().is_ok());
    }

    #[test]
    fn point_on_a_collapsed_line_solves_without_diverging() {
        let (sketch, start) = point(&Sketch3::new(), 2.0, 2.0, 0.0);
        let (sketch, end) = point(&sketch, 2.0, 2.0, 0.0);
        let (sketch, segment) = line(&sketch, start, end);
        let (sketch, free) = point(&sketch, 9.0, -3.0, 4.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(start));
        let sketch = relation(&sketch, Sketch3Relation::Fix(end));
        let sketch = relation(
            &sketch,
            Sketch3Relation::PointOnLine {
                point: free,
                line: segment,
            },
        );
        let Ok(solved) = sketch.solve() else {
            panic!("a point-on-line over a zero-length line still converges");
        };
        let (x, y, z) = coords(&solved, free);
        assert!(
            x.is_finite() && y.is_finite() && z.is_finite(),
            "the degenerate cross-product residual never yields NaN: ({x}, {y}, {z})",
        );
        assert_eq!(
            solved.status().status(),
            bone_types::SketchStatus::UnderDefined,
            "a collapsed line cannot constrain the point, so its dof survives",
        );
    }

    #[test]
    fn without_relation_drops_only_the_named_relation() {
        let (sketch, p) = point(&Sketch3::new(), 1.0, 2.0, 3.0);
        let (sketch, rid) = sketch.with_relation(Sketch3Relation::Fix(p));
        let pruned = sketch.without_relation(rid);
        assert!(pruned.relation_order().is_empty());
        assert!(
            pruned.entities().get(p).is_some(),
            "the entity is untouched"
        );
        assert!(pruned.validate().is_ok());
    }

    #[test]
    fn with_construction_flips_only_a_line() {
        let (sketch, start) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, end) = point(&sketch, 5.0, 0.0, 0.0);
        let (sketch, segment) = line(&sketch, start, end);
        let toggled = sketch.with_construction(segment, true);
        let Some(Sketch3Entity::Line(l)) = toggled.entities().get(segment) else {
            panic!("the segment stays a line");
        };
        assert!(l.for_construction(), "construction flag is set");
        let untouched = toggled.with_construction(start, true);
        assert!(
            matches!(
                untouched.entities().get(start),
                Some(Sketch3Entity::Point(_))
            ),
            "a point ignores the construction toggle",
        );
    }

    fn circle(
        sketch: &Sketch3,
        center: SketchEntityId,
        radius_mm: f64,
    ) -> (Sketch3, SketchEntityId) {
        let (next, outcome) = sketch.apply(Sketch3Edit::AddEntity(Sketch3Entity::circle(
            center,
            Length::new::<millimeter>(radius_mm),
            UnitVec3::z_axis(),
            false,
        )));
        let Edit3Outcome::Entity(id) = outcome else {
            panic!("expected entity outcome");
        };
        (next, id)
    }

    fn circle_radius(sketch: &Sketch3, id: SketchEntityId) -> f64 {
        let Some(Sketch3Entity::Circle(c)) = sketch.entities().get(id) else {
            panic!("expected a circle");
        };
        c.radius().get::<millimeter>()
    }

    fn arc(
        sketch: &Sketch3,
        center: SketchEntityId,
        start: SketchEntityId,
        end: SketchEntityId,
    ) -> (Sketch3, SketchEntityId) {
        let (next, outcome) = sketch.apply(Sketch3Edit::AddEntity(Sketch3Entity::arc(
            center,
            start,
            end,
            UnitVec3::z_axis(),
            false,
        )));
        let Edit3Outcome::Entity(id) = outcome else {
            panic!("expected entity outcome");
        };
        (next, id)
    }

    #[test]
    fn radius_dimension_resizes_a_circle() {
        let (sketch, center) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, circle) = circle(&sketch, center, 3.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(center));
        let sketch = relation(
            &sketch,
            Sketch3Relation::Radius {
                target: circle,
                value: Length::new::<millimeter>(11.0),
                reference: false,
            },
        );
        let Ok(solved) = sketch.solve() else {
            panic!("a radius over a pinned circle is well-posed");
        };
        assert!((circle_radius(&solved, circle) - 11.0).abs() < 1e-6);
    }

    #[test]
    fn diameter_dimension_halves_into_the_radius() {
        let (sketch, center) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, circle) = circle(&sketch, center, 3.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(center));
        let sketch = relation(
            &sketch,
            Sketch3Relation::Diameter {
                target: circle,
                value: Length::new::<millimeter>(10.0),
                reference: false,
            },
        );
        let Ok(solved) = sketch.solve() else {
            panic!("a diameter over a pinned circle is well-posed");
        };
        assert!((circle_radius(&solved, circle) - 5.0).abs() < 1e-6);
    }

    #[test]
    fn equal_radius_copies_a_dimensioned_circle() {
        let (sketch, ca) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, cb) = point(&sketch, 20.0, 0.0, 0.0);
        let (sketch, circle_a) = circle(&sketch, ca, 7.0);
        let (sketch, circle_b) = circle(&sketch, cb, 2.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(ca));
        let sketch = relation(&sketch, Sketch3Relation::Fix(cb));
        let sketch = relation(
            &sketch,
            Sketch3Relation::Radius {
                target: circle_a,
                value: Length::new::<millimeter>(6.0),
                reference: false,
            },
        );
        let sketch = relation(&sketch, Sketch3Relation::Equal(circle_a, circle_b));
        let Ok(solved) = sketch.solve() else {
            panic!("equal-radius over a dimensioned circle is well-posed");
        };
        assert!((circle_radius(&solved, circle_b) - 6.0).abs() < 1e-6);
    }

    #[test]
    fn concentric_pulls_a_circle_center_onto_another() {
        let (sketch, ca) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, cb) = point(&sketch, 9.0, -4.0, 2.0);
        let (sketch, circle_a) = circle(&sketch, ca, 3.0);
        let (sketch, circle_b) = circle(&sketch, cb, 5.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(ca));
        let sketch = relation(&sketch, Sketch3Relation::Concentric(circle_a, circle_b));
        let Ok(solved) = sketch.solve() else {
            panic!("concentric over a pinned center is linear and must converge");
        };
        let (x, y, z) = coords(&solved, cb);
        assert!(
            x.abs() < 1e-6 && y.abs() < 1e-6 && z.abs() < 1e-6,
            "({x}, {y}, {z})"
        );
    }

    #[test]
    fn parallel_lays_a_free_line_along_its_reference() {
        let (sketch, sa) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, ea) = point(&sketch, 5.0, 0.0, 0.0);
        let (sketch, reference) = line(&sketch, sa, ea);
        let (sketch, sb) = point(&sketch, 0.0, 3.0, 0.0);
        let (sketch, eb) = point(&sketch, 9.0, 7.0, 2.0);
        let (sketch, free) = line(&sketch, sb, eb);
        let sketch = relation(&sketch, Sketch3Relation::Fix(sa));
        let sketch = relation(&sketch, Sketch3Relation::Fix(ea));
        let sketch = relation(&sketch, Sketch3Relation::Fix(sb));
        let sketch = relation(&sketch, Sketch3Relation::Parallel(reference, free));
        let Ok(solved) = sketch.solve() else {
            panic!("parallel over a fixed reference is well-posed");
        };
        let Some(axis) = solved.line_axis(free) else {
            panic!("the solved line yields an axis");
        };
        let (dx, dy, dz) = axis.direction().components();
        assert!(dx.abs() > 1.0 - 1e-6, "free line points along x: {dx}");
        assert!(dy.abs() < 1e-6 && dz.abs() < 1e-6, "({dy}, {dz})");
    }

    #[test]
    fn parallel_rotates_a_free_line_onto_a_fixed_one() {
        let (sketch, sa) = point(&Sketch3::new(), 0.0, 2.0, 0.0);
        let (sketch, ea) = point(&sketch, 3.0, 5.0, 1.0);
        let (sketch, free) = line(&sketch, sa, ea);
        let (sketch, sb) = point(&sketch, 0.0, 0.0, 0.0);
        let (sketch, eb) = point(&sketch, 5.0, 0.0, 0.0);
        let (sketch, fixed) = line(&sketch, sb, eb);
        let sketch = relation(&sketch, Sketch3Relation::Fix(sb));
        let sketch = relation(&sketch, Sketch3Relation::Fix(eb));
        let sketch = relation(&sketch, Sketch3Relation::Fix(sa));
        let sketch = relation(&sketch, Sketch3Relation::Parallel(free, fixed));
        let Ok(solved) = sketch.solve() else {
            panic!("parallel against a fixed line is well-posed");
        };
        let Some(axis) = solved.line_axis(free) else {
            panic!("the solved line yields an axis");
        };
        let (dx, dy, dz) = axis.direction().components();
        assert!(dx.abs() > 1.0 - 1e-6, "the free line rotates onto x: {dx}");
        assert!(dy.abs() < 1e-6 && dz.abs() < 1e-6, "({dy}, {dz})");
    }

    #[test]
    fn validate_relation_rejects_unlowerable_operand_pairs() {
        let (sketch, a) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, b) = point(&sketch, 1.0, 0.0, 0.0);
        let (sketch, line_a) = line(&sketch, a, b);
        let (sketch, c) = point(&sketch, 2.0, 0.0, 0.0);
        let (sketch, d) = point(&sketch, 3.0, 0.0, 0.0);
        let (sketch, line_b) = line(&sketch, c, d);
        let (sketch, center) = point(&sketch, 0.0, 5.0, 0.0);
        let (sketch, circle_a) = circle(&sketch, center, 2.0);
        let (sketch, e_major) = point(&sketch, 4.0, 5.0, 0.0);
        let (sketch, e_minor) = point(&sketch, 0.0, 7.0, 0.0);
        let (sketch, outcome) = sketch.apply(Sketch3Edit::AddEntity(Sketch3Entity::ellipse(
            center,
            e_major,
            e_minor,
            UnitVec3::z_axis(),
            false,
        )));
        let Edit3Outcome::Entity(ellipse) = outcome else {
            panic!("expected an ellipse entity");
        };

        assert!(
            sketch
                .validate_relation(Sketch3Relation::Equal(line_a, line_b))
                .is_ok()
        );
        assert!(
            sketch
                .validate_relation(Sketch3Relation::Tangent(line_a, circle_a))
                .is_ok()
        );
        assert_eq!(
            sketch.validate_relation(Sketch3Relation::Equal(line_a, circle_a)),
            Err(Sketch3IntegrityError::IncompatibleOperands(
                line_a, circle_a
            )),
        );
        assert_eq!(
            sketch.validate_relation(Sketch3Relation::Tangent(line_a, line_b)),
            Err(Sketch3IntegrityError::IncompatibleOperands(line_a, line_b)),
        );
        assert_eq!(
            sketch.validate_relation(Sketch3Relation::Equal(ellipse, circle_a)),
            Err(Sketch3IntegrityError::NotMeasurable(ellipse)),
        );
        assert_eq!(
            sketch.validate_relation(Sketch3Relation::Tangent(ellipse, circle_a)),
            Err(Sketch3IntegrityError::NotMeasurable(ellipse)),
        );
    }

    #[test]
    fn tangent_separates_two_circles_by_their_radius_sum() {
        let (sketch, ca) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, cb) = point(&sketch, 10.0, 0.0, 0.0);
        let (sketch, circle_a) = circle(&sketch, ca, 3.0);
        let (sketch, circle_b) = circle(&sketch, cb, 2.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(ca));
        let sketch = relation(&sketch, Sketch3Relation::Fix(cb));
        let sketch = relation(
            &sketch,
            Sketch3Relation::Radius {
                target: circle_a,
                value: Length::new::<millimeter>(3.0),
                reference: false,
            },
        );
        let sketch = relation(&sketch, Sketch3Relation::Tangent(circle_a, circle_b));
        let Ok(solved) = sketch.solve() else {
            panic!("external tangency over pinned centers is well-posed");
        };
        assert!(
            (circle_radius(&solved, circle_b) - 7.0).abs() < 1e-6,
            "the free circle grows to close the gap: {}",
            circle_radius(&solved, circle_b),
        );
    }

    #[test]
    fn tangent_nests_one_circle_inside_another_when_internal() {
        let (sketch, ca) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, cb) = point(&sketch, 3.0, 0.0, 0.0);
        let (sketch, circle_a) = circle(&sketch, ca, 5.0);
        let (sketch, circle_b) = circle(&sketch, cb, 1.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(ca));
        let sketch = relation(&sketch, Sketch3Relation::Fix(cb));
        let sketch = relation(
            &sketch,
            Sketch3Relation::Radius {
                target: circle_a,
                value: Length::new::<millimeter>(5.0),
                reference: false,
            },
        );
        let sketch = relation(&sketch, Sketch3Relation::Tangent(circle_a, circle_b));
        let Ok(solved) = sketch.solve() else {
            panic!("internal tangency over pinned centers is well-posed");
        };
        assert!(
            (circle_radius(&solved, circle_b) - 2.0).abs() < 1e-6,
            "the inner circle resizes to the radius difference: {}",
            circle_radius(&solved, circle_b),
        );
    }

    #[test]
    fn arc_intrinsic_holds_its_endpoints_in_the_frozen_plane() {
        let (sketch, center) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, start) = point(&sketch, 3.0, 0.0, 2.0);
        let (sketch, end) = point(&sketch, 0.0, 3.0, 0.0);
        let (sketch, _arc) = arc(&sketch, center, start, end);
        let sketch = relation(&sketch, Sketch3Relation::Fix(center));
        let sketch = relation(&sketch, Sketch3Relation::Fix(end));
        let Ok(solved) = sketch.solve() else {
            panic!("the coplanarity intrinsic over a fixed center is well-posed");
        };
        let (_, _, z) = coords(&solved, start);
        assert!(
            z.abs() < 1e-6,
            "the off-plane start returns to the frozen z=0 plane: {z}",
        );
    }

    #[test]
    fn a_consistent_duplicate_dimension_is_reported_redundant() {
        let distance = |a, b| Sketch3Relation::Distance {
            a,
            b,
            value: Length::new::<millimeter>(4.0),
            reference: false,
        };
        let (sketch, a) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, b) = point(&sketch, 4.0, 0.0, 0.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(a));
        let sketch = relation(&sketch, distance(a, b));
        let sketch = relation(&sketch, distance(a, b));
        let report = sketch.status();
        assert_eq!(
            report.redundant().len(),
            1,
            "one of the two identical distance dimensions is redundant: {:?}",
            report.redundant(),
        );
    }

    #[test]
    fn point_on_circle_coincidence_is_not_flagged_redundant() {
        let (sketch, center) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, circle) = circle(&sketch, center, 3.0);
        let (sketch, rim) = point(&sketch, 3.0, 0.0, 0.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(center));
        let sketch = relation(
            &sketch,
            Sketch3Relation::Radius {
                target: circle,
                value: Length::new::<millimeter>(3.0),
                reference: false,
            },
        );
        let sketch = relation(&sketch, Sketch3Relation::Coincident(rim, circle));
        let report = sketch.status();
        assert!(
            report.redundant().is_empty(),
            "the radial row of a live point-on-circle coincidence carries constraint, \
             so its auxiliary plane row alone must not flag the relation redundant: {:?}",
            report.redundant(),
        );
    }

    #[test]
    fn symmetric_reflects_a_free_point_across_the_axis() {
        let (sketch, axis_a) = point(&Sketch3::new(), 0.0, 0.0, 0.0);
        let (sketch, axis_b) = point(&sketch, 10.0, 0.0, 0.0);
        let (sketch, axis) = line(&sketch, axis_a, axis_b);
        let (sketch, a) = point(&sketch, 2.0, 5.0, 0.0);
        let (sketch, b) = point(&sketch, 9.0, -9.0, 9.0);
        let sketch = relation(&sketch, Sketch3Relation::Fix(axis_a));
        let sketch = relation(&sketch, Sketch3Relation::Fix(axis_b));
        let sketch = relation(&sketch, Sketch3Relation::Fix(a));
        let sketch = relation(&sketch, Sketch3Relation::Symmetric { a, b, axis });
        let Ok(solved) = sketch.solve() else {
            panic!("symmetric over a fixed axis and source is well-posed");
        };
        let (bx, by, bz) = coords(&solved, b);
        assert!((bx - 2.0).abs() < 1e-6, "x mirrors to the source: {bx}");
        assert!((by + 5.0).abs() < 1e-6, "y flips across the axis: {by}");
        assert!(bz.abs() < 1e-6, "z collapses onto the axis plane: {bz}");
    }

    #[test]
    fn free_point_has_three_dof_and_fixing_removes_them() {
        let (sketch, p) = point(&Sketch3::new(), 0.5, -1.0, 2.0);
        assert_eq!(sketch.analyze_dof().dof().value(), 3);
        let fixed = relation(&sketch, Sketch3Relation::Fix(p));
        let report = fixed.analyze_dof();
        assert_eq!(report.dof().value(), 0);
        assert!(report.under_constrained().is_empty());
        assert!(report.over_constrained().is_empty());
    }
}
