Skip to main content

geop_ops_sheetmetal/
hem.rs

1//! [`Hem`]: the edge of a sheet folded right back on itself.
2
3use geop_core_editor::{
4    edit_form::{EditForm, FormState},
5    editable::{Context, OperationGroup, OperationInfo, OperationTier},
6    editing::{Choice, Number, Unit},
7    part_operation,
8    part_operation::PartOperation,
9};
10use geop_core_math::{
11    geop_error::{GeopError, GeopResult, WithContext},
12    scalars::Scalar,
13    with_context,
14};
15use geop_core_program::{part::StepContext, reader::Reader};
16use geop_ops_model::{ModelOperation, NamedModel, Namer, read_model};
17use serde::{Deserialize, Serialize};
18
19use crate::{
20    body::{self, Made, Making, SheetStep, with_edge},
21    edge_flange::{
22        Corner, EdgeRef, FlangeShape, add_flange, check_offsets, edge_field, offset_fields,
23    },
24    sheet::SheetMetalRules,
25};
26
27/// How tight a hem is folded.
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum HemKind {
31    /// As tight as the body's bends go: its inner radius the body's bend
32    /// radius. A fold of no radius at all, the hem pressed flat onto the
33    /// sheet, would leave a solid touching itself, which is not one.
34    #[default]
35    Closed,
36    /// Folded round with a gap between the hem and the sheet: its inner
37    /// radius half the gap.
38    Open,
39}
40
41/// Folds the straight edge `edge` of a sheet-metal body right back on
42/// itself, for the operation `H`: a bend of half a turn and a flat lying
43/// back over the sheet, the fold's outside where the edge was — the sheet
44/// set back by the fold's outer radius — and the hem `length` long from
45/// there. The body is consumed and rebuilt as `hem(H)`, its entities named
46/// as [`crate::EdgeFlange`] names a flange's, `hem(H,bend)`,
47/// `hem(H,flange)`, ...
48///
49/// `edge` is an edge of a flat face on the body's outline, and the hem
50/// folds towards that face's side. `kind` says how tight (`gap` is the
51/// space between an open hem and the sheet), and `offset_start` /
52/// `offset_end` how far in from the edge's ends it runs, the body's relief
53/// beside it where it does not reach them.
54#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
55pub struct Hem;
56
57#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
58pub struct HemArgs {
59    /// The edge, of a flat face of a sheet-metal body; none yet, for a new
60    /// hem waiting for one to be picked.
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub edge: Option<EdgeRef>,
63    #[serde(default)]
64    pub kind: HemKind,
65    /// How far the hem reaches back over the sheet, from the fold's
66    /// outside.
67    pub length: f64,
68    /// The space between an open hem and the sheet.
69    #[serde(default)]
70    pub gap: f64,
71    #[serde(default)]
72    pub offset_start: f64,
73    #[serde(default)]
74    pub offset_end: f64,
75}
76
77impl HemArgs {
78    /// The hem these arguments describe, on a body of `rules`: a flange
79    /// turning half a turn, set back by its outer radius.
80    fn shape(&self, rules: &SheetMetalRules) -> GeopResult<FlangeShape> {
81        let radius = match self.kind {
82            HemKind::Closed => rules.bend_radius,
83            HemKind::Open => {
84                if !(self.gap.is_finite() && self.gap > 0.0) {
85                    return Err(GeopError::new(format!(
86                        "an open hem's gap must be positive, not {}: for the tightest hem, close it",
87                        self.gap
88                    )));
89                }
90                self.gap / 2.0
91            }
92        };
93        check_offsets(self.offset_start, self.offset_end)?;
94        let s = Scalar::from_f64;
95        let r = s(radius);
96        let outer = r.add(s(rules.thickness));
97        let flat = s(self.length).sub(outer);
98        if !flat.definitely_greater(Scalar::ZERO) {
99            return Err(GeopError::new(format!(
100                "a hem {} long is no longer than its fold, {outer:?} round the outside: it leaves nothing flat",
101                self.length
102            )));
103        }
104        Ok(FlangeShape {
105            angle: Scalar::PI,
106            radius: r,
107            set_back: Some(outer),
108            flat,
109            offset_start: self.offset_start,
110            offset_end: self.offset_end,
111            corner: Corner::Open,
112        })
113    }
114}
115
116impl SheetStep for Hem {
117    fn plan(reader: &Reader, step: &str, args: &HemArgs) -> GeopResult<Made> {
118        let ctx = with_context!("hem({step}, {args:?})");
119        let namer = Namer::new("hem", step)?;
120        let model = read_model(reader).with_context(ctx)?;
121        let (solid, mut sheet, (f, k, toward_b)) =
122            with_edge(&model, reader, args.edge.as_ref(), "a hem").with_context(ctx)?;
123        let shape = args.shape(&sheet.rules).with_context(ctx)?;
124        add_flange(&mut sheet, &namer, f, k, toward_b, &shape).with_context(ctx)?;
125        Ok(Made {
126            consumes: Some(solid),
127            solid: namer.root(),
128            making: Making::Bent(sheet),
129        })
130    }
131}
132
133part_operation!(Hem(HemArgs));
134
135impl PartOperation for Hem {
136    const KIND: &'static str = "hem";
137    const INFO: OperationInfo = OperationInfo::new(
138        "Hem",
139        "Fold the straight edge of a sheet-metal body right back on itself.",
140        OperationGroup::SheetMetal,
141        OperationTier::Small,
142    )
143    .icon(crate::icons::HEM);
144    type Args = HemArgs;
145    type Session = ();
146
147    /// No edge yet: a closed hem, a third of a unit long.
148    fn new_args(_: &Context) -> HemArgs {
149        HemArgs {
150            edge: None,
151            kind: HemKind::Closed,
152            length: 0.3,
153            gap: 0.1,
154            offset_start: 0.0,
155            offset_end: 0.0,
156        }
157    }
158
159    fn run(ctx: &mut StepContext, args: &HemArgs) -> GeopResult<()> {
160        let _step = ctx.step();
161        body::run::<Self>(ctx, args)
162    }
163
164    /// The edge, picked; how tight, and the gap if open; how long; and how
165    /// far in from the edge's ends.
166    fn form<'a>(
167        context: &'a Context<'a>,
168        args: &HemArgs,
169        _: &FormState<()>,
170    ) -> EditForm<'a, HemArgs> {
171        let mut f = EditForm::<HemArgs>::default();
172        edge_field(&mut f, &context.before, args.edge.as_ref(), |edit, edge| {
173            edit.args.edge = Some(edge)
174        });
175        f.select(
176            "kind",
177            "hem",
178            match args.kind {
179                HemKind::Closed => "closed",
180                HemKind::Open => "open",
181            },
182            vec![Choice::new("closed", "Closed"), Choice::new("open", "Open")],
183            false,
184            |edit, choice| {
185                edit.args.kind = match choice {
186                    "open" => HemKind::Open,
187                    _ => HemKind::Closed,
188                }
189            },
190        );
191        if args.kind == HemKind::Open {
192            let gap = Number::new("gap", args.gap, Unit::Length).range(0.0, 1.0);
193            f.number("gap", gap, |edit, g| edit.args.gap = g);
194        }
195        let length = Number::new("length", args.length, Unit::Length).range(0.0, 5.0);
196        f.number("length", length, |edit, l| edit.args.length = l);
197        offset_fields(
198            &mut f,
199            args.offset_start,
200            args.offset_end,
201            |args, [start, end]| {
202                args.offset_start = start;
203                args.offset_end = end;
204            },
205        );
206        f
207    }
208}
209
210impl ModelOperation for Hem {
211    fn build(model: &mut NamedModel, reader: &Reader, step: &str, _: &HemArgs) -> GeopResult<()> {
212        body::build(model, reader, step)
213    }
214}