Files
hyperframes/packages/studio/src/player/components/timelineStacking.ts
Miguel Angel Simon Sierra ed8bf475d3 feat(studio): lane-model timeline (non-overlapping clips share a row)
Rework the timeline row model from one-row-per-stacking-layer to NLE
"lanes": a row holds a time-sequence of non-overlapping clips, ordered
top = front. Stacking only matters between clips that overlap in time.

- Lane packing: per stacking context, sort clips by effective z-index
  desc (DOM order tiebreak) and greedily pack each onto the first lane
  whose members don't overlap it in time; else open a new lane. Lane's
  representative z is the max member z. Audio stays one clip per lane.
- Lane-aware drag: dropping onto a lane joins it (clip takes the lane's
  z) when there's no time-conflict; a conflicting drop is rejected and
  the preview snaps to the nearest valid lane or a new-lane insertion.
  Between/above/below still create a new lane at that stacking level.
- Overlap checks use the drag-preview start/duration, so conflict is
  judged by where the clip lands, not where it started.

Only explicit drags write z-index; authored z is preserved and
data-track-index is never rewritten.
2026-07-08 23:46:26 -04:00

63 lines
1.8 KiB
TypeScript

import { getTimelineElementIdentity } from "../lib/timelineElementHelpers";
export interface TimelineStackingElement {
id: string;
key?: string;
tag?: string;
start: number;
duration: number;
track: number;
zIndex?: number;
stackingContextId?: string | null;
parentCompositionId?: string | null;
compositionAncestors?: string[];
// Locator for resolving the live element at commit time (sub-comp children
// aren't in the top-level element list, so the reorder intent must be
// self-contained rather than re-looked-up by identity).
domId?: string;
selector?: string;
selectorIndex?: number;
sourceFile?: string;
}
export interface TimelineStackingOrderItem {
key: string;
track: number;
zIndex: number;
stackingContextId: string | null;
parentCompositionId: string | null;
compositionAncestors: readonly string[];
}
export type TimelineLayerDropPlacement =
| { type: "onto"; layerId: string }
| { type: "between"; beforeLayerId: string; afterLayerId: string }
| { type: "above"; layerId: string }
| { type: "below"; layerId: string };
export interface TimelineStackingZIndexChange {
key: string;
zIndex: number;
domId?: string;
selector?: string;
selectorIndex?: number;
sourceFile?: string;
}
export interface TimelineStackingReorderIntent {
contextKey: string;
placement: TimelineLayerDropPlacement;
zIndexChanges: TimelineStackingZIndexChange[];
}
export function toStackingOrderItem(element: TimelineStackingElement): TimelineStackingOrderItem {
return {
key: getTimelineElementIdentity(element),
track: element.track,
zIndex: element.zIndex ?? 0,
stackingContextId: element.stackingContextId ?? null,
parentCompositionId: element.parentCompositionId ?? null,
compositionAncestors: element.compositionAncestors ?? [],
};
}