Skip to main content

geop_ops_edit/
direct.rs

1//! Direct edits of a solid's faces, its topology kept: [`MoveFace`] shifts
2//! faces, [`OffsetFace`] pushes or pulls them along their normals, and
3//! [`ReplaceFace`] puts a face on the surface of another. Each puts the
4//! faces on their new surfaces and rebuilds the solid around them
5//! ([`geop_lib_shell::replace_faces`]): every vertex and edge they reach
6//! moves to where the surfaces now meet, and every name is kept.
7
8use geop_core_editor::{
9    edit_form::{Edit, EditForm, FormState, choices, chosen},
10    editable::{Context, OperationGroup, OperationInfo, OperationTier},
11    editing::{CanvasEvent, Change, Gizmo, Number, Unit},
12    part_operation,
13    part_operation::PartOperation,
14};
15use geop_core_geometry::{nurb_surface::NurbSurface3D, search::PROJECT_ITERATIONS};
16use geop_core_math::{
17    geop_error::{GeopError, GeopResult},
18    primitives::Motion,
19    scalars::Scalar,
20    vector::Vector3,
21};
22use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
23use geop_core_topology::FaceId;
24use geop_lib_shell::{Heal, Smooth, delete_faces, replace_faces};
25use geop_ops_model::{
26    Body, Face, ModelOperation, NamedModel, Namer, build_model, model, read_model, resolve_bodies,
27};
28use serde::{Deserialize, Serialize};
29
30/// A face of the model, by name.
31type FaceRef = ElementReference<NamedModel, Face>;
32
33/// The faces `faces` refers to, in `model`.
34fn face_ids(model: &NamedModel, faces: &[FaceRef]) -> GeopResult<Vec<FaceId>> {
35    faces.iter().map(|f| model.face_id(f.name())).collect()
36}
37
38/// Fails unless a value typed in is a number.
39fn finite(what: &str, value: f64) -> GeopResult<()> {
40    if value.is_finite() {
41        Ok(())
42    } else {
43        Err(GeopError::new(format!("the {what} is not a number")))
44    }
45}
46
47/// The middle of the corners of the faces `faces` in the part `reader`
48/// reads, where a handle sits: their average, a free choice.
49fn faces_middle(reader: &Reader, faces: &[FaceRef]) -> Option<Vector3> {
50    let part = read_model(reader).ok()?;
51    let model = part.topology();
52    let mut sum = Vector3::zero();
53    let mut count = 0usize;
54    for face in face_ids(&part, faces).ok()? {
55        for edge in model.face_edges(face) {
56            let start = model.get_edge(edge).ok()?.start_vertex;
57            sum = sum.add(&model.get_vertex(start).ok()?.point);
58            count += 1;
59        }
60    }
61    let share = Scalar::from_ratio(1, i64::try_from(count).ok()?).ok()?;
62    Some(sum.prod_scalar(share))
63}
64
65/// Shifts the faces `faces` of a solid by `translation`, along the world's
66/// `x`, `y` and `z`: each is put on its surface shifted, and the solid
67/// rebuilt around them — a face pulled out of a box makes it longer, a
68/// face of a hole shifted moves the hole. A face takes along the faces it
69/// runs smoothly into: the other pieces of a hole's wall, a fillet along
70/// it. The solid is consumed and the
71/// result named `move_face(M)` for the operation `M`; every face, edge and
72/// vertex keeps its name.
73#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
74pub struct MoveFace;
75
76#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
77pub struct MoveFaceArgs {
78    /// The faces to move, all of one solid.
79    pub faces: Vec<FaceRef>,
80    /// The shift, along the world's `x`, `y` and `z`.
81    #[serde(default)]
82    pub translation: [f64; 3],
83}
84
85/// Pushes or pulls the faces `faces` of a solid `distance` along their
86/// outward normals — out of the solid for a positive distance, into it for
87/// a negative one: each is put on its surface offset, and the solid
88/// rebuilt around them. A face takes along the faces it runs smoothly
89/// into, offset as far: the other pieces of a cylinder, a fillet. The
90/// solid is consumed and the result named
91/// `offset_face(O)` for the operation `O`; every face, edge and vertex
92/// keeps its name.
93#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
94pub struct OffsetFace;
95
96#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
97pub struct OffsetFaceArgs {
98    /// The faces to offset, all of one solid.
99    pub faces: Vec<FaceRef>,
100    /// How far, out of the solid; into it if negative.
101    pub distance: f64,
102}
103
104/// Puts the face `face` of a solid on the surface of the face `with` — of
105/// a sheet, or of another body — and rebuilds the solid around it: its
106/// neighbours trimmed or grown to where they meet the new surface. The
107/// solid is consumed and the result named `replace_face(R)` for the
108/// operation `R`; every face, edge and vertex keeps its name.
109#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
110pub struct ReplaceFace;
111
112#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
113pub struct ReplaceFaceArgs {
114    /// The face to put on another surface.
115    pub face: FaceRef,
116    /// The face whose surface it goes on.
117    pub with: FaceRef,
118}
119
120/// Takes the faces `faces` out of their solid, and leaves the gap as
121/// `heal` says: healed over by the faces around it — a hole, a boss or a
122/// pocket filled in, a chamfer or a fillet taken back to its sharp edge, a
123/// cut-off corner restored — or open, the rest of the solid a sheet. The
124/// solid is consumed; what is left is named `delete_face(D)` for the
125/// operation `D`, a sheet not at all, and every face, edge and vertex that
126/// stays keeps its name.
127#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
128pub struct DeleteFace;
129
130#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
131pub struct DeleteFaceArgs {
132    /// The faces to take out, all of one solid.
133    pub faces: Vec<FaceRef>,
134    /// What to leave where they were.
135    #[serde(default)]
136    pub heal: HealGap,
137    /// Take out the whole feature each face picked is part of — a hole, a
138    /// boss, a pocket: every face that fills one hole of the faces around
139    /// it — not only the faces picked.
140    #[serde(default)]
141    pub whole_feature: bool,
142}
143
144/// What [`DeleteFace`] leaves where the faces were.
145#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(rename_all = "snake_case")]
147pub enum HealGap {
148    /// The faces around the gap grown until they meet.
149    #[default]
150    Heal,
151    /// Nothing: the solid left open.
152    Open,
153}
154
155impl HealGap {
156    const ALL: [(HealGap, &'static str, &'static str); 2] = [
157        (HealGap::Heal, "heal", "Heal the gap"),
158        (HealGap::Open, "open", "Leave it open"),
159    ];
160}
161
162/// Takes out of the bodies `bodies` the topology they do not need: faces
163/// that lie on one surface — two blocks united flush leave two in one
164/// plane — made one, and vertices between two edges of the same two
165/// faces dropped, the edges made one (see
166/// [`geop_ops_model::NamedModel::merge_faces_on_one_surface`]). What is
167/// made one keeps the name of the oldest of what it is made of, and the
168/// others' names stand for it.
169#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
170pub struct MergeFaces;
171
172#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
173pub struct MergeFacesArgs {
174    /// The bodies to simplify: solids, or a face of each sheet.
175    pub bodies: Vec<ElementReference<NamedModel, Body>>,
176}
177
178part_operation!(
179    MergeFaces(MergeFacesArgs),
180    MoveFace(MoveFaceArgs),
181    OffsetFace(OffsetFaceArgs),
182    ReplaceFace(ReplaceFaceArgs),
183    DeleteFace(DeleteFaceArgs)
184);
185
186/// How the direct edits are offered: features that change a solid.
187const fn info(label: &'static str, doc: &'static str) -> OperationInfo {
188    OperationInfo::new(label, doc, OperationGroup::Features, OperationTier::Menu)
189}
190
191impl PartOperation for MoveFace {
192    const KIND: &'static str = "move_face";
193    const INFO: OperationInfo = info(
194        "Move face",
195        "Shift faces of a solid, its neighbours trimmed or grown to meet them.",
196    )
197    .icon(crate::icons::MOVE_FACE);
198    type Args = MoveFaceArgs;
199    type Session = ();
200
201    /// Nothing picked yet, not shifted.
202    fn new_args(_: &Context) -> MoveFaceArgs {
203        MoveFaceArgs {
204            faces: Vec::new(),
205            translation: [0.0; 3],
206        }
207    }
208
209    fn run(ctx: &mut StepContext, args: &MoveFaceArgs) -> GeopResult<()> {
210        let _step = ctx.step();
211        build_model::<Self>(ctx, args)
212    }
213
214    /// The faces, picked; the shift along each axis, dragged by a gizmo
215    /// where the faces' middle is moved to.
216    fn form<'a>(
217        context: &'a Context<'a>,
218        args: &MoveFaceArgs,
219        _: &FormState<()>,
220    ) -> EditForm<'a, MoveFaceArgs> {
221        let before = &context.before;
222        let mut f = EditForm::<MoveFaceArgs>::default();
223        let shift = Vector3::from_array(args.translation.map(Scalar::from_f64));
224        f.form.gizmo =
225            faces_middle(before, &args.faces).map(|c| Gizmo::new(c.add(&shift)).translate());
226        f.pick(
227            "faces",
228            "faces",
229            before,
230            &args.faces,
231            true,
232            |edit, faces| edit.args.faces = faces,
233        );
234        for (k, (key, label)) in [("x", "shift x"), ("y", "shift y"), ("z", "shift z")]
235            .into_iter()
236            .enumerate()
237        {
238            let number = Number::new(label, args.translation[k], Unit::Length);
239            f.number(key, number, move |edit, v| edit.args.translation[k] = v);
240        }
241        f
242    }
243
244    /// The gizmo dragged: the shift moved by as much.
245    fn canvas(_: &Context, edit: Edit<'_, MoveFaceArgs, ()>, event: &CanvasEvent) {
246        if let CanvasEvent::Gizmo { drag, .. } = event
247            && let Change::Translate(by) = &drag.change
248        {
249            for (k, shift) in edit.args.translation.iter_mut().enumerate() {
250                *shift += by[k].to_f64();
251            }
252        }
253    }
254}
255
256impl ModelOperation for MoveFace {
257    fn build(
258        model: &mut NamedModel,
259        _: &Reader,
260        step: &str,
261        args: &MoveFaceArgs,
262    ) -> GeopResult<()> {
263        let namer = Namer::new("move_face", step)?;
264        for v in args.translation {
265            finite("shift", v)?;
266        }
267        let motion =
268            Motion::translation(Vector3::from_array(args.translation.map(Scalar::from_f64)));
269        let faces = face_ids(model, &args.faces)?;
270        replace_faces(model, &namer, &faces, Smooth::Follow, |_, surface| {
271            Ok(surface.transform(&motion))
272        })?;
273        Ok(())
274    }
275}
276
277impl PartOperation for OffsetFace {
278    const KIND: &'static str = "offset_face";
279    const INFO: OperationInfo = info(
280        "Offset face",
281        "Push or pull faces of a solid along their normals, its neighbours trimmed or grown to meet them.",
282    )
283    .icon(crate::icons::OFFSET_FACE);
284    type Args = OffsetFaceArgs;
285    type Session = ();
286
287    /// Nothing picked yet, pulled out by one.
288    fn new_args(_: &Context) -> OffsetFaceArgs {
289        OffsetFaceArgs {
290            faces: Vec::new(),
291            distance: 1.0,
292        }
293    }
294
295    fn run(ctx: &mut StepContext, args: &OffsetFaceArgs) -> GeopResult<()> {
296        let _step = ctx.step();
297        build_model::<Self>(ctx, args)
298    }
299
300    /// The faces, picked, and the distance.
301    fn form<'a>(
302        context: &'a Context<'a>,
303        args: &OffsetFaceArgs,
304        _: &FormState<()>,
305    ) -> EditForm<'a, OffsetFaceArgs> {
306        let mut f = EditForm::<OffsetFaceArgs>::default();
307        f.pick(
308            "faces",
309            "faces",
310            &context.before,
311            &args.faces,
312            true,
313            |edit, faces| edit.args.faces = faces,
314        );
315        let distance = Number::new("distance", args.distance, Unit::Length);
316        f.number("distance", distance, |edit, v| edit.args.distance = v);
317        f
318    }
319}
320
321impl ModelOperation for OffsetFace {
322    fn build(
323        model: &mut NamedModel,
324        _: &Reader,
325        step: &str,
326        args: &OffsetFaceArgs,
327    ) -> GeopResult<()> {
328        let namer = Namer::new("offset_face", step)?;
329        finite("distance", args.distance)?;
330        if args.distance == 0.0 {
331            return Err(GeopError::new(
332                "an offset of no distance leaves the faces where they are: give a distance",
333            ));
334        }
335        let distance = Scalar::from_f64(args.distance);
336        let faces = face_ids(model, &args.faces)?;
337        replace_faces(model, &namer, &faces, Smooth::Follow, |name, surface| {
338            surface
339                .offset(distance)
340                .map_err(|e| e.with_context(format!("offsetting face {name}")))?
341                .ok_or_else(|| {
342                    GeopError::new(format!(
343                        "face {name} collapses offset by {distance:?}: every point of it passes its centre of curvature"
344                    ))
345                })
346        })?;
347        Ok(())
348    }
349}
350
351impl PartOperation for ReplaceFace {
352    const KIND: &'static str = "replace_face";
353    const INFO: OperationInfo = info(
354        "Replace face",
355        "Put a face of a solid on the surface of another face, its neighbours trimmed or grown to meet it.",
356    )
357    .icon(crate::icons::REPLACE_FACE);
358    type Args = ReplaceFaceArgs;
359    type Session = ();
360
361    /// Nothing picked yet: no face has the empty name.
362    fn new_args(_: &Context) -> ReplaceFaceArgs {
363        ReplaceFaceArgs {
364            face: ElementReference::new(model(), ""),
365            with: ElementReference::new(model(), ""),
366        }
367    }
368
369    fn run(ctx: &mut StepContext, args: &ReplaceFaceArgs) -> GeopResult<()> {
370        let _step = ctx.step();
371        build_model::<Self>(ctx, args)
372    }
373
374    /// The face, and the face whose surface it goes on, picked.
375    fn form<'a>(
376        context: &'a Context<'a>,
377        args: &ReplaceFaceArgs,
378        _: &FormState<()>,
379    ) -> EditForm<'a, ReplaceFaceArgs> {
380        let before = &context.before;
381        let mut f = EditForm::<ReplaceFaceArgs>::default();
382        f.pick_one("face", "face", before, Some(&args.face), |edit, face| {
383            edit.args.face = face
384        });
385        f.pick_one("with", "onto", before, Some(&args.with), |edit, with| {
386            edit.args.with = with
387        });
388        f
389    }
390}
391
392impl ModelOperation for ReplaceFace {
393    fn build(
394        model: &mut NamedModel,
395        _: &Reader,
396        step: &str,
397        args: &ReplaceFaceArgs,
398    ) -> GeopResult<()> {
399        let namer = Namer::new("replace_face", step)?;
400        let face = model.face_id(args.face.name())?;
401        let with = model.face_id(args.with.name())?;
402        if face == with {
403            return Err(GeopError::new(
404                "a face replaced by its own surface stays where it is: pick another face",
405            ));
406        }
407        let topology = model.topology();
408        let corners: Vec<Vector3> = (topology.face_edges(face))
409            .map(|e| {
410                Ok(topology
411                    .get_vertex(topology.get_edge(e)?.start_vertex)?
412                    .point)
413            })
414            .collect::<GeopResult<_>>()?;
415        let target = topology.get_face(with)?.surface.covering(&corners)?;
416        let anchor = face_point(model, face)?;
417        replace_faces(model, &namer, &[face], Smooth::Refuse, |name, old| {
418            facing_as(&target, old, anchor)
419                .map_err(|e| e.with_context(format!("putting face {name} on another surface")))
420        })?;
421        Ok(())
422    }
423}
424
425impl PartOperation for MergeFaces {
426    const KIND: &'static str = "merge_faces";
427    const INFO: OperationInfo = info(
428        "Merge faces",
429        "Make faces that lie on one surface one face, and drop the edges and vertices between them.",
430    )
431    .icon(crate::icons::MERGE_FACES);
432    type Args = MergeFacesArgs;
433    type Session = ();
434
435    /// The newest solid.
436    fn new_args(context: &Context) -> MergeFacesArgs {
437        MergeFacesArgs {
438            bodies: (read_model(&context.before).ok())
439                .and_then(|model| model.newest_solid())
440                .map(|name| ElementReference::new(model(), name))
441                .into_iter()
442                .collect(),
443        }
444    }
445
446    fn run(ctx: &mut StepContext, args: &MergeFacesArgs) -> GeopResult<()> {
447        let _step = ctx.step();
448        build_model::<Self>(ctx, args)
449    }
450
451    /// The bodies, picked.
452    fn form<'a>(
453        context: &'a Context<'a>,
454        args: &MergeFacesArgs,
455        _: &FormState<()>,
456    ) -> EditForm<'a, MergeFacesArgs> {
457        let mut f = EditForm::<MergeFacesArgs>::default();
458        f.pick(
459            "bodies",
460            "bodies",
461            &context.before,
462            &args.bodies,
463            true,
464            |edit, picked| edit.args.bodies = picked,
465        );
466        f
467    }
468}
469
470impl ModelOperation for MergeFaces {
471    fn build(
472        model: &mut NamedModel,
473        reader: &Reader,
474        _: &str,
475        args: &MergeFacesArgs,
476    ) -> GeopResult<()> {
477        for body in resolve_bodies(reader, &args.bodies)? {
478            model.merge_faces_on_one_surface(body, |_| true, |_| true)?;
479        }
480        Ok(())
481    }
482}
483
484impl PartOperation for DeleteFace {
485    const KIND: &'static str = "delete_face";
486    const INFO: OperationInfo = info(
487        "Delete face",
488        "Take faces out of a solid: holes, bosses, chamfers and fillets healed over by the faces around them, or the solid left open.",
489    )
490    .icon(crate::icons::DELETE_FACE);
491    type Args = DeleteFaceArgs;
492    type Session = ();
493
494    /// Nothing picked yet, healed over.
495    fn new_args(_: &Context) -> DeleteFaceArgs {
496        DeleteFaceArgs {
497            faces: Vec::new(),
498            heal: HealGap::Heal,
499            whole_feature: false,
500        }
501    }
502
503    fn run(ctx: &mut StepContext, args: &DeleteFaceArgs) -> GeopResult<()> {
504        let _step = ctx.step();
505        build_model::<Self>(ctx, args)
506    }
507
508    /// The faces, picked, and what to leave.
509    fn form<'a>(
510        context: &'a Context<'a>,
511        args: &DeleteFaceArgs,
512        _: &FormState<()>,
513    ) -> EditForm<'a, DeleteFaceArgs> {
514        let mut f = EditForm::<DeleteFaceArgs>::default();
515        f.pick(
516            "faces",
517            "faces",
518            &context.before,
519            &args.faces,
520            true,
521            |edit, faces| edit.args.faces = faces,
522        );
523        let (heal, heals) = choices(&HealGap::ALL, &args.heal);
524        f.select("heal", "leave", heal, heals, false, |edit, value| {
525            if let Some(heal) = chosen(&HealGap::ALL, value) {
526                edit.args.heal = heal;
527            }
528        });
529        f.checkbox(
530            "whole_feature",
531            "whole feature",
532            args.whole_feature,
533            |edit, b| edit.args.whole_feature = b,
534        );
535        f
536    }
537}
538
539impl ModelOperation for DeleteFace {
540    fn build(
541        model: &mut NamedModel,
542        _: &Reader,
543        step: &str,
544        args: &DeleteFaceArgs,
545    ) -> GeopResult<()> {
546        let namer = Namer::new("delete_face", step)?;
547        let faces = face_ids(model, &args.faces)?;
548        let heal = match args.heal {
549            HealGap::Heal => Heal::Extend,
550            HealGap::Open => Heal::Open,
551        };
552        delete_faces(model, &namer, &faces, args.whole_feature, heal)?;
553        Ok(())
554    }
555}
556
557/// A point of the face `face` of `part` and where it is on the face's
558/// surface: where its first coedge starts.
559fn face_point(part: &NamedModel, face: FaceId) -> GeopResult<(Vector3, (Scalar, Scalar))> {
560    let model = part.topology();
561    let Some(coedge) = model.iterate_face_coedges(face).next() else {
562        return Err(GeopError::new(format!(
563            "face {} has no boundary",
564            part.name(face)?
565        )));
566    };
567    let uv = model.get_coedge(coedge)?.pcurve.ends()?.0;
568    let surface = &model.get_face(face)?.surface;
569    Ok((surface.evaluate(uv[0], uv[1])?, (uv[0], uv[1])))
570}
571
572/// `surface`, turned around if need be so that its normal points the way
573/// `old`'s does at `anchor` — a point of the face on `old`, and where it is
574/// on it — where `surface` comes nearest it. Refused where which way it
575/// points cannot be told: the two square to each other there.
576fn facing_as(
577    surface: &NurbSurface3D,
578    old: &NurbSurface3D,
579    (point, (u, v)): (Vector3, (Scalar, Scalar)),
580) -> GeopResult<NurbSurface3D> {
581    let normal = old.normal(u, v)?;
582    let ((u0, u1), (v0, v1)) = (surface.domain_u(), surface.domain_v());
583    let half = Scalar::from_f64(0.5);
584    let (su, sv) = surface.project_in_domain(
585        point,
586        Scalar::interpolate(u0, u1, half).sharpen(),
587        Scalar::interpolate(v0, v1, half).sharpen(),
588        PROJECT_ITERATIONS,
589    )?;
590    let along = surface
591        .normal(su.sharpen(), sv.sharpen())?
592        .prod_dot(&normal);
593    if along.definitely_greater(Scalar::ZERO) {
594        Ok(surface.clone())
595    } else if along.definitely_less(Scalar::ZERO) {
596        Ok(surface.reverse_u())
597    } else {
598        Err(GeopError::new(format!(
599            "the new surface is square to the face at {point:?}: which way it faces cannot be told"
600        )))
601    }
602}