Let the clip's IN handle drag when the Length covers the whole clip

A Length as long as the clip or longer locked IN at frame 0, and the only
way to trim the start was to turn Length off. A workflow that sets a
Length as an upper limit (240 frames, say) hit this with every shorter
clip. Now such a Length only caps the clip: the clip's own end is OUT and
IN moves over the whole clip. A Length shorter than the clip still stops
IN where OUT meets the end, as before. The row reads "= the whole clip"
and the tooltips say what dragging does.

Every IN this allows was already reachable with Length off; the backend
runs each one (checked on a 116-frame clip with Length 240: IN at 0,
1.83 s, 4.5 s and the last frame all run).
This commit is contained in:
ausboss
2026-11-13 10:08:47 -06:00
parent 8618d8ca56
commit 3c1eeaa486
5 changed files with 39 additions and 9 deletions
+8
View File
@@ -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
@@ -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
+10
View File
@@ -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
+7 -6
View File
@@ -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.` : "";
};
}
+11 -1
View File
@@ -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", () => {