1use geop_core_editor::{
5 edit_form::{Edit, EditForm, FormState},
6 editable::{Context, OperationGroup, OperationInfo, OperationTier},
7 editing::{Choice, Number, Unit},
8 part_operation,
9 part_operation::PartOperation,
10};
11use geop_core_geometry::nurb_curve::NurbCurve2D;
12use geop_core_math::{
13 geop_error::{GeopError, GeopResult, WithContext},
14 scalars::Scalar,
15 vector::Vector2,
16 with_context,
17};
18use geop_core_program::{
19 element::ElementReference, numeric::NumericInput, part::StepContext, reader::Reader,
20};
21use geop_ops_model::{Line, ModelOperation, NamedModel, Namer, read_model};
22use serde::{Deserialize, Serialize};
23
24use crate::{
25 body::{self, Made, Making, SheetStep, with_edge},
26 sheet::{Bend, BendFrame, Flat, FlatEdge, Sheet, SheetMetalRules, straight},
27};
28
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum LengthReference {
33 #[default]
36 OuterSharp,
37 InnerSharp,
39 Tangent,
41}
42
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum FlangePosition {
47 #[default]
50 MaterialInside,
51 MaterialOutside,
53 BendOutside,
55}
56
57#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum Corner {
62 #[default]
64 Open,
65 Closed,
70}
71
72#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
99pub struct EdgeFlange;
100
101pub type EdgeRef = ElementReference<NamedModel, Line>;
103
104pub fn edge(name: impl Into<String>) -> EdgeRef {
106 ElementReference::new(geop_ops_model::model(), name)
107}
108
109#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
110pub struct EdgeFlangeArgs {
111 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub edge: Option<EdgeRef>,
115 pub angle: NumericInput,
118 pub length: NumericInput,
121 #[serde(default)]
122 pub reference: LengthReference,
123 #[serde(default)]
124 pub position: FlangePosition,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub radius: Option<f64>,
128 #[serde(default)]
129 pub offset_start: f64,
130 #[serde(default)]
131 pub offset_end: f64,
132 #[serde(default)]
133 pub corner: Corner,
134}
135
136impl SheetStep for EdgeFlange {
137 fn plan(reader: &Reader, step: &str, args: &EdgeFlangeArgs) -> GeopResult<Made> {
138 let ctx = with_context!("edge_flange({step}, {args:?})");
139 let namer = Namer::new("edge_flange", step)?;
140 let model = read_model(reader).with_context(ctx)?;
141 let (solid, mut sheet, (f, k, toward_b)) =
142 with_edge(&model, reader, args.edge.as_ref(), "a flange").with_context(ctx)?;
143 let angle = args.angle.resolve(reader).with_context(ctx)?;
144 let length = args.length.resolve(reader).with_context(ctx)?;
145 let shape = args.shape(&sheet.rules, angle, length).with_context(ctx)?;
146 add_flange(&mut sheet, &namer, f, k, toward_b, &shape).with_context(ctx)?;
147 Ok(Made {
148 consumes: Some(solid),
149 solid: namer.root(),
150 making: Making::Bent(sheet),
151 })
152 }
153}
154
155pub(crate) fn edge_field<'a, A>(
158 f: &mut EditForm<'a, A>,
159 reader: &Reader<'a>,
160 edge: Option<&EdgeRef>,
161 set: impl Fn(Edit<'_, A, ()>, EdgeRef) + 'a,
162) {
163 f.pick_one("edge", "edge", reader, edge, set);
164}
165
166part_operation!(EdgeFlange(EdgeFlangeArgs));
167
168impl PartOperation for EdgeFlange {
169 const KIND: &'static str = "edge_flange";
170 const INFO: OperationInfo = OperationInfo::new(
171 "Edge flange",
172 "Bend a flange up from a straight edge of a sheet-metal body.",
173 OperationGroup::SheetMetal,
174 OperationTier::Small,
175 )
176 .icon(crate::icons::EDGE_FLANGE);
177 type Args = EdgeFlangeArgs;
178 type Session = ();
179
180 fn new_args(_: &Context) -> EdgeFlangeArgs {
182 EdgeFlangeArgs {
183 edge: None,
184 angle: NumericInput::Plain(90.0),
185 length: NumericInput::Plain(0.5),
186 reference: LengthReference::default(),
187 position: FlangePosition::default(),
188 radius: None,
189 offset_start: 0.0,
190 offset_end: 0.0,
191 corner: Corner::Open,
192 }
193 }
194
195 fn run(ctx: &mut StepContext, args: &EdgeFlangeArgs) -> GeopResult<()> {
196 let _step = ctx.step();
197 body::run::<Self>(ctx, args)
198 }
199
200 fn form<'a>(
204 context: &'a Context<'a>,
205 args: &EdgeFlangeArgs,
206 _: &FormState<()>,
207 ) -> EditForm<'a, EdgeFlangeArgs> {
208 let before = &context.before;
209 let mut f = EditForm::<EdgeFlangeArgs>::default();
210 edge_field(&mut f, before, args.edge.as_ref(), |edit, edge| {
211 edit.args.edge = Some(edge)
212 });
213 let angle = Number::numeric(&args.angle, "angle", before, Unit::Angle);
214 f.numeric("angle", angle.range(0.0, 180.0), |edit, angle| {
215 edit.args.angle = angle
216 });
217 let length = Number::numeric(&args.length, "length", before, Unit::Length);
218 f.numeric("length", length.range(0.0, 5.0), |edit, length| {
219 edit.args.length = length
220 });
221 f.select(
222 "reference",
223 "measured from",
224 match args.reference {
225 LengthReference::OuterSharp => "outer_sharp",
226 LengthReference::InnerSharp => "inner_sharp",
227 LengthReference::Tangent => "tangent",
228 },
229 vec![
230 Choice::new("outer_sharp", "Outer virtual sharp"),
231 Choice::new("inner_sharp", "Inner virtual sharp"),
232 Choice::new("tangent", "Bend tangent"),
233 ],
234 false,
235 |edit, choice| {
236 edit.args.reference = match choice {
237 "inner_sharp" => LengthReference::InnerSharp,
238 "tangent" => LengthReference::Tangent,
239 _ => LengthReference::OuterSharp,
240 }
241 },
242 );
243 f.select(
244 "position",
245 "position",
246 match args.position {
247 FlangePosition::MaterialInside => "material_inside",
248 FlangePosition::MaterialOutside => "material_outside",
249 FlangePosition::BendOutside => "bend_outside",
250 },
251 vec![
252 Choice::new("material_inside", "Material inside"),
253 Choice::new("material_outside", "Material outside"),
254 Choice::new("bend_outside", "Bend outside"),
255 ],
256 false,
257 |edit, choice| {
258 edit.args.position = match choice {
259 "material_outside" => FlangePosition::MaterialOutside,
260 "bend_outside" => FlangePosition::BendOutside,
261 _ => FlangePosition::MaterialInside,
262 }
263 },
264 );
265 f.checkbox(
266 "own_radius",
267 "own bend radius",
268 args.radius.is_some(),
269 |edit, on| edit.args.radius = on.then_some(edit.args.radius.unwrap_or(0.1)),
270 );
271 if let Some(radius) = args.radius {
272 let radius = Number::new("bend radius", radius, Unit::Length).range(0.0, 1.0);
273 f.number("radius", radius, |edit, r| edit.args.radius = Some(r));
274 }
275 offset_fields(
276 &mut f,
277 args.offset_start,
278 args.offset_end,
279 |args, [start, end]| {
280 args.offset_start = start;
281 args.offset_end = end;
282 },
283 );
284 f.select(
285 "corner",
286 "corner",
287 match args.corner {
288 Corner::Open => "open",
289 Corner::Closed => "closed",
290 },
291 vec![Choice::new("open", "Open"), Choice::new("closed", "Closed")],
292 false,
293 |edit, choice| {
294 edit.args.corner = match choice {
295 "closed" => Corner::Closed,
296 _ => Corner::Open,
297 }
298 },
299 );
300 f
301 }
302}
303
304impl ModelOperation for EdgeFlange {
305 fn build(
306 model: &mut NamedModel,
307 reader: &Reader,
308 step: &str,
309 _: &EdgeFlangeArgs,
310 ) -> GeopResult<()> {
311 body::build(model, reader, step)
312 }
313}
314
315pub(crate) fn offset_fields<'a, A>(
319 f: &mut EditForm<'a, A>,
320 offset_start: f64,
321 offset_end: f64,
322 set: impl Fn(&mut A, [f64; 2]) + Clone + 'a,
323) {
324 let start = Number::new("offset at start", offset_start, Unit::Length).range(0.0, 5.0);
325 let set_start = set.clone();
326 f.number("offset_start", start, move |edit, o| {
327 set_start(edit.args, [o, offset_end])
328 });
329 let end = Number::new("offset at end", offset_end, Unit::Length).range(0.0, 5.0);
330 f.number("offset_end", end, move |edit, o| {
331 set(edit.args, [offset_start, o])
332 });
333}
334
335pub(crate) struct FlangeShape {
341 pub angle: Scalar,
342 pub radius: Scalar,
343 pub set_back: Option<Scalar>,
344 pub flat: Scalar,
345 pub offset_start: f64,
346 pub offset_end: f64,
347 pub corner: Corner,
348}
349
350pub(crate) fn check_offsets(offset_start: f64, offset_end: f64) -> GeopResult<()> {
352 for (what, offset) in [("start", offset_start), ("end", offset_end)] {
353 if !(offset.is_finite() && offset >= 0.0) {
354 return Err(GeopError::new(format!(
355 "the offset at the {what} must not be negative, not {offset}"
356 )));
357 }
358 }
359 Ok(())
360}
361
362impl EdgeFlangeArgs {
363 fn shape(&self, rules: &SheetMetalRules, angle: f64, length: f64) -> GeopResult<FlangeShape> {
366 if !(angle > 0.0 && angle < 180.0) {
367 return Err(GeopError::new(format!(
368 "a flange turns by more than 0 and less than 180 degrees, not {angle}"
369 )));
370 }
371 let radius = self.radius.unwrap_or(rules.bend_radius);
372 if !(radius.is_finite() && radius > 0.0) {
373 return Err(GeopError::new(format!(
374 "the bend radius must be positive, not {radius}"
375 )));
376 }
377 check_offsets(self.offset_start, self.offset_end)?;
378 let s = Scalar::from_f64;
379 let t = s(rules.thickness);
380 let r = s(radius);
381 let angle = s(angle).degrees_to_radians()?;
382 let half = angle.div(Scalar::TWO)?;
383 let half_tan = half.sin().div(half.cos())?;
384 let set_back = match self.position {
385 FlangePosition::MaterialInside => Some(r.add(t).mul(half_tan)),
386 FlangePosition::MaterialOutside => Some(r.mul(half_tan)),
387 FlangePosition::BendOutside => None,
388 };
389 let flat = s(length).sub(match self.reference {
390 LengthReference::OuterSharp => r.add(t).mul(half_tan),
391 LengthReference::InnerSharp => r.mul(half_tan),
392 LengthReference::Tangent => Scalar::ZERO,
393 });
394 if !flat.definitely_greater(Scalar::ZERO) {
395 return Err(GeopError::new(format!(
396 "a flange {length} long leaves nothing flat after its bend ({flat:?})"
397 )));
398 }
399 Ok(FlangeShape {
400 angle,
401 radius: r,
402 set_back,
403 flat,
404 offset_start: self.offset_start,
405 offset_end: self.offset_end,
406 corner: self.corner,
407 })
408 }
409}
410
411pub(crate) fn add_flange(
414 sheet: &mut Sheet,
415 namer: &Namer,
416 f: usize,
417 k: usize,
418 toward_b: bool,
419 shape: &FlangeShape,
420) -> GeopResult<()> {
421 let rules = sheet.rules.clone();
422 let flat = &sheet.flats[f];
423 let edge = &flat.outer[k];
424 let name = edge.key();
425 let [a, b] = edge.line().ok_or_else(|| {
426 GeopError::new(format!(
427 "edge {name} is not straight: a flange is bent along a straight edge"
428 ))
429 })??;
430 if sheet.is_bent(&name) {
431 return Err(GeopError::new(format!("edge {name} is already bent")));
432 }
433 if sheet.layout()?.changed.contains(&name) {
434 return Err(GeopError::new(format!(
435 "edge {name} has been cut: a flange is bent along an edge as a flange or base flange built it — flange before cutting"
436 )));
437 }
438 let s = Scalar::from_f64;
439 let t = s(rules.thickness);
440 let (angle, r, set_back, length) = (shape.angle, shape.radius, shape.set_back, shape.flat);
441 let n = flat.outer.len();
442 let (prev, next) = ((k + n - 1) % n, (k + 1) % n);
443
444 let edge_length = b.sub(&a).norm();
446 let tau = b.sub(&a).normalize()?;
447 let inward = tau.perp();
448 let at = |along: Scalar, into: Scalar| {
449 a.add(&tau.prod_scalar(along))
450 .add(&inward.prod_scalar(into))
451 };
452 let d = set_back.unwrap_or(Scalar::ZERO);
453 let relief = rules.relief_size().map(s);
454 let depth = relief.map(|w| d.add(w));
455
456 let corner = |neighbour: usize, end: &str, offset: f64| -> GeopResult<(f64, Option<Scalar>)> {
460 let key = flat.outer[neighbour].key();
461 if !sheet.is_bent(&key) {
462 return Ok((0.0, None));
463 }
464 if rules.corner_gap <= 0.0 {
465 return Err(GeopError::new(format!(
466 "edge {name} meets the bent edge {key} at its {end}, and the corner gap is 0: the two bends would touch"
467 )));
468 }
469 if let Some(other) = sheet
473 .bends
474 .iter()
475 .find(|b| b.parent == f && b.parent_edge == key)
476 && other.toward_b == toward_b
477 {
478 let (from, along) = if neighbour == prev {
479 (a, tau)
480 } else {
481 (b, tau.neg())
482 };
483 let (from, along) = (flat.place.point(&from), flat.place.direction(&along));
484 let child = &sheet.flats[other.child];
485 let mut reach: Option<Scalar> = None;
486 for place in [child.place.clone(), child.place.offset(t)] {
487 for e in std::iter::once(&child.outer).chain(&child.holes).flatten() {
488 let r = place.point(&e.curve.start()?).sub(&from).prod_dot(&along);
489 reach = Some(reach.map_or(r, |x| x.max(r)));
490 }
491 }
492 if let Some(reach) = reach
493 && !s(offset + rules.corner_gap)
494 .sub(reach)
495 .definitely_greater(Scalar::ZERO)
496 {
497 return Err(GeopError::new(format!(
498 "edge {name} meets the bent edge {key} at its {end}, and the flat {} bent from it folds back over the sheet, {:.3} along edge {name}: a flange starting a corner gap from that end would pass through it — offset the flange from its {end} by more than {:.3}, or bend it the other way",
499 child.name.root(),
500 reach.to_f64(),
501 reach.to_f64() - rules.corner_gap
502 )));
503 }
504 }
505 let room = offset + rules.corner_gap;
509 match relief {
510 None if set_back.is_some() => {
511 return Err(GeopError::new(format!(
512 "edge {name} meets the bent edge {key} at its {end}: the flange starts the corner gap short of that bend, and with a tear its own bend, set back into the sheet, would touch the sheet left beside it — use a rectangular relief narrower than the corner gap, or bend the flange outside"
513 )));
514 }
515 None => {}
516 Some(w) if !s(room).sub(w).definitely_greater(Scalar::ZERO) => {
517 return Err(GeopError::new(format!(
518 "edge {name} meets the bent edge {key} at its {end}: the relief beside the flange, {} wide, does not fit in the corner gap of {} and the offset of {offset} — make the corner gap wider than the relief, the relief ratio smaller, or offset the flange further",
519 w.to_f64(),
520 rules.corner_gap
521 )));
522 }
523 Some(_) => {}
524 }
525 if shape.corner == Corner::Open || offset > 0.0 {
526 return Ok((rules.corner_gap, None));
527 }
528 let refuse = |why: &str| {
529 GeopError::new(format!(
530 "a closed corner at the {end} of edge {name}, where it meets {key}: {why} — close only corners of two flanges at right angles, turning the same way, of one radius, on edges at right angles"
531 ))
532 };
533 let other = sheet
534 .bends
535 .iter()
536 .find(|b| b.parent == f && b.parent_edge == key)
537 .ok_or_else(|| refuse("that edge is where a bend ends, not a flange beside it"))?;
538 let right = Scalar::PI.div(Scalar::TWO)?;
539 if !(other.angle.could_be_equal(right) && angle.could_be_equal(right)) {
540 return Err(refuse("the flanges do not both turn by 90 degrees"));
541 }
542 if other.toward_b != toward_b {
543 return Err(refuse("the flanges turn opposite ways"));
544 }
545 if !other.radius.could_be_equal(r) {
546 return Err(refuse("the bends' radii differ"));
547 }
548 let [p, q] = flat.outer[neighbour]
549 .line()
550 .ok_or_else(|| refuse("that edge is not straight"))??;
551 if !q
552 .sub(&p)
553 .normalize()?
554 .prod_dot(&b.sub(&a).normalize()?)
555 .could_be_equal(Scalar::ZERO)
556 {
557 return Err(refuse("the edges do not meet at a right angle"));
558 }
559 Ok((rules.corner_gap, Some(other.radius)))
562 };
563 let (gap_start, close_start) = corner(prev, "start", shape.offset_start)?;
564 let (gap_end, close_end) = corner(next, "end", shape.offset_end)?;
565 let inset_start = shape.offset_start + gap_start;
566 let inset_end = shape.offset_end + gap_end;
567 let (s0, s1) = (s(inset_start), edge_length.sub(s(inset_end)));
568 if !s1.sub(s0).definitely_greater(Scalar::ZERO) {
569 return Err(GeopError::new(format!(
570 "edge {name} is {edge_length:?} long, too short for a flange set in {inset_start} at its start and {inset_end} at its end"
571 )));
572 }
573
574 if set_back.is_some() && relief.is_none() && !(inset_start == 0.0 && inset_end == 0.0) {
577 return Err(GeopError::new(format!(
578 "a flange on part of edge {name}, set back into the sheet, needs a relief beside it: with a tear, the bend would touch the sheet beside it — use a rectangular relief, run the flange along the whole edge, or bend it outside"
579 )));
580 }
581
582 let full = (inset_start == 0.0, inset_end == 0.0);
585 let notched = relief.is_some() && !(full.0 && full.1);
586 let height = if notched {
587 depth.expect("a relief has a depth")
588 } else {
589 d
590 };
591 if height.definitely_greater(Scalar::ZERO) {
592 let w = if notched {
593 relief.expect("a relief")
594 } else {
595 Scalar::ZERO
596 };
597 let lo = if full.0 { Scalar::ZERO } else { s0.sub(w) };
598 let hi = if full.1 { edge_length } else { s1.add(w) };
599 for e in std::iter::once(&flat.outer).chain(&flat.holes).flatten() {
600 let p = e.curve.start()?;
601 if p.could_be_equal(&a) || p.could_be_equal(&b) {
602 continue;
603 }
604 let (along, into) = (p.sub(&a).prod_dot(&tau), p.sub(&a).prod_dot(&inward));
605 if along.could_be_greater(lo)
606 && along.could_be_less(hi)
607 && into.could_be_greater(Scalar::ZERO)
608 && into.could_be_less(height)
609 {
610 return Err(GeopError::new(format!(
611 "a flange on edge {name} cuts {height:?} into flat {}, through its corner where {} starts: make the flange narrower or shorter, or bend it outside",
612 flat.name.root(),
613 e.key()
614 )));
615 }
616 }
617 }
618
619 let mut run: Vec<(Vector2, Namer)> = Vec::new();
621 let mut corner_start = None;
622 let mut corner_end = None;
623 if inset_start == 0.0 {
624 match set_back {
625 Some(d) => {
626 let corner = trim_point(flat, prev, at(Scalar::ZERO, d), d, &name, "start")?;
627 corner_start = Some(corner);
628 run.push((corner, namer.scoped("line")));
629 }
630 None => run.push((a, namer.scoped("line"))),
631 }
632 } else {
633 run.push((a, namer.scoped("before")));
634 if let (Some(w), Some(depth)) = (relief, depth) {
635 fits(s0.sub(w), &name, "start")?;
636 let relief = namer.scoped("relief0");
637 run.push((at(s0.sub(w), Scalar::ZERO), relief.scoped("in")));
638 run.push((at(s0.sub(w), depth), relief.scoped("across")));
639 run.push((at(s0, depth), relief.scoped("out")));
640 }
641 run.push((at(s0, d), namer.scoped("line")));
642 }
643 let line_start = run.last().expect("the flange's line").0;
644 let line_end;
645 if inset_end == 0.0 {
646 match set_back {
647 Some(d) => {
648 let corner = trim_point(flat, next, at(edge_length, d), d, &name, "end")?;
649 corner_end = Some(corner);
650 line_end = corner;
651 }
652 None => line_end = b,
653 }
654 } else {
655 line_end = at(s1, d);
656 match (relief, depth) {
657 (Some(w), Some(depth)) => {
658 fits(edge_length.sub(s1.add(w)), &name, "end")?;
659 let relief = namer.scoped("relief1");
660 run.push((line_end, relief.scoped("in")));
661 run.push((at(s1, depth), relief.scoped("across")));
662 run.push((at(s1.add(w), depth), relief.scoped("out")));
663 run.push((at(s1.add(w), Scalar::ZERO), namer.scoped("after")));
664 }
665 _ => run.push((line_end, namer.scoped("after"))),
666 }
667 }
668 let end_point = if inset_end == 0.0 { line_end } else { b };
669 let mut edges = Vec::new();
670 for (i, (from, name)) in run.iter().enumerate() {
671 let to = run.get(i + 1).map_or(end_point, |(p, _)| *p);
672 edges.push(straight(*from, to, name.clone())?);
673 }
674
675 let out = inward.neg().prod_scalar(length);
678 let flange = namer.scoped("flange");
679 let (c_a, c_b) = (line_start, line_end);
680 let e_a = close_start.map_or(c_a, |x| c_a.sub(&tau.prod_scalar(x)));
683 let e_b = close_end.map_or(c_b, |x| c_b.add(&tau.prod_scalar(x)));
684 let mut outer = vec![straight(c_b, c_a, flange.scoped("line"))?];
685 if close_start.is_some() {
686 outer.push(straight(c_a, e_a, flange.scoped("corner0"))?);
687 }
688 outer.push(straight(e_a, e_a.add(&out), flange.scoped("side0"))?);
689 outer.push(straight(
690 e_a.add(&out),
691 e_b.add(&out),
692 flange.scoped("end"),
693 )?);
694 outer.push(straight(e_b.add(&out), e_b, flange.scoped("side1"))?);
695 if close_end.is_some() {
696 outer.push(straight(e_b, c_b, flange.scoped("corner1"))?);
697 }
698 let place = flat.place.clone();
699 let frame = BendFrame::new(&place, [c_a, c_b], angle, r, t, toward_b)?;
700 let child = Flat {
701 place: frame.child_placement(&place),
702 outer,
703 holes: Vec::new(),
704 name: flange.clone(),
705 };
706
707 let flat = &mut sheet.flats[f];
708 if let Some(corner) = corner_start {
709 trim(&mut flat.outer[prev], None, Some(corner))?;
710 }
711 if let Some(corner) = corner_end {
712 trim(&mut flat.outer[next], Some(corner), None)?;
713 }
714 flat.outer.splice(k..k + 1, edges);
715 sheet.flats.push(child);
716 sheet.bends.push(Bend {
717 parent: f,
718 parent_edge: namer.scoped("line").root(),
719 child: sheet.flats.len() - 1,
720 child_edge: flange.scoped("line").root(),
721 angle,
722 radius: r,
723 toward_b,
724 name: namer.scoped("bend"),
725 });
726 Ok(())
727}
728
729fn fits(left: Scalar, name: &str, end: &str) -> GeopResult<()> {
732 if left.definitely_greater(Scalar::ZERO) {
733 Ok(())
734 } else {
735 Err(GeopError::new(format!(
736 "the relief at the {end} of edge {name} does not fit beside it: offset the flange further, or use a tear"
737 )))
738 }
739}
740
741fn trim_point(
746 flat: &Flat,
747 neighbour: usize,
748 corner: Vector2,
749 d: Scalar,
750 name: &str,
751 end: &str,
752) -> GeopResult<Vector2> {
753 let edge = &flat.outer[neighbour];
754 let refuse = |why: &str| {
755 GeopError::new(format!(
756 "a flange set back along all of edge {name} cuts back the edge {} at its {end}, which {why}: offset the flange from that end, or bend it outside",
757 edge.key()
758 ))
759 };
760 let [p, q] = edge.line().ok_or_else(|| refuse("is not straight"))??;
761 let this = &flat.outer[flat
762 .outer
763 .iter()
764 .position(|e| e.key() == name)
765 .expect("the edge is in its flat")];
766 let [a, b] = this.line().expect("the edge is straight")?;
767 let along = q.sub(&p).normalize()?;
768 if !along
769 .prod_dot(&b.sub(&a).normalize()?)
770 .could_be_equal(Scalar::ZERO)
771 {
772 return Err(refuse("does not meet it at a right angle"));
773 }
774 if !q.sub(&p).norm().definitely_greater(d) {
775 return Err(refuse("is shorter than the set back"));
776 }
777 Ok(corner)
778}
779
780fn trim(edge: &mut FlatEdge, start: Option<Vector2>, end: Option<Vector2>) -> GeopResult<()> {
782 let [p, q] = edge
783 .line()
784 .ok_or_else(|| GeopError::new(format!("edge {} is not straight", edge.key())))??;
785 edge.curve = NurbCurve2D::line(start.unwrap_or(p), end.unwrap_or(q))?;
786 Ok(())
787}