@glissade/scene 0.12.0 → 0.13.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,6 @@
1
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";
2
+ import { BindableSignal, BoundTimeline, CompiledTimeline, CoverageReport, EaseSpec, FontMode, FontUsage, MeshPaint as MeshPaint$1, PathValue, Playhead, Rng, Timeline, Track, Vec2 } from "@glissade/core";
3
+ import { ChannelOverride, Clip } from "@glissade/core/clips";
3
4
 
4
5
  //#region src/taxonomy.d.ts
5
6
 
@@ -137,6 +138,125 @@ declare function typewriter(target: string, edits: readonly TypeEdit[], opts?: {
137
138
  gap?: number;
138
139
  }): TypewriterResult;
139
140
  //#endregion
141
+ //#region src/each.d.ts
142
+ /** An aspect-fraction placement: [fx, fy], each conventionally in [0, 1]. */
143
+ type Place = readonly [number, number];
144
+ /**
145
+ * Built-in layouts (a discriminated union — NOT factory fns) plus the escape
146
+ * hatch `(i, n) => [fx, fy]`. Every built-in is PURE arithmetic in aspect
147
+ * fractions; mapping to px happens only when `box` is given (see `places`).
148
+ */
149
+ type EachLayout = {
150
+ kind: 'row';
151
+ gap?: number;
152
+ align?: number;
153
+ } | {
154
+ kind: 'column';
155
+ gap?: number;
156
+ align?: number;
157
+ } | {
158
+ kind: 'grid';
159
+ cols: number;
160
+ rows?: number;
161
+ gapX?: number;
162
+ gapY?: number;
163
+ order?: 'row' | 'column';
164
+ } | {
165
+ kind: 'ring';
166
+ radius?: number;
167
+ center?: Place;
168
+ startAngle?: number;
169
+ sweep?: number;
170
+ } | ((i: number, n: number) => Place);
171
+ /** How a `stagger` delay distributes across the clones. */
172
+ type EachDistribute = 'delay' | 'from-center' | 'from-edges';
173
+ /** Per-index motion: a clip fanned across the clones with stagger + jitter. */
174
+ interface EachMotion {
175
+ /** The motion clip applied to every clone (TYPE: `Clip` from core/clips). */
176
+ clip: Clip;
177
+ /** Wall-clock start second of the first clone. Default 0. */
178
+ startSec?: number;
179
+ /** Per-index delay (seconds) or a function of the index. Default 0. */
180
+ stagger?: number | ((i: number) => number);
181
+ /**
182
+ * Shape a numeric `stagger` gap into a distribution. `from-center` ramps the
183
+ * delay outward from the middle clone, `from-edges` inward toward it; `delay`
184
+ * (the default) is the plain `i * gap` ramp. Ignored when `stagger` is a fn.
185
+ */
186
+ distribute?: EachDistribute;
187
+ /** Per-index clip overrides, seeded — `(i, rng, n) => overrides`. */
188
+ jitter?: (i: number, rng: Rng, n: number) => Record<string, ChannelOverride>;
189
+ /** Clip speed (passed straight to `clip.apply`). */
190
+ speed?: number;
191
+ }
192
+ /** Pixel box for mapping aspect-fraction places to a concrete coordinate frame. */
193
+ interface EachBox {
194
+ w: number;
195
+ h: number;
196
+ /** Top-left of the box in scene coords; default [0, 0]. */
197
+ origin?: Place;
198
+ }
199
+ interface EachOpts {
200
+ /** Stable id prefix; clones are `${id}/${i}`, the wrapping group is `${id}`. */
201
+ id: string;
202
+ layout: EachLayout;
203
+ motion?: EachMotion;
204
+ /** Seed for per-clone RNG; defaults to a stable hash of `id`. */
205
+ seed?: number;
206
+ /** When given, `places` also carries the px-mapped points (see EachResult). */
207
+ box?: EachBox;
208
+ }
209
+ /** The per-clone authoring context handed to the factory. */
210
+ interface EachContext {
211
+ /** Clone index, 0..n-1. */
212
+ i: number;
213
+ /** Total clone count. */
214
+ n: number;
215
+ /** This clone's id (`${opts.id}/${i}`). */
216
+ id: string;
217
+ /** Aspect-fraction placement [fx, fy] — ALWAYS a fraction (px is separate). */
218
+ place: Place;
219
+ /** Seeded generator for this clone: `random(mix(seed, i))`. */
220
+ rng: Rng;
221
+ /** The resolved base seed (`opts.seed ?? hash(id)`). */
222
+ seed: number;
223
+ }
224
+ interface EachResult {
225
+ /** The wrapping group (`id: opts.id`) holding every generated child. */
226
+ node: Group;
227
+ /** The generated children, in index order. */
228
+ children: Node[];
229
+ /** The compiled motion tracks (empty when no `motion`). */
230
+ tracks: Track[];
231
+ /** Max child clip end (== startSec when no motion). */
232
+ end: number;
233
+ /**
234
+ * Per-clone placement. `frac` is the aspect-fraction [fx, fy] every layout
235
+ * produces; `px` is present only when `opts.box` was given (frac mapped into
236
+ * the box). Authoring a `box` once here is the single place fraction→px lives.
237
+ */
238
+ places: {
239
+ frac: Place;
240
+ px?: Place;
241
+ }[];
242
+ }
243
+ declare class EachError extends Error {
244
+ constructor(message: string);
245
+ }
246
+ /**
247
+ * Generate `n` clones from `factory`, lay them out, and (optionally) stagger a
248
+ * motion clip across them.
249
+ *
250
+ * const grid = each(9, (i) => new Rect({ width: 40, height: 40, fill: '#9ef0c0' }), {
251
+ * id: 'card',
252
+ * layout: { kind: 'grid', cols: 3 },
253
+ * box: { w: 600, h: 360 },
254
+ * motion: { clip: popIn(), stagger: 0.08, distribute: 'from-center' },
255
+ * });
256
+ * // scene children: [grid.node]; timeline tracks: [...grid.tracks]
257
+ */
258
+ declare function each(n: number, factory: (i: number, ctx: EachContext) => Node, opts: EachOpts): EachResult;
259
+ //#endregion
140
260
  //#region src/drawOn.d.ts
