@hyperframes/studio 0.8.79 → 0.8.80

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 (74) hide show
  1. package/dist/assets/hyperframes-player-CgnoxDoD.js +464 -0
  2. package/dist/assets/{index-DBKySIo7.js → index-CsG24Jub.js} +190 -187
  3. package/dist/assets/{index-CJP92F4-.js → index-DZGVvqDT.js} +1 -1
  4. package/dist/assets/{index-ORZXYB0k.js → index-J9HXe7mU.js} +1 -1
  5. package/dist/index.d.ts +18 -1
  6. package/dist/index.html +1 -1
  7. package/dist/index.js +1491 -1177
  8. package/dist/index.js.map +1 -1
  9. package/package.json +8 -8
  10. package/src/App.tsx +5 -1
  11. package/src/components/editor/TimelineFxPopover.test.tsx +1 -1
  12. package/src/components/editor/TimelineFxPopover.tsx +1 -1
  13. package/src/components/editor/manualEdits.ts +1 -0
  14. package/src/components/editor/manualEditsParsing.ts +4 -0
  15. package/src/components/editor/useFxAudition.ts +3 -3
  16. package/src/components/nle/useTimelineEditCallbacks.ts +4 -0
  17. package/src/contexts/DomEditContext.tsx +4 -0
  18. package/src/contexts/TimelineEditContext.tsx +2 -0
  19. package/src/hooks/liveLanes.ts +147 -0
  20. package/src/hooks/timelineAudioGroupVolume.ts +81 -47
  21. package/src/hooks/timelineEditPermission.ts +9 -1
  22. package/src/hooks/timelineEditingHelpers.test.ts +16 -0
  23. package/src/hooks/timelineEditingHelpers.ts +69 -43
  24. package/src/hooks/timelineElementFxAttribute.ts +82 -36
  25. package/src/hooks/useAppHotkeys.ts +1 -1
  26. package/src/hooks/useAutomationSelectionKeyboard.test.tsx +125 -21
  27. package/src/hooks/useAutomationSelectionKeyboard.ts +21 -25
  28. package/src/hooks/useBlockHandlers.test.tsx +65 -0
  29. package/src/hooks/useBlockHandlers.ts +7 -4
  30. package/src/hooks/useDomEditAttributeCommits.ts +3 -17
  31. package/src/hooks/useDomEditSession.ts +1 -0
  32. package/src/hooks/useEditHistoryActions.ts +4 -3
  33. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +49 -0
  34. package/src/hooks/useExternalFileChangeCoordinator.ts +15 -6
  35. package/src/hooks/useTimelineEditing.ts +19 -16
  36. package/src/hooks/useTimelineGroupEditing.ts +2 -1
  37. package/src/hooks/useToast.ts +2 -1
  38. package/src/player/components/ImageThumbnail.tsx +3 -12
  39. package/src/player/components/Player.test.ts +23 -0
  40. package/src/player/components/Player.tsx +4 -2
  41. package/src/player/components/TimelineAutomationLane.test.tsx +38 -0
  42. package/src/player/components/TimelineAutomationLane.tsx +18 -13
  43. package/src/player/components/TimelineAutomationLaneSlot.test.tsx +0 -2
  44. package/src/player/components/TimelineClipFades.test.tsx +24 -3
  45. package/src/player/components/TimelineClipFades.tsx +5 -2
  46. package/src/player/components/TimelineFxButton.tsx +1 -1
  47. package/src/player/components/TimelineGroupHeader.tsx +1 -1
  48. package/src/player/components/TimelineGroupRow.test.tsx +0 -1
  49. package/src/player/components/TimelineGroupRow.tsx +17 -11
  50. package/src/player/components/TimelineTrackHeader.tsx +21 -6
  51. package/src/player/components/groupAutomationElement.ts +9 -1
  52. package/src/player/components/timelineCallbacks.ts +2 -0
  53. package/src/player/components/timelineClipDragPreview.test.ts +41 -0
  54. package/src/player/components/timelineClipDragPreview.ts +15 -24
  55. package/src/player/components/timelineEditing.test.ts +99 -1
  56. package/src/player/components/timelineEditing.ts +5 -8
  57. package/src/player/components/timelineGroupEditing.test.ts +27 -0
  58. package/src/player/components/timelineGroupEditing.ts +18 -15
  59. package/src/player/components/timelineStackingSync.ts +5 -5
  60. package/src/player/components/timelineZones.ts +2 -3
  61. package/src/player/components/useAutomationEdgeStretch.ts +4 -4
  62. package/src/player/components/useAutomationLaneGestures.ts +1 -1
  63. package/src/player/components/useAutomationLanes.save.test.tsx +1406 -0
  64. package/src/player/components/useAutomationLanes.ts +58 -74
  65. package/src/player/lib/automationStoreSync.ts +30 -3
  66. package/src/player/lib/thumbnailImageDecoder.test.ts +82 -0
  67. package/src/player/lib/thumbnailImageDecoder.ts +73 -0
  68. package/src/player/lib/timelineDOM.test.ts +28 -8
  69. package/src/player/lib/timelineElementHelpers.ts +3 -4
  70. package/src/player/lib/timelineGroupInfo.ts +23 -23
  71. package/src/utils/studioFileHistory.ts +36 -30
  72. package/dist/assets/hyperframes-player-Bx_Kw4Ey.js +0 -464
  73. package/src/player/components/automationGestureKeys.test.ts +0 -49
  74. package/src/player/components/automationGestureKeys.ts +0 -49
