@glissade/scene 0.13.0 → 0.14.0-pre.1

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
@@ -653,6 +653,15 @@ declare function tokenHighlight(text: Text, props: Omit<TokenHighlightProps, 'te
653
653
  //#region src/fontUsage.d.ts
654
654
  /** Walk `scene` for Text nodes; one usage per node carrying its full text. */
655
655
  declare function collectTextUsages(scene: Scene): FontUsage[];
656
+ /**
657
+ * Collect font usages from the POST-localize document's STRING tracks (FIX 3,
658
+ * 0.14 canary). For every `'string'` track whose target node is a Text node,
659
+ * emit one usage per distinct localized KEY VALUE under that node's fontFamily —
660
+ * so a localized CJK message bound to a Latin-only font surfaces as an uncovered
661
+ * glyph. `collectTextUsages` only sees the authored BASE `node.text()`, which is
662
+ * resolved BEFORE the localized string tracks bind, so it misses this.
663
+ */
664
+ declare function collectLocalizedTextUsages(scene: Scene, doc: Timeline): FontUsage[];
656
665
  /**
657
666
  * Caller-supplied I/O: fetch the raw bytes for a font face URL (the export
658
667
  * paths read a file / fetch a URL; this keeps core pure). Returning undefined
@@ -664,6 +673,13 @@ interface ValidateSceneFontsOptions {
664
673
  mode?: FontMode;
665
674
  /** OS-installed families to treat as registered (case-insensitive). */
666
675
  osFamilies?: ReadonlySet<string> | undefined;
676
+ /**
677
+ * Additional usages to validate alongside the scene's authored Text (FIX 3):
678
+ * the POST-localize document's localized string-track values, which the
679
+ * scene-walk can't see (they bind AFTER `node.text()` is read). Build them
680
+ * with `collectLocalizedTextUsages(scene, localizedDoc)`.
681
+ */
682
+ extraUsages?: readonly FontUsage[] | undefined;
667
683
  }
668
684
  /**
669
685
  * Run §3.6 font validation for a scene + its timeline document. Builds the
@@ -876,4 +892,4 @@ declare function meshRasterSize(bw: number, bh: number): {
876
892
  h: number;
877
893
  };
878
894
  //#endregion
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 };
895
+ 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, collectLocalizedTextUsages, 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
@@ -48,8 +48,8 @@ var Highlight = class extends Node {
48
48
  this.progress = init$2(signal(1), props.progress);
49
49
  this.padding = props.padding ?? [4, 2];
50
50
  this.cornerRadius = props.cornerRadius ?? 4;
51
- this.registerTarget("progress", this.progress);
52
- this.registerTarget("color", this.color);
51
+ this.registerTarget("progress", this.progress, "number");
52
+ this.registerTarget("color", this.color, "color");
53
53
  }
54
54
  draw(out, ctx) {
55
55
  const progress = Math.min(1, Math.max(0, this.progress()));
@@ -132,7 +132,7 @@ var TextCursor = class extends Node {
132
132
  this.solidWhileTyping = props.solidWhileTyping ?? true;
133
133
  this.caretWidth = props.width ?? 2;
134
134
  this.fill = init$1(signal(""), props.fill);
135
- this.registerTarget("fill", this.fill);
135
+ this.registerTarget("fill", this.fill, "color");
136
136
  }
137
137
  draw(out, ctx) {
138
138
  const head = this.target.revealHead(ctx.measurer);
@@ -288,6 +288,14 @@ function mix(seed, i) {
288
288
  h = Math.imul(h ^ h >>> 15, 1935289751);
289
289
  return (h ^ h >>> 15) >>> 0;
290
290
  }
291
+ /**
292
+ * Salt folded into the motion-jitter seed so the per-index jitter rng
293
+ * decorrelates from `ctx.rng` (the factory rng). Both axes derive from
294
+ * `mix(baseSeed, i)`; without a distinct salt they would be the SAME stream,
295
+ * so a factory that draws from `ctx.rng` and a `jitter` callback would see
296
+ * correlated "independent" randomness. An arbitrary fixed odd constant.
297
+ */
298
+ const JITTER_SALT = 1779033703;
291
299
  /** Resolve a built-in layout (or call the escape-hatch fn) to a fraction. */
