//! Builds and solves two-dimensional parametric constraints through the geometry kernel. //! Unsupported entity shapes, invalid target values, and incompatible planes are skipped. use std::collections::{HashMap, HashSet}; use std::rc::Rc; use acadrust::entities::EntityType; use acadrust::types::{Handle, Vector3}; use cadkernel_constraints::constraints::angle_distance::L2LAngle; use cadkernel_constraints::constraints::bspline::{Coord as SplineCoord, PointOnBSpline}; use cadkernel_constraints::constraints::circle_arc::{C2LDistance, P2CDistance, TangentCircumf}; use cadkernel_constraints::constraints::conic::{ EqualMajorAxesConic, PointOnEllipse, TangentEllipseLine, }; use cadkernel_constraints::constraints::curve_generic::CurveValue; use cadkernel_constraints::constraints::point_line::{ CenterOfGravity, Difference, Equal, EqualLineLength, MidpointOnLine, P2PDistance, Parallel as ParallelConstraint, Perpendicular as PerpendicularConstraint, PointOnLine, ProjectedDistance, ProjectedDistanceAlongLine, }; use cadkernel_constraints::constraints::Constraint; use cadkernel_constraints::geo::{ Arc as GArc, BSpline as GBSpline, Circle as GCircle, Conic as GConic, Ellipse as GEllipse, Line as GLine, Point as GPoint, }; use cadkernel_constraints::solvers::dogleg::solve_dl; use cadkernel_constraints::system::System; use super::named_parameters::ParameterTable; use super::parametric_constraints::{ angle_sector, distance_direction_type, ConstraintId, ConstraintKind, ParametricConstraint, ParametricConstraintSet, ParametricRef, }; use super::{ChangeKind, Scene}; /// One referenced entity's geometry, registered into an `cadkernel_constraints::System`'s /// parameter store. #[derive(Clone)] enum EntityGeom { Point(GPoint), Line(GLine), Ray(GLine), XLine(GLine), Polyline { points: Rc>, straight: Rc>, closed: bool, }, Circle(GCircle), /// Full `geo::Arc` — center/radius (via `.circle`), `start_angle`/ /// `end_angle`, and real `start`/`end` points kept consistent with /// those by the arc-rules `CurveValue` constraints `solve_scope` adds /// for every registered arc. See this module's doc comment. Arc(GArc), /// See `register_entity` for the entity-to-kernel parametrization. Ellipse(GEllipse), Spline { curve: GBSpline, control_z: Rc>, }, } impl EntityGeom { /// The `cadkernel_constraints::geo::Point` for a marker on this entity — `None` for /// a marker this entity type/value doesn't support (see /// `parametric_constraints::resolve_point` for the same convention on the /// read side). fn point_for_marker(&self, marker: i32) -> Option { match (self, marker) { (EntityGeom::Point(point), 0) => Some(*point), (EntityGeom::Line(l), 0) => Some(l.p1), (EntityGeom::Line(l), 1) => Some(l.p2), (EntityGeom::Ray(l), 0) => Some(l.p1), (EntityGeom::Polyline { points, .. }, marker) if marker >= 0 => { points.get(marker as usize).copied() } (EntityGeom::Circle(c), -3) => Some(c.center), (EntityGeom::Arc(a), 0) => Some(a.start), (EntityGeom::Arc(a), 1) => Some(a.end), (EntityGeom::Arc(a), -3) => Some(a.circle.center), (EntityGeom::Ellipse(e), -3) => Some(e.center), (EntityGeom::Spline { curve, .. }, 0) if !curve.periodic => Some(curve.start), (EntityGeom::Spline { curve, .. }, 1) if !curve.periodic => Some(curve.end), _ => None, } } fn line_segment(&self, index: usize) -> Option { let EntityGeom::Polyline { points, straight, closed, } = self else { return None; }; if !straight.get(index).copied().unwrap_or(false) { return None; } let p1 = *points.get(index)?; let p2 = if index + 1 < points.len() { points[index + 1] } else if *closed { *points.first()? } else { return None; }; Some(GLine { p1, p2 }) } } /// `Tangent`/`Normal` only ever care about "a whole circle-shaped curve" or /// "a whole line" — an `Arc` collapses to its `.circle` here exactly like /// `whole_circle` does, so those two constraints' own inline matches don't /// need every Circle/Arc combination spelled out separately. enum CircleOrLine { Circle(GCircle), Line(GLine), Ellipse(GEllipse), Other, } fn as_circle_or_line(g: EntityGeom, reference: ParametricRef) -> CircleOrLine { match g { EntityGeom::Circle(c) => CircleOrLine::Circle(c), EntityGeom::Arc(a) => CircleOrLine::Circle(a.circle), EntityGeom::Line(l) | EntityGeom::Ray(l) | EntityGeom::XLine(l) => CircleOrLine::Line(l), EntityGeom::Ellipse(ellipse) => CircleOrLine::Ellipse(ellipse), EntityGeom::Point(_) => CircleOrLine::Other, EntityGeom::Spline { .. } => CircleOrLine::Other, EntityGeom::Polyline { .. } => reference .segment_index() .and_then(|index| g.line_segment(index)) .map(CircleOrLine::Line) .unwrap_or(CircleOrLine::Other), } } /// Reads `handle`'s live geometry and registers it as fresh, free (never /// `driven`) parameters in `sys` — every referenced entity is solved for, /// nothing is a priori fixed; per the 2D kernel, an under- /// constrained scope simply converges to the nearest configuration to its /// current one, which is the correct behavior for a persistent system where /// "what stays put" is a property of how many constraints exist, not a /// convention about which entity was picked first (contrast the one-shot /// `constrain::apply_*` commands, which do fix the first-picked entity — /// they solve exactly one constraint in isolation, so they need that /// convention; a whole scope's constraint graph does not). fn register_entity( document: &acadrust::CadDocument, sys: &mut System, handle: Handle, ) -> Option { let entity = document.get_entity(handle)?; match entity { EntityType::Point(point) => Some(EntityGeom::Point(GPoint::new( sys.add_param(point.location.x, false), sys.add_param(point.location.y, false), ))), EntityType::Insert(insert) => Some(EntityGeom::Point(GPoint::new( sys.add_param(insert.insert_point.x, false), sys.add_param(insert.insert_point.y, false), ))), EntityType::Text(text) => Some(EntityGeom::Point(GPoint::new( sys.add_param(text.insertion_point.x, false), sys.add_param(text.insertion_point.y, false), ))), EntityType::MText(text) => Some(EntityGeom::Point(GPoint::new( sys.add_param(text.insertion_point.x, false), sys.add_param(text.insertion_point.y, false), ))), EntityType::AttributeDefinition(attribute) => Some(EntityGeom::Point(GPoint::new( sys.add_param(attribute.insertion_point.x, false), sys.add_param(attribute.insertion_point.y, false), ))), EntityType::AttributeEntity(attribute) => Some(EntityGeom::Point(GPoint::new( sys.add_param(attribute.insertion_point.x, false), sys.add_param(attribute.insertion_point.y, false), ))), EntityType::Table(table) => Some(EntityGeom::Point(GPoint::new( sys.add_param(table.insertion_point.x, false), sys.add_param(table.insertion_point.y, false), ))), EntityType::LwPolyline(polyline) => Some(EntityGeom::Polyline { points: Rc::new( super::dimension_assoc::source_points(entity) .into_iter() .map(|point| { GPoint::new(sys.add_param(point.x, false), sys.add_param(point.y, false)) }) .collect(), ), straight: Rc::new( polyline .vertices .iter() .map(|vertex| vertex.bulge.abs() <= 1e-9) .collect(), ), closed: polyline.is_closed, }), EntityType::Polyline2D(polyline) => Some(EntityGeom::Polyline { points: Rc::new( super::dimension_assoc::source_points(entity) .into_iter() .map(|point| { GPoint::new(sys.add_param(point.x, false), sys.add_param(point.y, false)) }) .collect(), ), straight: Rc::new( polyline .vertices .iter() .map(|vertex| vertex.bulge.abs() <= 1e-9) .collect(), ), closed: polyline.is_closed(), }), EntityType::Line(l) => { let p1 = GPoint::new( sys.add_param(l.start.x, false), sys.add_param(l.start.y, false), ); let p2 = GPoint::new(sys.add_param(l.end.x, false), sys.add_param(l.end.y, false)); Some(EntityGeom::Line(GLine { p1, p2 })) } EntityType::Ray(ray) => { let p1 = GPoint::new( sys.add_param(ray.base_point.x, false), sys.add_param(ray.base_point.y, false), ); let through = ray.base_point + ray.direction; let p2 = GPoint::new( sys.add_param(through.x, false), sys.add_param(through.y, false), ); Some(EntityGeom::Ray(GLine { p1, p2 })) } EntityType::XLine(line) => { let p1 = GPoint::new( sys.add_param(line.base_point.x, false), sys.add_param(line.base_point.y, false), ); let through = line.base_point + line.direction; let p2 = GPoint::new( sys.add_param(through.x, false), sys.add_param(through.y, false), ); Some(EntityGeom::XLine(GLine { p1, p2 })) } EntityType::Circle(c) => { let center = GPoint::new( sys.add_param(c.center.x, false), sys.add_param(c.center.y, false), ); let rad = sys.add_param(c.radius, false); Some(EntityGeom::Circle(GCircle { center, rad })) } EntityType::Arc(a) => { // Full registration: center/radius, both angles, and real // start/end points seeded from acadrust's own `start_point`/ // `end_point` (already consistent with center/radius/angle at // registration time) — `solve_scope` adds the arc-rules // constraints that keep them that way under solving. See this // module's doc comment. let center = GPoint::new( sys.add_param(a.center.x, false), sys.add_param(a.center.y, false), ); let rad = sys.add_param(a.radius, false); let start_angle = sys.add_param(a.start_angle, false); let end_angle = sys.add_param(a.end_angle, false); let start_seed = a.start_point(); let end_seed = a.end_point(); let start = GPoint::new( sys.add_param(start_seed.x, false), sys.add_param(start_seed.y, false), ); let end = GPoint::new( sys.add_param(end_seed.x, false), sys.add_param(end_seed.y, false), ); Some(EntityGeom::Arc(GArc { circle: GCircle { center, rad }, start, end, start_angle, end_angle, })) } EntityType::Ellipse(el) => { // acadrust's `Ellipse` is center + major-axis vector (its length // is the major radius) + minor/major ratio; `cadkernel_constraints::geo:: // Ellipse` is center + one focus + minor radius. Converting: // minor_radius = major_radius * ratio, then the focus distance // c follows from a² = b² + c² (standard ellipse identity), and // focus1 sits `c` along the major-axis direction from center. let major_radius = el.major_axis.length(); let minor_radius = major_radius * el.minor_axis_ratio; let focus_dist = (major_radius * major_radius - minor_radius * minor_radius) .max(0.0) .sqrt(); // Direction is meaningless once major_radius is ~0 (a // degenerate point-ellipse) — arbitrarily fall back to +X // rather than dividing by ~0 in `normalize`. let unit_major = if major_radius > 1e-9 { el.major_axis.normalize() } else { acadrust::types::Vector3::UNIT_X }; let focus1_point = el.center + unit_major * focus_dist; let center = GPoint::new( sys.add_param(el.center.x, false), sys.add_param(el.center.y, false), ); let focus1 = GPoint::new( sys.add_param(focus1_point.x, false), sys.add_param(focus1_point.y, false), ); let radmin = sys.add_param(minor_radius, false); Some(EntityGeom::Ellipse(GEllipse { center, focus1, radmin, })) } EntityType::Spline(spline) => { let source = crate::entities::spline::nurbs3(spline)?; let poles: Vec<_> = source .control_points() .iter() .map(|point| { GPoint::new( sys.add_param(point[0], false), sys.add_param(point[1], false), ) }) .collect(); let start = *poles.first()?; let end = *poles.last()?; let weights = source .weights() .iter() .map(|weight| sys.add_param(*weight, true)) .collect(); Some(EntityGeom::Spline { curve: GBSpline { poles, weights, knots: source.knots().to_vec(), start, end, degree: source.degree(), periodic: source.periodicity(), }, control_z: Rc::new( source .control_points() .iter() .map(|point| point[2]) .collect(), ), }) } _ => None, } } /// Resolves one [`ParametricRef`] against the entity-geometry cache, registering /// the entity on first use. `None` if the handle is dangling, isn't a /// supported entity type, or (for a point ref) uses a marker that entity /// type doesn't support. fn resolve_ref( document: &acadrust::CadDocument, sys: &mut System, cache: &mut HashMap, r: ParametricRef, ) -> Option { if let Some(geometry) = cache.get(&r.entity) { return Some(geometry.clone()); } let geometry = register_entity(document, sys, r.entity)?; cache.insert(r.entity, geometry.clone()); Some(geometry) } /// Resolves a point reference and installs the kernel equations for standard /// implicit midpoints. Endpoint and center markers reuse entity parameters; /// midpoint markers are derived parameters so moving either parent geometry /// or another constrained object recomputes them in the same solve. fn resolve_constraint_point( document: &acadrust::CadDocument, sys: &mut System, cache: &mut HashMap, reference: ParametricRef, ) -> Option { let marker = reference.marker?; let geometry = resolve_ref(document, sys, cache, reference)?; if let Some(point) = geometry.point_for_marker(marker) { return Some(point); } let midpoint_line = reference .segment_midpoint_index() .and_then(|index| geometry.line_segment(index)) .or_else(|| { (marker == -2) .then_some(&geometry) .and_then(|geometry| match geometry { EntityGeom::Line(line) => Some(*line), _ => None, }) }); if let Some(line) = midpoint_line { let (x1, y1, x2, y2) = { let store = sys.store(); ( store.get(line.p1.x), store.get(line.p1.y), store.get(line.p2.x), store.get(line.p2.y), ) }; let point = GPoint::new( sys.add_param((x1 + x2) * 0.5, false), sys.add_param((y1 + y2) * 0.5, false), ); sys.add_constraint(Rc::new(CenterOfGravity::new( point.x, vec![line.p1.x, line.p2.x], vec![0.5, 0.5], ))); sys.add_constraint(Rc::new(CenterOfGravity::new( point.y, vec![line.p1.y, line.p2.y], vec![0.5, 0.5], ))); return Some(point); } if marker != -2 { return None; } match geometry { EntityGeom::Arc(arc) => { let (start, end) = { let store = sys.store(); (store.get(arc.start_angle), store.get(arc.end_angle)) }; let parameter_value = if end < start { (start + end + std::f64::consts::TAU) * 0.5 } else { (start + end) * 0.5 }; let end_parameter = if end < start { let adjusted = sys.add_param(end + std::f64::consts::TAU, false); let turn = sys.add_param(std::f64::consts::TAU, true); sys.add_constraint(Rc::new(Difference::new(arc.end_angle, adjusted, turn))); adjusted } else { arc.end_angle }; let parameter = sys.add_param(parameter_value, false); sys.add_constraint(Rc::new(CenterOfGravity::new( parameter, vec![arc.start_angle, end_parameter], vec![0.5, 0.5], ))); let point = { let value = cadkernel_constraints::geo::Curve::value( &arc, sys.store(), parameter_value, 0.0, None, ); GPoint::new(sys.add_param(value.x, false), sys.add_param(value.y, false)) }; let curve = Rc::new(arc); sys.add_constraint(Rc::new(CurveValue::new( point, point.x, curve.clone(), parameter, ))); sys.add_constraint(Rc::new(CurveValue::new(point, point.y, curve, parameter))); Some(point) } EntityGeom::Ellipse(ellipse) => { let EntityType::Ellipse(source) = document.get_entity(reference.entity)? else { return None; }; if source.is_full() { return None; } let parameter_value = (source.start_parameter + source.end_parameter) * 0.5; let value = cadkernel_constraints::geo::Curve::value( &ellipse, sys.store(), parameter_value, 0.0, None, ); let point = GPoint::new(sys.add_param(value.x, false), sys.add_param(value.y, false)); let parameter = sys.add_param(parameter_value, true); let curve = Rc::new(ellipse); sys.add_constraint(Rc::new(CurveValue::new( point, point.x, curve.clone(), parameter, ))); sys.add_constraint(Rc::new(CurveValue::new(point, point.y, curve, parameter))); Some(point) } EntityGeom::Spline { curve, .. } if !curve.periodic => { let from = curve.knots[curve.degree]; let to = curve.knots[curve.poles.len()]; let parameter_value = (from + to) * 0.5; let value = cadkernel_constraints::geo::Curve::value( &curve, sys.store(), parameter_value, 0.0, None, ); let point = GPoint::new(sys.add_param(value.x, false), sys.add_param(value.y, false)); let parameter = sys.add_param(parameter_value, true); sys.add_constraint(Rc::new(PointOnBSpline::new( point.x, parameter, SplineCoord::X, curve.clone(), ))); sys.add_constraint(Rc::new(PointOnBSpline::new( point.y, parameter, SplineCoord::Y, curve, ))); Some(point) } _ => None, } } const PLANE_EPS: f64 = 1e-9; fn is_world_z(normal: acadrust::types::Vector3) -> bool { normal.x.abs() <= PLANE_EPS && normal.y.abs() <= PLANE_EPS && (normal.z - 1.0).abs() <= PLANE_EPS } /// Returns the world-XY plane elevation supported by the current 2D bridge. fn entity_plane_z(entity: &EntityType) -> Option { let z = match entity { EntityType::Point(point) if is_world_z(point.normal) => point.location.z, EntityType::Insert(insert) if is_world_z(insert.normal) => insert.insert_point.z, EntityType::Text(text) if is_world_z(text.normal) => text.insertion_point.z, EntityType::MText(text) if is_world_z(text.normal) => text.insertion_point.z, EntityType::AttributeDefinition(attribute) if is_world_z(attribute.normal) => { attribute.insertion_point.z } EntityType::AttributeEntity(attribute) if is_world_z(attribute.normal) => { attribute.insertion_point.z } EntityType::Table(table) if is_world_z(table.normal) => table.insertion_point.z, EntityType::Line(line) if is_world_z(line.normal) && (line.start.z - line.end.z).abs() <= PLANE_EPS => { line.start.z } EntityType::Ray(ray) if ray.direction.z.abs() <= PLANE_EPS && ray.base_point.z.is_finite() => { ray.base_point.z } EntityType::XLine(line) if line.direction.z.abs() <= PLANE_EPS && line.base_point.z.is_finite() => { line.base_point.z } EntityType::Circle(circle) if is_world_z(circle.normal) => circle.center.z, EntityType::Arc(arc) if is_world_z(arc.normal) => arc.center.z, EntityType::Ellipse(ellipse) if is_world_z(ellipse.normal) && ellipse.major_axis.z.abs() <= PLANE_EPS => { ellipse.center.z } EntityType::Spline(spline) => { let curve = crate::entities::spline::nurbs3(spline)?; let first = *curve.control_points().first()?; if !curve .control_points() .iter() .all(|point| (point[2] - first[2]).abs() <= PLANE_EPS) { return None; } first[2] } EntityType::LwPolyline(polyline) if is_world_z(polyline.normal) => { let points = super::dimension_assoc::source_points(entity); let first = *points.first()?; if !points .iter() .all(|point| (point.z - first.z).abs() <= PLANE_EPS) { return None; } first.z } EntityType::Polyline2D(polyline) if is_world_z(polyline.normal) => { let points = super::dimension_assoc::source_points(entity); let first = *points.first()?; if !points .iter() .all(|point| (point.z - first.z).abs() <= PLANE_EPS) { return None; } first.z } _ => return None, }; z.is_finite().then_some(z) } fn refs_share_supported_plane(document: &acadrust::CadDocument, refs: &[ParametricRef]) -> bool { let mut plane_z = None; for reference in refs { let Some(entity) = document.get_entity(reference.entity) else { return false; }; let Some(z) = entity_plane_z(entity) else { return false; }; if plane_z.is_some_and(|expected: f64| (expected - z).abs() > PLANE_EPS) { return false; } plane_z = Some(z); } plane_z.is_some() } fn resolved_target(params: &ParameterTable, constraint: &ParametricConstraint) -> Option { let value = constraint.driving_param.as_ref()?.resolve(params).ok()?; if !value.is_finite() { return None; } let must_be_positive = matches!( constraint.kind, ConstraintKind::Distance | ConstraintKind::Radius | ConstraintKind::Diameter ); (!must_be_positive || value > 0.0).then_some(value) } fn oriented_distance(target: f64, current_projection: f64) -> f64 { if target >= 0.0 && current_projection < 0.0 { -target } else { target } } /// One [`ParametricConstraint`]'s `cadkernel_constraints` construction: every system-level /// constraint it contributes (most kinds contribute exactly one; a few — /// `Coincident`/`Concentric`/`CenterPoint`'s X+Y halves, `Colinear`'s two /// endpoints, `Midpoint`'s X+Y, `Symmetric`'s midpoint+perpendicular pair, /// `Fixed`'s per-coordinate pins — contribute more). Empty means it can't be /// built (an unsupported/not-yet-mapped kind, a dangling ref, a ref shape /// the kind doesn't expect — e.g. `Parallel` needs two whole-line refs — or, /// for a dimensional kind, a `driving_param` that fails to resolve). A constraint /// that can't be built is simply skipped for this solve, not an error: /// geometry it would have constrained is left alone, matching how a /// dangling associative-dimension reference degrades today rather than /// aborting the whole recompute. (A named-parameter *cycle* should be /// unreachable here — `ParameterTable::set` already refuses to create one — /// so it gets the same treatment as any other resolve failure rather than a /// special case.) fn build_constraint( document: &acadrust::CadDocument, sys: &mut System, cache: &mut HashMap, params: &ParameterTable, c: &ParametricConstraint, ) -> Vec> { if !c.enabled || !refs_share_supported_plane(document, &c.refs) { return Vec::new(); } let whole_line = |sys: &mut System, cache: &mut HashMap<_, _>, r: ParametricRef| { let geometry = resolve_ref(document, sys, cache, r)?; match geometry { EntityGeom::Line(line) | EntityGeom::Ray(line) | EntityGeom::XLine(line) => Some(line), EntityGeom::Polyline { .. } => geometry.line_segment(r.segment_index()?), _ => None, } }; let bounded_line = |sys: &mut System, cache: &mut HashMap<_, _>, r: ParametricRef| { let geometry = resolve_ref(document, sys, cache, r)?; match geometry { EntityGeom::Line(line) => Some(line), EntityGeom::Polyline { .. } => geometry.line_segment(r.segment_index()?), _ => None, } }; let whole_circle = |sys: &mut System, cache: &mut HashMap<_, _>, r: ParametricRef| match resolve_ref( document, sys, cache, r, )? { EntityGeom::Circle(circ) => Some(circ), EntityGeom::Arc(a) => Some(a.circle), EntityGeom::Point(_) | EntityGeom::Line(_) | EntityGeom::Ray(_) | EntityGeom::XLine(_) | EntityGeom::Polyline { .. } | EntityGeom::Ellipse(_) | EntityGeom::Spline { .. } => None, }; let point_ref = |sys: &mut System, cache: &mut HashMap<_, _>, r: ParametricRef| { resolve_constraint_point(document, sys, cache, r) }; // `Coincident`/`Concentric`/`CenterPoint` all solve identically — two // points (an endpoint, a circle's center via the existing `-3` marker, // or a plain point) held equal on both axes. Only the DWG-native class // name and the UI entry point that produces their `refs` differ. let point_pair_equal = |sys: &mut System, cache: &mut HashMap<_, _>, refs: &[ParametricRef]| -> Vec> { let [a, b] = refs else { return Vec::new() }; let (Some(pa), Some(pb)) = (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) else { return Vec::new(); }; vec![ Rc::new(Equal::new(pa.x, pb.x, 1.0)), Rc::new(Equal::new(pa.y, pb.y, 1.0)), ] }; match c.kind { ConstraintKind::Coincident | ConstraintKind::Concentric | ConstraintKind::CenterPoint => { point_pair_equal(sys, cache, &c.refs) } ConstraintKind::Horizontal => match c.refs.as_slice() { [r] => whole_line(sys, cache, *r) .map(|line| { vec![Rc::new(Equal::new(line.p1.y, line.p2.y, 1.0)) as Rc] }) .unwrap_or_default(), [a, b] => match (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) { (Some(a), Some(b)) => vec![Rc::new(Equal::new(a.y, b.y, 1.0))], _ => Vec::new(), }, _ => Vec::new(), }, ConstraintKind::Vertical => match c.refs.as_slice() { [r] => whole_line(sys, cache, *r) .map(|line| { vec![Rc::new(Equal::new(line.p1.x, line.p2.x, 1.0)) as Rc] }) .unwrap_or_default(), [a, b] => match (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) { (Some(a), Some(b)) => vec![Rc::new(Equal::new(a.x, b.x, 1.0))], _ => Vec::new(), }, _ => Vec::new(), }, ConstraintKind::Parallel => { let [a, b] = c.refs.as_slice() else { return Vec::new(); }; let (Some(fixed), Some(moving)) = (whole_line(sys, cache, *a), whole_line(sys, cache, *b)) else { return Vec::new(); }; vec![Rc::new(ParallelConstraint::new(sys.store(), moving, fixed))] } ConstraintKind::Perpendicular => { let [a, b] = c.refs.as_slice() else { return Vec::new(); }; let (Some(fixed), Some(moving)) = (whole_line(sys, cache, *a), whole_line(sys, cache, *b)) else { return Vec::new(); }; vec![Rc::new(PerpendicularConstraint::new( sys.store(), moving, fixed, ))] } ConstraintKind::Equal => { let [a, b] = c.refs.as_slice() else { return Vec::new(); }; if let (Some(la), Some(lb)) = (bounded_line(sys, cache, *a), bounded_line(sys, cache, *b)) { return vec![Rc::new(EqualLineLength::new(lb, la))]; } if let (Some(EntityGeom::Ellipse(a)), Some(EntityGeom::Ellipse(b))) = ( resolve_ref(document, sys, cache, *a), resolve_ref(document, sys, cache, *b), ) { return vec![Rc::new(EqualMajorAxesConic::new( GConic::Ellipse(a), GConic::Ellipse(b), ))]; } let (Some(ca), Some(cb)) = (whole_circle(sys, cache, *a), whole_circle(sys, cache, *b)) else { return Vec::new(); }; vec![Rc::new(Equal::new(cb.rad, ca.rad, 1.0))] } ConstraintKind::EqualDistance => { let [a, b, c2, d] = c.refs.as_slice() else { return Vec::new(); }; let (Some(pa), Some(pb), Some(pc), Some(pd)) = ( point_ref(sys, cache, *a), point_ref(sys, cache, *b), point_ref(sys, cache, *c2), point_ref(sys, cache, *d), ) else { return Vec::new(); }; vec![Rc::new(EqualLineLength::new( GLine { p1: pc, p2: pd }, GLine { p1: pa, p2: pb }, ))] } ConstraintKind::Colinear => { let [a, b] = c.refs.as_slice() else { return Vec::new(); }; let (Some(onto), Some(moving)) = (whole_line(sys, cache, *a), whole_line(sys, cache, *b)) else { return Vec::new(); }; // Pinning both of `moving`'s endpoints onto `onto`'s infinite // line forces the two to coincide (as long as `moving`'s own // two points stay distinct) — no dedicated "colinear" primitive // needed, `PointOnLine` applied twice does it. vec![ Rc::new(PointOnLine::new(moving.p1, onto)), Rc::new(PointOnLine::new(moving.p2, onto)), ] } ConstraintKind::Midpoint => { let [a, b] = c.refs.as_slice() else { return Vec::new(); }; let (Some(p), Some(l)) = (point_ref(sys, cache, *a), bounded_line(sys, cache, *b)) else { return Vec::new(); }; vec![ Rc::new(CenterOfGravity::new( p.x, vec![l.p1.x, l.p2.x], vec![0.5, 0.5], )), Rc::new(CenterOfGravity::new( p.y, vec![l.p1.y, l.p2.y], vec![0.5, 0.5], )), ] } ConstraintKind::PointOnCurve => { let [a, b] = c.refs.as_slice() else { return Vec::new(); }; let (Some(p), Some(geom)) = ( point_ref(sys, cache, *a), resolve_ref(document, sys, cache, *b), ) else { return Vec::new(); }; match geom { EntityGeom::Line(l) | EntityGeom::Ray(l) | EntityGeom::XLine(l) => { vec![Rc::new(PointOnLine::new(p, l))] } EntityGeom::Circle(circ) => { let zero = sys.add_param(0.0, true); vec![Rc::new(P2CDistance::new(circ, p, zero))] } // Same "distance to the underlying circle is zero" relation // as the `Circle` arm — doesn't restrict the point to // within the arc's own sweep, matching that same // pre-existing simplification for `Circle`. EntityGeom::Arc(a) => { let zero = sys.add_param(0.0, true); vec![Rc::new(P2CDistance::new(a.circle, p, zero))] } EntityGeom::Ellipse(ellipse) => { vec![Rc::new(PointOnEllipse::new(p, ellipse))] } EntityGeom::Spline { curve, .. } => { let (x, y) = { let store = sys.store(); (store.get(p.x), store.get(p.y)) }; let Some(EntityType::Spline(source)) = document.get_entity(b.entity) else { return Vec::new(); }; let Some(source_curve) = crate::entities::spline::nurbs3(source) else { return Vec::new(); }; let Some(z) = source_curve.control_points().first().map(|point| point[2]) else { return Vec::new(); }; let parameter = sys.add_param(source_curve.parameter_at([x, y, z]), false); vec![ Rc::new(PointOnBSpline::new( p.x, parameter, SplineCoord::X, curve.clone(), )), Rc::new(PointOnBSpline::new(p.y, parameter, SplineCoord::Y, curve)), ] } EntityGeom::Polyline { .. } => b .segment_index() .and_then(|index| geom.line_segment(index)) .map(|line| vec![Rc::new(PointOnLine::new(p, line)) as Rc]) .unwrap_or_default(), EntityGeom::Point(_) => Vec::new(), } } ConstraintKind::Symmetric => { let [a, b, m] = c.refs.as_slice() else { return Vec::new(); }; let (Some(pa), Some(pb), Some(mirror)) = ( point_ref(sys, cache, *a), point_ref(sys, cache, *b), whole_line(sys, cache, *m), ) else { return Vec::new(); }; let pair = GLine { p1: pa, p2: pb }; vec![ Rc::new(MidpointOnLine::new(pair, mirror)), Rc::new(PerpendicularConstraint::new(sys.store(), pair, mirror)), ] } ConstraintKind::Fixed => { let Some(r) = c.refs.first() else { return Vec::new(); }; if r.segment_index().is_some() { let Some(line) = whole_line(sys, cache, *r) else { return Vec::new(); }; let (x1, y1, x2, y2) = { let store = sys.store(); ( store.get(line.p1.x), store.get(line.p1.y), store.get(line.p2.x), store.get(line.p2.y), ) }; return vec![ Rc::new(Equal::new(line.p1.x, sys.add_param(x1, true), 1.0)), Rc::new(Equal::new(line.p1.y, sys.add_param(y1, true), 1.0)), Rc::new(Equal::new(line.p2.x, sys.add_param(x2, true), 1.0)), Rc::new(Equal::new(line.p2.y, sys.add_param(y2, true), 1.0)), ]; } if r.marker.is_some() { let Some(point) = point_ref(sys, cache, *r) else { return Vec::new(); }; let (x, y) = { let store = sys.store(); (store.get(point.x), store.get(point.y)) }; return vec![ Rc::new(Equal::new(point.x, sys.add_param(x, true), 1.0)), Rc::new(Equal::new(point.y, sys.add_param(y, true), 1.0)), ]; } let Some(geom) = resolve_ref(document, sys, cache, *r) else { return Vec::new(); }; match geom { EntityGeom::Point(point) => { let (x, y) = { let store = sys.store(); (store.get(point.x), store.get(point.y)) }; vec![ Rc::new(Equal::new(point.x, sys.add_param(x, true), 1.0)), Rc::new(Equal::new(point.y, sys.add_param(y, true), 1.0)), ] } EntityGeom::Polyline { points, .. } => { let values: Vec<_> = points .iter() .map(|point| { let store = sys.store(); (point, store.get(point.x), store.get(point.y)) }) .collect(); values .into_iter() .flat_map(|(point, x, y)| { [ Rc::new(Equal::new(point.x, sys.add_param(x, true), 1.0)) as Rc, Rc::new(Equal::new(point.y, sys.add_param(y, true), 1.0)) as Rc, ] }) .collect() } EntityGeom::Line(l) | EntityGeom::Ray(l) | EntityGeom::XLine(l) => { let (x1, y1, x2, y2) = { let store = sys.store(); ( store.get(l.p1.x), store.get(l.p1.y), store.get(l.p2.x), store.get(l.p2.y), ) }; vec![ Rc::new(Equal::new(l.p1.x, sys.add_param(x1, true), 1.0)), Rc::new(Equal::new(l.p1.y, sys.add_param(y1, true), 1.0)), Rc::new(Equal::new(l.p2.x, sys.add_param(x2, true), 1.0)), Rc::new(Equal::new(l.p2.y, sys.add_param(y2, true), 1.0)), ] } EntityGeom::Circle(circ) => { let (cx, cy, r) = { let store = sys.store(); ( store.get(circ.center.x), store.get(circ.center.y), store.get(circ.rad), ) }; vec![ Rc::new(Equal::new(circ.center.x, sys.add_param(cx, true), 1.0)), Rc::new(Equal::new(circ.center.y, sys.add_param(cy, true), 1.0)), Rc::new(Equal::new(circ.rad, sys.add_param(r, true), 1.0)), ] } // Pinning the 5 intrinsic params (center, radius, both // angles) is enough — `start`/`end` are already tied to // those via the arc-rules `CurveValue` constraints // `solve_scope` adds for every arc, so pinning them too // would just be redundant. EntityGeom::Arc(a) => { let (cx, cy, r, sa, ea) = { let store = sys.store(); ( store.get(a.circle.center.x), store.get(a.circle.center.y), store.get(a.circle.rad), store.get(a.start_angle), store.get(a.end_angle), ) }; vec![ Rc::new(Equal::new(a.circle.center.x, sys.add_param(cx, true), 1.0)), Rc::new(Equal::new(a.circle.center.y, sys.add_param(cy, true), 1.0)), Rc::new(Equal::new(a.circle.rad, sys.add_param(r, true), 1.0)), Rc::new(Equal::new(a.start_angle, sys.add_param(sa, true), 1.0)), Rc::new(Equal::new(a.end_angle, sys.add_param(ea, true), 1.0)), ] } // Pinning center + radmin is enough — `focus1` is already // tied to `center` by the ellipse-rules `Difference` // constraints `solve_scope` adds for every registered // ellipse (see this module's doc comment), so with `center` // pinned here too, pinning `focus1` again would just be // redundant (same reasoning as Arc's `Fixed` arm above). EntityGeom::Ellipse(el) => { let (cx, cy, b) = { let store = sys.store(); ( store.get(el.center.x), store.get(el.center.y), store.get(el.radmin), ) }; vec![ Rc::new(Equal::new(el.center.x, sys.add_param(cx, true), 1.0)), Rc::new(Equal::new(el.center.y, sys.add_param(cy, true), 1.0)), Rc::new(Equal::new(el.radmin, sys.add_param(b, true), 1.0)), ] } EntityGeom::Spline { curve, .. } => { let values: Vec<_> = curve .poles .iter() .map(|point| { let store = sys.store(); (point, store.get(point.x), store.get(point.y)) }) .collect(); values .into_iter() .flat_map(|(point, x, y)| { [ Rc::new(Equal::new(point.x, sys.add_param(x, true), 1.0)) as Rc, Rc::new(Equal::new(point.y, sys.add_param(y, true), 1.0)) as Rc, ] }) .collect() } } } ConstraintKind::Distance => { let [a, b] = c.refs.as_slice() else { return Vec::new(); }; let (Some(pa), Some(pb)) = (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) else { return Vec::new(); }; let Some(resolved) = resolved_target(params, c) else { return Vec::new(); }; let target = sys.add_param(resolved, true); vec![Rc::new(P2PDistance::new(pa, pb, target))] } ConstraintKind::DistanceDirected => { let Some((&a, rest)) = c.refs.split_first() else { return Vec::new(); }; let Some((&b, direction_ref)) = rest.split_first() else { return Vec::new(); }; let (Some(pa), Some(pb)) = (point_ref(sys, cache, a), point_ref(sys, cache, b)) else { return Vec::new(); }; let Some(resolved) = resolved_target(params, c) else { return Vec::new(); }; if matches!( c.distance_direction_type, distance_direction_type::PARALLEL_TO_LINE | distance_direction_type::PERPENDICULAR_TO_LINE ) { if let Some(line) = direction_ref .first() .and_then(|reference| whole_line(sys, cache, *reference)) { let current = { let store = sys.store(); let delta = [ store.get(pb.x) - store.get(pa.x), store.get(pb.y) - store.get(pa.y), ]; let axis = [ store.get(line.p2.x) - store.get(line.p1.x), store.get(line.p2.y) - store.get(line.p1.y), ]; let length = axis[0].hypot(axis[1]); if length <= f64::EPSILON { 0.0 } else if c.distance_direction_type == distance_direction_type::PERPENDICULAR_TO_LINE { delta[0] * (-axis[1] / length) + delta[1] * (axis[0] / length) } else { delta[0] * (axis[0] / length) + delta[1] * (axis[1] / length) } }; let target = sys.add_param(oriented_distance(resolved, current), true); return vec![Rc::new(ProjectedDistanceAlongLine::new( pa, pb, target, line, c.distance_direction_type == distance_direction_type::PERPENDICULAR_TO_LINE, ))]; } } let Some(direction) = c.distance_direction else { return Vec::new(); }; let current = { let store = sys.store(); let length = direction.x.hypot(direction.y); if length <= f64::EPSILON { 0.0 } else { (store.get(pb.x) - store.get(pa.x)) * direction.x / length + (store.get(pb.y) - store.get(pa.y)) * direction.y / length } }; let target = sys.add_param(oriented_distance(resolved, current), true); ProjectedDistance::new(pa, pb, target, [direction.x, direction.y]) .map(|constraint| vec![Rc::new(constraint) as Rc]) .unwrap_or_default() } ConstraintKind::Angle => { let [a, b] = c.refs.as_slice() else { return Vec::new(); }; let (Some(fixed), Some(moving)) = (whole_line(sys, cache, *a), whole_line(sys, cache, *b)) else { return Vec::new(); }; let Some(resolved) = resolved_target(params, c) else { return Vec::new(); }; let angle = resolved.to_radians(); let angle = match c.angle_sector { angle_sector::ANTIPARALLEL_CLOCKWISE => std::f64::consts::PI - angle, angle_sector::PARALLEL_CLOCKWISE => -angle, angle_sector::ANTIPARALLEL_COUNTERCLOCKWISE => std::f64::consts::PI + angle, _ => angle, }; let angle = sys.add_param(angle, true); vec![Rc::new(L2LAngle::new(fixed, moving, angle))] } ConstraintKind::Angle3Point => { let [a, vertex, b] = c.refs.as_slice() else { return Vec::new(); }; let (Some(a), Some(vertex), Some(b)) = ( point_ref(sys, cache, *a), point_ref(sys, cache, *vertex), point_ref(sys, cache, *b), ) else { return Vec::new(); }; let Some(resolved) = resolved_target(params, c) else { return Vec::new(); }; let angle = resolved.to_radians(); let angle = match c.angle_sector { angle_sector::ANTIPARALLEL_CLOCKWISE => std::f64::consts::PI - angle, angle_sector::PARALLEL_CLOCKWISE => -angle, angle_sector::ANTIPARALLEL_COUNTERCLOCKWISE => std::f64::consts::PI + angle, _ => angle, }; let angle = sys.add_param(angle, true); vec![Rc::new(L2LAngle::new( GLine { p1: vertex, p2: a }, GLine { p1: vertex, p2: b }, angle, ))] } ConstraintKind::Radius => { let Some(r) = c.refs.first() else { return Vec::new(); }; let Some(circle) = whole_circle(sys, cache, *r) else { return Vec::new(); }; let Some(resolved) = resolved_target(params, c) else { return Vec::new(); }; let target = sys.add_param(resolved, true); vec![Rc::new(Equal::new(circle.rad, target, 1.0))] } // Same math as `Radius`, just `radius = target / 2` instead of // `radius = target` — `Equal`'s `ratio` param already supports // this, matching how the native format stores Diameter as the same // `ACRADIUSDIAMETERCONSTRAINT` class with a different mode byte // rather than a distinct one (`dwg_native_constraints.rs`). ConstraintKind::Diameter => { let Some(r) = c.refs.first() else { return Vec::new(); }; let Some(circle) = whole_circle(sys, cache, *r) else { return Vec::new(); }; let Some(resolved) = resolved_target(params, c) else { return Vec::new(); }; let target = sys.add_param(resolved, true); vec![Rc::new(Equal::new(circle.rad, target, 0.5))] } // X-only/Y-only distance between two points. Positive standard // dimension values are magnitudes, so retain the geometry's current // orientation; an explicitly negative command value remains signed. ConstraintKind::DistanceX => { let [a, b] = c.refs.as_slice() else { return Vec::new(); }; let (Some(pa), Some(pb)) = (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) else { return Vec::new(); }; let Some(resolved) = resolved_target(params, c) else { return Vec::new(); }; let current = { let store = sys.store(); store.get(pb.x) - store.get(pa.x) }; let target = sys.add_param(oriented_distance(resolved, current), true); vec![Rc::new(Difference::new(pa.x, pb.x, target))] } ConstraintKind::DistanceY => { let [a, b] = c.refs.as_slice() else { return Vec::new(); }; let (Some(pa), Some(pb)) = (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) else { return Vec::new(); }; let Some(resolved) = resolved_target(params, c) else { return Vec::new(); }; let current = { let store = sys.store(); store.get(pb.y) - store.get(pa.y) }; let target = sys.add_param(oriented_distance(resolved, current), true); vec![Rc::new(Difference::new(pa.y, pb.y, target))] } ConstraintKind::Tangent => { let [a, b] = c.refs.as_slice() else { return Vec::new(); }; let (Some(ga), Some(gb)) = ( resolve_ref(document, sys, cache, *a), resolve_ref(document, sys, cache, *b), ) else { return Vec::new(); }; // An `Arc`'s `.circle` behaves identically to a plain `Circle` // for tangency math — normalize both refs down first so the // match below doesn't need every Circle/Arc combination // written out separately. match (as_circle_or_line(ga, *a), as_circle_or_line(gb, *b)) { (CircleOrLine::Circle(c1), CircleOrLine::Circle(c2)) => { let store = sys.store(); let (x1, y1, r1) = ( store.get(c1.center.x), store.get(c1.center.y), store.get(c1.rad), ); let (x2, y2, r2) = ( store.get(c2.center.x), store.get(c2.center.y), store.get(c2.rad), ); let center_dist = ((x1 - x2).powi(2) + (y1 - y2).powi(2)).sqrt(); // One circle already sits inside the other's span, rather // than the two side by side — pick internal tangency so // the first solve doesn't have to cross the singularity // between the two tangency configurations. let internal = center_dist < (r1 - r2).abs(); vec![Rc::new(TangentCircumf::new( c1.center, c2.center, c1.rad, c2.rad, internal, ))] } (CircleOrLine::Circle(circ), CircleOrLine::Line(line)) | (CircleOrLine::Line(line), CircleOrLine::Circle(circ)) => { let store = sys.store(); let (cx, cy) = (store.get(circ.center.x), store.get(circ.center.y)); let (x1, y1) = (store.get(line.p1.x), store.get(line.p1.y)); let (x2, y2) = (store.get(line.p2.x), store.get(line.p2.y)); // Signed area of (p2-p1) × (center-p1) — same formula // `C2LDistance::signed_value` itself uses — so `ccw`'s // sign matches whichever side the circle already sits on. let area = (x2 - x1) * (cy - y1) - (y2 - y1) * (cx - x1); let ccw = area >= 0.0; // A driven, fixed zero: with `internal: false` this makes // `C2LDistance`'s target exactly the circle's own radius // (see its `error_grad`), i.e. plain tangency rather than // an offset distance. let zero = sys.add_param(0.0, true); vec![Rc::new(C2LDistance::new(circ, line, zero, ccw, false))] } (CircleOrLine::Ellipse(ellipse), CircleOrLine::Line(line)) | (CircleOrLine::Line(line), CircleOrLine::Ellipse(ellipse)) => { vec![Rc::new(TangentEllipseLine::new(line, ellipse))] } _ => Vec::new(), } } // Smooth edits the endpoint controls through cadkernel's spatial // NURBS operation after the ordinary 2D system has settled. ConstraintKind::Smooth => Vec::new(), ConstraintKind::RigidSet => { let points: Vec<_> = c .rigid_points .iter() .filter_map(|(reference, original)| { point_ref(sys, cache, *reference).map(|point| (point, *original)) }) .collect(); let Some(second) = (1..points.len()).find(|&index| { let delta = points[index].1 - points[0].1; delta.x * delta.x + delta.y * delta.y > 1.0e-18 }) else { return Vec::new(); }; let mut pairs = vec![(0, second)]; for index in 1..points.len() { if index == second { continue; } pairs.push((0, index)); pairs.push((second, index)); } pairs .into_iter() .map(|(a, b)| { let delta = points[b].1 - points[a].1; let target = sys.add_param((delta.x * delta.x + delta.y * delta.y).sqrt(), true); Rc::new(P2PDistance::new(points[a].0, points[b].0, target)) as Rc }) .collect() } // A circle's radius is always normal to its own tangent, so // "line normal to circle/arc" reduces to "line passes through the // circle's center" — the same `PointOnLine` primitive // `PointOnCurve` already uses for a point-on-line case. ConstraintKind::Normal => { let [a, b] = c.refs.as_slice() else { return Vec::new(); }; let (Some(ga), Some(gb)) = ( resolve_ref(document, sys, cache, *a), resolve_ref(document, sys, cache, *b), ) else { return Vec::new(); }; match (as_circle_or_line(ga, *a), as_circle_or_line(gb, *b)) { (CircleOrLine::Circle(circ), CircleOrLine::Line(line)) | (CircleOrLine::Line(line), CircleOrLine::Circle(circ)) => { vec![Rc::new(PointOnLine::new(circ.center, line))] } // Line-Line has no meaning here (that's `Perpendicular`); // Circle-Circle "normal" (orthogonal circles) needs a // different, not-yet-implemented relation. _ => Vec::new(), } } } } fn smooth_target_jet(entity: &EntityType, marker: i32) -> Option { let parameter = match marker { 0 => 0.0, 1 => 1.0, _ => return None, }; let curve = crate::entities::curve::entity_curve(entity)?; if curve.is_closed() { return None; } let step = if marker == 0 { 1.0e-4 } else { -1.0e-4 }; let point = curve.point_at(parameter); let tangent = curve.tangent_at(parameter); let next = curve.point_at(parameter + step); let third = curve.point_at(parameter + 2.0 * step); Some(cadkernel::space::CurveJet { point, tangent, curvature: cadkernel::space::curve::curvature_through(point, next, third), }) } fn apply_smooth_constraints( document: &acadrust::CadDocument, set: &ParametricConstraintSet, results: &mut Vec<(Handle, EntityType)>, ) { for constraint in set .constraints .iter() .filter(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth) { let [source_ref, target_ref] = constraint.refs.as_slice() else { continue; }; let source = results .iter() .find(|(handle, _)| *handle == source_ref.entity) .map(|(_, entity)| entity.clone()) .or_else(|| document.get_entity(source_ref.entity).cloned()); let target = results .iter() .find(|(handle, _)| *handle == target_ref.entity) .map(|(_, entity)| entity.clone()) .or_else(|| document.get_entity(target_ref.entity).cloned()); let (Some(EntityType::Spline(mut spline)), Some(target)) = (source, target) else { continue; }; let endpoint = match source_ref.marker { Some(0) => cadkernel::space::SplineEnd::Start, Some(1) => cadkernel::space::SplineEnd::End, _ => continue, }; let Some(target_marker) = target_ref.marker else { continue; }; let Some(target_jet) = smooth_target_jet(&target, target_marker) else { continue; }; let Some(curve) = crate::entities::spline::nurbs3(&spline) else { continue; }; let Some(smoothed) = cadkernel::space::smooth_nurbs_endpoint(&curve, endpoint, target_jet) else { continue; }; crate::entities::spline::replace_with_nurbs(&mut spline, &smoothed); let updated = EntityType::Spline(spline); if let Some((_, entity)) = results .iter_mut() .find(|(handle, _)| *handle == source_ref.entity) { *entity = updated; } else { results.push((source_ref.entity, updated)); } } } const MOVE_EPS: f64 = 1e-9; type SolveResult = ( Vec<(Handle, EntityType)>, usize, Vec<( ConstraintId, cadkernel_constraints::diagnosis::RedundancyKind, )>, ); fn retained_line_length(entity: &EntityType, segment: Option) -> Option { match (entity, segment) { (EntityType::Line(line), None) => Some((line.end - line.start).length()), (EntityType::LwPolyline(polyline), Some(index)) => { let world = crate::entities::curve::lwpolyline_world_xy(polyline)?; let a = world.vertices.get(index)?.location; let b = if let Some(vertex) = world.vertices.get(index + 1) { vertex.location } else if world.is_closed { world.vertices.first()?.location } else { return None; }; Some((b - a).length()) } (EntityType::Polyline2D(polyline), Some(index)) => { let a = polyline.vertices.get(index)?.location; let b = if let Some(vertex) = polyline.vertices.get(index + 1) { vertex.location } else if polyline.is_closed() { polyline.vertices.first()?.location } else { return None; }; Some((b - a).length()) } _ => None, } } fn retained_radius(entity: &EntityType) -> Option { match entity { EntityType::Circle(circle) => Some(circle.radius), EntityType::Arc(arc) => Some(arc.radius), _ => None, } } /// Rebuilds `set`'s entire `cadkernel_constraints::System` from current document /// geometry, solves it, and returns the resulting entity states for every /// handle whose registered coordinates actually moved beyond floating-point /// noise, plus the scope's total remaining degrees of freedom and any /// redundant/conflicting constraints, resolved /// back to the `ConstraintId`s a `ConflictResolverPanel` can name and offer /// to remove. `None` if nothing in the scope could be built (no constraint /// resolved to anything). DOF/conflicts are still returned even when every /// subsystem fails to solve (rank is a structural property of the Jacobian, /// not of whether Dogleg converged) — only the geometry write-back is gated /// on a successful solve. fn solve_scope( document: &acadrust::CadDocument, drawing_params: &ParameterTable, set: &ParametricConstraintSet, driven_refs: &[ParametricRef], retain_size: bool, retained_before: &HashMap>, ) -> Option { let params = if set.local_parameters.is_empty() { drawing_params } else { &set.local_parameters }; let mut sys = System::new(); let mut cache: HashMap = HashMap::new(); // Tracks which system-level `cadkernel_constraints` constraint(s) came from which // `ParametricConstraint` — a `SubSystem`'s redundant-row indices (from // `cadkernel_constraints::diagnosis`) are local to that partition's own constraint // list, not `set.constraints`' indices, and several kinds contribute // more than one system-level constraint per `ParametricConstraint` (see // `build_constraint`'s doc comment). `Rc::ptr_eq` against this after // partitioning resolves a row back to the `ConstraintId` the UI // actually names. let mut owner: Vec<(Rc, ConstraintId)> = Vec::new(); for c in &set.constraints { for constraint in build_constraint(document, &mut sys, &mut cache, params, c) { owner.push((constraint.clone(), c.id)); sys.add_constraint(constraint); } } let has_smooth = set .constraints .iter() .any(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth); if cache.is_empty() && !has_smooth { return None; } // Preserve current line lengths and radii for this solve when // CONSTRAINTSOLVEMODE requests it. These temporary equations guide the // solution without becoming persistent dimensional constraints. if retain_size { for (handle, geom) in &cache { match geom { EntityGeom::Line(line) => { let current_length = { let store = sys.store(); let dx = store.get(line.p2.x) - store.get(line.p1.x); let dy = store.get(line.p2.y) - store.get(line.p1.y); (dx * dx + dy * dy).sqrt() }; let length = retained_before .get(handle) .and_then(|entity| retained_line_length(entity, None)) .unwrap_or(current_length); let target = sys.add_param(length, true); sys.add_constraint(Rc::new(P2PDistance::new(line.p1, line.p2, target))); } EntityGeom::Polyline { points, straight, closed, } => { for index in 0..straight.len() { if !straight[index] { continue; } let Some(a) = points.get(index).copied() else { continue; }; let b = if let Some(point) = points.get(index + 1).copied() { Some(point) } else if *closed { points.first().copied() } else { None }; let Some(b) = b else { continue }; let current_length = { let store = sys.store(); let dx = store.get(b.x) - store.get(a.x); let dy = store.get(b.y) - store.get(a.y); (dx * dx + dy * dy).sqrt() }; let length = retained_before .get(handle) .and_then(|entity| retained_line_length(entity, Some(index))) .unwrap_or(current_length); let target = sys.add_param(length, true); sys.add_constraint(Rc::new(P2PDistance::new(a, b, target))); } } EntityGeom::Circle(circle) => { let radius = retained_before .get(handle) .and_then(|entity| retained_radius(entity)) .unwrap_or_else(|| sys.store().get(circle.rad)); let target = sys.add_param(radius, true); sys.add_constraint(Rc::new(Equal::new(circle.rad, target, 1.0))); } EntityGeom::Arc(arc) => { let radius = retained_before .get(handle) .and_then(|entity| retained_radius(entity)) .unwrap_or_else(|| sys.store().get(arc.circle.rad)); let target = sys.add_param(radius, true); sys.add_constraint(Rc::new(Equal::new(arc.circle.rad, target, 1.0))); } _ => {} } } } // Arc rules (this module's doc comment): keep every registered arc's // `start`/`end` points consistent with its center/radius/angle, // unconditionally — the kernel adds these the moment an arc // exists, not only when some `ParametricConstraint` happens to reference // its endpoint. Not tracked in `owner`: these are solver-internal // bookkeeping, never redundant with anything a user-facing constraint // could name, so there's no `ConstraintId` for them to report against. for geom in cache.values() { let EntityGeom::Arc(arc) = geom else { continue }; let curve = Rc::new(*arc); sys.add_constraint(Rc::new(CurveValue::new( arc.start, arc.start.x, curve.clone(), arc.start_angle, ))); sys.add_constraint(Rc::new(CurveValue::new( arc.start, arc.start.y, curve.clone(), arc.start_angle, ))); sys.add_constraint(Rc::new(CurveValue::new( arc.end, arc.end.x, curve.clone(), arc.end_angle, ))); sys.add_constraint(Rc::new(CurveValue::new( arc.end, arc.end.y, curve, arc.end_angle, ))); } // A point explicitly edited by a grip, STRETCH, or Properties is the // driver's target for this solve. Pin its current coordinates for this // solve only so constrained neighbours respond while the chosen point // stays at the exact cursor or typed coordinate. for reference in driven_refs { if !set .constraints .iter() .any(|constraint| constraint.refs.iter().any(|r| r.entity == reference.entity)) { continue; } let Some(point) = resolve_constraint_point(document, &mut sys, &mut cache, *reference) else { continue; }; let (x, y) = { let store = sys.store(); (store.get(point.x), store.get(point.y)) }; let target_x = sys.add_param(x, true); let target_y = sys.add_param(y, true); sys.add_constraint(Rc::new(Equal::new(point.x, target_x, 1.0))); sys.add_constraint(Rc::new(Equal::new(point.y, target_y, 1.0))); } // Ellipse rules (same motivation as arc rules, different mechanism): // `cadkernel_constraints::geo::Ellipse` stores `focus1` as an absolute point, not an // offset from `center`. If some constraint (e.g. Concentric) pulls only // on `center` and nothing references `focus1`/`radmin`, the solver // correctly leaves `focus1`'s *absolute* coordinates untouched — but // `center` moved and `focus1` didn't, so the derived vector // `focus1 - center` (which is what `major_axis` direction/length and // `minor_axis_ratio` are actually computed from on write-back) changes // anyway, making the ellipse appear to rotate and reshape even though // no shape parameter was itself pulled on. Pinning `focus1 - center` to // its pre-solve (seed) value forces `focus1` to translate rigidly with // `center`, keeping orientation/eccentricity invariant unless something // actually constrains `focus1`/`radmin` directly (only `Fixed` does // today). `radmin` needs no equivalent rule — as a lone scalar it isn't // coupled to `center`'s movement, so it already stays put on its own. for geom in cache.values() { let EntityGeom::Ellipse(el) = geom else { continue; }; let (cx, cy, fx, fy) = { let store = sys.store(); ( store.get(el.center.x), store.get(el.center.y), store.get(el.focus1.x), store.get(el.focus1.y), ) }; let dx = sys.add_param(fx - cx, true); let dy = sys.add_param(fy - cy, true); sys.add_constraint(Rc::new(Difference::new(el.center.x, el.focus1.x, dx))); sys.add_constraint(Rc::new(Difference::new(el.center.y, el.focus1.y, dy))); } let partitions = sys.partition(); for sub in &partitions { solve_dl(sub, sys.store_mut()); } // `System::partition` only includes parameters referenced by a // constraint, so an otherwise registered but unconstrained coordinate (e.g. a // line's X after only a Horizontal constraint pins its Ys) is invisible // to `diagnose` entirely, undercounting DOF. Every such untouched free // param is unconstrained on its own, i.e. exactly 1 DOF each — added // back here rather than fixed in `cadkernel_constraints::diagnosis`, which correctly // has no opinion on params outside the `SubSystem` it was handed. let total_free: usize = cache .values() .map(|g| match g { EntityGeom::Point(_) => 2, EntityGeom::Line(_) | EntityGeom::Ray(_) | EntityGeom::XLine(_) => 4, EntityGeom::Polyline { points, .. } => points.len() * 2, EntityGeom::Circle(_) => 3, // Raw param count (center×2, rad, start×2, end×2, both // angles) — same "raw, not netted against its own // constraints" convention as every other arm here. The four // arc-rules `CurveValue` constraints (always present, added // just above) already reduce this to 5 *effective* DOF through // the normal `touched_free`/`diag.dof` accounting below, the // same way any other constraint would. EntityGeom::Arc(_) => 9, EntityGeom::Ellipse(_) => 5, EntityGeom::Spline { curve, .. } => curve.poles.len() * 2, }) .sum(); let touched_free: usize = partitions.iter().map(|s| s.p_size()).sum(); let mut dof = total_free.saturating_sub(touched_free); let mut conflicts: Vec<( ConstraintId, cadkernel_constraints::diagnosis::RedundancyKind, )> = Vec::new(); for sub in &partitions { let diag = cadkernel_constraints::diagnosis::diagnose(sub, sys.store()); dof += diag.dof; if diag.redundant.is_empty() { continue; } let mut seen = HashSet::new(); let reportable: Vec<_> = diag .redundant .iter() .copied() .filter(|row| { owner .iter() .find(|(rc, _)| Rc::ptr_eq(rc, &sub.constraints()[*row])) .is_some_and(|(_, id)| seen.insert(*id)) }) .collect(); for (row, kind) in cadkernel_constraints::diagnosis::classify_redundant(sub, sys.store(), &reportable) { let row_constraint = &sub.constraints()[row]; if let Some(&(_, id)) = owner.iter().find(|(rc, _)| Rc::ptr_eq(rc, row_constraint)) { conflicts.push((id, kind)); } } } let store = sys.store(); let mut results = Vec::new(); for (&handle, geom) in &cache { let Some(entity) = document.get_entity(handle) else { continue; }; match (entity, geom) { (EntityType::Point(point), EntityGeom::Point(geometry)) => { let x = store.get(geometry.x); let y = store.get(geometry.y); if (x - point.location.x).abs() > MOVE_EPS || (y - point.location.y).abs() > MOVE_EPS { let mut updated = point.clone(); updated.location.x = x; updated.location.y = y; results.push((handle, EntityType::Point(updated))); } } (EntityType::Insert(insert), EntityGeom::Point(geometry)) => { let mut updated = insert.clone(); let next = Vector3::new( store.get(geometry.x), store.get(geometry.y), insert.insert_point.z, ); if (next - insert.insert_point).length() > MOVE_EPS { updated.insert_point = next; results.push((handle, EntityType::Insert(updated))); } } (EntityType::Text(text), EntityGeom::Point(geometry)) => { let mut updated = text.clone(); let next = Vector3::new( store.get(geometry.x), store.get(geometry.y), text.insertion_point.z, ); let delta = next - text.insertion_point; if delta.length() > MOVE_EPS { updated.insertion_point = next; if let Some(alignment) = &mut updated.alignment_point { *alignment = *alignment + delta; } results.push((handle, EntityType::Text(updated))); } } (EntityType::MText(text), EntityGeom::Point(geometry)) => { let mut updated = text.clone(); let next = Vector3::new( store.get(geometry.x), store.get(geometry.y), text.insertion_point.z, ); if (next - text.insertion_point).length() > MOVE_EPS { updated.insertion_point = next; results.push((handle, EntityType::MText(updated))); } } (EntityType::AttributeDefinition(attribute), EntityGeom::Point(geometry)) => { let mut updated = attribute.clone(); let next = Vector3::new( store.get(geometry.x), store.get(geometry.y), attribute.insertion_point.z, ); let delta = next - attribute.insertion_point; if delta.length() > MOVE_EPS { updated.insertion_point = next; updated.alignment_point = updated.alignment_point + delta; results.push((handle, EntityType::AttributeDefinition(updated))); } } (EntityType::AttributeEntity(attribute), EntityGeom::Point(geometry)) => { let mut updated = attribute.clone(); let next = Vector3::new( store.get(geometry.x), store.get(geometry.y), attribute.insertion_point.z, ); let delta = next - attribute.insertion_point; if delta.length() > MOVE_EPS { updated.insertion_point = next; updated.alignment_point = updated.alignment_point + delta; results.push((handle, EntityType::AttributeEntity(updated))); } } (EntityType::Table(table), EntityGeom::Point(geometry)) => { let mut updated = table.clone(); let next = Vector3::new( store.get(geometry.x), store.get(geometry.y), table.insertion_point.z, ); if (next - table.insertion_point).length() > MOVE_EPS { updated.insertion_point = next; results.push((handle, EntityType::Table(updated))); } } (EntityType::Line(l), EntityGeom::Line(g)) => { let (x1, y1, x2, y2) = ( store.get(g.p1.x), store.get(g.p1.y), store.get(g.p2.x), store.get(g.p2.y), ); if (x1 - l.start.x).abs() > MOVE_EPS || (y1 - l.start.y).abs() > MOVE_EPS || (x2 - l.end.x).abs() > MOVE_EPS || (y2 - l.end.y).abs() > MOVE_EPS { let mut updated = l.clone(); updated.start.x = x1; updated.start.y = y1; updated.end.x = x2; updated.end.y = y2; results.push((handle, EntityType::Line(updated))); } } (EntityType::Ray(ray), EntityGeom::Ray(g)) => { let (x1, y1, x2, y2) = ( store.get(g.p1.x), store.get(g.p1.y), store.get(g.p2.x), store.get(g.p2.y), ); let length = (x2 - x1).hypot(y2 - y1); let direction = if length > MOVE_EPS { Vector3::new((x2 - x1) / length, (y2 - y1) / length, 0.0) } else { ray.direction }; if (x1 - ray.base_point.x).abs() > MOVE_EPS || (y1 - ray.base_point.y).abs() > MOVE_EPS || (direction - ray.direction).length() > MOVE_EPS { let mut updated = ray.clone(); updated.base_point.x = x1; updated.base_point.y = y1; updated.direction = direction; results.push((handle, EntityType::Ray(updated))); } } (EntityType::XLine(line), EntityGeom::XLine(g)) => { let (x1, y1, x2, y2) = ( store.get(g.p1.x), store.get(g.p1.y), store.get(g.p2.x), store.get(g.p2.y), ); let length = (x2 - x1).hypot(y2 - y1); let direction = if length > MOVE_EPS { Vector3::new((x2 - x1) / length, (y2 - y1) / length, 0.0) } else { line.direction }; if (x1 - line.base_point.x).abs() > MOVE_EPS || (y1 - line.base_point.y).abs() > MOVE_EPS || (direction - line.direction).length() > MOVE_EPS { let mut updated = line.clone(); updated.base_point.x = x1; updated.base_point.y = y1; updated.direction = direction; results.push((handle, EntityType::XLine(updated))); } } (EntityType::LwPolyline(polyline), EntityGeom::Polyline { points, .. }) => { let mut updated = polyline.clone(); let mut changed = false; for (vertex, point) in updated.vertices.iter_mut().zip(points.iter()) { let x = store.get(point.x); let y = store.get(point.y); if (x - vertex.location.x).abs() > MOVE_EPS || (y - vertex.location.y).abs() > MOVE_EPS { vertex.location.x = x; vertex.location.y = y; changed = true; } } if changed { results.push((handle, EntityType::LwPolyline(updated))); } } (EntityType::Polyline2D(polyline), EntityGeom::Polyline { points, .. }) => { let mut updated = polyline.clone(); let mut changed = false; for (vertex, point) in updated.vertices.iter_mut().zip(points.iter()) { let x = store.get(point.x); let y = store.get(point.y); if (x - vertex.location.x).abs() > MOVE_EPS || (y - vertex.location.y).abs() > MOVE_EPS { vertex.location.x = x; vertex.location.y = y; changed = true; } } if changed { results.push((handle, EntityType::Polyline2D(updated))); } } (EntityType::Circle(c), EntityGeom::Circle(g)) => { let (cx, cy, r) = ( store.get(g.center.x), store.get(g.center.y), store.get(g.rad), ); if (cx - c.center.x).abs() > MOVE_EPS || (cy - c.center.y).abs() > MOVE_EPS || (r - c.radius).abs() > MOVE_EPS { let mut updated = c.clone(); updated.center.x = cx; updated.center.y = cy; updated.radius = r; results.push((handle, EntityType::Circle(updated))); } } (EntityType::Arc(a), EntityGeom::Arc(g)) => { // Center/radius/angles only — `start`/`end` are derived // (acadrust's `Arc` has no separate stored fields for them; // `start_point()`/`end_point()` compute them from these // same four), so nothing further needs writing back for // them specifically. let (cx, cy, r) = ( store.get(g.circle.center.x), store.get(g.circle.center.y), store.get(g.circle.rad), ); let (new_start_angle, new_end_angle) = (store.get(g.start_angle), store.get(g.end_angle)); if (cx - a.center.x).abs() > MOVE_EPS || (cy - a.center.y).abs() > MOVE_EPS || (r - a.radius).abs() > MOVE_EPS || (new_start_angle - a.start_angle).abs() > MOVE_EPS || (new_end_angle - a.end_angle).abs() > MOVE_EPS { let mut updated = a.clone(); updated.center.x = cx; updated.center.y = cy; updated.radius = r; updated.start_angle = new_start_angle; updated.end_angle = new_end_angle; results.push((handle, EntityType::Arc(updated))); } } (EntityType::Ellipse(el), EntityGeom::Ellipse(g)) => { // Converting back to acadrust's center + major-axis-vector + // minor/major-ratio parametrization — the inverse of // `register_entity`'s `Ellipse` arm. `rad_maj_at` (already // provided by `cadkernel_constraints::geo::Ellipse`) does the a²=b²+c² // algebra; only the major-axis *direction* needs deriving // here, from the (possibly moved) focus relative to center. let (cx, cy) = (store.get(g.center.x), store.get(g.center.y)); let (fx, fy) = (store.get(g.focus1.x), store.get(g.focus1.y)); let minor_radius = store.get(g.radmin); let (major_radius, _) = g.rad_maj_at(store, None); let focus_dist = ((fx - cx).powi(2) + (fy - cy).powi(2)).sqrt(); // Direction is ill-defined once the ellipse is (near) // circular — keep whatever direction it already had rather // than snapping to an arbitrary axis. let unit_major = if focus_dist > 1e-9 { acadrust::types::Vector3::new(fx - cx, fy - cy, 0.0).normalize() } else { el.major_axis.normalize() }; let new_major_axis = unit_major * major_radius; let new_ratio = if major_radius > 1e-9 { minor_radius / major_radius } else { 0.0 }; if (cx - el.center.x).abs() > MOVE_EPS || (cy - el.center.y).abs() > MOVE_EPS || (new_major_axis.x - el.major_axis.x).abs() > MOVE_EPS || (new_major_axis.y - el.major_axis.y).abs() > MOVE_EPS || (new_ratio - el.minor_axis_ratio).abs() > MOVE_EPS { let mut updated = el.clone(); updated.center.x = cx; updated.center.y = cy; updated.major_axis = new_major_axis; updated.minor_axis_ratio = new_ratio; results.push((handle, EntityType::Ellipse(updated))); } } (EntityType::Spline(spline), EntityGeom::Spline { curve, control_z }) => { let mut changed = spline.fit_points.len() > 0 || spline.control_points.len() != curve.poles.len(); let control_points: Vec<_> = curve .poles .iter() .zip(control_z.iter()) .enumerate() .map(|(index, (point, z))| { let next = Vector3::new(store.get(point.x), store.get(point.y), *z); changed |= spline .control_points .get(index) .is_none_or(|old| (next - *old).length() > MOVE_EPS); next }) .collect(); if changed { let mut updated = spline.clone(); updated.degree = curve.degree as i32; updated.knots = curve.knots.clone(); updated.control_points = control_points; updated.weights = curve.weights.iter().map(|id| store.get(*id)).collect(); updated.fit_points.clear(); updated.begin_tangent = Vector3::ZERO; updated.end_tangent = Vector3::ZERO; updated.flags.rational = updated .weights .windows(2) .any(|pair| (pair[0] - pair[1]).abs() > 1.0e-12); updated.flags.closed = curve.periodic; updated.flags.periodic = curve.periodic; results.push((handle, EntityType::Spline(updated))); } } _ => {} } } apply_smooth_constraints(document, set, &mut results); Some((results, dof, conflicts)) } impl Scene { /// Rejects constraints the 2D kernel bridge cannot represent before they /// are persisted or reported as applied. pub(crate) fn validate_parametric_constraint( &self, kind: ConstraintKind, refs: &[ParametricRef], driving_param: Option<&super::named_parameters::DrivingValue>, ) -> Result<(), &'static str> { if !refs_share_supported_plane(&self.document, refs) { return Err( "Constraint references must be supported entities on the same world-XY plane.", ); } if kind == ConstraintKind::Smooth { let [source_ref, target_ref] = refs else { return Err("Smooth requires one spline endpoint and one target endpoint."); }; if source_ref.entity == target_ref.entity { return Err("Smooth requires two different entities."); } let Some(EntityType::Spline(spline)) = self.document.get_entity(source_ref.entity) else { return Err("Smooth requires an open spline as its first reference."); }; if spline.flags.closed || spline.flags.periodic { return Err("Smooth requires an open spline."); } let endpoint = match source_ref.marker { Some(0) => cadkernel::space::SplineEnd::Start, Some(1) => cadkernel::space::SplineEnd::End, _ => return Err("Smooth requires a spline endpoint."), }; let Some(target_marker) = target_ref.marker else { return Err("Smooth requires a target endpoint."); }; let Some(target) = self.document.get_entity(target_ref.entity) else { return Err("Smooth target does not exist."); }; let Some(target_jet) = smooth_target_jet(target, target_marker) else { return Err("The selected target does not support endpoint smoothing."); }; let Some(curve) = crate::entities::spline::nurbs3(spline) else { return Err("The selected spline cannot be evaluated."); }; if cadkernel::space::smooth_nurbs_endpoint(&curve, endpoint, target_jet).is_none() { return Err("The selected spline cannot satisfy curvature continuity."); } return Ok(()); } let validation_target = match driving_param { Some(super::named_parameters::DrivingValue::Named(name)) if !self.named_parameters.contains(name) => { Some(super::named_parameters::DrivingValue::Literal(1.0)) } value => value.cloned(), }; let candidate = ParametricConstraint { id: 0, kind, refs: refs.to_vec(), driving_param: validation_target, enabled: true, native_origin: None, rigid_points: Vec::new(), distance_direction_type: 0, distance_direction: None, angle_sector: angle_sector::PARALLEL_COUNTERCLOCKWISE, }; let mut system = System::new(); let mut cache = HashMap::new(); if build_constraint( &self.document, &mut system, &mut cache, &self.named_parameters, &candidate, ) .is_empty() { return Err("The selected entities or target value do not support this constraint."); } Ok(()) } /// Re-solves every constraint scope touched by `changes`. Returns /// the resulting `(Handle, ChangeKind::Modified)` entries the same way /// `refresh_associative_dimensions`/`_hatches` do, for `bump_entities` /// to fold into its own `changes` vec. #[allow(dead_code)] pub(crate) fn refresh_parametric_constraints( &mut self, changes: &[(Handle, ChangeKind)], ) -> Vec<(Handle, ChangeKind)> { self.refresh_parametric_constraints_with_policy(changes, &[], false) } #[allow(dead_code)] pub(crate) fn refresh_parametric_constraints_with_driven( &mut self, changes: &[(Handle, ChangeKind)], driven_refs: &[ParametricRef], ) -> Vec<(Handle, ChangeKind)> { self.refresh_parametric_constraints_with_policy(changes, driven_refs, false) } pub(crate) fn refresh_parametric_constraints_with_policy( &mut self, changes: &[(Handle, ChangeKind)], driven_refs: &[ParametricRef], retain_size: bool, ) -> Vec<(Handle, ChangeKind)> { self.refresh_parametric_constraints_with_originals( changes, driven_refs, retain_size, &[], ) } pub(crate) fn refresh_parametric_constraints_with_originals( &mut self, changes: &[(Handle, ChangeKind)], driven_refs: &[ParametricRef], retain_size: bool, retained_originals: &[(Handle, EntityType)], ) -> Vec<(Handle, ChangeKind)> { // An erased entity takes its constraints with it instead of leaving // dangling references. Done first, // and unconditionally over every scope (not just ones a `touched` // check would catch), so a scope left with zero constraints after // this doesn't attempt a pointless resolve below. // // Recorded into the *same* undo transaction as the entity removal // that caused it (audit finding: ERASE undo silently lost constraint // state, since `parametric_constraints` lives outside `document`/ // `document.objects` and neither of those directories ever saw this // mutation) — one undo press now restores both the entity and its // constraints together. for (handle, kind) in changes { if *kind != ChangeKind::Removed { continue; } for i in 0..self.parametric_constraints.len() { if self.parametric_constraints[i] .constraints .iter() .any(|constraint| constraint.refs.iter().any(|r| r.entity == *handle)) { let scope = self.parametric_constraints[i].scope; let before = self.parametric_constraints[i].clone(); self.record_undo_parametric_constraints_before(scope, before); self.parametric_constraints[i].remove_all_touching(*handle); } } } let retained_before: HashMap> = if retain_size { let mut retained: HashMap> = self .undo_recording .as_ref() .into_iter() .flat_map(|recording| recording.before.iter()) .filter_map(|(handle, entity)| { entity .as_ref() .map(|entity| (*handle, std::sync::Arc::clone(entity))) }) .collect(); for (handle, entity) in retained_originals { retained.insert(*handle, std::sync::Arc::new(entity.clone())); } retained } else { HashMap::new() }; let mut result = Vec::new(); for i in 0..self.parametric_constraints.len() { let touched = changes.iter().any(|(handle, _)| { self.parametric_constraints[i] .constraints_touching(*handle) .next() .is_some() }); if !touched { continue; } let Some((solved, dof, conflicts)) = solve_scope( &self.document, &self.named_parameters, &self.parametric_constraints[i], driven_refs, retain_size, &retained_before, ) else { continue; }; self.parametric_constraints[i].dof = Some(dof); self.parametric_constraints[i].conflicts = conflicts; for (handle, new_entity) in solved { if let Some(before) = self.document.get_entity_arc(handle) { self.record_undo_before(handle, Some(before)); } if let Some(slot) = self.document.get_entity_mut(handle) { *slot = new_entity; } result.push((handle, ChangeKind::Modified)); } } result } /// Re-solves touched scopes during a grip drag without mutating the /// document or recording undo. The caller owns preview write-back and the /// final gesture history entry. pub(crate) fn solve_parametric_constraints_preview( &self, touched: &[Handle], driven_refs: &[ParametricRef], retain_size: bool, retained_originals: &[(Handle, EntityType)], ) -> Vec<(Handle, EntityType)> { if self.parametric_constraints.is_empty() || touched.is_empty() { return Vec::new(); } let retained_before: HashMap> = retained_originals .iter() .map(|(handle, entity)| (*handle, std::sync::Arc::new(entity.clone()))) .collect(); let mut result = Vec::new(); for set in &self.parametric_constraints { let is_touched = touched .iter() .any(|handle| set.constraints_touching(*handle).next().is_some()); if !is_touched { continue; } let Some((solved, _dof, _conflicts)) = solve_scope( &self.document, &self.named_parameters, set, driven_refs, retain_size, &retained_before, ) else { continue; }; result.extend(solved); } result } } #[cfg(test)] mod tests { use super::super::named_parameters::DrivingValue; use super::super::parametric_constraints::{ ConstraintKind, ParametricConstraintSet, ParametricRef, ParametricScope, }; use super::Scene; use acadrust::entities::EntityType; use acadrust::types::Vector3; #[test] fn validation_rejects_unsupported_entity_shapes_and_planes() { let mut scene = Scene::new(); let circle = scene.add_entity(EntityType::Circle( acadrust::entities::Circle::from_center_radius(Vector3::ZERO, 2.0), )); assert!(scene .validate_parametric_constraint( ConstraintKind::Horizontal, &[ParametricRef::whole(circle)], None ) .is_err()); let lower = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0), ))); let upper = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( Vector3::new(0.0, 0.0, 1.0), Vector3::new(1.0, 0.0, 1.0), ))); assert!(scene .validate_parametric_constraint( ConstraintKind::Parallel, &[ParametricRef::whole(lower), ParametricRef::whole(upper)], None, ) .is_err()); } #[test] fn validation_enforces_dimensional_value_domains() { let mut scene = Scene::new(); let line = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0), ))); let refs = [ParametricRef::point(line, 0), ParametricRef::point(line, 1)]; assert!(scene .validate_parametric_constraint( ConstraintKind::Distance, &refs, Some(&DrivingValue::Literal(-1.0)), ) .is_err()); assert!(scene .validate_parametric_constraint( ConstraintKind::DistanceX, &refs, Some(&DrivingValue::Literal(-1.0)), ) .is_ok()); assert!(scene .validate_parametric_constraint( ConstraintKind::DistanceX, &refs, Some(&DrivingValue::Literal(f64::NAN)), ) .is_err()); } #[test] fn ray_and_construction_line_constraints_solve_through_the_kernel() { let mut scene = Scene::new(); let ray = scene.add_entity(EntityType::Ray(acadrust::entities::Ray::new( Vector3::ZERO, Vector3::new(1.0, 1.0, 0.0), ))); let xline = scene.add_entity(EntityType::XLine(acadrust::entities::XLine::new( Vector3::new(5.0, 5.0, 0.0), Vector3::new(1.0, 1.0, 0.0), ))); let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace); set.add( ConstraintKind::Horizontal, vec![ParametricRef::whole(ray)], None, ); set.add( ConstraintKind::Vertical, vec![ParametricRef::whole(xline)], None, ); scene.refresh_parametric_constraints(&[ (ray, super::ChangeKind::Modified), (xline, super::ChangeKind::Modified), ]); let Some(EntityType::Ray(ray)) = scene.document.get_entity(ray) else { panic!("expected ray") }; assert!(ray.direction.y.abs() < 1e-7); let Some(EntityType::XLine(xline)) = scene.document.get_entity(xline) else { panic!("expected construction line") }; assert!(xline.direction.x.abs() < 1e-7); } // The bulk of this stage's coverage (Horizontal/Parallel/Distance/ // Coincident solving, unrelated edits not triggering a resolve) lives // in `tests/parametric_constraints_solve.rs`, which only needs `pub` API. // This one test needs `record_undo_before`/`take_undo_recording` // (`pub(crate)`), so it stays internal. #[test] fn one_edit_that_ripples_through_a_constraint_still_records_as_one_undo_step() { let mut scene = Scene::new(); let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0), ))); let b = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( Vector3::new(0.0, 5.0, 0.0), Vector3::new(10.0, 5.0, 0.0), ))); let mut set = ParametricConstraintSet::new(ParametricScope::ModelSpace); set.add( ConstraintKind::Parallel, vec![ParametricRef::whole(a), ParametricRef::whole(b)], None, ); scene.parametric_constraints.push(set); scene.begin_undo_recording(); // A real command (Move, grip-drag commit, ...) records its own // before-image before mutating. let before_a = scene.document.get_entity_arc(a); scene.record_undo_before(a, before_a); if let Some(EntityType::Line(l)) = scene.document.get_entity_mut(a) { l.end = Vector3::new(10.0, 6.0, 0.0); } scene.bump_entities(&[(a, super::ChangeKind::Modified)]); let recording = scene .take_undo_recording() .expect("an undo recording should still be open"); let (entities, _objects, _parametric_constraints, _named_parameters) = recording.into_recorded_images(); let touched: std::collections::HashSet<_> = entities.iter().map(|(h, _)| *h).collect(); assert!( touched.contains(&a), "the directly-edited line must be in the undo delta" ); assert!( touched.contains(&b), "the constraint-solved neighbor must ride the same undo delta" ); } /// Erasing an entity records the removed constraints in the same undo /// transaction as the geometry. #[test] fn erasing_a_constrained_entity_records_its_scope_for_undo() { let mut scene = Scene::new(); let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0), ))); let b = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( Vector3::new(10.0, 0.0, 0.0), Vector3::new(20.0, 5.0, 0.0), ))); scene .parametric_constraint_set_mut(ParametricScope::ModelSpace) .add( ConstraintKind::Coincident, vec![ParametricRef::point(a, 1), ParametricRef::point(b, 0)], None, ); assert_eq!( scene .parametric_constraint_set(ParametricScope::ModelSpace) .unwrap() .constraints .len(), 1 ); scene.begin_undo_recording(); scene.erase_entities(&[a]); // Ensure the test exercises restoration rather than a no-op. assert_eq!( scene .parametric_constraint_set(ParametricScope::ModelSpace) .map_or(0, |s| s.constraints.len()), 0, "the constraint touching the erased line should be gone from the live set" ); let recording = scene .take_undo_recording() .expect("an undo recording should still be open"); let (_entities, _objects, parametric_constraints, _named_parameters) = recording.into_recorded_images(); assert_eq!( parametric_constraints.len(), 1, "the touched scope's before-image must be captured" ); let (scope, before) = ¶metric_constraints[0]; assert_eq!(*scope, ParametricScope::ModelSpace); assert_eq!( before.constraints.len(), 1, "the before-image must still hold the constraint as it was before the erase" ); assert_eq!(before.constraints[0].kind, ConstraintKind::Coincident); assert_eq!( before.constraints[0].refs, vec![ParametricRef::point(a, 1), ParametricRef::point(b, 0)] ); // What `apply_delta_state` does with this on undo: install the // before-image back as the live scope state. *scene.parametric_constraint_set_mut(*scope) = before.clone(); assert_eq!( scene .parametric_constraint_set(ParametricScope::ModelSpace) .unwrap() .constraints .len(), 1, "restoring the before-image must bring the constraint back" ); } /// `solve_parametric_constraints_preview` reports the constrained /// neighbor's solved position without writing anything into the /// document itself — the grip-drag caller owns applying it. #[test] fn preview_solve_reports_the_neighbors_new_state_without_mutating_the_document() { let mut scene = Scene::new(); let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0), ))); let b = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( Vector3::new(0.0, 5.0, 0.0), Vector3::new(10.0, 5.0, 0.0), ))); let mut set = ParametricConstraintSet::new(ParametricScope::ModelSpace); set.add( ConstraintKind::Parallel, vec![ParametricRef::whole(a), ParametricRef::whole(b)], None, ); scene.parametric_constraints.push(set); // Simulate a live grip drag: mutate `a` directly (as `apply_grip` // would each frame), without going through `bump_entities` at all. if let Some(EntityType::Line(l)) = scene.document.get_entity_mut(a) { l.end = Vector3::new(10.0, 6.0, 0.0); } let b_before = scene.document.get_entity(b).cloned(); let solved = scene.solve_parametric_constraints_preview(&[a], &[], false, &[]); assert_eq!( scene.document.get_entity(b), b_before.as_ref(), "preview must not mutate the document" ); // The whole scope solves together (every registered entity's params // are free, not just `b`'s — see `register_entity`'s doc comment), // so `a` itself may also have shifted slightly to reach the nearest // mutually-parallel configuration; read whichever position `a` ends // up at from `solved` too, rather than assuming it stayed exactly // where the simulated drag put it. let line_dir = |entity: &EntityType| { let EntityType::Line(l) = entity else { panic!("expected a Line") }; (l.end.x - l.start.x, l.end.y - l.start.y) }; let dir_a = solved .iter() .find(|(h, _)| *h == a) .map(|(_, e)| line_dir(e)) .unwrap_or((10.0, 6.0)); let (_, moved_entity) = solved .iter() .find(|(h, _)| *h == b) .expect("b should be reported as moved"); let dir_b = line_dir(moved_entity); let cross = dir_a.0 * dir_b.1 - dir_a.1 * dir_b.0; assert!( cross.abs() < 1e-6, "the previewed positions should be mutually parallel: dir_a={dir_a:?} dir_b={dir_b:?}" ); } #[test] fn rigid_set_preserves_relative_geometry_after_one_member_moves() { let mut scene = Scene::new(); let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0), ))); let b = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( Vector3::new(0.0, 5.0, 0.0), Vector3::new(10.0, 5.0, 0.0), ))); let mut set = ParametricConstraintSet::new(ParametricScope::ModelSpace); set.add( ConstraintKind::RigidSet, vec![ParametricRef::whole(a), ParametricRef::whole(b)], None, ); set.constraints[0].rigid_points = vec![ (ParametricRef::point(a, 0), Vector3::new(0.0, 0.0, 0.0)), (ParametricRef::point(a, 1), Vector3::new(10.0, 0.0, 0.0)), (ParametricRef::point(b, 0), Vector3::new(0.0, 5.0, 0.0)), (ParametricRef::point(b, 1), Vector3::new(10.0, 5.0, 0.0)), ]; scene.parametric_constraints.push(set); if let Some(EntityType::Line(line)) = scene.document.get_entity_mut(b) { line.start.y += 10.0; line.end.y += 10.0; } scene.bump_entities(&[(b, super::ChangeKind::Modified)]); let endpoints = |handle| { let EntityType::Line(line) = scene.document.get_entity(handle).unwrap() else { panic!("expected a line") }; (line.start, line.end) }; let (a0, a1) = endpoints(a); let (b0, b1) = endpoints(b); let distance = |first: Vector3, second: Vector3| { let delta = second - first; (delta.x * delta.x + delta.y * delta.y).sqrt() }; assert!((distance(a0, a1) - 10.0).abs() < 1.0e-6); assert!((distance(b0, b1) - 10.0).abs() < 1.0e-6); assert!((distance(a0, b0) - 5.0).abs() < 1.0e-6); assert!((distance(a1, b1) - 5.0).abs() < 1.0e-6); } #[test] fn implicit_midpoint_recomputes_with_its_parent_line() { let mut scene = Scene::new(); let carrier = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0), ))); let anchor = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( Vector3::new(7.0, 3.0, 0.0), Vector3::new(7.0, 8.0, 0.0), ))); let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace); set.add( ConstraintKind::Fixed, vec![ParametricRef::whole(anchor)], None, ); set.add( ConstraintKind::Coincident, vec![ ParametricRef::point(carrier, -2), ParametricRef::point(anchor, 0), ], None, ); scene.bump_entities(&[(carrier, super::ChangeKind::Modified)]); let EntityType::Line(line) = scene.document.get_entity(carrier).unwrap() else { panic!("expected line"); }; let midpoint = (line.start + line.end) * 0.5; assert!((midpoint - Vector3::new(7.0, 3.0, 0.0)).length() < 1.0e-7); } #[test] fn spline_endpoint_participates_in_the_kernel_constraint_system() { let mut scene = Scene::new(); let spline = scene.add_entity(EntityType::Spline( acadrust::entities::Spline::from_control_points( 3, vec![ Vector3::new(0.0, 0.0, 0.0), Vector3::new(2.0, 1.0, 0.0), Vector3::new(4.0, 1.0, 0.0), Vector3::new(6.0, 0.0, 0.0), ], ), )); let anchor = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( Vector3::new(9.0, 4.0, 0.0), Vector3::new(9.0, 7.0, 0.0), ))); let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace); set.add( ConstraintKind::Fixed, vec![ParametricRef::whole(anchor)], None, ); set.add( ConstraintKind::Coincident, vec![ ParametricRef::point(spline, 1), ParametricRef::point(anchor, 0), ], None, ); scene.bump_entities(&[(anchor, super::ChangeKind::Modified)]); let EntityType::Spline(spline) = scene.document.get_entity(spline).unwrap() else { panic!("expected spline"); }; let end = spline.control_points.last().copied().unwrap(); assert!((end - Vector3::new(9.0, 4.0, 0.0)).length() < 1.0e-7); } #[test] fn smooth_constraint_recomputes_the_spline_after_its_target_moves() { let mut scene = Scene::new(); let target = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( Vector3::new(0.0, 0.0, 0.0), Vector3::new(5.0, 0.0, 0.0), ))); let mut spline = acadrust::entities::Spline::new(); spline.degree = 3; spline.control_points = vec![ Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 2.0, 0.0), Vector3::new(3.0, 2.0, 0.0), Vector3::new(5.0, 3.0, 0.0), ]; spline.knots = cadkernel::space::clamped_uniform_knots(3, 4); let spline = scene.add_entity(EntityType::Spline(spline)); scene .parametric_constraint_set_mut(ParametricScope::ModelSpace) .add( ConstraintKind::Smooth, vec![ ParametricRef::point(spline, 0), ParametricRef::point(target, 0), ], None, ); if let Some(EntityType::Line(line)) = scene.document.get_entity_mut(target) { line.start.y = 1.0; line.end.y = 1.0; } scene.bump_entities(&[(target, super::ChangeKind::Modified)]); let EntityType::Spline(spline) = scene.document.get_entity(spline).unwrap() else { panic!("expected spline"); }; let curve = crate::entities::spline::nurbs3(spline).unwrap(); let jet = cadkernel::space::CurveJet::from_nurbs(&curve, cadkernel::space::SplineEnd::Start) .unwrap(); assert!((jet.point[0] - 0.0).abs() < 1e-9); assert!((jet.point[1] - 1.0).abs() < 1e-9); assert!(jet.tangent[1].abs() < 1e-9, "jet={jet:?}"); assert!(cadkernel::space::Vec3::from(jet.curvature).length() < 5e-3); } }