Another project
1use std::collections::{HashMap, HashSet};
2
3use bone_types::{
4 EdgeLabel, EdgeRole, FaceLabel, FaceRole, FeatureId, LoopIndex, Parameter, Plane3, Point2,
5 PositiveLength, SideKind, SketchEntityId, Tolerance, VertexLabel, VertexRole,
6};
7use core::f64::consts::TAU;
8use truck_modeling::{Edge, EdgeID, FaceID, Solid, Vector3, Vertex, VertexID, Wire, builder};
9use uom::si::length::millimeter;
10
11use crate::curve2::{Curve2, Curve2Kind};
12use crate::extrude::{ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense};
13use crate::intersect::{IntersectionSet, intersect_curves};
14
15use super::build::{SolidLabeling, assemble};
16use super::edges::{EdgeCurve3, lift_curve2};
17use super::profile::{ExtrudeProfile, ProfileLoop};
18use super::{BrepError, BrepSolid, ProfileDefect, TruckGap};
19
20const PROFILE_TOLERANCE_MM: f64 = 1.0e-6;
21const PROFILE_TOLERANCE: Tolerance = Tolerance::new(PROFILE_TOLERANCE_MM);
22const AREA_TOLERANCE_MM2: f64 = 1.0e-9;
23const EXTRUDE_MIN_DEPTH_MM: f64 = 1.0e-6;
24const MAX_ARC_SEGMENTS: u32 = 64;
25const MAX_SUBARC_RAD: f64 = TAU / 3.0;
26const AREA_SAMPLES: u32 = 32;
27
28pub fn evaluate_extrude(
29 extrude: FeatureId,
30 profile: &ExtrudeProfile,
31 feature: &ExtrudeFeature,
32) -> Result<BrepSolid, BrepError> {
33 let plan = sweep_plan(feature)?;
34 let resolved = resolve_profile(profile)?;
35 let parent_curves = parent_curves(profile);
36 let (solid, labeling) = build_solid(
37 profile.plane(),
38 &resolved,
39 plan,
40 extrude,
41 closed_curves(profile),
42 &parent_curves,
43 )?;
44 assemble(solid, &labeling)
45}
46
47fn parent_curves(profile: &ExtrudeProfile) -> HashMap<SketchEntityId, Curve2Kind> {
48 profile
49 .loops()
50 .iter()
51 .flat_map(|profile_loop| match profile_loop {
52 ProfileLoop::Closed {
53 curve,
54 curve_entity,
55 } => vec![(*curve_entity, *curve)],
56 ProfileLoop::Open(edges) => edges
57 .iter()
58 .map(|edge| (edge.curve_entity(), edge.curve()))
59 .collect(),
60 })
61 .collect()
62}
63
64fn closed_curves(profile: &ExtrudeProfile) -> HashSet<SketchEntityId> {
65 profile
66 .loops()
67 .iter()
68 .filter_map(|profile_loop| match profile_loop {
69 ProfileLoop::Closed { curve_entity, .. } => Some(*curve_entity),
70 ProfileLoop::Open(_) => None,
71 })
72 .collect()
73}
74
75fn invalid(reason: ProfileDefect) -> BrepError {
76 BrepError::InvalidProfile { reason }
77}
78
79#[derive(Copy, Clone)]
80struct SweepPlan {
81 depth_mm: f64,
82 base_offset_mm: f64,
83}
84
85fn sweep_plan(feature: &ExtrudeFeature) -> Result<SweepPlan, BrepError> {
86 if feature.draft.is_some() {
87 return Err(unsupported(TruckGap::Draft));
88 }
89 if feature.thin_wall.is_some() {
90 return Err(unsupported(TruckGap::ThinWall));
91 }
92 match feature.direction {
93 ExtrudeDirection::Normal {
94 sense: ExtrudeSense::Forward,
95 } => {}
96 ExtrudeDirection::Normal {
97 sense: ExtrudeSense::Reverse,
98 } => return Err(unsupported(TruckGap::ReverseNormal)),
99 ExtrudeDirection::AlongAxis(_) => return Err(unsupported(TruckGap::AxisDirection)),
100 ExtrudeDirection::BetweenReferences { .. } => {
101 return Err(unsupported(TruckGap::ReferenceDirection));
102 }
103 }
104 match feature.end_condition {
105 ExtrudeEndCondition::Blind { depth } => Ok(SweepPlan {
106 depth_mm: depth_mm(depth)?,
107 base_offset_mm: 0.0,
108 }),
109 ExtrudeEndCondition::MidPlane { depth } => {
110 let total = depth_mm(depth)?;
111 Ok(SweepPlan {
112 depth_mm: total,
113 base_offset_mm: -total / 2.0,
114 })
115 }
116 ExtrudeEndCondition::ThroughAll => Err(unsupported(TruckGap::ThroughAll)),
117 ExtrudeEndCondition::UpToNext => Err(unsupported(TruckGap::UpToNext)),
118 ExtrudeEndCondition::UpToVertex { .. } => Err(unsupported(TruckGap::UpToVertex)),
119 ExtrudeEndCondition::UpToSurface { .. } => Err(unsupported(TruckGap::UpToSurface)),
120 ExtrudeEndCondition::OffsetFromSurface { .. } => {
121 Err(unsupported(TruckGap::OffsetFromSurface))
122 }
123 ExtrudeEndCondition::UpToBody { .. } => Err(unsupported(TruckGap::UpToBody)),
124 }
125}
126
127fn unsupported(detail: TruckGap) -> BrepError {
128 BrepError::TruckUnsupported { detail }
129}
130
131fn depth_mm(depth: PositiveLength) -> Result<f64, BrepError> {
132 let value = depth.get().get::<millimeter>();
133 if value <= EXTRUDE_MIN_DEPTH_MM {
134 Err(BrepError::EmptyExtrudeDepth)
135 } else {
136 Ok(value)
137 }
138}
139
140#[derive(Copy, Clone)]
141struct Segment {
142 curve: Curve2Kind,
143 curve_entity: SketchEntityId,
144 corner: Option<SketchEntityId>,
145}
146
147struct LoopInfo {
148 segments: Vec<Segment>,
149 polygon: Vec<(f64, f64)>,
150 area: f64,
151}
152
153struct ResolvedLoop {
154 segments: Vec<Segment>,
155 reversed: bool,
156 loop_index: LoopIndex,
157}
158
159fn resolve_profile(profile: &ExtrudeProfile) -> Result<Vec<ResolvedLoop>, BrepError> {
160 let loops = profile.loops();
161 if loops.is_empty() {
162 return Err(invalid(ProfileDefect::ZeroArea));
163 }
164 let infos: Vec<LoopInfo> = loops.iter().map(analyze_loop).collect::<Result<_, _>>()?;
165
166 let outer = (0..infos.len())
167 .max_by(|&a, &b| {
168 infos[a]
169 .area
170 .abs()
171 .total_cmp(&infos[b].area.abs())
172 .then_with(|| min_entity(&infos[b].segments).cmp(&min_entity(&infos[a].segments)))
173 })
174 .unwrap_or(0);
175
176 validate_arrangement(&infos, outer)?;
177
178 let mut holes: Vec<usize> = (0..infos.len()).filter(|&i| i != outer).collect();
179 holes.sort_by_key(|&i| min_entity(&infos[i].segments));
180
181 let outer_resolved = ResolvedLoop {
182 segments: infos[outer].segments.clone(),
183 reversed: infos[outer].area < 0.0,
184 loop_index: LoopIndex::OUTER,
185 };
186 let hole_resolved = holes.iter().enumerate().map(|(rank, &index)| ResolvedLoop {
187 segments: infos[index].segments.clone(),
188 reversed: infos[index].area > 0.0,
189 loop_index: LoopIndex::new(u16::try_from(rank + 1).unwrap_or(u16::MAX)),
190 });
191 Ok(core::iter::once(outer_resolved)
192 .chain(hole_resolved)
193 .collect())
194}
195
196fn analyze_loop(profile_loop: &ProfileLoop) -> Result<LoopInfo, BrepError> {
197 let segments = match profile_loop {
198 ProfileLoop::Closed {
199 curve,
200 curve_entity,
201 } => {
202 if !curve_closed(*curve) {
203 return Err(invalid(ProfileDefect::OpenLoop));
204 }
205 vec![Segment {
206 curve: *curve,
207 curve_entity: *curve_entity,
208 corner: None,
209 }]
210 }
211 ProfileLoop::Open(edges) => {
212 if edges.is_empty() || edges.iter().any(|edge| curve_closed(edge.curve())) {
213 return Err(invalid(ProfileDefect::OpenLoop));
214 }
215 let segments: Vec<Segment> = edges
216 .iter()
217 .map(|edge| Segment {
218 curve: edge.curve(),
219 curve_entity: edge.curve_entity(),
220 corner: Some(edge.corner()),
221 })
222 .collect();
223 if !loop_closed(&segments) {
224 return Err(invalid(ProfileDefect::OpenLoop));
225 }
226 if self_intersects(&segments) {
227 return Err(invalid(ProfileDefect::SelfIntersectingLoop));
228 }
229 segments
230 }
231 };
232 let polygon = loop_polygon(&segments);
233 let area = signed_area(&polygon);
234 if area.abs() < AREA_TOLERANCE_MM2 {
235 return Err(invalid(ProfileDefect::ZeroArea));
236 }
237 Ok(LoopInfo {
238 segments,
239 polygon,
240 area,
241 })
242}
243
244fn validate_arrangement(infos: &[LoopInfo], outer: usize) -> Result<(), BrepError> {
245 let outer_info = &infos[outer];
246 let holes: Vec<usize> = (0..infos.len()).filter(|&i| i != outer).collect();
247
248 holes.iter().try_for_each(|&i| {
249 let contained = !loops_cross(&infos[i].segments, &outer_info.segments)
250 && any_point_inside(&infos[i], outer_info);
251 contained
252 .then_some(())
253 .ok_or(invalid(ProfileDefect::UncontainedLoop))
254 })?;
255
256 holes.iter().enumerate().try_for_each(|(rank, &a)| {
257 holes[rank + 1..].iter().try_for_each(|&b| {
258 let overlaps = loops_cross(&infos[a].segments, &infos[b].segments)
259 || any_point_inside(&infos[a], &infos[b])
260 || any_point_inside(&infos[b], &infos[a]);
261 (!overlaps)
262 .then_some(())
263 .ok_or(invalid(ProfileDefect::OverlappingLoops))
264 })
265 })
266}
267
268fn any_point_inside(inner: &LoopInfo, container: &LoopInfo) -> bool {
269 inner
270 .polygon
271 .iter()
272 .any(|&point| point_in_polygon(point, &container.polygon))
273}
274
275fn loops_cross(a: &[Segment], b: &[Segment]) -> bool {
276 a.iter().any(|sa| {
277 b.iter().any(|sb| {
278 !matches!(
279 intersect_curves(&sa.curve, &sb.curve, PROFILE_TOLERANCE),
280 IntersectionSet::Empty
281 )
282 })
283 })
284}
285
286fn self_intersects(segments: &[Segment]) -> bool {
287 let count = segments.len();
288 (0..count).any(|i| {
289 let last = if i == 0 { count - 1 } else { count };
290 ((i + 2)..last).any(|j| {
291 !matches!(
292 intersect_curves(&segments[i].curve, &segments[j].curve, PROFILE_TOLERANCE),
293 IntersectionSet::Empty
294 )
295 })
296 })
297}
298
299fn curve_closed(curve: Curve2Kind) -> bool {
300 let start = curve.evaluate(Parameter::new(0.0));
301 let end = curve.evaluate(Parameter::new(1.0));
302 (end - start).norm_mm() < PROFILE_TOLERANCE_MM
303}
304
305fn min_entity(segments: &[Segment]) -> Option<SketchEntityId> {
306 segments.iter().map(|seg| seg.curve_entity).min()
307}
308
309fn loop_closed(segments: &[Segment]) -> bool {
310 let count = segments.len();
311 (0..count).all(|index| {
312 let end = segments[index].curve.evaluate(Parameter::new(1.0));
313 let next = segments[(index + 1) % count]
314 .curve
315 .evaluate(Parameter::new(0.0));
316 (end - next).norm_mm() < PROFILE_TOLERANCE_MM
317 })
318}
319
320fn loop_polygon(segments: &[Segment]) -> Vec<(f64, f64)> {
321 segments
322 .iter()
323 .flat_map(|seg| {
324 let curve = seg.curve;
325 let samples = samples_for(curve);
326 (0..samples).map(move |i| {
327 let t = f64::from(i) / f64::from(samples);
328 curve.evaluate(Parameter::new(t)).coords_mm()
329 })
330 })
331 .collect()
332}
333
334fn signed_area(points: &[(f64, f64)]) -> f64 {
335 let count = points.len();
336 points
337 .iter()
338 .enumerate()
339 .map(|(index, &(x0, y0))| {
340 let (x1, y1) = points[(index + 1) % count];
341 x0 * y1 - x1 * y0
342 })
343 .sum::<f64>()
344 / 2.0
345}
346
347fn point_in_polygon(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
348 let (px, py) = point;
349 let count = polygon.len();
350 (0..count).fold(false, |inside, index| {
351 let (xi, yi) = polygon[index];
352 let (xj, yj) = polygon[(index + count - 1) % count];
353 let crosses = (yi > py) != (yj > py) && px < (xj - xi) * (py - yi) / (yj - yi) + xi;
354 inside ^ crosses
355 })
356}
357
358fn samples_for(curve: Curve2Kind) -> u32 {
359 match curve {
360 Curve2Kind::Line(_) => 1,
361 Curve2Kind::Arc(_) | Curve2Kind::Circle(_) => AREA_SAMPLES,
362 }
363}
364
365struct Step {
366 start: Point2,
367 transit: Option<Point2>,
368 curve_entity: SketchEntityId,
369 vertex_entry: (SideKind, SketchEntityId),
370}
371
372fn loop_steps(segments: &[Segment], reversed: bool) -> Vec<Step> {
373 let count = segments.len();
374 let order: Vec<usize> = if reversed {
375 (0..count).rev().collect()
376 } else {
377 (0..count).collect()
378 };
379 order
380 .into_iter()
381 .flat_map(|index| {
382 let segment = segments[index];
383 let first_entry = if reversed {
384 junction_entry(segments, (index + 1) % count)
385 } else {
386 junction_entry(segments, index)
387 };
388 sub_edges(segment.curve, reversed)
389 .into_iter()
390 .enumerate()
391 .map(move |(position, sub)| Step {
392 start: sub.0,
393 transit: sub.2,
394 curve_entity: segment.curve_entity,
395 vertex_entry: if position == 0 {
396 first_entry
397 } else {
398 (SideKind::Seam, segment.curve_entity)
399 },
400 })
401 })
402 .collect()
403}
404
405fn junction_entry(segments: &[Segment], index: usize) -> (SideKind, SketchEntityId) {
406 let segment = segments[index];
407 segment
408 .corner
409 .map_or((SideKind::Seam, segment.curve_entity), |corner| {
410 (SideKind::Corner, corner)
411 })
412}
413
414fn sub_edges(curve: Curve2Kind, reversed: bool) -> Vec<(Point2, Point2, Option<Point2>)> {
415 match curve {
416 Curve2Kind::Line(line) => {
417 let (start, end) = if reversed {
418 (line.end(), line.start())
419 } else {
420 (line.start(), line.end())
421 };
422 vec![(start, end, None)]
423 }
424 Curve2Kind::Arc(arc) => sub_arcs(curve, arc.sweep_rad().abs(), reversed),
425 Curve2Kind::Circle(_) => sub_arcs(curve, TAU, reversed),
426 }
427}
428
429fn sub_arcs(
430 curve: Curve2Kind,
431 sweep_abs_rad: f64,
432 reversed: bool,
433) -> Vec<(Point2, Point2, Option<Point2>)> {
434 let segments = arc_segments(sweep_abs_rad);
435 let knots: Vec<f64> = (0..=segments)
436 .map(|i| {
437 let forward = f64::from(i) / f64::from(segments);
438 if reversed { 1.0 - forward } else { forward }
439 })
440 .collect();
441 knots
442 .windows(2)
443 .map(|pair| {
444 let (t0, t1) = (pair[0], pair[1]);
445 let start = curve.evaluate(Parameter::new(t0));
446 let end = curve.evaluate(Parameter::new(t1));
447 let transit = curve.evaluate(Parameter::new(f64::midpoint(t0, t1)));
448 (start, end, Some(transit))
449 })
450 .collect()
451}
452
453fn arc_segments(sweep_abs_rad: f64) -> u32 {
454 (1..=MAX_ARC_SEGMENTS)
455 .find(|&segments| sweep_abs_rad / f64::from(segments) <= MAX_SUBARC_RAD)
456 .unwrap_or(MAX_ARC_SEGMENTS)
457}
458
459#[derive(Default)]
460struct Bookkeeping {
461 edge_entry: HashMap<EdgeID, (LoopIndex, SketchEntityId)>,
462 vertex_entry: HashMap<VertexID, (SideKind, SketchEntityId)>,
463}
464
465fn build_solid(
466 plane: Plane3,
467 resolved: &[ResolvedLoop],
468 plan: SweepPlan,
469 extrude: FeatureId,
470 closed: HashSet<SketchEntityId>,
471 parent_curves: &HashMap<SketchEntityId, Curve2Kind>,
472) -> Result<(Solid, SolidLabeling), BrepError> {
473 let mut book = Bookkeeping::default();
474 let wires: Vec<Wire> = resolved
475 .iter()
476 .map(|resolved_loop| {
477 let steps = loop_steps(&resolved_loop.segments, resolved_loop.reversed);
478 build_loop(
479 plane,
480 plan.base_offset_mm,
481 &steps,
482 resolved_loop.loop_index,
483 &mut book,
484 )
485 })
486 .collect();
487
488 let face = builder::try_attach_plane(&wires)
489 .map_err(|_| invalid(ProfileDefect::SelfIntersectingLoop))?;
490 let (nx, ny, nz) = plane.normal().components();
491 let sweep = Vector3::new(nx * plan.depth_mm, ny * plan.depth_mm, nz * plan.depth_mm);
492 let solid = builder::tsweep(&face, sweep);
493 let labeling = label_solid(&solid, &book, extrude, closed, plane, plan, parent_curves);
494 Ok((solid, labeling))
495}
496
497fn build_loop(
498 plane: Plane3,
499 offset_mm: f64,
500 steps: &[Step],
501 loop_index: LoopIndex,
502 book: &mut Bookkeeping,
503) -> Wire {
504 let count = steps.len();
505 let vertices: Vec<Vertex> = steps
506 .iter()
507 .map(|step| builder::vertex(lift(plane, offset_mm, step.start)))
508 .collect();
509 vertices.iter().zip(steps).for_each(|(vertex, step)| {
510 book.vertex_entry.insert(vertex.id(), step.vertex_entry);
511 });
512 let edges: Vec<Edge> = (0..count)
513 .map(|index| {
514 let from = &vertices[index];
515 let to = &vertices[(index + 1) % count];
516 let step = &steps[index];
517 let edge = match step.transit {
518 None => builder::line(from, to),
519 Some(transit) => builder::circle_arc(from, to, lift(plane, offset_mm, transit)),
520 };
521 book.edge_entry
522 .insert(edge.id(), (loop_index, step.curve_entity));
523 edge
524 })
525 .collect();
526 edges.into()
527}
528
529fn lift(plane: Plane3, offset_mm: f64, point: Point2) -> truck_modeling::Point3 {
530 let (u, v) = point.coords_mm();
531 let (wx, wy, wz) = plane.point_at_local(u, v, offset_mm).coords_mm();
532 truck_modeling::Point3::new(wx, wy, wz)
533}
534
535struct Labels {
536 faces: HashMap<FaceID, FaceLabel>,
537 edges: HashMap<EdgeID, EdgeLabel>,
538 vertices: HashMap<VertexID, VertexLabel>,
539 edge_curves: HashMap<EdgeID, EdgeCurve3>,
540 feature: FeatureId,
541 closed: HashSet<SketchEntityId>,
542}
543
544impl Labels {
545 fn new(feature: FeatureId, closed: HashSet<SketchEntityId>) -> Self {
546 Self {
547 faces: HashMap::new(),
548 edges: HashMap::new(),
549 vertices: HashMap::new(),
550 edge_curves: HashMap::new(),
551 feature,
552 closed,
553 }
554 }
555
556 fn face(&mut self, id: FaceID, role: FaceRole) {
557 self.faces.insert(
558 id,
559 FaceLabel {
560 feature: self.feature,
561 role,
562 },
563 );
564 }
565
566 fn edge(&mut self, id: EdgeID, role: EdgeRole) {
567 self.edges.insert(
568 id,
569 EdgeLabel {
570 feature: self.feature,
571 role,
572 },
573 );
574 }
575
576 fn vertex(&mut self, id: VertexID, role: VertexRole) {
577 self.vertices.insert(
578 id,
579 VertexLabel {
580 feature: self.feature,
581 role,
582 },
583 );
584 }
585
586 fn edge_curve(&mut self, id: EdgeID, curve: EdgeCurve3) {
587 self.edge_curves.insert(id, curve);
588 }
589
590 fn into_labeling(self) -> SolidLabeling {
591 SolidLabeling {
592 faces: self.faces,
593 edges: self.edges,
594 vertices: self.vertices,
595 closed_curves: self.closed,
596 edge_curves: self.edge_curves,
597 }
598 }
599}
600
601#[derive(Copy, Clone)]
602struct SideFace {
603 id: FaceID,
604 profile_edge: EdgeID,
605 front: VertexID,
606 back: VertexID,
607}
608
609fn label_solid(
610 solid: &Solid,
611 book: &Bookkeeping,
612 extrude: FeatureId,
613 closed: HashSet<SketchEntityId>,
614 plane: Plane3,
615 plan: SweepPlan,
616 parent_curves: &HashMap<SketchEntityId, Curve2Kind>,
617) -> SolidLabeling {
618 let mut labels = solid
619 .boundaries()
620 .iter()
621 .flat_map(|shell| shell.face_iter())
622 .fold(Labels::new(extrude, closed), |mut labels, face| {
623 let face_edges: Vec<(EdgeID, VertexID, VertexID)> = face
624 .boundaries()
625 .iter()
626 .flat_map(|wire| {
627 wire.edge_iter()
628 .map(|edge| (edge.id(), edge.front().id(), edge.back().id()))
629 })
630 .collect();
631 let profile: Vec<(EdgeID, VertexID, VertexID)> = face_edges
632 .iter()
633 .copied()
634 .filter(|(edge_id, _, _)| book.edge_entry.contains_key(edge_id))
635 .collect();
636
637 match profile.first() {
638 None => labels.face(face.id(), FaceRole::EndCap),
639 Some(_) if profile.len() == face_edges.len() => {
640 labels.face(face.id(), FaceRole::StartCap);
641 }
642 Some(&(profile_edge, front, back)) => label_side_face(
643 &mut labels,
644 SideFace {
645 id: face.id(),
646 profile_edge,
647 front,
648 back,
649 },
650 &face_edges,
651 book,
652 ),
653 }
654 labels
655 });
656 record_cap_curves(&mut labels, plane, plan, parent_curves);
657 labels.into_labeling()
658}
659
660fn record_cap_curves(
661 labels: &mut Labels,
662 plane: Plane3,
663 plan: SweepPlan,
664 parent_curves: &HashMap<SketchEntityId, Curve2Kind>,
665) {
666 let entries: Vec<(EdgeID, EdgeRole)> = labels
667 .edges
668 .iter()
669 .map(|(id, label)| (*id, label.role))
670 .collect();
671 entries.into_iter().for_each(|(edge_id, role)| {
672 let (offset_mm, curve_entity) = match role {
673 EdgeRole::StartCapEdge { from } => (plan.base_offset_mm, from),
674 EdgeRole::EndCapEdge { from } => (plan.base_offset_mm + plan.depth_mm, from),
675 _ => return,
676 };
677 let Some(parent) = parent_curves.get(&curve_entity) else {
678 return;
679 };
680 let Some(lifted) = lift_curve2(*parent, plane, offset_mm, PROFILE_TOLERANCE) else {
681 return;
682 };
683 labels.edge_curve(edge_id, lifted);
684 });
685}
686
687fn label_side_face(
688 labels: &mut Labels,
689 side: SideFace,
690 face_edges: &[(EdgeID, VertexID, VertexID)],
691 book: &Bookkeeping,
692) {
693 let (loop_index, curve_entity) = book.edge_entry[&side.profile_edge];
694 let (front_kind, front_entity) = book.vertex_entry[&side.front];
695 let (back_kind, back_entity) = book.vertex_entry[&side.back];
696
697 labels.face(
698 side.id,
699 FaceRole::Side {
700 loop_index,
701 from: curve_entity,
702 },
703 );
704 labels.edge(
705 side.profile_edge,
706 EdgeRole::StartCapEdge { from: curve_entity },
707 );
708 labels.vertex(
709 side.front,
710 VertexRole::StartCapVertex {
711 from: front_entity,
712 side: front_kind,
713 },
714 );
715 labels.vertex(
716 side.back,
717 VertexRole::StartCapVertex {
718 from: back_entity,
719 side: back_kind,
720 },
721 );
722
723 face_edges
724 .iter()
725 .filter(|(edge_id, _, _)| *edge_id != side.profile_edge)
726 .for_each(|&(edge_id, edge_front, edge_back)| {
727 let touches_front = edge_front == side.front || edge_back == side.front;
728 let touches_back = edge_front == side.back || edge_back == side.back;
729 match (touches_front, touches_back) {
730 (true, false) => {
731 labels.edge(
732 edge_id,
733 EdgeRole::SideEdge {
734 from: front_entity,
735 side: front_kind,
736 },
737 );
738 let top = if edge_front == side.front {
739 edge_back
740 } else {
741 edge_front
742 };
743 labels.vertex(
744 top,
745 VertexRole::EndCapVertex {
746 from: front_entity,
747 side: front_kind,
748 },
749 );
750 }
751 (false, true) => {
752 labels.edge(
753 edge_id,
754 EdgeRole::SideEdge {
755 from: back_entity,
756 side: back_kind,
757 },
758 );
759 let top = if edge_front == side.back {
760 edge_back
761 } else {
762 edge_front
763 };
764 labels.vertex(
765 top,
766 VertexRole::EndCapVertex {
767 from: back_entity,
768 side: back_kind,
769 },
770 );
771 }
772 _ => labels.edge(edge_id, EdgeRole::EndCapEdge { from: curve_entity }),
773 }
774 });
775}