Another project
0

Configure Feed

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

at main 11 kB View raw
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}