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
//! Face-profile extraction — a resident solid FACE as an extrude/revolve profile.
//!
//! Given a resolved `FaceRef`, order the face's boundary edges into closed
//! head-to-tail loop(s) so the profile-consumers (extrude, revolve) can build
//! from a prior solid's face exactly like from a sketch.
//!
//! Unlike the retired builder (which reconstructs lines/arcs from render polylines and
//! rejects everything else), this reads the EXACT trimmed edge curves off the
//! BREP — every boundary curve type a planar face can carry is supported. The
//! coedges of a loop are already stored in traversal order, so no endpoint
//! matching is needed: a `forward` coedge contributes the edge curve trimmed to
//! `[t0, t1]` as-is, a reversed one contributes it reversed, and the chain is
//! head-to-tail by construction.
//!
//! Loop convention (kernel-wide, asserted by `validate_uv_wire`): `loops[0]` is
//! the OUTER boundary, later loops are holes; the outer loop traversed in coedge
//! order runs CCW around the OUTWARD face normal for either `same_sense`, so the
//! emitted profile winds positively about its `z_axis` and the consumers' name↔
//! face alignment holds without a winding flip.
//!
//! Planar carriers only (the `common::face_frame` guard) — a curved face errors
//! loudly, no fallback.
use crate::feature_pipeline::features::common;
use crate::feature_pipeline::{FaceRef, ProfileLoop, SketchProfile};
/// Extract a [`SketchProfile`] (outer loop first, holes after; plane frame from
/// the face carrier) from a resident face. Errors loudly on a non-planar
/// carrier or a boundary the kernel cannot trim — never a silent drop.
pub fn face_profile(face: FaceRef) -> Result<SketchProfile, String> {
// Plane frame first (short registry borrow inside): origin at the boundary
// AABB center, z_axis = OUTWARD face normal. Non-planar carrier → loud error.
let frame = common::face_frame(face).map_err(|error| format!("face-profile: {error}"))?;
let loops = crate::with_registered_solid_str(face.handle, |solid| {
let record = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|candidate| candidate.id == face.face_id)
.ok_or_else(|| format!("face-profile: face {} not found on solid", face.face_id))?;
let mut loops: Vec<ProfileLoop> = Vec::with_capacity(record.loops.len());
for loop_record in &record.loops {
let mut curves = Vec::with_capacity(loop_record.coedges.len());
let mut edge_names = Vec::with_capacity(loop_record.coedges.len());
for coedge in &loop_record.coedges {
let edge = solid
.edges
.iter()
.find(|candidate| candidate.id == coedge.edge_id)
.ok_or_else(|| {
format!("face-profile: edge {} not found on solid", coedge.edge_id)
})?;
if edge.degenerate {
continue; // pole/apex edges bound no length — skip
}
// The edge curve's domain may exceed [t0, t1] — trim to the
// segment, then orient head-to-tail via the coedge sense.
let trimmed = common::trimmed_curve(&edge.curve, edge.t0, edge.t1).map_err(|error| {
format!("face-profile: edge {} trim failed: {error}", edge.id)
})?;
let oriented = if coedge.forward {
trimmed
} else {
trimmed.reversed()?
};
curves.push(oriented);
edge_names.push(edge.name.clone());
}
if curves.is_empty() {
continue; // an all-degenerate loop encloses nothing
}
// A single closed curve (a full-circle boundary — cylinder cap,
// drilled hole) must become TWO segments: the extrude/revolve
// builders require >= 2 curves, and `subtract_hole` silently skips
// 1-curve loops. Mirror sketch.rs's circle-as-two-arcs convention.
if curves.len() == 1 {
let only = curves.pop().ok_or("face-profile: loop vanished")?;
let [start, end] = only.domain()?;
let (first, second) = only.split((start + end) * 0.5)?;
let name = edge_names.pop().flatten();
curves.push(first);
curves.push(second);
edge_names.push(name.clone());
edge_names.push(name);
}
// No sketch behind a FACE profile, so no loop id — `ProfileLoop::key`
// falls back to the loop's own lowest source edge name.
loops.push(ProfileLoop {
curves,
edge_names,
loop_id: None,
});
}
if loops.is_empty() {
return Err(format!(
"face-profile: face {} has no usable boundary loop",
face.face_id
));
}
Ok(loops)
})?;
Ok(SketchProfile {
origin: frame.origin,
x_axis: frame.x_axis,
y_axis: frame.y_axis,
z_axis: frame.z_axis,
// ONE region: the face's outer loop + its holes (a face's hole loops
// never nest islands, so the region is flat by construction).
regions: vec![loops],
})
}