1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
55pub struct FlatPattern;
56
57pub type SolidRef = ElementReference<NamedModel, Solid>;
59
60#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
61pub struct FlatPatternArgs {
62 pub solid: SolidRef,
64 #[serde(default)]
66 pub keep: bool,
67}
68
69#[derive(Clone, Debug, PartialEq)]
72pub struct BendLine {
73 pub bend: String,
75 pub start: [f64; 3],
76 pub end: [f64; 3],
77 pub angle: f64,
79 pub radius: f64,
81 pub toward_b: bool,
83}
84
85#[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
125pub(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 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 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 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
208fn 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
228fn 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}