@glissade/scene 0.19.1 → 0.20.0-pre.3

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.
@@ -0,0 +1,71 @@
1
+ import { n as DisplayList } from "./displayList.js";
2
+ import { I as BindablePropTarget, W as TextMeasurer, i as Group, z as Node } from "./nodes.js";
3
+ import { BoundTimeline, CompiledTimeline, Playhead, Timeline } from "@glissade/core";
4
+
5
+ //#region src/scene.d.ts
6
+
7
+ interface Scene {
8
+ readonly root: Group;
9
+ readonly nodes: ReadonlyMap<string, Node>;
10
+ readonly size: {
11
+ w: number;
12
+ h: number;
13
+ };
14
+ /** Per-scene playhead; players and evaluate() write it. */
15
+ readonly playhead: Playhead;
16
+ resolveTarget(target: string): BindablePropTarget | undefined;
17
+ /**
18
+ * Inject the active backend's TextMeasurer (§3.2) so line breaking always
19
+ * measures with the rasterizer that will draw. Defaults to an estimator.
20
+ */
21
+ setTextMeasurer(measurer: TextMeasurer): void;
22
+ readonly textMeasurer: TextMeasurer;
23
+ }
24
+ declare class DuplicateNodeIdError extends Error {
25
+ constructor(id: string);
26
+ }
27
+ declare class ReservedNodeIdError extends Error {
28
+ constructor(id: string);
29
+ }
30
+ interface SceneInit {
31
+ size: {
32
+ w: number;
33
+ h: number;
34
+ };
35
+ children: Node[];
36
+ }
37
+ /**
38
+ * The scene-module convention: what `gs render`, the golden harness, and the
39
+ * studio load. createScene is a factory — every consumer gets a fresh graph.
40
+ */
41
+ interface SceneModule {
42
+ createScene(): Scene;
43
+ timeline: Timeline;
44
+ }
45
+ declare function createScene(init: SceneInit): Scene;
46
+ interface BindingCacheEntry {
47
+ compiled: CompiledTimeline;
48
+ bound: BoundTimeline;
49
+ }
50
+ declare function bindScene(scene: Scene, doc: Timeline): BindingCacheEntry;
51
+ /**
52
+ * The non-negotiable contract (§2.5): same (scene, timeline, t) → identical
53
+ * DisplayList, in any call order. Never awaits; asset readiness is the
54
+ * caller's precondition.
55
+ */
56
+ declare function evaluate(scene: Scene, doc: Timeline, t: number): DisplayList;
57
+ /**
58
+ * Controlled / imperative drive (0.19): `evaluate(scene)` with NO timeline —
59
+ * the host owns the clock. It evaluates against an EMPTY timeline at the
60
+ * scene's current playhead value (`peek()`, 0 by default), so values set
61
+ * imperatively via `node.set(...)` between frames survive into the DisplayList
62
+ * (no track clobbers them). See docs/controlled-drive.md for the loop and the
63
+ * `.set()`-vs-timeline precedence contract.
64
+ *
65
+ * PRECEDENCE: a timeline track ALWAYS overrides `.set()` on the prop it
66
+ * targets while that track is live — pass the timeline through the 3-arg form
67
+ * for animated props; reserve this overload for host-owned props.
68
+ */
69
+ declare function evaluate(scene: Scene): DisplayList;
70
+ //#endregion
71
+ export { SceneModule as a, evaluate as c, SceneInit as i, ReservedNodeIdError as n, bindScene as o, Scene as r, createScene as s, DuplicateNodeIdError as t };
package/dist/scene.js ADDED
@@ -0,0 +1,120 @@
1
+ import { L as createDisplayListBuilder, r as Group, y as fallbackMeasurer } from "./nodes.js";
2
+ import { r as isConstructionProp } from "./constructionProps.js";
3
+ import { bindTimeline, compileTimeline, createPlayhead, evaluateAt, signal } from "@glissade/core";
4
+ //#region src/scene.ts
5
+ /**
6
+ * Scene assembly + the canonical evaluate(scene, timeline, t) (DESIGN.md §2.5,
7
+ * §3.1): pure, total, deterministic — driver writes the playhead (sanctioned
8
+ * entry write), then a pull-only read phase collects the DisplayList.
9
+ */
10
+ var DuplicateNodeIdError = class extends Error {
11
+ constructor(id) {
12
+ super(`duplicate node id '${id}' — explicit ids must be unique (§3.1/§6.5)`);
13
+ this.name = "DuplicateNodeIdError";
14
+ }
15
+ };
16
+ var ReservedNodeIdError = class extends Error {
17
+ constructor(id) {
18
+ super(`node id '${id}' uses the reserved '~' prefix — that namespace is for structural fallback ids (§6.5), which are inspection-only and never track targets; choose another id`);
19
+ this.name = "ReservedNodeIdError";
20
+ }
21
+ };
22
+ function indexNodes(root, into, measurerSource) {
23
+ root.measurerSource = measurerSource;
24
+ if (root.id !== void 0) {
25
+ if (root.id.startsWith("~")) throw new ReservedNodeIdError(root.id);
26
+ if (into.has(root.id)) throw new DuplicateNodeIdError(root.id);
27
+ into.set(root.id, root);
28
+ }
29
+ if (root instanceof Group) for (const child of root.children) indexNodes(child, into, measurerSource);
30
+ }
31
+ function createScene(init) {
32
+ const root = new Group({
33
+ id: "__root",
34
+ children: init.children
35
+ });
36
+ const nodes = /* @__PURE__ */ new Map();
37
+ const playhead = createPlayhead();
38
+ const measurer = signal(null);
39
+ indexNodes(root, nodes, () => measurer() ?? fallbackMeasurer());
40
+ return {
41
+ root,
42
+ nodes,
43
+ size: init.size,
44
+ playhead,
45
+ resolveTarget: (target) => {
46
+ for (let slash = target.lastIndexOf("/"); slash > 0; slash = target.lastIndexOf("/", slash - 1)) {
47
+ const node = nodes.get(target.slice(0, slash));
48
+ if (node) return node.resolveTarget(target.slice(slash + 1));
49
+ }
50
+ },
51
+ setTextMeasurer: (m) => {
52
+ measurer.set(m);
53
+ },
54
+ get textMeasurer() {
55
+ return measurer.peek() ?? fallbackMeasurer();
56
+ }
57
+ };
58
+ }
59
+ const bindings = /* @__PURE__ */ new WeakMap();
60
+ /**
61
+ * When a target fails to resolve, return a friendlier reason IF it names a
62
+ * known CONSTRUCTION prop (animatable: false — e.g. `assetId`, `fontFamily`):
63
+ * the target IS correctly rejected, but "no signal resolves to it" hides WHY.
64
+ * Resolves the node by the same longest-registered-id-prefix walk as
65
+ * `resolveTarget`, then checks the node's construction-prop schema. Returns
66
+ * undefined (⇒ the generic message) for a genuinely-unknown prop or unknown id.
67
+ */
68
+ function constructionPropMessage(nodes, target) {
69
+ for (let slash = target.lastIndexOf("/"); slash > 0; slash = target.lastIndexOf("/", slash - 1)) {
70
+ const node = nodes.get(target.slice(0, slash));
71
+ if (!node) continue;
72
+ const prop = target.slice(slash + 1);
73
+ if (isConstructionProp(node.describeType, prop)) return `'${target}' is a construction prop (animatable:false) — set it at construction (new ${node.describeType}({ ${prop} })); it is not an animatable target.`;
74
+ return;
75
+ }
76
+ }
77
+ function bindScene(scene, doc) {
78
+ let perScene = bindings.get(scene);
79
+ if (!perScene) {
80
+ perScene = /* @__PURE__ */ new WeakMap();
81
+ bindings.set(scene, perScene);
82
+ }
83
+ let entry = perScene.get(doc);
84
+ if (!entry) {
85
+ const compiled = compileTimeline(doc);
86
+ entry = {
87
+ compiled,
88
+ bound: bindTimeline(compiled, scene.resolveTarget, scene.playhead, { unboundMessage: (target) => constructionPropMessage(scene.nodes, target) })
89
+ };
90
+ perScene.set(doc, entry);
91
+ }
92
+ return entry;
93
+ }
94
+ /**
95
+ * Empty timeline — zero tracks, so binding installs ZERO computed sources and
96
+ * any imperative `node.set(...)` value survives evaluate untouched. Shared and
97
+ * frozen so the WeakMap binding cache keys on a single stable document across
98
+ * the whole controlled-drive loop (one bind, reused every frame).
99
+ */
100
+ const EMPTY_TIMELINE = Object.freeze({
101
+ version: 1,
102
+ tracks: []
103
+ });
104
+ function evaluate(scene, doc, t = 0) {
105
+ if (doc === void 0) return evaluate(scene, EMPTY_TIMELINE, scene.playhead.peek());
106
+ bindScene(scene, doc);
107
+ const fps = doc.fps;
108
+ const ctx = {
109
+ time: t,
110
+ frame: fps !== void 0 ? Math.round(t * fps) : -1,
111
+ measurer: scene.textMeasurer
112
+ };
113
+ return evaluateAt(scene.playhead, t, () => {
114
+ const out = createDisplayListBuilder(scene.size);
115
+ scene.root.emit(out, ctx);
116
+ return out.finish();
117
+ });
118
+ }
119
+ //#endregion
120
+ export { evaluate as a, createScene as i, ReservedNodeIdError as n, bindScene as r, DuplicateNodeIdError as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/scene",
3
- "version": "0.19.1",
3
+ "version": "0.20.0-pre.3",
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": {
@@ -17,6 +17,14 @@
17
17
  "types": "./dist/layout.d.ts",
18
18
  "default": "./dist/layout.js"
19
19
  },
