1use 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
26pub const CUT: Layer = Layer {
28 name: "CUT",
29 width: 0.25,
30 line_type: LineType::Continuous,
31 color: 7,
32};
33
34pub const BEND: Layer = Layer {
36 name: "BEND",
37 width: 0.25,
38 line_type: LineType::Center,
39 color: 5,
40};
41
42const NOTE_HEIGHT: f64 = 0.025;
44
45pub fn cutting_sheet(sheet: &Sheet) -> GeopResult<Paper> {
51 let ctx = with_context!("cutting_sheet");
52 let layout = sheet.layout().with_context(ctx)?;
53 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 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
110pub struct FlatPatternDxf;
114
115#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
117#[serde(default)]
118pub struct FlatPatternOptions {
119 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}