Skip to main content

u_nesting_cutting/
config.rs

1//! Configuration for cutting path optimization.
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6use crate::bridge::BridgeConfig;
7use crate::leadin::LeadInConfig;
8use crate::thermal::ThermalConfig;
9
10/// Configuration parameters for cutting path optimization.
11#[derive(Debug, Clone)]
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13pub struct CuttingConfig {
14    /// Kerf width (cutting tool width) in the same units as geometry coordinates.
15    /// Used for kerf compensation. Set to 0.0 to disable.
16    pub kerf_width: f64,
17
18    /// Weight factor for pierce count in the cost function.
19    /// Cost = total_rapid_distance + pierce_weight * pierce_count
20    pub pierce_weight: f64,
21
22    /// Maximum number of 2-opt improvement iterations.
23    /// Set to 0 to use only the nearest-neighbor solution.
24    pub max_2opt_iterations: usize,
25
26    /// Wall-clock budget (in milliseconds) for the 2-opt improvement phase.
27    ///
28    /// The 2-opt neighborhood is `O(n^2)` candidate moves per pass, and each
29    /// pass may repeat up to `max_2opt_iterations` times, so on large inputs
30    /// the iteration cap alone can still run for many seconds — blocking the
31    /// (browser main) thread. When this budget is exceeded the optimizer stops
32    /// and returns the best sequence found so far; cut-path order is a
33    /// heuristic, so early termination never invalidates the result.
34    ///
35    /// Set to `0` for no wall-clock limit (bounded only by
36    /// `max_2opt_iterations`). Default: `5000` (5 seconds).
37    pub time_limit_ms: u64,
38
39    /// Machine rapid traverse speed (mm/s or units/s).
40    /// Used only for time estimation, not for optimization.
41    pub rapid_speed: f64,
42
43    /// Machine cutting speed (mm/s or units/s).
44    /// Used only for time estimation, not for optimization.
45    pub cut_speed: f64,
46
47    /// Default cut direction for exterior contours.
48    pub exterior_direction: CutDirectionPreference,
49
50    /// Default cut direction for interior contours (holes).
51    pub interior_direction: CutDirectionPreference,
52
53    /// Home position for the cutting head (start/end point).
54    /// Default is (0.0, 0.0).
55    pub home_position: (f64, f64),
56
57    /// Number of candidate pierce points to evaluate per contour.
58    /// Higher values give better pierce point selection but slower optimization.
59    /// Default: 1 (use nearest point on contour to previous endpoint).
60    pub pierce_candidates: usize,
61
62    /// Tolerance for geometric comparisons.
63    pub tolerance: f64,
64
65    /// Lead-in/lead-out configuration.
66    pub lead_in: LeadInConfig,
67
68    /// Bridge/tab (micro-joint) configuration.
69    pub bridge: BridgeConfig,
70
71    /// Thermal model configuration.
72    pub thermal: ThermalConfig,
73}
74
75/// Preference for cutting direction.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
78pub enum CutDirectionPreference {
79    /// Counter-clockwise (conventional for exterior contours).
80    Ccw,
81    /// Clockwise (conventional for interior/hole contours).
82    Cw,
83    /// Automatically determine based on contour type.
84    Auto,
85}
86
87impl Default for CuttingConfig {
88    fn default() -> Self {
89        Self {
90            kerf_width: 0.0,
91            pierce_weight: 10.0,
92            max_2opt_iterations: 1000,
93            time_limit_ms: 5000,
94            rapid_speed: 1000.0,
95            cut_speed: 100.0,
96            exterior_direction: CutDirectionPreference::Auto,
97            interior_direction: CutDirectionPreference::Auto,
98            home_position: (0.0, 0.0),
99            pierce_candidates: 1,
100            tolerance: 1e-6,
101            lead_in: LeadInConfig::default(),
102            bridge: BridgeConfig::default(),
103            thermal: ThermalConfig::default(),
104        }
105    }
106}
107
108impl CuttingConfig {
109    /// Creates a new default configuration.
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    /// Sets the kerf width.
115    pub fn with_kerf_width(mut self, width: f64) -> Self {
116        self.kerf_width = width;
117        self
118    }
119
120    /// Sets the pierce weight factor.
121    pub fn with_pierce_weight(mut self, weight: f64) -> Self {
122        self.pierce_weight = weight;
123        self
124    }
125
126    /// Sets the maximum 2-opt iterations.
127    pub fn with_max_2opt_iterations(mut self, iterations: usize) -> Self {
128        self.max_2opt_iterations = iterations;
129        self
130    }
131
132    /// Sets the wall-clock budget (ms) for the 2-opt phase. `0` = unlimited.
133    pub fn with_time_limit_ms(mut self, time_limit_ms: u64) -> Self {
134        self.time_limit_ms = time_limit_ms;
135        self
136    }
137
138    /// Sets the home position.
139    pub fn with_home_position(mut self, x: f64, y: f64) -> Self {
140        self.home_position = (x, y);
141        self
142    }
143
144    /// Sets the number of pierce candidates per contour.
145    pub fn with_pierce_candidates(mut self, candidates: usize) -> Self {
146        self.pierce_candidates = candidates.max(1);
147        self
148    }
149
150    /// Sets the lead-in/lead-out configuration.
151    pub fn with_lead_in(mut self, lead_in: LeadInConfig) -> Self {
152        self.lead_in = lead_in;
153        self
154    }
155
156    /// Sets the bridge/tab configuration.
157    pub fn with_bridge(mut self, bridge: BridgeConfig) -> Self {
158        self.bridge = bridge;
159        self
160    }
161
162    /// Sets the thermal model configuration.
163    pub fn with_thermal(mut self, thermal: ThermalConfig) -> Self {
164        self.thermal = thermal;
165        self
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_default_config() {
175        let config = CuttingConfig::default();
176        assert_eq!(config.kerf_width, 0.0);
177        assert_eq!(config.pierce_weight, 10.0);
178        assert_eq!(config.max_2opt_iterations, 1000);
179        assert_eq!(config.time_limit_ms, 5000);
180        assert_eq!(config.home_position, (0.0, 0.0));
181    }
182
183    #[test]
184    fn test_builder() {
185        let config = CuttingConfig::new()
186            .with_kerf_width(0.5)
187            .with_pierce_weight(20.0)
188            .with_home_position(10.0, 10.0);
189
190        assert_eq!(config.kerf_width, 0.5);
191        assert_eq!(config.pierce_weight, 20.0);
192        assert_eq!(config.home_position, (10.0, 10.0));
193    }
194
195    #[test]
196    fn test_pierce_candidates_minimum() {
197        let config = CuttingConfig::new().with_pierce_candidates(0);
198        assert_eq!(config.pierce_candidates, 1);
199    }
200}