feat(visual): implement the slice 4d renderer core
The first visual code that draws. Sections 17.4 through 17.14 are implemented against Format Specification revision 0.8, and the standalone artifact now has a stage canvas that renders a declared scene. The renderer is split so that the automated traces of 17.16 can run without a display, which is what those traces require. visual-engine.js resolves an exhibit's objects once at their instantiation boundary, composes the scene-to-device chain, sorts by depth, applies fog and the compositing order, and emits a **frame plan** in device pixels; visual-canvas2d.js turns a plan into drawing calls. A plan carries the numeric oracles the traces ask for - a known scene point at a known display point, a perspective factor, a fog fraction, a sort order - where a canvas carries only pixels. visual-math.js implements the affine matrices, the normative local transform of 17.11, the directed arc sweep of 17.9 with its bound checked before normalization, and section 2 color parsing with the fog and ramp arithmetic of 17.6 and 18.2. Colors outside the four documented forms are ERR_TYPE_MISMATCH: exact components are what make per-object fog renderer-independent. visual-geometry.js reduces all fourteen primitives to subpaths of lines and cubic Beziers, so an affine map carries geometry exactly rather than approximately. It implements the local extents and anchors of 17.9 - a rectangle anchored top-left, circular primitives centered - the uniform cardinal form of catmull-rom with its open-duplication and closed-wrap index rules, the straight closing segment of a closed bezier spline, and endpoint-parameterized elliptical arcs. The composition chain of 19.3 is implemented as written: the fit matrix carries the contain and cover centering offsets, the camera center is mapped into CSS coordinates before the translation is formed, perspective acts about the projection center, and the device-pixel multiplier is applied exactly once and drops below 1 on a display larger than 4096. Scalars with no axis - stroke widths, radii, dash lengths - take the single uniform factor g, so a nonuniform stretch never distorts a stroke. Depth follows 17.6: effective depth accumulates z and translate.z up the tree, sortable units order farthest-first with the documented tie-break, a point's own z projects that point without making it a sortable unit, and an object at or behind the eye is culled with no diagnostic. Compositing follows 17.12: opacity multiplies down the tree while layer opacity applies once at the layer, and the sixteen buffer allocations per frame are granted nearest-first so refusals fall on the far content, each diagnosed under the 19.5 cadence rather than omitted silently. visual-validation.js gains the object tree: the fifteen object types and their declared fields, no `id` and no `layer` on an object, a declared fill on a stroke-only primitive, a mask only on a group and only naming its own child, path legality, spline modes and point counts, gradient stop order, and every authoring limit of 19.5 that applies to a drawn object. While implementing V10 I corrected the buffer-accounting rule I had written into 17.12: counting buffers as concurrently live made 19.5's farthest-first shedding nearly unreachable, since group nesting is already capped at 8. The rule is now 16 allocations per frame, which is the per-frame reading "pass budget" has carried since 17.5 first used it. The arc-sweep prose is likewise corrected to normalize the signed delta, so 350 -> 10 clockwise is the 20-degree sweep the text always claimed rather than 340 the long way round. test/phase4-renderer.test.mjs adds 22 tests covering traces 1 through 13 of 17.16 plus buffer shedding and the backend's call order. npm test goes from 131 to 153 passing. Traces 15 and 16 remain user-observed, and no procedural system, automation track, or post-effect executes yet - 4e and 4f own those. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0162Jb1J36judZNT8fHabGVt
This commit is contained in:
@@ -1768,7 +1768,7 @@ d = endAngle - startAngle when direction is clockwise
|
||||
d = startAngle - endAngle when direction is counter-clockwise
|
||||
```
|
||||
|
||||
`|d| > 360` is `ERR_OUT_OF_BOUNDS`, and that check runs on the authored numbers **before** any normalization, so `startAngle: 0, endAngle: 720` is rejected rather than silently folded to a full turn. Otherwise the drawn sweep is `|d|` degrees from `startAngle` in `direction`, with two boundary cases stated rather than left to a modulo: `|d| == 360` draws a complete turn, and `d == 0` draws nothing and raises no diagnostic. Wrapping across `0` needs no special case — `startAngle: 350, endAngle: 10` clockwise is a `20`-degree sweep — and a negative `d` under `clockwise` is the same sweep taken the long way round, which is why the direction, not the sign, decides.
|
||||
`|d| > 360` is `ERR_OUT_OF_BOUNDS`, and that check runs on the authored numbers **before** any normalization, so `startAngle: 0, endAngle: 720` is rejected rather than silently folded to a full turn. Otherwise the drawn sweep is `((d mod 360) + 360) mod 360` degrees from `startAngle` in `direction`, with two boundary cases stated rather than left to the modulo: `|d| == 360` draws a complete turn, and `d == 0` draws nothing and raises no diagnostic. Normalizing the **signed** delta is what makes wrapping across `0` need no special case — `startAngle: 350, endAngle: 10` clockwise is a `20`-degree sweep through zero, not a `340`-degree one the long way round — and it is why `direction`, not the sign of `d`, decides which way the pen travels.
|
||||
|
||||
**Groups.** A `group` composes a transform (17.11), a style scope (17.12), and an optional clip or mask over its `children`. Group nesting deeper than `8` levels is `ERR_VISUAL_LIMIT_EXCEEDED`, matching the audio component nesting bound of 15.15. A `group` with an empty `children` object is `ERR_SCHEMA_VALIDATION`.
|
||||
|
||||
@@ -1921,7 +1921,9 @@ A layer is then composited as:
|
||||
| Layer buffer | A layer whose `opacity` or `blend` is not the default (17.5) |
|
||||
| Object buffer | An object or group with a non-default `blend`, a `mask`, a `blur`, a `glow`, or any `filters` entry |
|
||||
|
||||
The pool holds `16` concurrently live buffers (19.5). A buffer is returned to the pool as soon as its owner composites into its parent, so what the ceiling bounds is the number live **at one moment** — the nesting depth of buffered groups plus the buffered layers still open — not the number allocated over a frame. Allocation order is fixed: layer buffers first, in layer document key order; then object buffers in draw order (17.6).
|
||||
The budget is `16` buffer allocations **per frame** (19.5). That is the per-frame reading the phrase "pass budget" has carried since 17.5 and 17.12 first used it, and it is the one worth bounding: a buffer costs an allocation, a clear, and a composite every frame it is used, so counting allocations bounds the work, where counting only the peak — the nesting depth of buffered groups, which 17.9 already caps at `8` — would bound the memory and almost nothing else. A buffer's storage may of course be reused once its owner has composited into its parent; the ceiling counts allocations, not surfaces.
|
||||
|
||||
Allocation order is fixed: every layer buffer first, in layer document key order, then object buffers in **ascending representative depth** (17.6) — nearest first, so that what runs out is the far content.
|
||||
|
||||
When an allocation would exceed the pool, it is **refused** and the owner is drawn without its buffer-requiring features: a non-default `blend` composites as `normal`, a `mask` is ignored, and `blur`, `glow`, and `filters` are omitted. Refusals are taken **farthest first** — among the objects competing for the last buffers, the greatest representative depth (17.6) loses first, ties broken by draw order — so what degrades is the content furthest from the viewer. Layer buffers are never refused; a layer set is at most `16` (17.5) and layers are the structural allocation the rest is composed into.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user