Another project
0

Configure Feed

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

fix(document): feature-tree edges for equality

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

author
Lewis
date (Jun 3, 2026, 8:33 PM +0300) commit 237b590b parent 0f31545b change-id zxyqonnz
+156 -9
+1 -1
Cargo.toml
··· 16 16 version = "0.0.0" 17 17 edition = "2024" 18 18 license = "AGPL-3.0-or-later" 19 - rust-version = "1.95" 19 + rust-version = "1.96" 20 20 21 21 [workspace.lints.rust] 22 22 unsafe_code = "forbid"
+35 -2
crates/bone-document/src/document/feature_tree.rs
··· 18 18 Extrude(ExtrudeId), 19 19 } 20 20 21 - #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 21 + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 22 22 pub enum FeatureEdge { 23 23 SketchToExtrude { 24 24 sketch: FeatureId, ··· 114 114 let edge = self.sketch_edge(id, feature); 115 115 self.drop_edges_incident(id); 116 116 self.edges.extend(edge); 117 + self.edges.sort_unstable(); 117 118 id 118 119 } 119 120 ··· 133 134 &mut self, 134 135 extrudes: impl Iterator<Item = (ExtrudeId, &'a ExtrudeFeature)>, 135 136 ) { 136 - let rebuilt: Vec<FeatureEdge> = extrudes 137 + let mut rebuilt: Vec<FeatureEdge> = extrudes 137 138 .filter_map(|(extrude, feature)| { 138 139 let extrude = self.feature_of_extrude(extrude)?; 139 140 self.sketch_edge(extrude, feature) 140 141 }) 141 142 .collect(); 143 + rebuilt.sort_unstable(); 142 144 self.edges = rebuilt; 143 145 } 144 146 ··· 313 315 panic!("feature tree deserializes"); 314 316 }; 315 317 assert!(back.edges().is_empty()); 318 + } 319 + 320 + fn extrude_key(n: u64) -> ExtrudeId { 321 + use slotmap::KeyData; 322 + ExtrudeId::from(KeyData::from_ffi((1u64 << 32) | n)) 323 + } 324 + 325 + #[test] 326 + fn edges_canonical_regardless_of_push_order() { 327 + let sketch = SketchId::default(); 328 + let high = extrude_key(5); 329 + let low = extrude_key(2); 330 + 331 + let mut pushed = FeatureTree::seeded(); 332 + pushed.push_sketch(sketch); 333 + pushed.push_extrude(high, &sample_blind_extrude(sketch)); 334 + pushed.push_extrude(low, &sample_blind_extrude(sketch)); 335 + 336 + let mut rebuilt = pushed.clone(); 337 + let extrudes = std::collections::BTreeMap::from([ 338 + (low, sample_blind_extrude(sketch)), 339 + (high, sample_blind_extrude(sketch)), 340 + ]); 341 + rebuilt.rebuild_edges(extrudes.iter().map(|(id, feature)| (*id, feature))); 342 + 343 + assert_eq!( 344 + pushed.edges(), 345 + rebuilt.edges(), 346 + "live edge order must match the load rebuild irrespective of push order" 347 + ); 348 + assert_eq!(pushed.edges().len(), 2); 316 349 } 317 350 }
-1
crates/bone-document/src/evaluator.rs
··· 34 34 } 35 35 36 36 impl EvaluatedExtrude { 37 - #[must_use] 38 37 pub fn result(&self) -> &Result<BrepSolid, ExtrudeError> { 39 38 &self.result 40 39 }
+1 -2
crates/bone-document/src/profile.rs
··· 501 501 panic!("arc edge present in the loop"); 502 502 }; 503 503 assert!( 504 - forward.sweep_rad() > 0.0 505 - && (forward.sweep_rad() - core::f64::consts::PI).abs() < 1e-9, 504 + forward.sweep_rad() > 0.0 && (forward.sweep_rad() - core::f64::consts::PI).abs() < 1e-9, 506 505 "forward ordering sweeps ccw through the bottom semicircle" 507 506 ); 508 507 let Ok(solid) = extrude(&sketch) else {
+19
crates/bone-document/tests/folder_roundtrip.rs
··· 582 582 } 583 583 584 584 #[test] 585 + fn two_extrudes_keep_canonical_edge_order_through_folder() { 586 + let dir = ok_dir(); 587 + let folder = DocumentFolder::new(dir.path().join("two_extrude.bone")); 588 + let mut doc = Document::new(document_id(1), "two".to_owned()); 589 + doc.insert_sketch(sketch_id(1), "S".to_owned(), rectangle()); 590 + doc.insert_extrude(extrude_id(5), blind_extrude(sketch_id(1))); 591 + doc.insert_extrude(extrude_id(2), blind_extrude(sketch_id(1))); 592 + assert_save(&doc, &folder); 593 + 594 + let loaded = assert_load(&folder); 595 + assert_eq!( 596 + loaded.feature_tree(), 597 + doc.feature_tree(), 598 + "save then load must preserve feature-tree equality with multiple edges" 599 + ); 600 + assert_eq!(loaded.feature_tree().edges().len(), 2); 601 + } 602 + 603 + #[test] 585 604 fn load_refuses_tree_extrude_without_entry() { 586 605 let dir = ok_dir(); 587 606 let folder = DocumentFolder::new(dir.path().join("dangling_extrude.bone"));
+99 -2
crates/bone-document/tests/undo.rs
··· 1 1 use std::num::NonZeroUsize; 2 2 3 - use bone_document::{Document, Sketch, UndoStack}; 4 - use bone_types::{DocumentId, Point3, SketchId, SketchPlaneBasis, Tolerance, UnitVec3}; 3 + use bone_document::{Document, EditOutcome, Sketch, SketchEdit, SketchEntity, UndoStack}; 4 + use bone_kernel::{ 5 + ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense, MergeResult, 6 + }; 7 + use bone_types::{ 8 + DocumentId, ExtrudeId, Length, Point2, Point3, PositiveLength, SketchEntityId, SketchId, 9 + SketchPlaneBasis, Tolerance, UnitVec3, millimeter, 10 + }; 5 11 use slotmap::KeyData; 6 12 7 13 fn plane() -> SketchPlaneBasis { ··· 40 46 doc 41 47 } 42 48 49 + fn extrude_id(idx: u32) -> ExtrudeId { 50 + ExtrudeId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx))) 51 + } 52 + 53 + fn entity_of(outcome: EditOutcome) -> SketchEntityId { 54 + let EditOutcome::Entity(id) = outcome else { 55 + panic!("entity outcome"); 56 + }; 57 + id 58 + } 59 + 60 + fn rectangle() -> Sketch { 61 + let Ok((with_points, outcomes)) = Sketch::new(plane()).apply_all(vec![ 62 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))), 63 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(10.0, 0.0))), 64 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(10.0, 5.0))), 65 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 5.0))), 66 + ]) else { 67 + panic!("rectangle corners"); 68 + }; 69 + let [c0, c1, c2, c3] = [0, 1, 2, 3].map(|i| entity_of(outcomes[i])); 70 + let Ok((closed, _)) = with_points.apply_all(vec![ 71 + SketchEdit::AddEntity(SketchEntity::line(c0, c1, false)), 72 + SketchEdit::AddEntity(SketchEntity::line(c1, c2, false)), 73 + SketchEdit::AddEntity(SketchEntity::line(c2, c3, false)), 74 + SketchEdit::AddEntity(SketchEntity::line(c3, c0, false)), 75 + ]) else { 76 + panic!("rectangle edges"); 77 + }; 78 + closed 79 + } 80 + 81 + fn blind_extrude(sketch: SketchId) -> ExtrudeFeature { 82 + let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(10.0)) else { 83 + panic!("10 mm is positive"); 84 + }; 85 + ExtrudeFeature { 86 + sketch, 87 + direction: ExtrudeDirection::Normal { 88 + sense: ExtrudeSense::Forward, 89 + }, 90 + end_condition: ExtrudeEndCondition::Blind { depth }, 91 + draft: None, 92 + thin_wall: None, 93 + merge_result: MergeResult::Merge, 94 + } 95 + } 96 + 43 97 #[test] 44 98 fn fresh_stack_has_no_history() { 45 99 let stack = UndoStack::with_capacity(cap(5)); ··· 148 202 assert!(stack.redo(&mut live)); 149 203 assert_eq!(live, c); 150 204 } 205 + 206 + #[test] 207 + fn undo_extrude_restores_sketch_only_document() { 208 + let sid = sketch_id(1); 209 + let xid = extrude_id(1); 210 + 211 + let mut live = base_doc(); 212 + live.insert_sketch(sid, "Sketch1".to_owned(), rectangle()); 213 + let pre_extrude = live.clone(); 214 + 215 + let mut stack = UndoStack::with_capacity(cap(5)); 216 + stack.record(pre_extrude.clone()); 217 + 218 + live.insert_extrude(xid, blind_extrude(sid)); 219 + assert_ne!( 220 + live, pre_extrude, 221 + "inserting an extrude must change the document" 222 + ); 223 + 224 + assert!(stack.undo(&mut live)); 225 + assert_eq!( 226 + live, pre_extrude, 227 + "undo restores the sketch-only document exactly" 228 + ); 229 + assert!( 230 + live.feature_tree().feature_of_extrude(xid).is_none(), 231 + "no extrude node survives the undo" 232 + ); 233 + assert!( 234 + live.header().extrudes.is_empty(), 235 + "no extrude payload survives the undo" 236 + ); 237 + 238 + assert!(stack.redo(&mut live)); 239 + let Some(feature) = live.feature_tree().feature_of_extrude(xid) else { 240 + panic!("redo restores the extrude node"); 241 + }; 242 + assert_eq!( 243 + live.extrude_of_feature(feature), 244 + Some(&blind_extrude(sid)), 245 + "redo restores the extrude payload" 246 + ); 247 + }
+1 -1
rust-toolchain.toml
··· 1 1 [toolchain] 2 - channel = "1.95.0" 2 + channel = "1.96.0" 3 3 components = ["rustfmt", "clippy"]