Skip to main content

geop_ops_sheetmetal/
cutting.rs

1//! A sheet-metal body's flat pattern laid out for cutting: its outline
2//! and holes on the `CUT` layer, its bend lines on the `BEND` layer with
3//! how each is bent beside it ([`cutting_sheet`]), written as DXF by the
4//! [`FlatPatternDxf`] exporter.
5
6use std::collections::BTreeMap;
7
8use geop_core_editor::output::{Content, Export, Exporter, ExporterInfo, Output};
9use geop_core_geometry::{aabb::union_aabb, nurb_curve::NurbCurve2D};
10use geop_core_math::{
11    geop_error::{GeopError, GeopResult, WithContext},
12    with_context,
13};
14use geop_lib_sheet::{
15    Anchor, Format, Label, Layer, LineType, Shape, Sheet as Paper, decimal, strokes_of,
16};
17use geop_ops_model::parameters::stem;
18use geop_ops_model::read_model;
19use serde::Deserialize;
20
21use crate::{
22    body::{SheetBody, sheet_body},
23    sheet::Sheet,
24};
25
26/// What a laser or a punch cuts: the flat pattern's outline and holes.
27pub const CUT: Layer = Layer {
28    name: "CUT",
29    width: 0.25,
30    line_type: LineType::Continuous,
31    color: 7,
32};
33
34/// Where the flat pattern is bent, and how.
35pub const BEND: Layer = Layer {
36    name: "BEND",
37    width: 0.25,
38    line_type: LineType::Center,
39    color: 5,
40};
41
42/// How high a bend's note is, as a share of the pattern's larger side.
43const NOTE_HEIGHT: f64 = 0.025;
44
45/// The flat pattern of `sheet` on paper, one model unit a unit, its
46/// lower left corner at the origin, seen from its B side — the first
47/// flat's sheet coordinates: every curve of the unfolded sheet's boundary
48/// on the `CUT` layer, each bend's line on the `BEND` layer, and beside it
49/// `UP 90° R0.1` — `UP` for a bend towards the viewer.
50pub fn cutting_sheet(sheet: &Sheet) -> GeopResult<Paper> {
51    let ctx = with_context!("cutting_sheet");
52    let layout = sheet.layout().with_context(ctx)?;
53    // The boundary: the edges only one face has.
54    let mut uses: BTreeMap<usize, usize> = BTreeMap::new();
55    for face in &layout.faces {
56        for &(e, _) in face.loops().flatten() {
57            *uses.entry(e).or_default() += 1;
58        }
59    }
60    let boundary: Vec<&NurbCurve2D> = uses
61        .iter()
62        .filter(|(_, n)| **n == 1)
63        .map(|(&e, _)| &layout.edges[e].curve)
64        .collect();
65    let Some(bounds) = union_aabb(boundary.iter().map(|c| c.aabb())) else {
66        return Err(GeopError::new("the flat pattern has no outline")).with_context(ctx);
67    };
68    let lo: [f64; 2] = std::array::from_fn(|k| bounds[k].lower().to_f64());
69    let hi: [f64; 2] = std::array::from_fn(|k| bounds[k].upper().to_f64());
70    let place = |p: [f64; 2]| [p[0] - lo[0], p[1] - lo[1]];
71    let curves: Vec<(Layer, &NurbCurve2D)> = boundary.iter().map(|&c| (CUT, c)).collect();
72    let mut paper = Paper {
73        width: hi[0] - lo[0],
74        height: hi[1] - lo[1],
75        layers: vec![CUT, BEND],
76        strokes: strokes_of(&curves, &place, 1.0).with_context(ctx)?,
77        labels: Vec::new(),
78    };
79    let height = NOTE_HEIGHT * paper.width.max(paper.height);
80    for (bend, [a, b]) in sheet.bends.iter().zip(sheet.bend_lines()?) {
81        let (a, b) = (
82            place([a[0].to_f64(), a[1].to_f64()]),
83            place([b[0].to_f64(), b[1].to_f64()]),
84        );
85        paper.stroke(BEND, Shape::Line(a, b));
86        // Along the line, reading left to right.
87        let mut angle = (b[1] - a[1]).atan2(b[0] - a[0]).to_degrees();
88        if angle > 90.0 {
89            angle -= 180.0;
90        } else if angle <= -90.0 {
91            angle += 180.0;
92        }
93        let turn = if bend.toward_b { "UP" } else { "DOWN" };
94        paper.labels.push(Label {
95            layer: BEND,
96            at: [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0],
97            height,
98            angle,
99            anchor: Anchor::Middle,
100            text: format!(
101                "{turn} {}° R{}",
102                decimal(bend.angle.to_f64().to_degrees(), 3),
103                decimal(bend.radius.to_f64(), 3)
104            ),
105        });
106    }
107    Ok(paper)
108}
109
110/// Writes the flat pattern of a sheet-metal body as a DXF file for laser
111/// cutting (see [`cutting_sheet`]): of a body as it is bent, or of one a
112/// flat pattern unfolded.
113pub struct FlatPatternDxf;
114
115/// Which body's flat pattern is written.
116#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
117#[serde(default)]
118pub struct FlatPatternOptions {
119    /// The body, by its solid's name: the newest sheet-metal body, if none.
120    pub solid: Option<String>,
121}
122
123impl Exporter for FlatPatternDxf {
124    const INFO: ExporterInfo = ExporterInfo {
125        kind: "flat_pattern",
126        label: "Flat pattern",
127        doc: "a sheet-metal body laid flat, for laser cutting",
128        formats: &["dxf"],
129    };
130    type Options = FlatPatternOptions;
131
132    fn export(output: &mut Output, _: &str, options: FlatPatternOptions) -> GeopResult<Export> {
133        let part = output.part();
134        let candidates: Vec<String> = match &options.solid {
135            Some(name) => vec![name.clone()],
136            None => read_model(&part)?.solid_names().into_iter().rev().collect(),
137        };
138        let found = candidates.into_iter().find_map(|name| {
139            let body = sheet_body(&part, &name)?;
140            Some((name, body))
141        });
142        let Some((_, body)) = found else {
143            return Err(GeopError::new(match &options.solid {
144                Some(name) => format!(
145                    "{name} is no sheet-metal body: only a body built by a base flange and its flanges, or its flat pattern, is laid out for cutting"
146                ),
147                None => "the part has no sheet-metal body to lay out for cutting".to_string(),
148            }));
149        };
150        let paper = match body {
151            SheetBody::Bent(sheet) => cutting_sheet(sheet)?,
152            SheetBody::Flat(flat) => flat.pattern.clone(),
153        };
154        let format = Format::Dxf;
155        Ok(Export {
156            name: format!("{}_flat.{}", stem(output.file_name()), format.extension()),
157            media_type: format.media_type().into(),
158            content: Content::Text(paper.write(format)),
159        })
160    }
161}