@@ -13,6 +13,12 @@ function roundTimelineTime(value: number): number {
13
13
  return roundToCenti(value);
14
14
  }
15
15
 
16
+ const SAVED_MEDIA_OFFSET_TOLERANCE = 1e-5;
17
+
18
+ function ceilTimelineTime(value: number): number {
19
+ return Math.ceil((value - SAVED_MEDIA_OFFSET_TOLERANCE) * 100) / 100;
20
+ }
21
+
16
22
  export function resolveTimelineMinDuration(minDuration?: number): number {
17
23
  return Math.max(ABSOLUTE_TIMELINE_MIN_DURATION, minDuration ?? DEFAULT_TIMELINE_MIN_DURATION);
18
24
  }
@@ -33,7 +39,7 @@ interface TimelineStartTrimClip {
33
39
  * Delta bounds for trimming a clip's START edge (shared by single-clip and group
34
40
  * resize). Left-bounded by how far the start can move toward `minStart` and by the
35
41
  * media in-point (`playbackStart / playbackRate`); right-bounded by `minDuration`.
36
- * Returned deltas are unrounded — callers round with their own centisecond helper.
42
+ * Returned deltas are unrounded; applyClipStartTrimDelta does the rounding.
37
43
  */
38
44
  export function clipStartTrimDeltaBounds(
39
45
  clip: TimelineStartTrimClip,
@@ -50,21 +56,24 @@ export function clipStartTrimDeltaBounds(
50
56
  }
51
57
 
52
58
  /**
53
- * Apply a start-edge delta to one clip (unrounded): moves the start, shrinks the
54
- * duration by the same amount, and shifts the media in-point by the delta scaled to
55
- * the playback rate (clamped at 0).
59
+ * Apply a start-edge delta to one clip: rounds the start once, keeps the end fixed,
60
+ * and derives the media in-point from the rounded start (clamped at 0), so the media
61
+ * clock `start - playbackStart / playbackRate` survives any number of head trims.
56
62
  */
57
63
  export function applyClipStartTrimDelta(
58
64
  clip: TimelineStartTrimClip,
59
65
  delta: number,
60
66
  ): { start: number; duration: number; playbackStart?: number } {
61
67
  const playbackRate = resolveTimelinePlaybackRate(clip.playbackRate);
68
+ const mediaZero =
69
+ clip.playbackStart != null ? clip.start - clip.playbackStart / playbackRate : -Infinity;
70
+ const start = Math.max(roundTimelineTime(clip.start + delta), ceilTimelineTime(mediaZero));
62
71
  return {
63
- start: clip.start + delta,
64
- duration: clip.duration - delta,
72
+ start,
73
+ duration: roundTimelineTime(clip.start + clip.duration - start),
65
74
  playbackStart:
66
75
  clip.playbackStart != null
67
- ? Math.max(0, clip.playbackStart + delta * playbackRate)
76
+ ? Math.max(0, clip.playbackStart + (start - clip.start) * playbackRate)
68
77
  : undefined,
69
78
  };
70
79
  }
@@ -126,7 +135,7 @@ export function clampTimelineGroupResizeDelta(
126
135
 
127
136
  // Rigid group: the applied delta is bounded by the most-constrained member.
128
137
  const bounds = members.map((member) => clipStartTrimDeltaBounds(member, 0, minDuration));
129
- const minDelta = Math.max(...bounds.map((b) => b.minDelta));
138
+ const minDelta = ceilTimelineTime(Math.max(...bounds.map((b) => b.minDelta)));
130
139
  const maxDelta = Math.min(...bounds.map((b) => b.maxDelta));
131
140
  return roundTimelineTime(clamp(rawDelta, minDelta, maxDelta));
132
141
  }
@@ -149,13 +158,7 @@ export function resolveTimelineGroupResize(
149
158
  };
150
159
  }
151
160
 
152
- const trimmed = applyClipStartTrimDelta(member, delta);
153
- return {
154
- start: roundTimelineTime(trimmed.start),
155
- duration: roundTimelineTime(trimmed.duration),
156
- playbackStart:
157
- trimmed.playbackStart != null ? roundTimelineTime(trimmed.playbackStart) : undefined,
158
- };
161
+ return applyClipStartTrimDelta(member, delta);
159
162
  }),
160
163
  };
161
164
  }
@@ -17,6 +17,8 @@
17
17
  * style) and apply the returned `StackingPatch[]` however they persist styles.
18
18
  */
19
19
 
