Skip to main content

dotloom_constraints/
rules.rs

1//! Residual builders for the supported rule classes.
2//!
3//! Linear rules: fix, equal, linear relation (sum/difference/ratio/min/max with
4//! constant coefficients), equal spacing.
5//!
6//! Geometric rules: coincident, horizontal, vertical, fixed point, distance
7//! (point–point, signed point–line), point on line/circle, length, equal length,
8//! parallel, perpendicular, angle, concentric, radius, equal radius, line–circle and
9//! circle–circle tangency.
10//!
11//! Every builder returns residual rows; the solver decides linearity from the
12//! expressions themselves. Residuals of direction rules are normalized (sine/cosine
13//! of angles) so their scale is 1 regardless of segment length.
14
15use crate::{Expr, PointExpr, Relation, Row, VarId};
16
17fn eq(expr: Expr, scale: f64) -> Row {
18    Row { expr, relation: Relation::Eq, scale }
19}
20
21fn le(expr: Expr, scale: f64) -> Row {
22    Row { expr, relation: Relation::Le, scale }
23}
24
25/// Linear comparison operators.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub enum Cmp {
29    /// `=`.
30    Eq,
31    /// `≤`.
32    Le,
33    /// `≥`.
34    Ge,
35}
36
37/// `v = value`.
38#[must_use]
39pub fn fix(v: Expr, value: f64, scale: f64) -> Vec<Row> {
40    vec![eq(Expr::sub(v, Expr::c(value)), scale)]
41}
42
43/// `a = b`.
44#[must_use]
45pub fn equal(a: Expr, b: Expr, scale: f64) -> Vec<Row> {
46    vec![eq(Expr::sub(a, b), scale)]
47}
48
49/// `Σ cᵢ·eᵢ (cmp) rhs`.
50#[must_use]
51pub fn linear(terms: &[(f64, Expr)], cmp: Cmp, rhs: f64, scale: f64) -> Vec<Row> {
52    let sum = terms.iter().fold(Expr::c(0.0), |acc, (c, e)| Expr::add(acc, Expr::mul(Expr::c(*c), e.clone())));
53    let lhs = Expr::sub(sum, Expr::c(rhs));
54    match cmp {
55        Cmp::Eq => vec![eq(lhs, scale)],
56        Cmp::Le => vec![le(lhs, scale)],
57        Cmp::Ge => vec![le(Expr::neg(lhs), scale)],
58    }
59}
60
61/// `a = k · b` (constant ratio).
62#[must_use]
63pub fn ratio(a: Expr, b: Expr, k: f64, scale: f64) -> Vec<Row> {
64    vec![eq(Expr::sub(a, Expr::mul(Expr::c(k), b)), scale)]
65}
66
67/// `a ≥ min`.
68#[must_use]
69pub fn at_least(a: Expr, min: f64, scale: f64) -> Vec<Row> {
70    vec![le(Expr::sub(Expr::c(min), a), scale)]
71}
72
73/// `a ≤ max`.
74#[must_use]
75pub fn at_most(a: Expr, max: f64, scale: f64) -> Vec<Row> {
76    vec![le(Expr::sub(a, Expr::c(max)), scale)]
77}
78
79/// Consecutive differences are equal: `v₁−v₀ = v₂−v₁ = …`.
80#[must_use]
81pub fn equal_spacing(values: &[Expr], scale: f64) -> Vec<Row> {
82    let mut rows = Vec::new();
83    let mut iter = values.windows(2);
84    let Some(first) = iter.next() else { return rows };
85    let d0 = Expr::sub(first[1].clone(), first[0].clone());
86    for w in iter {
87        rows.push(eq(Expr::sub(Expr::sub(w[1].clone(), w[0].clone()), d0.clone()), scale));
88    }
89    rows
90}
91
92/// Equal values: `v₀ = v₁ = …`.
93#[must_use]
94pub fn all_equal(values: &[Expr], scale: f64) -> Vec<Row> {
95    values.windows(2).map(|w| eq(Expr::sub(w[1].clone(), w[0].clone()), scale)).collect()
96}
97
98/// Coincident points.
99#[must_use]
100pub fn coincident(p: &PointExpr, q: &PointExpr, scale: f64) -> Vec<Row> {
101    vec![eq(Expr::sub(p.x.clone(), q.x.clone()), scale), eq(Expr::sub(p.y.clone(), q.y.clone()), scale)]
102}
103
104/// Same Y.
105#[must_use]
106pub fn horizontal(p: &PointExpr, q: &PointExpr, scale: f64) -> Vec<Row> {
107    vec![eq(Expr::sub(p.y.clone(), q.y.clone()), scale)]
108}
109
110/// Same X.
111#[must_use]
112pub fn vertical(p: &PointExpr, q: &PointExpr, scale: f64) -> Vec<Row> {
113    vec![eq(Expr::sub(p.x.clone(), q.x.clone()), scale)]
114}
115
116/// Point fixed at a location.
117#[must_use]
118pub fn fix_point(p: &PointExpr, at: (f64, f64), scale: f64) -> Vec<Row> {
119    vec![eq(Expr::sub(p.x.clone(), Expr::c(at.0)), scale), eq(Expr::sub(p.y.clone(), Expr::c(at.1)), scale)]
120}
121
122fn dx(p: &PointExpr, q: &PointExpr) -> Expr {
123    Expr::sub(q.x.clone(), p.x.clone())
124}
125
126fn dy(p: &PointExpr, q: &PointExpr) -> Expr {
127    Expr::sub(q.y.clone(), p.y.clone())
128}
129
130fn dist(p: &PointExpr, q: &PointExpr) -> Expr {
131    Expr::hypot(dx(p, q), dy(p, q))
132}
133
134/// `|q − p| = d`.
135#[must_use]
136pub fn distance(p: &PointExpr, q: &PointExpr, d: f64, scale: f64) -> Vec<Row> {
137    vec![eq(Expr::sub(dist(p, q), Expr::c(d)), scale)]
138}
139
140/// `|q − p| (cmp) d`.
141#[must_use]
142pub fn distance_cmp(p: &PointExpr, q: &PointExpr, cmp: Cmp, d: f64, scale: f64) -> Vec<Row> {
143    let r = Expr::sub(dist(p, q), Expr::c(d));
144    match cmp {
145        Cmp::Eq => vec![eq(r, scale)],
146        Cmp::Le => vec![le(r, scale)],
147        Cmp::Ge => vec![le(Expr::neg(r), scale)],
148    }
149}
150
151/// Signed distance from `p` to the line `a → b` (positive on the left).
152#[must_use]
153pub fn signed_line_distance(p: &PointExpr, a: &PointExpr, b: &PointExpr) -> Expr {
154    // cross(b − a, p − a) / |b − a|
155    let cross = Expr::sub(Expr::mul(dx(a, b), dy(a, p)), Expr::mul(dy(a, b), dx(a, p)));
156    Expr::div(cross, dist(a, b))
157}
158
159/// Signed point–line distance equals `d` (sign selects the side).
160#[must_use]
161pub fn point_line_distance(p: &PointExpr, a: &PointExpr, b: &PointExpr, signed_d: f64, scale: f64) -> Vec<Row> {
162    vec![eq(Expr::sub(signed_line_distance(p, a, b), Expr::c(signed_d)), scale)]
163}
164
165/// Point lies on the infinite line through `a`, `b`.
166#[must_use]
167pub fn point_on_line(p: &PointExpr, a: &PointExpr, b: &PointExpr, scale: f64) -> Vec<Row> {
168    vec![eq(signed_line_distance(p, a, b), scale)]
169}
170
171/// Point lies on the circle `(c, r)`.
172#[must_use]
173pub fn point_on_circle(p: &PointExpr, c: &PointExpr, r: Expr, scale: f64) -> Vec<Row> {
174    vec![eq(Expr::sub(dist(c, p), r), scale)]
175}
176
177/// Segment length.
178#[must_use]
179pub fn length(a: &PointExpr, b: &PointExpr, len: f64, scale: f64) -> Vec<Row> {
180    distance(a, b, len, scale)
181}
182
183/// Equal segment lengths.
184#[must_use]
185pub fn equal_length(a1: &PointExpr, b1: &PointExpr, a2: &PointExpr, b2: &PointExpr, scale: f64) -> Vec<Row> {
186    vec![eq(Expr::sub(dist(a1, b1), dist(a2, b2)), scale)]
187}
188
189fn cross_dot(a1: &PointExpr, b1: &PointExpr, a2: &PointExpr, b2: &PointExpr) -> (Expr, Expr, Expr) {
190    let (ux, uy) = (dx(a1, b1), dy(a1, b1));
191    let (vx, vy) = (dx(a2, b2), dy(a2, b2));
192    let cross = Expr::sub(Expr::mul(ux.clone(), vy.clone()), Expr::mul(uy.clone(), vx.clone()));
193    let dot = Expr::add(Expr::mul(ux.clone(), vx.clone()), Expr::mul(uy.clone(), vy.clone()));
194    let norm = Expr::mul(Expr::hypot(ux, uy), Expr::hypot(vx, vy));
195    (cross, dot, norm)
196}
197
198/// Parallel (or anti-parallel) segments: `sin(angle) = 0`.
199#[must_use]
200pub fn parallel(a1: &PointExpr, b1: &PointExpr, a2: &PointExpr, b2: &PointExpr) -> Vec<Row> {
201    let (cross, _, norm) = cross_dot(a1, b1, a2, b2);
202    vec![eq(Expr::div(cross, norm), 1.0)]
203}
204
205/// Perpendicular segments: `cos(angle) = 0`.
206#[must_use]
207pub fn perpendicular(a1: &PointExpr, b1: &PointExpr, a2: &PointExpr, b2: &PointExpr) -> Vec<Row> {
208    let (_, dot, norm) = cross_dot(a1, b1, a2, b2);
209    vec![eq(Expr::div(dot, norm), 1.0)]
210}
211
212/// Signed angle from segment 1 to segment 2 equals `theta` (radians). The
213/// residual is the wrapped angle difference, smooth everywhere except at ±π.
214#[must_use]
215pub fn angle(a1: &PointExpr, b1: &PointExpr, a2: &PointExpr, b2: &PointExpr, theta: f64) -> Vec<Row> {
216    let (cross, dot, _) = cross_dot(a1, b1, a2, b2);
217    let (s, c) = libm::sincos(theta);
218    // angle(φ − θ) = atan2(sinφ cosθ − cosφ sinθ, cosφ cosθ + sinφ sinθ), scaled by |u||v|.
219    let y = Expr::sub(Expr::mul(cross.clone(), Expr::c(c)), Expr::mul(dot.clone(), Expr::c(s)));
220    let x = Expr::add(Expr::mul(dot, Expr::c(c)), Expr::mul(cross, Expr::c(s)));
221    vec![eq(Expr::atan2(y, x), 1.0)]
222}
223
224/// Concentric circles/arcs.
225#[must_use]
226pub fn concentric(c1: &PointExpr, c2: &PointExpr, scale: f64) -> Vec<Row> {
227    coincident(c1, c2, scale)
228}
229
230/// Radius equals `value`.
231#[must_use]
232pub fn radius(r: Expr, value: f64, scale: f64) -> Vec<Row> {
233    fix(r, value, scale)
234}
235
236/// Equal radii.
237#[must_use]
238pub fn equal_radius(r1: Expr, r2: Expr, scale: f64) -> Vec<Row> {
239    equal(r1, r2, scale)
240}
241
242/// Line `a → b` tangent to circle `(c, r)`; `side` (±1) keeps the circle on its
243/// current side of the line.
244#[must_use]
245pub fn tangent_line_circle(a: &PointExpr, b: &PointExpr, c: &PointExpr, r: Expr, side: f64, scale: f64) -> Vec<Row> {
246    let d = signed_line_distance(c, a, b);
247    vec![eq(Expr::sub(Expr::mul(Expr::c(side.signum()), d), r), scale)]
248}
249
250/// Tangency between two circles.
251#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
252#[serde(rename_all = "camelCase")]
253pub enum CircleTangency {
254    /// Circles touch from outside: `|c₂ − c₁| = r₁ + r₂`.
255    External,
256    /// One circle inside the other: `|c₂ − c₁| = sign·(r₁ − r₂)`; `sign` keeps which
257    /// circle is the outer one.
258    Internal {
259        /// +1 when circle 1 is the outer circle, −1 otherwise.
260        sign: f64,
261    },
262}
263
264/// Circle–circle tangency.
265#[must_use]
266pub fn tangent_circles(
267    c1: &PointExpr,
268    r1: Expr,
269    c2: &PointExpr,
270    r2: Expr,
271    kind: CircleTangency,
272    scale: f64,
273) -> Vec<Row> {
274    let d = dist(c1, c2);
275    let target = match kind {
276        CircleTangency::External => Expr::add(r1, r2),
277        CircleTangency::Internal { sign } => Expr::mul(Expr::c(sign.signum()), Expr::sub(r1, r2)),
278    };
279    vec![eq(Expr::sub(d, target), scale)]
280}
281
282/// Point expression of an arc/circle point at an angle: `c + r·(cos a, sin a)`.
283#[must_use]
284pub fn polar_point(cx: Expr, cy: Expr, r: Expr, angle: Expr) -> PointExpr {
285    PointExpr {
286        x: Expr::add(cx, Expr::mul(r.clone(), Expr::cos(angle.clone()))),
287        y: Expr::add(cy, Expr::mul(r, Expr::sin(angle))),
288    }
289}
290
291/// Convenience: variable expression.
292#[must_use]
293pub const fn v(id: VarId) -> Expr {
294    Expr::Var(id)
295}