1use geop_core_editor::{
37 edit_form::{EditForm, FormState},
38 editable::{Context, OperationGroup, OperationInfo, OperationTier},
39 part_operation,
40 part_operation::PartOperation,
41};
42use geop_core_geometry::{
43 nurb_curve::{NurbCurve, NurbCurve2D, NurbCurve3D, flatten_point, loop_signed_area},
44 nurb_surface::{FlatPatch, Neighbour, NurbSurface3D},
45 shape::Plane,
46};
47use geop_core_math::{
48 geop_error::{GeopError, GeopResult},
49 scalars::Scalar,
50 vector::{Vector3, Vector4},
51};
52use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
53use geop_core_topology::{
54 Curve3, Sense,
55 build::{BodySpec, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec},
56};
57use geop_ops_model::sketch3d::CurveSource;
58use geop_ops_model::{
59 BodyNames, Chain, Edge, ModelOperation, NamedModel, Namer, build_model, read_model,
60};
61use serde::{Deserialize, Serialize};
62
63use crate::{NamedCurve, SheetSpec};
64
65#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
81pub struct BoundarySurface;
82
83#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
84pub struct BoundarySurfaceArgs {
85 pub edges: Vec<CurveSource>,
87 #[serde(default, skip_serializing_if = "Vec::is_empty")]
90 pub tangent: Vec<ElementReference<NamedModel, Edge>>,
91}
92
93part_operation!(BoundarySurface(BoundarySurfaceArgs));
94
95impl PartOperation for BoundarySurface {
96 const KIND: &'static str = "boundary_surface";
97 const INFO: OperationInfo = OperationInfo::new(
98 "Boundary surface",
99 "Span a face standing on its own between two edges, or fill a closed loop of edges — optionally tangent to the faces along them.",
100 OperationGroup::Surface,
101 OperationTier::Small,
102 )
103 .experimental()
104 .icon(crate::icons::BOUNDARY_SURFACE);
105 type Args = BoundarySurfaceArgs;
106 type Session = ();
107
108 fn new_args(_: &Context) -> BoundarySurfaceArgs {
110 BoundarySurfaceArgs::default()
111 }
112
113 fn run(ctx: &mut StepContext, args: &BoundarySurfaceArgs) -> GeopResult<()> {
114 let _step = ctx.step();
115 build_model::<Self>(ctx, args)
116 }
117
118 fn form<'a>(
120 context: &'a Context<'a>,
121 args: &BoundarySurfaceArgs,
122 _: &FormState<()>,
123 ) -> EditForm<'a, BoundarySurfaceArgs> {
124 let before = &context.before;
125 let mut f = EditForm::<BoundarySurfaceArgs>::default();
126 f.pick(
127 "edges",
128 "edges",
129 before,
130 &args.edges,
131 true,
132 |edit, picked| edit.args.edges = picked,
133 );
134 f.pick(
135 "tangent",
136 "tangent along",
137 before,
138 &args.tangent,
139 true,
140 |edit, picked| edit.args.tangent = picked,
141 );
142 f.form.dialog.optional("tangent");
143 f
144 }
145}
146
147impl ModelOperation for BoundarySurface {
148 fn build(
149 model: &mut NamedModel,
150 reader: &Reader,
151 step: &str,
152 args: &BoundarySurfaceArgs,
153 ) -> GeopResult<()> {
154 let namer = Namer::new("boundary", step)?;
155 let edges = (args.edges.iter())
156 .map(|e| e.resolve(reader))
157 .collect::<GeopResult<Vec<_>>>()?;
158 let tangent: Vec<&str> = args.tangent.iter().map(|t| t.name()).collect();
159 let (spec, names) = boundary_surface(&*read_model(reader)?, &namer, &edges, &tangent)?;
160 model.build_body(spec, names)?;
161 Ok(())
162 }
163}
164
165#[derive(Clone, Debug)]
169struct Side {
170 entity: String,
172 name: String,
173 curve: Curve3,
175 forward: bool,
176 start: Vector3,
177 end: Vector3,
178 start_name: String,
179 end_name: String,
180}
181
182impl Side {
183 fn of(chain: &Chain<NurbCurve3D>) -> GeopResult<Self> {
184 let curve = NamedCurve::of(chain)?;
185 Ok(Self {
186 entity: curve.name.clone(),
187 name: curve.name,
188 curve: curve.curve,
189 forward: true,
190 start: curve.start.1,
191 end: curve.end.1,
192 start_name: curve.start.0,
193 end_name: curve.end.0,
194 })
195 }
196
197 fn turned(&self) -> Self {
198 Self {
199 forward: !self.forward,
200 start: self.end,
201 end: self.start,
202 start_name: self.end_name.clone(),
203 end_name: self.start_name.clone(),
204 ..self.clone()
205 }
206 }
207
208 fn oriented(&self) -> Curve3 {
210 self.sense().orient(&self.curve)
211 }
212
213 fn sense(&self) -> Sense {
215 Sense::from_forward(self.forward)
216 }
217}
218
219fn distance(a: &Vector3, b: &Vector3) -> f64 {
221 a.sub(b).norm().to_f64()
222}
223
224pub fn boundary_surface(
229 part: &NamedModel,
230 namer: &Namer,
231 edges: &[Chain<NurbCurve3D>],
232 tangent: &[&str],
233) -> GeopResult<SheetSpec> {
234 let sides = edges.iter().map(Side::of).collect::<GeopResult<Vec<_>>>()?;
235 if let Some(t) = (tangent.iter()).find(|t| !sides.iter().any(|s| s.entity == **t)) {
236 return Err(GeopError::new(format!(
237 "edge {t} is to be tangent along, but is not one of the curves spanned"
238 )));
239 }
240 let (spec, names) = match sides.as_slice() {
241 [] => return Err(GeopError::new("no edges to span a surface between")),
242 [a, b] if !closes(a, b) => {
243 if !tangent.is_empty() {
244 return Err(GeopError::new(
245 "a ruled face between two edges cannot be made tangent: tangency is supported along a loop of four edges",
246 ));
247 }
248 ruled(namer, a, b)?
249 }
250 _ => {
251 let sides = chain(&sides)?;
252 let faces = tangent
253 .iter()
254 .map(|&t| Ok((t.to_string(), Tangency::along(part, t)?)))
255 .collect::<GeopResult<Vec<_>>>()?;
256 fill(namer, &sides, &faces)?
257 }
258 };
259 Ok((spec, names))
260}
261
262fn closes(a: &Side, b: &Side) -> bool {
265 let meet = |p: &Vector3, q: &Vector3| p.could_be_equal(q);
266 (meet(&a.end, &b.start) && meet(&b.end, &a.start))
267 || (meet(&a.end, &b.end) && meet(&b.start, &a.start))
268}
269
270fn ruled(namer: &Namer, a: &Side, b: &Side) -> GeopResult<(BodySpec, BodyNames)> {
274 let along = distance(&a.start, &b.start) + distance(&a.end, &b.end);
275 let across = distance(&a.start, &b.end) + distance(&a.end, &b.start);
276 let b = if across < along {
277 b.turned()
278 } else {
279 b.clone()
280 };
281 let (apex_start, apex_end) = (
282 a.start.could_be_equal(&b.start),
283 a.end.could_be_equal(&b.end),
284 );
285 if apex_start && apex_end {
286 return Err(GeopError::new(format!(
287 "edges {} and {} share both ends: they close a loop, not two sides of a ruled face",
288 a.name, b.name
289 )));
290 }
291 let mut vertices = vec![a.start, a.end];
294 let mut vertex_names = vec![namer.name(&[&a.start_name]), namer.name(&[&a.end_name])];
295 let mut vertex = |shared: bool, of_a: usize, point: Vector3, name: &str| {
296 if shared {
297 return of_a;
298 }
299 vertices.push(point);
300 vertex_names.push(namer.name(&[name]));
301 vertices.len() - 1
302 };
303 let b_start = vertex(apex_start, 0, b.start, &b.start_name);
304 let b_end = vertex(apex_end, 1, b.end, &b.end_name);
305 let copy = |side: &Side, start: usize, end: usize| EdgeSpec {
306 curve: side.curve.clone(),
307 start: if side.forward { start } else { end },
308 end: if side.forward { end } else { start },
309 };
310 let mut edges = vec![copy(a, 0, 1), copy(&b, b_start, b_end)];
311 let mut edge_names = vec![namer.name(&[&a.name]), namer.name(&[&b.name])];
312 let mut side = |shared: bool, from: (usize, Vector3, &str), to: (usize, Vector3, &str)| {
314 if shared {
315 return Ok(CoedgeOn::Vertex(from.0));
316 }
317 edges.push(EdgeSpec {
318 curve: NurbCurve3D::line(from.1, to.1)?,
319 start: from.0,
320 end: to.0,
321 });
322 edge_names.push(namer.name(&[from.2, to.2]));
323 Ok::<_, GeopError>(CoedgeOn::Edge(edges.len() - 1, Sense::Forward))
324 };
325 let end_side = side(
326 apex_end,
327 (1, a.end, &a.end_name),
328 (b_end, b.end, &b.end_name),
329 )?;
330 let start_side = side(
331 apex_start,
332 (b_start, b.start, &b.start_name),
333 (0, a.start, &a.start_name),
334 )?;
335 let surface = NurbSurface3D::ruled(&a.oriented(), &b.oriented())?;
336 let face = FaceSpec::on_unit_square(
337 surface,
338 [
339 CoedgeOn::Edge(0, a.sense()),
340 end_side,
341 CoedgeOn::Edge(1, b.sense().opposite()),
342 start_side,
343 ],
344 )?;
345 Ok((
346 BodySpec {
347 vertices,
348 edges,
349 faces: vec![face],
350 shells: vec![vec![0]],
351 solid: false,
352 },
353 BodyNames {
354 vertices: vertex_names,
355 edges: edge_names,
356 faces: vec![namer.root()],
357 solid: None,
358 },
359 ))
360}
361
362fn chain(sides: &[Side]) -> GeopResult<Vec<Side>> {
366 let mut left: Vec<Side> = sides[1..].to_vec();
367 let mut out = vec![sides[0].clone()];
368 for _ in 1..sides.len() {
370 let end = out.last().expect("one side at least").end;
371 let mut next: Vec<(usize, bool)> = Vec::new();
372 for (i, s) in left.iter().enumerate() {
373 if s.start.could_be_equal(&end) {
374 next.push((i, false));
375 }
376 if s.end.could_be_equal(&end) {
377 next.push((i, true));
378 }
379 }
380 let last = &out.last().expect("one side at least").name;
381 match next.as_slice() {
382 [] => {
383 return Err(GeopError::new(format!(
384 "the edges are not a closed loop: none of them goes on from where edge {last} ends"
385 )));
386 }
387 [(i, turn)] => {
388 let s = left.remove(*i);
389 out.push(if *turn { s.turned() } else { s });
390 }
391 _ => {
392 return Err(GeopError::new(format!(
393 "the edges are not a simple loop: several of them go on from where edge {last} ends"
394 )));
395 }
396 }
397 }
398 let (first, last) = (&out[0], out.last().expect("one side at least"));
399 if !last.end.could_be_equal(&first.start) {
400 return Err(GeopError::new(format!(
401 "the edges are not a closed loop: edge {} ends where edge {} does not start",
402 last.name, first.name
403 )));
404 }
405 Ok(out)
406}
407
408#[derive(Clone, Debug)]
412struct Tangency {
413 surface: NurbSurface3D,
414 plane: Option<Plane>,
415 forward: bool,
416}
417
418impl Tangency {
419 fn along(part: &NamedModel, name: &str) -> GeopResult<Self> {
422 let model = part.topology();
423 let edge = part.edge_id(name)?;
424 let coedges = model.coedges_of_edge(edge);
425 let [coedge] = coedges.as_slice() else {
426 return Err(GeopError::new(format!(
427 "edge {name} bounds {} faces: tangency is to the one face an edge bounds, standing on its own",
428 coedges.len()
429 )));
430 };
431 let coedge = model.get_coedge(*coedge)?;
432 let surface = model.get_face(coedge.face)?.surface.clone();
433 Ok(Self {
434 plane: surface.as_plane()?,
435 surface,
436 forward: coedge.sense == Sense::Forward,
437 })
438 }
439
440 fn neighbour(&self, side: &Side) -> GeopResult<Neighbour<'_>> {
443 Neighbour::new(&self.surface, side.forward == self.forward)
444 }
445}
446
447fn has_corner(a: &Curve3, b: &Curve3) -> GeopResult<bool> {
450 let end = a.tangent(a.domain().1)?;
451 let start = b.tangent(b.domain().0)?;
452 Ok(!end
453 .prod_cross(&start)
454 .norm_sq()
455 .could_be_equal(Scalar::ZERO))
456}
457
458fn fill(
461 namer: &Namer,
462 sides: &[Side],
463 faces: &[(String, Tangency)],
464) -> GeopResult<(BodySpec, BodyNames)> {
465 let n = sides.len();
466 let mut spec = BodySpec {
469 vertices: sides.iter().map(|s| s.start).collect(),
470 edges: Vec::new(),
471 faces: Vec::new(),
472 shells: Vec::new(),
473 solid: false,
474 };
475 let mut names = BodyNames {
476 vertices: sides.iter().map(|s| namer.name(&[&s.start_name])).collect(),
477 ..Default::default()
478 };
479
480 if let Some(plane) = flat(sides)? {
481 for (e, face) in faces {
482 let same = face.plane.as_ref().is_some_and(|p| {
483 p.normal
484 .prod_cross(&plane.normal)
485 .norm_sq()
486 .could_be_equal(Scalar::ZERO)
487 && p.signed_distance(&plane.point).could_be_equal(Scalar::ZERO)
488 });
489 if !same {
490 let side = sides.iter().find(|s| s.entity == *e).expect("a side");
491 return Err(GeopError::new(format!(
492 "the loop is flat, in a plane other than that of the face along edge {}: the fill cannot be tangent to it",
493 side.name
494 )));
495 }
496 }
497 for (k, s) in sides.iter().enumerate() {
498 let (a, b) = (k, (k + 1) % n);
499 spec.edges.push(EdgeSpec {
500 curve: s.curve.clone(),
501 start: if s.forward { a } else { b },
502 end: if s.forward { b } else { a },
503 });
504 names.edges.push(namer.name(&[&s.name]));
505 }
506 spec.faces.push(flat_face(sides, &plane)?);
507 spec.shells.push(vec![0]);
508 names.faces.push(namer.root());
509 return Ok((spec, names));
510 }
511
512 let curves: Vec<Curve3> = sides.iter().map(Side::oriented).collect();
513 if n < 3 {
514 return Err(GeopError::new(format!(
515 "a loop of {n} edges that is not flat is not supported: it needs three corners at least"
516 )));
517 }
518 let neighbours = sides
520 .iter()
521 .map(|side| {
522 faces
523 .iter()
524 .find(|(e, _)| side.entity == *e)
525 .map(|(_, face)| face.neighbour(side))
526 .transpose()
527 })
528 .collect::<GeopResult<Vec<_>>>()?;
529 if n != 4 {
530 return quads(namer, sides, &curves, &neighbours, spec, names);
531 }
532 for k in 0..n {
533 if !has_corner(&curves[(k + n - 1) % n], &curves[k])? {
534 return Err(GeopError::new(format!(
535 "the loop runs on smoothly through vertex {}, between edges {} and {}: a Coons patch filling it needs a corner there",
536 sides[k].start_name,
537 sides[(k + n - 1) % n].name,
538 sides[k].name
539 )));
540 }
541 }
542 for (k, s) in sides.iter().enumerate() {
543 spec.edges.push(EdgeSpec {
544 curve: s.curve.clone(),
545 start: if s.forward { k } else { (k + 1) % 4 },
546 end: if s.forward { (k + 1) % 4 } else { k },
547 });
548 names.edges.push(namer.name(&[&s.name]));
549 }
550 let mut surface = NurbSurface3D::coons([&curves[0], &curves[1], &curves[2], &curves[3]])?;
551 if !faces.is_empty() {
552 let neighbours: [Option<Neighbour<'_>>; 4] = std::array::from_fn(|k| neighbours[k].clone());
553 surface = surface.tangent_to(&neighbours)?;
554 }
555 spec.faces.push(FaceSpec::on_unit_square(
556 surface,
557 [0, 1, 2, 3].map(|k| CoedgeOn::Edge(k, sides[k].sense())),
558 )?);
559 spec.shells.push(vec![0]);
560 names.faces.push(namer.root());
561 Ok((spec, names))
562}
563
564fn flat(sides: &[Side]) -> GeopResult<Option<Plane>> {
568 let points: Vec<Vector3> = sides
569 .iter()
570 .flat_map(|s| &s.curve.control_points)
571 .map(Vector4::dehomogenized)
572 .collect::<GeopResult<_>>()?;
573 let p0 = points[0];
576 let far = |key: &dyn Fn(&Vector3) -> f64| {
577 *points
578 .iter()
579 .max_by(|a, b| key(a).total_cmp(&key(b)))
580 .expect("points")
581 };
582 let p1 = far(&|p| distance(p, &p0));
583 let p2 = far(&|p| p.sub(&p0).prod_cross(&p1.sub(&p0)).norm().to_f64());
584 let normal = p1.sub(&p0).prod_cross(&p2.sub(&p0));
585 if normal.norm_sq().could_be_equal(Scalar::ZERO) {
586 return Err(GeopError::new(
587 "the edges all lie along one line: they enclose nothing to fill",
588 ));
589 }
590 let plane = Plane::try_new(p0, normal)?;
591 Ok(points
592 .iter()
593 .all(|p| plane.signed_distance(p).could_be_equal(Scalar::ZERO))
594 .then_some(plane))
595}
596
597fn flat_face(sides: &[Side], plane: &Plane) -> GeopResult<FaceSpec> {
603 let origin = plane.point;
604 let first = &sides[0].curve.control_points;
605 let towards = Vector3::from_array([0, 1, 2].map(|k| first[first.len() - 1][k]))
606 .prod_scalar(Scalar::ONE.div(first[first.len() - 1][3])?);
607 let mut e1 = towards.sub(&origin);
610 if e1.norm_sq().could_be_equal(Scalar::ZERO) {
611 e1 = plane
612 .normal
613 .orthonormal_complement()?
614 .into_iter()
615 .next()
616 .expect("a complement");
617 }
618 let e1 = e1
619 .sub(&plane.normal.prod_scalar(e1.prod_dot(&plane.normal)))
620 .normalize()?;
621 let e2 = plane.normal.prod_cross(&e1);
622 let local = sides
625 .iter()
626 .map(|side| {
627 let curve = side.oriented();
628 let control_points = curve
629 .control_points
630 .iter()
631 .map(|cp| flatten_point(cp, &origin, &e1, &e2))
632 .collect();
633 NurbCurve::try_new(curve.degree, control_points, curve.knot_vector.clone())
634 })
635 .collect::<GeopResult<Vec<NurbCurve2D>>>()?;
636 let area = loop_signed_area(&local)?;
637 let swapped = if area.definitely_greater(Scalar::ZERO) {
638 false
639 } else if area.definitely_less(Scalar::ZERO) {
640 true
641 } else {
642 return Err(GeopError::new(
643 "cannot tell which way the loop runs round: it encloses no area",
644 ));
645 };
646 let patch = FlatPatch::around(origin, e1, e2, &local, swapped)?;
647 let surface = patch.surface()?;
648 let pcurves = local
649 .iter()
650 .map(|curve| patch.pcurve(curve))
651 .collect::<GeopResult<Vec<_>>>()?;
652 let outer = sides
653 .iter()
654 .zip(pcurves)
655 .enumerate()
656 .map(|(k, (s, pcurve))| CoedgeSpec {
657 on: CoedgeOn::Edge(k, s.sense()),
658 pcurve,
659 })
660 .collect();
661 Ok(FaceSpec {
662 surface,
663 outer,
664 holes: Vec::new(),
665 })
666}
667
668fn quads(
673 namer: &Namer,
674 sides: &[Side],
675 curves: &[Curve3],
676 neighbours: &[Option<Neighbour<'_>>],
677 mut spec: BodySpec,
678 mut names: BodyNames,
679) -> GeopResult<(BodySpec, BodyNames)> {
680 let n = sides.len();
681 let fill = NurbSurface3D::fill(curves, neighbours)?;
682 let inner = &fill.inner;
683 for (s, p) in sides.iter().zip(inner) {
685 spec.vertices.push(*p);
686 names.vertices.push(namer.name(&[&s.start_name, "inner"]));
687 }
688 let inner_at = |k: usize| n + k % n;
689 let mut inner_sides = Vec::with_capacity(n);
693 for (k, s) in sides.iter().enumerate() {
694 let (a, b) = (k, (k + 1) % n);
695 spec.edges.push(EdgeSpec {
696 curve: s.curve.clone(),
697 start: if s.forward { a } else { b },
698 end: if s.forward { b } else { a },
699 });
700 names.edges.push(namer.name(&[&s.name]));
701 spec.edges.push(EdgeSpec {
702 curve: fill.spokes[k].clone(),
703 start: k,
704 end: inner_at(k),
705 });
706 names.edges.push(namer.name(&[&s.start_name, "spoke"]));
707 let curve = NurbCurve3D::line(inner[k], inner[(k + 1) % n])?;
708 spec.edges.push(EdgeSpec {
709 curve: curve.clone(),
710 start: inner_at(k),
711 end: inner_at(k + 1),
712 });
713 names.edges.push(namer.name(&[&s.name, "inner"]));
714 inner_sides.push(Side {
715 entity: s.entity.clone(),
716 name: namer.name(&[&s.name, "inner"]),
717 curve,
718 forward: true,
719 start: inner[k],
720 end: inner[(k + 1) % n],
721 start_name: String::new(),
722 end_name: String::new(),
723 });
724 }
725 let mut faces = Vec::with_capacity(n + 1);
726 for (k, surface) in fill.patches.iter().enumerate() {
727 let next = (k + 1) % n;
728 let surface = surface.clone();
729 faces.push(spec.faces.len());
730 spec.faces.push(FaceSpec::on_unit_square(
731 surface,
732 [
733 CoedgeOn::Edge(3 * k, sides[k].sense()),
734 CoedgeOn::Edge(3 * next + 1, Sense::Forward),
735 CoedgeOn::Edge(3 * k + 2, Sense::Reversed),
736 CoedgeOn::Edge(3 * k + 1, Sense::Reversed),
737 ],
738 )?);
739 names.faces.push(namer.name(&[&sides[k].name, "patch"]));
740 }
741 let mut middle = flat_face(&inner_sides, &fill.plane)?;
743 for (k, c) in middle.outer.iter_mut().enumerate() {
744 c.on = CoedgeOn::Edge(3 * k + 2, Sense::Forward);
745 }
746 faces.push(spec.faces.len());
747 spec.faces.push(middle);
748 names.faces.push(namer.name(&["inner"]));
749 spec.shells.push(faces);
750 Ok((spec, names))
751}
752
753#[cfg(test)]
754pub(crate) mod tests;