1use 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#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct Transaction {
27 #[serde(default)]
29 pub label: String,
30 pub commands: Vec<Command>,
32}
33
34impl Transaction {
35 #[must_use]
37 pub fn new(label: impl Into<String>, commands: Vec<Command>) -> Self {
38 Self { label: label.into(), commands }
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct ApplyOptions {
46 #[serde(default)]
48 pub expected_revision: Option<u64>,
49}
50
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct CommitReport {
55 pub revision: u64,
57 pub status: Status,
59 pub diagnostics: Vec<crate::error::DiagnosticReport>,
61 pub created: Vec<EntityId>,
63 pub created_constraints: Vec<ConstraintId>,
65 pub deleted: Vec<EntityId>,
67 pub removed_constraints: Vec<ConstraintId>,
69 pub changed: Vec<EntityId>,
71 pub notes: Vec<String>,
73 pub undo_available: bool,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub solver: Option<SolverStats>,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct SolverStats {
84 pub iterations: u32,
86 pub attempts: u32,
88 pub variables: u32,
90 pub rules: u32,
92 pub components: u32,
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
98#[serde(tag = "type", rename_all = "camelCase", rename_all_fields = "camelCase")]
99pub enum Event {
100 Committed {
102 revision: u64,
104 label: String,
106 cause: String,
108 entities: Vec<EntityId>,
110 constraints: Vec<ConstraintId>,
112 },
113 SelectionChanged {
115 selection: Vec<EntityId>,
117 },
118 HistoryChanged {
120 can_undo: bool,
122 can_redo: bool,
124 undo_label: Option<String>,
126 redo_label: Option<String>,
128 },
129 PluginsChanged,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq)]
135pub struct EngineOptions {
136 pub solve: SolveOptions,
138 pub history_entries: usize,
140 pub history_bytes: usize,
142 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#[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 pub fallbacks: std::collections::VecDeque<(dotloom_constraints::Problem, Plan)>,
170 pub first: Option<(dotloom_constraints::Solution, dotloom_constraints::Problem, Plan)>,
172 pub chosen: Option<(dotloom_constraints::Solution, dotloom_constraints::Problem, Plan)>,
174 pub attempts: u32,
176}
177
178#[derive(Debug, Clone, PartialEq)]
180pub enum PendingState {
181 Running {
183 id: u64,
185 },
186 Done(Box<Result<CommitReport, EngineError>>),
188}
189
190#[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 #[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 #[must_use]
243 pub fn document(&self) -> &Document {
244 &self.doc
245 }
246
247 #[must_use]
249 pub fn revision(&self) -> u64 {
250 self.revision
251 }
252
253 #[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 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 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 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 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 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 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 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 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 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 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, ¬es, &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 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, ¬es, 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 #[must_use]
486 pub fn has_pending(&self) -> bool {
487 self.pending.is_some()
488 }
489
490 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 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 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, ¬es, &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 ¬es.created {
588 self.reserved.remove(&id.0);
589 }
590 for id in ¬es.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 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 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 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 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 #[must_use]
720 pub fn can_undo(&self) -> bool {
721 self.history.can_undo()
722 }
723
724 #[must_use]
726 pub fn can_redo(&self) -> bool {
727 self.history.can_redo()
728 }
729
730 #[must_use]
732 pub fn history_bytes(&self) -> usize {
733 self.history.bytes()
734 }
735
736 pub fn take_events(&mut self) -> Vec<Event> {
738 core::mem::take(&mut self.events)
739 }
740
741 #[must_use]
745 pub fn selection(&self) -> Vec<EntityId> {
746 self.selection.iter().copied().collect()
747 }
748
749 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 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 #[must_use]
770 pub fn constraints_of(&self, id: EntityId) -> Vec<ConstraintId> {
771 self.deps.constraints_of(id).collect()
772 }
773
774 #[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, ¬es, 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 #[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 #[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}