BREP_kernel 0.15.0

A boundary representation (BREP) geometry kernel for building CAD applications.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
use super::*;
use super::bend::build_bend;
use super::collar::{build_collar, segment_endpoints_2d};
use super::frame::Frame;
use super::plate::build_flat_plate;

/// One flat's placement in the FOLDED (`fold = 1`) part, plus the local data a
/// profile-mapping consumer (SM.CUTOUT) needs: the world frame, the outline
/// polygon, and the bend-wedge footprint strips adjoining the flat (regions a
/// cut loop must not enter — the fold arcs live there).
pub struct FlatPlacement {
    /// The flat's tree id (`Flat::id`) — the key for mutating it back.
    pub id: String,
    /// World frame: local `(x, y, z)` maps to `origin + x·u + y·v + z·w`.
    pub origin: Vec3,
    pub u: Vec3,
    pub v: Vec3,
    /// The flat normal (thickness direction).
    pub w: Vec3,
    /// The flat's closed CCW footprint polygon (local 2D). Curved outline
    /// segments ([`Flat::outline_curves`]) are DENSIFIED (16 samples each) so
    /// the consumers' polygon overlap/extent tests see the true bulge, not the
    /// chord skeleton — every consumer is a permissive containment/extent test,
    /// never index-aligned with `Flat::outline`.
    pub outline: Vec<[f64; 2]>,
    /// Bend-wedge footprints adjoining this flat, in ITS local 2D frame.
    pub wedge_strips: Vec<WedgeStrip>,
}

/// A conservative bend-wedge footprint: the strip beyond the directed edge
/// `a -> b` (on its RIGHT side), reaching `reach` outward. The parent flat gets
/// one per folded edge; the child flat gets one along its attach seam.
pub struct WedgeStrip {
    pub a: [f64; 2],
    pub b: [f64; 2],
    /// Outward reach: the bend's OUTER radius (>= the wedge's true in-plane reach).
    pub reach: f64,
    /// The bend id, for error messages.
    pub bend_id: String,
}

/// Walk `tree` at `fold = 1.0` and return every flat's world placement (the
/// same frames [`evaluate`] places plates with) plus bend-strip metadata.
/// Bend wedges built during the walk are discarded — this reuses [`build_bend`]
/// verbatim so the frames can never diverge from the geometry.
pub fn folded_flat_placements(tree: &SheetTree) -> Result<Vec<FlatPlacement>, String> {
    let mut placements = Vec::new();
    let root = Frame::from_placement(&tree.root_transform);
    collect_placements(&tree.root, root, tree.thickness, None, &mut placements)?;
    Ok(placements)
}

