Another project
0

Configure Feed

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

test(document): extrude eval

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (Jun 3, 2026, 10:12 AM +0300) commit 0f31545b parent 40f6a5d0 change-id vpmtvvrw
+163 -4
+163 -4
crates/bone-document/tests/evaluator.rs
··· 1 1 use bone_document::{ 2 - DimensionKind, Document, EditOutcome, EvaluatedSketch, FeatureCache, FeatureNode, Sketch, 3 - SketchDimension, SketchEdit, SketchEntity, SketchRelation, evaluate_sketch, 2 + DimensionKind, DimensionValue, Document, EditOutcome, EvaluatedSketch, FeatureCache, 3 + FeatureNode, Sketch, SketchDimension, SketchEdit, SketchEntity, SketchRelation, 4 + evaluate_extrude, evaluate_sketch, 5 + }; 6 + use bone_kernel::{ 7 + BrepEdge, BrepFace, BrepSolid, ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, 8 + ExtrudeSense, MergeResult, 4 9 }; 5 10 use bone_types::{ 6 - DocumentId, FeatureId, Length, Point2, Point3, SketchId, SketchPlaneBasis, Tolerance, UnitVec3, 7 - millimeter, 11 + DocumentId, EdgeLabel, FaceLabel, FeatureId, Length, Point2, Point3, PositiveLength, 12 + SketchDimensionId, SketchId, SketchPlaneBasis, Tolerance, UnitVec3, millimeter, 8 13 }; 9 14 use slotmap::KeyData; 15 + 16 + const TOL: Tolerance = Tolerance::new(1e-9); 10 17 11 18 fn plane() -> SketchPlaneBasis { 12 19 let Ok(basis) = SketchPlaneBasis::new( ··· 252 259 }; 253 260 assert_ne!(solved_a, solved_b, "cache must refresh on reused FeatureId"); 254 261 } 262 + 263 + fn dimensioned_rectangle(width_mm: f64, height_mm: f64) -> (Sketch, SketchDimensionId) { 264 + let Ok((with_points, points)) = Sketch::new(plane()).apply_all(vec![ 265 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))), 266 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(7.0, 0.5))), 267 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(7.5, 3.2))), 268 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.3, 2.9))), 269 + ]) else { 270 + panic!("corners"); 271 + }; 272 + let [c0, c1, c2, c3] = [0, 1, 2, 3].map(|i| entity_of(points[i])); 273 + let Ok((with_edges, edges)) = with_points.apply_all(vec![ 274 + SketchEdit::AddEntity(SketchEntity::line(c0, c1, false)), 275 + SketchEdit::AddEntity(SketchEntity::line(c1, c2, false)), 276 + SketchEdit::AddEntity(SketchEntity::line(c2, c3, false)), 277 + SketchEdit::AddEntity(SketchEntity::line(c3, c0, false)), 278 + ]) else { 279 + panic!("edges"); 280 + }; 281 + let [bottom, right, top, left] = [0, 1, 2, 3].map(|i| entity_of(edges[i])); 282 + let Ok((constrained, outcomes)) = with_edges.apply_all(vec![ 283 + SketchEdit::AddRelation(SketchRelation::Horizontal(bottom)), 284 + SketchEdit::AddRelation(SketchRelation::Horizontal(top)), 285 + SketchEdit::AddRelation(SketchRelation::Vertical(right)), 286 + SketchEdit::AddRelation(SketchRelation::Vertical(left)), 287 + SketchEdit::AddRelation(SketchRelation::Fix(c0)), 288 + SketchEdit::AddDimension(SketchDimension::Linear { 289 + a: c0, 290 + b: c1, 291 + value: mm(width_mm), 292 + kind: DimensionKind::Driving, 293 + }), 294 + SketchEdit::AddDimension(SketchDimension::Linear { 295 + a: c1, 296 + b: c2, 297 + value: mm(height_mm), 298 + kind: DimensionKind::Driving, 299 + }), 300 + ]) else { 301 + panic!("constraints"); 302 + }; 303 + let Some(width_dim) = outcomes.iter().find_map(|outcome| match outcome { 304 + EditOutcome::Dimension(id) => Some(*id), 305 + _ => None, 306 + }) else { 307 + panic!("width dimension allocated"); 308 + }; 309 + (constrained, width_dim) 310 + } 311 + 312 + fn entity_of(outcome: EditOutcome) -> bone_types::SketchEntityId { 313 + let EditOutcome::Entity(id) = outcome else { 314 + panic!("expected an entity outcome"); 315 + }; 316 + id 317 + } 318 + 319 + fn blind(depth_mm: f64) -> ExtrudeFeature { 320 + let Ok(depth) = PositiveLength::new(mm(depth_mm)) else { 321 + panic!("{depth_mm} mm is positive"); 322 + }; 323 + ExtrudeFeature { 324 + sketch: SketchId::default(), 325 + direction: ExtrudeDirection::Normal { 326 + sense: ExtrudeSense::Forward, 327 + }, 328 + end_condition: ExtrudeEndCondition::Blind { depth }, 329 + draft: None, 330 + thin_wall: None, 331 + merge_result: MergeResult::Merge, 332 + } 333 + } 334 + 335 + fn face_labels(solid: &BrepSolid) -> Vec<FaceLabel> { 336 + solid.iter_faces().map(BrepFace::label).collect() 337 + } 338 + 339 + fn edge_labels(solid: &BrepSolid) -> Vec<EdgeLabel> { 340 + solid.iter_edges().map(BrepEdge::label).collect() 341 + } 342 + 343 + #[test] 344 + fn evaluate_extrude_reports_failed_sketch() { 345 + let extrude = evaluate_extrude( 346 + feature_id(9), 347 + &evaluate_sketch(&conflicting_sketch()), 348 + &blind(5.0), 349 + ); 350 + assert!(extrude.result().is_err()); 351 + assert!(extrude.solid().is_none()); 352 + assert!(extrude.generation().is_none()); 353 + } 354 + 355 + #[test] 356 + fn feature_cache_extrude_refreshes_when_sketch_changes() { 357 + let mut cache = FeatureCache::new(); 358 + let sketch_feature = feature_id(20); 359 + let extrude_feature = feature_id(21); 360 + let feature = blind(8.0); 361 + 362 + let (narrow, _) = dimensioned_rectangle(10.0, 5.0); 363 + let solved_narrow = cache.evaluate(sketch_feature, &narrow).clone(); 364 + let first = cache 365 + .evaluate_extrude(extrude_feature, &solved_narrow, &feature) 366 + .generation(); 367 + let cached = cache 368 + .evaluate_extrude(extrude_feature, &solved_narrow, &feature) 369 + .generation(); 370 + assert_eq!(first, cached, "identical input hits the cache"); 371 + 372 + let (wide, _) = dimensioned_rectangle(25.0, 5.0); 373 + let solved_wide = cache.evaluate(sketch_feature, &wide).clone(); 374 + let refreshed = cache 375 + .evaluate_extrude(extrude_feature, &solved_wide, &feature) 376 + .generation(); 377 + assert_ne!( 378 + first, refreshed, 379 + "a changed upstream sketch refreshes the extrude" 380 + ); 381 + assert!(cache.lookup_extrude(extrude_feature).is_some()); 382 + assert_eq!(cache.len(), 2); 383 + } 384 + 385 + proptest::proptest! { 386 + #[test] 387 + fn extrude_re_evaluates_under_edited_width(width_mm in 2.0f64..40.0) { 388 + let extrude_feature = feature_id(42); 389 + let feature = blind(10.0); 390 + let (base, width_dim) = dimensioned_rectangle(10.0, 5.0); 391 + let baseline = evaluate_extrude(extrude_feature, &evaluate_sketch(&base), &feature); 392 + 393 + let Ok((edited, _)) = base.apply(SketchEdit::UpdateDimensionValue { 394 + id: width_dim, 395 + value: DimensionValue::Length(mm(width_mm)), 396 + }) else { 397 + panic!("width edit applies"); 398 + }; 399 + let widened = evaluate_extrude(extrude_feature, &evaluate_sketch(&edited), &feature); 400 + 401 + let (Some(before), Some(after)) = (baseline.solid(), widened.solid()) else { 402 + panic!("both widths extrude"); 403 + }; 404 + proptest::prop_assert_eq!(face_labels(before), face_labels(after)); 405 + proptest::prop_assert_eq!(edge_labels(before), edge_labels(after)); 406 + proptest::prop_assert!(after.validate(TOL).is_ok()); 407 + let Some(bbox) = after.bounding_box() else { 408 + panic!("solid has a bounding box"); 409 + }; 410 + let (dx, _, _) = bbox.extent().coords_mm(); 411 + proptest::prop_assert!((dx - width_mm).abs() < 1e-6, "x-extent {} tracks width {}", dx, width_mm); 412 + } 413 + }