@glissade/scene 0.11.0 → 0.12.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { $ as breakLines, A as Polyline, B as validateHachure, C as Video, Ct as IDENTITY, D as revealSchedule, Dt as invert, E as pathFromSegs, Et as fromTRS, F as flatten, G as HitArea, H as AnchorSpec, I as hachureLines, J as PropInit, K as Node, L as resolveSketch, M as SketchStyle, N as SketchValidationError, O as roundedRectSegs, Ot as matEquals, P as arcLength, Q as TextMetricsLite, R as roughen, S as TextProps, St as validateFilters, T as WordBox, Tt as applyToPoint, U as BindablePropTarget, V as validateSketch, W as EvalContext, X as MEASURE_QUANTUM_PX, Y as resolveAnchor, Z as TextMeasurer, _ as PathProps, _t as ShaderRef, a as LayoutEngineMissingError, at as BlendMode, b as ShapeProps, bt as filtersToCanvasFilter, c as requireLayoutEngine, ct as DrawCommand, d as Custom, dt as FontSpec, et as estimatingMeasurer, f as Group, ft as Paint, g as Path, gt as ResourceId, h as LineBox, ht as Resource, i as LayoutEngine, it as setDefaultMeasurer, j as ResolvedSketch, k as HachureSpec, kt as multiply, l as setLayoutEngine, lt as FilterSpec, m as ImageProps, mt as Rect$1, n as LayoutChildSpec, nt as segmentGraphemes, ot as DisplayList, p as ImageNode, pt as PathSeg, q as NodeProps, r as LayoutContainerSpec, rt as segmentWords, s as getLayoutEngine, st as DisplayListBuilder, t as LayoutBox, tt as quantize, u as Circle, ut as FilterValidationError, v as Rect, vt as StrokeStyle, w as VideoProps, wt as Mat2x3, x as Text, xt as glow, y as RevealMark, yt as createDisplayListBuilder, z as sketchStrokes } from "./layoutEngine.js";
2
- import { BindableSignal, BoundTimeline, CompiledTimeline, CoverageReport, EaseSpec, FontMode, FontUsage, PathValue, Playhead, Timeline, Track, Vec2 } from "@glissade/core";
1
+ import { $ as breakLines, A as Polyline, At as invert, B as validateHachure, C as Video, Ct as filtersToCanvasFilter, D as revealSchedule, Dt as Mat2x3, E as pathFromSegs, Et as IDENTITY, F as flatten, G as HitArea, H as AnchorSpec, I as hachureLines, J as PropInit, K as Node, L as resolveSketch, M as SketchStyle, Mt as multiply, N as SketchValidationError, O as roundedRectSegs, Ot as applyToPoint, P as arcLength, Q as TextMetricsLite, R as roughen, S as TextProps, St as createDisplayListBuilder, T as WordBox, Tt as validateFilters, U as BindablePropTarget, V as validateSketch, W as EvalContext, X as MEASURE_QUANTUM_PX, Y as resolveAnchor, Z as TextMeasurer, _ as PathProps, _t as Rect$1, a as LayoutEngineMissingError, at as BlendMode, b as ShapeProps, bt as ShaderRef, c as requireLayoutEngine, ct as DrawCommand, d as Custom, dt as FontSpec, et as estimatingMeasurer, f as Group, ft as MeshInterpolation, g as Path, gt as PathSeg, h as LineBox, ht as Paint, i as LayoutEngine, it as setDefaultMeasurer, j as ResolvedSketch, jt as matEquals, k as HachureSpec, kt as fromTRS, l as setLayoutEngine, lt as FilterSpec, m as ImageProps, mt as MeshPoint, n as LayoutChildSpec, nt as segmentGraphemes, ot as DisplayList, p as ImageNode, pt as MeshPaint, q as NodeProps, r as LayoutContainerSpec, rt as segmentWords, s as getLayoutEngine, st as DisplayListBuilder, t as LayoutBox, tt as quantize, u as Circle, ut as FilterValidationError, v as Rect, vt as Resource, w as VideoProps, wt as glow, x as Text, xt as StrokeStyle, y as RevealMark, yt as ResourceId, z as sketchStrokes } from "./layoutEngine.js";
2
+ import { BindableSignal, BoundTimeline, CompiledTimeline, CoverageReport, EaseSpec, FontMode, FontUsage, MeshPaint as MeshPaint$1, PathValue, Playhead, Timeline, Track, Vec2 } from "@glissade/core";
3
3
 
4
4
  //#region src/taxonomy.d.ts
5
5
 
@@ -180,6 +180,102 @@ type GuardMode = 'throw' | 'warn' | 'off';
180
180
  */
181
181
  declare function withDeterminismGuards<T>(mode: GuardMode, fn: () => T): T;
182
182
  //#endregion
