Skip to main content

geop_ops_sheetmetal/
body.rs

1//! What the sheet-metal steps record of the bodies they build — the
2//! part's target [`sheets`], which every one of them revises — and how a
3//! step of them runs ([`run`]): it works out what it makes ([`made`]),
4//! records it, and builds it into the model.
5//!
6//! A body's [`Sheet`] says which faces are flat and which are bends, and
7//! how they join: so the next flange and the flat pattern work from what
8//! the body is made of, never from guessing it from its geometry.
9
10use geop_core_editor::part_operation::PartOperation;
11use geop_core_math::{
12    geop_error::{GeopError, GeopResult, WithContext},
13    with_context,
14};
15use geop_core_program::{
16    element::{ElementReference, HasElements, Kind, whole},
17    part::StepContext,
18    reader::Reader,
19    target::{Target, TargetReference},
20};
21use geop_ops_model::sketch::{PlacedSketch, sketch};
22use geop_ops_model::{
23    ModelOperation, NamedModel, Namer, Records, SolidNotes, build_model, solid_notes,
24};
25
26use crate::{edge_flange::EdgeRef, flat_pattern::FlatPatternData, sheet::Sheet};
27
28/// What a sheet-metal body is recorded as.
29#[derive(Clone, Debug, PartialEq)]
30pub enum SheetBody {
31    /// A body as it is bent: its sheet.
32    Bent(Sheet),
33    /// The flat pattern of one: what a drawing and cutting read of it.
34    Flat(FlatPatternData),
35}
36
37impl SheetBody {
38    /// Its sheet, if it is a body as it is bent.
39    pub fn bent(&self) -> Option<&Sheet> {
40        match self {
41            SheetBody::Bent(sheet) => Some(sheet),
42            SheetBody::Flat(_) => None,
43        }
44    }
45
46    /// How thick its sheet is.
47    pub fn thickness(&self) -> f64 {
48        match self {
49            SheetBody::Bent(sheet) => sheet.rules.thickness,
50            SheetBody::Flat(flat) => flat.thickness,
51        }
52    }
53}
54
55/// The part's sheet-metal bodies, by the names of their solids: the value
56/// of its target [`sheets`].
57pub type Sheets = Records<SheetBody>;
58
59/// The part's sheet-metal bodies.
60pub fn sheets() -> TargetReference<Sheets> {
61    TargetReference::new("sheets")
62}
63
64/// A sheet-metal body, by its solid's name: an element of [`sheets`].
65impl Kind for SheetBody {
66    const NAME: &'static str = "sheet_body";
67}
68
69impl HasElements<SheetBody> for Sheets {
70    type Element<'a> = &'a SheetBody;
71    const SINGLETON: Option<&'static str> = Some("sheets");
72
73    fn element(&self, name: &str) -> GeopResult<&SheetBody> {
74        self.get(name).ok_or_else(|| {
75            GeopError::new(format!(
76                "{name:?} is no sheet-metal body: none was recorded for it"
77            ))
78        })
79    }
80}
81
82/// The sheet-metal body of the solid `solid` in the part `reader` reads,
83/// if it is one: none before a step built one.
84pub fn sheet_body<'a>(reader: &Reader<'a>, solid: &str) -> Option<&'a SheetBody> {
85    reader.defines(&sheets()).then_some(())?;
86    reader
87        .retrieve_element(&ElementReference::new(sheets(), solid))
88        .ok()
89}
90
91/// A sheet-metal body found by an edge: its solid, its sheet, and the
92/// edge as [`Sheet::edge_named`] finds it.
93pub(crate) type WithEdge = (String, Sheet, (usize, usize, bool));
94
95/// The sheet-metal body of `model`, in the part `reader` reads, with the
96/// edge `edge` on a flat's outline: its solid, its sheet, and the edge as
97/// [`Sheet::edge_named`] finds it. `what` is for the error: what is bent
98/// from such an edge.
99pub(crate) fn with_edge(
100    model: &NamedModel,
101    reader: &Reader,
102    edge: Option<&EdgeRef>,
103    what: &str,
104) -> GeopResult<WithEdge> {
105    let edge = edge
106        .ok_or_else(|| GeopError::new(format!("no edge is picked: {what} is bent from one")))?
107        .name();
108    (model.solid_names().into_iter())
109        .find_map(|solid| {
110            let sheet = sheet_body(reader, &solid)?.bent()?;
111            let at = sheet.edge_named(edge)?;
112            Some((solid, sheet.clone(), at))
113        })
114        .ok_or_else(|| {
115            GeopError::new(format!(
116                "{edge:?} is no edge of a sheet-metal body's flat face on its outline: {what} is bent from one of those"
117            ))
118        })
119}
120
121/// What a sheet-metal step builds.
122#[derive(Clone, Debug, PartialEq)]
123pub enum Making {
124    /// A body as it is bent, from its sheet.
125    Bent(Sheet),
126    /// The flat pattern of a body.
127    Flat(Box<Unfolded>),
128}
129
130/// The flat pattern of the body of `sheet`: the sheet unfolded, the sketch
131/// of its bend lines, and what it records.
132#[derive(Clone, Debug, PartialEq)]
133pub struct Unfolded {
134    pub sheet: Sheet,
135    pub bend_lines: PlacedSketch,
136    pub data: FlatPatternData,
137}
138
139/// What a sheet-metal step makes: the body `making`, the solid `solid`, in
140/// place of the sheet-metal body `consumes`, if any.
141#[derive(Clone, Debug, PartialEq)]
142pub struct Made {
143    pub consumes: Option<String>,
144    pub solid: String,
145    pub making: Making,
146}
147
148/// What the step `step` makes.
149pub fn made(step: &str) -> TargetReference<Made> {
150    TargetReference::new(format!("sheet_metal:{step}"))
151}
152
153/// Each step's own.
154impl Target for Made {}
155
156/// Read whole.
157impl Kind for Made {
158    const NAME: &'static str = "sheet_metal";
159}
160
161impl HasElements<Made> for Made {
162    type Element<'a> = &'a Made;
163
164    fn element(&self, name: &str) -> GeopResult<&Made> {
165        whole(self, name)
166    }
167}
168
169/// What the step `step` made, as `reader` reads it.
170fn made_by<'a>(reader: &Reader<'a>, step: &str) -> GeopResult<&'a Made> {
171    reader.retrieve_element(&ElementReference::whole(made(step)))
172}
173
174/// A sheet-metal operation: it works out what it makes from the part read
175/// — the sketches, the sheet-metal bodies there are — and is then built
176/// alike ([`run`]).
177pub(crate) trait SheetStep: PartOperation + ModelOperation {
178    /// What the step `step` with `args` makes, as `reader` reads the part.
179    fn plan(reader: &Reader, step: &str, args: &Self::Args) -> GeopResult<Made>;
180}
181
182/// The generator of [`made`].
183fn planned<O: SheetStep>(reader: &Reader, (step, args): &(String, O::Args)) -> GeopResult<Made> {
184    O::plan(reader, step, args)
185}
186
187/// Records what the step `step` made: its body in place of the one it
188/// consumed.
189// A generator's arguments are a type of their own: the step's id, a `String`.
190#[allow(clippy::ptr_arg)]
191fn record(reader: &Reader, step: &String) -> GeopResult<Sheets> {
192    let mut sheets: Sheets = reader.previous()?;
193    let made = made_by(reader, step)?;
194    if let Some(consumed) = &made.consumes {
195        sheets.remove(consumed);
196    }
197    let body = match &made.making {
198        Making::Bent(sheet) => SheetBody::Bent(sheet.clone()),
199        Making::Flat(unfolded) => SheetBody::Flat(unfolded.data.clone()),
200    };
201    sheets.insert(made.solid.clone(), body);
202    Ok(sheets)
203}
204
205/// Notes the thickness of what the step `step` made, in place of the body
206/// it consumed (see [`geop_ops_model::solid_notes`]).
207#[allow(clippy::ptr_arg)]
208fn note(reader: &Reader, step: &String) -> GeopResult<SolidNotes> {
209    let mut notes: SolidNotes = reader.previous()?;
210    let made = made_by(reader, step)?;
211    if let Some(consumed) = &made.consumes {
212        notes.remove(consumed);
213    }
214    let thickness = match &made.making {
215        Making::Bent(sheet) => sheet.rules.thickness,
216        Making::Flat(unfolded) => unfolded.data.thickness,
217    };
218    notes.note_mut(made.solid.clone()).thickness = Some(thickness);
219    Ok(notes)
220}
221
222/// Runs the step of `O`: works out what it makes, records it, and builds
223/// it into the model.
224pub(crate) fn run<O: SheetStep>(ctx: &mut StepContext, args: &O::Args) -> GeopResult<()> {
225    let step = ctx.step();
226    ctx.produce(&made(step), &(step.to_string(), args.clone()), planned::<O>)?;
227    ctx.produce(&sheets(), &step.to_string(), record)?;
228    ctx.produce(&solid_notes(), &step.to_string(), note)?;
229    build_model::<O>(ctx, args)
230}
231
232/// The sketch of the bend lines of the flat pattern the step `step` made.
233#[allow(clippy::ptr_arg)]
234fn bend_lines(reader: &Reader, step: &String) -> GeopResult<PlacedSketch> {
235    match &made_by(reader, step)?.making {
236        Making::Flat(unfolded) => Ok(unfolded.bend_lines.clone()),
237        Making::Bent(_) => Err(GeopError::new(format!(
238            "step {step:?} made no flat pattern to draw the bend lines of"
239        ))),
240    }
241}
242
243/// Adds the sketch `name` of the bend lines of the flat pattern the step
244/// made.
245pub(crate) fn add_bend_lines(ctx: &mut StepContext, name: &str) -> GeopResult<()> {
246    let step = ctx.step();
247    ctx.produce(&sketch(name), &step.to_string(), bend_lines)
248}
249
250/// Builds what the step `step` made into `model` ([`ModelOperation::build`]
251/// of every sheet-metal operation).
252pub(crate) fn build(model: &mut NamedModel, reader: &Reader, step: &str) -> GeopResult<()> {
253    let ctx = with_context!("building the sheet metal of step {step}");
254    let made = made_by(reader, step).with_context(ctx)?;
255    match &made.making {
256        Making::Bent(sheet) => sheet
257            .build(model, made.consumes.as_deref(), &made.solid)
258            .with_context(ctx),
259        Making::Flat(unfolded) => {
260            let namer = Namer::new("flat_pattern", step)?;
261            crate::flat_pattern::build(model, &unfolded.sheet, made.consumes.as_deref(), &namer)
262                .with_context(ctx)
263        }
264    }
265}