Another project
0

Configure Feed

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

test(kernel): extrude snapshot tests

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

author
Lewis
date (May 29, 2026, 4:05 PM +0300) commit d137ebdf parent 5caac9d7 change-id ymkuvmzn
+716
+1
Cargo.lock
··· 433 433 dependencies = [ 434 434 "bone-types", 435 435 "insta", 436 + "proptest", 436 437 "ron", 437 438 "serde", 438 439 "slotmap",
+1
crates/bone-kernel/Cargo.toml
··· 15 15 16 16 [dev-dependencies] 17 17 insta = { workspace = true } 18 + proptest = { workspace = true } 18 19 ron = { workspace = true } 19 20 20 21 [lints]
+504
crates/bone-kernel/tests/extrude.rs
··· 1 + use bone_kernel::{ 2 + Arc2, BrepError, BrepSolid, Circle2, Curve2Kind, ExtrudeDirection, ExtrudeEndCondition, 3 + ExtrudeFeature, ExtrudeProfile, ExtrudeSense, Line2, MergeResult, ProfileDefect, ProfileEdge, 4 + ProfileLoop, TruckGap, evaluate_extrude, 5 + }; 6 + use bone_types::{ 7 + Angle, FeatureId, Length, Plane3, Point2, PositiveLength, SketchEntityId, SketchId, Tolerance, 8 + UnitVec3, degree, millimeter, 9 + }; 10 + use slotmap::{Key, SlotMap}; 11 + 12 + const TOLERANCE: Tolerance = Tolerance::new(1.0e-9); 13 + 14 + struct Ids { 15 + features: SlotMap<FeatureId, ()>, 16 + entities: SlotMap<SketchEntityId, ()>, 17 + } 18 + 19 + impl Ids { 20 + fn new() -> Self { 21 + Self { 22 + features: SlotMap::with_key(), 23 + entities: SlotMap::with_key(), 24 + } 25 + } 26 + 27 + fn feature(&mut self) -> FeatureId { 28 + self.features.insert(()) 29 + } 30 + 31 + fn entity(&mut self) -> SketchEntityId { 32 + self.entities.insert(()) 33 + } 34 + } 35 + 36 + fn length_mm(value: f64) -> Length { 37 + Length::new::<millimeter>(value) 38 + } 39 + 40 + fn point(x: f64, y: f64) -> Point2 { 41 + Point2::from_mm(x, y) 42 + } 43 + 44 + fn line(a: Point2, b: Point2) -> Curve2Kind { 45 + let Ok(segment) = Line2::new(a, b, TOLERANCE) else { 46 + panic!("line endpoints are distinct"); 47 + }; 48 + Curve2Kind::Line(segment) 49 + } 50 + 51 + fn circle(center: Point2, radius: f64) -> Curve2Kind { 52 + let Ok(disk) = Circle2::new(center, length_mm(radius), TOLERANCE) else { 53 + panic!("circle radius is positive"); 54 + }; 55 + Curve2Kind::Circle(disk) 56 + } 57 + 58 + fn arc(center: Point2, radius: f64, start_deg: f64, sweep_deg: f64) -> Curve2Kind { 59 + let Ok(segment) = Arc2::new( 60 + center, 61 + length_mm(radius), 62 + Angle::new::<degree>(start_deg), 63 + Angle::new::<degree>(sweep_deg), 64 + TOLERANCE, 65 + ) else { 66 + panic!("arc sweep is within range"); 67 + }; 68 + Curve2Kind::Arc(segment) 69 + } 70 + 71 + fn xy_plane() -> Plane3 { 72 + let Ok(plane) = Plane3::new( 73 + bone_types::Point3::origin(), 74 + UnitVec3::x_axis(), 75 + UnitVec3::y_axis(), 76 + TOLERANCE, 77 + ) else { 78 + panic!("x and y axes are orthonormal"); 79 + }; 80 + plane 81 + } 82 + 83 + fn rectangle(ids: &mut Ids, x0: f64, y0: f64, width: f64, height: f64) -> ProfileLoop { 84 + let corners = [ 85 + point(x0, y0), 86 + point(x0 + width, y0), 87 + point(x0 + width, y0 + height), 88 + point(x0, y0 + height), 89 + ]; 90 + let edges = (0..4) 91 + .map(|index| { 92 + let start = corners[index]; 93 + let end = corners[(index + 1) % 4]; 94 + let corner = ids.entity(); 95 + let segment = ids.entity(); 96 + ProfileEdge::new(line(start, end), segment, corner) 97 + }) 98 + .collect(); 99 + ProfileLoop::Open(edges) 100 + } 101 + 102 + fn circle_loop(ids: &mut Ids, center: Point2, radius: f64) -> ProfileLoop { 103 + let entity = ids.entity(); 104 + ProfileLoop::Closed { 105 + curve: circle(center, radius), 106 + curve_entity: entity, 107 + } 108 + } 109 + 110 + fn polygon(ids: &mut Ids, corners: &[Point2]) -> ProfileLoop { 111 + let count = corners.len(); 112 + let edges = (0..count) 113 + .map(|index| { 114 + let corner = ids.entity(); 115 + let segment = ids.entity(); 116 + ProfileEdge::new(line(corners[index], corners[(index + 1) % count]), segment, corner) 117 + }) 118 + .collect(); 119 + ProfileLoop::Open(edges) 120 + } 121 + 122 + fn blind(depth: f64) -> ExtrudeFeature { 123 + feature(ExtrudeEndCondition::Blind { 124 + depth: positive(depth), 125 + }) 126 + } 127 + 128 + fn midplane(depth: f64) -> ExtrudeFeature { 129 + feature(ExtrudeEndCondition::MidPlane { 130 + depth: positive(depth), 131 + }) 132 + } 133 + 134 + fn feature(end_condition: ExtrudeEndCondition) -> ExtrudeFeature { 135 + ExtrudeFeature { 136 + sketch: SketchId::null(), 137 + direction: ExtrudeDirection::Normal { 138 + sense: ExtrudeSense::Forward, 139 + }, 140 + end_condition, 141 + draft: None, 142 + thin_wall: None, 143 + merge_result: MergeResult::Merge, 144 + } 145 + } 146 + 147 + fn positive(depth: f64) -> PositiveLength { 148 + let Ok(value) = PositiveLength::new(length_mm(depth)) else { 149 + panic!("{depth} mm is a positive length"); 150 + }; 151 + value 152 + } 153 + 154 + fn evaluate(ids: &mut Ids, profile: &ExtrudeProfile, feature: &ExtrudeFeature) -> BrepSolid { 155 + let extrude = ids.feature(); 156 + let Ok(solid) = evaluate_extrude(extrude, profile, feature) else { 157 + panic!("the profile and feature describe a buildable extrude"); 158 + }; 159 + assert!( 160 + solid.validate(TOLERANCE).is_ok(), 161 + "extrude is a closed solid" 162 + ); 163 + solid 164 + } 165 + 166 + fn dump(solid: &BrepSolid) -> String { 167 + let faces = solid 168 + .iter_faces() 169 + .map(|face| format!(" {}", face.label())) 170 + .collect::<Vec<_>>() 171 + .join("\n"); 172 + let edges = solid 173 + .iter_edges() 174 + .map(|edge| format!(" {}", edge.label())) 175 + .collect::<Vec<_>>() 176 + .join("\n"); 177 + let vertices = solid 178 + .iter_vertices() 179 + .map(|vertex| format!(" {}", vertex.label())) 180 + .collect::<Vec<_>>() 181 + .join("\n"); 182 + let counts = format!( 183 + "counts: faces={} edges={} vertices={} loops={}", 184 + solid.iter_faces().count(), 185 + solid.iter_edges().count(), 186 + solid.iter_vertices().count(), 187 + solid.iter_loops().count(), 188 + ); 189 + let bbox = solid.bounding_box().map_or_else( 190 + || "bbox: none".to_owned(), 191 + |aabb| { 192 + let (min_x, min_y, min_z) = aabb.min().coords_mm(); 193 + let (max_x, max_y, max_z) = aabb.max().coords_mm(); 194 + format!( 195 + "bbox: min=({min_x:.3}, {min_y:.3}, {min_z:.3}) max=({max_x:.3}, {max_y:.3}, {max_z:.3})" 196 + ) 197 + }, 198 + ); 199 + format!("{counts}\n{bbox}\nfaces:\n{faces}\nedges:\n{edges}\nvertices:\n{vertices}") 200 + } 201 + 202 + #[test] 203 + fn unit_cube_extrude() { 204 + let mut ids = Ids::new(); 205 + let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 1.0, 1.0)]); 206 + let solid = evaluate(&mut ids, &profile, &blind(1.0)); 207 + insta::assert_snapshot!(dump(&solid)); 208 + } 209 + 210 + #[test] 211 + fn cylinder_extrude() { 212 + let mut ids = Ids::new(); 213 + let profile = ExtrudeProfile::new( 214 + xy_plane(), 215 + vec![circle_loop(&mut ids, point(0.0, 0.0), 5.0)], 216 + ); 217 + let solid = evaluate(&mut ids, &profile, &blind(10.0)); 218 + insta::assert_snapshot!(dump(&solid)); 219 + } 220 + 221 + #[test] 222 + fn donut_extrude() { 223 + let mut ids = Ids::new(); 224 + let outer = circle_loop(&mut ids, point(0.0, 0.0), 10.0); 225 + let inner = circle_loop(&mut ids, point(0.0, 0.0), 4.0); 226 + let profile = ExtrudeProfile::new(xy_plane(), vec![outer, inner]); 227 + let solid = evaluate(&mut ids, &profile, &blind(6.0)); 228 + insta::assert_snapshot!(dump(&solid)); 229 + } 230 + 231 + #[test] 232 + fn rectangle_with_two_holes_extrude() { 233 + let mut ids = Ids::new(); 234 + let outer = rectangle(&mut ids, 0.0, 0.0, 10.0, 6.0); 235 + let hole_a = rectangle(&mut ids, 1.0, 1.0, 2.0, 2.0); 236 + let hole_b = rectangle(&mut ids, 6.0, 2.0, 2.0, 2.0); 237 + let profile = ExtrudeProfile::new(xy_plane(), vec![outer, hole_a, hole_b]); 238 + let solid = evaluate(&mut ids, &profile, &blind(3.0)); 239 + insta::assert_snapshot!(dump(&solid)); 240 + } 241 + 242 + #[test] 243 + fn half_disk_extrude() { 244 + let mut ids = Ids::new(); 245 + let arc_corner = ids.entity(); 246 + let arc_entity = ids.entity(); 247 + let line_corner = ids.entity(); 248 + let line_entity = ids.entity(); 249 + let curved = ProfileEdge::new(arc(point(0.0, 0.0), 5.0, 0.0, 180.0), arc_entity, arc_corner); 250 + let chord = ProfileEdge::new( 251 + line(point(-5.0, 0.0), point(5.0, 0.0)), 252 + line_entity, 253 + line_corner, 254 + ); 255 + let profile = ExtrudeProfile::new(xy_plane(), vec![ProfileLoop::Open(vec![curved, chord])]); 256 + let solid = evaluate(&mut ids, &profile, &blind(2.0)); 257 + insta::assert_snapshot!(dump(&solid)); 258 + } 259 + 260 + #[test] 261 + fn wide_arc_wedge_extrude() { 262 + let mut ids = Ids::new(); 263 + let arc_corner = ids.entity(); 264 + let arc_entity = ids.entity(); 265 + let down_corner = ids.entity(); 266 + let down_entity = ids.entity(); 267 + let out_corner = ids.entity(); 268 + let out_entity = ids.entity(); 269 + let curved = ProfileEdge::new(arc(point(0.0, 0.0), 5.0, 0.0, 270.0), arc_entity, arc_corner); 270 + let radius_down = ProfileEdge::new( 271 + line(point(0.0, -5.0), point(0.0, 0.0)), 272 + down_entity, 273 + down_corner, 274 + ); 275 + let radius_out = ProfileEdge::new(line(point(0.0, 0.0), point(5.0, 0.0)), out_entity, out_corner); 276 + let profile = ExtrudeProfile::new( 277 + xy_plane(), 278 + vec![ProfileLoop::Open(vec![curved, radius_down, radius_out])], 279 + ); 280 + let solid = evaluate(&mut ids, &profile, &blind(2.0)); 281 + insta::assert_snapshot!(dump(&solid)); 282 + } 283 + 284 + #[test] 285 + fn midplane_cube_is_centered() { 286 + let mut ids = Ids::new(); 287 + let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 2.0)]); 288 + let solid = evaluate(&mut ids, &profile, &midplane(4.0)); 289 + let Some(bbox) = solid.bounding_box() else { 290 + panic!("solid has geometry"); 291 + }; 292 + let (_, _, min_z) = bbox.min().coords_mm(); 293 + let (_, _, max_z) = bbox.max().coords_mm(); 294 + assert!( 295 + (min_z + 2.0).abs() < 1.0e-6, 296 + "midplane base sits at -depth/2" 297 + ); 298 + assert!( 299 + (max_z - 2.0).abs() < 1.0e-6, 300 + "midplane top sits at +depth/2" 301 + ); 302 + } 303 + 304 + #[test] 305 + fn evaluation_is_deterministic() { 306 + let mut ids = Ids::new(); 307 + let plane = xy_plane(); 308 + let cube = ExtrudeProfile::new(plane, vec![rectangle(&mut ids, 0.0, 0.0, 1.0, 1.0)]); 309 + let cylinder = ExtrudeProfile::new(plane, vec![circle_loop(&mut ids, point(0.0, 0.0), 5.0)]); 310 + let extrude = ids.feature(); 311 + [cube, cylinder].iter().for_each(|profile| { 312 + let Ok(first) = evaluate_extrude(extrude, profile, &blind(3.0)) else { 313 + panic!("profile extrudes"); 314 + }; 315 + let Ok(second) = evaluate_extrude(extrude, profile, &blind(3.0)) else { 316 + panic!("profile extrudes"); 317 + }; 318 + assert_eq!(dump(&first), dump(&second)); 319 + }); 320 + } 321 + 322 + proptest::proptest! { 323 + #[test] 324 + fn varied_rectangles_are_deterministic( 325 + width in 0.5f64..50.0, 326 + height in 0.5f64..50.0, 327 + depth in 0.5f64..50.0, 328 + ) { 329 + let mut ids = Ids::new(); 330 + let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, width, height)]); 331 + let extrude = ids.feature(); 332 + let feature = blind(depth); 333 + let Ok(first) = evaluate_extrude(extrude, &profile, &feature) else { 334 + panic!("rectangle extrudes"); 335 + }; 336 + let Ok(second) = evaluate_extrude(extrude, &profile, &feature) else { 337 + panic!("rectangle extrudes"); 338 + }; 339 + proptest::prop_assert_eq!(dump(&first), dump(&second)); 340 + } 341 + } 342 + 343 + #[test] 344 + fn open_loop_is_rejected() { 345 + let mut ids = Ids::new(); 346 + let segment = ids.entity(); 347 + let corner = ids.entity(); 348 + let open = ProfileLoop::Open(vec![ProfileEdge::new( 349 + line(point(0.0, 0.0), point(1.0, 0.0)), 350 + segment, 351 + corner, 352 + )]); 353 + let profile = ExtrudeProfile::new(xy_plane(), vec![open]); 354 + let extrude = ids.feature(); 355 + assert!(matches!( 356 + evaluate_extrude(extrude, &profile, &blind(1.0)), 357 + Err(BrepError::InvalidProfile { 358 + reason: ProfileDefect::OpenLoop 359 + }) 360 + )); 361 + } 362 + 363 + #[test] 364 + fn disjoint_loops_are_rejected() { 365 + let mut ids = Ids::new(); 366 + let near = rectangle(&mut ids, 0.0, 0.0, 4.0, 4.0); 367 + let far = rectangle(&mut ids, 10.0, 10.0, 2.0, 2.0); 368 + let profile = ExtrudeProfile::new(xy_plane(), vec![near, far]); 369 + let extrude = ids.feature(); 370 + assert!(matches!( 371 + evaluate_extrude(extrude, &profile, &blind(1.0)), 372 + Err(BrepError::InvalidProfile { 373 + reason: ProfileDefect::UncontainedLoop 374 + }) 375 + )); 376 + } 377 + 378 + #[test] 379 + fn near_zero_depth_is_rejected() { 380 + let mut ids = Ids::new(); 381 + let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 1.0, 1.0)]); 382 + let extrude = ids.feature(); 383 + assert!(matches!( 384 + evaluate_extrude(extrude, &profile, &blind(1.0e-9)), 385 + Err(BrepError::EmptyExtrudeDepth) 386 + )); 387 + } 388 + 389 + #[test] 390 + fn deferred_paths_report_truck_gap() { 391 + let mut ids = Ids::new(); 392 + let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 1.0, 1.0)]); 393 + let extrude = ids.feature(); 394 + let through_all = feature(ExtrudeEndCondition::ThroughAll); 395 + assert!(matches!( 396 + evaluate_extrude(extrude, &profile, &through_all), 397 + Err(BrepError::TruckUnsupported { 398 + detail: TruckGap::ThroughAll 399 + }) 400 + )); 401 + } 402 + 403 + #[test] 404 + fn self_intersecting_loop_is_rejected() { 405 + let mut ids = Ids::new(); 406 + let bowtie = polygon( 407 + &mut ids, 408 + &[point(0.0, 0.0), point(4.0, 0.0), point(1.0, 3.0), point(3.0, 3.0)], 409 + ); 410 + let profile = ExtrudeProfile::new(xy_plane(), vec![bowtie]); 411 + let extrude = ids.feature(); 412 + assert!(matches!( 413 + evaluate_extrude(extrude, &profile, &blind(2.0)), 414 + Err(BrepError::InvalidProfile { 415 + reason: ProfileDefect::SelfIntersectingLoop 416 + }) 417 + )); 418 + } 419 + 420 + #[test] 421 + fn overlapping_holes_are_rejected() { 422 + let mut ids = Ids::new(); 423 + let outer = rectangle(&mut ids, 0.0, 0.0, 20.0, 20.0); 424 + let hole_a = rectangle(&mut ids, 4.0, 4.0, 8.0, 8.0); 425 + let hole_b = rectangle(&mut ids, 8.0, 8.0, 8.0, 8.0); 426 + let profile = ExtrudeProfile::new(xy_plane(), vec![outer, hole_a, hole_b]); 427 + let extrude = ids.feature(); 428 + assert!(matches!( 429 + evaluate_extrude(extrude, &profile, &blind(2.0)), 430 + Err(BrepError::InvalidProfile { 431 + reason: ProfileDefect::OverlappingLoops 432 + }) 433 + )); 434 + } 435 + 436 + #[test] 437 + fn nested_holes_are_rejected() { 438 + let mut ids = Ids::new(); 439 + let outer = circle_loop(&mut ids, point(0.0, 0.0), 10.0); 440 + let hole = circle_loop(&mut ids, point(0.0, 0.0), 6.0); 441 + let nested = circle_loop(&mut ids, point(0.0, 0.0), 3.0); 442 + let profile = ExtrudeProfile::new(xy_plane(), vec![outer, hole, nested]); 443 + let extrude = ids.feature(); 444 + assert!(matches!( 445 + evaluate_extrude(extrude, &profile, &blind(2.0)), 446 + Err(BrepError::InvalidProfile { 447 + reason: ProfileDefect::OverlappingLoops 448 + }) 449 + )); 450 + } 451 + 452 + #[test] 453 + fn hole_near_outer_wall_is_accepted() { 454 + let mut ids = Ids::new(); 455 + let outer = circle_loop(&mut ids, point(0.0, 0.0), 10.0); 456 + let hole = polygon( 457 + &mut ids, 458 + &[ 459 + point(9.932, 0.978), 460 + point(9.6, 0.978), 461 + point(9.6, 0.6), 462 + point(9.932, 0.6), 463 + ], 464 + ); 465 + let profile = ExtrudeProfile::new(xy_plane(), vec![outer, hole]); 466 + let solid = evaluate(&mut ids, &profile, &blind(2.0)); 467 + assert_eq!(solid.iter_faces().count(), 7); 468 + } 469 + 470 + #[test] 471 + fn full_turn_arc_as_closed_loop_builds() { 472 + let mut ids = Ids::new(); 473 + let entity = ids.entity(); 474 + let profile = ExtrudeProfile::new( 475 + xy_plane(), 476 + vec![ProfileLoop::Closed { 477 + curve: arc(point(0.0, 0.0), 5.0, 0.0, 360.0), 478 + curve_entity: entity, 479 + }], 480 + ); 481 + let solid = evaluate(&mut ids, &profile, &blind(10.0)); 482 + assert_eq!(solid.iter_faces().count(), 3); 483 + assert_eq!(solid.iter_vertices().count(), 2); 484 + } 485 + 486 + #[test] 487 + fn full_turn_arc_in_open_chain_is_rejected() { 488 + let mut ids = Ids::new(); 489 + let segment = ids.entity(); 490 + let corner = ids.entity(); 491 + let loop_ = ProfileLoop::Open(vec![ProfileEdge::new( 492 + arc(point(0.0, 0.0), 5.0, 0.0, 360.0), 493 + segment, 494 + corner, 495 + )]); 496 + let profile = ExtrudeProfile::new(xy_plane(), vec![loop_]); 497 + let extrude = ids.feature(); 498 + assert!(matches!( 499 + evaluate_extrude(extrude, &profile, &blind(2.0)), 500 + Err(BrepError::InvalidProfile { 501 + reason: ProfileDefect::OpenLoop 502 + }) 503 + )); 504 + }
+17
crates/bone-kernel/tests/snapshots/extrude__cylinder_extrude.snap
··· 1 + --- 2 + source: crates/bone-kernel/tests/extrude.rs 3 + expression: dump(&solid) 4 + --- 5 + counts: faces=3 edges=3 vertices=2 loops=4 6 + bbox: min=(-5.000, -5.000, 0.000) max=(5.000, 5.000, 10.000) 7 + faces: 8 + face[FeatureId(1v1)]:start_cap 9 + face[FeatureId(1v1)]:side(loop#0, from=SketchEntityId(1v1)) 10 + face[FeatureId(1v1)]:end_cap 11 + edges: 12 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(1v1)) 13 + edge[FeatureId(1v1)]:side_edge(seam, from=SketchEntityId(1v1)) 14 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(1v1)) 15 + vertices: 16 + vertex[FeatureId(1v1)]:start_cap_vertex(seam, from=SketchEntityId(1v1)) 17 + vertex[FeatureId(1v1)]:end_cap_vertex(seam, from=SketchEntityId(1v1))
+23
crates/bone-kernel/tests/snapshots/extrude__donut_extrude.snap
··· 1 + --- 2 + source: crates/bone-kernel/tests/extrude.rs 3 + expression: dump(&solid) 4 + --- 5 + counts: faces=4 edges=6 vertices=4 loops=8 6 + bbox: min=(-10.000, -10.000, 0.000) max=(10.000, 10.000, 6.000) 7 + faces: 8 + face[FeatureId(1v1)]:start_cap 9 + face[FeatureId(1v1)]:side(loop#0, from=SketchEntityId(1v1)) 10 + face[FeatureId(1v1)]:side(loop#1, from=SketchEntityId(2v1)) 11 + face[FeatureId(1v1)]:end_cap 12 + edges: 13 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(1v1)) 14 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(2v1)) 15 + edge[FeatureId(1v1)]:side_edge(seam, from=SketchEntityId(1v1)) 16 + edge[FeatureId(1v1)]:side_edge(seam, from=SketchEntityId(2v1)) 17 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(1v1)) 18 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(2v1)) 19 + vertices: 20 + vertex[FeatureId(1v1)]:start_cap_vertex(seam, from=SketchEntityId(1v1)) 21 + vertex[FeatureId(1v1)]:start_cap_vertex(seam, from=SketchEntityId(2v1)) 22 + vertex[FeatureId(1v1)]:end_cap_vertex(seam, from=SketchEntityId(1v1)) 23 + vertex[FeatureId(1v1)]:end_cap_vertex(seam, from=SketchEntityId(2v1))
+23
crates/bone-kernel/tests/snapshots/extrude__half_disk_extrude.snap
··· 1 + --- 2 + source: crates/bone-kernel/tests/extrude.rs 3 + expression: dump(&solid) 4 + --- 5 + counts: faces=4 edges=6 vertices=4 loops=4 6 + bbox: min=(-5.000, 0.000, 0.000) max=(5.000, 5.000, 2.000) 7 + faces: 8 + face[FeatureId(1v1)]:start_cap 9 + face[FeatureId(1v1)]:side(loop#0, from=SketchEntityId(2v1)) 10 + face[FeatureId(1v1)]:side(loop#0, from=SketchEntityId(4v1)) 11 + face[FeatureId(1v1)]:end_cap 12 + edges: 13 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(2v1)) 14 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(4v1)) 15 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(1v1)) 16 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(3v1)) 17 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(2v1)) 18 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(4v1)) 19 + vertices: 20 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(1v1)) 21 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(3v1)) 22 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(1v1)) 23 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(3v1))
+83
crates/bone-kernel/tests/snapshots/extrude__rectangle_with_two_holes_extrude.snap
··· 1 + --- 2 + source: crates/bone-kernel/tests/extrude.rs 3 + expression: dump(&solid) 4 + --- 5 + counts: faces=14 edges=36 vertices=24 loops=18 6 + bbox: min=(0.000, 0.000, 0.000) max=(10.000, 6.000, 3.000) 7 + faces: 8 + face[FeatureId(1v1)]:start_cap 9 + face[FeatureId(1v1)]:side(loop#0, from=SketchEntityId(2v1)) 10 + face[FeatureId(1v1)]:side(loop#0, from=SketchEntityId(4v1)) 11 + face[FeatureId(1v1)]:side(loop#0, from=SketchEntityId(6v1)) 12 + face[FeatureId(1v1)]:side(loop#0, from=SketchEntityId(8v1)) 13 + face[FeatureId(1v1)]:side(loop#1, from=SketchEntityId(10v1)) 14 + face[FeatureId(1v1)]:side(loop#1, from=SketchEntityId(12v1)) 15 + face[FeatureId(1v1)]:side(loop#1, from=SketchEntityId(14v1)) 16 + face[FeatureId(1v1)]:side(loop#1, from=SketchEntityId(16v1)) 17 + face[FeatureId(1v1)]:side(loop#2, from=SketchEntityId(18v1)) 18 + face[FeatureId(1v1)]:side(loop#2, from=SketchEntityId(20v1)) 19 + face[FeatureId(1v1)]:side(loop#2, from=SketchEntityId(22v1)) 20 + face[FeatureId(1v1)]:side(loop#2, from=SketchEntityId(24v1)) 21 + face[FeatureId(1v1)]:end_cap 22 + edges: 23 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(2v1)) 24 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(4v1)) 25 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(6v1)) 26 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(8v1)) 27 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(10v1)) 28 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(12v1)) 29 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(14v1)) 30 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(16v1)) 31 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(18v1)) 32 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(20v1)) 33 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(22v1)) 34 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(24v1)) 35 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(1v1)) 36 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(3v1)) 37 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(5v1)) 38 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(7v1)) 39 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(9v1)) 40 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(11v1)) 41 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(13v1)) 42 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(15v1)) 43 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(17v1)) 44 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(19v1)) 45 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(21v1)) 46 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(23v1)) 47 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(2v1)) 48 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(4v1)) 49 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(6v1)) 50 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(8v1)) 51 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(10v1)) 52 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(12v1)) 53 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(14v1)) 54 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(16v1)) 55 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(18v1)) 56 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(20v1)) 57 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(22v1)) 58 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(24v1)) 59 + vertices: 60 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(1v1)) 61 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(3v1)) 62 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(5v1)) 63 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(7v1)) 64 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(9v1)) 65 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(11v1)) 66 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(13v1)) 67 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(15v1)) 68 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(17v1)) 69 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(19v1)) 70 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(21v1)) 71 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(23v1)) 72 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(1v1)) 73 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(3v1)) 74 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(5v1)) 75 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(7v1)) 76 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(9v1)) 77 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(11v1)) 78 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(13v1)) 79 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(15v1)) 80 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(17v1)) 81 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(19v1)) 82 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(21v1)) 83 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(23v1))
+35
crates/bone-kernel/tests/snapshots/extrude__unit_cube_extrude.snap
··· 1 + --- 2 + source: crates/bone-kernel/tests/extrude.rs 3 + expression: dump(&solid) 4 + --- 5 + counts: faces=6 edges=12 vertices=8 loops=6 6 + bbox: min=(0.000, 0.000, 0.000) max=(1.000, 1.000, 1.000) 7 + faces: 8 + face[FeatureId(1v1)]:start_cap 9 + face[FeatureId(1v1)]:side(loop#0, from=SketchEntityId(2v1)) 10 + face[FeatureId(1v1)]:side(loop#0, from=SketchEntityId(4v1)) 11 + face[FeatureId(1v1)]:side(loop#0, from=SketchEntityId(6v1)) 12 + face[FeatureId(1v1)]:side(loop#0, from=SketchEntityId(8v1)) 13 + face[FeatureId(1v1)]:end_cap 14 + edges: 15 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(2v1)) 16 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(4v1)) 17 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(6v1)) 18 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(8v1)) 19 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(1v1)) 20 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(3v1)) 21 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(5v1)) 22 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(7v1)) 23 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(2v1)) 24 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(4v1)) 25 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(6v1)) 26 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(8v1)) 27 + vertices: 28 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(1v1)) 29 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(3v1)) 30 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(5v1)) 31 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(7v1)) 32 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(1v1)) 33 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(3v1)) 34 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(5v1)) 35 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(7v1))
+29
crates/bone-kernel/tests/snapshots/extrude__wide_arc_wedge_extrude.snap
··· 1 + --- 2 + source: crates/bone-kernel/tests/extrude.rs 3 + expression: dump(&solid) 4 + --- 5 + counts: faces=5 edges=9 vertices=6 loops=5 6 + bbox: min=(-5.000, -5.000, 0.000) max=(5.000, 5.000, 2.000) 7 + faces: 8 + face[FeatureId(1v1)]:start_cap 9 + face[FeatureId(1v1)]:side(loop#0, from=SketchEntityId(2v1)) 10 + face[FeatureId(1v1)]:side(loop#0, from=SketchEntityId(4v1)) 11 + face[FeatureId(1v1)]:side(loop#0, from=SketchEntityId(6v1)) 12 + face[FeatureId(1v1)]:end_cap 13 + edges: 14 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(2v1)) 15 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(4v1)) 16 + edge[FeatureId(1v1)]:start_cap_edge(from=SketchEntityId(6v1)) 17 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(1v1)) 18 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(3v1)) 19 + edge[FeatureId(1v1)]:side_edge(corner, from=SketchEntityId(5v1)) 20 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(2v1)) 21 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(4v1)) 22 + edge[FeatureId(1v1)]:end_cap_edge(from=SketchEntityId(6v1)) 23 + vertices: 24 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(1v1)) 25 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(3v1)) 26 + vertex[FeatureId(1v1)]:start_cap_vertex(corner, from=SketchEntityId(5v1)) 27 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(1v1)) 28 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(3v1)) 29 + vertex[FeatureId(1v1)]:end_cap_vertex(corner, from=SketchEntityId(5v1))