292
300
  function placeAt(layout, i, n) {
293
301
  if (typeof layout === "function") return layout(i, n);
@@ -394,7 +402,7 @@ function each(n, factory, opts) {
394
402
  const start = m.startSec ?? 0;
395
403
  const at = staggerFn(m, n);
396
404
  for (let i = 0; i < n; i++) {
397
- const rngI = random(mix(baseSeed, i));
405
+ const rngI = random(mix(mix(baseSeed, i), JITTER_SALT));
398
406
  const overrides = m.jitter?.(i, rngI, n);
399
407
  const applyOpts = {
400
408
  ...overrides !== void 0 ? { overrides } : {},
@@ -815,7 +823,7 @@ var FollowPath = class extends Node {
815
823
  this.progress = signal(1);
816
824
  if (typeof props.progress === "function") this.progress.bindSource(props.progress);
817
825
  else if (props.progress !== void 0) this.progress.set(props.progress);
818
- this.registerTarget("progress", this.progress);
826
+ this.registerTarget("progress", this.progress, "number");
819
827
  const sOpts = props.samplesPerSegment !== void 0 ? { samplesPerSegment: props.samplesPerSegment } : {};
820
828
  const getPath = props.path instanceof Path ? () => props.path.data() : () => props.path;
821
829
  let cachedPath = getPath();
@@ -919,13 +927,13 @@ var TokenHighlight = class extends Node {
919
927
  run,
920
928
  bound: runText(boxes, run)
921
929
  };
922
- this.registerTarget(`${id}/fill`, r.fill);
923
- this.registerTarget(`${id}/opacity`, r.opacity);
924
- this.registerTarget(`${id}/progress`, r.progress);
925
- this.registerTarget(`${id}/scale`, r.scale);
926
- this.registerTarget(`${id}/offset`, r.offset);
927
- this.registerTarget(`${id}/offset.x`, r.offset.x);
928
- this.registerTarget(`${id}/offset.y`, r.offset.y);
930
+ this.registerTarget(`${id}/fill`, r.fill, "color");
931
+ this.registerTarget(`${id}/opacity`, r.opacity, "number");
932
+ this.registerTarget(`${id}/progress`, r.progress, "number");
933
+ this.registerTarget(`${id}/scale`, r.scale, "number");
934
+ this.registerTarget(`${id}/offset`, r.offset, ["vec2", "vec2-arc"]);
935
+ this.registerTarget(`${id}/offset.x`, r.offset.x, "number");
936
+ this.registerTarget(`${id}/offset.y`, r.offset.y, "number");
929
937
  return r;
930
938
  });
931
939
  }
@@ -1065,6 +1073,35 @@ function collectTextUsages(scene) {
1065
1073
  visit(scene.root);
1066
1074
  return out;
1067
1075
  }
1076
+ /** The node-id of a track target ('<nodeId>/<prop.path>' → '<nodeId>'). */
1077
+ function nodeIdOf(target) {
1078
+ const slash = target.indexOf("/");
1079
+ return slash >= 0 ? target.slice(0, slash) : target;
1080
+ }
1081
+ /**
1082
+ * Collect font usages from the POST-localize document's STRING tracks (FIX 3,
1083
+ * 0.14 canary). For every `'string'` track whose target node is a Text node,
1084
+ * emit one usage per distinct localized KEY VALUE under that node's fontFamily —
1085
+ * so a localized CJK message bound to a Latin-only font surfaces as an uncovered
1086
+ * glyph. `collectTextUsages` only sees the authored BASE `node.text()`, which is
1087
+ * resolved BEFORE the localized string tracks bind, so it misses this.
1088
+ */
1089
+ function collectLocalizedTextUsages(scene, doc) {
1090
+ const out = [];
1091
+ for (const tr of doc.tracks) {
1092
+ if (tr.type !== "string") continue;
1093
+ const node = scene.nodes.get(nodeIdOf(tr.target));
1094
+ if (!(node instanceof Text)) continue;
1095
+ for (const k of tr.keys) {
1096
+ const value = k.value;
1097
+ if (typeof value === "string" && value) out.push({
1098
+ family: node.fontFamily,
1099
+ text: value
1100
+ });
1101
+ }
1102
+ }
1103
+ return out;
1104
+ }
1068
1105
  /**
1069
1106
  * Run §3.6 font validation for a scene + its timeline document. Builds the
1070
1107
  * registry from `doc.assets`, loads each registered face's cmap via `loadBytes`
@@ -1074,7 +1111,7 @@ function collectTextUsages(scene) {
1074
1111
  async function validateSceneFonts(scene, doc, loadBytes, options = {}) {
1075
1112
  const mode = options.mode ?? "dev";
1076
1113
  const registry = buildFontRegistry(doc.assets);
1077
- const usages = collectTextUsages(scene);
1114
+ const usages = [...collectTextUsages(scene), ...options.extraUsages ?? []];
1078
1115
  const wanted = /* @__PURE__ */ new Set();
1079
1116
  for (const u of usages) if (registry.has(u.family)) for (const f of registry.fallbackChain(u.family)) wanted.add(f);
1080
1117
  const cmaps = /* @__PURE__ */ new Map();
@@ -1121,7 +1158,7 @@ var ShaderEffect = class extends Group {
1121
1158
  for (const [name, value] of Object.entries(props.uniforms ?? {})) {
1122
1159
  const sig = signal(value);
1123
1160
  map.set(name, sig);
1124
- this.registerTarget(`u.${name}`, sig);
1161
+ this.registerTarget(`u.${name}`, sig, "number");
1125
1162
  }
1126
1163
  this.uniformSignals = map;
1127
1164
  }
@@ -1813,4 +1850,4 @@ var Raster2D = class {
1813
1850
  }
1814
1851
  };
1815
1852
  //#endregion
1816
- 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 };
1853
+ 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, collectLocalizedTextUsages, 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
@@ -53,10 +53,10 @@ var Layout = class extends Group {
53
53
  this.direction = props.direction ?? "row";
54
54
  this.justify = props.justify ?? "start";
55
55
  this.align = props.align ?? "center";
56
- this.registerTarget("width", this.width);
57
- this.registerTarget("height", this.height);
58
- this.registerTarget("gap", this.gap);
59
- this.registerTarget("padding", this.padding);
56
+ this.registerTarget("width", this.width, "number");
57
+ this.registerTarget("height", this.height, "number");
58
+ this.registerTarget("gap", this.gap, "number");
59
+ this.registerTarget("padding", this.padding, "number");
60
60
  }
