···11+DejaVu Sans Mono (unmodified redistribution)
22+33+Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.
44+Bitstream Vera is a trademark of Bitstream, Inc.
55+DejaVu changes are in public domain.
66+77+Permission is hereby granted, free of charge, to any person obtaining a copy
88+of the fonts accompanying this license ("Fonts") and associated documentation
99+files (the "Font Software"), to reproduce and distribute the Font Software,
1010+including without limitation the rights to use, copy, merge, publish,
1111+distribute, and/or sell copies of the Font Software, and to permit persons to
1212+whom the Font Software is furnished to do so, subject to the following
1313+conditions:
1414+1515+The above copyright and trademark notices and this permission notice shall be
1616+included in all copies of one or more of the Font Software typefaces.
1717+1818+The Font Software may be modified, altered, or added to, and in particular
1919+the designs of glyphs or characters in the Fonts may be modified and
2020+additional glyphs or characters may be added to the Fonts, only if the fonts
2121+are renamed to names not containing either the words "Bitstream" or the word
2222+"Vera".
2323+2424+This License becomes null and void to the extent applicable to Fonts or Font
2525+Software that has been modified and is distributed under the "Bitstream Vera"
2626+names.
2727+2828+The Font Software may be sold as part of a larger software package but no
2929+copy of one or more of the Font Software typefaces may be sold by itself.
3030+3131+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
3232+OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
3333+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
3434+TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION
3535+BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL,
3636+SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION
3737+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO
3838+USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
3939+4040+Except as contained in this notice, the names of Gnome, the Gnome Foundation,
4141+and Bitstream Inc., shall not be used in advertising or otherwise to promote
4242+the sale, use or other dealings in this Font Software without prior written
4343+authorization from the Gnome Foundation or Bitstream Inc., respectively. For
4444+further information, contact: fonts at gnome dot org.
···11pub mod arc;
22+pub mod glyph;
23pub mod grid;
34pub mod lines;
55+pub mod text;
4657pub use arc::ArcPipeline;
88+pub use glyph::GlyphPipeline;
69pub use grid::GridPipeline;
710pub use lines::LinesPipeline;
1111+pub use text::TextPipeline;
812913use crate::camera::Camera2;
1014use crate::snapshot::Style;
···9898fn arc_circle_at_100x_zoom_matches_golden() {
9999 let size = extent(256);
100100 let ctx = make_context(size);
101101- let renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
101101+ let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
102102 let scene = arc_circle_scene();
103103 let camera = Camera2::new(size).with_zoom(PixelsPerMm::new(100.0));
104104 let style = Style::default();
+1-1
crates/bone-render/tests/construction_styling.rs
···113113114114fn render_scene(ctx: &OffscreenContext, scene: &SketchScene) -> SnapshotFrame {
115115 let camera = Camera2::new(ctx.extent()).with_zoom(PixelsPerMm::new(100.0));
116116- let renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
116116+ let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
117117 let style = Style::default();
118118 let Ok(frame) = renderer.render(ctx, scene, camera, &style) else {
119119 panic!("SketchRenderer::render failed");
+265
crates/bone-render/tests/dimensions.rs
···11+use std::path::PathBuf;
22+33+use bone_document::{
44+ DimensionKind, EditOutcome, Sketch, SketchDimension, SketchEdit, SketchEntity,
55+};
66+use bone_render::{
77+ Camera2, OffscreenContext, PixelDiff, PixelDiffThreshold, PixelsPerMm, RenderError,
88+ SketchRenderer, SketchScene, Style, ViewportExtent, ViewportPx, decode_png, encode_png,
99+};
1010+use bone_types::{
1111+ Angle, Length, Point2, Point3, SketchEntityId, SketchPlaneBasis, Tolerance, UnitVec3,
1212+};
1313+use uom::si::angle::degree;
1414+use uom::si::length::millimeter;
1515+1616+const GOLDEN: &str = "tests/goldens/dimensions_256.png";
1717+const UPDATE_ENV: &str = "BONE_UPDATE_DIMENSIONS_GOLDEN";
1818+const DIFF_TOLERANCE: f64 = 20.0 / 255.0;
1919+2020+fn extent(side: u32) -> ViewportExtent {
2121+ ViewportExtent::square(ViewportPx::new(side))
2222+}
2323+2424+fn make_context(extent: ViewportExtent) -> OffscreenContext {
2525+ match pollster::block_on(OffscreenContext::new(extent)) {
2626+ Ok(ctx) => ctx,
2727+ Err(RenderError::NoAdapter(e)) => panic!(
2828+ "no wgpu adapter available, configure lavapipe or an iGPU for this test host: {e}"
2929+ ),
3030+ Err(e) => panic!("offscreen context init failed: {e}"),
3131+ }
3232+}
3333+3434+fn plane() -> SketchPlaneBasis {
3535+ let Ok(basis) = SketchPlaneBasis::new(
3636+ Point3::origin(),
3737+ UnitVec3::x_axis(),
3838+ UnitVec3::y_axis(),
3939+ Tolerance::new(1e-9),
4040+ ) else {
4141+ panic!("xy plane basis is orthogonal");
4242+ };
4343+ basis
4444+}
4545+4646+fn add_point(s: Sketch, x: f64, y: f64) -> (Sketch, SketchEntityId) {
4747+ let Ok((next, EditOutcome::Entity(id))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
4848+ Point2::from_mm(x, y),
4949+ ))) else {
5050+ panic!("add point");
5151+ };
5252+ (next, id)
5353+}
5454+5555+fn add_line(s: Sketch, a: SketchEntityId, b: SketchEntityId) -> (Sketch, SketchEntityId) {
5656+ let Ok((next, EditOutcome::Entity(id))) =
5757+ s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
5858+ else {
5959+ panic!("add line");
6060+ };
6161+ (next, id)
6262+}
6363+6464+fn add_circle(s: Sketch, center: SketchEntityId, radius_mm: f64) -> (Sketch, SketchEntityId) {
6565+ let Ok((next, EditOutcome::Entity(id))) = s.apply(SketchEdit::AddEntity(SketchEntity::circle(
6666+ center,
6767+ Length::new::<millimeter>(radius_mm),
6868+ false,
6969+ ))) else {
7070+ panic!("add circle");
7171+ };
7272+ (next, id)
7373+}
7474+7575+fn add_dimension(s: Sketch, dim: SketchDimension) -> Sketch {
7676+ let Ok((next, _)) = s.apply(SketchEdit::AddDimension(dim)) else {
7777+ panic!("add dimension {dim:?}");
7878+ };
7979+ next
8080+}
8181+8282+fn four_kind_scene() -> (Sketch, SketchScene) {
8383+ let s = Sketch::new(plane());
8484+8585+ let (s, lp0) = add_point(s, -6.0, 3.0);
8686+ let (s, lp1) = add_point(s, 0.0, 3.0);
8787+ let s = add_dimension(
8888+ s,
8989+ SketchDimension::Linear {
9090+ a: lp0,
9191+ b: lp1,
9292+ value: Length::new::<millimeter>(6.0),
9393+ kind: DimensionKind::Driving,
9494+ },
9595+ );
9696+9797+ let (s, rc) = add_point(s, 4.5, 3.0);
9898+ let (s, rcirc) = add_circle(s, rc, 1.5);
9999+ let s = add_dimension(
100100+ s,
101101+ SketchDimension::Radius {
102102+ target: rcirc,
103103+ value: Length::new::<millimeter>(1.5),
104104+ kind: DimensionKind::Driving,
105105+ },
106106+ );
107107+108108+ let (s, dc) = add_point(s, -3.0, -2.5);
109109+ let (s, dcirc) = add_circle(s, dc, 2.0);
110110+ let s = add_dimension(
111111+ s,
112112+ SketchDimension::Diameter {
113113+ target: dcirc,
114114+ value: Length::new::<millimeter>(4.0),
115115+ kind: DimensionKind::Driving,
116116+ },
117117+ );
118118+119119+ let (s, ap0) = add_point(s, 2.5, -4.0);
120120+ let (s, ap1) = add_point(s, 5.5, -4.0);
121121+ let (s, aline_a) = add_line(s, ap0, ap1);
122122+ let (s, ap2) = add_point(s, 4.0, -1.0);
123123+ let (s, aline_b) = add_line(s, ap0, ap2);
124124+ let s = add_dimension(
125125+ s,
126126+ SketchDimension::Angular {
127127+ a: aline_a,
128128+ b: aline_b,
129129+ value: Angle::new::<degree>(45.0),
130130+ kind: DimensionKind::Driving,
131131+ },
132132+ );
133133+134134+ let Ok(scene) = SketchScene::extract(&s) else {
135135+ panic!("scene extract");
136136+ };
137137+ (s, scene)
138138+}
139139+140140+fn golden_path() -> PathBuf {
141141+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(GOLDEN)
142142+}
143143+144144+#[test]
145145+fn four_dimension_kinds_match_golden() {
146146+ let size = extent(256);
147147+ let ctx = make_context(size);
148148+ let (_sketch, scene) = four_kind_scene();
149149+ assert_eq!(scene.dimensions().len(), 4);
150150+ let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
151151+ let camera = Camera2::new(ctx.extent()).with_zoom(PixelsPerMm::new(18.0));
152152+ let style = Style::default();
153153+ let Ok(frame) = renderer.render(&ctx, &scene, camera, &style) else {
154154+ panic!("SketchRenderer::render failed");
155155+ };
156156+ let path = golden_path();
157157+158158+ if std::env::var(UPDATE_ENV).is_ok() {
159159+ let Ok(bytes) = encode_png(&frame) else {
160160+ panic!("encode_png failed");
161161+ };
162162+ if let Some(parent) = path.parent()
163163+ && let Err(e) = std::fs::create_dir_all(parent)
164164+ {
165165+ panic!("create goldens dir {}: {e}", parent.display());
166166+ }
167167+ if let Err(e) = std::fs::write(&path, &bytes) {
168168+ panic!("write golden {}: {e}", path.display());
169169+ }
170170+ return;
171171+ }
172172+173173+ let Ok(bytes) = std::fs::read(&path) else {
174174+ panic!(
175175+ "golden missing at {}: rerun with {UPDATE_ENV}=1 to generate",
176176+ path.display()
177177+ );
178178+ };
179179+ let Ok((golden_extent, golden_rgba)) = decode_png(&bytes) else {
180180+ panic!("failed to decode golden PNG");
181181+ };
182182+ assert_eq!(golden_extent, size);
183183+ let threshold = PixelDiffThreshold::new(DIFF_TOLERANCE);
184184+ let Ok(report) = PixelDiff::compare(&frame, &golden_rgba, threshold) else {
185185+ panic!("PixelDiff rejected inputs");
186186+ };
187187+ assert!(
188188+ report.is_clean(),
189189+ "dimension render drifted: {} mismatches, worst {:?}, backend {}",
190190+ report.over_threshold(),
191191+ report.worst(),
192192+ frame.backend(),
193193+ );
194194+}
195195+196196+#[test]
197197+fn cache_reuses_across_repeated_renders_same_text() {
198198+ let size = extent(128);
199199+ let ctx = make_context(size);
200200+ let (_sketch, scene) = four_kind_scene();
201201+ let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
202202+ renderer.prepare(&scene, &Style::default());
203203+ let first_len = renderer.text_cache_len();
204204+ renderer.prepare(&scene, &Style::default());
205205+ let second_len = renderer.text_cache_len();
206206+ assert_eq!(
207207+ first_len, second_len,
208208+ "cache size should be stable across repeated prepares on identical scene",
209209+ );
210210+}
211211+212212+#[test]
213213+fn cache_refreshes_on_value_change() {
214214+ use bone_document::SketchEdit;
215215+ let size = extent(128);
216216+ let ctx = make_context(size);
217217+ let (sketch, scene) = four_kind_scene();
218218+ let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
219219+ renderer.prepare(&scene, &Style::default());
220220+ let initial_len = renderer.text_cache_len();
221221+222222+ let Some(&target_id) = sketch.dimension_order().first() else {
223223+ panic!("scene has at least one dimension");
224224+ };
225225+ let Ok((updated_sketch, _)) = sketch.apply(SketchEdit::UpdateDimensionValue {
226226+ id: target_id,
227227+ value: bone_document::DimensionValue::Length(Length::new::<millimeter>(999.0)),
228228+ }) else {
229229+ panic!("update dimension value");
230230+ };
231231+ let Ok(updated_scene) = SketchScene::extract(&updated_sketch) else {
232232+ panic!("scene extract after update");
233233+ };
234234+235235+ let Some(first) = updated_scene.dimensions().first() else {
236236+ panic!("updated scene has dimensions");
237237+ };
238238+ let new_text = first.text().to_owned();
239239+ assert!(
240240+ new_text.contains("999"),
241241+ "expected refreshed text to reflect updated value, got {new_text:?}",
242242+ );
243243+244244+ renderer.prepare(&updated_scene, &Style::default());
245245+ let refreshed_len = renderer.text_cache_len();
246246+ assert_eq!(initial_len, refreshed_len);
247247+}
248248+249249+#[test]
250250+fn cache_refreshes_on_font_size_change() {
251251+ let size = extent(128);
252252+ let ctx = make_context(size);
253253+ let (_sketch, scene) = four_kind_scene();
254254+ let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
255255+ let small = Style::default();
256256+ renderer.prepare(&scene, &small);
257257+ let baseline_len = renderer.text_cache_len();
258258+ let big = Style::default().with_text(small.text().with_font_size_px(28.0));
259259+ renderer.prepare(&scene, &big);
260260+ assert_eq!(
261261+ baseline_len,
262262+ renderer.text_cache_len(),
263263+ "font-size change must keep cache size equal to dimension count",
264264+ );
265265+}
···3333fn grid_empty_sketch_matches_golden() {
3434 let size = extent(256);
3535 let ctx = make_context(size);
3636- let renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
3636+ let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
3737 let scene = SketchScene::empty();
3838 let camera = Camera2::new(size);
3939 let style = Style::default();
+4-4
crates/bone-render/tests/picker.rs
···133133#[test]
134134fn each_entity_centroid_round_trips_to_pick_id() {
135135 let ctx = make_context();
136136- let renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
136136+ let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
137137 let camera = Camera2::new(extent());
138138 let style = Style::default();
139139···177177#[test]
178178fn empty_space_decodes_to_none() {
179179 let ctx = make_context();
180180- let renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
180180+ let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
181181 let camera = Camera2::new(extent());
182182 let style = Style::default();
183183···200200#[test]
201201fn repeated_render_picks_deterministic() {
202202 let ctx = make_context();
203203- let renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
203203+ let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
204204 let camera = Camera2::new(extent());
205205 let style = Style::default();
206206···209209 panic!("pick index");
210210 };
211211212212- let collect_picks = || {
212212+ let mut collect_picks = || {
213213 let Ok(_) = renderer.render(&ctx, &fx.scene, camera, &style) else {
214214 panic!("render");
215215 };
+1-1
crates/bone-render/tests/rectangle.rs
···7979fn rectangle_sketch_matches_golden() {
8080 let size = extent(256);
8181 let ctx = make_context(size);
8282- let renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
8282+ let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
8383 let scene = rectangle_scene();
8484 let camera = Camera2::new(size);
8585 let style = Style::default();