@glissade/core 0.9.0-pre.0 → 0.9.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.js CHANGED
@@ -364,7 +364,9 @@ function parseFormat4(dv, base, into) {
364
364
  }
365
365
  }
366
366
  function parseFormat12(dv, base, into) {
367
- const nGroups = u32(dv, base + 12);
367
+ const declared = u32(dv, base + 12);
368
+ const maxGroups = Math.max(0, Math.floor((dv.byteLength - (base + 16)) / 12));
369
+ const nGroups = Math.min(declared, maxGroups);
368
370
  let off = base + 16;
369
371
  for (let i = 0; i < nGroups; i++, off += 12) {
370
372
  const startChar = u32(dv, off);
package/dist/sidecar.js CHANGED
@@ -1118,6 +1118,7 @@ function assignKeyIds(keys) {
1118
1118
  * with no baseline). Keys get stable `k<N>` ids. Returns a new document.
1119
1119
  */
1120
1120
  function setSidecarTrack(doc, timelineId, target, type, keys, codeBaselineKeys) {
1121
+ if (!isEditableNodeId(targetNodeId(target))) throw new TrackValidationError(target, "structural/un-id'd nodes cannot host editor tracks (§6.5) — only nodes with an explicit id");
1121
1122
  const tl = doc.timelines[timelineId] ?? { tracks: {} };
1122
1123
  const entry = {
1123
1124
  type,
@@ -16,10 +16,12 @@ type TimelinePatch = {
16
16
  type: ValueTypeId;
17
17
  keys: Key[];
18
18
  baseHash?: string | null;
19
+ verbatim?: boolean;
19
20
  } | {
20
21
  op: 'removeTrack';
21
22
  timelineId: string;
22
23
  target: string;
24
+ prune?: boolean;
23
25
  } | {
24
26
  op: 'addKey';
25
27
  timelineId: string;
@@ -38,11 +38,20 @@ function putEntry(doc, timelineId, target, entry) {
38
38
  }
39
39
  };
40
40
  }
41
- function removeEntry(doc, timelineId, target) {
41
+ function removeEntry(doc, timelineId, target, prune = false) {
42
42
  const tl = doc.timelines[timelineId];
43
43
  if (!tl?.tracks[target]) return doc;
44
44
  const tracks = { ...tl.tracks };
45
45
  delete tracks[target];
46
+ const labels = tl.labels;
47
+ if (prune && Object.keys(tracks).length === 0 && (!labels || Object.keys(labels).length === 0)) {
48
+ const timelines = { ...doc.timelines };
49
+ delete timelines[timelineId];
50
+ return {
51
+ ...doc,
52
+ timelines
53
+ };
54
+ }
46
55
  return {
47
56
  ...doc,
48
57
  timelines: {
@@ -54,11 +63,16 @@ function removeEntry(doc, timelineId, target) {
54
63
  }
55
64
  };
56
65
  }
57
- /** Apply a key-list edit: normalize (§2.7 spring re-pin + collision nudge) and (re)assign stable ids. */
58
- function withKeys(entry, keys) {
66
+ /**
67
+ * Apply a key-list edit. `verbatim` restores keys EXACTLY as given (the inverse
68
+ * path — it is replaying a prior valid state, so re-normalizing would re-pin
69
+ * springs / re-nudge collisions and break byte-exact undo). The forward editor
70
+ * path normalizes (§2.7 spring re-pin + collision nudge) and (re)assigns ids.
71
+ */
72
+ function withKeys(entry, keys, verbatim = false) {
59
73
  return {
60
74
  ...entry,
61
- keys: assignKeyIds(normalizeEditedKeys(keys))
75
+ keys: verbatim ? keys.map((k) => ({ ...k })) : assignKeyIds(normalizeEditedKeys(keys))
62
76
  };
63
77
  }
64
78
  /**
@@ -72,7 +86,9 @@ function applyPatches(doc, patches, baseline) {
72
86
  let work = doc;
73
87
  const trackSnap = /* @__PURE__ */ new Map();
74
88
  const labelSnap = /* @__PURE__ */ new Map();
89
+ const tlExisted = /* @__PURE__ */ new Map();
75
90
  const snapTrack = (timelineId, target) => {
91
+ if (!tlExisted.has(timelineId)) tlExisted.set(timelineId, work.timelines[timelineId] !== void 0);
76
92
  const k = refKey(timelineId, target);
77
93
  if (!trackSnap.has(k)) trackSnap.set(k, work.timelines[timelineId]?.tracks[target] ?? null);
78
94
  };
@@ -82,13 +98,17 @@ function applyPatches(doc, patches, baseline) {
82
98
  };
83
99
  for (const p of patches) switch (p.op) {
84
100
  case "setTrackKeys": {
101
+ if (!p.verbatim && !isEditableNodeId(targetNodeId(p.target))) return {
102
+ ok: false,
103
+ error: `setTrackKeys: '${p.target}' is not an editable host (structural/un-id'd nodes cannot host tracks, §6.5)`
104
+ };
85
105
  snapTrack(p.timelineId, p.target);
86
106
  const prev = work.timelines[p.timelineId]?.tracks[p.target];
87
107
  const entry = withKeys({
88
108
  type: p.type,
89
109
  baseHash: p.baseHash !== void 0 ? p.baseHash : prev?.baseHash ?? null,
90
110
  keys: []
91
- }, p.keys.map((k) => ({ ...k })));
111
+ }, p.keys.map((k) => ({ ...k })), p.verbatim);
92
112
  work = putEntry(work, p.timelineId, p.target, entry);
93
113
  break;
94
114
  }
@@ -98,9 +118,13 @@ function applyPatches(doc, patches, baseline) {
98
118
  error: `removeTrack: no sidecar track '${p.target}'`
99
119
  };
100
120
  snapTrack(p.timelineId, p.target);
101
- work = removeEntry(work, p.timelineId, p.target);
121
+ work = removeEntry(work, p.timelineId, p.target, p.prune);
102
122
  break;
103
123
  case "addKey": {
124
+ if (!isEditableNodeId(targetNodeId(p.target))) return {
125
+ ok: false,
126
+ error: `addKey: '${p.target}' is not an editable host (structural/un-id'd nodes cannot host tracks, §6.5)`
127
+ };
104
128
  const entry = resolveEntry(work, p, baseline);
105
129
  if (!entry) return {
106
130
  ok: false,
@@ -191,7 +215,12 @@ function applyPatches(doc, patches, baseline) {
191
215
  const inverse = [];
192
216
  for (const [k, snap] of trackSnap) {
193
217
  const [timelineId, target] = parseRefKey(k);
194
- if (snap === null) inverse.push({
218
+ if (snap === null) inverse.push(tlExisted.get(timelineId) === false ? {
219
+ op: "removeTrack",
220
+ timelineId,
221
+ target,
222
+ prune: true
223
+ } : {
195
224
  op: "removeTrack",
196
225
  timelineId,
197
226
  target
@@ -202,7 +231,8 @@ function applyPatches(doc, patches, baseline) {
202
231
  target,
203
232
  type: snap.type,
204
233
  keys: snap.keys.map((x) => ({ ...x })),
205
- baseHash: snap.baseHash
234
+ baseHash: snap.baseHash,
235
+ verbatim: true
206
236
  });
207
237
  }
208
238
  for (const [k, value] of labelSnap) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/core",
3
- "version": "0.9.0-pre.0",
3
+ "version": "0.9.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",