Skip to main content

geop_ops_subd/
operation.rs

1//! [`Subd`]: a freeform body shaped by its control cage, built as the
2//! cage's Catmull–Clark limit surface — a solid for a closed cage, a sheet
3//! for an open one.
4//!
5//! The step holds the cage itself, not a history of edits: edits are made
6//! in the editor and leave the cage they made behind. A new cage is a
7//! basic shape — a box, a cylinder, a sphere or a plane — [`SIZE`] across,
8//! whose width, depth and height (or radius) are typed in the dialog while
9//! the cage is still that shape (see [`Basic::of`]). Its vertices, edges
10//! and faces are drawn, and picked by clicking them — an edge's loop by
11//! double-clicking it, its ring with shift held, everything in a window
12//! dragged after `b`; what is selected is moved, turned and scaled about
13//! its centre with the gizmo there — along the world's axes, or, for
14//! faces, their own — or by typing its centre's coordinates, a scale and
15//! turns; and edited with the actions: extruding and insetting faces,
16//! bridging two of them, bevelling edges, cutting a loop across them,
17//! sliding them, dissolving and merging, filling a hole, creasing edges
18//! sharp or soft, deleting. An element not selected is dragged on its
19//! own, in the plane facing the eye. The limit surface is drawn as the
20//! part, the cage over it; "cage only" hides the surface.
21
22use geop_core_editor::{
23    edit_form::{Edit, EditForm, FormState, choices, chosen},
24    editable::{Context, OperationGroup, OperationInfo, OperationTier},
25    editing::{
26        Action, CanvasEvent, DRAG_SNAP, Gizmo, InHand, Number, Pointer, Shape, Style, Tone, Unit,
27        Visual,
28        hit::{Window, hit_visuals, no_instances},
29    },
30    part_operation,
31    part_operation::PartOperation,
32};
33use geop_core_program::{part::StepContext, reader::Reader};
34use std::collections::BTreeSet;
35
36use geop_core_math::{
37    geop_error::{GeopResult, WithContext},
38    scalars::Scalar,
39    vector::Vector3,
40    with_context,
41};
42use geop_ops_model::{ModelOperation, NamedModel, Namer, build_model};
43use serde::{Deserialize, Serialize};
44
45use crate::{
46    brep::{face_names, limit_body},
47    cage::{Cage, Mesh, Mirror, edge_key, face_key, undirected, vertex_key},
48    edit::Element,
49};
50
51/// Builds the limit surface of `cage` — mirrored in `mirror`'s plane, if
52/// any — as the step `Scalar`: a solid named `subd(Scalar)`, or a sheet. What it is
53/// made of is named after the cage elements, see [`crate::brep`].
54#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
55pub struct Subd;
56
57#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
58pub struct SubdArgs {
59    pub cage: Cage,
60    /// The plane the cage is the half of a symmetric body on.
61    #[serde(default)]
62    pub mirror: Mirror,
63}
64
65/// What a subd step keeps while it is edited.
66pub type SubdEditing = FormState<SubdSession>;
67
68/// What a scale or turn of the selection is applied to: the cage as it was
69/// when the selection started being scaled or turned, so that a new value
70/// replaces the old one rather than adding to it.
71#[derive(Clone, Debug)]
72struct Relative {
73    selection: Vec<String>,
74    base: Cage,
75    scale: f64,
76    /// About `x`, `y` and `z`, in degrees, in that order.
77    angles: [f64; 3],
78}
79
80/// How far each edit goes, as typed; the defaults until set.
81#[derive(Clone, Copy, Debug, PartialEq)]
82pub struct Amounts {
83    /// How far an extrude goes.
84    pub distance: f64,
85    /// How far in an inset goes.
86    pub thickness: f64,
87    /// How wide a bevel is.
88    pub width: f64,
89    /// Where across its ring a loop is cut, as a fraction.
90    pub cut: f64,
91    /// How far a loop slides, as a fraction of the edges it slides along.
92    pub slide: f64,
93}
94
95impl Default for Amounts {
96    /// For a cage [`SIZE`] across: an extrude a quarter of it, an inset and
97    /// a bevel a tenth.
98    fn default() -> Self {
99        Self {
100            distance: SIZE / 4.0,
101            thickness: SIZE / 10.0,
102            width: SIZE / 10.0,
103            cut: 0.5,
104            slide: 0.25,
105        }
106    }
107}
108
109/// What editing a subd step keeps besides its arguments and the keys of
110/// the cage elements selected.
111#[derive(Clone, Debug, Default)]
112pub struct SubdSession {
113    relative: Option<Relative>,
114    pub amounts: Amounts,
115    /// Whether the next drag selects everything within its window.
116    pub box_select: bool,
117    /// The window being dragged, to draw.
118    window: Option<(Pointer, Pointer)>,
119    /// Whether the limit surface is hidden, to see the cage alone.
120    pub cage_only: bool,
121    /// Why the last edit could not be made.
122    problem: Option<String>,
123}
124
125/// How large a basic shape a cage starts from is: across, in each
126/// direction.
127pub const SIZE: f64 = 100.0;
128
129/// How many quads round a basic cylinder's cage.
130const CYLINDER_SEGMENTS: usize = 8;
131
132/// How many quads along each side of a basic plane's cage.
133const PLANE_QUADS: usize = 3;
134
135/// A basic shape a cage starts from, with its dimensions.
136#[derive(Clone, Copy, Debug, PartialEq)]
137pub enum Basic {
138    Box { width: f64, depth: f64, height: f64 },
139    Cylinder { radius: f64, height: f64 },
140    Sphere { radius: f64 },
141    Plane { width: f64 },
142}
143
144impl Basic {
145    /// The basic shape the action `name` starts from, [`SIZE`] across.
146    pub fn named(name: &str) -> Option<Basic> {
147        Some(match name {
148            "box" => Basic::Box {
149                width: SIZE,
150                depth: SIZE,
151                height: SIZE,
152            },
153            "cylinder" => Basic::Cylinder {
154                radius: SIZE / 2.0,
155                height: SIZE,
156            },
157            "sphere" => Basic::Sphere { radius: SIZE / 2.0 },
158            "plane" => Basic::Plane { width: SIZE },
159            _ => return None,
160        })
161    }
162
163    /// Its cage, around the origin.
164    pub fn cage(self) -> Cage {
165        match self {
166            Basic::Box {
167                width,
168                depth,
169                height,
170            } => Cage::cuboid([width, depth, height]),
171            Basic::Cylinder { radius, height } => Cage::cylinder(radius, height, CYLINDER_SEGMENTS),
172            Basic::Sphere { radius } => Cage::sphere(radius),
173            Basic::Plane { width } => Cage::plane(width, PLANE_QUADS),
174        }
175    }
176
177    /// Its cage, mirrored in `mirror`'s plane: the half the step holds.
178    fn cage_in(self, mirror: Mirror) -> GeopResult<Cage> {
179        let mut cage = self.cage();
180        if let Some(axis) = mirror.axis() {
181            cage.halve(axis)?;
182        }
183        Ok(cage)
184    }
185
186    /// The basic shape the cage `cage` — the half `mirror` mirrors, if it
187    /// does — is, as it was started from: each kind built with the
188    /// dimensions the whole cage's extent gives it, and compared. A cage
189    /// edited since is none: its dimensions are no longer the shape's to
190    /// set.
191    fn of(cage: &Cage, mirror: Mirror) -> Option<Basic> {
192        let whole = cage.unmirrored(mirror).ok()?;
193        let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
194        for v in &whole.vertices {
195            for k in 0..3 {
196                lo[k] = lo[k].min(v.at[k]);
197                hi[k] = hi[k].max(v.at[k]);
198            }
199        }
200        let [w, d, h] = [0, 1, 2].map(|k| hi[k] - lo[k]);
201        [
202            Basic::Box {
203                width: w,
204                depth: d,
205                height: h,
206            },
207            Basic::Cylinder {
208                radius: w / 2.0,
209                height: h,
210            },
211            Basic::Sphere { radius: w / 2.0 },
212            Basic::Plane { width: w },
213        ]
214        .into_iter()
215        .find(|basic| basic.cage_in(mirror).is_ok_and(|c| c == *cage))
216    }
217
218    /// Its dimensions: each one's key in the dialog, its label and its
219    /// value.
220    fn dimensions(self) -> Vec<(&'static str, &'static str, f64)> {
221        match self {
222            Basic::Box {
223                width,
224                depth,
225                height,
226            } => vec![
227                ("shape_width", "width", width),
228                ("shape_depth", "depth", depth),
229                ("shape_height", "height", height),
230            ],
231            Basic::Cylinder { radius, height } => vec![
232                ("shape_radius", "radius", radius),
233                ("shape_height", "height", height),
234            ],
235            Basic::Sphere { radius } => vec![("shape_radius", "radius", radius)],
236            Basic::Plane { width } => vec![("shape_width", "width", width)],
237        }
238    }
239
240    /// It with the dimension keyed `key` set to `value`.
241    fn with(mut self, key: &str, value: f64) -> Basic {
242        match (&mut self, key) {
243            (Basic::Box { width, .. } | Basic::Plane { width }, "shape_width") => *width = value,
244            (Basic::Box { depth, .. }, "shape_depth") => *depth = value,
245            (Basic::Box { height, .. } | Basic::Cylinder { height, .. }, "shape_height") => {
246                *height = value
247            }
248            (Basic::Cylinder { radius, .. } | Basic::Sphere { radius }, "shape_radius") => {
249                *radius = value
250            }
251            _ => {}
252        }
253        self
254    }
255}
256
257fn vector(p: [f64; 3]) -> Vector3 {
258    Vector3::from_array(p.map(Scalar::from_f64))
259}
260
261/// `p` turned by `angles` degrees about `x`, then `y`, then `z`.
262fn rotate(p: [f64; 3], angles: [f64; 3]) -> [f64; 3] {
263    let mut p = p;
264    for (axis, angle) in angles.iter().enumerate() {
265        let (s, c) = angle.to_radians().sin_cos();
266        let (i, j) = ((axis + 1) % 3, (axis + 2) % 3);
267        let (a, b) = (p[i], p[j]);
268        p[i] = c * a - s * b;
269        p[j] = s * a + c * b;
270    }
271    p
272}
273
274/// Keeps only the keys of elements `cage` still has.
275fn prune(selection: &mut Vec<String>, cage: &Cage) {
276    selection.retain(|key| Element::parse(key).is_some_and(|e| cage.has(e)));
277}
278
279/// What `selection` names: the vertices named as such, the edges, and the
280/// faces.
281#[derive(Clone, Debug, Default)]
282struct Selected {
283    vertices: BTreeSet<u32>,
284    edges: Vec<[u32; 2]>,
285    faces: BTreeSet<u32>,
286}
287
288fn selected(selection: &[String]) -> Selected {
289    let mut s = Selected::default();
290    for key in selection {
291        match Element::parse(key) {
292            Some(Element::Vertex(v)) => {
293                s.vertices.insert(v);
294            }
295            Some(Element::Edge(a, b)) => s.edges.push([a, b]),
296            Some(Element::Face(f)) => {
297                s.faces.insert(f);
298            }
299            None => {}
300        }
301    }
302    s
303}
304
305/// Adds the keys `keys` to `selection`, each once.
306fn extend(selection: &mut Vec<String>, keys: impl IntoIterator<Item = String>) {
307    for key in keys {
308        if !selection.contains(&key) {
309            selection.push(key);
310        }
311    }
312}
313
314/// The cage drawn: every vertex, edge and face of the half the step holds,
315/// selectable and draggable, creased edges drawn fixed; the mirror image,
316/// if any, as reference.
317fn cage_visuals(args: &SubdArgs) -> Vec<Visual> {
318    let cage = &args.cage;
319    let mut visuals = Vec::new();
320    let at = |v: u32| cage.vertex(v).map(|x| x.at).ok();
321    for face in &cage.faces {
322        let points: Option<Vec<[f64; 3]>> = face.vertices.iter().map(|&v| at(v)).collect();
323        let Some(points) = points else { continue };
324        let triangles = (1..points.len().saturating_sub(1))
325            .map(|k| [points[0], points[k], points[k + 1]].map(vector))
326            .collect();
327        visuals.push(
328            Visual::new(
329                face_key(face.id),
330                Shape::Triangles { triangles },
331                Style::Region,
332            )
333            .selectable()
334            .draggable_from_grab(),
335        );
336    }
337    for [a, b] in cage.edges() {
338        let (Some(p), Some(q)) = (at(a), at(b)) else {
339            continue;
340        };
341        let style = if cage.is_crease(a, b) {
342            Style::Fixed
343        } else {
344            Style::Free
345        };
346        visuals.push(
347            Visual::new(
348                edge_key(a, b),
349                Shape::Polyline {
350                    points: vec![vector(p), vector(q)],
351                },
352                style,
353            )
354            .selectable()
355            .draggable_from_grab(),
356        );
357    }
358    for v in &cage.vertices {
359        visuals.push(
360            Visual::new(
361                vertex_key(v.id),
362                Shape::Point { at: vector(v.at) },
363                Style::Free,
364            )
365            .selectable()
366            .draggable_from_grab(),
367        );
368    }
369    if let Some(axis) = args.mirror.axis()
370        && let Ok(mesh) = Mesh::new(cage, args.mirror)
371    {
372        for (f, face) in mesh.faces.iter().enumerate() {
373            if !mesh.face_names[f].ends_with('m') {
374                continue;
375            }
376            let n = face.len();
377            for k in 0..n {
378                let (a, b) = (face[k], face[(k + 1) % n]);
379                // Each mirrored edge once, from its smaller index.
380                if a < b || mesh.positions[a][axis] == 0.0 && mesh.positions[b][axis] == 0.0 {
381                    visuals.push(Visual::new(
382                        format!("mirror:{}", mesh.edge_name(a, b)),
383                        Shape::Polyline {
384                            points: vec![vector(mesh.positions[a]), vector(mesh.positions[b])],
385                        },
386                        Style::Reference,
387                    ));
388                }
389            }
390        }
391    }
392    visuals
393}
394
395/// The window `from`–`to` drawn where the cage is: a rectangle at the
396/// depth of the cage's centre.
397fn window_visual(args: &SubdArgs, from: &Pointer, to: &Pointer) -> Option<Visual> {
398    let window = Window::new(from, to)?;
399    let all: BTreeSet<u32> = args.cage.vertices.iter().map(|v| v.id).collect();
400    let centre = vector(args.cage.centre(&all)?);
401    let depth = window.depth(&centre);
402    let mut points = window.corners(depth).to_vec();
403    points.push(points[0]);
404    Some(Visual::new(
405        "window",
406        Shape::Polyline { points },
407        Style::Draft,
408    ))
409}
410
411/// The names of what the step `step` builds from `args`, to hide: its
412/// solid, and, for an open cage, each of its faces.
413fn built_names(step: &str, args: &SubdArgs) -> Vec<String> {
414    let Ok(namer) = Namer::new("subd", step) else {
415        return Vec::new();
416    };
417    let mut names = vec![namer.root()];
418    if let Ok(mesh) = Mesh::new(&args.cage, args.mirror) {
419        names.extend(face_names(&mesh, &namer));
420    }
421    names
422}
423
424impl Relative {
425    /// The scale and turn the session holds for `selection` on `cage`, or
426    /// a fresh one from the cage as it is.
427    fn of(session: &mut SubdSession, selection: &[String], cage: &Cage) -> Relative {
428        match session.relative.take() {
429            Some(r) if r.selection == selection => r,
430            _ => Relative {
431                selection: selection.to_vec(),
432                base: cage.clone(),
433                scale: 1.0,
434                angles: [0.0; 3],
435            },
436        }
437    }
438
439    /// The base cage, the selection scaled and turned about its centre.
440    fn apply(&self, mirror: Mirror) -> Cage {
441        let mut cage = self.base.clone();
442        let vertices = cage.vertices_of(&self.selection);
443        if let Some(centre) = cage.centre(&vertices) {
444            let (scale, angles) = (self.scale, self.angles);
445            cage.transform(&vertices, mirror, |p| {
446                let d = rotate([0, 1, 2].map(|c| p[c] - centre[c]), angles);
447                [0, 1, 2].map(|c| centre[c] + scale * d[c])
448            });
449        }
450        cage
451    }
452}
453
454/// Makes the edit `f` of the cage: kept if it succeeds, with the selection
455/// pruned to what is left; else its reason is shown.
456fn apply(edit: &mut Edit<'_, SubdArgs, SubdSession>, f: impl FnOnce(&mut Cage) -> GeopResult<()>) {
457    let mut cage = edit.args.cage.clone();
458    match f(&mut cage) {
459        Ok(()) => {
460            edit.args.cage = cage;
461            edit.session.problem = None;
462            edit.session.relative = None;
463            prune(edit.selection, &edit.args.cage);
464        }
465        Err(e) => edit.session.problem = Some(e.root_message().to_string()),
466    }
467}
468
469/// An edit of the cage, ready to make.
470type CageEdit = Box<dyn FnOnce(&mut Cage) -> GeopResult<()>>;
471
472/// The edits by the value of their action: what each does to the cage
473/// for the selection `s` with the amounts `amounts`, or `None` for a value
474/// that is no edit.
475fn edit_action(
476    value: &str,
477    s: &Selected,
478    all_vertices: &BTreeSet<u32>,
479    amounts: Amounts,
480    mirror: Mirror,
481) -> Option<CageEdit> {
482    let s = s.clone();
483    let all_vertices = all_vertices.clone();
484    Some(match value {
485        "extrude" => Box::new(move |cage| cage.extrude(&s.faces, amounts.distance, mirror)),
486        "inset" => Box::new(move |cage| cage.inset(&s.faces, amounts.thickness, mirror)),
487        "bridge" => Box::new(move |cage| {
488            let mut faces = s.faces.iter();
489            match (faces.next(), faces.next(), faces.next()) {
490                (Some(&a), Some(&b), None) => cage.bridge(a, b, mirror),
491                _ => Err(geop_core_math::geop_error::GeopError::new(
492                    "bridge: select exactly two faces",
493                )),
494            }
495        }),
496        "loop_cut" => Box::new(move |cage| {
497            // Two selected edges of one ring are one cut: the first cut
498            // split the second edge already.
499            for &[a, b] in &s.edges {
500                if cage.has(Element::Edge(a, b)) {
501                    cage.insert_loop(a, b, amounts.cut)?;
502                }
503            }
504            Ok(())
505        }),
506        "slide" => Box::new(move |cage| cage.edge_slide(&s.edges, amounts.slide, mirror)),
507        "bevel" => Box::new(move |cage| cage.bevel(&s.edges, amounts.width, mirror)),
508        "dissolve" => Box::new(move |cage| {
509            if !s.edges.is_empty() {
510                cage.dissolve_edges(&s.edges, mirror)?;
511            }
512            if !s.vertices.is_empty() {
513                cage.dissolve_vertices(&s.vertices, mirror)?;
514            }
515            Ok(())
516        }),
517        "merge" => Box::new(move |cage| cage.merge_vertices(&all_vertices, mirror)),
518        "fill" => Box::new(move |cage| {
519            let open = (s.edges.iter()).find(|&&[a, b]| cage.is_boundary(a, b));
520            match open {
521                Some(&[a, b]) => cage.fill_hole(a, b, mirror),
522                None => Err(geop_core_math::geop_error::GeopError::new(
523                    "fill: select an edge on the hole's rim",
524                )),
525            }
526        }),
527        "delete" => Box::new(move |cage| {
528            let mut faces = s.faces.clone();
529            faces.extend(cage.faces_along(&s.edges));
530            faces.extend(cage.faces_around(&s.vertices));
531            if faces.is_empty() {
532                return Err(geop_core_math::geop_error::GeopError::new(
533                    "delete: nothing selected",
534                ));
535            }
536            cage.delete_faces(&faces);
537            Ok(())
538        }),
539        _ => return None,
540    })
541}
542
543/// The keys of the selected edges' loops, or their rings.
544fn chains(cage: &Cage, edges: &[[u32; 2]], ring: bool) -> GeopResult<Vec<String>> {
545    let mut keys = Vec::new();
546    for &[a, b] in edges {
547        let found: Vec<[u32; 2]> = if ring {
548            (cage.edge_ring(a, b)?.edges.iter())
549                .map(|&(x, y)| undirected(x, y))
550                .collect()
551        } else {
552            cage.edge_loop(a, b)?
553        };
554        keys.extend(found.into_iter().map(|[x, y]| edge_key(x, y)));
555    }
556    Ok(keys)
557}
558
559/// The dialog of a subd step: the cage to start from, the mirror, what is
560/// selected and how to move, turn and scale it, and the edits; the cage
561/// drawn.
562fn form<'a>(
563    step: &str,
564    args: &SubdArgs,
565    session: &SubdSession,
566    selection: &[String],
567) -> EditForm<'a, SubdArgs, SubdSession> {
568    let mut f = EditForm::<SubdArgs, SubdSession>::default();
569    let start = ["box", "cylinder", "sphere", "plane"]
570        .into_iter()
571        .map(|p| {
572            let label = format!("{}{}", p[..1].to_uppercase(), &p[1..]);
573            Action::new(p, label)
574                .group("Start from")
575                .title("Replace the cage with this one")
576        })
577        .collect();
578    f.actions("start", start, |edit, value| {
579        let Some(basic) = Basic::named(value) else {
580            return;
581        };
582        match basic.cage_in(edit.args.mirror) {
583            Ok(cage) => {
584                edit.args.cage = cage;
585                edit.session.problem = None;
586                edit.session.relative = None;
587                edit.selection.clear();
588            }
589            Err(e) => edit.session.problem = Some(e.root_message().to_string()),
590        }
591    });
592    // While the cage is the basic shape it started from, its dimensions
593    // are set: the shape built again with them.
594    if let Some(basic) = Basic::of(&args.cage, args.mirror) {
595        for (key, label, value) in basic.dimensions() {
596            let number = Number::new(label, value, Unit::Length);
597            f.number(key, number, move |edit, value| {
598                if value <= 0.0 {
599                    edit.session.problem = Some(format!("the {label} has to be more than 0"));
600                    return;
601                }
602                match basic.with(key, value).cage_in(edit.args.mirror) {
603                    Ok(cage) => {
604                        edit.args.cage = cage;
605                        edit.session.problem = None;
606                        edit.session.relative = None;
607                        prune(edit.selection, &edit.args.cage);
608                    }
609                    Err(e) => edit.session.problem = Some(e.root_message().to_string()),
610                }
611            });
612        }
613    }
614    // Each choice of mirror: what the dialog sends, and what it shows.
615    const MIRRORS: [(Mirror, &str, &str); 4] = [
616        (Mirror::None, "none", "none"),
617        (Mirror::X, "x", "in x = 0"),
618        (Mirror::Y, "y", "in y = 0"),
619        (Mirror::Z, "z", "in z = 0"),
620    ];
621    let (mirror, options) = choices(&MIRRORS, &args.mirror);
622    f.select(
623        "mirror",
624        "mirror",
625        mirror,
626        options,
627        false,
628        move |edit, value| {
629            let mirror = chosen(&MIRRORS, value).unwrap_or_default();
630            if mirror == edit.args.mirror {
631                return;
632            }
633            let changed = edit
634                .args
635                .cage
636                .unmirrored(edit.args.mirror)
637                .and_then(|mut cage| {
638                    if let Some(axis) = mirror.axis() {
639                        cage.halve(axis)?;
640                    }
641                    Ok(cage)
642                });
643            match changed {
644                Ok(cage) => {
645                    edit.args.cage = cage;
646                    edit.args.mirror = mirror;
647                    edit.session.problem = None;
648                    edit.session.relative = None;
649                    prune(edit.selection, &edit.args.cage);
650                }
651                Err(e) => edit.session.problem = Some(e.root_message().to_string()),
652            }
653        },
654    );
655    f.checkbox("cage_only", "cage only", session.cage_only, |edit, on| {
656        edit.session.cage_only = on;
657    });
658
659    let vertices = args.cage.vertices_of(selection);
660    let s = selected(selection);
661    let count = |n: usize, one: &str, many: &str| match n {
662        0 => None,
663        1 => Some(format!("1 {one}")),
664        n => Some(format!("{n} {many}")),
665    };
666    let parts: Vec<String> = [
667        count(s.vertices.len(), "vertex", "vertices"),
668        count(s.edges.len(), "edge", "edges"),
669        count(s.faces.len(), "face", "faces"),
670    ]
671    .into_iter()
672    .flatten()
673    .collect();
674    if parts.is_empty() {
675        f.form.dialog.text(
676            "selection",
677            "Click vertices, edges and faces of the cage to select them; double-click an edge for its loop, with shift for its ring; press b and drag a window around several. Move, turn or scale the selection with the gizmo; drag one element to move it alone.",
678            Tone::Hint,
679        );
680    } else {
681        f.form.dialog.text(
682            "selection",
683            format!("{} selected", parts.join(", ")),
684            Tone::Normal,
685        );
686    }
687    if let Some(problem) = &session.problem {
688        f.form.dialog.text("problem", problem.clone(), Tone::Error);
689    }
690    let select_action = |value: &str, label: &str, title: &str| {
691        let action = Action::new(value, label).group("Select").title(title);
692        if s.edges.is_empty() && value != "box" {
693            action.disabled("Select an edge first")
694        } else {
695            action
696        }
697    };
698    let select = vec![
699        select_action(
700            "loop",
701            "Loop",
702            "Select the edge loop through each selected edge",
703        ),
704        select_action(
705            "ring",
706            "Ring",
707            "Select the edge ring across each selected edge",
708        ),
709        select_action(
710            "box",
711            "Box",
712            "Drag a window to select what is within it (b)",
713        )
714        .active(session.box_select),
715    ];
716    f.actions("select", select, |edit, value| match value {
717        "box" => {
718            edit.session.box_select = !edit.session.box_select;
719            edit.session.window = None;
720        }
721        "loop" | "ring" => {
722            let s = selected(edit.selection);
723            match chains(&edit.args.cage, &s.edges, value == "ring") {
724                Ok(keys) => {
725                    extend(edit.selection, keys);
726                    edit.session.problem = None;
727                }
728                Err(e) => edit.session.problem = Some(e.root_message().to_string()),
729            }
730        }
731        _ => {}
732    });
733
734    if let Some(centre) = args.cage.centre(&vertices) {
735        let mut gizmo = Gizmo::new(vector(centre)).translate().rotate().scale();
736        if let Some(axes) = args.cage.frame(&s.faces) {
737            gizmo = gizmo.local(axes.map(vector));
738        }
739        f.form.gizmo = Some(gizmo);
740        for (axis, key) in ["x", "y", "z"].into_iter().enumerate() {
741            let number = Number::new(key, centre[axis], Unit::Length);
742            f.number(key, number, move |edit, to| {
743                let cage = &mut edit.args.cage;
744                let vertices = cage.vertices_of(edit.selection);
745                let Some(centre) = cage.centre(&vertices) else {
746                    return;
747                };
748                let by = to - centre[axis];
749                cage.transform(&vertices, edit.args.mirror, |mut p| {
750                    p[axis] += by;
751                    p
752                });
753                edit.session.relative = None;
754            });
755        }
756        let relative = session
757            .relative
758            .as_ref()
759            .filter(|r| r.selection == selection);
760        let scale = relative.map_or(1.0, |r| r.scale);
761        let scale_number = Number::new("scale", scale, Unit::Fraction);
762        f.number("scale", scale_number, |edit, scale| {
763            let mut relative = Relative::of(edit.session, edit.selection, &edit.args.cage);
764            relative.scale = scale;
765            edit.args.cage = relative.apply(edit.args.mirror);
766            edit.session.relative = Some(relative);
767        });
768        for (axis, key) in ["turn_x", "turn_y", "turn_z"].into_iter().enumerate() {
769            let angle = relative.map_or(0.0, |r| r.angles[axis]);
770            let label = format!("turn about {}", ["x", "y", "z"][axis]);
771            let number = Number::new(label, angle, Unit::Angle);
772            f.number(key, number, move |edit, angle| {
773                let mut relative = Relative::of(edit.session, edit.selection, &edit.args.cage);
774                relative.angles[axis] = angle;
775                edit.args.cage = relative.apply(edit.args.mirror);
776                edit.session.relative = Some(relative);
777            });
778        }
779    }
780
781    let amounts = session.amounts;
782    if !s.faces.is_empty() {
783        let number = Number::new("extrude distance", amounts.distance, Unit::Length);
784        f.number("distance", number, |edit, d| {
785            edit.session.amounts.distance = d
786        });
787        let number = Number::new("inset thickness", amounts.thickness, Unit::Length);
788        f.number("thickness", number, |edit, d| {
789            edit.session.amounts.thickness = d
790        });
791    }
792    if !s.edges.is_empty() {
793        let sharpness = args.cage.sharpness(s.edges[0][0], s.edges[0][1]);
794        let number = Number::new("crease sharpness", sharpness, Unit::Fraction).range(0.0, 1.0);
795        f.number("sharpness", number, |edit, sharpness| {
796            let s = selected(edit.selection);
797            edit.args.cage.set_sharpness(&s.edges, sharpness);
798            edit.session.problem = None;
799        });
800        let number = Number::new("bevel width", amounts.width, Unit::Length);
801        f.number("width", number, |edit, w| edit.session.amounts.width = w);
802        let number = Number::new("loop cut at", amounts.cut, Unit::Fraction).range(0.0, 1.0);
803        f.number("cut", number, |edit, t| edit.session.amounts.cut = t);
804        let number = Number::new("slide by", amounts.slide, Unit::Fraction).range(-1.0, 1.0);
805        f.number("slide", number, |edit, t| edit.session.amounts.slide = t);
806    }
807    let action = |value: &str, label: &str, enabled: bool, why: &str, title: &str| {
808        let action = Action::new(value, label).group("Edit");
809        if enabled {
810            action.title(title)
811        } else {
812            action.disabled(why)
813        }
814    };
815    let on_boundary = (s.edges.iter()).any(|&[a, b]| args.cage.is_boundary(a, b));
816    let edits = vec![
817        action(
818            "extrude",
819            "Extrude",
820            !s.faces.is_empty(),
821            "Select faces to extrude",
822            "Extrude the selected faces along their normals (e)",
823        ),
824        action(
825            "inset",
826            "Inset",
827            !s.faces.is_empty(),
828            "Select faces to inset",
829            "Inset the selected faces: a ring of new faces inside their rim (i)",
830        ),
831        action(
832            "bridge",
833            "Bridge",
834            s.faces.len() == 2,
835            "Select two faces to bridge",
836            "Join the two selected faces by a tunnel between their rims",
837        ),
838        action(
839            "loop_cut",
840            "Loop cut",
841            !s.edges.is_empty(),
842            "Select an edge to cut a loop across",
843            "Cut an edge loop across each selected edge, where \"loop cut at\" says",
844        ),
845        action(
846            "slide",
847            "Slide",
848            !s.edges.is_empty(),
849            "Select the edges of a loop to slide",
850            "Slide the selected edges along the edges leaving them, by \"slide by\"",
851        ),
852        action(
853            "bevel",
854            "Bevel",
855            !s.edges.is_empty(),
856            "Select edges to bevel",
857            "Replace each selected edge by a strip \"bevel width\" wide",
858        ),
859        action(
860            "dissolve",
861            "Dissolve",
862            !s.edges.is_empty() || !s.vertices.is_empty(),
863            "Select edges or vertices to dissolve",
864            "Remove the selected edges and vertices, merging the faces around them",
865        ),
866        action(
867            "merge",
868            "Merge",
869            vertices.len() >= 2,
870            "Select at least two vertices to merge",
871            "Merge the vertices of the selection into one at their centre",
872        ),
873        action(
874            "fill",
875            "Fill",
876            on_boundary,
877            "Select an edge on an open rim to fill",
878            "Fill the hole the selected edge is on with one face",
879        ),
880        action(
881            "delete",
882            "Delete",
883            !selection.is_empty(),
884            "Select what to delete",
885            "Delete the selected faces, and the faces along the selected edges and vertices",
886        ),
887    ];
888    f.actions("edit", edits, move |mut edit, value| {
889        let s = selected(edit.selection);
890        let all = edit.args.cage.vertices_of(edit.selection);
891        let mirror = edit.args.mirror;
892        if let Some(run) = edit_action(value, &s, &all, edit.session.amounts, mirror) {
893            apply(&mut edit, run);
894        }
895    });
896    let mut visuals = cage_visuals(args);
897    if let Some((from, to)) = &session.window {
898        visuals.extend(window_visual(args, from, to));
899    }
900    f.form.visuals = visuals;
901    f.form.selection = selection.to_vec();
902    f.form.tool = if session.box_select {
903        InHand::Strokes
904    } else {
905        InHand::Nothing
906    };
907    if session.cage_only {
908        f.form.hidden = built_names(step, args);
909    }
910    f
911}
912
913/// A cage element dragged: what is selected, if it is, else it alone,
914/// moved as far as the pointer from where it was grabbed, snapped to
915/// [`DRAG_SNAP`] unless shift is held — to the cage as it was then, which
916/// the editor gives each event of the drag (see
917/// [`Visual::from_grab`]). The gizmo dragged: what is selected moved,
918/// turned or scaled as the drag did. A double click on an edge selects its
919/// loop, with shift its ring. With box select on (`b`), a drag selects
920/// everything within its window, shift adding to the selection, and a click
921/// selects as usual. `e` extrudes and `i` insets the selected faces.
922/// Escape clears the selection and puts box select down; Delete deletes
923/// what is selected.
924fn on_canvas(mut edit: Edit<'_, SubdArgs, SubdSession>, event: &CanvasEvent) {
925    match event {
926        CanvasEvent::Move {
927            key,
928            from,
929            to,
930            shift,
931            ..
932        } => {
933            let keys = if edit.selection.contains(key) {
934                edit.selection.clone()
935            } else {
936                vec![key.clone()]
937            };
938            let by = [0, 1, 2].map(|c| {
939                // Where the pointer is: a free choice, made by whoever
940                // moved it.
941                let d = to[c].sub(from[c]).to_f64();
942                if *shift {
943                    d
944                } else {
945                    (d / DRAG_SNAP).round() * DRAG_SNAP
946                }
947            });
948            let mirror = edit.args.mirror;
949            let vertices = edit.args.cage.vertices_of(&keys);
950            (edit.args.cage).transform(&vertices, mirror, |p| [0, 1, 2].map(|c| p[c] + by[c]));
951            edit.session.relative = None;
952        }
953        CanvasEvent::Gizmo { drag, .. } => {
954            let mirror = edit.args.mirror;
955            let vertices = edit.args.cage.vertices_of(edit.selection);
956            (edit.args.cage).transform(&vertices, mirror, |p| drag.apply_f64(p));
957            edit.session.relative = None;
958        }
959        CanvasEvent::Delete => {
960            let s = selected(edit.selection);
961            let all = edit.args.cage.vertices_of(edit.selection);
962            if let Some(run) =
963                edit_action("delete", &s, &all, edit.session.amounts, edit.args.mirror)
964            {
965                apply(&mut edit, run);
966            }
967        }
968        CanvasEvent::Click {
969            pointer,
970            double,
971            shift,
972        } => {
973            let visuals = cage_visuals(edit.args);
974            let hit = hit_visuals(&visuals, pointer, no_instances, |v| v.selectable)
975                .map(|hit| hit.visual.key.clone());
976            let Some(key) = hit else {
977                return;
978            };
979            if *double {
980                if let Some(Element::Edge(a, b)) = Element::parse(&key) {
981                    match chains(&edit.args.cage, &[[a, b]], *shift) {
982                        Ok(keys) => {
983                            extend(edit.selection, keys);
984                            edit.session.problem = None;
985                        }
986                        Err(e) => edit.session.problem = Some(e.root_message().to_string()),
987                    }
988                }
989            } else if edit.session.box_select {
990                // The editor leaves clicks to a tool in hand: select as it
991                // would.
992                match edit.selection.iter().position(|k| *k == key) {
993                    Some(i) => {
994                        edit.selection.remove(i);
995                    }
996                    None => edit.selection.push(key),
997                }
998            }
999        }
1000        CanvasEvent::Stroke {
1001            from,
1002            to,
1003            done,
1004            shift,
1005        } => {
1006            if !edit.session.box_select {
1007                return;
1008            }
1009            if !*done {
1010                edit.session.window = Some((*from, *to));
1011                return;
1012            }
1013            edit.session.window = None;
1014            edit.session.box_select = false;
1015            let Some(window) = Window::new(from, to) else {
1016                return;
1017            };
1018            let cage = &edit.args.cage;
1019            let within = |v: u32| cage.vertex(v).is_ok_and(|x| window.contains(&vector(x.at)));
1020            let mut keys: Vec<String> = Vec::new();
1021            keys.extend(
1022                (cage.vertices.iter())
1023                    .filter(|v| within(v.id))
1024                    .map(|v| vertex_key(v.id)),
1025            );
1026            keys.extend(
1027                (cage.edges().into_iter())
1028                    .filter(|&[a, b]| within(a) && within(b))
1029                    .map(|[a, b]| edge_key(a, b)),
1030            );
1031            keys.extend(
1032                (cage.faces.iter())
1033                    .filter(|f| f.vertices.iter().all(|&v| within(v)))
1034                    .map(|f| face_key(f.id)),
1035            );
1036            if !*shift {
1037                edit.selection.clear();
1038            }
1039            extend(edit.selection, keys);
1040            edit.session.relative = None;
1041        }
1042        CanvasEvent::Key { key } => {
1043            let value = match key.as_str() {
1044                "b" => {
1045                    edit.session.box_select = !edit.session.box_select;
1046                    edit.session.window = None;
1047                    return;
1048                }
1049                "e" => "extrude",
1050                "i" => "inset",
1051                _ => return,
1052            };
1053            let s = selected(edit.selection);
1054            let all = edit.args.cage.vertices_of(edit.selection);
1055            if let Some(run) = edit_action(value, &s, &all, edit.session.amounts, edit.args.mirror)
1056            {
1057                apply(&mut edit, run);
1058            }
1059        }
1060        CanvasEvent::Cancel => {
1061            edit.session.box_select = false;
1062            edit.session.window = None;
1063        }
1064        _ => {}
1065    }
1066}
1067
1068/// Builds the limit surface of `args`' cage into `model` as the step
1069/// `step` (see [`Subd`]), as a test builds it.
1070pub fn build_subd(model: &mut NamedModel, step: &str, args: &SubdArgs) -> GeopResult<()> {
1071    let ctx = with_context!("subd({step})");
1072    let namer = Namer::new("subd", step)?;
1073    let mesh = Mesh::new(&args.cage, args.mirror).with_context(ctx)?;
1074    let (spec, names) = limit_body(&mesh, &namer).with_context(ctx)?;
1075    model.build_body(spec, names).with_context(ctx)?;
1076    Ok(())
1077}
1078
1079part_operation!(Subd(SubdArgs));
1080
1081impl PartOperation for Subd {
1082    const KIND: &'static str = "subd";
1083    const INFO: OperationInfo = OperationInfo::new(
1084        "SubD",
1085        "Shape a freeform body by dragging the vertices, edges and faces of a control cage, built as its smooth subdivision surface.",
1086        OperationGroup::Surface,
1087        OperationTier::Big,
1088    )
1089    .icon(crate::icons::SUBD);
1090    type Args = SubdArgs;
1091    type Session = SubdSession;
1092
1093    /// A box cage, [`SIZE`] a side, around the origin: its dimensions
1094    /// set in the dialog.
1095    fn new_args(_: &Context) -> SubdArgs {
1096        SubdArgs {
1097            cage: Basic::Box {
1098                width: SIZE,
1099                depth: SIZE,
1100                height: SIZE,
1101            }
1102            .cage(),
1103            mirror: Mirror::None,
1104        }
1105    }
1106
1107    fn run(ctx: &mut StepContext, args: &SubdArgs) -> GeopResult<()> {
1108        let _step = ctx.step();
1109        build_model::<Self>(ctx, args)
1110    }
1111
1112    fn form<'a>(
1113        ctx: &'a Context<'a>,
1114        args: &SubdArgs,
1115        editing: &SubdEditing,
1116    ) -> EditForm<'a, SubdArgs, SubdSession> {
1117        form(ctx.step, args, &editing.session, &editing.selection)
1118    }
1119
1120    fn canvas(_: &Context, edit: Edit<'_, SubdArgs, SubdSession>, event: &CanvasEvent) {
1121        on_canvas(edit, event)
1122    }
1123}
1124
1125impl ModelOperation for Subd {
1126    fn build(model: &mut NamedModel, _: &Reader, step: &str, args: &SubdArgs) -> GeopResult<()> {
1127        build_subd(model, step, args)
1128    }
1129}