141
261
  interface DrawOnOptions {
142
262
  /** when the stroke-on starts, seconds; default 0 */
@@ -192,12 +312,21 @@ declare function withDeterminismGuards<T>(mode: GuardMode, fn: () => T): T;
192
312
  * - ArrayBuffer / typed-array views collapse to a `ab:<len>` / `view:<len>`
193
313
  * length marker (opaque binary never belongs in a structural key).
194
314
  * - Functions drop (JSON would already drop them; explicit for parity).
315
+ * - Non-finite numbers (`NaN`/`Infinity`/`-Infinity`) collapse to DISTINCT
316
+ * string sentinels. `JSON.stringify` natively serializes all three to the
317
+ * SAME token (`null`), which would collide the cacheKey of two DisplayLists
318
+ * that differ only in WHICH non-finite value reaches a draw field — a stale
319
+ * raster + an `auditCacheCold` false-OK. The distinct sentinels keep them
320
+ * apart. This does NOT touch FINITE numbers (the common path): only the
321
+ * three non-finite inputs change, so the §3.5 cacheKey bytes for every real
322
+ * (finite) list are byte-identical — pinned by the regression guard.
195
323
  *
196
324
  * NOTE: `-0` is intentionally NOT normalized here. The matrix layer
197
325
  * (`matrix.ts`) already normalizes `-0 → 0` at the source, and adding a `-0`
198
326
  * 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.
327
+ * carried a raw `-0` — silently invalidating the cache cluster-wide. (`-0` is
328
+ * finite, so the non-finite branch never touches it.) Byte preservation wins;
329
+ * the regression guard pins the exact key.
201
330
  */
202
331
  declare function collapseReplacer(_key: string, value: unknown): unknown;
203
332
  /**
@@ -747,4 +876,4 @@ declare function meshRasterSize(bw: number, bh: number): {
747
876
  h: number;
748
877
  };
749
878
  //#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 };
879
+ 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 EachBox, type EachContext, type EachDistribute, EachError, type EachLayout, type EachMotion, type EachOpts, type EachResult, 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 Place, 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, each, 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 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";
1
+ import { $ as multiply, A as estimatingMeasurer, B as validateFilters, C as sketchStrokes, D as resolveAnchor, E as Node, F as setDefaultMeasurer, G as formatDisplayDiff, H as DlSnapshotError, I as FilterValidationError, J as IDENTITY, K as parseDisplaySnapshot, L as createDisplayListBuilder, M as quantize, N as segmentGraphemes, O as MEASURE_QUANTUM_PX, P as segmentWords, Q as matEquals, R as filtersToCanvasFilter, S as roughen, T as validateSketch, U as collapseReplacer, V as DL_SNAPSHOT_VERSION, W as diffDisplayLists, X as fromTRS, Y as applyToPoint, Z as invert, _ as arcLength, a as Circle, b as hashStr, c as ImageNode, d as Text, f as Video, g as SketchValidationError, h as roundedRectSegs, i as setLayoutEngine, j as fallbackMeasurer, k as breakLines, l as Path, m as revealSchedule, n as getLayoutEngine, o as Custom, p as pathFromSegs, q as serializeDisplayList, r as requireLayoutEngine, s as Group, t as LayoutEngineMissingError, u as Rect, v as flatten, w as validateHachure, x as resolveSketch, y as hachureLines, z as glow } from "./layoutEngine.js";
2
+ import { bindTimeline, buildFontRegistry, compileTimeline, createPlayhead, emitDevWarning, evaluateAt, key, lerpColor, oklabToRgba, parseCmap, parseColor, random, 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.
@@ -257,6 +257,163 @@ function typewriter(target, edits, opts = {}) {
257
257
  };
258
258
  }
259
259
  //#endregion
260
+ //#region src/each.ts
261
+ /**
262
+ * `each()` — deterministic parametric instancing (0.13 clip-tier sugar). Pure
263
+ * BUILD-TIME fan-out: it generates N scene nodes from a factory, lays them out
264
+ * in aspect-fraction space, and (optionally) staggers a motion `clip` across
265
+ * them — compiling to ordinary keyed `Track[]` plus a `Group` of children with
266
+ * stable `${id}/${i}` ids. Nothing executes at play time; the emitted tracks are
267
+ * byte-indistinguishable from hand-authored ones, so goldens hold by
268
+ * construction and every `--workers` export shard reconstructs the same id set.
269
+ *
270
+ * The clip runtime is imported TYPE-ONLY (the `Clip` instance the author passes
271
+ * carries its own `apply`), so `each` adds no clip bytes to the embed: the
272
+ * `@glissade/core/clips` runtime lands in the consumer's bundle, never scene's.
273
+ */
274
+ var EachError = class extends Error {
275
+ constructor(message) {
276
+ super(message);
277
+ this.name = "EachError";
278
+ }
279
+ };
280
+ /**
281
+ * Fold a base seed and an index into a fresh per-clone seed. splitmix-style
282
+ * avalanche so adjacent indices decorrelate (a bare `seed + i` would hand
283
+ * near-identical streams to neighbours).
284
+ */
285
+ function mix(seed, i) {
286
+ let h = (seed ^ Math.imul(i + 1, 2654435769)) >>> 0;
287
+ h = Math.imul(h ^ h >>> 16, 569420461);
288
+ h = Math.imul(h ^ h >>> 15, 1935289751);
289
+ return (h ^ h >>> 15) >>> 0;
290
+ }
291
+ /** Resolve a built-in layout (or call the escape-hatch fn) to a fraction. */
292
+ function placeAt(layout, i, n) {
293
+ if (typeof layout === "function") return layout(i, n);
294
+ switch (layout.kind) {
295
+ case "row": {
296
+ const gap = layout.gap ?? (n > 1 ? 1 / (n - 1) : 0);
297
+ const align = layout.align ?? .5;
298
+ const x0 = .5 - gap * (n - 1) / 2;
299
+ return [n === 1 ? .5 : x0 + gap * i, align];
300
+ }
301
+ case "column": {
302
+ const gap = layout.gap ?? (n > 1 ? 1 / (n - 1) : 0);
303
+ const align = layout.align ?? .5;
304
+ const y0 = .5 - gap * (n - 1) / 2;
305
+ return [align, n === 1 ? .5 : y0 + gap * i];
306
+ }
307
+ case "grid": {
308
+ const cols = layout.cols;
309
+ if (!(cols >= 1)) throw new EachError(`grid layout needs cols >= 1 (got ${cols})`);
310
+ const rows = layout.rows ?? Math.ceil(n / cols);
311
+ const order = layout.order ?? "row";
312
+ const col = order === "row" ? i % cols : Math.floor(i / rows);
313
+ const row = order === "row" ? Math.floor(i / cols) : i % rows;
314
+ const gapX = layout.gapX ?? (cols > 1 ? 1 / (cols - 1) : 0);
315
+ const gapY = layout.gapY ?? (rows > 1 ? 1 / (rows - 1) : 0);
316
+ const spanX = gapX * (cols - 1);
317
+ const spanY = gapY * (rows - 1);
318
+ return [cols === 1 ? .5 : .5 - spanX / 2 + gapX * col, rows === 1 ? .5 : .5 - spanY / 2 + gapY * row];
319
+ }
320
+ case "ring": {
321
+ const radius = layout.radius ?? .5;
322
+ const [cx, cy] = layout.center ?? [.5, .5];
323
+ const theta = (layout.startAngle ?? -Math.PI / 2) + (layout.sweep ?? Math.PI * 2) * (n === 0 ? 0 : i / n);
324
+ return [cx + radius * Math.cos(theta), cy + radius * Math.sin(theta)];
325
+ }
326
+ }
327
+ }
328
+ /** Compile a `distribute` mode + numeric gap into a stagger delay fn. */
329
+ function distributeFn(distribute, gap, n) {
330
+ const mid = (n - 1) / 2;
331
+ switch (distribute) {
332
+ case "from-center": return (i) => Math.abs(i - mid) * gap;
333
+ case "from-edges": return (i) => (mid - Math.abs(i - mid)) * gap;
334
+ case "delay": return (i) => i * gap;
335
+ }
336
+ }
337
+ /** Resolve the motion's per-index delay into a plain `(i) => seconds` fn. */
338
+ function staggerFn(motion, n) {
339
+ const s = motion.stagger ?? 0;
340
+ if (typeof s === "function") return s;
341
+ return distributeFn(motion.distribute ?? "delay", s, n);
342
+ }
343
+ /**
344
+ * Generate `n` clones from `factory`, lay them out, and (optionally) stagger a
345
+ * motion clip across them.
346
+ *
347
+ * const grid = each(9, (i) => new Rect({ width: 40, height: 40, fill: '#9ef0c0' }), {
348
+ * id: 'card',
349
+ * layout: { kind: 'grid', cols: 3 },
350
+ * box: { w: 600, h: 360 },
351
+ * motion: { clip: popIn(), stagger: 0.08, distribute: 'from-center' },
352
+ * });
353
+ * // scene children: [grid.node]; timeline tracks: [...grid.tracks]
354
+ */
355
+ function each(n, factory, opts) {
356
+ if (!Number.isInteger(n) || n < 0) throw new EachError(`each() count must be a non-negative integer (got ${n})`);
357
+ const baseSeed = (opts.seed ?? hashStr(opts.id)) >>> 0;
358
+ const box = opts.box;
359
+ const [ox, oy] = box?.origin ?? [0, 0];
360
+ const children = [];
361
+ const places = [];
362
+ const seen = /* @__PURE__ */ new Set();
363
+ for (let i = 0; i < n; i++) {
364
+ const id = `${opts.id}/${i}`;
365
+ const frac = placeAt(opts.layout, i, n);
366
+ const ctx = {
367
+ i,
368
+ n,
369
+ id,
370
+ place: frac,
371
+ rng: random(mix(baseSeed, i)),
372
+ seed: baseSeed
373
+ };
374
+ const child = factory(i, ctx);
375
+ if (!(child instanceof Node)) throw new EachError(`each() factory must return a Node for index ${i} (got ${typeof child})`);
376
+ if (seen.has(child)) throw new EachError(`each() factory returned the same Node instance for index ${i} — the factory must construct a new node per index (it is called once per clone)`);
377
+ seen.add(child);
378
+ if (child.id === void 0) child.id = id;
379
+ else if (child.id !== id) throw new EachError(`each() factory set id '${child.id}' on index ${i}, but each owns the id namespace — leave it unset so it becomes '${id}'`);
380
+ children.push(child);
381
+ places.push(box ? {
382
+ frac,
383
+ px: [ox + frac[0] * box.w, oy + frac[1] * box.h]
384
+ } : { frac });
385
+ }
386
+ const node = new Group({
387
+ id: opts.id,
388
+ children
389
+ });
390
+ const tracks = [];
391
+ let end = opts.motion?.startSec ?? 0;
392
+ if (opts.motion) {
393
+ const m = opts.motion;
394
+ const start = m.startSec ?? 0;
395
+ const at = staggerFn(m, n);
396
+ for (let i = 0; i < n; i++) {
397
+ const rngI = random(mix(baseSeed, i));
398
+ const overrides = m.jitter?.(i, rngI, n);
399
+ const applyOpts = {
400
+ ...overrides !== void 0 ? { overrides } : {},
401
+ ...m.speed !== void 0 ? { speed: m.speed } : {}
402
+ };
403
+ const r = m.clip.apply(`${opts.id}/${i}`, start + at(i), applyOpts);
404
+ tracks.push(...r.tracks);
405
+ if (r.end > end) end = r.end;
406
+ }
407
+ }
408
+ return {
409
+ node,
410
+ children,
411
+ tracks,
412
+ end,
413
+ places
414
+ };
415
+ }
416
+ //#endregion
260
417
  //#region src/drawOn.ts
261
418
  /**
262
419
  * Whiteboard kit: one-call "draw this shape on" tracks. A stroked or sketched
@@ -404,7 +561,7 @@ function createScene(init) {
404
561
  size: init.size,
405
562
  playhead,
406
563
  resolveTarget: (target) => {
407
- const slash = target.indexOf("/");
564
+ const slash = target.lastIndexOf("/");
408
565
  if (slash < 0) return void 0;
409
566
  return nodes.get(target.slice(0, slash))?.resolveTarget(target.slice(slash + 1));
410
567
  },
@@ -1655,4 +1812,4 @@ var Raster2D = class {
1655
1812
  }
1656
1813
  };
1657
1814
  //#endregion
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 };
1815
+ export { ALL_FILTER_KINDS, Circle, ColdAssetError, Custom, DL_SNAPSHOT_VERSION, DeterminismViolationError, DlSnapshotError, DuplicateNodeIdError, EachError, 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, each, 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/layout.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as fallbackMeasurer, i as setLayoutEngine, n as getLayoutEngine, r as requireLayoutEngine, s as Group, t as LayoutEngineMissingError } from "./layoutEngine.js";
1
+ import { i as setLayoutEngine, j as fallbackMeasurer, n as getLayoutEngine, r as requireLayoutEngine, s as Group, t as LayoutEngineMissingError } from "./layoutEngine.js";
2
2
  import { computed, signal } from "@glissade/core";
3
3
  //#region src/layout.ts
4
4
  /**
@@ -73,17 +73,30 @@ function matEquals(a, b) {
73
73
  * - ArrayBuffer / typed-array views collapse to a `ab:<len>` / `view:<len>`
74
74
  * length marker (opaque binary never belongs in a structural key).
75
75
  * - Functions drop (JSON would already drop them; explicit for parity).
76
+ * - Non-finite numbers (`NaN`/`Infinity`/`-Infinity`) collapse to DISTINCT
77
+ * string sentinels. `JSON.stringify` natively serializes all three to the
78
+ * SAME token (`null`), which would collide the cacheKey of two DisplayLists
79
+ * that differ only in WHICH non-finite value reaches a draw field — a stale
80
+ * raster + an `auditCacheCold` false-OK. The distinct sentinels keep them
81
+ * apart. This does NOT touch FINITE numbers (the common path): only the
82
+ * three non-finite inputs change, so the §3.5 cacheKey bytes for every real
83
+ * (finite) list are byte-identical — pinned by the regression guard.
76
84
  *
77
85
  * NOTE: `-0` is intentionally NOT normalized here. The matrix layer
78
86
  * (`matrix.ts`) already normalizes `-0 → 0` at the source, and adding a `-0`
79
87
  * 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.
88
+ * carried a raw `-0` — silently invalidating the cache cluster-wide. (`-0` is
89
+ * finite, so the non-finite branch never touches it.) Byte preservation wins;
90
+ * the regression guard pins the exact key.
82
91
  */
83
92
  function collapseReplacer(_key, value) {
84
93
  if (value instanceof ArrayBuffer) return `ab:${value.byteLength}`;
85
94
  if (ArrayBuffer.isView(value)) return `view:${value.byteLength}`;
86
95
  if (typeof value === "function") return void 0;
96
+ if (typeof value === "number" && !Number.isFinite(value)) {
97
+ if (Number.isNaN(value)) return "NaN";
98
+ return value > 0 ? "Infinity" : "-Infinity";
99
+ }
87
100
  return value;
88
101
  }
89
102
  /** A flat, stable JSON value for one command with its resource ids INLINED to content. */
@@ -2004,4 +2017,4 @@ function requireLayoutEngine() {
2004
2017
  return engine;
2005
2018
  }
2006
2019
  //#endregion
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 };
2020
+ export { multiply as $, estimatingMeasurer as A, validateFilters as B, sketchStrokes as C, resolveAnchor as D, Node as E, setDefaultMeasurer as F, formatDisplayDiff as G, DlSnapshotError as H, FilterValidationError as I, IDENTITY as J, parseDisplaySnapshot as K, createDisplayListBuilder as L, quantize as M, segmentGraphemes as N, MEASURE_QUANTUM_PX as O, segmentWords as P, matEquals as Q, filtersToCanvasFilter as R, roughen as S, validateSketch as T, collapseReplacer as U, DL_SNAPSHOT_VERSION as V, diffDisplayLists as W, fromTRS as X, applyToPoint as Y, invert as Z, arcLength as _, Circle as a, hashStr as b, ImageNode as c, Text as d, Video as f, SketchValidationError as g, roundedRectSegs as h, setLayoutEngine as i, fallbackMeasurer as j, breakLines as k, Path as l, revealSchedule as m, getLayoutEngine as n, Custom as o, pathFromSegs as p, serializeDisplayList as q, requireLayoutEngine as r, Group as s, LayoutEngineMissingError as t, Rect as u, flatten as v, validateHachure as w, resolveSketch as x, hachureLines as y, glow as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/scene",
3
- "version": "0.12.0",
3
+ "version": "0.13.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.12.0"
26
+ "@glissade/core": "0.13.0-pre.0"
27
27
  },
28
28
  "repository": {
29
29
  "type": "git",