1use geop_core_editor::{
5 edit_form::{EditForm, FormState, choices, chosen},
6 editable::{Context, OperationGroup, OperationInfo, OperationTier},
7 part_operation,
8 part_operation::PartOperation,
9};
10use geop_core_geometry::{
11 nurb_curve::NurbCurve3D, nurb_surface::NurbSurface3D, search::PROJECT_ITERATIONS,
12};
13use geop_core_math::{
14 geop_error::{GeopError, GeopResult, WithContext},
15 plain,
16 primitives::CoordinateSystem,
17 scalars::Scalar,
18 vector::{Vector3, Vector4},
19 with_context,
20};
21use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
22use geop_core_topology::{
23 Body, FaceId, Sense, ShellId,
24 build::{BodySpec, CoedgeOn, EdgeSpec, FaceSpec},
25};
26use geop_lib_booleans::{RemeshParams, imprint_on_face};
27use geop_lib_sweep::{SweepLoop, extrude};
28use geop_ops_model::sketch::{PlacedSketch, SketchRef, sketch};
29use geop_ops_model::{
30 BodyNames, Chain, Face, ModelOperation, NamedModel, Namer, build_model, face_hull, spread_along,
31};
32use serde::{Deserialize, Serialize};
33
34#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
61pub struct SplitFace;
62
63#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
64pub struct SplitFaceArgs {
65 pub face: Option<ElementReference<NamedModel, Face>>,
67 pub sketch: SketchRef,
69 #[serde(default, skip_serializing_if = "Along::is_sketch_normal")]
71 pub along: Along,
72}
73
74#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum Along {
78 #[default]
80 SketchNormal,
81 FaceNormal,
84}
85
86impl Along {
87 fn is_sketch_normal(&self) -> bool {
88 *self == Along::SketchNormal
89 }
90
91 const OPTIONS: [(Along, &'static str, &'static str); 2] = [
93 (Along::SketchNormal, "sketch_normal", "the sketch's normal"),
94 (Along::FaceNormal, "face_normal", "the face's normal"),
95 ];
96}
97
98part_operation!(SplitFace(SplitFaceArgs));
99
100impl PartOperation for SplitFace {
101 const KIND: &'static str = "split_face";
102 const INFO: OperationInfo = OperationInfo::new(
103 "Split face",
104 "Divide a face along a sketch's curves, projected onto it, leaving the solid's shape as it is.",
105 OperationGroup::Bodies,
106 OperationTier::Menu,
107 )
108 .icon(crate::icons::SPLIT_FACE);
109 type Args = SplitFaceArgs;
110 type Session = ();
111
112 fn new_args(context: &Context) -> SplitFaceArgs {
114 let newest = context.before.targets_of::<PlacedSketch>().pop();
115 SplitFaceArgs {
116 face: None,
117 sketch: SketchRef::whole(newest.unwrap_or_else(|| sketch(""))),
118 along: Along::SketchNormal,
119 }
120 }
121
122 fn run(ctx: &mut StepContext, args: &SplitFaceArgs) -> GeopResult<()> {
123 let _step = ctx.step();
124 build_model::<Self>(ctx, args)
125 }
126
127 fn form<'a>(
129 context: &'a Context<'a>,
130 args: &SplitFaceArgs,
131 _: &FormState<()>,
132 ) -> EditForm<'a, SplitFaceArgs> {
133 let before = &context.before;
134 let mut f = EditForm::<SplitFaceArgs>::default();
135 f.pick_one("face", "face", before, args.face.as_ref(), |edit, face| {
136 edit.args.face = Some(face)
137 })
138 .pick_one(
139 "sketch",
140 "sketch",
141 before,
142 Some(&args.sketch),
143 |edit, sketch| edit.args.sketch = sketch,
144 );
145 let (way, options) = choices(&Along::OPTIONS, &args.along);
146 f.select("along", "along", way, options, false, |edit, way| {
147 if let Some(along) = chosen(&Along::OPTIONS, way) {
148 edit.args.along = along;
149 }
150 });
151 f
152 }
153}
154
155impl ModelOperation for SplitFace {
156 fn build(
157 model: &mut NamedModel,
158 reader: &Reader,
159 step: &str,
160 args: &SplitFaceArgs,
161 ) -> GeopResult<()> {
162 let namer = Namer::new("split_face", step)?;
163 let Some(face_ref) = &args.face else {
164 return Err(GeopError::new("pick the face to divide").concerning("face"));
165 };
166 let placed = reader.retrieve_element(&args.sketch).concerning("sketch")?;
167 let face = reader.retrieve_element(face_ref).concerning("face")?.id;
168 let runs = placed.runs().concerning("sketch")?;
169 if runs.is_empty() {
170 return Err(GeopError::new(format!(
171 "the sketch {:?} has no curve to divide the face {:?} along: construction curves do not divide",
172 placed.name,
173 face_ref.name()
174 ))
175 .concerning("sketch"));
176 }
177 let plane = &placed.plane;
178 let shells = match args.along {
179 Along::SketchNormal => {
180 let (from, to) = reach_across(model, face, plane)?;
181 let heights = [Scalar::from_f64(from), Scalar::from_f64(to)];
182 let mut shells = Vec::with_capacity(runs.len());
183 for run in runs {
184 let lp = [SweepLoop::plain(run)];
185 shells.extend(extrude(model, &namer, None, plane, &heights, &lp)?.shells);
186 }
187 shells
188 }
189 Along::FaceNormal => {
190 let mut chains = Vec::with_capacity(runs.len());
191 for run in runs {
192 let curves = (run.curves.iter())
193 .map(|c| c.embed(plane.origin(), plane.u(), plane.v()))
194 .collect::<GeopResult<_>>()?;
195 chains.push(Chain {
196 name: run.name,
197 curves,
198 curve_names: run.curve_names,
199 joint_names: run.joint_names,
200 });
201 }
202 normal_sheets(model, &namer, face, &chains)?
203 }
204 };
205 let tools: Vec<Body> = shells.into_iter().map(Body::Sheet).collect();
206 imprint_on_face(model, &namer, face, &tools, RemeshParams::default()).with_context(
207 with_context!(
208 "dividing the face {:?} along the sketch {:?}",
209 face_ref.name(),
210 placed.name
211 ),
212 )?;
213 Ok(())
214 }
215}
216
217const FEET_PER_KNOT_SPAN: usize = 8;
220
221const RAIL_DRIFT: f64 = 1e-7;
226
227const MOST_DOUBLINGS: usize = 5;
230
231const MOST_HALVINGS: usize = 10;
235
236pub(crate) fn normal_sheets(
266 model: &mut NamedModel,
267 namer: &Namer,
268 face: FaceId,
269 chains: &[Chain<NurbCurve3D>],
270) -> GeopResult<Vec<ShellId>> {
271 let surface = model.topology().get_face(face)?.surface.clone();
272 let hull = face_hull(model.topology(), &[face])?;
273 let (lo, hi) = plain::bounds(hull.iter().map(|p| p.to_f64_array()))
274 .ok_or_else(|| GeopError::new(format!("face {face:?} has no control points")))?;
275 let extent = plain::diagonal(lo, hi);
276 let reach = Scalar::from_f64(extent / 16.0);
277 let drift = extent * RAIL_DRIFT;
278 let ends = |p: &Vector3| -> GeopResult<((Scalar, Scalar), [Vector3; 2])> {
281 let (u, v, foot) = surface.closest_point(p)?;
282 let normal = surface.normal(u, v)?.prod_scalar(reach);
283 Ok(((u, v), [foot.add(&normal), foot.sub(&normal)]))
284 };
285 let mut shells = Vec::with_capacity(chains.len());
286 for chain in chains {
287 let n = chain.curves.len();
288 let closed = chain.is_closed();
289 let joints = chain.joints()?;
290 let mut spec = BodySpec::new(false);
291 let mut names = BodyNames::default();
292 let mut joint_feet = Vec::with_capacity(joints.len());
293 for (p, name) in joints.iter().zip(&chain.joint_names) {
294 let (at, [near, far]) = ends(p)?;
295 joint_feet.push(at);
296 spec.vertices.extend([near, far]);
297 names
298 .vertices
299 .extend(["near", "far"].map(|end| namer.name(&[name, end])));
300 spec.edges.push(EdgeSpec {
301 curve: NurbCurve3D::line(near, far)?,
302 start: spec.vertices.len() - 2,
303 end: spec.vertices.len() - 1,
304 });
305 names.edges.push(namer.name(&[name]));
306 }
307 let joint = |k: usize| if closed { k % n } else { k };
308 for (k, (curve, name)) in chain.curves.iter().zip(&chain.curve_names).enumerate() {
309 let (a, b) = (joint(k), joint(k + 1));
310 let (t0, t1) = curve.domain();
311 let mut count = FEET_PER_KNOT_SPAN * curve.breakpoints().len().saturating_sub(1).max(1);
312 let mut doublings = 0;
313 let (near, far) = loop {
314 let mut rails: [Vec<Vector4>; 2] = Default::default();
315 let mut params = Vec::with_capacity(count + 1);
316 let mut feet = Vec::with_capacity(count + 1);
317 for i in 0..=count {
318 let f = Scalar::from_f64(i as f64 / count as f64);
320 let (foot, at) = if i == 0 {
321 (
322 joint_feet[a],
323 [spec.vertices[2 * a], spec.vertices[2 * a + 1]],
324 )
325 } else if i == count {
326 (
327 joint_feet[b],
328 [spec.vertices[2 * b], spec.vertices[2 * b + 1]],
329 )
330 } else {
331 ends(&curve.evaluate(Scalar::interpolate(t0, t1, f).sharpen())?)?
332 };
333 for (rail, p) in rails.iter_mut().zip(at) {
334 rail.push(p.homogeneous(Scalar::ONE));
335 }
336 params.push(f);
337 feet.push(foot);
338 }
339 let [near, far] =
340 rails.map(|rail| NurbCurve3D::interpolate_homogeneous(&rail, ¶ms, 3));
341 let (mut near, mut far) = (near?, far?);
342 let mut seen = 0.0f64;
345 for (pair, &(u, v)) in params.windows(2).zip(&feet) {
346 let f = pair[0].add(pair[1]).div(Scalar::TWO)?.sharpen();
347 let p = curve.evaluate(Scalar::interpolate(t0, t1, f))?;
348 let (u, v) = surface.project(p, u, v, PROJECT_ITERATIONS)?;
349 let (foot, normal) = (surface.evaluate(u, v)?, surface.normal(u, v)?);
350 let normal = normal.prod_scalar(reach);
351 for (rail, end) in [(&near, foot.add(&normal)), (&far, foot.sub(&normal))] {
352 seen = seen.max(rail.evaluate(f)?.sub(&end).norm().to_f64());
353 }
354 }
355 if seen > drift / 4.0 && doublings < MOST_DOUBLINGS {
356 count *= 2;
357 doublings += 1;
358 continue;
359 }
360 let miss = rail_miss(&surface, curve, [&near, &far], ¶ms, &feet, reach, drift)
361 .map_err(|e| {
362 e.with_context(format!("projecting curve {name} along the face's normal"))
363 })?;
364 let pad = [Scalar::from_f64(miss); 3];
365 near.widen(&pad);
366 far.widen(&pad);
367 break (near, far);
368 };
369 let surface = NurbSurface3D::ruled(&near, &far)?;
370 for (rail, (from, to), end) in [
371 (near, (2 * a, 2 * b), "near"),
372 (far, (2 * a + 1, 2 * b + 1), "far"),
373 ] {
374 spec.edges.push(EdgeSpec {
375 curve: rail,
376 start: from,
377 end: to,
378 });
379 names.edges.push(namer.name(&[name, end]));
380 }
381 let rail = spec.edges.len() - 2;
382 spec.faces.push(FaceSpec::on_unit_square(
383 surface,
384 [
385 CoedgeOn::Edge(rail, Sense::Forward),
386 CoedgeOn::Edge(b, Sense::Forward),
387 CoedgeOn::Edge(rail + 1, Sense::Reversed),
388 CoedgeOn::Edge(a, Sense::Reversed),
389 ],
390 )?);
391 names.faces.push(namer.name(&[name]));
392 }
393 spec.shells.push((0..spec.faces.len()).collect());
394 shells.extend(model.build_body(spec, names)?.shells);
395 }
396 Ok(shells)
397}
398
399fn rail_miss(
414 surface: &NurbSurface3D,
415 curve: &NurbCurve3D,
416 rails: [&NurbCurve3D; 2],
417 params: &[Scalar],
418 feet: &[(Scalar, Scalar)],
419 reach: Scalar,
420 drift: f64,
421) -> GeopResult<f64> {
422 let (t0, t1) = curve.domain();
423 let span = t1.sub(t0);
424 let mut worst = 0.0f64;
425 for (pair, &seed) in params.windows(2).zip(feet) {
426 let mut seed = seed;
427 let mut stack = vec![(pair[0], pair[1], 0usize)];
428 while let Some((lo, hi, depth)) = stack.pop() {
429 let stretch = lo.union(hi);
430 let middle = lo.add(hi).div(Scalar::TWO)?.sharpen();
431 let at = |f: Scalar| curve.evaluate(Scalar::interpolate(t0, t1, f));
432 let moving = curve
433 .tangent(Scalar::interpolate(t0, t1, stretch))?
434 .prod_scalar(span);
435 let over = surface.foot_moving(&at(stretch)?, &moving, seed)?;
436 let here = surface.foot_moving(&at(middle)?, &Vector3::zero(), seed)?;
437 let (Some(over), Some(here)) = (&over, &here) else {
438 if depth < MOST_HALVINGS {
439 stack.push((middle, hi, depth + 1));
440 stack.push((lo, middle, depth + 1));
441 continue;
442 }
443 return Err(GeopError::new(format!(
444 "no foot point on the face proven for the curve's points at {stretch:?} of its domain: projected along the face's normal, a curve has to stay over the face's surface (the points {:?}, from a foot at {seed:?}, proven {} over them and {} at their middle)",
445 at(stretch)?,
446 if over.is_some() { "" } else { "none" },
447 if here.is_some() { "" } else { "none" },
448 )));
449 };
450 seed = (here.at.0.midpoint(), here.at.1.midpoint());
451 let mut miss = 0.0f64;
452 for (rail, side) in rails.iter().zip([Scalar::ONE, Scalar::ONE.neg()]) {
453 let end = here.point.add(&here.normal.prod_scalar(reach.mul(side)));
454 let ends_move = over
455 .velocity
456 .add(&over.turning.prod_scalar(reach.mul(side)));
457 let off = rail.evaluate(middle)?.sub(&end);
458 let turn = rail.tangent(stretch)?.sub(&ends_move);
459 let e = off.add(&turn.prod_scalar(stretch.sub(middle)));
460 for k in 0..3 {
461 miss = miss.max(e[k].abs().upper().to_f64());
462 }
463 }
464 if miss > drift / 2.0 && depth < MOST_HALVINGS {
465 stack.push((middle, hi, depth + 1));
466 stack.push((lo, middle, depth + 1));
467 continue;
468 }
469 worst = worst.max(miss);
470 }
471 }
472 Ok(worst)
473}
474
475fn reach_across(
480 model: &NamedModel,
481 face: FaceId,
482 plane: &CoordinateSystem,
483) -> GeopResult<(f64, f64)> {
484 let hull = face_hull(model.topology(), &[face])?;
485 let (near, far) = spread_along(&hull, plane);
486 let (lo, hi) = plain::bounds(hull.iter().map(|p| p.to_f64_array()))
487 .ok_or_else(|| GeopError::new(format!("face {face:?} has no control points")))?;
488 let diagonal = plain::diagonal(lo, hi);
489 Ok((near - diagonal, far + diagonal))
490}