1use geop_core_geometry::{
22 aabb::union_aabb,
23 nurb_curve::{NurbCurve2D, NurbCurve3D},
24 nurb_surface::{NurbSurface, NurbSurface3D},
25};
26use geop_core_math::{
27 geop_error::{GeopError, GeopResult},
28 primitives::CoordinateSystem,
29 scalars::Scalar,
30 vector::{Vector2, Vector3, Vector4},
31};
32use geop_core_topology::Body;
33use geop_ops_model::{NamedModel, Namer};
34use serde::{Deserialize, Serialize};
35
36use crate::thicken::thicken;
37
38#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41pub struct SheetMetalRules {
42 pub thickness: f64,
44 pub bend_radius: f64,
46 pub k_factor: f64,
50 #[serde(default)]
52 pub relief: Relief,
53 pub relief_ratio: f64,
56 pub corner_gap: f64,
59}
60
61impl Default for SheetMetalRules {
62 fn default() -> Self {
63 Self {
64 thickness: 0.1,
65 bend_radius: 0.1,
66 k_factor: 0.44,
67 relief: Relief::Rectangular,
68 relief_ratio: 0.5,
69 corner_gap: 0.1,
70 }
71 }
72}
73
74impl SheetMetalRules {
75 pub fn check(&self) -> GeopResult<()> {
77 let positive = [
78 ("thickness", self.thickness),
79 ("bend radius", self.bend_radius),
80 ("relief ratio", self.relief_ratio),
81 ];
82 for (what, value) in positive {
83 if !(value.is_finite() && value > 0.0) {
84 return Err(GeopError::new(format!(
85 "the sheet's {what} must be positive, not {value}"
86 )));
87 }
88 }
89 if !(0.0..=1.0).contains(&self.k_factor) {
90 return Err(GeopError::new(format!(
91 "the K-factor must lie between 0 and 1, not {}",
92 self.k_factor
93 )));
94 }
95 if !(self.corner_gap.is_finite() && self.corner_gap >= 0.0) {
96 return Err(GeopError::new(format!(
97 "the corner gap must not be negative, not {}",
98 self.corner_gap
99 )));
100 }
101 Ok(())
102 }
103
104 pub fn relief_size(&self) -> Option<f64> {
107 match self.relief {
108 Relief::Rectangular => Some(self.relief_ratio * self.thickness),
109 Relief::Tear => None,
110 }
111 }
112}
113
114#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "snake_case")]
118pub enum Relief {
119 #[default]
121 Rectangular,
122 Tear,
124}
125
126#[derive(Clone, Debug, PartialEq)]
130pub struct Placement {
131 pub origin: Vector3,
132 pub e1: Vector3,
133 pub e2: Vector3,
134 pub n: Vector3,
135}
136
137impl Placement {
138 pub fn point(&self, p: &Vector2) -> Vector3 {
140 self.origin
141 .add(&self.e1.prod_scalar(p[0]))
142 .add(&self.e2.prod_scalar(p[1]))
143 }
144
145 pub fn direction(&self, d: &Vector2) -> Vector3 {
147 self.e1.prod_scalar(d[0]).add(&self.e2.prod_scalar(d[1]))
148 }
149
150 pub fn curve(&self, curve: &NurbCurve2D) -> GeopResult<NurbCurve3D> {
153 curve.embed(&self.origin, &self.e1, &self.e2)
154 }
155
156 pub fn offset(&self, t: Scalar) -> Self {
158 Self {
159 origin: self.origin.add(&self.n.prod_scalar(t)),
160 ..self.clone()
161 }
162 }
163
164 pub fn coordinate_system(&self) -> GeopResult<CoordinateSystem> {
166 CoordinateSystem::try_new(self.origin, self.e1, self.e2, self.n)
167 }
168
169 pub(crate) fn plane(&self, curves: &[&NurbCurve2D]) -> GeopResult<NurbSurface3D> {
173 let bounds = union_aabb(curves.iter().map(|c| c.aabb()))
174 .ok_or_else(|| GeopError::new("Placement::plane: no curves to span"))?;
175 let lo: [Scalar; 2] = std::array::from_fn(|k| Scalar::from_f64(bounds[k].lower().to_f64()));
179 let hi: [Scalar; 2] = std::array::from_fn(|k| Scalar::from_f64(bounds[k].upper().to_f64()));
180 let corner = |x: Scalar, y: Scalar| {
181 let p = self.point(&Vector2::from_array([x, y]));
182 Vector4::from_array([p[0], p[1], p[2], Scalar::ONE])
183 };
184 NurbSurface::try_new(
185 1,
186 1,
187 vec![
188 corner(lo[0], lo[1]),
189 corner(lo[0], hi[1]),
190 corner(hi[0], lo[1]),
191 corner(hi[0], hi[1]),
192 ],
193 vec![lo[0], lo[0], hi[0], hi[0]],
194 vec![lo[1], lo[1], hi[1], hi[1]],
195 )
196 }
197}
198
199#[derive(Clone, Debug, PartialEq)]
202pub struct FlatEdge {
203 pub curve: NurbCurve2D,
204 pub name: Namer,
207}
208
209impl FlatEdge {
210 pub fn key(&self) -> String {
212 self.name.root()
213 }
214
215 pub fn line(&self) -> Option<GeopResult<[Vector2; 2]>> {
217 (self.curve.degree == 1 && self.curve.control_points.len() == 2)
218 .then(|| Ok([self.curve.start()?, self.curve.end()?]))
219 }
220}
221
222#[derive(Clone, Debug, PartialEq)]
226pub struct Flat {
227 pub place: Placement,
228 pub outer: Vec<FlatEdge>,
229 pub holes: Vec<Vec<FlatEdge>>,
230 pub name: Namer,
232}
233
234#[derive(Clone, Debug, PartialEq)]
238pub struct Bend {
239 pub parent: usize,
240 pub parent_edge: String,
241 pub child: usize,
242 pub child_edge: String,
243 pub angle: Scalar,
245 pub radius: Scalar,
247 pub toward_b: bool,
249 pub name: Namer,
253}
254
255#[derive(Clone, Debug, PartialEq)]
259pub struct Sheet {
260 pub rules: SheetMetalRules,
261 pub flats: Vec<Flat>,
262 pub bends: Vec<Bend>,
263 pub cuts: Vec<Cut>,
266}
267
268#[derive(Clone, Debug, PartialEq)]
273pub struct Cut {
274 pub flat: usize,
275 pub loops: Vec<Vec<FlatEdge>>,
276 pub name: Namer,
278}
279
280pub struct BendFrame {
283 pub tau: Vector3,
285 pub m: Vector3,
287 pub n: Vector3,
289 pub sigma: Scalar,
291 pub r_a: Scalar,
293 pub r_b: Scalar,
294 pub cos: Scalar,
295 pub sin: Scalar,
296 pub center: Vector3,
298}
299
300impl BendFrame {
301 pub fn new(
305 place: &Placement,
306 [a, b]: [Vector2; 2],
307 angle: Scalar,
308 radius: Scalar,
309 thickness: Scalar,
310 toward_b: bool,
311 ) -> GeopResult<Self> {
312 let tau = place.direction(&b.sub(&a)).normalize()?;
313 let n = place.n;
314 let m = tau.prod_cross(&n);
315 let (sigma, r_a, r_b) = if toward_b {
316 (Scalar::ONE, radius.add(thickness), radius)
317 } else {
318 (Scalar::ONE.neg(), radius, radius.add(thickness))
319 };
320 Ok(Self {
321 center: place.point(&a).add(&n.prod_scalar(sigma.mul(r_a))),
322 tau,
323 m,
324 n,
325 sigma,
326 r_a,
327 r_b,
328 cos: angle.cos(),
329 sin: angle.sin(),
330 })
331 }
332
333 pub fn rotate(&self, x: &Vector3) -> Vector3 {
336 let (along_m, along_tau, along_n) = (
337 x.prod_dot(&self.m),
338 x.prod_dot(&self.tau),
339 x.prod_dot(&self.n),
340 );
341 let sigma_sin = self.sigma.mul(self.sin);
342 let m = self
343 .m
344 .prod_scalar(self.cos)
345 .add(&self.n.prod_scalar(sigma_sin));
346 let n = self
347 .n
348 .prod_scalar(self.cos)
349 .sub(&self.m.prod_scalar(sigma_sin));
350 m.prod_scalar(along_m)
351 .add(&self.tau.prod_scalar(along_tau))
352 .add(&n.prod_scalar(along_n))
353 }
354
355 pub fn child_placement(&self, parent: &Placement) -> Placement {
359 Placement {
360 origin: self
361 .center
362 .add(&self.rotate(&parent.origin.sub(&self.center))),
363 e1: self.rotate(&parent.e1),
364 e2: self.rotate(&parent.e2),
365 n: self.rotate(&parent.n),
366 }
367 }
368}
369
370impl Sheet {
371 pub fn outer_edge(&self, key: &str) -> Option<(usize, usize)> {
373 self.flats.iter().enumerate().find_map(|(f, flat)| {
374 flat.outer
375 .iter()
376 .position(|e| e.key() == key)
377 .map(|k| (f, k))
378 })
379 }
380
381 pub fn is_bent(&self, key: &str) -> bool {
383 self.bends
384 .iter()
385 .any(|b| b.parent_edge == key || b.child_edge == key)
386 }
387
388 pub fn edge_named(&self, name: &str) -> Option<(usize, usize, bool)> {
391 self.flats.iter().enumerate().find_map(|(f, flat)| {
392 flat.outer.iter().enumerate().find_map(|(k, e)| {
393 if e.name.name(&["a"]) == name {
394 Some((f, k, false))
395 } else if e.name.name(&["b"]) == name {
396 Some((f, k, true))
397 } else {
398 None
399 }
400 })
401 })
402 }
403
404 pub(crate) fn parent_line(&self, bend: &Bend) -> GeopResult<[Vector2; 2]> {
406 let (f, k) = self.outer_edge(&bend.parent_edge).ok_or_else(|| {
407 GeopError::new(format!(
408 "bend {} starts at {}, which is no edge of a flat's outline",
409 bend.name.root(),
410 bend.parent_edge
411 ))
412 })?;
413 self.flats[f].outer[k].line().ok_or_else(|| {
414 GeopError::new(format!(
415 "bend {} starts at {}, which is not straight",
416 bend.name.root(),
417 bend.parent_edge
418 ))
419 })?
420 }
421
422 pub fn bend_frame(&self, bend: &Bend) -> GeopResult<BendFrame> {
424 BendFrame::new(
425 &self.flats[bend.parent].place,
426 self.parent_line(bend)?,
427 bend.angle,
428 bend.radius,
429 Scalar::from_f64(self.rules.thickness),
430 bend.toward_b,
431 )
432 }
433
434 pub fn developed_length(&self, bend: &Bend) -> Scalar {
437 let neutral = bend
438 .radius
439 .add(Scalar::from_f64(self.rules.k_factor).mul(Scalar::from_f64(self.rules.thickness)));
440 bend.angle.mul(neutral)
441 }
442
443 pub fn bend_lines(&self) -> GeopResult<Vec<[Vector2; 2]>> {
448 let shifts = self.flat_shifts()?;
449 self.bends
450 .iter()
451 .map(|bend| {
452 let [a, b] = self.parent_line(bend)?;
453 let tau = b.sub(&a).normalize()?;
455 let out = Vector2::from_array([tau[1], tau[0].neg()]);
456 let half = out.prod_scalar(self.developed_length(bend).div(Scalar::TWO)?);
457 let shift = shifts[bend.parent].add(&half);
458 Ok([a.add(&shift), b.add(&shift)])
459 })
460 .collect()
461 }
462
463 pub fn flat_shifts(&self) -> GeopResult<Vec<Vector2>> {
467 let mut shifts: Vec<Option<Vector2>> = vec![None; self.flats.len()];
468 shifts[0] = Some(Vector2::zero());
469 for bend in &self.bends {
470 let [a, b] = self.parent_line(bend)?;
471 let tau = b.sub(&a).normalize()?;
472 let out = Vector2::from_array([tau[1], tau[0].neg()]);
473 let parent = shifts[bend.parent].ok_or_else(|| {
474 GeopError::new(format!(
475 "bend {} hangs off a flat that is not laid out before it",
476 bend.name.root()
477 ))
478 })?;
479 shifts[bend.child] = Some(parent.add(&out.prod_scalar(self.developed_length(bend))));
480 }
481 shifts
482 .into_iter()
483 .enumerate()
484 .map(|(f, s)| {
485 s.ok_or_else(|| {
486 GeopError::new(format!(
487 "flat {} is joined to the rest by no bend",
488 self.flats[f].name.root()
489 ))
490 })
491 })
492 .collect()
493 }
494}
495
496impl Sheet {
497 pub(crate) fn build(
500 &self,
501 model: &mut NamedModel,
502 old: Option<&str>,
503 name: &str,
504 ) -> GeopResult<()> {
505 let folded = self.folded()?;
506 if let Some(old) = old {
507 let id = model.solid_id(old)?;
508 model.delete_bodies(&[Body::Solid(id)])?;
509 }
510 thicken(model, &folded, name, &|n| n)?;
511 Ok(())
512 }
513}
514
515pub fn straight(a: Vector2, b: Vector2, name: Namer) -> GeopResult<FlatEdge> {
517 Ok(FlatEdge {
518 curve: NurbCurve2D::line(a, b)?,
519 name,
520 })
521}