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_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}