diff --git a/packages/studio/src/components/editor/LayersPanel.tsx b/packages/studio/src/components/editor/LayersPanel.tsx index 52ecaa8a4..ceba38168 100644 --- a/packages/studio/src/components/editor/LayersPanel.tsx +++ b/packages/studio/src/components/editor/LayersPanel.tsx @@ -8,7 +8,10 @@ import { import { useStudioContext } from "../../contexts/StudioContext"; import { useDomEditContext } from "../../contexts/DomEditContext"; import { usePlayerStore } from "../../player"; -import { findMatchingTimelineElementId } from "../../utils/studioHelpers"; +import { + findMatchingTimelineElementId, + resolveTimelineSelectionSeekTime, +} from "../../utils/studioHelpers"; import { Layers } from "../../icons/SystemIcons"; const TAG_ICONS: Record = { @@ -49,8 +52,14 @@ interface CollapsedState { } export const LayersPanel = memo(function LayersPanel() { - const { previewIframeRef, activeCompPath, refreshKey, compositionLoading, timelineElements } = - useStudioContext(); + const { + previewIframeRef, + activeCompPath, + refreshKey, + compositionLoading, + timelineElements, + currentTime, + } = useStudioContext(); const { domEditSelection, applyDomSelection, updateDomEditHoverSelection } = useDomEditContext(); const [layers, setLayers] = useState([]); @@ -140,11 +149,12 @@ export const LayersPanel = memo(function LayersPanel() { if (matchedId) { const el = timelineElements.find((e) => (e.key ?? e.id) === matchedId); if (el) { - usePlayerStore.getState().requestSeek(el.start + el.duration / 2); + const nextTime = resolveTimelineSelectionSeekTime(currentTime, el); + if (nextTime != null) usePlayerStore.getState().requestSeek(nextTime); } } }, - [resolveSelection, timelineElements], + [currentTime, resolveSelection, timelineElements], ); const handleSelectLayer = useCallback( diff --git a/packages/studio/src/utils/studioHelpers.test.ts b/packages/studio/src/utils/studioHelpers.test.ts new file mode 100644 index 000000000..9172b4460 --- /dev/null +++ b/packages/studio/src/utils/studioHelpers.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { resolveTimelineSelectionSeekTime } from "./studioHelpers"; + +describe("resolveTimelineSelectionSeekTime", () => { + it("keeps the current time when it is already inside the clip range", () => { + expect(resolveTimelineSelectionSeekTime(3, { start: 0, duration: 5 })).toBe(3); + }); + + it("clamps to the clip start when current time is before the clip", () => { + expect(resolveTimelineSelectionSeekTime(1, { start: 4, duration: 3 })).toBe(4); + }); + + it("clamps to the clip end when current time is after the clip", () => { + expect(resolveTimelineSelectionSeekTime(10, { start: 4, duration: 3 })).toBe(7); + }); + + it("falls back to the clip start for invalid current time", () => { + expect(resolveTimelineSelectionSeekTime(Number.NaN, { start: 2, duration: 5 })).toBe(2); + }); +}); diff --git a/packages/studio/src/utils/studioHelpers.ts b/packages/studio/src/utils/studioHelpers.ts index d5a23adff..e676c1545 100644 --- a/packages/studio/src/utils/studioHelpers.ts +++ b/packages/studio/src/utils/studioHelpers.ts @@ -158,6 +158,20 @@ export function findMatchingTimelineElementId( return null; } +export function resolveTimelineSelectionSeekTime( + currentTime: number, + element: Pick | null | undefined, +): number | null { + if (!element) return null; + if (!Number.isFinite(element.start) || !Number.isFinite(element.duration)) return null; + + const start = Math.max(0, element.start); + const end = Math.max(start, start + Math.max(0, element.duration)); + const time = Number.isFinite(currentTime) ? currentTime : start; + + return clampNumber(time, start, end); +} + export function clampNumber(value: number, min: number, max: number): number { if (max < min) return min; return Math.min(Math.max(value, min), max);