Another project
0

Configure Feed

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

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}