Another project
0

Configure Feed

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

at main 7.4 kB View raw
1use bone_kernel::{ 2 BrepEdge, BrepFace, BrepSolid, BrepVertex, Circle2, Curve2Kind, ExtrudeDirection, 3 ExtrudeEndCondition, ExtrudeFeature, ExtrudeProfile, ExtrudeSense, MergeResult, ProfileLoop, 4 evaluate_extrude, 5}; 6use bone_render::{ 7 OffscreenContext, PickId, PickIndex, PickQuery, PickedItem, SnapshotFrame, SolidRenderer, 8 SolidScene, Style, ViewportPx, 9}; 10use bone_types::{ 11 Aabb3, AngleTolerance, Camera3, ChordHeightTolerance, FeatureId, Length, Plane3, Point2, 12 Point3, PositiveLength, Projection, ShadingModel, SketchEntityId, SketchId, Tolerance, 13 UnitVec3, millimeter, 14}; 15use slotmap::{Key, SlotMap}; 16 17mod common; 18 19use common::{check_golden, extent_square as extent, make_context}; 20 21const TOLERANCE: Tolerance = Tolerance::new(1.0e-9); 22const UPDATE_ENV: &str = "BONE_UPDATE_SOLID_GOLDENS"; 23const CHORD: f64 = 0.05; 24const ANGLE: 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 circle_loop(entities: &mut SlotMap<SketchEntityId, ()>, radius_mm: f64) -> ProfileLoop { 39 let Ok(disk) = Circle2::new( 40 Point2::from_mm(0.0, 0.0), 41 Length::new::<millimeter>(radius_mm), 42 TOLERANCE, 43 ) else { 44 panic!("circle radius is positive"); 45 }; 46 ProfileLoop::Closed { 47 curve: Curve2Kind::Circle(disk), 48 curve_entity: entities.insert(()), 49 } 50} 51 52fn blind(depth_mm: f64) -> ExtrudeFeature { 53 let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(depth_mm)) else { 54 panic!("{depth_mm} mm is a positive length"); 55 }; 56 ExtrudeFeature { 57 sketch: SketchId::null(), 58 direction: ExtrudeDirection::Normal { 59 sense: ExtrudeSense::Forward, 60 }, 61 end_condition: ExtrudeEndCondition::Blind { depth }, 62 draft: None, 63 thin_wall: None, 64 merge_result: MergeResult::Merge, 65 } 66} 67 68fn solid_from(loops: Vec<ProfileLoop>, depth_mm: f64) -> BrepSolid { 69 let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key(); 70 let profile = ExtrudeProfile::new(xy_plane(), loops); 71 let Ok(solid) = evaluate_extrude(features.insert(()), &profile, &blind(depth_mm)) else { 72 panic!("the profile and feature describe a buildable extrude"); 73 }; 74 solid 75} 76 77fn cylinder() -> BrepSolid { 78 let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key(); 79 solid_from(vec![circle_loop(&mut entities, 5.0)], 10.0) 80} 81 82fn donut() -> BrepSolid { 83 let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key(); 84 let outer = circle_loop(&mut entities, 10.0); 85 let inner = circle_loop(&mut entities, 4.0); 86 solid_from(vec![outer, inner], 6.0) 87} 88 89fn direction(x: f64, y: f64, z: f64) -> UnitVec3 { 90 let Ok(unit) = UnitVec3::try_from_components(x, y, z, TOLERANCE) else { 91 panic!("({x}, {y}, {z}) is a nonzero direction"); 92 }; 93 unit 94} 95 96fn framed(aabb: Aabb3, from: UnitVec3, up: UnitVec3) -> Camera3 { 97 let center = aabb.center(); 98 let span = 0.5 * aabb.extent().norm_mm(); 99 let eye = center + from.into_vec(Length::new::<millimeter>(span * 3.0)); 100 let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(span * 1.2)) else { 101 panic!("half height is positive"); 102 }; 103 let Ok(camera) = Camera3::new(eye, center, up, projection) else { 104 panic!("camera is non-degenerate"); 105 }; 106 camera 107} 108 109fn mesh_scene(solid: &BrepSolid) -> SolidScene { 110 let Ok(mesh) = solid.tessellate( 111 ChordHeightTolerance::from_mm(CHORD), 112 AngleTolerance::from_radians(ANGLE), 113 ) else { 114 panic!("the solid tessellates"); 115 }; 116 let Ok(scene) = SolidScene::from_mesh(&mesh) else { 117 panic!("the mesh packs face pick ids"); 118 }; 119 scene 120} 121 122fn render(ctx: &OffscreenContext, scene: &SolidScene, camera: Camera3) -> SnapshotFrame { 123 let mut renderer = SolidRenderer::new(ctx.gpu(), ctx.color_format()); 124 let Ok(frame) = renderer.render(ctx, scene, camera, &Style::default()) else { 125 panic!("SolidRenderer::render failed"); 126 }; 127 frame 128} 129 130fn aabb_of(solid: &BrepSolid) -> Aabb3 { 131 let Some(aabb) = solid.bounding_box() else { 132 panic!("the solid has a bounding box"); 133 }; 134 aabb 135} 136 137#[test] 138fn cylinder_front_matches_golden() { 139 let ctx = make_context(extent(256)); 140 let solid = cylinder(); 141 let camera = framed( 142 aabb_of(&solid), 143 direction(1.0, 1.0, 0.0), 144 UnitVec3::z_axis(), 145 ); 146 let frame = render(&ctx, &mesh_scene(&solid), camera); 147 check_golden( 148 &frame, 149 "tests/goldens/solid_cylinder_front_256.png", 150 UPDATE_ENV, 151 ); 152} 153 154#[test] 155fn donut_top_matches_golden() { 156 let ctx = make_context(extent(256)); 157 let solid = donut(); 158 let camera = framed(aabb_of(&solid), UnitVec3::z_axis(), UnitVec3::y_axis()); 159 let frame = render(&ctx, &mesh_scene(&solid), camera); 160 check_golden(&frame, "tests/goldens/solid_donut_top_256.png", UPDATE_ENV); 161} 162 163#[test] 164fn solid_pass_writes_unpackable_face_pick_ids() { 165 let ctx = make_context(extent(256)); 166 let solid = cylinder(); 167 let scene = mesh_scene(&solid); 168 let camera = framed( 169 aabb_of(&solid), 170 direction(1.0, 1.0, 0.0), 171 UnitVec3::z_axis(), 172 ); 173 let _frame = render(&ctx, &scene, camera); 174 175 let Ok(index) = PickIndex::build_solid( 176 solid.iter_faces().map(BrepFace::id), 177 solid.iter_edges().map(BrepEdge::id), 178 solid.iter_vertices().map(BrepVertex::id), 179 ) else { 180 panic!("the solid's faces, edges, and vertices build a pick index"); 181 }; 182 let picker = ctx.picker(index.clone()); 183 184 let Ok(corner) = picker.raw_at(PickQuery::new(ViewportPx::new(3), ViewportPx::new(3))) else { 185 panic!("the corner pixel reads back"); 186 }; 187 assert_eq!( 188 corner, 189 PickId::NONE, 190 "background must clear to PickId::NONE, got {corner}", 191 ); 192 193 let Ok(center) = picker.raw_at(PickQuery::new(ViewportPx::new(128), ViewportPx::new(128))) 194 else { 195 panic!("the center pixel reads back"); 196 }; 197 let Some(item) = center.unpack(&index) else { 198 panic!("center pick {center} did not resolve against the solid"); 199 }; 200 assert!( 201 matches!(item, PickedItem::BrepFace(_)), 202 "the center of the cylinder must pick a B-rep face, got {item:?}", 203 ); 204} 205 206#[test] 207fn phong_variant_adds_specular_highlight() { 208 let ctx = make_context(extent(128)); 209 let solid = cylinder(); 210 let scene = mesh_scene(&solid); 211 let camera = framed( 212 aabb_of(&solid), 213 direction(-0.302, -0.503, 0.809), 214 UnitVec3::z_axis(), 215 ); 216 let style = Style::default(); 217 let mut lit = SolidRenderer::new(ctx.gpu(), ctx.color_format()); 218 let mut phong = 219 SolidRenderer::new(ctx.gpu(), ctx.color_format()).with_shading_model(ShadingModel::Phong); 220 let Ok(default_frame) = lit.render(&ctx, &scene, camera, &style) else { 221 panic!("default render failed"); 222 }; 223 let Ok(phong_frame) = phong.render(&ctx, &scene, camera, &style) else { 224 panic!("phong render failed"); 225 }; 226 assert_ne!( 227 default_frame.rgba(), 228 phong_frame.rgba(), 229 "the Phong specular highlight must change the shaded output", 230 ); 231}