Files
cadcraft/crates/engine/src/cmd/constraints.rs
T

1187 lines
49 KiB
Rust

//! Parametric drawing: GEOMCONSTRAINT and the GC* commands, DIMCONSTRAINT and the DC* commands,
//! AUTOCONSTRAIN, DELCONSTRAINT, PARAMETERS, CONSTRAINTBAR, DCDISPLAY, CONSTRAINTSETTINGS,
//! `constraints.inspect` and `constraints.dof`, plus the post-command re-solve hook
//! ([`after_command`]).
//!
//! The solver lives in `cadcraft-constraints`; this module turns picks and JSON parameters into
//! constraint references and keeps the drawing's constraints satisfied after edits.
use std::sync::Arc;
use cadcraft_constraints as cs;
use cadcraft_doc::{Constraint, ConstraintKind as K, DistAxis, Drawing, EntityKind, GeomRef, Handle, Sub};
use cadcraft_geom::{Line, Vec2};
use serde_json::{Value, json};
use super::machines::{SelOutcome, SelectPhase};
use super::*;
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
macro_rules! gc {
($id:literal, $label:literal, $kind:expr, $menu:literal) => {
CommandSpec::new($id, $label, |s, p| run_gc(s, p, $kind))
.menu(&["Tools", "Parametric", "Geometric Constraints", $menu])
.params("{h1, p1?, h2?, p2?, h3?, p3?} (h = handle; p = \"start\"/\"end\"/\"mid\"/\"center\"/\"v<i>\"/\"seg<i>\"/\"object\" or a pick point [x, y])")
.interactive(|_| Ok(Box::new(GcM::new($kind))))
};
}
macro_rules! dc {
($id:literal, $label:literal, $dc:expr, $menu:literal) => {
CommandSpec::new($id, $label, |s, p| run_dc(s, p, $dc))
.menu(&["Tools", "Parametric", "Dimensional Constraints", $menu])
.params("{h1, p1?, h2?, p2?, expr? | value?, name?}")
.interactive(|_| Ok(Box::new(DcM::new(Some($dc)))))
};
}
pub fn specs() -> Vec<CommandSpec> {
vec![
CommandSpec::new("geomconstraint", "Geometric Constraint", run_geomconstraint)
.alias(&["gcon"])
.params("{type: coincident|collinear|concentric|fix|parallel|perpendicular|horizontal|vertical|tangent|smooth|symmetric|equal, h1, p1?, h2?, p2?, h3?, p3?}")
.interactive(|_| Ok(Box::new(GeomM::default()))),
gc!("gccoincident", "Coincident", K::Coincident, "Coincident"),
gc!("gcperpendicular", "Perpendicular", K::Perpendicular, "Perpendicular"),
gc!("gcparallel", "Parallel", K::Parallel, "Parallel"),
gc!("gctangent", "Tangent", K::Tangent, "Tangent"),
gc!("gchorizontal", "Horizontal", K::Horizontal, "Horizontal"),
gc!("gcvertical", "Vertical", K::Vertical, "Vertical"),
gc!("gccollinear", "Collinear", K::Collinear, "Collinear"),
gc!("gcconcentric", "Concentric", K::Concentric, "Concentric"),
gc!("gcsmooth", "Smooth", K::Smooth, "Smooth"),
gc!("gcsymmetric", "Symmetric", K::Symmetric, "Symmetric"),
gc!("gcequal", "Equal", K::Equal, "Equal"),
gc!("gcfix", "Fix", K::Fix, "Fix"),
CommandSpec::new("autoconstrain", "AutoConstrain", run_autoconstrain)
.menu(&["Tools", "Parametric", "AutoConstrain"])
.params("{handles?, tolerance?, angleTolerance? (degrees), types?: [names]}")
.interactive(|_| Ok(Box::new(SelM::new(SelOp::Auto)))),
CommandSpec::new("constraintbar", "Constraint Bars", run_constraintbar)
.menu(&["Tools", "Parametric", "Constraint Bars", "Select Objects"])
.params("{mode?: show|hide|showall|hideall, handles?}")
.interactive(|_| Ok(Box::new(SelM::new(SelOp::Bars)))),
CommandSpec::new("constraintbar.showall", "Show All Constraint Bars", |s, _| set_bars(s, false, "showall", &[]))
.menu(&["Tools", "Parametric", "Constraint Bars", "Show All"]),
CommandSpec::new("constraintbar.hideall", "Hide All Constraint Bars", |s, _| set_bars(s, false, "hideall", &[]))
.menu(&["Tools", "Parametric", "Constraint Bars", "Hide All"]),
CommandSpec::new("dimconstraint", "Dimensional Constraint", run_dimconstraint)
.alias(&["dcon"])
.params("{type: linear|aligned|horizontal|vertical|angular|radius|diameter, h1, p1?, h2?, p2?, expr? | value?, name?}")
.interactive(|_| Ok(Box::new(DcM::new(None)))),
dc!("dcaligned", "Aligned", Dc::Aligned, "Aligned"),
dc!("dchorizontal", "Horizontal", Dc::Horizontal, "Horizontal"),
dc!("dcvertical", "Vertical", Dc::Vertical, "Vertical"),
dc!("dcangular", "Angular", Dc::Angular, "Angular"),
dc!("dcradius", "Radius", Dc::Radius, "Radius"),
dc!("dcdiameter", "Diameter", Dc::Diameter, "Diameter"),
CommandSpec::new("dclinear", "Linear", |s, p| run_dc(s, p, Dc::Linear))
.params("{h1, p1?, h2?, p2?, expr? | value?, name?}")
.interactive(|_| Ok(Box::new(DcM::new(Some(Dc::Linear))))),
CommandSpec::new("dcdisplay", "Dynamic Dimensions", run_dcdisplay)
.menu(&["Tools", "Parametric", "Dynamic Dimensions", "Select Objects"])
.params("{mode?: show|hide|showall|hideall, handles?}")
.interactive(|_| Ok(Box::new(SelM::new(SelOp::Dims)))),
CommandSpec::new("dcdisplay.showall", "Show All Dynamic Dimensions", |s, _| set_bars(s, true, "showall", &[]))
.menu(&["Tools", "Parametric", "Dynamic Dimensions", "Show All"]),
CommandSpec::new("dcdisplay.hideall", "Hide All Dynamic Dimensions", |s, _| set_bars(s, true, "hideall", &[]))
.menu(&["Tools", "Parametric", "Dynamic Dimensions", "Hide All"]),
CommandSpec::new("delconstraint", "Delete Constraints", run_delconstraint)
.menu(&["Tools", "Parametric", "Delete Constraints"])
.params("{handles? | ids?: [n]}")
.interactive(|_| Ok(Box::new(SelM::new(SelOp::Delete)))),
CommandSpec::new("parameters", "Parameters Manager", run_parameters)
.menu(&["Tools", "Parametric", "Parameters Manager"])
.alias(&["par", "parametersclose"])
.params("{} lists; {name, expr | value, description?} sets (new name = user parameter) and re-solves; {delete: name}; {name, rename: newName} renames it in every expression"),
CommandSpec::new("constraintsettings", "Constraint Settings", run_constraintsettings)
.menu(&["Tools", "Parametric", "Constraint Settings"])
.alias(&["csettings"])
.params("{infer?, distanceTolerance?, angleTolerance?, autoTypes?: [names], barsVisible?, barTransparency?, dimsVisible?}"),
CommandSpec::new("constraintinfer", "Infer Constraints", run_constraintinfer)
.params("{value?: 0|1} sets CONSTRAINTINFER (no value: reports it)")
.interactive(|_| Ok(Box::new(InferM))),
CommandSpec::new("constraints.inspect", "Inspect Constraints", run_inspect).noundo().params("{} → constraints, glyph anchors, parameters, settings, dof"),
CommandSpec::new("constraints.dof", "Degrees of Freedom", run_dof)
.noundo()
.params("{handles? | handle?} (default: the selection, else every constrained object) → {dof, status: unconstrained|under|fully|over|unknown, redundant, constraints, objects: [{handle, type, params, dof, status}], message}"),
]
}
fn err(e: cs::SolveError) -> EngineError {
EngineError::Other(e.to_string())
}
fn h_param(p: &Value, k: &str) -> Option<Handle> {
p.get(k).and_then(|v| v.as_str().and_then(Handle::parse_hex).or_else(|| v.as_u64().map(Handle)))
}
// ---------------- references ----------------
/// What a constraint slot needs from the picked object.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Want {
Point,
Line,
/// A line, polyline segment or ellipse (its major axis).
Axis,
Curve,
/// A line, polyline segment, circle, arc or point object.
Object,
/// A characteristic point near the pick, else the object.
PointOrObject,
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Hint {
None,
Sub(Sub),
Pick(Vec2),
}
fn hint_param(cmd: &str, p: &Value, key: &str) -> Result<Hint> {
match p.get(key) {
None | Some(Value::Null) => Ok(Hint::None),
Some(Value::String(t)) => match Sub::parse(t) {
Some(sub) => Ok(Hint::Sub(sub)),
None => point_value(&Value::String(t.clone())).map(Hint::Pick).ok_or_else(|| bad(cmd, format!("`{key}`: unknown point `{t}`"))),
},
Some(v) => point_value(v).map(Hint::Pick).ok_or_else(|| bad(cmd, format!("`{key}` must be a point name or [x, y]"))),
}
}
/// Characteristic points of an object (end points, mid points, centres, vertices).
fn char_points(kind: &EntityKind) -> Vec<(Sub, Vec2)> {
match kind {
EntityKind::Line(l) => vec![(Sub::Start, l.a.xy()), (Sub::End, l.b.xy()), (Sub::Mid, l.a.xy().mid(l.b.xy()))],
EntityKind::Arc(a) => {
let g = cadcraft_geom::Arc::new(a.center.xy(), a.radius, a.start, a.end);
vec![(Sub::Start, g.start_point()), (Sub::End, g.end_point()), (Sub::Mid, g.mid_point()), (Sub::Center, g.center)]
}
EntityKind::Circle(c) => vec![(Sub::Center, c.center.xy())],
EntityKind::Point(p) => vec![(Sub::Whole, p.p.xy())],
EntityKind::LwPolyline(p) => p.vertices.iter().take(100_000).enumerate().map(|(i, v)| (Sub::Vertex(i as u32), v.p)).collect(),
_ => Vec::new(),
}
}
/// Characteristic points of an object, including the solver's points of ellipses (centre, axis
/// ends, arc ends), splines (defining points) and block references (insertion point, the
/// block's points).
fn object_points(d: &Drawing, h: Handle, kind: &EntityKind) -> Vec<(Sub, Vec2)> {
match kind {
EntityKind::Ellipse(_) | EntityKind::Spline(_) | EntityKind::Insert(_) => cs::ref_points(d, h),
_ => char_points(kind),
}
}
/// Line-like parts of an object (a line, or each polyline segment).
fn line_parts(kind: &EntityKind) -> Vec<(Sub, Vec2, Vec2)> {
match kind {
EntityKind::Line(l) => vec![(Sub::Whole, l.a.xy(), l.b.xy())],
EntityKind::LwPolyline(p) => {
let n = p.vertices.len().min(100_000);
let segs = if p.closed && n > 2 { n } else { n.saturating_sub(1) };
(0..segs).filter_map(|i| Some((Sub::Segment(i as u32), p.vertices.get(i)?.p, p.vertices.get((i + 1) % n)?.p))).collect()
}
_ => Vec::new(),
}
}
fn object_size(kind: &EntityKind) -> f64 {
match kind {
EntityKind::Line(l) => l.a.xy().dist(l.b.xy()),
EntityKind::Arc(a) => a.radius,
EntityKind::Circle(c) => c.radius,
EntityKind::Ellipse(e) => e.major.xy().len(),
EntityKind::Spline(s) => s.control.first().zip(s.control.last()).map_or(0.0, |(a, b)| a.dist(*b)),
EntityKind::LwPolyline(p) => p.vertices.windows(2).filter_map(|w| Some(w.first()?.p.dist(w.get(1)?.p))).fold(0.0, f64::max),
_ => 0.0,
}
.abs()
}
fn resolve(d: &Drawing, h: Handle, hint: Hint, want: Want, aperture: f64) -> std::result::Result<GeomRef, String> {
let e = d.entity(h).ok_or_else(|| format!("no such object {}", h.hex()))?;
if !cs::supported(&e.kind) {
return Err(format!("{} objects cannot be constrained", e.kind.type_name()));
}
let k = &e.kind;
let object = |at: Option<Vec2>| -> Option<Sub> {
let parts = line_parts(k);
if !parts.is_empty() {
let best = match at {
Some(p) => parts.iter().min_by(|a, b| Line::new(a.1, a.2).dist(p).total_cmp(&Line::new(b.1, b.2).dist(p))),
None => parts.first(),
};
return best.map(|b| b.0);
}
matches!(
k,
EntityKind::Circle(_)
| EntityKind::Arc(_)
| EntityKind::Point(_)
| EntityKind::Ellipse(_)
| EntityKind::Spline(_)
| EntityKind::Insert(_)
)
.then_some(Sub::Whole)
};
let sub = match (hint, want) {
(Hint::Sub(s), _) => Some(s),
(Hint::None, Want::Point) => object_points(d, h, k).first().map(|c| c.0),
(Hint::None, _) => object(None),
(Hint::Pick(p), Want::Point) => {
object_points(d, h, k).into_iter().filter(|c| c.1.is_finite()).min_by(|a, b| a.1.dist(p).total_cmp(&b.1.dist(p))).map(|c| c.0)
}
(Hint::Pick(p), Want::PointOrObject) => {
let near = object_points(d, h, k).into_iter().filter(|c| c.1.is_finite()).min_by(|a, b| a.1.dist(p).total_cmp(&b.1.dist(p)));
let limit = (aperture * 2.0).max(object_size(k) * 0.15);
match near {
Some((s, q)) if q.dist(p) <= limit => Some(s),
_ => object(Some(p)),
}
}
(Hint::Pick(p), _) => object(Some(p)),
};
let sub = sub.ok_or_else(|| format!("cannot use this {} here", k.type_name()))?;
// Shape checks for clearer messages.
let ok = match want {
Want::Line => !line_parts(k).is_empty() && matches!(sub, Sub::Whole | Sub::Segment(_)),
Want::Axis => {
(!line_parts(k).is_empty() && matches!(sub, Sub::Whole | Sub::Segment(_))) || (matches!(k, EntityKind::Ellipse(_)) && sub == Sub::Whole)
}
Want::Curve => matches!(k, EntityKind::Circle(_) | EntityKind::Arc(_) | EntityKind::Ellipse(_)),
_ => true,
};
if !ok {
return Err(match want {
Want::Line => "Select a line or polyline segment".into(),
Want::Axis => "Select a line, polyline segment or ellipse".into(),
_ => "Select an arc, circle or ellipse".into(),
});
}
Ok(GeomRef::new(h, sub))
}
/// The closest pair of characteristic points of two objects (COINCIDENT with no hints).
fn closest_points(d: &Drawing, a: Handle, b: Handle) -> Option<(GeomRef, GeomRef)> {
let (ea, eb) = (d.entity(a)?, d.entity(b)?);
let pa: Vec<_> = object_points(d, a, &ea.kind).into_iter().filter(|c| c.0 != Sub::Mid).collect();
let pb: Vec<_> = object_points(d, b, &eb.kind).into_iter().filter(|c| c.0 != Sub::Mid).collect();
let mut best: Option<(f64, Sub, Sub)> = None;
for (sa, qa) in pa.iter().take(1000) {
for (sb, qb) in pb.iter().take(1000) {
let dd = qa.dist(*qb);
if dd.is_finite() && best.is_none_or(|b| dd < b.0) {
best = Some((dd, *sa, *sb));
}
}
}
best.map(|(_, sa, sb)| (GeomRef::new(a, sa), GeomRef::new(b, sb)))
}
fn wants(kind: K, n: usize) -> Vec<Want> {
match kind {
K::Coincident => vec![Want::PointOrObject; 2],
K::Horizontal | K::Vertical => {
if n >= 2 {
vec![Want::Point; 2]
} else {
vec![Want::Axis]
}
}
K::Parallel | K::Perpendicular | K::Collinear => vec![Want::Line; 2],
K::Concentric => vec![Want::Curve; 2],
K::Symmetric => vec![Want::Object, Want::Object, Want::Line],
K::Fix => vec![Want::PointOrObject],
_ => vec![Want::Object; 2],
}
}
fn aperture(s: &Session) -> f64 {
s.pixel_size() * s.settings.pickbox.max(1.0) * 1.5
}
fn build_refs(s: &Session, kind: K, picks: &[(Handle, Hint)]) -> std::result::Result<Vec<GeomRef>, String> {
let d = s.doc().map_err(|e| e.to_string())?;
let want = wants(kind, picks.len());
if picks.len() < want.len() {
return Err(format!("{} needs {} object(s)", kind.name(), want.len()));
}
if kind == K::Coincident
&& let [(a, Hint::None), (b, Hint::None)] = picks
{
return closest_points(d, *a, *b).map(|(x, y)| vec![x, y]).ok_or_else(|| "Select two points".into());
}
let ap = aperture(s);
let mut refs: Vec<GeomRef> = Vec::new();
for ((h, hint), w) in picks.iter().zip(want) {
refs.push(resolve(d, *h, *hint, w, ap)?);
}
Ok(refs)
}
/// Add a constraint to the active drawing; the drawing changes only on success.
fn add_constraint(s: &mut Session, c: Constraint) -> Result<u32> {
let keep: Vec<Handle> = c.refs.first().map(|r| r.handle).into_iter().collect();
let prefer_move: Vec<Handle> = c.refs.iter().skip(1).map(|r| r.handle).filter(|h| !keep.contains(h)).collect();
let mut d = s.doc()?.clone();
let id = cs::add(&mut d, c, &cs::SolveOptions { keep, prefer_move }).map_err(err)?;
*s.doc_mut()? = d;
Ok(id)
}
fn constraint_json(d: &Drawing, id: u32) -> Value {
match d.constraints.iter().find(|c| c.id == id) {
Some(c) => {
let mut v = json!({ "id": id, "type": c.kind.name(), "refs": c.refs.iter().map(|r| json!({"handle": r.handle.hex(), "sub": r.sub.label()})).collect::<Vec<_>>() });
if c.kind.is_dimensional() {
v["name"] = json!(c.name);
v["expr"] = json!(c.expr);
v["value"] = json!(cs::constraint_value(&mut cs::env(d), c).ok());
}
v
}
None => json!({ "id": id }),
}
}
// ---------------- geometric constraints ----------------
fn kind_from_name(t: &str) -> Option<K> {
Some(match t.trim().trim_start_matches("gc").to_ascii_lowercase().as_str() {
"coincident" => K::Coincident,
"collinear" => K::Collinear,
"concentric" => K::Concentric,
"fix" => K::Fix,
"parallel" => K::Parallel,
"perpendicular" => K::Perpendicular,
"horizontal" => K::Horizontal,
"vertical" => K::Vertical,
"tangent" => K::Tangent,
"smooth" => K::Smooth,
"symmetric" => K::Symmetric,
"equal" => K::Equal,
_ => return None,
})
}
fn run_geomconstraint(s: &mut Session, p: &Value) -> Result<Value> {
let t = str_param(p, "type").ok_or_else(|| bad("geomconstraint", "`type` is required"))?;
let kind = kind_from_name(t).ok_or_else(|| bad("geomconstraint", format!("unknown constraint type `{t}`")))?;
run_gc(s, p, kind)
}
fn run_gc(s: &mut Session, p: &Value, kind: K) -> Result<Value> {
let cmd = "geomconstraint";
let mut picks = Vec::new();
for (hk, pk) in [("h1", "p1"), ("h2", "p2"), ("h3", "p3")] {
let hint = hint_param(cmd, p, pk)?;
match h_param(p, hk) {
Some(h) => picks.push((h, hint)),
// Two points on the same object: {h1, p1, p2}.
None if hint != Hint::None => {
if let Some(first) = picks.first().map(|x: &(Handle, Hint)| x.0) {
picks.push((first, hint));
}
}
None => {}
}
}
if picks.is_empty() {
return Err(bad(cmd, "`h1` is required"));
}
let refs = build_refs(s, kind, &picks).map_err(|m| bad(cmd, m))?;
let id = add_constraint(s, Constraint { id: 0, kind, refs, name: String::new(), expr: String::new() })?;
Ok(constraint_json(s.doc()?, id))
}
struct GcM {
kind: K,
picks: Vec<(Handle, Vec2)>,
two_points: bool,
}
impl GcM {
fn new(kind: K) -> Self {
GcM { kind, picks: Vec::new(), two_points: false }
}
fn needed(&self) -> usize {
match self.kind {
K::Horizontal | K::Vertical => {
if self.two_points {
2
} else {
1
}
}
K::Fix => 1,
K::Symmetric => 3,
_ => 2,
}
}
fn finish(&mut self, s: &mut Session) -> Result<Step> {
let picks: Vec<(Handle, Hint)> = self.picks.iter().map(|(h, p)| (*h, Hint::Pick(*p))).collect();
let r = build_refs(s, self.kind, &picks)
.map_err(EngineError::Other)
.and_then(|refs| add_constraint(s, Constraint { id: 0, kind: self.kind, refs, name: String::new(), expr: String::new() }));
s.set_selection(Vec::new());
match r {
Ok(id) => s.echo(format!("{} constraint #{id} applied.", self.kind.name())),
Err(e) => s.echo(e.to_string()),
}
Ok(Step::Done)
}
}
impl Interactive for GcM {
fn name(&self) -> &'static str {
"GEOMCONSTRAINT"
}
fn prompt(&self, _s: &Session) -> Prompt {
let i = self.picks.len();
let point_wise = self.kind == K::Coincident || self.two_points;
match (self.kind, i) {
(K::Horizontal | K::Vertical, 0) if !self.two_points => Prompt::new("Select an object", Accept::POINT).kw(&["2Points"]),
(K::Fix, _) => Prompt::new("Select point or object", Accept::POINT),
(K::Symmetric, 2) => Prompt::new("Select symmetry line", Accept::POINT),
(_, 0) if point_wise => Prompt::new("Select first point", Accept::POINT),
(_, _) if point_wise => Prompt::new("Select second point", Accept::POINT),
(_, 0) => Prompt::new("Select first object", Accept::POINT),
_ => Prompt::new("Select second object", Accept::POINT),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
match i {
Input::Keyword(k) if k == "2Points" => {
self.two_points = true;
Ok(Step::Continue)
}
Input::Point(p) => {
let space = s.space();
let Some(h) = crate::select::pick(s.doc()?, &space, p, aperture(s)) else {
s.echo("*Invalid selection*");
return Ok(Step::Continue);
};
self.picks.push((h, p));
s.set_selection(self.picks.iter().map(|x| x.0).collect());
if self.picks.len() >= self.needed() { self.finish(s) } else { Ok(Step::Continue) }
}
Input::Enter => Ok(Step::Done),
_ => Ok(Step::Continue),
}
}
}
/// GEOMCONSTRAINT: ask for the type, then run that constraint's prompts.
#[derive(Default)]
struct GeomM {
inner: Option<GcM>,
}
const GEOM_KW: &[&str] = &[
"Horizontal",
"Vertical",
"Perpendicular",
"PArallel",
"Tangent",
"SMooth",
"Coincident",
"CONcentric",
"COLlinear",
"Symmetric",
"Equal",
"Fix",
];
impl Interactive for GeomM {
fn name(&self) -> &'static str {
"GEOMCONSTRAINT"
}
fn prompt(&self, s: &Session) -> Prompt {
match &self.inner {
Some(m) => m.prompt(s),
None => Prompt::new("Enter constraint type", Accept::TEXT).kw(GEOM_KW).default("Horizontal"),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
if let Some(m) = &mut self.inner {
return m.input(s, i);
}
let name = match i {
Input::Keyword(k) | Input::Text(k) => k,
Input::Enter => "Horizontal".into(),
_ => return Ok(Step::Continue),
};
let kind = kind_from_name(&name).ok_or_else(|| EngineError::Other("Invalid option keyword.".into()))?;
self.inner = Some(GcM::new(kind));
Ok(Step::Continue)
}
}
// ---------------- dimensional constraints ----------------
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Dc {
Linear,
Aligned,
Horizontal,
Vertical,
Angular,
Radius,
Diameter,
}
fn dc_from_name(t: &str) -> Option<Dc> {
Some(match t.trim().trim_start_matches("dc").to_ascii_lowercase().as_str() {
"linear" => Dc::Linear,
"aligned" | "distance" => Dc::Aligned,
"horizontal" => Dc::Horizontal,
"vertical" => Dc::Vertical,
"angular" | "angle" => Dc::Angular,
"radius" | "radial" => Dc::Radius,
"diameter" => Dc::Diameter,
_ => return None,
})
}
/// The constraint kind for a DC type; LINEAR picks horizontal or vertical from the geometry.
fn dc_kind(d: &Drawing, dc: Dc, refs: &[GeomRef]) -> K {
match dc {
Dc::Aligned => K::Distance(DistAxis::Aligned),
Dc::Horizontal => K::Distance(DistAxis::Horizontal),
Dc::Vertical => K::Distance(DistAxis::Vertical),
Dc::Angular => K::Angular,
Dc::Radius => K::Radius,
Dc::Diameter => K::Diameter,
Dc::Linear => {
let h = cs::measure(d, K::Distance(DistAxis::Horizontal), refs).unwrap_or(0.0);
let v = cs::measure(d, K::Distance(DistAxis::Vertical), refs).unwrap_or(0.0);
K::Distance(if h >= v { DistAxis::Horizontal } else { DistAxis::Vertical })
}
}
}
fn run_dimconstraint(s: &mut Session, p: &Value) -> Result<Value> {
let t = str_param(p, "type").unwrap_or("aligned");
let dc = dc_from_name(t).ok_or_else(|| bad("dimconstraint", format!("unknown type `{t}`")))?;
run_dc(s, p, dc)
}
fn run_dc(s: &mut Session, p: &Value, dc: Dc) -> Result<Value> {
let cmd = "dimconstraint";
let h1 = h_param(p, "h1").ok_or_else(|| bad(cmd, "`h1` is required"))?;
let (p1, p2) = (hint_param(cmd, p, "p1")?, hint_param(cmd, p, "p2")?);
let h2 = h_param(p, "h2");
let d = s.doc()?;
let ap = aperture(s);
let r = |h, hint, w| resolve(d, h, hint, w, ap).map_err(|m| bad(cmd, m));
let refs = match dc {
Dc::Linear | Dc::Aligned | Dc::Horizontal | Dc::Vertical => {
if h2.is_some() || p2 != Hint::None {
// `p2: "object"` on a line: point-to-line distance (aligned only).
let second_whole = matches!(p2, Hint::Sub(Sub::Whole | Sub::Segment(_)));
let w2 = if second_whole && dc == Dc::Aligned { Want::Line } else { Want::Point };
// Two points of one object default to its start and end.
let p2 = if p2 == Hint::None && h2.is_none_or(|h| h == h1) { Hint::Sub(Sub::End) } else { p2 };
vec![r(h1, p1, Want::Point)?, r(h2.unwrap_or(h1), p2, w2)?]
} else {
vec![r(h1, p1, Want::Line)?]
}
}
Dc::Angular => match h2 {
Some(h2) => vec![r(h1, p1, Want::Line)?, r(h2, p2, Want::Line)?],
None => vec![r(h1, p1, Want::Curve)?],
},
Dc::Radius | Dc::Diameter => vec![r(h1, p1, Want::Curve)?],
};
let kind = dc_kind(d, dc, &refs);
let expr = match (str_param(p, "expr"), p.get("value").and_then(Value::as_f64)) {
(Some(e), _) => e.trim().to_string(),
(None, Some(v)) if v.is_finite() => cs::format_value(v),
_ => String::new(),
};
let name = str_param(p, "name").unwrap_or("").trim().to_string();
let id = add_constraint(s, Constraint { id: 0, kind, refs, name, expr })?;
Ok(constraint_json(s.doc()?, id))
}
struct DcM {
dc: Option<Dc>,
refs: Vec<GeomRef>,
points: bool,
/// Picking done: waiting for the value.
value: Option<(K, f64)>,
}
impl DcM {
fn new(dc: Option<Dc>) -> Self {
DcM { dc, refs: Vec::new(), points: false, value: None }
}
fn picked(&mut self, s: &mut Session) -> Result<Step> {
let d = s.doc()?;
let Some(dc) = self.dc else { return Ok(Step::Continue) };
let kind = dc_kind(d, dc, &self.refs);
match cs::measure(d, kind, &self.refs) {
Ok(v) => {
self.value = Some((kind, v));
Ok(Step::Continue)
}
Err(m) => {
self.refs.clear();
Err(EngineError::Other(m))
}
}
}
fn commit(&mut self, s: &mut Session, text: Option<&str>) -> Result<Step> {
let Some((kind, v)) = self.value else { return Ok(Step::Continue) };
let (name, expr) = match text.map(str::trim).filter(|t| !t.is_empty()) {
Some(t) => match t.split_once('=') {
Some((n, e)) => (n.trim().to_string(), e.trim().to_string()),
None => (String::new(), t.to_string()),
},
None => (String::new(), cs::format_value(v)),
};
let c = Constraint { id: 0, kind, refs: std::mem::take(&mut self.refs), name, expr };
match add_constraint(s, c) {
Ok(id) => {
let d = s.doc()?;
if let Some(c) = d.constraints.iter().find(|c| c.id == id) {
let msg = format!("{} = {}", c.name, c.expr);
s.echo(msg);
}
}
Err(e) => s.echo(e.to_string()),
}
s.set_selection(Vec::new());
Ok(Step::Done)
}
}
const DC_KW: &[&str] = &["LInear", "Horizontal", "Vertical", "ALigned", "ANgular", "Radius", "Diameter"];
impl Interactive for DcM {
fn name(&self) -> &'static str {
"DIMCONSTRAINT"
}
fn prompt(&self, s: &Session) -> Prompt {
if let Some((_, v)) = self.value {
return Prompt::new("Enter dimension value or [name=expression]", Accept::TEXT).default(cs::format_value(v));
}
let _ = s;
match self.dc {
None => Prompt::new("Select a dimension constraint type", Accept::TEXT).kw(DC_KW).default("ALigned"),
Some(Dc::Radius | Dc::Diameter) => Prompt::new("Select arc or circle", Accept::POINT),
Some(Dc::Angular) if self.refs.is_empty() => Prompt::new("Select first line or arc", Accept::POINT),
Some(Dc::Angular) => Prompt::new("Select second line", Accept::POINT),
Some(_) if self.points && self.refs.is_empty() => Prompt::new("Specify first constraint point", Accept::POINT),
Some(_) if self.points => Prompt::new("Specify second constraint point", Accept::POINT),
Some(_) => Prompt::new("Select object", Accept::POINT).kw(&["Points"]),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
if self.value.is_some() {
return match i {
Input::Text(t) | Input::Keyword(t) => self.commit(s, Some(&t)),
Input::Enter => self.commit(s, None),
_ => Ok(Step::Continue),
};
}
let Some(dc) = self.dc else {
let name = match i {
Input::Keyword(k) | Input::Text(k) => k,
Input::Enter => "aligned".into(),
_ => return Ok(Step::Continue),
};
self.dc = Some(dc_from_name(&name).ok_or_else(|| EngineError::Other("Invalid option keyword.".into()))?);
return Ok(Step::Continue);
};
match i {
Input::Keyword(k) if k == "Points" => {
self.points = true;
Ok(Step::Continue)
}
Input::Point(p) => {
let space = s.space();
let ap = aperture(s);
let d = s.doc()?;
let Some(h) = crate::select::pick(d, &space, p, ap) else {
s.echo("*Invalid selection*");
return Ok(Step::Continue);
};
let want = match dc {
Dc::Radius | Dc::Diameter => Want::Curve,
Dc::Angular if self.refs.is_empty() => {
if matches!(d.entity(h).map(|e| &e.kind), Some(EntityKind::Arc(_))) {
Want::Curve
} else {
Want::Line
}
}
Dc::Angular => Want::Line,
_ if self.points => Want::Point,
_ => Want::Line,
};
let r = resolve(d, h, Hint::Pick(p), want, ap).map_err(EngineError::Other)?;
self.refs.push(r);
let done = match dc {
Dc::Radius | Dc::Diameter => true,
Dc::Angular => want == Want::Curve || self.refs.len() >= 2,
_ => !self.points || self.refs.len() >= 2,
};
if done { self.picked(s) } else { Ok(Step::Continue) }
}
Input::Enter => Ok(Step::Done),
_ => Ok(Step::Continue),
}
}
}
// ---------------- selection-based commands ----------------
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SelOp {
Auto,
Delete,
Bars,
Dims,
}
struct SelM {
op: SelOp,
phase: SelectPhase,
}
impl SelM {
fn new(op: SelOp) -> Self {
SelM { op, phase: SelectPhase::default() }
}
fn apply(&self, s: &mut Session, hs: Vec<Handle>) -> Result<Step> {
let handles = json!(hs.iter().map(|h| h.hex()).collect::<Vec<_>>());
let r = match self.op {
SelOp::Auto => run_autoconstrain(s, &json!({ "handles": handles })),
SelOp::Delete => run_delconstraint(s, &json!({ "handles": handles })),
SelOp::Bars => set_bars(s, false, "show", &hs),
SelOp::Dims => set_bars(s, true, "show", &hs),
};
match r {
Ok(v) => {
if let Some(m) = v.get("message").and_then(Value::as_str) {
s.echo(m.to_string());
}
}
Err(e) => s.echo(e.to_string()),
}
s.set_selection(Vec::new());
Ok(Step::Done)
}
}
impl Interactive for SelM {
fn name(&self) -> &'static str {
match self.op {
SelOp::Auto => "AUTOCONSTRAIN",
SelOp::Delete => "DELCONSTRAINT",
SelOp::Bars => "CONSTRAINTBAR",
SelOp::Dims => "DCDISPLAY",
}
}
fn begin(&mut self, s: &mut Session) -> Result<Step> {
self.phase = SelectPhase::begin(s);
if self.phase.done {
let hs = self.phase.picked.clone();
return self.apply(s, hs);
}
Ok(Step::Continue)
}
fn prompt(&self, _s: &Session) -> Prompt {
self.phase.prompt()
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
match self.phase.feed(s, &i)? {
SelOutcome::More => Ok(Step::Continue),
SelOutcome::Empty => Ok(Step::Done),
SelOutcome::Done(hs) => self.apply(s, hs),
}
}
}
fn run_autoconstrain(s: &mut Session, p: &Value) -> Result<Value> {
let hs = targets(s, p)?;
if hs.is_empty() {
return Err(bad("autoconstrain", "select objects (or pass `handles`)"));
}
let set = &s.doc()?.parametric.settings;
let mut o = cs::InferOptions { distance_tolerance: set.distance_tolerance, angle_tolerance: set.angle_tolerance, types: set.auto_types.clone() };
o.distance_tolerance = f64_or(p, "tolerance", o.distance_tolerance);
o.angle_tolerance = f64_or(p, "angleTolerance", o.angle_tolerance);
if let Some(t) = p.get("types").and_then(Value::as_array) {
o.types = t.iter().filter_map(Value::as_str).map(|n| kind_from_name(n).map_or_else(|| n.to_string(), |k| k.name().to_string())).collect();
}
let mut d = s.doc()?.clone();
let ids = cs::autoconstrain(&mut d, &hs, &o);
if !ids.is_empty() {
*s.doc_mut()? = d;
}
Ok(json!({ "added": ids.len(), "ids": ids, "message": format!("{} constraint(s) applied to {} object(s)", ids.len(), hs.len()) }))
}
fn run_delconstraint(s: &mut Session, p: &Value) -> Result<Value> {
let ids: Option<Vec<u32>> =
p.get("ids").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).filter_map(|v| u32::try_from(v).ok()).collect());
let hs = targets(s, p)?;
if ids.is_none() && hs.is_empty() {
return Err(bad("delconstraint", "select objects (or pass `handles` / `ids`)"));
}
let doomed = |c: &Constraint| match &ids {
Some(ids) => ids.contains(&c.id),
None => c.refs.iter().any(|r| hs.contains(&r.handle)),
};
let n = s.doc()?.constraints.iter().filter(|c| doomed(c)).count();
if n > 0 {
let mut d = s.doc()?.clone();
let broken_before: Vec<String> = cs::parameter_table(&d).into_iter().filter(|r| r.error.is_some()).map(|r| r.name).collect();
let removed: Vec<String> = d.constraints.iter().filter(|c| doomed(c)).map(|c| c.name.clone()).collect();
d.constraints.retain(|c| !doomed(c));
// Reject when a surviving expression would lose a dimension it refers to (same guard as `parameters delete`).
if let Some(row) = cs::parameter_table(&d).into_iter().find(|r| r.error.is_some() && !broken_before.contains(&r.name)) {
let mut refs = Vec::new();
if let Ok(e) = cs::expr::parse(&row.expr) {
cs::expr::names(&e, &mut refs);
}
let used = removed.iter().find(|n| refs.contains(*n)).or(removed.first()).cloned().unwrap_or_default();
return Err(EngineError::Other(format!("`{used}` is used by `{}`", row.name)));
}
*s.doc_mut()? = d;
}
Ok(json!({ "removed": n, "message": format!("{n} constraint(s) removed") }))
}
fn run_parameters(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(name) = str_param(p, "delete") {
let d = s.doc()?;
if d.constraints.iter().any(|c| c.name == name) {
return Err(EngineError::Other(format!("`{name}` belongs to a dimensional constraint; delete the constraint instead")));
}
if !d.parametric.parameters.iter().any(|q| q.name == name) {
return Err(EngineError::Other(format!("no parameter `{name}`")));
}
let mut d = d.clone();
d.parametric.parameters.retain(|q| q.name != name);
if let Some(row) = cs::parameter_table(&d).into_iter().find(|r| r.error.is_some()) {
return Err(EngineError::Other(format!("`{name}` is used by `{}`", row.name)));
}
*s.doc_mut()? = d;
} else if let (Some(old), Some(to)) = (str_param(p, "name"), str_param(p, "rename")) {
let mut d = s.doc()?.clone();
cs::rename_parameter(&mut d, old.trim(), to.trim()).map_err(err)?;
*s.doc_mut()? = d;
} else if let Some(name) = str_param(p, "name") {
let expr = match (str_param(p, "expr"), p.get("value").and_then(Value::as_f64)) {
(Some(e), _) => e.to_string(),
(None, Some(v)) if v.is_finite() => cs::format_value(v),
_ => return Err(bad("parameters", "`expr` or `value` is required")),
};
let mut d = s.doc()?.clone();
cs::set_parameter(&mut d, name.trim(), &expr, &cs::SolveOptions::default()).map_err(err)?;
if let Some(desc) = str_param(p, "description")
&& let Some(q) = d.parametric.parameters.iter_mut().find(|q| q.name == name.trim())
{
q.description = desc.to_string();
}
*s.doc_mut()? = d;
}
let rows = cs::parameter_table(s.doc()?);
let text: Vec<String> =
rows.iter().map(|r| format!("{} = {} ({})", r.name, r.expr, r.value.map(cs::format_value).unwrap_or_else(|| "error".into()))).collect();
let message = if text.is_empty() { "No parameters.".to_string() } else { text.join("; ") };
Ok(json!({ "parameters": rows, "message": message }))
}
/// Show/hide constraint bars (`dims` = false) or dynamic dimensional constraints.
fn set_bars(s: &mut Session, dims: bool, mode: &str, hs: &[Handle]) -> Result<Value> {
let d = s.doc_mut()?;
let set = &mut d.parametric.settings;
let (visible, exceptions) =
if dims { (&mut set.dims_visible, &mut set.dim_exceptions) } else { (&mut set.bars_visible, &mut set.bar_exceptions) };
match mode {
"showall" => {
*visible = true;
exceptions.clear();
}
"hideall" => {
*visible = false;
exceptions.clear();
}
"show" | "hide" => {
// Exceptions are hidden objects when visible, shown objects when hidden.
let add = (mode == "hide") == *visible;
for h in hs {
exceptions.retain(|x| x != h);
if add {
exceptions.push(*h);
}
}
exceptions.truncate(100_000);
}
other => return Err(bad(if dims { "dcdisplay" } else { "constraintbar" }, format!("unknown mode `{other}`"))),
}
let what = if dims { "Dynamic constraints" } else { "Constraint bars" };
Ok(json!({ "visible": *visible, "exceptions": exceptions.iter().map(|h| h.hex()).collect::<Vec<_>>(), "message": format!("{what}: {mode}") }))
}
fn bar_cmd(s: &mut Session, p: &Value, dims: bool) -> Result<Value> {
let mode = str_param(p, "mode").unwrap_or("show").to_ascii_lowercase();
let hs = targets(s, p)?;
if matches!(mode.as_str(), "show" | "hide") && hs.is_empty() {
return Err(bad(if dims { "dcdisplay" } else { "constraintbar" }, "select objects (or pass `handles`)"));
}
set_bars(s, dims, &mode, &hs)
}
fn run_constraintbar(s: &mut Session, p: &Value) -> Result<Value> {
bar_cmd(s, p, false)
}
fn run_dcdisplay(s: &mut Session, p: &Value) -> Result<Value> {
bar_cmd(s, p, true)
}
fn run_constraintsettings(s: &mut Session, p: &Value) -> Result<Value> {
let cur = s.doc()?.parametric.settings.clone();
let mut set = cur.clone();
set.infer = bool_or(p, "infer", set.infer);
set.distance_tolerance = f64_or(p, "distanceTolerance", set.distance_tolerance).clamp(0.0, 1e6);
set.angle_tolerance = f64_or(p, "angleTolerance", set.angle_tolerance).clamp(0.0, 45.0);
set.bars_visible = bool_or(p, "barsVisible", set.bars_visible);
set.dims_visible = bool_or(p, "dimsVisible", set.dims_visible);
// The checkboxes act like Show All / Hide All: exceptions are relative to the old value and would flip.
if set.bars_visible != cur.bars_visible {
set.bar_exceptions.clear();
}
if set.dims_visible != cur.dims_visible {
set.dim_exceptions.clear();
}
if let Some(t) = p.get("barTransparency").and_then(Value::as_f64).filter(|v| v.is_finite()) {
set.bar_transparency = t.clamp(0.0, 90.0) as u8;
}
if let Some(t) = p.get("autoTypes").and_then(Value::as_array) {
set.auto_types = t.iter().filter_map(Value::as_str).filter_map(|n| kind_from_name(n).map(|k| k.name().to_string())).take(32).collect();
}
if set != cur {
s.doc_mut()?.parametric.settings = set.clone();
}
serde_json::to_value(&set).map_err(|e| EngineError::Other(e.to_string()))
}
/// CONSTRAINTINFER typed as a command: report or set the variable.
fn run_constraintinfer(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(v) = p.get("value").filter(|v| !v.is_null()) {
let on =
v.as_bool().or_else(|| v.as_f64().filter(|f| *f == 0.0 || *f == 1.0).map(|f| f == 1.0)).or_else(|| match v.as_str().map(str::trim) {
Some("0") => Some(false),
Some("1") => Some(true),
_ => None,
});
let on = on.ok_or_else(|| bad("constraintinfer", "`value` must be 0 or 1"))?;
if s.doc()?.parametric.settings.infer != on {
s.doc_mut()?.parametric.settings.infer = on;
}
}
let on = s.doc()?.parametric.settings.infer;
Ok(json!({ "value": i32::from(on), "message": format!("CONSTRAINTINFER = {}", i32::from(on)) }))
}
/// `Enter new value for CONSTRAINTINFER <0>:`
struct InferM;
impl Interactive for InferM {
fn name(&self) -> &'static str {
"CONSTRAINTINFER"
}
fn prompt(&self, s: &Session) -> Prompt {
let cur = s.doc().map(|d| i32::from(d.parametric.settings.infer)).unwrap_or(0);
Prompt::new("Enter new value for CONSTRAINTINFER", Accept { number: true, ..Accept::TEXT }).default(cur.to_string())
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
let text = match i {
Input::Enter | Input::Cancel => return Ok(Step::Done),
Input::Text(t) | Input::Keyword(t) => t,
_ => return Ok(Step::Continue),
};
match run_constraintinfer(s, &json!({ "value": text.trim() })) {
Ok(_) => Ok(Step::Done),
Err(_) => {
s.echo("Requires an integer between 0 and 1.");
Ok(Step::Continue)
}
}
}
}
fn run_inspect(s: &mut Session, _p: &Value) -> Result<Value> {
let d = s.doc()?;
let dof = cs::dof(d, &[]).ok().map(|r| json!({ "dof": r.dof, "status": r.status, "redundant": r.redundant, "message": r.message }));
Ok(json!({
"constraints": d.constraints,
"glyphs": cs::glyphs(d),
"parameters": cs::parameter_table(d),
"settings": d.parametric.settings,
"satisfied": cs::all_satisfied(d),
"dof": dof,
}))
}
/// `constraints.dof`: remaining degrees of freedom of the given objects (default: the selection),
/// or of every constrained object.
fn run_dof(s: &mut Session, p: &Value) -> Result<Value> {
let hs = targets(s, p)?;
let r = cs::dof(s.doc()?, &hs).map_err(err)?;
serde_json::to_value(&r).map_err(|e| EngineError::Other(e.to_string()))
}
// ---------------- re-solve after edits ----------------
/// CONSTRAINTINFER: most objects one command may place for inference to run.
const MAX_INFER_PLACED: usize = 50;
/// CONSTRAINTINFER: most objects scanned for what the placed ones touch.
const MAX_INFER_SCAN: usize = 50_000;
/// CONSTRAINTINFER: after a command drew or edited objects, constrain what it placed exactly onto
/// existing geometry. Snapped points are exact, so tight tolerances find what the object snaps
/// (running snaps, overrides, tracking) and ortho/polar established, whatever way the point came
/// in. Commands that changed the constraints themselves are left alone.
fn infer_after(s: &mut Session, before: &Drawing) {
let space = s.space();
let Ok(d) = s.doc() else { return };
let set = &d.parametric.settings;
if !set.infer || d.constraints != before.constraints {
return;
}
let Some(store) = d.space(&space) else { return };
let changed = |e: &Arc<cadcraft_doc::Entity>| before.entity(e.handle).is_none_or(|b| !Arc::ptr_eq(b, e) && **b != **e);
let placed: Vec<Handle> = store.iter().filter(|e| cs::supported(&e.kind) && changed(e)).map(|e| e.handle).take(MAX_INFER_PLACED + 1).collect();
if placed.is_empty() || placed.len() > MAX_INFER_PLACED {
return;
}
let types: Vec<String> = cs::INFER_TYPES
.iter()
.map(|k| k.name())
.filter(|n| set.auto_types.is_empty() || set.auto_types.iter().any(|t| t.eq_ignore_ascii_case(n)))
.map(str::to_string)
.collect();
if types.is_empty() {
return;
}
// The points the placed objects reach, and how big they are (for the tolerance).
let mut pts: Vec<Vec2> = Vec::new();
let mut dirs: Vec<Vec2> = Vec::new();
for h in &placed {
let Some(e) = d.entity(*h) else { continue };
pts.extend(object_points(d, *h, &e.kind).into_iter().map(|(_, p)| p).filter(|p| p.is_finite()).take(1000));
if let EntityKind::Line(l) = &e.kind {
let v = (l.b.xy() - l.a.xy()).normalized();
if v.x.abs() > 1e-9 && v.y.abs() > 1e-9 {
dirs.push(v);
}
}
}
let scale = pts.iter().fold(1.0f64, |m, p| m.max(p.x.abs()).max(p.y.abs()));
let tol = 1e-9 * scale;
// Others: what the placed points touch, and the nearest line exactly parallel to a sloped
// placed line (a parallel snap).
let mut others: Vec<Handle> = Vec::new();
let mut parallel: Option<(f64, Handle)> = None;
let near = pts.first().copied().unwrap_or_default();
for e in store.iter().take(MAX_INFER_SCAN) {
if placed.contains(&e.handle) || !cs::supported(&e.kind) || !d.is_visible(e) {
continue;
}
let b = cadcraft_doc::entity_bounds(d, e, 0).expand(tol);
if pts.iter().any(|p| b.contains(*p)) {
if others.len() < 150 {
others.push(e.handle);
}
} else if let EntityKind::Line(l) = &e.kind
&& dirs.iter().any(|v| v.cross((l.b.xy() - l.a.xy()).normalized()).abs() <= 1e-9)
{
let dist = Line::new(l.a.xy(), l.b.xy()).dist(near);
if dist.is_finite() && parallel.is_none_or(|p| dist < p.0) {
parallel = Some((dist, e.handle));
}
}
}
others.extend(parallel.map(|p| p.1));
let o = cs::InferOptions { distance_tolerance: tol, angle_tolerance: 1e-7, types };
let cands = cs::infer_placed(d, &placed, &others, &o);
if cands.is_empty() {
return;
}
let mut nd = d.clone();
let opts = cs::SolveOptions { keep: others, prefer_move: placed };
let added = cands.into_iter().take(200).filter(|c| cs::add(&mut nd, c.clone(), &opts).is_ok()).count();
if added > 0
&& let Ok(st) = s.state_mut()
{
st.doc = Arc::new(nd);
}
}
/// Called after every command (programmatic or interactive). With CONSTRAINTINFER on, first
/// infer constraints for what the command placed ([`infer_after`]). When the command changed the
/// drawing and its constraints no longer hold, re-solve them, keeping the geometry the command
/// edited where it was put; constraints whose objects are gone are removed.
pub fn after_command(s: &mut Session, before: Option<&Arc<Drawing>>, ok: bool) {
if !ok {
return;
}
if let (Some(b), Ok(st)) = (before, s.state())
&& !Arc::ptr_eq(b, &st.doc)
{
infer_after(s, b);
}
let (Some(before), Ok(st)) = (before, s.state()) else { return };
if st.doc.constraints.is_empty() || Arc::ptr_eq(before, &st.doc) {
return;
}
let mut d = (*st.doc).clone();
let purged = cs::purge_invalid(&mut d);
let restored = cs::restore_fixed(&mut d, before);
let satisfied = cs::all_satisfied(&d);
if purged.is_empty() && !restored && satisfied {
return;
}
let mut msg = Vec::new();
if !purged.is_empty() {
msg.push(format!("{} constraint(s) removed with their objects.", purged.len()));
}
if !satisfied {
let mut keep: Vec<Handle> = Vec::new();
for h in d.constraints.iter().flat_map(|c| c.refs.iter().map(|r| r.handle)) {
let changed = match (before.entity(h), d.entity(h)) {
(Some(a), Some(b)) => !Arc::ptr_eq(a, b) && a != b,
_ => false,
};
if changed && !keep.contains(&h) {
keep.push(h);
}
}
let mut trial = d.clone();
match cs::solve(&mut trial, &cs::SolveOptions { keep, prefer_move: Vec::new() }) {
Ok(_) => d = trial,
Err(e) => msg.push(format!("Constraints could not be satisfied: {e}")),
}
}
if let Ok(st) = s.state_mut() {
st.doc = Arc::new(d);
}
for m in msg {
s.echo(m);
}
}
#[cfg(test)]
#[path = "constraints_tests.rs"]
mod tests;