use super::*;
use super::bend::build_bend;
use super::collar::{build_collar, segment_endpoints_2d};
use super::frame::Frame;
use super::plate::build_flat_plate;
pub struct FlatPlacement {
pub id: String,
pub origin: Vec3,
pub u: Vec3,
pub v: Vec3,
pub w: Vec3,
pub outline: Vec<[f64; 2]>,
pub wedge_strips: Vec<WedgeStrip>,
}
pub struct WedgeStrip {
pub a: [f64; 2],
pub b: [f64; 2],
pub reach: f64,
pub bend_id: String,
}
pub fn folded_flat_placements(tree: &SheetTree) -> Result<Vec<FlatPlacement>, String> {
let mut placements = Vec::new();
let root = Frame::from_placement(&tree.root_transform);
collect_placements(&tree.root, root, tree.thickness, None, &mut placements)?;
Ok(placements)
}
fn collect_placements(
flat: &Flat,
frame: Frame,
thickness: f64,
seam: Option<WedgeStrip>,
out: &mut Vec<FlatPlacement>,
) -> Result<(), String> {
let mut placement = FlatPlacement {
id: flat.id.clone(),
origin: frame.o,
u: frame.u,
v: frame.v,
w: frame.w,
outline: flat.densified_outline()?,
wedge_strips: seam.into_iter().collect(),
};
let n = flat.outline.len();
let mut children = Vec::new();
for (index, edge) in flat.edges.iter().enumerate() {
let Some(bend) = &edge.bend else { continue };
let a = flat.outline[index];
let b = flat.outline[(index + 1) % n];
let r_out = bend.mid_radius(thickness) + thickness * 0.5;
placement.wedge_strips.push(WedgeStrip {
a,
b,
reach: r_out,
bend_id: bend.id.clone(),
});
let (_wedge, child_frame) = build_bend(bend, a, b, frame, thickness, 1.0)?;
let seam_len = ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
let child_seam = WedgeStrip {
a: [0.0, seam_len],
b: [0.0, 0.0],
reach: r_out,
bend_id: bend.id.clone(),
};
children.push((&*bend.child, child_frame, child_seam));
}
for hole_bend in &flat.hole_bends {
let HoleBendKind::Straight { child } = &hole_bend.kind else {
continue;
};
let hole = flat.holes.get(hole_bend.hole).ok_or_else(|| {
format!(
"sheet-metal: hole bend `{}` references missing hole {} on flat `{}`",
hole_bend.id, hole_bend.hole, flat.id
)
})?;
let curve = hole.outer.get(hole_bend.segment).ok_or_else(|| {
format!(
"sheet-metal: hole bend `{}` references missing segment {} of hole {}",
hole_bend.id, hole_bend.segment, hole_bend.hole
)
})?;
let (mut a, mut b) = segment_endpoints_2d(curve)?;
if hole_bend.reversed {
std::mem::swap(&mut a, &mut b);
}
let bend = Bend {
id: hole_bend.id.clone(),
angle_deg: hole_bend.angle_deg,
inside_radius: hole_bend.inside_radius,
k_factor: hole_bend.k_factor,
child: child.clone(),
reliefs: Vec::new(),
};
let r_out = bend.mid_radius(thickness) + thickness * 0.5;
placement.wedge_strips.push(WedgeStrip {
a,
b,
reach: r_out,
bend_id: bend.id.clone(),
});
let (_wedge, child_frame) = build_bend(&bend, a, b, frame, thickness, 1.0)?;
let seam_len = ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
let child_seam = WedgeStrip {
a: [0.0, seam_len],
b: [0.0, 0.0],
reach: r_out,
bend_id: bend.id.clone(),
};
children.push((&**child, child_frame, child_seam));
}
out.push(placement);
for (child, child_frame, child_seam) in children {
collect_placements(child, child_frame, thickness, Some(child_seam), out)?;
}
Ok(())
}
pub fn evaluate(tree: &SheetTree, fold: f64) -> Result<BrepSolid, String> {
if tree.thickness <= 0.0 {
return Err(format!(
"sheet-metal: thickness must be positive, got {}",
tree.thickness
));
}
guard_curved_edge_bends(&tree.root)?;
let mut parts: Vec<BrepSolid> = Vec::new();
let root = Frame::from_placement(&tree.root_transform);
emit_flat(&tree.root, root, tree.thickness, fold, &mut parts)?;
let joint_parts = (fold.abs() >= 1e-9 && has_adjacent_bends(&tree.root) && adjacent_bend_fallback_allowed())
.then(|| parts.clone());
let mut solid = match union_all(parts, fold.abs() >= 1e-9) {
Ok(solid) => solid,
Err(error) => match joint_parts {
Some(parts) => crate::boolean_operation_nary_with_options(
&parts,
BooleanOperation::Union,
&folded_union_options(true),
)
.map_err(|_| error)?,
None => return Err(error),
},
};
crate::feature_pipeline::features::common::stamp_derived_edge_names(&mut solid);
for suffix in &tree.name_suffixes {
crate::feature_pipeline::features::common::namespace_copy_names(&mut solid, suffix);
}
Ok(solid)
}
fn adjacent_bend_fallback_allowed() -> bool {
true
}
fn has_adjacent_bends(flat: &Flat) -> bool {
let n = flat.edges.len();
flat.edges.iter().enumerate().any(|(i, edge)| {
edge.bend.as_ref().is_some_and(|bend| {
flat.edges[(i + 1) % n].bend.is_some() || has_adjacent_bends(&bend.child)
})
})
}
fn guard_curved_edge_bends(flat: &Flat) -> Result<(), String> {
for edge in &flat.edges {
let Some(bend) = &edge.bend else { continue };
if flat.outline_curves.contains_key(&edge.id) {
return Err(format!(
"sheet-metal: edge `{}` of flat `{}` is a curved outline edge — \
flanges/hems fold straight edges only",
edge.id, flat.id
));
}
guard_curved_edge_bends(&bend.child)?;
}
for hole_bend in &flat.hole_bends {
if let HoleBendKind::Straight { child } = &hole_bend.kind {
guard_curved_edge_bends(child)?;
}
}
Ok(())
}
fn emit_flat(
flat: &Flat,
frame: Frame,
thickness: f64,
fold: f64,
parts: &mut Vec<BrepSolid>,
) -> Result<(), String> {
parts.push(build_flat_plate(flat, thickness, frame)?);
for (index, edge) in flat.edges.iter().enumerate() {
let Some(bend) = &edge.bend else { continue };
let n = flat.outline.len();
let a = flat.outline[index];
let b = flat.outline[(index + 1) % n];
let (wedge, child_frame) = build_bend(bend, a, b, frame, thickness, fold)?;
if let Some(wedge) = wedge {
parts.push(wedge);
}
emit_flat(&bend.child, child_frame, thickness, fold, parts)?;
}
for hole_bend in &flat.hole_bends {
emit_hole_bend(flat, hole_bend, frame, thickness, fold, parts)?;
}
Ok(())
}
fn emit_hole_bend(
flat: &Flat,
hole_bend: &HoleBend,
frame: Frame,
thickness: f64,
fold: f64,
parts: &mut Vec<BrepSolid>,
) -> Result<(), String> {
let hole = flat.holes.get(hole_bend.hole).ok_or_else(|| {
format!(
"sheet-metal: hole bend `{}` references missing hole {} on flat `{}`",
hole_bend.id, hole_bend.hole, flat.id
)
})?;
match &hole_bend.kind {
HoleBendKind::Straight { child } => {
let curve = hole.outer.get(hole_bend.segment).ok_or_else(|| {
format!(
"sheet-metal: hole bend `{}` references missing segment {} of hole {}",
hole_bend.id, hole_bend.segment, hole_bend.hole
)
})?;
let (mut a, mut b) = segment_endpoints_2d(curve)?;
if hole_bend.reversed {
std::mem::swap(&mut a, &mut b);
}
let bend = Bend {
id: hole_bend.id.clone(),
angle_deg: hole_bend.angle_deg,
inside_radius: hole_bend.inside_radius,
k_factor: hole_bend.k_factor,
child: child.clone(),
reliefs: Vec::new(),
};
let (wedge, child_frame) = build_bend(&bend, a, b, frame, thickness, fold)?;
if let Some(wedge) = wedge {
parts.push(wedge);
}
emit_flat(child, child_frame, thickness, fold, parts)
}
HoleBendKind::Collar { leg } => {
if let Some(collar) =
build_collar(hole_bend, &hole.outer, *leg, frame, thickness, fold)?
{
parts.push(collar);
}
Ok(())
}
}
}
fn folded_union_options(merge_coplanar: bool) -> BooleanOptions {
BooleanOptions {
merge_coplanar_faces: merge_coplanar,
keep_unmerged_name_substrs: vec![
":SIDE:".to_string(),
":END:".to_string(),
":CUTOUT:".to_string(),
":wall:".to_string(),
],
..BooleanOptions::default()
}
}
fn union_all(parts: Vec<BrepSolid>, merge_coplanar: bool) -> Result<BrepSolid, String> {
let options = folded_union_options(merge_coplanar);
let mut current: Option<BrepSolid> = None;
for part in parts {
if part.shells.is_empty() {
continue; }
current = Some(match current.take() {
None => part,
Some(running) => {
let running_handle = crate::register_solid_value(running);
let part_handle = crate::register_solid_value(part);
let folded = crate::with_two_registered_solids(
running_handle,
part_handle,
|run, add| boolean_operation(run, add, BooleanOperation::Union, &options),
);
crate::free_registered_solid(running_handle);
crate::free_registered_solid(part_handle);
folded.map_err(|error| format!("sheet-metal union failed: {error}"))?
}
});
}
current.ok_or_else(|| "sheet-metal: tree produced no geometry".to_string())
}