Another project
0

Configure Feed

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

at main 21 kB View raw
1use bone_kernel::{ 2 Arc2, BrepSolid, Circle2, Curve2Kind, Curve3, EdgeCurve3, EdgePolyline, ExtrudeDirection, 3 ExtrudeEndCondition, ExtrudeFeature, ExtrudeProfile, ExtrudeSense, Line2, MergeResult, 4 ProfileEdge, ProfileLoop, evaluate_extrude, 5}; 6use bone_types::{ 7 Angle, ChordHeightTolerance, EdgeRole, FeatureId, Length, Plane3, Point2, PositiveLength, 8 SideKind, SketchEntityId, SketchId, Tolerance, UnitVec3, degree, millimeter, 9}; 10use core::f64::consts::{FRAC_PI_2, PI}; 11use proptest::prelude::*; 12use slotmap::{Key, SlotMap}; 13 14const TOLERANCE: Tolerance = Tolerance::new(1.0e-9); 15const RIGHT_ANGLE_EPS: f64 = 1.0e-6; 16 17fn chord() -> ChordHeightTolerance { 18 ChordHeightTolerance::from_mm(0.05) 19} 20 21struct Ids { 22 features: SlotMap<FeatureId, ()>, 23 entities: SlotMap<SketchEntityId, ()>, 24} 25 26impl 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 43fn length_mm(value: f64) -> Length { 44 Length::new::<millimeter>(value) 45} 46 47fn point(x: f64, y: f64) -> Point2 { 48 Point2::from_mm(x, y) 49} 50 51fn 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 58fn 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 65fn 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 78fn 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 90fn 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 109fn 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 117fn 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 128struct HalfDisk { 129 arc_corner: SketchEntityId, 130 arc_entity: SketchEntityId, 131 line_corner: SketchEntityId, 132 line_entity: SketchEntityId, 133 radius_mm: f64, 134} 135 136impl 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 159fn 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 174fn 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 181fn 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 189fn approx(a: f64, b: f64, eps: f64) -> bool { 190 (a - b).abs() < eps 191} 192 193#[test] 194fn 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!( 202 matches!(edge.curve(), EdgeCurve3::Line(_)), 203 "cube edge is a line" 204 ); 205 assert!(edge.points().len() >= 2); 206 let crease = edge.crease(); 207 assert!( 208 approx(crease.radians(), FRAC_PI_2, RIGHT_ANGLE_EPS), 209 "cube edge crease ≈ 90°, got {} rad", 210 crease.radians() 211 ); 212 }); 213} 214 215#[test] 216fn cylinder_caps_emit_circle_curves() { 217 let mut ids = Ids::new(); 218 let profile = ExtrudeProfile::new( 219 xy_plane(), 220 vec![circle_loop(&mut ids, point(0.0, 0.0), 5.0)], 221 ); 222 let solid = evaluate(&mut ids, &profile, &blind(10.0)); 223 let polylines = solid.edges_for_render(chord()); 224 let kinds: Vec<&EdgeCurve3> = polylines.iter().map(EdgePolyline::curve).collect(); 225 assert_eq!( 226 kinds.len(), 227 2, 228 "cylinder emits two cap edges, seam side edge is filtered" 229 ); 230 kinds.iter().for_each(|kind| { 231 assert!( 232 matches!(kind, EdgeCurve3::Circle(_)), 233 "cap edge is a circle" 234 ); 235 }); 236 polylines.iter().for_each(|edge| { 237 let role = edge.label().role; 238 assert!( 239 matches!( 240 role, 241 EdgeRole::StartCapEdge { .. } | EdgeRole::EndCapEdge { .. } 242 ), 243 "cap edge role" 244 ); 245 let crease = edge.crease(); 246 assert!( 247 approx(crease.radians(), FRAC_PI_2, RIGHT_ANGLE_EPS), 248 "cap crease ≈ 90°" 249 ); 250 assert!(edge.points().len() > 8); 251 }); 252} 253 254#[test] 255fn cylinder_seam_side_edge_is_suppressed() { 256 let mut ids = Ids::new(); 257 let profile = ExtrudeProfile::new( 258 xy_plane(), 259 vec![circle_loop(&mut ids, point(0.0, 0.0), 5.0)], 260 ); 261 let solid = evaluate(&mut ids, &profile, &blind(10.0)); 262 let polylines = solid.edges_for_render(chord()); 263 let seam_emitted = polylines.iter().any(|edge| { 264 matches!( 265 edge.label().role, 266 EdgeRole::SideEdge { 267 side: SideKind::Seam, 268 .. 269 } 270 ) 271 }); 272 assert!( 273 !seam_emitted, 274 "seam side edges are filtered from render output" 275 ); 276} 277 278#[test] 279fn donut_emits_four_circles() { 280 let mut ids = Ids::new(); 281 let outer = circle_loop(&mut ids, point(0.0, 0.0), 10.0); 282 let inner = circle_loop(&mut ids, point(0.0, 0.0), 4.0); 283 let profile = ExtrudeProfile::new(xy_plane(), vec![outer, inner]); 284 let solid = evaluate(&mut ids, &profile, &blind(6.0)); 285 let polylines = solid.edges_for_render(chord()); 286 assert_eq!(polylines.len(), 4); 287 polylines.iter().for_each(|edge| { 288 assert!(matches!(edge.curve(), EdgeCurve3::Circle(_))); 289 let crease = edge.crease(); 290 assert!(approx(crease.radians(), FRAC_PI_2, RIGHT_ANGLE_EPS)); 291 }); 292} 293 294#[test] 295fn half_disk_cap_emits_line_plus_arc() { 296 let mut ids = Ids::new(); 297 let (parts, half_disk) = HalfDisk::build(&mut ids, 5.0); 298 let profile = ExtrudeProfile::new(xy_plane(), vec![half_disk]); 299 let solid = evaluate(&mut ids, &profile, &blind(2.0)); 300 let polylines = solid.edges_for_render(chord()); 301 302 let cap_kinds: Vec<&EdgeCurve3> = polylines 303 .iter() 304 .filter(|edge| { 305 matches!( 306 edge.label().role, 307 EdgeRole::StartCapEdge { from } if from == parts.arc_entity || from == parts.line_entity 308 ) || matches!( 309 edge.label().role, 310 EdgeRole::EndCapEdge { from } if from == parts.arc_entity || from == parts.line_entity 311 ) 312 }) 313 .map(EdgePolyline::curve) 314 .collect(); 315 let arc_count = cap_kinds 316 .iter() 317 .filter(|kind| matches!(kind, EdgeCurve3::Arc(_))) 318 .count(); 319 let line_count = cap_kinds 320 .iter() 321 .filter(|kind| matches!(kind, EdgeCurve3::Line(_))) 322 .count(); 323 assert_eq!(arc_count, 2, "two arc cap edges (start + end)"); 324 assert_eq!(line_count, 2, "two line cap edges (start + end)"); 325} 326 327#[test] 328fn half_disk_arc_centers_lie_on_cap_planes() { 329 let mut ids = Ids::new(); 330 let depth_mm = 2.0; 331 let (parts, half_disk) = HalfDisk::build(&mut ids, 5.0); 332 let profile = ExtrudeProfile::new(xy_plane(), vec![half_disk]); 333 let solid = evaluate(&mut ids, &profile, &blind(depth_mm)); 334 let polylines = solid.edges_for_render(chord()); 335 336 let cap_arcs: Vec<(f64, &EdgeCurve3)> = polylines 337 .iter() 338 .filter_map(|edge| match edge.label().role { 339 EdgeRole::StartCapEdge { from } if from == parts.arc_entity => { 340 Some((0.0, edge.curve())) 341 } 342 EdgeRole::EndCapEdge { from } if from == parts.arc_entity => { 343 Some((depth_mm, edge.curve())) 344 } 345 _ => None, 346 }) 347 .collect(); 348 assert_eq!(cap_arcs.len(), 2, "one arc per cap"); 349 cap_arcs.iter().for_each(|(z_expected, curve)| { 350 let EdgeCurve3::Arc(arc3) = curve else { 351 panic!("cap arc lifts to Arc3"); 352 }; 353 let (cx, cy, cz) = arc3.center().coords_mm(); 354 assert!(approx(cx, 0.0, 1.0e-9), "arc center x"); 355 assert!(approx(cy, 0.0, 1.0e-9), "arc center y"); 356 assert!(approx(cz, *z_expected, 1.0e-9), "arc center z"); 357 let (nx, ny, nz) = arc3.normal().components(); 358 assert!(approx(nx, 0.0, 1.0e-9), "arc normal x"); 359 assert!(approx(ny, 0.0, 1.0e-9), "arc normal y"); 360 assert!(approx(nz, 1.0, 1.0e-9), "arc normal z"); 361 assert!( 362 approx(arc3.radius().get::<millimeter>(), parts.radius_mm, 1.0e-9), 363 "arc radius preserved" 364 ); 365 assert!(approx(arc3.sweep_rad(), PI, 1.0e-9), "arc sweeps π rad"); 366 }); 367} 368 369#[test] 370fn half_disk_emits_corner_side_pillars() { 371 let mut ids = Ids::new(); 372 let depth_mm = 2.0; 373 let (_parts, half_disk) = HalfDisk::build(&mut ids, 5.0); 374 let profile = ExtrudeProfile::new(xy_plane(), vec![half_disk]); 375 let solid = evaluate(&mut ids, &profile, &blind(depth_mm)); 376 let polylines = solid.edges_for_render(chord()); 377 378 let pillars: Vec<&EdgePolyline> = polylines 379 .iter() 380 .filter(|edge| { 381 matches!( 382 edge.label().role, 383 EdgeRole::SideEdge { 384 side: SideKind::Corner, 385 .. 386 } 387 ) 388 }) 389 .collect(); 390 assert_eq!(pillars.len(), 2, "two corner pillars (line/arc junctions)"); 391 pillars.iter().for_each(|edge| { 392 let EdgeCurve3::Line(segment) = edge.curve() else { 393 panic!("pillar is a line"); 394 }; 395 let (sx, sy, sz) = segment.start().coords_mm(); 396 let (ex, ey, ez) = segment.end().coords_mm(); 397 assert!(approx(sx.abs(), 5.0, 1.0e-9), "pillar foot x"); 398 assert!(approx(sy, 0.0, 1.0e-9), "pillar foot y"); 399 assert!(approx(ex.abs(), 5.0, 1.0e-9), "pillar head x"); 400 assert!(approx(ey, 0.0, 1.0e-9), "pillar head y"); 401 assert!( 402 approx((ez - sz).abs(), depth_mm, 1.0e-9), 403 "pillar spans depth" 404 ); 405 }); 406} 407 408#[test] 409fn equilateral_prism_side_creases_are_120_degrees() { 410 let mut ids = Ids::new(); 411 let half = 0.5_f64 * 3.0_f64.sqrt(); 412 let profile = ExtrudeProfile::new( 413 xy_plane(), 414 vec![triangle( 415 &mut ids, 416 [point(0.0, 0.0), point(1.0, 0.0), point(0.5, half)], 417 )], 418 ); 419 let solid = evaluate(&mut ids, &profile, &blind(1.0)); 420 let polylines = solid.edges_for_render(chord()); 421 422 let side_creases: Vec<f64> = polylines 423 .iter() 424 .filter(|edge| { 425 matches!( 426 edge.label().role, 427 EdgeRole::SideEdge { 428 side: SideKind::Corner, 429 .. 430 } 431 ) 432 }) 433 .map(|edge| edge.crease().radians()) 434 .collect(); 435 assert_eq!(side_creases.len(), 3, "three vertical corner pillars"); 436 let expected_side = 2.0 * PI / 3.0; 437 side_creases.iter().for_each(|value| { 438 assert!( 439 approx(*value, expected_side, 1.0e-9), 440 "side crease ≈ 120°, got {value} rad" 441 ); 442 }); 443 444 let cap_creases: Vec<f64> = polylines 445 .iter() 446 .filter(|edge| { 447 matches!( 448 edge.label().role, 449 EdgeRole::StartCapEdge { .. } | EdgeRole::EndCapEdge { .. } 450 ) 451 }) 452 .map(|edge| edge.crease().radians()) 453 .collect(); 454 assert_eq!(cap_creases.len(), 6, "three start + three end cap edges"); 455 cap_creases.iter().for_each(|value| { 456 assert!( 457 approx(*value, FRAC_PI_2, 1.0e-9), 458 "cap crease ≈ 90°, got {value} rad" 459 ); 460 }); 461} 462 463#[test] 464fn edges_for_render_is_deterministic() { 465 let mut ids = Ids::new(); 466 let cube = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 1.0, 1.0)]); 467 let cube_solid = evaluate(&mut ids, &cube, &blind(1.0)); 468 let first = cube_solid.edges_for_render(chord()); 469 let second = cube_solid.edges_for_render(chord()); 470 assert_eq!(first, second); 471 472 let mut cyl_ids = Ids::new(); 473 let cyl = ExtrudeProfile::new( 474 xy_plane(), 475 vec![circle_loop(&mut cyl_ids, point(0.0, 0.0), 5.0)], 476 ); 477 let cyl_solid = evaluate(&mut cyl_ids, &cyl, &blind(10.0)); 478 let first = cyl_solid.edges_for_render(chord()); 479 let second = cyl_solid.edges_for_render(chord()); 480 assert_eq!(first, second); 481} 482 483#[test] 484fn edges_for_render_is_deterministic_across_evaluations() { 485 let mut first_ids = Ids::new(); 486 let first_profile = ExtrudeProfile::new( 487 xy_plane(), 488 vec![circle_loop(&mut first_ids, point(0.0, 0.0), 5.0)], 489 ); 490 let first_extrude = first_ids.feature(); 491 let Ok(first_solid) = evaluate_extrude(first_extrude, &first_profile, &blind(10.0)) else { 492 panic!("first evaluation succeeds"); 493 }; 494 495 let mut second_ids = Ids::new(); 496 let second_profile = ExtrudeProfile::new( 497 xy_plane(), 498 vec![circle_loop(&mut second_ids, point(0.0, 0.0), 5.0)], 499 ); 500 let second_extrude = second_ids.feature(); 501 let Ok(second_solid) = evaluate_extrude(second_extrude, &second_profile, &blind(10.0)) else { 502 panic!("second evaluation succeeds"); 503 }; 504 505 assert_eq!( 506 first_solid.edges_for_render(chord()), 507 second_solid.edges_for_render(chord()), 508 "two evaluations of the same profile + feature produce identical render polylines", 509 ); 510} 511 512#[test] 513fn polylines_carry_label_and_crease_for_each_edge() { 514 let mut ids = Ids::new(); 515 let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 3.0)]); 516 let solid = evaluate(&mut ids, &profile, &blind(4.0)); 517 let polylines = solid.edges_for_render(chord()); 518 polylines.iter().for_each(|edge| { 519 let id = edge.edge(); 520 let label = edge.label(); 521 let crease = edge.crease(); 522 assert_ne!(id, bone_types::BrepEdgeId::default()); 523 assert!(label.feature != FeatureId::null()); 524 assert!(crease.radians().is_finite()); 525 }); 526} 527 528#[test] 529fn polyline_endpoints_align_with_vertices_order() { 530 let mut ids = Ids::new(); 531 let (_parts, half_disk) = HalfDisk::build(&mut ids, 5.0); 532 let profile = ExtrudeProfile::new(xy_plane(), vec![half_disk]); 533 let solid = evaluate(&mut ids, &profile, &blind(2.0)); 534 let positions: std::collections::HashMap<bone_types::BrepVertexId, bone_types::Point3> = solid 535 .iter_vertices() 536 .map(|v| (v.id(), v.position())) 537 .collect(); 538 solid.iter_edges().for_each(|edge| { 539 let [start_id, end_id] = edge.vertices(); 540 if start_id == end_id { 541 return; 542 } 543 let Some(start_pos) = positions.get(&start_id) else { 544 return; 545 }; 546 let Some(end_pos) = positions.get(&end_id) else { 547 return; 548 }; 549 let curve = edge.curve(); 550 let curve_start = curve.evaluate(bone_types::Parameter::new(0.0)); 551 let curve_end = curve.evaluate(bone_types::Parameter::new(1.0)); 552 let (sx, sy, sz) = start_pos.coords_mm(); 553 let (cx, cy, cz) = curve_start.coords_mm(); 554 assert!( 555 approx(sx, cx, 1.0e-9) && approx(sy, cy, 1.0e-9) && approx(sz, cz, 1.0e-9), 556 "vertices[0] should sit at curve.evaluate(0)" 557 ); 558 let (ex, ey, ez) = end_pos.coords_mm(); 559 let (kx, ky, kz) = curve_end.coords_mm(); 560 assert!( 561 approx(ex, kx, 1.0e-9) && approx(ey, ky, 1.0e-9) && approx(ez, kz, 1.0e-9), 562 "vertices[1] should sit at curve.evaluate(1)" 563 ); 564 }); 565 let render = solid.edges_for_render(chord()); 566 let edges_by_id: std::collections::HashMap<_, _> = 567 solid.iter_edges().map(|edge| (edge.id(), edge)).collect(); 568 render.iter().for_each(|polyline| { 569 let edge = edges_by_id[&polyline.edge()]; 570 let [start_id, _] = edge.vertices(); 571 if let Some(start_pos) = positions.get(&start_id) { 572 let first = polyline.points()[0]; 573 let (sx, sy, sz) = start_pos.coords_mm(); 574 let (fx, fy, fz) = first.coords_mm(); 575 assert!( 576 approx(sx, fx, 1.0e-9) && approx(sy, fy, 1.0e-9) && approx(sz, fz, 1.0e-9), 577 "polyline.points()[0] sits at vertices[0]" 578 ); 579 } 580 }); 581} 582 583proptest! { 584 #[test] 585 fn edges_for_render_is_deterministic_across_rectangles( 586 width_mm in 0.5f64..=20.0f64, 587 height_mm in 0.5f64..=20.0f64, 588 depth_mm in 0.5f64..=20.0f64, 589 ) { 590 let mut ids = Ids::new(); 591 let profile = ExtrudeProfile::new( 592 xy_plane(), 593 vec![rectangle(&mut ids, 0.0, 0.0, width_mm, height_mm)], 594 ); 595 let solid = evaluate(&mut ids, &profile, &blind(depth_mm)); 596 let first = solid.edges_for_render(chord()); 597 let second = solid.edges_for_render(chord()); 598 prop_assert_eq!(first, second); 599 } 600 601 #[test] 602 fn edges_for_render_is_deterministic_across_cylinders( 603 radius_mm in 0.5f64..=20.0f64, 604 depth_mm in 0.5f64..=20.0f64, 605 ) { 606 let mut ids = Ids::new(); 607 let profile = ExtrudeProfile::new( 608 xy_plane(), 609 vec![circle_loop(&mut ids, point(0.0, 0.0), radius_mm)], 610 ); 611 let solid = evaluate(&mut ids, &profile, &blind(depth_mm)); 612 let first = solid.edges_for_render(chord()); 613 let second = solid.edges_for_render(chord()); 614 prop_assert_eq!(first, second); 615 } 616 617 #[test] 618 fn edges_for_render_is_deterministic_across_half_disks( 619 radius_mm in 1.0f64..=10.0f64, 620 depth_mm in 0.5f64..=10.0f64, 621 ) { 622 let mut ids = Ids::new(); 623 let (_parts, half_disk) = HalfDisk::build(&mut ids, radius_mm); 624 let profile = ExtrudeProfile::new(xy_plane(), vec![half_disk]); 625 let solid = evaluate(&mut ids, &profile, &blind(depth_mm)); 626 let first = solid.edges_for_render(chord()); 627 let second = solid.edges_for_render(chord()); 628 prop_assert_eq!(first, second); 629 } 630}