#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::contour::CutContour;
use crate::cost::point_distance;
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BridgeConfig {
pub bridge_width: f64,
pub max_spacing: f64,
pub min_bridges: usize,
pub corner_clearance: f64,
pub enabled: bool,
}
impl Default for BridgeConfig {
fn default() -> Self {
Self {
bridge_width: 2.0,
max_spacing: 50.0,
min_bridges: 2,
corner_clearance: 5.0,
enabled: false,
}
}
}
#[derive(Debug, Clone)]
pub struct Bridge {
pub start: (f64, f64),
pub end: (f64, f64),
pub midpoint: (f64, f64),
pub edge_index: usize,
pub edge_t: f64,
pub perimeter_distance: f64,
}
#[derive(Debug, Clone)]
pub struct BridgePlacement {
pub contour_id: usize,
pub bridges: Vec<Bridge>,
}
pub fn place_bridges(contour: &CutContour, config: &BridgeConfig) -> BridgePlacement {
if !config.enabled {
return BridgePlacement {
contour_id: contour.id,
bridges: Vec::new(),
};
}
let vertices = &contour.vertices;
let nv = vertices.len();
if nv < 3 {
return BridgePlacement {
contour_id: contour.id,
bridges: Vec::new(),
};
}
let mut edge_lengths = Vec::with_capacity(nv);
let mut cumulative = Vec::with_capacity(nv + 1);
cumulative.push(0.0);
for i in 0..nv {
let j = (i + 1) % nv;
let len = point_distance(vertices[i], vertices[j]);
edge_lengths.push(len);
cumulative.push(cumulative[i] + len);
}
let perimeter = cumulative[nv];
if perimeter < config.bridge_width * 2.0 {
return BridgePlacement {
contour_id: contour.id,
bridges: Vec::new(),
};
}
let n_bridges = compute_bridge_count(perimeter, config);
let spacing = perimeter / n_bridges as f64;
let half_width = config.bridge_width / 2.0;
let mut bridges = Vec::with_capacity(n_bridges);
for k in 0..n_bridges {
let target_dist = k as f64 * spacing + spacing / 2.0; let target_dist = target_dist % perimeter;
if !is_clear_of_corners(
target_dist,
half_width,
&cumulative,
config.corner_clearance,
) {
if let Some(adjusted) = find_valid_position(
target_dist,
half_width,
&cumulative,
config.corner_clearance,
perimeter,
) {
if let Some(bridge) = create_bridge(
adjusted,
half_width,
vertices,
&cumulative,
&edge_lengths,
perimeter,
) {
bridges.push(bridge);
}
}
continue;
}
if let Some(bridge) = create_bridge(
target_dist,
half_width,
vertices,
&cumulative,
&edge_lengths,
perimeter,
) {
bridges.push(bridge);
}
}
BridgePlacement {
contour_id: contour.id,
bridges,
}
}
fn compute_bridge_count(perimeter: f64, config: &BridgeConfig) -> usize {
let count_from_spacing = (perimeter / config.max_spacing).ceil() as usize;
count_from_spacing.max(config.min_bridges)
}
fn is_clear_of_corners(
center_dist: f64,
half_width: f64,
cumulative: &[f64],
clearance: f64,
) -> bool {
let bridge_start = center_dist - half_width;
let bridge_end = center_dist + half_width;
for &vertex_dist in cumulative.iter() {
let dist_to_start = (vertex_dist - bridge_start).abs();
let dist_to_end = (vertex_dist - bridge_end).abs();
let dist_to_center = (vertex_dist - center_dist).abs();
if dist_to_center < clearance + half_width
&& (dist_to_start < clearance || dist_to_end < clearance)
{
return false;
}
}
true
}
fn find_valid_position(
target: f64,
half_width: f64,
cumulative: &[f64],
clearance: f64,
perimeter: f64,
) -> Option<f64> {
let step = clearance / 2.0;
for i in 1..=10 {
let offset = i as f64 * step;
let pos_plus = (target + offset) % perimeter;
if is_clear_of_corners(pos_plus, half_width, cumulative, clearance) {
return Some(pos_plus);
}
let pos_minus = (target - offset + perimeter) % perimeter;
if is_clear_of_corners(pos_minus, half_width, cumulative, clearance) {
return Some(pos_minus);
}
}
None
}
fn create_bridge(
center_dist: f64,
half_width: f64,
vertices: &[(f64, f64)],
cumulative: &[f64],
edge_lengths: &[f64],
perimeter: f64,
) -> Option<Bridge> {
let start_dist = (center_dist - half_width + perimeter) % perimeter;
let end_dist = (center_dist + half_width) % perimeter;
let start = point_at_perimeter_distance(start_dist, vertices, cumulative, edge_lengths)?;
let end = point_at_perimeter_distance(end_dist, vertices, cumulative, edge_lengths)?;
let (mid, edge_index) =
point_at_perimeter_distance_with_edge(center_dist, vertices, cumulative, edge_lengths)?;
let edge_len = edge_lengths[edge_index];
let edge_t = if edge_len > 1e-12 {
(center_dist - cumulative[edge_index]) / edge_len
} else {
0.0
};
Some(Bridge {
start,
end,
midpoint: mid,
edge_index,
edge_t,
perimeter_distance: center_dist,
})
}
fn point_at_perimeter_distance(
dist: f64,
vertices: &[(f64, f64)],
cumulative: &[f64],
edge_lengths: &[f64],
) -> Option<(f64, f64)> {
point_at_perimeter_distance_with_edge(dist, vertices, cumulative, edge_lengths)
.map(|(pt, _)| pt)
}
fn point_at_perimeter_distance_with_edge(
dist: f64,
vertices: &[(f64, f64)],
cumulative: &[f64],
edge_lengths: &[f64],
) -> Option<((f64, f64), usize)> {
let nv = vertices.len();
let perimeter = cumulative[nv];
if perimeter < 1e-12 {
return None;
}
let d = dist % perimeter;
for i in 0..nv {
if d >= cumulative[i] && d <= cumulative[i + 1] + 1e-12 {
let edge_len = edge_lengths[i];
if edge_len < 1e-12 {
return Some((vertices[i], i));
}
let t = (d - cumulative[i]) / edge_len;
let t = t.clamp(0.0, 1.0);
let j = (i + 1) % nv;
let px = vertices[i].0 + t * (vertices[j].0 - vertices[i].0);
let py = vertices[i].1 + t * (vertices[j].1 - vertices[i].1);
return Some(((px, py), i));
}
}
Some((vertices[nv - 1], nv - 1))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contour::ContourType;
fn make_rect(id: usize, w: f64, h: f64) -> CutContour {
CutContour {
id,
geometry_id: format!("part{}", id),
instance: 0,
contour_type: ContourType::Exterior,
vertices: vec![(0.0, 0.0), (w, 0.0), (w, h), (0.0, h)],
perimeter: 2.0 * (w + h),
centroid: (w / 2.0, h / 2.0),
}
}
#[test]
fn test_disabled_no_bridges() {
let contour = make_rect(0, 100.0, 50.0);
let config = BridgeConfig::default(); let result = place_bridges(&contour, &config);
assert!(result.bridges.is_empty());
}
#[test]
fn test_enabled_places_bridges() {
let contour = make_rect(0, 100.0, 50.0);
let config = BridgeConfig {
enabled: true,
..BridgeConfig::default()
};
let result = place_bridges(&contour, &config);
assert!(
result.bridges.len() >= config.min_bridges,
"Should place at least {} bridges, got {}",
config.min_bridges,
result.bridges.len()
);
}
#[test]
fn test_bridge_count_from_spacing() {
let contour = make_rect(0, 100.0, 100.0); let config = BridgeConfig {
enabled: true,
max_spacing: 50.0,
min_bridges: 2,
..BridgeConfig::default()
};
let result = place_bridges(&contour, &config);
assert!(
result.bridges.len() >= 8,
"Expected >= 8 bridges for perimeter 400 with spacing 50, got {}",
result.bridges.len()
);
}
#[test]
fn test_bridge_count_min() {
let contour = make_rect(0, 20.0, 20.0); let config = BridgeConfig {
enabled: true,
max_spacing: 100.0, min_bridges: 3,
..BridgeConfig::default()
};
let result = place_bridges(&contour, &config);
assert!(
result.bridges.len() >= 3,
"Should respect min_bridges=3, got {}",
result.bridges.len()
);
}
#[test]
fn test_bridge_midpoint_on_edge() {
let contour = make_rect(0, 100.0, 50.0);
let config = BridgeConfig {
enabled: true,
corner_clearance: 3.0,
..BridgeConfig::default()
};
let result = place_bridges(&contour, &config);
for bridge in &result.bridges {
let (mx, my) = bridge.midpoint;
let on_boundary = (-0.1..=100.1).contains(&mx) && (-0.1..=50.1).contains(&my);
assert!(
on_boundary,
"Bridge midpoint ({}, {}) should be on contour boundary",
mx, my
);
}
}
#[test]
fn test_bridge_width() {
let contour = make_rect(0, 100.0, 50.0);
let config = BridgeConfig {
enabled: true,
bridge_width: 3.0,
corner_clearance: 3.0,
..BridgeConfig::default()
};
let result = place_bridges(&contour, &config);
for bridge in &result.bridges {
let width = point_distance(bridge.start, bridge.end);
assert!(
width < config.bridge_width + 1.0,
"Bridge width {} should be close to {}",
width,
config.bridge_width
);
}
}
#[test]
fn test_small_contour_no_bridges() {
let contour = CutContour {
id: 0,
geometry_id: "tiny".to_string(),
instance: 0,
contour_type: ContourType::Exterior,
vertices: vec![(0.0, 0.0), (1.0, 0.0), (0.5, 0.5)],
perimeter: 3.0,
centroid: (0.5, 0.17),
};
let config = BridgeConfig {
enabled: true,
bridge_width: 2.0,
..BridgeConfig::default()
};
let result = place_bridges(&contour, &config);
assert!(
result.bridges.is_empty(),
"Too-small contour should get no bridges"
);
}
#[test]
fn test_default_config() {
let config = BridgeConfig::default();
assert!(!config.enabled);
assert!((config.bridge_width - 2.0).abs() < 1e-10);
assert!((config.max_spacing - 50.0).abs() < 1e-10);
assert_eq!(config.min_bridges, 2);
assert!((config.corner_clearance - 5.0).abs() < 1e-10);
}
#[test]
fn test_bridge_edge_index_valid() {
let contour = make_rect(0, 100.0, 50.0);
let config = BridgeConfig {
enabled: true,
..BridgeConfig::default()
};
let result = place_bridges(&contour, &config);
for bridge in &result.bridges {
assert!(
bridge.edge_index < contour.vertices.len(),
"Edge index {} should be < vertex count {}",
bridge.edge_index,
contour.vertices.len()
);
assert!(bridge.edge_t >= 0.0 && bridge.edge_t <= 1.0);
}
}
}