Another project
1use bone_document::{
2 DimensionKind, Document, DocumentFolder, Sketch, SketchDimension, SketchEdit, SketchEntity,
3 SketchEntityKind, SketchParameter, SketchRelation, load, save,
4};
5use bone_kernel::{
6 ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense, MergeResult,
7};
8use bone_types::{
9 Angle, DocumentId, ExtrudeId, Length, Parameter, Point2, Point3, PositiveLength, SketchId,
10 SketchPlaneBasis, Tolerance, UnitVec3, degree, millimeter,
11};
12use proptest::prelude::*;
13use slotmap::KeyData;
14use tempfile::tempdir;
15
16fn plane() -> SketchPlaneBasis {
17 let Ok(basis) = SketchPlaneBasis::new(
18 Point3::origin(),
19 UnitVec3::x_axis(),
20 UnitVec3::y_axis(),
21 Tolerance::new(1e-9),
22 ) else {
23 panic!("xy plane");
24 };
25 basis
26}
27
28fn mm(v: f64) -> Length {
29 Length::new::<millimeter>(v)
30}
31
32fn deg(v: f64) -> Angle {
33 Angle::new::<degree>(v)
34}
35
36fn sketch_id(idx: u32) -> SketchId {
37 SketchId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
38}
39
40fn document_id(idx: u32) -> DocumentId {
41 DocumentId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
42}
43
44fn extrude_id(idx: u32) -> ExtrudeId {
45 ExtrudeId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
46}
47
48#[derive(Copy, Clone, Debug)]
49enum Step {
50 Point(i16, i16),
51 Parameter(i16),
52 Line {
53 ai: u8,
54 bi: u8,
55 cons: bool,
56 },
57 Circle {
58 ci: u8,
59 r: u16,
60 cons: bool,
61 },
62 Arc {
63 ci: u8,
64 si: u8,
65 ei: u8,
66 },
67 Coincident {
68 pi: u8,
69 xi: u8,
70 },
71 Horizontal {
72 li: u8,
73 },
74 Vertical {
75 li: u8,
76 },
77 Parallel {
78 ai: u8,
79 bi: u8,
80 },
81 Perpendicular {
82 ai: u8,
83 bi: u8,
84 },
85 Tangent {
86 ai: u8,
87 bi: u8,
88 },
89 EqualLines {
90 ai: u8,
91 bi: u8,
92 },
93 EqualRounds {
94 ai: u8,
95 bi: u8,
96 },
97 Concentric {
98 ai: u8,
99 bi: u8,
100 },
101 Midpoint {
102 pi: u8,
103 li: u8,
104 },
105 Fix {
106 ei: u8,
107 },
108 LinearDim {
109 ai: u8,
110 bi: u8,
111 v: u16,
112 driven: bool,
113 },
114 RadiusDim {
115 ti: u8,
116 v: u16,
117 driven: bool,
118 },
119 DiameterDim {
120 ti: u8,
121 v: u16,
122 driven: bool,
123 },
124 AngularDim {
125 ai: u8,
126 bi: u8,
127 deg: u16,
128 },
129 ToggleConstruction {
130 ei: u8,
131 },
132}
133
134fn arb_step() -> impl Strategy<Value = Step> {
135 prop_oneof![
136 (any::<i16>(), any::<i16>()).prop_map(|(x, y)| Step::Point(x, y)),
137 any::<i16>().prop_map(Step::Parameter),
138 (any::<u8>(), any::<u8>(), any::<bool>()).prop_map(|(ai, bi, cons)| Step::Line {
139 ai,
140 bi,
141 cons
142 }),
143 (any::<u8>(), any::<u16>(), any::<bool>()).prop_map(|(ci, r, cons)| Step::Circle {
144 ci,
145 r: r % 200 + 1,
146 cons
147 }),
148 (any::<u8>(), any::<u8>(), any::<u8>()).prop_map(|(ci, si, ei)| Step::Arc { ci, si, ei }),
149 (any::<u8>(), any::<u8>()).prop_map(|(pi, xi)| Step::Coincident { pi, xi }),
150 any::<u8>().prop_map(|li| Step::Horizontal { li }),
151 any::<u8>().prop_map(|li| Step::Vertical { li }),
152 (any::<u8>(), any::<u8>()).prop_map(|(ai, bi)| Step::Parallel { ai, bi }),
153 (any::<u8>(), any::<u8>()).prop_map(|(ai, bi)| Step::Perpendicular { ai, bi }),
154 (any::<u8>(), any::<u8>()).prop_map(|(ai, bi)| Step::Tangent { ai, bi }),
155 (any::<u8>(), any::<u8>()).prop_map(|(ai, bi)| Step::EqualLines { ai, bi }),
156 (any::<u8>(), any::<u8>()).prop_map(|(ai, bi)| Step::EqualRounds { ai, bi }),
157 (any::<u8>(), any::<u8>()).prop_map(|(ai, bi)| Step::Concentric { ai, bi }),
158 (any::<u8>(), any::<u8>()).prop_map(|(pi, li)| Step::Midpoint { pi, li }),
159 any::<u8>().prop_map(|ei| Step::Fix { ei }),
160 (any::<u8>(), any::<u8>(), any::<u16>(), any::<bool>())
161 .prop_map(|(ai, bi, v, driven)| Step::LinearDim { ai, bi, v, driven }),
162 (any::<u8>(), any::<u16>(), any::<bool>()).prop_map(|(ti, v, driven)| Step::RadiusDim {
163 ti,
164 v: v % 200 + 1,
165 driven
166 }),
167 (any::<u8>(), any::<u16>(), any::<bool>()).prop_map(|(ti, v, driven)| Step::DiameterDim {
168 ti,
169 v: v % 200 + 1,
170 driven
171 }),
172 (any::<u8>(), any::<u8>(), any::<u16>()).prop_map(|(ai, bi, deg)| Step::AngularDim {
173 ai,
174 bi,
175 deg: deg % 359 + 1
176 }),
177 any::<u8>().prop_map(|ei| Step::ToggleConstruction { ei }),
178 ]
179}
180
181fn entities_of_kind(s: &Sketch, kind: SketchEntityKind) -> Vec<bone_types::SketchEntityId> {
182 s.entity_order()
183 .iter()
184 .copied()
185 .filter(|id| s.entities()[*id].kind() == kind)
186 .collect()
187}
188
189fn pick<T: Copy>(xs: &[T], i: u8) -> Option<T> {
190 if xs.is_empty() {
191 None
192 } else {
193 Some(xs[usize::from(i) % xs.len()])
194 }
195}
196
197fn pick_two_distinct<T: Copy + Eq>(xs: &[T], ai: u8, bi: u8) -> Option<(T, T)> {
198 if xs.len() < 2 {
199 return None;
200 }
201 let a = usize::from(ai) % xs.len();
202 let offset = usize::from(bi) % (xs.len() - 1) + 1;
203 let b = (a + offset) % xs.len();
204 Some((xs[a], xs[b]))
205}
206
207fn pick_three_distinct<T: Copy + Eq>(xs: &[T], ai: u8, bi: u8, ci: u8) -> Option<(T, T, T)> {
208 if xs.len() < 3 {
209 return None;
210 }
211 let a = usize::from(ai) % xs.len();
212 let b_off = usize::from(bi) % (xs.len() - 1) + 1;
213 let b = (a + b_off) % xs.len();
214 let remaining: Vec<_> = (0..xs.len()).filter(|&i| i != a && i != b).collect();
215 let c = remaining[usize::from(ci) % remaining.len()];
216 Some((xs[a], xs[b], xs[c]))
217}
218
219fn dim_kind(driven: bool) -> DimensionKind {
220 if driven {
221 DimensionKind::Driven
222 } else {
223 DimensionKind::Driving
224 }
225}
226
227fn rounds(s: &Sketch) -> Vec<bone_types::SketchEntityId> {
228 entities_of_kind(s, SketchEntityKind::Arc)
229 .into_iter()
230 .chain(entities_of_kind(s, SketchEntityKind::Circle))
231 .collect()
232}
233
234fn resolve(s: &Sketch, step: Step) -> Option<SketchEdit> {
235 match step {
236 Step::Point(..) | Step::Parameter(..) => Some(resolve_atom(step)),
237 Step::Line { .. } | Step::Circle { .. } | Step::Arc { .. } => resolve_entity(s, step),
238 Step::Coincident { .. }
239 | Step::Horizontal { .. }
240 | Step::Vertical { .. }
241 | Step::Parallel { .. }
242 | Step::Perpendicular { .. }
243 | Step::Tangent { .. }
244 | Step::EqualLines { .. }
245 | Step::EqualRounds { .. }
246 | Step::Concentric { .. }
247 | Step::Midpoint { .. }
248 | Step::Fix { .. } => resolve_relation(s, step),
249 Step::LinearDim { .. }
250 | Step::RadiusDim { .. }
251 | Step::DiameterDim { .. }
252 | Step::AngularDim { .. } => resolve_dimension(s, step),
253 Step::ToggleConstruction { ei } => {
254 let id = pick(s.entity_order(), ei)?;
255 (!s.entities()[id].is_point()).then_some(SketchEdit::SetConstruction {
256 id,
257 for_construction: true,
258 })
259 }
260 }
261}
262
263fn resolve_atom(step: Step) -> SketchEdit {
264 match step {
265 Step::Point(x, y) => SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(
266 f64::from(x) / 100.0,
267 f64::from(y) / 100.0,
268 ))),
269 Step::Parameter(v) => {
270 SketchEdit::AddParameter(SketchParameter::new(Parameter::new(f64::from(v) / 100.0)))
271 }
272 _ => unreachable!("caller routes only Point and Parameter here"),
273 }
274}
275
276fn resolve_entity(s: &Sketch, step: Step) -> Option<SketchEdit> {
277 match step {
278 Step::Line { ai, bi, cons } => {
279 let (a, b) = pick_two_distinct(&entities_of_kind(s, SketchEntityKind::Point), ai, bi)?;
280 Some(SketchEdit::AddEntity(SketchEntity::line(a, b, cons)))
281 }
282 Step::Circle { ci, r, cons } => {
283 let c = pick(&entities_of_kind(s, SketchEntityKind::Point), ci)?;
284 Some(SketchEdit::AddEntity(SketchEntity::circle(
285 c,
286 mm(f64::from(r)),
287 cons,
288 )))
289 }
290 Step::Arc { ci, si, ei } => {
291 let (c, start, end) =
292 pick_three_distinct(&entities_of_kind(s, SketchEntityKind::Point), ci, si, ei)?;
293 Some(SketchEdit::AddEntity(SketchEntity::arc(
294 c, start, end, false,
295 )))
296 }
297 _ => unreachable!("caller routes only Line, Circle, Arc here"),
298 }
299}
300
301fn resolve_relation(s: &Sketch, step: Step) -> Option<SketchEdit> {
302 let rel = match step {
303 Step::Coincident { pi, xi } => {
304 let p = pick(&entities_of_kind(s, SketchEntityKind::Point), pi)?;
305 let others: Vec<_> = s
306 .entity_order()
307 .iter()
308 .copied()
309 .filter(|id| *id != p)
310 .collect();
311 SketchRelation::Coincident(p, pick(&others, xi)?)
312 }
313 Step::Horizontal { li } => {
314 SketchRelation::Horizontal(pick(&entities_of_kind(s, SketchEntityKind::Line), li)?)
315 }
316 Step::Vertical { li } => {
317 SketchRelation::Vertical(pick(&entities_of_kind(s, SketchEntityKind::Line), li)?)
318 }
319 Step::Parallel { ai, bi } => {
320 let (a, b) = pick_two_distinct(&entities_of_kind(s, SketchEntityKind::Line), ai, bi)?;
321 SketchRelation::Parallel(a, b)
322 }
323 Step::Perpendicular { ai, bi } => {
324 let (a, b) = pick_two_distinct(&entities_of_kind(s, SketchEntityKind::Line), ai, bi)?;
325 SketchRelation::Perpendicular(a, b)
326 }
327 Step::Tangent { ai, bi } => {
328 let round = rounds(s);
329 let lines = entities_of_kind(s, SketchEntityKind::Line);
330 let a = pick(&round, ai)?;
331 let b = if lines.is_empty() {
332 let remaining: Vec<_> = round.iter().copied().filter(|id| *id != a).collect();
333 pick(&remaining, bi)?
334 } else {
335 pick(&lines, bi)?
336 };
337 SketchRelation::Tangent(a, b)
338 }
339 Step::EqualLines { ai, bi } => {
340 let (a, b) = pick_two_distinct(&entities_of_kind(s, SketchEntityKind::Line), ai, bi)?;
341 SketchRelation::Equal(a, b)
342 }
343 Step::EqualRounds { ai, bi } => {
344 let (a, b) = pick_two_distinct(&rounds(s), ai, bi)?;
345 SketchRelation::Equal(a, b)
346 }
347 Step::Concentric { ai, bi } => {
348 let (a, b) = pick_two_distinct(&rounds(s), ai, bi)?;
349 SketchRelation::Concentric(a, b)
350 }
351 Step::Midpoint { pi, li } => {
352 let point = pick(&entities_of_kind(s, SketchEntityKind::Point), pi)?;
353 let line = pick(&entities_of_kind(s, SketchEntityKind::Line), li)?;
354 SketchRelation::Midpoint { point, line }
355 }
356 Step::Fix { ei } => SketchRelation::Fix(pick(s.entity_order(), ei)?),
357 _ => unreachable!("caller routes only relations here"),
358 };
359 Some(SketchEdit::AddRelation(rel))
360}
361
362fn resolve_dimension(s: &Sketch, step: Step) -> Option<SketchEdit> {
363 let dim = match step {
364 Step::LinearDim { ai, bi, v, driven } => {
365 let (a, b) = pick_two_distinct(&entities_of_kind(s, SketchEntityKind::Point), ai, bi)?;
366 SketchDimension::Linear {
367 a,
368 b,
369 value: mm(f64::from(v)),
370 kind: dim_kind(driven),
371 }
372 }
373 Step::RadiusDim { ti, v, driven } => SketchDimension::Radius {
374 target: pick(&rounds(s), ti)?,
375 value: mm(f64::from(v)),
376 kind: dim_kind(driven),
377 },
378 Step::DiameterDim { ti, v, driven } => SketchDimension::Diameter {
379 target: pick(&rounds(s), ti)?,
380 value: mm(f64::from(v)),
381 kind: dim_kind(driven),
382 },
383 Step::AngularDim { ai, bi, deg: d } => {
384 let (a, b) = pick_two_distinct(&entities_of_kind(s, SketchEntityKind::Line), ai, bi)?;
385 SketchDimension::Angular {
386 a,
387 b,
388 value: deg(f64::from(d)),
389 kind: DimensionKind::Driving,
390 }
391 }
392 _ => unreachable!("caller routes only dimensions here"),
393 };
394 Some(SketchEdit::AddDimension(dim))
395}
396
397fn build(steps: Vec<Step>) -> Sketch {
398 steps.into_iter().fold(Sketch::new(plane()), |sk, step| {
399 resolve(&sk, step)
400 .and_then(|edit| sk.clone().apply(edit).ok().map(|(s, _)| s))
401 .unwrap_or(sk)
402 })
403}
404
405proptest! {
406 #![proptest_config(ProptestConfig {
407 cases: 32,
408 .. ProptestConfig::default()
409 })]
410
411 #[test]
412 fn load_save_roundtrip_preserves_sketch(steps in prop::collection::vec(arb_step(), 0..30)) {
413 let sketch = build(steps);
414 let dir = tempdir().map_err(|e| TestCaseError::Fail(format!("tempdir: {e}").into()))?;
415 let folder = DocumentFolder::new(dir.path().join("fuzz.bone"));
416 let mut doc = Document::new(document_id(1), "fuzz".to_owned());
417 doc.insert_sketch(sketch_id(1), "S".to_owned(), sketch.clone());
418 save(&doc, &folder).map_err(|e| TestCaseError::Fail(format!("save: {e}").into()))?;
419 let loaded = load(&folder).map_err(|e| TestCaseError::Fail(format!("load: {e}").into()))?;
420 let Some(round) = loaded.sketch(sketch_id(1)) else {
421 return Err(TestCaseError::Fail("sketch missing".into()));
422 };
423 prop_assert_eq!(round, &sketch);
424 }
425
426 #[test]
427 fn sketch_plus_extrude_roundtrips_whole_document(
428 steps in prop::collection::vec(arb_step(), 0..30),
429 depth_mm in 1u16..500,
430 ) {
431 let sketch = build(steps);
432 let dir = tempdir().map_err(|e| TestCaseError::Fail(format!("tempdir: {e}").into()))?;
433 let folder = DocumentFolder::new(dir.path().join("ext.bone"));
434 let mut doc = Document::new(document_id(1), "ext".to_owned());
435 doc.insert_sketch(sketch_id(1), "S".to_owned(), sketch);
436 let depth = PositiveLength::new(mm(f64::from(depth_mm)))
437 .map_err(|e| TestCaseError::Fail(format!("depth: {e}").into()))?;
438 doc.insert_extrude(
439 extrude_id(1),
440 ExtrudeFeature {
441 sketch: sketch_id(1),
442 direction: ExtrudeDirection::Normal {
443 sense: ExtrudeSense::Forward,
444 },
445 end_condition: ExtrudeEndCondition::Blind { depth },
446 draft: None,
447 thin_wall: None,
448 merge_result: MergeResult::Merge,
449 },
450 );
451 save(&doc, &folder).map_err(|e| TestCaseError::Fail(format!("save: {e}").into()))?;
452 let loaded = load(&folder).map_err(|e| TestCaseError::Fail(format!("load: {e}").into()))?;
453 prop_assert_eq!(loaded, doc);
454 }
455
456 #[test]
457 fn double_save_is_bit_identical(steps in prop::collection::vec(arb_step(), 0..30)) {
458 let sketch = build(steps);
459 let dir = tempdir().map_err(|e| TestCaseError::Fail(format!("tempdir: {e}").into()))?;
460 let folder = DocumentFolder::new(dir.path().join("det.bone"));
461 let mut doc = Document::new(document_id(1), "det".to_owned());
462 doc.insert_sketch(sketch_id(1), "S".to_owned(), sketch);
463 save(&doc, &folder).map_err(|e| TestCaseError::Fail(format!("save: {e}").into()))?;
464 let first_doc = std::fs::read(folder.document_file())
465 .map_err(|e| TestCaseError::Fail(format!("read: {e}").into()))?;
466 let first_sketch = std::fs::read(folder.sketch_path(sketch_id(1)))
467 .map_err(|e| TestCaseError::Fail(format!("read: {e}").into()))?;
468 save(&doc, &folder).map_err(|e| TestCaseError::Fail(format!("save2: {e}").into()))?;
469 let second_doc = std::fs::read(folder.document_file())
470 .map_err(|e| TestCaseError::Fail(format!("read: {e}").into()))?;
471 let second_sketch = std::fs::read(folder.sketch_path(sketch_id(1)))
472 .map_err(|e| TestCaseError::Fail(format!("read: {e}").into()))?;
473 prop_assert_eq!(first_doc, second_doc);
474 prop_assert_eq!(first_sketch, second_sketch);
475 }
476}