Skip to main content

u_nesting_cutting/
bridge.rs

1//! Bridge/tab (micro-joint) placement for cut contours.
2//!
3//! Places small uncut segments (bridges/tabs) along contour edges to prevent
4//! parts from falling through the machine bed during cutting. Bridges enable
5//! chain cutting (continuous path across multiple contours) which reduces
6//! pierce count.
7//!
8//! # Placement Rules
9//!
10//! - Bridges are placed on straight segments only (corner avoidance)
11//! - Minimum clearance from vertices (corners) is enforced
12//! - Bridges are distributed evenly around the contour perimeter
13//! - Number of bridges is determined by contour perimeter and config
14//!
15//! # References
16//!
17//! - Hu et al. (2022), "A robust fast bridging algorithm for laser cutting"
18
19#[cfg(feature = "serde")]
20use serde::{Deserialize, Serialize};
21
22use crate::contour::CutContour;
23use crate::cost::point_distance;
24
25/// Configuration for bridge/tab placement.
26#[derive(Debug, Clone, Copy)]
27#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
28pub struct BridgeConfig {
29    /// Width of each bridge (uncut segment length) in mm.
30    pub bridge_width: f64,
31    /// Maximum spacing between bridges along the perimeter.
32    /// More bridges are added if the spacing exceeds this value.
33    pub max_spacing: f64,
34    /// Minimum number of bridges per contour.
35    pub min_bridges: usize,
36    /// Minimum clearance from vertices (corners) in mm.
37    /// Bridges won't be placed within this distance of a vertex.
38    pub corner_clearance: f64,
39    /// Whether bridge placement is enabled.
40    pub enabled: bool,
41}
42
43impl Default for BridgeConfig {
44    fn default() -> Self {
45        Self {
46            bridge_width: 2.0,
47            max_spacing: 50.0,
48            min_bridges: 2,
49            corner_clearance: 5.0,
50            enabled: false,
51        }
52    }
53}
54
55/// A bridge (micro-joint) placed on a contour edge.
56#[derive(Debug, Clone)]
57pub struct Bridge {
58    /// Start point of the bridge (uncut segment begins here).
59    pub start: (f64, f64),
60    /// End point of the bridge (uncut segment ends here).
61    pub end: (f64, f64),
62    /// Midpoint of the bridge.
63    pub midpoint: (f64, f64),
64    /// Edge index on which the bridge is placed.
65    pub edge_index: usize,
66    /// Parameter along the edge where the bridge center is [0, 1].
67    pub edge_t: f64,
68    /// Distance along the perimeter to the bridge center.
69    pub perimeter_distance: f64,
70}
71
72/// Result of bridge placement for a contour.
73#[derive(Debug, Clone)]
74pub struct BridgePlacement {
75    /// Contour ID.
76    pub contour_id: usize,
77    /// Placed bridges.
78    pub bridges: Vec<Bridge>,
79}
80
81/// Places bridges on a contour according to the configuration.
82///
83/// Bridges are distributed evenly along the perimeter, avoiding vertices
84/// (corners) by the configured clearance distance.
85pub fn place_bridges(contour: &CutContour, config: &BridgeConfig) -> BridgePlacement {
86    if !config.enabled {
87        return BridgePlacement {
88            contour_id: contour.id,
89            bridges: Vec::new(),
90        };
91    }
92
93    let vertices = &contour.vertices;
94    let nv = vertices.len();
95    if nv < 3 {
96        return BridgePlacement {
97            contour_id: contour.id,
98            bridges: Vec::new(),
99        };
100    }
101
102    // Compute edge lengths and cumulative distances
103    let mut edge_lengths = Vec::with_capacity(nv);
104    let mut cumulative = Vec::with_capacity(nv + 1);
105    cumulative.push(0.0);
106
107    for i in 0..nv {
108        let j = (i + 1) % nv;
109        let len = point_distance(vertices[i], vertices[j]);
110        edge_lengths.push(len);
111        cumulative.push(cumulative[i] + len);
112    }
113
114    let perimeter = cumulative[nv];
115    if perimeter < config.bridge_width * 2.0 {
116        return BridgePlacement {
117            contour_id: contour.id,
118            bridges: Vec::new(),
119        };
120    }
121
122    // Determine number of bridges
123    let n_bridges = compute_bridge_count(perimeter, config);
124
125    // Generate candidate positions (equidistant along perimeter)
126    let spacing = perimeter / n_bridges as f64;
127    let half_width = config.bridge_width / 2.0;
128
129    let mut bridges = Vec::with_capacity(n_bridges);
130
131    for k in 0..n_bridges {
132        let target_dist = k as f64 * spacing + spacing / 2.0; // Offset by half spacing
133        let target_dist = target_dist % perimeter;
134
135        // Check corner clearance
136        if !is_clear_of_corners(
137            target_dist,
138            half_width,
139            &cumulative,
140            config.corner_clearance,
141        ) {
142            // Try to shift the bridge position to find a valid spot
143            if let Some(adjusted) = find_valid_position(
144                target_dist,
145                half_width,
146                &cumulative,
147                config.corner_clearance,
148                perimeter,
149            ) {
150                if let Some(bridge) = create_bridge(
151                    adjusted,
152                    half_width,
153                    vertices,
154                    &cumulative,
155                    &edge_lengths,
156                    perimeter,
157                ) {
158                    bridges.push(bridge);
159                }
160            }
161            continue;
162        }
163
164        if let Some(bridge) = create_bridge(
165            target_dist,
166            half_width,
167            vertices,
168            &cumulative,
169            &edge_lengths,
170            perimeter,
171        ) {
172            bridges.push(bridge);
173        }
174    }
175
176    BridgePlacement {
177        contour_id: contour.id,
178        bridges,
179    }
180}
181
182/// Computes the number of bridges needed for a contour.
183fn compute_bridge_count(perimeter: f64, config: &BridgeConfig) -> usize {
184    let count_from_spacing = (perimeter / config.max_spacing).ceil() as usize;
185    count_from_spacing.max(config.min_bridges)
186}
187
188/// Checks if a bridge position is clear of all corners (vertices).
189fn is_clear_of_corners(
190    center_dist: f64,
191    half_width: f64,
192    cumulative: &[f64],
193    clearance: f64,
194) -> bool {
195    let bridge_start = center_dist - half_width;
196    let bridge_end = center_dist + half_width;
197
198    // Check distance from each vertex (cumulative distance)
199    for &vertex_dist in cumulative.iter() {
200        let dist_to_start = (vertex_dist - bridge_start).abs();
201        let dist_to_end = (vertex_dist - bridge_end).abs();
202        let dist_to_center = (vertex_dist - center_dist).abs();
203
204        if dist_to_center < clearance + half_width
205            && (dist_to_start < clearance || dist_to_end < clearance)
206        {
207            return false;
208        }
209    }
210
211    true
212}
213
214/// Tries to find a valid bridge position near the target.
215fn find_valid_position(
216    target: f64,
217    half_width: f64,
218    cumulative: &[f64],
219    clearance: f64,
220    perimeter: f64,
221) -> Option<f64> {
222    // Try shifting in small increments
223    let step = clearance / 2.0;
224    for i in 1..=10 {
225        let offset = i as f64 * step;
226
227        let pos_plus = (target + offset) % perimeter;
228        if is_clear_of_corners(pos_plus, half_width, cumulative, clearance) {
229            return Some(pos_plus);
230        }
231
232        let pos_minus = (target - offset + perimeter) % perimeter;
233        if is_clear_of_corners(pos_minus, half_width, cumulative, clearance) {
234            return Some(pos_minus);
235        }
236    }
237
238    None
239}
240
241/// Creates a Bridge at the given perimeter distance.
242fn create_bridge(
243    center_dist: f64,
244    half_width: f64,
245    vertices: &[(f64, f64)],
246    cumulative: &[f64],
247    edge_lengths: &[f64],
248    perimeter: f64,
249) -> Option<Bridge> {
250    let start_dist = (center_dist - half_width + perimeter) % perimeter;
251    let end_dist = (center_dist + half_width) % perimeter;
252
253    let start = point_at_perimeter_distance(start_dist, vertices, cumulative, edge_lengths)?;
254    let end = point_at_perimeter_distance(end_dist, vertices, cumulative, edge_lengths)?;
255    let (mid, edge_index) =
256        point_at_perimeter_distance_with_edge(center_dist, vertices, cumulative, edge_lengths)?;
257
258    let edge_len = edge_lengths[edge_index];
259    let edge_t = if edge_len > 1e-12 {
260        (center_dist - cumulative[edge_index]) / edge_len
261    } else {
262        0.0
263    };
264
265    Some(Bridge {
266        start,
267        end,
268        midpoint: mid,
269        edge_index,
270        edge_t,
271        perimeter_distance: center_dist,
272    })
273}
274
275/// Returns the point at a given distance along the contour perimeter.
276fn point_at_perimeter_distance(
277    dist: f64,
278    vertices: &[(f64, f64)],
279    cumulative: &[f64],
280    edge_lengths: &[f64],
281) -> Option<(f64, f64)> {
282    point_at_perimeter_distance_with_edge(dist, vertices, cumulative, edge_lengths)
283        .map(|(pt, _)| pt)
284}
285
286/// Returns the point and edge index at a given perimeter distance.
287fn point_at_perimeter_distance_with_edge(
288    dist: f64,
289    vertices: &[(f64, f64)],
290    cumulative: &[f64],
291    edge_lengths: &[f64],
292) -> Option<((f64, f64), usize)> {
293    let nv = vertices.len();
294    let perimeter = cumulative[nv];
295    if perimeter < 1e-12 {
296        return None;
297    }
298
299    let d = dist % perimeter;
300
301    for i in 0..nv {
302        if d >= cumulative[i] && d <= cumulative[i + 1] + 1e-12 {
303            let edge_len = edge_lengths[i];
304            if edge_len < 1e-12 {
305                return Some((vertices[i], i));
306            }
307            let t = (d - cumulative[i]) / edge_len;
308            let t = t.clamp(0.0, 1.0);
309            let j = (i + 1) % nv;
310            let px = vertices[i].0 + t * (vertices[j].0 - vertices[i].0);
311            let py = vertices[i].1 + t * (vertices[j].1 - vertices[i].1);
312            return Some(((px, py), i));
313        }
314    }
315
316    Some((vertices[nv - 1], nv - 1))
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use crate::contour::ContourType;
323
324    fn make_rect(id: usize, w: f64, h: f64) -> CutContour {
325        CutContour {
326            id,
327            geometry_id: format!("part{}", id),
328            instance: 0,
329            contour_type: ContourType::Exterior,
330            vertices: vec![(0.0, 0.0), (w, 0.0), (w, h), (0.0, h)],
331            perimeter: 2.0 * (w + h),
332            centroid: (w / 2.0, h / 2.0),
333        }
334    }
335
336    #[test]
337    fn test_disabled_no_bridges() {
338        let contour = make_rect(0, 100.0, 50.0);
339        let config = BridgeConfig::default(); // enabled = false
340        let result = place_bridges(&contour, &config);
341        assert!(result.bridges.is_empty());
342    }
343
344    #[test]
345    fn test_enabled_places_bridges() {
346        let contour = make_rect(0, 100.0, 50.0);
347        let config = BridgeConfig {
348            enabled: true,
349            ..BridgeConfig::default()
350        };
351        let result = place_bridges(&contour, &config);
352        assert!(
353            result.bridges.len() >= config.min_bridges,
354            "Should place at least {} bridges, got {}",
355            config.min_bridges,
356            result.bridges.len()
357        );
358    }
359
360    #[test]
361    fn test_bridge_count_from_spacing() {
362        let contour = make_rect(0, 100.0, 100.0); // perimeter = 400
363        let config = BridgeConfig {
364            enabled: true,
365            max_spacing: 50.0,
366            min_bridges: 2,
367            ..BridgeConfig::default()
368        };
369        let result = place_bridges(&contour, &config);
370        // perimeter 400 / max_spacing 50 = 8 bridges needed
371        assert!(
372            result.bridges.len() >= 8,
373            "Expected >= 8 bridges for perimeter 400 with spacing 50, got {}",
374            result.bridges.len()
375        );
376    }
377
378    #[test]
379    fn test_bridge_count_min() {
380        let contour = make_rect(0, 20.0, 20.0); // perimeter = 80
381        let config = BridgeConfig {
382            enabled: true,
383            max_spacing: 100.0, // Would give 1 bridge
384            min_bridges: 3,
385            ..BridgeConfig::default()
386        };
387        let result = place_bridges(&contour, &config);
388        assert!(
389            result.bridges.len() >= 3,
390            "Should respect min_bridges=3, got {}",
391            result.bridges.len()
392        );
393    }
394
395    #[test]
396    fn test_bridge_midpoint_on_edge() {
397        let contour = make_rect(0, 100.0, 50.0);
398        let config = BridgeConfig {
399            enabled: true,
400            corner_clearance: 3.0,
401            ..BridgeConfig::default()
402        };
403        let result = place_bridges(&contour, &config);
404
405        for bridge in &result.bridges {
406            // Midpoint should be on the contour boundary
407            let (mx, my) = bridge.midpoint;
408            let on_boundary = (-0.1..=100.1).contains(&mx) && (-0.1..=50.1).contains(&my);
409            assert!(
410                on_boundary,
411                "Bridge midpoint ({}, {}) should be on contour boundary",
412                mx, my
413            );
414        }
415    }
416
417    #[test]
418    fn test_bridge_width() {
419        let contour = make_rect(0, 100.0, 50.0);
420        let config = BridgeConfig {
421            enabled: true,
422            bridge_width: 3.0,
423            corner_clearance: 3.0,
424            ..BridgeConfig::default()
425        };
426        let result = place_bridges(&contour, &config);
427
428        for bridge in &result.bridges {
429            let width = point_distance(bridge.start, bridge.end);
430            // Width should be approximately bridge_width (may differ slightly at corners)
431            assert!(
432                width < config.bridge_width + 1.0,
433                "Bridge width {} should be close to {}",
434                width,
435                config.bridge_width
436            );
437        }
438    }
439
440    #[test]
441    fn test_small_contour_no_bridges() {
442        // Contour too small for bridges
443        let contour = CutContour {
444            id: 0,
445            geometry_id: "tiny".to_string(),
446            instance: 0,
447            contour_type: ContourType::Exterior,
448            vertices: vec![(0.0, 0.0), (1.0, 0.0), (0.5, 0.5)],
449            perimeter: 3.0,
450            centroid: (0.5, 0.17),
451        };
452        let config = BridgeConfig {
453            enabled: true,
454            bridge_width: 2.0,
455            ..BridgeConfig::default()
456        };
457        let result = place_bridges(&contour, &config);
458        assert!(
459            result.bridges.is_empty(),
460            "Too-small contour should get no bridges"
461        );
462    }
463
464    #[test]
465    fn test_default_config() {
466        let config = BridgeConfig::default();
467        assert!(!config.enabled);
468        assert!((config.bridge_width - 2.0).abs() < 1e-10);
469        assert!((config.max_spacing - 50.0).abs() < 1e-10);
470        assert_eq!(config.min_bridges, 2);
471        assert!((config.corner_clearance - 5.0).abs() < 1e-10);
472    }
473
474    #[test]
475    fn test_bridge_edge_index_valid() {
476        let contour = make_rect(0, 100.0, 50.0);
477        let config = BridgeConfig {
478            enabled: true,
479            ..BridgeConfig::default()
480        };
481        let result = place_bridges(&contour, &config);
482
483        for bridge in &result.bridges {
484            assert!(
485                bridge.edge_index < contour.vertices.len(),
486                "Edge index {} should be < vertex count {}",
487                bridge.edge_index,
488                contour.vertices.len()
489            );
490            assert!(bridge.edge_t >= 0.0 && bridge.edge_t <= 1.0);
491        }
492    }
493}