1use std::collections::BTreeSet;
19
20use dotloom_constraints::{LinearSession, Problem, Solution, SolveJob, SolveOptions, Status};
21use dotloom_document::EntityId;
22use dotloom_geometry::{Affine, Point, TransformPolicy};
23use dotloom_scene::SceneDelta;
24use serde::{Deserialize, Serialize};
25
26use crate::{
27 Command, DocView, Engine, Overlay,
28 build::RuleOrigin,
29 command::{Applier, ApplyNotes},
30 engine::CommitReport,
31 error::{DiagnosticReport, EngineError},
32 eval::{Ctx, evaluate},
33 solve,
34 view::OverlayData,
35};
36
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
40pub enum DragSpec {
41 Move {
43 ids: Vec<EntityId>,
45 from: Point,
47 },
48 Anchor {
50 entity: EntityId,
52 anchor: String,
54 },
55}
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct DragPreview {
61 pub accepted: bool,
63 pub status: Status,
65 pub diagnostics: Vec<DiagnosticReport>,
67 #[serde(default)]
69 pub incremental: bool,
70}
71
72#[derive(Debug, Default)]
74enum SessionSlot {
75 #[default]
77 Untried,
78 Active(Box<LinearSession>),
80 Unavailable,
82}
83
84fn solve_attempt(
86 slot: Option<&mut SessionSlot>,
87 problem: &Problem,
88 plan: &solve::Plan,
89 opts: SolveOptions,
90) -> (Solution, bool) {
91 let Some(slot) = slot else {
92 return (SolveJob::new(problem.clone(), opts).into_solution(), false);
93 };
94 if let SessionSlot::Active(session) = slot {
95 if let Some(sol) = session.resolve(problem) {
96 return (sol, true);
97 }
98 if session.is_broken() {
99 *slot = SessionSlot::Untried;
100 }
101 }
102 let sol = SolveJob::new(problem.clone(), opts).into_solution();
103 if matches!(slot, SessionSlot::Untried) && sol.accepted() {
106 let moving: Vec<usize> =
107 plan.origins.iter().enumerate().filter(|(_, o)| matches!(o, RuleOrigin::Drag)).map(|(i, _)| i).collect();
108 *slot = LinearSession::new(problem, &moving, opts)
109 .map_or(SessionSlot::Unavailable, |s| SessionSlot::Active(Box::new(s)));
110 }
111 (sol, false)
112}
113
114#[derive(Debug)]
116pub(crate) struct DragSession {
117 spec: DragSpec,
118 base_revision: u64,
119 last: Option<(OverlayData, ApplyNotes)>,
120 shown: BTreeSet<EntityId>,
121 sessions: [SessionSlot; 3],
122}
123
124fn preview_options(base: SolveOptions) -> SolveOptions {
126 SolveOptions { analyze: false, max_iterations: base.max_iterations.min(40), ..base }
127}
128
129impl Engine {
130 pub fn begin_drag(&mut self, spec: DragSpec) -> Result<(), EngineError> {
132 if self.pending.is_some() || self.drag.is_some() {
133 return Err(EngineError::Busy { reason: "an interaction is in progress".into() });
134 }
135 let ids: Vec<EntityId> = match &spec {
136 DragSpec::Move { ids, .. } => ids.clone(),
137 DragSpec::Anchor { entity, anchor } => {
138 let ctx = Ctx { view: &self.doc, registry: &self.registry };
139 if evaluate(ctx, *entity).anchor(anchor).is_none() {
140 return Err(EngineError::Invalid { message: format!("{entity} has no anchor `{anchor}`") });
141 }
142 vec![*entity]
143 }
144 };
145 if ids.is_empty() {
146 return Err(EngineError::Invalid { message: "nothing to drag".into() });
147 }
148 for id in &ids {
149 let e =
150 self.doc.entity(*id).ok_or_else(|| EngineError::Invalid { message: format!("{id} does not exist") })?;
151 if e.locked || self.doc.layer(e.layer).is_some_and(|l| l.locked) {
152 return Err(EngineError::Command {
153 error: crate::CommandError::ReadOnly { what: id.to_string(), reason: "locked".into() },
154 });
155 }
156 }
157 self.drag = Some(DragSession {
158 spec,
159 base_revision: self.revision,
160 last: None,
161 shown: BTreeSet::new(),
162 sessions: Default::default(),
163 });
164 Ok(())
165 }
166
167 #[must_use]
169 pub fn is_dragging(&self) -> bool {
170 self.drag.is_some()
171 }
172
173 pub fn drag_to(&mut self, to: Point) -> Result<(DragPreview, SceneDelta), EngineError> {
176 let Some(session) = self.drag.as_ref() else {
177 return Err(EngineError::NotActive { what: "drag".into() });
178 };
179 if session.base_revision != self.revision || !to.is_finite() {
180 return Err(EngineError::Stale { expected: session.base_revision, actual: self.revision });
181 }
182 let spec = session.spec.clone();
183 let mut ov = Overlay::new(&self.doc);
184 let mut notes = ApplyNotes::default();
185 let anchor_target = match &spec {
186 DragSpec::Move { ids, from } => {
187 let mut ap = Applier {
188 ov: &mut ov,
189 registry: &self.registry,
190 reserved: &self.reserved,
191 notes: ApplyNotes::default(),
192 };
193 ap.apply(Command::Transform {
194 ids: ids.clone(),
195 transform: Affine::translate(to - *from),
196 policy: TransformPolicy::Strict,
197 })?;
198 notes = ap.notes;
199 None
200 }
201 DragSpec::Anchor { entity, anchor } => {
202 notes.touched.insert(*entity);
203 Some((*entity, anchor.clone(), to))
204 }
205 };
206 let opts = preview_options(self.options.solve);
207 let target = anchor_target.as_ref().map(|(e, a, p)| (*e, a.as_str(), *p));
208 let mut chosen = None;
213 let attempts: &[(bool, bool)] = if target.is_some() {
214 &[(true, true), (false, true), (false, false)]
215 } else {
216 &[(true, false), (false, false)]
217 };
218 for (i, &(pin, hard)) in attempts.iter().enumerate() {
219 let attempt = solve::Attempt { target, pin, hard_target: hard };
220 let Some((problem, plan)) = solve::plan_with(&ov, &self.deps, &self.registry, ¬es, true, attempt) else {
221 break;
222 };
223 let slot = self.drag.as_mut().and_then(|s| s.sessions.get_mut(i));
224 let (sol, incremental) = solve_attempt(slot, &problem, &plan, opts);
225 let good = sol.accepted() && solve::targets_met(&problem, &plan, &sol);
226 let last = i + 1 == attempts.len();
227 if good || last {
228 if sol.accepted() || chosen.is_none() {
229 chosen = Some((sol, problem, plan, incremental));
230 }
231 break;
232 }
233 if sol.accepted() && chosen.is_none() {
234 chosen = Some((sol, problem, plan, incremental));
235 }
236 }
237 let incremental = chosen.as_ref().is_some_and(|c| c.3);
238 let (accepted, status, diagnostics) = match chosen {
239 None => (true, Status::Solved, Vec::new()),
240 Some((sol, problem, plan, _)) => {
241 match solve::finish(&mut ov, &self.registry, &problem, &plan, &sol, &mut notes) {
242 Ok(()) => (true, sol.status, Vec::new()),
243 Err(EngineError::Solve { failure }) => (false, failure.status, failure.diagnostics),
244 Err(EngineError::Validation { .. } | EngineError::Invalid { .. }) => {
245 (false, sol.status, Vec::new())
246 }
247 Err(other) => return Err(other),
248 }
249 }
250 };
251 let mut delta = SceneDelta { revision: self.revision, preview: true, ..SceneDelta::default() };
252 if accepted {
253 let changed: Vec<EntityId> = ov.entities.keys().copied().collect();
255 let affected = self.dependents_closure(&changed);
256 let ctx = Ctx { view: &ov, registry: &self.registry };
257 let evs: Vec<(EntityId, crate::eval::Evaluated, Option<dotloom_document::Entity>)> =
258 affected.iter().map(|id| (*id, evaluate(ctx, *id), ov.entity(*id).cloned())).collect();
259 let data = ov.into_data();
260 for (id, ev, ent) in evs {
261 if let Some(item) = self.scene_item(id, Some(&ev), ent.as_ref()) {
262 delta.upserts.push(item);
263 }
264 }
265 if let Some(s) = self.drag.as_mut() {
266 s.shown.extend(affected.iter().copied());
267 s.last = Some((data, notes));
268 }
269 }
270 Ok((DragPreview { accepted, status, diagnostics, incremental }, delta))
271 }
272
273 pub fn end_drag(&mut self, commit: bool) -> Result<Option<CommitReport>, EngineError> {
276 let Some(session) = self.drag.take() else {
277 return Err(EngineError::NotActive { what: "drag".into() });
278 };
279 self.scene.preview.extend(session.shown.iter().copied());
280 if !commit || session.base_revision != self.revision {
281 return Ok(None);
282 }
283 let Some((data, notes)) = session.last else { return Ok(None) };
284 let label = match session.spec {
285 DragSpec::Move { .. } => "Move",
286 DragSpec::Anchor { .. } => "Drag point",
287 };
288 let id = self.start_pending(data, notes, label.into(), "drag", true)?;
290 loop {
291 match self.step_pending(u32::MAX)? {
292 crate::engine::PendingState::Done(r) => return (*r).map(Some),
293 crate::engine::PendingState::Running { id: pid } if pid == id => {}
294 crate::engine::PendingState::Running { .. } => {
295 return Err(EngineError::Busy { reason: "unexpected pending request".into() });
296 }
297 }
298 }
299 }
300}