1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum TrimKeep {
40 #[default]
42 Front,
43 Back,
45}
46
47#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
53pub struct TrimSurface;
54
55#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
56pub struct TrimSurfaceArgs {
57 pub face: ElementReference<NamedModel, Face>,
59 pub tool: ElementReference<NamedModel, Face>,
61 #[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 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 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
157fn 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
179pub 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;