Another project
0

Configure Feed

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

feat(document): extrude nodes w/ derived sketch edges

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

author
Lewis
date (Jun 1, 2026, 7:24 PM +0300) commit f3b3cc3e parent 6671610e change-id mkvxxyuw
+500 -27
+218 -6
crates/bone-document/src/document/feature_tree.rs
··· 1 - use bone_types::{FeatureId, SketchId}; 1 + use bone_kernel::ExtrudeFeature; 2 + use bone_types::{ExtrudeId, FeatureId, SketchId}; 2 3 use serde::{Deserialize, Serialize}; 3 4 use slotmap::{Key, KeyData}; 4 5 ··· 14 15 Origin, 15 16 PrincipalPlane(PrincipalPlane), 16 17 Sketch(SketchId), 18 + Extrude(ExtrudeId), 19 + } 20 + 21 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 22 + pub enum FeatureEdge { 23 + SketchToExtrude { 24 + sketch: FeatureId, 25 + extrude: FeatureId, 26 + }, 17 27 } 18 28 19 29 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] ··· 27 37 #[serde(deny_unknown_fields)] 28 38 pub struct FeatureTree { 29 39 entries: Vec<FeatureEntry>, 40 + #[serde(skip)] 41 + edges: Vec<FeatureEdge>, 30 42 } 31 43 32 44 impl FeatureTree { ··· 46 58 node, 47 59 }) 48 60 .collect(); 49 - Self { entries } 61 + Self { 62 + entries, 63 + edges: Vec::new(), 64 + } 50 65 } 51 66 52 67 #[must_use] ··· 60 75 61 76 #[must_use] 62 77 pub fn feature_of_sketch(&self, sketch: SketchId) -> Option<FeatureId> { 63 - self.entries 64 - .iter() 65 - .find(|e| matches!(e.node, FeatureNode::Sketch(s) if s == sketch)) 66 - .map(|e| e.id) 78 + self.feature_of(|node| matches!(node, FeatureNode::Sketch(s) if *s == sketch)) 67 79 } 68 80 69 81 pub fn push_sketch(&mut self, sketch: SketchId) -> FeatureId { ··· 81 93 pub fn remove_sketch(&mut self, sketch: SketchId) -> Option<FeatureId> { 82 94 let id = self.feature_of_sketch(sketch)?; 83 95 self.entries.retain(|e| e.id != id); 96 + self.drop_edges_incident(id); 84 97 Some(id) 85 98 } 86 99 100 + #[must_use] 101 + pub fn feature_of_extrude(&self, extrude: ExtrudeId) -> Option<FeatureId> { 102 + self.feature_of(|node| matches!(node, FeatureNode::Extrude(x) if *x == extrude)) 103 + } 104 + 105 + pub fn push_extrude(&mut self, extrude: ExtrudeId, feature: &ExtrudeFeature) -> FeatureId { 106 + let id = self.feature_of_extrude(extrude).unwrap_or_else(|| { 107 + let id = self.allocate(); 108 + self.entries.push(FeatureEntry { 109 + id, 110 + node: FeatureNode::Extrude(extrude), 111 + }); 112 + id 113 + }); 114 + let edge = self.sketch_edge(id, feature); 115 + self.drop_edges_incident(id); 116 + self.edges.extend(edge); 117 + id 118 + } 119 + 120 + pub fn remove_extrude(&mut self, extrude: ExtrudeId) -> Option<FeatureId> { 121 + let id = self.feature_of_extrude(extrude)?; 122 + self.entries.retain(|e| e.id != id); 123 + self.drop_edges_incident(id); 124 + Some(id) 125 + } 126 + 127 + #[must_use] 128 + pub fn edges(&self) -> &[FeatureEdge] { 129 + &self.edges 130 + } 131 + 132 + pub(crate) fn rebuild_edges<'a>( 133 + &mut self, 134 + extrudes: impl Iterator<Item = (ExtrudeId, &'a ExtrudeFeature)>, 135 + ) { 136 + let rebuilt: Vec<FeatureEdge> = extrudes 137 + .filter_map(|(extrude, feature)| { 138 + let extrude = self.feature_of_extrude(extrude)?; 139 + self.sketch_edge(extrude, feature) 140 + }) 141 + .collect(); 142 + self.edges = rebuilt; 143 + } 144 + 145 + fn sketch_edge(&self, extrude: FeatureId, feature: &ExtrudeFeature) -> Option<FeatureEdge> { 146 + let sketch = self.feature_of_sketch(feature.sketch)?; 147 + Some(FeatureEdge::SketchToExtrude { sketch, extrude }) 148 + } 149 + 150 + fn drop_edges_incident(&mut self, feature: FeatureId) { 151 + self.edges.retain(|edge| match edge { 152 + FeatureEdge::SketchToExtrude { sketch, extrude } => { 153 + *sketch != feature && *extrude != feature 154 + } 155 + }); 156 + } 157 + 158 + fn feature_of(&self, matches_node: impl Fn(&FeatureNode) -> bool) -> Option<FeatureId> { 159 + self.entries 160 + .iter() 161 + .find(|e| matches_node(&e.node)) 162 + .map(|e| e.id) 163 + } 164 + 87 165 fn allocate(&self) -> FeatureId { 88 166 let highest = self.entries.iter().map(|e| idx_of(e.id)).max().unwrap_or(0); 89 167 let Some(next) = highest.checked_add(1) else { ··· 103 181 }; 104 182 idx 105 183 } 184 + 185 + #[cfg(test)] 186 + pub(crate) fn sample_blind_extrude(sketch: SketchId) -> ExtrudeFeature { 187 + use bone_kernel::{ExtrudeDirection, ExtrudeEndCondition, ExtrudeSense, MergeResult}; 188 + use bone_types::{Length, PositiveLength, millimeter}; 189 + let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(10.0)) else { 190 + panic!("10 mm is a positive length"); 191 + }; 192 + ExtrudeFeature { 193 + sketch, 194 + direction: ExtrudeDirection::Normal { 195 + sense: ExtrudeSense::Forward, 196 + }, 197 + end_condition: ExtrudeEndCondition::Blind { depth }, 198 + draft: None, 199 + thin_wall: None, 200 + merge_result: MergeResult::Merge, 201 + } 202 + } 203 + 204 + #[cfg(test)] 205 + mod tests { 206 + use super::{FeatureEdge, FeatureNode, FeatureTree, sample_blind_extrude}; 207 + use bone_types::{ExtrudeId, SketchId}; 208 + 209 + #[test] 210 + fn push_extrude_adds_node_and_links_sketch() { 211 + let mut tree = FeatureTree::seeded(); 212 + let sketch = SketchId::default(); 213 + let sketch_feature = tree.push_sketch(sketch); 214 + let extrude = ExtrudeId::default(); 215 + let extrude_feature = tree.push_extrude(extrude, &sample_blind_extrude(sketch)); 216 + 217 + assert_eq!( 218 + tree.node(extrude_feature), 219 + Some(FeatureNode::Extrude(extrude)) 220 + ); 221 + assert_eq!(tree.feature_of_extrude(extrude), Some(extrude_feature)); 222 + assert_eq!( 223 + tree.edges(), 224 + &[FeatureEdge::SketchToExtrude { 225 + sketch: sketch_feature, 226 + extrude: extrude_feature, 227 + }] 228 + ); 229 + } 230 + 231 + #[test] 232 + fn push_extrude_is_idempotent_on_duplicate_id() { 233 + let mut tree = FeatureTree::seeded(); 234 + let sketch = SketchId::default(); 235 + tree.push_sketch(sketch); 236 + let extrude = ExtrudeId::default(); 237 + let feature = sample_blind_extrude(sketch); 238 + let first = tree.push_extrude(extrude, &feature); 239 + let count_before = tree.iter().count(); 240 + let second = tree.push_extrude(extrude, &feature); 241 + 242 + assert_eq!(first, second); 243 + assert_eq!(tree.iter().count(), count_before); 244 + assert_eq!(tree.edges().len(), 1); 245 + } 246 + 247 + #[test] 248 + fn push_extrude_without_sketch_node_records_no_edge() { 249 + let mut tree = FeatureTree::seeded(); 250 + let extrude = ExtrudeId::default(); 251 + let extrude_feature = 252 + tree.push_extrude(extrude, &sample_blind_extrude(SketchId::default())); 253 + 254 + assert_eq!( 255 + tree.node(extrude_feature), 256 + Some(FeatureNode::Extrude(extrude)) 257 + ); 258 + assert!(tree.edges().is_empty()); 259 + } 260 + 261 + fn sketch_key(n: u64) -> SketchId { 262 + use slotmap::KeyData; 263 + SketchId::from(KeyData::from_ffi((1u64 << 32) | n)) 264 + } 265 + 266 + #[test] 267 + fn push_extrude_retargets_edge_to_new_sketch() { 268 + let mut tree = FeatureTree::seeded(); 269 + let sketch_a = sketch_key(1); 270 + let sketch_b = sketch_key(2); 271 + tree.push_sketch(sketch_a); 272 + let feat_b = tree.push_sketch(sketch_b); 273 + let extrude = ExtrudeId::default(); 274 + let ext_feat = tree.push_extrude(extrude, &sample_blind_extrude(sketch_a)); 275 + tree.push_extrude(extrude, &sample_blind_extrude(sketch_b)); 276 + assert_eq!( 277 + tree.edges(), 278 + &[FeatureEdge::SketchToExtrude { 279 + sketch: feat_b, 280 + extrude: ext_feat, 281 + }] 282 + ); 283 + } 284 + 285 + #[test] 286 + fn remove_extrude_drops_node_and_edge() { 287 + let mut tree = FeatureTree::seeded(); 288 + let sketch = SketchId::default(); 289 + tree.push_sketch(sketch); 290 + let extrude = ExtrudeId::default(); 291 + tree.push_extrude(extrude, &sample_blind_extrude(sketch)); 292 + assert_eq!(tree.edges().len(), 1); 293 + 294 + let removed = tree.remove_extrude(extrude); 295 + 296 + assert!(removed.is_some()); 297 + assert_eq!(tree.feature_of_extrude(extrude), None); 298 + assert!(tree.edges().is_empty()); 299 + } 300 + 301 + #[test] 302 + fn edges_are_derived_not_persisted() { 303 + let mut tree = FeatureTree::seeded(); 304 + let sketch = SketchId::default(); 305 + tree.push_sketch(sketch); 306 + tree.push_extrude(ExtrudeId::default(), &sample_blind_extrude(sketch)); 307 + assert_eq!(tree.edges().len(), 1); 308 + let Ok(ron) = crate::io::ron_io::to_string(&tree) else { 309 + panic!("feature tree serializes"); 310 + }; 311 + assert!(!ron.contains("edges")); 312 + let Ok(back) = crate::io::ron_io::from_str::<FeatureTree>(&ron) else { 313 + panic!("feature tree deserializes"); 314 + }; 315 + assert!(back.edges().is_empty()); 316 + } 317 + }
+123 -5
crates/bone-document/src/document/mod.rs
··· 1 1 use std::collections::BTreeMap; 2 2 3 - use bone_types::{DocumentId, FeatureId, SchemaHeader, SketchId}; 3 + use bone_kernel::ExtrudeFeature; 4 + use bone_types::{DocumentId, ExtrudeId, FeatureId, SchemaHeader, SketchId}; 4 5 use serde::{Deserialize, Serialize}; 5 6 6 7 use crate::Sketch; 7 8 8 9 pub mod feature_tree; 9 10 10 - pub use feature_tree::{FeatureNode, FeatureTree, PrincipalPlane}; 11 + pub use feature_tree::{FeatureEdge, FeatureNode, FeatureTree, PrincipalPlane}; 11 12 12 13 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 13 14 #[serde(deny_unknown_fields)] ··· 130 131 pub parameters: DocumentParameters, 131 132 pub feature_tree: FeatureTree, 132 133 pub sketches: SketchRegistry, 134 + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] 135 + pub extrudes: BTreeMap<ExtrudeId, ExtrudeFeature>, 133 136 } 134 137 135 138 impl DocumentHeader { ··· 143 146 parameters: DocumentParameters::new(), 144 147 feature_tree: FeatureTree::seeded(), 145 148 sketches: SketchRegistry::new(), 149 + extrudes: BTreeMap::new(), 146 150 } 151 + } 152 + 153 + pub(crate) fn rebuild_edges(&mut self) { 154 + self.feature_tree 155 + .rebuild_edges(self.extrudes.iter().map(|(id, feature)| (*id, feature))); 147 156 } 148 157 } 149 158 ··· 179 188 } 180 189 } 181 190 182 - pub(crate) fn from_parts(header: DocumentHeader, sketches: BTreeMap<SketchId, Sketch>) -> Self { 191 + pub(crate) fn from_parts( 192 + mut header: DocumentHeader, 193 + sketches: BTreeMap<SketchId, Sketch>, 194 + ) -> Self { 195 + header.rebuild_edges(); 183 196 Self { header, sketches } 184 197 } 185 198 ··· 270 283 .insert(id, SketchRegistryEntry { label, filename }); 271 284 self.header.feature_tree.push_sketch(id); 272 285 self.sketches.insert(id, sketch); 286 + let dependents: Vec<ExtrudeId> = self.extrudes_of_sketch(id).collect(); 287 + dependents.iter().for_each(|extrude| { 288 + if let Some(feature) = self.header.extrudes.get(extrude).copied() { 289 + self.header.feature_tree.push_extrude(*extrude, &feature); 290 + } 291 + }); 273 292 } 274 293 275 294 pub fn replace_sketch(&mut self, id: SketchId, sketch: Sketch) -> Option<Sketch> { 276 295 self.sketches.insert(id, sketch) 277 296 } 278 297 298 + pub fn insert_extrude(&mut self, id: ExtrudeId, feature: ExtrudeFeature) { 299 + self.header.feature_tree.push_extrude(id, &feature); 300 + self.header.extrudes.insert(id, feature); 301 + } 302 + 303 + pub fn remove_extrude(&mut self, id: ExtrudeId) -> Option<ExtrudeFeature> { 304 + self.header.feature_tree.remove_extrude(id); 305 + self.header.extrudes.remove(&id) 306 + } 307 + 308 + fn extrudes_of_sketch(&self, sketch: SketchId) -> impl Iterator<Item = ExtrudeId> + '_ { 309 + self.header 310 + .extrudes 311 + .iter() 312 + .filter(move |(_, feature)| feature.sketch == sketch) 313 + .map(|(extrude, _)| *extrude) 314 + } 315 + 279 316 pub fn remove_sketch(&mut self, id: SketchId) -> Option<Sketch> { 317 + let dependents: Vec<ExtrudeId> = self.extrudes_of_sketch(id).collect(); 318 + dependents.iter().for_each(|extrude| { 319 + self.remove_extrude(*extrude); 320 + }); 280 321 self.header.sketches.remove(id); 281 322 self.header.feature_tree.remove_sketch(id); 282 323 self.sketches.remove(&id) ··· 290 331 pub fn sketch_of_feature(&self, feature: FeatureId) -> Option<&Sketch> { 291 332 match self.header.feature_tree.node(feature)? { 292 333 FeatureNode::Sketch(id) => self.sketches.get(&id), 293 - FeatureNode::Origin | FeatureNode::PrincipalPlane(_) => None, 334 + FeatureNode::Origin | FeatureNode::PrincipalPlane(_) | FeatureNode::Extrude(_) => None, 335 + } 336 + } 337 + 338 + #[must_use] 339 + pub fn extrude_of_feature(&self, feature: FeatureId) -> Option<&ExtrudeFeature> { 340 + match self.header.feature_tree.node(feature)? { 341 + FeatureNode::Extrude(id) => self.header.extrudes.get(&id), 342 + FeatureNode::Origin | FeatureNode::PrincipalPlane(_) | FeatureNode::Sketch(_) => None, 294 343 } 295 344 } 296 345 } ··· 303 352 304 353 #[cfg(test)] 305 354 mod tests { 355 + use super::feature_tree::sample_blind_extrude; 306 356 use super::{Document, RenameSketchError, Sketch, SketchId}; 307 - use bone_types::{DocumentId, Point3, SketchPlaneBasis, Tolerance, UnitVec3}; 357 + use bone_types::{DocumentId, ExtrudeId, Point3, SketchPlaneBasis, Tolerance, UnitVec3}; 308 358 309 359 fn xy_basis() -> SketchPlaneBasis { 310 360 let Ok(basis) = SketchPlaneBasis::new( ··· 354 404 } 355 405 356 406 #[test] 407 + fn insert_extrude_resolves_through_feature_node() { 408 + let (mut document, sketch) = doc_with_sketch(); 409 + let extrude = ExtrudeId::default(); 410 + let feature = sample_blind_extrude(sketch); 411 + document.insert_extrude(extrude, feature); 412 + 413 + let Some(extrude_feature) = document.feature_tree().feature_of_extrude(extrude) else { 414 + panic!("extrude node present after insert"); 415 + }; 416 + assert_eq!(document.extrude_of_feature(extrude_feature), Some(&feature)); 417 + assert_eq!(document.sketch_of_feature(extrude_feature), None); 418 + } 419 + 420 + #[test] 357 421 fn rename_sketch_rejects_unknown_id() { 358 422 let mut document = Document::new(DocumentId::default(), "doc".to_owned()); 359 423 let stranger = SketchId::default(); ··· 361 425 panic!("unknown sketch must be rejected"); 362 426 }; 363 427 assert_eq!(err, RenameSketchError::UnknownSketch(stranger)); 428 + } 429 + 430 + #[test] 431 + fn remove_extrude_resolves_to_none_afterward() { 432 + let (mut document, sketch) = doc_with_sketch(); 433 + let extrude = ExtrudeId::default(); 434 + document.insert_extrude(extrude, sample_blind_extrude(sketch)); 435 + let Some(feature) = document.feature_tree().feature_of_extrude(extrude) else { 436 + panic!("extrude node present after insert"); 437 + }; 438 + assert!(document.extrude_of_feature(feature).is_some()); 439 + 440 + let removed = document.remove_extrude(extrude); 441 + 442 + assert_eq!(removed, Some(sample_blind_extrude(sketch))); 443 + assert_eq!(document.feature_tree().feature_of_extrude(extrude), None); 444 + } 445 + 446 + #[test] 447 + fn remove_sketch_cascades_dependent_extrude() { 448 + let (mut document, sketch) = doc_with_sketch(); 449 + let extrude = ExtrudeId::default(); 450 + document.insert_extrude(extrude, sample_blind_extrude(sketch)); 451 + assert_eq!(document.feature_tree().edges().len(), 1); 452 + 453 + document.remove_sketch(sketch); 454 + 455 + assert_eq!(document.feature_tree().feature_of_extrude(extrude), None); 456 + assert!(document.feature_tree().edges().is_empty()); 457 + } 458 + 459 + #[test] 460 + fn insert_sketch_backlinks_pending_extrude() { 461 + let mut document = Document::new(DocumentId::default(), "doc".to_owned()); 462 + let sketch = SketchId::default(); 463 + let extrude = ExtrudeId::default(); 464 + document.insert_extrude(extrude, sample_blind_extrude(sketch)); 465 + assert!(document.feature_tree().edges().is_empty()); 466 + 467 + document.insert_sketch(sketch, "Sketch1".to_owned(), Sketch::new(xy_basis())); 468 + 469 + let Some(sketch_feature) = document.feature_tree().feature_of_sketch(sketch) else { 470 + panic!("sketch node present after insert"); 471 + }; 472 + let Some(extrude_feature) = document.feature_tree().feature_of_extrude(extrude) else { 473 + panic!("extrude node present"); 474 + }; 475 + assert_eq!( 476 + document.feature_tree().edges(), 477 + &[super::FeatureEdge::SketchToExtrude { 478 + sketch: sketch_feature, 479 + extrude: extrude_feature, 480 + }] 481 + ); 364 482 } 365 483 }
+42 -4
crates/bone-document/src/io/folder.rs
··· 4 4 use std::sync::atomic::{AtomicU64, Ordering}; 5 5 use std::{fs, io}; 6 6 7 - use bone_types::{SchemaHeader, SchemaVersion, SketchId}; 7 + use bone_types::{ExtrudeId, SchemaHeader, SchemaVersion, SketchId}; 8 8 9 9 use crate::document::{Document, DocumentHeader, FeatureNode, SketchFile, sketch_filename}; 10 10 use crate::io::blob::{BlobHash, BlobKind}; ··· 90 90 DanglingTreeSketch { id: SketchId }, 91 91 #[error("registry has sketch {id:?} absent from feature tree")] 92 92 OrphanRegistered { id: SketchId }, 93 + #[error("feature tree references extrude {id:?} not in extrudes")] 94 + DanglingTreeExtrude { id: ExtrudeId }, 95 + #[error("stored extrude {id:?} absent from feature tree")] 96 + OrphanExtrude { id: ExtrudeId }, 97 + #[error("stored extrude {extrude:?} references sketch {sketch:?} absent from the document")] 98 + DanglingExtrudeSketch { 99 + extrude: ExtrudeId, 100 + sketch: SketchId, 101 + }, 93 102 #[error("geometry blob at {path}: {source}")] 94 103 Blob { 95 104 path: PathBuf, ··· 221 230 } 222 231 223 232 fn validate_header(header: &DocumentHeader) -> Result<(), FolderError> { 233 + let tree = &header.feature_tree; 234 + 235 + let tree_extrudes: BTreeSet<ExtrudeId> = tree 236 + .iter() 237 + .filter_map(|(_, node)| match node { 238 + FeatureNode::Extrude(id) => Some(id), 239 + FeatureNode::Origin | FeatureNode::PrincipalPlane(_) | FeatureNode::Sketch(_) => None, 240 + }) 241 + .collect(); 242 + let stored_extrudes: BTreeSet<ExtrudeId> = header.extrudes.keys().copied().collect(); 243 + if let Some(&id) = tree_extrudes.difference(&stored_extrudes).next() { 244 + return Err(FolderErrorKind::DanglingTreeExtrude { id }.wrap()); 245 + } 246 + if let Some(&id) = stored_extrudes.difference(&tree_extrudes).next() { 247 + return Err(FolderErrorKind::OrphanExtrude { id }.wrap()); 248 + } 249 + 224 250 let registered: BTreeSet<SketchId> = header.sketches.order().iter().copied().collect(); 225 - let tree_sketches: BTreeSet<SketchId> = header 226 - .feature_tree 251 + let tree_sketches: BTreeSet<SketchId> = tree 227 252 .iter() 228 253 .filter_map(|(_, node)| match node { 229 254 FeatureNode::Sketch(id) => Some(id), 230 - FeatureNode::Origin | FeatureNode::PrincipalPlane(_) => None, 255 + FeatureNode::Origin | FeatureNode::PrincipalPlane(_) | FeatureNode::Extrude(_) => None, 231 256 }) 232 257 .collect(); 233 258 if let Some(&id) = tree_sketches.difference(&registered).next() { ··· 236 261 if let Some(&id) = registered.difference(&tree_sketches).next() { 237 262 return Err(FolderErrorKind::OrphanRegistered { id }.wrap()); 238 263 } 264 + 265 + if let Some((&extrude, feature)) = header 266 + .extrudes 267 + .iter() 268 + .find(|(_, feature)| !registered.contains(&feature.sketch)) 269 + { 270 + return Err(FolderErrorKind::DanglingExtrudeSketch { 271 + extrude, 272 + sketch: feature.sketch, 273 + } 274 + .wrap()); 275 + } 276 + 239 277 Ok(()) 240 278 } 241 279
+3 -3
crates/bone-document/src/lib.rs
··· 5 5 pub mod undo; 6 6 7 7 pub use document::{ 8 - Document, DocumentHeader, DocumentParameters, FeatureNode, FeatureTree, PrincipalPlane, 9 - RenameSketchError, SketchFile, SketchRegistry, SketchRegistryEntry, UnitsPreference, 10 - sketch_filename, 8 + Document, DocumentHeader, DocumentParameters, FeatureEdge, FeatureNode, FeatureTree, 9 + PrincipalPlane, RenameSketchError, SketchFile, SketchRegistry, SketchRegistryEntry, 10 + UnitsPreference, sketch_filename, 11 11 }; 12 12 pub use evaluator::{EvaluatedSketch, FeatureCache, evaluate_sketch}; 13 13 pub use io::{
+103 -3
crates/bone-document/tests/folder_roundtrip.rs
··· 3 3 SketchDimension, SketchEdit, SketchEntity, SketchRegistry, SketchRegistryEntry, SketchRelation, 4 4 from_str, load, save, to_string, 5 5 }; 6 + use bone_kernel::{ 7 + ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense, MergeResult, 8 + }; 6 9 use bone_types::{ 7 - Angle, DocumentId, Length, Point2, Point3, SketchEntityId, SketchId, SketchPlaneBasis, 8 - Tolerance, UnitVec3, degree, millimeter, 10 + Angle, DocumentId, ExtrudeId, Length, Point2, Point3, PositiveLength, SketchEntityId, SketchId, 11 + SketchPlaneBasis, Tolerance, UnitVec3, degree, millimeter, 9 12 }; 10 13 use slotmap::{Key, KeyData}; 11 14 use tempfile::{TempDir, tempdir}; ··· 43 46 44 47 fn document_id(idx: u32) -> DocumentId { 45 48 DocumentId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx))) 49 + } 50 + 51 + fn extrude_id(idx: u32) -> ExtrudeId { 52 + ExtrudeId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx))) 53 + } 54 + 55 + fn blind_extrude(sketch: SketchId) -> ExtrudeFeature { 56 + let Ok(depth) = PositiveLength::new(mm(10.0)) else { 57 + panic!("positive depth"); 58 + }; 59 + ExtrudeFeature { 60 + sketch, 61 + direction: ExtrudeDirection::Normal { 62 + sense: ExtrudeSense::Forward, 63 + }, 64 + end_condition: ExtrudeEndCondition::Blind { depth }, 65 + draft: None, 66 + thin_wall: None, 67 + merge_result: MergeResult::Merge, 68 + } 46 69 } 47 70 48 71 fn rectangle() -> Sketch { ··· 206 229 else { 207 230 panic!("expected UnsupportedMajor"); 208 231 }; 209 - assert_eq!(found, SchemaVersion::new(9999, 0)); 232 + assert_eq!(found, SchemaVersion::new(9999, 1)); 210 233 assert_eq!( 211 234 supported, 212 235 SchemaVersion::new( ··· 536 559 panic!("expected DanglingTreeSketch, got {result:?}"); 537 560 }; 538 561 assert_eq!(id, sketch_id(99)); 562 + } 563 + 564 + #[test] 565 + fn extrude_roundtrips_through_folder() { 566 + let dir = ok_dir(); 567 + let folder = DocumentFolder::new(dir.path().join("extrude.bone")); 568 + let mut doc = Document::new(document_id(1), "extrude".to_owned()); 569 + doc.insert_sketch(sketch_id(1), "S".to_owned(), rectangle()); 570 + doc.insert_extrude(extrude_id(1), blind_extrude(sketch_id(1))); 571 + assert_save(&doc, &folder); 572 + 573 + let loaded = assert_load(&folder); 574 + let Some(feature) = loaded.feature_tree().feature_of_extrude(extrude_id(1)) else { 575 + panic!("extrude node survives the round-trip"); 576 + }; 577 + assert_eq!( 578 + loaded.extrude_of_feature(feature), 579 + Some(&blind_extrude(sketch_id(1))) 580 + ); 581 + assert_eq!(loaded.feature_tree().edges().len(), 1); 582 + } 583 + 584 + #[test] 585 + fn load_refuses_tree_extrude_without_entry() { 586 + let dir = ok_dir(); 587 + let folder = DocumentFolder::new(dir.path().join("dangling_extrude.bone")); 588 + let mut doc = Document::new(document_id(1), "dangling".to_owned()); 589 + doc.insert_sketch(sketch_id(1), "S".to_owned(), rectangle()); 590 + doc.insert_extrude(extrude_id(1), blind_extrude(sketch_id(1))); 591 + assert_save(&doc, &folder); 592 + 593 + patch_header(&folder.document_file(), |h| { 594 + h.extrudes.remove(&extrude_id(1)); 595 + }); 596 + 597 + let result = load(&folder).map_err(bone_document::FolderError::into_kind); 598 + let Err(bone_document::FolderErrorKind::DanglingTreeExtrude { id }) = result else { 599 + panic!("expected DanglingTreeExtrude, got {result:?}"); 600 + }; 601 + assert_eq!(id, extrude_id(1)); 602 + } 603 + 604 + #[test] 605 + fn load_refuses_orphan_extrude() { 606 + let dir = ok_dir(); 607 + let folder = DocumentFolder::new(dir.path().join("orphan_extrude.bone")); 608 + let mut doc = Document::new(document_id(1), "orphan".to_owned()); 609 + doc.insert_sketch(sketch_id(1), "S".to_owned(), rectangle()); 610 + doc.insert_extrude(extrude_id(1), blind_extrude(sketch_id(1))); 611 + assert_save(&doc, &folder); 612 + 613 + patch_header(&folder.document_file(), |h| { 614 + h.feature_tree.remove_extrude(extrude_id(1)); 615 + }); 616 + 617 + let result = load(&folder).map_err(bone_document::FolderError::into_kind); 618 + let Err(bone_document::FolderErrorKind::OrphanExtrude { id }) = result else { 619 + panic!("expected OrphanExtrude, got {result:?}"); 620 + }; 621 + assert_eq!(id, extrude_id(1)); 622 + } 623 + 624 + #[test] 625 + fn load_refuses_extrude_with_unregistered_sketch() { 626 + let dir = ok_dir(); 627 + let folder = DocumentFolder::new(dir.path().join("ghost_sketch.bone")); 628 + let mut doc = Document::new(document_id(1), "ghost".to_owned()); 629 + doc.insert_extrude(extrude_id(1), blind_extrude(sketch_id(99))); 630 + assert_save(&doc, &folder); 631 + 632 + let result = load(&folder).map_err(bone_document::FolderError::into_kind); 633 + let Err(bone_document::FolderErrorKind::DanglingExtrudeSketch { extrude, sketch }) = result 634 + else { 635 + panic!("expected DanglingExtrudeSketch, got {result:?}"); 636 + }; 637 + assert_eq!(extrude, extrude_id(1)); 638 + assert_eq!(sketch, sketch_id(99)); 539 639 } 540 640 541 641 #[test]
+1 -1
crates/bone-document/tests/snapshots/folder_snapshots__document_header.snap
··· 10 10 name: "bone-document", 11 11 version: SchemaVersion( 12 12 major: 1, 13 - minor: 0, 13 + minor: 1, 14 14 ), 15 15 ), 16 16 id: SerKey(
+1 -1
crates/bone-document/tests/snapshots/folder_snapshots__sketch_file.snap
··· 10 10 name: "bone-document", 11 11 version: SchemaVersion( 12 12 major: 1, 13 - minor: 0, 13 + minor: 1, 14 14 ), 15 15 ), 16 16 sketch: Sketch(
+3 -1
crates/bone-interop/src/step.rs
··· 15 15 const fn schema_token(schema: StepSchema) -> &'static str { 16 16 match schema { 17 17 StepSchema::Ap214 => "AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }", 18 - StepSchema::Ap242E2 => "AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 3 1 4 }", 18 + StepSchema::Ap242E2 => { 19 + "AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 3 1 4 }" 20 + } 19 21 } 20 22 } 21 23
+5 -2
crates/bone-interop/tests/step.rs
··· 105 105 106 106 fn donut(entities: &mut SlotMap<SketchEntityId, ()>) -> Vec<ProfileLoop> { 107 107 let ring = |radius_mm: f64, entities: &mut SlotMap<SketchEntityId, ()>| { 108 - let Ok(disk) = Circle2::new(Point2::from_mm(0.0, 0.0), Length::new::<millimeter>(radius_mm), TOL) 109 - else { 108 + let Ok(disk) = Circle2::new( 109 + Point2::from_mm(0.0, 0.0), 110 + Length::new::<millimeter>(radius_mm), 111 + TOL, 112 + ) else { 110 113 panic!("positive radius"); 111 114 }; 112 115 ProfileLoop::Closed {
+1 -1
crates/bone-types/src/schema.rs
··· 30 30 impl SchemaHeader { 31 31 pub const BONE_DOCUMENT_NAME: &'static str = "bone-document"; 32 32 pub const BONE_DOCUMENT_MAJOR: u32 = 1; 33 - pub const BONE_DOCUMENT_MINOR: u32 = 0; 33 + pub const BONE_DOCUMENT_MINOR: u32 = 1; 34 34 35 35 #[must_use] 36 36 pub fn bone_document() -> Self {