Another project
0

Configure Feed

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

test(kernel): extrude & edge render snapshots

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

author
Lewis
date (Jun 1, 2026, 2:53 PM +0300) commit 6671610e parent d8666a3b change-id zqpnossk
+334 -19
+16 -13
crates/bone-kernel/tests/edges_for_render.rs
··· 7 7 Angle, ChordHeightTolerance, EdgeRole, FeatureId, Length, Plane3, Point2, PositiveLength, 8 8 SideKind, SketchEntityId, SketchId, Tolerance, UnitVec3, degree, millimeter, 9 9 }; 10 - use proptest::prelude::*; 11 10 use core::f64::consts::{FRAC_PI_2, PI}; 11 + use proptest::prelude::*; 12 12 use slotmap::{Key, SlotMap}; 13 13 14 14 const TOLERANCE: Tolerance = Tolerance::new(1.0e-9); ··· 198 198 let polylines = solid.edges_for_render(chord()); 199 199 assert_eq!(polylines.len(), 12); 200 200 polylines.iter().for_each(|edge| { 201 - assert!(matches!(edge.curve(), EdgeCurve3::Line(_)), "cube edge is a line"); 201 + assert!( 202 + matches!(edge.curve(), EdgeCurve3::Line(_)), 203 + "cube edge is a line" 204 + ); 202 205 assert!(edge.points().len() >= 2); 203 206 let crease = edge.crease(); 204 207 assert!( ··· 225 228 "cylinder emits two cap edges, seam side edge is filtered" 226 229 ); 227 230 kinds.iter().for_each(|kind| { 228 - assert!(matches!(kind, EdgeCurve3::Circle(_)), "cap edge is a circle"); 231 + assert!( 232 + matches!(kind, EdgeCurve3::Circle(_)), 233 + "cap edge is a circle" 234 + ); 229 235 }); 230 236 polylines.iter().for_each(|edge| { 231 237 let role = edge.label().role; ··· 263 269 } 264 270 ) 265 271 }); 266 - assert!(!seam_emitted, "seam side edges are filtered from render output"); 272 + assert!( 273 + !seam_emitted, 274 + "seam side edges are filtered from render output" 275 + ); 267 276 } 268 277 269 278 #[test] ··· 404 413 xy_plane(), 405 414 vec![triangle( 406 415 &mut ids, 407 - [ 408 - point(0.0, 0.0), 409 - point(1.0, 0.0), 410 - point(0.5, half), 411 - ], 416 + [point(0.0, 0.0), point(1.0, 0.0), point(0.5, half)], 412 417 )], 413 418 ); 414 419 let solid = evaluate(&mut ids, &profile, &blind(1.0)); ··· 558 563 ); 559 564 }); 560 565 let render = solid.edges_for_render(chord()); 561 - let edges_by_id: std::collections::HashMap<_, _> = solid 562 - .iter_edges() 563 - .map(|edge| (edge.id(), edge)) 564 - .collect(); 566 + let edges_by_id: std::collections::HashMap<_, _> = 567 + solid.iter_edges().map(|edge| (edge.id(), edge)).collect(); 565 568 render.iter().for_each(|polyline| { 566 569 let edge = edges_by_id[&polyline.edge()]; 567 570 let [start_id, _] = edge.vertices();
+315 -5
crates/bone-kernel/tests/extrude.rs
··· 1 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, 2 + Arc2, BrepEdge, BrepError, BrepFace, BrepSolid, BrepVertex, Circle2, Curve2Kind, 3 + ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeProfile, ExtrudeSense, Line2, 4 + MergeResult, ProfileDefect, ProfileEdge, ProfileLoop, TruckGap, evaluate_extrude, 5 5 }; 6 6 use bone_types::{ 7 - Angle, FeatureId, Length, Plane3, Point2, PositiveLength, SketchEntityId, SketchId, Tolerance, 8 - UnitVec3, degree, millimeter, 7 + Angle, EdgeLabel, FaceLabel, FaceRole, FeatureId, Length, Plane3, Point2, PositiveLength, 8 + SketchEntityId, SketchId, Tolerance, UnitVec3, VertexLabel, VertexRole, degree, millimeter, 9 9 }; 10 10 use slotmap::{Key, SlotMap}; 11 11 ··· 99 99 ProfileLoop::Open(edges) 100 100 } 101 101 102 + fn rectangle_from( 103 + x0: f64, 104 + y0: f64, 105 + width: f64, 106 + height: f64, 107 + edge_ids: [SketchEntityId; 4], 108 + corner_ids: [SketchEntityId; 4], 109 + ) -> ProfileLoop { 110 + let corners = [ 111 + point(x0, y0), 112 + point(x0 + width, y0), 113 + point(x0 + width, y0 + height), 114 + point(x0, y0 + height), 115 + ]; 116 + let edges = (0..4) 117 + .map(|index| { 118 + let start = corners[index]; 119 + let end = corners[(index + 1) % 4]; 120 + ProfileEdge::new(line(start, end), edge_ids[index], corner_ids[index]) 121 + }) 122 + .collect(); 123 + ProfileLoop::Open(edges) 124 + } 125 + 102 126 fn circle_loop(ids: &mut Ids, center: Point2, radius: f64) -> ProfileLoop { 103 127 let entity = ids.entity(); 104 128 ProfileLoop::Closed { ··· 167 191 solid 168 192 } 169 193 194 + fn face_labels(solid: &BrepSolid) -> Vec<FaceLabel> { 195 + solid.iter_faces().map(BrepFace::label).collect() 196 + } 197 + 198 + fn edge_labels(solid: &BrepSolid) -> Vec<EdgeLabel> { 199 + solid.iter_edges().map(BrepEdge::label).collect() 200 + } 201 + 202 + fn vertex_labels(solid: &BrepSolid) -> Vec<VertexLabel> { 203 + solid.iter_vertices().map(BrepVertex::label).collect() 204 + } 205 + 206 + fn side_provenance(solid: &BrepSolid) -> Vec<Option<SketchEntityId>> { 207 + solid 208 + .iter_faces() 209 + .map(|face| match face.label().role { 210 + FaceRole::Side { from, .. } => Some(from), 211 + _ => None, 212 + }) 213 + .collect() 214 + } 215 + 170 216 fn dump(solid: &BrepSolid) -> String { 171 217 let faces = solid 172 218 .iter_faces() ··· 354 400 }; 355 401 proptest::prop_assert_eq!(dump(&first), dump(&second)); 356 402 } 403 + } 404 + 405 + proptest::proptest! { 406 + #[test] 407 + fn rectangle_label_set_is_stable_across_runs( 408 + width in 0.5f64..50.0, 409 + height in 0.5f64..50.0, 410 + depth in 0.5f64..50.0, 411 + ) { 412 + let mut ids = Ids::new(); 413 + let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, width, height)]); 414 + let extrude = ids.feature(); 415 + let feature = blind(depth); 416 + let Ok(first) = evaluate_extrude(extrude, &profile, &feature) else { 417 + panic!("rectangle extrudes"); 418 + }; 419 + let Ok(second) = evaluate_extrude(extrude, &profile, &feature) else { 420 + panic!("rectangle extrudes"); 421 + }; 422 + proptest::prop_assert_eq!(face_labels(&first), face_labels(&second)); 423 + proptest::prop_assert_eq!(edge_labels(&first), edge_labels(&second)); 424 + proptest::prop_assert_eq!(vertex_labels(&first), vertex_labels(&second)); 425 + } 426 + } 427 + 428 + #[test] 429 + fn dimension_edit_preserves_label_provenance() { 430 + let mut ids = Ids::new(); 431 + let edge_ids = [ids.entity(), ids.entity(), ids.entity(), ids.entity()]; 432 + let corner_ids = [ids.entity(), ids.entity(), ids.entity(), ids.entity()]; 433 + let extrude = ids.feature(); 434 + 435 + let narrow = ExtrudeProfile::new( 436 + xy_plane(), 437 + vec![rectangle_from(0.0, 0.0, 10.0, 6.0, edge_ids, corner_ids)], 438 + ); 439 + let wide = ExtrudeProfile::new( 440 + xy_plane(), 441 + vec![rectangle_from(0.0, 0.0, 25.0, 6.0, edge_ids, corner_ids)], 442 + ); 443 + 444 + let Ok(before) = evaluate_extrude(extrude, &narrow, &blind(3.0)) else { 445 + panic!("narrow rectangle extrudes"); 446 + }; 447 + let Ok(after) = evaluate_extrude(extrude, &wide, &blind(3.0)) else { 448 + panic!("wide rectangle extrudes"); 449 + }; 450 + 451 + assert_eq!(face_labels(&before), face_labels(&after)); 452 + assert_eq!(edge_labels(&before), edge_labels(&after)); 453 + assert_eq!(vertex_labels(&before), vertex_labels(&after)); 454 + 455 + assert_eq!(side_provenance(&before), side_provenance(&after)); 456 + let sides: Vec<SketchEntityId> = side_provenance(&after).into_iter().flatten().collect(); 457 + assert_eq!(sides.len(), 4); 458 + assert!(sides.iter().all(|id| edge_ids.contains(id))); 459 + } 460 + 461 + fn blob_round_trip(solid: &BrepSolid) -> BrepSolid { 462 + let Ok(blob) = solid.to_blob() else { 463 + panic!("solid serializes to a blob"); 464 + }; 465 + match BrepSolid::from_blob(&blob, solid.reattach_data()) { 466 + Ok(restored) => restored, 467 + Err(error) => panic!("blob reattach failed: {error}"), 468 + } 469 + } 470 + 471 + fn assert_blob_round_trip(solid: &BrepSolid) { 472 + let restored = blob_round_trip(solid); 473 + assert_eq!(face_labels(solid), face_labels(&restored)); 474 + assert_eq!(edge_labels(solid), edge_labels(&restored)); 475 + assert_eq!(vertex_labels(solid), vertex_labels(&restored)); 476 + assert_eq!(solid.content_key(), restored.content_key()); 477 + assert_eq!( 478 + solid.iter_faces().count(), 479 + restored.iter_faces().count(), 480 + "face count survives the blob round trip" 481 + ); 482 + assert!( 483 + restored.validate(TOLERANCE).is_ok(), 484 + "restored solid is a closed manifold" 485 + ); 486 + } 487 + 488 + #[test] 489 + fn blob_round_trip_preserves_cube_labels() { 490 + let mut ids = Ids::new(); 491 + let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 3.0)]); 492 + let solid = evaluate(&mut ids, &profile, &blind(5.0)); 493 + assert_blob_round_trip(&solid); 494 + } 495 + 496 + #[test] 497 + fn blob_round_trip_preserves_cylinder_labels() { 498 + let mut ids = Ids::new(); 499 + let profile = ExtrudeProfile::new( 500 + xy_plane(), 501 + vec![circle_loop(&mut ids, point(0.0, 0.0), 5.0)], 502 + ); 503 + let solid = evaluate(&mut ids, &profile, &blind(10.0)); 504 + assert_blob_round_trip(&solid); 505 + let restored = blob_round_trip(&solid); 506 + let curved = restored 507 + .edges_for_render(bone_types::ChordHeightTolerance::from_mm(0.05)) 508 + .iter() 509 + .any(|edge| matches!(edge.curve(), bone_kernel::EdgeCurve3::Circle(_))); 510 + assert!(curved, "cylinder cap circles survive as analytic curves"); 511 + } 512 + 513 + #[test] 514 + fn blob_round_trip_preserves_donut_labels() { 515 + let mut ids = Ids::new(); 516 + let outer = circle_loop(&mut ids, point(0.0, 0.0), 10.0); 517 + let inner = circle_loop(&mut ids, point(0.0, 0.0), 4.0); 518 + let profile = ExtrudeProfile::new(xy_plane(), vec![outer, inner]); 519 + let solid = evaluate(&mut ids, &profile, &blind(6.0)); 520 + assert_blob_round_trip(&solid); 521 + } 522 + 523 + #[test] 524 + fn content_key_is_stable_and_geometry_sensitive() { 525 + let mut ids = Ids::new(); 526 + let narrow = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 2.0)]); 527 + let extrude = ids.feature(); 528 + let Ok(first) = evaluate_extrude(extrude, &narrow, &blind(4.0)) else { 529 + panic!("rectangle extrudes"); 530 + }; 531 + let Ok(second) = evaluate_extrude(extrude, &narrow, &blind(4.0)) else { 532 + panic!("rectangle extrudes"); 533 + }; 534 + assert_eq!(first.content_key(), second.content_key()); 535 + 536 + let wide = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 9.0, 2.0)]); 537 + let Ok(other) = evaluate_extrude(extrude, &wide, &blind(4.0)) else { 538 + panic!("rectangle extrudes"); 539 + }; 540 + assert_ne!(first.content_key(), other.content_key()); 541 + } 542 + 543 + fn wrap_step(body: &str) -> String { 544 + format!( 545 + "ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\nFILE_NAME('','',(''),(''),'','','');\nFILE_SCHEMA(('AUTOMOTIVE_DESIGN'));\nENDSEC;\nDATA;\n{body}ENDSEC;\nEND-ISO-10303-21;\n" 546 + ) 547 + } 548 + 549 + fn assert_cap_vertices(solid: &BrepSolid, depth: f64) { 550 + solid.iter_vertices().for_each(|vertex| { 551 + let (_, _, z) = vertex.position().coords_mm(); 552 + match vertex.label().role { 553 + VertexRole::StartCapVertex { .. } => { 554 + assert!(z.abs() < 1.0e-6, "start cap vertex at z=0, got {z}"); 555 + } 556 + VertexRole::EndCapVertex { .. } => { 557 + assert!( 558 + (z - depth).abs() < 1.0e-6, 559 + "end cap vertex at z={depth}, got {z}" 560 + ); 561 + } 562 + VertexRole::Imported { .. } => {} 563 + } 564 + }); 565 + } 566 + 567 + #[test] 568 + fn step_round_trip_reattaches_cube_labels() { 569 + let mut ids = Ids::new(); 570 + let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 3.0)]); 571 + let solid = evaluate(&mut ids, &profile, &blind(5.0)); 572 + let Ok(body) = solid.to_step_body() else { 573 + panic!("solid lowers to a step body"); 574 + }; 575 + let step = wrap_step(&body); 576 + let feature = ids.feature(); 577 + let Ok(restored) = BrepSolid::from_step( 578 + &step, 579 + feature, 580 + Some((solid.content_key(), solid.reattach_data())), 581 + ) else { 582 + panic!("step body reattaches under a matching sidecar"); 583 + }; 584 + assert_eq!(face_labels(&solid), face_labels(&restored)); 585 + assert_eq!(solid.content_key(), restored.content_key()); 586 + assert_cap_vertices(&restored, 5.0); 587 + assert!(restored.validate(TOLERANCE).is_ok()); 588 + } 589 + 590 + #[test] 591 + fn step_without_sidecar_imports_as_dumb_body() { 592 + let mut ids = Ids::new(); 593 + let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 3.0)]); 594 + let solid = evaluate(&mut ids, &profile, &blind(5.0)); 595 + let Ok(body) = solid.to_step_body() else { 596 + panic!("solid lowers to a step body"); 597 + }; 598 + let step = wrap_step(&body); 599 + let feature = ids.feature(); 600 + let Ok(imported) = BrepSolid::from_step(&step, feature, None) else { 601 + panic!("step body imports as a dumb body"); 602 + }; 603 + assert!( 604 + imported 605 + .iter_faces() 606 + .all(|face| matches!(face.label().role, FaceRole::Imported { .. })), 607 + "a sidecar-less import carries only Imported roles" 608 + ); 609 + assert_eq!(imported.iter_faces().count(), solid.iter_faces().count()); 610 + } 611 + 612 + #[test] 613 + fn step_reattach_falls_back_when_count_mismatches() { 614 + let mut ids = Ids::new(); 615 + let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 3.0)]); 616 + let solid = evaluate(&mut ids, &profile, &blind(5.0)); 617 + let cylinder_loop = circle_loop(&mut ids, point(0.0, 0.0), 5.0); 618 + let cylinder_profile = ExtrudeProfile::new(xy_plane(), vec![cylinder_loop]); 619 + let cylinder = evaluate(&mut ids, &cylinder_profile, &blind(8.0)); 620 + assert_ne!( 621 + solid.reattach_data().faces().len(), 622 + cylinder.reattach_data().faces().len(), 623 + "the two solids disagree on entity count" 624 + ); 625 + let Ok(body) = solid.to_step_body() else { 626 + panic!("solid lowers to a step body"); 627 + }; 628 + let step = wrap_step(&body); 629 + let feature = ids.feature(); 630 + let Ok(imported) = BrepSolid::from_step( 631 + &step, 632 + feature, 633 + Some((solid.content_key(), cylinder.reattach_data())), 634 + ) else { 635 + panic!("a matching key with a wrong-count reattach degrades, it does not error"); 636 + }; 637 + assert!( 638 + imported 639 + .iter_faces() 640 + .all(|face| matches!(face.label().role, FaceRole::Imported { .. })), 641 + "a count mismatch drops to a labelless import" 642 + ); 643 + } 644 + 645 + #[test] 646 + fn step_reattach_falls_back_when_key_mismatches() { 647 + let mut ids = Ids::new(); 648 + let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 3.0)]); 649 + let solid = evaluate(&mut ids, &profile, &blind(5.0)); 650 + let Ok(body) = solid.to_step_body() else { 651 + panic!("solid lowers to a step body"); 652 + }; 653 + let step = wrap_step(&body); 654 + let feature = ids.feature(); 655 + let wrong_key = bone_types::SolidKey::from_bytes([0u8; 16]); 656 + let Ok(imported) = 657 + BrepSolid::from_step(&step, feature, Some((wrong_key, solid.reattach_data()))) 658 + else { 659 + panic!("a mismatched key still imports"); 660 + }; 661 + assert!( 662 + imported 663 + .iter_faces() 664 + .all(|face| matches!(face.label().role, FaceRole::Imported { .. })), 665 + "a hash mismatch drops to a labelless import" 666 + ); 357 667 } 358 668 359 669 #[test]
+3 -1
crates/bone-render/src/pick.rs
··· 1099 1099 1100 1100 #[test] 1101 1101 fn pick_priority_prefers_smaller_brep_entities() { 1102 - assert!(EntityKindTag::BrepVertex.pick_priority() < EntityKindTag::BrepEdge.pick_priority()); 1102 + assert!( 1103 + EntityKindTag::BrepVertex.pick_priority() < EntityKindTag::BrepEdge.pick_priority() 1104 + ); 1103 1105 assert!(EntityKindTag::BrepEdge.pick_priority() < EntityKindTag::BrepFace.pick_priority()); 1104 1106 } 1105 1107