Another project
0

Configure Feed

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

at main 13 kB View raw
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 &current, 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}