Another project
0

Configure Feed

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

at main 17 kB View raw
1use std::collections::BTreeSet; 2use std::num::NonZeroUsize; 3 4use bone_document::{ 5 Document, EditOutcome, EvaluatedModel, FeatureEdge, RecomputeScope, Sketch, SketchEdit, 6 SketchEntity, UndoStack, evaluate_extrude, evaluate_sketch, 7}; 8use bone_kernel::{ 9 BrepSolid, ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense, MergeResult, 10}; 11use bone_types::{ 12 DocumentId, ExtrudeId, FeatureId, Length, Point2, Point3, PositiveLength, RollbackMarker, 13 SketchEntityId, SketchId, SketchPlaneBasis, Tolerance, UnitVec3, millimeter, 14}; 15use slotmap::{Key, KeyData}; 16 17fn plane() -> SketchPlaneBasis { 18 let Ok(basis) = SketchPlaneBasis::new( 19 Point3::origin(), 20 UnitVec3::x_axis(), 21 UnitVec3::y_axis(), 22 Tolerance::new(1e-9), 23 ) else { 24 panic!("xy plane"); 25 }; 26 basis 27} 28 29fn sketch_id(idx: u32) -> SketchId { 30 SketchId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx))) 31} 32 33fn document_id(idx: u32) -> DocumentId { 34 DocumentId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx))) 35} 36 37fn cap(n: usize) -> NonZeroUsize { 38 let Some(nz) = NonZeroUsize::new(n) else { 39 panic!("capacity"); 40 }; 41 nz 42} 43 44fn base_doc() -> Document { 45 Document::new(document_id(1), "d".to_owned()) 46} 47 48fn with_sketch(mut doc: Document, sid: SketchId) -> Document { 49 doc.insert_sketch(sid, format!("S{sid:?}"), Sketch::new(plane())); 50 doc 51} 52 53fn extrude_id(idx: u32) -> ExtrudeId { 54 ExtrudeId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx))) 55} 56 57fn entity_of(outcome: EditOutcome) -> SketchEntityId { 58 let EditOutcome::Entity(id) = outcome else { 59 panic!("entity outcome"); 60 }; 61 id 62} 63 64fn rectangle() -> Sketch { 65 rectangle_width(10.0) 66} 67 68fn rectangle_width(width_mm: f64) -> Sketch { 69 let Ok((with_points, outcomes)) = Sketch::new(plane()).apply_all(vec![ 70 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))), 71 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(width_mm, 0.0))), 72 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(width_mm, 5.0))), 73 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 5.0))), 74 ]) else { 75 panic!("rectangle corners"); 76 }; 77 let [c0, c1, c2, c3] = [0, 1, 2, 3].map(|i| entity_of(outcomes[i])); 78 let Ok((closed, _)) = with_points.apply_all(vec![ 79 SketchEdit::AddEntity(SketchEntity::line(c0, c1, false)), 80 SketchEdit::AddEntity(SketchEntity::line(c1, c2, false)), 81 SketchEdit::AddEntity(SketchEntity::line(c2, c3, false)), 82 SketchEdit::AddEntity(SketchEntity::line(c3, c0, false)), 83 ]) else { 84 panic!("rectangle edges"); 85 }; 86 closed 87} 88 89fn sample_solid(feature: FeatureId) -> BrepSolid { 90 let evaluated = evaluate_sketch(&rectangle()); 91 let extruded = evaluate_extrude(feature, &evaluated, &blind_extrude(SketchId::null())); 92 let Some(solid) = extruded.solid() else { 93 panic!("rectangle extrudes to a solid"); 94 }; 95 solid.clone() 96} 97 98fn blind_extrude(sketch: SketchId) -> ExtrudeFeature { 99 let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(10.0)) else { 100 panic!("10 mm is positive"); 101 }; 102 ExtrudeFeature { 103 sketch, 104 direction: ExtrudeDirection::Normal { 105 sense: ExtrudeSense::Forward, 106 }, 107 end_condition: ExtrudeEndCondition::Blind { depth }, 108 draft: None, 109 thin_wall: None, 110 merge_result: MergeResult::Merge, 111 } 112} 113 114#[test] 115fn fresh_stack_has_no_history() { 116 let stack = UndoStack::with_capacity(cap(5)); 117 assert!(!stack.can_undo()); 118 assert!(!stack.can_redo()); 119 assert_eq!(stack.past_len(), 0); 120 assert_eq!(stack.future_len(), 0); 121} 122 123#[test] 124fn undo_swaps_current_with_previous() { 125 let a = base_doc(); 126 let mut live = with_sketch(a.clone(), sketch_id(1)); 127 let mut stack = UndoStack::with_capacity(cap(5)); 128 stack.record(a.clone()); 129 assert!(stack.undo(&mut live)); 130 assert_eq!(live, a); 131 assert!(!stack.can_undo()); 132 assert!(stack.can_redo()); 133} 134 135#[test] 136fn undo_on_empty_leaves_current_unchanged() { 137 let mut live = base_doc(); 138 let snapshot = live.clone(); 139 let mut stack = UndoStack::with_capacity(cap(5)); 140 assert!(!stack.undo(&mut live)); 141 assert_eq!(live, snapshot); 142 assert_eq!(stack.past_len(), 0); 143 assert_eq!(stack.future_len(), 0); 144} 145 146#[test] 147fn redo_on_empty_leaves_current_unchanged() { 148 let mut live = base_doc(); 149 let snapshot = live.clone(); 150 let mut stack = UndoStack::with_capacity(cap(5)); 151 assert!(!stack.redo(&mut live)); 152 assert_eq!(live, snapshot); 153 assert_eq!(stack.past_len(), 0); 154 assert_eq!(stack.future_len(), 0); 155} 156 157#[test] 158fn redo_replays_undone_state() { 159 let a = base_doc(); 160 let b = with_sketch(a.clone(), sketch_id(1)); 161 let mut live = b.clone(); 162 let mut stack = UndoStack::with_capacity(cap(5)); 163 stack.record(a.clone()); 164 assert!(stack.undo(&mut live)); 165 assert!(stack.redo(&mut live)); 166 assert_eq!(live, b); 167 assert!(stack.can_undo()); 168 assert!(!stack.can_redo()); 169} 170 171#[test] 172fn recording_after_undo_clears_redo() { 173 let a = base_doc(); 174 let b = with_sketch(a.clone(), sketch_id(1)); 175 let c = with_sketch(a.clone(), sketch_id(2)); 176 let mut live = b; 177 let mut stack = UndoStack::with_capacity(cap(5)); 178 stack.record(a.clone()); 179 assert!(stack.undo(&mut live)); 180 assert_eq!(live, a); 181 stack.record(live.clone()); 182 let _ = c; 183 assert!(stack.can_undo()); 184 assert!(!stack.can_redo()); 185 assert_eq!(stack.future_len(), 0); 186} 187 188#[test] 189fn capacity_drops_oldest_entries() { 190 let mut stack = UndoStack::with_capacity(cap(2)); 191 let snapshots: Vec<Document> = (1..=4) 192 .map(|i| with_sketch(base_doc(), sketch_id(i))) 193 .collect(); 194 snapshots.iter().cloned().for_each(|doc| stack.record(doc)); 195 assert_eq!(stack.past_len(), 2); 196 let mut live = with_sketch(base_doc(), sketch_id(99)); 197 assert!(stack.undo(&mut live)); 198 assert_eq!(live, snapshots[3]); 199 assert!(stack.undo(&mut live)); 200 assert_eq!(live, snapshots[2]); 201 assert!(!stack.can_undo()); 202} 203 204#[test] 205fn redo_never_exceeds_capacity() { 206 let mut stack = UndoStack::with_capacity(cap(2)); 207 let mut live = base_doc(); 208 [1u32, 2, 3, 4].iter().for_each(|i| { 209 stack.record(live.clone()); 210 live.insert_sketch(sketch_id(*i), format!("S{i}"), Sketch::new(plane())); 211 assert!(stack.past_len() <= 2, "record stays within capacity"); 212 }); 213 std::iter::from_fn(|| { 214 stack 215 .undo(&mut live) 216 .then(|| assert!(stack.past_len() <= 2 && stack.future_len() <= 2)) 217 }) 218 .for_each(drop); 219 std::iter::from_fn(|| { 220 stack.redo(&mut live).then(|| { 221 assert!( 222 stack.past_len() <= 2, 223 "redo must not grow past beyond capacity" 224 ); 225 }) 226 }) 227 .for_each(drop); 228 assert!(!stack.can_redo()); 229} 230 231#[test] 232fn undo_redo_cycle_preserves_determinism() { 233 let a = base_doc(); 234 let b = with_sketch(a.clone(), sketch_id(1)); 235 let c = with_sketch(b.clone(), sketch_id(2)); 236 let mut live = c.clone(); 237 let mut stack = UndoStack::with_capacity(cap(5)); 238 stack.record(a.clone()); 239 stack.record(b.clone()); 240 assert!(stack.undo(&mut live)); 241 assert_eq!(live, b); 242 assert!(stack.undo(&mut live)); 243 assert_eq!(live, a); 244 assert!(stack.redo(&mut live)); 245 assert_eq!(live, b); 246 assert!(stack.redo(&mut live)); 247 assert_eq!(live, c); 248} 249 250#[test] 251fn undo_extrude_restores_sketch_only_document() { 252 let sid = sketch_id(1); 253 let xid = extrude_id(1); 254 255 let mut live = base_doc(); 256 live.insert_sketch(sid, "Sketch1".to_owned(), rectangle()); 257 let pre_extrude = live.clone(); 258 259 let mut stack = UndoStack::with_capacity(cap(5)); 260 stack.record(pre_extrude.clone()); 261 262 live.insert_extrude(xid, blind_extrude(sid)); 263 assert_ne!( 264 live, pre_extrude, 265 "inserting an extrude must change the document" 266 ); 267 let Some(sketch_feature) = live.feature_tree().feature_of_sketch(sid) else { 268 panic!("sketch feature present"); 269 }; 270 let Some(extrude_feature) = live.feature_tree().feature_of_extrude(xid) else { 271 panic!("extrude feature present"); 272 }; 273 let wired = [FeatureEdge::SketchToExtrude { 274 sketch: sketch_feature, 275 extrude: extrude_feature, 276 }]; 277 assert_eq!( 278 live.feature_tree().edges(), 279 wired, 280 "inserting an extrude wires the sketch-to-extrude edge" 281 ); 282 283 assert!(stack.undo(&mut live)); 284 assert_eq!( 285 live, pre_extrude, 286 "undo restores the sketch-only document exactly" 287 ); 288 assert!( 289 live.feature_tree().feature_of_extrude(xid).is_none(), 290 "no extrude node survives the undo" 291 ); 292 assert!( 293 live.header().extrudes.is_empty(), 294 "no extrude payload survives the undo" 295 ); 296 assert!( 297 live.feature_tree().edges().is_empty(), 298 "undo drops the derived sketch-to-extrude edge" 299 ); 300 301 assert!(stack.redo(&mut live)); 302 let Some(feature) = live.feature_tree().feature_of_extrude(xid) else { 303 panic!("redo restores the extrude node"); 304 }; 305 assert_eq!( 306 live.extrude_of_feature(feature), 307 Some(&blind_extrude(sid)), 308 "redo restores the extrude payload" 309 ); 310 assert_eq!( 311 live.feature_tree().edges(), 312 wired, 313 "redo restores the derived edge even though it is never serialized" 314 ); 315} 316 317#[test] 318fn undo_and_redo_cross_a_recompute() { 319 let sid = sketch_id(1); 320 let xid = extrude_id(1); 321 let mut live = base_doc(); 322 live.insert_sketch(sid, "Sketch1".to_owned(), rectangle_width(10.0)); 323 live.insert_extrude(xid, blind_extrude(sid)); 324 325 let Some(extrude_feature) = live.feature_tree().feature_of_extrude(xid) else { 326 panic!("extrude feature present"); 327 }; 328 let Some(sketch_feature) = live.feature_tree().feature_of_sketch(sid) else { 329 panic!("sketch feature present"); 330 }; 331 332 let mut model = EvaluatedModel::new(); 333 let active = BTreeSet::new(); 334 model.recompute(&live, &active, RollbackMarker::AtEnd, RecomputeScope::Full); 335 let Some(narrow) = model.body(extrude_feature).map(BrepSolid::content_key) else { 336 panic!("the base extrude builds a body"); 337 }; 338 339 let mut stack = UndoStack::with_capacity(cap(5)); 340 stack.record(live.clone()); 341 live.replace_sketch(sid, rectangle_width(14.0)); 342 model.recompute( 343 &live, 344 &active, 345 RollbackMarker::AtEnd, 346 RecomputeScope::Edited(sketch_feature), 347 ); 348 let Some(wide) = model.body(extrude_feature).map(BrepSolid::content_key) else { 349 panic!("the widened extrude builds a body"); 350 }; 351 assert_ne!(narrow, wide, "widening the profile must change the body"); 352 353 assert!(stack.undo(&mut live)); 354 model.recompute(&live, &active, RollbackMarker::AtEnd, RecomputeScope::Full); 355 assert_eq!( 356 model.body(extrude_feature).map(BrepSolid::content_key), 357 Some(narrow), 358 "undo restores the recipe and the rebuild reproduces the pre-edit body", 359 ); 360 361 assert!(stack.redo(&mut live)); 362 model.recompute(&live, &active, RollbackMarker::AtEnd, RecomputeScope::Full); 363 assert_eq!( 364 model.body(extrude_feature).map(BrepSolid::content_key), 365 Some(wide), 366 "redo restores the edited recipe and the body widens again", 367 ); 368} 369 370#[test] 371fn rollback_and_suppression_survive_undo_and_redo() { 372 let sid = sketch_id(1); 373 let xid = extrude_id(1); 374 let mut live = base_doc(); 375 live.insert_sketch(sid, "Sketch1".to_owned(), rectangle()); 376 live.insert_extrude(xid, blind_extrude(sid)); 377 let Some(feature) = live.feature_tree().feature_of_extrude(xid) else { 378 panic!("extrude feature present"); 379 }; 380 let before = live.clone(); 381 382 let mut stack = UndoStack::with_capacity(cap(5)); 383 stack.record(live.clone()); 384 live.roll_to_here(feature); 385 live.suppress(feature); 386 assert_eq!(live.rollback(), RollbackMarker::Above(feature)); 387 assert!(live.suppressed().contains(&feature)); 388 389 assert!(stack.undo(&mut live)); 390 assert_eq!(live, before, "undo restores the pre-history-edit document"); 391 assert_eq!( 392 live.rollback(), 393 RollbackMarker::AtEnd, 394 "undo clears the rollback marker set by the undone step", 395 ); 396 assert!( 397 live.suppressed().is_empty(), 398 "undo clears the suppression set by the undone step", 399 ); 400 401 assert!(stack.redo(&mut live)); 402 assert_eq!( 403 live.rollback(), 404 RollbackMarker::Above(feature), 405 "redo restores the rollback marker", 406 ); 407 assert!( 408 live.suppressed().contains(&feature), 409 "redo restores the suppression", 410 ); 411} 412 413#[test] 414fn cloning_a_document_shares_imported_body_storage() { 415 let mut live = base_doc(); 416 let Ok((_, body)) = live.import_body(|feature| Ok::<_, ()>(sample_solid(feature))) else { 417 panic!("import a body"); 418 }; 419 420 let snapshot = live.clone(); 421 let Some(live_solid) = live.imported_body(body) else { 422 panic!("live body present"); 423 }; 424 let Some(snapshot_solid) = snapshot.imported_body(body) else { 425 panic!("snapshot body present"); 426 }; 427 assert!( 428 std::ptr::eq(live_solid, snapshot_solid), 429 "a clone shares the imported body payload, the snapshot is not a deep brep copy", 430 ); 431} 432 433#[test] 434fn an_undo_needs_a_full_recompute_to_restore_an_out_of_scope_body() { 435 let sid_a = sketch_id(1); 436 let xid_a = extrude_id(1); 437 let sid_b = sketch_id(2); 438 let xid_b = extrude_id(2); 439 440 let mut live = base_doc(); 441 live.insert_sketch(sid_a, "A".to_owned(), rectangle_width(10.0)); 442 live.insert_extrude(xid_a, blind_extrude(sid_a)); 443 live.insert_sketch(sid_b, "B".to_owned(), rectangle_width(10.0)); 444 live.insert_extrude(xid_b, blind_extrude(sid_b)); 445 446 let Some(a_sketch_feature) = live.feature_tree().feature_of_sketch(sid_a) else { 447 panic!("sketch a feature present"); 448 }; 449 let Some(b_sketch_feature) = live.feature_tree().feature_of_sketch(sid_b) else { 450 panic!("sketch b feature present"); 451 }; 452 let Some(b_extrude_feature) = live.feature_tree().feature_of_extrude(xid_b) else { 453 panic!("extrude b feature present"); 454 }; 455 456 let mut model = EvaluatedModel::new(); 457 let active = BTreeSet::new(); 458 model.recompute(&live, &active, RollbackMarker::AtEnd, RecomputeScope::Full); 459 let Some(b_original) = model.body(b_extrude_feature).map(BrepSolid::content_key) else { 460 panic!("b builds a body"); 461 }; 462 463 let mut stack = UndoStack::with_capacity(cap(5)); 464 stack.record(live.clone()); 465 live.replace_sketch(sid_b, rectangle_width(18.0)); 466 model.recompute( 467 &live, 468 &active, 469 RollbackMarker::AtEnd, 470 RecomputeScope::Edited(b_sketch_feature), 471 ); 472 let Some(b_wide) = model.body(b_extrude_feature).map(BrepSolid::content_key) else { 473 panic!("b rebuilds wide"); 474 }; 475 assert_ne!(b_original, b_wide, "widening b must change its body"); 476 477 assert!(stack.undo(&mut live)); 478 model.recompute( 479 &live, 480 &active, 481 RollbackMarker::AtEnd, 482 RecomputeScope::Edited(a_sketch_feature), 483 ); 484 assert_eq!( 485 model.body(b_extrude_feature).map(BrepSolid::content_key), 486 Some(b_wide), 487 "an edit-scoped recompute after undo cannot see b reverted outside the scope", 488 ); 489 490 model.recompute(&live, &active, RollbackMarker::AtEnd, RecomputeScope::Full); 491 assert_eq!( 492 model.body(b_extrude_feature).map(BrepSolid::content_key), 493 Some(b_original), 494 "a full recompute after undo restores every reverted body, the contract the app must use", 495 ); 496} 497 498#[test] 499fn cloning_a_document_shares_sketch_storage() { 500 let sid = sketch_id(1); 501 let mut live = base_doc(); 502 live.insert_sketch(sid, "Sketch1".to_owned(), rectangle_width(10.0)); 503 504 let snapshot = live.clone(); 505 let Some(live_sketch) = live.sketch(sid) else { 506 panic!("live sketch present"); 507 }; 508 let Some(snapshot_sketch) = snapshot.sketch(sid) else { 509 panic!("snapshot sketch present"); 510 }; 511 assert!( 512 std::ptr::eq(live_sketch.entities(), snapshot_sketch.entities()), 513 "a clone shares the sketch entity payload, the snapshot is not a deep copy", 514 ); 515 516 live.replace_sketch(sid, rectangle_width(14.0)); 517 let Some(edited_sketch) = live.sketch(sid) else { 518 panic!("edited sketch present"); 519 }; 520 let Some(snapshot_after) = snapshot.sketch(sid) else { 521 panic!("snapshot survives the edit"); 522 }; 523 assert!( 524 !std::ptr::eq(edited_sketch.entities(), snapshot_after.entities()), 525 "editing the live document leaves the snapshot's payload untouched", 526 ); 527}