Another project
0

Configure Feed

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

feat(document): persist imported solids as body blobs w/ label sidecars

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

author
Lewis
date (Jun 7, 2026, 6:04 PM +0300) commit 04f7e8ca parent a4fbbcaf change-id mqqoooln
+312 -41
+20 -17
crates/bone-document/src/document/feature_tree.rs
··· 1 1 use bone_kernel::ExtrudeFeature; 2 - use bone_types::{ExtrudeId, FeatureId, SketchId}; 2 + use bone_types::{BodyId, ExtrudeId, FeatureId, SketchId}; 3 3 use serde::{Deserialize, Serialize}; 4 - use slotmap::{Key, KeyData}; 4 + 5 + use super::{key_from_index, key_index}; 5 6 6 7 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 7 8 pub enum PrincipalPlane { ··· 16 17 PrincipalPlane(PrincipalPlane), 17 18 Sketch(SketchId), 18 19 Extrude(ExtrudeId), 20 + ImportedBody(BodyId), 19 21 } 20 22 21 23 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] ··· 54 56 .into_iter() 55 57 .zip(1u32..) 56 58 .map(|(node, idx)| FeatureEntry { 57 - id: feature_id_from_idx(idx), 59 + id: key_from_index(idx), 58 60 node, 59 61 }) 60 62 .collect(); ··· 125 127 Some(id) 126 128 } 127 129 130 + pub fn push_imported_body(&mut self, feature: FeatureId, body: BodyId) { 131 + self.entries.push(FeatureEntry { 132 + id: feature, 133 + node: FeatureNode::ImportedBody(body), 134 + }); 135 + } 136 + 128 137 #[must_use] 129 138 pub fn edges(&self) -> &[FeatureEdge] { 130 139 &self.edges ··· 164 173 .map(|e| e.id) 165 174 } 166 175 167 - fn allocate(&self) -> FeatureId { 168 - let highest = self.entries.iter().map(|e| idx_of(e.id)).max().unwrap_or(0); 176 + pub(crate) fn allocate(&self) -> FeatureId { 177 + let highest = self 178 + .entries 179 + .iter() 180 + .map(|e| key_index(e.id)) 181 + .max() 182 + .unwrap_or(0); 169 183 let Some(next) = highest.checked_add(1) else { 170 184 panic!("FeatureTree exhausted 32-bit feature id space"); 171 185 }; 172 - feature_id_from_idx(next) 186 + key_from_index(next) 173 187 } 174 - } 175 - 176 - fn feature_id_from_idx(idx: u32) -> FeatureId { 177 - FeatureId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx))) 178 - } 179 - 180 - fn idx_of(id: FeatureId) -> u32 { 181 - let Ok(idx) = u32::try_from(id.data().as_ffi() & 0xFFFF_FFFF) else { 182 - panic!("lower 32 bits of ffi key fit in u32"); 183 - }; 184 - idx 185 188 } 186 189 187 190 #[cfg(test)]
+136 -9
crates/bone-document/src/document/mod.rs
··· 1 1 use std::collections::BTreeMap; 2 2 3 - use bone_kernel::ExtrudeFeature; 4 - use bone_types::{DocumentId, ExtrudeId, FeatureId, SchemaHeader, SketchId}; 3 + use bone_kernel::{BrepEdge, BrepFace, BrepSolid, BrepVertex, ExtrudeFeature}; 4 + use bone_types::{BodyId, DocumentId, ExtrudeId, FeatureId, SchemaHeader, SketchId}; 5 5 use serde::{Deserialize, Serialize}; 6 + use slotmap::KeyData; 6 7 7 8 use crate::Sketch; 8 9 ··· 10 11 11 12 pub use feature_tree::{FeatureEdge, FeatureNode, FeatureTree, PrincipalPlane}; 12 13 14 + #[derive(Clone)] 15 + pub struct ImportedSolid { 16 + solid: BrepSolid, 17 + } 18 + 19 + impl ImportedSolid { 20 + #[must_use] 21 + pub fn new(solid: BrepSolid) -> Self { 22 + Self { solid } 23 + } 24 + 25 + #[must_use] 26 + pub fn solid(&self) -> &BrepSolid { 27 + &self.solid 28 + } 29 + } 30 + 31 + impl core::fmt::Debug for ImportedSolid { 32 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 33 + f.debug_struct("ImportedSolid") 34 + .field("content_key", &self.solid.content_key()) 35 + .finish() 36 + } 37 + } 38 + 39 + impl PartialEq for ImportedSolid { 40 + fn eq(&self, other: &Self) -> bool { 41 + self.solid.content_key() == other.solid.content_key() 42 + && self 43 + .solid 44 + .iter_faces() 45 + .map(BrepFace::label) 46 + .eq(other.solid.iter_faces().map(BrepFace::label)) 47 + && self 48 + .solid 49 + .iter_edges() 50 + .map(BrepEdge::label) 51 + .eq(other.solid.iter_edges().map(BrepEdge::label)) 52 + && self 53 + .solid 54 + .iter_vertices() 55 + .map(BrepVertex::label) 56 + .eq(other.solid.iter_vertices().map(BrepVertex::label)) 57 + } 58 + } 59 + 13 60 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 14 61 #[serde(deny_unknown_fields)] 15 62 pub enum UnitsPreference { ··· 194 241 pub struct Document { 195 242 header: DocumentHeader, 196 243 sketches: BTreeMap<SketchId, Sketch>, 244 + bodies: BTreeMap<BodyId, ImportedSolid>, 197 245 } 198 246 199 247 impl Document { ··· 202 250 Self { 203 251 header: DocumentHeader::new(id, name), 204 252 sketches: BTreeMap::new(), 253 + bodies: BTreeMap::new(), 205 254 } 206 255 } 207 256 208 257 pub(crate) fn from_parts( 209 258 mut header: DocumentHeader, 210 259 sketches: BTreeMap<SketchId, Sketch>, 260 + bodies: BTreeMap<BodyId, ImportedSolid>, 211 261 ) -> Self { 212 262 header.rebuild_edges(); 213 - Self { header, sketches } 263 + Self { 264 + header, 265 + sketches, 266 + bodies, 267 + } 214 268 } 215 269 216 270 #[must_use] ··· 348 402 pub fn sketch_of_feature(&self, feature: FeatureId) -> Option<&Sketch> { 349 403 match self.header.feature_tree.node(feature)? { 350 404 FeatureNode::Sketch(id) => self.sketches.get(&id), 351 - FeatureNode::Origin | FeatureNode::PrincipalPlane(_) | FeatureNode::Extrude(_) => None, 405 + FeatureNode::Origin 406 + | FeatureNode::PrincipalPlane(_) 407 + | FeatureNode::Extrude(_) 408 + | FeatureNode::ImportedBody(_) => None, 352 409 } 353 410 } 354 411 ··· 356 413 pub fn extrude_of_feature(&self, feature: FeatureId) -> Option<&ExtrudeFeature> { 357 414 match self.header.feature_tree.node(feature)? { 358 415 FeatureNode::Extrude(id) => self.header.extrudes.get(&id), 359 - FeatureNode::Origin | FeatureNode::PrincipalPlane(_) | FeatureNode::Sketch(_) => None, 416 + FeatureNode::Origin 417 + | FeatureNode::PrincipalPlane(_) 418 + | FeatureNode::Sketch(_) 419 + | FeatureNode::ImportedBody(_) => None, 420 + } 421 + } 422 + 423 + pub fn import_body<E>( 424 + &mut self, 425 + build: impl FnOnce(FeatureId) -> Result<BrepSolid, E>, 426 + ) -> Result<(FeatureId, BodyId), E> { 427 + let feature = self.header.feature_tree.allocate(); 428 + let body = self.next_body_id(); 429 + let solid = build(feature)?; 430 + self.header.feature_tree.push_imported_body(feature, body); 431 + self.bodies.insert(body, ImportedSolid::new(solid)); 432 + Ok((feature, body)) 433 + } 434 + 435 + #[must_use] 436 + pub fn imported_body(&self, body: BodyId) -> Option<&BrepSolid> { 437 + self.bodies.get(&body).map(ImportedSolid::solid) 438 + } 439 + 440 + #[must_use] 441 + pub fn imported_body_of_feature(&self, feature: FeatureId) -> Option<&BrepSolid> { 442 + match self.header.feature_tree.node(feature)? { 443 + FeatureNode::ImportedBody(body) => self.imported_body(body), 444 + FeatureNode::Origin 445 + | FeatureNode::PrincipalPlane(_) 446 + | FeatureNode::Sketch(_) 447 + | FeatureNode::Extrude(_) => None, 360 448 } 361 449 } 450 + 451 + pub fn imported_bodies(&self) -> impl Iterator<Item = (BodyId, &BrepSolid)> + '_ { 452 + self.bodies.iter().map(|(id, body)| (*id, body.solid())) 453 + } 454 + 455 + fn next_body_id(&self) -> BodyId { 456 + let highest = self 457 + .bodies 458 + .keys() 459 + .copied() 460 + .map(key_index) 461 + .max() 462 + .unwrap_or(0); 463 + let Some(next) = highest.checked_add(1) else { 464 + panic!("document exhausted 32-bit body id space"); 465 + }; 466 + key_from_index(next) 467 + } 362 468 } 363 469 364 - fn id_filename<K: slotmap::Key>(id: K) -> String { 365 - format!("{:016x}.ron", id.data().as_ffi()) 470 + pub(crate) fn key_index<K: slotmap::Key>(id: K) -> u32 { 471 + let Ok(idx) = u32::try_from(id.data().as_ffi() & 0xFFFF_FFFF) else { 472 + panic!("lower 32 bits of an ffi key fit in u32"); 473 + }; 474 + idx 475 + } 476 + 477 + pub(crate) fn key_from_index<K: slotmap::Key>(idx: u32) -> K { 478 + K::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx))) 479 + } 480 + 481 + fn id_stem<K: slotmap::Key>(id: K) -> String { 482 + format!("{:016x}", id.data().as_ffi()) 366 483 } 367 484 368 485 #[must_use] 369 486 pub fn sketch_filename(id: SketchId) -> String { 370 - id_filename(id) 487 + format!("{}.ron", id_stem(id)) 371 488 } 372 489 373 490 #[must_use] 374 491 pub fn extrude_filename(id: ExtrudeId) -> String { 375 - id_filename(id) 492 + format!("{}.ron", id_stem(id)) 493 + } 494 + 495 + #[must_use] 496 + pub fn body_brep_filename(id: BodyId) -> String { 497 + format!("{}.brep", id_stem(id)) 498 + } 499 + 500 + #[must_use] 501 + pub fn body_labels_filename(id: BodyId) -> String { 502 + format!("{}.labels", id_stem(id)) 376 503 } 377 504 378 505 #[cfg(test)]
+155 -14
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_kernel::ExtrudeFeature; 7 + use bone_kernel::{BrepSolid, ExtrudeFeature}; 8 8 use bone_types::{ 9 - AngleTolerance, ChordHeightTolerance, ExtrudeId, SchemaHeader, SchemaVersion, SketchId, 9 + AngleTolerance, BodyId, ChordHeightTolerance, ExtrudeId, SchemaHeader, SchemaVersion, SketchId, 10 10 }; 11 11 12 12 use crate::document::{ 13 - Document, DocumentHeader, ExtrudeFile, FeatureNode, SketchFile, extrude_filename, 14 - sketch_filename, 13 + Document, DocumentHeader, ExtrudeFile, FeatureNode, ImportedSolid, SketchFile, 14 + body_brep_filename, body_labels_filename, extrude_filename, sketch_filename, 15 15 }; 16 16 use crate::io::blob::{BlobHash, BlobKind}; 17 + use crate::io::labels::LabelSidecar; 17 18 use crate::io::ron_io::{RonError, from_str, to_string}; 18 19 use crate::sketch::SketchEditError; 19 20 20 21 pub const DOCUMENT_FILE: &str = "document.ron"; 21 22 pub const SKETCHES_DIR: &str = "sketches"; 22 23 pub const EXTRUDES_DIR: &str = "extrudes"; 24 + pub const BODIES_DIR: &str = "bodies"; 23 25 pub const BLOBS_DIR: &str = "blobs"; 24 26 pub const CACHES_DIR: &str = "caches"; 25 27 pub const TESSELLATIONS_DIR: &str = "tessellations"; ··· 100 102 OrphanRegistered { id: SketchId }, 101 103 #[error("feature tree references extrude {id:?} with no file on disk")] 102 104 MissingExtrudeFile { id: ExtrudeId }, 105 + #[error("feature tree references imported body {id:?} with no blob on disk")] 106 + MissingBodyFile { id: BodyId }, 103 107 #[error("stored extrude {extrude:?} references sketch {sketch:?} absent from the document")] 104 108 DanglingExtrudeSketch { 105 109 extrude: ExtrudeId, ··· 153 157 } 154 158 155 159 #[must_use] 160 + pub fn bodies_dir(&self) -> PathBuf { 161 + self.path.join(BODIES_DIR) 162 + } 163 + 164 + #[must_use] 156 165 pub fn blobs_dir(&self) -> PathBuf { 157 166 self.path.join(BLOBS_DIR) 158 167 } ··· 173 182 } 174 183 175 184 #[must_use] 185 + pub fn body_brep_path(&self, id: BodyId) -> PathBuf { 186 + self.bodies_dir().join(body_brep_filename(id)) 187 + } 188 + 189 + #[must_use] 190 + pub fn body_labels_path(&self, id: BodyId) -> PathBuf { 191 + self.bodies_dir().join(body_labels_filename(id)) 192 + } 193 + 194 + #[must_use] 176 195 pub fn blob_path(&self, hash: BlobHash, kind: BlobKind) -> PathBuf { 177 196 self.blobs_dir().join(hash.relative_path(kind)) 178 197 } ··· 224 243 write_if_different(&path, &ron) 225 244 })?; 226 245 246 + document 247 + .imported_bodies() 248 + .try_for_each(|(id, solid)| write_body(folder, id, solid))?; 249 + 227 250 let document_ron = to_ron(&folder.document_file(), document.header())?; 228 251 write_if_different(&folder.document_file(), &document_ron)?; 229 252 ··· 234 257 .copied() 235 258 .map(sketch_filename) 236 259 .collect(); 237 - remove_stale_files(&folder.sketches_dir(), &live_sketches)?; 260 + remove_stale_files(&folder.sketches_dir(), &live_sketches, is_ron)?; 238 261 let live_extrudes = tree_extrudes 239 262 .iter() 240 263 .copied() 241 264 .map(extrude_filename) 242 265 .collect(); 243 - remove_stale_files(&folder.extrudes_dir(), &live_extrudes)?; 266 + remove_stale_files(&folder.extrudes_dir(), &live_extrudes, is_ron)?; 267 + let live_bodies = tree_body_ids(document.header()) 268 + .iter() 269 + .flat_map(|id| [body_brep_filename(*id), body_labels_filename(*id)]) 270 + .collect(); 271 + remove_stale_files(&folder.bodies_dir(), &live_bodies, is_body_blob)?; 244 272 245 273 Ok(()) 246 274 } 247 275 276 + fn write_body(folder: &DocumentFolder, id: BodyId, solid: &BrepSolid) -> Result<(), FolderError> { 277 + let brep_path = folder.body_brep_path(id); 278 + let labels_path = folder.body_labels_path(id); 279 + let blob = solid.to_blob().map_err(|source| { 280 + FolderErrorKind::Blob { 281 + path: brep_path.clone(), 282 + source, 283 + } 284 + .wrap() 285 + })?; 286 + let sidecar = LabelSidecar::capture(solid).to_ron().map_err(|source| { 287 + FolderErrorKind::Ron { 288 + path: labels_path.clone(), 289 + source, 290 + } 291 + .wrap() 292 + })?; 293 + write_bytes_if_different(&brep_path, &blob)?; 294 + write_bytes_if_different(&labels_path, sidecar.as_bytes()) 295 + } 296 + 297 + fn is_ron(ext: &str) -> bool { 298 + ext.eq_ignore_ascii_case("ron") 299 + } 300 + 301 + fn is_body_blob(ext: &str) -> bool { 302 + ext.eq_ignore_ascii_case("brep") || ext.eq_ignore_ascii_case("labels") 303 + } 304 + 248 305 fn tree_extrude_ids(header: &DocumentHeader) -> BTreeSet<ExtrudeId> { 249 306 header 250 307 .feature_tree 251 308 .iter() 252 309 .filter_map(|(_, node)| match node { 253 310 FeatureNode::Extrude(id) => Some(id), 254 - FeatureNode::Origin | FeatureNode::PrincipalPlane(_) | FeatureNode::Sketch(_) => None, 311 + FeatureNode::Origin 312 + | FeatureNode::PrincipalPlane(_) 313 + | FeatureNode::Sketch(_) 314 + | FeatureNode::ImportedBody(_) => None, 315 + }) 316 + .collect() 317 + } 318 + 319 + fn tree_body_ids(header: &DocumentHeader) -> BTreeSet<BodyId> { 320 + header 321 + .feature_tree 322 + .iter() 323 + .filter_map(|(_, node)| match node { 324 + FeatureNode::ImportedBody(id) => Some(id), 325 + FeatureNode::Origin 326 + | FeatureNode::PrincipalPlane(_) 327 + | FeatureNode::Sketch(_) 328 + | FeatureNode::Extrude(_) => None, 255 329 }) 256 330 .collect() 257 331 } ··· 294 368 Ok::<_, FolderError>(acc) 295 369 })?; 296 370 297 - Ok(Document::from_parts(header, sketches)) 371 + let bodies = read_bodies(folder, &header)?; 372 + 373 + Ok(Document::from_parts(header, sketches, bodies)) 374 + } 375 + 376 + fn read_bodies( 377 + folder: &DocumentFolder, 378 + header: &DocumentHeader, 379 + ) -> Result<BTreeMap<BodyId, ImportedSolid>, FolderError> { 380 + tree_body_ids(header) 381 + .into_iter() 382 + .try_fold(BTreeMap::new(), |mut acc, id| { 383 + acc.insert(id, ImportedSolid::new(read_body(folder, id)?)); 384 + Ok::<_, FolderError>(acc) 385 + }) 386 + } 387 + 388 + fn read_body(folder: &DocumentFolder, id: BodyId) -> Result<BrepSolid, FolderError> { 389 + let brep_path = folder.body_brep_path(id); 390 + let labels_path = folder.body_labels_path(id); 391 + let blob = read_bytes(&brep_path).map_err(|e| missing_body(e, id))?; 392 + let sidecar_text = read_to_string(&labels_path).map_err(|e| missing_body(e, id))?; 393 + let sidecar = LabelSidecar::from_ron(&sidecar_text).map_err(|source| { 394 + FolderErrorKind::Ron { 395 + path: labels_path.clone(), 396 + source, 397 + } 398 + .wrap() 399 + })?; 400 + let solid = BrepSolid::from_blob(&blob, sidecar.reattach()).map_err(|source| { 401 + FolderErrorKind::Blob { 402 + path: brep_path, 403 + source, 404 + } 405 + .wrap() 406 + })?; 407 + if sidecar.matches(solid.content_key()) { 408 + Ok(solid) 409 + } else { 410 + Err(FolderErrorKind::SidecarMismatch { path: labels_path }.wrap()) 411 + } 412 + } 413 + 414 + fn missing_body(error: FolderError, id: BodyId) -> FolderError { 415 + match error.into_kind() { 416 + FolderErrorKind::Io { source, .. } if source.kind() == io::ErrorKind::NotFound => { 417 + FolderErrorKind::MissingBodyFile { id }.wrap() 418 + } 419 + other => other.wrap(), 420 + } 298 421 } 299 422 300 423 fn read_extrudes( ··· 326 449 .iter() 327 450 .filter_map(|(_, node)| match node { 328 451 FeatureNode::Sketch(id) => Some(id), 329 - FeatureNode::Origin | FeatureNode::PrincipalPlane(_) | FeatureNode::Extrude(_) => None, 452 + FeatureNode::Origin 453 + | FeatureNode::PrincipalPlane(_) 454 + | FeatureNode::Extrude(_) 455 + | FeatureNode::ImportedBody(_) => None, 330 456 }) 331 457 .collect(); 332 458 if let Some(&id) = tree_sketches.difference(&registered).next() { ··· 408 534 atomic_write(path, contents) 409 535 } 410 536 537 + fn write_bytes_if_different(path: &Path, contents: &[u8]) -> Result<(), FolderError> { 538 + if let Ok(existing) = fs::read(path) 539 + && existing == contents 540 + { 541 + return Ok(()); 542 + } 543 + atomic_write_bytes(path, contents) 544 + } 545 + 411 546 pub(crate) fn write_if_absent(path: &Path, contents: &[u8]) -> Result<(), FolderError> { 412 547 match path.try_exists() { 413 548 Ok(true) => Ok(()), ··· 559 694 }) 560 695 } 561 696 562 - fn remove_stale_files(dir: &Path, live_names: &BTreeSet<String>) -> Result<(), FolderError> { 697 + fn remove_stale_files( 698 + dir: &Path, 699 + live_names: &BTreeSet<String>, 700 + is_managed_ext: impl Fn(&str) -> bool, 701 + ) -> Result<(), FolderError> { 563 702 let entries = match fs::read_dir(dir) { 564 703 Ok(iter) => iter, 565 704 Err(ref source) if source.kind() == io::ErrorKind::NotFound => return Ok(()), ··· 580 719 .wrap() 581 720 })?; 582 721 let name = entry.file_name().to_string_lossy().into_owned(); 583 - let looks_like_ron = Path::new(&name) 722 + let stale = Path::new(&name) 584 723 .extension() 585 - .is_some_and(|ext| ext.eq_ignore_ascii_case("ron")); 586 - if looks_like_ron && !live_names.contains(&name) { 724 + .and_then(|ext| ext.to_str()) 725 + .is_some_and(&is_managed_ext) 726 + && !live_names.contains(&name); 727 + if stale { 587 728 let path = entry.path(); 588 729 fs::remove_file(&path).map_err(|source| FolderErrorKind::Io { path, source }.wrap())?; 589 730 Ok::<_, FolderError>(true) ··· 646 787 header.feature_tree.feature_of_extrude(orphan).is_none(), 647 788 "the orphan starts with no feature-tree node" 648 789 ); 649 - let doc = Document::from_parts(header, BTreeMap::new()); 790 + let doc = Document::from_parts(header, BTreeMap::new(), BTreeMap::new()); 650 791 651 792 let Ok(()) = save(&doc, &folder) else { 652 793 panic!("save");
+1 -1
crates/bone-document/src/lib.rs
··· 7 7 8 8 pub use document::{ 9 9 Document, DocumentHeader, DocumentParameters, ExtrudeFile, FeatureEdge, FeatureNode, 10 - FeatureTree, PrincipalPlane, RenameSketchError, SketchFile, SketchRegistry, 10 + FeatureTree, ImportedSolid, PrincipalPlane, RenameSketchError, SketchFile, SketchRegistry, 11 11 SketchRegistryEntry, UnitsPreference, extrude_filename, sketch_filename, 12 12 }; 13 13 pub use evaluator::{