Skip to content

chore: release v0.0.5#6

Open
MagicalTux wants to merge 1 commit into
masterfrom
release-plz-2026-06-15T05-43-57Z
Open

chore: release v0.0.5#6
MagicalTux wants to merge 1 commit into
masterfrom
release-plz-2026-06-15T05-43-57Z

Conversation

@MagicalTux

@MagicalTux MagicalTux commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

🤖 New release

  • oxideav-obj: 0.0.4 -> 0.0.5 (✓ API compatible changes)
Changelog

0.0.5 - 2026-06-27

Other

  • broaden round-trip property corpus to free-form + 2000 seeds
  • g group directive is state-setting — split primitive on mid-stream change
  • fix vt index drift for duplicate-value texcoords + add fixed-point property test
  • preserve leading header comment block on round-trip (spec §Comments)
  • round 370 — byte-faithful vt re-emission for 1D / 3D texture coords
  • round 361 — backfill degenerate normals on partial-vn primitives
  • round 361 — synthesise vertex normals from smoothing groups (opt-in)
  • round 353 — typed obj:superseded accessor for bzp/bsp/cdc/cdp
  • round 353 — tessellate superseded cdc Cardinal curve + bzp Bezier patch
  • byte-faithful vp re-emit via parallel obj:vp_widths token-width vector
  • stech cparma/cparmb surface-approximation resolution drives tessellation
  • honour ctech cparm res curve-approximation resolution in tessellation
  • round 324 — MTL PBR scalar-field texture-map siblings round-trip
  • round 320 — variable-arity -o/-s/-t texture-map option flags
  • Round 315: smooth per-vertex normals on tessellated surf surfaces
  • refresh to current status, drop per-round changelog cruft

Added

  • Round 376: the file's leading comment block now round-trips verbatim
    (spec §"Comments"). Real-world OBJ exporters open with a header comment
    block — Blender's # Blender v2.93.0 OBJ File: banner, Maya / Meshroom
    banners, the spec's own §"Comments" example # 4 vertices /
    # 4 texture vertices / # 4 normals — and the decoder previously
    stripped every #… comment, so a decode → encode round-trip dropped the
    header and substituted a synthetic # OBJ generated by oxideav-obj
    line. The decoder now captures the contiguous run of comment / blank
    lines that precedes the first non-comment directive verbatim (each line
    with its leading # retained, blank separator lines preserved as empty
    strings so the block's internal shape survives, and trailing blank lines
    dropped so repeated round-trips can't accumulate growth) into
    Scene3D::extras["obj:header_comments"]; the encoder re-emits that block
    in place of its synthetic banner when the key is present. Files that open
    directly with a directive set no key and keep the synthetic banner.
    Comments embedded after the first directive aren't positionally anchored
    by a line-oriented parser and are still discarded — virtually all
    real-world annotation lives in the header.

Fixed

  • Round 376: a mid-stream g group directive is now correctly
    state-setting (spec §"Grouping": elements following a g statement are
    assigned to the named groups). The decoder appended the new group names
    onto the current primitive even when it already held faces, so a g
    that appeared after some elements retroactively re-tagged them; on
    round-trip the encoder then emitted the g line one element too early
    and decode → encode wasn't a fixed point. A g whose membership differs
    from the open primitive's now splits into a fresh primitive (mirroring
    the s / usemtl / mg state-setters), inheriting the other active
    state, so the group applies only to subsequent elements. A g before
    any element still tags the first primitive with no split. Surfaced by
    the new round-trip property test; pinned in tests/group_state_split.rs.

  • Round 376: a face that references the later of two vt lines sharing
    a [u, v] value now keeps its exact vt index on round-trip. The typed
    model stores only the UV value on Primitive::uvs, so two source vt
    lines with identical [u, v] but different source indices (e.g. a 1D
    vt 0 padded to [0, 0] and a vt 0 0) collapse to one value; the
    encoder's value-keyed tex_map then re-resolved every face UV to the
    first matching slot, silently rewriting an f v/10 reference to
    f v/3. The decoder now records the original 1-based vt index per
    primitive vertex into Primitive::extras["obj:vt_src_index"] — but only
    when the texcoord pool actually holds a duplicate value (the sole
    ambiguous case), keeping the common distinct-valued case free of the
    channel — and the encoder restores the exact source slot whenever the
    pool was seeded 1:1 from obj:texcoords. Found by a new generative
    property test (tests/roundtrip_fixed_point_property.rs) that asserts
    the decode → encode → decode textual fixed point over a 600-seed corpus
    of valid OBJ documents; the specific case is pinned in
    tests/vt_duplicate_value_index_fidelity.rs.

  • Round 370: vt texture-coordinate re-emission is now byte-faithful
    for the 1D and 3D forms. Spec §"vt u v w" makes both v and w
    optional (each defaulting to 0): a 1D texture writes vt u, a 2D
    texture vt u v, and a 3D texture vt u v w. The polygonal pool
    keeps only the [u, v] pair a glTF UV can carry, so the decoder
    previously collapsed every line to the canonical vt u v form — a
    vt 0.25 re-emitted as vt 0.25 0 and a vt u v w dropped its w
    depth coordinate entirely. The decoder now records each line's source
    token width into Scene3D::extras["obj:texcoord_widths"] (a vector
    parallel to the new obj:texcoords source pool) and the dropped 3rd
    w into ["obj:texcoord_w"]; the encoder seeds its texture-coordinate
    pool from that source pool (1:1, un-deduplicated so two vt lines that
    share [u, v] but differ in w stay distinct slots) and re-emits each
    line at its exact arity. Decode → encode is now a fixed point for the
    full 1D / 2D / 3D vt mix. Files whose vt lines were all 2D leave
    the new keys absent and keep the prior canonical 2-token emit, so the
    common case is unchanged; scenes assembled without going through the
    decoder (no obj:texcoords key) likewise keep the 2-token emit.

  • Round 345: vp parameter-space-vertex re-emission is now
    byte-faithful for lines whose trailing (or middle) coordinate is a
    genuine zero. The decoder pads every vp entry to a [u, v, w]
    triple with 0.0 (spec §"vp u v w" allows 1, 2, or 3 coordinates
    depending on usage), which is indistinguishable from an
    explicitly-written 0.0; the encoder previously inferred the
    original arity by stripping trailing zeros, so vp 0.5 0.0 (a
    surface special point on the v = 0 edge) round-tripped to
    vp 0.5 and vp 0.5 0.5 0.0 (a zero-weight rational trimming-curve
    control point) to vp 0.5 0.5. The decoder now records each line's
    source token width into Scene3D::extras["obj:vp_widths"] (a vector
    parallel to obj:vp) and the encoder reproduces the exact arity.
    Decode → encode is now a fixed point for the full 1D/2D/3D vp mix;
    scenes assembled without going through the decoder (no vp_widths
    key) keep the prior strip-trailing-zeros fallback.