183
+ //#region src/displayDiff.d.ts
184
+ /**
185
+ * The collapse-replacer shared by the cacheKey serializer
186
+ * (`createDisplayListBuilder().cacheKey`), `cacheColdAudit.hashDisplayList`,
187
+ * and `serializeDisplayList` here. CRITICAL: this is BYTE-PRESERVING — the
188
+ * cacheKey it backs stamps into pushGroup and keys the §3.5 raster cache, so
189
+ * its output must not move. The three call sites previously DUPLICATED this
190
+ * byte-for-byte; it lives here once.
191
+ *
192
+ * - ArrayBuffer / typed-array views collapse to a `ab:<len>` / `view:<len>`
193
+ * length marker (opaque binary never belongs in a structural key).
194
+ * - Functions drop (JSON would already drop them; explicit for parity).
195
+ *
196
+ * NOTE: `-0` is intentionally NOT normalized here. The matrix layer
197
+ * (`matrix.ts`) already normalizes `-0 → 0` at the source, and adding a `-0`
198
+ * pass to THIS replacer would change the cacheKey bytes for any list that ever
199
+ * carried a raw `-0` — silently invalidating the cache cluster-wide. Byte
200
+ * preservation wins; the regression guard pins the exact key.
201
+ */
202
+ declare function collapseReplacer(_key: string, value: unknown): unknown;
203
+ /**
204
+ * One index-aligned, positional delta between two DisplayList command streams.
205
+ *
206
+ * This is the natural per-command shape that `gs verify-determinism --bisect`
207
+ * (a later card) will consume to drill a cache-cold divergence down to the
208
+ * exact op/field. Keep it stable.
209
+ */
210
+ interface CommandDelta {
211
+ /** Positional command index in BOTH lists (or the longer one for add/remove). */
212
+ index: number;
213
+ /**
214
+ * - `change` — same index present in both, but the commands differ.
215
+ * - `add` — present in `b` only (b is longer past this index).
216
+ * - `remove` — present in `a` only (a is longer past this index).
217
+ */
218
+ kind: 'change' | 'add' | 'remove';
219
+ /** op of the `a` command (undefined for `add`). */
220
+ opA?: DrawCommand['op'];
221
+ /** op of the `b` command (undefined for `remove`). */
222
+ opB?: DrawCommand['op'];
223
+ /**
224
+ * Field-level changes when both ops match (op changed → a single `op` field).
225
+ * Each entry names the changed prop path with its `from`/`to` JSON values.
226
+ */
227
+ fields: FieldChange[];
228
+ }
229
+ interface FieldChange {
230
+ /** dotted field path within the command, e.g. `paint.color`, `m`, `text`, `filters`. */
231
+ path: string;
232
+ from: unknown;
233
+ to: unknown;
234
+ }
235
+ interface DisplayDiff {
236
+ /** true when the two lists are byte-identical (the §3.3 determinism contract holds). */
237
+ equal: boolean;
238
+ /** per-command positional deltas, in index order (empty iff `equal`). */
239
+ deltas: CommandDelta[];
240
+ /** size mismatch, when the canvases differ (rare, but a real divergence). */
241
+ size?: {
242
+ from: DisplayList['size'];
243
+ to: DisplayList['size'];
244
+ };
245
+ }
246
+ /**
247
+ * Index-aligned positional diff of two DisplayLists. Command i in `a` is
248
+ * compared to command i in `b`; trailing commands become `add`/`remove`. A
249
+ * single insert/remove cascades — this is the documented v1 cliff.
250
+ */
251
+ declare function diffDisplayLists(a: DisplayList, b: DisplayList): DisplayDiff;
252
+ /** Human-readable, multi-line rendering of a DisplayDiff (CLI command-tree). */
253
+ declare function formatDisplayDiff(diff: DisplayDiff): string;
254
+ /**
255
+ * The `.dl.json` snapshot interchange schema (DESIGN §7.4). Users commit
256
+ * `.dl.json` baselines, so this carries the SAME break-policy obligation as
257
+ * `Timeline.version` and `SidecarDoc.sidecarVersion`: bump on a breaking shape
258
+ * change. Independent of the API version.
259
+ */
260
+ declare const DL_SNAPSHOT_VERSION: 1;
261
+ interface DlSnapshot {
262
+ /** Interchange schema version (§7.4) — the third versioned interchange document. */
263
+ dlSnapshotVersion: typeof DL_SNAPSHOT_VERSION;
264
+ size: DisplayList['size'];
265
+ commands: DrawCommand[];
266
+ resources: Resource[];
267
+ }
268
+ /**
269
+ * Serialize a DisplayList to a stable `.dl.json` document string (reusing the
270
+ * byte-preserving collapse-replacer). Round-trips through `parseDisplaySnapshot`.
271
+ */
272
+ declare function serializeDisplayList(dl: DisplayList): string;
273
+ declare class DlSnapshotError extends Error {
274
+ constructor(message: string);
275
+ }
276
+ /** Parse a `.dl.json` snapshot back into a DisplayList (validates the version). */
277
+ declare function parseDisplaySnapshot(json: string): DisplayList;
278
+ //#endregion
183
279
  //#region src/scene.d.ts
184
280
  interface Scene {
185
281
  readonly root: Group;
@@ -237,6 +333,14 @@ interface CacheColdResult {
237
333
  ok: boolean;
238
334
  /** id of the first node whose isolated emit() diverged (set only when !ok). */
239
335
  node?: string;
336
+ /**
337
+ * The FIRST command-level delta of the divergent node's isolated emit (set only
338
+ * when a specific leaf diverged — never for a Group fallback or a missing node).
339
+ * The WHOLE `CommandDelta` is embedded — index, kind, opA/opB, and every
340
+ * field change — so a multi-field divergence isn't flattened away. `gs
341
+ * verify-determinism --bisect` consumes this to name the (frame, node, op).
342
+ */
343
+ delta?: CommandDelta;
240
344
  }
241
345
  /**
242
346
  * Evaluate two fresh scenes from `createScene` at `t` and confirm the
@@ -505,6 +609,17 @@ interface Ctx2DLike<TPath, TDrawable> {
505
609
  globalCompositeOperation: string;
506
610
  filter: string;
507
611
  imageSmoothingEnabled: boolean;
612
+ /** §3 mesh Paint: write a straight-RGBA buffer into an offscreen tile, then
613
+ * blit it (clip + drawImage). Both DOM and @napi-rs/canvas expose these. */
614
+ createImageData(w: number, h: number): ImageDataLike;
615
+ putImageData(data: ImageDataLike, x: number, y: number): void;
616
+ }
617
+ /** The structural ImageData surface the mesh blit drives — DOM ImageData and
618
+ * @napi-rs/canvas ImageData both satisfy it (writable `.data`). */
619
+ interface ImageDataLike {
620
+ readonly data: Uint8ClampedArray;
621
+ readonly width: number;
622
+ readonly height: number;
508
623
  }