/// Recursive body of [`folded_flat_placements`]. `seam` is the child-side strip
/// of the bend that placed this flat (None for the root).
fn collect_placements(
    flat: &Flat,
    frame: Frame,
    thickness: f64,
    seam: Option<WedgeStrip>,
    out: &mut Vec<FlatPlacement>,
) -> Result<(), String> {
    let mut placement = FlatPlacement {
        id: flat.id.clone(),
        origin: frame.o,
        u: frame.u,
        v: frame.v,
        w: frame.w,
        outline: flat.densified_outline()?,
        wedge_strips: seam.into_iter().collect(),
    };
    let n = flat.outline.len();
    let mut children = Vec::new();
    for (index, edge) in flat.edges.iter().enumerate() {
        let Some(bend) = &edge.bend else { continue };
        let a = flat.outline[index];
        let b = flat.outline[(index + 1) % n];
        let r_out = bend.mid_radius(thickness) + thickness * 0.5;
        // Parent-side strip: beyond the fold edge a -> b (outward = right, CCW).
        placement.wedge_strips.push(WedgeStrip {
            a,
            b,
            reach: r_out,
            bend_id: bend.id.clone(),
        });
        let (_wedge, child_frame) = build_bend(bend, a, b, frame, thickness, 1.0)?;
        let seam_len = ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
        // Child-side strip: the seam runs along child-local v from (0,0) to
        // (0, L); the wedge lies on the -u side, i.e. right of (0,L) -> (0,0).
        let child_seam = WedgeStrip {
            a: [0.0, seam_len],
            b: [0.0, 0.0],
            reach: r_out,
            bend_id: bend.id.clone(),
        };
        children.push((&*bend.child, child_frame, child_seam));
    }
    // Hole-rim flaps are flats too: recurse into every straight hole-bend child
    // (mirroring [`emit_hole_bend`]'s frame construction) so consumers like
    // SM.CUTOUT can target walls hanging off hole flaps. The parent gets the
    // flap's bend strip (the fold arc reaches INTO the opening); a collar has
    // no child flat — nothing to place. KNOWN GAP (documented, loud-free): a
    // collar's annular bend footprint is not represented as wedge strips, so a
    // flat-pattern cut crossing a collar rim is not rejected; the cut bakes
    // into the plate and leaves the collar untouched.
    for hole_bend in &flat.hole_bends {
        let HoleBendKind::Straight { child } = &hole_bend.kind else {
            continue;
        };
        let hole = flat.holes.get(hole_bend.hole).ok_or_else(|| {
            format!(
                "sheet-metal: hole bend `{}` references missing hole {} on flat `{}`",
                hole_bend.id, hole_bend.hole, flat.id
            )
        })?;
        let curve = hole.outer.get(hole_bend.segment).ok_or_else(|| {
            format!(
                "sheet-metal: hole bend `{}` references missing segment {} of hole {}",
                hole_bend.id, hole_bend.segment, hole_bend.hole
            )
        })?;
        let (mut a, mut b) = segment_endpoints_2d(curve)?;
        if hole_bend.reversed {
            std::mem::swap(&mut a, &mut b);
        }
        let bend = Bend {
            id: hole_bend.id.clone(),
            angle_deg: hole_bend.angle_deg,
            inside_radius: hole_bend.inside_radius,
            k_factor: hole_bend.k_factor,
            child: child.clone(),
            reliefs: Vec::new(),
        };
        let r_out = bend.mid_radius(thickness) + thickness * 0.5;
        placement.wedge_strips.push(WedgeStrip {
            a,
            b,
            reach: r_out,
            bend_id: bend.id.clone(),
        });
        let (_wedge, child_frame) = build_bend(&bend, a, b, frame, thickness, 1.0)?;
        let seam_len = ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
        let child_seam = WedgeStrip {
            a: [0.0, seam_len],
            b: [0.0, 0.0],
            reach: r_out,
            bend_id: bend.id.clone(),
        };
        children.push((&**child, child_frame, child_seam));
    }
    out.push(placement);
    for (child, child_frame, child_seam) in children {
        collect_placements(child, child_frame, thickness, Some(child_seam), out)?;
    }
    Ok(())
}


/// Build the exact-BREP solid for `tree` at the given `fold` fraction
/// (`1.0` folded, `0.0` flat). Returns one merged solid.
pub fn evaluate(tree: &SheetTree, fold: f64) -> Result<BrepSolid, String> {
    if tree.thickness <= 0.0 {
        return Err(format!(
            "sheet-metal: thickness must be positive, got {}",
            tree.thickness
        ));
    }
    guard_curved_edge_bends(&tree.root)?;
    let mut parts: Vec<BrepSolid> = Vec::new();
    let root = Frame::from_placement(&tree.root_transform);
    emit_flat(&tree.root, root, tree.thickness, fold, &mut parts)?;
    // Flat pattern (fold ≈ 0): keep coplanar faces UN-merged so every bend's
    // allowance strip survives as named `{bendId}:BAND:…` faces and the fold
    // lines stay real BREP edges (see [`strip_flat`] for the naming contract).
    // Folded (fold ≠ 0): merge as before — the coplanar plate/wedge interfaces
    // are construction seams, not annotations.
    // Adjacent bends share the already-trimmed plate corner. If sequential
    // assembly cannot join that contact, assemble all of its interfaces in
    // one arrangement, before intermediate edge coalescing changes a rim's
    // parameterization. Existing successful assemblies keep their topology.
    let joint_parts = (fold.abs() >= 1e-9 && has_adjacent_bends(&tree.root) && adjacent_bend_fallback_allowed())
        .then(|| parts.clone());
    let mut solid = match union_all(parts, fold.abs() >= 1e-9) {
        Ok(solid) => solid,
        Err(error) => match joint_parts {
            // The same options as the sequential path: a side wall, bend end
            // cap, cutout wall or collar wall stays its own face on EVERY
            // assembly route (owner 2026-10-27 — the fallback under default
            // options fused the plate's remaining walls into the flange end
            // caps at t = 1 while t = 7.5, assembled sequentially, kept them).
            Some(parts) => crate::boolean_operation_nary_with_options(
                &parts,
                BooleanOperation::Union,
                &folded_union_options(true),
            )
            .map_err(|_| error)?,
            None => return Err(error),
        },
    };
    // Name every plate/bend edge `{faceA}|{faceB}[n]` (sorted face names + a
    // per-pair counter) so the SM body's edges become pickable by name — the app
    // resolves an edge selection strictly by its scene name. In particular a
    // vertical thickness CORNER edge between two `{flat}:SIDE:{e}` walls becomes
    // `{flat}:SIDE:{eA}|{flat}:SIDE:{eB}[n]`, which the corner fillet/chamfer
    // feature parses back into that adjacent SIDE-face pair. This pass is the
    // shared assembly point for every SM feature (all call `evaluate`), at both
    // fold=0 and fold=1. It is purely ADDITIVE — `extrude_profile_brep` leaves SM
    // edges unnamed — and deterministic given SM's deterministic face names +
    // union topology. The UNFOLD feature re-stamps via `register_added`; that pass
    // is idempotent (it re-derives the same `{faceA}|{faceB}[n]` names), so
    // unfold's output stays byte-identical.
    crate::feature_pipeline::features::common::stamp_derived_edge_names(&mut solid);
    // A mirrored part carries the names Mirror gave its copy (`::{suffix}`).
    for suffix in &tree.name_suffixes {
        crate::feature_pipeline::features::common::namespace_copy_names(&mut solid, suffix);
    }
    Ok(solid)
}

