diff --git a/CHANGELOG.md b/CHANGELOG.md index c06815d..9dc86a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to ComfyUI-AusBoss are documented here. +## Unreleased + +- **The video clip's IN handle always drags.** On Video Crop + Rotate + Pad + (Clip), a Length as long as the clip or longer used to lock IN, and the + only way to trim the start was to turn Length off. Now IN drags anyway: + the Length then only caps the clip, and the clip's own end is OUT. A + Length shorter than the clip works as before. + ## 2.25.1 - 2026-11-13 - **Edit Lock Restore 🆎 takes out the pink tint.** MiniMax H3 Image gives diff --git a/js/docs/AUSBOSS_NODES_VideoCropRotatePadClip.md b/js/docs/AUSBOSS_NODES_VideoCropRotatePadClip.md index f1c6798..37c311c 100644 --- a/js/docs/AUSBOSS_NODES_VideoCropRotatePadClip.md +++ b/js/docs/AUSBOSS_NODES_VideoCropRotatePadClip.md @@ -60,8 +60,9 @@ model that paints black regions. sits that many frames after IN and follows it when you drag IN, and dragging OUT or typing the number changes the Length. To move the whole clip, drag IN or the small grip in the middle of the kept part (the grip also slides a trimmed window with - Length off). When the Length is the whole clip there is nowhere to move it, and the - row says so: "= the whole clip. Shorten it to move IN." The Length stays when you + Length off). When the Length is as long as the clip or longer, the row says + "= the whole clip": the clip's own end is OUT, and dragging IN starts the clip + later without changing the Length. The Length stays when you swap the video, so a workflow built for 97 frames keeps taking 97. Turning it on or off never moves anything: the count becomes the OUT, or the OUT the count. If the source ends first, the footer says how many frames are left. **To start** moves IN diff --git a/js/shared/timeline_math.mjs b/js/shared/timeline_math.mjs index 557406b..f4ab4a4 100644 --- a/js/shared/timeline_math.mjs +++ b/js/shared/timeline_math.mjs @@ -217,6 +217,16 @@ export function latestFirstFor(info, frames, everyNth = 1) { return Math.max(0, info.count - 1 - (lastFrameFor(0, frames, everyNth))); } +// How far a drag can move IN while a Length is on. A Length that fits stops +// IN where OUT meets the source's end, so the count stays whole. A Length +// as long as the clip or longer cannot keep its count anywhere: it only caps +// the clip, the source's end is OUT, and IN moves over the whole clip. +export function latestInFor(info, frames, everyNth = 1) { + if (!info?.count) return 0; + const latest = latestFirstFor(info, frames, everyNth); + return latest > 0 ? latest : info.count - 1; +} + // Everything the rail draws for the clip node, from the stored values: // mode which way sets the length (lengthMode) // first IN, the first source frame the run keeps diff --git a/js/shared/transform_trim.mjs b/js/shared/transform_trim.mjs index 5b795bb..a41aaf2 100644 --- a/js/shared/transform_trim.mjs +++ b/js/shared/transform_trim.mjs @@ -32,7 +32,7 @@ import { framesThrough, keyboardStep, lastFrameFor, - latestFirstFor, + latestInFor, railZone, setTrimFrame, slideRange, @@ -235,8 +235,9 @@ export function mountTransformTrim({ get, set, has = () => true, driven = () => let parked; if (edge === "start") { if (current.mode === "length") { - // A Length drags the whole window, stopping where OUT meets the end. - parked = Math.max(0, Math.min(clampFrame(frame, clip), latestFirstFor(clip, current.requested ?? get("max_frames", 1), nth))); + // A Length drags the whole window, stopping where OUT meets the end; + // a Length past the clip's end only caps it, so IN moves freely. + parked = Math.max(0, Math.min(clampFrame(frame, clip), latestInFor(clip, current.requested ?? get("max_frames", 1), nth))); set("end_seconds", 0); } else if (current.mode === "wired" && current.latestStart != null && !countAtRunTime()) { parked = Math.min(clampFrame(frame, clip), current.latestStart); @@ -359,7 +360,7 @@ export function mountTransformTrim({ get, set, has = () => true, driven = () => handle.title = isLocked ? lockTip(edge) : edge === "start" ? current.mode === "free" ? "Drag IN to trim; Shift + arrow moves one frame." - : fills ? "The Length is the whole clip, so IN has nowhere to go. Shorten the Length to move IN." + : fills ? "Drag IN to start later. The Length is as long as the clip, so the clip's end is OUT." : "Drag IN to move the clip; OUT follows at the Length. Shift + arrow moves one frame." : current.mode === "free" ? "Drag OUT to trim; it lands on the last frame the run keeps." : "Drag OUT to change the Length; IN stays."; handle.setAttribute("aria-valuemin", "0"); @@ -606,9 +607,9 @@ export function mountTransformTrim({ get, set, has = () => true, driven = () => count.set(blocked ? (current.requested ?? current.frames ?? 0) : (current.requested ?? get("max_frames", 1))); disableControl(count, blocked, lockTip("end")); const fills = lengthFillsClip(info(), current, everyNth()); - hint.textContent = blocked ? `from ${wired}` : current.mode === "fixed" ? "exact (older setting)" : fills ? "= the whole clip. Shorten it to move IN" : active ? "" : "OUT ends the clip"; + hint.textContent = blocked ? `from ${wired}` : current.mode === "fixed" ? "exact (older setting)" : fills ? "= the whole clip" : active ? "" : "OUT ends the clip"; hint.classList.toggle("note", fills && !blocked); - hint.title = fills && !blocked ? `The Length (${current.requested ?? current.frames} frames) covers the whole clip, so IN and the clip cannot move. The bar still scrubs.` : ""; + hint.title = fills && !blocked ? `The Length (${current.requested ?? current.frames} frames) is as long as the clip, so the clip's end is OUT. Drag IN to start later; drag OUT or type a smaller Length to end sooner.` : ""; }; } diff --git a/tests/timeline_math.test.mjs b/tests/timeline_math.test.mjs index a659237..ba887e9 100644 --- a/tests/timeline_math.test.mjs +++ b/tests/timeline_math.test.mjs @@ -181,7 +181,7 @@ test("fixed frame duration follows output fps while position follows source fps" }); // --- One way to set the length ------------------------------------------------- -import { clipLengthPlan, framesThrough, lastFrameFor, latestFirstFor, lengthMode, snapStep, snapToValid } from "../js/shared/timeline_math.mjs"; +import { clipLengthPlan, framesThrough, lastFrameFor, latestFirstFor, latestInFor, lengthMode, snapStep, snapToValid } from "../js/shared/timeline_math.mjs"; test("one mode sets the length: a wired input, an old fixed count, a Length, or OUT", () => { assert.equal(lengthMode({}), "free"); @@ -214,6 +214,16 @@ test("OUT lands on a frame the run keeps", () => { assert.equal(latestFirstFor(info, 20), 77); assert.equal(latestFirstFor(info, 97), 0); assert.equal(latestFirstFor(info, 200), 0); + // A Length that fits stops IN where OUT meets the end; one as long as the + // clip or longer only caps it, so IN reaches every frame (a 240-frame + // Length on a 116-frame clip used to pin IN at 0). + assert.equal(latestInFor(info, 20), 77); + assert.equal(latestInFor(info, 97), 96); + assert.equal(latestInFor(info, 200), 96); + const short = clipInfo({ fps: 24, frame_count: 116, duration: 116 / 24 }); + assert.equal(latestInFor(short, 240), 115); + const late = clipLengthPlan(short, { start: windowSeconds(short, 40, 40).start_seconds, end: 0, maxFrames: 240 }); + assert.deepEqual([late.first, late.last, late.frames, late.truncated], [40, 115, 76, true]); }); test("free trim: the handles are IN and the last frame the run keeps", () => {