1use super::{FeedsSpeeds, Heights, OperationId, Point3, SetupId, TargetSelection, ToolId, WorkOffset};
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12
13#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
15#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct Operation {
18 pub id: OperationId,
19 #[serde(default)]
20 pub name: String,
21 pub setup: SetupId,
22 pub tool: ToolId,
23 #[serde(default = "default_true")]
25 pub enabled: bool,
26 #[serde(default)]
27 pub targets: TargetSelection,
28 pub strategy: Strategy,
29 #[serde(default = "default_tolerance")]
31 pub tolerance: f64,
32 #[serde(default)]
33 pub stock_to_leave: StockToLeave,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub feeds: Option<FeedsSpeeds>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub heights: Option<Heights>,
40 #[serde(default)]
41 pub linking: LinkingParams,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub tool_axis: Option<Point3>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub live_tool: Option<bool>,
48 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
50 pub post_properties: BTreeMap<String, String>,
51 #[serde(default)]
53 pub polish: PolishParams,
54}
55
56fn default_true() -> bool {
57 true
58}
59
60fn default_tolerance() -> f64 {
61 0.01
62}
63
64impl Default for Operation {
65 fn default() -> Self {
66 Self::new(OperationId::default(), "", SetupId::default(), ToolId::default(), Strategy::default())
67 }
68}
69
70impl Operation {
71 pub fn new(
72 id: impl Into<OperationId>,
73 name: impl Into<String>,
74 setup: impl Into<SetupId>,
75 tool: impl Into<ToolId>,
76 strategy: Strategy,
77 ) -> Self {
78 Self {
79 id: id.into(),
80 name: name.into(),
81 setup: setup.into(),
82 tool: tool.into(),
83 enabled: true,
84 targets: TargetSelection::default(),
85 strategy,
86 tolerance: default_tolerance(),
87 stock_to_leave: StockToLeave::default(),
88 feeds: None,
89 heights: None,
90 linking: LinkingParams::default(),
91 tool_axis: None,
92 live_tool: None,
93 post_properties: BTreeMap::new(),
94 polish: PolishParams::default(),
95 }
96 }
97
98 pub fn validate(&self) -> Result<(), String> {
99 if !self.tolerance.is_finite() || self.tolerance <= 0.0 {
100 return Err("machining tolerance must be finite and positive".into());
101 }
102 self.targets.validate()?;
103 if self.enabled
104 && self.targets.target_solids.is_empty()
105 && self.targets.chains.is_empty()
106 && !self.strategy.is_turning()
107 && !self.strategy.is_targetless()
108 {
109 return Err("an enabled operation needs at least one target body".into());
110 }
111 if let Some(axis) = self.tool_axis {
112 if axis.normalized().is_none() {
113 return Err("tool axis must be a non-zero vector".into());
114 }
115 }
116 if let Some(f) = &self.feeds {
117 f.validate()?;
118 }
119 if let Some(h) = &self.heights {
120 h.validate()?;
121 }
122 self.stock_to_leave.validate()?;
123 self.linking.validate()?;
124 self.polish.validate()?;
125 self.strategy.validate()
126 }
127}
128
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub struct EnumChoice {
132 pub path: &'static str,
133 pub variants: &'static [&'static str],
134}
135
136impl Operation {
137 pub const ENUM_CHOICES: &'static [EnumChoice] = &[
139 EnumChoice { path: "linking.retractPolicy", variants: RetractPolicy::VARIANTS },
140 EnumChoice { path: "linking.leadIn.kind", variants: LeadKind::VARIANTS },
141 EnumChoice { path: "linking.leadOut.kind", variants: LeadKind::VARIANTS },
142 EnumChoice { path: "linking.ramp.kind", variants: RampKind::VARIANTS },
143 EnumChoice { path: "feeds.spindleDirection", variants: super::SpindleDirection::VARIANTS },
144 EnumChoice { path: "targets.chains[].source.kind", variants: super::ChainSource::VARIANTS },
145 EnumChoice { path: "targets.chains[].side", variants: super::ChainSide::VARIANTS },
146 ];
147}
148
149#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
152#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
153#[serde(rename_all = "camelCase")]
154#[serde(default)]
155pub struct PolishParams {
156 pub feed_optimization: FeedOptimizationParams,
157 pub smoothing: SmoothingParams,
158}
159
160impl PolishParams {
161 pub fn validate(&self) -> Result<(), String> {
162 self.feed_optimization.validate()?;
163 self.smoothing.validate()
164 }
165}
166
167#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
172#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
173#[serde(rename_all = "camelCase")]
174#[serde(default)]
175pub struct FeedOptimizationParams {
176 pub enabled: bool,
177 pub max_angle_deg: f64,
178 pub max_radius: f64,
179 pub reduced_feed_ratio: f64,
180 pub distance: f64,
181}
182
183impl Default for FeedOptimizationParams {
184 fn default() -> Self {
185 Self { enabled: false, max_angle_deg: 30.0, max_radius: 2.0, reduced_feed_ratio: 0.5, distance: 2.0 }
186 }
187}
188
189impl FeedOptimizationParams {
190 pub fn validate(&self) -> Result<(), String> {
191 if !self.enabled {
192 return Ok(());
193 }
194 if !self.max_angle_deg.is_finite() || self.max_angle_deg < 0.0 || self.max_angle_deg > 180.0 {
195 return Err("feed optimisation angle must be finite and within [0, 180] degrees".into());
196 }
197 positive_or_zero("feed optimisation radius", self.max_radius)?;
198 if !self.reduced_feed_ratio.is_finite() || self.reduced_feed_ratio <= 0.0 || self.reduced_feed_ratio > 1.0 {
199 return Err("reduced feed ratio must be within (0, 1]".into());
200 }
201 positive_or_zero("feed optimisation distance", self.distance)
202 }
203}
204
205#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
207#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase")]
209#[serde(default)]
210pub struct SmoothingParams {
211 pub enabled: bool,
212 pub tolerance: f64,
213}
214
215impl Default for SmoothingParams {
216 fn default() -> Self {
217 Self { enabled: false, tolerance: 0.01 }
218 }
219}
220
221impl SmoothingParams {
222 pub fn validate(&self) -> Result<(), String> {
223 if self.enabled {
224 positive("smoothing tolerance", self.tolerance)?;
225 }
226 Ok(())
227 }
228}
229
230#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
232#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
233#[serde(rename_all = "camelCase")]
234#[serde(default)]
235pub struct StockToLeave {
236 pub radial: f64,
237 pub axial: f64,
238}
239
240impl StockToLeave {
241 pub fn validate(&self) -> Result<(), String> {
242 if !self.radial.is_finite() || !self.axial.is_finite() {
243 return Err("stock to leave is not finite".into());
244 }
245 Ok(())
246 }
247}
248
249#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
251#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
252#[serde(rename_all = "camelCase")]
253#[serde(default)]
254pub struct LinkingParams {
255 pub retract_policy: RetractPolicy,
256 pub keep_down_distance: f64,
258 pub lead_in: Lead,
259 pub lead_out: Lead,
260 pub ramp: Ramp,
261 pub fit_arcs: bool,
263 pub smoothing_tolerance: f64,
265 pub safe_distance: f64,
267}
268
269impl Default for LinkingParams {
270 fn default() -> Self {
271 Self {
272 retract_policy: RetractPolicy::MinimumRetract,
273 keep_down_distance: 0.0,
274 lead_in: Lead::default(),
275 lead_out: Lead::default(),
276 ramp: Ramp::default(),
277 fit_arcs: true,
278 smoothing_tolerance: 0.0,
279 safe_distance: 2.0,
280 }
281 }
282}
283
284impl LinkingParams {
285 pub fn validate(&self) -> Result<(), String> {
286 if !self.keep_down_distance.is_finite() || self.keep_down_distance < 0.0 {
287 return Err("keep-down distance must be finite and non-negative".into());
288 }
289 if !self.smoothing_tolerance.is_finite() || self.smoothing_tolerance < 0.0 {
290 return Err("smoothing tolerance must be finite and non-negative".into());
291 }
292 if !self.safe_distance.is_finite() || self.safe_distance < 0.0 {
293 return Err("safe distance must be finite and non-negative".into());
294 }
295 self.lead_in.validate()?;
296 self.lead_out.validate()?;
297 self.ramp.validate()
298 }
299}
300
301#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
303#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
304#[serde(rename_all = "kebab-case")]
305pub enum RetractPolicy {
306 FullRetract,
308 RetractHeight,
310 #[default]
312 MinimumRetract,
313}
314
315impl RetractPolicy {
316 pub const VARIANTS: &'static [&'static str] = &["full-retract", "retract-height", "minimum-retract"];
318}
319
320#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
322#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
323#[serde(rename_all = "camelCase")]
324#[serde(default)]
325pub struct Lead {
326 pub kind: LeadKind,
327 pub radius: f64,
329 pub sweep_deg: f64,
331 pub linear_length: f64,
333 pub vertical_radius: f64,
335}
336
337impl Default for Lead {
338 fn default() -> Self {
339 Self { kind: LeadKind::None, radius: 0.0, sweep_deg: 90.0, linear_length: 0.0, vertical_radius: 0.0 }
340 }
341}
342
343impl Lead {
344 pub fn validate(&self) -> Result<(), String> {
345 for (name, v) in [
346 ("lead radius", self.radius),
347 ("lead sweep", self.sweep_deg),
348 ("lead linear length", self.linear_length),
349 ("lead vertical radius", self.vertical_radius),
350 ] {
351 if !v.is_finite() || v < 0.0 {
352 return Err(format!("{name} must be finite and non-negative"));
353 }
354 }
355 Ok(())
356 }
357}
358
359#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
360#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
361#[serde(rename_all = "kebab-case")]
362pub enum LeadKind {
363 #[default]
364 None,
365 Arc,
366 Linear,
367 Vertical,
368}
369
370impl LeadKind {
371 pub const VARIANTS: &'static [&'static str] = &["none", "arc", "linear", "vertical"];
373}
374
375#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
377#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
378#[serde(rename_all = "camelCase")]
379#[serde(default)]
380pub struct Ramp {
381 pub kind: RampKind,
382 pub angle_deg: f64,
384 pub helix_diameter: f64,
386 pub clearance: f64,
388}
389
390impl Default for Ramp {
391 fn default() -> Self {
392 Self { kind: RampKind::Plunge, angle_deg: 2.0, helix_diameter: 0.0, clearance: 0.0 }
393 }
394}
395
396impl Ramp {
397 pub fn validate(&self) -> Result<(), String> {
398 if !self.angle_deg.is_finite() || self.angle_deg < 0.0 || self.angle_deg > 90.0 {
399 return Err("ramp angle must be between 0 and 90 degrees".into());
400 }
401 if !self.helix_diameter.is_finite() || self.helix_diameter < 0.0 {
402 return Err("helix diameter must be finite and non-negative".into());
403 }
404 if !self.clearance.is_finite() || self.clearance < 0.0 {
405 return Err("ramp clearance must be finite and non-negative".into());
406 }
407 Ok(())
408 }
409}
410
411#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
412#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
413#[serde(rename_all = "kebab-case")]
414pub enum RampKind {
415 #[default]
416 Plunge,
417 Helix,
418 Profile,
419 ZigZag,
420 Smooth,
421}
422
423impl RampKind {
424 pub const VARIANTS: &'static [&'static str] = &["plunge", "helix", "profile", "zig-zag", "smooth"];
426}
427
428#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
431#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
432#[serde(rename_all = "camelCase")]
433#[serde(default)]
434pub struct Depths {
435 #[serde(skip_serializing_if = "Option::is_none")]
437 pub top: Option<f64>,
438 #[serde(skip_serializing_if = "Option::is_none")]
440 pub bottom: Option<f64>,
441 pub step_down: f64,
443 pub finishing_step_down: f64,
445 pub finishing_passes: u32,
446 pub depth_first: bool,
449}
450
451impl Default for Depths {
452 fn default() -> Self {
453 Self {
454 top: None,
455 bottom: None,
456 step_down: 0.0,
457 finishing_step_down: 0.0,
458 finishing_passes: 0,
459 depth_first: true,
460 }
461 }
462}
463
464impl Depths {
465 pub fn validate(&self) -> Result<(), String> {
466 for (name, v) in [("step down", self.step_down), ("finishing step down", self.finishing_step_down)] {
467 if !v.is_finite() || v < 0.0 {
468 return Err(format!("{name} must be finite and non-negative"));
469 }
470 }
471 if let (Some(t), Some(b)) = (self.top, self.bottom) {
472 if !t.is_finite() || !b.is_finite() || t < b {
473 return Err("top of cut must be above bottom of cut".into());
474 }
475 }
476 Ok(())
477 }
478}
479
480#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
482#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
483#[serde(rename_all = "camelCase")]
484#[serde(default)]
485pub struct Stepover {
486 pub distance: f64,
488 pub direction: CutDirection,
490}
491
492impl Default for Stepover {
493 fn default() -> Self {
494 Self { distance: 1.0, direction: CutDirection::Climb }
495 }
496}
497
498impl Stepover {
499 pub fn validate(&self) -> Result<(), String> {
500 if !self.distance.is_finite() || self.distance <= 0.0 {
501 return Err("stepover must be finite and positive".into());
502 }
503 Ok(())
504 }
505}
506
507#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
508#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
509#[serde(rename_all = "kebab-case")]
510pub enum CutDirection {
511 #[default]
512 Climb,
513 Conventional,
514 Either,
516}
517
518impl CutDirection {
519 pub const VARIANTS: &'static [&'static str] = &["climb", "conventional", "either"];
521}
522
523#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
525#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
526#[serde(rename_all = "kebab-case")]
527pub enum ContainmentMode {
528 #[default]
530 ToolCenterInside,
531 ToolInside,
533 ToolOutside,
535}
536
537impl ContainmentMode {
538 pub const VARIANTS: &'static [&'static str] = &["tool-center-inside", "tool-inside", "tool-outside"];
540}
541
542#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
544#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
545#[serde(rename_all = "camelCase")]
546#[serde(default)]
547pub struct Containment {
548 #[serde(skip_serializing_if = "Vec::is_empty")]
550 pub boundary: Vec<Vec<super::Point2>>,
551 pub mode: ContainmentMode,
552 pub offset: f64,
554 #[serde(skip_serializing_if = "Option::is_none")]
556 pub slope_deg: Option<[f64; 2]>,
557 #[serde(skip_serializing_if = "Option::is_none")]
559 pub z_range: Option<[f64; 2]>,
560}
561
562impl Containment {
563 pub fn validate(&self) -> Result<(), String> {
564 if !self.offset.is_finite() {
565 return Err("containment offset is not finite".into());
566 }
567 if let Some([lo, hi]) = self.slope_deg {
568 if !lo.is_finite() || !hi.is_finite() || lo < 0.0 || hi > 90.0 || lo > hi {
569 return Err("slope range must lie within 0..90 degrees".into());
570 }
571 }
572 if let Some([lo, hi]) = self.z_range {
573 if !lo.is_finite() || !hi.is_finite() || lo > hi {
574 return Err("z range must have min <= max".into());
575 }
576 }
577 for loop_ in &self.boundary {
578 if loop_.len() < 3 {
579 return Err("a containment boundary loop needs at least three points".into());
580 }
581 if loop_.iter().any(|p| !p.is_finite()) {
582 return Err("a containment boundary point is not finite".into());
583 }
584 }
585 Ok(())
586 }
587}
588
589#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
591#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
592#[serde(rename_all = "kebab-case")]
593pub enum DrillCycle {
594 #[default]
596 Drill,
597 DrillDwell,
599 Peck,
601 ChipBreak,
603 TapRigid,
605 TapFloat,
607 Ream,
609 BoreFine,
611 Bore,
613 BackBore,
615 BoreDwell,
617}
618
619impl DrillCycle {
620 pub const VARIANTS: &'static [&'static str] = &["drill", "drill-dwell", "peck", "chip-break", "tap-rigid", "tap-float", "ream", "bore-fine", "bore", "back-bore", "bore-dwell"];
622}
623
624#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
627#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
628#[serde(tag = "kind", rename_all = "camelCase")]
629#[non_exhaustive]
630pub enum Strategy {
631 Face(FaceParams),
633 Pocket2D(Pocket2dParams),
634 Contour2D(Contour2dParams),
635 Adaptive2D(Adaptive2dParams),
636 Slot(SlotParams),
637 Trace(TraceParams),
638 Chamfer2D(Chamfer2dParams),
639 Engrave(EngraveParams),
640 Bore(BoreParams),
641 Circular(CircularParams),
642 ThreadMill(ThreadMillParams),
643 Drill(DrillParams),
645 Adaptive3D(Adaptive3dParams),
647 Pocket3D(Pocket3dParams),
648 Parallel(ParallelParams),
650 Contour3D(Contour3dParams),
651 Scallop(ScallopParams),
652 Pencil(PencilParams),
653 SteepShallow(SteepShallowParams),
654 Radial(RadialParams),
655 Spiral(SpiralParams),
656 MorphedSpiral(MorphedSpiralParams),
657 Horizontal(HorizontalParams),
658 Ramp(RampFinishParams),
659 Project(ProjectParams),
660 Flow(FlowParams),
661 Morph(MorphParams),
662 RestFinish(RestFinishParams),
663 ProbeWcs(super::ProbeWcsParams),
665 ProbeGeometry(super::ProbeGeometryParams),
667 Pattern(PatternParams),
669 ManualNc(ManualNcParams),
671 Swarf(SwarfParams),
673 MultiAxisContour(MultiAxisContourParams),
674 RotaryWrap(RotaryWrapParams),
675 RotaryFinish(RotaryFinishParams),
677 MultiAxisFinish(MultiAxisFinishParams),
679 TurnFace(TurnFaceParams),
681 TurnProfile(TurnProfileParams),
682 TurnGroove(TurnGrooveParams),
683 TurnThread(TurnThreadParams),
684 TurnPartOff(TurnPartOffParams),
685}
686
687impl Default for Strategy {
688 fn default() -> Self {
689 Strategy::Parallel(ParallelParams::default())
690 }
691}
692
693impl Strategy {
694 pub const KINDS: &'static [&'static str] = &[
697 "face",
698 "pocket2D",
699 "contour2D",
700 "adaptive2D",
701 "slot",
702 "trace",
703 "chamfer2D",
704 "engrave",
705 "bore",
706 "circular",
707 "threadMill",
708 "drill",
709 "adaptive3D",
710 "pocket3D",
711 "parallel",
712 "contour3D",
713 "scallop",
714 "pencil",
715 "steepShallow",
716 "radial",
717 "spiral",
718 "morphedSpiral",
719 "horizontal",
720 "ramp",
721 "project",
722 "flow",
723 "morph",
724 "restFinish",
725 "probeWcs",
726 "probeGeometry",
727 "pattern",
728 "manualNc",
729 "swarf",
730 "multiAxisContour",
731 "rotaryWrap",
732 "rotaryFinish",
733 "multiAxisFinish",
734 "turnFace",
735 "turnProfile",
736 "turnGroove",
737 "turnThread",
738 "turnPartOff",
739 ];
740
741 pub fn kind_index(&self) -> usize {
743 match self {
744 Strategy::Face(_) => 0,
745 Strategy::Pocket2D(_) => 1,
746 Strategy::Contour2D(_) => 2,
747 Strategy::Adaptive2D(_) => 3,
748 Strategy::Slot(_) => 4,
749 Strategy::Trace(_) => 5,
750 Strategy::Chamfer2D(_) => 6,
751 Strategy::Engrave(_) => 7,
752 Strategy::Bore(_) => 8,
753 Strategy::Circular(_) => 9,
754 Strategy::ThreadMill(_) => 10,
755 Strategy::Drill(_) => 11,
756 Strategy::Adaptive3D(_) => 12,
757 Strategy::Pocket3D(_) => 13,
758 Strategy::Parallel(_) => 14,
759 Strategy::Contour3D(_) => 15,
760 Strategy::Scallop(_) => 16,
761 Strategy::Pencil(_) => 17,
762 Strategy::SteepShallow(_) => 18,
763 Strategy::Radial(_) => 19,
764 Strategy::Spiral(_) => 20,
765 Strategy::MorphedSpiral(_) => 21,
766 Strategy::Horizontal(_) => 22,
767 Strategy::Ramp(_) => 23,
768 Strategy::Project(_) => 24,
769 Strategy::Flow(_) => 25,
770 Strategy::Morph(_) => 26,
771 Strategy::RestFinish(_) => 27,
772 Strategy::ProbeWcs(_) => 28,
773 Strategy::ProbeGeometry(_) => 29,
774 Strategy::Pattern(_) => 30,
775 Strategy::ManualNc(_) => 31,
776 Strategy::Swarf(_) => 32,
777 Strategy::MultiAxisContour(_) => 33,
778 Strategy::RotaryWrap(_) => 34,
779 Strategy::RotaryFinish(_) => 35,
780 Strategy::MultiAxisFinish(_) => 36,
781 Strategy::TurnFace(_) => 37,
782 Strategy::TurnProfile(_) => 38,
783 Strategy::TurnGroove(_) => 39,
784 Strategy::TurnThread(_) => 40,
785 Strategy::TurnPartOff(_) => 41,
786 }
787 }
788
789 pub fn kind_name(&self) -> &'static str {
791 Self::KINDS[self.kind_index()]
792 }
793
794 pub fn enum_choices(kind: &str) -> &'static [EnumChoice] {
798 const STEPOVER: EnumChoice = EnumChoice { path: "stepover.direction", variants: CutDirection::VARIANTS };
799 const DIRECTION: EnumChoice = EnumChoice { path: "direction", variants: CutDirection::VARIANTS };
800 const CONTAINMENT: EnumChoice = EnumChoice { path: "containment.mode", variants: ContainmentMode::VARIANTS };
801 match kind {
802 "face" | "pocket2D" | "circular" => &[STEPOVER],
803 "contour2D" => &[DIRECTION],
804 "adaptive2D" => &[DIRECTION, EnumChoice { path: "restSource", variants: RestSource::VARIANTS }],
805 "slot" | "trace" | "chamfer2D" | "engrave" | "bore" | "threadMill" | "swarf" | "turnFace" | "turnGroove"
806 | "turnThread" | "turnPartOff" | "probeGeometry" => &[],
807 "drill" => &[EnumChoice { path: "cycle", variants: DrillCycle::VARIANTS }],
808 "pencil" | "radial" | "project" | "multiAxisContour" => &[CONTAINMENT],
809 "adaptive3D" => &[CONTAINMENT, EnumChoice { path: "restSource", variants: RestSource::VARIANTS }],
810 "parallel" | "scallop" | "spiral" | "morphedSpiral" | "horizontal" | "flow" | "morph" => {
811 &[STEPOVER, CONTAINMENT]
812 }
813 "pocket3D" => &[STEPOVER, CONTAINMENT, EnumChoice { path: "restSource", variants: RestSource::VARIANTS }],
814 "restFinish" => &[STEPOVER, CONTAINMENT, EnumChoice { path: "source", variants: RestSource::VARIANTS }],
815 "contour3D" | "ramp" => &[DIRECTION, CONTAINMENT],
816 "steepShallow" => &[EnumChoice { path: "shallowStepover.direction", variants: CutDirection::VARIANTS }, CONTAINMENT],
817 "rotaryWrap" | "rotaryFinish" => &[STEPOVER],
818 "multiAxisFinish" => &[EnumChoice { path: "mode.kind", variants: AxisMode::VARIANTS }],
819 "probeWcs" => &[EnumChoice { path: "probeKind", variants: super::ProbeKind::VARIANTS }],
820 "pattern" => &[
821 EnumChoice { path: "patternKind.kind", variants: PatternKind::VARIANTS },
822 EnumChoice { path: "order", variants: PatternOrder::VARIANTS },
823 ],
824 "manualNc" => &[EnumChoice { path: "manualKind.kind", variants: ManualNcKind::VARIANTS }],
825 "turnProfile" => &[EnumChoice { path: "direction", variants: RoughingDirection::VARIANTS }],
826 _ => &[],
827 }
828 }
829
830 pub fn default_of(kind: &str) -> Option<Strategy> {
834 Some(match kind {
835 "face" => Strategy::Face(Default::default()),
836 "pocket2D" => Strategy::Pocket2D(Default::default()),
837 "contour2D" => Strategy::Contour2D(Default::default()),
838 "adaptive2D" => Strategy::Adaptive2D(Default::default()),
839 "slot" => Strategy::Slot(Default::default()),
840 "trace" => Strategy::Trace(Default::default()),
841 "chamfer2D" => Strategy::Chamfer2D(Default::default()),
842 "engrave" => Strategy::Engrave(Default::default()),
843 "bore" => Strategy::Bore(Default::default()),
844 "circular" => Strategy::Circular(Default::default()),
845 "threadMill" => Strategy::ThreadMill(Default::default()),
846 "drill" => Strategy::Drill(Default::default()),
847 "adaptive3D" => Strategy::Adaptive3D(Default::default()),
848 "pocket3D" => Strategy::Pocket3D(Default::default()),
849 "parallel" => Strategy::Parallel(Default::default()),
850 "contour3D" => Strategy::Contour3D(Default::default()),
851 "scallop" => Strategy::Scallop(Default::default()),
852 "pencil" => Strategy::Pencil(Default::default()),
853 "steepShallow" => Strategy::SteepShallow(Default::default()),
854 "radial" => Strategy::Radial(Default::default()),
855 "spiral" => Strategy::Spiral(Default::default()),
856 "morphedSpiral" => Strategy::MorphedSpiral(Default::default()),
857 "horizontal" => Strategy::Horizontal(Default::default()),
858 "ramp" => Strategy::Ramp(Default::default()),
859 "project" => Strategy::Project(Default::default()),
860 "flow" => Strategy::Flow(Default::default()),
861 "morph" => Strategy::Morph(Default::default()),
862 "restFinish" => Strategy::RestFinish(Default::default()),
863 "probeWcs" => Strategy::ProbeWcs(Default::default()),
864 "probeGeometry" => Strategy::ProbeGeometry(Default::default()),
865 "pattern" => Strategy::Pattern(Default::default()),
866 "manualNc" => Strategy::ManualNc(Default::default()),
867 "swarf" => Strategy::Swarf(Default::default()),
868 "multiAxisContour" => Strategy::MultiAxisContour(Default::default()),
869 "rotaryWrap" => Strategy::RotaryWrap(Default::default()),
870 "rotaryFinish" => Strategy::RotaryFinish(Default::default()),
871 "multiAxisFinish" => Strategy::MultiAxisFinish(Default::default()),
872 "turnFace" => Strategy::TurnFace(Default::default()),
873 "turnProfile" => Strategy::TurnProfile(Default::default()),
874 "turnGroove" => Strategy::TurnGroove(Default::default()),
875 "turnThread" => Strategy::TurnThread(Default::default()),
876 "turnPartOff" => Strategy::TurnPartOff(Default::default()),
877 _ => return None,
878 })
879 }
880
881 pub fn is_targetless(&self) -> bool {
884 matches!(self, Strategy::Pattern(_) | Strategy::ManualNc(_))
885 }
886
887 pub fn is_turning(&self) -> bool {
888 matches!(
889 self,
890 Strategy::TurnFace(_)
891 | Strategy::TurnProfile(_)
892 | Strategy::TurnGroove(_)
893 | Strategy::TurnThread(_)
894 | Strategy::TurnPartOff(_)
895 )
896 }
897
898 pub fn validate(&self) -> Result<(), String> {
899 match self {
900 Strategy::Face(p) => {
901 p.depths.validate()?;
902 p.stepover.validate()
903 }
904 Strategy::Pocket2D(p) => {
905 p.depths.validate()?;
906 p.stepover.validate()
907 }
908 Strategy::Contour2D(p) => {
909 p.depths.validate()?;
910 positive_or_zero("finishing overlap", p.finishing_overlap)?;
911 positive_or_zero("roughing stepover", p.roughing_stepover)
912 }
913 Strategy::Adaptive2D(p) => {
914 p.depths.validate()?;
915 positive("optimal load", p.optimal_load)?;
916 positive_or_zero("minimum cutting radius", p.minimum_cutting_radius)
917 }
918 Strategy::Slot(p) => {
919 p.depths.validate()?;
920 positive("slot width", p.width)
921 }
922 Strategy::Trace(p) => p.depths.validate(),
923 Strategy::Chamfer2D(p) => {
924 positive("chamfer width", p.width)?;
925 positive_or_zero("chamfer tip offset", p.tip_offset)
926 }
927 Strategy::Engrave(p) => p.depths.validate(),
928 Strategy::Bore(p) => {
929 p.depths.validate()?;
930 positive_or_zero("bore pitch", p.pitch)
931 }
932 Strategy::Circular(p) => {
933 p.depths.validate()?;
934 p.stepover.validate()
935 }
936 Strategy::ThreadMill(p) => {
937 p.depths.validate()?;
938 positive("thread pitch", p.pitch)?;
939 positive("thread diameter", p.diameter)
940 }
941 Strategy::Drill(p) => {
942 positive_or_zero("peck depth", p.peck_depth)?;
943 positive_or_zero("dwell", p.dwell_seconds)?;
944 positive_or_zero("break-through depth", p.break_through)?;
945 positive_or_zero("chip-break retract", p.chip_break_retract)?;
946 positive_or_zero("bore shift", p.bore_shift)?;
947 positive_or_zero("back-bore clearance", p.back_bore_clearance)?;
948 if p.cycle == DrillCycle::ChipBreak {
949 positive("chip-break retract", p.chip_break_retract)?;
950 }
951 if p.cycle == DrillCycle::BackBore {
952 positive("back-bore clearance", p.back_bore_clearance)?;
953 }
954 Ok(())
955 }
956 Strategy::Adaptive3D(p) => {
957 p.depths.validate()?;
958 positive("optimal load", p.optimal_load)?;
959 positive_or_zero("fine step down", p.fine_step_down)
960 }
961 Strategy::Pocket3D(p) => {
962 p.depths.validate()?;
963 p.stepover.validate()
964 }
965 Strategy::Parallel(p) => {
966 p.stepover.validate()?;
967 p.containment.validate()?;
968 finite("pass angle", p.pass_angle_deg)
969 }
970 Strategy::Contour3D(p) => {
971 p.containment.validate()?;
972 positive("step down", p.step_down)
973 }
974 Strategy::Scallop(p) => {
975 p.stepover.validate()?;
976 if let Some(h) = p.cusp_height {
977 positive("cusp height", h)?;
978 }
979 p.containment.validate()
980 }
981 Strategy::Pencil(p) => {
982 p.containment.validate()?;
983 positive_or_zero("pencil bitangency angle", p.bitangency_angle_deg)
984 }
985 Strategy::SteepShallow(p) => {
986 p.containment.validate()?;
987 positive("steep step down", p.steep_step_down)?;
988 p.shallow_stepover.validate()?;
989 positive_or_zero("steep/shallow overlap", p.overlap)?;
990 finite("threshold angle", p.threshold_angle_deg)
991 }
992 Strategy::Radial(p) => {
993 p.containment.validate()?;
994 positive("radial angular step", p.angular_step_deg)
995 }
996 Strategy::Spiral(p) => {
997 p.containment.validate()?;
998 p.stepover.validate()
999 }
1000 Strategy::MorphedSpiral(p) => {
1001 p.containment.validate()?;
1002 p.stepover.validate()
1003 }
1004 Strategy::Horizontal(p) => {
1005 p.containment.validate()?;
1006 p.stepover.validate()
1007 }
1008 Strategy::Ramp(p) => {
1009 p.containment.validate()?;
1010 positive("ramp step down", p.step_down)
1011 }
1012 Strategy::Project(p) => p.containment.validate(),
1013 Strategy::Flow(p) => {
1014 p.containment.validate()?;
1015 p.stepover.validate()
1016 }
1017 Strategy::Morph(p) => {
1018 p.containment.validate()?;
1019 p.stepover.validate()?;
1020 for curve in [&p.first_curve, &p.second_curve] {
1021 if curve.len() < 2 || !curve.iter().all(super::Point2::is_finite) {
1022 return Err("morph needs two finite boundary polylines".into());
1023 }
1024 }
1025 Ok(())
1026 }
1027 Strategy::RestFinish(p) => {
1028 p.containment.validate()?;
1029 p.stepover.validate()?;
1030 positive_or_zero("rest threshold", p.minimum_rest)?;
1031 if !p.pass_angle_deg.is_finite() || (p.source == RestSource::ReferenceTool && p.reference_tool.0.is_empty()) {
1032 return Err("rest finishing needs a reference tool and finite pass angle".into());
1033 }
1034 Ok(())
1035 }
1036 Strategy::ProbeWcs(p) => p.validate(),
1037 Strategy::ProbeGeometry(p) => p.validate(),
1038 Strategy::Pattern(p) => p.validate(),
1039 Strategy::ManualNc(p) => p.validate(),
1040 Strategy::Swarf(p) => positive_or_zero("swarf tilt", p.tilt_deg),
1041 Strategy::MultiAxisContour(p) => {
1042 p.containment.validate()?;
1043 positive_or_zero("lead angle", p.lead_deg)?;
1044 positive_or_zero("tilt angle", p.tilt_deg)
1045 }
1046 Strategy::RotaryWrap(p) => {
1047 p.stepover.validate()?;
1048 positive("wrap diameter", p.wrap_diameter)
1049 }
1050 Strategy::RotaryFinish(p) => p.validate(),
1051 Strategy::MultiAxisFinish(p) => p.validate(),
1052 Strategy::TurnFace(p) => {
1053 positive("step over", p.step_over)?;
1054 positive_or_zero("facing end x", p.end_x)?;
1055 positive_or_zero("finish allowance", p.finish_allowance)
1056 }
1057 Strategy::TurnProfile(p) => positive("depth of cut", p.depth_of_cut),
1058 Strategy::TurnGroove(p) => {
1059 positive("groove width", p.width)?;
1060 positive_or_zero("groove depth", p.depth)?;
1061 positive("groove step over", p.step_over)?;
1062 positive_or_zero("finish allowance", p.finish_allowance)
1063 }
1064 Strategy::TurnThread(p) => {
1065 positive("thread pitch", p.pitch)?;
1066 positive("thread depth", p.depth)?;
1067 if p.passes == 0 {
1068 return Err("thread passes must be at least 1".into());
1069 }
1070 if !p.infeed_angle_deg.is_finite() || p.infeed_angle_deg < 0.0 || p.infeed_angle_deg >= 60.0 {
1071 return Err("thread infeed angle must be finite and within [0, 60) degrees".into());
1072 }
1073 Ok(())
1074 }
1075 Strategy::TurnPartOff(p) => {
1076 positive_or_zero("part-off x", p.end_x)?;
1077 positive_or_zero("part-off chamfer", p.chamfer)?;
1078 positive_or_zero("peck depth", p.peck_depth)?;
1079 positive_or_zero("peck retract", p.peck_retract)?;
1080 positive_or_zero("reduced feed radius", p.reduced_feed_radius)?;
1081 if !p.reduced_feed_factor.is_finite() || p.reduced_feed_factor <= 0.0 || p.reduced_feed_factor > 1.0 {
1082 return Err("reduced feed factor must be within (0, 1]".into());
1083 }
1084 Ok(())
1085 }
1086 }
1087 }
1088}
1089
1090fn finite(name: &str, v: f64) -> Result<(), String> {
1091 if v.is_finite() {
1092 Ok(())
1093 } else {
1094 Err(format!("{name} is not finite"))
1095 }
1096}
1097
1098fn positive(name: &str, v: f64) -> Result<(), String> {
1099 if v.is_finite() && v > 0.0 {
1100 Ok(())
1101 } else {
1102 Err(format!("{name} must be finite and positive"))
1103 }
1104}
1105
1106fn positive_or_zero(name: &str, v: f64) -> Result<(), String> {
1107 if v.is_finite() && v >= 0.0 {
1108 Ok(())
1109 } else {
1110 Err(format!("{name} must be finite and non-negative"))
1111 }
1112}
1113
1114#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1119#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1120#[serde(rename_all = "camelCase")]
1121#[serde(default)]
1122pub struct FaceParams {
1123 pub depths: Depths,
1124 pub stepover: Stepover,
1125 pub pass_angle_deg: f64,
1127 pub pass_extension: f64,
1129 pub zig_zag: bool,
1130}
1131
1132#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1133#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1134#[serde(rename_all = "camelCase")]
1135#[serde(default)]
1136pub struct Pocket2dParams {
1137 pub depths: Depths,
1138 pub stepover: Stepover,
1139 pub offset_pattern: bool,
1141 pub wall_finish: bool,
1143 pub keep_islands: bool,
1145}
1146
1147#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1148#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1149#[serde(rename_all = "camelCase")]
1150#[serde(default)]
1151pub struct Contour2dParams {
1152 pub depths: Depths,
1153 pub outside: bool,
1155 pub roughing_passes: u32,
1157 pub roughing_stepover: f64,
1158 pub finishing_passes: u32,
1160 pub finishing_overlap: f64,
1162 pub tabs: Vec<Tab>,
1163 pub direction: CutDirection,
1164}
1165
1166impl Default for Contour2dParams {
1167 fn default() -> Self {
1168 Self {
1169 depths: Depths::default(),
1170 outside: true,
1171 roughing_passes: 0,
1172 roughing_stepover: 0.0,
1173 finishing_passes: 1,
1174 finishing_overlap: 0.0,
1175 tabs: Vec::new(),
1176 direction: CutDirection::Climb,
1177 }
1178 }
1179}
1180
1181#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1183#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
1184#[serde(rename_all = "camelCase")]
1185#[serde(default)]
1186pub struct Tab {
1187 pub at: f64,
1189 pub width: f64,
1190 pub height: f64,
1191}
1192
1193#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1194#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1195#[serde(rename_all = "camelCase")]
1196#[serde(default)]
1197pub struct Adaptive2dParams {
1198 pub depths: Depths,
1199 pub optimal_load: f64,
1201 pub minimum_cutting_radius: f64,
1202 pub rest_machining: bool,
1204 pub rest_source: RestSource,
1206 pub direction: CutDirection,
1207}
1208
1209impl Default for Adaptive2dParams {
1210 fn default() -> Self {
1211 Self {
1212 depths: Depths::default(),
1213 optimal_load: 1.0,
1214 minimum_cutting_radius: 0.0,
1215 rest_machining: false,
1216 rest_source: RestSource::default(),
1217 direction: CutDirection::Climb,
1218 }
1219 }
1220}
1221
1222#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1223#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1224#[serde(rename_all = "camelCase")]
1225#[serde(default)]
1226pub struct SlotParams {
1227 pub depths: Depths,
1228 pub width: f64,
1230 pub trochoidal: bool,
1231}
1232
1233impl Default for SlotParams {
1234 fn default() -> Self {
1235 Self { depths: Depths::default(), width: 6.0, trochoidal: false }
1236 }
1237}
1238
1239#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1240#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1241#[serde(rename_all = "camelCase")]
1242#[serde(default)]
1243pub struct TraceParams {
1244 pub depths: Depths,
1245 pub side: i8,
1247}
1248
1249#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1250#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1251#[serde(rename_all = "camelCase")]
1252#[serde(default)]
1253pub struct Chamfer2dParams {
1254 pub width: f64,
1256 pub tip_offset: f64,
1258}
1259
1260impl Default for Chamfer2dParams {
1261 fn default() -> Self {
1262 Self { width: 1.0, tip_offset: 0.5 }
1263 }
1264}
1265
1266#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1267#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1268#[serde(rename_all = "camelCase")]
1269#[serde(default)]
1270pub struct EngraveParams {
1271 pub depths: Depths,
1272 pub v_carve: bool,
1274}
1275
1276#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1277#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1278#[serde(rename_all = "camelCase")]
1279#[serde(default)]
1280pub struct BoreParams {
1281 pub depths: Depths,
1282 pub pitch: f64,
1284 pub finish_pass: bool,
1285}
1286
1287#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1288#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1289#[serde(rename_all = "camelCase")]
1290#[serde(default)]
1291pub struct CircularParams {
1292 pub depths: Depths,
1293 pub stepover: Stepover,
1294}
1295
1296#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1297#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1298#[serde(rename_all = "camelCase")]
1299#[serde(default)]
1300pub struct ThreadMillParams {
1301 pub depths: Depths,
1302 pub pitch: f64,
1303 pub diameter: f64,
1305 pub internal: bool,
1306 pub left_hand: bool,
1307 pub passes: u32,
1308}
1309
1310impl Default for ThreadMillParams {
1311 fn default() -> Self {
1312 Self { depths: Depths::default(), pitch: 1.0, diameter: 10.0, internal: true, left_hand: false, passes: 1 }
1313 }
1314}
1315
1316#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1321#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1322#[serde(rename_all = "camelCase")]
1323#[serde(default)]
1324pub struct DrillParams {
1325 pub cycle: DrillCycle,
1326 #[serde(skip_serializing_if = "Option::is_none")]
1328 pub depth: Option<f64>,
1329 pub peck_depth: f64,
1330 pub dwell_seconds: f64,
1331 pub break_through: f64,
1333 pub optimise_order: bool,
1335 pub tip_depth: bool,
1337 pub chip_break_retract: f64,
1339 pub bore_shift: f64,
1341 pub back_bore_clearance: f64,
1343}
1344
1345#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1350#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1351#[serde(rename_all = "camelCase")]
1352#[serde(default)]
1353pub struct Adaptive3dParams {
1354 pub depths: Depths,
1355 pub optimal_load: f64,
1356 pub fine_step_down: f64,
1358 pub rest_machining: bool,
1359 pub rest_source: RestSource,
1361 pub containment: Containment,
1362}
1363
1364impl Default for Adaptive3dParams {
1365 fn default() -> Self {
1366 Self {
1367 depths: Depths::default(),
1368 optimal_load: 1.0,
1369 fine_step_down: 0.0,
1370 rest_machining: false,
1371 rest_source: RestSource::default(),
1372 containment: Containment::default(),
1373 }
1374 }
1375}
1376
1377#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1378#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1379#[serde(rename_all = "camelCase")]
1380#[serde(default)]
1381pub struct Pocket3dParams {
1382 pub depths: Depths,
1383 pub stepover: Stepover,
1384 pub rest_machining: bool,
1385 pub rest_source: RestSource,
1387 pub containment: Containment,
1388}
1389
1390#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1396#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1397#[serde(rename_all = "camelCase")]
1398#[serde(default)]
1399pub struct ParallelParams {
1400 pub stepover: Stepover,
1401 pub pass_angle_deg: f64,
1403 pub zig_zag: bool,
1405 pub sample_step: f64,
1407 pub adaptive_sampling: bool,
1409 pub containment: Containment,
1410 #[serde(skip_serializing_if = "Option::is_none")]
1412 pub floor_z: Option<f64>,
1413}
1414
1415fn default_sample_step() -> f64 {
1416 0.5
1417}
1418
1419impl Default for ParallelParams {
1420 fn default() -> Self {
1421 Self {
1422 stepover: Stepover::default(),
1423 pass_angle_deg: 0.0,
1424 zig_zag: true,
1425 sample_step: default_sample_step(),
1426 adaptive_sampling: true,
1427 containment: Containment::default(),
1428 floor_z: None,
1429 }
1430 }
1431}
1432
1433#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1435#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1436#[serde(rename_all = "camelCase")]
1437#[serde(default)]
1438pub struct Contour3dParams {
1439 pub step_down: f64,
1440 pub adaptive_step_down: bool,
1443 pub fiber_step: f64,
1445 pub direction: CutDirection,
1446 pub containment: Containment,
1447}
1448
1449impl Default for Contour3dParams {
1450 fn default() -> Self {
1451 Self {
1452 step_down: 1.0,
1453 adaptive_step_down: false,
1454 fiber_step: default_sample_step(),
1455 direction: CutDirection::Climb,
1456 containment: Containment::default(),
1457 }
1458 }
1459}
1460
1461#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1462#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1463#[serde(rename_all = "camelCase")]
1464#[serde(default)]
1465pub struct ScallopParams {
1466 pub stepover: Stepover,
1467 pub containment: Containment,
1468 pub inside_out: bool,
1470 #[serde(skip_serializing_if = "Option::is_none")]
1473 pub cusp_height: Option<f64>,
1474}
1475
1476#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1477#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1478#[serde(rename_all = "camelCase")]
1479#[serde(default)]
1480pub struct PencilParams {
1481 pub containment: Containment,
1482 pub bitangency_angle_deg: f64,
1484 pub extra_passes: u32,
1486}
1487
1488#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1489#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1490#[serde(rename_all = "camelCase")]
1491#[serde(default)]
1492pub struct SteepShallowParams {
1493 pub containment: Containment,
1494 pub threshold_angle_deg: f64,
1496 pub steep_step_down: f64,
1497 pub shallow_stepover: Stepover,
1498 pub overlap: f64,
1501}
1502
1503impl Default for SteepShallowParams {
1504 fn default() -> Self {
1505 Self {
1506 containment: Containment::default(),
1507 threshold_angle_deg: 45.0,
1508 steep_step_down: 1.0,
1509 shallow_stepover: Stepover::default(),
1510 overlap: 0.0,
1511 }
1512 }
1513}
1514
1515#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1516#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1517#[serde(rename_all = "camelCase")]
1518#[serde(default)]
1519pub struct RadialParams {
1520 pub containment: Containment,
1521 pub center: super::Point2,
1523 pub angular_step_deg: f64,
1524 pub inner_radius: f64,
1525 #[serde(skip_serializing_if = "Option::is_none")]
1526 pub outer_radius: Option<f64>,
1527}
1528
1529impl Default for RadialParams {
1530 fn default() -> Self {
1531 Self {
1532 containment: Containment::default(),
1533 center: super::Point2::default(),
1534 angular_step_deg: 1.0,
1535 inner_radius: 0.0,
1536 outer_radius: None,
1537 }
1538 }
1539}
1540
1541#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1542#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1543#[serde(rename_all = "camelCase")]
1544#[serde(default)]
1545pub struct SpiralParams {
1546 pub containment: Containment,
1547 pub center: super::Point2,
1548 pub stepover: Stepover,
1549 pub inside_out: bool,
1550}
1551
1552#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1553#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1554#[serde(rename_all = "camelCase")]
1555#[serde(default)]
1556pub struct MorphedSpiralParams {
1557 pub containment: Containment,
1558 pub stepover: Stepover,
1559}
1560
1561#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1563#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1564#[serde(rename_all = "camelCase")]
1565#[serde(default)]
1566pub struct HorizontalParams {
1567 pub containment: Containment,
1568 pub stepover: Stepover,
1569 pub flat_angle_deg: f64,
1571}
1572
1573#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1574#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1575#[serde(rename_all = "camelCase")]
1576#[serde(default)]
1577pub struct RampFinishParams {
1578 pub containment: Containment,
1579 pub step_down: f64,
1580 pub direction: CutDirection,
1581}
1582
1583impl Default for RampFinishParams {
1584 fn default() -> Self {
1585 Self { containment: Containment::default(), step_down: 1.0, direction: CutDirection::Climb }
1586 }
1587}
1588
1589#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1591#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1592#[serde(rename_all = "camelCase")]
1593#[serde(default)]
1594pub struct ProjectParams {
1595 pub containment: Containment,
1596 pub curves: Vec<Vec<super::Point2>>,
1598}
1599
1600#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1601#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1602#[serde(rename_all = "camelCase")]
1603#[serde(default)]
1604pub struct FlowParams {
1605 pub containment: Containment,
1606 pub stepover: Stepover,
1607 pub along_u: bool,
1609}
1610
1611#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1613#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1614#[serde(rename_all = "camelCase", default)]
1615pub struct MorphParams {
1616 pub containment: Containment,
1617 pub stepover: Stepover,
1618 pub first_curve: Vec<super::Point2>,
1619 pub second_curve: Vec<super::Point2>,
1620}
1621
1622#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1624#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1625#[serde(rename_all = "camelCase")]
1626pub enum RestSource { #[default] ReferenceTool, Stock }
1627
1628impl RestSource {
1629 pub const VARIANTS: &'static [&'static str] = &["referenceTool", "stock"];
1631}
1632
1633#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1635#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1636#[serde(rename_all = "camelCase", default)]
1637pub struct RestFinishParams {
1638 pub source: RestSource,
1639 pub containment: Containment,
1640 pub stepover: Stepover,
1641 pub reference_tool: super::ToolId,
1642 pub minimum_rest: f64,
1643 pub pass_angle_deg: f64,
1644}
1645
1646#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1651#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1652#[serde(rename_all = "camelCase")]
1653#[serde(default)]
1654pub struct SwarfParams {
1655 pub tilt_deg: f64,
1657 pub passes: u32,
1659}
1660
1661#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1662#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1663#[serde(rename_all = "camelCase")]
1664#[serde(default)]
1665pub struct MultiAxisContourParams {
1666 pub containment: Containment,
1667 pub lead_deg: f64,
1668 pub tilt_deg: f64,
1669}
1670
1671#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1672#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1673#[serde(rename_all = "camelCase")]
1674#[serde(default)]
1675pub struct RotaryWrapParams {
1676 pub stepover: Stepover,
1677 pub wrap_diameter: f64,
1679 pub axis: String,
1681}
1682
1683fn default_rotary_axis() -> String {
1684 "A".to_string()
1685}
1686
1687impl Default for RotaryWrapParams {
1688 fn default() -> Self {
1689 Self { stepover: Stepover::default(), wrap_diameter: 50.0, axis: default_rotary_axis() }
1690 }
1691}
1692
1693#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1694#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1695#[serde(rename_all = "camelCase")]
1696#[serde(default)]
1697pub struct RotaryFinishParams {
1698 pub axis: String,
1700 pub stock_diameter: f64,
1702 pub stepover: Stepover,
1705 pub spiral: bool,
1707 pub zig_zag: bool,
1709 pub sample_step: f64,
1711 #[serde(skip_serializing_if = "Option::is_none")]
1713 pub range: Option<[f64; 2]>,
1714}
1715
1716impl Default for RotaryFinishParams {
1717 fn default() -> Self {
1718 Self { axis: "A".into(), stock_diameter: 50.0, stepover: Stepover::default(), spiral: false, zig_zag: true, sample_step: 1.0, range: None }
1719 }
1720}
1721
1722impl RotaryFinishParams {
1723 pub fn validate(&self) -> Result<(), String> {
1724 if !matches!(self.axis.as_str(), "A" | "a" | "B" | "b") {
1725 return Err("rotary axis must be A or B".into());
1726 }
1727 if !self.stock_diameter.is_finite() || self.stock_diameter <= 0.0 {
1728 return Err("rotary stock diameter must be positive".into());
1729 }
1730 if !self.stepover.distance.is_finite() || self.stepover.distance <= 0.0 {
1731 return Err("rotary stepover must be positive".into());
1732 }
1733 if !self.sample_step.is_finite() || self.sample_step <= 0.0 {
1734 return Err("rotary sample step must be positive".into());
1735 }
1736 if let Some([lo, hi]) = self.range {
1737 if !lo.is_finite() || !hi.is_finite() || lo >= hi {
1738 return Err("rotary range must be finite with lo < hi".into());
1739 }
1740 }
1741 Ok(())
1742 }
1743}
1744
1745#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1748#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1749#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
1750pub enum AxisMode {
1751 Normal {
1754 #[serde(default)]
1755 lead_deg: f64,
1756 #[serde(default)]
1757 tilt_deg: f64,
1758 },
1759 Point { point: Point3, away: bool },
1762 Line { origin: Point3, direction: Point3, away: bool },
1765 FixedFromVertical { angle_deg: f64, azimuth_deg: f64 },
1767}
1768
1769impl AxisMode {
1770 pub const VARIANTS: &'static [&'static str] = &["normal", "point", "line", "fixedFromVertical"];
1772}
1773
1774impl Default for AxisMode {
1775 fn default() -> Self {
1776 AxisMode::Normal { lead_deg: 0.0, tilt_deg: 0.0 }
1777 }
1778}
1779
1780impl AxisMode {
1781 pub fn validate(&self) -> Result<(), String> {
1782 match self {
1783 AxisMode::Normal { lead_deg, tilt_deg } => {
1784 for (n, v) in [("lead", lead_deg), ("tilt", tilt_deg)] {
1785 if !v.is_finite() || v.abs() >= 90.0 {
1786 return Err(format!("axis {n} must be within (-90, 90) degrees"));
1787 }
1788 }
1789 Ok(())
1790 }
1791 AxisMode::Point { point, .. } => {
1792 if !point.is_finite() {
1793 return Err("axis point is not finite".into());
1794 }
1795 Ok(())
1796 }
1797 AxisMode::Line { origin, direction, .. } => {
1798 if !origin.is_finite() || direction.normalized().is_none() {
1799 return Err("axis line needs a finite origin and a non-zero direction".into());
1800 }
1801 Ok(())
1802 }
1803 AxisMode::FixedFromVertical { angle_deg, azimuth_deg } => {
1804 if !angle_deg.is_finite() || *angle_deg < 0.0 || *angle_deg >= 90.0 || !azimuth_deg.is_finite() {
1805 return Err("fixed axis angle must be in [0, 90) degrees with a finite azimuth".into());
1806 }
1807 Ok(())
1808 }
1809 }
1810 }
1811}
1812
1813#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1815#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1816#[serde(rename_all = "camelCase")]
1817#[serde(default)]
1818pub struct MultiAxisFinishParams {
1819 pub mode: AxisMode,
1820 pub max_axis_step_deg: f64,
1822 pub max_from_vertical_deg: f64,
1824 pub reach_tilt_deg: f64,
1827 #[serde(default, skip_serializing_if = "UnreachablePolicy::is_trim")]
1830 pub unreachable: UnreachablePolicy,
1831}
1832
1833#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1836#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1837#[serde(rename_all = "camelCase")]
1838pub enum UnreachablePolicy {
1839 #[default]
1842 Trim,
1843 Fail,
1845}
1846
1847impl UnreachablePolicy {
1848 pub const VARIANTS: &'static [&'static str] = &["trim", "fail"];
1850
1851 pub fn is_trim(&self) -> bool {
1852 *self == UnreachablePolicy::Trim
1853 }
1854}
1855
1856impl Default for MultiAxisFinishParams {
1857 fn default() -> Self {
1858 Self { mode: AxisMode::default(), max_axis_step_deg: 5.0, max_from_vertical_deg: 60.0, reach_tilt_deg: 30.0, unreachable: UnreachablePolicy::Trim }
1859 }
1860}
1861
1862impl MultiAxisFinishParams {
1863 pub fn validate(&self) -> Result<(), String> {
1864 let bad = |m: &str| Err(format!("multi-axis finish: {m}"));
1865 if let Err(e) = self.mode.validate() {
1866 return bad(&e);
1867 }
1868 if !self.max_axis_step_deg.is_finite() || self.max_axis_step_deg <= 0.0 {
1869 return bad("axis step must be positive");
1870 }
1871 if !self.max_from_vertical_deg.is_finite() || self.max_from_vertical_deg < 0.0 || self.max_from_vertical_deg >= 90.0 {
1872 return bad("angle from vertical must be in [0, 90)");
1873 }
1874 if !self.reach_tilt_deg.is_finite() || self.reach_tilt_deg < 0.0 || self.reach_tilt_deg >= 90.0 {
1875 return bad("reach tilt must be in [0, 90)");
1876 }
1877 Ok(())
1878 }
1879}
1880
1881#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1886#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1887#[serde(rename_all = "camelCase")]
1888#[serde(default)]
1889pub struct TurnFaceParams {
1890 pub step_over: f64,
1891 pub finish_pass: bool,
1892 pub end_x: f64,
1894 pub finish_allowance: f64,
1896}
1897
1898impl Default for TurnFaceParams {
1899 fn default() -> Self {
1900 Self { step_over: 1.0, finish_pass: true, end_x: 0.0, finish_allowance: 0.1 }
1901 }
1902}
1903
1904#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1905#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1906#[serde(rename_all = "camelCase")]
1907#[serde(default)]
1908pub struct TurnProfileParams {
1909 pub depth_of_cut: f64,
1910 pub roughing: bool,
1911 pub finishing: bool,
1912 pub outside: bool,
1914 pub direction: RoughingDirection,
1916}
1917
1918impl Default for TurnProfileParams {
1919 fn default() -> Self {
1920 Self { depth_of_cut: 1.0, roughing: true, finishing: true, outside: true, direction: RoughingDirection::Axial }
1921 }
1922}
1923
1924#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1926#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
1927#[serde(rename_all = "kebab-case")]
1928pub enum RoughingDirection {
1929 #[default]
1930 Axial,
1931 Radial,
1932}
1933
1934impl RoughingDirection {
1935 pub const VARIANTS: &'static [&'static str] = &["axial", "radial"];
1937}
1938
1939#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1940#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1941#[serde(rename_all = "camelCase")]
1942#[serde(default)]
1943pub struct TurnGrooveParams {
1944 pub width: f64,
1945 pub depth: f64,
1946 pub step_over: f64,
1948 pub finish_allowance: f64,
1949 pub finish_pass: bool,
1950}
1951
1952impl Default for TurnGrooveParams {
1953 fn default() -> Self {
1954 Self { width: 3.0, depth: 2.0, step_over: 1.0, finish_allowance: 0.1, finish_pass: true }
1955 }
1956}
1957
1958#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1959#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1960#[serde(rename_all = "camelCase")]
1961#[serde(default)]
1962pub struct TurnThreadParams {
1963 pub pitch: f64,
1964 pub depth: f64,
1965 pub passes: u32,
1966 pub internal: bool,
1967 pub constant_area: bool,
1969 pub infeed_angle_deg: f64,
1971 pub alternate_flanks: bool,
1972 pub spring_passes: u32,
1973}
1974
1975impl Default for TurnThreadParams {
1976 fn default() -> Self {
1977 Self { pitch: 1.5, depth: 0.92, passes: 6, internal: false, constant_area: true, infeed_angle_deg: 0.0, alternate_flanks: false, spring_passes: 0 }
1978 }
1979}
1980
1981#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1982#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1983#[serde(rename_all = "camelCase")]
1984#[serde(default)]
1985pub struct TurnPartOffParams {
1986 pub end_x: f64,
1988 pub chamfer: f64,
1989 pub peck_depth: f64,
1991 pub peck_retract: f64,
1992 pub reduced_feed_radius: f64,
1994 pub reduced_feed_factor: f64,
1995}
1996
1997impl Default for TurnPartOffParams {
1998 fn default() -> Self {
1999 Self { end_x: 0.0, chamfer: 0.0, peck_depth: 0.0, peck_retract: 0.0, reduced_feed_radius: 0.0, reduced_feed_factor: 1.0 }
2000 }
2001}
2002
2003#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2012#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2013#[serde(rename_all = "camelCase")]
2014#[serde(default)]
2015pub struct PatternParams {
2016 pub sources: Vec<OperationId>,
2017 #[serde(rename = "patternKind")]
2019 pub kind: PatternKind,
2020 pub order: PatternOrder,
2021}
2022
2023impl Default for PatternParams {
2024 fn default() -> Self {
2025 Self { sources: Vec::new(), kind: PatternKind::default(), order: PatternOrder::default() }
2026 }
2027}
2028
2029impl PatternParams {
2030 pub fn validate(&self) -> Result<(), String> {
2031 if self.sources.is_empty() || self.sources.iter().any(|s| s.as_str().is_empty()) {
2032 return Err("a pattern needs at least one source operation".into());
2033 }
2034 self.kind.validate()
2035 }
2036}
2037
2038#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2040#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2041#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
2042pub enum PatternKind {
2043 Linear { count: u32, spacing: f64, direction: Point3 },
2045 Circular { count: u32, angle_deg: f64, axis: Point3, origin: Point3 },
2047 Mirror { origin: Point3, normal: Point3 },
2049 WcsOffsets { list: Vec<WorkOffset> },
2051}
2052
2053impl PatternKind {
2054 pub const VARIANTS: &'static [&'static str] = &["linear", "circular", "mirror", "wcsOffsets"];
2056}
2057
2058impl Default for PatternKind {
2059 fn default() -> Self {
2060 PatternKind::Linear { count: 2, spacing: 10.0, direction: Point3::X }
2061 }
2062}
2063
2064impl PatternKind {
2065 pub fn validate(&self) -> Result<(), String> {
2066 match self {
2067 PatternKind::Linear { count, spacing, direction } => {
2068 if *count < 1 {
2069 return Err("linear pattern count must be at least 1".into());
2070 }
2071 positive("pattern spacing", *spacing)?;
2072 if direction.normalized().is_none() {
2073 return Err("linear pattern direction must be a non-zero vector".into());
2074 }
2075 Ok(())
2076 }
2077 PatternKind::Circular { count, angle_deg, axis, origin } => {
2078 if *count < 1 {
2079 return Err("circular pattern count must be at least 1".into());
2080 }
2081 if !angle_deg.is_finite() || *angle_deg == 0.0 {
2082 return Err("circular pattern angle must be finite and non-zero".into());
2083 }
2084 if axis.normalized().is_none() || !origin.is_finite() {
2085 return Err("circular pattern needs a non-zero axis and a finite origin".into());
2086 }
2087 Ok(())
2088 }
2089 PatternKind::Mirror { origin, normal } => {
2090 if normal.normalized().is_none() || !origin.is_finite() {
2091 return Err("mirror pattern needs a non-zero normal and a finite origin".into());
2092 }
2093 Ok(())
2094 }
2095 PatternKind::WcsOffsets { list } => {
2096 if list.is_empty() {
2097 return Err("WCS pattern needs at least one work offset".into());
2098 }
2099 if list.iter().any(|w| w.number == 0 || !w.origin.is_finite()) {
2100 return Err("WCS pattern offsets need a number of at least 1 and a finite origin".into());
2101 }
2102 Ok(())
2103 }
2104 }
2105 }
2106}
2107
2108#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2110#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
2111#[serde(rename_all = "kebab-case")]
2112pub enum PatternOrder {
2113 #[default]
2115 ByInstance,
2116 ByTool,
2118}
2119
2120impl PatternOrder {
2121 pub const VARIANTS: &'static [&'static str] = &["by-instance", "by-tool"];
2123}
2124
2125#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2128#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
2129#[serde(rename_all = "camelCase")]
2130#[serde(default)]
2131pub struct ManualNcParams {
2132 #[serde(rename = "manualKind")]
2134 pub kind: ManualNcKind,
2135}
2136
2137impl ManualNcParams {
2138 pub fn validate(&self) -> Result<(), String> {
2139 match &self.kind {
2140 ManualNcKind::Dwell { seconds } => positive_or_zero("manual dwell", *seconds),
2141 ManualNcKind::ToolChange { tool } if tool.as_str().is_empty() => Err("manual tool change needs a tool".into()),
2142 _ => Ok(()),
2143 }
2144 }
2145}
2146
2147#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2149#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2150#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
2151pub enum ManualNcKind {
2152 Stop,
2153 OptionalStop,
2154 Dwell { seconds: f64 },
2155 Comment { text: String },
2156 PassThrough { text: String },
2157 ToolChange { tool: ToolId },
2158}
2159
2160impl ManualNcKind {
2161 pub const VARIANTS: &'static [&'static str] = &["stop", "optionalStop", "dwell", "comment", "passThrough", "toolChange"];
2163}
2164
2165impl Default for ManualNcKind {
2166 fn default() -> Self {
2167 ManualNcKind::Comment { text: String::new() }
2168 }
2169}