Skip to main content

geop_ops/ui/
step.rs

1//! [`StepEditor`]: editing one step — the interaction every operation
2//! shares, so a pick, a selection or a drag means the same whatever is
3//! edited.
4
5use 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
19/// Dragged values snap to this: a drag is for rough shaping, a dialog for
20/// exact values.
21pub const DRAG_SNAP: f64 = 0.01;
22
23/// What a drag holds, as it was grabbed.
24enum Grab {
25    /// The handle of the number field `key`, and the value that had.
26    Handle { key: String, value: f64 },
27    /// A draggable visual of the operation's.
28    Visual { key: String },
29}
30
31/// A step being edited: the step with its arguments as they now are, and
32/// the state of the edit — the operation's session, and the editor's own:
33/// what is selected, which reference field waits for a click and what a
34/// click would pick for it, where the pointer is, what is being dragged.
35///
36/// It turns every event into what the operation understands, the same for
37/// every operation:
38///
39/// - A reference field (see [`Reference`]) is armed by pressing it, and
40///   then a click in the viewport picks for it — one entity, or, for a
41///   field of several, one more or one less. Removing one of its entities
42///   or clearing it is done here too. The operation is only ever sent what
43///   the field holds now, as [`Value::Entities`].
44/// - A number field with a handle is dragged by it, snapped to
45///   [`DRAG_SNAP`], and set as a [`Value::Number`].
46/// - A click on a selectable visual selects it, or takes it out of the
47///   selection again; a click on nothing clears the selection, unless shift
48///   is held, and Escape clears it. The selection is the operation's to
49///   read, and to change as its fields are used.
50/// - A draggable visual is dragged in the plane worked in, as a
51///   [`CanvasEvent::Move`].
52///
53/// Whatever else the pointer and the keys do goes to the operation as a
54/// [`CanvasEvent`]: clicks while it has a tool in hand, and those on
55/// nothing, hovers, keys.
56pub struct StepEditor<O, S: Scalar> {
57    step: O,
58    session: Box<dyn Any>,
59    /// The keys of the visuals selected, in the order they were.
60    selection: Vec<String>,
61    /// The reference field waiting for a click.
62    armed: Option<String>,
63    /// What a click would pick for it.
64    hover: Option<EntityRef>,
65    /// Where the pointer last was, if over the viewport.
66    pointer: Option<Pointer<S>>,
67    grab: Option<Grab>,
68}
69
70/// Takes `x` out of `xs`, or adds it at the end.
71fn 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
80/// The handles of `dialog`'s number fields, as visuals under their keys.
81fn 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
105/// Whether a press on `visual` grabs it: a handle always, a draggable
106/// visual unless a tool is in hand.
107fn grabs<S: Scalar>(visual: &Visual<S>, tool: bool) -> bool {
108    is_handle(visual) || (visual.draggable && !tool)
109}
110
111/// Says what each entity `reference` holds is, in `part`: what it can be
112/// used as, if the field takes several kinds — or why it cannot be used.
113fn 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    /// Editing `step` against `before`. A new step whose first reference
150    /// field is still empty starts by picking for it: what it is built on
151    /// is what it needs first. One that already holds something — the
152    /// newest sketch, say — needs no click, and leaves the pointer to the
153    /// rest of the step: its handles, its drawing.
154    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    /// The step, with its arguments as they now are.
179    pub fn step(&self) -> &O {
180        &self.step
181    }
182
183    /// The operation's session: its own, to look into.
184    pub fn session(&self) -> &dyn Any {
185        &*self.session
186    }
187
188    /// The keys of the visuals selected.
189    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    /// Applies `event` — `view` is `before` as drawn, what picks test
208    /// against.
209    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    /// The dialog field `key` used: a reference field armed, or an entity
274    /// taken out of it, or all of them; any other field set.
275    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    /// A pointer event while the reference field `key` waits for a click:
298    /// a hover finds what a click would pick, a click picks it — and, for a
299    /// field of one entity, is done. Escape stops picking.
300    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    /// A drag: of a number field's handle — its value when grabbed, moved
338    /// by how far the pointer has moved along the handle's track, snapped
339    /// to [`DRAG_SNAP`] — or of a draggable visual, in the plane worked in.
340    ///
341    /// What is dragged is hit-tested where the drag started, once: while
342    /// dragged, it moves away from there. After that it is found by its key.
343    /// Nothing for a drag that did not start on either, and for a pointer
344    /// that cannot be followed (looking straight along a handle's track, or
345    /// along the plane).
346    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                    // Released: the next drag grabs afresh.
377                    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    /// What to show: the step's form, with its reference fields saying what
424    /// they hold, the handles of its number fields, what is selected and
425    /// what the pointer is over drawn so, what the reference fields hold
426    /// and what a click would pick lit, and what a click picks now. While a
427    /// field waits for a pick, there is no plane to work in: what can be
428    /// picked is shown in the part as it stands.
429    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}