1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum HemKind {
31 #[default]
35 Closed,
36 Open,
39}
40
41#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
55pub struct Hem;
56
57#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
58pub struct HemArgs {
59 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub edge: Option<EdgeRef>,
63 #[serde(default)]
64 pub kind: HemKind,
65 pub length: f64,
68 #[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 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 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 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}