@hyperframes/studio 0.8.132 → 0.8.134

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.
Files changed (34) hide show
  1. package/dist/assets/{hyperframes-player-Cp8e_Zst.js → hyperframes-player-Bro1gj0Z.js} +1 -1
  2. package/dist/assets/{index-CASEBNy2.js → index-BwNDTmAD.js} +1 -1
  3. package/dist/assets/{index-DMTt9nTf.js → index-DnVGIbEC.js} +1 -1
  4. package/dist/assets/{index-oE1pOdAy.js → index-wnXyQjW-.js} +143 -143
  5. package/dist/index.html +1 -1
  6. package/dist/index.js +717 -702
  7. package/dist/index.js.map +1 -1
  8. package/package.json +8 -8
  9. package/src/components/editor/manualEditsDom.ts +5 -1
  10. package/src/components/editor/plainTranslate.test.ts +21 -2
  11. package/src/components/editor/plainTranslate.ts +7 -0
  12. package/src/hooks/draggedGsapPosition.test.ts +34 -0
  13. package/src/hooks/draggedGsapPosition.ts +40 -15
  14. package/src/hooks/gsapDragCommit.ts +17 -10
  15. package/src/hooks/gsapDragPositionCommit.ts +8 -9
  16. package/src/hooks/gsapResizeIntercept.test.ts +72 -1
  17. package/src/hooks/gsapResizeIntercept.ts +84 -17
  18. package/src/hooks/gsapRuntimeBridge.ts +4 -1
  19. package/src/hooks/gsapTrailingKeyframes.realGsap.test.ts +44 -0
  20. package/src/hooks/useEditHistoryActions.test.tsx +20 -0
  21. package/src/hooks/useEditHistoryActions.ts +3 -0
  22. package/src/hooks/useEnableKeyframes.ts +32 -37
  23. package/src/hooks/useGsapAwareEditing.test.tsx +1 -0
  24. package/src/hooks/useGsapAwareEditing.ts +10 -2
  25. package/src/player/components/timelineClipDragCommit.test.ts +22 -0
  26. package/src/player/components/timelineClipDragCommit.ts +7 -10
  27. package/src/player/components/timelineStackingSync.test.ts +101 -142
  28. package/src/player/components/timelineStackingSync.ts +51 -235
  29. package/src/player/components/useTimelineStackingSync.ts +1 -1
  30. package/src/timelineStackingSyncExport.test.tsx +3 -2
  31. package/src/utils/elementGsap.ts +6 -5
  32. package/src/utils/gsapUndoRestore.test.ts +22 -0
  33. package/src/utils/gsapUndoRestore.ts +4 -2
  34. package/src/utils/studioPendingEdits.ts +12 -2
@@ -3,6 +3,7 @@ import {
3
3
  computeStackingPatches,
4
4
  laneIsAbove,
5
5
  samePaintScope,
6
+ type StackingDirection,
6
7
  type StackingElement,
7
8
  } from "./timelineStackingSync";
8
9
 
@@ -18,9 +19,13 @@ function el(
18
19
  return { key, track, start, duration, zIndex, isAudio, domIndex };
19
20
  }
20
21
 
21
- function patchMap(elements: StackingElement[], edited: string[]): Record<string, number> {
22
+ function patchMap(
23
+ elements: StackingElement[],
24
+ edited: string[],
25
+ direction: StackingDirection,
26
+ ): Record<string, number> {
22
27
  const out: Record<string, number> = {};
23
- for (const p of computeStackingPatches(elements, edited)) out[p.key] = p.zIndex;
28
+ for (const p of computeStackingPatches(elements, edited, direction)) out[p.key] = p.zIndex;
24
29
  return out;
25
30
  }
26
31
 
@@ -58,7 +63,7 @@ describe("stacking-context partitioning", () => {
58
63
  stackingContextId: null,
59
64
  };
60
65
 
61
- expect(patchMap([root, scene], ["root"])).toEqual({});
66
+ expect(patchMap([root, scene], ["root"], "up")).toEqual({});
62
67
  });
63
68
 
