1use geop_core_editor::{
34 edit_form::{EditForm, FormState},
35 editable::{Context, OperationGroup, OperationInfo, OperationTier},
36 editing::{Number, Unit},
37 part_operation,
38 part_operation::PartOperation,
39};
40use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
41use std::collections::HashMap;
42
43use geop_core_geometry::{
44 contains::curve::curve_could_contain,
45 nurb_surface::NurbSurface3D,
46 search::{MAX_NODES, PROJECT_ITERATIONS, min_subdivision_size},
47};
48use geop_core_math::{
49 geop_error::{GeopError, GeopResult},
50 scalars::Scalar,
51 solvers::walk::{Take, flood},
52 union_find::UnionFind,
53 vector::{Vector3, Vector4},
54};
55use geop_core_topology::{
56 Body, Curve2, Curve3, Sense, ShellId,
57 build::{BodySpec, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec},
58 validation::{Outward, ValidationParameters, normal_points_outward},
59};
60use geop_lib_shell::rebuilt;
61use geop_ops_model::{BodyNames, Face, ModelOperation, NamedModel, Namer, build_model};
62use serde::{Deserialize, Serialize};
63
64#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
70pub struct Knit;
71
72#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
73pub struct KnitArgs {
74 pub faces: Vec<ElementReference<NamedModel, Face>>,
76 #[serde(default)]
78 pub solid: bool,
79 #[serde(default)]
83 pub gap: f64,
84}
85
86part_operation!(Knit(KnitArgs));
87
88impl PartOperation for Knit {
89 const KIND: &'static str = "knit";
90 const INFO: OperationInfo = OperationInfo::new(
91 "Knit",
92 "Join faces standing on their own along the edges where they meet, into a solid once they close up.",
93 OperationGroup::Surface,
94 OperationTier::Menu,
95 )
96 .experimental()
97 .icon(crate::icons::KNIT);
98 type Args = KnitArgs;
99 type Session = ();
100
101 fn new_args(_: &Context) -> KnitArgs {
103 KnitArgs {
104 faces: Vec::new(),
105 solid: true,
106 gap: 0.0,
107 }
108 }
109
110 fn run(ctx: &mut StepContext, args: &KnitArgs) -> GeopResult<()> {
111 let _step = ctx.step();
112 build_model::<Self>(ctx, args)
113 }
114
115 fn form<'a>(
117 context: &'a Context<'a>,
118 args: &KnitArgs,
119 _: &FormState<()>,
120 ) -> EditForm<'a, KnitArgs> {
121 let mut f = EditForm::<KnitArgs>::default();
122 f.pick(
123 "faces",
124 "sheets",
125 &context.before,
126 &args.faces,
127 true,
128 |edit, faces| edit.args.faces = faces,
129 );
130 f.checkbox("solid", "close into a solid", args.solid, |edit, solid| {
131 edit.args.solid = solid
132 });
133 let gap = Number::new("gap", args.gap, Unit::Length).range(0.0, 1.0);
134 f.number("gap", gap, |edit, gap| edit.args.gap = gap);
135 f
136 }
137}
138
139impl ModelOperation for Knit {
140 fn build(model: &mut NamedModel, _: &Reader, step: &str, args: &KnitArgs) -> GeopResult<()> {
141 let namer = Namer::new("knit", step)?;
142 let mut sheets: Vec<ShellId> = Vec::new();
143 for face in &args.faces {
144 let sheet = model.sheet_of_face(model.face_id(face.name())?, "sheets are knit")?;
145 if !sheets.contains(&sheet) {
146 sheets.push(sheet);
147 }
148 }
149 if !(args.gap.is_finite() && args.gap >= 0.0) {
150 return Err(GeopError::new(format!(
151 "a gap is a distance of zero or more, not {}",
152 args.gap
153 )));
154 }
155 knit(
156 model,
157 &namer,
158 &sheets,
159 args.solid,
160 Scalar::from_f64(args.gap),
161 )?;
162 Ok(())
163 }
164}
165
166pub fn knit(
171 part: &mut NamedModel,
172 namer: &Namer,
173 sheets: &[ShellId],
174 solid: bool,
175 gap: Scalar,
176) -> GeopResult<Body> {
177 if sheets.is_empty() {
178 return Err(GeopError::new(
179 "knitting needs faces standing on their own: pick the sheets to join",
180 ));
181 }
182 if sheets.len() < 2 && !solid {
183 return Err(GeopError::new("knitting needs two sheets at least"));
184 }
185 let model = part.topology();
186 let mut faces = Vec::new();
187 let mut sheet_of_face = Vec::new();
188 for (k, &sheet) in sheets.iter().enumerate() {
189 for face in model.body_faces(Body::Sheet(sheet))? {
190 faces.push(face);
191 sheet_of_face.push(k);
192 }
193 }
194 let (mut spec, sources) = model.body_spec(&faces, false)?;
195 let BodyNames {
196 vertices: mut vertex_names,
197 edges: mut edge_names,
198 faces: face_names,
199 ..
200 } = part.source_names(&sources)?;
201 let near = Near { gap };
202 split_at_junctions(
203 &mut spec,
204 &sheet_of_face,
205 &mut vertex_names,
206 &mut edge_names,
207 namer,
208 near,
209 )?;
210
211 let joined = Joined::new(&spec, &sheet_of_face, &vertex_names, &edge_names, near)?;
212 let (mut out, uses) = joined.spec(&spec)?;
213 let open: Vec<&str> = uses
214 .iter()
215 .enumerate()
216 .filter(|&(_, &n)| n == 1)
217 .map(|(e, _)| edge_names[joined.edge_source[e]].as_str())
218 .collect();
219 if solid && !open.is_empty() {
220 return Err(GeopError::new(format!(
221 "the sheets do not close up into a solid: edges {open:?} are open — each is used by one face only, with no edge of another sheet running along it from end to end"
222 )));
223 }
224 out.solid = open.is_empty();
225 let names = BodyNames {
226 vertices: joined
227 .vertex_source
228 .iter()
229 .map(|&v| vertex_names[v].clone())
230 .collect(),
231 edges: joined
232 .edge_source
233 .iter()
234 .map(|&e| edge_names[e].clone())
235 .collect(),
236 faces: face_names,
237 solid: out.solid.then(|| namer.root()),
238 };
239 turn_consistently(&mut out, &names)?;
240 if gap.definitely_greater(Scalar::ZERO) && out.solid {
247 let reseat: Vec<bool> = (0..out.vertices.len())
248 .map(|v| (joined.vertex.iter()).filter(|&&w| w == v).count() > 1)
249 .collect();
250 let kept = vec![false; out.faces.len()];
251 out = rebuilt(&out, &names, &kept, &reseat).map_err(|e| {
252 e.with_context("moving what was joined across the gap onto where the surfaces meet")
253 })?;
254 }
255 let consumed: Vec<Body> = sheets.iter().map(|&s| Body::Sheet(s)).collect();
256 part.delete_bodies(&consumed)?;
257 let built = part.build_body(out, names)?;
258 let Some(solid_id) = built.solid else {
259 return built.body();
260 };
261 let params = ValidationParameters::default();
264 let model = part.topology();
265 let mut inward = None;
266 for &f in &built.faces {
267 if let Some(verdict) = normal_points_outward(¶ms, model, solid_id, built.shells[0], f)?
268 {
269 inward = Some(matches!(verdict, Outward::No(_)));
270 break;
271 }
272 }
273 match inward {
274 Some(true) => {
275 for &f in &built.faces {
276 part.reverse_face(f)?;
277 }
278 }
279 Some(false) => {}
280 None => {
281 return Err(GeopError::new(
282 "the sheets close up, but no face tells which side of them the solid is on",
283 ));
284 }
285 }
286 Ok(Body::Solid(solid_id))
287}
288
289struct Joined {
292 vertex: Vec<usize>,
294 vertex_source: Vec<usize>,
296 edge: Vec<(usize, bool)>,
299 edge_source: Vec<usize>,
301}
302
303impl Joined {
304 fn new(
305 spec: &BodySpec,
306 sheet_of_face: &[usize],
307 vertex_names: &[String],
308 edge_names: &[String],
309 near: Near,
310 ) -> GeopResult<Self> {
311 let mut vertex_sheet = vec![usize::MAX; spec.vertices.len()];
313 let mut edge_sheet = vec![usize::MAX; spec.edges.len()];
314 for (f, face) in spec.faces.iter().enumerate() {
315 for c in std::iter::once(&face.outer).chain(&face.holes).flatten() {
316 match c.on {
317 CoedgeOn::Edge(e, _) => {
318 edge_sheet[e] = sheet_of_face[f];
319 vertex_sheet[spec.edges[e].start] = sheet_of_face[f];
320 vertex_sheet[spec.edges[e].end] = sheet_of_face[f];
321 }
322 CoedgeOn::Vertex(v) => vertex_sheet[v] = sheet_of_face[f],
323 }
324 }
325 }
326
327 let n = spec.vertices.len();
329 let mut groups = UnionFind::new(n);
330 for i in 0..n {
331 for j in i + 1..n {
332 if vertex_sheet[i] != vertex_sheet[j]
333 && near.points(&spec.vertices[i], &spec.vertices[j])
334 {
335 groups.union(i, j);
336 }
337 }
338 }
339 let mut vertex = vec![0; n];
340 let mut vertex_source = Vec::new();
341 for group in groups.groups() {
342 let mut sheets: Vec<usize> = group.iter().map(|&v| vertex_sheet[v]).collect();
343 sheets.sort();
344 if sheets.windows(2).any(|w| w[0] == w[1]) {
345 let names: Vec<&str> = group.iter().map(|&v| vertex_names[v].as_str()).collect();
346 return Err(GeopError::new(format!(
347 "vertices {names:?} could all be at one point, two of them of the same sheet: which to join is ambiguous"
348 )));
349 }
350 for &v in &group {
351 vertex[v] = vertex_source.len();
352 }
353 vertex_source.push(group[0]);
354 }
355
356 let ends = |e: usize| {
359 let (a, b) = (vertex[spec.edges[e].start], vertex[spec.edges[e].end]);
360 (a.min(b), a.max(b))
361 };
362 let mut between: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
363 for e in 0..spec.edges.len() {
364 between.entry(ends(e)).or_default().push(e);
365 }
366 let mut partner: Vec<Option<usize>> = vec![None; spec.edges.len()];
367 let mut keys: Vec<_> = between.keys().copied().collect();
368 keys.sort();
369 for key in keys {
370 let candidates = &between[&key];
371 for (i, &a) in candidates.iter().enumerate() {
372 for &b in &candidates[i + 1..] {
373 if edge_sheet[a] == edge_sheet[b]
374 || !coincide(&spec.edges[a], &spec.edges[b], near)?
375 {
376 continue;
377 }
378 if let Some(other) = partner[a].or(partner[b]) {
379 return Err(GeopError::new(format!(
380 "edges {}, {} and {} could all run along one another: which to join is ambiguous",
381 edge_names[a], edge_names[b], edge_names[other]
382 )));
383 }
384 partner[a] = Some(b);
385 partner[b] = Some(a);
386 }
387 }
388 }
389 let mut edge = vec![(0, false); spec.edges.len()];
390 let mut edge_source = Vec::new();
391 for e in 0..spec.edges.len() {
392 match partner[e] {
393 Some(first) if first < e => {
394 let (kept, this) = (&spec.edges[first], &spec.edges[e]);
395 let turned = vertex[kept.start] != vertex[this.start]
396 || (kept.start == kept.end) && turned_closed(kept, this)?;
397 edge[e] = (edge[first].0, turned);
398 }
399 _ => {
400 edge[e] = (edge_source.len(), false);
401 edge_source.push(e);
402 }
403 }
404 }
405 Ok(Self {
406 vertex,
407 vertex_source,
408 edge,
409 edge_source,
410 })
411 }
412
413 fn spec(&self, spec: &BodySpec) -> GeopResult<(BodySpec, Vec<usize>)> {
416 let mut uses = vec![0; self.edge_source.len()];
417 let mut faces = Vec::with_capacity(spec.faces.len());
418 for face in &spec.faces {
419 let mut remap = |lp: &Vec<geop_core_topology::build::CoedgeSpec>| {
420 lp.iter()
421 .map(|c| {
422 let mut c = c.clone();
423 c.on = match c.on {
424 CoedgeOn::Edge(e, sense) => {
425 let (kept, turned) = self.edge[e];
426 uses[kept] += 1;
427 CoedgeOn::Edge(kept, if turned { sense.opposite() } else { sense })
428 }
429 CoedgeOn::Vertex(v) => CoedgeOn::Vertex(self.vertex[v]),
430 };
431 c
432 })
433 .collect::<Vec<_>>()
434 };
435 let outer = remap(&face.outer);
436 let holes = face.holes.iter().map(&mut remap).collect();
437 faces.push(FaceSpec {
438 surface: face.surface.clone(),
439 outer,
440 holes,
441 });
442 }
443 let out = BodySpec {
444 vertices: self
445 .vertex_source
446 .iter()
447 .map(|&v| {
448 (0..spec.vertices.len())
450 .filter(|&w| self.vertex[w] == self.vertex[v])
451 .map(|w| spec.vertices[w])
452 .reduce(|a, b| a.union(&b))
453 .expect("the vertex itself")
454 })
455 .collect(),
456 edges: self
457 .edge_source
458 .iter()
459 .map(|&e| {
460 let edge = &spec.edges[e];
461 EdgeSpec {
462 curve: edge.curve.clone(),
463 start: self.vertex[edge.start],
464 end: self.vertex[edge.end],
465 }
466 })
467 .collect(),
468 faces,
469 shells: vec![(0..spec.faces.len()).collect()],
470 solid: false,
471 };
472 Ok((out, uses))
473 }
474}
475
476fn coincide(a: &EdgeSpec, b: &EdgeSpec, near: Near) -> GeopResult<bool> {
479 let middle = |e: &EdgeSpec| e.curve.evaluate(e.curve.mid_parameter()?);
480 let on = |e: &EdgeSpec, p| -> GeopResult<bool> { Ok(near.on_curve(&e.curve, &p)?.is_some()) };
481 Ok(on(b, middle(a)?)? && on(a, middle(b)?)?)
482}
483
484#[derive(Clone, Copy, Debug)]
488struct Near {
489 gap: Scalar,
490}
491
492impl Near {
493 fn grown(&self, p: &Vector3) -> Vector3 {
495 Vector3::from_array(std::array::from_fn(|k| {
496 p[k].sub(self.gap).union(p[k].add(self.gap))
497 }))
498 }
499
500 fn points(&self, a: &Vector3, b: &Vector3) -> bool {
502 self.grown(a).could_be_equal(b)
503 }
504
505 fn on_curve(&self, curve: &Curve3, p: &Vector3) -> GeopResult<Option<Scalar>> {
507 curve_could_contain(curve, &self.grown(p), MAX_NODES, min_subdivision_size())
508 }
509}
510
511fn split_at_junctions(
521 spec: &mut BodySpec,
522 sheet_of_face: &[usize],
523 vertex_names: &mut Vec<String>,
524 edge_names: &mut Vec<String>,
525 namer: &Namer,
526 near: Near,
527) -> GeopResult<()> {
528 let mut edge_sheet = vec![usize::MAX; spec.edges.len()];
529 let mut vertex_sheet = vec![usize::MAX; spec.vertices.len()];
530 for (f, face) in spec.faces.iter().enumerate() {
531 for c in face.loops().into_iter().flatten() {
532 let (a, b) = spec.ends(c.on)?;
533 vertex_sheet[a] = sheet_of_face[f];
534 vertex_sheet[b] = sheet_of_face[f];
535 if let CoedgeOn::Edge(e, _) = c.on {
536 edge_sheet[e] = sheet_of_face[f];
537 }
538 }
539 }
540 let mut e = 0;
541 while e < spec.edges.len() {
542 let mut split = None;
543 for (v, &point) in spec.vertices.iter().enumerate() {
544 let edge = &spec.edges[e];
545 if vertex_sheet[v] == edge_sheet[e]
546 || near.points(&point, &spec.vertices[edge.start])
547 || near.points(&point, &spec.vertices[edge.end])
548 {
549 continue;
550 }
551 let Some(t) = near.on_curve(&edge.curve, &point)? else {
552 continue;
553 };
554 let t = edge.curve.closest_parameter_from(&point, t.sharpen())?;
555 let (t0, t1) = edge.curve.domain();
556 if t.definitely_greater(t0)
557 && t.definitely_less(t1)
558 && near.points(&edge.curve.evaluate(t)?, &point)
559 {
560 split = Some((v, t));
561 break;
562 }
563 }
564 match split {
565 Some((v, t)) => {
566 let name = namer.piece(&edge_names[e], &vertex_names[v]);
567 split_edge(spec, e, t)?;
568 vertex_sheet.push(edge_sheet[e]);
569 vertex_names.push(name.clone());
570 edge_sheet.push(edge_sheet[e]);
571 edge_names.push(name);
572 }
574 None => e += 1,
575 }
576 }
577 Ok(())
578}
579
580fn split_edge(spec: &mut BodySpec, e: usize, t: Scalar) -> GeopResult<()> {
585 let edge = spec.edges[e].clone();
586 let (t0, t1) = edge.curve.domain();
587 let (first, second) = (edge.curve.sub_curve(t0, t)?, edge.curve.sub_curve(t, t1)?);
588 let point = edge.curve.evaluate(t)?;
589 let v = spec.vertices.len();
590 spec.vertices.push(point);
591 spec.edges[e] = EdgeSpec {
592 curve: first,
593 start: edge.start,
594 end: v,
595 };
596 let next = spec.edges.len();
597 spec.edges.push(EdgeSpec {
598 curve: second,
599 start: v,
600 end: edge.end,
601 });
602 for face in &mut spec.faces {
603 let surface = face.surface.clone();
604 for lp in std::iter::once(&mut face.outer).chain(face.holes.iter_mut()) {
605 let Some(k) = lp
606 .iter()
607 .position(|c| matches!(c.on, CoedgeOn::Edge(x, _) if x == e))
608 else {
609 continue;
610 };
611 let coedge = lp[k].clone();
612 let CoedgeOn::Edge(_, sense) = coedge.on else {
613 unreachable!("found on an edge")
614 };
615 let (before, after) = split_pcurve(&surface, &coedge.pcurve, &point)?;
616 let pieces = match sense {
617 Sense::Forward => [(e, before), (next, after)],
618 Sense::Reversed => [(next, before), (e, after)],
619 };
620 lp.splice(
621 k..=k,
622 pieces.map(|(edge, pcurve)| CoedgeSpec {
623 on: CoedgeOn::Edge(edge, sense),
624 pcurve,
625 }),
626 );
627 }
628 }
629 Ok(())
630}
631
632fn split_pcurve(
636 surface: &NurbSurface3D,
637 pcurve: &Curve2,
638 point: &Vector3,
639) -> GeopResult<(Curve2, Curve2)> {
640 let (s0, s1) = pcurve.domain();
641 let middle = pcurve.evaluate(pcurve.mid_parameter()?)?;
642 let (u, v) = surface.project(
643 *point,
644 middle[0].sharpen(),
645 middle[1].sharpen(),
646 PROJECT_ITERATIONS,
647 )?;
648 let lifted = Curve3::try_new(
649 pcurve.degree,
650 (pcurve.control_points.iter())
651 .map(|cp| Vector4::from_array([cp[0], cp[1], Scalar::ZERO, cp[2]]))
652 .collect(),
653 pcurve.knot_vector.clone(),
654 )?;
655 let target = Vector3::from_array([u, v, Scalar::ZERO]);
656 let s = lifted.closest_parameter_from(&target, pcurve.mid_parameter()?.sharpen())?;
657 if !(s.definitely_greater(s0) && s.definitely_less(s1)) {
658 return Err(GeopError::new(format!(
659 "the pcurve does not pass the split point's foot point {:?} inside it",
660 (u, v)
661 )));
662 }
663 Ok((pcurve.sub_curve(s0, s)?, pcurve.sub_curve(s, s1)?))
664}
665
666fn turned_closed(a: &EdgeSpec, b: &EdgeSpec) -> GeopResult<bool> {
669 let ta = a.curve.tangent(a.curve.domain().0)?;
670 let tb = b.curve.tangent(b.curve.domain().0)?;
671 let along = ta.prod_dot(&tb);
672 if along.definitely_less(Scalar::ZERO) {
673 Ok(true)
674 } else if along.definitely_greater(Scalar::ZERO) {
675 Ok(false)
676 } else {
677 Err(GeopError::new(
678 "cannot tell which way round two closed edges run along one another",
679 ))
680 }
681}
682
683fn turn_consistently(spec: &mut BodySpec, names: &BodyNames) -> GeopResult<()> {
688 let mut users: Vec<Vec<(usize, geop_core_topology::Sense)>> =
690 vec![Vec::new(); spec.edges.len()];
691 for (f, face) in spec.faces.iter().enumerate() {
692 for c in std::iter::once(&face.outer).chain(&face.holes).flatten() {
693 if let CoedgeOn::Edge(e, sense) = c.on {
694 users[e].push((f, sense));
695 }
696 }
697 }
698 let n = spec.faces.len();
699 let mut turned: Vec<Option<bool>> = vec![None; n];
700 turned[0] = Some(false);
701 flood([0], Take::Newest, |&f| {
702 let flip = turned[f].expect("visited");
703 let mut found = Vec::new();
704 for (e, at) in users.iter().enumerate() {
705 let [(a, sa), (b, sb)] = at.as_slice() else {
706 continue;
707 };
708 let (g, same) = if *a == f {
709 (*b, sa == sb)
710 } else if *b == f {
711 (*a, sa == sb)
712 } else {
713 continue;
714 };
715 let want = flip ^ same;
717 match turned[g] {
718 None => {
719 turned[g] = Some(want);
720 found.push(g);
721 }
722 Some(have) if have != want => {
723 return Err(GeopError::new(format!(
724 "faces {} and {} cannot be turned to wind the same way along their shared edge {}: the sheets make a one-sided surface",
725 names.faces[f], names.faces[g], names.edges[e]
726 )));
727 }
728 Some(_) => {}
729 }
730 }
731 Ok(found)
732 })?;
733 if let Some(f) = turned.iter().position(Option::is_none) {
734 return Err(GeopError::new(format!(
735 "face {} meets none of the others along an edge: the sheets do not hang together",
736 names.faces[f]
737 )));
738 }
739 for (f, t) in turned.into_iter().enumerate() {
740 if t == Some(true) {
741 spec.faces[f] = spec.faces[f].reversed();
742 }
743 }
744 Ok(())
745}
746
747#[cfg(test)]
748mod tests;