@glissade/core 0.8.1 → 0.9.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.
@@ -0,0 +1,124 @@
1
+ import { H as Key, N as Timeline, S as isEditableNodeId, St as ValueTypeId, a as SidecarOrphan, lt as EaseSpec, r as SidecarDoc, w as targetNodeId } from "./sidecar.js";
2
+
3
+ //#region src/timelinePatch.d.ts
4
+
5
+ /** A new key's data (no id — one is assigned on apply). */
6
+ interface NewKey {
7
+ t: number;
8
+ value: unknown;
9
+ ease?: EaseSpec;
10
+ interp?: 'default' | 'hold';
11
+ }
12
+ type TimelinePatch = {
13
+ op: 'setTrackKeys';
14
+ timelineId: string;
15
+ target: string;
16
+ type: ValueTypeId;
17
+ keys: Key[];
18
+ baseHash?: string | null;
19
+ } | {
20
+ op: 'removeTrack';
21
+ timelineId: string;
22
+ target: string;
23
+ } | {
24
+ op: 'addKey';
25
+ timelineId: string;
26
+ target: string;
27
+ key: NewKey;
28
+ } | {
29
+ op: 'removeKey';
30
+ timelineId: string;
31
+ target: string;
32
+ id: string;
33
+ } | {
34
+ op: 'moveKey';
35
+ timelineId: string;
36
+ target: string;
37
+ id: string;
38
+ t: number;
39
+ } | {
40
+ op: 'setKeyValue';
41
+ timelineId: string;
42
+ target: string;
43
+ id: string;
44
+ value: unknown;
45
+ } | {
46
+ op: 'setKeyEase';
47
+ timelineId: string;
48
+ target: string;
49
+ id: string;
50
+ ease?: EaseSpec;
51
+ interp?: 'default' | 'hold';
52
+ } | {
53
+ op: 'setLabel';
54
+ timelineId: string;
55
+ name: string;
56
+ t: number;
57
+ } | {
58
+ op: 'removeLabel';
59
+ timelineId: string;
60
+ name: string;
61
+ };
62
+ interface PatchOk {
63
+ ok: true;
64
+ doc: SidecarDoc;
65
+ /** inverse transaction — apply to undo (restores each touched track/label). */
66
+ inverse: TimelinePatch[];
67
+ }
68
+ interface PatchErr {
69
+ ok: false;
70
+ error: string;
71
+ }
72
+ type PatchResult = PatchOk | PatchErr;
73
+ /** The code-baseline track for a target, so a first edit on a code-only track seeds + records its baseHash. */
74
+ interface TrackBaseline {
75
+ type: ValueTypeId;
76
+ keys: readonly Key[];
77
+ }
78
+ type BaselineLookup = (timelineId: string, target: string) => TrackBaseline | null;
79
+ /**
80
+ * Apply a batch of patches **atomically**: validate-and-build against a working
81
+ * copy; on the FIRST invalid patch, return `{ ok: false }` with the original
82
+ * doc untouched (immutable updates never mutate the input). On success, return
83
+ * the new doc plus the inverse transaction (one snapshot-restore per touched
84
+ * track/label). `baseline` seeds a first edit on a code-only track.
85
+ */
86
+ declare function applyPatches(doc: SidecarDoc, patches: readonly TimelinePatch[], baseline?: BaselineLookup): PatchResult;
87
+ /** Apply a single patch (convenience over `applyPatches`). */
88
+ declare function applyPatch(doc: SidecarDoc, patch: TimelinePatch, baseline?: BaselineLookup): PatchResult;
89
+ //#endregion
90
+ //#region src/studioHost.d.ts
91
+ /** The merged code+sidecar view plus the tracks parked off the merge (§6.2 rule 3). */
92
+ type MergedTimeline = Timeline & {
93
+ orphans: Record<string, SidecarOrphan>;
94
+ };
95
+ /** `${nodeId}/${propPath}` — the §2.2 canonical track grammar. */
96
+ type SignalPath = string;
97
+ type Unsubscribe = () => void;
98
+ interface PropDescriptor {
99
+ name: string;
100
+ type: ValueTypeId;
101
+ /** editable IFF the node has an explicit id AND a merged/editor-created track exists (§6.4 sub-decision). */
102
+ editable: boolean;
103
+ }
104
+ interface NodeDescriptor {
105
+ id: string;
106
+ /** the node kind (Group/Rect/Text/…). */
107
+ type: string;
108
+ props: PropDescriptor[];
109
+ }
110
+ type StudioEvent = 'tree-changed' | 'doc-patched' | 'playhead-moved';
111
+ interface StudioHost {
112
+ /** ids, types, prop schemas + editability — deep plain data, clone-safe. */
113
+ getSceneTree(): NodeDescriptor[];
114
+ /** merged code+sidecar timeline + orphans (§6.2). */
115
+ getTimeline(): MergedTimeline;
116
+ /** bridge a node/prop signal to a callback; returns an unsubscribe. */
117
+ subscribeSignal(path: SignalPath, cb: (v: unknown) => void): Unsubscribe;
118
+ /** apply an edit transaction — validated + atomic; returns the inverse for undo. */
119
+ applyPatch(patches: TimelinePatch[]): PatchResult;
120
+ setPlayhead(t: number): void;
121
+ on(ev: StudioEvent, cb: (...args: unknown[]) => void): Unsubscribe;
122
+ }
123
+ //#endregion
124
+ export { type BaselineLookup, MergedTimeline, type NewKey, NodeDescriptor, type PatchErr, type PatchOk, type PatchResult, PropDescriptor, SignalPath, StudioEvent, StudioHost, type TimelinePatch, type TrackBaseline, Unsubscribe, applyPatch, applyPatches, isEditableNodeId, targetNodeId };
@@ -0,0 +1,232 @@
1
+ import { a as hashKeys, h as targetNodeId, l as normalizeEditedKeys, n as assignKeyIds, p as isEditableNodeId } from "./sidecar.js";
2
+ //#region src/timelinePatch.ts
3
+ /** Unambiguous composite map key (no control-char separators). */
4
+ function refKey(timelineId, name) {
5
+ return JSON.stringify([timelineId, name]);
6
+ }
7
+ function parseRefKey(k) {
8
+ return JSON.parse(k);
9
+ }
10
+ function getTimeline(doc, id) {
11
+ return doc.timelines[id] ?? { tracks: {} };
12
+ }
13
+ /** The current sidecar entry for a target, seeding from the code baseline (with ids) if absent. */
14
+ function resolveEntry(doc, p, baseline) {
15
+ const existing = doc.timelines[p.timelineId]?.tracks[p.target];
16
+ if (existing) return existing;
17
+ const base = baseline?.(p.timelineId, p.target) ?? null;
18
+ if (!base) return null;
19
+ return {
20
+ type: base.type,
21
+ baseHash: hashKeys(base.keys),
22
+ keys: assignKeyIds(base.keys.map((k) => ({ ...k })))
23
+ };
24
+ }
25
+ function putEntry(doc, timelineId, target, entry) {
26
+ const tl = getTimeline(doc, timelineId);
27
+ return {
28
+ ...doc,
29
+ timelines: {
30
+ ...doc.timelines,
31
+ [timelineId]: {
32
+ ...tl,
33
+ tracks: {
34
+ ...tl.tracks,
35
+ [target]: entry
36
+ }
37
+ }
38
+ }
39
+ };
40
+ }
41
+ function removeEntry(doc, timelineId, target) {
42
+ const tl = doc.timelines[timelineId];
43
+ if (!tl?.tracks[target]) return doc;
44
+ const tracks = { ...tl.tracks };
45
+ delete tracks[target];
46
+ return {
47
+ ...doc,
48
+ timelines: {
49
+ ...doc.timelines,
50
+ [timelineId]: {
51
+ ...tl,
52
+ tracks
53
+ }
54
+ }
55
+ };
56
+ }
57
+ /** Apply a key-list edit: normalize (§2.7 spring re-pin + collision nudge) and (re)assign stable ids. */
58
+ function withKeys(entry, keys) {
59
+ return {
60
+ ...entry,
61
+ keys: assignKeyIds(normalizeEditedKeys(keys))
62
+ };
63
+ }
64
+ /**
65
+ * Apply a batch of patches **atomically**: validate-and-build against a working
66
+ * copy; on the FIRST invalid patch, return `{ ok: false }` with the original
67
+ * doc untouched (immutable updates never mutate the input). On success, return
68
+ * the new doc plus the inverse transaction (one snapshot-restore per touched
69
+ * track/label). `baseline` seeds a first edit on a code-only track.
70
+ */
71
+ function applyPatches(doc, patches, baseline) {
72
+ let work = doc;
73
+ const trackSnap = /* @__PURE__ */ new Map();
74
+ const labelSnap = /* @__PURE__ */ new Map();
75
+ const snapTrack = (timelineId, target) => {
76
+ const k = refKey(timelineId, target);
77
+ if (!trackSnap.has(k)) trackSnap.set(k, work.timelines[timelineId]?.tracks[target] ?? null);
78
+ };
79
+ const snapLabel = (timelineId, name) => {
80
+ const k = refKey(timelineId, name);
81
+ if (!labelSnap.has(k)) labelSnap.set(k, work.timelines[timelineId]?.labels?.[name]);
82
+ };
83
+ for (const p of patches) switch (p.op) {
84
+ case "setTrackKeys": {
85
+ snapTrack(p.timelineId, p.target);
86
+ const prev = work.timelines[p.timelineId]?.tracks[p.target];
87
+ const entry = withKeys({
88
+ type: p.type,
89
+ baseHash: p.baseHash !== void 0 ? p.baseHash : prev?.baseHash ?? null,
90
+ keys: []
91
+ }, p.keys.map((k) => ({ ...k })));
92
+ work = putEntry(work, p.timelineId, p.target, entry);
93
+ break;
94
+ }
95
+ case "removeTrack":
96
+ if (!work.timelines[p.timelineId]?.tracks[p.target]) return {
97
+ ok: false,
98
+ error: `removeTrack: no sidecar track '${p.target}'`
99
+ };
100
+ snapTrack(p.timelineId, p.target);
101
+ work = removeEntry(work, p.timelineId, p.target);
102
+ break;
103
+ case "addKey": {
104
+ const entry = resolveEntry(work, p, baseline);
105
+ if (!entry) return {
106
+ ok: false,
107
+ error: `addKey: unknown track '${p.target}' (no sidecar entry and no code baseline)`
108
+ };
109
+ snapTrack(p.timelineId, p.target);
110
+ const k = {
111
+ t: p.key.t,
112
+ value: p.key.value,
113
+ ...p.key.ease !== void 0 ? { ease: p.key.ease } : {},
114
+ ...p.key.interp !== void 0 ? { interp: p.key.interp } : {}
115
+ };
116
+ work = putEntry(work, p.timelineId, p.target, withKeys(entry, [...entry.keys, k]));
117
+ break;
118
+ }
119
+ case "removeKey":
120
+ case "moveKey":
121
+ case "setKeyValue":
122
+ case "setKeyEase": {
123
+ const entry = resolveEntry(work, p, baseline);
124
+ if (!entry) return {
125
+ ok: false,
126
+ error: `${p.op}: unknown track '${p.target}'`
127
+ };
128
+ if (!entry.keys.some((k) => k.id === p.id)) return {
129
+ ok: false,
130
+ error: `${p.op}: no key '${p.id}' in '${p.target}'`
131
+ };
132
+ snapTrack(p.timelineId, p.target);
133
+ const next = p.op === "removeKey" ? entry.keys.filter((k) => k.id !== p.id) : entry.keys.map((k) => {
134
+ if (k.id !== p.id) return k;
135
+ if (p.op === "moveKey") return {
136
+ ...k,
137
+ t: Math.max(0, p.t)
138
+ };
139
+ if (p.op === "setKeyValue") return {
140
+ ...k,
141
+ value: p.value
142
+ };
143
+ const u = { ...k };
144
+ if (p.ease !== void 0) u.ease = p.ease;
145
+ else delete u.ease;
146
+ if (p.interp !== void 0) u.interp = p.interp;
147
+ else delete u.interp;
148
+ return u;
149
+ });
150
+ work = putEntry(work, p.timelineId, p.target, withKeys(entry, next));
151
+ break;
152
+ }
153
+ case "setLabel": {
154
+ snapLabel(p.timelineId, p.name);
155
+ const tl = getTimeline(work, p.timelineId);
156
+ work = {
157
+ ...work,
158
+ timelines: {
159
+ ...work.timelines,
160
+ [p.timelineId]: {
161
+ ...tl,
162
+ labels: {
163
+ ...tl.labels ?? {},
164
+ [p.name]: p.t
165
+ }
166
+ }
167
+ }
168
+ };
169
+ break;
170
+ }
171
+ case "removeLabel": {
172
+ snapLabel(p.timelineId, p.name);
173
+ const tl = work.timelines[p.timelineId];
174
+ if (tl?.labels && p.name in tl.labels) {
175
+ const labels = { ...tl.labels };
176
+ delete labels[p.name];
177
+ work = {
178
+ ...work,
179
+ timelines: {
180
+ ...work.timelines,
181
+ [p.timelineId]: {
182
+ ...tl,
183
+ labels
184
+ }
185
+ }
186
+ };
187
+ }
188
+ break;
189
+ }
190
+ }
191
+ const inverse = [];
192
+ for (const [k, snap] of trackSnap) {
193
+ const [timelineId, target] = parseRefKey(k);
194
+ if (snap === null) inverse.push({
195
+ op: "removeTrack",
196
+ timelineId,
197
+ target
198
+ });
199
+ else inverse.push({
200
+ op: "setTrackKeys",
201
+ timelineId,
202
+ target,
203
+ type: snap.type,
204
+ keys: snap.keys.map((x) => ({ ...x })),
205
+ baseHash: snap.baseHash
206
+ });
207
+ }
208
+ for (const [k, value] of labelSnap) {
209
+ const [timelineId, name] = parseRefKey(k);
210
+ inverse.push(value === void 0 ? {
211
+ op: "removeLabel",
212
+ timelineId,
213
+ name
214
+ } : {
215
+ op: "setLabel",
216
+ timelineId,
217
+ name,
218
+ t: value
219
+ });
220
+ }
221
+ return {
222
+ ok: true,
223
+ doc: work,
224
+ inverse
225
+ };
226
+ }
227
+ /** Apply a single patch (convenience over `applyPatches`). */
228
+ function applyPatch(doc, patch, baseline) {
229
+ return applyPatches(doc, [patch], baseline);
230
+ }
231
+ //#endregion
232
+ export { applyPatch, applyPatches, isEditableNodeId, targetNodeId };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/core",
3
- "version": "0.8.1",
3
+ "version": "0.9.0-pre.0",
4
4
  "description": "glissade core: signals, tracks, timeline document, evaluation, easing, springs, seeded RNG. Zero DOM/Node dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -9,6 +9,10 @@
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
11
  "default": "./dist/index.js"
12
+ },
13
+ "./studio-host": {
14
+ "types": "./dist/studioHost.d.ts",
15
+ "default": "./dist/studioHost.js"
12
16
  }
13
17
  },
14
18
  "files": [