20
+ "./layout-ctors": {
21
+ "types": "./dist/layoutCtors.d.ts",
22
+ "default": "./dist/layoutCtors.js"
23
+ },
24
+ "./grid": {
25
+ "types": "./dist/grid.d.ts",
26
+ "default": "./dist/grid.js"
27
+ },
20
28
  "./path": {
21
29
  "types": "./dist/path.d.ts",
22
30
  "default": "./dist/path.js"
@@ -28,6 +36,18 @@
28
36
  "./type": {
29
37
  "types": "./dist/type.d.ts",
30
38
  "default": "./dist/type.js"
39
+ },
40
+ "./diagnostics": {
41
+ "types": "./dist/diagnostics.d.ts",
42
+ "default": "./dist/diagnostics.js"
43
+ },
44
+ "./motion": {
45
+ "types": "./dist/motion.d.ts",
46
+ "default": "./dist/motion.js"
47
+ },
48
+ "./identity": {
49
+ "types": "./dist/identity.d.ts",
50
+ "default": "./dist/identity.js"
31
51
  }
32
52
  },
33
53
  "files": [
@@ -35,7 +55,7 @@
35
55
  ],
36
56
  "dependencies": {
37
57
  "yoga-layout": "^3.2.1",
38
- "@glissade/core": "0.19.1"
58
+ "@glissade/core": "0.20.0-pre.3"
39
59
  },
40
60
  "repository": {
41
61
  "type": "git",
@@ -44,6 +64,6 @@
44
64
  },
45
65
  "scripts": {
46
66
  "build": "tsdown",
47
- "typecheck": "tsc --noEmit"
67
+ "typecheck": "tsc --noEmit && tsc --noEmit -p spike/tsconfig.json"
48
68
  }
49
69
  }