Another project
1use bone_document::{
2 DimensionKind, DimensionValue, Document, EditOutcome, EvaluatedSketch, FeatureNode, Sketch,
3 SketchDimension, SketchEdit, SketchEntity, SketchRelation, evaluate_extrude, evaluate_sketch,
4};
5use bone_kernel::{
6 BrepEdge, BrepFace, BrepSolid, ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature,
7 ExtrudeSense, MergeResult,
8};
9use bone_types::{
10 DocumentId, EdgeLabel, FaceLabel, FeatureId, Length, Point2, Point3, PositiveLength,
11 SketchDimensionId, SketchId, SketchPlaneBasis, Tolerance, UnitVec3, millimeter,
12};
13use slotmap::KeyData;
14
15const TOL: Tolerance = Tolerance::new(1e-9);
16
17fn plane() -> SketchPlaneBasis {
18 let Ok(basis) = SketchPlaneBasis::new(
19 Point3::origin(),
20 UnitVec3::x_axis(),
21 UnitVec3::y_axis(),
22 Tolerance::new(1e-9),
23 ) else {
24 panic!("xy plane");
25 };
26 basis
27}
28
29fn mm(v: f64) -> Length {
30 Length::new::<millimeter>(v)
31}
32
33fn sketch_id(idx: u32) -> SketchId {
34 SketchId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
35}
36
37fn document_id(idx: u32) -> DocumentId {
38 DocumentId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
39}
40
41fn feature_id(idx: u32) -> FeatureId {
42 FeatureId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
43}
44
45fn horizontal_line_sketch(dim_mm: f64) -> Sketch {
46 let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity(
47 SketchEntity::point(Point2::from_mm(0.0, 0.0)),
48 )) else {
49 panic!("a");
50 };
51 let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
52 Point2::from_mm(1.0, 0.0),
53 ))) else {
54 panic!("b");
55 };
56 let Ok((s, EditOutcome::Entity(line))) =
57 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
58 else {
59 panic!("line");
60 };
61 let Ok((s, _)) = s.apply(SketchEdit::AddRelation(SketchRelation::Fix(a))) else {
62 panic!("fix");
63 };
64 let Ok((s, _)) = s.apply(SketchEdit::AddRelation(SketchRelation::Horizontal(line))) else {
65 panic!("horizontal");
66 };
67 let Ok((s, _)) = s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
68 a,
69 b,
70 value: mm(dim_mm),
71 kind: DimensionKind::Driving,
72 })) else {
73 panic!("dim");
74 };
75 s
76}
77
78fn conflicting_sketch() -> Sketch {
79 let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity(
80 SketchEntity::point(Point2::from_mm(0.0, 0.0)),
81 )) else {
82 panic!("a");
83 };
84 let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
85 Point2::from_mm(1.0, 0.0),
86 ))) else {
87 panic!("b");
88 };
89 let Ok((s, _)) = s.apply(SketchEdit::AddRelation(SketchRelation::Fix(a))) else {
90 panic!("fix");
91 };
92 let Ok((s, _)) = s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
93 a,
94 b,
95 value: mm(5.0),
96 kind: DimensionKind::Driving,
97 })) else {
98 panic!("dim1");
99 };
100 let Ok((s, _)) = s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
101 a,
102 b,
103 value: mm(10.0),
104 kind: DimensionKind::Driving,
105 })) else {
106 panic!("dim2");
107 };
108 s
109}
110
111#[test]
112fn evaluate_sketch_returns_solved_variant_on_success() {
113 let sketch = horizontal_line_sketch(10.0);
114 let output = evaluate_sketch(&sketch);
115 let EvaluatedSketch::Solved(solved) = output else {
116 panic!("expected Solved, got {output:?}");
117 };
118 let order = solved.entity_order();
119 let b = order[1];
120 let SketchEntity::Point(p) = solved.entities()[b] else {
121 panic!("b is point");
122 };
123 let (x, y) = p.at().coords_mm();
124 assert!((x - 10.0).abs() < 1e-9, "x = {x}");
125 assert!(y.abs() < 1e-9, "y = {y}");
126}
127
128#[test]
129fn evaluate_sketch_returns_failed_variant_on_conflict() {
130 let sketch = conflicting_sketch();
131 let output = evaluate_sketch(&sketch);
132 assert!(
133 matches!(output, EvaluatedSketch::Failed(_)),
134 "expected Failed, got {output:?}"
135 );
136}
137
138#[test]
139fn document_resolves_feature_id_back_to_sketch() {
140 let mut doc = Document::new(document_id(1), "d".to_owned());
141 let sid = sketch_id(7);
142 doc.insert_sketch(sid, "S".to_owned(), horizontal_line_sketch(10.0));
143 let feature = doc.feature_tree().iter().find_map(|(fid, node)| {
144 matches!(node, FeatureNode::Sketch(id) if id == sid).then_some(fid)
145 });
146 let Some(fid) = feature else {
147 panic!("sketch feature present");
148 };
149 let Some(sketch) = doc.sketch_of_feature(fid) else {
150 panic!("sketch addressable through feature id");
151 };
152 let Some(direct) = doc.sketch(sid) else {
153 panic!("sketch in map");
154 };
155 assert_eq!(sketch, direct);
156}
157
158#[test]
159fn removed_feature_id_is_never_reused() {
160 let mut doc = Document::new(document_id(1), "d".to_owned());
161 let sid_a = sketch_id(10);
162 let sid_b = sketch_id(11);
163 doc.insert_sketch(sid_a, "A".to_owned(), horizontal_line_sketch(10.0));
164 let Some(fid_a) = doc.feature_tree().iter().find_map(|(fid, node)| {
165 matches!(node, FeatureNode::Sketch(id) if id == sid_a).then_some(fid)
166 }) else {
167 panic!("A feature present");
168 };
169
170 let Some(sketch_a) = doc.sketch(sid_a) else {
171 panic!("A in map");
172 };
173 let EvaluatedSketch::Solved(solved_a) = evaluate_sketch(sketch_a) else {
174 panic!("A solves");
175 };
176
177 doc.remove_sketch(sid_a);
178 doc.insert_sketch(sid_b, "B".to_owned(), horizontal_line_sketch(20.0));
179 let Some(fid_b) = doc.feature_tree().iter().find_map(|(fid, node)| {
180 matches!(node, FeatureNode::Sketch(id) if id == sid_b).then_some(fid)
181 }) else {
182 panic!("B feature present");
183 };
184 assert_ne!(
185 fid_a, fid_b,
186 "a removed feature id is never handed to a later feature"
187 );
188
189 let Some(sketch_b) = doc.sketch(sid_b) else {
190 panic!("B in map");
191 };
192 let EvaluatedSketch::Solved(solved_b) = evaluate_sketch(sketch_b) else {
193 panic!("B solves");
194 };
195 assert_ne!(
196 solved_a, solved_b,
197 "distinct feature ids cache distinct geometry"
198 );
199}
200
201fn dimensioned_rectangle(width_mm: f64, height_mm: f64) -> (Sketch, SketchDimensionId) {
202 let Ok((with_points, points)) = Sketch::new(plane()).apply_all(vec![
203 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))),
204 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(7.0, 0.5))),
205 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(7.5, 3.2))),
206 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.3, 2.9))),
207 ]) else {
208 panic!("corners");
209 };
210 let [c0, c1, c2, c3] = [0, 1, 2, 3].map(|i| entity_of(points[i]));
211 let Ok((with_edges, edges)) = with_points.apply_all(vec![
212 SketchEdit::AddEntity(SketchEntity::line(c0, c1, false)),
213 SketchEdit::AddEntity(SketchEntity::line(c1, c2, false)),
214 SketchEdit::AddEntity(SketchEntity::line(c2, c3, false)),
215 SketchEdit::AddEntity(SketchEntity::line(c3, c0, false)),
216 ]) else {
217 panic!("edges");
218 };
219 let [bottom, right, top, left] = [0, 1, 2, 3].map(|i| entity_of(edges[i]));
220 let Ok((constrained, outcomes)) = with_edges.apply_all(vec![
221 SketchEdit::AddRelation(SketchRelation::Horizontal(bottom)),
222 SketchEdit::AddRelation(SketchRelation::Horizontal(top)),
223 SketchEdit::AddRelation(SketchRelation::Vertical(right)),
224 SketchEdit::AddRelation(SketchRelation::Vertical(left)),
225 SketchEdit::AddRelation(SketchRelation::Fix(c0)),
226 SketchEdit::AddDimension(SketchDimension::Linear {
227 a: c0,
228 b: c1,
229 value: mm(width_mm),
230 kind: DimensionKind::Driving,
231 }),
232 SketchEdit::AddDimension(SketchDimension::Linear {
233 a: c1,
234 b: c2,
235 value: mm(height_mm),
236 kind: DimensionKind::Driving,
237 }),
238 ]) else {
239 panic!("constraints");
240 };
241 let Some(width_dim) = outcomes.iter().find_map(|outcome| match outcome {
242 EditOutcome::Dimension(id) => Some(*id),
243 _ => None,
244 }) else {
245 panic!("width dimension allocated");
246 };
247 (constrained, width_dim)
248}
249
250fn entity_of(outcome: EditOutcome) -> bone_types::SketchEntityId {
251 let EditOutcome::Entity(id) = outcome else {
252 panic!("expected an entity outcome");
253 };
254 id
255}
256
257fn blind(depth_mm: f64) -> ExtrudeFeature {
258 let Ok(depth) = PositiveLength::new(mm(depth_mm)) else {
259 panic!("{depth_mm} mm is positive");
260 };
261 ExtrudeFeature {
262 sketch: SketchId::default(),
263 direction: ExtrudeDirection::Normal {
264 sense: ExtrudeSense::Forward,
265 },
266 end_condition: ExtrudeEndCondition::Blind { depth },
267 draft: None,
268 thin_wall: None,
269 merge_result: MergeResult::Merge,
270 }
271}
272
273fn face_labels(solid: &BrepSolid) -> Vec<FaceLabel> {
274 solid.iter_faces().map(BrepFace::label).collect()
275}
276
277fn edge_labels(solid: &BrepSolid) -> Vec<EdgeLabel> {
278 solid.iter_edges().map(BrepEdge::label).collect()
279}
280
281#[test]
282fn evaluate_extrude_reports_failed_sketch() {
283 let extrude = evaluate_extrude(
284 feature_id(9),
285 &evaluate_sketch(&conflicting_sketch()),
286 &blind(5.0),
287 );
288 assert!(extrude.result().is_err());
289 assert!(extrude.solid().is_none());
290 assert!(extrude.generation().is_none());
291}
292
293proptest::proptest! {
294 #[test]
295 fn extrude_re_evaluates_under_edited_width(width_mm in 2.0f64..40.0) {
296 let extrude_feature = feature_id(42);
297 let feature = blind(10.0);
298 let (base, width_dim) = dimensioned_rectangle(10.0, 5.0);
299 let baseline = evaluate_extrude(extrude_feature, &evaluate_sketch(&base), &feature);
300
301 let Ok((edited, _)) = base.apply(SketchEdit::UpdateDimensionValue {
302 id: width_dim,
303 value: DimensionValue::Length(mm(width_mm)),
304 }) else {
305 panic!("width edit applies");
306 };
307 let widened = evaluate_extrude(extrude_feature, &evaluate_sketch(&edited), &feature);
308
309 let (Some(before), Some(after)) = (baseline.solid(), widened.solid()) else {
310 panic!("both widths extrude");
311 };
312 proptest::prop_assert_eq!(face_labels(before), face_labels(after));
313 proptest::prop_assert_eq!(edge_labels(before), edge_labels(after));
314 proptest::prop_assert!(after.validate(TOL).is_ok());
315 let Some(bbox) = after.bounding_box() else {
316 panic!("solid has a bounding box");
317 };
318 let (dx, _, _) = bbox.extent().coords_mm();
319 proptest::prop_assert!((dx - width_mm).abs() < 1e-6, "x-extent {} tracks width {}", dx, width_mm);
320 }
321}