Another project
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}