Another project
1use core::time::Duration;
2
3use bone_kernel::{
4 BrepSolid, Curve2Kind, ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeProfile,
5 ExtrudeSense, Line2, MergeResult, ProfileEdge, ProfileLoop, evaluate_extrude,
6};
7use bone_render::{
8 CameraTween, EdgeScene, OffscreenContext, SnapshotFrame, SolidRenderer, SolidScene, Style,
9 frame_standard_view,
10};
11use bone_types::{
12 Aabb3, AngleTolerance, Camera3, ChordHeightTolerance, CubicEasing, DisplayMode, FeatureId,
13 Length, Point2, Point3, PositiveLength, SketchEntityId, SketchId, StandardView, Tolerance,
14 UnitVec3, millimeter,
15};
16use slotmap::{Key, SlotMap};
17
18mod common;
19
20use common::{check_golden, extent_square as extent, make_context};
21
22const TOLERANCE: Tolerance = Tolerance::new(1.0e-9);
23const CHORD_MM: f64 = 0.05;
24const ANGLE_RAD: f64 = 0.2;
25const UPDATE_ENV: &str = "BONE_UPDATE_STANDARD_VIEW_GOLDENS";
26
27fn slab() -> BrepSolid {
28 let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
29 let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key();
30 let Ok(plane) = bone_types::Plane3::new(
31 Point3::origin(),
32 UnitVec3::x_axis(),
33 UnitVec3::y_axis(),
34 TOLERANCE,
35 ) else {
36 panic!("x and y axes are orthonormal");
37 };
38 let corners = [
39 Point2::from_mm(0.0, 0.0),
40 Point2::from_mm(2.0, 0.0),
41 Point2::from_mm(2.0, 1.0),
42 Point2::from_mm(0.0, 1.0),
43 ];
44 let edges = (0..4)
45 .map(|index| {
46 let start = corners[index];
47 let end = corners[(index + 1) % 4];
48 let Ok(segment) = Line2::new(start, end, TOLERANCE) else {
49 panic!("rectangle endpoints are distinct");
50 };
51 ProfileEdge::new(
52 Curve2Kind::Line(segment),
53 entities.insert(()),
54 entities.insert(()),
55 )
56 })
57 .collect();
58 let profile = ExtrudeProfile::new(plane, vec![ProfileLoop::Open(edges)]);
59 let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(3.0)) else {
60 panic!("3 mm is a positive length");
61 };
62 let feature = ExtrudeFeature {
63 sketch: SketchId::null(),
64 direction: ExtrudeDirection::Normal {
65 sense: ExtrudeSense::Forward,
66 },
67 end_condition: ExtrudeEndCondition::Blind { depth },
68 draft: None,
69 thin_wall: None,
70 merge_result: MergeResult::Merge,
71 };
72 let Ok(solid) = evaluate_extrude(features.insert(()), &profile, &feature) else {
73 panic!("the rectangle extrudes into a 2x1x3 slab");
74 };
75 solid
76}
77
78fn aabb_of(solid: &BrepSolid) -> Aabb3 {
79 let Some(aabb) = solid.bounding_box() else {
80 panic!("the slab has a bounding box");
81 };
82 aabb
83}
84
85fn scenes(solid: &BrepSolid) -> (SolidScene, EdgeScene) {
86 let Ok(mesh) = solid.tessellate(
87 ChordHeightTolerance::from_mm(CHORD_MM),
88 AngleTolerance::from_radians(ANGLE_RAD),
89 ) else {
90 panic!("the solid tessellates");
91 };
92 let Ok(faces) = SolidScene::from_mesh(&mesh) else {
93 panic!("the mesh packs face pick ids");
94 };
95 let Ok(edges) = EdgeScene::from_solid(solid, &mesh, ChordHeightTolerance::from_mm(CHORD_MM))
96 else {
97 panic!("the solid packs edge pick ids");
98 };
99 (faces, edges)
100}
101
102fn render(
103 ctx: &OffscreenContext,
104 faces: &SolidScene,
105 edges: &EdgeScene,
106 camera: Camera3,
107) -> SnapshotFrame {
108 let mut renderer = SolidRenderer::new(ctx.gpu(), ctx.color_format());
109 let Ok(frame) = renderer.render_display(
110 ctx,
111 faces,
112 edges,
113 camera,
114 &Style::default(),
115 DisplayMode::ShadedWithEdges,
116 ) else {
117 panic!("SolidRenderer::render_display failed");
118 };
119 frame
120}
121
122fn view(solid: &BrepSolid, which: StandardView, size: bone_render::ViewportExtent) -> Camera3 {
123 let Ok(camera) = frame_standard_view(aabb_of(solid), size, which, None) else {
124 panic!("a fixed standard view frames the slab");
125 };
126 camera
127}
128
129#[test]
130fn front_view_matches_golden() {
131 let size = extent(256);
132 let ctx = make_context(size);
133 let solid = slab();
134 let (faces, edges) = scenes(&solid);
135 let frame = render(
136 &ctx,
137 &faces,
138 &edges,
139 view(&solid, StandardView::Front, size),
140 );
141 check_golden(
142 &frame,
143 "tests/goldens/standard_view_front_256.png",
144 UPDATE_ENV,
145 );
146}
147
148#[test]
149fn top_view_matches_golden() {
150 let size = extent(256);
151 let ctx = make_context(size);
152 let solid = slab();
153 let (faces, edges) = scenes(&solid);
154 let frame = render(&ctx, &faces, &edges, view(&solid, StandardView::Top, size));
155 check_golden(
156 &frame,
157 "tests/goldens/standard_view_top_256.png",
158 UPDATE_ENV,
159 );
160}
161
162#[test]
163fn right_view_matches_golden() {
164 let size = extent(256);
165 let ctx = make_context(size);
166 let solid = slab();
167 let (faces, edges) = scenes(&solid);
168 let frame = render(
169 &ctx,
170 &faces,
171 &edges,
172 view(&solid, StandardView::Right, size),
173 );
174 check_golden(
175 &frame,
176 "tests/goldens/standard_view_right_256.png",
177 UPDATE_ENV,
178 );
179}
180
181#[test]
182fn front_to_top_tween_midpoint_matches_golden() {
183 let size = extent(256);
184 let ctx = make_context(size);
185 let solid = slab();
186 let (faces, edges) = scenes(&solid);
187 let tween = CameraTween::eased(
188 view(&solid, StandardView::Front, size),
189 view(&solid, StandardView::Top, size),
190 Duration::from_millis(180),
191 CubicEasing::STANDARD,
192 );
193 let Ok(mid) = tween.sample(Duration::from_millis(90)) else {
194 panic!("the tween samples a valid midpoint camera");
195 };
196 let frame = render(&ctx, &faces, &edges, mid);
197 check_golden(
198 &frame,
199 "tests/goldens/view_tween_front_to_top_mid_256.png",
200 UPDATE_ENV,
201 );
202}