20
+ import { spansOverlap } from "@hyperframes/core/clip-facts";
21
+
20
22
  /** Minimal element view this module reasons over. */
21
23
  export interface StackingElement {
22
24
  /** Stable identity (TimelineElement.key ?? id). */
@@ -69,8 +71,6 @@ export interface StackingPatch {
69
71
  zIndex: number;
70
72
  }
71
73
 
72
- const EPS = 1e-6;
73
-
74
74
  /**
75
75
  * Canonical paint-scope key: leaf z-indexes are comparable only within the same
76
76
  * source document and CSS stacking context. The ONLY place this normalization
@@ -90,17 +90,17 @@ export function samePaintScope(
90
90
  /**
91
91
  * Two clips overlap in time when their half-open [start, end) intervals intersect.
92
92
  *
93
- * NOTE the `- EPS`: this DELIBERATELY diverges from `timeRangesOverlap`'s exact
93
+ * NOTE the float slack: this DELIBERATELY diverges from `timeRangesOverlap`'s exact
94
94
  * strict-`<` (timelineCollision.ts). A boolean collision decision is idempotent, so
95
95
  * exact `<` is fine there; here the result drives a VISIBLE stacking re-lane, so the
96
- * epsilon guards against float fuzz (e.g. 5.0000001 vs 5) spuriously overlapping two
96
+ * epsilon guards against float fuzz (e.g. 19.8 + 6.4 vs 26.2) spuriously overlapping two
97
97
  * abutting clips and shuffling lanes. The two are intended to differ, not align.
98
98
  */
99
99
  function overlapsInTime(
100
100
  a: Pick<StackingElement, "start" | "duration">,
101
101
  b: Pick<StackingElement, "start" | "duration">,
102
102
  ): boolean {
103
- return a.start < b.start + b.duration - EPS && b.start < a.start + a.duration - EPS;
103
+ return spansOverlap(a.start, a.start + a.duration, b.start, b.start + b.duration);
104
104
  }
105
105
 
106
106
  /**
@@ -1,3 +1,4 @@
1
+ import { spansOverlap } from "@hyperframes/core/clip-facts";
1
2
  import type { TimelineElement } from "../store/playerStore";
2
3
  import { isAudioTimelineElement } from "../../utils/timelineInspector";
3
4
  import { isTransitionPair } from "./timelineTransitionSeams";
@@ -23,11 +24,9 @@ export function isMainTrackElement(el: TimelineElement): boolean {
23
24
 
24
25
  const keyOf = (el: TimelineElement) => el.key ?? el.id;
25
26
 
26
- const EPS = 1e-6;
27
-
28
27
  /** Two clips overlap when their half-open [start, end) intervals intersect. */
29
28
  function overlaps(a: TimelineElement, b: TimelineElement): boolean {
30
- return a.start < b.start + b.duration - EPS && b.start < a.start + a.duration - EPS;
29
+ return spansOverlap(a.start, a.start + a.duration, b.start, b.start + b.duration);
31
30
  }
32
31
 
33
32
  /** Deterministic order on the stable clip id (never the mutated lane/track). */
@@ -51,7 +51,7 @@ export interface UseAutomationEdgeStretchInput {
51
51
  /** Pointer position as a clip-local time and a parameter value. */
52
52
  pointAt(clientX: number, clientY: number): { t: number; v: number };
53
53
  xOf(t: number): number;
54
- commitPoints(points: HfAutomationLane["points"], persist: boolean): void;
54
+ commitPoints(points: HfAutomationLane["points"], persist: boolean, ended?: boolean): void;
55
55
  /** Clamp bound for the dragged edge. */
56
56
  duration: number;
57
57
  readOnly?: boolean | undefined;
@@ -252,9 +252,9 @@ export function useAutomationEdgeStretch({
252
252
  onHint(null);
253
253
  if (!crossed.current) return;
254
254
  crossed.current = false;
255
- // A live write is a preview, so putting the snapshot back through the same
256
- // channel is the whole revert — there is nothing persisted to undo.
257
- commitPoints(points, false);
255
+ // Nothing was persisted: the snapshot goes back as the last live write, which
256
+ // also ends the lane's gesture so its before-value does not outlive it.
257
+ commitPoints(points, false, true);
258
258
  onRangeSelect?.(origin.t0, origin.t1, origin.v0, origin.v1);
259
259
  }, [drag, onHint, commitPoints, onRangeSelect]);
260
260
 
@@ -46,7 +46,7 @@ export interface UseAutomationLaneGesturesInput {
46
46
  pointAt(clientX: number, clientY: number): { t: number; v: number };
47
47
  xOf(t: number): number;
48
48
  yOf(v: number): number;
49
- commitPoints(points: HfAutomationLane["points"], persist: boolean): void;
49
+ commitPoints(points: HfAutomationLane["points"], persist: boolean, ended?: boolean): void;
50
50
  /** Clip-local times a dragged point snaps to, on top of its own neighbours. */
51
51
  snapTimes?: readonly number[] | undefined;
52
52
  readOnly?: boolean | undefined;