64
69
  it("never compares or patches across stacking contexts", () => {
@@ -86,7 +91,7 @@ describe("stacking-context partitioning", () => {
86
91
  };
87
92
  // X edited: only same-context neighbours participate — none here, so X keeps
88
93
  // its z (nothing to fix WITHIN its context) and Y is never touched.
89
- expect(patchMap([x, y], ["x"])).toEqual({});
94
+ expect(patchMap([x, y], ["x"], "up")).toEqual({});
90
95
  });
91
96
 
92
97
  it("still resolves within the edited clip's own context", () => {
@@ -115,7 +120,7 @@ describe("stacking-context partitioning", () => {
115
120
  // by flipping z so a MUST be lifted).
116
121
  const aWrong = { ...a, track: 0, zIndex: 1 };
117
122
  const bLow = { ...b, track: 1, zIndex: 5 };
118
- const patches = patchMap([aWrong, bLow], ["a"]);
123
+ const patches = patchMap([aWrong, bLow], ["a"], "up");
119
124
  expect(patches.a).toBeGreaterThan(5);
120
125
  });
121
126
  });
@@ -132,214 +137,168 @@ describe("computeStackingPatches", () => {
132
137
  it("no overlapping clips → no patch", () => {
133
138
  // a (0..5 on track 0) and b (10..15 on track 1) never overlap in time.
134
139
  const elements = [el("a", 0, 0, 5, 10), el("b", 1, 10, 5, 5)];
135
- expect(patchMap(elements, ["a"])).toEqual({});
140
+ expect(patchMap(elements, ["a"], "up")).toEqual({});
136
141
  });
137
142
 
138
- it("edited clip moved to a HIGHER lane (top) but z too low → raised above the below-neighbour", () => {
139
- // a on top lane (0) overlaps b on lane 1; a.z=1 is below b.z=5 → wrong.
143
+ it("moved up with z too low → raised above the clip it now sits above", () => {
140
144
  const elements = [el("a", 0, 0, 10, 1), el("b", 1, 0, 10, 5)];
141
- // Only a is edited → only a gets a patch, lifting it above b (5) → 6.
142
- expect(patchMap(elements, ["a"])).toEqual({ a: 6 });
145
+ expect(patchMap(elements, ["a"], "up")).toEqual({ a: 6 });
143
146
  });
144
147
 
145
- it("edited clip moved to a LOWER lane (bottom) but z too high → lowered below the above-neighbour", () => {
146
- // a on lane 2 (bottom) overlaps b on lane 0 (top); a.z=9 above b.z=5 → wrong.
148
+ it("moved down with z too high → lowered below the clip now above it", () => {
147
149
  const elements = [el("a", 2, 0, 10, 9), el("b", 0, 0, 10, 5)];
148
- expect(patchMap(elements, ["a"])).toEqual({ a: 4 });
150
+ expect(patchMap(elements, ["a"], "down")).toEqual({ a: 4 });
149
151
  });
150
152
 
151
- it("edited clip already correctly ordered → no patch (authored z preserved)", () => {
152
- // a on top lane already has higher z than the lower-lane b it overlaps.
153
+ it("already in order → no patch (authored z preserved)", () => {
153
154
  const elements = [el("a", 0, 0, 10, 8), el("b", 1, 0, 10, 3)];
154
- expect(patchMap(elements, ["a"])).toEqual({});
155
+ expect(patchMap(elements, ["a"], "up")).toEqual({});
155
156
  });
156
157
 
157
158
  it("untouched clips never get a patch even when they overlap the edit", () => {
158
- // b is out of order relative to a, but a is the only edited clip.
159
159
  const elements = [el("a", 0, 0, 10, 1), el("b", 1, 0, 10, 5), el("c", 2, 0, 10, 9)];
160
- const patches = computeStackingPatches(elements, ["a"]);
160
+ const patches = computeStackingPatches(elements, ["a"], "up");
161
161
  expect(patches.map((p) => p.key)).toEqual(["a"]);
162
162
  });
163
163
 
164
- it("sits strictly between neighbours when there is integer room", () => {
165
- // edited a on middle lane 1 between below-lane-2 (z=2) and above-lane-0 (z=10).
164
+ it("moved up rises one above the clips now below it and ignores the clips above it", () => {
166
165
  const elements = [el("a", 1, 0, 10, 0), el("below", 2, 0, 10, 2), el("above", 0, 0, 10, 10)];
167
- // Between 2 and 10 → floor((2+10)/2)=6.
168
- expect(patchMap(elements, ["a"])).toEqual({ a: 6 });
166
+ expect(patchMap(elements, ["a"], "up")).toEqual({ a: 3 });
169
167
  });
170
168
 
171
- it("adjacent neighbours (no integer gap) → edited lands above lower, upper is bumped", () => {
172
- // below z=4, above z=5 (adjacent). There is no integer strictly between 4 and
173
- // 5, so the old single-patch a=5 left `a` TIED with `above` — and with no DOM
174
- // order to break the tie, `above` no longer paints strictly above `a` (the
175
- // under-patch bug). Tie-aware cascade: a→5 (above below's 4) AND above→6 so it
176
- // stays strictly on top. Minimal: only the two overlapping neighbours move.
177
- const elements = [el("a", 1, 0, 10, 0), el("below", 2, 0, 10, 4), el("above", 0, 0, 10, 5)];
178
- expect(patchMap(elements, ["a"])).toEqual({ a: 5, above: 6 });
169
+ it("moved down sinks one below the clips now above it and ignores the clips below it", () => {
170
+ const elements = [el("a", 1, 0, 10, 20), el("below", 2, 0, 10, 2), el("above", 0, 0, 10, 10)];
171
+ expect(patchMap(elements, ["a"], "down")).toEqual({ a: 9 });
179
172
  });
180
173
 
181
174
  it("audio clips are excluded — an audio edit yields no patch", () => {
182
175
  const elements = [el("music", 3, 0, 10, 0, true), el("v", 0, 0, 10, 5)];
183
- expect(patchMap(elements, ["music"])).toEqual({});
176
+ expect(patchMap(elements, ["music"], "down")).toEqual({});
184
177
  });
185
178
 
186
179
  it("audio clips are excluded as neighbours — a visual edit ignores overlapping audio", () => {
187
- // The only overlapping clip is audio → treated as no visual overlap → no patch.
188
180
  const elements = [el("v", 0, 0, 10, 3), el("music", 3, 0, 10, 99, true)];
189
- expect(patchMap(elements, ["v"])).toEqual({});
181
+ expect(patchMap(elements, ["v"], "up")).toEqual({});
190
182
  });
191
183
 
192
- it("only-below neighbours → maxBelow + 1", () => {
184
+ it("moved up over several clips → one above the highest of them", () => {
193
185
  const elements = [el("a", 0, 0, 10, 0), el("b", 1, 0, 10, 3), el("c", 2, 0, 10, 7)];
194
- // a on top overlaps b(3) and c(7), both below → 7+1=8.
195
- expect(patchMap(elements, ["a"])).toEqual({ a: 8 });
186
+ expect(patchMap(elements, ["a"], "up")).toEqual({ a: 8 });
196
187
  });
197
188
 
198
- it("only-above neighbours → minAbove - 1 (clamped ≥ 0)", () => {
189
+ it("moved down under several clips → one below the lowest of them, never under 0", () => {
199
190
  const elements = [el("a", 2, 0, 10, 9), el("b", 0, 0, 10, 1), el("c", 1, 0, 10, 4)];
200
- // a on bottom overlaps b(1) and c(4), both above → min(1)-1=0.
201
- expect(patchMap(elements, ["a"])).toEqual({ a: 0 });
191
+ expect(patchMap(elements, ["a"], "down")).toEqual({ a: 0 });
202
192
  });
203
193
 
204
194
  it("partial time overlap still counts", () => {
205
- // a: 0..6, b: 5..15 overlap in [5,6).
206
195
  const elements = [el("a", 0, 0, 6, 1), el("b", 1, 5, 10, 5)];
207
- expect(patchMap(elements, ["a"])).toEqual({ a: 6 });
196
+ expect(patchMap(elements, ["a"], "up")).toEqual({ a: 6 });
208
197
  });
209
198
 
210
199
  it("touching-but-not-overlapping intervals do NOT count", () => {
211
- // a ends exactly where b starts (t=5) → half-open, no overlap.
212
200
  const elements = [el("a", 0, 0, 5, 1), el("b", 1, 5, 5, 5)];
213
- expect(patchMap(elements, ["a"])).toEqual({});
201
+ expect(patchMap(elements, ["a"], "up")).toEqual({});
214
202
  });
215
203
 
216
- it("multi-clip edit: two dragged clips resolve consistently against the region", () => {
217
- // Drag a (lane 0) and b (lane 1) onto a region already holding c (lane 2, z=5).
218
- // Both overlap c. Lower-lane b resolves first (above c → 6), then a (above b → 7).
204
+ it("multi-clip move up: the bottom member resolves first, the top one rises above it", () => {
219
205
  const elements = [el("a", 0, 0, 10, 0), el("b", 1, 0, 10, 0), el("c", 2, 0, 10, 5)];
220
- expect(patchMap(elements, ["a", "b"])).toEqual({ a: 7, b: 6 });
206
+ expect(patchMap(elements, ["a", "b"], "up")).toEqual({ a: 7, b: 6 });
221
207
  });
222
208
 
223
- it("multi-clip edit skips a member that is already correctly ordered", () => {
209
+ it("multi-clip move down: the top member resolves first, the bottom one sinks below it", () => {
210
+ const elements = [el("a", 1, 0, 10, 9), el("b", 2, 0, 10, 9), el("c", 0, 0, 10, 5)];
211
+ expect(patchMap(elements, ["a", "b"], "down")).toEqual({ a: 4, b: 3 });
212
+ });
213
+
214
+ it("multi-clip move skips a member that is already in order", () => {
224
215
  const elements = [el("a", 0, 0, 10, 20), el("b", 1, 0, 10, 0), el("c", 2, 0, 10, 5)];
225
- // a(20) already above everything → no patch. b (lane 1) sits between
226
- // below-neighbour c(5) and above-neighbour a(20) → floor((5+20)/2)=12.
227
- expect(patchMap(elements, ["a", "b"])).toEqual({ b: 12 });
216
+ expect(patchMap(elements, ["a", "b"], "up")).toEqual({ b: 6 });
228
217
  });
229
218
 
230
219
  it("empty edited set → no patches", () => {
231
220
  const elements = [el("a", 0, 0, 10, 1), el("b", 1, 0, 10, 5)];
232
- expect(computeStackingPatches(elements, [])).toEqual([]);
221
+ expect(computeStackingPatches(elements, [], "up")).toEqual([]);
233
222
  });
234
223
 
235
- it("item 13: an unresolved neighbour (non-finite z) is EXCLUDED, not treated as z=0", () => {
236
- // `ghost` is an overlapping upper-lane clip whose live node could not be
237
- // resolved, so its z came back NaN. If it were fabricated to 0 it would be a
238
- // real above-neighbour at the z-floor and drag the edited clip's cascade down.
239
- // Excluded, the only real overlap is below-neighbour b(3) → a rises to 4.
224
+ it("an unresolved neighbour (non-finite z) is excluded, not treated as z=0", () => {
240
225
  const elements = [
241
- el("a", 1, 0, 10, 0),
242
- el("b", 2, 0, 10, 3),
243
- el("ghost", 0, 0, 10, Number.NaN),
226
+ el("a", 2, 0, 10, 9),
227
+ el("b", 0, 0, 10, 3),
228
+ el("ghost", 1, 0, 10, Number.NaN),
244
229
  ];
245
- expect(patchMap(elements, ["a"])).toEqual({ a: 4 });
230
+ expect(patchMap(elements, ["a"], "down")).toEqual({ a: 2 });
246
231
  });
247
232
 
248
- it("item 13: an edited clip whose own z is unresolved (non-finite) yields no patch", () => {
249
- // The edited clip itself couldn't be resolved → it is dropped from the working
250
- // set and produces nothing (no fabricated-0 self-patch).
233
+ it("an edited clip whose own z is unresolved (non-finite) yields no patch", () => {
251
234
  const elements = [el("a", 0, 0, 10, Number.NaN), el("b", 1, 0, 10, 5)];
252
- expect(patchMap(elements, ["a"])).toEqual({});
235
+ expect(patchMap(elements, ["a"], "up")).toEqual({});
253
236
  });
254
237
  });
255
238
 
256
- describe("computeStackingPatches — tie-aware cascade (lane move always realisable)", () => {
257
- it("drag below an overlapping z=0 neighbour → cascade bumps the neighbour, edit→0", () => {
258
- // edited `v` (z=2) dragged to the BOTTOM lane, overlapping `r` (z=0) which is
259
- // now on the upper lane. No z ≥ 0 fits strictly below 0, so the old resolver
260
- // clamped v to 0 (tied with r) and nothing changed on canvas — the reported
261
- // bug. Tie-aware: v→0 AND r bumped to 1 so r paints strictly above v.
262
- const elements = [el("v", 1, 0, 10, 2), el("r", 0, 0, 10, 0)];
263
- expect(patchMap(elements, ["v"])).toEqual({ v: 0, r: 1 });
264
- });
265
-
266
- it("equal-z + domIndex: dragging the LATER-in-DOM clip below is realised via a bump", () => {
267
- // Two equal-z clips; `v` is later in DOM (domIndex 1) so it currently paints
268
- // ON TOP of `r` (domIndex 0). User drags v to the lower lane (track 1). With
269
- // domIndex the sync SEES that v is currently above r and must be lowered:
270
- // v→0 (already 0, stays) then r bumped to 1 so r wins. Without domIndex the
271
- // equal z would look already-correct and under-patch.
239
+ describe("computeStackingPatches — only the moved clip changes", () => {
240
+ it("a full-frame scene on the top row stays behind a caption moved up a row under it", () => {
241
+ // Agent-made films often put the scene on track 0. The caption (z1, later in the
242
+ // file) moves from track 3 to 2, still under the scene's row: nothing is below
243
+ // it, so nothing changes. Lifting the scene over it would hide the caption.
244
+ const elements = [el("scene", 0, 0, 10, 0, false, 0), el("caption", 2, 0, 10, 1, false, 1)];
245
+ expect(patchMap(elements, ["caption"], "up")).toEqual({});
246
+ });
247
+
248
+ it("a caption moved to the top row comes in front of the full-frame scene", () => {
249
+ const elements = [el("scene", 0, 0, 10, 5, false, 0), el("caption", -0.5, 0, 10, 1, false, 1)];
250
+ expect(patchMap(elements, ["caption"], "up")).toEqual({ caption: 6 });
251
+ });
252
+
253
+ it("moved down under a z-0 clip earlier in the file: drops to 0 and the neighbour stays", () => {
254
+ // z never goes negative (it could paint behind the composition's background),
255
+ // and neighbours never move, so this order cannot be fully expressed.
256
+ const elements = [el("r", 0, 0, 10, 0, false, 0), el("v", 1, 0, 10, 2, false, 1)];
257
+ expect(patchMap(elements, ["v"], "down")).toEqual({ v: 0 });
258
+ });
259
+
260
+ it("moved down when it cannot go lower → no patch", () => {
272
261
  const elements = [el("r", 0, 0, 10, 0, false, 0), el("v", 1, 0, 10, 0, false, 1)];
273
- expect(patchMap(elements, ["v"])).toEqual({ r: 1 });
274
- });
275
-
276
- it("equal-z without domIndex is ambiguous → conservatively bumps to guarantee order", () => {
277
- // Same shape but NO domIndex: equal z is ambiguous, so the resolver cannot
278
- // prove v is already below r and patches to make the order explicit (r above).
279
- const elements = [el("r", 0, 0, 10, 0), el("v", 1, 0, 10, 0)];
280
- const out = patchMap(elements, ["v"]);
281
- // r must end up strictly above v (higher z) regardless of the exact numbers.
282
- const vz = out.v ?? 0;
283
- const rz = out.r ?? 0;
284
- expect(rz).toBeGreaterThan(vz);
285
- });
286
-
287
- it("#2198 (Abhai repro): a lift cascades transitively so an UNTOUCHED pair never inverts", () => {
288
- // m (z1, lane0, dom0), n (z0, lane1, dom1), e (z2, lane2, dom2, edited).
289
- // e overlaps n [5,10); n overlaps m [12,15); e does NOT overlap m.
290
- // Dragging e to the bottom lane forces n up to paint above e. A naive lift sets
291
- // n→1, which TIES m (z1) and — n being later in the DOM — paints n above m,
292
- // inverting the untouched (m,n) pair (which the next normalize would reshuffle).
293
- // The transitive cascade lifts m too (→2) so m stays strictly above n.
294
- const elements = [
295
- el("m", 0, 12, 8, 1, false, 0),
296
- el("n", 1, 5, 10, 0, false, 1),
297
- el("e", 2, 0, 10, 2, false, 2),
298
- ];
299
- expect(patchMap(elements, ["e"])).toEqual({ e: 0, n: 1, m: 2 });
262
+ expect(patchMap(elements, ["v"], "down")).toEqual({});
300
263
  });
301
264
 
302
- it("cascade patches as FEW clips as possible (only the blockers move)", () => {
303
- // v dragged to bottom under r(z0) and s(z0) both on higher lanes; a distant
304
- // non-overlapping clip x is never touched.
265
+ it("moved down never goes under a clip on a lower row it paints over now", () => {
266
+ // v cannot get under r (z0, earlier in the file); dropping it to 0 would hide it under w.
305
267
  const elements = [
306
- el("v", 2, 0, 10, 5),
307
- el("r", 0, 0, 10, 0),
308
- el("s", 1, 0, 10, 0),
309
- el("x", 3, 50, 10, 0), // no time overlap → untouched
268
+ el("r", 0, 0, 10, 0, false, 0),
269
+ el("v", 1, 0, 10, 2, false, 1),
270
+ el("w", 2, 0, 10, 1, false, 2),
310
271
  ];
311
- const out = patchMap(elements, ["v"]);
312
- expect("x" in out).toBe(false);
313
- // v below both r and s.
314
- expect(out.v).toBeLessThan(out.r ?? 0);
315
- expect(out.v).toBeLessThan(out.s ?? 0);
272
+ expect(patchMap(elements, ["v"], "down")).toEqual({});
316
273
  });
317
- });
318
274
 
319
- describe("computeStackingPatches — DOM tie-break gates the cascade (item 12)", () => {
320
- it("tie ACCEPTABLE: edited may sit AT minAbove when the above-neighbour is later in DOM → single patch, no neighbour bump", () => {
321
- // below b (z3, lane2, dom0), edited e (lane1, dom1), above a (z4, lane0, dom2).
322
- // e must paint above b and below a. There is no integer strictly between 3 and
323
- // 4, but a is LATER in the DOM, so e=4 ties a and a still paints on top by DOM
324
- // order — a valid SINGLE patch. The old gap<2 rule cascaded and needlessly
325
- // bumped a's authored z (the over-patch). Only e changes here.
326
- const elements = [
327
- el("b", 2, 0, 10, 3, false, 0),
328
- el("e", 1, 0, 10, 0, false, 1),
329
- el("a", 0, 0, 10, 4, false, 2),
330
- ];
331
- expect(patchMap(elements, ["e"])).toEqual({ e: 4 });
275
+ it("moved down sinks only as far as the clips on lower rows allow", () => {
276
+ const elements = [el("s", 0, 0, 10, 5), el("v", 1, 0, 10, 9), el("w", 2, 0, 10, 7)];
277
+ expect(patchMap(elements, ["v"], "down")).toEqual({ v: 8 });
332
278
  });
333
279
 
334
- it("tie INVERTING: edited tying minAbove would paint ABOVE it (earlier in DOM) → cascade bumps the neighbour", () => {
335
- // Same z's, but a is EARLIER in the DOM than e (dom0 vs dom2). Now e=4 would
336
- // tie a AND paint on top (e later in DOM), violating the lane order, so the
337
- // tie-break can't save it: e→4 and a is bumped to 5.
280
+ it("moved down with equal z: the clip above, later in the file, already paints over it", () => {
281
+ const elements = [el("v", 1, 0, 10, 3, false, 0), el("r", 0, 0, 10, 3, false, 1)];
282
+ expect(patchMap(elements, ["v"], "down")).toEqual({});
283
+ });
284
+
285
+ it("#2198: an untouched pair keeps its order because neighbours never move", () => {
286
+ // e overlaps n [5,10); n overlaps m [12,15); e does not overlap m.
338
287
  const elements = [
339
- el("a", 0, 0, 10, 4, false, 0),
340
- el("b", 2, 0, 10, 3, false, 1),
341
- el("e", 1, 0, 10, 0, false, 2),
288
+ el("m", 0, 12, 8, 1, false, 0),
289
+ el("n", 1, 5, 10, 0, false, 1),
290
+ el("e", 2, 0, 10, 2, false, 2),
342
291
  ];
343
- expect(patchMap(elements, ["e"])).toEqual({ e: 4, a: 5 });
292
+ expect(patchMap(elements, ["e"], "down")).toEqual({ e: 0 });
293
+ });
294
+
295
+ it("equal z broken by file order: the clip later in the file already paints above", () => {
296
+ const elements = [el("b", 1, 0, 10, 3, false, 0), el("e", 0, 0, 10, 3, false, 1)];
297
+ expect(patchMap(elements, ["e"], "up")).toEqual({});
298
+ });
299
+
300
+ it("equal z with no file order is ambiguous → the move still writes a z", () => {
301
+ const elements = [el("b", 1, 0, 10, 3), el("e", 0, 0, 10, 3)];
302
+ expect(patchMap(elements, ["e"], "up")).toEqual({ e: 4 });
344
303
  });
345
304
  });