Another project
0

Configure Feed

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

at main 23 kB View raw
1use core::num::NonZeroU32; 2 3use bone_document::{ 4 ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense, MergeResult, Sketch, 5 SketchDimension, SketchEntity, 6}; 7use bone_types::{ 8 ExtrudeId, Length, Point2, Point3, PositiveLength, SketchEntityId, SketchId, SketchPlaneBasis, 9 Tolerance, UnitVec3, 10}; 11use bone_ui::hotkey::ActionId; 12use uom::si::length::millimeter; 13 14#[derive(Copy, Clone, Debug, PartialEq, Eq)] 15pub enum SketchTool { 16 Point, 17 Line, 18 CenterpointArc, 19 TangentArc, 20 ThreePointArc, 21 Circle, 22 PerimeterCircle, 23 CornerRectangle, 24 CenterRectangle, 25 ThreePointCornerRectangle, 26 ThreePointCenterRectangle, 27 Parallelogram, 28} 29 30impl SketchTool { 31 pub const ENTITIES: &'static [Self] = &[ 32 Self::Point, 33 Self::Line, 34 Self::CenterpointArc, 35 Self::TangentArc, 36 Self::ThreePointArc, 37 Self::Circle, 38 Self::PerimeterCircle, 39 Self::CornerRectangle, 40 Self::CenterRectangle, 41 Self::ThreePointCornerRectangle, 42 Self::ThreePointCenterRectangle, 43 Self::Parallelogram, 44 ]; 45} 46 47#[derive(Copy, Clone, Debug, PartialEq, Eq)] 48pub enum FeatureTool { 49 ExtrudedBossBase, 50 ExtrudedCut, 51} 52 53impl FeatureTool { 54 pub const ALL: &'static [Self] = &[Self::ExtrudedBossBase, Self::ExtrudedCut]; 55} 56 57#[derive(Copy, Clone, Debug, PartialEq)] 58pub enum ExtrudeArming { 59 Profile { 60 feature: ExtrudeFeature, 61 target: Option<ExtrudeId>, 62 }, 63 AwaitingSketch, 64} 65 66impl ExtrudeArming { 67 #[must_use] 68 pub fn profile(sketch: SketchId) -> Self { 69 Self::Profile { 70 feature: default_extrude_feature(sketch), 71 target: None, 72 } 73 } 74 75 #[must_use] 76 pub fn edit(target: ExtrudeId, feature: ExtrudeFeature) -> Self { 77 Self::Profile { 78 feature, 79 target: Some(target), 80 } 81 } 82} 83 84const DEFAULT_EXTRUDE_DEPTH_MM: f64 = 10.0; 85 86#[must_use] 87pub fn default_extrude_depth() -> PositiveLength { 88 let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(DEFAULT_EXTRUDE_DEPTH_MM)) else { 89 unreachable!("constant default extrude depth is positive and finite"); 90 }; 91 depth 92} 93 94#[must_use] 95pub fn default_extrude_feature(sketch: SketchId) -> ExtrudeFeature { 96 ExtrudeFeature { 97 sketch, 98 direction: ExtrudeDirection::Normal { 99 sense: ExtrudeSense::Forward, 100 }, 101 end_condition: ExtrudeEndCondition::Blind { 102 depth: default_extrude_depth(), 103 }, 104 draft: None, 105 thin_wall: None, 106 merge_result: MergeResult::Merge, 107 } 108} 109 110#[derive(Copy, Clone, Debug, PartialEq, Eq)] 111pub enum EndConditionKind { 112 Blind, 113 MidPlane, 114} 115 116impl EndConditionKind { 117 pub const SUPPORTED: &'static [Self] = &[Self::Blind, Self::MidPlane]; 118 119 #[must_use] 120 pub fn of(condition: &ExtrudeEndCondition) -> Option<Self> { 121 match condition { 122 ExtrudeEndCondition::Blind { .. } => Some(Self::Blind), 123 ExtrudeEndCondition::MidPlane { .. } => Some(Self::MidPlane), 124 _ => None, 125 } 126 } 127 128 #[must_use] 129 pub fn with_depth(self, depth: PositiveLength) -> ExtrudeEndCondition { 130 match self { 131 Self::Blind => ExtrudeEndCondition::Blind { depth }, 132 Self::MidPlane => ExtrudeEndCondition::MidPlane { depth }, 133 } 134 } 135} 136 137#[must_use] 138pub fn end_condition_depth(condition: &ExtrudeEndCondition) -> Option<PositiveLength> { 139 match condition { 140 ExtrudeEndCondition::Blind { depth } | ExtrudeEndCondition::MidPlane { depth } => { 141 Some(*depth) 142 } 143 _ => None, 144 } 145} 146 147#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 148pub enum Plane { 149 Xy, 150 Yz, 151 Zx, 152} 153 154impl Plane { 155 #[must_use] 156 pub fn basis(self) -> SketchPlaneBasis { 157 let (x, y) = match self { 158 Self::Xy => (UnitVec3::x_axis(), UnitVec3::y_axis()), 159 Self::Yz => (UnitVec3::y_axis(), UnitVec3::z_axis()), 160 Self::Zx => (UnitVec3::z_axis(), UnitVec3::x_axis()), 161 }; 162 let Ok(basis) = SketchPlaneBasis::new(Point3::origin(), x, y, Tolerance::new(1e-9)) else { 163 unreachable!("canonical principal-plane axes are orthonormal"); 164 }; 165 basis 166 } 167} 168 169const fn action_id(value: u32) -> ActionId { 170 let Some(nz) = NonZeroU32::new(value) else { 171 panic!("ActionId must be non-zero"); 172 }; 173 ActionId::new(nz) 174} 175 176pub const ESCAPE_ACTION: ActionId = action_id(1); 177pub const UNDO_ACTION: ActionId = action_id(2); 178pub const REDO_ACTION: ActionId = action_id(3); 179 180#[derive(Copy, Clone, Debug, PartialEq)] 181pub enum ClickAnchor { 182 Position(Point2), 183 Endpoint(SketchEntityId), 184 MidpointOf { 185 line: SketchEntityId, 186 position: Point2, 187 }, 188} 189 190#[derive(Copy, Clone, Debug, PartialEq)] 191pub enum Pending { 192 First(ClickAnchor), 193 Second(ClickAnchor, ClickAnchor), 194} 195 196#[derive(Copy, Clone, Debug, PartialEq)] 197pub struct PendingDimension { 198 pub proto: SketchDimension, 199 pub anchor: Point2, 200} 201 202#[derive(Copy, Clone, Debug, Default, PartialEq)] 203pub struct DragPins([Option<(SketchEntityId, Point2)>; 3]); 204 205impl DragPins { 206 #[cfg(test)] 207 #[must_use] 208 pub const fn from_array(arr: [Option<(SketchEntityId, Point2)>; 3]) -> Self { 209 Self(arr) 210 } 211 212 #[must_use] 213 pub fn from_sketch_entity(sketch: &Sketch, id: SketchEntityId) -> Option<Self> { 214 let entity = sketch.entities().get(id).copied()?; 215 let pin = |pid: SketchEntityId| -> Option<(SketchEntityId, Point2)> { 216 match sketch.entities().get(pid)? { 217 SketchEntity::Point(p) => Some((pid, p.at())), 218 _ => None, 219 } 220 }; 221 match entity { 222 SketchEntity::Point(p) => Some(Self([Some((id, p.at())), None, None])), 223 SketchEntity::Line(l) => Some(Self([Some(pin(l.a())?), Some(pin(l.b())?), None])), 224 SketchEntity::Arc(a) => Some(Self([ 225 Some(pin(a.center())?), 226 Some(pin(a.start())?), 227 Some(pin(a.end())?), 228 ])), 229 SketchEntity::Circle(c) => Some(Self([Some(pin(c.center())?), None, None])), 230 } 231 } 232 233 pub fn iter(&self) -> impl Iterator<Item = (SketchEntityId, Point2)> + '_ { 234 self.0.iter().filter_map(|p| *p) 235 } 236 237 #[must_use] 238 pub fn to_targets(self, press: Point2, cursor: Point2) -> Vec<(SketchEntityId, Point2)> { 239 let (px, py) = press.coords_mm(); 240 let (cx, cy) = cursor.coords_mm(); 241 let (dx, dy) = (cx - px, cy - py); 242 self.iter() 243 .map(|(id, original)| { 244 let (ox, oy) = original.coords_mm(); 245 (id, Point2::from_mm(ox + dx, oy + dy)) 246 }) 247 .collect() 248 } 249} 250 251#[derive(Copy, Clone, Debug, PartialEq)] 252pub struct DragSession { 253 pub entity: SketchEntityId, 254 pub press: Point2, 255 pub pins: DragPins, 256} 257 258#[derive(Copy, Clone, Debug, PartialEq)] 259pub enum DimensionFlow { 260 Editing(PendingDimension), 261 Conflict(PendingDimension), 262} 263 264#[derive(Copy, Clone, Debug, Default, PartialEq)] 265pub struct SketchSession { 266 pub tool: Option<SketchTool>, 267 pub pending: Option<Pending>, 268 pub drag: Option<DragSession>, 269 pub dim_flow: Option<DimensionFlow>, 270} 271 272#[derive(Clone, Debug, Default, PartialEq)] 273pub enum Mode { 274 #[default] 275 Idle, 276 Sketch { 277 sketch_id: SketchId, 278 session: Box<SketchSession>, 279 }, 280 Extrude(ExtrudeArming), 281} 282 283impl Mode { 284 #[must_use] 285 pub fn enter_sketch(sketch_id: SketchId) -> Self { 286 Self::Sketch { 287 sketch_id, 288 session: Box::default(), 289 } 290 } 291 292 #[must_use] 293 fn map_session(self, f: impl FnOnce(SketchSession) -> SketchSession) -> Self { 294 match self { 295 Self::Sketch { sketch_id, session } => Self::Sketch { 296 sketch_id, 297 session: Box::new(f(*session)), 298 }, 299 Self::Idle | Self::Extrude(_) => self, 300 } 301 } 302 303 #[must_use] 304 pub fn arm_tool(self, kind: SketchTool) -> Self { 305 self.map_session(|_| SketchSession { 306 tool: Some(kind), 307 ..SketchSession::default() 308 }) 309 } 310 311 #[must_use] 312 pub fn disarm_tool(self) -> Self { 313 self.map_session(|_| SketchSession::default()) 314 } 315 316 #[must_use] 317 pub fn clear_pending(self) -> Self { 318 self.map_session(|s| SketchSession { pending: None, ..s }) 319 } 320 321 #[must_use] 322 pub fn start_drag(self, drag: DragSession) -> Self { 323 self.map_session(|s| SketchSession { 324 drag: Some(drag), 325 ..s 326 }) 327 } 328 329 #[must_use] 330 pub fn end_drag(self) -> Self { 331 self.map_session(|s| SketchSession { drag: None, ..s }) 332 } 333 334 #[must_use] 335 pub fn start_dimension(self, pending: PendingDimension) -> Self { 336 self.map_session(|s| SketchSession { 337 tool: None, 338 pending: None, 339 dim_flow: Some(DimensionFlow::Editing(pending)), 340 ..s 341 }) 342 } 343 344 #[must_use] 345 pub fn cancel_dimension(self) -> Self { 346 self.map_session(|s| match s.dim_flow { 347 Some(DimensionFlow::Editing(_)) => SketchSession { 348 dim_flow: None, 349 ..s 350 }, 351 _ => s, 352 }) 353 } 354 355 #[must_use] 356 pub fn start_dim_conflict(self, pending: PendingDimension) -> Self { 357 self.map_session(|s| SketchSession { 358 dim_flow: Some(DimensionFlow::Conflict(pending)), 359 ..s 360 }) 361 } 362 363 #[must_use] 364 pub fn cancel_dim_conflict(self) -> Self { 365 self.map_session(|s| match s.dim_flow { 366 Some(DimensionFlow::Conflict(_)) => SketchSession { 367 dim_flow: None, 368 ..s 369 }, 370 _ => s, 371 }) 372 } 373 374 #[must_use] 375 pub const fn is_sketch(&self) -> bool { 376 matches!(self, Self::Sketch { .. }) 377 } 378 379 #[must_use] 380 pub const fn is_extrude(&self) -> bool { 381 matches!(self, Self::Extrude(_)) 382 } 383 384 #[must_use] 385 pub fn sketch_id(&self) -> Option<SketchId> { 386 match self { 387 Self::Sketch { sketch_id, .. } => Some(*sketch_id), 388 Self::Idle | Self::Extrude(_) => None, 389 } 390 } 391 392 #[must_use] 393 pub fn active_tool(&self) -> Option<SketchTool> { 394 match self { 395 Self::Sketch { session, .. } => session.tool, 396 Self::Idle | Self::Extrude(_) => None, 397 } 398 } 399 400 #[must_use] 401 pub fn dim_flow(&self) -> Option<DimensionFlow> { 402 match self { 403 Self::Sketch { session, .. } => session.dim_flow, 404 Self::Idle | Self::Extrude(_) => None, 405 } 406 } 407} 408 409#[cfg(test)] 410mod tests { 411 use super::{ 412 ClickAnchor, DEFAULT_EXTRUDE_DEPTH_MM, DimensionFlow, DragPins, DragSession, 413 EndConditionKind, ExtrudeArming, ExtrudeDirection, ExtrudeEndCondition, ExtrudeSense, 414 FeatureTool, MergeResult, Mode, Pending, Plane, SketchSession, SketchTool, 415 default_extrude_feature, end_condition_depth, 416 }; 417 use bone_types::{Length, Point2, PositiveLength, SketchEntityId, SketchId}; 418 use uom::si::length::millimeter; 419 420 #[test] 421 fn sketch_mode_projections_read_session() { 422 let id = SketchId::default(); 423 let mode = Mode::enter_sketch(id).arm_tool(SketchTool::Line); 424 assert_eq!(mode.sketch_id(), Some(id)); 425 assert_eq!(mode.active_tool(), Some(SketchTool::Line)); 426 assert!(!mode.is_extrude()); 427 } 428 429 #[test] 430 fn off_sketch_projections_are_empty() { 431 [Mode::Idle, Mode::Extrude(ExtrudeArming::AwaitingSketch)] 432 .into_iter() 433 .for_each(|mode| { 434 assert_eq!(mode.sketch_id(), None); 435 assert_eq!(mode.active_tool(), None); 436 assert_eq!(mode.dim_flow(), None); 437 }); 438 } 439 440 #[test] 441 fn session_transforms_leave_extrude_untouched() { 442 let mode = Mode::Extrude(ExtrudeArming::AwaitingSketch); 443 assert_eq!(mode.clone().arm_tool(SketchTool::Line), mode); 444 assert_eq!(mode.clone().disarm_tool(), mode); 445 assert_eq!(mode.clone().clear_pending(), mode); 446 } 447 448 #[test] 449 fn feature_tool_all_lists_both_extrude_buttons() { 450 assert_eq!( 451 FeatureTool::ALL, 452 &[FeatureTool::ExtrudedBossBase, FeatureTool::ExtrudedCut], 453 ); 454 } 455 456 #[test] 457 fn arm_tool_in_sketch_records_kind() { 458 let mode = Mode::enter_sketch(SketchId::default()).arm_tool(SketchTool::Line); 459 let Mode::Sketch { session, .. } = mode else { 460 panic!("expected sketch mode"); 461 }; 462 assert_eq!(session.tool, Some(SketchTool::Line)); 463 } 464 465 #[test] 466 fn arm_tool_in_idle_is_noop() { 467 assert_eq!(Mode::Idle.arm_tool(SketchTool::Line), Mode::Idle); 468 } 469 470 #[test] 471 fn arm_tool_clears_pending() { 472 let session = SketchSession { 473 tool: Some(SketchTool::Line), 474 pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 475 1.0, 2.0, 476 )))), 477 ..SketchSession::default() 478 }; 479 let mode = Mode::Sketch { 480 sketch_id: SketchId::default(), 481 session: Box::new(session), 482 } 483 .arm_tool(SketchTool::Point); 484 let Mode::Sketch { session, .. } = mode else { 485 panic!("expected sketch mode"); 486 }; 487 assert_eq!(session.tool, Some(SketchTool::Point)); 488 assert_eq!(session.pending, None); 489 } 490 491 #[test] 492 fn clear_pending_in_sketch_drops_pending_keeps_tool() { 493 let session = SketchSession { 494 tool: Some(SketchTool::Line), 495 pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 496 3.0, 4.0, 497 )))), 498 ..SketchSession::default() 499 }; 500 let mode = Mode::Sketch { 501 sketch_id: SketchId::default(), 502 session: Box::new(session), 503 } 504 .clear_pending(); 505 let Mode::Sketch { session, .. } = mode else { 506 panic!("expected sketch mode"); 507 }; 508 assert_eq!(session.tool, Some(SketchTool::Line)); 509 assert_eq!(session.pending, None); 510 } 511 512 #[test] 513 fn clear_pending_in_idle_is_noop() { 514 assert_eq!(Mode::Idle.clear_pending(), Mode::Idle); 515 } 516 517 fn sample_drag_session() -> DragSession { 518 let entity = SketchEntityId::default(); 519 DragSession { 520 entity, 521 press: Point2::origin(), 522 pins: DragPins::from_array([Some((entity, Point2::origin())), None, None]), 523 } 524 } 525 526 #[test] 527 fn start_drag_records_entity_keeps_tool_and_pending() { 528 let drag = sample_drag_session(); 529 let mode = Mode::Sketch { 530 sketch_id: SketchId::default(), 531 session: Box::new(SketchSession { 532 tool: Some(SketchTool::Line), 533 pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 534 0.0, 0.0, 535 )))), 536 ..SketchSession::default() 537 }), 538 } 539 .start_drag(drag); 540 let Mode::Sketch { session, .. } = mode else { 541 panic!("expected sketch mode"); 542 }; 543 assert_eq!(session.drag, Some(drag)); 544 assert_eq!(session.tool, Some(SketchTool::Line)); 545 assert!(session.pending.is_some()); 546 } 547 548 #[test] 549 fn end_drag_clears_drag_only() { 550 let mode = Mode::Sketch { 551 sketch_id: SketchId::default(), 552 session: Box::new(SketchSession { 553 tool: Some(SketchTool::Line), 554 drag: Some(sample_drag_session()), 555 ..SketchSession::default() 556 }), 557 } 558 .end_drag(); 559 let Mode::Sketch { session, .. } = mode else { 560 panic!("expected sketch mode"); 561 }; 562 assert_eq!(session.drag, None); 563 assert_eq!(session.tool, Some(SketchTool::Line)); 564 } 565 566 #[test] 567 fn start_drag_in_idle_is_noop() { 568 assert_eq!(Mode::Idle.start_drag(sample_drag_session()), Mode::Idle); 569 } 570 571 #[test] 572 fn default_mode_is_idle() { 573 assert_eq!(Mode::default(), Mode::Idle); 574 } 575 576 #[test] 577 fn entities_table_holds_day_one_set() { 578 assert_eq!(SketchTool::ENTITIES.len(), 12, "day-1 entity tools"); 579 } 580 581 #[test] 582 fn fresh_session_has_no_tool() { 583 assert_eq!(SketchSession::default().tool, None); 584 assert_eq!(SketchSession::default().pending, None); 585 } 586 587 #[test] 588 fn is_sketch_distinguishes_states() { 589 assert!(!Mode::Idle.is_sketch()); 590 assert!(Mode::enter_sketch(SketchId::default()).is_sketch()); 591 } 592 593 #[test] 594 fn start_dimension_records_pending_and_clears_tool() { 595 use bone_document::{DimensionKind, SketchDimension}; 596 use bone_types::Length; 597 use uom::si::length::millimeter; 598 let proto = SketchDimension::Linear { 599 a: SketchEntityId::default(), 600 b: SketchEntityId::default(), 601 value: Length::new::<millimeter>(5.0), 602 kind: DimensionKind::Driving, 603 }; 604 let pending = super::PendingDimension { 605 proto, 606 anchor: Point2::origin(), 607 }; 608 let mode = Mode::enter_sketch(SketchId::default()) 609 .arm_tool(SketchTool::Line) 610 .start_dimension(pending); 611 let Mode::Sketch { session, .. } = mode else { 612 panic!("expected sketch mode"); 613 }; 614 assert_eq!(session.tool, None); 615 assert_eq!(session.pending, None); 616 assert_eq!(session.dim_flow, Some(DimensionFlow::Editing(pending))); 617 } 618 619 #[test] 620 fn cancel_dimension_clears_editing_only() { 621 use bone_document::{DimensionKind, SketchDimension}; 622 use bone_types::Length; 623 use uom::si::length::millimeter; 624 let proto = SketchDimension::Linear { 625 a: SketchEntityId::default(), 626 b: SketchEntityId::default(), 627 value: Length::new::<millimeter>(5.0), 628 kind: DimensionKind::Driving, 629 }; 630 let pending = super::PendingDimension { 631 proto, 632 anchor: Point2::origin(), 633 }; 634 let mode = Mode::enter_sketch(SketchId::default()) 635 .start_dimension(pending) 636 .cancel_dimension(); 637 let Mode::Sketch { session, .. } = mode else { 638 panic!("expected sketch mode"); 639 }; 640 assert_eq!(session.dim_flow, None); 641 } 642 643 #[test] 644 fn cancel_dimension_leaves_conflict_intact() { 645 use bone_document::{DimensionKind, SketchDimension}; 646 use bone_types::Length; 647 use uom::si::length::millimeter; 648 let proto = SketchDimension::Linear { 649 a: SketchEntityId::default(), 650 b: SketchEntityId::default(), 651 value: Length::new::<millimeter>(5.0), 652 kind: DimensionKind::Driving, 653 }; 654 let pending = super::PendingDimension { 655 proto, 656 anchor: Point2::origin(), 657 }; 658 let mode = Mode::enter_sketch(SketchId::default()) 659 .start_dim_conflict(pending) 660 .cancel_dimension(); 661 let Mode::Sketch { session, .. } = mode else { 662 panic!("expected sketch mode"); 663 }; 664 assert_eq!(session.dim_flow, Some(DimensionFlow::Conflict(pending))); 665 } 666 667 #[test] 668 fn start_dimension_after_existing_keeps_drag_replaces_proto() { 669 use bone_document::{DimensionKind, SketchDimension}; 670 use bone_types::Length; 671 use uom::si::length::millimeter; 672 let radius = SketchDimension::Radius { 673 target: SketchEntityId::default(), 674 value: Length::new::<millimeter>(3.0), 675 kind: DimensionKind::Driving, 676 }; 677 let diameter = SketchDimension::Diameter { 678 target: SketchEntityId::default(), 679 value: Length::new::<millimeter>(6.0), 680 kind: DimensionKind::Driving, 681 }; 682 let initial = super::PendingDimension { 683 proto: radius, 684 anchor: Point2::origin(), 685 }; 686 let next = super::PendingDimension { 687 proto: diameter, 688 anchor: Point2::from_mm(1.0, 0.0), 689 }; 690 let mode = Mode::Sketch { 691 sketch_id: SketchId::default(), 692 session: Box::new(SketchSession { 693 drag: Some(sample_drag_session()), 694 dim_flow: Some(DimensionFlow::Editing(initial)), 695 ..SketchSession::default() 696 }), 697 } 698 .start_dimension(next); 699 let Mode::Sketch { session, .. } = mode else { 700 panic!("expected sketch mode"); 701 }; 702 assert_eq!(session.dim_flow, Some(DimensionFlow::Editing(next))); 703 assert!(session.drag.is_some(), "drag preserved across swap"); 704 } 705 706 #[test] 707 fn principal_planes_are_pairwise_distinct() { 708 let planes = [Plane::Xy, Plane::Yz, Plane::Zx]; 709 let bases: Vec<_> = planes.iter().copied().map(Plane::basis).collect(); 710 bases.iter().enumerate().for_each(|(i, a)| { 711 bases.iter().enumerate().skip(i + 1).for_each(|(j, b)| { 712 assert_ne!(a, b, "{:?} == {:?}", planes[i], planes[j]); 713 }); 714 }); 715 } 716 717 #[test] 718 fn default_extrude_feature_is_blind_forward_merge() { 719 let feature = default_extrude_feature(SketchId::default()); 720 assert!(matches!( 721 feature.direction, 722 ExtrudeDirection::Normal { 723 sense: ExtrudeSense::Forward 724 } 725 )); 726 assert!(matches!( 727 feature.end_condition, 728 ExtrudeEndCondition::Blind { .. } 729 )); 730 assert_eq!(feature.merge_result, MergeResult::Merge); 731 assert!(feature.draft.is_none()); 732 assert!(feature.thin_wall.is_none()); 733 let Some(depth) = end_condition_depth(&feature.end_condition) else { 734 panic!("blind carries depth"); 735 }; 736 assert!((depth.get().get::<millimeter>() - DEFAULT_EXTRUDE_DEPTH_MM).abs() < 1e-9); 737 } 738 739 #[test] 740 fn supported_end_conditions_are_blind_and_midplane() { 741 assert_eq!( 742 EndConditionKind::SUPPORTED, 743 &[EndConditionKind::Blind, EndConditionKind::MidPlane], 744 ); 745 } 746 747 #[test] 748 fn end_condition_kind_round_trips_through_depth() { 749 let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(7.0)) else { 750 unreachable!("7 mm is positive and finite"); 751 }; 752 EndConditionKind::SUPPORTED.iter().for_each(|kind| { 753 let condition = kind.with_depth(depth); 754 assert_eq!(EndConditionKind::of(&condition), Some(*kind)); 755 assert_eq!(end_condition_depth(&condition), Some(depth)); 756 }); 757 } 758 759 #[test] 760 fn extrude_arming_profile_carries_the_sketch() { 761 let id = SketchId::default(); 762 let ExtrudeArming::Profile { feature, target } = ExtrudeArming::profile(id) else { 763 panic!("profile arming holds a feature"); 764 }; 765 assert_eq!(feature.sketch, id); 766 assert_eq!(target, None); 767 } 768}