use super::tree::{Edge, Flat, HoleBendKind, SheetTree};
use crate::{point_in_polygon, segment_intersection, NurbsCurve, Vec2};
type Seg = ([f64; 2], [f64; 2]);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LoopRole {
Cut,
Fold,
BendInternal,
}
#[derive(Clone, Debug)]
pub struct Polyline {
pub points: Vec<[f64; 2]>,
pub closed: bool,
pub role: LoopRole,
}
#[derive(Clone, Debug)]
pub struct Annotation {
pub pos: [f64; 2],
pub rotation_deg: f64,
pub text: String,
}
#[derive(Clone, Debug)]
pub struct FlatPattern {
pub polylines: Vec<Polyline>,
pub annotations: Vec<Annotation>,
}
#[derive(Clone, Copy)]
struct Placement {
o: [f64; 2],
u: [f64; 2],
v: [f64; 2],
}
impl Placement {
fn identity() -> Self {
Placement { o: [0.0, 0.0], u: [1.0, 0.0], v: [0.0, 1.0] }
}
fn map(&self, p: [f64; 2]) -> [f64; 2] {
[
self.o[0] + p[0] * self.u[0] + p[1] * self.v[0],
self.o[1] + p[0] * self.u[1] + p[1] * self.v[1],
]
}
fn map_dir(&self, d: [f64; 2]) -> [f64; 2] {
[
d[0] * self.u[0] + d[1] * self.v[0],
d[0] * self.u[1] + d[1] * self.v[1],
]
}
}
pub fn build(tree: &SheetTree) -> Result<FlatPattern, String> {
if tree.thickness <= 0.0 {
return Err(format!(
"sheet-metal flat pattern: thickness must be positive, got {}",
tree.thickness
));
}
let mut out = FlatPattern { polylines: Vec::new(), annotations: Vec::new() };
walk(&tree.root, Placement::identity(), tree.thickness, None, false, &mut out)?;
if out.polylines.is_empty() {
return Err("sheet-metal flat pattern: tree produced no geometry".into());
}
Ok(out)
}
fn walk(
flat: &Flat,
place: Placement,
thickness: f64,
seam: Option<Seg>,
seam_relieved: bool,
out: &mut FlatPattern,
) -> Result<(), String> {
let trimmed;
let flat = if flat.end_cuts.is_empty() {
flat
} else {
trimmed = midplane_trimmed(flat);
&trimmed
};
let n = flat.outline.len();
let region = Region::sample(flat)?;
emit_outline(flat, ®ion, place, seam, seam_relieved, out)?;
for hole in &flat.holes {
emit_loop(&hole.outer, ®ion, place, out)?;
for island in &hole.islands {
emit_loop(island, ®ion, place, out)?;
}
}
let mut children: Vec<(&Flat, Placement, Seg, bool)> = Vec::new();
for index in 0..n {
let Some(bend) = flat.edges.get(index).and_then(|edge| edge.bend.as_ref()) else {
continue;
};
let a = flat.outline[index];
let b = flat.outline[(index + 1) % n];
let allowance = bend.allowance(thickness);
emit_bend(
a,
b,
allowance,
bend.angle_deg,
bend.inside_radius,
&bend.child,
&bend.reliefs,
place,
&mut children,
out,
)?;
}
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 flat pattern: 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 flat pattern: hole bend `{}` references missing segment {} of hole {}",
hole_bend.id, hole_bend.segment, hole_bend.hole
)
})?;
let (mut a, mut b) = curve_endpoints_2d(curve)?;
if hole_bend.reversed {
std::mem::swap(&mut a, &mut b);
}
let allowance = bend_allowance(
hole_bend.angle_deg,
hole_bend.inside_radius,
hole_bend.k_factor,
thickness,
);
emit_bend(
a,
b,
allowance,
hole_bend.angle_deg,
hole_bend.inside_radius,
child,
&[],
place,
&mut children,
out,
)?;
}
for (child, child_place, child_seam, relieved) in children {
walk(child, child_place, thickness, Some(child_seam), relieved, out)?;
}
Ok(())
}
fn midplane_trimmed(flat: &Flat) -> Flat {
let mut trimmed = flat.clone();
trimmed.end_cuts.clear();
if !flat.outline_curves.is_empty() || flat.edges.len() != flat.outline.len() {
return trimmed;
}
for (index, cut) in flat.end_cuts.iter().enumerate() {
let [nx, ny, nz] = cut.normal;
if nx.hypot(ny) <= 1e-12 {
continue;
}
let k = nx * cut.point[0] + ny * cut.point[1] + nz * cut.point[2];
let side = |p: [f64; 2]| nx * p[0] + ny * p[1] - k;
let n = trimmed.outline.len();
let mut outline = Vec::with_capacity(n + 1);
let mut edges = Vec::with_capacity(n + 1);
for i in 0..n {
let (p, q) = (trimmed.outline[i], trimmed.outline[(i + 1) % n]);
let (sp, sq) = (side(p), side(q));
if sp <= 0.0 {
outline.push(p);
edges.push(trimmed.edges[i].clone());
}
if (sp <= 0.0) != (sq <= 0.0) {
let t = sp / (sp - sq);
outline.push([p[0] + t * (q[0] - p[0]), p[1] + t * (q[1] - p[1])]);
edges.push(if sp <= 0.0 {
Edge { id: format!("cut{index}"), bend: None }
} else {
trimmed.edges[i].clone()
});
}
}
if outline.len() >= 3 {
trimmed.outline = outline;
trimmed.edges = edges;
}
}
trimmed
}
fn emit_outline(
flat: &Flat,
region: &Region,
place: Placement,
seam: Option<Seg>,
seam_relieved: bool,
out: &mut FlatPattern,
) -> Result<(), String> {
let n = flat.outline.len();
if n == 0 {
return Ok(());
}
let suppressed: Vec<bool> = (0..n)
.map(|i| {
let is_bend = flat
.edges
.get(i)
.and_then(|edge| edge.bend.as_ref())
.is_some();
let is_seam = seam.is_some_and(|s| {
let pa = place.map(flat.outline[i]);
let pb = place.map(flat.outline[(i + 1) % n]);
seg_matches(pa, pb, s)
});
is_bend || is_seam
})
.collect();
for (i, &is_fold) in suppressed.iter().enumerate() {
let relieved = match flat.edges.get(i).and_then(|edge| edge.bend.as_ref()) {
Some(bend) => !bend.reliefs.is_empty(),
None => seam_relieved,
};
if is_fold && !relieved {
region.check_fold_edge_uncut(
flat.outline[i],
flat.outline[(i + 1) % n],
&flat.id,
flat.edges.get(i).map_or("", |edge| edge.id.as_str()),
)?;
}
}
if suppressed.iter().all(|&drop| !drop) {
let outline = flat.densified_outline()?;
push_clipped(&outline, true, region, place, LoopRole::Cut, out);
return Ok(());
}
let start = suppressed.iter().position(|&drop| drop).unwrap();
let mut run: Vec<[f64; 2]> = Vec::new();
for step in 0..n {
let i = (start + 1 + step) % n;
if suppressed[i] {
flush_run(&mut run, region, place, out);
} else {
let pts = edge_points(flat, i)?;
if run.is_empty() {
run.extend(pts);
} else {
run.extend(pts.into_iter().skip(1));
}
}
}
flush_run(&mut run, region, place, out);
Ok(())
}
fn flush_run(run: &mut Vec<[f64; 2]>, region: &Region, place: Placement, out: &mut FlatPattern) {
if run.len() >= 2 {
push_clipped(run, false, region, place, LoopRole::Cut, out);
}
run.clear();
}
fn emit_loop(
curves: &[NurbsCurve],
region: &Region,
place: Placement,
out: &mut FlatPattern,
) -> Result<(), String> {
let points = sample_loop(curves)?;
push_clipped(&points, true, region, place, LoopRole::Cut, out);
Ok(())
}
fn push_clipped(
points: &[[f64; 2]],
closed: bool,
region: &Region,
place: Placement,
role: LoopRole,
out: &mut FlatPattern,
) {
for (run, run_closed) in region.clip(points, closed) {
if run.len() < 2 || (run_closed && run.len() < 3) {
continue;
}
out.polylines.push(Polyline {
points: run.iter().map(|p| place.map(*p)).collect(),
closed: run_closed,
role,
});
}
}
fn edge_points(flat: &Flat, i: usize) -> Result<Vec<[f64; 2]>, String> {
let n = flat.outline.len();
let start = flat.outline[i];
let end = flat.outline[(i + 1) % n];
match flat
.edges
.get(i)
.and_then(|edge| flat.outline_curves.get(&edge.id))
{
Some(curve) => {
let [t0, t1] = curve.domain()?;
let mut pts = Vec::with_capacity(17);
for step in 0..=16 {
let t = t0 + (t1 - t0) * (step as f64) / 16.0;
let p = curve.evaluate(t)?;
pts.push([p.x, p.y]);
}
Ok(pts)
}
None => Ok(vec![start, end]),
}
}
fn seg_matches(pa: [f64; 2], pb: [f64; 2], seg: Seg) -> bool {
let (s0, s1) = seg;
(near(pa, s0) && near(pb, s1)) || (near(pa, s1) && near(pb, s0))
}
fn near(p: [f64; 2], q: [f64; 2]) -> bool {
(p[0] - q[0]).abs() < 1e-6 && (p[1] - q[1]).abs() < 1e-6
}
fn emit_bend<'a>(
a: [f64; 2],
b: [f64; 2],
allowance: f64,
angle_deg: f64,
inside_radius: f64,
child: &'a Flat,
reliefs: &[super::tree::Hole],
place: Placement,
children: &mut Vec<(&'a Flat, Placement, Seg, bool)>,
out: &mut FlatPattern,
) -> Result<(), String> {
let e = normalized([b[0] - a[0], b[1] - a[1]]);
if e == [0.0, 0.0] {
return Ok(()); }
let out_dir = [e[1], -e[0]]; let a2 = [a[0] + out_dir[0] * allowance, a[1] + out_dir[1] * allowance];
let b2 = [b[0] + out_dir[0] * allowance, b[1] + out_dir[1] * allowance];
if allowance > 1e-9 && !reliefs.is_empty() {
let length = ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
let mut holes = Vec::with_capacity(reliefs.len());
for relief in reliefs {
holes.push((Ring::new(sample_loop(&relief.outer)?), Vec::new()));
}
let strip = Region {
outline: Ring::new(vec![[0.0, 0.0], [allowance, 0.0], [allowance, length], [0.0, length]]),
holes,
};
let band = Placement { o: place.map(a), u: place.map_dir(out_dir), v: place.map_dir(e) };
let lines = [
([0.0, 0.0], [allowance, 0.0], LoopRole::Cut),
([0.0, length], [allowance, length], LoopRole::Cut),
([0.0, 0.0], [0.0, length], LoopRole::BendInternal),
([allowance, 0.0], [allowance, length], LoopRole::BendInternal),
];
for (p, q, role) in lines {
push_clipped(&[p, q], false, &strip, band, role, out);
}
let half = allowance * 0.5;
for (p, q) in strip.split([half, 0.0], [half, length]) {
if strip.material([(p[0] + q[0]) * 0.5, (p[1] + q[1]) * 0.5]) {
out.polylines.push(open_line(band.map(p), band.map(q), LoopRole::Fold));
}
}
for relief in reliefs {
emit_loop(&relief.outer, &strip, band, out)?;
}
let mid = band.map([half, length * 0.5]);
let dir = place.map_dir(e);
out.annotations.push(Annotation {
pos: mid,
rotation_deg: readable_angle(dir[1].atan2(dir[0]).to_degrees()),
text: bend_label(angle_deg, inside_radius),
});
} else if allowance > 1e-9 {
out.polylines.push(open_line(place.map(a), place.map(a2), LoopRole::Cut));
out.polylines.push(open_line(place.map(b), place.map(b2), LoopRole::Cut));
out.polylines
.push(open_line(place.map(a), place.map(b), LoopRole::BendInternal));
out.polylines
.push(open_line(place.map(a2), place.map(b2), LoopRole::BendInternal));
let half = allowance * 0.5;
let c_a = [a[0] + out_dir[0] * half, a[1] + out_dir[1] * half];
let c_b = [b[0] + out_dir[0] * half, b[1] + out_dir[1] * half];
out.polylines
.push(open_line(place.map(c_a), place.map(c_b), LoopRole::Fold));
let mid = place.map([(c_a[0] + c_b[0]) * 0.5, (c_a[1] + c_b[1]) * 0.5]);
let dir = place.map_dir(e);
let rotation_deg = readable_angle(dir[1].atan2(dir[0]).to_degrees());
out.annotations.push(Annotation {
pos: mid,
rotation_deg,
text: bend_label(angle_deg, inside_radius),
});
}
let seam = (place.map(a2), place.map(b2));
children.push((
child,
Placement {
o: place.map(a2),
u: place.map_dir(out_dir),
v: place.map_dir(e),
},
seam,
!reliefs.is_empty(),
));
Ok(())
}
fn open_line(p0: [f64; 2], p1: [f64; 2], role: LoopRole) -> Polyline {
Polyline { points: vec![p0, p1], closed: false, role }
}
fn bend_label(angle_deg: f64, inside_radius: f64) -> String {
let dir = if angle_deg < 0.0 { "UP" } else { "DOWN" };
format!(
"{dir} {}° R{}",
trim_number(angle_deg.abs()),
trim_number(inside_radius)
)
}
fn trim_number(v: f64) -> String {
let s = format!("{v:.2}");
let trimmed = s.trim_end_matches('0').trim_end_matches('.');
trimmed.to_string()
}
fn readable_angle(mut deg: f64) -> f64 {
deg %= 360.0;
if deg > 180.0 {
deg -= 360.0;
} else if deg <= -180.0 {
deg += 360.0;
}
if deg > 90.0 {
deg -= 180.0;
} else if deg <= -90.0 {
deg += 180.0;
}
deg
}
fn sample_loop(curves: &[NurbsCurve]) -> Result<Vec<[f64; 2]>, String> {
let mut points = Vec::new();
for curve in curves {
let [t0, t1] = curve.domain()?;
for step in 0..16 {
let t = t0 + (t1 - t0) * (step as f64) / 16.0;
let p = curve.evaluate(t)?;
points.push([p.x, p.y]);
}
}
if points.len() < 3 {
return Err("sheet-metal flat pattern: degenerate hole loop".into());
}
Ok(points)
}
fn curve_endpoints_2d(curve: &NurbsCurve) -> Result<([f64; 2], [f64; 2]), String> {
let [t0, t1] = curve.domain()?;
let p0 = curve.evaluate(t0)?;
let p1 = curve.evaluate(t1)?;
Ok(([p0.x, p0.y], [p1.x, p1.y]))
}
fn bend_allowance(angle_deg: f64, inside_radius: f64, k_factor: f64, thickness: f64) -> f64 {
let mid = (inside_radius + thickness * 0.5).max(1e-6);
let neutral = mid + (k_factor - 0.5) * thickness;
angle_deg.to_radians().abs() * neutral
}
const CLIP_TOL: f64 = 1e-9;
const PROBE_EPS: f64 = 1e-6;
struct Region {
outline: Ring,
holes: Vec<(Ring, Vec<Ring>)>,
}
struct Ring {
points: Vec<[f64; 2]>,
ring: Vec<Vec2>,
min: [f64; 2],
max: [f64; 2],
}
impl Ring {
fn new(points: Vec<[f64; 2]>) -> Self {
let mut min = [f64::INFINITY; 2];
let mut max = [f64::NEG_INFINITY; 2];
for p in &points {
for axis in 0..2 {
min[axis] = min[axis].min(p[axis]);
max[axis] = max[axis].max(p[axis]);
}
}
let ring = points.iter().map(|p| Vec2 { x: p[0], y: p[1] }).collect();
Ring { points, ring, min, max }
}
fn contains(&self, p: [f64; 2]) -> bool {
if p[0] < self.min[0] - CLIP_TOL
|| p[0] > self.max[0] + CLIP_TOL
|| p[1] < self.min[1] - CLIP_TOL
|| p[1] > self.max[1] + CLIP_TOL
{
return false;
}
point_in_polygon(Vec2 { x: p[0], y: p[1] }, &self.ring, CLIP_TOL) == "in"
}
fn box_overlaps(&self, min: [f64; 2], max: [f64; 2]) -> bool {
min[0] <= self.max[0] + CLIP_TOL
&& max[0] >= self.min[0] - CLIP_TOL
&& min[1] <= self.max[1] + CLIP_TOL
&& max[1] >= self.min[1] - CLIP_TOL
}
}
impl Region {
fn sample(flat: &Flat) -> Result<Self, String> {
let outline = Ring::new(flat.densified_outline()?);
let mut holes = Vec::with_capacity(flat.holes.len());
for hole in &flat.holes {
let outer = Ring::new(sample_loop(&hole.outer)?);
let islands = hole
.islands
.iter()
.map(|island| sample_loop(island).map(Ring::new))
.collect::<Result<Vec<_>, _>>()?;
holes.push((outer, islands));
}
Ok(Region { outline, holes })
}
fn rings(&self) -> impl Iterator<Item = &Ring> {
std::iter::once(&self.outline).chain(
self.holes
.iter()
.flat_map(|(outer, islands)| std::iter::once(outer).chain(islands)),
)
}
fn material(&self, p: [f64; 2]) -> bool {
if !self.outline.contains(p) {
return false;
}
!self.holes.iter().any(|(outer, islands)| {
outer.contains(p) && !islands.iter().any(|island| island.contains(p))
})
}
fn bounds_material(&self, a: [f64; 2], b: [f64; 2]) -> bool {
let d = [b[0] - a[0], b[1] - a[1]];
let length = (d[0] * d[0] + d[1] * d[1]).sqrt();
if length <= CLIP_TOL {
return false;
}
let eps = PROBE_EPS.min(length * 0.25);
let n = [-d[1] / length * eps, d[0] / length * eps];
let mid = [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5];
self.material([mid[0] + n[0], mid[1] + n[1]])
!= self.material([mid[0] - n[0], mid[1] - n[1]])
}
fn check_fold_edge_uncut(
&self,
a: [f64; 2],
b: [f64; 2],
flat_id: &str,
edge_id: &str,
) -> Result<(), String> {
let cut = self
.split(a, b)
.into_iter()
.any(|(pa, pb)| !self.bounds_material(pa, pb));
if cut {
return Err(format!(
"sheet-metal flat pattern: a cut on flat `{flat_id}` eats into fold edge \
`{edge_id}` — a fold line cannot be cut (the bend's tangent lines, side \
edges and centerline all span the whole edge)"
));
}
Ok(())
}
fn split(&self, a: [f64; 2], b: [f64; 2]) -> Vec<([f64; 2], [f64; 2])> {
let av = Vec2 { x: a[0], y: a[1] };
let bv = Vec2 { x: b[0], y: b[1] };
let length = ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
if length <= CLIP_TOL {
return Vec::new();
}
let parameter_tol = (CLIP_TOL / length).min(0.25);
let min = [a[0].min(b[0]), a[1].min(b[1])];
let max = [a[0].max(b[0]), a[1].max(b[1])];
let mut cuts: Vec<f64> = Vec::new();
for loop_ring in self.rings() {
if !loop_ring.box_overlaps(min, max) {
continue;
}
let poly = &loop_ring.points;
for i in 0..poly.len() {
let (c, d) = (poly[i], poly[(i + 1) % poly.len()]);
if c[0].min(d[0]) > max[0] + CLIP_TOL
|| c[0].max(d[0]) < min[0] - CLIP_TOL
|| c[1].min(d[1]) > max[1] + CLIP_TOL
|| c[1].max(d[1]) < min[1] - CLIP_TOL
{
continue;
}
let hit = segment_intersection(
av,
bv,
Vec2 { x: c[0], y: c[1] },
Vec2 { x: d[0], y: d[1] },
CLIP_TOL,
);
if let Some([t, _]) = hit {
if t > parameter_tol && t < 1.0 - parameter_tol {
cuts.push(t);
}
}
}
}
cuts.sort_by(f64::total_cmp);
let mut parameters = vec![0.0];
for t in cuts {
if t - parameters[parameters.len() - 1] > parameter_tol {
parameters.push(t);
}
}
if 1.0 - parameters[parameters.len() - 1] <= parameter_tol {
parameters.pop();
}
parameters.push(1.0);
let at = |t: f64| -> [f64; 2] {
match t {
t if t == 0.0 => a,
t if t == 1.0 => b,
t => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t],
}
};
parameters
.windows(2)
.map(|pair| (at(pair[0]), at(pair[1])))
.collect()
}
fn clip(&self, points: &[[f64; 2]], closed: bool) -> Vec<(Vec<[f64; 2]>, bool)> {
let n = points.len();
if n < 2 || (closed && n < 3) {
return Vec::new();
}
let mut pieces: Vec<([f64; 2], [f64; 2], bool)> = Vec::new();
let mut split_any = false;
let last = if closed { n } else { n - 1 };
for i in 0..last {
let (a, b) = (points[i], points[(i + 1) % n]);
let parts = self.split(a, b);
split_any |= parts.len() != 1;
for (pa, pb) in parts {
let keep = self.bounds_material(pa, pb);
pieces.push((pa, pb, keep));
}
}
if pieces.is_empty() {
return Vec::new();
}
if pieces.iter().all(|piece| piece.2) {
if !split_any {
return vec![(points.to_vec(), closed)];
}
let mut chain: Vec<[f64; 2]> = pieces.iter().map(|piece| piece.0).collect();
if !closed {
chain.push(pieces[pieces.len() - 1].1);
}
return vec![(chain, closed)];
}
let start = if closed {
pieces.iter().position(|piece| !piece.2).unwrap() + 1
} else {
0
};
let mut runs = Vec::new();
let mut run: Vec<[f64; 2]> = Vec::new();
for step in 0..pieces.len() {
let (a, b, keep) = pieces[(start + step) % pieces.len()];
if keep {
if run.is_empty() {
run.push(a);
}
run.push(b);
} else if run.len() >= 2 {
runs.push((std::mem::take(&mut run), false));
} else {
run.clear();
}
}
if run.len() >= 2 {
runs.push((run, false));
}
runs
}
}
fn normalized(d: [f64; 2]) -> [f64; 2] {
let len = (d[0] * d[0] + d[1] * d[1]).sqrt();
if len > 1e-12 {
[d[0] / len, d[1] / len]
} else {
[0.0, 0.0]
}
}
fn layer(role: LoopRole) -> &'static str {
match role {
LoopRole::Cut => "CUT",
LoopRole::Fold => "FOLD",
LoopRole::BendInternal => "TANGENT",
}
}
const BEND_LAYER: &str = "BEND";
const CUT_RGB: u32 = 0x00B3A4;
fn dxf_style(role: LoopRole) -> (i32, Option<u32>, &'static str) {
match role {
LoopRole::Cut => (3, Some(CUT_RGB), "CONTINUOUS"), LoopRole::Fold => (1, None, "PHANTOM"), LoopRole::BendInternal => (4, None, "DASHED"), }
}
fn dxf_color_ltype(role: LoopRole) -> String {
let (aci, tc, lt) = dxf_style(role);
let mut s = format!("6\n{lt}\n62\n{aci}\n");
if let Some(c) = tc {
s.push_str(&format!("420\n{c}\n"));
}
s
}
fn bounds(polys: &[Polyline]) -> Option<[f64; 4]> {
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for poly in polys {
for p in &poly.points {
min_x = min_x.min(p[0]);
min_y = min_y.min(p[1]);
max_x = max_x.max(p[0]);
max_y = max_y.max(p[1]);
}
}
if min_x.is_finite() {
Some([min_x, min_y, max_x, max_y])
} else {
None
}
}
fn text_height(b: &[f64; 4]) -> f64 {
let span = (b[2] - b[0]).max(b[3] - b[1]).max(1.0);
(span * 0.05).max(1.0)
}
pub fn to_dxf(fp: &FlatPattern) -> String {
let bbox = bounds(&fp.polylines);
let span = bbox
.map(|b| (b[2] - b[0]).max(b[3] - b[1]).max(1.0))
.unwrap_or(1.0);
let ltscale = (span * 0.01).max(0.1);
let height = bbox.map(|b| text_height(&b)).unwrap_or(1.0);
let mut s = String::new();
s.push_str("0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1009\n");
s.push_str(&format!("9\n$LTSCALE\n40\n{ltscale:.3}\n"));
s.push_str("0\nENDSEC\n");
s.push_str("0\nSECTION\n2\nTABLES\n0\nTABLE\n2\nLTYPE\n70\n3\n");
s.push_str("0\nLTYPE\n2\nCONTINUOUS\n70\n64\n3\nSolid line\n72\n65\n73\n0\n40\n0.0\n");
s.push_str(
"0\nLTYPE\n2\nDASHED\n70\n64\n3\nDashed __ __ __\n72\n65\n73\n2\n40\n0.75\n49\n0.5\n49\n-0.25\n",
);
s.push_str(
"0\nLTYPE\n2\nPHANTOM\n70\n64\n3\nPhantom ___ _ _ ___\n72\n65\n73\n6\n40\n2.5\n49\n1.25\n49\n-0.25\n49\n0.25\n49\n-0.25\n49\n0.25\n49\n-0.25\n",
);
s.push_str("0\nENDTAB\n0\nENDSEC\n");
s.push_str("0\nSECTION\n2\nENTITIES\n");
for poly in &fp.polylines {
let layer = layer(poly.role);
s.push_str("0\nPOLYLINE\n8\n");
s.push_str(layer);
s.push('\n');
s.push_str(&dxf_color_ltype(poly.role));
s.push_str("66\n1\n70\n");
s.push_str(if poly.closed { "1" } else { "0" });
s.push('\n');
for p in &poly.points {
s.push_str("0\nVERTEX\n8\n");
s.push_str(layer);
s.push_str(&format!("\n10\n{:.6}\n20\n{:.6}\n30\n0.0\n", p[0], p[1]));
}
s.push_str("0\nSEQEND\n");
}
for ann in &fp.annotations {
let text = ann.text.replace('°', "%%d");
s.push_str("0\nTEXT\n8\n");
s.push_str(BEND_LAYER);
s.push_str("\n62\n2\n");
s.push_str(&format!("10\n{:.6}\n20\n{:.6}\n30\n0.0\n", ann.pos[0], ann.pos[1]));
s.push_str(&format!("40\n{height:.6}\n1\n{text}\n"));
s.push_str(&format!("50\n{:.6}\n72\n1\n", ann.rotation_deg));
s.push_str(&format!("11\n{:.6}\n21\n{:.6}\n31\n0.0\n", ann.pos[0], ann.pos[1]));
}
s.push_str("0\nENDSEC\n0\nEOF\n");
s
}
pub fn to_svg(fp: &FlatPattern) -> String {
let Some(b @ [min_x, min_y, max_x, max_y]) = bounds(&fp.polylines) else {
return String::from(
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"0mm\" height=\"0mm\" viewBox=\"0 0 0 0\"></svg>\n",
);
};
let span = (max_x - min_x).max(max_y - min_y).max(1.0);
let margin = span * 0.02;
let width = (max_x - min_x) + 2.0 * margin;
let height = (max_y - min_y) + 2.0 * margin;
let stroke = (span * 0.003).max(0.05);
let dash = (span * 0.02).max(0.5);
let font = text_height(&b);
let long = dash * 2.0;
let short = dash * 0.5;
let phantom =
format!("{long:.6} {dash:.6} {short:.6} {dash:.6} {short:.6} {dash:.6}");
let dashed = format!("{:.6} {:.6}", dash, dash * 0.6);
let sx = |x: f64| x - min_x + margin;
let sy = |y: f64| max_y - y + margin;
let mut s = String::new();
s.push_str(&format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{width:.6}mm\" height=\"{height:.6}mm\" viewBox=\"0 0 {width:.6} {height:.6}\">\n"
));
s.push_str(&format!(
"<rect x=\"0\" y=\"0\" width=\"{width:.6}\" height=\"{height:.6}\" fill=\"#151515\"/>\n"
));
s.push_str(&format!("<g fill=\"none\" stroke-width=\"{stroke:.6}\">\n"));
for poly in &fp.polylines {
if poly.points.is_empty() {
continue;
}
let mut d = String::new();
for (i, p) in poly.points.iter().enumerate() {
d.push_str(if i == 0 { "M " } else { " L " });
d.push_str(&format!("{:.6} {:.6}", sx(p[0]), sy(p[1])));
}
if poly.closed {
d.push_str(" Z");
}
match poly.role {
LoopRole::Cut => {
s.push_str(&format!("<path d=\"{d}\" stroke=\"#00b3a4\"/>\n"));
}
LoopRole::Fold => {
s.push_str(&format!(
"<path d=\"{d}\" stroke=\"#ff4d4d\" stroke-dasharray=\"{phantom}\"/>\n"
));
}
LoopRole::BendInternal => {
s.push_str(&format!(
"<path d=\"{d}\" stroke=\"#22ccff\" stroke-dasharray=\"{dashed}\"/>\n"
));
}
}
}
for ann in &fp.annotations {
s.push_str(&format!(
"<text transform=\"translate({:.6} {:.6}) rotate({:.6})\" \
text-anchor=\"middle\" dy=\"{:.6}\" fill=\"#ffd400\" font-size=\"{font:.6}\">{}</text>\n",
sx(ann.pos[0]),
sy(ann.pos[1]),
-ann.rotation_deg,
-font * 0.3,
ann.text,
));
}
s.push_str("</g>\n</svg>\n");
s
}