509
624
  interface CanvasLike {
510
625
  width: number;
@@ -569,6 +684,20 @@ declare class Raster2D<TCanvas extends CanvasLike, TPath extends PathLike, TDraw
569
684
  */
570
685
  private cacheStore;
571
686
  private cacheTouch;
687
+ /**
688
+ * §3 mesh Paint blit (the spike-chosen mechanism: clip + drawImage, NOT
689
+ * createPattern — the pattern path leaks edge-AA/alpha contamination and an
690
+ * uncontrolled resample filter across backends, breaking SSIM; clip+drawImage
691
+ * is fully controlled and clips to the actual path, not just its bounds box).
692
+ *
693
+ * The mesh is rasterized by the SHARED kernel into a fixed downscaled buffer
694
+ * (identical bytes on both backends), written into an offscreen tile, then
695
+ * upscaled into the path-local bounds with `imageSmoothingEnabled` PINNED true.
696
+ * The clip is the real fill path, so a circle/star fills correctly. Only this
697
+ * final blit's AA differs per backend — the source ImageData is byte-identical,
698
+ * which is what makes the golden byte-exact and browser↔Skia SSIM ≥ 0.97.
699
+ */
700
+ private fillMesh;
572
701
  /**
573
702
  * §3.4/§3.5 composite of a finished group layer onto its parent — the EXACT
574
703
  * same save/resetTransform/clip/globalAlpha/filter/blend/drawImage sequence
@@ -582,4 +711,40 @@ declare class Raster2D<TCanvas extends CanvasLike, TPath extends PathLike, TDraw
582
711
  render(target: TCanvas, list: DisplayList): void;
583
712
  }
584
713
  //#endregion
585
- export { ALL_FILTER_KINDS, type AnchorSpec, type BackendCaps, type BindablePropTarget, type BlendMode, type CacheColdResult, type CanvasLike, Circle, ColdAssetError, type Ctx2DLike, Custom, DeterminismViolationError, type DisplayList, type DisplayListBuilder, type DrawCommand, type DrawOnEachOptions, type DrawOnOptions, DuplicateNodeIdError, type EditMark, type EvalContext, type FilterKind, type FilterSpec, FilterValidationError, FollowPath, type FollowPathProps, type FontByteLoader, type FontSpec, Group, type GuardMode, type HachureSpec, Highlight, type HighlightProps, type HitArea, IDENTITY, ImageNode as Image, ImageNode, type ImageHandle, type ImageProps, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type LineBox, MEASURE_QUANTUM_PX, type Mat2x3, NODE_TAXONOMY, Node, type NodeProps, type NodeTypeName, type Paint, Path, type PathLike, type PathProps, type PathSampler, type PathSeg, type Polyline, type PropInit, Raster2D, type Raster2DHost, Rect, type Rect$1 as RectShape, type RenderBackend, ReservedNodeIdError, type ResolvedSketch, type Resource, type ResourceId, type RevealMark, type Scene, type SceneInit, type SceneModule, type ShaderCaps, ShaderEffect, type ShaderEffectProps, type ShaderRef, type ShapeProps, type SketchStyle, SketchValidationError, type StepMark, type StrokeStyle, Text, TextCursor, type TextCursorProps, type TextMeasurer, type TextMetricsLite, type TextProps, TokenHighlight, type TokenHighlightProps, TokenMatchError, type TokenRange, type TypeEdit, type TypewriterResult, type ValidateSceneFontsOptions, Video, type VideoFrameSource, type VideoProps, type WordBox, applyToPoint, arcLength, auditCacheCold, bindScene, breakLines, collectTextUsages, createDisplayListBuilder, createScene, drawOn, drawOnEach, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, followPath, fontString, fromTRS, getLayoutEngine, glow, hachureLines, highlight, invert, matEquals, matchTokenRun, motionPath, multiply, pathFromSegs, pathLength, pointAtLength, quantize, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, tokenHighlight, typewriter, validateFilters, validateHachure, validateSceneFonts, validateSketch, withDeterminismGuards };
714
+ //#region src/meshGradient.d.ts
715
+ /**
716
+ * The mesh raster resolution divisor. The mesh is computed at
717
+ * ceil(bounds / MESH_DOWNSCALE) and the backend upscales it (clip + drawImage,
718
+ * `imageSmoothingEnabled` pinned true) — a gradient is low-frequency, so the
719
+ * downscale is invisible while cutting the per-pixel kernel cost ~16×. PINNED:
720
+ * both backends compute the identical low-res ImageData.
721
+ */
722
+ declare const MESH_DOWNSCALE = 4;
723
+ /** Inverse-distance exponent for `smooth`/`oklab` (Shepard's method). Higher =
724
+ * sharper points; 2 is the classic IDW value and the pinned default. */
725
+ declare const MESH_SHEPARD_POWER = 2;
726
+ /**
727
+ * Gaussian weight sigma for `gaussian` mode, in NORMALIZED [0,1] mesh space
728
+ * (a point's influence falls to ~60% one sigma away). Pinned so the melt width
729
+ * is identical on both backends — the GAUSS_K precedent from gradient.ts.
730
+ */
731
+ declare const MESH_SIGMA = 0.32;
732
+ /**
733
+ * Rasterize a mesh Paint into an RGBA `Uint8ClampedArray` of `w*h` pixels
734
+ * (row-major, premultiply-free straight alpha). PURE function of
735
+ * (mesh, w, h): identical inputs → byte-identical buffer, on any backend.
736
+ *
737
+ * `w`/`h` are the DOWNSCALED dimensions (the caller divides the fill bounds by
738
+ * MESH_DOWNSCALE). Each output pixel's center maps to mesh space [0,1]²; the
739
+ * blend is Shepard IDW (smooth/oklab) or a pinned-sigma gaussian (gaussian) of
740
+ * the point colors in OKLab, with an optional `bg` color as a zero-weight floor
741
+ * (a baseline so sparse meshes don't smear a single point across the whole rect).
742
+ */
743
+ declare function rasterizeMesh(mesh: MeshPaint$1, w: number, h: number): Uint8ClampedArray;
744
+ /** Downscaled raster dimensions for a fill of `bw×bh` local px (≥1, capped). */
745
+ declare function meshRasterSize(bw: number, bh: number): {
746
+ w: number;
747
+ h: number;
748
+ };
749
+ //#endregion
750
+ export { ALL_FILTER_KINDS, type AnchorSpec, type BackendCaps, type BindablePropTarget, type BlendMode, type CacheColdResult, type CanvasLike, Circle, ColdAssetError, type CommandDelta, type Ctx2DLike, Custom, DL_SNAPSHOT_VERSION, DeterminismViolationError, type DisplayDiff, type DisplayList, type DisplayListBuilder, type DlSnapshot, DlSnapshotError, type DrawCommand, type DrawOnEachOptions, type DrawOnOptions, DuplicateNodeIdError, type EditMark, type EvalContext, type FieldChange, type FilterKind, type FilterSpec, FilterValidationError, FollowPath, type FollowPathProps, type FontByteLoader, type FontSpec, Group, type GuardMode, type HachureSpec, Highlight, type HighlightProps, type HitArea, IDENTITY, ImageNode as Image, ImageNode, type ImageDataLike, type ImageHandle, type ImageProps, type LayoutBox, type LayoutChildSpec, type LayoutContainerSpec, type LayoutEngine, LayoutEngineMissingError, type LineBox, MEASURE_QUANTUM_PX, MESH_DOWNSCALE, MESH_SHEPARD_POWER, MESH_SIGMA, type Mat2x3, type MeshInterpolation, type MeshPaint, type MeshPoint, NODE_TAXONOMY, Node, type NodeProps, type NodeTypeName, type Paint, Path, type PathLike, type PathProps, type PathSampler, type PathSeg, type Polyline, type PropInit, Raster2D, type Raster2DHost, Rect, type Rect$1 as RectShape, type RenderBackend, ReservedNodeIdError, type ResolvedSketch, type Resource, type ResourceId, type RevealMark, type Scene, type SceneInit, type SceneModule, type ShaderCaps, ShaderEffect, type ShaderEffectProps, type ShaderRef, type ShapeProps, type SketchStyle, SketchValidationError, type StepMark, type StrokeStyle, Text, TextCursor, type TextCursorProps, type TextMeasurer, type TextMetricsLite, type TextProps, TokenHighlight, type TokenHighlightProps, TokenMatchError, type TokenRange, type TypeEdit, type TypewriterResult, type ValidateSceneFontsOptions, Video, type VideoFrameSource, type VideoProps, type WordBox, applyToPoint, arcLength, auditCacheCold, bindScene, breakLines, collapseReplacer, collectTextUsages, createDisplayListBuilder, createScene, diffDisplayLists, drawOn, drawOnEach, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, followPath, fontString, formatDisplayDiff, fromTRS, getLayoutEngine, glow, hachureLines, highlight, invert, matEquals, matchTokenRun, meshRasterSize, motionPath, multiply, parseDisplaySnapshot, pathFromSegs, pathLength, pointAtLength, quantize, rasterizeMesh, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, serializeDisplayList, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, tokenHighlight, typewriter, validateFilters, validateHachure, validateSceneFonts, validateSketch, withDeterminismGuards };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { A as fallbackMeasurer, B as IDENTITY, C as validateHachure, D as MEASURE_QUANTUM_PX, E as resolveAnchor, F as FilterValidationError, G as multiply, H as fromTRS, I as createDisplayListBuilder, L as filtersToCanvasFilter, M as segmentGraphemes, N as segmentWords, O as breakLines, P as setDefaultMeasurer, R as glow, S as sketchStrokes, T as Node, U as invert, V as applyToPoint, W as matEquals, _ as arcLength, a as Circle, b as resolveSketch, c as ImageNode, d as Text, f as Video, g as SketchValidationError, h as roundedRectSegs, i as setLayoutEngine, j as quantize, k as estimatingMeasurer, l as Path, m as revealSchedule, n as getLayoutEngine, o as Custom, p as pathFromSegs, r as requireLayoutEngine, s as Group, t as LayoutEngineMissingError, u as Rect, v as flatten, w as validateSketch, x as roughen, y as hachureLines, z as validateFilters } from "./layoutEngine.js";
2
- import { bindTimeline, buildFontRegistry, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, key, lerpColor, parseCmap, signal, stagger, track, validateFonts, vec2Signal } from "@glissade/core";
1
+ import { A as fallbackMeasurer, B as DL_SNAPSHOT_VERSION, C as validateHachure, D as MEASURE_QUANTUM_PX, E as resolveAnchor, F as FilterValidationError, G as parseDisplaySnapshot, H as collapseReplacer, I as createDisplayListBuilder, J as applyToPoint, K as serializeDisplayList, L as filtersToCanvasFilter, M as segmentGraphemes, N as segmentWords, O as breakLines, P as setDefaultMeasurer, Q as multiply, R as glow, S as sketchStrokes, T as Node, U as diffDisplayLists, V as DlSnapshotError, W as formatDisplayDiff, X as invert, Y as fromTRS, Z as matEquals, _ as arcLength, a as Circle, b as resolveSketch, c as ImageNode, d as Text, f as Video, g as SketchValidationError, h as roundedRectSegs, i as setLayoutEngine, j as quantize, k as estimatingMeasurer, l as Path, m as revealSchedule, n as getLayoutEngine, o as Custom, p as pathFromSegs, q as IDENTITY, r as requireLayoutEngine, s as Group, t as LayoutEngineMissingError, u as Rect, v as flatten, w as validateSketch, x as roughen, y as hachureLines, z as validateFilters } from "./layoutEngine.js";
2
+ import { bindTimeline, buildFontRegistry, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, key, lerpColor, oklabToRgba, parseCmap, parseColor, rgbaToOklab, signal, stagger, track, validateFonts, vec2Signal } from "@glissade/core";
3
3
  //#region src/taxonomy.ts
4
4
  /**
5
5
  * The CLOSED node taxonomy (DESIGN.md §3.1): exactly nine built-in node TYPES.
@@ -457,12 +457,7 @@ function evaluate(scene, doc, t) {
457
457
  //#region src/cacheColdAudit.ts
458
458
  /** Stable string of a DisplayList for comparison (opaque resources collapse to a marker). */
459
459
  function hashDisplayList(dl) {
460
- return JSON.stringify(dl, (_key, value) => {
461
- if (value instanceof ArrayBuffer) return `ab:${value.byteLength}`;
462
- if (ArrayBuffer.isView(value)) return `view:${value.byteLength}`;
463
- if (typeof value === "function") return void 0;
464
- return value;
465
- });
460
+ return JSON.stringify(dl, collapseReplacer);
466
461
  }
467
462
  /**
468
463
  * Evaluate two fresh scenes from `createScene` at `t` and confirm the
@@ -498,14 +493,18 @@ function auditCacheCold(createScene, doc, t) {
498
493
  nodeA.emit(ea, ctxA);
499
494
  const eb = createDisplayListBuilder(cold.size);
500
495
  nodeB.emit(eb, ctxB);
501
- if (hashDisplayList(ea.finish()) === hashDisplayList(eb.finish())) continue;
496
+ const dlA = ea.finish();
497
+ const dlB = eb.finish();
498
+ if (hashDisplayList(dlA) === hashDisplayList(dlB)) continue;
502
499
  if (nodeA instanceof Group) {
503
500
  groupFallback ??= id;
504
501
  continue;
505
502
  }
503
+ const first = diffDisplayLists(dlA, dlB).deltas[0];
506
504
  return {
507
505
  ok: false,
508
- node: id
506
+ node: id,
507
+ ...first !== void 0 ? { delta: first } : {}
509
508
  };
510
509
  }
511
510
  return {
@@ -1016,6 +1015,149 @@ function densifyStops(stops, mode) {
1016
1015
  return out;
1017
1016
  }
1018
1017
  //#endregion
1018
+ //#region src/meshGradient.ts
1019
+ /**
1020
+ * Mesh-gradient kernel (§3 Paint, 0.12). A `mesh` Paint is N color points
1021
+ * scattered across the [0,1]² fill rectangle, blended into ONE animatable fill
1022
+ * — the native replacement for the "N blurred blobs" aurora backdrop.
1023
+ *
1024
+ * THE DETERMINISM TENTPOLE: `@napi-rs/canvas` exposes NO SkSL RuntimeEffect /
1025
+ * makeShader, so there is no SkSL-vs-fallback fork — there is exactly ONE shared
1026
+ * CPU kernel that BOTH backends run. This file is that kernel, mirroring
1027
+ * `densifyStops` (gradient.ts): pinned named constants (`MESH_SIGMA`,
1028
+ * `MESH_SHEPARD_POWER`, `MESH_DOWNSCALE`) so neither backend picks its own, OKLab
1029
+ * blend reused bit-identically from `@glissade/core`, and integer `Uint8ClampedArray`
1030
+ * quantization so the buffer is reproducible run-to-run and byte-identical across
1031
+ * backends. Only the FINAL blit (clip + drawImage upscale) differs per backend —
1032
+ * the source pixels are identical, which is what makes browser↔Skia SSIM ≥ 0.97.
1033
+ *
1034
+ * The blend is ONE deterministic Shepard inverse-distance kernel with a
1035
+ * colorspace knob: `smooth` = inverse-distance weighting in OKLab; `gaussian` =
1036
+ * a pinned-sigma gaussian weight (a softer, blurrier melt); `oklab` is an alias
1037
+ * for `smooth` (the blend space is always OKLab — the gradient path's contract).
1038
+ * NO triangulator (Gouraud/Delaunay/Coons are deferred).
1039
+ */
1040
+ /**
1041
+ * The mesh raster resolution divisor. The mesh is computed at
1042
+ * ceil(bounds / MESH_DOWNSCALE) and the backend upscales it (clip + drawImage,
1043
+ * `imageSmoothingEnabled` pinned true) — a gradient is low-frequency, so the
1044
+ * downscale is invisible while cutting the per-pixel kernel cost ~16×. PINNED:
1045
+ * both backends compute the identical low-res ImageData.
1046
+ */
1047
+ const MESH_DOWNSCALE = 4;
1048
+ /** Inverse-distance exponent for `smooth`/`oklab` (Shepard's method). Higher =
1049
+ * sharper points; 2 is the classic IDW value and the pinned default. */
1050
+ const MESH_SHEPARD_POWER = 2;
1051
+ /**
1052
+ * Gaussian weight sigma for `gaussian` mode, in NORMALIZED [0,1] mesh space
1053
+ * (a point's influence falls to ~60% one sigma away). Pinned so the melt width
1054
+ * is identical on both backends — the GAUSS_K precedent from gradient.ts.
1055
+ */
1056
+ const MESH_SIGMA = .32;
1057
+ /** Epsilon so a sample sitting exactly on a point doesn't divide by zero; small
1058
+ * enough to be a hard pin to that point's color. Pinned (folds into the bytes). */
1059
+ const MESH_EPS = 1e-6;
1060
+ /** Cap the mesh raster so a pathological tiny-downscale × huge-bounds can't OOM;
1061
+ * far above any real fill at /4. Pinned (it changes nothing for real meshes). */
1062
+ const MESH_MAX_DIM = 1024;
1063
+ /**
1064
+ * Rasterize a mesh Paint into an RGBA `Uint8ClampedArray` of `w*h` pixels
1065
+ * (row-major, premultiply-free straight alpha). PURE function of
1066
+ * (mesh, w, h): identical inputs → byte-identical buffer, on any backend.
1067
+ *
1068
+ * `w`/`h` are the DOWNSCALED dimensions (the caller divides the fill bounds by
1069
+ * MESH_DOWNSCALE). Each output pixel's center maps to mesh space [0,1]²; the
1070
+ * blend is Shepard IDW (smooth/oklab) or a pinned-sigma gaussian (gaussian) of
1071
+ * the point colors in OKLab, with an optional `bg` color as a zero-weight floor
1072
+ * (a baseline so sparse meshes don't smear a single point across the whole rect).
1073
+ */
1074
+ function rasterizeMesh(mesh, w, h) {
1075
+ const out = new Uint8ClampedArray(Math.max(1, w) * Math.max(1, h) * 4);
1076
+ const pts = mesh.points.map((p) => ({
1077
+ u: p.pos[0],
1078
+ v: p.pos[1],
1079
+ lab: rgbaToOklab(parseColor(p.color))
1080
+ }));
1081
+ const bg = mesh.bg !== void 0 ? rgbaToOklab(parseColor(mesh.bg)) : null;
1082
+ const gaussian = mesh.interpolation === "gaussian";
1083
+ const twoSigmaSq = 2 * MESH_SIGMA * MESH_SIGMA;
1084
+ for (let y = 0; y < h; y++) {
1085
+ const v = h > 1 ? (y + .5) / h : .5;
1086
+ for (let x = 0; x < w; x++) {
1087
+ const u = w > 1 ? (x + .5) / w : .5;
1088
+ let wsum = 0;
1089
+ let L = 0;
1090
+ let A = 0;
1091
+ let B = 0;
1092
+ let alpha = 0;
1093
+ if (bg) {
1094
+ wsum = 1;
1095
+ L = bg.L;
1096
+ A = bg.a;
1097
+ B = bg.b;
1098
+ alpha = bg.alpha;
1099
+ }
1100
+ let pinned = false;
1101
+ for (let i = 0; i < pts.length; i++) {
1102
+ const p = pts[i];
1103
+ const du = u - p.u;
1104
+ const dv = v - p.v;
1105
+ const d2 = du * du + dv * dv;
1106
+ if (d2 <= MESH_EPS * MESH_EPS) {
1107
+ L = p.lab.L;
1108
+ A = p.lab.a;
1109
+ B = p.lab.b;
1110
+ alpha = p.lab.alpha;
1111
+ pinned = true;
1112
+ break;
1113
+ }
1114
+ let weight;
1115
+ if (gaussian) weight = Math.exp(-d2 / twoSigmaSq);
1116
+ else weight = 1 / d2;
1117
+ wsum += weight;
1118
+ L += weight * p.lab.L;
1119
+ A += weight * p.lab.a;
1120
+ B += weight * p.lab.b;
1121
+ alpha += weight * p.lab.alpha;
1122
+ }
1123
+ let rgba;
1124
+ if (pinned) rgba = oklabToRgba({
1125
+ L,
1126
+ a: A,
1127
+ b: B,
1128
+ alpha
1129
+ });
1130
+ else if (wsum > 0) {
1131
+ const inv = 1 / wsum;
1132
+ rgba = oklabToRgba({
1133
+ L: L * inv,
1134
+ a: A * inv,
1135
+ b: B * inv,
1136
+ alpha: alpha * inv
1137
+ });
1138
+ } else rgba = {
1139
+ r: 0,
1140
+ g: 0,
1141
+ b: 0,
1142
+ a: 0
1143
+ };
1144
+ const o = (y * w + x) * 4;
1145
+ out[o] = rgba.r;
1146
+ out[o + 1] = rgba.g;
1147
+ out[o + 2] = rgba.b;
1148
+ out[o + 3] = Math.round(rgba.a * 255);
1149
+ }
1150
+ }
1151
+ return out;
1152
+ }
1153
+ /** Downscaled raster dimensions for a fill of `bw×bh` local px (≥1, capped). */
1154
+ function meshRasterSize(bw, bh) {
1155
+ return {
1156
+ w: Math.min(MESH_MAX_DIM, Math.max(1, Math.ceil(Math.abs(bw) / 4))),
1157
+ h: Math.min(MESH_MAX_DIM, Math.max(1, Math.ceil(Math.abs(bh) / 4)))
1158
+ };
1159
+ }
1160
+ //#endregion
1019
1161
  //#region src/raster2d.ts
1020
1162
  /**
1021
1163
  * The shared DisplayList interpreter (§3.4): one command walk over the
@@ -1024,8 +1166,19 @@ function densifyStops(stops, mode) {
1024
1166
  * four-line adapters, so the twin rasterizers structurally cannot drift —
1025
1167
  * the golden + SSIM suites verify the refactor preserved every byte.
1026
1168
  */
1169
+ /** Stroke/text mesh paint has no clippable fill region — pick a deterministic
1170
+ * representative solid (bg, else the first point) so it renders without a crash. */
1171
+ let warnedMeshStroke = false;
1172
+ function meshRepresentativeColor(paint) {
1173
+ if (!warnedMeshStroke) {
1174
+ warnedMeshStroke = true;
1175
+ emitDevWarning("mesh Paint on a stroke/text resolves to a representative solid color (mesh fill is supported only on filled paths, §3 Paint) — use a fillPath for the mesh gradient");
1176
+ }
1177
+ return paint.bg ?? paint.points[0]?.color ?? "#00000000";
1178
+ }
1027
1179
  function resolveFill(ctx, paint, bounds) {
1028
1180
  if (paint.kind === "color") return paint.color;
1181
+ if (paint.kind === "mesh") return meshRepresentativeColor(paint);
1029
1182
  let g;
1030
1183
  if (paint.kind === "radial") {
1031
1184
  const cx = paint.center ? paint.center[0] : bounds ? (bounds.minX + bounds.maxX) / 2 : 0;
@@ -1256,6 +1409,37 @@ var Raster2D = class {
1256
1409
  return hit;
1257
1410
  }
1258
1411
  /**
1412
+ * §3 mesh Paint blit (the spike-chosen mechanism: clip + drawImage, NOT
1413
+ * createPattern — the pattern path leaks edge-AA/alpha contamination and an
1414
+ * uncontrolled resample filter across backends, breaking SSIM; clip+drawImage
1415
+ * is fully controlled and clips to the actual path, not just its bounds box).
1416
+ *
1417
+ * The mesh is rasterized by the SHARED kernel into a fixed downscaled buffer
1418
+ * (identical bytes on both backends), written into an offscreen tile, then
1419
+ * upscaled into the path-local bounds with `imageSmoothingEnabled` PINNED true.
1420
+ * The clip is the real fill path, so a circle/star fills correctly. Only this
1421
+ * final blit's AA differs per backend — the source ImageData is byte-identical,
1422
+ * which is what makes the golden byte-exact and browser↔Skia SSIM ≥ 0.97.
1423
+ */
1424
+ fillMesh(ctx, path, paint, bounds) {
1425
+ const bw = bounds.maxX - bounds.minX;
1426
+ const bh = bounds.maxY - bounds.minY;
1427
+ if (bw <= 0 || bh <= 0) return;
1428
+ const { w, h } = meshRasterSize(bw, bh);
1429
+ const buf = rasterizeMesh(paint, w, h);
1430
+ const tile = this.acquire(w, h);
1431
+ const tileCtx = this.host.context(tile);
1432
+ const img = tileCtx.createImageData(w, h);
1433
+ img.data.set(buf);
1434
+ tileCtx.putImageData(img, 0, 0);
1435
+ ctx.save();
1436
+ ctx.clip(path, "nonzero");
1437
+ ctx.imageSmoothingEnabled = true;
1438
+ ctx.drawImage(tile, 0, 0, w, h, bounds.minX, bounds.minY, bw, bh);
1439
+ ctx.restore();
1440
+ this.release(tile);
1441
+ }
1442
+ /**
1259
1443
  * §3.4/§3.5 composite of a finished group layer onto its parent — the EXACT
1260
1444
  * same save/resetTransform/clip/globalAlpha/filter/blend/drawImage sequence
1261
1445
  * for both the freshly-rasterized miss path and a cache-blit hit, so a HIT is
@@ -1337,8 +1521,11 @@ var Raster2D = class {
1337
1521
  case "fillPath": {
1338
1522
  const ctx = ctxOf();
1339
1523
  const b = this.pathBounds(list.resources, cmd.path);
1340
- ctx.fillStyle = resolveFill(ctx, cmd.paint, b);
1341
- ctx.fill(this.path(list.resources, cmd.path));
1524
+ if (cmd.paint.kind === "mesh" && b) this.fillMesh(ctx, this.path(list.resources, cmd.path), cmd.paint, b);
1525
+ else {
1526
+ ctx.fillStyle = resolveFill(ctx, cmd.paint, b);
1527
+ ctx.fill(this.path(list.resources, cmd.path));
1528
+ }
1342
1529
  if (b) accumulateRect(top(), mat, b.minX, b.minY, b.maxX, b.maxY);
1343
1530
  break;
1344
1531
  }
@@ -1468,4 +1655,4 @@ var Raster2D = class {
1468
1655
  }
1469
1656
  };
1470
1657
  //#endregion
1471
- export { ALL_FILTER_KINDS, Circle, ColdAssetError, Custom, DeterminismViolationError, DuplicateNodeIdError, FilterValidationError, FollowPath, Group, Highlight, IDENTITY, ImageNode as Image, ImageNode, LayoutEngineMissingError, MEASURE_QUANTUM_PX, NODE_TAXONOMY, Node, Path, Raster2D, Rect, ReservedNodeIdError, ShaderEffect, SketchValidationError, Text, TextCursor, TokenHighlight, TokenMatchError, Video, applyToPoint, arcLength, auditCacheCold, bindScene, breakLines, collectTextUsages, createDisplayListBuilder, createScene, drawOn, drawOnEach, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, followPath, fontString, fromTRS, getLayoutEngine, glow, hachureLines, highlight, invert, matEquals, matchTokenRun, motionPath, multiply, pathFromSegs, pathLength, pointAtLength, quantize, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, tokenHighlight, typewriter, validateFilters, validateHachure, validateSceneFonts, validateSketch, withDeterminismGuards };
1658
+ export { ALL_FILTER_KINDS, Circle, ColdAssetError, Custom, DL_SNAPSHOT_VERSION, DeterminismViolationError, DlSnapshotError, DuplicateNodeIdError, FilterValidationError, FollowPath, Group, Highlight, IDENTITY, ImageNode as Image, ImageNode, LayoutEngineMissingError, MEASURE_QUANTUM_PX, MESH_DOWNSCALE, MESH_SHEPARD_POWER, MESH_SIGMA, NODE_TAXONOMY, Node, Path, Raster2D, Rect, ReservedNodeIdError, ShaderEffect, SketchValidationError, Text, TextCursor, TokenHighlight, TokenMatchError, Video, applyToPoint, arcLength, auditCacheCold, bindScene, breakLines, collapseReplacer, collectTextUsages, createDisplayListBuilder, createScene, diffDisplayLists, drawOn, drawOnEach, estimatingMeasurer, evaluate, filtersToCanvasFilter, flatten, followPath, fontString, formatDisplayDiff, fromTRS, getLayoutEngine, glow, hachureLines, highlight, invert, matEquals, matchTokenRun, meshRasterSize, motionPath, multiply, parseDisplaySnapshot, pathFromSegs, pathLength, pointAtLength, quantize, rasterizeMesh, requireLayoutEngine, resolveAnchor, resolveSketch, revealSchedule, roughen, roundedRectSegs, segmentGraphemes, segmentWords, serializeDisplayList, setDefaultMeasurer, setLayoutEngine, sketchStrokes, textCursor, tokenHighlight, typewriter, validateFilters, validateHachure, validateSceneFonts, validateSketch, withDeterminismGuards };
@@ -1,4 +1,4 @@
1
- import { BindableSignal, Paint, PathValue, ReadonlySignal, Rng, Track, Vec2, Vec2Signal } from "@glissade/core";
1
+ import { BindableSignal, MeshInterpolation, MeshPaint as MeshPaint$1, MeshPoint, Paint, PathValue, ReadonlySignal, Rng, Track, Vec2, Vec2Signal } from "@glissade/core";
2
2
 
3
3
  //#region src/matrix.d.ts
4
4
 
@@ -846,4 +846,4 @@ declare function setLayoutEngine(e: LayoutEngine): void;
846
846
  declare function getLayoutEngine(): LayoutEngine | null;
847
847
  declare function requireLayoutEngine(): LayoutEngine;
848
848
  //#endregion
849
- export { breakLines as $, Polyline as A, validateHachure as B, Video as C, IDENTITY as Ct, revealSchedule as D, invert as Dt, pathFromSegs as E, fromTRS as Et, flatten as F, HitArea as G, AnchorSpec as H, hachureLines as I, PropInit as J, Node as K, resolveSketch as L, SketchStyle as M, SketchValidationError as N, roundedRectSegs as O, matEquals as Ot, arcLength as P, TextMetricsLite as Q, roughen as R, TextProps as S, validateFilters as St, WordBox as T, applyToPoint as Tt, BindablePropTarget as U, validateSketch as V, EvalContext as W, MEASURE_QUANTUM_PX as X, resolveAnchor as Y, TextMeasurer as Z, PathProps as _, ShaderRef as _t, LayoutEngineMissingError as a, BlendMode as at, ShapeProps as b, filtersToCanvasFilter as bt, requireLayoutEngine as c, DrawCommand as ct, Custom as d, FontSpec as dt, estimatingMeasurer as et, Group as f, Paint as ft, Path as g, ResourceId as gt, LineBox as h, Resource as ht, LayoutEngine as i, setDefaultMeasurer as it, ResolvedSketch as j, HachureSpec as k, multiply as kt, setLayoutEngine as l, FilterSpec as lt, ImageProps as m, Rect$1 as mt, LayoutChildSpec as n, segmentGraphemes as nt, LayoutResult as o, DisplayList as ot, ImageNode as p, PathSeg as pt, NodeProps as q, LayoutContainerSpec as r, segmentWords as rt, getLayoutEngine as s, DisplayListBuilder as st, LayoutBox as t, quantize as tt, Circle as u, FilterValidationError as ut, Rect as v, StrokeStyle as vt, VideoProps as w, Mat2x3 as wt, Text as x, glow as xt, RevealMark as y, createDisplayListBuilder as yt, sketchStrokes as z };
849
+ export { breakLines as $, Polyline as A, invert as At, validateHachure as B, Video as C, filtersToCanvasFilter as Ct, revealSchedule as D, Mat2x3 as Dt, pathFromSegs as E, IDENTITY as Et, flatten as F, HitArea as G, AnchorSpec as H, hachureLines as I, PropInit as J, Node as K, resolveSketch as L, SketchStyle as M, multiply as Mt, SketchValidationError as N, roundedRectSegs as O, applyToPoint as Ot, arcLength as P, TextMetricsLite as Q, roughen as R, TextProps as S, createDisplayListBuilder as St, WordBox as T, validateFilters as Tt, BindablePropTarget as U, validateSketch as V, EvalContext as W, MEASURE_QUANTUM_PX as X, resolveAnchor as Y, TextMeasurer as Z, PathProps as _, Rect$1 as _t, LayoutEngineMissingError as a, BlendMode as at, ShapeProps as b, ShaderRef as bt, requireLayoutEngine as c, DrawCommand as ct, Custom as d, FontSpec as dt, estimatingMeasurer as et, Group as f, MeshInterpolation as ft, Path as g, PathSeg as gt, LineBox as h, Paint as ht, LayoutEngine as i, setDefaultMeasurer as it, ResolvedSketch as j, matEquals as jt, HachureSpec as k, fromTRS as kt, setLayoutEngine as l, FilterSpec as lt, ImageProps as m, MeshPoint as mt, LayoutChildSpec as n, segmentGraphemes as nt, LayoutResult as o, DisplayList as ot, ImageNode as p, MeshPaint$1 as pt, NodeProps as q, LayoutContainerSpec as r, segmentWords as rt, getLayoutEngine as s, DisplayListBuilder as st, LayoutBox as t, quantize as tt, Circle as u, FilterValidationError as ut, Rect as v, Resource as vt, VideoProps as w, glow as wt, Text as x, StrokeStyle as xt, RevealMark as y, ResourceId as yt, sketchStrokes as z };
@@ -61,6 +61,162 @@ function matEquals(a, b) {
61
61
  return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3] && a[4] === b[4] && a[5] === b[5];
62
62
  }
63
63
  //#endregion
64
+ //#region src/displayDiff.ts
65
+ /**
66
+ * The collapse-replacer shared by the cacheKey serializer
67
+ * (`createDisplayListBuilder().cacheKey`), `cacheColdAudit.hashDisplayList`,
68
+ * and `serializeDisplayList` here. CRITICAL: this is BYTE-PRESERVING — the
69
+ * cacheKey it backs stamps into pushGroup and keys the §3.5 raster cache, so
70
+ * its output must not move. The three call sites previously DUPLICATED this
71
+ * byte-for-byte; it lives here once.
72
+ *
73
+ * - ArrayBuffer / typed-array views collapse to a `ab:<len>` / `view:<len>`
74
+ * length marker (opaque binary never belongs in a structural key).
75
+ * - Functions drop (JSON would already drop them; explicit for parity).
76
+ *
77
+ * NOTE: `-0` is intentionally NOT normalized here. The matrix layer
78
+ * (`matrix.ts`) already normalizes `-0 → 0` at the source, and adding a `-0`
79
+ * pass to THIS replacer would change the cacheKey bytes for any list that ever
80
+ * carried a raw `-0` — silently invalidating the cache cluster-wide. Byte
81
+ * preservation wins; the regression guard pins the exact key.
82
+ */
83
+ function collapseReplacer(_key, value) {
84
+ if (value instanceof ArrayBuffer) return `ab:${value.byteLength}`;
85
+ if (ArrayBuffer.isView(value)) return `view:${value.byteLength}`;
86
+ if (typeof value === "function") return void 0;
87
+ return value;
88
+ }
89
+ /** A flat, stable JSON value for one command with its resource ids INLINED to content. */
90
+ function commandView(cmd, resources) {
91
+ const out = {};
92
+ for (const [k, v] of Object.entries(cmd)) out[k] = v;
93
+ if (cmd.op === "clip" || cmd.op === "fillPath" || cmd.op === "strokePath") out["path"] = resources[cmd.path];
94
+ else if (cmd.op === "drawImage") out["image"] = resources[cmd.image];
95
+ return out;
96
+ }
97
+ const stable = (v) => JSON.stringify(v, collapseReplacer);
98
+ /** Field-level diff of two same-op command views (shallow over top-level props). */
99
+ function diffFields(a, b) {
100
+ const changes = [];
101
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
102
+ for (const k of keys) {
103
+ if (k === "op") continue;
104
+ if (stable(a[k]) !== stable(b[k])) changes.push({
105
+ path: k,
106
+ from: a[k],
107
+ to: b[k]
108
+ });
109
+ }
110
+ return changes;
111
+ }
112
+ /**
113
+ * Index-aligned positional diff of two DisplayLists. Command i in `a` is
114
+ * compared to command i in `b`; trailing commands become `add`/`remove`. A
115
+ * single insert/remove cascades — this is the documented v1 cliff.
116
+ */
117
+ function diffDisplayLists(a, b) {
118
+ const deltas = [];
119
+ const n = Math.max(a.commands.length, b.commands.length);
120
+ for (let i = 0; i < n; i++) {
121
+ const ca = a.commands[i];
122
+ const cb = b.commands[i];
123
+ if (ca !== void 0 && cb !== void 0) {
124
+ const va = commandView(ca, a.resources);
125
+ const vb = commandView(cb, b.resources);
126
+ if (stable(va) === stable(vb)) continue;
127
+ if (ca.op !== cb.op) deltas.push({
128
+ index: i,
129
+ kind: "change",
130
+ opA: ca.op,
131
+ opB: cb.op,
132
+ fields: [{
133
+ path: "op",
134
+ from: ca.op,
135
+ to: cb.op
136
+ }]
137
+ });
138
+ else deltas.push({
139
+ index: i,
140
+ kind: "change",
141
+ opA: ca.op,
142
+ opB: cb.op,
143
+ fields: diffFields(va, vb)
144
+ });
145
+ } else if (cb !== void 0) deltas.push({
146
+ index: i,
147
+ kind: "add",
148
+ opB: cb.op,
149
+ fields: []
150
+ });
151
+ else if (ca !== void 0) deltas.push({
152
+ index: i,
153
+ kind: "remove",
154
+ opA: ca.op,
155
+ fields: []
156
+ });
157
+ }
158
+ const sizeDiffers = a.size.w !== b.size.w || a.size.h !== b.size.h;
159
+ return {
160
+ equal: deltas.length === 0 && !sizeDiffers,
161
+ deltas,
162
+ ...sizeDiffers ? { size: {
163
+ from: a.size,
164
+ to: b.size
165
+ } } : {}
166
+ };
167
+ }
168
+ /** Human-readable, multi-line rendering of a DisplayDiff (CLI command-tree). */
169
+ function formatDisplayDiff(diff) {
170
+ if (diff.equal) return "DisplayLists are identical.";
171
+ const lines = [];
172
+ if (diff.size) lines.push(`size ${diff.size.from.w}x${diff.size.from.h} -> ${diff.size.to.w}x${diff.size.to.h}`);
173
+ for (const d of diff.deltas) if (d.kind === "add") lines.push(`+ [${d.index}] add ${d.opB ?? "?"}`);
174
+ else if (d.kind === "remove") lines.push(`- [${d.index}] remove ${d.opA ?? "?"}`);
175
+ else {
176
+ const op = d.opA === d.opB ? d.opA : `${d.opA ?? "?"} -> ${d.opB ?? "?"}`;
177
+ lines.push(`~ [${d.index}] ${op}`);
178
+ for (const f of d.fields) lines.push(` ${f.path}: ${stable(f.from)} -> ${stable(f.to)}`);
179
+ }
180
+ return lines.join("\n");
181
+ }
182
+ /**
183
+ * The `.dl.json` snapshot interchange schema (DESIGN §7.4). Users commit
184
+ * `.dl.json` baselines, so this carries the SAME break-policy obligation as
185
+ * `Timeline.version` and `SidecarDoc.sidecarVersion`: bump on a breaking shape
186
+ * change. Independent of the API version.
187
+ */
188
+ const DL_SNAPSHOT_VERSION = 1;
189
+ /**
190
+ * Serialize a DisplayList to a stable `.dl.json` document string (reusing the
191
+ * byte-preserving collapse-replacer). Round-trips through `parseDisplaySnapshot`.
192
+ */
193
+ function serializeDisplayList(dl) {
194
+ const snapshot = {
195
+ dlSnapshotVersion: 1,
196
+ size: dl.size,
197
+ commands: dl.commands,
198
+ resources: dl.resources
199
+ };
200
+ return JSON.stringify(snapshot, collapseReplacer, 2);
201
+ }
202
+ var DlSnapshotError = class extends Error {
203
+ constructor(message) {
204
+ super(message);
205
+ this.name = "DlSnapshotError";
206
+ }
207
+ };
208
+ /** Parse a `.dl.json` snapshot back into a DisplayList (validates the version). */
209
+ function parseDisplaySnapshot(json) {
210
+ const doc = JSON.parse(json);
211
+ if (doc.dlSnapshotVersion !== 1) throw new DlSnapshotError(`unsupported dlSnapshotVersion ${String(doc.dlSnapshotVersion)}; this build reads 1`);
212
+ if (!doc.size || !Array.isArray(doc.commands) || !Array.isArray(doc.resources)) throw new DlSnapshotError("malformed .dl.json snapshot (need size, commands[], resources[])");
213
+ return {
214
+ size: doc.size,
215
+ commands: doc.commands,
216
+ resources: doc.resources
217
+ };
218
+ }
219
+ //#endregion
64
220
  //#region src/displayList.ts
65
221
  var FilterValidationError = class extends Error {
66
222
  constructor(message) {
@@ -190,12 +346,7 @@ function createDisplayListBuilder(size) {
190
346
  return fnv1a(JSON.stringify({
191
347
  c: sliceCmds,
192
348
  r: usedResources
193
- }, (_k, value) => {
194
- if (value instanceof ArrayBuffer) return `ab:${value.byteLength}`;
195
- if (ArrayBuffer.isView(value)) return `view:${value.byteLength}`;
196
- if (typeof value === "function") return void 0;
197
- return value;
198
- }));
349
+ }, collapseReplacer));
199
350
  },
200
351
  patchCacheKey: (i, key) => {
201
352
  const cmd = commands[i];
@@ -1853,4 +2004,4 @@ function requireLayoutEngine() {
1853
2004
  return engine;
1854
2005
  }
1855
2006
  //#endregion
1856
- export { fallbackMeasurer as A, IDENTITY as B, validateHachure as C, MEASURE_QUANTUM_PX as D, resolveAnchor as E, FilterValidationError as F, multiply as G, fromTRS as H, createDisplayListBuilder as I, filtersToCanvasFilter as L, segmentGraphemes as M, segmentWords as N, breakLines as O, setDefaultMeasurer as P, glow as R, sketchStrokes as S, Node as T, invert as U, applyToPoint as V, matEquals as W, arcLength as _, Circle as a, resolveSketch as b, ImageNode as c, Text as d, Video as f, SketchValidationError as g, roundedRectSegs as h, setLayoutEngine as i, quantize as j, estimatingMeasurer as k, Path as l, revealSchedule as m, getLayoutEngine as n, Custom as o, pathFromSegs as p, requireLayoutEngine as r, Group as s, LayoutEngineMissingError as t, Rect as u, flatten as v, validateSketch as w, roughen as x, hachureLines as y, validateFilters as z };
2007
+ export { fallbackMeasurer as A, DL_SNAPSHOT_VERSION as B, validateHachure as C, MEASURE_QUANTUM_PX as D, resolveAnchor as E, FilterValidationError as F, parseDisplaySnapshot as G, collapseReplacer as H, createDisplayListBuilder as I, applyToPoint as J, serializeDisplayList as K, filtersToCanvasFilter as L, segmentGraphemes as M, segmentWords as N, breakLines as O, setDefaultMeasurer as P, multiply as Q, glow as R, sketchStrokes as S, Node as T, diffDisplayLists as U, DlSnapshotError as V, formatDisplayDiff as W, invert as X, fromTRS as Y, matEquals as Z, arcLength as _, Circle as a, resolveSketch as b, ImageNode as c, Text as d, Video as f, SketchValidationError as g, roundedRectSegs as h, setLayoutEngine as i, quantize as j, estimatingMeasurer as k, Path as l, revealSchedule as m, getLayoutEngine as n, Custom as o, pathFromSegs as p, IDENTITY as q, requireLayoutEngine as r, Group as s, LayoutEngineMissingError as t, Rect as u, flatten as v, validateSketch as w, roughen as x, hachureLines as y, validateFilters as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/scene",
3
- "version": "0.11.0",
3
+ "version": "0.12.0-pre.0",
4
4
  "description": "glissade scene graph: nodes, transforms, DisplayList emission. Renderer-agnostic; zero DOM/Node dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "yoga-layout": "^3.2.1",
26
- "@glissade/core": "0.11.0"
26
+ "@glissade/core": "0.12.0-pre.0"
27
27
  },
28
28
  "repository": {
29
29
  "type": "git",