Another project
0

Configure Feed

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

at main 11 kB View raw
1use bone_kernel::{ 2 BrepSolid, Circle2, Curve2Kind, ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, 3 ExtrudeProfile, ExtrudeSense, Line2, MergeResult, ProfileEdge, ProfileLoop, SolidMesh, 4 evaluate_extrude, 5}; 6use bone_render::{ 7 EdgeScene, OffscreenContext, SnapshotFrame, SolidRenderer, SolidScene, Style, frame_isometric, 8}; 9use bone_types::{ 10 Aabb3, Angle, AngleTolerance, Camera3, ChordHeightTolerance, DisplayMode, FeatureId, Length, 11 Plane3, Point2, Point3, PositiveLength, Projection, SketchEntityId, SketchId, Tolerance, 12 UnitVec3, millimeter, radian, 13}; 14use slotmap::{Key, SlotMap}; 15 16mod common; 17 18use common::{check_golden, extent_square as extent, make_context}; 19 20const TOLERANCE: Tolerance = Tolerance::new(1.0e-9); 21const UPDATE_ENV: &str = "BONE_UPDATE_EDGES_GOLDENS"; 22const DISPLAY_ENV: &str = "BONE_UPDATE_DISPLAY_GOLDENS"; 23const CHORD_MM: f64 = 0.05; 24const ANGLE_RAD: f64 = 0.2; 25 26fn xy_plane() -> Plane3 { 27 let Ok(plane) = Plane3::new( 28 Point3::origin(), 29 UnitVec3::x_axis(), 30 UnitVec3::y_axis(), 31 TOLERANCE, 32 ) else { 33 panic!("x and y axes are orthonormal"); 34 }; 35 plane 36} 37 38fn blind(depth_mm: f64) -> ExtrudeFeature { 39 let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(depth_mm)) else { 40 panic!("{depth_mm} mm is a positive length"); 41 }; 42 ExtrudeFeature { 43 sketch: SketchId::null(), 44 direction: ExtrudeDirection::Normal { 45 sense: ExtrudeSense::Forward, 46 }, 47 end_condition: ExtrudeEndCondition::Blind { depth }, 48 draft: None, 49 thin_wall: None, 50 merge_result: MergeResult::Merge, 51 } 52} 53 54fn unit_cube() -> BrepSolid { 55 let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key(); 56 let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key(); 57 let corners = [ 58 Point2::from_mm(0.0, 0.0), 59 Point2::from_mm(1.0, 0.0), 60 Point2::from_mm(1.0, 1.0), 61 Point2::from_mm(0.0, 1.0), 62 ]; 63 let edges = (0..4) 64 .map(|index| { 65 let start = corners[index]; 66 let end = corners[(index + 1) % 4]; 67 let Ok(segment) = Line2::new(start, end, TOLERANCE) else { 68 panic!("rectangle endpoints are distinct"); 69 }; 70 ProfileEdge::new( 71 Curve2Kind::Line(segment), 72 entities.insert(()), 73 entities.insert(()), 74 ) 75 }) 76 .collect(); 77 let profile = ExtrudeProfile::new(xy_plane(), vec![ProfileLoop::Open(edges)]); 78 let Ok(solid) = evaluate_extrude(features.insert(()), &profile, &blind(1.0)) else { 79 panic!("the unit square extrudes into a cube"); 80 }; 81 solid 82} 83 84fn cylinder() -> BrepSolid { 85 let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key(); 86 let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key(); 87 let Ok(disk) = Circle2::new( 88 Point2::from_mm(0.0, 0.0), 89 Length::new::<millimeter>(5.0), 90 TOLERANCE, 91 ) else { 92 panic!("circle radius is positive"); 93 }; 94 let profile = ExtrudeProfile::new( 95 xy_plane(), 96 vec![ProfileLoop::Closed { 97 curve: Curve2Kind::Circle(disk), 98 curve_entity: entities.insert(()), 99 }], 100 ); 101 let Ok(solid) = evaluate_extrude(features.insert(()), &profile, &blind(10.0)) else { 102 panic!("the disk extrudes into a cylinder"); 103 }; 104 solid 105} 106 107fn mesh_of(solid: &BrepSolid) -> SolidMesh { 108 let Ok(mesh) = solid.tessellate( 109 ChordHeightTolerance::from_mm(CHORD_MM), 110 AngleTolerance::from_radians(ANGLE_RAD), 111 ) else { 112 panic!("the solid tessellates"); 113 }; 114 mesh 115} 116 117fn scenes(solid: &BrepSolid) -> (SolidScene, EdgeScene) { 118 let mesh = mesh_of(solid); 119 let Ok(faces) = SolidScene::from_mesh(&mesh) else { 120 panic!("the mesh packs face pick ids"); 121 }; 122 let Ok(edges) = EdgeScene::from_solid(solid, &mesh, ChordHeightTolerance::from_mm(CHORD_MM)) 123 else { 124 panic!("the solid packs edge pick ids"); 125 }; 126 (faces, edges) 127} 128 129fn aabb_of(solid: &BrepSolid) -> Aabb3 { 130 let Some(aabb) = solid.bounding_box() else { 131 panic!("the solid has a bounding box"); 132 }; 133 aabb 134} 135 136fn direction(x: f64, y: f64, z: f64) -> UnitVec3 { 137 let Ok(unit) = UnitVec3::try_from_components(x, y, z, TOLERANCE) else { 138 panic!("({x}, {y}, {z}) is a nonzero direction"); 139 }; 140 unit 141} 142 143fn framed(aabb: Aabb3, from: UnitVec3, up: UnitVec3) -> Camera3 { 144 let center = aabb.center(); 145 let span = 0.5 * aabb.extent().norm_mm(); 146 let eye = center + from.into_vec(Length::new::<millimeter>(span * 3.0)); 147 let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(span * 1.2)) else { 148 panic!("half height is positive"); 149 }; 150 let Ok(camera) = Camera3::new(eye, center, up, projection) else { 151 panic!("camera is non-degenerate"); 152 }; 153 camera 154} 155 156fn cube_camera(solid: &BrepSolid, size: bone_render::ViewportExtent) -> Camera3 { 157 let Ok(camera) = frame_isometric(aabb_of(solid), size) else { 158 panic!("the cube frames isometrically"); 159 }; 160 camera 161} 162 163fn framed_perspective(aabb: Aabb3, from: UnitVec3, up: UnitVec3) -> Camera3 { 164 let center = aabb.center(); 165 let span = 0.5 * aabb.extent().norm_mm(); 166 let eye = center + from.into_vec(Length::new::<millimeter>(span * 4.0)); 167 let Ok(projection) = Projection::perspective( 168 Angle::new::<radian>(core::f64::consts::FRAC_PI_4), 169 Length::new::<millimeter>(span), 170 Length::new::<millimeter>(span * 12.0), 171 ) else { 172 panic!("the field of view is within (0, pi)"); 173 }; 174 let Ok(camera) = Camera3::new(eye, center, up, projection) else { 175 panic!("camera is non-degenerate"); 176 }; 177 camera 178} 179 180fn render( 181 ctx: &OffscreenContext, 182 faces: &SolidScene, 183 edges: &EdgeScene, 184 camera: Camera3, 185 mode: DisplayMode, 186) -> SnapshotFrame { 187 let mut renderer = SolidRenderer::new(ctx.gpu(), ctx.color_format()); 188 let Ok(frame) = renderer.render_display(ctx, faces, edges, camera, &Style::default(), mode) 189 else { 190 panic!("SolidRenderer::render_display failed"); 191 }; 192 frame 193} 194 195fn cube_frame(mode: DisplayMode) -> SnapshotFrame { 196 let size = extent(256); 197 let ctx = make_context(size); 198 let solid = unit_cube(); 199 let (faces, edges) = scenes(&solid); 200 render(&ctx, &faces, &edges, cube_camera(&solid, size), mode) 201} 202 203fn cylinder_frame(mode: DisplayMode) -> SnapshotFrame { 204 let size = extent(256); 205 let ctx = make_context(size); 206 let solid = cylinder(); 207 let (faces, edges) = scenes(&solid); 208 let camera = framed( 209 aabb_of(&solid), 210 direction(1.0, 1.0, 0.6), 211 UnitVec3::z_axis(), 212 ); 213 render(&ctx, &faces, &edges, camera, mode) 214} 215 216#[test] 217fn cube_shaded_with_edges_matches_golden() { 218 check_golden( 219 &cube_frame(DisplayMode::ShadedWithEdges), 220 "tests/goldens/shaded_with_edges_256.png", 221 UPDATE_ENV, 222 ); 223} 224 225#[test] 226fn cube_hidden_line_removed_matches_golden() { 227 check_golden( 228 &cube_frame(DisplayMode::HiddenLineRemoved), 229 "tests/goldens/hidden_line_removed_256.png", 230 UPDATE_ENV, 231 ); 232} 233 234#[test] 235fn cube_perspective_with_edges_matches_golden() { 236 let size = extent(256); 237 let ctx = make_context(size); 238 let solid = unit_cube(); 239 let (faces, edges) = scenes(&solid); 240 let camera = framed_perspective( 241 aabb_of(&solid), 242 direction(1.0, 1.0, 0.6), 243 UnitVec3::z_axis(), 244 ); 245 let frame = render(&ctx, &faces, &edges, camera, DisplayMode::ShadedWithEdges); 246 check_golden( 247 &frame, 248 "tests/goldens/perspective_with_edges_256.png", 249 UPDATE_ENV, 250 ); 251} 252 253#[test] 254fn cylinder_shaded_with_edges_matches_golden() { 255 check_golden( 256 &cylinder_frame(DisplayMode::ShadedWithEdges), 257 "tests/goldens/shaded_with_edges_cylinder_256.png", 258 UPDATE_ENV, 259 ); 260} 261 262#[test] 263fn cylinder_hidden_line_removed_matches_golden() { 264 check_golden( 265 &cylinder_frame(DisplayMode::HiddenLineRemoved), 266 "tests/goldens/hidden_line_removed_cylinder_256.png", 267 UPDATE_ENV, 268 ); 269} 270 271#[test] 272fn cube_wireframe_matches_golden() { 273 check_golden( 274 &cube_frame(DisplayMode::Wireframe), 275 "tests/goldens/wireframe_256.png", 276 DISPLAY_ENV, 277 ); 278} 279 280#[test] 281fn cylinder_wireframe_matches_golden() { 282 check_golden( 283 &cylinder_frame(DisplayMode::Wireframe), 284 "tests/goldens/wireframe_cylinder_256.png", 285 DISPLAY_ENV, 286 ); 287} 288 289#[test] 290fn cube_hidden_line_gray_matches_golden() { 291 check_golden( 292 &cube_frame(DisplayMode::HiddenLineGray), 293 "tests/goldens/hidden_line_gray_256.png", 294 DISPLAY_ENV, 295 ); 296} 297 298#[test] 299fn cylinder_hidden_line_gray_matches_golden() { 300 check_golden( 301 &cylinder_frame(DisplayMode::HiddenLineGray), 302 "tests/goldens/hidden_line_gray_cylinder_256.png", 303 DISPLAY_ENV, 304 ); 305} 306 307#[test] 308fn cube_shaded_no_edges_matches_golden() { 309 check_golden( 310 &cube_frame(DisplayMode::ShadedNoEdges), 311 "tests/goldens/shaded_no_edges_256.png", 312 DISPLAY_ENV, 313 ); 314} 315 316#[test] 317fn cylinder_shaded_no_edges_matches_golden() { 318 check_golden( 319 &cylinder_frame(DisplayMode::ShadedNoEdges), 320 "tests/goldens/shaded_no_edges_cylinder_256.png", 321 DISPLAY_ENV, 322 ); 323} 324 325#[test] 326fn cube_genuine_edges_classify_as_creases() { 327 let solid = unit_cube(); 328 let mesh = mesh_of(&solid); 329 let Ok(edges) = EdgeScene::from_solid(&solid, &mesh, ChordHeightTolerance::from_mm(CHORD_MM)) 330 else { 331 panic!("the cube packs edge pick ids"); 332 }; 333 assert!(!edges.genuine().is_empty(), "the cube emits genuine edges"); 334 let right_angle = core::f64::consts::FRAC_PI_2; 335 assert!( 336 edges 337 .genuine() 338 .iter() 339 .all(|edge| (edge.crease().radians() - right_angle).abs() < 1.0e-6), 340 "every cube edge is a 90-degree crease", 341 ); 342} 343 344#[test] 345fn cube_flat_faces_emit_no_silhouette_candidates() { 346 let solid = unit_cube(); 347 let mesh = mesh_of(&solid); 348 let Ok(edges) = EdgeScene::from_solid(&solid, &mesh, ChordHeightTolerance::from_mm(CHORD_MM)) 349 else { 350 panic!("the cube packs edge pick ids"); 351 }; 352 assert!( 353 edges.silhouettes().is_empty(), 354 "coplanar interior edges never straddle, so a flat-faced cube yields no silhouette candidates", 355 ); 356} 357 358#[test] 359fn cylinder_side_yields_silhouette_candidates() { 360 let solid = cylinder(); 361 let mesh = mesh_of(&solid); 362 let Ok(edges) = EdgeScene::from_solid(&solid, &mesh, ChordHeightTolerance::from_mm(CHORD_MM)) 363 else { 364 panic!("the cylinder packs edge pick ids"); 365 }; 366 assert!( 367 !edges.silhouettes().is_empty(), 368 "the tessellated cylinder side yields silhouette candidates", 369 ); 370 assert!( 371 edges 372 .silhouettes() 373 .iter() 374 .any(|candidate| candidate.normal_a() != candidate.normal_b()), 375 "neighboring side facets disagree in orientation so a silhouette can form", 376 ); 377}