Another project
1use std::path::PathBuf;
2
3use bone_document::{
4 DimensionKind, EditOutcome, Sketch, SketchDimension, SketchEdit, SketchEntity,
5};
6use bone_render::{
7 Camera2, PixelDiff, PixelDiffThreshold, PixelsPerMm, SketchRenderer, SketchScene, Style,
8 decode_png, encode_png,
9};
10use bone_types::{
11 Angle, Length, Point2, Point3, SketchEntityId, SketchPlaneBasis, Tolerance, UnitVec3,
12};
13use uom::si::angle::degree;
14use uom::si::length::millimeter;
15
16mod common;
17
18use common::{extent_square as extent, make_context};
19
20const GOLDEN: &str = "tests/goldens/dimensions_256.png";
21const UPDATE_ENV: &str = "BONE_UPDATE_DIMENSIONS_GOLDEN";
22const DIFF_TOLERANCE: f64 = 20.0 / 255.0;
23
24fn plane() -> SketchPlaneBasis {
25 let Ok(basis) = SketchPlaneBasis::new(
26 Point3::origin(),
27 UnitVec3::x_axis(),
28 UnitVec3::y_axis(),
29 Tolerance::new(1e-9),
30 ) else {
31 panic!("xy plane basis is orthogonal");
32 };
33 basis
34}
35
36fn add_point(s: Sketch, x: f64, y: f64) -> (Sketch, SketchEntityId) {
37 let Ok((next, EditOutcome::Entity(id))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
38 Point2::from_mm(x, y),
39 ))) else {
40 panic!("add point");
41 };
42 (next, id)
43}
44
45fn add_line(s: Sketch, a: SketchEntityId, b: SketchEntityId) -> (Sketch, SketchEntityId) {
46 let Ok((next, EditOutcome::Entity(id))) =
47 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
48 else {
49 panic!("add line");
50 };
51 (next, id)
52}
53
54fn add_circle(s: Sketch, center: SketchEntityId, radius_mm: f64) -> (Sketch, SketchEntityId) {
55 let Ok((next, EditOutcome::Entity(id))) = s.apply(SketchEdit::AddEntity(SketchEntity::circle(
56 center,
57 Length::new::<millimeter>(radius_mm),
58 false,
59 ))) else {
60 panic!("add circle");
61 };
62 (next, id)
63}
64
65fn add_dimension(s: Sketch, dim: SketchDimension) -> Sketch {
66 let Ok((next, _)) = s.apply(SketchEdit::AddDimension(dim)) else {
67 panic!("add dimension {dim:?}");
68 };
69 next
70}
71
72fn four_kind_scene() -> (Sketch, SketchScene) {
73 let s = Sketch::new(plane());
74
75 let (s, lp0) = add_point(s, -6.0, 3.0);
76 let (s, lp1) = add_point(s, 0.0, 3.0);
77 let s = add_dimension(
78 s,
79 SketchDimension::Linear {
80 a: lp0,
81 b: lp1,
82 value: Length::new::<millimeter>(6.0),
83 kind: DimensionKind::Driving,
84 },
85 );
86
87 let (s, rc) = add_point(s, 4.5, 3.0);
88 let (s, rcirc) = add_circle(s, rc, 1.5);
89 let s = add_dimension(
90 s,
91 SketchDimension::Radius {
92 target: rcirc,
93 value: Length::new::<millimeter>(1.5),
94 kind: DimensionKind::Driving,
95 },
96 );
97
98 let (s, dc) = add_point(s, -3.0, -2.5);
99 let (s, dcirc) = add_circle(s, dc, 2.0);
100 let s = add_dimension(
101 s,
102 SketchDimension::Diameter {
103 target: dcirc,
104 value: Length::new::<millimeter>(4.0),
105 kind: DimensionKind::Driving,
106 },
107 );
108
109 let (s, ap0) = add_point(s, 2.5, -4.0);
110 let (s, ap1) = add_point(s, 5.5, -4.0);
111 let (s, aline_a) = add_line(s, ap0, ap1);
112 let (s, ap2) = add_point(s, 4.0, -1.0);
113 let (s, aline_b) = add_line(s, ap0, ap2);
114 let s = add_dimension(
115 s,
116 SketchDimension::Angular {
117 a: aline_a,
118 b: aline_b,
119 value: Angle::new::<degree>(45.0),
120 kind: DimensionKind::Driving,
121 },
122 );
123
124 let Ok(scene) = SketchScene::extract(&s) else {
125 panic!("scene extract");
126 };
127 (s, scene)
128}
129
130fn golden_path() -> PathBuf {
131 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(GOLDEN)
132}
133
134#[test]
135fn four_dimension_kinds_match_golden() {
136 let size = extent(256);
137 let ctx = make_context(size);
138 let (_sketch, scene) = four_kind_scene();
139 assert_eq!(scene.dimensions().len(), 4);
140 let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
141 let camera = Camera2::new(ctx.extent()).with_zoom(PixelsPerMm::new(18.0));
142 let style = Style::default();
143 let Ok(frame) = renderer.render(&ctx, &scene, camera, &style) else {
144 panic!("SketchRenderer::render failed");
145 };
146 let path = golden_path();
147
148 if std::env::var(UPDATE_ENV).is_ok() {
149 let Ok(bytes) = encode_png(&frame) else {
150 panic!("encode_png failed");
151 };
152 if let Some(parent) = path.parent()
153 && let Err(e) = std::fs::create_dir_all(parent)
154 {
155 panic!("create goldens dir {}: {e}", parent.display());
156 }
157 if let Err(e) = std::fs::write(&path, &bytes) {
158 panic!("write golden {}: {e}", path.display());
159 }
160 return;
161 }
162
163 let Ok(bytes) = std::fs::read(&path) else {
164 panic!(
165 "golden missing at {}: rerun with {UPDATE_ENV}=1 to generate",
166 path.display()
167 );
168 };
169 let Ok((golden_extent, golden_rgba)) = decode_png(&bytes) else {
170 panic!("failed to decode golden PNG");
171 };
172 assert_eq!(golden_extent, size);
173 let threshold = PixelDiffThreshold::new(DIFF_TOLERANCE);
174 let Ok(report) = PixelDiff::compare(&frame, &golden_rgba, threshold) else {
175 panic!("PixelDiff rejected inputs");
176 };
177 assert!(
178 report.is_clean(),
179 "dimension render drifted: {} mismatches, worst {:?}, backend {}",
180 report.over_threshold(),
181 report.worst(),
182 frame.backend(),
183 );
184}
185
186#[test]
187fn cache_reuses_across_repeated_renders_same_text() {
188 let size = extent(128);
189 let ctx = make_context(size);
190 let (_sketch, scene) = four_kind_scene();
191 let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
192 renderer.prepare(&scene, &Style::default());
193 let first_len = renderer.text_cache_len();
194 renderer.prepare(&scene, &Style::default());
195 let second_len = renderer.text_cache_len();
196 assert_eq!(
197 first_len, second_len,
198 "cache size should be stable across repeated prepares on identical scene",
199 );
200}
201
202#[test]
203fn cache_refreshes_on_value_change() {
204 use bone_document::SketchEdit;
205 let size = extent(128);
206 let ctx = make_context(size);
207 let (sketch, scene) = four_kind_scene();
208 let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
209 renderer.prepare(&scene, &Style::default());
210 let initial_len = renderer.text_cache_len();
211
212 let Some(&target_id) = sketch.dimension_order().first() else {
213 panic!("scene has at least one dimension");
214 };
215 let Ok((updated_sketch, _)) = sketch.apply(SketchEdit::UpdateDimensionValue {
216 id: target_id,
217 value: bone_document::DimensionValue::Length(Length::new::<millimeter>(999.0)),
218 }) else {
219 panic!("update dimension value");
220 };
221 let Ok(updated_scene) = SketchScene::extract(&updated_sketch) else {
222 panic!("scene extract after update");
223 };
224
225 let Some(first) = updated_scene.dimensions().first() else {
226 panic!("updated scene has dimensions");
227 };
228 let new_text = first.text().to_owned();
229 assert!(
230 new_text.contains("999"),
231 "expected refreshed text to reflect updated value, got {new_text:?}",
232 );
233
234 renderer.prepare(&updated_scene, &Style::default());
235 let refreshed_len = renderer.text_cache_len();
236 assert_eq!(initial_len, refreshed_len);
237}
238
239#[test]
240fn cache_refreshes_on_font_size_change() {
241 let size = extent(128);
242 let ctx = make_context(size);
243 let (_sketch, scene) = four_kind_scene();
244 let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
245 let small = Style::default();
246 renderer.prepare(&scene, &small);
247 let baseline_len = renderer.text_cache_len();
248 let big = Style::default().with_text(small.text().with_font_size_px(28.0));
249 renderer.prepare(&scene, &big);
250 assert_eq!(
251 baseline_len,
252 renderer.text_cache_len(),
253 "font-size change must keep cache size equal to dimension count",
254 );
255}