Another project
0

Configure Feed

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

feat(kernel): brep extrude build

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

author
Lewis
date (May 28, 2026, 11:54 PM +0300) commit 94bcf6dd parent 7cb98fda change-id vpkykqmv
+531 -213
+1 -1
crates/bone-app/src/sketch_mode.rs
··· 402 402 assert_eq!( 403 403 SketchTool::ENTITIES.len(), 404 404 12, 405 - "ADR 0008 day-1 entity tools" 405 + "day-1 entity tools" 406 406 ); 407 407 } 408 408
+353 -178
crates/bone-kernel/src/brep/build.rs
··· 1 - #![allow( 2 - dead_code, 3 - reason = "facade construction seam exercised only by tests until the extrude evaluator wires the production caller" 4 - )] 5 - 6 1 use std::collections::HashMap; 7 - use std::collections::hash_map::Entry; 2 + use std::collections::HashSet; 8 3 9 4 use bone_types::{ 10 - BrepEdgeId, BrepFaceId, BrepLoopId, BrepShellId, BrepVertexId, EdgeLabel, FaceLabel, Tolerance, 11 - VertexLabel, 5 + BrepEdgeId, BrepFaceId, BrepLoopId, BrepShellId, BrepVertexId, EdgeLabel, EdgeRole, FaceLabel, 6 + SideKind, SketchEntityId, Tolerance, VertexLabel, VertexRole, 12 7 }; 13 8 use slotmap::{Key, SlotMap}; 14 9 use truck_modeling::{ ··· 41 36 42 37 const LENGTH_DIVISION_TOLERANCE: Tolerance = Tolerance::new(1.0e-4); 43 38 44 - pub(crate) fn edge_length(edge: &Edge) -> f64 { 39 + pub(crate) fn edge_points(edge: &Edge) -> Vec<truck_modeling::Point3> { 45 40 let curve = edge.curve(); 46 41 let (_, points) = 47 42 curve.parameter_division(curve.range_tuple(), LENGTH_DIVISION_TOLERANCE.value()); 48 43 points 44 + } 45 + 46 + pub(crate) fn edge_length(edge: &Edge) -> f64 { 47 + edge_points(edge) 49 48 .windows(2) 50 49 .map(|pair| { 51 50 let span = pair[1] - pair[0]; ··· 58 57 pub(crate) faces: HashMap<FaceID, FaceLabel>, 59 58 pub(crate) edges: HashMap<EdgeID, EdgeLabel>, 60 59 pub(crate) vertices: HashMap<VertexID, VertexLabel>, 60 + pub(crate) closed_curves: HashSet<SketchEntityId>, 61 61 } 62 62 63 - struct VertexTable { 64 - vertices: SlotMap<BrepVertexId, BrepVertex>, 65 - by_truck: HashMap<VertexID, BrepVertexId>, 63 + fn suppressed_edge(label: EdgeLabel, closed: &HashSet<SketchEntityId>) -> bool { 64 + matches!(label.role, EdgeRole::SideEdge { from, side: SideKind::Seam } if !closed.contains(&from)) 66 65 } 67 66 68 - struct EdgeTable { 69 - edges: SlotMap<BrepEdgeId, BrepEdge>, 70 - by_truck: HashMap<EdgeID, BrepEdgeId>, 71 - arena: Vec<Edge>, 72 - } 73 - 74 - struct FaceDescription { 75 - label: FaceLabel, 76 - loops: Vec<Vec<BrepEdgeId>>, 77 - } 78 - 79 - struct ShellDescription { 80 - boundary: BoundaryIndex, 81 - faces: Vec<FaceDescription>, 67 + fn suppressed_vertex(label: VertexLabel, closed: &HashSet<SketchEntityId>) -> bool { 68 + matches!( 69 + label.role, 70 + VertexRole::StartCapVertex { from, side: SideKind::Seam } 71 + | VertexRole::EndCapVertex { from, side: SideKind::Seam } 72 + if !closed.contains(&from) 73 + ) 82 74 } 83 75 84 76 pub(crate) fn assemble(solid: Solid, labeling: &SolidLabeling) -> Result<BrepSolid, BrepError> { 85 - let VertexTable { 86 - vertices, 87 - by_truck: vertex_dedup, 88 - } = build_vertices(&solid, labeling)?; 89 - let EdgeTable { 90 - edges, 91 - by_truck: edge_dedup, 92 - arena: arena_edges, 93 - } = build_edges(&solid, labeling, &vertex_dedup)?; 94 - let described = describe_shells(&solid, labeling, &edge_dedup)?; 95 - let (shells, faces, loops) = mint_topology(described); 77 + let vertices = build_vertices(&solid, labeling)?; 78 + let edges = build_edges(&solid, labeling, &vertices.by_truck)?; 79 + let faces = build_faces(&solid, labeling, &edges)?; 96 80 97 - let shell_order = ordered_by(&shells, |shell| shell.boundary_index); 98 - let face_order = ordered_by_label(&faces, BrepFace::label, LabelKind::Face)?; 99 - let edge_order = ordered_by_label(&edges, BrepEdge::label, LabelKind::Edge)?; 100 - let vertex_order = ordered_by_label(&vertices, BrepVertex::label, LabelKind::Vertex)?; 81 + let shell_order = ordered_by(&faces.shells, |shell| shell.boundary_index); 82 + let face_order = ordered_by(&faces.faces, BrepFace::label); 83 + let edge_order = ordered_by(&edges.map, BrepEdge::label); 84 + let vertex_order = ordered_by(&vertices.map, BrepVertex::label); 101 85 let loop_order: Vec<BrepLoopId> = face_order 102 86 .iter() 103 - .flat_map(|face_id| faces[*face_id].loops.iter().copied()) 87 + .flat_map(|face_id| faces.faces[*face_id].loops().iter().copied()) 104 88 .collect(); 105 89 106 90 Ok(BrepSolid { 107 91 arena: Arena { 108 92 solid, 109 - edges: arena_edges, 93 + edges: edges.arena, 110 94 }, 111 - shells, 112 - faces, 113 - loops, 114 - edges, 115 - vertices, 95 + shells: faces.shells, 96 + faces: faces.faces, 97 + loops: faces.loops, 98 + edges: edges.map, 99 + vertices: vertices.map, 116 100 shell_order, 117 101 face_order, 118 102 loop_order, ··· 127 111 keys 128 112 } 129 113 130 - fn ordered_by_label<K: Key, V, T: Ord>( 131 - map: &SlotMap<K, V>, 132 - label: impl Fn(&V) -> T, 133 - kind: LabelKind, 134 - ) -> Result<Vec<K>, BrepError> { 135 - let order = ordered_by(map, &label); 136 - let unique = order 137 - .windows(2) 138 - .all(|pair| label(&map[pair[0]]) != label(&map[pair[1]])); 139 - if unique { 140 - Ok(order) 141 - } else { 142 - Err(BrepError::DuplicateLabel { kind }) 143 - } 114 + struct VertexTable { 115 + map: SlotMap<BrepVertexId, BrepVertex>, 116 + by_truck: HashMap<VertexID, BrepVertexId>, 144 117 } 145 118 146 119 fn build_vertices(solid: &Solid, labeling: &SolidLabeling) -> Result<VertexTable, BrepError> { 147 - solid.vertex_iter().try_fold( 148 - VertexTable { 149 - vertices: SlotMap::with_key(), 150 - by_truck: HashMap::new(), 151 - }, 152 - |mut table, vertex| { 153 - if let Entry::Vacant(slot) = table.by_truck.entry(vertex.id()) { 154 - let label = 155 - *labeling 156 - .vertices 157 - .get(&vertex.id()) 158 - .ok_or(BrepError::MissingLabel { 159 - kind: LabelKind::Vertex, 160 - })?; 161 - let id = table 162 - .vertices 163 - .insert_with_key(|id| BrepVertex { id, label }); 164 - slot.insert(id); 120 + let (map, _by_label, by_truck) = solid.vertex_iter().try_fold( 121 + ( 122 + SlotMap::with_key(), 123 + HashMap::<VertexLabel, BrepVertexId>::new(), 124 + HashMap::<VertexID, BrepVertexId>::new(), 125 + ), 126 + |(mut map, mut by_label, mut by_truck), vertex| { 127 + if by_truck.contains_key(&vertex.id()) { 128 + return Ok::<_, BrepError>((map, by_label, by_truck)); 165 129 } 166 - Ok(table) 130 + let label = *labeling 131 + .vertices 132 + .get(&vertex.id()) 133 + .ok_or(BrepError::MissingLabel { 134 + kind: LabelKind::Vertex, 135 + })?; 136 + if suppressed_vertex(label, &labeling.closed_curves) { 137 + return Ok((map, by_label, by_truck)); 138 + } 139 + let brep_id = by_label.get(&label).copied().unwrap_or_else(|| { 140 + let id = map.insert_with_key(|id| BrepVertex { id, label }); 141 + by_label.insert(label, id); 142 + id 143 + }); 144 + by_truck.insert(vertex.id(), brep_id); 145 + Ok((map, by_label, by_truck)) 167 146 }, 168 - ) 147 + )?; 148 + Ok(VertexTable { map, by_truck }) 149 + } 150 + 151 + struct EdgeTable { 152 + map: SlotMap<BrepEdgeId, BrepEdge>, 153 + by_truck: HashMap<EdgeID, BrepEdgeId>, 154 + arena: Vec<Edge>, 155 + } 156 + 157 + struct EdgeAccum { 158 + handles: Vec<EdgeArenaHandle>, 159 + incident: Vec<BrepVertexId>, 169 160 } 170 161 171 162 fn build_edges( ··· 173 164 labeling: &SolidLabeling, 174 165 vertex_dedup: &HashMap<VertexID, BrepVertexId>, 175 166 ) -> Result<EdgeTable, BrepError> { 176 - solid.edge_iter().try_fold( 177 - EdgeTable { 178 - edges: SlotMap::with_key(), 179 - by_truck: HashMap::new(), 180 - arena: Vec::new(), 167 + let (accum, arena, truck_label) = solid.edge_iter().try_fold( 168 + ( 169 + HashMap::<EdgeLabel, EdgeAccum>::new(), 170 + Vec::<Edge>::new(), 171 + HashMap::<EdgeID, EdgeLabel>::new(), 172 + ), 173 + |(mut accum, mut arena, mut truck_label), edge| { 174 + if truck_label.contains_key(&edge.id()) { 175 + return Ok::<_, BrepError>((accum, arena, truck_label)); 176 + } 177 + let label = *labeling 178 + .edges 179 + .get(&edge.id()) 180 + .ok_or(BrepError::MissingLabel { 181 + kind: LabelKind::Edge, 182 + })?; 183 + if suppressed_edge(label, &labeling.closed_curves) { 184 + return Ok((accum, arena, truck_label)); 185 + } 186 + let handle = EdgeArenaHandle(arena.len()); 187 + arena.push(edge.clone()); 188 + let accumulator = accum.entry(label).or_insert_with(|| EdgeAccum { 189 + handles: Vec::new(), 190 + incident: Vec::new(), 191 + }); 192 + accumulator.handles.push(handle); 193 + [edge.front().id(), edge.back().id()] 194 + .into_iter() 195 + .filter_map(|truck_vertex| vertex_dedup.get(&truck_vertex).copied()) 196 + .for_each(|brep_vertex| accumulator.incident.push(brep_vertex)); 197 + truck_label.insert(edge.id(), label); 198 + Ok((accum, arena, truck_label)) 199 + }, 200 + )?; 201 + 202 + let (map, label_to_id) = accum.into_iter().fold( 203 + (SlotMap::with_key(), HashMap::<EdgeLabel, BrepEdgeId>::new()), 204 + |(mut map, mut label_to_id), (label, slot)| { 205 + let vertices = logical_endpoints(&slot.incident); 206 + let id = map.insert_with_key(|id| BrepEdge { 207 + id, 208 + label, 209 + handles: slot.handles, 210 + vertices, 211 + }); 212 + label_to_id.insert(label, id); 213 + (map, label_to_id) 181 214 }, 182 - |mut table, edge| { 183 - if let Entry::Vacant(slot) = table.by_truck.entry(edge.id()) { 184 - let label = *labeling 185 - .edges 186 - .get(&edge.id()) 187 - .ok_or(BrepError::MissingLabel { 188 - kind: LabelKind::Edge, 189 - })?; 190 - let front = 191 - *vertex_dedup 192 - .get(&edge.front().id()) 193 - .ok_or(BrepError::MissingLabel { 194 - kind: LabelKind::Vertex, 195 - })?; 196 - let back = *vertex_dedup 197 - .get(&edge.back().id()) 198 - .ok_or(BrepError::MissingLabel { 199 - kind: LabelKind::Vertex, 200 - })?; 201 - let handle = EdgeArenaHandle(table.arena.len()); 202 - table.arena.push(edge.clone()); 203 - let id = table.edges.insert_with_key(|id| BrepEdge { 204 - id, 205 - label, 206 - handle, 207 - vertices: [front, back], 208 - }); 209 - slot.insert(id); 210 - } 211 - Ok(table) 215 + ); 216 + 217 + let by_truck = truck_label 218 + .into_iter() 219 + .map(|(truck_id, label)| (truck_id, label_to_id[&label])) 220 + .collect(); 221 + 222 + Ok(EdgeTable { 223 + map, 224 + by_truck, 225 + arena, 226 + }) 227 + } 228 + 229 + fn logical_endpoints(incident: &[BrepVertexId]) -> [BrepVertexId; 2] { 230 + let counts = incident.iter().fold( 231 + HashMap::<BrepVertexId, usize>::new(), 232 + |mut counts, vertex| { 233 + *counts.entry(*vertex).or_insert(0) += 1; 234 + counts 212 235 }, 213 - ) 236 + ); 237 + let mut odd: Vec<BrepVertexId> = counts 238 + .iter() 239 + .filter_map(|(vertex, count)| (*count % 2 == 1).then_some(*vertex)) 240 + .collect(); 241 + odd.sort_unstable(); 242 + if let [front, back] = odd.as_slice() { 243 + [*front, *back] 244 + } else { 245 + let representative = counts.keys().copied().min().unwrap_or_default(); 246 + [representative, representative] 247 + } 248 + } 249 + 250 + struct FaceTable { 251 + faces: SlotMap<BrepFaceId, BrepFace>, 252 + loops: SlotMap<BrepLoopId, BrepLoop>, 253 + shells: SlotMap<BrepShellId, BrepShell>, 214 254 } 215 255 216 - fn describe_shells( 256 + #[derive(Default)] 257 + struct Gathered { 258 + adjacency: HashMap<EdgeID, Vec<FaceLabel>>, 259 + patches: HashMap<FaceLabel, Vec<Vec<Vec<EdgeID>>>>, 260 + shell_members: Vec<Vec<FaceLabel>>, 261 + } 262 + 263 + fn build_faces( 217 264 solid: &Solid, 218 265 labeling: &SolidLabeling, 219 - edge_dedup: &HashMap<EdgeID, BrepEdgeId>, 220 - ) -> Result<Vec<ShellDescription>, BrepError> { 266 + edges: &EdgeTable, 267 + ) -> Result<FaceTable, BrepError> { 268 + let Gathered { 269 + adjacency, 270 + patches, 271 + shell_members, 272 + } = gather_faces(solid, labeling)?; 273 + 274 + let (faces, loops, label_to_face) = patches.into_iter().fold( 275 + ( 276 + SlotMap::with_key(), 277 + SlotMap::with_key(), 278 + HashMap::<FaceLabel, BrepFaceId>::new(), 279 + ), 280 + |(mut faces, mut loops, mut label_to_face), (label, patch_list)| { 281 + let loop_ids = face_loops(&patch_list, label, &adjacency, edges, &mut loops); 282 + let id = faces.insert_with_key(|id| BrepFace { 283 + id, 284 + label, 285 + loops: loop_ids, 286 + }); 287 + label_to_face.insert(label, id); 288 + (faces, loops, label_to_face) 289 + }, 290 + ); 291 + 292 + let shells = shell_members.into_iter().enumerate().fold( 293 + SlotMap::with_key(), 294 + |mut shells, (boundary, members)| { 295 + let face_ids = dedup_preserving(members) 296 + .into_iter() 297 + .map(|label| label_to_face[&label]) 298 + .collect(); 299 + shells.insert_with_key(|id| BrepShell { 300 + id, 301 + boundary_index: BoundaryIndex(boundary), 302 + faces: face_ids, 303 + }); 304 + shells 305 + }, 306 + ); 307 + 308 + Ok(FaceTable { 309 + faces, 310 + loops, 311 + shells, 312 + }) 313 + } 314 + 315 + fn gather_faces(solid: &Solid, labeling: &SolidLabeling) -> Result<Gathered, BrepError> { 221 316 solid 222 317 .boundaries() 223 318 .iter() 224 - .enumerate() 225 - .map(|(boundary_index, shell)| { 226 - let faces = shell 319 + .try_fold(Gathered::default(), |mut gathered, shell| { 320 + let members = shell 227 321 .face_iter() 228 322 .map(|face| { 229 323 let label = *labeling ··· 232 326 .ok_or(BrepError::MissingLabel { 233 327 kind: LabelKind::Face, 234 328 })?; 235 - let loops = face 329 + let wires: Vec<Vec<EdgeID>> = face 236 330 .boundaries() 237 331 .iter() 238 - .map(|wire| { 239 - wire.edge_iter() 240 - .map(|edge| { 241 - edge_dedup.get(&edge.id()).copied().ok_or( 242 - BrepError::MissingLabel { 243 - kind: LabelKind::Edge, 244 - }, 245 - ) 246 - }) 247 - .collect::<Result<Vec<_>, _>>() 248 - }) 249 - .collect::<Result<Vec<_>, _>>()?; 250 - Ok(FaceDescription { label, loops }) 332 + .map(|wire| wire.edge_iter().map(Edge::id).collect()) 333 + .collect(); 334 + wires.iter().flatten().for_each(|edge_id| { 335 + gathered.adjacency.entry(*edge_id).or_default().push(label); 336 + }); 337 + gathered.patches.entry(label).or_default().push(wires); 338 + Ok::<FaceLabel, BrepError>(label) 251 339 }) 252 340 .collect::<Result<Vec<_>, _>>()?; 253 - Ok(ShellDescription { 254 - boundary: BoundaryIndex(boundary_index), 255 - faces, 256 - }) 341 + gathered.shell_members.push(members); 342 + Ok(gathered) 257 343 }) 258 - .collect() 259 344 } 260 345 261 - fn mint_topology( 262 - described: Vec<ShellDescription>, 263 - ) -> ( 264 - SlotMap<BrepShellId, BrepShell>, 265 - SlotMap<BrepFaceId, BrepFace>, 266 - SlotMap<BrepLoopId, BrepLoop>, 267 - ) { 268 - described.into_iter().fold( 269 - ( 270 - SlotMap::with_key(), 271 - SlotMap::with_key(), 272 - SlotMap::with_key(), 273 - ), 274 - |(mut shells, mut faces, mut loops), shell_desc| { 275 - let face_ids = shell_desc 276 - .faces 277 - .into_iter() 278 - .map(|face_desc| { 279 - let loop_ids = face_desc 280 - .loops 281 - .into_iter() 282 - .map(|edges| loops.insert_with_key(|id| BrepLoop { id, edges })) 283 - .collect(); 284 - faces.insert_with_key(|id| BrepFace { 346 + fn face_loops( 347 + patches: &[Vec<Vec<EdgeID>>], 348 + label: FaceLabel, 349 + adjacency: &HashMap<EdgeID, Vec<FaceLabel>>, 350 + edges: &EdgeTable, 351 + loops: &mut SlotMap<BrepLoopId, BrepLoop>, 352 + ) -> Vec<BrepLoopId> { 353 + if let [single] = patches { 354 + single 355 + .iter() 356 + .filter_map(|wire| { 357 + let loop_edges = 358 + dedup_cycle(wire.iter().map(|edge_id| edges.by_truck[edge_id]).collect()); 359 + (!loop_edges.is_empty()).then(|| { 360 + loops.insert_with_key(|id| BrepLoop { 285 361 id, 286 - label: face_desc.label, 287 - loops: loop_ids, 362 + edges: loop_edges, 288 363 }) 289 364 }) 290 - .collect(); 291 - shells.insert_with_key(|id| BrepShell { 292 - id, 293 - boundary_index: shell_desc.boundary, 294 - faces: face_ids, 295 - }); 296 - (shells, faces, loops) 297 - }, 298 - ) 365 + }) 366 + .collect() 367 + } else { 368 + let mut seen = HashSet::new(); 369 + let boundary: Vec<BrepEdgeId> = patches 370 + .iter() 371 + .flatten() 372 + .flatten() 373 + .filter(|edge_id| !internal_to_group(adjacency, **edge_id, label)) 374 + .filter_map(|edge_id| { 375 + let brep_id = edges.by_truck[edge_id]; 376 + seen.insert(brep_id).then_some(brep_id) 377 + }) 378 + .collect(); 379 + chain_loops(boundary, &edges.map) 380 + .into_iter() 381 + .map(|loop_edges| { 382 + loops.insert_with_key(|id| BrepLoop { 383 + id, 384 + edges: loop_edges, 385 + }) 386 + }) 387 + .collect() 388 + } 389 + } 390 + 391 + fn internal_to_group( 392 + adjacency: &HashMap<EdgeID, Vec<FaceLabel>>, 393 + edge_id: EdgeID, 394 + label: FaceLabel, 395 + ) -> bool { 396 + adjacency 397 + .get(&edge_id) 398 + .is_some_and(|adjacent| adjacent.len() == 2 && adjacent.iter().all(|other| *other == label)) 399 + } 400 + 401 + fn chain_loops( 402 + boundary: Vec<BrepEdgeId>, 403 + edges: &SlotMap<BrepEdgeId, BrepEdge>, 404 + ) -> Vec<Vec<BrepEdgeId>> { 405 + let mut sorted = boundary; 406 + sorted.sort_by_key(|edge| edges[*edge].label()); 407 + let (closed, open): (Vec<BrepEdgeId>, Vec<BrepEdgeId>) = sorted 408 + .into_iter() 409 + .partition(|edge| edges[*edge].vertices()[0] == edges[*edge].vertices()[1]); 410 + let closed_loops = closed.into_iter().map(|edge| vec![edge]); 411 + closed_loops.chain(walk_open(open, edges)).collect() 412 + } 413 + 414 + fn walk_open(open: Vec<BrepEdgeId>, edges: &SlotMap<BrepEdgeId, BrepEdge>) -> Vec<Vec<BrepEdgeId>> { 415 + let mut remaining = open; 416 + std::iter::from_fn(move || { 417 + let first = (!remaining.is_empty()).then(|| remaining.remove(0))?; 418 + let start = edges[first].vertices()[0]; 419 + let next = edges[first].vertices()[1]; 420 + Some(extend_loop(next, start, &mut remaining, edges, vec![first])) 421 + }) 422 + .collect() 423 + } 424 + 425 + fn extend_loop( 426 + current: BrepVertexId, 427 + start: BrepVertexId, 428 + remaining: &mut Vec<BrepEdgeId>, 429 + edges: &SlotMap<BrepEdgeId, BrepEdge>, 430 + acc: Vec<BrepEdgeId>, 431 + ) -> Vec<BrepEdgeId> { 432 + if current == start { 433 + return acc; 434 + } 435 + match remaining 436 + .iter() 437 + .position(|edge| edges[*edge].vertices().contains(&current)) 438 + { 439 + None => acc, 440 + Some(position) => { 441 + let edge = remaining.remove(position); 442 + let ends = edges[edge].vertices(); 443 + let next = if ends[0] == current { ends[1] } else { ends[0] }; 444 + extend_loop( 445 + next, 446 + start, 447 + remaining, 448 + edges, 449 + acc.into_iter().chain(std::iter::once(edge)).collect(), 450 + ) 451 + } 452 + } 453 + } 454 + 455 + fn dedup_cycle(ids: Vec<BrepEdgeId>) -> Vec<BrepEdgeId> { 456 + let mut out = ids.into_iter().fold(Vec::new(), |mut acc, id| { 457 + if acc.last() != Some(&id) { 458 + acc.push(id); 459 + } 460 + acc 461 + }); 462 + if out.len() > 1 && out.first() == out.last() { 463 + out.pop(); 464 + } 465 + out 466 + } 467 + 468 + fn dedup_preserving(labels: Vec<FaceLabel>) -> Vec<FaceLabel> { 469 + let mut seen = HashSet::new(); 470 + labels 471 + .into_iter() 472 + .filter(|label| seen.insert(*label)) 473 + .collect() 299 474 }
+103 -34
crates/bone-kernel/src/brep/mod.rs
··· 1 + use std::collections::HashSet; 2 + 1 3 use bone_types::{ 2 - BrepEdgeId, BrepFaceId, BrepLoopId, BrepShellId, BrepVertexId, EdgeLabel, FaceLabel, Tolerance, 3 - VertexLabel, 4 + Aabb3, BrepEdgeId, BrepFaceId, BrepLoopId, BrepShellId, BrepVertexId, EdgeLabel, EdgeRole, 5 + FaceLabel, Point3, SideKind, Tolerance, VertexLabel, 4 6 }; 5 7 use slotmap::SlotMap; 6 8 use truck_modeling::ShellCondition; 7 9 8 10 mod build; 9 - use build::{Arena, BoundaryIndex, EdgeArenaHandle, edge_length}; 11 + pub mod eval; 12 + pub mod profile; 13 + use build::{Arena, BoundaryIndex, EdgeArenaHandle, edge_length, edge_points}; 10 14 11 15 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 12 - pub enum TruckGap {} 16 + pub enum TruckGap { 17 + ReverseNormal, 18 + AxisDirection, 19 + ReferenceDirection, 20 + ThroughAll, 21 + UpToNext, 22 + UpToVertex, 23 + UpToSurface, 24 + OffsetFromSurface, 25 + UpToBody, 26 + Draft, 27 + ThinWall, 28 + } 13 29 14 30 impl core::fmt::Display for TruckGap { 15 - fn fmt(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 16 - match *self {} 31 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 32 + f.write_str(match self { 33 + Self::ReverseNormal => "reverse-normal extrude direction", 34 + Self::AxisDirection => "along-axis extrude direction", 35 + Self::ReferenceDirection => "between-references extrude direction", 36 + Self::ThroughAll => "through-all end condition", 37 + Self::UpToNext => "up-to-next end condition", 38 + Self::UpToVertex => "up-to-vertex end condition", 39 + Self::UpToSurface => "up-to-surface end condition", 40 + Self::OffsetFromSurface => "offset-from-surface end condition", 41 + Self::UpToBody => "up-to-body end condition", 42 + Self::Draft => "draft taper", 43 + Self::ThinWall => "thin-wall shelling", 44 + }) 17 45 } 18 46 } 19 47 ··· 39 67 OpenLoop, 40 68 SelfIntersectingLoop, 41 69 ZeroArea, 70 + UncontainedLoop, 71 + OverlappingLoops, 42 72 } 43 73 44 74 impl core::fmt::Display for ProfileDefect { ··· 47 77 Self::OpenLoop => "an open loop", 48 78 Self::SelfIntersectingLoop => "a self-intersecting loop", 49 79 Self::ZeroArea => "zero area", 80 + Self::UncontainedLoop => "a loop outside the outer boundary", 81 + Self::OverlappingLoops => "overlapping inner loops", 50 82 }) 51 83 } 52 84 } ··· 57 89 InvalidProfile { reason: ProfileDefect }, 58 90 #[error("extrude depth is zero")] 59 91 EmptyExtrudeDepth, 60 - #[error("solid self-intersects")] 61 - SelfIntersecting { faces: Vec<BrepFaceId> }, 62 92 #[error("boundary shell is not a closed manifold")] 63 93 ShellNotClosed, 64 94 #[error("edge {edge:?} collapses below the validation tolerance")] 65 95 DegenerateEdge { edge: BrepEdgeId }, 96 + #[error("edge {edge:?} belongs to no loop")] 97 + DanglingEdge { edge: BrepEdgeId }, 98 + #[error("vertex {vertex:?} belongs to no loop")] 99 + DanglingVertex { vertex: BrepVertexId }, 66 100 #[error("kernel facade does not yet wrap this path: {detail}")] 67 101 TruckUnsupported { detail: TruckGap }, 68 102 #[error("{kind} carries no extrude label")] 69 103 MissingLabel { kind: LabelKind }, 70 - #[error("duplicate {kind} label breaks per-element naming")] 71 - DuplicateLabel { kind: LabelKind }, 72 104 } 73 105 74 106 #[derive(Clone, Debug)] ··· 136 168 pub struct BrepEdge { 137 169 id: BrepEdgeId, 138 170 label: EdgeLabel, 139 - handle: EdgeArenaHandle, 171 + handles: Vec<EdgeArenaHandle>, 140 172 vertices: [BrepVertexId; 2], 141 173 } 142 174 ··· 225 257 } 226 258 })?; 227 259 self.iter_edges().try_for_each(|edge| { 228 - if edge_length(self.arena.edge(edge.handle)) <= tolerance.value() { 260 + let length: f64 = edge 261 + .handles 262 + .iter() 263 + .map(|handle| edge_length(self.arena.edge(*handle))) 264 + .sum(); 265 + if length <= tolerance.value() { 229 266 Err(BrepError::DegenerateEdge { edge: edge.id }) 230 267 } else { 231 268 Ok(()) 232 269 } 270 + })?; 271 + let loop_edges: HashSet<BrepEdgeId> = self 272 + .iter_loops() 273 + .flat_map(|brep_loop| brep_loop.edges().iter().copied()) 274 + .collect(); 275 + self.iter_edges().try_for_each(|edge| { 276 + if loop_edges.contains(&edge.id) 277 + || matches!(edge.label.role, EdgeRole::SideEdge { side: SideKind::Seam, .. }) 278 + { 279 + Ok(()) 280 + } else { 281 + Err(BrepError::DanglingEdge { edge: edge.id }) 282 + } 283 + })?; 284 + let loop_vertices: HashSet<BrepVertexId> = loop_edges 285 + .iter() 286 + .flat_map(|edge| self.edges[*edge].vertices()) 287 + .collect(); 288 + self.iter_vertices().try_for_each(|vertex| { 289 + if loop_vertices.contains(&vertex.id) { 290 + Ok(()) 291 + } else { 292 + Err(BrepError::DanglingVertex { vertex: vertex.id }) 293 + } 233 294 }) 234 295 } 296 + 297 + #[must_use] 298 + pub fn bounding_box(&self) -> Option<Aabb3> { 299 + let points = self.iter_edges().flat_map(|edge| { 300 + edge.handles.iter().flat_map(|handle| { 301 + edge_points(self.arena.edge(*handle)) 302 + .into_iter() 303 + .map(|p| Point3::from_mm(p.x, p.y, p.z)) 304 + }) 305 + }); 306 + Aabb3::from_points(points) 307 + } 235 308 } 236 309 237 310 #[cfg(test)] ··· 243 316 SketchEntityId, Tolerance, VertexLabel, VertexRole, 244 317 }; 245 318 use slotmap::SlotMap; 246 - use std::collections::HashMap; 319 + use std::collections::{HashMap, HashSet}; 247 320 use truck_modeling::{Face, Point3, Shell, Solid, Vector3, builder}; 248 321 249 322 const LOW: f64 = 0.25; ··· 390 463 faces, 391 464 edges, 392 465 vertices, 466 + closed_curves: HashSet::new(), 393 467 } 394 468 } 395 469 ··· 519 593 } 520 594 521 595 #[test] 522 - fn duplicate_label_is_rejected() { 596 + fn shared_face_label_groups_into_one() { 523 597 let profile = profile(); 524 598 let solid = unit_cube(); 525 599 let mut labeling = label_solid(&solid, &profile); ··· 530 604 .faces 531 605 .values_mut() 532 606 .for_each(|label| *label = shared); 533 - assert!(matches!( 534 - assemble(solid, &labeling), 535 - Err(BrepError::DuplicateLabel { 536 - kind: LabelKind::Face 537 - }) 538 - )); 607 + let Ok(brep) = assemble(solid, &labeling) else { 608 + panic!("faces sharing one label group rather than collide"); 609 + }; 610 + assert_eq!(brep.iter_faces().count(), 1); 539 611 } 540 612 541 613 #[test] ··· 570 642 faces: HashMap::new(), 571 643 edges: HashMap::new(), 572 644 vertices: HashMap::new(), 645 + closed_curves: HashSet::new(), 573 646 }; 574 647 let Ok(brep) = assemble(solid, &labeling) else { 575 648 panic!("empty solid has nothing to label"); ··· 609 682 } 610 683 611 684 #[test] 612 - fn duplicate_edge_label_is_rejected() { 685 + fn shared_edge_label_groups_into_one() { 613 686 let profile = profile(); 614 687 let solid = unit_cube(); 615 688 let mut labeling = label_solid(&solid, &profile); ··· 620 693 .edges 621 694 .values_mut() 622 695 .for_each(|label| *label = shared); 623 - assert!(matches!( 624 - assemble(solid, &labeling), 625 - Err(BrepError::DuplicateLabel { 626 - kind: LabelKind::Edge 627 - }) 628 - )); 696 + let Ok(brep) = assemble(solid, &labeling) else { 697 + panic!("edges sharing one label group rather than collide"); 698 + }; 699 + assert_eq!(brep.iter_edges().count(), 1); 629 700 } 630 701 631 702 #[test] 632 - fn duplicate_vertex_label_is_rejected() { 703 + fn shared_vertex_label_groups_into_one() { 633 704 let profile = profile(); 634 705 let solid = unit_cube(); 635 706 let mut labeling = label_solid(&solid, &profile); ··· 640 711 .vertices 641 712 .values_mut() 642 713 .for_each(|label| *label = shared); 643 - assert!(matches!( 644 - assemble(solid, &labeling), 645 - Err(BrepError::DuplicateLabel { 646 - kind: LabelKind::Vertex 647 - }) 648 - )); 714 + let Ok(brep) = assemble(solid, &labeling) else { 715 + panic!("vertices sharing one label group rather than collide"); 716 + }; 717 + assert_eq!(brep.iter_vertices().count(), 1); 649 718 } 650 719 }
+72
crates/bone-kernel/src/brep/profile.rs
··· 1 + use bone_types::{Plane3, SketchEntityId}; 2 + 3 + use crate::curve2::Curve2Kind; 4 + 5 + #[derive(Copy, Clone, Debug, PartialEq)] 6 + pub struct ProfileEdge { 7 + curve: Curve2Kind, 8 + curve_entity: SketchEntityId, 9 + corner: SketchEntityId, 10 + } 11 + 12 + impl ProfileEdge { 13 + #[must_use] 14 + pub const fn new( 15 + curve: Curve2Kind, 16 + curve_entity: SketchEntityId, 17 + corner: SketchEntityId, 18 + ) -> Self { 19 + Self { 20 + curve, 21 + curve_entity, 22 + corner, 23 + } 24 + } 25 + 26 + #[must_use] 27 + pub const fn curve(self) -> Curve2Kind { 28 + self.curve 29 + } 30 + 31 + #[must_use] 32 + pub const fn curve_entity(self) -> SketchEntityId { 33 + self.curve_entity 34 + } 35 + 36 + #[must_use] 37 + pub const fn corner(self) -> SketchEntityId { 38 + self.corner 39 + } 40 + } 41 + 42 + #[derive(Clone, Debug, PartialEq)] 43 + pub enum ProfileLoop { 44 + Closed { 45 + curve: Curve2Kind, 46 + curve_entity: SketchEntityId, 47 + }, 48 + Open(Vec<ProfileEdge>), 49 + } 50 + 51 + #[derive(Clone, Debug, PartialEq)] 52 + pub struct ExtrudeProfile { 53 + plane: Plane3, 54 + loops: Vec<ProfileLoop>, 55 + } 56 + 57 + impl ExtrudeProfile { 58 + #[must_use] 59 + pub fn new(plane: Plane3, loops: Vec<ProfileLoop>) -> Self { 60 + Self { plane, loops } 61 + } 62 + 63 + #[must_use] 64 + pub fn plane(&self) -> Plane3 { 65 + self.plane 66 + } 67 + 68 + #[must_use] 69 + pub fn loops(&self) -> &[ProfileLoop] { 70 + &self.loops 71 + } 72 + }
+2
crates/bone-kernel/src/lib.rs
··· 24 24 pub use aabb::Aabb2; 25 25 pub use arc2::{Arc2, arc_bounding_box}; 26 26 pub use arc3::Arc3; 27 + pub use brep::eval::evaluate_extrude; 28 + pub use brep::profile::{ExtrudeProfile, ProfileEdge, ProfileLoop}; 27 29 pub use brep::{ 28 30 BrepEdge, BrepError, BrepFace, BrepLoop, BrepShell, BrepSolid, BrepVertex, LabelKind, 29 31 ProfileDefect, TruckGap,