/// Whether [`evaluate`] may fall back to the one-arrangement assembly of an
/// adjacent-bend tree. Always, outside the private tests: a test probing the
/// SEQUENTIAL route alone (the thin-stock tangent-contact row, 2026-10-28)
/// switches the fallback off on its thread so the sequential refusal is the
/// verdict instead of the safety net's success.
fn adjacent_bend_fallback_allowed() -> bool {
    true
}


fn has_adjacent_bends(flat: &Flat) -> bool {
    let n = flat.edges.len();
    flat.edges.iter().enumerate().any(|(i, edge)| {
        edge.bend.as_ref().is_some_and(|bend| {
            flat.edges[(i + 1) % n].bend.is_some() || has_adjacent_bends(&bend.child)
        })
    })
}

/// Refuse (loudly, before any geometry) a bend anchored on a CURVED outline
/// segment: the whole bend model — fold line, wedge revolve axis, setback
/// surgery, allowance strip — assumes a straight hinge. An outline segment is
/// curved iff its edge id keys [`Flat::outline_curves`]; flange setback surgery
/// keeps the fold segment's ORIGINAL id, so a graft onto a curved edge is
/// always caught here regardless of which surgery path produced it.
fn guard_curved_edge_bends(flat: &Flat) -> Result<(), String> {
    for edge in &flat.edges {
        let Some(bend) = &edge.bend else { continue };
        if flat.outline_curves.contains_key(&edge.id) {
            return Err(format!(
                "sheet-metal: edge `{}` of flat `{}` is a curved outline edge — \
                 flanges/hems fold straight edges only",
                edge.id, flat.id
            ));
        }
        guard_curved_edge_bends(&bend.child)?;
    }
    for hole_bend in &flat.hole_bends {
        if let HoleBendKind::Straight { child } = &hole_bend.kind {
            guard_curved_edge_bends(child)?;
        }
    }
    Ok(())
}

/// Emit the plate for `flat` (placed by `frame`) plus, recursively, every bend
/// hanging off its edges and the child flats they reach.
fn emit_flat(
    flat: &Flat,
    frame: Frame,
    thickness: f64,
    fold: f64,
    parts: &mut Vec<BrepSolid>,
) -> Result<(), String> {
    parts.push(build_flat_plate(flat, thickness, frame)?);

    for (index, edge) in flat.edges.iter().enumerate() {
        let Some(bend) = &edge.bend else { continue };
        let n = flat.outline.len();
        let a = flat.outline[index];
        let b = flat.outline[(index + 1) % n];
        let (wedge, child_frame) = build_bend(bend, a, b, frame, thickness, fold)?;
        if let Some(wedge) = wedge {
            parts.push(wedge);
        }
        emit_flat(&bend.child, child_frame, thickness, fold, parts)?;
    }
    for hole_bend in &flat.hole_bends {
        emit_hole_bend(flat, hole_bend, frame, thickness, fold, parts)?;
    }
    Ok(())
}

