Another project
0

Configure Feed

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

test(kernel): edges_for_render snapshots

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

author
Lewis
date (May 30, 2026, 4:01 PM +0300) commit 03a451ab parent b2fc5bcc change-id ultkxpzs
+627
+627
crates/bone-kernel/tests/edges_for_render.rs
··· 1 + use bone_kernel::{ 2 + Arc2, BrepSolid, Circle2, Curve2Kind, Curve3, EdgeCurve3, EdgePolyline, ExtrudeDirection, 3 + ExtrudeEndCondition, ExtrudeFeature, ExtrudeProfile, ExtrudeSense, Line2, MergeResult, 4 + ProfileEdge, ProfileLoop, evaluate_extrude, 5 + }; 6 + use bone_types::{ 7 + Angle, ChordHeightTolerance, EdgeRole, FeatureId, Length, Plane3, Point2, PositiveLength, 8 + SideKind, SketchEntityId, SketchId, Tolerance, UnitVec3, degree, millimeter, 9 + }; 10 + use proptest::prelude::*; 11 + use core::f64::consts::{FRAC_PI_2, PI}; 12 + use slotmap::{Key, SlotMap}; 13 + 14 + const TOLERANCE: Tolerance = Tolerance::new(1.0e-9); 15 + const RIGHT_ANGLE_EPS: f64 = 1.0e-6; 16 + 17 + fn chord() -> ChordHeightTolerance { 18 + ChordHeightTolerance::from_mm(0.05) 19 + } 20 + 21 + struct Ids { 22 + features: SlotMap<FeatureId, ()>, 23 + entities: SlotMap<SketchEntityId, ()>, 24 + } 25 + 26 + impl Ids { 27 + fn new() -> Self { 28 + Self { 29 + features: SlotMap::with_key(), 30 + entities: SlotMap::with_key(), 31 + } 32 + } 33 + 34 + fn feature(&mut self) -> FeatureId { 35 + self.features.insert(()) 36 + } 37 + 38 + fn entity(&mut self) -> SketchEntityId { 39 + self.entities.insert(()) 40 + } 41 + } 42 + 43 + fn length_mm(value: f64) -> Length { 44 + Length::new::<millimeter>(value) 45 + } 46 + 47 + fn point(x: f64, y: f64) -> Point2 { 48 + Point2::from_mm(x, y) 49 + } 50 + 51 + fn line(a: Point2, b: Point2) -> Curve2Kind { 52 + let Ok(segment) = Line2::new(a, b, TOLERANCE) else { 53 + panic!("line endpoints are distinct"); 54 + }; 55 + Curve2Kind::Line(segment) 56 + } 57 + 58 + fn circle(center: Point2, radius: f64) -> Curve2Kind { 59 + let Ok(disk) = Circle2::new(center, length_mm(radius), TOLERANCE) else { 60 + panic!("circle radius is positive"); 61 + }; 62 + Curve2Kind::Circle(disk) 63 + } 64 + 65 + fn arc(center: Point2, radius: f64, start_deg: f64, sweep_deg: f64) -> Curve2Kind { 66 + let Ok(segment) = Arc2::new( 67 + center, 68 + length_mm(radius), 69 + Angle::new::<degree>(start_deg), 70 + Angle::new::<degree>(sweep_deg), 71 + TOLERANCE, 72 + ) else { 73 + panic!("arc sweep is within range"); 74 + }; 75 + Curve2Kind::Arc(segment) 76 + } 77 + 78 + fn xy_plane() -> Plane3 { 79 + let Ok(plane) = Plane3::new( 80 + bone_types::Point3::origin(), 81 + UnitVec3::x_axis(), 82 + UnitVec3::y_axis(), 83 + TOLERANCE, 84 + ) else { 85 + panic!("x and y axes are orthonormal"); 86 + }; 87 + plane 88 + } 89 + 90 + fn rectangle(ids: &mut Ids, x0: f64, y0: f64, width: f64, height: f64) -> ProfileLoop { 91 + let corners = [ 92 + point(x0, y0), 93 + point(x0 + width, y0), 94 + point(x0 + width, y0 + height), 95 + point(x0, y0 + height), 96 + ]; 97 + let edges = (0..4) 98 + .map(|index| { 99 + let start = corners[index]; 100 + let end = corners[(index + 1) % 4]; 101 + let corner = ids.entity(); 102 + let segment = ids.entity(); 103 + ProfileEdge::new(line(start, end), segment, corner) 104 + }) 105 + .collect(); 106 + ProfileLoop::Open(edges) 107 + } 108 + 109 + fn circle_loop(ids: &mut Ids, center: Point2, radius: f64) -> ProfileLoop { 110 + let entity = ids.entity(); 111 + ProfileLoop::Closed { 112 + curve: circle(center, radius), 113 + curve_entity: entity, 114 + } 115 + } 116 + 117 + fn triangle(ids: &mut Ids, corners: [Point2; 3]) -> ProfileLoop { 118 + let edges = (0..3) 119 + .map(|index| { 120 + let start = corners[index]; 121 + let end = corners[(index + 1) % 3]; 122 + ProfileEdge::new(line(start, end), ids.entity(), ids.entity()) 123 + }) 124 + .collect(); 125 + ProfileLoop::Open(edges) 126 + } 127 + 128 + struct HalfDisk { 129 + arc_corner: SketchEntityId, 130 + arc_entity: SketchEntityId, 131 + line_corner: SketchEntityId, 132 + line_entity: SketchEntityId, 133 + radius_mm: f64, 134 + } 135 + 136 + impl HalfDisk { 137 + fn build(ids: &mut Ids, radius_mm: f64) -> (Self, ProfileLoop) { 138 + let parts = Self { 139 + arc_corner: ids.entity(), 140 + arc_entity: ids.entity(), 141 + line_corner: ids.entity(), 142 + line_entity: ids.entity(), 143 + radius_mm, 144 + }; 145 + let curved = ProfileEdge::new( 146 + arc(point(0.0, 0.0), radius_mm, 0.0, 180.0), 147 + parts.arc_entity, 148 + parts.arc_corner, 149 + ); 150 + let chord_edge = ProfileEdge::new( 151 + line(point(-radius_mm, 0.0), point(radius_mm, 0.0)), 152 + parts.line_entity, 153 + parts.line_corner, 154 + ); 155 + (parts, ProfileLoop::Open(vec![curved, chord_edge])) 156 + } 157 + } 158 + 159 + fn blind(depth: f64) -> ExtrudeFeature { 160 + ExtrudeFeature { 161 + sketch: SketchId::null(), 162 + direction: ExtrudeDirection::Normal { 163 + sense: ExtrudeSense::Forward, 164 + }, 165 + end_condition: ExtrudeEndCondition::Blind { 166 + depth: positive(depth), 167 + }, 168 + draft: None, 169 + thin_wall: None, 170 + merge_result: MergeResult::Merge, 171 + } 172 + } 173 + 174 + fn positive(depth: f64) -> PositiveLength { 175 + let Ok(value) = PositiveLength::new(length_mm(depth)) else { 176 + panic!("{depth} mm is a positive length"); 177 + }; 178 + value 179 + } 180 + 181 + fn evaluate(ids: &mut Ids, profile: &ExtrudeProfile, feature: &ExtrudeFeature) -> BrepSolid { 182 + let extrude = ids.feature(); 183 + let Ok(solid) = evaluate_extrude(extrude, profile, feature) else { 184 + panic!("the profile and feature describe a buildable extrude"); 185 + }; 186 + solid 187 + } 188 + 189 + fn approx(a: f64, b: f64, eps: f64) -> bool { 190 + (a - b).abs() < eps 191 + } 192 + 193 + #[test] 194 + fn unit_cube_emits_twelve_line_edges() { 195 + let mut ids = Ids::new(); 196 + let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 1.0, 1.0)]); 197 + let solid = evaluate(&mut ids, &profile, &blind(1.0)); 198 + let polylines = solid.edges_for_render(chord()); 199 + assert_eq!(polylines.len(), 12); 200 + polylines.iter().for_each(|edge| { 201 + assert!(matches!(edge.curve(), EdgeCurve3::Line(_)), "cube edge is a line"); 202 + assert!(edge.points().len() >= 2); 203 + let crease = edge.crease(); 204 + assert!( 205 + approx(crease.radians(), FRAC_PI_2, RIGHT_ANGLE_EPS), 206 + "cube edge crease ≈ 90°, got {} rad", 207 + crease.radians() 208 + ); 209 + }); 210 + } 211 + 212 + #[test] 213 + fn cylinder_caps_emit_circle_curves() { 214 + let mut ids = Ids::new(); 215 + let profile = ExtrudeProfile::new( 216 + xy_plane(), 217 + vec![circle_loop(&mut ids, point(0.0, 0.0), 5.0)], 218 + ); 219 + let solid = evaluate(&mut ids, &profile, &blind(10.0)); 220 + let polylines = solid.edges_for_render(chord()); 221 + let kinds: Vec<&EdgeCurve3> = polylines.iter().map(EdgePolyline::curve).collect(); 222 + assert_eq!( 223 + kinds.len(), 224 + 2, 225 + "cylinder emits two cap edges, seam side edge is filtered" 226 + ); 227 + kinds.iter().for_each(|kind| { 228 + assert!(matches!(kind, EdgeCurve3::Circle(_)), "cap edge is a circle"); 229 + }); 230 + polylines.iter().for_each(|edge| { 231 + let role = edge.label().role; 232 + assert!( 233 + matches!( 234 + role, 235 + EdgeRole::StartCapEdge { .. } | EdgeRole::EndCapEdge { .. } 236 + ), 237 + "cap edge role" 238 + ); 239 + let crease = edge.crease(); 240 + assert!( 241 + approx(crease.radians(), FRAC_PI_2, RIGHT_ANGLE_EPS), 242 + "cap crease ≈ 90°" 243 + ); 244 + assert!(edge.points().len() > 8); 245 + }); 246 + } 247 + 248 + #[test] 249 + fn cylinder_seam_side_edge_is_suppressed() { 250 + let mut ids = Ids::new(); 251 + let profile = ExtrudeProfile::new( 252 + xy_plane(), 253 + vec![circle_loop(&mut ids, point(0.0, 0.0), 5.0)], 254 + ); 255 + let solid = evaluate(&mut ids, &profile, &blind(10.0)); 256 + let polylines = solid.edges_for_render(chord()); 257 + let seam_emitted = polylines.iter().any(|edge| { 258 + matches!( 259 + edge.label().role, 260 + EdgeRole::SideEdge { 261 + side: SideKind::Seam, 262 + .. 263 + } 264 + ) 265 + }); 266 + assert!(!seam_emitted, "seam side edges are filtered from render output"); 267 + } 268 + 269 + #[test] 270 + fn donut_emits_four_circles() { 271 + let mut ids = Ids::new(); 272 + let outer = circle_loop(&mut ids, point(0.0, 0.0), 10.0); 273 + let inner = circle_loop(&mut ids, point(0.0, 0.0), 4.0); 274 + let profile = ExtrudeProfile::new(xy_plane(), vec![outer, inner]); 275 + let solid = evaluate(&mut ids, &profile, &blind(6.0)); 276 + let polylines = solid.edges_for_render(chord()); 277 + assert_eq!(polylines.len(), 4); 278 + polylines.iter().for_each(|edge| { 279 + assert!(matches!(edge.curve(), EdgeCurve3::Circle(_))); 280 + let crease = edge.crease(); 281 + assert!(approx(crease.radians(), FRAC_PI_2, RIGHT_ANGLE_EPS)); 282 + }); 283 + } 284 + 285 + #[test] 286 + fn half_disk_cap_emits_line_plus_arc() { 287 + let mut ids = Ids::new(); 288 + let (parts, half_disk) = HalfDisk::build(&mut ids, 5.0); 289 + let profile = ExtrudeProfile::new(xy_plane(), vec![half_disk]); 290 + let solid = evaluate(&mut ids, &profile, &blind(2.0)); 291 + let polylines = solid.edges_for_render(chord()); 292 + 293 + let cap_kinds: Vec<&EdgeCurve3> = polylines 294 + .iter() 295 + .filter(|edge| { 296 + matches!( 297 + edge.label().role, 298 + EdgeRole::StartCapEdge { from } if from == parts.arc_entity || from == parts.line_entity 299 + ) || matches!( 300 + edge.label().role, 301 + EdgeRole::EndCapEdge { from } if from == parts.arc_entity || from == parts.line_entity 302 + ) 303 + }) 304 + .map(EdgePolyline::curve) 305 + .collect(); 306 + let arc_count = cap_kinds 307 + .iter() 308 + .filter(|kind| matches!(kind, EdgeCurve3::Arc(_))) 309 + .count(); 310 + let line_count = cap_kinds 311 + .iter() 312 + .filter(|kind| matches!(kind, EdgeCurve3::Line(_))) 313 + .count(); 314 + assert_eq!(arc_count, 2, "two arc cap edges (start + end)"); 315 + assert_eq!(line_count, 2, "two line cap edges (start + end)"); 316 + } 317 + 318 + #[test] 319 + fn half_disk_arc_centers_lie_on_cap_planes() { 320 + let mut ids = Ids::new(); 321 + let depth_mm = 2.0; 322 + let (parts, half_disk) = HalfDisk::build(&mut ids, 5.0); 323 + let profile = ExtrudeProfile::new(xy_plane(), vec![half_disk]); 324 + let solid = evaluate(&mut ids, &profile, &blind(depth_mm)); 325 + let polylines = solid.edges_for_render(chord()); 326 + 327 + let cap_arcs: Vec<(f64, &EdgeCurve3)> = polylines 328 + .iter() 329 + .filter_map(|edge| match edge.label().role { 330 + EdgeRole::StartCapEdge { from } if from == parts.arc_entity => { 331 + Some((0.0, edge.curve())) 332 + } 333 + EdgeRole::EndCapEdge { from } if from == parts.arc_entity => { 334 + Some((depth_mm, edge.curve())) 335 + } 336 + _ => None, 337 + }) 338 + .collect(); 339 + assert_eq!(cap_arcs.len(), 2, "one arc per cap"); 340 + cap_arcs.iter().for_each(|(z_expected, curve)| { 341 + let EdgeCurve3::Arc(arc3) = curve else { 342 + panic!("cap arc lifts to Arc3"); 343 + }; 344 + let (cx, cy, cz) = arc3.center().coords_mm(); 345 + assert!(approx(cx, 0.0, 1.0e-9), "arc center x"); 346 + assert!(approx(cy, 0.0, 1.0e-9), "arc center y"); 347 + assert!(approx(cz, *z_expected, 1.0e-9), "arc center z"); 348 + let (nx, ny, nz) = arc3.normal().components(); 349 + assert!(approx(nx, 0.0, 1.0e-9), "arc normal x"); 350 + assert!(approx(ny, 0.0, 1.0e-9), "arc normal y"); 351 + assert!(approx(nz, 1.0, 1.0e-9), "arc normal z"); 352 + assert!( 353 + approx(arc3.radius().get::<millimeter>(), parts.radius_mm, 1.0e-9), 354 + "arc radius preserved" 355 + ); 356 + assert!(approx(arc3.sweep_rad(), PI, 1.0e-9), "arc sweeps π rad"); 357 + }); 358 + } 359 + 360 + #[test] 361 + fn half_disk_emits_corner_side_pillars() { 362 + let mut ids = Ids::new(); 363 + let depth_mm = 2.0; 364 + let (_parts, half_disk) = HalfDisk::build(&mut ids, 5.0); 365 + let profile = ExtrudeProfile::new(xy_plane(), vec![half_disk]); 366 + let solid = evaluate(&mut ids, &profile, &blind(depth_mm)); 367 + let polylines = solid.edges_for_render(chord()); 368 + 369 + let pillars: Vec<&EdgePolyline> = polylines 370 + .iter() 371 + .filter(|edge| { 372 + matches!( 373 + edge.label().role, 374 + EdgeRole::SideEdge { 375 + side: SideKind::Corner, 376 + .. 377 + } 378 + ) 379 + }) 380 + .collect(); 381 + assert_eq!(pillars.len(), 2, "two corner pillars (line/arc junctions)"); 382 + pillars.iter().for_each(|edge| { 383 + let EdgeCurve3::Line(segment) = edge.curve() else { 384 + panic!("pillar is a line"); 385 + }; 386 + let (sx, sy, sz) = segment.start().coords_mm(); 387 + let (ex, ey, ez) = segment.end().coords_mm(); 388 + assert!(approx(sx.abs(), 5.0, 1.0e-9), "pillar foot x"); 389 + assert!(approx(sy, 0.0, 1.0e-9), "pillar foot y"); 390 + assert!(approx(ex.abs(), 5.0, 1.0e-9), "pillar head x"); 391 + assert!(approx(ey, 0.0, 1.0e-9), "pillar head y"); 392 + assert!( 393 + approx((ez - sz).abs(), depth_mm, 1.0e-9), 394 + "pillar spans depth" 395 + ); 396 + }); 397 + } 398 + 399 + #[test] 400 + fn equilateral_prism_side_creases_are_120_degrees() { 401 + let mut ids = Ids::new(); 402 + let half = 0.5_f64 * 3.0_f64.sqrt(); 403 + let profile = ExtrudeProfile::new( 404 + xy_plane(), 405 + vec![triangle( 406 + &mut ids, 407 + [ 408 + point(0.0, 0.0), 409 + point(1.0, 0.0), 410 + point(0.5, half), 411 + ], 412 + )], 413 + ); 414 + let solid = evaluate(&mut ids, &profile, &blind(1.0)); 415 + let polylines = solid.edges_for_render(chord()); 416 + 417 + let side_creases: Vec<f64> = polylines 418 + .iter() 419 + .filter(|edge| { 420 + matches!( 421 + edge.label().role, 422 + EdgeRole::SideEdge { 423 + side: SideKind::Corner, 424 + .. 425 + } 426 + ) 427 + }) 428 + .map(|edge| edge.crease().radians()) 429 + .collect(); 430 + assert_eq!(side_creases.len(), 3, "three vertical corner pillars"); 431 + let expected_side = 2.0 * PI / 3.0; 432 + side_creases.iter().for_each(|value| { 433 + assert!( 434 + approx(*value, expected_side, 1.0e-9), 435 + "side crease ≈ 120°, got {value} rad" 436 + ); 437 + }); 438 + 439 + let cap_creases: Vec<f64> = polylines 440 + .iter() 441 + .filter(|edge| { 442 + matches!( 443 + edge.label().role, 444 + EdgeRole::StartCapEdge { .. } | EdgeRole::EndCapEdge { .. } 445 + ) 446 + }) 447 + .map(|edge| edge.crease().radians()) 448 + .collect(); 449 + assert_eq!(cap_creases.len(), 6, "three start + three end cap edges"); 450 + cap_creases.iter().for_each(|value| { 451 + assert!( 452 + approx(*value, FRAC_PI_2, 1.0e-9), 453 + "cap crease ≈ 90°, got {value} rad" 454 + ); 455 + }); 456 + } 457 + 458 + #[test] 459 + fn edges_for_render_is_deterministic() { 460 + let mut ids = Ids::new(); 461 + let cube = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 1.0, 1.0)]); 462 + let cube_solid = evaluate(&mut ids, &cube, &blind(1.0)); 463 + let first = cube_solid.edges_for_render(chord()); 464 + let second = cube_solid.edges_for_render(chord()); 465 + assert_eq!(first, second); 466 + 467 + let mut cyl_ids = Ids::new(); 468 + let cyl = ExtrudeProfile::new( 469 + xy_plane(), 470 + vec![circle_loop(&mut cyl_ids, point(0.0, 0.0), 5.0)], 471 + ); 472 + let cyl_solid = evaluate(&mut cyl_ids, &cyl, &blind(10.0)); 473 + let first = cyl_solid.edges_for_render(chord()); 474 + let second = cyl_solid.edges_for_render(chord()); 475 + assert_eq!(first, second); 476 + } 477 + 478 + #[test] 479 + fn edges_for_render_is_deterministic_across_evaluations() { 480 + let mut first_ids = Ids::new(); 481 + let first_profile = ExtrudeProfile::new( 482 + xy_plane(), 483 + vec![circle_loop(&mut first_ids, point(0.0, 0.0), 5.0)], 484 + ); 485 + let first_extrude = first_ids.feature(); 486 + let Ok(first_solid) = evaluate_extrude(first_extrude, &first_profile, &blind(10.0)) else { 487 + panic!("first evaluation succeeds"); 488 + }; 489 + 490 + let mut second_ids = Ids::new(); 491 + let second_profile = ExtrudeProfile::new( 492 + xy_plane(), 493 + vec![circle_loop(&mut second_ids, point(0.0, 0.0), 5.0)], 494 + ); 495 + let second_extrude = second_ids.feature(); 496 + let Ok(second_solid) = evaluate_extrude(second_extrude, &second_profile, &blind(10.0)) else { 497 + panic!("second evaluation succeeds"); 498 + }; 499 + 500 + assert_eq!( 501 + first_solid.edges_for_render(chord()), 502 + second_solid.edges_for_render(chord()), 503 + "two evaluations of the same profile + feature produce identical render polylines", 504 + ); 505 + } 506 + 507 + #[test] 508 + fn polylines_carry_label_and_crease_for_each_edge() { 509 + let mut ids = Ids::new(); 510 + let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 3.0)]); 511 + let solid = evaluate(&mut ids, &profile, &blind(4.0)); 512 + let polylines = solid.edges_for_render(chord()); 513 + polylines.iter().for_each(|edge| { 514 + let id = edge.edge(); 515 + let label = edge.label(); 516 + let crease = edge.crease(); 517 + assert_ne!(id, bone_types::BrepEdgeId::default()); 518 + assert!(label.feature != FeatureId::null()); 519 + assert!(crease.radians().is_finite()); 520 + }); 521 + } 522 + 523 + #[test] 524 + fn polyline_endpoints_align_with_vertices_order() { 525 + let mut ids = Ids::new(); 526 + let (_parts, half_disk) = HalfDisk::build(&mut ids, 5.0); 527 + let profile = ExtrudeProfile::new(xy_plane(), vec![half_disk]); 528 + let solid = evaluate(&mut ids, &profile, &blind(2.0)); 529 + let positions: std::collections::HashMap<bone_types::BrepVertexId, bone_types::Point3> = solid 530 + .iter_vertices() 531 + .map(|v| (v.id(), v.position())) 532 + .collect(); 533 + solid.iter_edges().for_each(|edge| { 534 + let [start_id, end_id] = edge.vertices(); 535 + if start_id == end_id { 536 + return; 537 + } 538 + let Some(start_pos) = positions.get(&start_id) else { 539 + return; 540 + }; 541 + let Some(end_pos) = positions.get(&end_id) else { 542 + return; 543 + }; 544 + let curve = edge.curve(); 545 + let curve_start = curve.evaluate(bone_types::Parameter::new(0.0)); 546 + let curve_end = curve.evaluate(bone_types::Parameter::new(1.0)); 547 + let (sx, sy, sz) = start_pos.coords_mm(); 548 + let (cx, cy, cz) = curve_start.coords_mm(); 549 + assert!( 550 + approx(sx, cx, 1.0e-9) && approx(sy, cy, 1.0e-9) && approx(sz, cz, 1.0e-9), 551 + "vertices[0] should sit at curve.evaluate(0)" 552 + ); 553 + let (ex, ey, ez) = end_pos.coords_mm(); 554 + let (kx, ky, kz) = curve_end.coords_mm(); 555 + assert!( 556 + approx(ex, kx, 1.0e-9) && approx(ey, ky, 1.0e-9) && approx(ez, kz, 1.0e-9), 557 + "vertices[1] should sit at curve.evaluate(1)" 558 + ); 559 + }); 560 + 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(); 565 + render.iter().for_each(|polyline| { 566 + let edge = edges_by_id[&polyline.edge()]; 567 + let [start_id, _] = edge.vertices(); 568 + if let Some(start_pos) = positions.get(&start_id) { 569 + let first = polyline.points()[0]; 570 + let (sx, sy, sz) = start_pos.coords_mm(); 571 + let (fx, fy, fz) = first.coords_mm(); 572 + assert!( 573 + approx(sx, fx, 1.0e-9) && approx(sy, fy, 1.0e-9) && approx(sz, fz, 1.0e-9), 574 + "polyline.points()[0] sits at vertices[0]" 575 + ); 576 + } 577 + }); 578 + } 579 + 580 + proptest! { 581 + #[test] 582 + fn edges_for_render_is_deterministic_across_rectangles( 583 + width_mm in 0.5f64..=20.0f64, 584 + height_mm in 0.5f64..=20.0f64, 585 + depth_mm in 0.5f64..=20.0f64, 586 + ) { 587 + let mut ids = Ids::new(); 588 + let profile = ExtrudeProfile::new( 589 + xy_plane(), 590 + vec![rectangle(&mut ids, 0.0, 0.0, width_mm, height_mm)], 591 + ); 592 + let solid = evaluate(&mut ids, &profile, &blind(depth_mm)); 593 + let first = solid.edges_for_render(chord()); 594 + let second = solid.edges_for_render(chord()); 595 + prop_assert_eq!(first, second); 596 + } 597 + 598 + #[test] 599 + fn edges_for_render_is_deterministic_across_cylinders( 600 + radius_mm in 0.5f64..=20.0f64, 601 + depth_mm in 0.5f64..=20.0f64, 602 + ) { 603 + let mut ids = Ids::new(); 604 + let profile = ExtrudeProfile::new( 605 + xy_plane(), 606 + vec![circle_loop(&mut ids, point(0.0, 0.0), radius_mm)], 607 + ); 608 + let solid = evaluate(&mut ids, &profile, &blind(depth_mm)); 609 + let first = solid.edges_for_render(chord()); 610 + let second = solid.edges_for_render(chord()); 611 + prop_assert_eq!(first, second); 612 + } 613 + 614 + #[test] 615 + fn edges_for_render_is_deterministic_across_half_disks( 616 + radius_mm in 1.0f64..=10.0f64, 617 + depth_mm in 0.5f64..=10.0f64, 618 + ) { 619 + let mut ids = Ids::new(); 620 + let (_parts, half_disk) = HalfDisk::build(&mut ids, radius_mm); 621 + let profile = ExtrudeProfile::new(xy_plane(), vec![half_disk]); 622 + let solid = evaluate(&mut ids, &profile, &blind(depth_mm)); 623 + let first = solid.edges_for_render(chord()); 624 + let second = solid.edges_for_render(chord()); 625 + prop_assert_eq!(first, second); 626 + } 627 + }