1use std::any::Any;
6
7use geop_core_math::{scalars::Scalar, vector::Vector3};
8
9use super::{
10 Button, CanvasEvent, Control, Dialog, PartView, Pointer, Presentation, Reference, Shape,
11 StepEditEvent, Style, Tone, Value, Visual, hit::hit_visuals,
12};
13use crate::{
14 Part,
15 operation::{Aspects, EntityRef, Operations, Role, describe_roles},
16 ui::Form,
17};
18
19pub const DRAG_SNAP: f64 = 0.01;
22
23enum Grab {
25 Handle { key: String, value: f64 },
27 Visual { key: String },
29}
30
31pub struct StepEditor<O, S: Scalar> {
57 step: O,
58 session: Box<dyn Any>,
59 selection: Vec<String>,
61 armed: Option<String>,
63 hover: Option<EntityRef>,
65 pointer: Option<Pointer<S>>,
67 grab: Option<Grab>,
68}
69
70fn toggle<T: PartialEq>(xs: &mut Vec<T>, x: T) {
72 match xs.iter().position(|y| *y == x) {
73 Some(i) => {
74 xs.remove(i);
75 }
76 None => xs.push(x),
77 }
78}
79
80fn handles<S: Scalar>(dialog: &Dialog<S>) -> Vec<Visual<S>> {
82 dialog
83 .0
84 .iter()
85 .filter_map(|f| match &f.control {
86 Control::Number(n) => n.handle.map(|track| {
87 Visual::new(
88 f.key.clone(),
89 Shape::Handle {
90 at: track.at,
91 direction: track.direction,
92 },
93 Style::Handle,
94 )
95 }),
96 _ => None,
97 })
98 .collect()
99}
100
101fn is_handle<S: Scalar>(visual: &Visual<S>) -> bool {
102 matches!(visual.shape, Shape::Handle { .. })
103}
104
105fn grabs<S: Scalar>(visual: &Visual<S>, tool: bool) -> bool {
108 is_handle(visual) || (visual.draggable && !tool)
109}
110
111fn describe<S: Scalar>(part: &Part<S>, reference: &mut Reference) {
114 for picked in &mut reference.value {
115 let fits: Vec<Role> = match Aspects::of(&picked.entity, part) {
116 Ok(aspects) => aspects
117 .roles()
118 .into_iter()
119 .filter(|r| reference.roles.contains(r))
120 .collect(),
121 Err(e) => {
122 picked.tone = Tone::Error;
123 picked.detail = Some(e.root_message().to_string());
124 continue;
125 }
126 };
127 let in_scope = reference
128 .scope
129 .as_ref()
130 .is_none_or(|scope| picked.entity.lies_in(scope));
131 if fits.is_empty() || !in_scope {
132 picked.tone = Tone::Error;
133 let scope = match &reference.scope {
134 Some(scope) => format!(" of {scope}"),
135 None => String::new(),
136 };
137 picked.detail = Some(format!(
138 "not {}{scope}",
139 describe_roles(&reference.roles, " or ")
140 ));
141 } else if reference.roles.len() > 1 {
142 let names: Vec<&str> = fits.iter().map(|r| r.name()).collect();
143 picked.detail = Some(names.join(" · "));
144 }
145 }
146}
147
148impl<O: Operations, S: Scalar> StepEditor<O, S> {
149 pub fn new(step: O, before: &Part<S>, new: bool) -> Self {
155 let session = step.new_session();
156 let armed = new
157 .then(|| {
158 step.form(before, &*session, &[])
159 .dialog
160 .0
161 .into_iter()
162 .find(|f| matches!(f.control, Control::Reference(_)))
163 .filter(|f| matches!(&f.control, Control::Reference(r) if r.value.is_empty()))
164 .map(|f| f.key)
165 })
166 .flatten();
167 Self {
168 step,
169 session,
170 selection: Vec::new(),
171 armed,
172 hover: None,
173 pointer: None,
174 grab: None,
175 }
176 }
177
178 pub fn step(&self) -> &O {
180 &self.step
181 }
182
183 pub fn session(&self) -> &dyn Any {
185 &*self.session
186 }
187
188 pub fn selection(&self) -> &[String] {
190 &self.selection
191 }
192
193 fn form<'a>(&self, before: &'a Part<S>) -> Form<'a, S> {
194 self.step.form(before, &*self.session, &self.selection)
195 }
196
197 fn set(&mut self, before: &Part<S>, key: &str, value: Value) {
198 self.step
199 .set(before, &mut *self.session, &mut self.selection, key, value);
200 }
201
202 fn pass(&mut self, before: &Part<S>, event: CanvasEvent<S>) {
203 self.step
204 .event(before, &mut *self.session, &mut self.selection, &event);
205 }
206
207 pub fn handle(&mut self, before: &Part<S>, view: &PartView<S>, event: &StepEditEvent<S>) {
210 let form = self.form(before);
211 if let StepEditEvent::Dialog { key, value } = event {
212 self.dialog(before, &form.dialog, key, value.clone());
213 return;
214 }
215 match event {
216 StepEditEvent::Hover { pointer } | StepEditEvent::Click { pointer, .. } => {
217 self.pointer = Some(*pointer)
218 }
219 StepEditEvent::Leave => self.pointer = None,
220 _ => {}
221 }
222 if let Some(key) = self.armed.clone() {
223 if let Some(Control::Reference(reference)) = form.dialog.get(&key) {
224 self.pick(before, view, event, &key, reference);
225 return;
226 }
227 self.armed = None;
228 }
229 match event {
230 StepEditEvent::Dialog { .. } => unreachable!("handled above"),
231 StepEditEvent::Hover { pointer } => {
232 self.pass(before, CanvasEvent::Hover { pointer: *pointer })
233 }
234 StepEditEvent::Leave => self.pass(before, CanvasEvent::Leave),
235 StepEditEvent::Click {
236 pointer,
237 button,
238 double,
239 shift,
240 } => {
241 let selectable = |v: &Visual<S>| v.selectable;
242 let hit = (*button == Button::Primary && !*double && !form.tool)
243 .then(|| hit_visuals(&form.visuals, pointer, selectable))
244 .flatten();
245 match hit {
246 Some(hit) => toggle(&mut self.selection, hit.visual.key.clone()),
247 None => {
248 if *button == Button::Primary && !*shift && !form.tool {
249 self.selection.clear();
250 }
251 self.pass(
252 before,
253 CanvasEvent::Click {
254 pointer: *pointer,
255 button: *button,
256 double: *double,
257 shift: *shift,
258 },
259 );
260 }
261 }
262 }
263 StepEditEvent::Drag { from, to, done } => self.drag(before, &form, from, to, *done),
264 StepEditEvent::Key { key } => {
265 if key == "Escape" {
266 self.selection.clear();
267 }
268 self.pass(before, CanvasEvent::Key { key: key.clone() });
269 }
270 }
271 }
272
273 fn dialog(&mut self, before: &Part<S>, dialog: &Dialog<S>, key: &str, value: Value) {
276 let Some(Control::Reference(reference)) = dialog.get(key) else {
277 self.set(before, key, value);
278 return;
279 };
280 let mut entities: Vec<EntityRef> = reference.entities().cloned().collect();
281 match value {
282 Value::Press => {
283 self.armed = (self.armed.as_deref() != Some(key)).then(|| key.to_string());
284 self.hover = None;
285 return;
286 }
287 Value::RemoveAt(i) if i < entities.len() => {
288 entities.remove(i);
289 }
290 Value::Clear => entities.clear(),
291 Value::Entities(set) => entities = set,
292 _ => return,
293 }
294 self.set(before, key, Value::Entities(entities));
295 }
296
297 fn pick(
301 &mut self,
302 before: &Part<S>,
303 view: &PartView<S>,
304 event: &StepEditEvent<S>,
305 key: &str,
306 reference: &Reference,
307 ) {
308 let pick = |pointer| view.pick(pointer, &reference.roles, reference.scope.as_ref());
309 match event {
310 StepEditEvent::Hover { pointer } => self.hover = pick(pointer).map(|h| h.entity),
311 StepEditEvent::Leave => self.hover = None,
312 StepEditEvent::Click {
313 pointer,
314 button: Button::Primary,
315 ..
316 } => {
317 self.hover = None;
318 if let Some(hit) = pick(pointer) {
319 let mut entities: Vec<EntityRef> = reference.entities().cloned().collect();
320 if reference.multiple {
321 toggle(&mut entities, hit.entity);
322 } else {
323 entities = vec![hit.entity];
324 self.armed = None;
325 }
326 self.set(before, key, Value::Entities(entities));
327 }
328 }
329 StepEditEvent::Key { key } if key == "Escape" => {
330 self.armed = None;
331 self.hover = None;
332 }
333 _ => {}
334 }
335 }
336
337 fn drag(
347 &mut self,
348 before: &Part<S>,
349 form: &Form<S>,
350 from: &Pointer<S>,
351 to: &Pointer<S>,
352 done: bool,
353 ) {
354 let handles = handles(&form.dialog);
355 if self.grab.is_none() {
356 let visuals: Vec<Visual<S>> = form.visuals.iter().chain(&handles).cloned().collect();
357 let Some(hit) = hit_visuals(&visuals, from, |v| grabs(v, form.tool)) else {
358 return;
359 };
360 let key = hit.visual.key.clone();
361 self.grab = Some(match form.dialog.get(&key) {
362 Some(Control::Number(n)) if is_handle(hit.visual) => Grab::Handle {
363 key,
364 value: n.value,
365 },
366 _ => Grab::Visual { key },
367 });
368 }
369 let Some(grab) = &self.grab else {
370 return;
371 };
372 match grab {
373 Grab::Handle { key, value } => {
374 let (key, value) = (key.clone(), *value);
375 if done {
376 self.grab = None;
378 }
379 let moved = handles
380 .iter()
381 .filter(|v| v.key == key)
382 .find_map(|v| match v.shape {
383 Shape::Handle { at, direction } => Some((at, direction)),
384 _ => None,
385 })
386 .and_then(|(at, direction)| {
387 Some(
388 to.ray
389 .line_parameter(&at, &direction)?
390 .sub(from.ray.line_parameter(&at, &direction)?),
391 )
392 });
393 if let Some(moved) = moved {
394 let value = value + moved.to_f64();
395 let snapped = (value / DRAG_SNAP).round() * DRAG_SNAP;
396 self.set(before, &key, Value::Number(snapped));
397 }
398 }
399 Grab::Visual { key } => {
400 let key = key.clone();
401 if done {
402 self.grab = None;
403 }
404 let in_plane = |pointer: &Pointer<S>| -> Option<Vector3<S>> {
405 let (t, _) = pointer.ray.intersect_uv_plane(form.focus.as_ref()?)?;
406 Some(pointer.ray.at(t))
407 };
408 if let (Some(from), Some(to)) = (in_plane(from), in_plane(to)) {
409 self.pass(
410 before,
411 CanvasEvent::Move {
412 key,
413 from,
414 to,
415 done,
416 },
417 );
418 }
419 }
420 }
421 }
422
423 pub fn presentation(&self, before: &Part<S>) -> Presentation<S> {
430 let mut form = self.form(before);
431 let mut pickable = Vec::new();
432 for (key, reference) in form.dialog.references_mut() {
433 describe(before, reference);
434 if self.armed.as_deref() == Some(key) {
435 reference.armed = true;
436 pickable = reference.roles.clone();
437 }
438 }
439 let mut visuals = form.visuals;
440 visuals.extend(handles(&form.dialog));
441 let tool = form.tool;
442 let hovered = self
443 .pointer
444 .filter(|_| self.armed.is_none())
445 .and_then(|pointer| {
446 hit_visuals(&visuals, &pointer, |v| v.selectable || grabs(v, tool))
447 .map(|hit| (hit.visual.key.clone(), grabs(hit.visual, tool)))
448 });
449 for visual in &mut visuals {
450 if is_handle(visual) {
451 continue;
452 }
453 if self.selection.contains(&visual.key) {
454 visual.style = Style::Selected;
455 } else if hovered.as_ref().is_some_and(|(key, _)| *key == visual.key) {
456 visual.style = Style::Hover;
457 }
458 }
459 let highlights = form
460 .dialog
461 .picked()
462 .cloned()
463 .chain(self.hover.clone())
464 .collect();
465 Presentation {
466 dialog: form.dialog,
467 visuals,
468 highlights,
469 pickable,
470 focus: if self.armed.is_some() {
471 None
472 } else {
473 form.focus
474 },
475 grab: self.grab.is_some() || hovered.is_some_and(|(_, grab)| grab),
476 }
477 }
478}