use super::*;
use super::frame::Frame;
pub(super) fn build_flat_plate(flat: &Flat, thickness: f64, frame: Frame) -> Result<BrepSolid, String> {
let outline = &flat.outline;
let n = outline.len();
if n < 3 {
return Err(format!(
"sheet-metal: flat `{}` outline needs >= 3 points, got {n}",
flat.id
));
}
let half_t = thickness * 0.5;
let drop = Vec3::new(0.0, 0.0, -half_t);
let mut profile = Vec::with_capacity(n);
for i in 0..n {
let a = outline[i];
let b = outline[(i + 1) % n];
let over = flat
.edges
.get(i)
.and_then(|edge| flat.outline_curves.get(&edge.id));
if let Some(curve) = over {
let domain = curve.domain()?;
let start = curve.evaluate(domain[0])?;
let end = curve.evaluate(domain[1])?;
let scale = (b[0] - a[0]).hypot(b[1] - a[1]).max(1.0);
let miss = |p: Vec3, v: [f64; 2]| (p.x - v[0]).hypot(p.y - v[1]) > 1e-6 * scale;
if miss(start, a) || miss(end, b) || start.z.abs().max(end.z.abs()) > 1e-6 {
return Err(format!(
"sheet-metal: flat `{}` segment {i} (`{}`): exact outline curve no longer \
spans its outline vertices — the outline was reshaped out from under it",
flat.id,
flat.edges[i].id
));
}
profile.push(crate::feature_pipeline::features::common::translate_curve(
curve, drop,
)?);
} else {
profile.push(make_line(
Vec3::new(a[0], a[1], -half_t),
Vec3::new(b[0], b[1], -half_t),
)?);
}
}
let mut solid = extrude_profile_brep(&profile, Vec3::new(0.0, 0.0, 1.0), thickness)?;
let faces = &mut solid
.shells
.get_mut(0)
.ok_or("sheet-metal: flat plate produced no shell")?
.faces;
if faces.len() != n + 2 {
return Err(format!(
"sheet-metal: flat `{}` plate produced {} faces, expected {}",
flat.id,
faces.len(),
n + 2
));
}
for (index, face) in faces.iter_mut().take(n).enumerate() {
let edge_id = flat
.edges
.get(index)
.map(|edge| edge.id.clone())
.filter(|id| !id.is_empty())
.unwrap_or_else(|| index.to_string());
face.name = Some(format!("{}:SIDE:{edge_id}", flat.id));
}
faces[n].name = Some(format!("{}:B", flat.id));
faces[n + 1].name = Some(format!("{}:A", flat.id));
let solid = subtract_holes(solid, flat, thickness, half_t)?;
transform_brep(&solid, frame.affine()?, false)
}
fn subtract_holes(
plate: BrepSolid,
flat: &Flat,
thickness: f64,
half_t: f64,
) -> Result<BrepSolid, String> {
if flat.holes.is_empty() {
return Ok(plate);
}
let options = BooleanOptions {
merge_coplanar_faces: true,
..BooleanOptions::default()
};
let mut current = plate;
for (hole_index, hole) in flat.holes.iter().enumerate() {
if hole.outer.is_empty() {
continue; }
let eps = 1e-9 * thickness.max(1.0);
let bottom = match hole.z_min {
Some(z) if z > -half_t + eps => z,
_ => -half_t - thickness,
};
let top = match hole.z_max {
Some(z) if z < half_t - eps => z,
_ => half_t + thickness,
};
if bottom >= half_t - eps || top <= -half_t + eps || top - bottom <= eps {
continue; }
let drop = Vec3::new(0.0, 0.0, bottom);
let mut profile = hole
.outer
.iter()
.map(|curve| crate::feature_pipeline::features::common::translate_curve(curve, drop))
.collect::<Result<Vec<_>, _>>()?;
if profile.len() == 1 {
let single = profile.pop().expect("one curve");
let domain = single.domain()?;
let (first, second) = single.split((domain[0] + domain[1]) * 0.5)?;
profile.push(first);
profile.push(second);
}
let mut cutter = extrude_profile_brep(&profile, Vec3::new(0.0, 0.0, 1.0), top - bottom)?;
let side_count = profile.len();
for (face_index, face) in cutter
.shells
.get_mut(0)
.map(|shell| shell.faces.iter_mut())
.into_iter()
.flatten()
.enumerate()
{
if face.name.is_none() {
face.name = Some(if face_index < side_count {
format!("{}:CUTOUT:{hole_index}:{face_index}", flat.id)
} else {
format!("{}:CUTOUT:{hole_index}", flat.id)
});
}
}
for (island_index, island) in hole.islands.iter().enumerate() {
if island.is_empty() {
continue; }
let island_drop = Vec3::new(0.0, 0.0, bottom - thickness * 0.5);
let mut island_profile = island
.iter()
.map(|curve| {
crate::feature_pipeline::features::common::translate_curve(curve, island_drop)
})
.collect::<Result<Vec<_>, _>>()?;
if island_profile.len() == 1 {
let single = island_profile.pop().expect("one curve");
let domain = single.domain()?;
let (first, second) = single.split((domain[0] + domain[1]) * 0.5)?;
island_profile.push(first);
island_profile.push(second);
}
let mut island_prism = extrude_profile_brep(
&island_profile,
Vec3::new(0.0, 0.0, 1.0),
(top - bottom) + thickness,
)?;
for face in island_prism
.shells
.get_mut(0)
.map(|shell| shell.faces.iter_mut())
.into_iter()
.flatten()
{
if face.name.is_none() {
face.name =
Some(format!("{}:CUTOUT:{hole_index}:ISLAND:{island_index}", flat.id));
}
}
let cutter_handle = crate::register_solid_value(cutter);
let island_handle = crate::register_solid_value(island_prism);
let carved =
crate::with_two_registered_solids(cutter_handle, island_handle, |body, tool| {
boolean_operation(body, tool, BooleanOperation::Subtract, &options)
});
crate::free_registered_solid(cutter_handle);
crate::free_registered_solid(island_handle);
cutter =
carved.map_err(|error| format!("sheet-metal island carve failed: {error}"))?;
}
let current_handle = crate::register_solid_value(current);
let cutter_handle = crate::register_solid_value(cutter);
let cut = crate::with_two_registered_solids(current_handle, cutter_handle, |body, tool| {
boolean_operation(body, tool, BooleanOperation::Subtract, &options)
});
crate::free_registered_solid(current_handle);
crate::free_registered_solid(cutter_handle);
current = cut.map_err(|error| format!("sheet-metal hole cut failed: {error}"))?;
}
Ok(current)
}