You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Cardinal (Catmull-Rom) surf surface tessellation under ObjDecoder::with_curve_tessellation(samples: u32). The decoder now
evaluates surf elements that sit under a cstype cardinal (or cstype rat cardinal) header into a triangulated Topology::Triangles primitive on the synthetic "obj:surfaces"
mesh, via the bivariate tensor-product Cardinal evaluation S(u, v) = Σ_i Σ_j C_i(u) · C_j(v) · d_{i,j} (spec §"Cardinal"). Each
parametric direction reuses the spec's Cardinal→Bezier per-segment
conversion (b0 = c1, b1 = c1 + (c2 − c0) / 6, b2 = c2 − (c3 − c1) / 6, b3 = c2, then a cubic Bernstein blend)
over a sliding 4-point window: the inner pass collapses every v-row at
the sample u, then a second 1-D Cardinal pass runs in v over the
collapsed points. Cardinal is cubic-only per spec ("Cardinal splines
are only defined for the cubic case"), so any deg other than 3 3
leaves the surface captured-only. The control grid is read from the parm u / parm v extents (K = parm_count + 1 per direction, from
the spec relation parm = K − n + 2 with n = 3); when parm only
carries the 2-value global parameter range (as the spec's
Cardinal-surface example does), the grid is taken to be the square
single patch (cols = rows = √total). Per spec §"Cardinal" — "For
surfaces, all but the first and last row and column of control points
are interpolated" — a single bicubic patch's parametric corners land
exactly on the interior 2×2 control block, which the tests verify, and
a cross-check confirms the tensor-product evaluator matches an
independent Cardinal→Bezier reference sample-for-sample. The rat cardinal qualifier routes to the same evaluator (spec
§"Free-form curve/surface body statements" notes the unit-weight
default is reasonable for Cardinal because its basis functions sum to
1), so per-vertex w weights are not applied. Synthetic primitives
carry obj:tessellated_surface = true, obj:surface_kind
("cardinal"), obj:surface_degree, obj:surface_u_range, obj:surface_v_range, and obj:surface_samples provenance plus the
shared obj:tessellated_curve = true sentinel so the encoder filters
the synthetic geometry out and replays the original cstype / deg / surf / parm u / parm v / end block verbatim
from Scene3D::extras["obj:freeform_directives"]. Non-cubic Cardinal,
Taylor, and basis-matrix surf bases remain captured-only.
B-spline / NURBS surf surface tessellation under ObjDecoder::with_curve_tessellation(samples: u32). The decoder now
evaluates surf elements that sit under a cstype bspline (or cstype rat bspline) header into a triangulated Topology::Triangles
primitive on the synthetic "obj:surfaces" mesh, via the bivariate
tensor-product Cox-deBoor formula S(u, v) = Σ_i Σ_j N_{i,nu}(u) · N_{j,nv}(v) · d_{i,j} (spec
§"B-spline" + §"Rational and non-rational curves and surfaces"). The
per-direction control-grid extents are derived from the parm u / parm v knot vectors ((len(parm u) − degu − 1) × (len(parm v) − degv − 1) per spec §"B-spline" condition 6, applied
independently in u and v); control points are read in the spec's
row-major u-fastest order (§"Surface vertex data — control points")
with negative relative-from-end indices honoured. Each surf line's s0 s1 t0 t1 range is clipped against the condition-5 evaluation
window [x_n, x_{K+1}] of its direction's knot vector, and the last
sample per direction is nudged fractionally below the upper bound so
the half-open knot-span convention doesn't zero the basis at the
endpoint (same NURBS-evaluator pattern as the round-8 curve path). The
rational (NURBS) form blends the per-vertex w weights from the v
lines and projects via the weighted denominator Σ N·N·w·d / Σ N·N·w. The basis is evaluated with the same bspline_basis Cox-deBoor routine the 1D curv path uses, so a
clamped quadratic B-spline patch (parm 0 0 0 1 1 1) reproduces the
equivalent quadratic Bezier patch sample-for-sample, and the spec's
cubic B-spline surface example tessellates inside its control-net
convex hull. Synthetic primitives carry obj:tessellated_surface = true, obj:surface_kind
("bspline" / "rat_bspline"), obj:surface_degree, obj:surface_u_range, obj:surface_v_range, and obj:surface_samples
provenance plus the shared obj:tessellated_curve = true sentinel so
the encoder filters the synthetic geometry out and replays the original cstype / deg / surf / parm u / parm v / end block verbatim
from Scene3D::extras["obj:freeform_directives"]. Knot/control-count
mismatches and malformed blocks are left captured-only. Cardinal /
Taylor / basis-matrix surf bases remain captured-only.
Bezier surf surface tessellation under ObjDecoder::with_curve_tessellation(samples: u32). The decoder now
evaluates surf elements that sit under a cstype bezier (or cstype rat bezier) header into a triangulated Topology::Triangles
primitive on a synthetic mesh named "obj:surfaces", via the
bivariate tensor-product de Casteljau algorithm (spec §"Rational and
non-rational curves and surfaces" + §"Bezier"). Control points are
read in the spec's row-major u-fastest order (§"Surface vertex data —
control points": "listed in the order i = 0 to K1 for j = 0, followed
by i = 0 to K1 for j = 1, …"); the surf line's v/vt/vn control-
vertex references are parsed for their leading position index, with
negative relative-from-end indices honoured. A single Bezier patch of
declared degree deg degu degv requires exactly (degu + 1) × (degv + 1) control points; counts that don't match a
single patch (multi-patch grids, which the Bezier basis can't
decompose without a step stride) are left captured-only. The patch
is sampled at a (samples + 1) × (samples + 1) lattice and
triangulated counter-clockwise (front = u-increases-right,
v-increases-up per the spec surf note). The rational form lifts each
control point to its homogeneous (w·x, w·y, w·z, w) form, runs both
de Casteljau passes in 4D, and projects back via x / w. Synthetic
primitives carry obj:tessellated_surface = true, obj:surface_kind
("bezier" / "rat_bezier"), obj:surface_degree ([degu, degv]), obj:surface_u_range ([s0, s1]), obj:surface_v_range
([t0, t1]), and obj:surface_samples provenance extras, plus the
shared obj:tessellated_curve = true sentinel so the encoder filters
the synthetic geometry out and replays the original cstype / deg / surf / parm / end block verbatim from Scene3D::extras["obj:freeform_directives"]. Non-Bezier surf bases
remain captured-only.
Basis-matrix curve tessellation under ObjDecoder::with_curve_tessellation(samples: u32). The decoder now
evaluates cstype bmatrixcurv directives per spec §"Basis matrix"
using the user-supplied (n + 1) × (n + 1) basis from bmat u and the
segment stride from step <stepu> (spec §"bmat u/v matrix" and
§"step stepu stepv"). Each polynomial segment i consumes the
control-point window c_{i·step + 1} .. c_{i·step + n + 1} (1-based)
and evaluates P(t) = Σ_i Σ_j B[i][j] · t^j · p_{base + i} per axis,
where B[i][j] is the row-major basis-matrix element with column
index j varying fastest (spec §"bmat u/v matrix": "matrix lists the
contents of the basis matrix with column subscript j varying the
fastest"). The rat bmatrix qualifier is accepted but does not apply
per-vertex weights (the spec note says the unit-weight default "may
or may not make sense for a representation given in basis-matrix form",
so the user's basis is the authoritative source). Synthetic primitives
carry the same obj:tessellated_curve = true / obj:curve_kind ("bmatrix") / obj:curve_degree (from deg) / obj:curve_u_range / obj:curve_samples
provenance extras. Malformed blocks (missing bmat u, missing step,
wrong-size matrix, fewer than n + 1 control points) are silently
dropped — the directive sequence still rides on Scene3D::extras["obj:freeform_directives"] for round-trip.
bmat and step free-form directive tracking — the parser now
captures these keywords into the obj:freeform_directives extra
alongside the existing cstype / deg / curv / parm / surf / trim / hole / scrv / sp / end / bzp / bsp directives,
so a cstype bmatrix block round-trips through a decode → encode →
decode cycle bit-exactly. The encoder replays the captured directive
sequence verbatim with no semantic interpretation.
Cardinal (Catmull-Rom) + Taylor polynomial curve tessellation under ObjDecoder::with_curve_tessellation(samples: u32). The decoder now
evaluates cstype cardinalcurv directives via the spec §"Cardinal"
conversion to Bezier control points
(b0 = c1, b1 = c1 + (c2 − c0) / 6, b2 = c2 − (c3 − c1) / 6, b3 = c2, then cubic Bezier blend) on a sliding 4-point window across
the control polygon, producing C¹-continuous polylines that
interpolate every interior control point exactly. Cardinal is cubic
only per spec; non-cubic deg is silently rejected (the directive
itself remains captured for round-trip). cstype taylorcurv directives evaluate via Horner's-rule
polynomial evaluation P(t) = Σ_{i=0..n} c_i · t^i per spec §"Taylor"
(control points are the polynomial coefficients) with sampling across
the [u_min, u_max] range supplied on the curv line. Both rat cardinal and rat taylor qualifiers are accepted but route to the
same evaluator (the spec note says the unit-weight default is
reasonable for Cardinal because its basis functions sum to 1, and
explicitly that the rational form "does not make sense for Taylor").
The resulting polylines land on the existing "obj:curves" synthetic
mesh with obj:tessellated_curve = true / obj:curve_kind
("cardinal" / "taylor") / obj:curve_degree (3 for Cardinal,
the deg value for Taylor) / obj:curve_u_range / obj:curve_samples
provenance extras. The encoder filters synthetic primitives out of
the polygonal section so a re-encode replays the original cstype cardinal / cstype taylor blocks unchanged.
B-spline / NURBS curve tessellation under ObjDecoder::with_curve_tessellation(samples: u32). The decoder now
evaluates cstype bspline and cstype rat bsplinecurv directives
via the Cox-deBoor recursive basis-function formula (spec §"B-spline"),
clipped against the [x_n, x_{K+1}] evaluation window of the knot
vector supplied by the most-recent parm u … body statement. The
resulting polyline lands on the existing "obj:curves" synthetic
mesh with the same obj:tessellated_curve = true / obj:curve_kind ("bspline" / "rat_bspline") / obj:curve_degree / obj:curve_u_range / obj:curve_samples
provenance extras. Rational form (NURBS) uses the per-vertex 4th w
weight and projects the weighted blend back to 3D, matching the
spec's Σ N_{i,n} · w_i · d_i / Σ N_{i,n} · w_i formulation.
Knot-vector length is validated against the spec condition len == K + degree + 2 and incomplete curves are skipped silently
(the directive itself remains captured for round-trip). The
tessellator now does two-pass per-block traversal so the curv
header (which precedes the parm u body statement per spec
§"Specifying free-form curves/surfaces") still resolves its knot
vector. Cardinal / Taylor / basis-matrix bases and surf
2-parameter surfaces remain captured-only.
ObjDecoder::with_curve_tessellation(samples: u32) evaluates every cstype bezier (and cstype rat bezier) curv directive via de
Casteljau's algorithm at samples + 1 uniformly-spaced parameter
values, producing real Topology::LineStrip primitives on a synthetic
mesh named "obj:curves". Rational form uses the per-vertex 4th w
weight (v x y z w) and projects the homogeneous blend back to 3D.
Each tessellated primitive carries obj:tessellated_curve = true, obj:curve_kind ("bezier" / "rat_bezier"), obj:curve_degree, obj:curve_u_range, and obj:curve_samples in extras so consumers
can filter / inspect derived geometry. samples == 0 (the default)
preserves the round 1-6 behaviour: directives ride as Scene3D::extras["obj:freeform_directives"] only, no synthetic mesh.
The encoder skips synthetic curve primitives so re-encoding produces
the original cstype / curv / end section unchanged.
Source position pool round-trip: when an OBJ's free-form section
(curv / curv2 / surf / bzp / bsp) references positions by
absolute index, those positions now ride on Scene3D::extras["obj:positions"] (and parallel obj:position_weights / obj:position_colors arrays for the
extension widths) so the encoder re-emits the full v block in
source order. Previously, positions only referenced by free-form
directives were silently dropped on re-encode, which made absolute
curve-control-point indices drift after a decode → encode → decode
cycle. The fix is invisible to polygon-only OBJs; the extras are
populated only when free-form directives that reference indices are
present.