Skip to main content

geop_ops_sketch3d/
editor.rs

1//! Editing a 3-D sketch step: drawing points, lines, arcs and splines in
2//! space, constraining them, dragging them — every gesture answered with a
3//! new sketch, solved before it is returned.
4//!
5//! A click places a point where the pointer hits: on a point the sketch
6//! has, it *is* that point; on a vertex of the part or a datum point, it is
7//! a fixed point there, kept there as the part changes; on an edge or a
8//! sketch's curve, it is constrained onto it; drawing a line, on the line
9//! along `x`, `y` or `z` through the point it starts at, it is there, and
10//! the line is made parallel to that axis; on a face or a datum plane, it
11//! is where the face is hit. Anywhere else it lies in the plane through
12//! the last point placed, facing the eye. Coordinates can be typed instead.
13//!
14//! As in a planar sketch, proximity only suggests: what a point snaps to is
15//! the constraint the sketch then holds, the snap is shown before the
16//! click, and holding shift turns snapping off — the point goes where the
17//! pointer meets a face or the plane through the last point. Arrows along
18//! `x`, `y` and `z` at the last point placed show the directions lines
19//! snap to.
20//!
21//! The lines a line tool draws chain from point to point, and so do the
22//! arcs of the arc tool: from where the last ended, through a point, to
23//! another. A spline takes points until it is finished — a double click,
24//! Enter — and goes on smoothly from a curve it starts at, or into one it
25//! ends at (see [`geop_core_sketch::space::Sketch3d::adopts`]).
26
27use geop_core_editor::view::{drawn, icons};
28use geop_core_editor::{
29    edit_form::{Edit, EditForm},
30    editing::{
31        Action, CanvasEvent, Gizmo, InHand, ListItem, Number, Pointer, Shape, Style, Tone, Unit,
32        Value, Visual,
33        hit::{Scene, hit_visuals, no_instances},
34    },
35    selectable,
36};
37use geop_core_math::{scalars::Scalar, vector::Vector3};
38use geop_core_program::reader::Reader;
39use geop_core_sketch::{
40    ConstraintId, CurveId, PointId,
41    space::{Arc3, Constraint3d, Coordinate, CurveKind3d, End, Solve3dReport},
42};
43use geop_ops_model::sketch::fields::constraint_status;
44
45/// How near the pointer, in its reaches, must pass the line along `x`,
46/// `y` or `z` through the point a line starts at for the line's end to
47/// snap onto it: an allowance on screen, like the pointer's own reach.
48const AXIS_SNAP: f64 = 2.0;
49
50use crate::{AddSketch3dArgs, Reference3d, Target};
51use geop_ops_model::sketch3d::{CurveSource, LineSource, PointSource};
52
53/// What a click in the viewport does.
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum Tool {
56    /// Selecting and dragging.
57    Select,
58    Point,
59    Line,
60    Arc,
61    Spline,
62    Helix,
63}
64
65impl Tool {
66    /// Every tool: how events name it, its label and its key.
67    /// Each tool, its name, its label, its key and its icon — the planar
68    /// sketch's; selecting has none, and shows its label.
69    const ALL: [(
70        Tool,
71        &'static str,
72        &'static str,
73        &'static str,
74        Option<&'static str>,
75    ); 6] = [
76        (Tool::Select, "select", "Select", "Escape", None),
77        (Tool::Point, "point", "Point", "p", Some(icons::POINT)),
78        (Tool::Line, "line", "Line", "l", Some(icons::LINE)),
79        (Tool::Arc, "arc", "3-point arc", "a", Some(icons::ARC)),
80        (Tool::Spline, "spline", "Spline", "s", Some(icons::SPLINE)),
81        (
82            Tool::Helix,
83            "helix",
84            "Helix",
85            "h",
86            Some(crate::icons::HELIX),
87        ),
88    ];
89
90    fn draws(self) -> bool {
91        self != Tool::Select
92    }
93}
94
95/// The state of editing a 3-D sketch beyond its arguments.
96#[derive(Clone, Debug, Default)]
97pub struct Sketch3dSession {
98    /// The tool in hand, once one is chosen: until then, a line for an
99    /// empty sketch, selecting for one with curves.
100    tool: Option<Tool>,
101    /// The points placed for what is being drawn: a chain's last point, an
102    /// arc's start and the point it passes through, a spline's points.
103    placed: Vec<PointId>,
104    /// The points placed that were new: taken out again if what is drawn
105    /// ends without using them.
106    fresh: Vec<PointId>,
107    /// Where a click would place a point now, and what it snapped to.
108    hover: Option<Located>,
109    /// The coordinates typed for the next point.
110    typed: [f64; 3],
111    /// How the next helix turns, once told: how many times, and whether
112    /// left-handed.
113    turns: Option<f64>,
114    left: bool,
115}
116
117/// How many times a helix turns, until told otherwise.
118const TURNS: f64 = 5.0;
119
120impl Sketch3dSession {
121    /// The tool in hand.
122    pub fn tool(&self, args: &AddSketch3dArgs) -> Tool {
123        self.tool.unwrap_or_else(|| {
124            if args.sketch.curves.values().any(|c| c.is_drawn()) {
125                Tool::Select
126            } else {
127                Tool::Line
128            }
129        })
130    }
131}
132
133/// What the selection holds of the sketch.
134#[derive(Default)]
135struct Picks {
136    points: Vec<PointId>,
137    curves: Vec<CurveId>,
138}
139
140/// The points the gizmo moves: those selected and those of the curves
141/// selected, each once — but none fixed, which do not move.
142fn moved(args: &AddSketch3dArgs, picks: &Picks) -> Vec<PointId> {
143    let sketch = &args.sketch;
144    let mut points: Vec<PointId> = picks.points.clone();
145    for c in &picks.curves {
146        points.extend(sketch.curves[c].points());
147    }
148    let mut seen = std::collections::BTreeSet::new();
149    points.retain(|p| seen.insert(*p) && !sketch.points[p].fixed);
150    points
151}
152
153fn picked(args: &AddSketch3dArgs, selection: &[String]) -> Picks {
154    let sketch = &args.sketch;
155    let mut picks = Picks::default();
156    for key in selection {
157        if let Some(p) = key.parse().ok().filter(|p| sketch.points.contains_key(p)) {
158            picks.points.push(p);
159        } else if let Some(c) = key.parse().ok().filter(|c| sketch.curves.contains_key(c)) {
160            picks.curves.push(c);
161        }
162    }
163    picks
164}
165
166/// `args` with what the part `before` gives it brought up to date, and how
167/// its constraints stand — or why that cannot be said.
168fn checked(
169    before: &Reader,
170    args: &AddSketch3dArgs,
171) -> Result<(AddSketch3dArgs, Solve3dReport), String> {
172    let mut args = args.clone();
173    args.resolve(before)
174        .map_err(|e| e.root_message().to_string())?;
175    let report = args
176        .sketch
177        .check()
178        .map_err(|e| e.root_message().to_string())?;
179    Ok((args, report))
180}
181
182/// Solves `args` on `before`, pulling `drags` — what the part gives
183/// brought up to date first. What cannot be solved is left as it is, and
184/// the form says why.
185fn solve(before: &Reader, args: &mut AddSketch3dArgs, drags: &[(PointId, Vector3)]) {
186    args.tidy();
187    if args.resolve(before).is_ok() {
188        let _ = args.sketch.solve_with_drag(drags);
189    }
190}
191
192// ── where a click goes ──────────────────────────────────────────────────────
193
194/// Where a click places a point (see the module docs).
195#[derive(Clone, Debug)]
196enum Located {
197    /// A point the sketch has.
198    Point(PointId),
199    /// A point of the part — a vertex, a datum point: a fixed point there.
200    Fixed(PointSource, Vector3),
201    /// A point of an edge of the part, or of a sketch's curve: on it.
202    OnEdge(CurveSource, Vector3),
203    /// A point on the line along `axis` through the point a line starts
204    /// at: there, the line parallel to the axis.
205    Along(Coordinate, Vector3),
206    /// Anywhere else.
207    Free(Vector3),
208}
209
210impl Located {
211    /// Where it is in the sketch `args` holds: none for a point of the
212    /// sketch it no longer has — a hover kept from before an edit that took
213    /// the point out (an undo, a drag started again from where it began).
214    fn at(&self, args: &AddSketch3dArgs) -> Option<Vector3> {
215        match self {
216            Located::Point(p) => args.sketch.points.get(p).map(|p| p.at),
217            Located::Fixed(_, at)
218            | Located::OnEdge(_, at)
219            | Located::Along(_, at)
220            | Located::Free(at) => Some(*at),
221        }
222    }
223}
224
225/// Where `pointer` snaps onto a line along `x`, `y` or `z` through `from`,
226/// if it passes within [`AXIS_SNAP`] reaches of one: the axis it passes
227/// nearest on screen, and the point of it nearest the pointer's ray.
228fn along_axis(from: &Vector3, pointer: &Pointer) -> Option<Located> {
229    Coordinate::ALL
230        .into_iter()
231        .filter_map(|axis| {
232            let d = Vector3::axis(axis.index());
233            let at = from.add(&d.prod_scalar(pointer.ray.line_parameter(from, &d)?));
234            let (dist, t) = pointer.ray.distance_to_point(&at);
235            // Which axis is nearer on screen is a free choice between
236            // those within reach: plain numbers decide it.
237            let on_screen = dist.to_f64() / pointer.reach_at(1.0, t).to_f64();
238            pointer
239                .within(dist, t, AXIS_SNAP)
240                .then_some((on_screen, axis, at))
241        })
242        .min_by(|a, b| a.0.total_cmp(&b.0))
243        .map(|(_, axis, at)| Located::Along(axis, at))
244}
245
246/// Where a click with `pointer` places a point (see the module docs) with
247/// the tool `tool` in hand — snapped, unless `shift` is held — on the part
248/// `before` as drawn, `scene`.
249fn locate(
250    before: &Reader,
251    scene: Option<&dyn Scene>,
252    args: &AddSketch3dArgs,
253    s: &Sketch3dSession,
254    tool: Tool,
255    pointer: &Pointer,
256    shift: bool,
257) -> Option<Located> {
258    if !shift {
259        let points: Vec<Visual> = args
260            .sketch
261            .points
262            .iter()
263            .map(|(id, p)| Visual::new(id.to_string(), Shape::Point { at: p.at }, Style::Free))
264            .collect();
265        if let Some(hit) = hit_visuals(&points, pointer, no_instances, |_| true) {
266            return Some(Located::Point(hit.visual.key.parse().ok()?));
267        }
268        if let Some(scene) = scene {
269            if let Some((point, _)) = selectable::pick::<PointSource>(scene, before, pointer) {
270                let at = point.resolve(before).ok()?;
271                return Some(Located::Fixed(point, at));
272            }
273            if let Some((curve, at)) = selectable::pick::<CurveSource>(scene, before, pointer) {
274                return Some(Located::OnEdge(curve, at));
275            }
276        }
277        let from = (s.placed.last()).and_then(|p| args.sketch.points.get(p).map(|p| p.at));
278        if tool == Tool::Line
279            && let Some(along) = from.and_then(|from| along_axis(&from, pointer))
280        {
281            return Some(along);
282        }
283    }
284    if let Some(scene) = scene
285        && let Some(hit) = scene.pick(pointer, &[drawn::FACE.into(), drawn::DATUM.into()])
286    {
287        return Some(Located::Free(hit.point));
288    }
289    // In the plane through the last point placed — or the origin — facing
290    // the eye.
291    let anchor = (s.placed.last())
292        .and_then(|p| args.sketch.points.get(p).map(|p| p.at))
293        .unwrap_or_else(Vector3::zero);
294    let (_, at) = pointer.ray.intersect_plane(&anchor, pointer.ray.dir())?;
295    Some(Located::Free(at))
296}
297
298/// The point a click at `located` places: the sketch's own, or a new one —
299/// fixed at a point of the part, or constrained onto an edge of it.
300fn place(args: &mut AddSketch3dArgs, s: &mut Sketch3dSession, located: Located) -> PointId {
301    let sketch = &mut args.sketch;
302    match located {
303        Located::Point(p) => p,
304        Located::Fixed(source, at) => {
305            let known = args.references.iter().find_map(|r| match r {
306                Reference3d::Point { point, from } if *from == source => Some(*point),
307                _ => None,
308            });
309            known.unwrap_or_else(|| {
310                let point = sketch.add_fixed_point(at);
311                args.references.push(Reference3d::Point {
312                    point,
313                    from: source,
314                });
315                s.fresh.push(point);
316                point
317            })
318        }
319        Located::OnEdge(source, at) => {
320            let known = args.references.iter().find_map(|r| match r {
321                Reference3d::Curve { curve, from } if *from == source => Some(*curve),
322                _ => None,
323            });
324            let curve = known.unwrap_or_else(|| {
325                let curve = sketch.add_curve(CurveKind3d::Reference);
326                args.references.push(Reference3d::Curve {
327                    curve,
328                    from: source,
329                });
330                curve
331            });
332            let point = sketch.add_point(at);
333            sketch.constrain(Constraint3d::OnCurve { point, curve });
334            s.fresh.push(point);
335            point
336        }
337        Located::Along(_, at) | Located::Free(at) => {
338            let point = sketch.add_point(at);
339            s.fresh.push(point);
340            point
341        }
342    }
343}
344
345/// Whether `a` and `b` are one point of the sketch.
346fn same(args: &AddSketch3dArgs, a: PointId, b: PointId) -> bool {
347    let class = args.sketch.point_classes();
348    class[&a] == class[&b]
349}
350
351/// Makes the spline ends of `curve`, and the spline ends it meets, go on
352/// smoothly where it meets one other curve — unless that end already has a
353/// direction.
354fn smooth_joints(args: &mut AddSketch3dArgs, curve: CurveId) {
355    let sketch = &args.sketch;
356    let is_spline = |c: CurveId| matches!(sketch.curves[&c].kind, CurveKind3d::Spline { .. });
357    let others: Vec<CurveId> = sketch
358        .curves
359        .iter()
360        .filter(|&(&c, k)| c != curve && k.is_drawn() && !k.construction)
361        .map(|(&c, _)| c)
362        .collect();
363    let mut tangents = Vec::new();
364    for &other in &others {
365        if !(is_spline(curve) || is_spline(other)) {
366            continue;
367        }
368        let Ok(Some((end, _))) = sketch.shared_end(curve, other) else {
369            continue;
370        };
371        // Only where the two meet alone.
372        let meeting = others
373            .iter()
374            .filter(|&&c| {
375                sketch
376                    .shared_end(curve, c)
377                    .ok()
378                    .flatten()
379                    .is_some_and(|(e, _)| e == end)
380            })
381            .count();
382        if meeting == 1 {
383            tangents.push(Constraint3d::Tangent { a: other, b: curve });
384        }
385    }
386    for tangent in tangents {
387        let id = args.sketch.constrain(tangent);
388        if args.sketch.validate().is_err() {
389            args.sketch.remove(&[], &[], &[id]);
390        }
391    }
392}
393
394/// A click of the tool `tool` at `located`.
395fn click(
396    before: &Reader,
397    args: &mut AddSketch3dArgs,
398    s: &mut Sketch3dSession,
399    tool: Tool,
400    located: Located,
401) {
402    let along = match located {
403        Located::Along(axis, _) => Some(axis),
404        _ => None,
405    };
406    let p = place(args, s, located);
407    match tool {
408        Tool::Select => {}
409        Tool::Point => {
410            s.fresh.clear();
411        }
412        Tool::Line => {
413            if let Some(&last) = s.placed.last()
414                && !same(args, last, p)
415            {
416                let line = args.sketch.add_line(last, p);
417                if let Some(axis) = along {
418                    args.sketch.constrain(Constraint3d::ParallelTo {
419                        line,
420                        direction: Vector3::axis(axis.index()),
421                    });
422                }
423                smooth_joints(args, line);
424                s.fresh.clear();
425            }
426            s.placed = vec![p];
427        }
428        Tool::Arc => {
429            if s.placed.last().is_none_or(|&last| !same(args, last, p)) {
430                s.placed.push(p);
431            }
432            if let [start, through, end] = s.placed[..] {
433                if !same(args, start, end) {
434                    let arc = args.sketch.add_arc(start, through, end);
435                    smooth_joints(args, arc);
436                    s.fresh.clear();
437                }
438                s.placed = vec![end];
439            }
440        }
441        Tool::Spline => {
442            if s.placed.last().is_none_or(|&last| !same(args, last, p)) {
443                s.placed.push(p);
444            }
445        }
446        Tool::Helix => {
447            if s.placed.iter().all(|&placed| !same(args, placed, p)) {
448                s.placed.push(p);
449            }
450            if let [base, top, start] = s.placed[..] {
451                let turns = Scalar::from_f64(s.turns.unwrap_or(TURNS));
452                if args
453                    .sketch
454                    .add_helix(start, base, top, turns, s.left)
455                    .is_ok()
456                {
457                    s.fresh.clear();
458                }
459                s.placed.clear();
460            }
461        }
462    }
463    solve(before, args, &[]);
464}
465
466/// Ends what is being drawn: a spline is added through the points placed,
467/// and points placed for nothing go again.
468fn finish(before: &Reader, args: &mut AddSketch3dArgs, s: &mut Sketch3dSession) {
469    if s.tool(args) == Tool::Spline && s.placed.len() >= 2 {
470        let spline = args.sketch.add_spline(std::mem::take(&mut s.placed));
471        smooth_joints(args, spline);
472        s.fresh.clear();
473    }
474    s.placed.clear();
475    let used = |p: &PointId| {
476        args.sketch.curves.values().any(|c| c.points().contains(p))
477            || args
478                .sketch
479                .constraints
480                .values()
481                .any(|k| !matches!(k, Constraint3d::OnCurve { .. }) && k.points().contains(p))
482    };
483    let unused: Vec<PointId> = s.fresh.iter().copied().filter(|p| !used(p)).collect();
484    s.fresh.clear();
485    if !unused.is_empty() {
486        args.sketch.remove(&unused, &[], &[]);
487    }
488    solve(before, args, &[]);
489}
490
491/// Takes up `tool`, ending what was being drawn.
492fn take_up(before: &Reader, args: &mut AddSketch3dArgs, s: &mut Sketch3dSession, tool: Tool) {
493    finish(before, args, s);
494    s.tool = Some(tool);
495    s.hover = None;
496}
497
498/// Removes what is selected, and everything on it.
499fn remove_selected(before: &Reader, args: &mut AddSketch3dArgs, selection: &mut Vec<String>) {
500    let picks = picked(args, selection);
501    args.sketch.remove(&picks.points, &picks.curves, &[]);
502    selection.clear();
503    solve(before, args, &[]);
504}
505
506/// A pointer or key event passed on (see the module docs), what the
507/// pointer is over found in the part `before` as the editor draws it,
508/// `scene`.
509pub(crate) fn event(
510    before: &Reader,
511    scene: Option<&dyn Scene>,
512    edit: Edit<'_, AddSketch3dArgs, Sketch3dSession>,
513    event: &CanvasEvent,
514) {
515    let Edit {
516        args,
517        session: s,
518        selection,
519        ..
520    } = edit;
521    let tool = s.tool(args);
522    s.tool = Some(tool);
523    match event {
524        CanvasEvent::Hover { pointer, shift } => {
525            s.hover = tool
526                .draws()
527                .then(|| locate(before, scene, args, s, tool, pointer, *shift))
528                .flatten();
529        }
530        CanvasEvent::Leave => s.hover = None,
531        CanvasEvent::Click { double: true, .. } if tool.draws() => finish(before, args, s),
532        CanvasEvent::Click { pointer, shift, .. } if tool.draws() => {
533            if let Some(located) = locate(before, scene, args, s, tool, pointer, *shift) {
534                click(before, args, s, tool, located);
535            }
536            s.hover = None;
537        }
538        CanvasEvent::Confirm => finish(before, args, s),
539        CanvasEvent::Cancel if !s.placed.is_empty() => finish(before, args, s),
540        CanvasEvent::Cancel => take_up(before, args, s, Tool::Select),
541        CanvasEvent::Delete => remove_selected(before, args, selection),
542        CanvasEvent::Move { key, to, .. } => {
543            let point = key.parse().ok();
544            let free = |p: &PointId| args.sketch.points.get(p).is_some_and(|p| !p.fixed);
545            if let Some(p) = point.filter(free) {
546                solve(before, args, &[(p, *to)]);
547            }
548        }
549        // The selection moved by the gizmo, from where it was when the drag
550        // started: what the arguments are again for each event of it.
551        CanvasEvent::Gizmo { drag, .. } => {
552            let points = moved(args, &picked(args, selection));
553            let drags: Vec<(PointId, Vector3)> = points
554                .into_iter()
555                .map(|p| (p, drag.apply(&args.sketch.points[&p].at)))
556                .collect();
557            solve(before, args, &drags);
558        }
559        CanvasEvent::Key { key } => {
560            if let Some(&(tool, ..)) = Tool::ALL.iter().find(|t| t.3 == key) {
561                take_up(before, args, s, tool);
562            }
563        }
564        _ => {}
565    }
566}
567
568// ── what the form shows ─────────────────────────────────────────────────────
569
570/// What the tool in hand asks for next.
571fn hint(tool: Tool, placed: usize) -> &'static str {
572    match (tool, placed) {
573        (Tool::Select, _) => {
574            "Select points and curves to constrain them or move them with the gizmo, or drag points"
575        }
576        (Tool::Point, _) => "Click to place a point: on the part, or in space",
577        (Tool::Line, 0) => "Click where the line starts",
578        (Tool::Line, _) => "Click the next point · Esc ends the chain",
579        (Tool::Arc, 0) => "Click where the arc starts",
580        (Tool::Arc, 1) => "Click a point it passes through",
581        (Tool::Arc, _) => "Click where it ends",
582        (Tool::Spline, _) => "Click the points it passes through · double-click or Enter finishes",
583        (Tool::Helix, 0) => "Click the bottom of its axis",
584        (Tool::Helix, 1) => "Click the top of its axis: how far it rises",
585        (Tool::Helix, _) => "Click where it starts: how far from the axis says its radius",
586    }
587}
588
589/// The world axis `direction` is exactly, if it is one.
590fn axis_of(direction: &Vector3) -> Option<Coordinate> {
591    Coordinate::ALL.into_iter().find(|axis| {
592        (0..3).all(|k| {
593            let want = if k == axis.index() { 1.0 } else { 0.0 };
594            direction[k].to_f64() == want
595        })
596    })
597}
598
599/// How the constraint `id` reads in a list.
600fn describe(args: &AddSketch3dArgs, id: ConstraintId, c: &Constraint3d) -> String {
601    use Constraint3d::*;
602    let direction =
603        |direction: &Vector3| match args.reference_of(Target::Direction { constraint: id }) {
604            Some(r) => r.source().label(),
605            None => match axis_of(direction) {
606                Some(axis) => axis.name().to_string(),
607                None => "a direction".to_string(),
608            },
609        };
610    let end = |e: End| match e {
611        End::Start => "start",
612        End::End => "end",
613    };
614    match c {
615        Coincident { a, b } => format!("Coincident {a} {b}"),
616        Coordinate { point, axis, .. } => format!("{} of {point}", axis.name()),
617        Distance { a, b, .. } => format!("Distance {a} {b}"),
618        Length { line, .. } => format!("Length {line}"),
619        Radius { arc, .. } => format!("Radius {arc}"),
620        Parallel { a, b } => format!("Parallel {a} {b}"),
621        ParallelTo { line, direction: d } => format!("{line} along {}", direction(d)),
622        TangentTo {
623            curve,
624            end: e,
625            direction: d,
626        } => format!("{curve} {} along {}", end(*e), direction(d)),
627        OnCurve { point, curve } => match args.reference_of(Target::Curve { curve: *curve }) {
628            Some(r) => format!("{point} on {}", r.source().label()),
629            None => format!("{point} on {curve}"),
630        },
631        Tangent { a, b } => format!("Tangent {a} {b}"),
632        HelixEnd { helix } => format!("End of {helix}"),
633    }
634}
635
636/// The constraints the selection `picks` can take, each to add: its name,
637/// label and what it adds.
638fn constraints_for(
639    args: &AddSketch3dArgs,
640    picks: &Picks,
641) -> Vec<(&'static str, &'static str, Vec<Constraint3d>)> {
642    let sketch = &args.sketch;
643    let kind = |c: &CurveId| &sketch.curves[c].kind;
644    let lines: Vec<CurveId> = picks
645        .curves
646        .iter()
647        .copied()
648        .filter(|c| matches!(kind(c), CurveKind3d::Line { .. }))
649        .collect();
650    let at = |p: &PointId| sketch.points[p].at;
651    let dist = |a: &Vector3, b: &Vector3| b.sub(a).norm();
652    let (np, nc) = (picks.points.len(), picks.curves.len());
653    let mut out = Vec::new();
654    if np == 2 && nc == 0 {
655        let (a, b) = (picks.points[0], picks.points[1]);
656        out.push((
657            "coincident",
658            "Coincident",
659            vec![Constraint3d::Coincident { a, b }],
660        ));
661        out.push((
662            "distance",
663            "Distance",
664            vec![Constraint3d::Distance {
665                a,
666                b,
667                value: dist(&at(&a), &at(&b)),
668            }],
669        ));
670    }
671    if np >= 1 && nc == 0 {
672        let fixes = picks
673            .points
674            .iter()
675            .filter(|p| !sketch.points[p].fixed)
676            .flat_map(|&point| {
677                Coordinate::ALL.map(|axis| Constraint3d::Coordinate {
678                    point,
679                    axis,
680                    value: at(&point)[axis.index()],
681                })
682            })
683            .collect::<Vec<_>>();
684        if !fixes.is_empty() {
685            out.push(("fix", "Fix", fixes));
686        }
687    }
688    if np == 0 && nc == 1 {
689        let c = picks.curves[0];
690        match kind(&c) {
691            CurveKind3d::Line { start, end } => out.push((
692                "length",
693                "Length",
694                vec![Constraint3d::Length {
695                    line: c,
696                    value: dist(&at(start), &at(end)),
697                }],
698            )),
699            CurveKind3d::Arc {
700                start,
701                through,
702                end,
703            } => {
704                let arc = Arc3 {
705                    s: at(start),
706                    m: at(through),
707                    e: at(end),
708                };
709                if let Ok(value) = arc.radius() {
710                    out.push((
711                        "radius",
712                        "Radius",
713                        vec![Constraint3d::Radius { arc: c, value }],
714                    ));
715                }
716            }
717            _ => {}
718        }
719    }
720    if np == 0 && lines.len() == 2 {
721        out.push((
722            "parallel",
723            "Parallel",
724            vec![Constraint3d::Parallel {
725                a: lines[0],
726                b: lines[1],
727            }],
728        ));
729    }
730    if np == 0 && nc == 2 {
731        let (a, b) = (picks.curves[0], picks.curves[1]);
732        if sketch.shared_end(a, b).ok().flatten().is_some() {
733            out.push(("tangent", "Tangent", vec![Constraint3d::Tangent { a, b }]));
734        }
735    }
736    if np == 1
737        && nc == 1
738        && matches!(
739            kind(&picks.curves[0]),
740            CurveKind3d::Line { .. } | CurveKind3d::Arc { .. }
741        )
742    {
743        out.push((
744            "on_curve",
745            "On curve",
746            vec![Constraint3d::OnCurve {
747                point: picks.points[0],
748                curve: picks.curves[0],
749            }],
750        ));
751    }
752    for (name, label, axis) in [
753        ("along_x", "Along x", Coordinate::X),
754        ("along_y", "Along y", Coordinate::Y),
755        ("along_z", "Along z", Coordinate::Z),
756    ] {
757        if let Some(c) = along(args, picks, Vector3::axis(axis.index())) {
758            out.push((name, label, vec![c]));
759        }
760    }
761    out
762}
763
764/// What makes the selection `picks` run along `direction`, if it can: a
765/// line parallel to it, or an arc or a spline with one of its end points
766/// leaving or arriving along it.
767fn along(args: &AddSketch3dArgs, picks: &Picks, direction: Vector3) -> Option<Constraint3d> {
768    let sketch = &args.sketch;
769    let [curve] = picks.curves[..] else {
770        return None;
771    };
772    match (&sketch.curves[&curve].kind, &picks.points[..]) {
773        (CurveKind3d::Line { .. }, []) => Some(Constraint3d::ParallelTo {
774            line: curve,
775            direction,
776        }),
777        (CurveKind3d::Arc { .. } | CurveKind3d::Spline { .. }, [p]) => {
778            let (start, end) = sketch.curves[&curve].endpoints()?;
779            let end = if same(args, *p, start) {
780                End::Start
781            } else if same(args, *p, end) {
782                End::End
783            } else {
784                return None;
785            };
786            Some(Constraint3d::TangentTo {
787                curve,
788                end,
789                direction,
790            })
791        }
792        _ => None,
793    }
794}
795
796/// How the sketch stands, in a line.
797fn status(checked: &Result<(AddSketch3dArgs, Solve3dReport), String>) -> (String, Tone) {
798    let (args, report) = match checked {
799        Ok(checked) => checked,
800        Err(e) => return (e.clone(), Tone::Error),
801    };
802    let chains = match args.sketch.chains() {
803        Ok(chains) if chains.is_empty() => "nothing drawn yet".to_string(),
804        Ok(chains) => format!(
805            "{} chain{}",
806            chains.len(),
807            if chains.len() == 1 { "" } else { "s" }
808        ),
809        Err(e) => e.root_message().to_string(),
810    };
811    let conflicts = report.failed_constraints.len();
812    constraint_status(report.converged, conflicts, report.dof, &chains)
813}
814
815/// What the sketch draws: its curves and points, styled by how free they
816/// are, and what the tool in hand would draw next.
817fn visuals(
818    args: &AddSketch3dArgs,
819    s: &Sketch3dSession,
820    report: Option<&Solve3dReport>,
821) -> Vec<Visual> {
822    let sketch = &args.sketch;
823    let failed: Vec<&Constraint3d> = report
824        .map(|r| {
825            r.failed_constraints
826                .iter()
827                .filter_map(|k| sketch.constraints.get(k))
828                .collect()
829        })
830        .unwrap_or_default();
831    let free = |p: &PointId| report.is_none_or(|r| r.free_points.get(p).copied().unwrap_or(true));
832    let mut out = Vec::new();
833    for (&id, curve) in sketch.curves.iter().filter(|(_, c)| c.is_drawn()) {
834        let Ok(polyline) = sketch.curve_polyline(id) else {
835            continue;
836        };
837        let style = if failed.iter().any(|k| k.curves().contains(&id)) {
838            Style::Failed
839        } else if curve.construction {
840            Style::Construction
841        } else if curve.points().iter().any(free) {
842            Style::Free
843        } else {
844            Style::Fixed
845        };
846        out.push(
847            Visual::new(
848                id.to_string(),
849                Shape::Polyline {
850                    points: polyline.to_vec(),
851                },
852                style,
853            )
854            .selectable(),
855        );
856    }
857    for (&id, p) in &sketch.points {
858        let style = if p.fixed {
859            Style::Reference
860        } else if failed.iter().any(|k| k.points().contains(&id)) {
861            Style::Failed
862        } else if free(&id) {
863            Style::Free
864        } else {
865            Style::Fixed
866        };
867        let visual = Visual::new(id.to_string(), Shape::Point { at: p.at }, style).selectable();
868        out.push(if p.fixed { visual } else { visual.draggable() });
869    }
870    let at = |p: &PointId| sketch.points.get(p).map(|p| p.at);
871    let last = s.placed.last().and_then(at);
872    if let Some(located) = &s.hover
873        && let Some(hover) = located.at(args)
874    {
875        let mut draft: Vec<Vector3> = match s.tool(args) {
876            Tool::Spline | Tool::Arc | Tool::Helix => s.placed.iter().filter_map(at).collect(),
877            _ => last.into_iter().collect(),
878        };
879        draft.push(hover);
880        if draft.len() > 1 {
881            out.push(Visual::new(
882                "draft",
883                Shape::Polyline {
884                    points: draft.to_vec(),
885                },
886                Style::Draft,
887            ));
888        }
889        if let (Located::Along(axis, _), Some(from)) = (located, last) {
890            // The axis snapped to, from the line's start through its end,
891            // and which it is.
892            out.push(Visual::new(
893                "snap_axis",
894                Shape::Polyline {
895                    points: vec![from, hover],
896                },
897                Style::Snap,
898            ));
899            out.push(Visual::new(
900                "snap_label",
901                Shape::Label {
902                    at: hover,
903                    text: axis.name().to_string(),
904                    offset: Vector3::from_array([Scalar::ONE, Scalar::ONE, Scalar::ZERO]),
905                },
906                Style::Snap,
907            ));
908        }
909        let style = match located {
910            Located::Free(_) => Style::Draft,
911            _ => Style::Snap,
912        };
913        out.push(Visual::new("snap", Shape::Point { at: hover }, style));
914    }
915    // The directions a line snaps to, from where it starts — or, before
916    // that, from where the next point goes.
917    let anchor = last.or_else(|| s.hover.as_ref().and_then(|l| l.at(args)));
918    if let Some(at) = anchor.filter(|_| s.tool(args).draws()) {
919        out.push(Visual::new(
920            "triad",
921            Shape::Triad {
922                at,
923                axes: [0, 1, 2].map(Vector3::axis),
924            },
925            Style::Guide,
926        ));
927    }
928    out
929}
930
931/// The form of a 3-D sketch step on the part `before` reads (see the module
932/// docs).
933pub(crate) fn form<'a>(
934    before: &'a Reader<'a>,
935    args: &AddSketch3dArgs,
936    s: &Sketch3dSession,
937    selection: &[String],
938) -> EditForm<'a, AddSketch3dArgs, Sketch3dSession> {
939    let mut f = EditForm::default();
940    let tool = s.tool(args);
941    f.form.tool = if tool.draws() {
942        InHand::Clicks
943    } else {
944        InHand::Nothing
945    };
946    let mut tools: Vec<Action> = Tool::ALL
947        .iter()
948        .map(|&(t, name, label, _, icon)| {
949            Action::new(name, label)
950                .group("Draw")
951                .active(t == tool)
952                .icon(icon)
953        })
954        .collect();
955    let finishing = if tool == Tool::Spline {
956        "Finish the spline"
957    } else {
958        "End what is drawn"
959    };
960    tools.push(
961        Action::new("finish", "Finish")
962            .title(finishing)
963            .group("Draw"),
964    );
965    f.actions("tools", tools, move |edit, value| {
966        let Edit { args, session, .. } = edit;
967        match value {
968            "finish" => finish(before, args, session),
969            name => {
970                if let Some(&(t, ..)) = Tool::ALL.iter().find(|t| t.1 == name) {
971                    take_up(before, args, session, t);
972                }
973            }
974        }
975    });
976    (f.form.dialog).text("hint", hint(tool, s.placed.len()), Tone::Hint);
977
978    // The coordinates of the one point selected, to move it; else, with a
979    // tool in hand, of the next point, to place it.
980    let picks = picked(args, selection);
981    match (&picks.points[..], &picks.curves[..]) {
982        ([p], []) if !args.sketch.points[p].fixed => {
983            let at = args.sketch.points[p].at;
984            let p = *p;
985            for axis in Coordinate::ALL {
986                f.number(
987                    axis.name(),
988                    Number::new(axis.name(), at[axis.index()].to_f64(), Unit::Length),
989                    move |Edit { args, .. }, v| {
990                        let mut to = args.sketch.points[&p].at;
991                        to[axis.index()] = Scalar::from_f64(v);
992                        solve(before, args, &[(p, to)]);
993                    },
994                );
995            }
996        }
997        _ if tool.draws() => {
998            for axis in Coordinate::ALL {
999                let number = Number::new(axis.name(), s.typed[axis.index()], Unit::Length);
1000                f.number(axis.name(), number, move |edit, v| {
1001                    edit.session.typed[axis.index()] = v;
1002                });
1003            }
1004            f.actions(
1005                "place",
1006                vec![
1007                    Action::new("place", "Place point")
1008                        .title("Place a point at the coordinates typed"),
1009                ],
1010                move |edit, _| {
1011                    let Edit { args, session, .. } = edit;
1012                    let at = Vector3::from_array(session.typed.map(Scalar::from_f64));
1013                    let tool = session.tool(args);
1014                    click(before, args, session, tool, Located::Free(at));
1015                },
1016            );
1017        }
1018        _ => {}
1019    }
1020
1021    // How the one helix selected turns; else, with the tool in hand, how
1022    // the next will.
1023    let selected = match picks.curves[..] {
1024        [c] if picks.points.is_empty() => match args.sketch.curves[&c].kind {
1025            CurveKind3d::Helix { turns, left, .. } => Some((c, turns.to_f64(), left)),
1026            _ => None,
1027        },
1028        _ => None,
1029    };
1030    let helix = selected
1031        .map(|(_, turns, left)| (turns, left))
1032        .or_else(|| (tool == Tool::Helix).then(|| (s.turns.unwrap_or(TURNS), s.left)));
1033    if let Some((turns, left)) = helix {
1034        let curve = selected.map(|(c, ..)| c);
1035        let set = move |edit: Edit<'_, AddSketch3dArgs, Sketch3dSession>,
1036                        to_turns: Option<f64>,
1037                        to_left: Option<bool>| {
1038            let Edit { args, session, .. } = edit;
1039            match curve {
1040                Some(c) => {
1041                    if let Some(CurveKind3d::Helix { turns, left, .. }) =
1042                        args.sketch.curves.get_mut(&c).map(|c| &mut c.kind)
1043                    {
1044                        *turns = to_turns.map_or(*turns, Scalar::from_f64);
1045                        *left = to_left.unwrap_or(*left);
1046                    }
1047                    solve(before, args, &[]);
1048                }
1049                None => {
1050                    session.turns = to_turns.or(session.turns);
1051                    session.left = to_left.unwrap_or(session.left);
1052                }
1053            }
1054        };
1055        let number = Number::new("turns", turns, Unit::Count).range(0.25, 1000.0);
1056        f.number("helix_turns", number, move |edit, v| {
1057            set(edit, Some(v), None)
1058        });
1059        f.checkbox("helix_left", "Left-handed", left, move |edit, b| {
1060            set(edit, None, Some(b));
1061        });
1062    }
1063
1064    // What the selection can be constrained by.
1065    let available = constraints_for(args, &picks);
1066    if !available.is_empty() {
1067        let actions = available
1068            .iter()
1069            .map(|(name, label, _)| Action::new(*name, *label).group("Constrain"))
1070            .collect();
1071        f.actions("constrain", actions, move |edit, value| {
1072            let Edit {
1073                args, selection, ..
1074            } = edit;
1075            let picks = picked(args, selection);
1076            if let Some((_, _, constraints)) = constraints_for(args, &picks)
1077                .into_iter()
1078                .find(|(name, ..)| *name == value)
1079            {
1080                for c in constraints {
1081                    args.sketch.constrain(c);
1082                }
1083                selection.clear();
1084                solve(before, args, &[]);
1085            }
1086        });
1087    }
1088    if along(args, &picks, Vector3::from_array([Scalar::ONE; 3])).is_some() {
1089        f.pick(
1090            "along",
1091            "Along an axis or edge",
1092            before,
1093            &[] as &[LineSource],
1094            false,
1095            move |edit, lines| {
1096                let Edit {
1097                    args, selection, ..
1098                } = edit;
1099                let [line] = &lines[..] else {
1100                    return;
1101                };
1102                let Ok(axis) = line.resolve(before) else {
1103                    return;
1104                };
1105                let picks = picked(args, selection);
1106                if let Some(c) = along(args, &picks, axis.direction) {
1107                    let constraint = args.sketch.constrain(c);
1108                    args.references.push(Reference3d::Direction {
1109                        constraint,
1110                        from: line.clone(),
1111                    });
1112                    selection.clear();
1113                    solve(before, args, &[]);
1114                }
1115            },
1116        );
1117        f.form.dialog.optional("along");
1118    }
1119
1120    let checked = checked(before, args);
1121    let (text, tone) = status(&checked);
1122    f.form.dialog.text("status", text, tone);
1123
1124    let items = args
1125        .sketch
1126        .constraints
1127        .iter()
1128        .map(|(&id, c)| {
1129            let mut item = ListItem::new(id.to_string(), describe(args, id, c));
1130            item.removable = true;
1131            item.value = c.value().map(|v| v.to_f64());
1132            if let Ok((_, report)) = &checked
1133                && report.failed_constraints.contains(&id)
1134            {
1135                item.tone = Tone::Error;
1136            }
1137            item
1138        })
1139        .collect();
1140    (f.form.dialog).list("constraints", items, "No constraints yet");
1141    for &id in args.sketch.constraints.keys() {
1142        f.on(id.to_string(), move |edit, value| {
1143            let args = edit.args;
1144            match value {
1145                Value::Remove => {
1146                    args.sketch.remove(&[], &[], &[id]);
1147                    solve(before, args, &[]);
1148                }
1149                Value::Number(v) => {
1150                    if let Some(c) = args.sketch.constraints.get_mut(&id) {
1151                        c.set_value(Scalar::from_f64(v));
1152                        solve(before, args, &[]);
1153                    }
1154                }
1155                _ => {}
1156            }
1157        });
1158    }
1159    f.form.visuals = visuals(args, s, checked.as_ref().ok().map(|(_, r)| r));
1160    f.form.selection = selection.to_vec();
1161    // What is selected is moved by a gizmo at its middle: along an axis,
1162    // in a plane, or freely.
1163    let moving = moved(args, &picks);
1164    if tool == Tool::Select && !moving.is_empty() {
1165        let sum = moving
1166            .iter()
1167            .fold(Vector3::zero(), |sum, p| sum.add(&args.sketch.points[p].at));
1168        let n = Scalar::from_f64(moving.len() as f64);
1169        if let Ok(inverse) = Scalar::ONE.div(n) {
1170            let middle = sum.prod_scalar(inverse);
1171            f.form.gizmo = Some(Gizmo::new(middle).translate());
1172        }
1173    }
1174    f
1175}