Another project
1use bone_document::{
2 DimensionKind, EditOutcome, Sketch, SketchDimension, SketchEdit, SketchEntity, SketchRelation,
3 evaluate_extrude, evaluate_sketch,
4};
5use bone_kernel::{
6 ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense, MergeResult,
7};
8use bone_render::{
9 Camera2, OffscreenContext, SketchRenderer, SketchScene, SolidRenderer, SolidScene, Style,
10};
11use bone_types::{
12 Aabb3, AngleTolerance, BudgetCeiling, Camera3, ChordHeightTolerance, FeatureId, Point2, Point3,
13 PositiveLength, Projection, SketchEntityId, SketchId, SketchPlaneBasis, Tolerance, UnitVec3,
14};
15use slotmap::{Key, SlotMap};
16use std::time::{Duration, Instant};
17use uom::si::f64::Length as UomLength;
18use uom::si::length::millimeter;
19
20const TOLERANCE: Tolerance = Tolerance::new(1.0e-9);
21const CHORD: f64 = 0.05;
22const ANGLE: f64 = 0.2;
23const BASE_DEPTH_MM: f64 = 4.0;
24const DEPTH_STEP_MM: f64 = 0.5;
25
26mod common;
27
28use common::{extent_square as extent, make_context};
29
30fn xy_plane() -> SketchPlaneBasis {
31 let Ok(basis) = SketchPlaneBasis::new(
32 Point3::origin(),
33 UnitVec3::x_axis(),
34 UnitVec3::y_axis(),
35 Tolerance::new(1e-9),
36 ) else {
37 panic!("xy plane basis is orthogonal");
38 };
39 basis
40}
41
42fn add_point(s: Sketch, x: f64, y: f64) -> (Sketch, SketchEntityId) {
43 let Ok((next, EditOutcome::Entity(id))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
44 Point2::from_mm(x, y),
45 ))) else {
46 panic!("add point");
47 };
48 (next, id)
49}
50
51fn add_line(s: Sketch, a: SketchEntityId, b: SketchEntityId) -> (Sketch, SketchEntityId) {
52 let Ok((next, EditOutcome::Entity(id))) =
53 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
54 else {
55 panic!("add line");
56 };
57 (next, id)
58}
59
60fn add_circle(s: Sketch, center: SketchEntityId, radius_mm: f64) -> Sketch {
61 let Ok((next, _)) = s.apply(SketchEdit::AddEntity(SketchEntity::circle(
62 center,
63 UomLength::new::<millimeter>(radius_mm),
64 false,
65 ))) else {
66 panic!("add circle");
67 };
68 next
69}
70
71fn add_arc(
72 s: Sketch,
73 center: SketchEntityId,
74 start: SketchEntityId,
75 end: SketchEntityId,
76) -> Sketch {
77 let Ok((next, _)) = s.apply(SketchEdit::AddEntity(SketchEntity::arc(
78 center, start, end, false,
79 ))) else {
80 panic!("add arc");
81 };
82 next
83}
84
85fn add_relation(s: Sketch, r: SketchRelation) -> Sketch {
86 let Ok((next, _)) = s.apply(SketchEdit::AddRelation(r)) else {
87 panic!("add relation");
88 };
89 next
90}
91
92fn add_linear_dim(s: Sketch, a: SketchEntityId, b: SketchEntityId, value_mm: f64) -> Sketch {
93 let Ok((next, _)) = s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
94 a,
95 b,
96 value: UomLength::new::<millimeter>(value_mm),
97 kind: DimensionKind::Driving,
98 })) else {
99 panic!("add linear dimension");
100 };
101 next
102}
103
104fn reference_sketch() -> (Sketch, SketchEntityId) {
105 let s = Sketch::new(xy_plane());
106 let (s, p0) = add_point(s, 0.0, 0.0);
107 let (s, p1) = add_point(s, 10.0, 0.0);
108 let (s, p2) = add_point(s, 10.0, 5.0);
109 let (s, p3) = add_point(s, 0.0, 5.0);
110 let (s, e_bottom) = add_line(s, p0, p1);
111 let (s, e_right) = add_line(s, p1, p2);
112 let (s, e_top) = add_line(s, p2, p3);
113 let (s, e_left) = add_line(s, p3, p0);
114 let s = add_relation(s, SketchRelation::Horizontal(e_bottom));
115 let s = add_relation(s, SketchRelation::Horizontal(e_top));
116 let s = add_relation(s, SketchRelation::Vertical(e_right));
117 let s = add_relation(s, SketchRelation::Vertical(e_left));
118 let s = add_relation(s, SketchRelation::Fix(p0));
119 let s = add_linear_dim(s, p0, p1, 10.0);
120 let s = add_circle(s, p0, 5.0);
121 let (s, arc_center) = add_point(s, -12.0, 0.0);
122 let (s, arc_start) = add_point(s, -8.0, 0.0);
123 let (s, arc_end) = add_point(s, -12.0, 4.0);
124 let s = add_arc(s, arc_center, arc_start, arc_end);
125 (s, p2)
126}
127
128#[allow(
129 clippy::too_many_arguments,
130 reason = "stepper bundles per-frame pipeline state alongside per-step drag inputs"
131)]
132fn drag_resolve_render(
133 current: &Sketch,
134 dragged: SketchEntityId,
135 target: Point2,
136 solver_budget: BudgetCeiling,
137 renderer: &mut SketchRenderer,
138 ctx: &OffscreenContext,
139 camera: Camera2,
140 style: &Style,
141) -> (Sketch, Duration) {
142 let started = Instant::now();
143 let Ok(next) = current.solve_with_drag(dragged, target, solver_budget) else {
144 panic!("drag must converge inside solver budget");
145 };
146 let Ok(scene) = SketchScene::extract(&next) else {
147 panic!("scene extract must succeed for solved sketch");
148 };
149 let Ok(_frame) = renderer.render(ctx, &scene, camera, style) else {
150 panic!("render must succeed for solved sketch");
151 };
152 (next, started.elapsed())
153}
154
155const DRAG_STEPS: u32 = 16;
156
157#[test]
158#[cfg_attr(
159 debug_assertions,
160 ignore = "frame budget assertions are only meaningful in release builds"
161)]
162fn drag_resolve_plus_render_fits_frame_budget_on_reference_sketch() {
163 let (sketch, dragged) = reference_sketch();
164 let Ok(sketch) = sketch.solve() else {
165 panic!("reference sketch must solve at rest");
166 };
167 let size = extent(256);
168 let ctx = make_context(size);
169 let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
170 let camera = Camera2::new(size);
171 let style = Style::default();
172 let frame_budget = Duration::from_millis(16);
173 let worst_ceiling = frame_budget * 2;
174 let solver_budget = BudgetCeiling::new(frame_budget / 2_u32);
175 let warmup_target = Point2::from_mm(10.0, 4.0);
176 let (warmed, _warmup_elapsed) = drag_resolve_render(
177 &sketch,
178 dragged,
179 warmup_target,
180 solver_budget,
181 &mut renderer,
182 &ctx,
183 camera,
184 &style,
185 );
186 let (_final_sketch, durations) = (0..DRAG_STEPS).fold(
187 (warmed, Vec::<Duration>::new()),
188 |(current, durations), i| {
189 let target = Point2::from_mm(10.0, 5.0 + 0.1 * f64::from(i));
190 let (next, elapsed) = drag_resolve_render(
191 ¤t,
192 dragged,
193 target,
194 solver_budget,
195 &mut renderer,
196 &ctx,
197 camera,
198 &style,
199 );
200 let next_durations = durations.into_iter().chain([elapsed]).collect();
201 (next, next_durations)
202 },
203 );
204 let sorted = {
205 let mut v = durations.clone();
206 v.sort();
207 v
208 };
209 let median = sorted[sorted.len() / 2];
210 let Some(&worst) = sorted.last() else {
211 panic!("drag loop produced zero samples");
212 };
213 assert!(
214 median <= frame_budget,
215 "median drag+render step {median:?} exceeds {frame_budget:?} frame budget; samples {durations:?}",
216 );
217 assert!(
218 worst <= worst_ceiling,
219 "worst drag+render step {worst:?} exceeds {worst_ceiling:?} relaxed ceiling; samples {durations:?}",
220 );
221 println!("median {median:?}, worst {worst:?}");
222}
223
224fn reference_rectangle() -> Sketch {
225 let s = Sketch::new(xy_plane());
226 let (s, p0) = add_point(s, 0.0, 0.0);
227 let (s, p1) = add_point(s, 10.0, 0.0);
228 let (s, p2) = add_point(s, 10.0, 5.0);
229 let (s, p3) = add_point(s, 0.0, 5.0);
230 let (s, e_bottom) = add_line(s, p0, p1);
231 let (s, e_right) = add_line(s, p1, p2);
232 let (s, e_top) = add_line(s, p2, p3);
233 let (s, e_left) = add_line(s, p3, p0);
234 let s = add_relation(s, SketchRelation::Horizontal(e_bottom));
235 let s = add_relation(s, SketchRelation::Horizontal(e_top));
236 let s = add_relation(s, SketchRelation::Vertical(e_right));
237 let s = add_relation(s, SketchRelation::Vertical(e_left));
238 let s = add_relation(s, SketchRelation::Fix(p0));
239 add_linear_dim(s, p0, p1, 10.0)
240}
241
242fn blind(depth_mm: f64) -> ExtrudeFeature {
243 let Ok(depth) = PositiveLength::new(UomLength::new::<millimeter>(depth_mm)) else {
244 panic!("{depth_mm} mm is a positive length");
245 };
246 ExtrudeFeature {
247 sketch: SketchId::null(),
248 direction: ExtrudeDirection::Normal {
249 sense: ExtrudeSense::Forward,
250 },
251 end_condition: ExtrudeEndCondition::Blind { depth },
252 draft: None,
253 thin_wall: None,
254 merge_result: MergeResult::Merge,
255 }
256}
257
258fn direction(x: f64, y: f64, z: f64) -> UnitVec3 {
259 let Ok(unit) = UnitVec3::try_from_components(x, y, z, TOLERANCE) else {
260 panic!("({x}, {y}, {z}) is a nonzero direction");
261 };
262 unit
263}
264
265fn framed(aabb: Aabb3, from: UnitVec3, up: UnitVec3) -> Camera3 {
266 let center = aabb.center();
267 let span = 0.5 * aabb.extent().norm_mm();
268 let eye = center + from.into_vec(UomLength::new::<millimeter>(span * 3.0));
269 let Ok(projection) = Projection::orthographic(UomLength::new::<millimeter>(span * 1.2)) else {
270 panic!("half height is positive");
271 };
272 let Ok(camera) = Camera3::new(eye, center, up, projection) else {
273 panic!("camera is non-degenerate");
274 };
275 camera
276}
277
278fn frame_slab(sketch: &Sketch, extrude: FeatureId, depth_mm: f64) -> Camera3 {
279 let evaluated = evaluate_sketch(sketch);
280 let extruded = evaluate_extrude(extrude, &evaluated, &blind(depth_mm));
281 let Some(solid) = extruded.solid() else {
282 panic!("the reference rectangle extrudes into a slab");
283 };
284 let Some(aabb) = solid.bounding_box() else {
285 panic!("the slab has a bounding box");
286 };
287 framed(aabb, direction(1.0, 1.0, 1.0), UnitVec3::z_axis())
288}
289
290struct SolidStage<'a> {
291 renderer: &'a mut SolidRenderer,
292 ctx: &'a OffscreenContext,
293 camera: Camera3,
294 style: &'a Style,
295}
296
297impl SolidStage<'_> {
298 fn step(&mut self, sketch: &Sketch, extrude: FeatureId, depth_mm: f64) -> Duration {
299 let started = Instant::now();
300 let evaluated = evaluate_sketch(sketch);
301 let extruded = evaluate_extrude(extrude, &evaluated, &blind(depth_mm));
302 let Some(solid) = extruded.solid() else {
303 panic!("the slab evaluates at {depth_mm} mm");
304 };
305 let Ok(mesh) = solid.tessellate(
306 ChordHeightTolerance::from_mm(CHORD),
307 AngleTolerance::from_radians(ANGLE),
308 ) else {
309 panic!("the slab tessellates at {depth_mm} mm");
310 };
311 let Ok(scene) = SolidScene::from_mesh(&mesh) else {
312 panic!("the slab mesh packs face pick ids");
313 };
314 let Ok(_frame) = self
315 .renderer
316 .render(self.ctx, &scene, self.camera, self.style)
317 else {
318 panic!("solid render must succeed at {depth_mm} mm");
319 };
320 started.elapsed()
321 }
322}
323
324#[test]
325#[cfg_attr(
326 debug_assertions,
327 ignore = "frame budget assertions are only meaningful in release builds"
328)]
329fn drag_resolve_plus_render_fits_frame_budget_on_reference_extrude() {
330 let rectangle = reference_rectangle();
331 let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key();
332 let extrude = features.insert(());
333 let size = extent(256);
334 let ctx = make_context(size);
335 let mut renderer = SolidRenderer::new(ctx.gpu(), ctx.color_format());
336 let style = Style::default();
337 let max_depth = BASE_DEPTH_MM + DEPTH_STEP_MM * f64::from(DRAG_STEPS - 1);
338 let camera = frame_slab(&rectangle, extrude, max_depth);
339 let mut stage = SolidStage {
340 renderer: &mut renderer,
341 ctx: &ctx,
342 camera,
343 style: &style,
344 };
345 let _warmup = stage.step(&rectangle, extrude, BASE_DEPTH_MM);
346 let durations: Vec<Duration> = (0..DRAG_STEPS)
347 .map(|i| {
348 let depth_mm = BASE_DEPTH_MM + DEPTH_STEP_MM * f64::from(i);
349 stage.step(&rectangle, extrude, depth_mm)
350 })
351 .collect();
352 let sorted = {
353 let mut v = durations.clone();
354 v.sort();
355 v
356 };
357 let median = sorted[sorted.len() / 2];
358 let Some(&worst) = sorted.last() else {
359 panic!("drag loop produced zero samples");
360 };
361 let budget = BudgetCeiling::FRAME_16MS.duration();
362 let worst_ceiling = budget * 2;
363 if std::env::var("BONE_FRAME_BUDGET_REPORT").is_ok() {
364 println!(
365 "reference extrude drag: median {median:?}, worst {worst:?}, samples {durations:?}"
366 );
367 }
368 assert!(
369 median <= budget,
370 "median solve+extrude+tessellate+render step {median:?} exceeds {budget:?} frame budget; samples {durations:?}",
371 );
372 assert!(
373 worst <= worst_ceiling,
374 "worst solve+extrude+tessellate+render step {worst:?} exceeds {worst_ceiling:?} relaxed ceiling; samples {durations:?}",
375 );
376}