1use std::collections::HashMap;
14
15use serde::{Deserialize, Serialize};
16
17use geop_core_geometry::{contains::surface::boundary_curve, nurb_surface::NurbSurface3D};
18use geop_core_math::{
19 geop_error::{GeopError, GeopResult},
20 primitives::Pose,
21 scalars::Scalar,
22 vector::{Vector2, Vector3},
23};
24
25use crate::{
26 Body, Coedge, CoedgeGeometry, CoedgeId, Curve2, Curve3, Edge, EdgeId, Face, FaceId, Model,
27 Sense, Shell, ShellId, Solid, SolidId, Vertex, VertexId, Wire, WireId, boundary::BoundaryType,
28};
29
30#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
35pub struct BodySpec {
36 #[serde(with = "geop_core_math::exact")]
37 pub vertices: Vec<Vector3>,
38 pub edges: Vec<EdgeSpec>,
39 pub faces: Vec<FaceSpec>,
40 pub shells: Vec<Vec<usize>>,
42 pub solid: bool,
45}
46
47#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
49pub struct EdgeSpec {
50 #[serde(with = "geop_core_math::exact")]
51 pub curve: Curve3,
52 pub start: usize,
53 pub end: usize,
54}
55
56#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
58pub struct FaceSpec {
59 #[serde(with = "geop_core_math::exact")]
60 pub surface: NurbSurface3D,
61 pub outer: Vec<CoedgeSpec>,
62 pub holes: Vec<Vec<CoedgeSpec>>,
63}
64
65#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
68pub struct CoedgeSpec {
69 pub on: CoedgeOn,
70 #[serde(with = "geop_core_math::exact")]
71 pub pcurve: Curve2,
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
77pub enum CoedgeOn {
78 Edge(usize, Sense),
79 Vertex(usize),
80}
81
82pub type FacesAround = Vec<(usize, Vector2)>;
85
86#[derive(Clone, Debug)]
89pub struct BuiltBody {
90 pub vertices: Vec<VertexId>,
91 pub edges: Vec<EdgeId>,
92 pub faces: Vec<FaceId>,
93 pub shells: Vec<ShellId>,
94 pub solid: Option<SolidId>,
95 pub wire: Option<WireId>,
97}
98
99impl BuiltBody {
100 pub fn body(&self) -> GeopResult<Body> {
103 match (self.solid, self.shells.first()) {
104 (Some(solid), _) => Ok(Body::Solid(solid)),
105 (None, Some(&shell)) => Ok(Body::Sheet(shell)),
106 (None, None) => Err(GeopError::new("built a wire, which is no body")),
107 }
108 }
109}
110
111#[derive(Clone, Debug)]
114pub struct SpecSources {
115 pub vertices: Vec<VertexId>,
116 pub edges: Vec<EdgeId>,
117 pub faces: Vec<FaceId>,
118}
119
120impl FaceSpec {
121 pub fn on_unit_square(surface: NurbSurface3D, sides: [CoedgeOn; 4]) -> GeopResult<Self> {
125 let corner = |k: usize| {
126 let (u, v) = [
127 (Scalar::ZERO, Scalar::ZERO),
128 (Scalar::ONE, Scalar::ZERO),
129 (Scalar::ONE, Scalar::ONE),
130 (Scalar::ZERO, Scalar::ONE),
131 ][k % 4];
132 Vector2::from_array([u, v])
133 };
134 let outer = sides
135 .into_iter()
136 .enumerate()
137 .map(|(k, on)| {
138 let pcurve = Curve2::line(corner(k), corner(k + 1))?;
139 Ok(CoedgeSpec { on, pcurve })
140 })
141 .collect::<GeopResult<_>>()?;
142 Ok(Self {
143 surface,
144 outer,
145 holes: Vec::new(),
146 })
147 }
148
149 pub fn loops(&self) -> Vec<&Vec<CoedgeSpec>> {
151 std::iter::once(&self.outer).chain(&self.holes).collect()
152 }
153
154 pub fn reversed(&self) -> Self {
159 let (u_lo, u_hi) = self.surface.domain_u();
160 let span = u_lo.add(u_hi);
161 let reverse = |lp: &Vec<CoedgeSpec>| {
162 lp.iter()
163 .rev()
164 .map(|c| {
165 let mut pcurve = c.pcurve.clone();
166 crate::edit::reverse_face::mirror_u(&mut pcurve, span);
167 CoedgeSpec {
168 on: match c.on {
169 CoedgeOn::Edge(e, sense) => CoedgeOn::Edge(e, sense.opposite()),
170 on @ CoedgeOn::Vertex(_) => on,
171 },
172 pcurve: pcurve.reverse(),
173 }
174 })
175 .collect()
176 };
177 Self {
178 surface: self.surface.reverse_u(),
179 outer: reverse(&self.outer),
180 holes: self.holes.iter().map(reverse).collect(),
181 }
182 }
183}
184
185impl BodySpec {
186 pub fn new(solid: bool) -> Self {
189 Self {
190 vertices: Vec::new(),
191 edges: Vec::new(),
192 faces: Vec::new(),
193 shells: Vec::new(),
194 solid,
195 }
196 }
197
198 pub fn sheet_on(surface: NurbSurface3D) -> GeopResult<Self> {
204 let ((u0, u1), (v0, v1)) = (surface.domain_u(), surface.domain_v());
205 let corners = [(u0, v0), (u1, v0), (u1, v1), (u0, v1)];
206 let mut spec = BodySpec::new(false);
207 for &(u, v) in &corners {
208 spec.vertices.push(surface.evaluate(u, v)?);
209 }
210 let sides = [
214 (false, true, false),
215 (true, false, false),
216 (false, false, true),
217 (true, true, true),
218 ];
219 let mut outer = Vec::new();
220 for (k, &(u_fixed, first, reversed)) in sides.iter().enumerate() {
221 let (from, to) = (k, (k + 1) % 4);
222 if spec.vertices[from].could_be_equal(&spec.vertices[to]) {
223 return Err(GeopError::new(format!(
224 "a sheet on a surface whose side {k} collapses to a point, {:?}, is not built",
225 spec.vertices[from]
226 )));
227 }
228 let curve = boundary_curve(&surface, u_fixed, first).ok_or_else(|| {
229 GeopError::new(format!(
230 "a sheet on a surface not clamped at its side {k} is not built"
231 ))
232 })?;
233 let (start, end) = if reversed { (to, from) } else { (from, to) };
234 spec.edges.push(EdgeSpec { curve, start, end });
235 let corner = |i: usize| Vector2::from_array([corners[i].0, corners[i].1]);
236 outer.push(CoedgeSpec {
237 on: CoedgeOn::Edge(
238 k,
239 if reversed {
240 Sense::Reversed
241 } else {
242 Sense::Forward
243 },
244 ),
245 pcurve: Curve2::line(corner(from), corner(to))?,
246 });
247 }
248 spec.faces.push(FaceSpec {
249 surface,
250 outer,
251 holes: Vec::new(),
252 });
253 spec.shells = vec![vec![0]];
254 Ok(spec)
255 }
256
257 pub fn square_face(
263 &self,
264 surface: NurbSurface3D,
265 corners: [usize; 4],
266 sides: [CoedgeOn; 4],
267 ) -> GeopResult<FaceSpec> {
268 let mut oriented = sides;
269 for (k, on) in oriented.iter_mut().enumerate() {
270 if let CoedgeOn::Edge(e, _) = *on {
271 let edge = &self.edges[e];
272 let (from, to) = (corners[k], corners[(k + 1) % 4]);
273 let sense = if (edge.start, edge.end) == (from, to) {
274 Sense::Forward
275 } else if (edge.start, edge.end) == (to, from) {
276 Sense::Reversed
277 } else {
278 return Err(GeopError::new(format!(
279 "a face's side runs from vertex {from} to {to} along edge {e}, between {} and {}",
280 edge.start, edge.end
281 )));
282 };
283 *on = CoedgeOn::Edge(e, sense);
284 }
285 }
286 FaceSpec::on_unit_square(surface, oriented)
287 }
288
289 pub fn placed(&self, pose: &Pose) -> Self {
293 Self {
294 vertices: self.vertices.iter().map(|p| pose.apply(p)).collect(),
295 edges: self
296 .edges
297 .iter()
298 .map(|e| EdgeSpec {
299 curve: e.curve.transform(&pose.motion()),
300 ..e.clone()
301 })
302 .collect(),
303 faces: self
304 .faces
305 .iter()
306 .map(|f| FaceSpec {
307 surface: f.surface.transform(&pose.motion()),
308 ..f.clone()
309 })
310 .collect(),
311 shells: self.shells.clone(),
312 solid: self.solid,
313 }
314 }
315
316 pub fn split_vertex(
322 &mut self,
323 v: usize,
324 moved: &[(usize, bool)],
325 point: Vector3,
326 curve: Curve3,
327 mut pcurve: impl FnMut(usize, Sense) -> GeopResult<Curve2>,
328 ) -> GeopResult<(usize, usize)> {
329 let w = self.vertices.len();
330 self.vertices.push(point);
331 for &(e, at_end) in moved {
332 let edge = (self.edges.get_mut(e))
333 .ok_or_else(|| GeopError::new(format!("BodySpec: no edge {e}")))?;
334 let end = if at_end {
335 &mut edge.end
336 } else {
337 &mut edge.start
338 };
339 if *end != v {
340 return Err(GeopError::new(format!(
341 "BodySpec: edge {e} does not {} at vertex {v}",
342 if at_end { "end" } else { "start" }
343 )));
344 }
345 *end = w;
346 }
347 let new = self.edges.len();
348 self.edges.push(EdgeSpec {
349 curve,
350 start: v,
351 end: w,
352 });
353 for f in 0..self.faces.len() {
354 let face = &self.faces[f];
355 let mut loops = Vec::with_capacity(1 + face.holes.len());
356 for lp in face.loops() {
357 let mut out = Vec::with_capacity(lp.len() + 1);
358 for (k, c) in lp.iter().enumerate() {
359 out.push(c.clone());
360 let (_, end) = self.ends(c.on)?;
361 let (start, _) = self.ends(lp[(k + 1) % lp.len()].on)?;
362 let sense = match (end, start) {
363 (a, b) if a == b => continue,
364 (a, b) if (a, b) == (v, w) => Sense::Forward,
365 (a, b) if (a, b) == (w, v) => Sense::Reversed,
366 (a, b) => {
367 return Err(GeopError::new(format!(
368 "BodySpec: a loop of face {f} runs to vertex {a} and on from vertex {b}"
369 )));
370 }
371 };
372 out.push(CoedgeSpec {
373 on: CoedgeOn::Edge(new, sense),
374 pcurve: pcurve(f, sense)?,
375 });
376 }
377 loops.push(out);
378 }
379 let mut loops = loops.into_iter();
380 let face = &mut self.faces[f];
381 face.outer = loops.next().unwrap_or_default();
382 face.holes = loops.collect();
383 }
384 Ok((w, new))
385 }
386
387 pub fn ends(&self, on: CoedgeOn) -> GeopResult<(usize, usize)> {
390 match on {
391 CoedgeOn::Edge(e, sense) => {
392 let edge = self
393 .edges
394 .get(e)
395 .ok_or_else(|| GeopError::new(format!("BodySpec: no edge {e}")))?;
396 Ok(match sense {
397 Sense::Forward => (edge.start, edge.end),
398 Sense::Reversed => (edge.end, edge.start),
399 })
400 }
401 CoedgeOn::Vertex(v) => Ok((v, v)),
402 }
403 }
404
405 pub fn edge_uses(&self) -> Vec<Vec<(usize, Sense, &Curve2)>> {
408 let mut uses = vec![Vec::new(); self.edges.len()];
409 for (f, face) in self.faces.iter().enumerate() {
410 for lp in face.loops() {
411 for c in lp {
412 if let CoedgeOn::Edge(e, sense) = c.on {
413 uses[e].push((f, sense, &c.pcurve));
414 }
415 }
416 }
417 }
418 uses
419 }
420
421 pub fn vertex_faces(&self) -> GeopResult<Vec<FacesAround>> {
425 let mut around: Vec<FacesAround> = vec![Vec::new(); self.vertices.len()];
426 for (f, face) in self.faces.iter().enumerate() {
427 for lp in face.loops() {
428 for c in lp {
429 let (_, end) = self.ends(c.on)?;
430 if !around[end].iter().any(|&(g, _)| g == f) {
431 around[end].push((f, c.pcurve.ends()?.1));
432 }
433 }
434 }
435 }
436 Ok(around)
437 }
438
439 pub fn is_wire(&self) -> bool {
441 self.faces.is_empty() && !self.vertices.is_empty()
442 }
443
444 pub fn check(&self) -> GeopResult<()> {
452 let fail = |what: String| Err(GeopError::new(format!("BodySpec: {what}")));
453 for (e, edge) in self.edges.iter().enumerate() {
454 if edge.start >= self.vertices.len() || edge.end >= self.vertices.len() {
455 return fail(format!("edge {e} runs between vertices that do not exist"));
456 }
457 }
458 if self.is_wire() {
459 if self.solid || !self.shells.is_empty() {
460 return fail("a solid or a shell without faces".to_string());
461 }
462 return Ok(());
463 }
464 let mut vertex_used = vec![false; self.vertices.len()];
465 for edge in &self.edges {
466 vertex_used[edge.start] = true;
467 vertex_used[edge.end] = true;
468 }
469 for (f, face) in self.faces.iter().enumerate() {
470 for (l, lp) in face.loops().into_iter().enumerate() {
471 if lp.is_empty() {
472 return fail(format!("loop {l} of face {f} is empty"));
473 }
474 for (k, coedge) in lp.iter().enumerate() {
475 let (_, end) = self.ends(coedge.on)?;
476 let (start, _) = self.ends(lp[(k + 1) % lp.len()].on)?;
477 if end != start {
478 return fail(format!(
479 "loop {l} of face {f}: coedge {k} ends at vertex {end}, but the next one starts at vertex {start}"
480 ));
481 }
482 if let CoedgeOn::Vertex(v) = coedge.on {
483 match vertex_used.get_mut(v) {
484 Some(used) => *used = true,
485 None => {
486 return fail(format!(
487 "face {f} sits at vertex {v}, which does not exist"
488 ));
489 }
490 }
491 }
492 }
493 }
494 }
495 for (e, uses) in self.edge_uses().iter().enumerate() {
497 let senses: Vec<Sense> = uses.iter().map(|&(_, sense, _)| sense).collect();
498 match senses[..] {
499 [] => return fail(format!("edge {e} is used by no face")),
500 [_] if self.solid => {
501 return fail(format!(
502 "edge {e} is used by one face only, so the solid is open"
503 ));
504 }
505 [_] => {}
506 [a, b] if a != b => {}
507 [_, _] => return fail(format!("edge {e} is used twice in the same sense")),
508 _ if senses.len().is_multiple_of(2)
511 && 2 * senses.iter().filter(|&&s| s == Sense::Forward).count()
512 == senses.len() => {}
513 _ => {
514 return fail(format!(
515 "edge {e} is used {} times, not as often one way as the other",
516 uses.len()
517 ));
518 }
519 }
520 }
521 if let Some(v) = vertex_used.iter().position(|used| !used) {
522 return fail(format!("vertex {v} is used by nothing"));
523 }
524 let mut shell_of = vec![None; self.faces.len()];
525 for (s, faces) in self.shells.iter().enumerate() {
526 for &f in faces {
527 match shell_of.get_mut(f) {
528 None => return fail(format!("shell {s} lists face {f}, which does not exist")),
529 Some(Some(other)) => {
530 return fail(format!("face {f} is in both shell {other} and shell {s}"));
531 }
532 Some(slot) => *slot = Some(s),
533 }
534 }
535 }
536 if let Some(f) = shell_of.iter().position(Option::is_none) {
537 return fail(format!("face {f} is in no shell"));
538 }
539 Ok(())
540 }
541}
542
543impl Model {
544 pub fn build_body(&mut self, spec: BodySpec) -> GeopResult<BuiltBody> {
548 spec.check()?;
549 let BodySpec {
550 vertices,
551 edges,
552 faces,
553 shells,
554 solid,
555 } = spec;
556 let vertices: Vec<VertexId> = vertices
557 .into_iter()
558 .map(|point| self.insert_vertex(Vertex { point }))
559 .collect();
560 let edges: Vec<EdgeId> = edges
561 .into_iter()
562 .map(|e| {
563 self.insert_edge(Edge {
564 curve: e.curve,
565 start_vertex: vertices[e.start],
566 end_vertex: vertices[e.end],
567 })
568 })
569 .collect();
570 let shell_ids: Vec<ShellId> = shells
571 .iter()
572 .map(|faces| {
573 self.insert_shell(Shell {
574 faces: Vec::with_capacity(faces.len()),
575 solid: None,
576 })
577 })
578 .collect();
579 let mut shell_of = vec![ShellId(0); faces.len()];
580 for (s, members) in shells.iter().enumerate() {
581 for &f in members {
582 shell_of[f] = shell_ids[s];
583 }
584 }
585
586 let mut face_ids = Vec::with_capacity(faces.len());
587 for (f, face) in faces.into_iter().enumerate() {
588 let face_id = self.insert_face(Face {
589 surface: face.surface,
590 outer: BoundaryType::Vertex(vertices[0]),
592 holes: Vec::new(),
593 shell: shell_of[f],
594 });
595 let insert_loop = |model: &mut Self, lp: Vec<CoedgeSpec>| {
596 let ids: Vec<CoedgeId> = lp
597 .into_iter()
598 .map(|c| {
599 let (geometry, sense) = match c.on {
600 CoedgeOn::Edge(e, sense) => (CoedgeGeometry::Edge(edges[e]), sense),
601 CoedgeOn::Vertex(v) => {
602 (CoedgeGeometry::Vertex(vertices[v]), Sense::Forward)
603 }
604 };
605 model.insert_coedge(Coedge {
606 geometry,
607 sense,
608 pcurve: c.pcurve,
609 next: CoedgeId(0),
611 prev: CoedgeId(0),
612 face: face_id,
613 })
614 })
615 .collect();
616 let n = ids.len();
617 for (k, &id) in ids.iter().enumerate() {
618 let coedge = model.coedges.get_mut(&id).expect("just inserted");
619 coedge.next = ids[(k + 1) % n];
620 coedge.prev = ids[(k + n - 1) % n];
621 }
622 BoundaryType::Loop(ids[0])
623 };
624 let outer = insert_loop(self, face.outer);
625 let holes = face
626 .holes
627 .into_iter()
628 .map(|h| insert_loop(self, h))
629 .collect();
630 let built = self.faces.get_mut(&face_id).expect("just inserted");
631 built.outer = outer;
632 built.holes = holes;
633 self.get_shell_mut(shell_of[f])?.faces.push(face_id);
634 face_ids.push(face_id);
635 }
636
637 let solid = solid.then(|| {
638 self.insert_solid(Solid {
639 shells: shell_ids.clone(),
640 })
641 });
642 for &shell in &shell_ids {
643 self.get_shell_mut(shell)?.solid = solid;
644 }
645 let wire = (face_ids.is_empty() && !vertices.is_empty()).then(|| {
646 self.insert_wire(Wire {
647 vertices: vertices.clone(),
648 edges: edges.clone(),
649 })
650 });
651 Ok(BuiltBody {
652 vertices,
653 edges,
654 faces: face_ids,
655 shells: shell_ids,
656 solid,
657 wire,
658 })
659 }
660
661 pub fn body_spec(&self, faces: &[FaceId], solid: bool) -> GeopResult<(BodySpec, SpecSources)> {
666 let mut vertex_index: HashMap<VertexId, usize> = HashMap::new();
667 let mut edge_index: HashMap<EdgeId, usize> = HashMap::new();
668 let mut sources = SpecSources {
669 vertices: Vec::new(),
670 edges: Vec::new(),
671 faces: faces.to_vec(),
672 };
673 let mut spec = BodySpec {
674 vertices: Vec::new(),
675 edges: Vec::new(),
676 faces: Vec::with_capacity(faces.len()),
677 shells: vec![(0..faces.len()).collect()],
678 solid,
679 };
680 let mut vertex = |spec: &mut BodySpec, sources: &mut SpecSources, id: VertexId| {
681 *vertex_index.entry(id).or_insert_with(|| {
682 spec.vertices.push(self.vertices[&id].point);
683 sources.vertices.push(id);
684 spec.vertices.len() - 1
685 })
686 };
687 for &face_id in faces {
688 let face = self.get_face(face_id)?;
689 let mut loops = Vec::new();
690 for boundary in face.boundaries() {
691 let BoundaryType::Loop(anchor) = boundary else {
692 return Err(GeopError::new(format!(
693 "Model::body_spec: face {face_id} is bounded by a bare vertex"
694 )));
695 };
696 let mut lp = Vec::new();
697 for coedge_id in self.iterate_loop_coedges(anchor) {
698 let coedge = self.get_coedge(coedge_id)?;
699 let on = match coedge.geometry {
700 CoedgeGeometry::Edge(e) => {
701 let index = match edge_index.get(&e) {
702 Some(&index) => index,
703 None => {
704 let edge = self.get_edge(e)?;
705 let start = vertex(&mut spec, &mut sources, edge.start_vertex);
706 let end = vertex(&mut spec, &mut sources, edge.end_vertex);
707 spec.edges.push(EdgeSpec {
708 curve: edge.curve.clone(),
709 start,
710 end,
711 });
712 sources.edges.push(e);
713 edge_index.insert(e, spec.edges.len() - 1);
714 spec.edges.len() - 1
715 }
716 };
717 CoedgeOn::Edge(index, coedge.sense)
718 }
719 CoedgeGeometry::Vertex(v) => {
720 CoedgeOn::Vertex(vertex(&mut spec, &mut sources, v))
721 }
722 };
723 lp.push(CoedgeSpec {
724 on,
725 pcurve: coedge.pcurve.clone(),
726 });
727 }
728 loops.push(lp);
729 }
730 let outer = loops.remove(0);
731 spec.faces.push(FaceSpec {
732 surface: face.surface.clone(),
733 outer,
734 holes: loops,
735 });
736 }
737 Ok((spec, sources))
738 }
739}
740
741#[cfg(test)]
742mod tests {
743 use geop_core_math::{scalars::Scalar, vector::Vector3};
744
745 use super::{BodySpec, EdgeSpec};
746 use crate::{
747 Body, CoedgeGeometry, Model,
748 test_fixtures::test_cube_solid,
749 validation::{ValidationParameters, validate, validate_manifold},
750 };
751
752 fn point(p: [f64; 3]) -> Vector3 {
753 Vector3::from_array(p.map(Scalar::from_f64))
754 }
755
756 fn line(a: [f64; 3], b: [f64; 3]) -> crate::Curve3 {
757 let p = |p: [f64; 3]| Vector3::from_array(p.map(Scalar::from_f64));
758 crate::Curve3::line(p(a), p(b)).unwrap()
759 }
760
761 fn wire_spec() -> BodySpec {
764 let corners = [[2.0, 0.0, 0.0], [3.0, 0.0, 0.0], [2.0, 1.0, 0.0]];
765 BodySpec {
766 vertices: corners
767 .iter()
768 .map(|&p| point(p))
769 .chain([point([2.0, 0.0, 1.0])])
770 .collect(),
771 edges: vec![
772 EdgeSpec {
773 curve: line(corners[0], corners[1]),
774 start: 0,
775 end: 1,
776 },
777 EdgeSpec {
778 curve: line(corners[0], corners[2]),
779 start: 0,
780 end: 2,
781 },
782 ],
783 faces: Vec::new(),
784 shells: Vec::new(),
785 solid: false,
786 }
787 }
788
789 #[test]
793 fn a_spec_without_faces_builds_a_wire() {
794 let mut model = Model::new();
795 test_cube_solid(&mut model);
796 let built = model.build_body(wire_spec()).unwrap();
797 let wire = built.wire.expect("a wire");
798 assert_eq!(built.solid, None);
799 let built_wire = &model.wires[&wire];
800 assert_eq!(built_wire.vertices.len(), 4);
801 assert_eq!(built_wire.edges.len(), 2);
802 assert!(built_wire.vertices.contains(&built.vertices[3]));
803 assert_eq!(model.wire_of_edge(built.edges[1]), Some(wire));
804 let params = ValidationParameters::default();
805 validate(¶ms, &model).unwrap();
806 validate_manifold(¶ms, &model).unwrap();
807 }
808
809 #[test]
812 fn a_wire_outlives_a_deleted_solid() {
813 let mut model = Model::new();
814 let solid = test_cube_solid(&mut model);
815 model.build_body(wire_spec()).unwrap();
816 model.assemble_solid(&[Body::Solid(solid)], &[]).unwrap();
817 assert_eq!(model.vertices.len(), 4);
818 assert_eq!(model.edges.len(), 2);
819 validate(&ValidationParameters::default(), &model).unwrap();
820 }
821
822 #[test]
824 fn a_face_on_a_wire_edge_is_invalid() {
825 let mut model = Model::new();
826 test_cube_solid(&mut model);
827 let built = model.build_body(wire_spec()).unwrap();
828 let first = *model.coedges.keys().next().unwrap();
829 let coedge = model.coedges.get_mut(&first).unwrap();
830 coedge.geometry = CoedgeGeometry::Edge(built.edges[0]);
831 let errors = validate(&ValidationParameters::default(), &model).unwrap_err();
832 assert!(
833 errors
834 .iter()
835 .any(|e| format!("{e:?}").contains("which bounds no face")),
836 "{errors:?}"
837 );
838 }
839
840 #[test]
843 fn crossing_wire_edges_are_invalid() {
844 let mut model = Model::new();
845 let mut spec = wire_spec();
846 spec.vertices.push(point([2.5, -1.0, 0.0]));
847 spec.vertices.push(point([2.5, 1.0, 0.0]));
848 spec.edges.push(EdgeSpec {
849 curve: line([2.5, -1.0, 0.0], [2.5, 1.0, 0.0]),
850 start: 4,
851 end: 5,
852 });
853 model.build_body(spec).unwrap();
854 assert!(validate(&ValidationParameters::default(), &model).is_err());
855 }
856}