Skip to main content

dotloom_document/
constraint.rs

1//! Document-level constraint descriptions.
2//!
3//! These are data: what the user (or a plugin template) asked for. The engine
4//! compiles them into solver rows. Every reference is explicit so validation can
5//! reject dangling references before commit.
6
7use std::collections::BTreeMap;
8
9use dotloom_geometry::Point;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::{AnchorRef, ConstraintId, EntityId};
14
15/// Which slot of an entity a numeric parameter lives in.
16#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub enum ParamSlot {
19    /// A numeric property (`props.width`).
20    Prop(String),
21    /// A geometry parameter of a built-in shape (`radius`, `a.x`, `width`, ...).
22    Geom(String),
23}
24
25/// Reference to a numeric parameter: `{"entity": 3, "prop": "width"}` or
26/// `{"entity": 3, "geom": "radius"}`.
27#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
28pub struct ParamRef {
29    /// Entity.
30    pub entity: EntityId,
31    /// Slot.
32    #[serde(flatten)]
33    pub slot: ParamSlot,
34}
35
36impl ParamRef {
37    /// Property parameter.
38    #[must_use]
39    pub fn prop(entity: EntityId, name: impl Into<String>) -> Self {
40        Self { entity, slot: ParamSlot::Prop(name.into()) }
41    }
42
43    /// Geometry parameter.
44    #[must_use]
45    pub fn geom(entity: EntityId, name: impl Into<String>) -> Self {
46        Self { entity, slot: ParamSlot::Geom(name.into()) }
47    }
48}
49
50/// A line defined by two anchors (segment endpoints, polyline vertices, wall ends...).
51#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
52pub struct LineRef {
53    /// Start anchor.
54    pub from: AnchorRef,
55    /// End anchor.
56    pub to: AnchorRef,
57}
58
59impl LineRef {
60    /// `start → end` of a line-like entity.
61    #[must_use]
62    pub fn of(entity: EntityId) -> Self {
63        Self { from: AnchorRef::new(entity, "start"), to: AnchorRef::new(entity, "end") }
64    }
65}
66
67/// Comparison operator for linear and expression rules.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
69pub enum Cmp {
70    /// `=`
71    #[serde(rename = "=")]
72    Eq,
73    /// `<=`
74    #[serde(rename = "<=")]
75    Le,
76    /// `>=`
77    #[serde(rename = ">=")]
78    Ge,
79}
80
81/// One linear term `coef · param`.
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83pub struct Term {
84    /// Coefficient.
85    pub coef: f64,
86    /// Parameter.
87    pub param: ParamRef,
88}
89
90/// Constraint kinds.
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
93pub enum RuleSpec {
94    /// `param = value`.
95    Fix {
96        /// Parameter.
97        param: ParamRef,
98        /// Value (canonical units).
99        value: f64,
100    },
101    /// `a = b`.
102    Equal {
103        /// First parameter.
104        a: ParamRef,
105        /// Second parameter.
106        b: ParamRef,
107    },
108    /// All parameters equal.
109    AllEqual {
110        /// Parameters.
111        params: Vec<ParamRef>,
112    },
113    /// `Σ coef·param (op) rhs` — sums, differences, minimums, maximums.
114    Linear {
115        /// Terms.
116        terms: Vec<Term>,
117        /// Operator.
118        op: Cmp,
119        /// Right-hand side.
120        rhs: f64,
121    },
122    /// `a = k · b`.
123    Ratio {
124        /// Numerator parameter.
125        a: ParamRef,
126        /// Denominator parameter.
127        b: ParamRef,
128        /// Constant ratio.
129        k: f64,
130    },
131    /// Consecutive differences equal (equally spaced values).
132    EqualSpacing {
133        /// Parameters in order.
134        params: Vec<ParamRef>,
135    },
136    /// Coincident anchors.
137    Coincident {
138        /// First anchor.
139        a: AnchorRef,
140        /// Second anchor.
141        b: AnchorRef,
142    },
143    /// Two anchors share Y.
144    Horizontal {
145        /// First anchor.
146        a: AnchorRef,
147        /// Second anchor.
148        b: AnchorRef,
149    },
150    /// Two anchors share X.
151    Vertical {
152        /// First anchor.
153        a: AnchorRef,
154        /// Second anchor.
155        b: AnchorRef,
156    },
157    /// Anchor fixed at a point.
158    FixPoint {
159        /// Anchor.
160        a: AnchorRef,
161        /// Location (world coordinates).
162        at: Point,
163    },
164    /// Distance between anchors.
165    Distance {
166        /// First anchor.
167        a: AnchorRef,
168        /// Second anchor.
169        b: AnchorRef,
170        /// Distance (mm, ≥ 0).
171        value: f64,
172    },
173    /// Signed distance from an anchor to a line (positive = left of the line).
174    PointLineDistance {
175        /// Point.
176        point: AnchorRef,
177        /// Line.
178        line: LineRef,
179        /// Signed distance (mm).
180        value: f64,
181    },
182    /// Anchor on the infinite line.
183    PointOnLine {
184        /// Point.
185        point: AnchorRef,
186        /// Line.
187        line: LineRef,
188    },
189    /// Anchor on a circle/arc entity.
190    PointOnCircle {
191        /// Point.
192        point: AnchorRef,
193        /// Circle or arc entity.
194        circle: EntityId,
195    },
196    /// Line length.
197    Length {
198        /// Line.
199        line: LineRef,
200        /// Length (mm).
201        value: f64,
202    },
203    /// Equal line lengths.
204    EqualLength {
205        /// First line.
206        a: LineRef,
207        /// Second line.
208        b: LineRef,
209    },
210    /// Parallel lines.
211    Parallel {
212        /// First line.
213        a: LineRef,
214        /// Second line.
215        b: LineRef,
216    },
217    /// Perpendicular lines.
218    Perpendicular {
219        /// First line.
220        a: LineRef,
221        /// Second line.
222        b: LineRef,
223    },
224    /// Signed angle from `a` to `b`.
225    Angle {
226        /// First line.
227        a: LineRef,
228        /// Second line.
229        b: LineRef,
230        /// Angle (radians).
231        value: f64,
232    },
233    /// Circles/arcs share their center.
234    Concentric {
235        /// First circle/arc.
236        a: EntityId,
237        /// Second circle/arc.
238        b: EntityId,
239    },
240    /// Radius value.
241    Radius {
242        /// Circle/arc.
243        circle: EntityId,
244        /// Radius (mm).
245        value: f64,
246    },
247    /// Equal radii.
248    EqualRadius {
249        /// First circle/arc.
250        a: EntityId,
251        /// Second circle/arc.
252        b: EntityId,
253    },
254    /// Line tangent to circle; `side` (±1) is the side of the line the circle is on.
255    TangentLineCircle {
256        /// Line.
257        line: LineRef,
258        /// Circle/arc.
259        circle: EntityId,
260        /// +1 left of the line, −1 right.
261        side: f64,
262    },
263    /// Circle–circle tangency.
264    TangentCircles {
265        /// First circle/arc.
266        a: EntityId,
267        /// Second circle/arc.
268        b: EntityId,
269        /// Internal tangency (one inside the other).
270        #[serde(default)]
271        internal: bool,
272        /// For internal tangency: +1 when `a` is the outer circle.
273        #[serde(default = "plus_one")]
274        sign: f64,
275    },
276    /// Expression rule in the Dotloom expression language, evaluated in Rust:
277    /// `lhs (op) rhs`, e.g. `"self.offset + self.width" <= "host.length"`.
278    Expression {
279        /// Entity whose namespace (`self`, references) the expressions use.
280        entity: EntityId,
281        /// Left expression.
282        lhs: String,
283        /// Operator.
284        op: Cmp,
285        /// Right expression.
286        rhs: String,
287    },
288}
289
290fn plus_one() -> f64 {
291    1.0
292}
293
294impl RuleSpec {
295    /// Entities referenced by the rule.
296    #[must_use]
297    pub fn entities(&self) -> Vec<EntityId> {
298        let mut v = Vec::new();
299        let line = |l: &LineRef, v: &mut Vec<EntityId>| {
300            v.push(l.from.entity);
301            v.push(l.to.entity);
302        };
303        match self {
304            Self::Fix { param, .. } => v.push(param.entity),
305            Self::Equal { a, b } | Self::Ratio { a, b, .. } => {
306                v.push(a.entity);
307                v.push(b.entity);
308            }
309            Self::AllEqual { params } | Self::EqualSpacing { params } => v.extend(params.iter().map(|p| p.entity)),
310            Self::Linear { terms, .. } => v.extend(terms.iter().map(|t| t.param.entity)),
311            Self::Coincident { a, b }
312            | Self::Horizontal { a, b }
313            | Self::Vertical { a, b }
314            | Self::Distance { a, b, .. } => {
315                v.push(a.entity);
316                v.push(b.entity);
317            }
318            Self::FixPoint { a, .. } => v.push(a.entity),
319            Self::PointLineDistance { point, line: l, .. } | Self::PointOnLine { point, line: l } => {
320                v.push(point.entity);
321                line(l, &mut v);
322            }
323            Self::PointOnCircle { point, circle } => {
324                v.push(point.entity);
325                v.push(*circle);
326            }
327            Self::Length { line: l, .. } => line(l, &mut v),
328            Self::EqualLength { a, b }
329            | Self::Parallel { a, b }
330            | Self::Perpendicular { a, b }
331            | Self::Angle { a, b, .. } => {
332                line(a, &mut v);
333                line(b, &mut v);
334            }
335            Self::Concentric { a, b } | Self::EqualRadius { a, b } | Self::TangentCircles { a, b, .. } => {
336                v.push(*a);
337                v.push(*b);
338            }
339            Self::Radius { circle, .. } => v.push(*circle),
340            Self::TangentLineCircle { line: l, circle, .. } => {
341                line(l, &mut v);
342                v.push(*circle);
343            }
344            Self::Expression { entity, .. } => v.push(*entity),
345        }
346        v.sort_unstable();
347        v.dedup();
348        v
349    }
350
351    /// Anchor references used by the rule.
352    #[must_use]
353    pub fn anchors(&self) -> Vec<&AnchorRef> {
354        match self {
355            Self::Coincident { a, b }
356            | Self::Horizontal { a, b }
357            | Self::Vertical { a, b }
358            | Self::Distance { a, b, .. } => {
359                vec![a, b]
360            }
361            Self::FixPoint { a, .. } => vec![a],
362            Self::PointLineDistance { point, line, .. } | Self::PointOnLine { point, line } => {
363                vec![point, &line.from, &line.to]
364            }
365            Self::PointOnCircle { point, .. } => vec![point],
366            Self::Length { line, .. } | Self::TangentLineCircle { line, .. } => vec![&line.from, &line.to],
367            Self::EqualLength { a, b }
368            | Self::Parallel { a, b }
369            | Self::Perpendicular { a, b }
370            | Self::Angle { a, b, .. } => {
371                vec![&a.from, &a.to, &b.from, &b.to]
372            }
373            _ => Vec::new(),
374        }
375    }
376
377    /// Parameter references used by the rule.
378    #[must_use]
379    pub fn params(&self) -> Vec<&ParamRef> {
380        match self {
381            Self::Fix { param, .. } => vec![param],
382            Self::Equal { a, b } | Self::Ratio { a, b, .. } => vec![a, b],
383            Self::AllEqual { params } | Self::EqualSpacing { params } => params.iter().collect(),
384            Self::Linear { terms, .. } => terms.iter().map(|t| &t.param).collect(),
385            _ => Vec::new(),
386        }
387    }
388
389    /// Remap entity references (used by copy/paste). Returns `false` if any
390    /// referenced entity has no mapping.
391    pub fn remap(&mut self, map: &BTreeMap<EntityId, EntityId>) -> bool {
392        let mut ok = true;
393        let mut m = |e: &mut EntityId| match map.get(e) {
394            Some(n) => *e = *n,
395            None => ok = false,
396        };
397        match self {
398            Self::Fix { param, .. } => m(&mut param.entity),
399            Self::Equal { a, b } | Self::Ratio { a, b, .. } => {
400                m(&mut a.entity);
401                m(&mut b.entity);
402            }
403            Self::AllEqual { params } | Self::EqualSpacing { params } => {
404                params.iter_mut().for_each(|p| m(&mut p.entity))
405            }
406            Self::Linear { terms, .. } => terms.iter_mut().for_each(|t| m(&mut t.param.entity)),
407            Self::Coincident { a, b }
408            | Self::Horizontal { a, b }
409            | Self::Vertical { a, b }
410            | Self::Distance { a, b, .. } => {
411                m(&mut a.entity);
412                m(&mut b.entity);
413            }
414            Self::FixPoint { a, .. } => m(&mut a.entity),
415            Self::PointLineDistance { point, line, .. } | Self::PointOnLine { point, line } => {
416                m(&mut point.entity);
417                m(&mut line.from.entity);
418                m(&mut line.to.entity);
419            }
420            Self::PointOnCircle { point, circle } => {
421                m(&mut point.entity);
422                m(circle);
423            }
424            Self::Length { line, .. } => {
425                m(&mut line.from.entity);
426                m(&mut line.to.entity);
427            }
428            Self::EqualLength { a, b }
429            | Self::Parallel { a, b }
430            | Self::Perpendicular { a, b }
431            | Self::Angle { a, b, .. } => {
432                m(&mut a.from.entity);
433                m(&mut a.to.entity);
434                m(&mut b.from.entity);
435                m(&mut b.to.entity);
436            }
437            Self::Concentric { a, b } | Self::EqualRadius { a, b } | Self::TangentCircles { a, b, .. } => {
438                m(a);
439                m(b);
440            }
441            Self::Radius { circle, .. } => m(circle),
442            Self::TangentLineCircle { line, circle, .. } => {
443                m(&mut line.from.entity);
444                m(&mut line.to.entity);
445                m(circle);
446            }
447            Self::Expression { entity, .. } => m(entity),
448        }
449        ok
450    }
451
452    /// Stable kind name.
453    #[must_use]
454    pub fn kind_name(&self) -> &'static str {
455        match self {
456            Self::Fix { .. } => "fix",
457            Self::Equal { .. } => "equal",
458            Self::AllEqual { .. } => "allEqual",
459            Self::Linear { .. } => "linear",
460            Self::Ratio { .. } => "ratio",
461            Self::EqualSpacing { .. } => "equalSpacing",
462            Self::Coincident { .. } => "coincident",
463            Self::Horizontal { .. } => "horizontal",
464            Self::Vertical { .. } => "vertical",
465            Self::FixPoint { .. } => "fixPoint",
466            Self::Distance { .. } => "distance",
467            Self::PointLineDistance { .. } => "pointLineDistance",
468            Self::PointOnLine { .. } => "pointOnLine",
469            Self::PointOnCircle { .. } => "pointOnCircle",
470            Self::Length { .. } => "length",
471            Self::EqualLength { .. } => "equalLength",
472            Self::Parallel { .. } => "parallel",
473            Self::Perpendicular { .. } => "perpendicular",
474            Self::Angle { .. } => "angle",
475            Self::Concentric { .. } => "concentric",
476            Self::Radius { .. } => "radius",
477            Self::EqualRadius { .. } => "equalRadius",
478            Self::TangentLineCircle { .. } => "tangentLineCircle",
479            Self::TangentCircles { .. } => "tangentCircles",
480            Self::Expression { .. } => "expression",
481        }
482    }
483
484    /// Numeric fields are finite and within their domain.
485    #[must_use]
486    pub fn values_valid(&self) -> bool {
487        match self {
488            Self::Fix { value, .. } | Self::PointLineDistance { value, .. } | Self::Angle { value, .. } => {
489                value.is_finite()
490            }
491            Self::Distance { value, .. } | Self::Length { value, .. } | Self::Radius { value, .. } => {
492                value.is_finite() && *value >= 0.0
493            }
494            Self::Linear { terms, rhs, .. } => rhs.is_finite() && terms.iter().all(|t| t.coef.is_finite()),
495            Self::Ratio { k, .. } => k.is_finite(),
496            Self::FixPoint { at, .. } => at.is_finite(),
497            Self::TangentLineCircle { side, .. } => *side == 1.0 || *side == -1.0,
498            Self::TangentCircles { sign, .. } => *sign == 1.0 || *sign == -1.0,
499            Self::Expression { lhs, rhs, .. } => lhs.len() <= 4096 && rhs.len() <= 4096,
500            _ => true,
501        }
502    }
503}
504
505/// Strength as stored in documents.
506#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
507#[serde(rename_all = "camelCase")]
508pub enum StrengthSpec {
509    /// Weak preference.
510    Weak,
511    /// Medium preference.
512    Medium,
513    /// Strong preference.
514    Strong,
515    /// Hard rule (default).
516    #[default]
517    Required,
518}
519
520#[allow(clippy::trivially_copy_pass_by_ref)]
521fn is_required(s: &StrengthSpec) -> bool {
522    *s == StrengthSpec::Required
523}
524
525fn yes() -> bool {
526    true
527}
528
529#[allow(clippy::trivially_copy_pass_by_ref)]
530fn is_true(v: &bool) -> bool {
531    *v
532}
533
534/// A stored constraint.
535#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
536#[serde(rename_all = "camelCase")]
537pub struct Constraint {
538    /// Identifier.
539    pub id: ConstraintId,
540    /// What is constrained.
541    pub rule: RuleSpec,
542    /// Strength (hard by default).
543    #[serde(default, skip_serializing_if = "is_required")]
544    pub strength: StrengthSpec,
545    /// Disabled constraints are kept but ignored by the solver.
546    #[serde(default = "yes", skip_serializing_if = "is_true")]
547    pub enabled: bool,
548    /// User-visible label.
549    #[serde(default, skip_serializing_if = "Option::is_none")]
550    pub label: Option<String>,
551    /// Source (`user`, `plugin:acme.wall`, `template:...`).
552    #[serde(default, skip_serializing_if = "Option::is_none")]
553    pub source: Option<String>,
554    /// Owning entity for template-generated constraints (deleted with it).
555    #[serde(default, skip_serializing_if = "Option::is_none")]
556    pub owner: Option<EntityId>,
557    /// Unknown fields preserved.
558    #[serde(flatten)]
559    pub extra: BTreeMap<String, Value>,
560}
561
562impl Constraint {
563    /// Hard, enabled user constraint.
564    #[must_use]
565    pub fn new(id: ConstraintId, rule: RuleSpec) -> Self {
566        Self {
567            id,
568            rule,
569            strength: StrengthSpec::Required,
570            enabled: true,
571            label: None,
572            source: None,
573            owner: None,
574            extra: BTreeMap::new(),
575        }
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582
583    #[test]
584    fn rule_json_shape() {
585        let c = Constraint::new(
586            ConstraintId(9),
587            RuleSpec::Distance {
588                a: AnchorRef::new(EntityId(1), "start"),
589                b: AnchorRef::new(EntityId(2), "end"),
590                value: 50.0,
591            },
592        );
593        let j = serde_json::to_string(&c).unwrap();
594        assert_eq!(
595            j,
596            r#"{"id":9,"rule":{"kind":"distance","a":{"entity":1,"anchor":"start"},"b":{"entity":2,"anchor":"end"},"value":50.0}}"#
597        );
598        let back: Constraint = serde_json::from_str(&j).unwrap();
599        assert_eq!(back, c);
600        let p = ParamRef::prop(EntityId(3), "width");
601        assert_eq!(serde_json::to_string(&p).unwrap(), r#"{"entity":3,"prop":"width"}"#);
602        let lin = RuleSpec::Linear { terms: vec![Term { coef: 1.0, param: p }], op: Cmp::Ge, rhs: 400.0 };
603        assert!(serde_json::to_string(&lin).unwrap().contains(r#""op":">=""#));
604    }
605
606    #[test]
607    fn unknown_fields_survive() {
608        let j = r#"{"id":1,"rule":{"kind":"radius","circle":4,"value":5.0},"futureFlag":{"x":1}}"#;
609        let c: Constraint = serde_json::from_str(j).unwrap();
610        assert!(c.extra.contains_key("futureFlag"));
611        let back = serde_json::to_string(&c).unwrap();
612        assert!(back.contains("futureFlag"));
613    }
614
615    #[test]
616    fn remap_reports_missing() {
617        let mut r = RuleSpec::Parallel { a: LineRef::of(EntityId(1)), b: LineRef::of(EntityId(2)) };
618        let map: BTreeMap<_, _> = [(EntityId(1), EntityId(11))].into_iter().collect();
619        assert!(!r.remap(&map));
620        let full: BTreeMap<_, _> = [(EntityId(1), EntityId(11)), (EntityId(2), EntityId(12))].into_iter().collect();
621        let mut r2 = RuleSpec::Parallel { a: LineRef::of(EntityId(1)), b: LineRef::of(EntityId(2)) };
622        assert!(r2.remap(&full));
623        assert_eq!(r2.entities(), vec![EntityId(11), EntityId(12)]);
624    }
625}