1use std::collections::{BTreeMap, BTreeSet};
4
5use dotloom_document::{
6 Clipboard, Color, Constraint, ConstraintId, Entity, EntityId, Group, GroupId, Layer, LayerId, PropValue, RuleSpec,
7 StrengthSpec, Style, TypeId,
8 builtin::{builtin_shape_kind, is_builtin, set_geometry_param, types},
9};
10use dotloom_geometry::{
11 Affine, GeometryError, ModelTolerance, Point, Shape, TransformPolicy, Vector,
12 edit::CurveEnd,
13 units::{LengthUnit, TimeAxis},
14};
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use thiserror::Error;
18
19use crate::{
20 DocView, Overlay,
21 eval::{Ctx, evaluate, plugin_for},
22 registry::{OnDelete, PropDef, Registry},
23};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub enum EditMode {
29 #[default]
31 Exact,
32 Prefer,
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub struct ParamValue {
39 pub entity: EntityId,
41 pub param: String,
43 pub value: f64,
45}
46
47#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase")]
50pub struct NewEntity {
51 #[serde(rename = "type")]
53 pub type_id: Option<TypeId>,
54 #[serde(default)]
56 pub layer: Option<LayerId>,
57 #[serde(default)]
59 pub geometry: Option<Shape>,
60 #[serde(default)]
62 pub props: BTreeMap<String, PropValue>,
63 #[serde(default)]
65 pub transform: Option<Affine>,
66 #[serde(default)]
68 pub name: Option<String>,
69 #[serde(default)]
71 pub style: Option<Style>,
72 #[serde(default)]
74 pub data: BTreeMap<String, Value>,
75}
76
77#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct EntityPatch {
81 #[serde(default)]
83 pub geometry: Option<Shape>,
84 #[serde(default)]
86 pub props: BTreeMap<String, Option<PropValue>>,
87 #[serde(default)]
89 pub transform: Option<Affine>,
90 #[serde(default)]
92 pub name: Option<String>,
93 #[serde(default)]
95 pub style: Option<Style>,
96 #[serde(default)]
98 pub layer: Option<LayerId>,
99 #[serde(default)]
101 pub locked: Option<bool>,
102 #[serde(default)]
104 pub hidden: Option<bool>,
105 #[serde(default)]
107 pub data: BTreeMap<String, Option<Value>>,
108}
109
110impl EntityPatch {
111 fn only_meta(&self) -> bool {
112 self.geometry.is_none() && self.props.is_empty() && self.transform.is_none() && self.data.is_empty()
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct ConstraintSpec {
120 pub rule: RuleSpec,
122 #[serde(default)]
124 pub strength: StrengthSpec,
125 #[serde(default = "yes")]
127 pub enabled: bool,
128 #[serde(default)]
130 pub label: Option<String>,
131 #[serde(default)]
133 pub source: Option<String>,
134}
135
136fn yes() -> bool {
137 true
138}
139
140#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct ConstraintPatch {
144 #[serde(default)]
146 pub rule: Option<RuleSpec>,
147 #[serde(default)]
149 pub strength: Option<StrengthSpec>,
150 #[serde(default)]
152 pub enabled: Option<bool>,
153 #[serde(default)]
155 pub label: Option<String>,
156}
157
158#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct LayerPatch {
162 #[serde(default)]
164 pub name: Option<String>,
165 #[serde(default)]
167 pub visible: Option<bool>,
168 #[serde(default)]
170 pub locked: Option<bool>,
171 #[serde(default)]
173 pub color: Option<Color>,
174}
175
176#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
178#[serde(rename_all = "camelCase")]
179pub struct SettingsPatch {
180 #[serde(default)]
182 pub display_unit: Option<LengthUnit>,
183 #[serde(default)]
185 pub grid_spacing: Option<f64>,
186 #[serde(default)]
188 pub time_axis: Option<TimeAxis>,
189 #[serde(default)]
191 pub title: Option<String>,
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub enum DeletePolicy {
198 #[default]
200 Cascade,
201 Reject,
203}
204
205#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
207#[serde(tag = "op", rename_all = "camelCase", rename_all_fields = "camelCase")]
208pub enum Command {
209 CreateEntity {
211 #[serde(default)]
213 id: Option<EntityId>,
214 entity: NewEntity,
216 },
217 UpdateEntity {
219 id: EntityId,
221 patch: EntityPatch,
223 },
224 SetParams {
226 values: Vec<ParamValue>,
228 #[serde(default)]
230 mode: EditMode,
231 },
232 Transform {
234 ids: Vec<EntityId>,
236 transform: Affine,
238 #[serde(default)]
240 policy: TransformPolicy,
241 },
242 Delete {
244 ids: Vec<EntityId>,
246 #[serde(default)]
248 policy: DeletePolicy,
249 },
250 Reorder {
252 id: EntityId,
254 index: usize,
256 },
257 AddConstraint {
259 #[serde(default)]
261 id: Option<ConstraintId>,
262 constraint: ConstraintSpec,
264 },
265 UpdateConstraint {
267 id: ConstraintId,
269 patch: ConstraintPatch,
271 },
272 RemoveConstraint {
274 id: ConstraintId,
276 },
277 AddLayer {
279 #[serde(default)]
281 id: Option<LayerId>,
282 name: String,
284 },
285 UpdateLayer {
287 id: LayerId,
289 patch: LayerPatch,
291 },
292 RemoveLayer {
294 id: LayerId,
296 },
297 MoveLayer {
299 id: LayerId,
301 index: usize,
303 },
304 Group {
306 #[serde(default)]
308 id: Option<GroupId>,
309 members: Vec<EntityId>,
311 #[serde(default)]
313 name: Option<String>,
314 },
315 Ungroup {
317 id: GroupId,
319 },
320 Paste {
322 clipboard: Clipboard,
324 offset: Vector,
326 },
327 Split {
329 id: EntityId,
331 at: Point,
333 #[serde(default)]
335 at2: Option<Point>,
336 },
337 Trim {
339 id: EntityId,
341 cutters: Vec<EntityId>,
343 pick: Point,
345 },
346 Extend {
348 id: EntityId,
350 end: CurveEnd,
352 boundaries: Vec<EntityId>,
354 },
355 SetSettings {
357 patch: SettingsPatch,
359 },
360}
361
362#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
364#[serde(tag = "code", rename_all = "camelCase")]
365#[non_exhaustive]
366pub enum CommandError {
367 #[error("{what} does not exist")]
369 NotFound {
370 what: String,
372 },
373 #[error("{what} is read-only: {reason}")]
375 ReadOnly {
376 what: String,
378 reason: String,
380 },
381 #[error("invalid {what}: {reason}")]
383 Invalid {
384 what: String,
386 reason: String,
388 },
389 #[error("cannot delete {what}: {reason}")]
391 HasDependents {
392 what: String,
394 reason: String,
396 },
397 #[error("geometry: {0}")]
399 Geometry(String),
400}
401
402impl From<GeometryError> for CommandError {
403 fn from(e: GeometryError) -> Self {
404 Self::Geometry(e.to_string())
405 }
406}
407
408#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
410#[serde(rename_all = "camelCase")]
411pub struct ApplyNotes {
412 pub created: Vec<EntityId>,
414 pub created_constraints: Vec<ConstraintId>,
416 pub deleted: Vec<EntityId>,
418 pub removed_constraints: Vec<ConstraintId>,
420 #[serde(skip)]
422 pub edits: Vec<(EntityId, String, f64, bool)>,
423 #[serde(skip)]
425 pub touched: BTreeSet<EntityId>,
426 #[serde(skip)]
428 pub touched_constraints: BTreeSet<ConstraintId>,
429 pub notes: Vec<String>,
431}
432
433fn not_found(what: impl ToString) -> CommandError {
434 CommandError::NotFound { what: what.to_string() }
435}
436
437fn invalid(what: impl Into<String>, reason: impl Into<String>) -> CommandError {
438 CommandError::Invalid { what: what.into(), reason: reason.into() }
439}
440
441pub(crate) struct Applier<'o, 'd> {
443 pub ov: &'o mut Overlay<'d>,
444 pub registry: &'o Registry,
445 pub reserved: &'o BTreeSet<u64>,
446 pub notes: ApplyNotes,
447}
448
449impl Applier<'_, '_> {
450 fn ctx(&self) -> Ctx<'_> {
451 Ctx { view: &*self.ov, registry: self.registry }
452 }
453
454 fn take_id(&mut self, wanted: Option<u64>) -> Result<u64, CommandError> {
455 match wanted {
456 None => Ok(self.ov.alloc_id()),
457 Some(id) => {
458 let free = self.reserved.contains(&id) || id >= self.ov.next_id;
459 let unused = self.ov.entity(EntityId(id)).is_none()
460 && self.ov.constraint(ConstraintId(id)).is_none()
461 && self.ov.layer(LayerId(id)).is_none()
462 && self.ov.group(GroupId(id)).is_none();
463 if !(free && unused) {
464 return Err(invalid("id", format!("{id} is not a reserved, unused id")));
465 }
466 if id >= self.ov.next_id {
467 self.ov.next_id = id.saturating_add(1);
468 }
469 Ok(id)
470 }
471 }
472 }
473
474 fn editable(&self, id: EntityId, meta_only: bool) -> Result<&Entity, CommandError> {
476 let e = self.ov.entity(id).ok_or_else(|| not_found(id))?;
477 if meta_only {
478 return Ok(e);
479 }
480 if !is_builtin(&e.type_id)
481 && let Err(r) = plugin_for(self.registry, e)
482 {
483 return Err(CommandError::ReadOnly { what: id.to_string(), reason: format!("{r:?}") });
484 }
485 if e.locked {
486 return Err(CommandError::ReadOnly { what: id.to_string(), reason: "entity is locked".into() });
487 }
488 if self.ov.layer(e.layer).is_some_and(|l| l.locked) {
489 return Err(CommandError::ReadOnly { what: id.to_string(), reason: "layer is locked".into() });
490 }
491 Ok(e)
492 }
493
494 fn default_layer(&self) -> Result<LayerId, CommandError> {
495 self.ov
496 .layers()
497 .iter()
498 .rev()
499 .find(|l| l.visible && !l.locked)
500 .or_else(|| self.ov.layers().last())
501 .map(|l| l.id)
502 .ok_or_else(|| invalid("layer", "document has no layers"))
503 }
504
505 fn check_props(
507 &self,
508 type_id: &TypeId,
509 props: &mut BTreeMap<String, PropValue>,
510 fill: bool,
511 ) -> Result<(), CommandError> {
512 if is_builtin(type_id) {
513 if type_id.as_str() == types::DIMENSION {
514 for (k, v) in props.iter() {
515 if !v.is_finite() {
516 return Err(invalid(format!("prop {k}"), "not finite"));
517 }
518 }
519 }
520 return Ok(());
521 }
522 let Some(entry) = self.registry.enabled(type_id) else {
523 return Err(CommandError::ReadOnly {
524 what: type_id.to_string(),
525 reason: "plugin type is not registered or disabled".into(),
526 });
527 };
528 let def = &entry.def;
529 for (k, v) in props.iter() {
530 let Some(pd) = def.props.get(k) else {
531 return Err(invalid(format!("prop {k}"), format!("not defined by {type_id}")));
532 };
533 let ok = match (pd, v) {
534 (PropDef::Number { .. }, PropValue::Number(n)) => n.is_finite(),
535 (PropDef::Point { .. }, PropValue::Point(p)) => p.is_finite(),
536 (PropDef::Bool { .. }, PropValue::Bool(_)) => true,
537 (PropDef::Text { max_len, .. }, PropValue::Text(t)) => t.chars().count() <= max_len.unwrap_or(10_000),
538 (PropDef::Enum { values, .. }, PropValue::Text(t)) => values.contains(t),
539 (PropDef::Ref { target, .. }, PropValue::Ref(r)) => match self.ov.entity(r.entity) {
540 None => false,
541 Some(t) => target.as_ref().is_none_or(|tt| *tt == t.type_id),
542 },
543 _ => false,
544 };
545 if !ok {
546 return Err(invalid(format!("prop {k}"), format!("value {} does not match the schema", v.kind_name())));
547 }
548 }
549 if fill {
550 for (k, pd) in &def.props {
551 if props.contains_key(k) {
552 continue;
553 }
554 let v = match pd {
555 PropDef::Number { dim, default, .. } => match default {
556 Some(l) => {
557 Some(PropValue::Number(l.value(dim.dim()).map_err(|e| invalid(format!("prop {k}"), e))?))
558 }
559 None => Some(PropValue::Number(0.0)),
560 },
561 PropDef::Point { default, .. } => {
562 Some(PropValue::Point(default.map_or(Point::ORIGIN, |[x, y]| Point::new(x, y))))
563 }
564 PropDef::Bool { default, .. } => Some(PropValue::Bool(default.unwrap_or(false))),
565 PropDef::Text { default, .. } => Some(PropValue::Text(default.clone().unwrap_or_default())),
566 PropDef::Enum { values, default, .. } => {
567 default.clone().or_else(|| values.first().cloned()).map(PropValue::Text)
568 }
569 PropDef::Ref { required, .. } => {
570 if *required {
571 return Err(invalid(format!("prop {k}"), "required reference is missing"));
572 }
573 None
574 }
575 };
576 if let Some(v) = v {
577 props.insert(k.clone(), v);
578 }
579 }
580 }
581 Ok(())
582 }
583
584 fn record_param_changes(&mut self, before: &Entity, after: &Entity, exact: bool) {
585 let ctx = self.ctx();
586 let names: Vec<String> = if is_builtin(&after.type_id) {
587 after
588 .geometry
589 .as_ref()
590 .map(|g| dotloom_document::builtin::geometry_params(g).into_iter().map(|(n, _, _)| n).collect())
591 .unwrap_or_default()
592 } else {
593 plugin_for(self.registry, after)
594 .map(|d| d.params.iter().map(|p| p.name.clone()).collect())
595 .unwrap_or_default()
596 };
597 let same_shape = match (&before.geometry, &after.geometry) {
598 (Some(a), Some(b)) => {
599 dotloom_document::builtin::geometry_params(a).len()
600 == dotloom_document::builtin::geometry_params(b).len()
601 }
602 _ => true,
603 };
604 let mut edits = Vec::new();
605 for n in names {
606 let (b, a) = (crate::eval::param_value(ctx, before, &n), crate::eval::param_value(ctx, after, &n));
607 if let Some(a) = a
608 && (b != Some(a) || !same_shape)
609 {
610 edits.push((after.id, n, a, exact));
611 }
612 }
613 if before.transform != after.transform {
615 for (id, n, v, _) in self.all_params(after) {
616 if !edits.iter().any(|e| e.0 == id && e.1 == n) {
617 edits.push((id, n, v, exact));
618 }
619 }
620 }
621 self.notes.edits.extend(edits);
622 }
623
624 fn all_params(&self, e: &Entity) -> Vec<(EntityId, String, f64, bool)> {
625 let ctx = self.ctx();
626 let names: Vec<String> = if is_builtin(&e.type_id) {
627 e.geometry
628 .as_ref()
629 .map(|g| dotloom_document::builtin::geometry_params(g).into_iter().map(|(n, _, _)| n).collect())
630 .unwrap_or_default()
631 } else {
632 plugin_for(self.registry, e).map(|d| d.params.iter().map(|p| p.name.clone()).collect()).unwrap_or_default()
633 };
634 names.into_iter().filter_map(|n| crate::eval::param_value(ctx, e, &n).map(|v| (e.id, n, v, true))).collect()
635 }
636
637 pub fn apply(&mut self, cmd: Command) -> Result<(), CommandError> {
639 match cmd {
640 Command::CreateEntity { id, entity } => self.create(id, entity),
641 Command::UpdateEntity { id, patch } => self.update(id, patch),
642 Command::SetParams { values, mode } => self.set_params(values, mode),
643 Command::Transform { ids, transform, policy } => self.transform(&ids, transform, policy),
644 Command::Delete { ids, policy } => self.delete(&ids, policy),
645 Command::Reorder { id, index } => {
646 self.ov.entity(id).ok_or_else(|| not_found(id))?;
647 self.ov.order_ops.push(crate::view::OrderOp::Move(id, index));
648 self.notes.touched.insert(id);
649 Ok(())
650 }
651 Command::AddConstraint { id, constraint } => {
652 let cid = ConstraintId(self.take_id(id.map(|c| c.0))?);
653 let c = Constraint {
654 id: cid,
655 rule: constraint.rule,
656 strength: constraint.strength,
657 enabled: constraint.enabled,
658 label: constraint.label,
659 source: constraint.source.or_else(|| Some("user".into())),
660 owner: None,
661 extra: BTreeMap::new(),
662 };
663 self.check_constraint(&c)?;
664 self.ov.put_constraint(c);
665 self.notes.created_constraints.push(cid);
666 self.notes.touched_constraints.insert(cid);
667 Ok(())
668 }
669 Command::UpdateConstraint { id, patch } => {
670 let mut c = self.ov.constraint(id).cloned().ok_or_else(|| not_found(id))?;
671 if let Some(r) = patch.rule {
672 c.rule = r;
673 }
674 if let Some(s) = patch.strength {
675 c.strength = s;
676 }
677 if let Some(e) = patch.enabled {
678 c.enabled = e;
679 }
680 if let Some(l) = patch.label {
681 c.label = (!l.is_empty()).then_some(l);
682 }
683 self.check_constraint(&c)?;
684 self.ov.put_constraint(c);
685 self.notes.touched_constraints.insert(id);
686 Ok(())
687 }
688 Command::RemoveConstraint { id } => {
689 self.ov.constraint(id).ok_or_else(|| not_found(id))?;
690 self.ov.delete_constraint(id);
691 self.notes.touched_constraints.insert(id);
692 Ok(())
693 }
694 Command::AddLayer { id, name } => {
695 if name.chars().count() > 256 {
696 return Err(invalid("layer name", "too long"));
697 }
698 let lid = LayerId(self.take_id(id.map(|l| l.0))?);
699 self.ov.layers_mut().push(Layer::new(lid, name));
700 Ok(())
701 }
702 Command::UpdateLayer { id, patch } => {
703 let layers = self.ov.layers_mut();
704 let l = layers.iter_mut().find(|l| l.id == id).ok_or_else(|| not_found(id))?;
705 if let Some(n) = patch.name {
706 l.name = n;
707 }
708 if let Some(v) = patch.visible {
709 l.visible = v;
710 }
711 if let Some(v) = patch.locked {
712 l.locked = v;
713 }
714 if let Some(c) = patch.color {
715 l.color = (c.components()[3] != 0).then_some(c);
716 }
717 let members: Vec<EntityId> = self
718 .ov
719 .entity_ids()
720 .into_iter()
721 .filter(|e| self.ov.entity(*e).is_some_and(|x| x.layer == id))
722 .collect();
723 self.notes.touched.extend(members);
724 Ok(())
725 }
726 Command::RemoveLayer { id } => {
727 if self.ov.entity_ids().iter().any(|e| self.ov.entity(*e).is_some_and(|x| x.layer == id)) {
728 return Err(CommandError::HasDependents {
729 what: id.to_string(),
730 reason: "layer is not empty".into(),
731 });
732 }
733 let layers = self.ov.layers_mut();
734 let pos = layers.iter().position(|l| l.id == id).ok_or_else(|| not_found(id))?;
735 if layers.len() == 1 {
736 return Err(invalid("layer", "a document needs at least one layer"));
737 }
738 layers.remove(pos);
739 Ok(())
740 }
741 Command::MoveLayer { id, index } => {
742 let layers = self.ov.layers_mut();
743 let pos = layers.iter().position(|l| l.id == id).ok_or_else(|| not_found(id))?;
744 let l = layers.remove(pos);
745 let i = index.min(layers.len());
746 layers.insert(i, l);
747 let all = self.ov.entity_ids();
748 self.notes.touched.extend(all);
749 Ok(())
750 }
751 Command::Group { id, members, name } => {
752 if members.is_empty() {
753 return Err(invalid("group", "no members"));
754 }
755 for m in &members {
756 self.ov.entity(*m).ok_or_else(|| not_found(m))?;
757 }
758 for gid in self.ov.group_ids() {
760 if let Some(g) = self.ov.group(gid).cloned()
761 && g.members.iter().any(|m| members.contains(m))
762 {
763 let mut g2 = g;
764 g2.members.retain(|m| !members.contains(m));
765 self.ov.groups.insert(gid, Some(g2));
766 }
767 }
768 let gid = GroupId(self.take_id(id.map(|g| g.0))?);
769 self.ov
770 .groups
771 .insert(gid, Some(Group { id: gid, name, members, children: Vec::new(), extra: BTreeMap::new() }));
772 Ok(())
773 }
774 Command::Ungroup { id } => {
775 self.ov.group(id).ok_or_else(|| not_found(id))?;
776 for gid in self.ov.group_ids() {
777 if let Some(g) = self.ov.group(gid).cloned()
778 && g.children.contains(&id)
779 {
780 let mut g2 = g;
781 g2.children.retain(|c| *c != id);
782 self.ov.groups.insert(gid, Some(g2));
783 }
784 }
785 self.ov.groups.insert(id, None);
786 Ok(())
787 }
788 Command::Paste { clipboard, offset } => self.paste(clipboard, offset),
789 Command::Split { id, at, at2 } => self.split(id, at, at2),
790 Command::Trim { id, cutters, pick } => self.trim(id, &cutters, pick),
791 Command::Extend { id, end, boundaries } => self.extend(id, end, &boundaries),
792 Command::SetSettings { patch } => {
793 if let Some(g) = patch.grid_spacing
794 && !(g.is_finite() && g > 0.0)
795 {
796 return Err(invalid("grid spacing", "must be > 0"));
797 }
798 let title = patch.title.clone();
799 let s = self.ov.settings_mut();
800 if let Some(u) = patch.display_unit {
801 s.display_unit = u;
802 }
803 if let Some(g) = patch.grid_spacing {
804 s.grid.spacing = g;
805 }
806 if let Some(t) = patch.time_axis {
807 TimeAxis::new(t.origin_s, t.mm_per_second)?;
808 s.time_axis = Some(t);
809 }
810 if let Some(t) = title {
811 s.extra.insert("title".into(), Value::String(t));
812 }
813 let all = self.ov.entity_ids();
815 self.notes.touched.extend(all);
816 Ok(())
817 }
818 }
819 }
820
821 fn create(&mut self, id: Option<EntityId>, ne: NewEntity) -> Result<(), CommandError> {
822 let type_id = match (&ne.type_id, &ne.geometry) {
823 (Some(t), _) => t.clone(),
824 (None, Some(g)) => dotloom_document::builtin::builtin_type_for(g.kind()),
825 (None, None) => return Err(invalid("entity", "needs a type or geometry")),
826 };
827 if let Some(kind) = builtin_shape_kind(&type_id) {
828 let g = ne.geometry.as_ref().ok_or_else(|| invalid("entity", "built-in type needs geometry"))?;
829 if g.kind() != kind {
830 return Err(invalid("geometry", format!("{type_id} needs {} geometry", kind.name())));
831 }
832 g.validate()?;
833 } else if is_builtin(&type_id) && type_id.as_str() != types::DIMENSION {
834 return Err(invalid("type", format!("unknown built-in type {type_id}")));
835 }
836 let layer = match ne.layer {
837 Some(l) => {
838 let lay = self.ov.layer(l).ok_or_else(|| not_found(l))?;
839 if lay.locked {
840 return Err(CommandError::ReadOnly { what: l.to_string(), reason: "layer is locked".into() });
841 }
842 l
843 }
844 None => self.default_layer()?,
845 };
846 let mut props = ne.props;
847 let given: BTreeSet<String> = props.keys().cloned().collect();
849 self.check_props(&type_id, &mut props, true)?;
850 let eid = EntityId(self.take_id(id.map(|e| e.0))?);
851 let mut e = Entity::new(eid, type_id, layer);
852 e.type_version = self.registry.get(&e.type_id).map_or(1, |t| t.compiled.def.version);
853 e.geometry = ne.geometry;
854 e.props = props;
855 e.data = ne.data;
856 if let Some(t) = ne.transform {
857 if t.inverse().is_err() {
858 return Err(invalid("transform", "singular"));
859 }
860 e.transform = t;
861 }
862 e.name = ne.name.filter(|n| !n.is_empty());
863 if let Some(s) = ne.style {
864 s.validate().map_err(|x| invalid("style", x.to_string()))?;
865 e.style = s;
866 }
867 let builtin = is_builtin(&e.type_id);
871 let pins: Vec<_> = self
872 .all_params(&e)
873 .into_iter()
874 .map(|(id, n, v, _)| {
875 let root = n.split('.').next().unwrap_or(&n);
876 let exact = builtin || given.contains(root);
877 (id, n, v, exact)
878 })
879 .collect();
880 self.ov.put_entity(e);
881 self.notes.edits.extend(pins);
882 self.notes.created.push(eid);
883 self.notes.touched.insert(eid);
884 Ok(())
885 }
886
887 fn update(&mut self, id: EntityId, patch: EntityPatch) -> Result<(), CommandError> {
888 let meta_only = patch.only_meta() && patch.layer.is_none() && patch.style.is_none();
890 let before = self.editable(id, meta_only)?.clone();
891 let mut e = before.clone();
892 if let Some(g) = patch.geometry {
893 if let Some(kind) = builtin_shape_kind(&e.type_id)
894 && g.kind() != kind
895 {
896 return Err(invalid("geometry", format!("{} needs {} geometry", e.type_id, kind.name())));
897 }
898 g.validate()?;
899 e.geometry = Some(g);
900 }
901 let mut set = BTreeMap::new();
902 for (k, v) in patch.props {
903 match v {
904 Some(v) => {
905 set.insert(k, v);
906 }
907 None => {
908 e.props.remove(&k);
909 }
910 }
911 }
912 self.check_props(&e.type_id, &mut set, false)?;
913 e.props.extend(set);
914 for (k, v) in patch.data {
915 match v {
916 Some(v) => {
917 e.data.insert(k, v);
918 }
919 None => {
920 e.data.remove(&k);
921 }
922 }
923 }
924 if let Some(t) = patch.transform {
925 if t.inverse().is_err() || !t.is_finite() {
926 return Err(invalid("transform", "singular or non-finite"));
927 }
928 e.transform = t;
929 }
930 if let Some(n) = patch.name {
931 e.name = (!n.is_empty()).then_some(n);
932 }
933 if let Some(s) = patch.style {
934 s.validate().map_err(|x| invalid("style", x.to_string()))?;
935 e.style = s;
936 }
937 if let Some(l) = patch.layer {
938 self.ov.layer(l).ok_or_else(|| not_found(l))?;
939 e.layer = l;
940 }
941 if let Some(l) = patch.locked {
942 e.locked = l;
943 }
944 if let Some(h) = patch.hidden {
945 e.hidden = h;
946 }
947 self.record_param_changes(&before, &e, true);
948 self.ov.put_entity(e);
949 self.notes.touched.insert(id);
950 Ok(())
951 }
952
953 fn set_params(&mut self, values: Vec<ParamValue>, mode: EditMode) -> Result<(), CommandError> {
954 for pv in values {
955 let before = self.editable(pv.entity, false)?.clone();
956 if !pv.value.is_finite() {
957 return Err(invalid(format!("{}.{}", pv.entity, pv.param), "not finite"));
958 }
959 let mut e = before.clone();
960 if is_builtin(&e.type_id) {
961 let g = e.geometry.as_mut().ok_or_else(|| invalid("entity", "has no geometry"))?;
962 set_geometry_param(g, &pv.param, pv.value).map_err(|x| invalid(pv.param.clone(), x.to_string()))?;
963 } else {
964 let def = plugin_for(self.registry, &e)
965 .map_err(|r| CommandError::ReadOnly { what: pv.entity.to_string(), reason: format!("{r:?}") })?;
966 if let Some((base, axis)) = pv.param.rsplit_once('.') {
967 let Some(PropValue::Point(mut p)) = e.props.get(base).cloned() else {
968 return Err(invalid(pv.param.clone(), "not a point property"));
969 };
970 match axis {
971 "x" => p.x = pv.value,
972 "y" => p.y = pv.value,
973 _ => return Err(invalid(pv.param.clone(), "unknown component")),
974 }
975 e.props.insert(base.to_owned(), PropValue::Point(p));
976 } else if matches!(def.def.props.get(&pv.param), Some(PropDef::Number { .. })) {
977 e.props.insert(pv.param.clone(), PropValue::Number(pv.value));
978 } else {
979 return Err(invalid(pv.param.clone(), "not a numeric property"));
980 }
981 }
982 if mode == EditMode::Prefer {
983 self.notes.edits.push((pv.entity, pv.param.clone(), pv.value, false));
985 self.notes.touched.insert(pv.entity);
986 continue;
987 }
988 self.notes.edits.push((pv.entity, pv.param.clone(), pv.value, true));
989 self.ov.put_entity(e);
990 self.notes.touched.insert(pv.entity);
991 }
992 Ok(())
993 }
994
995 fn transform(&mut self, ids: &[EntityId], t: Affine, policy: TransformPolicy) -> Result<(), CommandError> {
996 if !t.is_finite() || t.inverse().is_err() {
997 return Err(invalid("transform", "singular or non-finite"));
998 }
999 for id in ids {
1000 let before = self.editable(*id, false)?.clone();
1001 let mut e = before.clone();
1002 let mut kind_changed = false;
1003 match e.geometry.clone() {
1004 Some(g) if is_builtin(&e.type_id) => {
1005 let local_t = e.transform.then(t).then(e.transform.inverse()?);
1007 match g.transform(local_t, TransformPolicy::Strict) {
1008 Ok(ng) => e.geometry = Some(ng),
1009 Err(err) => {
1010 let similarity =
1011 matches!(local_t.linear_kind(1e-9), dotloom_geometry::LinearKind::Similarity { .. });
1012 if matches!(g, Shape::Rect(_)) && similarity {
1013 e.transform = e.transform.then(t);
1015 } else if policy == TransformPolicy::Convert {
1016 let converted = g.transform(e.transform.then(t), TransformPolicy::Convert)?;
1017 e.type_id = dotloom_document::builtin::builtin_type_for(converted.kind());
1018 e.geometry = Some(converted);
1019 e.transform = Affine::IDENTITY;
1020 kind_changed = true;
1021 } else {
1022 return Err(err.into());
1023 }
1024 }
1025 }
1026 }
1027 _ => e.transform = e.transform.then(t),
1028 }
1029 self.record_param_changes(&before, &e, true);
1030 self.ov.put_entity(e);
1031 if kind_changed {
1032 self.drop_anchor_constraints(*id);
1033 }
1034 self.notes.touched.insert(*id);
1035 }
1036 Ok(())
1037 }
1038
1039 fn drop_anchor_constraints(&mut self, id: EntityId) {
1043 for cid in self.ov.constraint_ids() {
1044 if self.ov.constraint(cid).is_some_and(|c| c.rule.entities().contains(&id)) {
1045 self.ov.delete_constraint(cid);
1046 self.notes.removed_constraints.push(cid);
1047 self.notes.notes.push(format!("{cid} removed: {id} changed shape kind"));
1048 }
1049 }
1050 let referrers: Vec<EntityId> = self
1051 .ov
1052 .entity_ids()
1053 .into_iter()
1054 .filter(|o| self.ov.entity(*o).is_some_and(|x| x.referenced_entities().any(|t| t == id)))
1055 .collect();
1056 for o in referrers {
1057 if evaluate(self.ctx(), o).error.is_some() {
1058 self.notes.notes.push(format!("{o} no longer resolves: {id} changed shape kind"));
1059 }
1060 }
1061 }
1062
1063 fn delete(&mut self, ids: &[EntityId], policy: DeletePolicy) -> Result<(), CommandError> {
1064 let mut queue: Vec<EntityId> = ids.to_vec();
1065 let mut doomed: BTreeSet<EntityId> = BTreeSet::new();
1066 for id in ids {
1067 self.editable(*id, true)?;
1068 if self.ov.layer(self.ov.entity(*id).map_or(LayerId(0), |e| e.layer)).is_some_and(|l| l.locked) {
1069 return Err(CommandError::ReadOnly { what: id.to_string(), reason: "layer is locked".into() });
1070 }
1071 }
1072 let all_ids = self.ov.entity_ids();
1073 while let Some(id) = queue.pop() {
1074 if !doomed.insert(id) {
1075 continue;
1076 }
1077 for other in &all_ids {
1079 if doomed.contains(other) {
1080 continue;
1081 }
1082 let Some(o) = self.ov.entity(*other) else { continue };
1083 let refs: Vec<String> =
1084 o.props.iter().filter(|(_, v)| v.referenced_entity() == Some(id)).map(|(k, _)| k.clone()).collect();
1085 if refs.is_empty() {
1086 continue;
1087 }
1088 if policy == DeletePolicy::Reject {
1089 return Err(CommandError::HasDependents {
1090 what: id.to_string(),
1091 reason: format!("{other} references it"),
1092 });
1093 }
1094 let rule = if o.type_id.as_str() == types::DIMENSION || is_builtin(&o.type_id) {
1095 OnDelete::Cascade
1096 } else {
1097 plugin_for(self.registry, o)
1098 .ok()
1099 .and_then(|d| {
1100 refs.iter().find_map(|k| match d.def.props.get(k) {
1101 Some(PropDef::Ref { on_delete, required, .. }) => {
1102 Some(if *required && *on_delete == OnDelete::Clear {
1103 OnDelete::Cascade
1104 } else {
1105 *on_delete
1106 })
1107 }
1108 _ => None,
1109 })
1110 })
1111 .unwrap_or(OnDelete::Cascade)
1112 };
1113 match rule {
1114 OnDelete::Cascade => queue.push(*other),
1115 OnDelete::Reject => {
1116 return Err(CommandError::HasDependents {
1117 what: id.to_string(),
1118 reason: format!("{other} references it"),
1119 });
1120 }
1121 OnDelete::Clear => {
1122 let mut o2 = o.clone();
1123 for k in &refs {
1124 o2.props.remove(k);
1125 }
1126 self.ov.put_entity(o2);
1127 self.notes.touched.insert(*other);
1128 self.notes.notes.push(format!("{other}: reference cleared"));
1129 }
1130 }
1131 }
1132 }
1133 for cid in self.ov.constraint_ids() {
1135 let Some(c) = self.ov.constraint(cid) else { continue };
1136 let hit =
1137 c.rule.entities().iter().any(|e| doomed.contains(e)) || c.owner.is_some_and(|o| doomed.contains(&o));
1138 if hit {
1139 if policy == DeletePolicy::Reject {
1140 return Err(CommandError::HasDependents {
1141 what: "entities".into(),
1142 reason: format!("{cid} references them"),
1143 });
1144 }
1145 self.ov.delete_constraint(cid);
1146 self.notes.removed_constraints.push(cid);
1147 }
1148 }
1149 for gid in self.ov.group_ids() {
1151 if let Some(g) = self.ov.group(gid).cloned()
1152 && g.members.iter().any(|m| doomed.contains(m))
1153 {
1154 let mut g2 = g;
1155 g2.members.retain(|m| !doomed.contains(m));
1156 if g2.members.is_empty() && g2.children.is_empty() {
1157 self.ov.groups.insert(gid, None);
1158 } else {
1159 self.ov.groups.insert(gid, Some(g2));
1160 }
1161 }
1162 }
1163 for id in &doomed {
1164 self.ov.delete_entity(*id);
1165 self.notes.deleted.push(*id);
1166 self.notes.touched.insert(*id);
1167 }
1168 Ok(())
1169 }
1170
1171 fn paste(&mut self, clip: Clipboard, offset: Vector) -> Result<(), CommandError> {
1172 if !(offset.x.is_finite() && offset.y.is_finite()) {
1173 return Err(invalid("offset", "not finite"));
1174 }
1175 let mut map = BTreeMap::new();
1176 for e in &clip.entities {
1177 map.insert(e.id, EntityId(self.ov.alloc_id()));
1178 }
1179 let fallback = self.default_layer()?;
1180 let t = Affine::translate(offset);
1181 for e in &clip.entities {
1182 let Some(&nid) = map.get(&e.id) else { continue };
1183 let mut n = e.clone();
1184 n.id = nid;
1185 if self.ov.layer(n.layer).is_none_or(|l| l.locked) {
1186 n.layer = fallback;
1187 }
1188 n.transform = n.transform.then(t);
1189 let mut dropped = Vec::new();
1190 for (k, v) in &mut n.props {
1191 match v {
1192 PropValue::Ref(r) => match map.get(&r.entity) {
1193 Some(m) => r.entity = *m,
1194 None if self.ov.entity(r.entity).is_none() => dropped.push(k.clone()),
1195 None => {}
1196 },
1197 PropValue::Anchor(a) => match map.get(&a.entity) {
1198 Some(m) => a.entity = *m,
1199 None if self.ov.entity(a.entity).is_none() => dropped.push(k.clone()),
1200 None => {}
1201 },
1202 _ => {}
1203 }
1204 }
1205 for k in dropped {
1206 n.props.remove(&k);
1207 self.notes.notes.push(format!("{nid}: reference `{k}` dropped (target not in this document)"));
1208 }
1209 if !is_builtin(&n.type_id) && plugin_for(self.registry, &n).is_ok() {
1210 let mut p = n.props.clone();
1211 self.check_props(&n.type_id.clone(), &mut p, true)?;
1212 n.props = p;
1213 }
1214 let pins = self.all_params(&n);
1215 self.ov.put_entity(n);
1216 self.notes.edits.extend(pins);
1217 self.notes.created.push(nid);
1218 self.notes.touched.insert(nid);
1219 }
1220 for c in &clip.constraints {
1221 let mut n = c.clone();
1222 if !n.rule.remap(&map) {
1223 continue;
1224 }
1225 if let RuleSpec::FixPoint { at, .. } = &mut n.rule {
1226 *at += offset;
1227 }
1228 n.owner = n.owner.and_then(|o| map.get(&o).copied());
1229 n.id = ConstraintId(self.ov.alloc_id());
1230 self.notes.created_constraints.push(n.id);
1231 self.notes.touched_constraints.insert(n.id);
1232 self.ov.put_constraint(n);
1233 }
1234 for g in &clip.groups {
1235 let members: Vec<EntityId> = g.members.iter().filter_map(|m| map.get(m).copied()).collect();
1236 if members.is_empty() {
1237 continue;
1238 }
1239 let gid = GroupId(self.ov.alloc_id());
1240 self.ov.groups.insert(
1241 gid,
1242 Some(Group { id: gid, name: g.name.clone(), members, children: Vec::new(), extra: BTreeMap::new() }),
1243 );
1244 }
1245 Ok(())
1246 }
1247
1248 fn check_constraint(&self, c: &Constraint) -> Result<(), CommandError> {
1249 if !c.rule.values_valid() {
1250 return Err(invalid("constraint", format!("{} has invalid values", c.rule.kind_name())));
1251 }
1252 for e in c.rule.entities() {
1253 self.ov.entity(e).ok_or_else(|| not_found(e))?;
1254 }
1255 let ctx = self.ctx();
1256 for a in c.rule.anchors() {
1257 if evaluate(ctx, a.entity).anchor(&a.anchor).is_none() {
1258 return Err(invalid("constraint", format!("{} has no anchor `{}`", a.entity, a.anchor)));
1259 }
1260 }
1261 for p in c.rule.params() {
1262 let name = match &p.slot {
1263 dotloom_document::ParamSlot::Geom(n) | dotloom_document::ParamSlot::Prop(n) => n,
1264 };
1265 let e = self.ov.entity(p.entity).ok_or_else(|| not_found(p.entity))?;
1266 if crate::eval::param_value(ctx, e, name).is_none() {
1267 return Err(invalid("constraint", format!("{} has no numeric parameter `{name}`", p.entity)));
1268 }
1269 }
1270 Ok(())
1271 }
1272
1273 fn world_shape(&self, id: EntityId) -> Result<Shape, CommandError> {
1274 let e = self.ov.entity(id).ok_or_else(|| not_found(id))?;
1275 let g = e.geometry.as_ref().ok_or_else(|| invalid("entity", format!("{id} has no editable geometry")))?;
1276 Ok(g.transform(e.transform, TransformPolicy::Convert)?)
1277 }
1278
1279 fn set_world_geometry(&mut self, id: EntityId, world: Shape) -> Result<(), CommandError> {
1280 let before = self.editable(id, false)?.clone();
1281 let mut e = before.clone();
1282 let inv = e.transform.inverse()?;
1283 let local = world.transform(inv, TransformPolicy::Strict)?;
1284 e.type_id = dotloom_document::builtin::builtin_type_for(local.kind());
1285 e.geometry = Some(local);
1286 let anchors_before = evaluate(self.ctx(), id).anchors;
1287 self.ov.put_entity(e.clone());
1288 let after = evaluate(self.ctx(), id);
1290 let moved: BTreeSet<String> = anchors_before
1291 .iter()
1292 .filter(|a| {
1293 after.anchor(&a.name).is_none_or(|p| p.distance(a.point) > 1e-9 * (1.0 + a.point.to_vector().length()))
1294 })
1295 .map(|a| a.name.to_string())
1296 .collect();
1297 for cid in self.ov.constraint_ids() {
1298 let Some(c) = self.ov.constraint(cid) else { continue };
1299 let refs_moved = c.rule.anchors().iter().any(|a| a.entity == id && moved.contains(&a.anchor))
1300 || (!c.rule.params().is_empty() && c.rule.params().iter().any(|p| p.entity == id))
1301 || (c.rule.entities().contains(&id) && c.rule.anchors().is_empty() && c.rule.params().is_empty());
1302 if refs_moved {
1303 self.ov.delete_constraint(cid);
1304 self.notes.removed_constraints.push(cid);
1305 self.notes.notes.push(format!("{cid} removed: its anchor on {id} changed"));
1306 }
1307 }
1308 let pins = self.all_params(&e);
1309 self.notes.edits.extend(pins);
1310 self.notes.touched.insert(id);
1311 Ok(())
1312 }
1313
1314 fn split(&mut self, id: EntityId, at: Point, at2: Option<Point>) -> Result<(), CommandError> {
1315 self.editable(id, false)?;
1316 let world = self.world_shape(id)?;
1317 let tol = ModelTolerance::DEFAULT;
1318 let pieces: Vec<Shape> = match (&world, at2) {
1319 (Shape::Circle(c), Some(b)) => {
1320 dotloom_geometry::edit::split_circle(*c, at, b, tol)?.into_iter().map(Shape::Arc).collect()
1321 }
1322 _ => dotloom_geometry::edit::split_at(&world, at, tol)?,
1323 };
1324 let mut it = pieces.into_iter();
1325 let first = it.next().ok_or_else(|| invalid("split", "no pieces"))?;
1326 let base = self.ov.entity(id).cloned().ok_or_else(|| not_found(id))?;
1327 self.set_world_geometry(id, first)?;
1328 for p in it {
1329 let nid = EntityId(self.ov.alloc_id());
1330 let mut n = base.clone();
1331 n.id = nid;
1332 n.transform = Affine::IDENTITY;
1333 n.type_id = dotloom_document::builtin::builtin_type_for(p.kind());
1334 n.geometry = Some(p);
1335 let pins = self.all_params(&n);
1336 self.ov.put_entity(n);
1337 self.notes.edits.extend(pins);
1338 self.notes.created.push(nid);
1339 self.notes.touched.insert(nid);
1340 }
1341 Ok(())
1342 }
1343
1344 fn trim(&mut self, id: EntityId, cutters: &[EntityId], pick: Point) -> Result<(), CommandError> {
1345 self.editable(id, false)?;
1346 let world = self.world_shape(id)?;
1347 let ctx = self.ctx();
1348 let cut: Vec<Shape> =
1349 cutters.iter().filter(|c| **c != id).flat_map(|c| evaluate(ctx, *c).shapes().collect::<Vec<_>>()).collect();
1350 let pieces = dotloom_geometry::edit::trim(&world, &cut, pick, ModelTolerance::DEFAULT)?;
1351 let base = self.ov.entity(id).cloned().ok_or_else(|| not_found(id))?;
1352 let mut it = pieces.into_iter();
1353 match it.next() {
1354 None => return self.delete(&[id], DeletePolicy::Cascade),
1355 Some(first) => self.set_world_geometry(id, first)?,
1356 }
1357 for p in it {
1358 let nid = EntityId(self.ov.alloc_id());
1359 let mut n = base.clone();
1360 n.id = nid;
1361 n.transform = Affine::IDENTITY;
1362 n.type_id = dotloom_document::builtin::builtin_type_for(p.kind());
1363 n.geometry = Some(p);
1364 let pins = self.all_params(&n);
1365 self.ov.put_entity(n);
1366 self.notes.edits.extend(pins);
1367 self.notes.created.push(nid);
1368 self.notes.touched.insert(nid);
1369 }
1370 Ok(())
1371 }
1372
1373 fn extend(&mut self, id: EntityId, end: CurveEnd, boundaries: &[EntityId]) -> Result<(), CommandError> {
1374 self.editable(id, false)?;
1375 let world = self.world_shape(id)?;
1376 let ctx = self.ctx();
1377 let bounds: Vec<Shape> = boundaries
1378 .iter()
1379 .filter(|b| **b != id)
1380 .flat_map(|b| evaluate(ctx, *b).shapes().collect::<Vec<_>>())
1381 .collect();
1382 let out = dotloom_geometry::edit::extend(&world, end, &bounds, ModelTolerance::DEFAULT)?;
1383 self.set_world_geometry(id, out)
1384 }
1385}