1use std::{collections::BTreeMap, sync::Arc};
10
11use dotloom_document::{Cmp, Color, StrengthSpec, TypeId, builtin::types};
12use dotloom_geometry::{
13 AnchorKind, HAlign, VAlign,
14 units::{Dim, Quantity},
15};
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use thiserror::Error;
19
20use crate::lang::{Binding, Compiled, LangError, Scope, Ty, compile, compile_as};
21
22#[derive(Debug, Clone, PartialEq, Error)]
24#[non_exhaustive]
25pub enum RegistryError {
26 #[error("type `{0}` is already registered")]
28 Duplicate(String),
29 #[error("type `{0}` is reserved by Dotloom")]
31 Reserved(String),
32 #[error("type `{0}` is not registered")]
34 Unknown(String),
35 #[error("invalid definition of `{type_id}` at {location}: {error}")]
37 Invalid {
38 type_id: String,
40 location: String,
42 error: String,
44 },
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase")]
50pub enum DimName {
51 #[default]
53 Length,
54 Angle,
56 Time,
58 Scalar,
60}
61
62impl DimName {
63 #[must_use]
65 pub const fn dim(self) -> Dim {
66 match self {
67 Self::Length => Dim::LENGTH,
68 Self::Angle => Dim::ANGLE,
69 Self::Time => Dim::TIME,
70 Self::Scalar => Dim::SCALAR,
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78#[serde(untagged)]
79pub enum NumberLit {
80 Plain(f64),
82 WithUnit(String),
84}
85
86impl NumberLit {
87 pub fn value(&self, dim: Dim) -> Result<f64, String> {
89 match self {
90 Self::Plain(v) if v.is_finite() => Ok(*v),
91 Self::Plain(_) => Err("not finite".into()),
92 Self::WithUnit(s) => {
93 let q = Quantity::parse(s).map_err(|e| e.to_string())?;
94 if q.dim == dim || q.dim == Dim::SCALAR {
95 Ok(q.value)
96 } else {
97 Err(format!("`{s}` has dimension {} but {dim} is required", q.dim))
98 }
99 }
100 }
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub enum OnDelete {
108 #[default]
110 Cascade,
111 Clear,
113 Reject,
115}
116
117fn yes() -> bool {
118 true
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub enum StayLevel {
126 Low,
128 #[default]
130 Normal,
131 High,
133}
134
135impl StayLevel {
136 #[must_use]
138 pub const fn factor(self) -> f64 {
139 match self {
140 Self::Low => 0.2,
141 Self::Normal => 1.0,
142 Self::High => 5.0,
143 }
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
149#[serde(tag = "type", rename_all = "camelCase", rename_all_fields = "camelCase")]
150pub enum PropDef {
151 Number {
153 #[serde(default)]
155 dim: DimName,
156 #[serde(default)]
158 default: Option<NumberLit>,
159 #[serde(default)]
161 min: Option<NumberLit>,
162 #[serde(default)]
164 max: Option<NumberLit>,
165 #[serde(default = "yes")]
167 solve: bool,
168 #[serde(default)]
170 stay: StayLevel,
171 #[serde(default)]
173 label: Option<String>,
174 },
175 Point {
177 #[serde(default)]
179 default: Option<[f64; 2]>,
180 #[serde(default)]
182 stay: StayLevel,
183 #[serde(default)]
185 label: Option<String>,
186 },
187 Bool {
189 #[serde(default)]
191 default: Option<bool>,
192 #[serde(default)]
194 label: Option<String>,
195 },
196 Text {
198 #[serde(default)]
200 default: Option<String>,
201 #[serde(default)]
203 max_len: Option<usize>,
204 #[serde(default)]
206 label: Option<String>,
207 },
208 Enum {
210 values: Vec<String>,
212 #[serde(default)]
214 default: Option<String>,
215 #[serde(default)]
217 label: Option<String>,
218 },
219 Ref {
221 #[serde(default)]
223 target: Option<TypeId>,
224 #[serde(default)]
226 on_delete: OnDelete,
227 #[serde(default)]
229 required: bool,
230 #[serde(default)]
232 label: Option<String>,
233 },
234}
235
236#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
238pub struct NamedExpr {
239 pub name: String,
241 pub expr: String,
243}
244
245#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
247pub struct AnchorDef {
248 pub name: String,
250 pub expr: String,
252 #[serde(default = "default_anchor_kind")]
254 pub kind: AnchorKind,
255}
256
257fn default_anchor_kind() -> AnchorKind {
258 AnchorKind::Vertex
259}
260
261#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
263#[serde(rename_all = "camelCase")]
264pub struct PrimStyle {
265 #[serde(default)]
267 pub stroke: Option<Color>,
268 #[serde(default)]
270 pub fill: Option<Color>,
271 #[serde(default)]
273 pub width: Option<f64>,
274 #[serde(default)]
276 pub dash: Option<Vec<f64>>,
277 #[serde(default)]
279 pub no_stroke: bool,
280}
281
282#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
284#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
285pub enum PrimitiveDef {
286 Line {
288 from: String,
290 to: String,
292 #[serde(default)]
294 style: PrimStyle,
295 },
296 Polyline {
298 points: Vec<String>,
300 #[serde(default)]
302 closed: bool,
303 #[serde(default)]
305 style: PrimStyle,
306 },
307 Polygon {
309 points: Vec<String>,
311 #[serde(default)]
313 style: PrimStyle,
314 },
315 Circle {
317 center: String,
319 radius: String,
321 #[serde(default)]
323 style: PrimStyle,
324 },
325 Arc {
327 center: String,
329 radius: String,
331 start: String,
333 sweep: String,
335 #[serde(default)]
337 style: PrimStyle,
338 },
339 Text {
341 position: String,
343 content: String,
345 height: String,
347 #[serde(default)]
349 rotation: Option<String>,
350 #[serde(default)]
352 halign: HAlign,
353 #[serde(default)]
355 valign: VAlign,
356 #[serde(default)]
358 style: PrimStyle,
359 },
360}
361
362#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
364pub struct TemplateDef {
365 pub lhs: String,
367 pub op: Cmp,
369 pub rhs: String,
371 #[serde(default)]
373 pub label: Option<String>,
374 #[serde(default)]
376 pub strength: StrengthSpec,
377}
378
379#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
381pub struct CircleDef {
382 pub center: String,
384 pub radius: String,
386}
387
388#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
390pub struct MigrationDef {
391 pub from: u32,
393 #[serde(default)]
395 pub rename: BTreeMap<String, String>,
396 #[serde(default)]
398 pub set: BTreeMap<String, Value>,
399 #[serde(default)]
401 pub remove: Vec<String>,
402}
403
404fn one() -> u32 {
405 1
406}
407
408#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
410#[serde(rename_all = "camelCase")]
411pub struct EntityTypeDef {
412 pub type_id: TypeId,
414 #[serde(default = "one")]
416 pub version: u32,
417 #[serde(default)]
419 pub label: String,
420 #[serde(default)]
422 pub props: BTreeMap<String, PropDef>,
423 #[serde(default)]
425 pub derived: Vec<NamedExpr>,
426 #[serde(default)]
428 pub anchors: Vec<AnchorDef>,
429 #[serde(default)]
431 pub primitives: Vec<PrimitiveDef>,
432 #[serde(default)]
434 pub constraints: Vec<TemplateDef>,
435 #[serde(default)]
437 pub circle: Option<CircleDef>,
438 #[serde(default)]
440 pub migrations: Vec<MigrationDef>,
441}
442
443#[derive(Debug, Clone, PartialEq)]
445pub enum CompiledPrim {
446 Line(Compiled, Compiled, PrimStyle),
448 Polyline(Vec<Compiled>, bool, PrimStyle),
450 Polygon(Vec<Compiled>, PrimStyle),
452 Circle(Compiled, Compiled, PrimStyle),
454 Arc(Compiled, Compiled, Compiled, Compiled, PrimStyle),
456 Text {
458 position: Compiled,
460 content: String,
462 height: Compiled,
464 rotation: Option<Compiled>,
466 halign: HAlign,
468 valign: VAlign,
470 style: PrimStyle,
472 },
473}
474
475#[derive(Debug, Clone, PartialEq)]
477pub struct CompiledTemplate {
478 pub def: TemplateDef,
480 pub diff: Compiled,
482 pub dim: Dim,
484}
485
486#[derive(Debug, Clone, PartialEq)]
488pub struct ParamInfo {
489 pub name: String,
491 pub dim: Dim,
493 pub stay: f64,
495}
496
497#[derive(Debug, Clone, PartialEq)]
499pub struct CompiledType {
500 pub def: EntityTypeDef,
502 pub anchors: Vec<(String, AnchorKind, Compiled)>,
504 pub primitives: Vec<CompiledPrim>,
506 pub templates: Vec<CompiledTemplate>,
508 pub circle: Option<(Compiled, Compiled)>,
510 pub params: Vec<ParamInfo>,
512 pub members: BTreeMap<String, Ty>,
514 pub scope: Scope,
516}
517
518#[derive(Debug, Clone)]
520pub struct TypeEntry {
521 pub compiled: Arc<CompiledType>,
523 pub enabled: bool,
525 pub plugin: String,
527}
528
529#[must_use]
531pub fn builtin_members(t: &TypeId) -> Option<BTreeMap<String, Ty>> {
532 let (anchors, lengths): (&[&str], &[&str]) = match t.as_str() {
533 types::POINT => (&["point"], &[]),
534 types::LINE => (&["start", "end", "mid"], &[]),
535 types::POLYLINE | types::PATH => (&["start", "end"], &[]),
536 types::RECT => (&["c0", "c1", "c2", "c3", "e0", "e1", "e2", "e3", "center"], &["width", "height"]),
537 types::CIRCLE => (&["center", "q0", "q1", "q2", "q3"], &["r"]),
538 types::ARC => (&["start", "end", "mid", "center"], &["r"]),
539 types::TEXT => (&["insert"], &[]),
540 _ => return None,
541 };
542 let mut m: BTreeMap<String, Ty> = anchors.iter().map(|n| ((*n).to_owned(), Ty::Vector(Dim::LENGTH))).collect();
543 m.extend(lengths.iter().map(|n| ((*n).to_owned(), Ty::Scalar(Dim::LENGTH))));
544 Some(m)
545}
546
547#[derive(Debug, Clone, Default)]
549pub struct Registry {
550 types: BTreeMap<TypeId, TypeEntry>,
551}
552
553fn invalid(t: &TypeId, location: impl Into<String>, e: impl ToString) -> RegistryError {
554 RegistryError::Invalid { type_id: t.to_string(), location: location.into(), error: e.to_string() }
555}
556
557impl Registry {
558 #[must_use]
560 pub fn new() -> Self {
561 Self::default()
562 }
563
564 #[must_use]
566 pub fn get(&self, t: &TypeId) -> Option<&TypeEntry> {
567 self.types.get(t)
568 }
569
570 #[must_use]
572 pub fn enabled(&self, t: &TypeId) -> Option<&Arc<CompiledType>> {
573 self.types.get(t).filter(|e| e.enabled).map(|e| &e.compiled)
574 }
575
576 pub fn types(&self) -> impl Iterator<Item = (&TypeId, &TypeEntry)> {
578 self.types.iter()
579 }
580
581 #[must_use]
583 pub fn members_of(&self, t: &TypeId) -> Option<BTreeMap<String, Ty>> {
584 builtin_members(t).or_else(|| self.types.get(t).map(|e| e.compiled.members.clone()))
585 }
586
587 pub fn register(&mut self, def: EntityTypeDef, plugin: impl Into<String>) -> Result<(), RegistryError> {
590 let t = def.type_id.clone();
591 if t.namespace() == "dotloom" {
592 return Err(RegistryError::Reserved(t.to_string()));
593 }
594 if self.types.contains_key(&t) {
595 return Err(RegistryError::Duplicate(t.to_string()));
596 }
597 let compiled = self.compile(def)?;
598 self.types.insert(t, TypeEntry { compiled: Arc::new(compiled), enabled: true, plugin: plugin.into() });
599 Ok(())
600 }
601
602 pub fn unregister(&mut self, t: &TypeId) -> Result<TypeEntry, RegistryError> {
604 self.types.remove(t).ok_or_else(|| RegistryError::Unknown(t.to_string()))
605 }
606
607 pub fn set_enabled(&mut self, t: &TypeId, enabled: bool) -> Result<(), RegistryError> {
609 let e = self.types.get_mut(t).ok_or_else(|| RegistryError::Unknown(t.to_string()))?;
610 e.enabled = enabled;
611 Ok(())
612 }
613
614 fn compile(&self, def: EntityTypeDef) -> Result<CompiledType, RegistryError> {
615 let t = def.type_id.clone();
616 if def.version == 0 {
617 return Err(invalid(&t, "version", "must be ≥ 1"));
618 }
619 let mut scope = Scope { names: BTreeMap::new(), has_axis: true };
620 let mut params = Vec::new();
621 let reserved = ["pi", "axis", "self"];
622 for (name, p) in &def.props {
623 if reserved.contains(&name.as_str()) || !valid_name(name) {
624 return Err(invalid(&t, format!("props.{name}"), "invalid property name"));
625 }
626 match p {
627 PropDef::Number { dim, default, min, max, solve, stay, .. } => {
628 for (what, lit) in [("default", default), ("min", min), ("max", max)] {
629 if let Some(l) = lit {
630 l.value(dim.dim()).map_err(|e| invalid(&t, format!("props.{name}.{what}"), e))?;
631 }
632 }
633 scope.names.insert(name.clone(), Binding::NumberProp(dim.dim()));
634 if *solve {
635 params.push(ParamInfo { name: name.clone(), dim: dim.dim(), stay: stay.factor() });
636 }
637 }
638 PropDef::Point { stay, .. } => {
639 scope.names.insert(name.clone(), Binding::PointProp);
640 params.push(ParamInfo { name: format!("{name}.x"), dim: Dim::LENGTH, stay: stay.factor() });
641 params.push(ParamInfo { name: format!("{name}.y"), dim: Dim::LENGTH, stay: stay.factor() });
642 }
643 PropDef::Ref { target, .. } => {
644 let members = match target {
645 Some(tt) if *tt == t => Some(BTreeMap::new()),
646 Some(tt) => Some(self.members_of(tt).ok_or_else(|| {
647 invalid(&t, format!("props.{name}.target"), format!("type `{tt}` must be registered first"))
648 })?),
649 None => None,
650 };
651 scope.names.insert(name.clone(), Binding::RefProp(members));
652 }
653 PropDef::Enum { values, default, .. } => {
654 if values.is_empty() || default.as_ref().is_some_and(|d| !values.contains(d)) {
655 return Err(invalid(&t, format!("props.{name}"), "enum needs values and a valid default"));
656 }
657 }
658 PropDef::Bool { .. } | PropDef::Text { .. } => {}
659 }
660 }
661 let err = |loc: String| {
662 let t = t.clone();
663 move |e: LangError| invalid(&t, loc, e)
664 };
665 for d in &def.derived {
666 if scope.names.contains_key(&d.name) || !valid_name(&d.name) {
667 return Err(invalid(&t, format!("derived.{}", d.name), "name is already defined or invalid"));
668 }
669 let c = compile(&d.expr, &scope).map_err(err(format!("derived.{}", d.name)))?;
670 scope.names.insert(d.name.clone(), Binding::Value(c));
671 }
672 let mut anchors = Vec::new();
673 let mut members: BTreeMap<String, Ty> = BTreeMap::new();
674 for a in &def.anchors {
675 if !valid_name(&a.name) {
676 return Err(invalid(&t, format!("anchors.{}", a.name), "invalid anchor name"));
677 }
678 let c = compile_as(&a.expr, &scope, Ty::Vector(Dim::LENGTH)).map_err(err(format!("anchors.{}", a.name)))?;
679 members.insert(a.name.clone(), Ty::Vector(Dim::LENGTH));
680 if !scope.names.contains_key(&a.name) {
681 scope.names.insert(a.name.clone(), Binding::Value(c.clone()));
682 }
683 anchors.push((a.name.clone(), a.kind, c));
684 }
685 for p in ¶ms {
686 if !p.name.contains('.') {
687 members.entry(p.name.clone()).or_insert(Ty::Scalar(p.dim));
688 }
689 }
690 let pt = |src: &str, loc: String| compile_as(src, &scope, Ty::Vector(Dim::LENGTH)).map_err(err(loc));
691 let len = |src: &str, loc: String| compile_as(src, &scope, Ty::Scalar(Dim::LENGTH)).map_err(err(loc));
692 let ang = |src: &str, loc: String| compile_as(src, &scope, Ty::Scalar(Dim::ANGLE)).map_err(err(loc));
693 let mut primitives = Vec::new();
694 for (i, p) in def.primitives.iter().enumerate() {
695 let loc = |f: &str| format!("primitives[{i}].{f}");
696 primitives.push(match p {
697 PrimitiveDef::Line { from, to, style } => {
698 CompiledPrim::Line(pt(from, loc("from"))?, pt(to, loc("to"))?, style.clone())
699 }
700 PrimitiveDef::Polyline { points, closed, style } => CompiledPrim::Polyline(
701 points
702 .iter()
703 .enumerate()
704 .map(|(k, s)| pt(s, loc(&format!("points[{k}]"))))
705 .collect::<Result<_, _>>()?,
706 *closed,
707 style.clone(),
708 ),
709 PrimitiveDef::Polygon { points, style } => {
710 if points.len() < 3 {
711 return Err(invalid(&t, loc("points"), "polygon needs at least 3 points"));
712 }
713 CompiledPrim::Polygon(
714 points
715 .iter()
716 .enumerate()
717 .map(|(k, s)| pt(s, loc(&format!("points[{k}]"))))
718 .collect::<Result<_, _>>()?,
719 style.clone(),
720 )
721 }
722 PrimitiveDef::Circle { center, radius, style } => {
723 CompiledPrim::Circle(pt(center, loc("center"))?, len(radius, loc("radius"))?, style.clone())
724 }
725 PrimitiveDef::Arc { center, radius, start, sweep, style } => CompiledPrim::Arc(
726 pt(center, loc("center"))?,
727 len(radius, loc("radius"))?,
728 ang(start, loc("start"))?,
729 ang(sweep, loc("sweep"))?,
730 style.clone(),
731 ),
732 PrimitiveDef::Text { position, content, height, rotation, halign, valign, style } => {
733 for name in placeholders(content) {
734 if !def.props.contains_key(&name) {
735 return Err(invalid(&t, loc("content"), format!("unknown placeholder {{{name}}}")));
736 }
737 }
738 CompiledPrim::Text {
739 position: pt(position, loc("position"))?,
740 content: content.clone(),
741 height: len(height, loc("height"))?,
742 rotation: rotation.as_ref().map(|r| ang(r, loc("rotation"))).transpose()?,
743 halign: *halign,
744 valign: *valign,
745 style: style.clone(),
746 }
747 }
748 });
749 }
750 let mut templates = Vec::new();
751 for (i, tpl) in def.constraints.iter().enumerate() {
752 let loc = format!("constraints[{i}]");
753 let l = compile(&tpl.lhs, &scope).map_err(err(loc.clone()))?;
754 let r = compile(&tpl.rhs, &scope).map_err(err(loc.clone()))?;
755 let (Ty::Scalar(dl), Ty::Scalar(dr)) = (l.ty, r.ty) else {
756 return Err(invalid(&t, loc, "constraint sides must be scalars"));
757 };
758 if dl != dr {
759 return Err(invalid(&t, loc, format!("cannot compare {dl} with {dr}")));
760 }
761 let diff = compile(&format!("({}) - ({})", tpl.lhs, tpl.rhs), &scope).map_err(err(loc))?;
762 templates.push(CompiledTemplate { def: tpl.clone(), diff, dim: dl });
763 }
764 for (name, p) in &def.props {
766 if let PropDef::Number { dim, min, max, .. } = p {
767 for (lit, op) in [(min, Cmp::Ge), (max, Cmp::Le)] {
768 if let Some(l) = lit {
769 let v = l.value(dim.dim()).map_err(|e| invalid(&t, format!("props.{name}"), e))?;
770 let tpl = TemplateDef {
771 lhs: name.clone(),
772 op,
773 rhs: format!("{v}"),
774 label: Some(format!("{name} {} {v}", if op == Cmp::Ge { "≥" } else { "≤" })),
775 strength: StrengthSpec::Required,
776 };
777 let mut diff = compile(name, &scope).map_err(err(format!("props.{name}")))?;
778 diff.parts = diff
779 .parts
780 .into_iter()
781 .map(|e| dotloom_constraints::Expr::sub(e, dotloom_constraints::Expr::c(v)))
782 .collect();
783 templates.push(CompiledTemplate { def: tpl, diff, dim: dim.dim() });
784 }
785 }
786 }
787 }
788 let circle = match &def.circle {
789 Some(c) => Some((pt(&c.center, "circle.center".into())?, len(&c.radius, "circle.radius".into())?)),
790 None => None,
791 };
792 let mut seen = std::collections::BTreeSet::new();
793 for m in &def.migrations {
794 if m.from == 0 || m.from >= def.version || !seen.insert(m.from) {
795 return Err(invalid(
796 &t,
797 format!("migrations.from={}", m.from),
798 "migration steps must be unique and below the current version",
799 ));
800 }
801 }
802 Ok(CompiledType { def, anchors, primitives, templates, circle, params, members, scope })
803 }
804}
805
806fn valid_name(n: &str) -> bool {
807 let mut c = n.chars();
808 c.next().is_some_and(|f| f.is_ascii_alphabetic() || f == '_')
809 && c.all(|x| x.is_ascii_alphanumeric() || x == '_')
810 && n.len() <= 64
811}
812
813#[must_use]
815pub fn placeholders(s: &str) -> Vec<String> {
816 let mut out = Vec::new();
817 let mut rest = s;
818 while let Some(i) = rest.find('{') {
819 let after = &rest[i + 1..];
820 match after.find('}') {
821 Some(j) => {
822 out.push(after[..j].to_owned());
823 rest = &after[j + 1..];
824 }
825 None => break,
826 }
827 }
828 out
829}