Another project
0

Configure Feed

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

at main 16 kB View raw
1use bone_kernel::{BrepFace, BrepSolid}; 2use bone_types::{ 3 BrepFaceId, EntityRef, FaceFingerprint, FaceLabel, FaceRef, FeatureId, Resolution, 4 SketchPlaneBasis, 5}; 6 7use crate::recompute::{EvaluatedModel, RebuildPass}; 8 9#[derive(Copy, Clone, Debug, PartialEq, Eq)] 10pub struct ResolvedFace { 11 body: FeatureId, 12 face: BrepFaceId, 13 built_at: RebuildPass, 14} 15 16impl ResolvedFace { 17 #[must_use] 18 pub const fn body(self) -> FeatureId { 19 self.body 20 } 21 22 #[must_use] 23 pub const fn face(self) -> BrepFaceId { 24 self.face 25 } 26} 27 28#[derive(Copy, Clone, Debug, PartialEq, Eq)] 29pub enum ResolvedEntity { 30 Face(ResolvedFace), 31} 32 33#[must_use] 34pub fn resolve(model: &EvaluatedModel, reference: EntityRef) -> Resolution<ResolvedEntity> { 35 match reference { 36 EntityRef::Face(label, fingerprint) => { 37 resolve_face(model, label, fingerprint).map(ResolvedEntity::Face) 38 } 39 EntityRef::Edge(..) | EntityRef::Vertex(..) => Resolution::Dangling { 40 last_known: reference, 41 }, 42 } 43} 44 45#[must_use] 46pub fn resolve_face( 47 model: &EvaluatedModel, 48 label: FaceLabel, 49 fingerprint: FaceFingerprint, 50) -> Resolution<ResolvedFace> { 51 let dangling = Resolution::Dangling { 52 last_known: EntityRef::Face(label, fingerprint), 53 }; 54 let (Some(solid), Some(built_at)) = (model.body(label.feature), model.built_at(label.feature)) 55 else { 56 return dangling; 57 }; 58 match unique_face(solid, label) { 59 Some(face) => Resolution::Resolved(ResolvedFace { 60 body: label.feature, 61 face: face.id(), 62 built_at, 63 }), 64 None => dangling, 65 } 66} 67 68#[must_use] 69pub(crate) fn unique_face(solid: &BrepSolid, label: FaceLabel) -> Option<&BrepFace> { 70 let mut matches = solid.iter_faces().filter(|face| face.label() == label); 71 let first = matches.next()?; 72 if matches.next().is_some() { 73 debug_assert!(false, "face label {label} matched multiple faces"); 74 return None; 75 } 76 Some(first) 77} 78 79impl EvaluatedModel { 80 #[must_use] 81 pub fn face(&self, resolved: ResolvedFace) -> Option<&BrepFace> { 82 (self.built_at(resolved.body) == Some(resolved.built_at)) 83 .then(|| self.body(resolved.body)) 84 .flatten() 85 .and_then(|solid| solid.iter_faces().find(|face| face.id() == resolved.face)) 86 } 87 88 #[must_use] 89 pub fn face_ref(&self, resolved: ResolvedFace) -> Option<FaceRef> { 90 let label = self.face(resolved)?.label(); 91 let fingerprint = self.body(resolved.body)?.face_fingerprint(resolved.face)?; 92 Some(FaceRef::new(label, fingerprint)) 93 } 94 95 #[must_use] 96 pub fn face_ref_at(&self, body: FeatureId, face: BrepFaceId) -> Option<FaceRef> { 97 let solid = self.body(body)?; 98 let label = solid 99 .iter_faces() 100 .find(|candidate| candidate.id() == face)? 101 .label(); 102 let fingerprint = solid.face_fingerprint(face)?; 103 Some(FaceRef::new(label, fingerprint)) 104 } 105 106 #[must_use] 107 pub fn face_ref_any(&self, face: BrepFaceId) -> Option<FaceRef> { 108 self.bodies() 109 .find_map(|(body, _)| self.face_ref_at(body, face)) 110 } 111 112 #[must_use] 113 pub fn face_for_sketch(&self, face: BrepFaceId) -> Option<(FaceRef, SketchPlaneBasis)> { 114 self.bodies().find_map(|(body, solid)| { 115 let basis = solid.face_plane_basis(face)?; 116 let face_ref = self.face_ref_at(body, face)?; 117 Some((face_ref, basis)) 118 }) 119 } 120} 121 122#[cfg(test)] 123mod tests { 124 use std::collections::BTreeSet; 125 126 use bone_kernel::BrepFace; 127 use bone_types::{ 128 DocumentId, EdgeFingerprint, EdgeLabel, EdgeRole, EntityRef, FaceFingerprint, FaceLabel, 129 FaceRole, FeatureId, Length, LoopIndex, Plane3, Point3, Resolution, RollbackMarker, 130 UnitVec3, millimeter, 131 }; 132 use proptest::prelude::*; 133 134 use super::{ResolvedEntity, ResolvedFace, resolve, resolve_face}; 135 use crate::document::Document; 136 use crate::recompute::{EvaluatedModel, RecomputeScope}; 137 use crate::sketch::{DimensionValue, SketchEdit}; 138 use crate::test_support::{ 139 blind_extrude, chain_handles, circle_with_radius_dim, full_model, push_chain, 140 }; 141 142 fn placeholder_fingerprint() -> FaceFingerprint { 143 FaceFingerprint { 144 plane: Plane3::new_unchecked(Point3::origin(), UnitVec3::x_axis(), UnitVec3::y_axis()), 145 centroid: Point3::origin(), 146 } 147 } 148 149 fn face_labels(model: &EvaluatedModel, body: FeatureId) -> Vec<FaceLabel> { 150 let Some(solid) = model.body(body) else { 151 panic!("body is built"); 152 }; 153 solid.iter_faces().map(BrepFace::label).collect() 154 } 155 156 fn resolved_label(model: &EvaluatedModel, resolved: ResolvedFace) -> FaceLabel { 157 let Some(face) = model.face(resolved) else { 158 panic!("a resolved face is present in its model"); 159 }; 160 face.label() 161 } 162 163 #[test] 164 fn face_ref_at_round_trips_a_picked_face() { 165 let mut document = Document::new(DocumentId::default(), "doc".to_owned()); 166 let chain = push_chain(&mut document, "Body", 6.0, 4.0); 167 let model = full_model(&document); 168 let Some(label) = face_labels(&model, chain.extrude).into_iter().next() else { 169 panic!("the body has faces"); 170 }; 171 let Resolution::Resolved(resolved) = resolve_face(&model, label, placeholder_fingerprint()) 172 else { 173 panic!("the label resolves on its own model"); 174 }; 175 let Some(face_ref) = model.face_ref_at(resolved.body(), resolved.face()) else { 176 panic!("a picked face yields a face reference"); 177 }; 178 let Resolution::Resolved(ResolvedEntity::Face(again)) = 179 resolve(&model, face_ref.entity_ref()) 180 else { 181 panic!("the reattach reference resolves back to a face"); 182 }; 183 assert_eq!( 184 again.face(), 185 resolved.face(), 186 "reattach targets the same face" 187 ); 188 assert_eq!( 189 model 190 .face_ref_any(resolved.face()) 191 .map(|reference| reference.label), 192 Some(face_ref.label), 193 ); 194 } 195 196 #[test] 197 fn face_for_sketch_yields_a_basis_for_a_planar_cap() { 198 let mut document = Document::new(DocumentId::default(), "doc".to_owned()); 199 let chain = push_chain(&mut document, "Body", 6.0, 4.0); 200 let model = full_model(&document); 201 let Some(cap) = face_labels(&model, chain.extrude) 202 .into_iter() 203 .find(|label| matches!(label.role, FaceRole::EndCap)) 204 else { 205 panic!("the cylinder has a planar cap"); 206 }; 207 let Resolution::Resolved(resolved) = resolve_face(&model, cap, placeholder_fingerprint()) 208 else { 209 panic!("the cap resolves on its own model"); 210 }; 211 assert!( 212 model.face_for_sketch(resolved.face()).is_some(), 213 "a planar cap yields a face reference and a sketch basis", 214 ); 215 } 216 217 proptest! { 218 #[test] 219 fn a_surviving_ref_resolves_and_a_deleted_target_dangles( 220 kept_radius in 2.0f64..18.0, 221 doomed_radius in 2.0f64..18.0, 222 ) { 223 let mut document = Document::new(DocumentId::default(), "doc".to_owned()); 224 let kept = push_chain(&mut document, "Keep", kept_radius, 4.0); 225 let doomed = push_chain(&mut document, "Drop", doomed_radius, 4.0); 226 let mut model = full_model(&document); 227 228 let Some(survivor) = face_labels(&model, kept.extrude).into_iter().next() else { 229 return Err(TestCaseError::fail("the kept body has faces")); 230 }; 231 let Some(lost) = face_labels(&model, doomed.extrude).into_iter().next() else { 232 return Err(TestCaseError::fail("the doomed body has faces")); 233 }; 234 235 document.remove_extrude(doomed.extrude_id); 236 model.recompute( 237 &document, 238 &BTreeSet::new(), 239 RollbackMarker::AtEnd, 240 RecomputeScope::Full, 241 ); 242 243 let Resolution::Resolved(resolved) = 244 resolve_face(&model, survivor, placeholder_fingerprint()) 245 else { 246 return Err(TestCaseError::fail(format!( 247 "{survivor} must survive a sibling deletion" 248 ))); 249 }; 250 prop_assert_eq!(resolved.body(), kept.extrude); 251 prop_assert_eq!(resolved_label(&model, resolved), survivor); 252 253 prop_assert_eq!( 254 resolve_face(&model, lost, placeholder_fingerprint()), 255 Resolution::Dangling { 256 last_known: EntityRef::Face(lost, placeholder_fingerprint()), 257 }, 258 "a deleted target dangles with its last-known reference", 259 ); 260 } 261 } 262 263 #[test] 264 fn a_no_match_label_never_binds_to_a_nearest_face() { 265 let mut document = Document::new(DocumentId::default(), "doc".to_owned()); 266 let sketch_id = document.allocate_sketch(); 267 let (sketch, circle, _dim) = circle_with_radius_dim(5.0); 268 document.insert_sketch(sketch_id, "Sketch1".to_owned(), sketch); 269 let extrude_id = document.commit_extrude(blind_extrude(sketch_id, 4.0)); 270 let chain = chain_handles(&document, sketch_id, extrude_id); 271 let model = full_model(&document); 272 273 let absent = FaceLabel { 274 feature: chain.extrude, 275 role: FaceRole::Side { 276 loop_index: LoopIndex::new(u16::MAX), 277 from: circle, 278 }, 279 }; 280 let resolution = resolve_face(&model, absent, placeholder_fingerprint()); 281 assert!(resolution.is_dangling()); 282 assert_eq!(resolution.id(), None, "no nearest-guess id is returned"); 283 } 284 285 #[test] 286 fn resolve_routes_a_face_ref_and_dangles_unimplemented_kinds() { 287 let mut document = Document::new(DocumentId::default(), "doc".to_owned()); 288 let sketch_id = document.allocate_sketch(); 289 let (sketch, circle, _dim) = circle_with_radius_dim(5.0); 290 document.insert_sketch(sketch_id, "Sketch1".to_owned(), sketch); 291 let extrude_id = document.commit_extrude(blind_extrude(sketch_id, 4.0)); 292 let chain = chain_handles(&document, sketch_id, extrude_id); 293 let model = full_model(&document); 294 295 let Some(label) = face_labels(&model, chain.extrude).into_iter().next() else { 296 panic!("the body has faces"); 297 }; 298 let Resolution::Resolved(ResolvedEntity::Face(resolved)) = 299 resolve(&model, EntityRef::Face(label, placeholder_fingerprint())) 300 else { 301 panic!("a face reference resolves through the dispatcher"); 302 }; 303 assert_eq!(resolved.body(), chain.extrude); 304 305 let edge_ref = EntityRef::Edge( 306 EdgeLabel { 307 feature: chain.extrude, 308 role: EdgeRole::StartCapEdge { from: circle }, 309 }, 310 EdgeFingerprint { 311 sample: Point3::origin(), 312 direction: UnitVec3::x_axis(), 313 }, 314 ); 315 assert!( 316 resolve(&model, edge_ref).is_dangling(), 317 "edge resolution is unimplemented, so it dangles rather than guessing", 318 ); 319 } 320 321 #[test] 322 fn a_resolved_face_goes_stale_when_its_body_rebuilds() { 323 let mut document = Document::new(DocumentId::default(), "doc".to_owned()); 324 let sketch_id = document.allocate_sketch(); 325 let (sketch, _circle, radius_dim) = circle_with_radius_dim(5.0); 326 document.insert_sketch(sketch_id, "Sketch1".to_owned(), sketch); 327 let extrude_id = document.commit_extrude(blind_extrude(sketch_id, 4.0)); 328 let chain = chain_handles(&document, sketch_id, extrude_id); 329 let mut model = full_model(&document); 330 331 let Some(label) = face_labels(&model, chain.extrude).into_iter().next() else { 332 panic!("the body has faces"); 333 }; 334 let Resolution::Resolved(stale) = resolve_face(&model, label, placeholder_fingerprint()) 335 else { 336 panic!("the face resolves before the edit"); 337 }; 338 assert!( 339 model.face(stale).is_some(), 340 "a fresh handle reads back on the model that produced it", 341 ); 342 343 let Some(original) = document.sketch(sketch_id).cloned() else { 344 panic!("the sketch is present"); 345 }; 346 let Ok((edited, _)) = original.apply(SketchEdit::UpdateDimensionValue { 347 id: radius_dim, 348 value: DimensionValue::Length(Length::new::<millimeter>(9.0)), 349 }) else { 350 panic!("the driving radius dimension accepts a new value"); 351 }; 352 document.replace_sketch(sketch_id, edited); 353 model.recompute( 354 &document, 355 &BTreeSet::new(), 356 RollbackMarker::AtEnd, 357 RecomputeScope::Edited(chain.sketch), 358 ); 359 360 assert!( 361 model.face(stale).is_none(), 362 "a handle from before the rebuild must not read a face out of the new arena", 363 ); 364 assert!( 365 resolve_face(&model, label, placeholder_fingerprint()) 366 .id() 367 .is_some(), 368 "re-resolving against the rebuilt model yields a usable handle", 369 ); 370 } 371 372 proptest! { 373 #[test] 374 fn a_dimension_edit_keeps_every_face_ref_resolved( 375 radius in 2.0f64..18.0, 376 edited in 2.0f64..18.0, 377 ) { 378 let mut document = Document::new(DocumentId::default(), "doc".to_owned()); 379 let sketch_id = document.allocate_sketch(); 380 let (sketch, _circle, radius_dim) = circle_with_radius_dim(radius); 381 document.insert_sketch(sketch_id, "Sketch1".to_owned(), sketch); 382 let extrude_id = document.commit_extrude(blind_extrude(sketch_id, 4.0)); 383 let chain = chain_handles(&document, sketch_id, extrude_id); 384 let mut model = full_model(&document); 385 386 let labels = face_labels(&model, chain.extrude); 387 388 let Some(original) = document.sketch(sketch_id).cloned() else { 389 prop_assert!(false, "the sketch is present"); 390 return Ok(()); 391 }; 392 let Ok((edited_sketch, _)) = original.apply(SketchEdit::UpdateDimensionValue { 393 id: radius_dim, 394 value: DimensionValue::Length(Length::new::<millimeter>(edited)), 395 }) else { 396 prop_assert!(false, "a driving radius dimension accepts a new value"); 397 return Ok(()); 398 }; 399 document.replace_sketch(sketch_id, edited_sketch); 400 model.recompute( 401 &document, 402 &BTreeSet::new(), 403 RollbackMarker::AtEnd, 404 RecomputeScope::Edited(chain.sketch), 405 ); 406 407 let unresolved: Vec<FaceLabel> = labels 408 .iter() 409 .copied() 410 .filter(|&label| { 411 match resolve_face(&model, label, placeholder_fingerprint()) { 412 Resolution::Resolved(resolved) => resolved_label(&model, resolved) != label, 413 Resolution::Repaired { .. } | Resolution::Dangling { .. } => true, 414 } 415 }) 416 .collect(); 417 prop_assert!( 418 unresolved.is_empty(), 419 "every face ref must resolve to its identical label across a dimension edit, failed: {unresolved:?}", 420 ); 421 } 422 } 423}