Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(kernel): extrude surface eval

Lewis: May this revision serve well! <lu5a@proton.me>

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