///|
// Exact B-spline B-Rep for curved `ExtrudedBezierProfile` contours.
//
// The polyline path in `compiler_bezier.mbt` flattens every cubic span. That is
// fine for straight-sided glyphs but loses curved geometry. This file emits an
// exact representation instead: one closed cubic B-spline curve per contour,
// two planar caps bounded by that curve, and one lateral face on a
// `SURFACE_OF_LINEAR_EXTRUSION` with a single vertical seam edge.
//
// Topology follows the analytic cylinder convention:
// bottom edge: lateral (forward) + bottom cap (reversed)
// top edge: lateral (reversed) + top cap (forward)
// seam edge: lateral only, used twice with opposite sense
// so every edge is used exactly twice with opposite orientation.
///|
/// BezierContourBoundary holds the STEP entities shared by the three faces of
/// one extruded contour.
priv struct BezierContourBoundary {
bottom_edge : @step.Ref
top_edge : @step.Ref
seam_edge : @step.Ref
bottom_start : @step.Ref
top_start : @step.Ref
start_dir : Vec3
surface : @step.Ref
up_dir : Vec3
}
///|
/// is_straight_span reports whether a cubic span actually traces a line.
fn is_straight_span(a0 : BezierAnchor, a1 : BezierAnchor) -> Bool {
a0.handle_out == a0.position && a1.handle_in == a1.position
}
///|
/// path_has_curved_span reports whether any span (including the wrap-around
/// span of a closed contour) has non-degenerate Bezier handles.
fn path_has_curved_span(path : BezierPath) -> Bool {
let anchors = path.anchors
let n = anchors.length()
if n < 2 {
return false
}
for i in 0.. BezierPath {
let anchors = path.anchors
let n = anchors.length()
let rev : Array[BezierAnchor] = []
for i in 0.. BezierPath {
let poly = flatten_bezier_path(path)
if poly.length() < 3 {
return path
}
let area = polygon_area_2d(poly)
if area == 0.0 {
return path
}
let want_positive = ccw
if (area > 0.0) != want_positive {
reverse_bezier_path(path)
} else {
path
}
}
///|
/// bspline_control_points converts a closed contour of cubic Bezier spans into
/// one clamped cubic B-spline control polygon.
///
/// Span `i` contributes `P0, C1, C2, P3`; the shared endpoint is emitted once.
/// Straight spans use collinear control points at one third and two thirds of
/// the way to the endpoint, which is an exact straight line (the task permits
/// this in place of degree-1 segments). Zero-length straight spans are dropped.
fn bspline_control_points(path : BezierPath) -> Array[Vec2] {
let anchors = path.anchors
let n = anchors.length()
let cps : Array[Vec2] = []
if n < 2 {
return cps
}
let mut spans = 0
for i in 0.. BezierCurveBuild {
let points = cps.map(fn(p) {
let w = xform.apply((p.0, p.1, z))
ctx.cartesian_point(w.0, w.1, w.2)
})
let span_count = (cps.length() - 1) / 3
let mults : Array[Int32] = [4]
for _ in 1.. BezierContourBoundary? {
let cps = bspline_control_points(path)
if cps.length() < 4 {
return None
}
let bottom = add_bspline_curve(ctx, cps, 0.0, xform)
let top = add_bspline_curve(ctx, cps, height, xform)
let vb = ctx.vertex_point(bottom.points[0])
let vt = ctx.vertex_point(top.points[0])
let bottom_edge = ctx.edge_curve(vb, vb, bottom.curve, true)
let top_edge = ctx.edge_curve(vt, vt, top.curve, true)
let up_dir = xform.apply_dir((0.0, 0.0, 1.0))
let up_ref = ctx.direction(up_dir.0, up_dir.1, up_dir.2)
let seam_line = ctx.line(bottom.points[0], ctx.vector(up_ref, 1.0))
let seam_edge = ctx.edge_curve(vb, vt, seam_line, true)
let surface = @geometry.SurfaceOfLinearExtrusion::new(
"",
bottom.curve,
ctx.vector(up_ref, height),
)
let surface_ref = ctx.add(
@repository.any_entity_surface_of_linear_extrusion(surface),
)
Some({
bottom_edge,
top_edge,
seam_edge,
bottom_start: bottom.points[0],
top_start: top.points[0],
start_dir: bottom.start_dir,
surface: surface_ref,
up_dir,
})
}
///|
/// add_cap_from_boundaries emits one planar cap bounded by the profile curves.
/// The bottom cap uses the reversed loops (outward normal -Z), the top cap the
/// forward loops (outward normal +Z).
fn add_cap_from_boundaries(
ctx : StepContext,
outer : BezierContourBoundary,
holes : Array[BezierContourBoundary],
bottom~ : Bool,
reverse~ : Bool,
) -> @step.Ref {
let (axis_raw, location) = if bottom {
((-outer.up_dir.0, -outer.up_dir.1, -outer.up_dir.2), outer.bottom_start)
} else {
(outer.up_dir, outer.top_start)
}
let axis_ref = ctx.direction(axis_raw.0, axis_raw.1, axis_raw.2)
let ref_ref = ctx.direction(
outer.start_dir.0,
outer.start_dir.1,
outer.start_dir.2,
)
let a2p_ref = ctx.axis2_placement_3d(location, axis_ref, ref_ref)
let plane = @geometry.Plane::new("", a2p_ref)
let plane_ref = ctx.add(@repository.any_entity_plane(plane))
let outer_loop_refs = loop_edges(ctx, outer, bottom~, forward=!bottom)
let outer_loop = ctx.edge_loop(outer_loop_refs)
let fob_ref = ctx.face_outer_bound(outer_loop, true)
let bounds : Array[@step.Ref] = [fob_ref]
for hole in holes {
let hole_loop_refs = loop_edges(ctx, hole, bottom~, forward=!bottom)
let hole_loop = ctx.edge_loop(hole_loop_refs)
bounds.push(ctx.face_bound(hole_loop, true))
}
ctx.advanced_face(bounds, plane_ref, !reverse)
}
///|
/// loop_edges returns the oriented edge list for a closed contour boundary.
fn loop_edges(
ctx : StepContext,
b : BezierContourBoundary,
bottom~ : Bool,
forward~ : Bool,
) -> Array[@step.Ref] {
let edge = if bottom { b.bottom_edge } else { b.top_edge }
if forward {
[ctx.oriented_edge(edge, true)]
} else {
[ctx.oriented_edge(edge, false)]
}
}
///|
/// add_lateral_from_boundary emits the `SURFACE_OF_LINEAR_EXTRUSION` band.
fn add_lateral_from_boundary(
ctx : StepContext,
b : BezierContourBoundary,
reverse~ : Bool,
) -> @step.Ref {
let edge_loop_ref = ctx.edge_loop([
ctx.oriented_edge(b.bottom_edge, true),
ctx.oriented_edge(b.seam_edge, true),
ctx.oriented_edge(b.top_edge, false),
ctx.oriented_edge(b.seam_edge, false),
])
let fob_ref = ctx.face_outer_bound(edge_loop_ref, true)
ctx.advanced_face([fob_ref], b.surface, !reverse)
}
///|
/// compile_curved_bezier_group emits one closed contour triplet per path.
fn compile_curved_bezier_group(
ctx : StepContext,
node : SceneNode,
group : Array[BezierPath],
height : Double,
reverse~ : Bool = false,
parent~ : Transform? = None,
) -> Array[@step.Ref] {
if group.is_empty() {
return []
}
let xform = node_transform(node, parent)
let outer_path = normalize_bezier_contour(group[0], ccw=true)
let outer = match compile_bezier_contour(ctx, outer_path, height, xform) {
Some(b) => b
None => return []
}
let holes : Array[BezierContourBoundary] = []
for i in 1.. holes.push(b)
None => ()
}
}
let faces : Array[@step.Ref] = []
faces.push(add_cap_from_boundaries(ctx, outer, holes, bottom=true, reverse~))
faces.push(add_cap_from_boundaries(ctx, outer, holes, bottom=false, reverse~))
faces.push(add_lateral_from_boundary(ctx, outer, reverse~))
for hole in holes {
faces.push(add_lateral_from_boundary(ctx, hole, reverse~))
}
faces
}