refactor(studio): single-source timeline id-resolution and resize-clamp math

Extract resolveTimelineIdForSelection so DOM-to-timeline id mapping lives in one place
with a single sourceFile / activeCompPath / index.html fallback, fixing a sub-composition
selection that previously diverged between callers.

Extract shared start-trim delta helpers used by both single-clip and group resize, and
remove the never-called refreshDomEditGroupSelectionsFromPreview.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-09 16:54:34 -04:00
parent 95ded6c026
commit 3c6c1d3f27
6 changed files with 149 additions and 129 deletions
@@ -4,7 +4,11 @@ import type { StackingTimelineLayer, TimelineLayerId } from "./timelineTrackOrde
import { resolveTimelineLayerStackingMove } from "./timelineLayerDrag";
import type { TimelineStackingElement, TimelineStackingReorderIntent } from "./timelineStacking";
import { resolveTimelineMinDuration } from "./timelineGroupEditing";
import {
applyClipStartTrimDelta,
clipStartTrimDeltaBounds,
resolveTimelineMinDuration,
} from "./timelineGroupEditing";
export {
clampTimelineGroupResizeDelta,
@@ -220,23 +224,15 @@ export function resolveTimelineResize(
};
}
const playbackRate = Math.max(0.1, input.playbackRate ?? 1);
const maxLeftExtensionFromMedia =
input.playbackStart != null ? input.playbackStart / playbackRate : Number.POSITIVE_INFINITY;
const minDelta = -Math.min(input.start - input.minStart, maxLeftExtensionFromMedia);
const maxDelta = input.duration - minDuration;
const { minDelta, maxDelta } = clipStartTrimDeltaBounds(input, input.minStart, minDuration);
const clampedDelta = clamp(deltaTime, minDelta, maxDelta);
const nextStart = roundToCentiseconds(input.start + clampedDelta);
const nextDuration = roundToCentiseconds(input.duration - clampedDelta);
const nextPlaybackStart =
input.playbackStart != null
? roundToCentiseconds(Math.max(0, input.playbackStart + clampedDelta * playbackRate))
: undefined;
const trimmed = applyClipStartTrimDelta(input, clampedDelta);
return {
start: nextStart,
duration: nextDuration,
playbackStart: nextPlaybackStart,
start: roundToCentiseconds(trimmed.start),
duration: roundToCentiseconds(trimmed.duration),
playbackStart:
trimmed.playbackStart != null ? roundToCentiseconds(trimmed.playbackStart) : undefined,
};
}
@@ -15,6 +15,58 @@ export function resolveTimelineMinDuration(minDuration?: number): number {
return Math.max(ABSOLUTE_TIMELINE_MIN_DURATION, minDuration ?? DEFAULT_TIMELINE_MIN_DURATION);
}
/** Playback rate never drops to zero (would make media-in-point math divide by ~0). */
function resolveTimelinePlaybackRate(rate?: number): number {
return Math.max(0.1, rate ?? 1);
}
interface TimelineStartTrimClip {
start: number;
duration: number;
playbackStart?: number;
playbackRate?: number;
}
/**
* Delta bounds for trimming a clip's START edge (shared by single-clip and group
* resize). Left-bounded by how far the start can move toward `minStart` and by the
* media in-point (`playbackStart / playbackRate`); right-bounded by `minDuration`.
* Returned deltas are unrounded — callers round with their own centisecond helper.
*/
export function clipStartTrimDeltaBounds(
clip: TimelineStartTrimClip,
minStart: number,
minDuration: number,
): { minDelta: number; maxDelta: number } {
const playbackRate = resolveTimelinePlaybackRate(clip.playbackRate);
const maxLeftExtensionFromMedia =
clip.playbackStart != null ? clip.playbackStart / playbackRate : Number.POSITIVE_INFINITY;
return {
minDelta: -Math.min(clip.start - minStart, maxLeftExtensionFromMedia),
maxDelta: clip.duration - minDuration,
};
}
/**
* Apply a start-edge delta to one clip (unrounded): moves the start, shrinks the
* duration by the same amount, and shifts the media in-point by the delta scaled to
* the playback rate (clamped at 0).
*/
export function applyClipStartTrimDelta(
clip: TimelineStartTrimClip,
delta: number,
): { start: number; duration: number; playbackStart?: number } {
const playbackRate = resolveTimelinePlaybackRate(clip.playbackRate);
return {
start: clip.start + delta,
duration: clip.duration - delta,
playbackStart:
clip.playbackStart != null
? Math.max(0, clip.playbackStart + delta * playbackRate)
: undefined,
};
}
export interface TimelineGroupTimingMember {
start: number;
duration: number;
@@ -70,17 +122,10 @@ export function clampTimelineGroupResizeDelta(
return roundTimelineTime(Math.max(rawDelta, minDelta));
}
const minDelta = Math.max(
...members.map((member) => {
const playbackRate = Math.max(0.1, member.playbackRate ?? 1);
const maxLeftExtensionFromMedia =
member.playbackStart != null
? member.playbackStart / playbackRate
: Number.POSITIVE_INFINITY;
return -Math.min(member.start, maxLeftExtensionFromMedia);
}),
);
const maxDelta = Math.min(...members.map((member) => member.duration - minDuration));
// Rigid group: the applied delta is bounded by the most-constrained member.
const bounds = members.map((member) => clipStartTrimDeltaBounds(member, 0, minDuration));
const minDelta = Math.max(...bounds.map((b) => b.minDelta));
const maxDelta = Math.min(...bounds.map((b) => b.maxDelta));
return roundTimelineTime(clamp(rawDelta, minDelta, maxDelta));
}
@@ -102,14 +147,12 @@ export function resolveTimelineGroupResize(
};
}
const playbackRate = Math.max(0.1, member.playbackRate ?? 1);
const trimmed = applyClipStartTrimDelta(member, delta);
return {
start: roundTimelineTime(member.start + delta),
duration: roundTimelineTime(member.duration - delta),
start: roundTimelineTime(trimmed.start),
duration: roundTimelineTime(trimmed.duration),
playbackStart:
member.playbackStart != null
? roundTimelineTime(Math.max(0, member.playbackStart + delta * playbackRate))
: undefined,
trimmed.playbackStart != null ? roundTimelineTime(trimmed.playbackStart) : undefined,
};
}),
};