Skip to main content

geop_ops_sketch/editor/
mod.rs

1//! Editing a sketch step: choosing its plane, then drawing in it with tools,
2//! constraining what is drawn, and dragging it — every gesture decided here
3//! and answered with a new sketch, which is solved before it is returned.
4//!
5//! Drawing snaps the way constraints say, never by moving geometry onto
6//! what it is near: a point placed on an existing point *is* that point, one
7//! placed on a curve is constrained onto it ([`Constraint::PointOnCurve`]),
8//! one placed on the sketch's origin is fixed there, and a line drawn nearly
9//! horizontal or vertical gets that constraint. Proximity only suggests;
10//! the constraint is what the sketch then holds.
11
12mod dialog;
13mod gestures;
14#[cfg(test)]
15mod tests;
16mod visuals;
17
18use dialog::draw_dialog;
19use visuals::visuals;
20
21use geop_core_math::{
22    primitives::CoordinateSystem,
23    scalars::Scalar,
24    vector::{Vector2, Vector3},
25};
26use geop_core_sketch::{
27    Constraint, ConstraintId, CurveId, CurveKind, PointId, Sketch, SolveReport,
28    point::{P2, add, dist, sub},
29    profile::curve_polyline,
30};
31use geop_ops::{
32    Part,
33    operation::Role,
34    ui::{
35        Action, Button, CanvasEvent, Edit, Form, ListItem, Pointer, Shape, Style, Tone, Value,
36        Visual, hit::hit_visuals,
37    },
38};
39
40use crate::{
41    AddSketchArgs,
42    constraints::{self, Selection},
43    geometry::sweep_through,
44};
45
46/// Lines drawn within this slope of horizontal or vertical get that
47/// constraint.
48const AUTO_HV_SLOPE: f64 = 0.034_920_769_491_747_23; // tan(2°)
49
50/// A drawing tool, or selecting.
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
52pub enum Tool {
53    #[default]
54    Select,
55    Line,
56    Rectangle,
57    Arc,
58    Circle,
59    Spline,
60    Point,
61}
62
63impl Tool {
64    /// Every tool, as the palette offers it: `(tool, name, label, shortcut)`.
65    const ALL: [(Tool, &'static str, &'static str, &'static str); 7] = [
66        (Tool::Select, "select", "Select", "Escape"),
67        (Tool::Line, "line", "Line", "l"),
68        (Tool::Rectangle, "rectangle", "Rectangle", "r"),
69        (Tool::Arc, "arc", "Arc", "a"),
70        (Tool::Circle, "circle", "Circle", "c"),
71        (Tool::Spline, "spline", "Spline", "s"),
72        (Tool::Point, "point", "Point", "p"),
73    ];
74
75    fn by_name(name: &str) -> Option<Tool> {
76        Tool::ALL.iter().find(|t| t.1 == name).map(|t| t.0)
77    }
78
79    fn by_shortcut(shortcut: &str) -> Option<Tool> {
80        Tool::ALL
81            .iter()
82            .find(|t| t.3 != "Escape" && t.3 == shortcut.to_lowercase())
83            .map(|t| t.0)
84    }
85}
86
87/// A curve being drawn: the points placed for it so far.
88#[derive(Clone, Debug, PartialEq)]
89enum Draft {
90    Line {
91        start: PointId,
92    },
93    Rectangle {
94        corner: PointId,
95    },
96    Arc {
97        start: PointId,
98        end: Option<PointId>,
99    },
100    Circle {
101        center: PointId,
102    },
103    Spline {
104        points: Vec<PointId>,
105    },
106}
107
108/// What a drag moves, as it was grabbed.
109#[derive(Clone, Debug, PartialEq)]
110enum Drag {
111    /// Points, pulled by how far the pointer moved from `grab`.
112    Points {
113        points: Vec<PointId>,
114        origins: Vec<P2>,
115        grab: P2,
116    },
117    /// A circle's radius.
118    Circle { curve: CurveId },
119    /// An arc's sweep.
120    Arc { curve: CurveId },
121}
122
123/// How the last solve went.
124#[derive(Clone, Debug, PartialEq)]
125enum Solved {
126    Solved {
127        report: SolveReport,
128    },
129    /// The sketch could not be solved at all — it is kept as drawn.
130    Failed {
131        error: String,
132    },
133}
134
135/// The temporary state of editing a sketch. What is selected is the
136/// editor's: the keys of the visuals of the points, curves and constraints
137/// selected.
138#[derive(Clone, Debug, Default, PartialEq)]
139pub struct SketchSession {
140    tool: Tool,
141    draft: Option<Draft>,
142    /// The pointer, in the plane.
143    cursor: Option<P2>,
144    drag: Option<Drag>,
145    /// Solved once when the edit starts, and after every change.
146    solved: Option<Solved>,
147}
148
149/// The id in a visual key: `p3` is point 3.
150fn key_id(key: &str, prefix: char) -> Option<u64> {
151    key.strip_prefix(prefix)?.parse().ok()
152}
153
154fn point_key(key: &str) -> Option<PointId> {
155    key_id(key, 'p').map(PointId)
156}
157
158fn curve_key(key: &str) -> Option<CurveId> {
159    key_id(key, 'c').map(CurveId)
160}
161
162/// The key of a constraint's glyph.
163fn glyph_key(key: &str) -> Option<ConstraintId> {
164    key_id(key, 'k').map(ConstraintId)
165}
166
167/// What the selection `keys` hold that `sketch` has: its points and curves,
168/// and its constraints.
169fn selected(sketch: &Sketch, keys: &[String]) -> (Selection, Vec<ConstraintId>) {
170    let mut selection = Selection::default();
171    let mut constraints = Vec::new();
172    for key in keys {
173        if let Some(p) = point_key(key).filter(|p| sketch.points.contains_key(p)) {
174            selection.points.push(p);
175        } else if let Some(c) = curve_key(key).filter(|c| sketch.curves.contains_key(c)) {
176            selection.curves.push(c);
177        } else if let Some(k) = glyph_key(key).filter(|k| sketch.constraints.contains_key(k)) {
178            constraints.push(k);
179        }
180    }
181    (selection, constraints)
182}
183
184/// The key of the visual among `visuals` the pointer is over, trying the
185/// kinds `stages` accept in order: a point drawn on the origin is that
186/// point, not the origin.
187fn hit_key<S: Scalar>(
188    visuals: &[Visual<S>],
189    pointer: &Pointer<S>,
190    stages: &[&dyn Fn(&str) -> bool],
191) -> Option<String> {
192    stages.iter().find_map(|accept| {
193        hit_visuals(visuals, pointer, |v| accept(&v.key)).map(|h| h.visual.key.clone())
194    })
195}
196
197fn is_point(key: &str) -> bool {
198    point_key(key).is_some()
199}
200
201fn is_curve(key: &str) -> bool {
202    curve_key(key).is_some()
203}
204
205fn is_origin(key: &str) -> bool {
206    key == "origin"
207}
208
209/// The point `id` of `sketch`.
210fn pt(sketch: &Sketch, id: PointId) -> P2 {
211    sketch.points[&id].xy()
212}
213
214/// The edit in progress: what it works on, what is selected, and where the
215/// plane is.
216struct Editing<'a, S: Scalar> {
217    args: &'a mut AddSketchArgs,
218    s: &'a mut SketchSession,
219    selection: &'a mut Vec<String>,
220    frame: CoordinateSystem<S>,
221}
222
223/// `p` of the sketch, in `frame`'s plane.
224fn to_world<S: Scalar>(frame: &CoordinateSystem<S>, p: P2) -> Vector3<S> {
225    frame.uv_to_xyz(&Vector2::from_array(p.map(S::from_f64)))
226}
227
228/// Solves `sketch`, pulling `drags` towards their targets, and records how
229/// that went. A sketch the solver rejects outright stays as drawn.
230fn solve(sketch: &mut Sketch, s: &mut SketchSession, drags: &[(PointId, P2)]) {
231    s.solved = Some(match sketch.solve_with_drag(drags) {
232        Ok(report) => Solved::Solved { report },
233        Err(e) => Solved::Failed {
234            error: e.root_message().to_string(),
235        },
236    });
237}
238
239impl<S: Scalar> Editing<'_, S> {
240    fn sketch(&self) -> &Sketch {
241        &self.args.sketch
242    }
243
244    /// Where `pointer`'s ray meets the plane, in the sketch's coordinates,
245    /// and how far along the ray.
246    fn in_plane(&self, pointer: &Pointer<S>) -> Option<(P2, S)> {
247        let (t, p) = pointer.ray.intersect_uv_plane(&self.frame)?;
248        Some(([p[0].to_f64(), p[1].to_f64()], t))
249    }
250
251    /// Makes `next` the sketch, solved.
252    fn commit(&mut self, mut next: Sketch) {
253        solve(&mut next, self.s, &[]);
254        self.args.sketch = next;
255    }
256
257    /// `p`, a point of the plane, in the sketch's coordinates.
258    fn to_sketch(&self, p: &Vector3<S>) -> P2 {
259        let uvw = self.frame.to_uvw(p);
260        [uvw[0].to_f64(), uvw[1].to_f64()]
261    }
262}
263
264/// What a sketch step shows: the plane to pick and, once it is one, the
265/// sketch drawn in it with the tools, constraints and state of the drawing.
266pub(crate) fn form<'a, S: Scalar>(
267    before: &'a Part<S>,
268    args: &AddSketchArgs,
269    s: &SketchSession,
270    selection: &[String],
271) -> Form<'a, S, AddSketchArgs, SketchSession> {
272    let mut s = s.clone();
273    if s.solved.is_none() {
274        solve(&mut args.sketch.clone(), &mut s, &[]);
275    }
276    let mut f = Form::<S, AddSketchArgs, SketchSession>::new();
277    // Only a plane is taken; another one moves the drawing onto it.
278    f.reference(
279        "plane",
280        "plane",
281        args.plane.iter().cloned().collect(),
282        &[Role::Plane],
283        None,
284        false,
285        move |edit, picked| {
286            if let [plane] = picked.as_slice()
287                && plane.resolve_plane(before).is_ok()
288            {
289                edit.args.plane = Some(plane.clone());
290                edit.session.draft = None;
291            }
292        },
293    );
294    let Some(plane) = &args.plane else {
295        return f;
296    };
297    let frame = match plane.resolve_plane(before) {
298        Ok(frame) => frame,
299        Err(e) => {
300            f.text("plane_error", e.root_message(), Tone::Error);
301            return f;
302        }
303    };
304    draw_dialog(&mut f, before, &args.sketch, &s, selection);
305    f.visuals = visuals(&args.sketch, &s, &frame);
306    f.focus = Some(frame);
307    f.tool = s.tool != Tool::Select;
308    f
309}
310
311/// A pointer or key event in the drawing.
312pub(crate) fn event<S: Scalar>(
313    before: &Part<S>,
314    edit: Edit<'_, AddSketchArgs, SketchSession>,
315    event: &CanvasEvent<S>,
316) {
317    editing(before, edit, |e| e.event(event));
318}
319
320/// `f` applied to the drawing, solved first if it is not yet. Nothing, while
321/// the plane is none.
322fn editing<S: Scalar>(
323    before: &Part<S>,
324    edit: Edit<'_, AddSketchArgs, SketchSession>,
325    f: impl FnOnce(&mut Editing<S>),
326) {
327    let Edit {
328        args,
329        session: s,
330        selection,
331    } = edit;
332    let Some(Ok(frame)) = args.plane.as_ref().map(|p| p.resolve_plane(before)) else {
333        return;
334    };
335    if s.solved.is_none() {
336        solve(&mut args.sketch, s, &[]);
337    }
338    f(&mut Editing {
339        args,
340        s,
341        selection,
342        frame,
343    });
344}