@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 +168 -3
- package/dist/index.js +200 -13
- package/dist/layoutEngine.d.ts +2 -2
- package/dist/layoutEngine.js +158 -7
- package/package.json +2 -2
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1341
|
-
|
|
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 };
|
package/dist/layoutEngine.d.ts
CHANGED
|
@@ -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,
|
|
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 };
|
package/dist/layoutEngine.js
CHANGED
|
@@ -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
|
-
},
|
|
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,
|
|
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.
|
|
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.
|
|
26
|
+
"@glissade/core": "0.12.0-pre.0"
|
|
27
27
|
},
|
|
28
28
|
"repository": {
|
|
29
29
|
"type": "git",
|