Skip to main content

geop_ops_sheetmetal/
flat_pattern.rs

1//! [`FlatPattern`]: a sheet-metal body unfolded into the flat blank it is
2//! cut from.
3
4use geop_core_editor::{
5    edit_form::{EditForm, FormState},
6    editable::{Context, OperationGroup, OperationInfo, OperationTier},
7    part_operation,
8    part_operation::PartOperation,
9};
10use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
11use std::collections::BTreeSet;
12
13use geop_core_math::{
14    geop_error::{GeopError, GeopResult, WithContext},
15    scalars::Scalar,
16    vector::Vector2,
17    with_context,
18};
19use geop_core_sketch::Sketch;
20use geop_core_topology::Body;
21use geop_lib_sheet::Sheet as Paper;
22use geop_ops_model::sketch::PlacedSketch;
23use geop_ops_model::{ModelOperation, NamedModel, Namer, Solid, model, read_model};
24use serde::{Deserialize, Serialize};
25
26use crate::{
27    body::{self, Made, Making, SheetBody, SheetStep, Unfolded, sheet_body},
28    cutting::cutting_sheet,
29    sheet::Sheet,
30    thicken::thicken,
31};
32
33/// Unfolds the sheet-metal body named `solid` into its flat pattern, for
34/// the operation `P`: every bend laid flat as a strip as wide as its
35/// developed length (`angle * (radius + k_factor * thickness)`, see
36/// [`crate::SheetMetalRules`]), every flat face moved with it, the body's
37/// first face where it was. The flat body is the solid `flat_pattern(P)`,
38/// and each of its entities is named after the one it is the unfolded
39/// copy of, `flat_pattern(P,X)` for `X` — so a bend's faces are
40/// `flat_pattern(P,X,a)` for its faces `X,a`, and the lines it is bent
41/// along the edges between it and its flat faces. The body is replaced by
42/// its flat pattern unless `keep`.
43///
44/// The bend lines — down the middle of each bend's strip — are also the
45/// sketch `flat_pattern(P,bend_lines)`, on the flat pattern's A side, one
46/// line per bend in the order the bends were made; and what each bend is
47/// is recorded for the flat body as [`FlatPatternData`] (see
48/// [`crate::body::sheets`]).
49///
50/// Only a body whose bends were recorded as it was built — by a base
51/// flange and its flanges — unfolds, and only while it is still made of
52/// just the faces they built: a body some other step has changed is
53/// refused, naming the faces that are not sheet metal.
54#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
55pub struct FlatPattern;
56
57/// A solid of the part, by name.
58pub type SolidRef = ElementReference<NamedModel, Solid>;
59
60#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
61pub struct FlatPatternArgs {
62    /// The sheet-metal body.
63    pub solid: SolidRef,
64    /// Keep the bent body beside the flat one.
65    #[serde(default)]
66    pub keep: bool,
67}
68
69/// One bend of a flat pattern: the line down its middle, its ends in space
70/// on the flat pattern's A side, and how it is bent.
71#[derive(Clone, Debug, PartialEq)]
72pub struct BendLine {
73    /// The bend's name, `X` for its faces `X,a` and `X,b`.
74    pub bend: String,
75    pub start: [f64; 3],
76    pub end: [f64; 3],
77    /// How far it turns, in degrees.
78    pub angle: f64,
79    /// Its inner radius.
80    pub radius: f64,
81    /// Whether it turns towards the B side — up, seen from the B side.
82    pub toward_b: bool,
83}
84
85/// What a flat pattern records of its body, for drawings and cutting:
86/// the sheet's thickness, the bends, and the pattern laid out for cutting
87/// (see [`cutting_sheet`]).
88#[derive(Clone, Debug, PartialEq)]
89pub struct FlatPatternData {
90    pub thickness: f64,
91    pub bends: Vec<BendLine>,
92    pub pattern: Paper,
93}
94
95impl SheetStep for FlatPattern {
96    fn plan(reader: &Reader, step: &str, args: &FlatPatternArgs) -> GeopResult<Made> {
97        let ctx = with_context!("flat_pattern({step}, {args:?})");
98        let namer = Namer::new("flat_pattern", step)?;
99        let solid = args.solid.name();
100        reader.retrieve_element(&args.solid).with_context(ctx)?;
101        let model = read_model(reader).with_context(ctx)?;
102        let sheet = (sheet_body(reader, solid).and_then(SheetBody::bent))
103            .ok_or_else(|| {
104                GeopError::new(format!(
105                    "{solid} is not a sheet-metal body: a flat pattern unfolds a body built by a base flange and its flanges, which record its bends"
106                ))
107            })
108            .with_context(ctx)?
109            .clone();
110        check_unchanged(&model, solid, &sheet).with_context(ctx)?;
111        let (bend_lines, data) =
112            bend_lines(&sheet, &namer.name(&["bend_lines"])).with_context(ctx)?;
113        Ok(Made {
114            consumes: (!args.keep).then(|| solid.to_string()),
115            solid: namer.root(),
116            making: Making::Flat(Box::new(Unfolded {
117                sheet,
118                bend_lines,
119                data,
120            })),
121        })
122    }
123}
124
125/// Builds the flat pattern of `sheet` into `model`, named by `namer`, in
126/// place of the solid `consumes`, if any.
127pub(crate) fn build(
128    model: &mut NamedModel,
129    sheet: &Sheet,
130    consumes: Option<&str>,
131    namer: &Namer,
132) -> GeopResult<()> {
133    let unfolded = sheet.unfolded()?;
134    if let Some(consumed) = consumes {
135        let id = model.solid_id(consumed)?;
136        model.delete_bodies(&[Body::Solid(id)])?;
137    }
138    thicken(model, &unfolded, &namer.root(), &|n| namer.name(&[&n]))?;
139    Ok(())
140}
141
142part_operation!(FlatPattern(FlatPatternArgs));
143
144impl PartOperation for FlatPattern {
145    const KIND: &'static str = "flat_pattern";
146    const INFO: OperationInfo = OperationInfo::new(
147        "Flat pattern",
148        "Unfold a sheet-metal body into the flat blank it is cut from, with its bend lines.",
149        OperationGroup::SheetMetal,
150        OperationTier::Small,
151    )
152    .icon(crate::icons::FLAT_PATTERN);
153    type Args = FlatPatternArgs;
154    type Session = ();
155
156    /// The newest sheet-metal body.
157    fn new_args(context: &Context) -> FlatPatternArgs {
158        let before = &context.before;
159        let solids = read_model(before).map(|model| model.solid_names());
160        let newest = (solids.unwrap_or_default().into_iter().rev())
161            .find(|s| sheet_body(before, s).is_some_and(|b| b.bent().is_some()));
162        FlatPatternArgs {
163            solid: ElementReference::new(model(), newest.unwrap_or_default()),
164            keep: false,
165        }
166    }
167
168    /// Unfolds the body, and draws its bend lines as a sketch.
169    fn run(ctx: &mut StepContext, args: &FlatPatternArgs) -> GeopResult<()> {
170        let step = ctx.step();
171        body::run::<Self>(ctx, args)?;
172        let namer = Namer::new("flat_pattern", step)?;
173        body::add_bend_lines(ctx, &namer.name(&["bend_lines"]))
174    }
175
176    /// The body, picked, and whether to keep it.
177    fn form<'a>(
178        context: &'a Context<'a>,
179        args: &FlatPatternArgs,
180        _: &FormState<()>,
181    ) -> EditForm<'a, FlatPatternArgs> {
182        let mut f = EditForm::<FlatPatternArgs>::default();
183        f.pick_one(
184            "solid",
185            "body",
186            &context.before,
187            Some(&args.solid),
188            |edit, solid| edit.args.solid = solid,
189        )
190        .checkbox("keep", "keep bent body", args.keep, |edit, b| {
191            edit.args.keep = b
192        });
193        f
194    }
195}
196
197impl ModelOperation for FlatPattern {
198    fn build(
199        model: &mut NamedModel,
200        reader: &Reader,
201        step: &str,
202        _: &FlatPatternArgs,
203    ) -> GeopResult<()> {
204        body::build(model, reader, step)
205    }
206}
207
208/// Checks that the solid `solid` is still made of exactly the faces
209/// `sheet` built: refuses, naming them, faces it does not know — another
210/// step's — and faces it built that are gone.
211fn check_unchanged(model: &NamedModel, solid: &str, sheet: &Sheet) -> GeopResult<()> {
212    let built: BTreeSet<String> = sheet.folded()?.face_names()?.into_iter().collect();
213    let faces = model.topology().solid_faces(model.solid_id(solid)?)?;
214    let present: BTreeSet<String> = faces
215        .iter()
216        .map(|&f| model.name_of(f).unwrap_or("?").to_string())
217        .collect();
218    let foreign: Vec<&String> = present.difference(&built).collect();
219    let missing: Vec<&String> = built.difference(&present).collect();
220    if foreign.is_empty() && missing.is_empty() {
221        return Ok(());
222    }
223    Err(GeopError::new(format!(
224        "{solid} is no longer only sheet metal, so it cannot be unfolded: its faces {foreign:?} are not faces its flanges built, and {missing:?} of those are gone"
225    )))
226}
227
228/// The bend lines of the flat pattern of `sheet`: the sketch `name` of
229/// them, on its A side, and what each bend is, with the pattern laid out
230/// for cutting.
231fn bend_lines(sheet: &Sheet, name: &str) -> GeopResult<(PlacedSketch, FlatPatternData)> {
232    let place = &sheet.flats[0].place;
233    let mut sketch = Sketch::new();
234    let mut bends = Vec::new();
235    for (bend, [a, b]) in sheet.bends.iter().zip(sheet.bend_lines()?) {
236        let design = |x: Scalar| Scalar::from_f64(x.to_f64());
237        let p = sketch.add_point(design(a[0]), design(a[1]));
238        let q = sketch.add_point(design(b[0]), design(b[1]));
239        sketch.add_line(p, q);
240        let at = |v: Vector2| place.point(&v).to_array().map(|c| c.to_f64());
241        bends.push(BendLine {
242            bend: bend.name.root(),
243            start: at(a),
244            end: at(b),
245            angle: bend.angle.to_f64().to_degrees(),
246            radius: bend.radius.to_f64(),
247            toward_b: bend.toward_b,
248        });
249    }
250    let sketch = PlacedSketch {
251        name: name.to_string(),
252        plane: place.coordinate_system()?,
253        sketch,
254    };
255    let data = FlatPatternData {
256        thickness: sheet.rules.thickness,
257        bends,
258        pattern: cutting_sheet(sheet)?,
259    };
260    Ok((sketch, data))
261}