Another project
0

Configure Feed

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

at main 5.7 kB View raw
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}