Skip to main content

dotloom_engine/
command.rs

1//! Commands and their application to a transaction overlay.
2
3use 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/// How numeric edits are applied.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub enum EditMode {
29    /// The value is a hard requirement of this transaction (rejected if infeasible).
30    #[default]
31    Exact,
32    /// The value is a strong preference (the solver may deviate to satisfy rules).
33    Prefer,
34}
35
36/// A numeric parameter assignment.
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub struct ParamValue {
39    /// Entity.
40    pub entity: EntityId,
41    /// Parameter (`width`, `a.x`, `start.x`, `r`, ...).
42    pub param: String,
43    /// Value in canonical units.
44    pub value: f64,
45}
46
47/// Data for a new entity.
48#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase")]
50pub struct NewEntity {
51    /// Type.
52    #[serde(rename = "type")]
53    pub type_id: Option<TypeId>,
54    /// Layer (default: first unlocked visible layer).
55    #[serde(default)]
56    pub layer: Option<LayerId>,
57    /// Geometry (built-in types).
58    #[serde(default)]
59    pub geometry: Option<Shape>,
60    /// Properties (missing ones get defaults).
61    #[serde(default)]
62    pub props: BTreeMap<String, PropValue>,
63    /// Transform.
64    #[serde(default)]
65    pub transform: Option<Affine>,
66    /// Name.
67    #[serde(default)]
68    pub name: Option<String>,
69    /// Style.
70    #[serde(default)]
71    pub style: Option<Style>,
72    /// Opaque payloads.
73    #[serde(default)]
74    pub data: BTreeMap<String, Value>,
75}
76
77/// Partial update of an entity. `props` entries set to `null` remove the property.
78#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct EntityPatch {
81    /// New geometry.
82    #[serde(default)]
83    pub geometry: Option<Shape>,
84    /// Property changes.
85    #[serde(default)]
86    pub props: BTreeMap<String, Option<PropValue>>,
87    /// New transform.
88    #[serde(default)]
89    pub transform: Option<Affine>,
90    /// New name (`""` clears).
91    #[serde(default)]
92    pub name: Option<String>,
93    /// New style.
94    #[serde(default)]
95    pub style: Option<Style>,
96    /// New layer.
97    #[serde(default)]
98    pub layer: Option<LayerId>,
99    /// Lock state.
100    #[serde(default)]
101    pub locked: Option<bool>,
102    /// Visibility.
103    #[serde(default)]
104    pub hidden: Option<bool>,
105    /// Payload changes (`null` removes).
106    #[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/// Constraint content (the engine assigns or validates the ID).
117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct ConstraintSpec {
120    /// Rule.
121    pub rule: RuleSpec,
122    /// Strength.
123    #[serde(default)]
124    pub strength: StrengthSpec,
125    /// Enabled.
126    #[serde(default = "yes")]
127    pub enabled: bool,
128    /// Label.
129    #[serde(default)]
130    pub label: Option<String>,
131    /// Source.
132    #[serde(default)]
133    pub source: Option<String>,
134}
135
136fn yes() -> bool {
137    true
138}
139
140/// Partial constraint update.
141#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct ConstraintPatch {
144    /// New rule.
145    #[serde(default)]
146    pub rule: Option<RuleSpec>,
147    /// New strength.
148    #[serde(default)]
149    pub strength: Option<StrengthSpec>,
150    /// Enable/disable.
151    #[serde(default)]
152    pub enabled: Option<bool>,
153    /// New label.
154    #[serde(default)]
155    pub label: Option<String>,
156}
157
158/// Layer update.
159#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct LayerPatch {
162    /// Name.
163    #[serde(default)]
164    pub name: Option<String>,
165    /// Visible.
166    #[serde(default)]
167    pub visible: Option<bool>,
168    /// Locked.
169    #[serde(default)]
170    pub locked: Option<bool>,
171    /// Color (`null` keeps; use `"#00000000"` for none).
172    #[serde(default)]
173    pub color: Option<Color>,
174}
175
176/// Settings update.
177#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
178#[serde(rename_all = "camelCase")]
179pub struct SettingsPatch {
180    /// Display unit.
181    #[serde(default)]
182    pub display_unit: Option<LengthUnit>,
183    /// Grid spacing (mm).
184    #[serde(default)]
185    pub grid_spacing: Option<f64>,
186    /// Time axis (`null` keeps).
187    #[serde(default)]
188    pub time_axis: Option<TimeAxis>,
189    /// Title.
190    #[serde(default)]
191    pub title: Option<String>,
192}
193
194/// What to do with constraints and referencing entities when deleting.
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub enum DeletePolicy {
198    /// Delete dependent constraints and apply each reference's `onDelete` policy.
199    #[default]
200    Cascade,
201    /// Fail if anything depends on the deleted entities.
202    Reject,
203}
204
205/// One command of a transaction.
206#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
207#[serde(tag = "op", rename_all = "camelCase", rename_all_fields = "camelCase")]
208pub enum Command {
209    /// Create an entity (with a reserved or freshly allocated ID).
210    CreateEntity {
211        /// Reserved ID (see `Engine::reserve_ids`).
212        #[serde(default)]
213        id: Option<EntityId>,
214        /// Content.
215        entity: NewEntity,
216    },
217    /// Patch an entity.
218    UpdateEntity {
219        /// Entity.
220        id: EntityId,
221        /// Changes.
222        patch: EntityPatch,
223    },
224    /// Set numeric parameters through the solver.
225    SetParams {
226        /// Values.
227        values: Vec<ParamValue>,
228        /// Exact (hard) or preferred.
229        #[serde(default)]
230        mode: EditMode,
231    },
232    /// Transform entities (move/rotate/scale/mirror).
233    Transform {
234        /// Entities.
235        ids: Vec<EntityId>,
236        /// World transform to apply.
237        transform: Affine,
238        /// Policy for shapes that cannot represent the result.
239        #[serde(default)]
240        policy: TransformPolicy,
241    },
242    /// Delete entities.
243    Delete {
244        /// Entities.
245        ids: Vec<EntityId>,
246        /// Dependency policy.
247        #[serde(default)]
248        policy: DeletePolicy,
249    },
250    /// Move an entity in draw order.
251    Reorder {
252        /// Entity.
253        id: EntityId,
254        /// New index (0 = bottom).
255        index: usize,
256    },
257    /// Add a constraint.
258    AddConstraint {
259        /// Reserved ID.
260        #[serde(default)]
261        id: Option<ConstraintId>,
262        /// Content.
263        constraint: ConstraintSpec,
264    },
265    /// Update a constraint.
266    UpdateConstraint {
267        /// Constraint.
268        id: ConstraintId,
269        /// Changes.
270        patch: ConstraintPatch,
271    },
272    /// Remove a constraint.
273    RemoveConstraint {
274        /// Constraint.
275        id: ConstraintId,
276    },
277    /// Add a layer.
278    AddLayer {
279        /// Reserved ID.
280        #[serde(default)]
281        id: Option<LayerId>,
282        /// Name.
283        name: String,
284    },
285    /// Update a layer.
286    UpdateLayer {
287        /// Layer.
288        id: LayerId,
289        /// Changes.
290        patch: LayerPatch,
291    },
292    /// Remove an empty layer.
293    RemoveLayer {
294        /// Layer.
295        id: LayerId,
296    },
297    /// Move a layer in the stack.
298    MoveLayer {
299        /// Layer.
300        id: LayerId,
301        /// New index (0 = bottom).
302        index: usize,
303    },
304    /// Group entities.
305    Group {
306        /// Reserved ID.
307        #[serde(default)]
308        id: Option<GroupId>,
309        /// Members.
310        members: Vec<EntityId>,
311        /// Name.
312        #[serde(default)]
313        name: Option<String>,
314    },
315    /// Dissolve a group (members stay).
316    Ungroup {
317        /// Group.
318        id: GroupId,
319    },
320    /// Paste clipboard content.
321    Paste {
322        /// Content.
323        clipboard: Clipboard,
324        /// Offset.
325        offset: Vector,
326    },
327    /// Split a line/arc/polyline at a point (circles need `at2`).
328    Split {
329        /// Entity.
330        id: EntityId,
331        /// Split point (world).
332        at: Point,
333        /// Second point for circles.
334        #[serde(default)]
335        at2: Option<Point>,
336    },
337    /// Trim the piece of an entity between cutting edges around `pick`.
338    Trim {
339        /// Entity.
340        id: EntityId,
341        /// Cutting entities.
342        cutters: Vec<EntityId>,
343        /// Pick point (world).
344        pick: Point,
345    },
346    /// Extend an end of a line/arc/polyline to boundaries.
347    Extend {
348        /// Entity.
349        id: EntityId,
350        /// Which end.
351        end: CurveEnd,
352        /// Boundary entities.
353        boundaries: Vec<EntityId>,
354    },
355    /// Update document settings.
356    SetSettings {
357        /// Changes.
358        patch: SettingsPatch,
359    },
360}
361
362/// Command errors (the transaction is rejected, nothing changes).
363#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
364#[serde(tag = "code", rename_all = "camelCase")]
365#[non_exhaustive]
366pub enum CommandError {
367    /// Unknown entity/constraint/layer/group.
368    #[error("{what} does not exist")]
369    NotFound {
370        /// Object.
371        what: String,
372    },
373    /// The entity cannot be edited (plugin missing/disabled/newer, locked).
374    #[error("{what} is read-only: {reason}")]
375    ReadOnly {
376        /// Object.
377        what: String,
378        /// Reason.
379        reason: String,
380    },
381    /// Invalid input.
382    #[error("invalid {what}: {reason}")]
383    Invalid {
384        /// What.
385        what: String,
386        /// Why.
387        reason: String,
388    },
389    /// Deletion refused because of dependents.
390    #[error("cannot delete {what}: {reason}")]
391    HasDependents {
392        /// What.
393        what: String,
394        /// Dependents.
395        reason: String,
396    },
397    /// Geometry operation failed.
398    #[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/// Side results of applying commands.
409#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
410#[serde(rename_all = "camelCase")]
411pub struct ApplyNotes {
412    /// Entities created (in order).
413    pub created: Vec<EntityId>,
414    /// Constraints created.
415    pub created_constraints: Vec<ConstraintId>,
416    /// Entities deleted (including cascades).
417    pub deleted: Vec<EntityId>,
418    /// Constraints removed implicitly (cascade, split/trim).
419    pub removed_constraints: Vec<ConstraintId>,
420    /// Explicit numeric edits `(entity, param, value, exact)`.
421    #[serde(skip)]
422    pub edits: Vec<(EntityId, String, f64, bool)>,
423    /// Entities whose content changed.
424    #[serde(skip)]
425    pub touched: BTreeSet<EntityId>,
426    /// Constraints added/changed.
427    #[serde(skip)]
428    pub touched_constraints: BTreeSet<ConstraintId>,
429    /// Messages (dropped references, ...).
430    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
441/// Applies commands to an overlay.
442pub(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    /// The entity must exist and be editable (plugin available, not locked).
475    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    /// Validate properties against the type schema and fill defaults.
506    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        // Transform changes move every anchor: pin all parameters.
614        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    /// Apply one command.
638    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                // Members leave their previous groups.
759                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                // Dimensions and time-based entities depend on settings.
814                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        // Properties the caller set explicitly (before defaults are filled in).
848        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        // New entities are pinned where they were created: explicit values (and
868        // built-in geometry, which is always explicit) are exact; defaults of plugin
869        // properties are strong preferences, so rules may adapt them.
870        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        // Unlocking/hiding is allowed on locked entities; content edits are not.
889        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                // Keep the old value in the overlay; the solver moves towards the target.
984                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                    // The world transform `t` expressed in local coordinates.
1006                    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                                // Rotated rectangles keep their semantics in the entity transform.
1014                                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    /// Remove constraints that reference an entity whose kind changed. Entities that
1040    /// reference it (dimensions, plugin references) are user content and stay; the
1041    /// ones that no longer resolve are reported.
1042    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            // Referencing entities.
1078            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        // Constraints.
1134        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        // Groups.
1150        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        // Constraints on anchors that moved or disappeared are removed.
1289        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}