Skip to main content

dotloom_engine/
engine.rs

1//! The engine: the single editable owner of a document.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use dotloom_constraints::{Progress, SolveJob, SolveOptions, Status};
6use dotloom_document::{ConstraintId, Document, EntityId, Limits, TypeId};
7use dotloom_geometry::SpatialIndex;
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    Command, DocView, Overlay,
12    command::{Applier, ApplyNotes},
13    error::{EngineError, SolveFailure},
14    eval::{Ctx, EvalCache},
15    history::{Change, History},
16    index::DepIndex,
17    registry::{EntityTypeDef, Registry},
18    scene_build::SceneState,
19    solve::{self, Plan},
20    view::OverlayData,
21};
22
23/// A group of commands applied atomically.
24#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct Transaction {
27    /// Label shown in history.
28    #[serde(default)]
29    pub label: String,
30    /// Commands.
31    pub commands: Vec<Command>,
32}
33
34impl Transaction {
35    /// Transaction with a label.
36    #[must_use]
37    pub fn new(label: impl Into<String>, commands: Vec<Command>) -> Self {
38        Self { label: label.into(), commands }
39    }
40}
41
42/// Options of a single request.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct ApplyOptions {
46    /// Reject the request unless the document is at this revision.
47    #[serde(default)]
48    pub expected_revision: Option<u64>,
49}
50
51/// Successful commit.
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct CommitReport {
55    /// New revision.
56    pub revision: u64,
57    /// Solver status of the affected rules (`solved` when nothing was solved).
58    pub status: Status,
59    /// Non-fatal diagnostics (redundant rules, ...).
60    pub diagnostics: Vec<crate::error::DiagnosticReport>,
61    /// Created entities.
62    pub created: Vec<EntityId>,
63    /// Created constraints.
64    pub created_constraints: Vec<ConstraintId>,
65    /// Deleted entities.
66    pub deleted: Vec<EntityId>,
67    /// Constraints removed as a consequence.
68    pub removed_constraints: Vec<ConstraintId>,
69    /// Entities whose content changed.
70    pub changed: Vec<EntityId>,
71    /// Notes.
72    pub notes: Vec<String>,
73    /// Whether the change can be undone (false if it exceeded the history budget).
74    pub undo_available: bool,
75    /// Solver statistics, when rules were solved.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub solver: Option<SolverStats>,
78}
79
80/// What the solver did for a commit (diagnostics and performance work).
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct SolverStats {
84    /// Iterations of the chosen attempt.
85    pub iterations: u32,
86    /// Solve attempts run (pinned, exact, relaxed).
87    pub attempts: u32,
88    /// Solver variables of the chosen attempt.
89    pub variables: u32,
90    /// Rules of the chosen attempt.
91    pub rules: u32,
92    /// Connected components solved.
93    pub components: u32,
94}
95
96/// Engine event, emitted after state changes (never during a commit).
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
98#[serde(tag = "type", rename_all = "camelCase", rename_all_fields = "camelCase")]
99pub enum Event {
100    /// A transaction, undo or redo was committed.
101    Committed {
102        /// New revision.
103        revision: u64,
104        /// History label.
105        label: String,
106        /// `apply` | `undo` | `redo` | `drag` | `load`.
107        cause: String,
108        /// Changed entities (created, updated, deleted).
109        entities: Vec<EntityId>,
110        /// Changed constraints.
111        constraints: Vec<ConstraintId>,
112    },
113    /// Selection changed.
114    SelectionChanged {
115        /// Selected entities.
116        selection: Vec<EntityId>,
117    },
118    /// Undo/redo availability changed.
119    HistoryChanged {
120        /// Undo possible.
121        can_undo: bool,
122        /// Redo possible.
123        can_redo: bool,
124        /// Next undo label.
125        undo_label: Option<String>,
126        /// Next redo label.
127        redo_label: Option<String>,
128    },
129    /// Plugin types changed.
130    PluginsChanged,
131}
132
133/// Engine configuration.
134#[derive(Debug, Clone, Copy, PartialEq)]
135pub struct EngineOptions {
136    /// Solver options for commits.
137    pub solve: SolveOptions,
138    /// History entry limit.
139    pub history_entries: usize,
140    /// History byte budget.
141    pub history_bytes: usize,
142    /// Document limits.
143    pub limits: Limits,
144}
145
146impl Default for EngineOptions {
147    fn default() -> Self {
148        Self {
149            solve: SolveOptions::default(),
150            history_entries: 500,
151            history_bytes: 64 << 20,
152            limits: Limits::default(),
153        }
154    }
155}
156
157/// A commit whose solve runs in budgeted steps.
158#[derive(Debug)]
159pub(crate) struct Pending {
160    pub id: u64,
161    pub base_revision: u64,
162    pub data: OverlayData,
163    pub notes: ApplyNotes,
164    pub label: String,
165    pub cause: &'static str,
166    pub job: Option<(SolveJob, dotloom_constraints::Problem, Plan)>,
167    /// Further attempts, tried in order when the current one is infeasible or
168    /// misses a requested value.
169    pub fallbacks: std::collections::VecDeque<(dotloom_constraints::Problem, Plan)>,
170    /// First accepted result, used if every later attempt fails.
171    pub first: Option<(dotloom_constraints::Solution, dotloom_constraints::Problem, Plan)>,
172    /// The attempt chosen for committing.
173    pub chosen: Option<(dotloom_constraints::Solution, dotloom_constraints::Problem, Plan)>,
174    /// Attempts started so far.
175    pub attempts: u32,
176}
177
178/// Progress of a pending commit.
179#[derive(Debug, Clone, PartialEq)]
180pub enum PendingState {
181    /// More steps needed.
182    Running {
183        /// Request.
184        id: u64,
185    },
186    /// Finished (boxed: the report is much larger than the running state).
187    Done(Box<Result<CommitReport, EngineError>>),
188}
189
190/// The Dotloom engine.
191#[derive(Debug)]
192pub struct Engine {
193    pub(crate) doc: Document,
194    pub(crate) registry: Registry,
195    pub(crate) history: History,
196    pub(crate) revision: u64,
197    pub(crate) selection: BTreeSet<EntityId>,
198    pub(crate) reserved: BTreeSet<u64>,
199    pub(crate) cache: EvalCache,
200    pub(crate) index: SpatialIndex<EntityId>,
201    pub(crate) deps: DepIndex,
202    pub(crate) scene: SceneState,
203    pub(crate) events: Vec<Event>,
204    pub(crate) pending: Option<Pending>,
205    pub(crate) drag: Option<crate::drag::DragSession>,
206    pub(crate) options: EngineOptions,
207    next_pending: u64,
208}
209
210impl Default for Engine {
211    fn default() -> Self {
212        Self::new(EngineOptions::default())
213    }
214}
215
216impl Engine {
217    /// Engine with an empty document.
218    #[must_use]
219    pub fn new(options: EngineOptions) -> Self {
220        let mut e = Self {
221            doc: Document::new(),
222            registry: Registry::new(),
223            history: History::with_limits(options.history_entries, options.history_bytes),
224            revision: 0,
225            selection: BTreeSet::new(),
226            reserved: BTreeSet::new(),
227            cache: EvalCache::default(),
228            index: SpatialIndex::new(),
229            deps: DepIndex::default(),
230            scene: SceneState::default(),
231            events: Vec::new(),
232            pending: None,
233            drag: None,
234            options,
235            next_pending: 1,
236        };
237        e.rebuild_derived();
238        e
239    }
240
241    /// The committed document (read-only).
242    #[must_use]
243    pub fn document(&self) -> &Document {
244        &self.doc
245    }
246
247    /// Current revision (increments on every commit, undo and redo).
248    #[must_use]
249    pub fn revision(&self) -> u64 {
250        self.revision
251    }
252
253    /// Type registry.
254    #[must_use]
255    pub fn registry(&self) -> &Registry {
256        &self.registry
257    }
258
259    pub(crate) fn ctx(&self) -> Ctx<'_> {
260        Ctx { view: &self.doc, registry: &self.registry }
261    }
262
263    /// Replace the document. Entity type migrations of registered plugins are
264    /// applied; the document is validated; history is cleared.
265    pub fn load(&mut self, mut doc: Document) -> Result<u64, EngineError> {
266        if self.pending.is_some() || self.drag.is_some() {
267            return Err(EngineError::Busy { reason: "an interaction is in progress".into() });
268        }
269        crate::migrate::migrate_entities(&mut doc, &self.registry);
270        if let Some(err) = doc.validate_all(&self.options.limits).into_iter().next() {
271            return Err(EngineError::Load { message: err.to_string() });
272        }
273        self.doc = doc;
274        self.history.clear();
275        self.selection.clear();
276        self.reserved.clear();
277        self.revision += 1;
278        self.rebuild_derived();
279        self.scene.reset = true;
280        self.events.push(Event::Committed {
281            revision: self.revision,
282            label: "load".into(),
283            cause: "load".into(),
284            entities: Vec::new(),
285            constraints: Vec::new(),
286        });
287        self.emit_history();
288        self.events.push(Event::SelectionChanged { selection: Vec::new() });
289        Ok(self.revision)
290    }
291
292    fn rebuild_derived(&mut self) {
293        self.cache.clear();
294        self.deps = DepIndex::build(&self.doc);
295        let ctx = Ctx { view: &self.doc, registry: &self.registry };
296        let mut items = Vec::with_capacity(self.doc.entity_count());
297        for e in self.doc.entities() {
298            let bb = self.cache.get(ctx, e.id).bbox;
299            items.push((e.id, bb));
300        }
301        self.index = SpatialIndex::bulk_load(items);
302        self.scene.reset = true;
303    }
304
305    // --- plugins --------------------------------------------------------------
306
307    /// Register a plugin entity type.
308    pub fn register_type(&mut self, def: EntityTypeDef, plugin: &str) -> Result<(), EngineError> {
309        let t = def.type_id.clone();
310        self.registry.register(def, plugin).map_err(|e| EngineError::Plugin { message: e.to_string() })?;
311        crate::migrate::migrate_entities(&mut self.doc, &self.registry);
312        self.refresh_type(&t);
313        self.events.push(Event::PluginsChanged);
314        Ok(())
315    }
316
317    /// Remove a plugin entity type (its entities become read-only, data is kept).
318    pub fn unregister_type(&mut self, t: &TypeId) -> Result<(), EngineError> {
319        self.registry.unregister(t).map_err(|e| EngineError::Plugin { message: e.to_string() })?;
320        self.refresh_type(t);
321        self.events.push(Event::PluginsChanged);
322        Ok(())
323    }
324
325    /// Enable or disable a plugin entity type.
326    pub fn set_type_enabled(&mut self, t: &TypeId, enabled: bool) -> Result<(), EngineError> {
327        self.registry.set_enabled(t, enabled).map_err(|e| EngineError::Plugin { message: e.to_string() })?;
328        self.refresh_type(t);
329        self.events.push(Event::PluginsChanged);
330        Ok(())
331    }
332
333    fn refresh_type(&mut self, t: &TypeId) {
334        let ids: Vec<EntityId> = self.doc.entities().filter(|e| &e.type_id == t).map(|e| e.id).collect();
335        let affected = self.dependents_closure(&ids);
336        self.refresh_entities(&affected);
337    }
338
339    /// Entities whose evaluation depends on `ids` (transitively through references).
340    pub(crate) fn dependents_closure(&self, ids: &[EntityId]) -> BTreeSet<EntityId> {
341        let mut out: BTreeSet<EntityId> = BTreeSet::new();
342        let mut q: Vec<EntityId> = ids.to_vec();
343        while let Some(id) = q.pop() {
344            if out.insert(id) {
345                q.extend(self.deps.referrers_of(id));
346            }
347        }
348        out
349    }
350
351    /// Re-evaluate entities, update the spatial index and mark them for the scene.
352    pub(crate) fn refresh_entities(&mut self, ids: &BTreeSet<EntityId>) {
353        self.cache.invalidate(ids.iter().copied());
354        let ctx = Ctx { view: &self.doc, registry: &self.registry };
355        for id in ids {
356            if self.doc.entity(*id).is_some() {
357                let bb = self.cache.get(ctx, *id).bbox;
358                self.index.upsert(*id, bb);
359                self.scene.dirty.insert(*id);
360            } else {
361                self.index.remove(*id);
362                self.scene.removed.insert(*id);
363            }
364        }
365    }
366
367    // --- ids ------------------------------------------------------------------
368
369    /// Reserve fresh IDs for use in the next transactions (e.g. a tool that
370    /// creates a line and a constraint on it in one transaction).
371    pub fn reserve_ids(&mut self, n: usize) -> Vec<u64> {
372        let n = n.min(10_000);
373        let mut out = Vec::with_capacity(n);
374        for _ in 0..n {
375            let id = self.doc.alloc_id();
376            self.reserved.insert(id);
377            out.push(id);
378        }
379        out
380    }
381
382    // --- transactions ---------------------------------------------------------
383
384    fn check_revision(&self, opts: ApplyOptions) -> Result<(), EngineError> {
385        match opts.expected_revision {
386            Some(r) if r != self.revision => Err(EngineError::Stale { expected: r, actual: self.revision }),
387            _ => Ok(()),
388        }
389    }
390
391    /// Apply a transaction synchronously.
392    pub fn apply(&mut self, tx: Transaction, opts: ApplyOptions) -> Result<CommitReport, EngineError> {
393        let id = self.begin_apply(tx, opts)?;
394        loop {
395            match self.step_pending(u32::MAX)? {
396                PendingState::Done(r) => return *r,
397                PendingState::Running { id: pid } if pid == id => {}
398                PendingState::Running { .. } => {
399                    return Err(EngineError::Busy { reason: "unexpected pending request".into() });
400                }
401            }
402        }
403    }
404
405    /// Start a transaction whose solve runs in budgeted steps
406    /// ([`Engine::step_pending`]). Returns the pending request ID.
407    pub fn begin_apply(&mut self, tx: Transaction, opts: ApplyOptions) -> Result<u64, EngineError> {
408        if self.pending.is_some() {
409            return Err(EngineError::Busy { reason: "another transaction is being solved".into() });
410        }
411        if self.drag.is_some() {
412            return Err(EngineError::Busy { reason: "a drag is in progress".into() });
413        }
414        self.check_revision(opts)?;
415        let (data, notes) = self.apply_commands(tx.commands)?;
416        self.start_pending(data, notes, if tx.label.is_empty() { "edit".into() } else { tx.label }, "apply", false)
417    }
418
419    pub(crate) fn apply_commands(&self, commands: Vec<Command>) -> Result<(OverlayData, ApplyNotes), EngineError> {
420        if commands.len() > 100_000 {
421            return Err(EngineError::Invalid { message: "too many commands in one transaction".into() });
422        }
423        let mut ov = Overlay::new(&self.doc);
424        let notes = {
425            let mut ap = Applier {
426                ov: &mut ov,
427                registry: &self.registry,
428                reserved: &self.reserved,
429                notes: ApplyNotes::default(),
430            };
431            for c in commands {
432                ap.apply(c)?;
433            }
434            ap.notes
435        };
436        crate::validate::validate_overlay(&ov, &self.registry, &notes, &self.options.limits)?;
437        Ok((ov.into_data(), notes))
438    }
439
440    pub(crate) fn start_pending(
441        &mut self,
442        data: OverlayData,
443        notes: ApplyNotes,
444        label: String,
445        cause: &'static str,
446        prefer: bool,
447    ) -> Result<u64, EngineError> {
448        let ov = Overlay::from_data(&self.doc, data);
449        // Attempts (see ADR-0004 §11): requested values exact with the rest pinned,
450        // then exact with everything free (by stay priority), then — only when some
451        // values are preferences — the relaxed nearest-feasible solve.
452        let soft = prefer || notes.edits.iter().any(|e| !e.3);
453        let attempts: &[(bool, bool)] =
454            if soft { &[(true, true), (false, true), (false, false)] } else { &[(true, false), (false, false)] };
455        let mut plans: std::collections::VecDeque<_> = attempts
456            .iter()
457            .filter_map(|&(pin, hard)| {
458                let a = solve::Attempt { target: None, pin, hard_target: hard };
459                solve::plan_with(&ov, &self.deps, &self.registry, &notes, prefer, a)
460            })
461            .collect();
462        let job = plans.pop_front().map(|(problem, plan)| {
463            let job = SolveJob::new(problem.clone(), self.options.solve);
464            (job, problem, plan)
465        });
466        let id = self.next_pending;
467        self.next_pending += 1;
468        self.pending = Some(Pending {
469            id,
470            base_revision: self.revision,
471            data: ov.into_data(),
472            notes,
473            label,
474            cause,
475            attempts: u32::from(job.is_some()),
476            job,
477            fallbacks: plans,
478            first: None,
479            chosen: None,
480        });
481        Ok(id)
482    }
483
484    /// Whether a pending commit exists.
485    #[must_use]
486    pub fn has_pending(&self) -> bool {
487        self.pending.is_some()
488    }
489
490    /// Run the pending commit for at most `budget` solver iterations.
491    pub fn step_pending(&mut self, budget: u32) -> Result<PendingState, EngineError> {
492        let Some(p) = self.pending.as_mut() else {
493            return Err(EngineError::NotActive { what: "pending transaction".into() });
494        };
495        let id = p.id;
496        if p.base_revision != self.revision {
497            let expected = p.base_revision;
498            self.pending = None;
499            return Ok(PendingState::Done(Box::new(Err(EngineError::Stale { expected, actual: self.revision }))));
500        }
501        if let Some((job, _, _)) = p.job.as_mut()
502            && job.step(budget) != Progress::Finished
503        {
504            return Ok(PendingState::Running { id });
505        }
506        // The current attempt finished: keep it if it is acceptable and meets every
507        // requested value; otherwise start the next attempt. The last attempt's
508        // result is used unless it failed and an earlier one was acceptable.
509        if let Some((job, problem, plan)) = p.job.take() {
510            let sol = job.into_solution();
511            let good = sol.accepted() && solve::targets_met(&problem, &plan, &sol);
512            match p.fallbacks.pop_front() {
513                Some((next, next_plan)) if !good => {
514                    if sol.accepted() && p.first.is_none() {
515                        p.first = Some((sol, problem, plan));
516                    }
517                    p.job = Some((SolveJob::new(next.clone(), self.options.solve), next, next_plan));
518                    p.attempts += 1;
519                    return Ok(PendingState::Running { id });
520                }
521                _ => {
522                    p.chosen = match p.first.take() {
523                        Some(f) if !sol.accepted() => Some(f),
524                        _ => Some((sol, problem, plan)),
525                    };
526                }
527            }
528        }
529        let Some(p) = self.pending.take() else {
530            return Err(EngineError::NotActive { what: "pending transaction".into() });
531        };
532        Ok(PendingState::Done(Box::new(self.finish_pending(p))))
533    }
534
535    /// Cancel the pending commit (the document is unchanged).
536    pub fn cancel_pending(&mut self) -> bool {
537        self.pending.take().is_some()
538    }
539
540    fn finish_pending(&mut self, p: Pending) -> Result<CommitReport, EngineError> {
541        let Pending { data, mut notes, label, cause, chosen, attempts, .. } = p;
542        let mut ov = Overlay::from_data(&self.doc, data);
543        let mut status = Status::Solved;
544        let mut diagnostics = Vec::new();
545        let solver = chosen.as_ref().map(|(sol, problem, _)| SolverStats {
546            iterations: sol.iterations,
547            attempts,
548            variables: u32::try_from(problem.vars.len()).unwrap_or(u32::MAX),
549            rules: u32::try_from(problem.rules.len()).unwrap_or(u32::MAX),
550            components: u32::try_from(sol.components.len()).unwrap_or(u32::MAX),
551        });
552        if let Some((sol, problem, plan)) = chosen {
553            status = sol.status;
554            if let Err(err) = solve::finish(&mut ov, &self.registry, &problem, &plan, &sol, &mut notes) {
555                let err = match err {
556                    EngineError::Solve { mut failure } => {
557                        failure.nearest = solve::nearest(&ov, &self.deps, &self.registry, &notes, &self.options.solve);
558                        EngineError::Solve { failure: SolveFailure { ..failure } }
559                    }
560                    other => other,
561                };
562                return Err(err);
563            }
564            diagnostics = solve::reports(&problem, &plan, &sol);
565        }
566        let data = ov.into_data();
567        let mut report = self.commit(data, notes, label, cause, status, diagnostics);
568        report.solver = solver;
569        Ok(report)
570    }
571
572    fn commit(
573        &mut self,
574        data: OverlayData,
575        notes: ApplyNotes,
576        label: String,
577        cause: &str,
578        status: Status,
579        diagnostics: Vec<crate::error::DiagnosticReport>,
580    ) -> CommitReport {
581        let ov = Overlay::from_data(&self.doc, data);
582        let change = crate::apply::change_of(&ov);
583        let next_id = ov.next_id;
584        drop(ov);
585        change.apply(&mut self.doc, true);
586        self.doc.reserve_id(next_id.saturating_sub(1));
587        for id in &notes.created {
588            self.reserved.remove(&id.0);
589        }
590        for id in &notes.created_constraints {
591            self.reserved.remove(&id.0);
592        }
593        self.revision += 1;
594        let changed_entities: Vec<EntityId> = change.entity_ids().collect();
595        let changed_constraints: Vec<ConstraintId> = change.constraints.iter().map(|c| c.0).collect();
596        self.update_after_change(&change);
597        let undo_available =
598            if change.is_empty() { self.history.can_undo() } else { self.history.push(label.clone(), change) };
599        self.events.push(Event::Committed {
600            revision: self.revision,
601            label,
602            cause: cause.into(),
603            entities: changed_entities.clone(),
604            constraints: changed_constraints,
605        });
606        self.emit_history();
607        CommitReport {
608            revision: self.revision,
609            status,
610            diagnostics,
611            created: notes.created,
612            created_constraints: notes.created_constraints,
613            deleted: notes.deleted,
614            removed_constraints: notes.removed_constraints,
615            changed: changed_entities,
616            notes: notes.notes,
617            undo_available,
618            solver: None,
619        }
620    }
621
622    /// Update indexes, caches, scene and selection after a change was applied.
623    pub(crate) fn update_after_change(&mut self, change: &Change) {
624        for (_, before, after) in &change.entities {
625            if let Some((e, _)) = before {
626                self.deps.remove_entity(e);
627            }
628            if let Some((e, _)) = after {
629                self.deps.add_entity(e);
630            }
631        }
632        for (_, before, after) in &change.constraints {
633            if let Some(c) = before {
634                self.deps.remove_constraint(c);
635            }
636            if let Some(c) = after {
637                self.deps.add_constraint(c);
638            }
639        }
640        let ids: Vec<EntityId> = change.entity_ids().collect();
641        // Before-states may have been referenced by entities that no longer reference them.
642        let mut affected = self.dependents_closure(&ids);
643        for (_, b, _) in &change.entities {
644            if let Some((e, _)) = b {
645                affected.extend(e.referenced_entities());
646            }
647        }
648        if change.layers.is_some() || change.settings.is_some() {
649            affected.extend(self.doc.order().iter().copied());
650        }
651        self.refresh_entities(&affected);
652        if change.entities.iter().any(|(_, b, a)| b.as_ref().map(|x| x.1) != a.as_ref().map(|x| x.1)) {
653            self.scene.order_changed = true;
654        }
655        let before = self.selection.len();
656        self.selection.retain(|id| self.doc.entity(*id).is_some());
657        if self.selection.len() != before {
658            self.events.push(Event::SelectionChanged { selection: self.selection.iter().copied().collect() });
659        }
660    }
661
662    pub(crate) fn emit_history(&mut self) {
663        self.events.push(Event::HistoryChanged {
664            can_undo: self.history.can_undo(),
665            can_redo: self.history.can_redo(),
666            undo_label: self.history.undo_label().map(ToOwned::to_owned),
667            redo_label: self.history.redo_label().map(ToOwned::to_owned),
668        });
669    }
670
671    /// Undo the last commit (applies the stored before-state; no solving).
672    pub fn undo(&mut self, opts: ApplyOptions) -> Result<u64, EngineError> {
673        self.check_idle()?;
674        self.check_revision(opts)?;
675        let entry = self.history.take_undo().ok_or(EngineError::NothingToUndo)?;
676        entry.change.apply(&mut self.doc, false);
677        self.revision += 1;
678        self.update_after_change(&entry.change);
679        self.events.push(Event::Committed {
680            revision: self.revision,
681            label: entry.label.clone(),
682            cause: "undo".into(),
683            entities: entry.change.entity_ids().collect(),
684            constraints: entry.change.constraints.iter().map(|c| c.0).collect(),
685        });
686        self.history.push_redo(entry);
687        self.emit_history();
688        Ok(self.revision)
689    }
690
691    /// Redo the last undone commit.
692    pub fn redo(&mut self, opts: ApplyOptions) -> Result<u64, EngineError> {
693        self.check_idle()?;
694        self.check_revision(opts)?;
695        let entry = self.history.take_redo().ok_or(EngineError::NothingToRedo)?;
696        entry.change.apply(&mut self.doc, true);
697        self.revision += 1;
698        self.update_after_change(&entry.change);
699        self.events.push(Event::Committed {
700            revision: self.revision,
701            label: entry.label.clone(),
702            cause: "redo".into(),
703            entities: entry.change.entity_ids().collect(),
704            constraints: entry.change.constraints.iter().map(|c| c.0).collect(),
705        });
706        self.history.push_undo_keep_redo(entry);
707        self.emit_history();
708        Ok(self.revision)
709    }
710
711    fn check_idle(&self) -> Result<(), EngineError> {
712        if self.pending.is_some() || self.drag.is_some() {
713            return Err(EngineError::Busy { reason: "an interaction is in progress".into() });
714        }
715        Ok(())
716    }
717
718    /// Whether undo is possible.
719    #[must_use]
720    pub fn can_undo(&self) -> bool {
721        self.history.can_undo()
722    }
723
724    /// Whether redo is possible.
725    #[must_use]
726    pub fn can_redo(&self) -> bool {
727        self.history.can_redo()
728    }
729
730    /// History memory estimate in bytes.
731    #[must_use]
732    pub fn history_bytes(&self) -> usize {
733        self.history.bytes()
734    }
735
736    /// Drain pending events.
737    pub fn take_events(&mut self) -> Vec<Event> {
738        core::mem::take(&mut self.events)
739    }
740
741    // --- selection ------------------------------------------------------------
742
743    /// Current selection.
744    #[must_use]
745    pub fn selection(&self) -> Vec<EntityId> {
746        self.selection.iter().copied().collect()
747    }
748
749    /// Replace the selection (unknown IDs are ignored).
750    pub fn set_selection(&mut self, ids: &[EntityId]) {
751        let new: BTreeSet<EntityId> = ids.iter().copied().filter(|id| self.doc.entity(*id).is_some()).collect();
752        if new == self.selection {
753            return;
754        }
755        let changed: BTreeSet<EntityId> = new.symmetric_difference(&self.selection).copied().collect();
756        self.selection = new;
757        self.scene.dirty.extend(changed);
758        self.events.push(Event::SelectionChanged { selection: self.selection.iter().copied().collect() });
759    }
760
761    /// Evaluation of an entity (anchors, drawables) from the committed document.
762    pub fn evaluate(&mut self, id: EntityId) -> Option<crate::eval::Evaluated> {
763        self.doc.entity(id)?;
764        let ctx = Ctx { view: &self.doc, registry: &self.registry };
765        Some(self.cache.get(ctx, id).clone())
766    }
767
768    /// Constraints that reference an entity.
769    #[must_use]
770    pub fn constraints_of(&self, id: EntityId) -> Vec<ConstraintId> {
771        self.deps.constraints_of(id).collect()
772    }
773
774    /// Analyse every constraint of the document (status, DOF, conflicts) without
775    /// changing anything.
776    #[must_use]
777    pub fn analyze(&self) -> (Status, Vec<crate::error::DiagnosticReport>) {
778        let ov = Overlay::new(&self.doc);
779        let mut notes = ApplyNotes::default();
780        for c in self.doc.constraints() {
781            notes.touched_constraints.insert(c.id);
782        }
783        for e in self.doc.entities() {
784            if !e.type_id.namespace().eq("dotloom") {
785                notes.touched.insert(e.id);
786            }
787        }
788        match solve::plan(&ov, &self.deps, &self.registry, &notes, false, false) {
789            None => (Status::Solved, Vec::new()),
790            Some((problem, plan)) => {
791                let sol = dotloom_constraints::solve(&problem, &self.options.solve);
792                (sol.status, solve::reports(&problem, &plan, &sol))
793            }
794        }
795    }
796
797    /// Check every enabled hard rule (document constraints and plugin templates)
798    /// against the stored values with the independent evaluator, without solving.
799    /// Returns violations as `(what, residual, tolerance)`.
800    #[must_use]
801    pub fn verify(&self) -> Vec<(String, f64, f64)> {
802        let ctx = self.ctx();
803        let ids: std::collections::BTreeSet<EntityId> = self.doc.order().iter().copied().collect();
804        let scales = solve::scales_for(ctx, &ids);
805        let mut checker = crate::check::Checker::new(ctx, scales);
806        let mut out = Vec::new();
807        for c in self.doc.constraints() {
808            if !c.enabled || c.strength != dotloom_document::StrengthSpec::Required {
809                continue;
810            }
811            match checker.constraint(c) {
812                Ok((r, t)) if r.is_nan() || r > t => out.push((c.id.to_string(), r, t)),
813                Ok(_) => {}
814                Err(m) => out.push((format!("{}: {m}", c.id), f64::INFINITY, 0.0)),
815            }
816        }
817        for id in &ids {
818            for (label, r, t) in checker.templates(*id) {
819                if r.is_nan() || r > t {
820                    out.push((format!("{id} {label}"), r, t));
821                }
822            }
823        }
824        out
825    }
826
827    /// Parameter values of an entity `(name, value)`.
828    #[must_use]
829    pub fn params_of(&self, id: EntityId) -> BTreeMap<String, f64> {
830        let ctx = self.ctx();
831        let Some(e) = self.doc.entity(id) else { return BTreeMap::new() };
832        let names: Vec<String> = if e.type_id.namespace() == "dotloom" {
833            e.geometry
834                .as_ref()
835                .map(|g| dotloom_document::builtin::geometry_params(g).into_iter().map(|(n, _, _)| n).collect())
836                .unwrap_or_default()
837        } else {
838            self.registry.get(&e.type_id).map(|t| t.compiled.def.props.keys().cloned().collect()).unwrap_or_default()
839        };
840        names.into_iter().filter_map(|n| crate::eval::param_value(ctx, e, &n).map(|v| (n, v))).collect()
841    }
842}
843
844impl DocView for Engine {
845    fn entity(&self, id: EntityId) -> Option<&dotloom_document::Entity> {
846        self.doc.entity(id)
847    }
848    fn constraint(&self, id: ConstraintId) -> Option<&dotloom_document::Constraint> {
849        self.doc.constraint(id)
850    }
851    fn layer(&self, id: dotloom_document::LayerId) -> Option<&dotloom_document::Layer> {
852        self.doc.layer(id)
853    }
854    fn group(&self, id: dotloom_document::GroupId) -> Option<&dotloom_document::Group> {
855        self.doc.group(id)
856    }
857    fn settings(&self) -> &dotloom_document::Settings {
858        &self.doc.settings
859    }
860}