Skip to main content

geop_ops_sheetmetal/
edge_flange.rs

1//! [`EdgeFlange`]: a flange bent up from a straight edge of a sheet-metal
2//! body.
3
4use 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/// What a flange's length is measured from.
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum LengthReference {
33    /// Where the outsides of the sheet and the flange would meet were the
34    /// bend sharp.
35    #[default]
36    OuterSharp,
37    /// Where their insides would meet.
38    InnerSharp,
39    /// Where the bend ends: the flange's flat part only.
40    Tangent,
41}
42
43/// Where a flange's bend sits relative to the edge it is bent from.
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum FlangePosition {
47    /// Inside the sheet's old outline: the outside of the flange where the
48    /// edge was — the sheet set back by the bend.
49    #[default]
50    MaterialInside,
51    /// The inside of the flange where the edge was.
52    MaterialOutside,
53    /// The bend starts at the edge: everything added outside.
54    BendOutside,
55}
56
57/// What a flange does at an end of its edge where a flange already stands
58/// on the edge beside it.
59#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum Corner {
62    /// It stops the body's corner gap short of the other flange's bend.
63    #[default]
64    Open,
65    /// Its bend stops the corner gap short of the other's, and its flat
66    /// reaches on, past its bend, to the corner gap from the other flange's
67    /// inside: the corner closed. For two flanges at right angles, turning
68    /// the same way, of one radius, on edges at right angles.
69    Closed,
70}
71
72/// Bends a flange up from the straight edge `edge` of a sheet-metal body,
73/// for the operation `F`: the body is consumed and rebuilt as
74/// `edge_flange(F)`, every face, edge and vertex it had keeping its name
75/// except the edge, which the flange replaces.
76///
77/// `edge` is an edge of a flat face of the body on its outline (where the
78/// face meets a wall across the thickness), and the flange turns `angle`
79/// degrees towards that face's side, `length` long as `reference` says,
80/// with its bend's inner radius `radius` (the body's own by default), set
81/// as `position` says. It runs along the edge from `offset_start` after
82/// the edge's start to `offset_end` before its end (the edge running with
83/// its face on its left, seen from outside); where it is narrower than the
84/// edge, the body's relief is cut beside the bend. An end of the edge that
85/// meets a bend already there keeps the body's corner gap from it — or,
86/// with a closed `corner`, its flat reaches on to the other flange.
87///
88/// What it builds is named `edge_flange(F,...)`: the bend `bend`, the
89/// flange's flat face `flange` with its edges `flange,line` where the bend
90/// ends, `flange,side0`, `flange,end` and `flange,side1` — and where a
91/// closed corner carries its flat past its bend, `flange,corner0` or
92/// `flange,corner1` on from its line; where the bend
93/// starts on the body `line`, the rest of the edge `before` and `after`,
94/// and the reliefs
95/// `relief0,in`, `relief0,across`, `relief0,out` (and `relief1,...`) — each
96/// a face's name with `a`/`b` added, an edge's or a wall's as
97/// [`crate::thicken`] says.
98#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
99pub struct EdgeFlange;
100
101/// A straight edge of the part, by name.
102pub type EdgeRef = ElementReference<NamedModel, Line>;
103
104/// The straight edge named `name`.
105pub 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    /// The edge, of a flat face of a sheet-metal body; none yet, for a new
112    /// flange waiting for one to be picked.
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub edge: Option<EdgeRef>,
115    /// How far the flange turns, in degrees: more than 0, less than 180 —
116    /// a number, or a formula of the part's parameters.
117    pub angle: NumericInput,
118    /// How long it is, measured from its `reference`: a number, or a
119    /// formula of the part's parameters.
120    pub length: NumericInput,
121    #[serde(default)]
122    pub reference: LengthReference,
123    #[serde(default)]
124    pub position: FlangePosition,
125    /// The bend's inner radius, if not the body's.
126    #[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
155/// The edge field of a flange or hem, holding `edge`, in the part `reader`
156/// reads; `set` gets the edge picked.
157pub(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    /// No edge yet: a right angle, half a unit long, material inside.
181    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    /// The edge, picked; angle and length; what the length is measured from
201    /// and where the bend sits; the radius, if its own; and how far in
202    /// from the edge's ends.
203    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
315/// The fields of how far in from the edge's ends a flange or hem runs,
316/// `offset_start` and `offset_end`. `set` puts the two offsets set where
317/// the arguments keep them.
318pub(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
335/// What a flange is, worked out from the arguments of the operation that
336/// adds it: how far it turns and at what inner radius, how far its bend is
337/// set back into the sheet (`None`: it starts at the edge), how long its
338/// flat is, how far in from the edge's ends it runs, and what it does at a
339/// corner.
340pub(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
350/// Checks that the offsets from the edge's ends are not negative.
351pub(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    /// The flange these arguments describe, on a body of `rules`, turning
364    /// `angle` degrees and `length` long — the values its formulas came to.
365    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
411/// Adds the flange `shape` describes to `sheet`, bent from edge `k` of flat
412/// `f`'s outline towards its B side if `toward_b` (see [`EdgeFlange`]).
413pub(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    // Along the edge `s`, into the flat `h`.
445    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    // Each end: how far in from the corner the flange starts if a bend
457    // meets it there, and how far its flat reaches on past its bend if it
458    // closes the corner.
459    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        // A bend there turning the same way past a quarter turn folds its
470        // flat back over this one (a hem): the flange must start past all
471        // of it, measured along the edge from the corner.
472        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        // The flange starts the gap short of that bend, and the sheet left
506        // in the gap would touch its bend: only a relief beside the flange,
507        // in the gap and the offset, keeps the two apart.
508        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        // The other flange's inside lies its radius short of where this
560        // bend starts, less the gap: however each is set back.
561        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    // A set-back bend narrower than its edge lies where the sheet beside
575    // it still is: only a relief keeps the two apart.
576    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    // What the flange cuts out of the flat — the strip it is set back by,
583    // the reliefs — must lie inside it: no corner of the flat in it.
584    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    // The new edges in place of the edge, as the points they start at.
620    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    // The flange: its line is the body's, run the other way; it reaches out
676    // across it, away from the flat.
677    let out = inward.neg().prod_scalar(length);
678    let flange = namer.scoped("flange");
679    let (c_a, c_b) = (line_start, line_end);
680    // Where a closed corner carries the flat on past the bend, along the
681    // line it starts at.
682    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
729/// Checks that a relief leaves `left` of the edge `name` beside it, at its
730/// `end`.
731fn 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
741/// Where the flange's set-back line meets the straight edge `neighbour` of
742/// `flat` at the `end` of the edge `name` it is bent from: `corner`, a set
743/// back `d` along it — the neighbour must meet the edge at a right angle,
744/// and be longer than that.
745fn 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
780/// `edge` with its start or end moved to the given point.
781fn 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}