@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.
- package/dist/index.d.ts +79 -442
- package/dist/index.js +253 -1180
- package/dist/sidecar.d.ts +510 -0
- package/dist/sidecar.js +1268 -0
- package/dist/studioHost.d.ts +124 -0
- package/dist/studioHost.js +232 -0
- package/package.json +5 -1
|
@@ -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.
|
|
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": [
|