61
61
  intrinsicSize(measurer) {
62
62
  if (!this.autoWidth && !this.autoHeight) return {
@@ -1,4 +1,4 @@
1
- import { BindableSignal, MeshInterpolation, MeshPaint as MeshPaint$1, MeshPoint, Paint, PathValue, ReadonlySignal, Rng, Track, Vec2, Vec2Signal } from "@glissade/core";
1
+ import { BindableSignal, MeshInterpolation, MeshPaint as MeshPaint$1, MeshPoint, Paint, PathValue, ReadonlySignal, Rng, Track, ValueTypeId, Vec2, Vec2Signal } from "@glissade/core";
2
2
 
3
3
  //#region src/matrix.d.ts
4
4
 
@@ -297,6 +297,13 @@ interface NodeProps {
297
297
  interface BindablePropTarget {
298
298
  bindSource(fn: () => unknown): void;
299
299
  unbindSource(): void;
300
+ /**
301
+ * The value type(s) this prop accepts — bindTimeline hard-throws a mismatched
302
+ * track (§2.2). An array for a polymorphic prop (a Shape `fill` is color|paint,
303
+ * a vec2 prop is vec2|vec2-arc). UNDEFINED for an untagged target (the 2-arg
304
+ * registerTarget form): bindTimeline skips the guard (0.13 back-compat seam).
305
+ */
306
+ readonly expects: ValueTypeId | readonly ValueTypeId[] | undefined;
300
307
  }
301
308
  /** Node-local hit-shape override (v2 §C.3) — fat targets for thin strokes. */
302
309
  type HitArea = {
@@ -345,7 +352,22 @@ declare abstract class Node {
345
352
  /** Track-target paths → bindable signals; subclasses register their own props. */
346
353
  protected readonly targets: Map<string, BindablePropTarget>;
347
354
  constructor(props?: NodeProps);
348
- protected registerTarget(path: string, sig: BindablePropTarget): void;
355
+ /**
356
+ * Register a track-target path → bindable signal, stamping the value type the
357
+ * signal accepts (§2.2). The stamp is what bindTimeline's bind-time guard
358
+ * reads to reject a mismatched track (a scalar on a vec2, a number on a paint
359
+ * prop, …) instead of silently sampling to NaN/undefined.
360
+ *
361
+ * `expects` is OPTIONAL: omitting it (the 2-arg form) leaves the target
362
+ * UNtagged — bindTimeline then skips the type guard for it, which is the
363
+ * back-compat seam for external `Custom`/`Node` subclasses (DESIGN.md §329)
364
+ * and prebuilt 0.13 nodes that called the 2-arg form (0.13 had no guard). A
365
+ * built-in node opts INTO the guard by tagging.
366
+ */
367
+ protected registerTarget(path: string, sig: {
368
+ bindSource(fn: () => unknown): void;
369
+ unbindSource(): void;
370
+ }, expects?: ValueTypeId | readonly ValueTypeId[]): void;
349
371
  resolveTarget(path: string): BindablePropTarget | undefined;
350
372
  /** Subclass drawing: emit own commands (and children for containers). */
351
373
  protected abstract draw(out: DisplayListBuilder, ctx: EvalContext): void;
@@ -609,6 +631,8 @@ interface ImageProps extends NodeProps {
609
631
  height?: PropInit<number>;
610
632
  }
611
633
  declare class ImageNode extends Node {
634
+ /** Marks this node as referencing a kind 'image' timeline asset (§2.3). */
635
+ static readonly assetKind: "image";
612
636
  readonly assetId: string;
613
637
  readonly width: BindableSignal<number>;
614
638
  readonly height: BindableSignal<number>;
@@ -644,6 +668,8 @@ interface VideoProps extends NodeProps {
644
668
  * and references the exact source-grid frame; backends resolve it.
645
669
  */
646
670
  declare class Video extends Node {
671
+ /** Marks this node as referencing a kind 'video' timeline asset (§3.8). */
672
+ static readonly assetKind: "video";
647
673
  readonly assetId: string;
648
674
  readonly at: number;
649
675
  readonly trimStart: number;
@@ -560,17 +560,30 @@ var Node = class {
560
560
  ]);
561
561
  }, { equals: matEquals });
562
562
  this.worldMatrix = computed(() => this.parent ? multiply(this.parent.worldMatrix(), this.localMatrix()) : this.localMatrix(), { equals: matEquals });
563
- this.registerTarget("position", this.position);
564
- this.registerTarget("position.x", this.position.x);
565
- this.registerTarget("position.y", this.position.y);
566
- this.registerTarget("rotation", this.rotation);
567
- this.registerTarget("scale", this.scale);
568
- this.registerTarget("scale.x", this.scale.x);
569
- this.registerTarget("scale.y", this.scale.y);
570
- this.registerTarget("opacity", this.opacity);
571
- this.registerTarget("zIndex", this.zIndex);
572
- }
573
- registerTarget(path, sig) {
563
+ this.registerTarget("position", this.position, ["vec2", "vec2-arc"]);
564
+ this.registerTarget("position.x", this.position.x, "number");
565
+ this.registerTarget("position.y", this.position.y, "number");
566
+ this.registerTarget("rotation", this.rotation, "number");
567
+ this.registerTarget("scale", this.scale, ["vec2", "vec2-arc"]);
568
+ this.registerTarget("scale.x", this.scale.x, "number");
569
+ this.registerTarget("scale.y", this.scale.y, "number");
570
+ this.registerTarget("opacity", this.opacity, "number");
571
+ this.registerTarget("zIndex", this.zIndex, "number");
572
+ }
573
+ /**
574
+ * Register a track-target path → bindable signal, stamping the value type the
575
+ * signal accepts (§2.2). The stamp is what bindTimeline's bind-time guard
576
+ * reads to reject a mismatched track (a scalar on a vec2, a number on a paint
577
+ * prop, …) instead of silently sampling to NaN/undefined.
578
+ *
579
+ * `expects` is OPTIONAL: omitting it (the 2-arg form) leaves the target
580
+ * UNtagged — bindTimeline then skips the type guard for it, which is the
581
+ * back-compat seam for external `Custom`/`Node` subclasses (DESIGN.md §329)
582
+ * and prebuilt 0.13 nodes that called the 2-arg form (0.13 had no guard). A
583
+ * built-in node opts INTO the guard by tagging.
584
+ */
585
+ registerTarget(path, sig, expects) {
586
+ sig.expects = expects;
574
587
  this.targets.set(path, sig);
575
588
  if (this.id !== void 0) sig[TARGET_PATH] = `${this.id}/${path}`;
576
589
  }
@@ -1141,10 +1154,10 @@ var Shape = class extends Node {
1141
1154
  this.stroke = initProp(signal(""), props.stroke);
1142
1155
  this.strokeWidth = initProp(signal(0), props.strokeWidth);
1143
1156
  this.reveal = initProp(signal(1), props.reveal);
1144
- this.registerTarget("fill", this.fill);
1145
- this.registerTarget("stroke", this.stroke);
1146
- this.registerTarget("strokeWidth", this.strokeWidth);
1147
- this.registerTarget("reveal", this.reveal);
1157
+ this.registerTarget("fill", this.fill, ["color", "paint"]);
1158
+ this.registerTarget("stroke", this.stroke, "color");
1159
+ this.registerTarget("strokeWidth", this.strokeWidth, "number");
1160
+ this.registerTarget("reveal", this.reveal, "number");
1148
1161
  if (props.sketch) validateSketch(props.sketch);
1149
1162
  if (props.sketchFill) validateHachure(props.sketchFill);
1150
1163
  if (props.sketchFill && !props.sketch) emitDevWarning(`${this.id !== void 0 ? `'${this.id}': ` : ""}sketchFill is ignored without sketch — hachure fill is drawn only by the sketch renderer. Set a sketch style (e.g. { kind: 'pencil' }) to see it.`);
@@ -1396,9 +1409,9 @@ var Rect = class extends Shape {
1396
1409
  this.width = initProp(signal(0), props.width);
1397
1410
  this.height = initProp(signal(0), props.height);
1398
1411
  this.cornerRadius = initProp(signal(0), props.cornerRadius);
1399
- this.registerTarget("width", this.width);
1400
- this.registerTarget("height", this.height);
1401
- this.registerTarget("cornerRadius", this.cornerRadius);
1412
+ this.registerTarget("width", this.width, "number");
1413
+ this.registerTarget("height", this.height, "number");
1414
+ this.registerTarget("cornerRadius", this.cornerRadius, "number");
1402
1415
  }
1403
1416
  intrinsicSize() {
1404
1417
  return {
@@ -1418,7 +1431,7 @@ var Circle = class extends Shape {
1418
1431
  constructor(props = {}) {
1419
1432
  super(props);
1420
1433
  this.radius = initProp(signal(0), props.radius);
1421
- this.registerTarget("radius", this.radius);
1434
+ this.registerTarget("radius", this.radius, "number");
1422
1435
  }
1423
1436
  intrinsicSize() {
1424
1437
  const d = this.radius() * 2;
@@ -1451,7 +1464,7 @@ var Path = class extends Shape {
1451
1464
  constructor(props = {}) {
1452
1465
  super(props);
1453
1466
  this.data = initProp(signal([]), props.data);
1454
- this.registerTarget("d", this.data);
1467
+ this.registerTarget("d", this.data, "path");
1455
1468
  }
1456
1469
  /** Control-point bounding box (conservative: contains the true curve). */
1457
1470
  bounds() {
@@ -1538,6 +1551,8 @@ var Path = class extends Shape {
1538
1551
  }
1539
1552
  };
1540
1553
  var ImageNode = class extends Node {
1554
+ /** Marks this node as referencing a kind 'image' timeline asset (§2.3). */
1555
+ static assetKind = "image";
1541
1556
  assetId;
1542
1557
  width;
1543
1558
  height;
@@ -1546,8 +1561,8 @@ var ImageNode = class extends Node {
1546
1561
  this.assetId = props.assetId;
1547
1562
  this.width = initProp(signal(0), props.width);
1548
1563
  this.height = initProp(signal(0), props.height);
1549
- this.registerTarget("width", this.width);
1550
- this.registerTarget("height", this.height);
1564
+ this.registerTarget("width", this.width, "number");
1565
+ this.registerTarget("height", this.height, "number");
1551
1566
  }
1552
1567
  intrinsicSize() {
1553
1568
  return {
@@ -1581,6 +1596,8 @@ var ImageNode = class extends Node {
1581
1596
  * and references the exact source-grid frame; backends resolve it.
1582
1597
  */
1583
1598
  var Video = class extends Node {
1599
+ /** Marks this node as referencing a kind 'video' timeline asset (§3.8). */
1600
+ static assetKind = "video";
1584
1601
  assetId;
1585
1602
  at;
1586
1603
  trimStart;
@@ -1599,8 +1616,8 @@ var Video = class extends Node {
1599
1616
  this.sourceFps = props.sourceFps;
1600
1617
  this.width = initProp(signal(0), props.width);
1601
1618
  this.height = initProp(signal(0), props.height);
1602
- this.registerTarget("width", this.width);
1603
- this.registerTarget("height", this.height);
1619
+ this.registerTarget("width", this.width, "number");
1620
+ this.registerTarget("height", this.height, "number");
1604
1621
  }
1605
1622
  /** Frame-indexed media time for timeline time t; null when outside the clip. */
1606
1623
  mediaTime(t) {
@@ -1657,11 +1674,11 @@ var Text = class extends Node {
1657
1674
  this.width = initProp(signal(0), props.width);
1658
1675
  this.lineHeight = props.lineHeight ?? 1.25;
1659
1676
  this.reveal = initProp(signal(Number.POSITIVE_INFINITY), props.reveal);
1660
- this.registerTarget("width", this.width);
1661
- this.registerTarget("text", this.text);
1662
- this.registerTarget("fill", this.fill);
1663
- this.registerTarget("fontSize", this.fontSize);
1664
- this.registerTarget("reveal", this.reveal);
1677
+ this.registerTarget("width", this.width, "number");
1678
+ this.registerTarget("text", this.text, "string");
1679
+ this.registerTarget("fill", this.fill, "color");
1680
+ this.registerTarget("fontSize", this.fontSize, "number");
1681
+ this.registerTarget("reveal", this.reveal, "number");
1665
1682
  }
1666
1683
  intrinsicSize(measurer) {
1667
1684
  const text = this.text();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/scene",
3
- "version": "0.13.0",
3
+ "version": "0.14.0-pre.1",
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.13.0"
26
+ "@glissade/core": "0.14.0-pre.1"
27
27
  },
28
28
  "repository": {
29
29
  "type": "git",