mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
feat(studio): drag a clip past the video end to extend its duration
Dragging a timeline clip (or its right resize edge) past the end of the video now extends the composition duration on drop, instead of clamping the clip at the current end. Extend-only and undoable. - Relax the move/resize horizontal clamps that pinned a clip's end at the current duration; effectiveDuration now folds in the active drag/resize preview so the ruler and track width grow live as you drag past the end. - On drop, extend the root composition data-duration (and the store) when the clip's new end exceeds it, via a shared extendRootDurationInSource helper extracted from the block installer (now the single owner of that logic). An extending edit routes through the server persist path since the SDK setTiming op can't express the root composition's own duration.
This commit is contained in:
@@ -8,6 +8,7 @@ import { collectHtmlIds } from "./studioHelpers";
|
||||
import { formatTimelineAttributeNumber } from "../player/components/timelineEditing";
|
||||
import { saveProjectFilesWithHistory } from "./studioFileHistory";
|
||||
import type { EditHistoryKind } from "./editHistory";
|
||||
import { extendRootDurationInSource } from "./rootDuration";
|
||||
|
||||
function getMaxZIndexFromIframe(iframe: HTMLIFrameElement | null): number {
|
||||
try {
|
||||
@@ -163,20 +164,7 @@ export async function addBlockToProject(
|
||||
].join("\n");
|
||||
|
||||
let patchedContent = insertTimelineAssetIntoSource(originalContent, subCompHtml);
|
||||
|
||||
const newEnd = start + duration;
|
||||
const rootDurMatch = patchedContent.match(
|
||||
/(<[^>]*data-composition-id="[^"]*"[^>]*data-duration=")([^"]*)(")/,
|
||||
);
|
||||
if (rootDurMatch) {
|
||||
const rootDur = parseFloat(rootDurMatch[2]!);
|
||||
if (newEnd > rootDur) {
|
||||
patchedContent = patchedContent.replace(
|
||||
rootDurMatch[0],
|
||||
`${rootDurMatch[1]}${formatTimelineAttributeNumber(newEnd)}${rootDurMatch[3]}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
patchedContent = extendRootDurationInSource(patchedContent, start + duration);
|
||||
|
||||
await saveProjectFilesWithHistory({
|
||||
projectId,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extendRootDurationInSource } from "./rootDuration";
|
||||
|
||||
describe("extendRootDurationInSource", () => {
|
||||
it("extends data-duration when the new end is bigger than the root duration", () => {
|
||||
const source = [
|
||||
`<div data-composition-id="main" data-duration="4">`,
|
||||
` <section id="clip" data-start="2" data-duration="3"></section>`,
|
||||
`</div>`,
|
||||
].join("\n");
|
||||
|
||||
expect(extendRootDurationInSource(source, 5.25)).toContain(
|
||||
`data-composition-id="main" data-duration="5.25"`,
|
||||
);
|
||||
});
|
||||
|
||||
it("does nothing when the new end is smaller than or equal to the root duration", () => {
|
||||
const source = `<div data-composition-id="main" data-duration="6"></div>`;
|
||||
|
||||
expect(extendRootDurationInSource(source, 5)).toBe(source);
|
||||
expect(extendRootDurationInSource(source, 6)).toBe(source);
|
||||
});
|
||||
|
||||
it("leaves non-root data-duration attributes untouched by the extension", () => {
|
||||
const source = [
|
||||
`<div data-duration="3"></div>`,
|
||||
`<div data-composition-id="main" data-duration="4"></div>`,
|
||||
].join("\n");
|
||||
const patched = extendRootDurationInSource(source, 7);
|
||||
|
||||
expect(patched).toContain(`<div data-duration="3"></div>`);
|
||||
expect(patched).toContain(`<div data-composition-id="main" data-duration="7"></div>`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { formatTimelineAttributeNumber } from "../player/components/timelineEditing";
|
||||
|
||||
export function extendRootDurationInSource(source: string, newEnd: number): string {
|
||||
const rootDurMatch = source.match(
|
||||
/(<[^>]*data-composition-id="[^"]*"[^>]*data-duration=")([^"]*)(")/,
|
||||
);
|
||||
if (rootDurMatch) {
|
||||
const rootDur = parseFloat(rootDurMatch[2]!);
|
||||
if (newEnd > rootDur) {
|
||||
return source.replace(
|
||||
rootDurMatch[0],
|
||||
`${rootDurMatch[1]}${formatTimelineAttributeNumber(newEnd)}${rootDurMatch[3]}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return source;
|
||||
}
|
||||
Reference in New Issue
Block a user