Another project
1use std::path::PathBuf;
2
3use bone_document::{EditOutcome, Sketch, SketchEdit, SketchEntity};
4use bone_render::{
5 Camera2, OffscreenContext, PixelDiff, PixelDiffThreshold, PixelsPerMm, SketchRenderer,
6 SketchScene, SnapshotFrame, Style, ViewportExtent, decode_png, encode_png,
7};
8use bone_types::{Length, Point2, Point3, SketchEntityId, SketchPlaneBasis, Tolerance, UnitVec3};
9use uom::si::length::millimeter;
10
11mod common;
12
13const REAL_GOLDEN: &str = "tests/goldens/construction_real_256.png";
14const DASHED_GOLDEN: &str = "tests/goldens/construction_dashed_256.png";
15const UPDATE_ENV: &str = "BONE_UPDATE_CONSTRUCTION_GOLDENS";
16const DIFF_TOLERANCE: f64 = 16.0 / 255.0;
17const DISTINCT_MIN_PIXELS: u32 = 50;
18
19fn extent() -> ViewportExtent {
20 common::extent_square(256)
21}
22
23fn plane() -> SketchPlaneBasis {
24 let Ok(basis) = SketchPlaneBasis::new(
25 Point3::origin(),
26 UnitVec3::x_axis(),
27 UnitVec3::y_axis(),
28 Tolerance::new(1e-9),
29 ) else {
30 panic!("xy plane basis is orthogonal");
31 };
32 basis
33}
34
35fn add_point(s: Sketch, x: f64, y: f64) -> (Sketch, SketchEntityId) {
36 let Ok((next, EditOutcome::Entity(id))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
37 Point2::from_mm(x, y),
38 ))) else {
39 panic!("add point");
40 };
41 (next, id)
42}
43
44fn add_line(s: Sketch, a: SketchEntityId, b: SketchEntityId, construction: bool) -> Sketch {
45 let Ok((next, _)) = s.apply(SketchEdit::AddEntity(SketchEntity::line(
46 a,
47 b,
48 construction,
49 ))) else {
50 panic!("add line");
51 };
52 next
53}
54
55fn add_circle(s: Sketch, center: SketchEntityId, radius_mm: f64, construction: bool) -> Sketch {
56 let Ok((next, _)) = s.apply(SketchEdit::AddEntity(SketchEntity::circle(
57 center,
58 Length::new::<millimeter>(radius_mm),
59 construction,
60 ))) else {
61 panic!("add circle");
62 };
63 next
64}
65
66fn add_arc(
67 s: Sketch,
68 center: SketchEntityId,
69 start: SketchEntityId,
70 end: SketchEntityId,
71 construction: bool,
72) -> Sketch {
73 let Ok((next, _)) = s.apply(SketchEdit::AddEntity(SketchEntity::arc(
74 center,
75 start,
76 end,
77 construction,
78 ))) else {
79 panic!("add arc");
80 };
81 next
82}
83
84fn build_scene(construction: bool) -> SketchScene {
85 let s = Sketch::new(plane());
86
87 let (s, la) = add_point(s, -0.9, 0.35);
88 let (s, lb) = add_point(s, 0.9, 0.35);
89 let s = add_line(s, la, lb, construction);
90
91 let (s, cc) = add_point(s, -0.5, -0.3);
92 let s = add_circle(s, cc, 0.25, construction);
93
94 let (s, ac) = add_point(s, 0.5, -0.3);
95 let (s, astart) = add_point(s, 0.85, -0.3);
96 let (s, aend) = add_point(s, 0.5, 0.05);
97 let s = add_arc(s, ac, astart, aend, construction);
98
99 let Ok(scene) = SketchScene::extract(&s) else {
100 panic!("scene extract");
101 };
102 scene
103}
104
105fn render_scene(ctx: &OffscreenContext, scene: &SketchScene) -> SnapshotFrame {
106 let camera = Camera2::new(ctx.extent()).with_zoom(PixelsPerMm::new(100.0));
107 let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
108 let style = Style::default();
109 let Ok(frame) = renderer.render(ctx, scene, camera, &style) else {
110 panic!("SketchRenderer::render failed");
111 };
112 frame
113}
114
115fn golden_path(name: &str) -> PathBuf {
116 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(name)
117}
118
119fn match_or_update_golden(frame: &SnapshotFrame, name: &str) {
120 let path = golden_path(name);
121 if std::env::var(UPDATE_ENV).is_ok() {
122 let Ok(bytes) = encode_png(frame) else {
123 panic!("encode_png failed");
124 };
125 if let Some(parent) = path.parent()
126 && let Err(e) = std::fs::create_dir_all(parent)
127 {
128 panic!("create goldens dir {}: {e}", parent.display());
129 }
130 if let Err(e) = std::fs::write(&path, &bytes) {
131 panic!("write golden {}: {e}", path.display());
132 }
133 return;
134 }
135 let Ok(bytes) = std::fs::read(&path) else {
136 panic!(
137 "golden missing at {}: rerun with {UPDATE_ENV}=1 to generate",
138 path.display()
139 );
140 };
141 let Ok((golden_extent, golden_rgba)) = decode_png(&bytes) else {
142 panic!("failed to decode golden PNG at {}", path.display());
143 };
144 assert_eq!(
145 golden_extent,
146 frame.extent(),
147 "golden extent drift for {name}"
148 );
149 let threshold = PixelDiffThreshold::new(DIFF_TOLERANCE);
150 let Ok(report) = PixelDiff::compare(frame, &golden_rgba, threshold) else {
151 panic!("PixelDiff rejected inputs for {name}");
152 };
153 assert!(
154 report.is_clean(),
155 "{name} drifted: {} mismatches, worst {:?}, backend {}",
156 report.over_threshold(),
157 report.worst(),
158 frame.backend(),
159 );
160}
161
162#[test]
163fn real_and_dashed_goldens_match() {
164 let ctx = common::make_context(extent());
165 let real = render_scene(&ctx, &build_scene(false));
166 match_or_update_golden(&real, REAL_GOLDEN);
167 let dashed = render_scene(&ctx, &build_scene(true));
168 match_or_update_golden(&dashed, DASHED_GOLDEN);
169}
170
171#[test]
172fn construction_render_differs_from_real_render() {
173 let ctx = common::make_context(extent());
174 let real = render_scene(&ctx, &build_scene(false));
175 let dashed = render_scene(&ctx, &build_scene(true));
176 assert_eq!(real.extent(), dashed.extent());
177 let Ok(report) = PixelDiff::compare(&real, dashed.rgba(), PixelDiffThreshold::EXACT) else {
178 panic!("PixelDiff rejected inputs");
179 };
180 assert!(
181 report.over_threshold() >= DISTINCT_MIN_PIXELS,
182 "construction styling not distinct from real: only {} pixels differ",
183 report.over_threshold(),
184 );
185}