///|
/// Number of line segments used to approximate each cubic Bezier span when
/// flattening an `ExtrudedBezierProfile` for STEP output.
///
/// STEP cannot express the glyph outlines directly, so the curves are sampled
/// into polylines before the profile is extruded. Eight segments per span keeps
/// the resulting files comparable in size to the `segments=8` polyline path.
let bezier_segments_per_span : Int32 = 8
///|
/// cubic_bezier_point evaluates the cubic Bezier defined by the four control
/// points at parameter `t`.
fn cubic_bezier_point(
p0 : Vec2,
c1 : Vec2,
c2 : Vec2,
p3 : Vec2,
t : Double,
) -> Vec2 {
let u = 1.0 - t
let uu = u * u
let tt = t * t
let uuu = uu * u
let ttt = tt * t
let ut3 = 3.0 * uu * t
let tu3 = 3.0 * u * tt
(
uuu * p0.0 + ut3 * c1.0 + tu3 * c2.0 + ttt * p3.0,
uuu * p0.1 + ut3 * c1.1 + tu3 * c2.1 + ttt * p3.1,
)
}
///|
/// flatten_bezier_path samples a `BezierPath` into a polyline.
///
/// Handles are stored as absolute positions, so the cubic span from anchor `i`
/// to anchor `i+1` uses `a[i].position`, `a[i].handle_out`,
/// `a[i+1].handle_in`, and `a[i+1].position`. Straight spans (zero-length
/// offsets) contribute only their start point, which avoids bloating glyphs
/// whose stems are simple lines. Closed paths include the wrap-around span.
fn flatten_bezier_path(path : BezierPath) -> Array[Vec2] {
let anchors = path.anchors
let n = anchors.length()
if n < 2 {
return []
}
let points : Array[Vec2] = []
let span_count = if path.closed { n } else { n - 1 }
for i in 0.. Array[Vec2] {
let eps_sq = 1.0e-18
let result : Array[Vec2] = []
for pt in points {
let duplicate = match result.last() {
Some(prev) => {
let dx = pt.0 - prev.0
let dy = pt.1 - prev.1
dx * dx + dy * dy <= eps_sq
}
None => false
}
if !duplicate {
result.push(pt)
}
}
if result.length() > 1 {
let first = result[0]
let last = result[result.length() - 1]
let dx = first.0 - last.0
let dy = first.1 - last.1
if dx * dx + dy * dy <= eps_sq {
let _ = result.pop()
}
}
result
}
///|
/// bezier_path_groups splits `BezierPath`s into (outer, holes) groups while
/// keeping the original curves.
///
/// Disconnected glyph components (for example the dot of an "i", or the pieces
/// of a "%") become separate groups so each can be emitted as its own closed
/// solid. `path.clear == true` marks a hole (see `BezierPath.clear`), and a
/// hole is attached to every outer loop that contains it. Degenerate paths with
/// fewer than three flattened points are skipped.
fn bezier_path_groups(paths : Array[BezierPath]) -> Array[Array[BezierPath]] {
let outers : Array[(BezierPath, Array[Vec2])] = []
let holes : Array[(BezierPath, Array[Vec2])] = []
for path in paths {
let flat = flatten_bezier_path(path)
if flat.length() < 3 {
continue
}
if path.clear {
holes.push((path, flat))
} else {
outers.push((path, flat))
}
}
let groups : Array[Array[BezierPath]] = []
for outer_pair in outers {
let outer = outer_pair.0
let outer_poly = outer_pair.1
let group : Array[BezierPath] = [outer]
for hole_pair in holes {
let hole = hole_pair.0
let hole_poly = hole_pair.1
if hole_poly.length() > 0 && is_point_in_polygon(hole_poly[0], outer_poly) {
group.push(hole)
}
}
groups.push(group)
}
groups
}
///|
/// flatten_group_profile flattens one (outer, holes) group into a `Profile2D`.
///
/// Outers are wound counter-clockwise and holes clockwise, matching
/// `collect_profiles`.
fn flatten_group_profile(group : Array[BezierPath]) -> Profile2D {
let outer = force_ccw(flatten_bezier_path(group[0]))
let holes = group
.iter()
.drop(1)
.map(fn(path) { force_cw(flatten_bezier_path(path)) })
.collect()
Profile2D::{ outer, holes, }
}
///|
/// compile_extruded_bezier_profile writes a B-Rep for an
/// `ExtrudedBezierProfile`.
///
/// Contours with only straight spans keep the cheap polyline/line path used by
/// `compile_extruded_profile`. A group with any curved span is emitted with
/// exact B-spline geometry: one closed cubic `B_SPLINE_CURVE_WITH_KNOTS` per
/// contour, two planar caps, and a `SURFACE_OF_LINEAR_EXTRUSION` lateral face.
pub fn compile_extruded_bezier_profile(
ctx : StepContext,
node : SceneNode,
extrusion : ExtrudedBezierProfile,
reverse~ : Bool = false,
parent~ : Transform? = None,
) -> Array[@step.Ref] {
if extrusion.height_mm == 0.0 {
return []
}
let faces : Array[@step.Ref] = []
for group in bezier_path_groups(extrusion.paths) {
if group.is_empty() {
continue
}
if group.any(fn(path) { path_has_curved_span(path) }) {
faces.push_iter(
compile_curved_bezier_group(
ctx,
node,
group,
extrusion.height_mm,
reverse~,
parent~,
).iter(),
)
} else {
let profile = flatten_group_profile(group)
if profile.outer.length() < 3 {
continue
}
let ext = ExtrudedProfile::{
profile,
height_mm: extrusion.height_mm,
round_bore: None,
}
faces.push_iter(
compile_extruded_profile(ctx, node, ext, reverse~, parent~).iter(),
)
}
}
faces
}