1use std::collections::BTreeMap;
8
9use dotloom_geometry::Point;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::{AnchorRef, ConstraintId, EntityId};
14
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub enum ParamSlot {
19 Prop(String),
21 Geom(String),
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
28pub struct ParamRef {
29 pub entity: EntityId,
31 #[serde(flatten)]
33 pub slot: ParamSlot,
34}
35
36impl ParamRef {
37 #[must_use]
39 pub fn prop(entity: EntityId, name: impl Into<String>) -> Self {
40 Self { entity, slot: ParamSlot::Prop(name.into()) }
41 }
42
43 #[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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
52pub struct LineRef {
53 pub from: AnchorRef,
55 pub to: AnchorRef,
57}
58
59impl LineRef {
60 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
69pub enum Cmp {
70 #[serde(rename = "=")]
72 Eq,
73 #[serde(rename = "<=")]
75 Le,
76 #[serde(rename = ">=")]
78 Ge,
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83pub struct Term {
84 pub coef: f64,
86 pub param: ParamRef,
88}
89
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
93pub enum RuleSpec {
94 Fix {
96 param: ParamRef,
98 value: f64,
100 },
101 Equal {
103 a: ParamRef,
105 b: ParamRef,
107 },
108 AllEqual {
110 params: Vec<ParamRef>,
112 },
113 Linear {
115 terms: Vec<Term>,
117 op: Cmp,
119 rhs: f64,
121 },
122 Ratio {
124 a: ParamRef,
126 b: ParamRef,
128 k: f64,
130 },
131 EqualSpacing {
133 params: Vec<ParamRef>,
135 },
136 Coincident {
138 a: AnchorRef,
140 b: AnchorRef,
142 },
143 Horizontal {
145 a: AnchorRef,
147 b: AnchorRef,
149 },
150 Vertical {
152 a: AnchorRef,
154 b: AnchorRef,
156 },
157 FixPoint {
159 a: AnchorRef,
161 at: Point,
163 },
164 Distance {
166 a: AnchorRef,
168 b: AnchorRef,
170 value: f64,
172 },
173 PointLineDistance {
175 point: AnchorRef,
177 line: LineRef,
179 value: f64,
181 },
182 PointOnLine {
184 point: AnchorRef,
186 line: LineRef,
188 },
189 PointOnCircle {
191 point: AnchorRef,
193 circle: EntityId,
195 },
196 Length {
198 line: LineRef,
200 value: f64,
202 },
203 EqualLength {
205 a: LineRef,
207 b: LineRef,
209 },
210 Parallel {
212 a: LineRef,
214 b: LineRef,
216 },
217 Perpendicular {
219 a: LineRef,
221 b: LineRef,
223 },
224 Angle {
226 a: LineRef,
228 b: LineRef,
230 value: f64,
232 },
233 Concentric {
235 a: EntityId,
237 b: EntityId,
239 },
240 Radius {
242 circle: EntityId,
244 value: f64,
246 },
247 EqualRadius {
249 a: EntityId,
251 b: EntityId,
253 },
254 TangentLineCircle {
256 line: LineRef,
258 circle: EntityId,
260 side: f64,
262 },
263 TangentCircles {
265 a: EntityId,
267 b: EntityId,
269 #[serde(default)]
271 internal: bool,
272 #[serde(default = "plus_one")]
274 sign: f64,
275 },
276 Expression {
279 entity: EntityId,
281 lhs: String,
283 op: Cmp,
285 rhs: String,
287 },
288}
289
290fn plus_one() -> f64 {
291 1.0
292}
293
294impl RuleSpec {
295 #[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 #[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 #[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 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 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
507#[serde(rename_all = "camelCase")]
508pub enum StrengthSpec {
509 Weak,
511 Medium,
513 Strong,
515 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
536#[serde(rename_all = "camelCase")]
537pub struct Constraint {
538 pub id: ConstraintId,
540 pub rule: RuleSpec,
542 #[serde(default, skip_serializing_if = "is_required")]
544 pub strength: StrengthSpec,
545 #[serde(default = "yes", skip_serializing_if = "is_true")]
547 pub enabled: bool,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
550 pub label: Option<String>,
551 #[serde(default, skip_serializing_if = "Option::is_none")]
553 pub source: Option<String>,
554 #[serde(default, skip_serializing_if = "Option::is_none")]
556 pub owner: Option<EntityId>,
557 #[serde(flatten)]
559 pub extra: BTreeMap<String, Value>,
560}
561
562impl Constraint {
563 #[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}