Another project
0

Configure Feed

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

at main 10 kB View raw
1use crate::KernelError; 2use bone_types::dimensioned_serde; 3use bone_types::{ 4 Angle, BodyId, BrepFaceId, BrepVertexId, FeatureId, Length, PositiveLength, SketchId, UnitVec3, 5 degree, radian, 6}; 7use serde::{Deserialize, Serialize}; 8 9#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 10pub enum ExtrudeSense { 11 Forward, 12 Reverse, 13} 14 15#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 16pub enum PlaneRef { 17 DatumPlane(FeatureId), 18 PlanarFace(BrepFaceId), 19} 20 21#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 22#[serde(deny_unknown_fields)] 23pub enum ExtrudeDirection { 24 Normal { sense: ExtrudeSense }, 25 AlongAxis(UnitVec3), 26 BetweenReferences { from: PlaneRef, to: PlaneRef }, 27} 28 29#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 30#[serde(deny_unknown_fields)] 31pub enum ExtrudeEndCondition { 32 Blind { 33 depth: PositiveLength, 34 }, 35 MidPlane { 36 depth: PositiveLength, 37 }, 38 ThroughAll, 39 UpToNext, 40 UpToVertex { 41 vertex: BrepVertexId, 42 }, 43 UpToSurface { 44 face: BrepFaceId, 45 }, 46 OffsetFromSurface { 47 face: BrepFaceId, 48 #[serde(with = "dimensioned_serde::length_si")] 49 offset: Length, 50 }, 51 UpToBody { 52 body: BodyId, 53 }, 54} 55 56#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 57pub enum DraftDirection { 58 Inward, 59 Outward, 60} 61 62#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 63#[serde(try_from = "f64", into = "f64")] 64pub struct DraftMagnitude(Angle); 65 66impl DraftMagnitude { 67 pub fn new(angle: Angle) -> Result<Self, KernelError> { 68 let degrees = angle.get::<degree>(); 69 if (0.0..90.0).contains(&degrees) { 70 Ok(Self(angle)) 71 } else { 72 Err(KernelError::DraftAngleOutOfRange(degrees)) 73 } 74 } 75 76 #[must_use] 77 pub fn get(self) -> Angle { 78 self.0 79 } 80} 81 82impl From<DraftMagnitude> for f64 { 83 fn from(value: DraftMagnitude) -> Self { 84 value.0.get::<radian>() 85 } 86} 87 88impl TryFrom<f64> for DraftMagnitude { 89 type Error = KernelError; 90 91 fn try_from(value: f64) -> Result<Self, Self::Error> { 92 Self::new(Angle::new::<radian>(value)) 93 } 94} 95 96#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 97#[serde(deny_unknown_fields)] 98pub struct DraftAngle { 99 angle: DraftMagnitude, 100 direction: DraftDirection, 101} 102 103impl DraftAngle { 104 #[must_use] 105 pub const fn new(angle: DraftMagnitude, direction: DraftDirection) -> Self { 106 Self { angle, direction } 107 } 108 109 #[must_use] 110 pub const fn angle(self) -> DraftMagnitude { 111 self.angle 112 } 113 114 #[must_use] 115 pub const fn direction(self) -> DraftDirection { 116 self.direction 117 } 118} 119 120#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 121pub enum ThinWallDirection { 122 Inward, 123 Outward, 124 MidPlane, 125} 126 127#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 128#[serde(deny_unknown_fields)] 129pub struct ThinWall { 130 pub thickness: PositiveLength, 131 pub direction: ThinWallDirection, 132} 133 134#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] 135pub enum MergeResult { 136 #[default] 137 Merge, 138 Separate, 139} 140 141#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 142#[serde(deny_unknown_fields)] 143pub struct ExtrudeFeature { 144 pub sketch: SketchId, 145 pub direction: ExtrudeDirection, 146 pub end_condition: ExtrudeEndCondition, 147 #[serde(default)] 148 pub draft: Option<DraftAngle>, 149 #[serde(default)] 150 pub thin_wall: Option<ThinWall>, 151 #[serde(default)] 152 pub merge_result: MergeResult, 153} 154 155#[cfg(test)] 156mod tests { 157 use super::{ 158 DraftAngle, DraftDirection, DraftMagnitude, ExtrudeDirection, ExtrudeEndCondition, 159 ExtrudeFeature, ExtrudeSense, MergeResult, ThinWall, ThinWallDirection, 160 }; 161 use bone_types::{ 162 Angle, BrepFaceId, Length, PositiveLength, SketchId, degree, millimeter, radian, 163 }; 164 use slotmap::Key; 165 use uom::si::length::meter; 166 167 fn pos_mm(value: f64) -> PositiveLength { 168 let Ok(length) = PositiveLength::new(Length::new::<millimeter>(value)) else { 169 panic!("{value} mm is a positive length"); 170 }; 171 length 172 } 173 174 fn draft_deg(value: f64) -> DraftMagnitude { 175 let Ok(magnitude) = DraftMagnitude::new(Angle::new::<degree>(value)) else { 176 panic!("{value} deg is a valid draft angle"); 177 }; 178 magnitude 179 } 180 181 fn sample() -> ExtrudeFeature { 182 ExtrudeFeature { 183 sketch: SketchId::null(), 184 direction: ExtrudeDirection::Normal { 185 sense: ExtrudeSense::Forward, 186 }, 187 end_condition: ExtrudeEndCondition::Blind { 188 depth: pos_mm(10.0), 189 }, 190 draft: Some(DraftAngle::new(draft_deg(3.0), DraftDirection::Outward)), 191 thin_wall: Some(ThinWall { 192 thickness: pos_mm(2.0), 193 direction: ThinWallDirection::Outward, 194 }), 195 merge_result: MergeResult::default(), 196 } 197 } 198 199 #[test] 200 fn extrude_feature_ron_round_trip() { 201 let feature = sample(); 202 let Ok(text) = ron::to_string(&feature) else { 203 panic!("serialize extrude feature"); 204 }; 205 let Ok(back) = ron::from_str::<ExtrudeFeature>(&text) else { 206 panic!("deserialize extrude feature"); 207 }; 208 assert_eq!(feature, back); 209 } 210 211 #[test] 212 fn blind_depth_serializes_in_meters() { 213 let blind = ExtrudeEndCondition::Blind { 214 depth: pos_mm(10.0), 215 }; 216 let Ok(text) = ron::to_string(&blind) else { 217 panic!("serialize end condition"); 218 }; 219 let Ok(back) = ron::from_str::<ExtrudeEndCondition>(&text) else { 220 panic!("deserialize end condition"); 221 }; 222 let ExtrudeEndCondition::Blind { depth } = back else { 223 panic!("round-trips back to a blind depth"); 224 }; 225 assert!((depth.get().get::<meter>() - 0.01).abs() < f64::EPSILON); 226 assert!(text.contains("0.01")); 227 } 228 229 #[test] 230 fn end_condition_rejects_unknown_field() { 231 let blind = ExtrudeEndCondition::Blind { depth: pos_mm(5.0) }; 232 let Ok(text) = ron::to_string(&blind) else { 233 panic!("serialize end condition"); 234 }; 235 let Some(idx) = text.find("Blind(") else { 236 panic!("expected Blind variant in {text}"); 237 }; 238 let mut munged = text.clone(); 239 munged.insert_str(idx + "Blind(".len(), "bogus:42,"); 240 assert!(ron::from_str::<ExtrudeEndCondition>(&munged).is_err()); 241 } 242 243 #[test] 244 fn blind_rejects_non_positive_depth() { 245 assert!(ron::from_str::<ExtrudeEndCondition>("Blind(depth:0.0)").is_err()); 246 assert!(ron::from_str::<ExtrudeEndCondition>("Blind(depth:-1.0)").is_err()); 247 assert!(ron::from_str::<ExtrudeEndCondition>("Blind(depth:inf)").is_err()); 248 assert!(ron::from_str::<ExtrudeEndCondition>("Blind(depth:-inf)").is_err()); 249 assert!(ron::from_str::<ExtrudeEndCondition>("Blind(depth:NaN)").is_err()); 250 } 251 252 #[test] 253 fn feature_fills_defaults() { 254 let Ok(sketch) = ron::to_string(&SketchId::null()) else { 255 panic!("serialize sketch id"); 256 }; 257 let text = format!( 258 "(sketch:{sketch},direction:Normal(sense:Forward),end_condition:Blind(depth:0.01))" 259 ); 260 let Ok(feature) = ron::from_str::<ExtrudeFeature>(&text) else { 261 panic!("deserialize feature with a defaulted tail in {text}"); 262 }; 263 assert_eq!(feature.draft, None); 264 assert_eq!(feature.thin_wall, None); 265 assert_eq!(feature.merge_result, MergeResult::Merge); 266 } 267 268 #[test] 269 fn draft_angle_serializes_in_radians() { 270 let Ok(magnitude) = DraftMagnitude::new(Angle::new::<radian>(0.5)) else { 271 panic!("0.5 rad is a valid draft angle"); 272 }; 273 let draft = DraftAngle::new(magnitude, DraftDirection::Inward); 274 let Ok(text) = ron::to_string(&draft) else { 275 panic!("serialize draft angle"); 276 }; 277 let Ok(back) = ron::from_str::<DraftAngle>(&text) else { 278 panic!("deserialize draft angle"); 279 }; 280 assert_eq!(draft, back); 281 assert!((back.angle().get().get::<radian>() - 0.5).abs() < f64::EPSILON); 282 assert_eq!(back.direction(), DraftDirection::Inward); 283 assert!(text.contains("0.5")); 284 } 285 286 #[test] 287 fn draft_magnitude_rejects_out_of_range() { 288 assert!(DraftMagnitude::new(Angle::new::<degree>(0.0)).is_ok()); 289 assert!(DraftMagnitude::new(Angle::new::<degree>(45.0)).is_ok()); 290 assert!(DraftMagnitude::new(Angle::new::<degree>(-5.0)).is_err()); 291 assert!(DraftMagnitude::new(Angle::new::<degree>(90.0)).is_err()); 292 assert!(DraftMagnitude::new(Angle::new::<degree>(200.0)).is_err()); 293 assert!(DraftMagnitude::new(Angle::new::<radian>(f64::INFINITY)).is_err()); 294 assert!(DraftMagnitude::new(Angle::new::<radian>(f64::NAN)).is_err()); 295 } 296 297 #[test] 298 fn draft_angle_deserialize_rejects_out_of_range() { 299 assert!(ron::from_str::<DraftAngle>("(angle:0.5,direction:Outward)").is_ok()); 300 assert!(ron::from_str::<DraftAngle>("(angle:3.5,direction:Outward)").is_err()); 301 } 302 303 #[test] 304 fn offset_from_surface_allows_negative_offset() { 305 let condition = ExtrudeEndCondition::OffsetFromSurface { 306 face: BrepFaceId::null(), 307 offset: Length::new::<meter>(-0.005), 308 }; 309 let Ok(text) = ron::to_string(&condition) else { 310 panic!("serialize offset end condition"); 311 }; 312 let Ok(back) = ron::from_str::<ExtrudeEndCondition>(&text) else { 313 panic!("deserialize offset end condition"); 314 }; 315 assert_eq!(condition, back); 316 assert!(text.contains("-0.005")); 317 } 318}