/// Emit the geometry of one hole-rim fold: a straight window segment folds a
/// child wall INTO the opening through the same [`build_bend`] machinery as an
/// outline flange; a circular loop grows a [`build_collar`] revolve.
fn emit_hole_bend(
    flat: &Flat,
    hole_bend: &HoleBend,
    frame: Frame,
    thickness: f64,
    fold: f64,
    parts: &mut Vec<BrepSolid>,
) -> Result<(), String> {
    let hole = flat.holes.get(hole_bend.hole).ok_or_else(|| {
        format!(
            "sheet-metal: hole bend `{}` references missing hole {} on flat `{}`",
            hole_bend.id, hole_bend.hole, flat.id
        )
    })?;
    match &hole_bend.kind {
        HoleBendKind::Straight { child } => {
            let curve = hole.outer.get(hole_bend.segment).ok_or_else(|| {
                format!(
                    "sheet-metal: hole bend `{}` references missing segment {} of hole {}",
                    hole_bend.id, hole_bend.segment, hole_bend.hole
                )
            })?;
            let (mut a, mut b) = segment_endpoints_2d(curve)?;
            if hole_bend.reversed {
                std::mem::swap(&mut a, &mut b);
            }
            // Reuse the outline-bend machinery through a transient Bend record —
            // the (possibly reversed) segment direction already puts the opening
            // on the fold side (`ê × n̂`).
            let bend = Bend {
                id: hole_bend.id.clone(),
                angle_deg: hole_bend.angle_deg,
                inside_radius: hole_bend.inside_radius,
                k_factor: hole_bend.k_factor,
                child: child.clone(),
                reliefs: Vec::new(),
            };
            let (wedge, child_frame) = build_bend(&bend, a, b, frame, thickness, fold)?;
            if let Some(wedge) = wedge {
                parts.push(wedge);
            }
            emit_flat(child, child_frame, thickness, fold, parts)
        }
        HoleBendKind::Collar { leg } => {
            if let Some(collar) =
                build_collar(hole_bend, &hole.outer, *leg, frame, thickness, fold)?
            {
                parts.push(collar);
            }
            Ok(())
        }
    }
}

/// The boolean options of every folded-body assembly route — the sequential
/// [`union_all`] and the one-arrangement fallback for adjacent bends alike.
fn folded_union_options(merge_coplanar: bool) -> BooleanOptions {
    BooleanOptions {
        merge_coplanar_faces: merge_coplanar,
        // Keep every PLANAR thickness/wall face per outline segment even when the
        // folded body coalesces coplanar faces: the big top/bottom plate faces
        // (`:A`/`:B`, and a blind pocket floor) still merge, but a plate side
        // wall (`:SIDE:`), a bend transverse end cap (`:END:`), a hole-bore or
        // island wall (`:CUTOUT:`), and a collar wall (`:wall:`) each stay their
        // own face. Fusing two collinear thickness faces (e.g. a plate side wall
        // meeting a flange's bend end in the same transverse plane) would erase
        // the segment boundary a later flange needs to attach to. Relief/corner
        // treatments bake into the outline (`:SIDE:`) or holes (`:CUTOUT:`), so
        // this list covers them too. The flat pattern passes
        // `merge_coplanar = false`, so the list is inert there.
        keep_unmerged_name_substrs: vec![
            ":SIDE:".to_string(),
            ":END:".to_string(),
            ":CUTOUT:".to_string(),
            ":wall:".to_string(),
        ],
        ..BooleanOptions::default()
    }
}

/// Union every part into one solid (base ∪ wedge ∪ child ∪ …). Empty/degenerate
/// parts are skipped. `merge_coplanar` merges the coplanar plate/bend interface
/// faces (the folded body); the flat pattern passes `false` so the bend BAND
/// faces — same plane as the plates on purpose — keep their identity.
fn union_all(parts: Vec<BrepSolid>, merge_coplanar: bool) -> Result<BrepSolid, String> {
    let options = folded_union_options(merge_coplanar);
    let mut current: Option<BrepSolid> = None;
    for part in parts {
        if part.shells.is_empty() {
            continue; // a zero-fold wedge, etc.
        }
        current = Some(match current.take() {
            None => part,
            Some(running) => {
                let running_handle = crate::register_solid_value(running);
                let part_handle = crate::register_solid_value(part);
                let folded = crate::with_two_registered_solids(
                    running_handle,
                    part_handle,
                    |run, add| boolean_operation(run, add, BooleanOperation::Union, &options),
                );
                crate::free_registered_solid(running_handle);
                crate::free_registered_solid(part_handle);
                folded.map_err(|error| format!("sheet-metal union failed: {error}"))?
            }
        });
    }
    current.ok_or_else(|| "sheet-metal: tree produced no geometry".to_string())
}