1use 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#[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 #[serde(default)]
62 pub mirror: Mirror,
63}
64
65pub type SubdEditing = FormState<SubdSession>;
67
68#[derive(Clone, Debug)]
72struct Relative {
73 selection: Vec<String>,
74 base: Cage,
75 scale: f64,
76 angles: [f64; 3],
78}
79
80#[derive(Clone, Copy, Debug, PartialEq)]
82pub struct Amounts {
83 pub distance: f64,
85 pub thickness: f64,
87 pub width: f64,
89 pub cut: f64,
91 pub slide: f64,
93}
94
95impl Default for Amounts {
96 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#[derive(Clone, Debug, Default)]
112pub struct SubdSession {
113 relative: Option<Relative>,
114 pub amounts: Amounts,
115 pub box_select: bool,
117 window: Option<(Pointer, Pointer)>,
119 pub cage_only: bool,
121 problem: Option<String>,
123}
124
125pub const SIZE: f64 = 100.0;
128
129const CYLINDER_SEGMENTS: usize = 8;
131
132const PLANE_QUADS: usize = 3;
134
135#[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 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 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 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 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 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 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
261fn 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
274fn prune(selection: &mut Vec<String>, cage: &Cage) {
276 selection.retain(|key| Element::parse(key).is_some_and(|e| cage.has(e)));
277}
278
279#[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
305fn 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
314fn 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 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
395fn 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(¢re);
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
411fn 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 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 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
454fn 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
469type CageEdit = Box<dyn FnOnce(&mut Cage) -> GeopResult<()>>;
471
472fn 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 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
543fn 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
559fn 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 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 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
913fn 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 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 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
1068pub 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 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}