Added

  • Round 361: opt-in vertex-normal synthesis from smoothing groups via
    ObjDecoder::with_normal_generation(NormalGeneration::FromSmoothingGroups)
    (or ParseOptions::generate_normals). The spec (§"Grouping",
    s group_number) describes smoothing groups as "a quick way to
    specify vertex normals": for a polygonal face primitive carrying no
    vn data of its own, an active smoothing group (s 1, s 2, …)
    yields smooth, area-weighted averaged normals across shared vertices,
    while smoothing off (s off / s 0, or no s directive) yields
    faceted per-face normals — the primitive's vertices are de-shared so
    the hard edge between adjacent faces survives. Explicit vn data
    "supersede smoothing groups" (§"Vertex normals"), so primitives that
    already carry normals are left untouched. Synthesised normals are
    flagged Primitive::extras["obj:generated_normals"] ("smooth" /
    "flat"); the encoder skips them so a decode → encode round-trip
    keeps the original vn-free face syntax rather than fabricating vn
    lines. The default (NormalGeneration::Disabled) preserves the prior
    behaviour of leaving vn-less primitives with normals == None. The
    same opt-in also backfills the degenerate [0,0,0] placeholder
    normals left for a primitive that mixes vn-bearing and vn-less
    faces (spec-illegal per §"f" but produced by lenient tools): the zero
    placeholders are filled from the triangulated face geometry while
    explicit normals are preserved exactly, so the buffer carries no
    degenerate normals for rendering.

  • Round 353: typed Scene3D::extras["obj:superseded"] accessor — a
    parse-time-only decomposition of every superseded geometry statement
    (bzp / bsp Bezier / B-spline patch, cdc Cardinal curve, cdp
    Cardinal patch; spec §"Superseded statements"). Each entry carries the
    keyword, a human-readable kind (bezier_patch / bspline_patch /
    cardinal_curve / cardinal_patch), the raw control_points vertex
    indices (1-based / negative-from-end, as written), and the res
    [useg, vseg] segment counts active at that statement (omitted when no
    res preceded it). Covers all four forms — including the
    verbatim-only bsp / cdp — so consumers can read the superseded
    geometry without re-walking obj:freeform_directives; the encoder is
    still driven by the verbatim channel.

  • Round 353: opt-in tessellation of the superseded cdc (Cardinal
    curve) and bzp (Bezier patch) statements (spec §"Superseded
    statements"). Previously these round-tripped verbatim only; with
    ObjDecoder::with_curve_tessellation(N) a cdc v1 v2 v3 v4 … line
    (minimum four control points) is now evaluated to a real
    Topology::LineStrip polyline via the same cubic Catmull-Rom basis
    as the modern cstype cardinal form, and a bzp v1 … v16 16-point
    bicubic patch is evaluated to a triangulated Topology::Triangles
    surface with smooth per-vertex normals via the shared
    tensor-product de Casteljau evaluator. The bzp control-mesh layout
    is taken directly from the spec's §"Comparison of 2.11 and 3.0
    syntax" §"Bezier patch" worked example (which writes the same patch
    as both bzp 1 … 16 and the modern surf … 13 14 15 16 … form). The
    superseded res useg vseg reference/display statement now modulates
    subdivision density for the cdc / bzp lines that follow it
    (per-direction segment count clamped to the spec's 3..=120 range;
    surfaces drive the shared isotropic lattice from the finer of the two
    directions). Synthetic geometry lands on dedicated
    obj:superseded_curves / obj:superseded_surfaces meshes carrying
    the shared obj:tessellated_curve = true sentinel, so the encoder
    drops it on re-emit and the verbatim cdc / bzp / res lines in
    Scene3D::extras["obj:freeform_directives"] remain the round-trip
    source of truth. The superseded bsp (B-spline patch) and cdp
    (Cardinal patch) 16-point forms still round-trip verbatim only — the
    spec describes their control-point distribution prose-only ("four
    distributed over the surface, the remainder around the perimeter")
    without a comparison example pinning the basis/knot layout, so their
    geometry is not synthesised.

  • Round 332: stech cparma ures vres / stech cparmb uvres
    surface-approximation resolution now drives the tessellated lattice
    density — the surface analog of the round-328 ctech cparm override.
    When a cstype … end surface block carries a constant-parametric
    stech directive (spec §"stech technique resolution"),
    with_curve_tessellation(N) evaluates each surf patch at
    n = round(res × degree) subdivisions per direction instead of the
    caller's uniform N budget; the shared isotropic lattice is driven
    from the finer (max) of the two per-direction counts, so the coarser
    direction is never under-sampled. cparmb's single value applies to
    both directions; cparma 0 0 collapses each patch to two triangles
    (one subdivision). The effective count is reported in
    Primitive::extras["obj:surface_samples"] and the source [ures, vres]
    pair in ["obj:surface_stech_cparm_res"]. Applies to every surf
    basis kind (Bezier / B-spline / Cardinal / Taylor / basis-matrix); the
    geometric stech cspace maxlength / stech curv maxdist maxangle
    techniques (which need iterative spatial / curvature refinement) stay
    on the uniform budget. The directive sequence still round-trips
    verbatim via Scene3D::extras["obj:freeform_directives"].

  • Round 328: ctech cparm res curve-approximation resolution now drives
    the tessellated subdivision density. When a cstype … end block carries
    a ctech cparm <res> directive (spec §"ctech technique resolution"),
    with_curve_tessellation(N) evaluates each curv at n = round(res × degree) subdivisions per the spec's constant-parametric formula —
    overriding the caller's uniform N budget for that block — with
    res = 0 collapsing each polynomial segment to a single line segment.
    The effective count is reported in Primitive::extras["obj:curve_samples"]
    and the source resolution in ["obj:curve_ctech_cparm_res"]. Applies to
    all 3D curv basis kinds (Bezier / B-spline / Cardinal / Taylor /
    basis-matrix); the geometric ctech cspace maxlength / ctech curv maxdist maxangle techniques (which need iterative chord-length /
    curvature refinement) stay on the uniform budget. The directive sequence
    still round-trips verbatim via Scene3D::extras["obj:freeform_directives"].

  • Round 324: PBR-extension scalar-field texture-map siblings round-trip.
    map_Ps (sheen), map_Pc (clearcoat-thickness), map_Pcr
    (clearcoat-roughness), map_aniso and map_anisor now parse like the
    other less-PBR-friendly maps — the file reference (plus any -flag value
    option chunks) is preserved verbatim in Material::extras["mtl:<map>"]
    and re-emitted by the encoder. Previously these were silently dropped,
    making a decode → encode round-trip lossy for the upper half of the
    Wavefront-PBR map family. The scalar fields Ps / Pc / Pcr /
    aniso / anisor were already captured; this closes the matching maps.

  • Round 315: smooth per-vertex normals on tessellated surf surfaces.
    with_curve_tessellation(N) now fills the synthetic Topology::Triangles
    primitive's normals buffer with area-weighted face-normal averages
    (front orientation per spec §"surf": u-right / v-up CCW winding), so a
    downstream renderer / glTF exporter shades curved surfaces smoothly
    rather than flat. Covers the base sample lattice plus trim-boundary and
    scrv-constraint vertices; degenerate fans fall back to a +Z unit
    normal so the buffer stays length-parallel with positions and NaN-free.


This PR was generated with release-plz.

@MagicalTux MagicalTux force-pushed the release-plz-2026-06-15T05-43-57Z branch 8 times, most recently from 57cd021 to 8d719b9 Compare June 22, 2026 08:00
@MagicalTux MagicalTux force-pushed the release-plz-2026-06-15T05-43-57Z branch 4 times, most recently from adea3a5 to 0d8f91c Compare June 27, 2026 15:23
@MagicalTux MagicalTux force-pushed the release-plz-2026-06-15T05-43-57Z branch from 0d8f91c to d1b78f4 Compare June 27, 2026 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant