Skip to main content

geop_ops_surface/
trim.rs

1//! [`TrimSurface`]: a sheet cut back to one side of a face.
2//!
3//! The face cuts the sheet as it cuts a solid in a split: a copy of it is
4//! imprinted onto the sheet (see [`remesh`]), which divides the sheet's
5//! faces along where the two cross. Every piece of the sheet then lies on
6//! one side of the face's surface — in front of it, where its normal
7//! points, or behind it — and the pieces on the side asked for are kept.
8//! The copy is deleted again.
9//!
10//! A piece that reaches both sides means the face does not cut the sheet
11//! clean through there: refused, naming the piece.
12
13use geop_core_editor::{
14    edit_form::{EditForm, FormState},
15    editable::{Context, OperationGroup, OperationInfo, OperationTier},
16    editing::Choice,
17    part_operation,
18    part_operation::PartOperation,
19};
20use geop_core_geometry::{
21    nurb_surface::NurbSurface3D,
22    search::{MAX_NODES, PROJECT_ITERATIONS, min_subdivision_size},
23};
24use geop_core_math::{
25    geop_error::{GeopError, GeopResult},
26    scalars::Scalar,
27    vector::Vector3,
28};
29use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
30use geop_core_topology::{Body, FaceId, contains::face::face_interior_point};
31use geop_lib_booleans::{RemeshParams, remesh};
32use geop_ops_model::{Face, ModelOperation, NamedModel, Namer, build_model, model};
33use serde::{Deserialize, Serialize};
34
35/// Which side of the cutting face a [`TrimSurface`] keeps, as its normal
36/// points.
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum TrimKeep {
40    /// Where the face's normal points.
41    #[default]
42    Front,
43    /// Behind it.
44    Back,
45}
46
47/// Cuts the sheet the face named `face` stands in back to the side `keep`
48/// of the face named `tool` — of a solid or standing on its own, left as it
49/// is — for the operation `T` (see the module docs). What survives of the
50/// sheet keeps its names; what the cut creates is named as a split names
51/// it, `trim(T,...)`.
52#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
53pub struct TrimSurface;
54
55#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
56pub struct TrimSurfaceArgs {
57    /// A face of the sheet to trim.
58    pub face: ElementReference<NamedModel, Face>,
59    /// The face to cut it with.
60    pub tool: ElementReference<NamedModel, Face>,
61    /// The side of the tool to keep.
62    #[serde(default)]
63    pub keep: TrimKeep,
64}
65
66impl TrimKeep {
67    fn key(self) -> &'static str {
68        match self {
69            TrimKeep::Front => "front",
70            TrimKeep::Back => "back",
71        }
72    }
73}
74
75part_operation!(TrimSurface(TrimSurfaceArgs));
76
77impl PartOperation for TrimSurface {
78    const KIND: &'static str = "trim_surface";
79    const INFO: OperationInfo = OperationInfo::new(
80        "Trim surface",
81        "Cut a face standing on its own back to one side of another face.",
82        OperationGroup::Surface,
83        OperationTier::Menu,
84    )
85    .experimental()
86    .icon(crate::icons::TRIM_SURFACE);
87    type Args = TrimSurfaceArgs;
88    type Session = ();
89
90    /// Nothing picked yet, keeping the front.
91    fn new_args(_: &Context) -> TrimSurfaceArgs {
92        TrimSurfaceArgs {
93            face: ElementReference::new(model(), ""),
94            tool: ElementReference::new(model(), ""),
95            keep: TrimKeep::Front,
96        }
97    }
98
99    fn run(ctx: &mut StepContext, args: &TrimSurfaceArgs) -> GeopResult<()> {
100        let _step = ctx.step();
101        build_model::<Self>(ctx, args)
102    }
103
104    /// The sheet and the tool, picked, and the side to keep.
105    fn form<'a>(
106        context: &'a Context<'a>,
107        args: &TrimSurfaceArgs,
108        _: &FormState<()>,
109    ) -> EditForm<'a, TrimSurfaceArgs> {
110        let before = &context.before;
111        let mut f = EditForm::<TrimSurfaceArgs>::default();
112        f.pick_one("face", "sheet", before, Some(&args.face), |edit, face| {
113            edit.args.face = face
114        });
115        f.pick_one(
116            "tool",
117            "cut with",
118            before,
119            Some(&args.tool),
120            |edit, tool| edit.args.tool = tool,
121        );
122        let sides = vec![
123            Choice::new("front", "in front of the face"),
124            Choice::new("back", "behind the face"),
125        ];
126        f.select(
127            "keep",
128            "keep",
129            args.keep.key(),
130            sides,
131            false,
132            |edit, choice| {
133                edit.args.keep = if choice == "back" {
134                    TrimKeep::Back
135                } else {
136                    TrimKeep::Front
137                }
138            },
139        );
140        f
141    }
142}
143
144impl ModelOperation for TrimSurface {
145    fn build(
146        model: &mut NamedModel,
147        _: &Reader,
148        step: &str,
149        args: &TrimSurfaceArgs,
150    ) -> GeopResult<()> {
151        let namer = Namer::new("trim", step)?;
152        let tool = model.face_id(args.tool.name())?;
153        trim(model, &namer, args.face.name(), tool, args.keep)
154    }
155}
156
157/// On which side of `surface` the point `p` lies: `Some(true)` in front,
158/// where its normal points, `None` if it could be on it — measured from
159/// the foot of `p` on it, refined by Newton from the point of the surface
160/// nearest it.
161fn in_front(surface: &NurbSurface3D, p: &Vector3) -> GeopResult<Option<bool>> {
162    let distance = if let Some(plane) = surface.as_plane()? {
163        plane.signed_distance(p)
164    } else {
165        let (u, v, _) = surface.closest_point(p)?;
166        let (u, v) = surface.project(*p, u, v, PROJECT_ITERATIONS)?;
167        let foot = surface.evaluate(u, v)?;
168        p.sub(&foot).prod_dot(&surface.normal(u, v)?)
169    };
170    Ok(if distance.definitely_greater(Scalar::ZERO) {
171        Some(true)
172    } else if distance.definitely_less(Scalar::ZERO) {
173        Some(false)
174    } else {
175        None
176    })
177}
178
179/// Cuts the sheet of the face named `face` back to the side `keep` of the
180/// face `tool` (see the module docs).
181pub fn trim(
182    part: &mut NamedModel,
183    namer: &Namer,
184    face: &str,
185    tool: FaceId,
186    keep: TrimKeep,
187) -> GeopResult<()> {
188    let sheet = part.sheet_of_face(part.face_id(face)?, "a sheet is trimmed")?;
189    let tool_name = part.name(tool).map(str::to_string)?;
190    if part
191        .topology()
192        .body_faces(Body::Sheet(sheet))?
193        .contains(&tool)
194    {
195        return Err(GeopError::new(format!(
196            "face {tool_name} is a face of the sheet to trim: it cannot cut its own sheet"
197        )));
198    }
199    let surface = part.topology().get_face(tool)?.surface.clone();
200    let cutter = part
201        .copy_faces(&[tool], None, |name| namer.name(&[name, "tool"]))?
202        .body()?;
203    remesh(
204        part,
205        namer,
206        Body::Sheet(sheet),
207        cutter,
208        RemeshParams::default(),
209    )?;
210
211    let model = part.topology();
212    let want = keep == TrimKeep::Front;
213    let mut kept = Vec::new();
214    let mut cut_away = 0;
215    for f in model.body_faces(Body::Sheet(sheet))? {
216        let (u, v) = face_interior_point(model, f, MAX_NODES, min_subdivision_size(), 0x7121)?;
217        let at = model.get_face(f)?.surface.evaluate(u, v)?;
218        let Some(side) = in_front(&surface, &at)? else {
219            return Err(GeopError::new(format!(
220                "face {} of the sheet lies along face {tool_name}: which side of it it is on is undecided",
221                part.name(f).map(str::to_string)?
222            )));
223        };
224        for c in model.iterate_face_coedges(f) {
225            let vertex = model.coedge_start_vertex(c)?;
226            if in_front(&surface, &vertex.point)? == Some(!side) {
227                return Err(GeopError::new(format!(
228                    "face {} of the sheet reaches both sides of face {tool_name}: the face does not cut the sheet clean through there",
229                    part.name(f).map(str::to_string)?
230                )));
231            }
232        }
233        if side == want {
234            kept.push(f);
235        } else {
236            cut_away += 1;
237        }
238    }
239    if kept.is_empty() || cut_away == 0 {
240        return Err(GeopError::new(format!(
241            "face {tool_name} leaves {} of the sheet on the side to keep: it does not cut the sheet",
242            if kept.is_empty() { "nothing" } else { "all" }
243        )));
244    }
245    part.delete_bodies(&[cutter])?;
246    part.assemble_sheet(&[Body::Sheet(sheet)], &kept)?;
247    Ok(())
248}
249
250#[cfg(test)]
251mod tests;