Another project
1use std::path::PathBuf;
2
3use bone_document::{EditOutcome, Sketch, SketchEdit, SketchEntity, SketchRelation};
4use bone_render::{
5 Camera2, OffscreenContext, PixelDiff, PixelDiffThreshold, PixelsPerMm, RelationGlyphKind,
6 SketchRenderer, SketchScene, Style, decode_png, encode_png,
7};
8use bone_types::{Length, Point2, Point3, SketchEntityId, SketchPlaneBasis, Tolerance, UnitVec3};
9use uom::si::length::millimeter;
10
11mod common;
12
13use common::{extent_square as extent, make_context};
14
15const GOLDEN: &str = "tests/goldens/relations_256.png";
16const UPDATE_ENV: &str = "BONE_UPDATE_RELATIONS_GOLDEN";
17const DIFF_TOLERANCE: f64 = 16.0 / 255.0;
18
19fn plane() -> SketchPlaneBasis {
20 let Ok(basis) = SketchPlaneBasis::new(
21 Point3::origin(),
22 UnitVec3::x_axis(),
23 UnitVec3::y_axis(),
24 Tolerance::new(1e-9),
25 ) else {
26 panic!("xy plane basis is orthogonal");
27 };
28 basis
29}
30
31fn add_point(s: Sketch, x: f64, y: f64) -> (Sketch, SketchEntityId) {
32 let Ok((next, EditOutcome::Entity(id))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
33 Point2::from_mm(x, y),
34 ))) else {
35 panic!("add point");
36 };
37 (next, id)
38}
39
40fn add_line(s: Sketch, a: SketchEntityId, b: SketchEntityId) -> (Sketch, SketchEntityId) {
41 let Ok((next, EditOutcome::Entity(id))) =
42 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
43 else {
44 panic!("add line");
45 };
46 (next, id)
47}
48
49fn add_circle(s: Sketch, center: SketchEntityId, radius_mm: f64) -> (Sketch, SketchEntityId) {
50 let Ok((next, EditOutcome::Entity(id))) = s.apply(SketchEdit::AddEntity(SketchEntity::circle(
51 center,
52 Length::new::<millimeter>(radius_mm),
53 false,
54 ))) else {
55 panic!("add circle");
56 };
57 (next, id)
58}
59
60fn add_relation(s: Sketch, rel: SketchRelation) -> Sketch {
61 let Ok((next, _)) = s.apply(SketchEdit::AddRelation(rel)) else {
62 panic!("add relation {rel:?}");
63 };
64 next
65}
66
67fn one_per_kind_scene() -> SketchScene {
68 let s = Sketch::new(plane());
69
70 let (s, coin_a) = add_point(s, -4.0, 4.0);
71 let (s, coin_b) = add_point(s, -4.0, 4.0);
72 let s = add_relation(s, SketchRelation::Coincident(coin_a, coin_b));
73
74 let (s, ha) = add_point(s, -1.5, 4.2);
75 let (s, hb) = add_point(s, 1.5, 4.2);
76 let (s, hl) = add_line(s, ha, hb);
77 let s = add_relation(s, SketchRelation::Horizontal(hl));
78
79 let (s, va) = add_point(s, 4.0, 3.0);
80 let (s, vb) = add_point(s, 4.0, 5.2);
81 let (s, vl) = add_line(s, va, vb);
82 let s = add_relation(s, SketchRelation::Vertical(vl));
83
84 let (s, pa0) = add_point(s, -5.0, 1.0);
85 let (s, pa1) = add_point(s, -2.5, 1.9);
86 let (s, pla) = add_line(s, pa0, pa1);
87 let (s, pb0) = add_point(s, -5.0, -0.5);
88 let (s, pb1) = add_point(s, -2.5, 0.4);
89 let (s, plb) = add_line(s, pb0, pb1);
90 let s = add_relation(s, SketchRelation::Parallel(pla, plb));
91
92 let (s, xa0) = add_point(s, -0.5, 1.4);
93 let (s, xa1) = add_point(s, 1.3, 1.4);
94 let (s, xla) = add_line(s, xa0, xa1);
95 let (s, xb0) = add_point(s, 0.5, 0.3);
96 let (s, xb1) = add_point(s, 0.5, 1.9);
97 let (s, xlb) = add_line(s, xb0, xb1);
98 let s = add_relation(s, SketchRelation::Perpendicular(xla, xlb));
99
100 let (s, tc) = add_point(s, 4.5, 1.2);
101 let (s, tcirc) = add_circle(s, tc, 0.7);
102 let (s, tla) = add_point(s, 3.0, 1.9);
103 let (s, tlb) = add_point(s, 6.0, 1.9);
104 let (s, tline) = add_line(s, tla, tlb);
105 let s = add_relation(s, SketchRelation::Tangent(tcirc, tline));
106
107 let (s, ea0) = add_point(s, -4.8, -2.5);
108 let (s, ea1) = add_point(s, -3.0, -2.5);
109 let (s, ela) = add_line(s, ea0, ea1);
110 let (s, eb0) = add_point(s, -4.8, -3.8);
111 let (s, eb1) = add_point(s, -3.0, -3.8);
112 let (s, elb) = add_line(s, eb0, eb1);
113 let s = add_relation(s, SketchRelation::Equal(ela, elb));
114
115 let (s, ccc) = add_point(s, -0.2, -2.8);
116 let (s, cca) = add_circle(s, ccc, 0.8);
117 let (s, ccb) = add_circle(s, ccc, 1.6);
118 let s = add_relation(s, SketchRelation::Concentric(cca, ccb));
119
120 let (s, fp) = add_point(s, 4.5, -3.0);
121 let s = add_relation(s, SketchRelation::Fix(fp));
122
123 let (s, mp_a) = add_point(s, 2.0, -3.0);
124 let (s, mp_b) = add_point(s, 5.0, -3.0);
125 let (s, mp_line) = add_line(s, mp_a, mp_b);
126 let (s, mp_point) = add_point(s, 3.5, -3.0);
127 let s = add_relation(
128 s,
129 SketchRelation::Midpoint {
130 point: mp_point,
131 line: mp_line,
132 },
133 );
134
135 let (s, sym_a0) = add_point(s, 6.5, -2.5);
136 let (s, sym_a1) = add_point(s, 9.0, -2.5);
137 let (s, sym_axis) = add_line(s, sym_a0, sym_a1);
138 let (s, sym_p) = add_point(s, 7.5, -1.5);
139 let (s, sym_q) = add_point(s, 7.5, -3.5);
140 let s = add_relation(
141 s,
142 SketchRelation::Symmetric {
143 a: sym_p,
144 b: sym_q,
145 axis: sym_axis,
146 },
147 );
148
149 let Ok(scene) = SketchScene::extract(&s) else {
150 panic!("scene extract");
151 };
152 assert_eq!(
153 scene.relations().len(),
154 RelationGlyphKind::all().len(),
155 "expected one scene glyph per relation kind",
156 );
157 scene
158}
159
160fn render_scene(ctx: &OffscreenContext, scene: &SketchScene) -> bone_render::SnapshotFrame {
161 let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
162 let camera = Camera2::new(ctx.extent()).with_zoom(PixelsPerMm::new(20.0));
163 let style = Style::default();
164 let Ok(frame) = renderer.render(ctx, scene, camera, &style) else {
165 panic!("SketchRenderer::render failed");
166 };
167 frame
168}
169
170fn golden_path() -> PathBuf {
171 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(GOLDEN)
172}
173
174#[test]
175fn relation_kinds_match_golden() {
176 let size = extent(256);
177 let ctx = make_context(size);
178 let scene = one_per_kind_scene();
179 let frame = render_scene(&ctx, &scene);
180 let path = golden_path();
181
182 if std::env::var(UPDATE_ENV).is_ok() {
183 let Ok(bytes) = encode_png(&frame) else {
184 panic!("encode_png failed");
185 };
186 if let Some(parent) = path.parent()
187 && let Err(e) = std::fs::create_dir_all(parent)
188 {
189 panic!("create goldens dir {}: {e}", parent.display());
190 }
191 if let Err(e) = std::fs::write(&path, &bytes) {
192 panic!("write golden {}: {e}", path.display());
193 }
194 return;
195 }
196
197 let Ok(bytes) = std::fs::read(&path) else {
198 panic!(
199 "golden missing at {}: rerun with {UPDATE_ENV}=1 to generate",
200 path.display()
201 );
202 };
203 let Ok((golden_extent, golden_rgba)) = decode_png(&bytes) else {
204 panic!("failed to decode golden PNG");
205 };
206 assert_eq!(golden_extent, size, "golden extent drift");
207 let threshold = PixelDiffThreshold::new(DIFF_TOLERANCE);
208 let Ok(report) = PixelDiff::compare(&frame, &golden_rgba, threshold) else {
209 panic!("PixelDiff rejected inputs");
210 };
211 assert!(
212 report.is_clean(),
213 "relation render drifted from golden: {} mismatches, worst {:?}, backend {}",
214 report.over_threshold(),
215 report.worst(),
216 frame.backend(),
217 );
218}
219
220#[test]
221fn relation_glyph_pick_ids_survive_render() {
222 let size = extent(256);
223 let ctx = make_context(size);
224 let scene = one_per_kind_scene();
225 let frame = render_scene(&ctx, &scene);
226 let Ok(index) = scene.pick_index() else {
227 panic!("pick index build");
228 };
229
230 let unresolved: Vec<RelationGlyphKind> = scene
231 .relations()
232 .iter()
233 .filter(|g| g.pick().unpack(&index).is_none())
234 .map(|g| g.kind())
235 .collect();
236 assert!(
237 unresolved.is_empty(),
238 "relation pick ids did not decode: {unresolved:?}",
239 );
240
241 assert_eq!(frame.extent(), size);
242}