Another project
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}