mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-09 12:00:26 +00:00
* feat(studio): timeline collision and placement model What: new pure module timelineCollision — zone-aware drop placement (clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow, resolvePlacement, lane/overlap predicates) with its full test suite. Why: the no-overlap core of the NLE clip-drag engine; plain functions, no DOM, no React, no store writes. How: new files only; type-only imports from the existing playerStore. First runtime consumer arrives with the drag-engine PRs. Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow audit clean (all exports test-consumed). * feat(studio): timeline magnetic snapping What: new pure module timelineSnapping — snap-target collection and pixel-threshold time snapping (collectTimelineSnapTargets, snapTimelineTime, snapMoveToTargets) with tests. Why: the magnet math for clip drags/trims, reviewable standalone. How: new files only; type-only playerStore imports; consumers land with the drag engine. Test plan: bunx vitest run timelineSnapping.test.ts; tsc --noEmit; fallow audit clean. * feat(studio): multi-clip drag preview math What: new pure module timelineMultiDragPreview — group-drag passenger offsets and clamped group deltas (isMultiDragActive, multiDragDeltaSeconds, multiDragPassengerOffsetPx, clampGroupMoveDelta) with tests. Why: the group-drag math, standalone and DOM-free. How: new files only; consumed later by TimelineLanes. Test plan: bunx vitest run timelineMultiDragPreview.test.ts; tsc --noEmit; fallow audit clean. * feat(studio): timeline z-stacking sync model What: new pure module timelineStackingSync — lane order ↔ z-index reconciliation (laneIsAbove, computeStackingPatches) with tests. Why: the single source of truth for how timeline lane order maps to canvas stacking; the ordering rules and tie-breaks live here. How: new files only; consumed later by timelineZones and the stacking-sync hook. Test plan: bunx vitest run timelineStackingSync.test.ts; tsc --noEmit; fallow audit clean. * feat(studio): timeline lane-zone model What: new pure module timelineZones — visual/audio track-zone classification (classifyZone) and normalizeToZones, which re-packs lanes into zone-consistent rows; tests cover the stacking/zones interaction. Why: completes the z-model started in the stacking-sync PR. How: new files; consumes isAudioTimelineElement (leaf-helpers PR) and computeStackingPatches (stacking-sync PR); type-only playerStore imports. Test plan: bunx vitest run timelineZones.test.ts; tsc --noEmit; fallow audit clean. * feat(studio): asset click policy and canvas nudge gate What: two small pure modules with tests — assetClickBehavior (click vs double-click policy for sidebar assets) and canvasNudgeGate (debounce gate for arrow-key canvas nudges). Why: policy dependencies of the upcoming asset card and nudge hook, reviewable as plain decision tables. How: new files only. Test plan: bunx vitest run on both test files; tsc --noEmit; fallow audit clean. --------- Co-authored-by: ukimsanov <ular.kimsanov@heygen.com>
This commit is contained in:
co-authored by
ukimsanov
parent
44ffe4b41f
commit
8e5b18f740
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Pure helpers for CapCut-style asset card click behavior.
|
||||
*
|
||||
* Clicking an asset card that is ALREADY ADDED to the timeline selects the
|
||||
* corresponding clip. Clicking one NOT yet in the timeline opens a lightweight
|
||||
* preview overlay. Both behaviors are gated on "this was a click, not a drag".
|
||||
*
|
||||
* Pure — unit-tested.
|
||||
*/
|
||||
import type { TimelineElement } from "../player/store/playerStore";
|
||||
|
||||
/**
|
||||
* Find the TimelineElement that references `assetPath`, returning the one with
|
||||
* the earliest start time when multiple clips share the same source.
|
||||
*
|
||||
* Matching mirrors `deriveUsedPaths` in AssetsTab: an element's `src` may be a
|
||||
* fully-absolute URL, a server-relative `/api/projects/…/preview/…` path, a
|
||||
* `./`-prefixed relative path, or a bare relative path — all normalised to the
|
||||
* project-relative form that `assetPath` carries.
|
||||
*
|
||||
* Returns `null` when no element matches.
|
||||
*/
|
||||
export function findClipForAsset(
|
||||
elements: TimelineElement[],
|
||||
assetPath: string,
|
||||
): TimelineElement | null {
|
||||
let best: TimelineElement | null = null;
|
||||
for (const el of elements) {
|
||||
if (!el.src) continue;
|
||||
if (normalizeSrc(el.src) !== assetPath) continue;
|
||||
if (best === null || el.start < best.start) best = el;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise a raw element `src` to the bare project-relative path so it can be
|
||||
* compared against the asset-list strings (which have no leading slash, no
|
||||
* origin, no query string).
|
||||
*
|
||||
* Mirrors the logic in `deriveUsedPaths` (AssetsTab.tsx) — keep in sync.
|
||||
*/
|
||||
function normalizeSrc(src: string): string {
|
||||
let s = src;
|
||||
try {
|
||||
const u = new URL(s);
|
||||
s = u.pathname;
|
||||
} catch {
|
||||
// Not an absolute URL — leave as-is
|
||||
}
|
||||
s = s
|
||||
.replace(/^\/api\/projects\/[^/]+\/preview\//, "")
|
||||
.replace(/^\.?\//, "")
|
||||
.split(/[?#]/)[0];
|
||||
try {
|
||||
s = decodeURIComponent(s);
|
||||
} catch {
|
||||
// Malformed encoding — use as-is
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Drag-detection threshold in pixels — movements within this are treated as clicks. */
|
||||
export const DRAG_THRESHOLD_PX = 4;
|
||||
|
||||
/**
|
||||
* Determine whether a pointer-up event should be treated as a click given the
|
||||
* total pointer displacement since pointer-down.
|
||||
*
|
||||
* @param dx Horizontal distance moved in pixels.
|
||||
* @param dy Vertical distance moved in pixels.
|
||||
*/
|
||||
export function isPointerClick(dx: number, dy: number): boolean {
|
||||
return Math.abs(dx) < DRAG_THRESHOLD_PX && Math.abs(dy) < DRAG_THRESHOLD_PX;
|
||||
}
|
||||
Reference in New Issue
Block a user