Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cc91ffba8 | ||
|
|
0450466422 | ||
|
|
d5442cd659 | ||
|
|
9a1096149a | ||
|
|
0401d9dd34 | ||
|
|
8961827079 | ||
|
|
dea2eac8cd | ||
|
|
3c1eeaa486 | ||
|
|
cf0eec8d31 |
@@ -2,6 +2,21 @@
|
||||
|
||||
All notable changes to ComfyUI-AusBoss are documented here.
|
||||
|
||||
## 2.25.3 - 2026-11-13
|
||||
|
||||
- **The video clip's frame readout explains itself.** On Video Crop + Rotate
|
||||
+ Pad (Clip), the small `fr 52 · 0:02.17` beside Play now says in plain
|
||||
words, when you point at it, which frame the picture shows and how many
|
||||
seconds into the video it is. The help page explains it too.
|
||||
|
||||
## 2.25.2 - 2026-11-13
|
||||
|
||||
- **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
|
||||
|
||||
+1
-1
@@ -193,7 +193,7 @@ example_workflows/ # example workflows (regular workflow JSON, not API JSON)
|
||||
drag). Messages to the user go through `showToast`, never `alert()`.
|
||||
- The mouse wheel over a node zooms the graph, on every part of its face.
|
||||
`js/canvas_passthrough/` hands the wheel on from the pack's panels, and
|
||||
keeps it only for a box that can still scroll or a field being typed in,
|
||||
keeps it only for a box that can still scroll, typed in or not,
|
||||
so a panel adds no wheel listener of its own
|
||||
(`tests/canvas_passthrough.test.mjs` lists the few that exist). After a
|
||||
change to a node face, `scripts/dev/wheel_sweep.mjs` turns the wheel at
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="105" height="20" role="img" aria-label="release: v2.25.1">
|
||||
<title>release: v2.25.1</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="105" height="20" role="img" aria-label="release: v2.25.3">
|
||||
<title>release: v2.25.3</title>
|
||||
<path fill="#555" d="M0 0h48v20H0z"/>
|
||||
<path fill="#00b4aa" d="M48 0h57v20H48z"/>
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
||||
<text x="24" y="14">release</text>
|
||||
<text x="76.5" y="14">v2.25.1</text>
|
||||
<text x="76.5" y="14">v2.25.3</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 425 B After Width: | Height: | Size: 425 B |
@@ -649,7 +649,6 @@ function startEdit(state) {
|
||||
if (event.target !== area) event.preventDefault();
|
||||
});
|
||||
wrap.addEventListener("pointerdown", (event) => event.stopPropagation());
|
||||
wrap.addEventListener("wheel", (event) => event.stopPropagation(), { passive: true });
|
||||
// Clicking another node on the canvas takes the keyboard from the text box.
|
||||
// That is how the link is picked, so the edit stays open then; a click on
|
||||
// empty canvas, or on the note itself, ends it.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// zooming stopped over Compare, the crop previews, the video viewers and every
|
||||
// control on a card. One listener here hands those gestures back, the same way
|
||||
// ComfyUI does for its own text boxes. A panel that wants the wheel for itself
|
||||
// (a scrolling list, a focused text field, an editor) still keeps it.
|
||||
// (a scrolling list, a long text box) keeps it while it can still scroll.
|
||||
//
|
||||
// A file dragged in from the desktop, or a picture's card from the Assets
|
||||
// panel, has the same problem in the classic renderer: dropped on a node's
|
||||
@@ -31,7 +31,7 @@ function onWheel(event) {
|
||||
const root = panelRoot(target);
|
||||
const lgCanvas = canvas();
|
||||
if (!root || !lgCanvas || event.defaultPrevented) return;
|
||||
if (panelKeepsWheel(target, event, { root, host: panelHost(target), styleOf: (el) => getComputedStyle(el), activeElement: document.activeElement })) return;
|
||||
if (panelKeepsWheel(target, event, { root, host: panelHost(target), styleOf: (el) => getComputedStyle(el) })) return;
|
||||
event.preventDefault();
|
||||
lgCanvas.processMouseWheel(event);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ model that paints black regions.
|
||||
- **Play / Pause** beside the scrubber previews the selected frames and stops at OUT. Press Play at OUT to start from IN again.
|
||||
- **Timeline** (right under the preview and size line): The rail on the node and in the editor is one timeline. Press or
|
||||
drag anywhere on it to scrub the **playhead**, with Length on or off; the stage shows
|
||||
that frame. Drag the **IN** or **OUT** handle to trim: the playhead rides on the handle, so what you see
|
||||
that frame. The small readout beside Play names it: `fr 52 · 0:02.17` means frame 52,
|
||||
2.17 seconds into the video. Drag the **IN** or **OUT** handle to trim: the playhead rides on the handle, so what you see
|
||||
is the first (IN) or last (OUT) frame the run keeps, and it stays there when you let
|
||||
go. IN and OUT are frames, snapped to the source's frame grid; the boxes under the
|
||||
rail take a frame number, and `start_seconds` / `end_seconds` are derived from them
|
||||
@@ -60,8 +61,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
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
// in js/canvas_passthrough/ wires them to the page.
|
||||
|
||||
// The wheel belongs to the panel while it can still scroll that way (a long
|
||||
// text box, a list of rows), or while a text field has the keyboard.
|
||||
// text box, a list of rows). A text box with the cursor in it is no
|
||||
// exception: when it has nothing left to scroll, the wheel zooms the graph,
|
||||
// as it does over ComfyUI's own text boxes.
|
||||
export function canScrollFurther(element, delta, axis = "y") {
|
||||
if (!element || !delta) return false;
|
||||
const horizontal = axis === "x";
|
||||
@@ -25,10 +27,11 @@ export function canScrollFurther(element, delta, axis = "y") {
|
||||
// and the panel's own root? `styleOf` is getComputedStyle, passed in so this
|
||||
// stays testable without a page. `host` is the frontend's wrapper around the
|
||||
// panel: a pointer on the wrapper itself is on nothing inside the panel.
|
||||
export function panelKeepsWheel(target, event, { root, host, styleOf, activeElement }) {
|
||||
// Ctrl + wheel, which is also how a trackpad pinch arrives, always zooms the
|
||||
// graph; left to a scrolling box it would zoom the whole page.
|
||||
export function panelKeepsWheel(target, event, { root, host, styleOf }) {
|
||||
if (event.ctrlKey || event.metaKey) return false;
|
||||
for (let element = target; element && element !== root && element !== host; element = element.parentElement) {
|
||||
const tag = String(element.tagName ?? "").toUpperCase();
|
||||
if ((tag === "TEXTAREA" || tag === "INPUT") && element === activeElement) return true;
|
||||
const style = styleOf(element);
|
||||
const overflow = style?.overflowY;
|
||||
if ((overflow === "auto" || overflow === "scroll") && canScrollFurther(element, event.deltaY)) return true;
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { app } from "../../../scripts/app.js";
|
||||
// Must match `version` in pyproject.toml — scripts/release_preflight.py
|
||||
// enforces it. The backend serves its copy at /ausboss/pack_version; a
|
||||
// mismatch means the browser cached JavaScript from an older install.
|
||||
export const AUSBOSS_JS_VERSION = "2.25.1";
|
||||
export const AUSBOSS_JS_VERSION = "2.25.3";
|
||||
|
||||
export const BRAND = "#00b4aa"; // AusBoss teal — keep node accents consistent.
|
||||
export const BRAND_DARK = "#007f78";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -321,7 +322,7 @@ export function mountTransformTrim({ get, set, has = () => true, driven = () =>
|
||||
readout.replaceChildren();
|
||||
if (clip.count) {
|
||||
readout.append(el("b", "", `fr ${head}`), document.createTextNode(` · ${formatTimecode(frameTime(head, clip))}`));
|
||||
readout.title = `Playhead: frame ${head} of ${clip.count} at ${frameTime(head, clip).toFixed(3)} s. Press or drag the rail to scrub; ${trim ? "the trim handles carry it with them." : "this is the frame the node outputs."}`;
|
||||
readout.title = `The picture shows frame ${head} of ${clip.count}, ${frameTime(head, clip).toFixed(2)} seconds into the video. Press or drag the bar to look at another frame; ${trim ? "dragging IN or OUT brings it along." : "this is the frame the node outputs."}`;
|
||||
}
|
||||
controls.frame?.set(head);
|
||||
if (!trim) return;
|
||||
@@ -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.` : "";
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ name = "ausboss-nodes"
|
||||
# Manager/registry search matches against this text, so it names the actual
|
||||
# nodes instead of describing the collection abstractly.
|
||||
description = "ComfyUI-AusBoss (AusBoss nodes): image and video nodes with compact controls: Image and Video Crop + Rotate + Pad (frame or clip), Load Image, Load Image + Pad, Image Folder, Image Resize, Align Image, Image Size, Color Match, Image Compare A/B, Realign to Source (experimental), Save Image (PNG, WebP/JXL, captions), Load Video, Load Audio, Save Video, Select Frame, Frame Interpolate, Mask Refine, Mask by Name and Mask by Touch, LaMa Inpaint, Crop For Inpaint, Stitch Inpaint, Tiled Upscale and Tiled Upscale Stitch, FlashVSR Upscale, Edit Lock and Edit Lock Restore, LoRA Loader, Krea 2 Encode and Outpaint Model Patch, Latent Size, Layout Boxes, Seed, Select Every Nth, Split Batch, Merge Batches, Text, Text Lines, Prompt Variations, H3 Prompt Writer, Integer, Float, Math Expression, Show Text, Free Memory, Workflow Note, Workflow Note Tile, Callout, Callout Tile, Workflow Switches, Run Timer, and Step Preview. Example workflows for Qwen Image 2.1, Krea 2, Klein 9B and LTX 2.3."
|
||||
version = "2.25.1"
|
||||
version = "2.25.3"
|
||||
# comfy-cli serializes this to JSON verbatim, so `file` publishes the literal
|
||||
# string {"file": "LICENSE"} to the registry listing. `text` puts a readable
|
||||
# name there instead. Both forms are supported; the LICENSE file is unchanged.
|
||||
|
||||
@@ -27,12 +27,19 @@ test("the graph gets the wheel unless the panel has a use for it", () => {
|
||||
const list = { tagName: "DIV", parentElement: root, ...box() };
|
||||
const field = { tagName: "INPUT", parentElement: root };
|
||||
const styleOf = (el) => ({ overflowY: el === list ? "auto" : "visible" });
|
||||
const ctx = (extra = {}) => ({ root, styleOf, activeElement: null, ...extra });
|
||||
const ctx = (extra = {}) => ({ root, styleOf, ...extra });
|
||||
assert.equal(panelKeepsWheel(stage, { deltaY: -100 }, ctx()), false, "plain picture stage");
|
||||
assert.equal(panelKeepsWheel(list, { deltaY: 100 }, ctx()), true, "list with room to scroll");
|
||||
assert.equal(panelKeepsWheel(list, { deltaY: -100 }, ctx()), false, "list at its top, wheel up zooms");
|
||||
assert.equal(panelKeepsWheel(field, { deltaY: 100 }, ctx()), false, "idle field");
|
||||
assert.equal(panelKeepsWheel(field, { deltaY: 100 }, ctx({ activeElement: field })), true, "field being typed in");
|
||||
// A text box with the cursor in it and nothing to scroll: the graph zooms
|
||||
// (issue #115, the prompt box in a workflow).
|
||||
assert.equal(panelKeepsWheel(field, { deltaY: 100 }, ctx({ activeElement: field })), false, "field being typed in, nothing to scroll");
|
||||
const prompt = { tagName: "TEXTAREA", parentElement: root, ...box() };
|
||||
const scrolls = (el) => ({ overflowY: el === prompt || el === list ? "auto" : "visible" });
|
||||
assert.equal(panelKeepsWheel(prompt, { deltaY: 100 }, ctx({ styleOf: scrolls })), true, "long text still scrolls");
|
||||
assert.equal(panelKeepsWheel(prompt, { deltaY: -100 }, ctx({ styleOf: scrolls })), false, "long text at its top, wheel up zooms");
|
||||
assert.equal(panelKeepsWheel(list, { deltaY: 100, ctrlKey: true }, ctx()), false, "Ctrl + wheel and pinch always zoom the graph");
|
||||
});
|
||||
|
||||
test("middle button and Ctrl+Shift drag go to the graph; ordinary clicks do not", () => {
|
||||
@@ -131,7 +138,6 @@ test("only the listed parts of the pack listen for the wheel", () => {
|
||||
// face of the node.
|
||||
const js = join(dirname(fileURLToPath(import.meta.url)), "..", "js");
|
||||
const listed = [
|
||||
"callout/index.js wrap", // the text box while a note is being edited
|
||||
"canvas_passthrough/index.js window", // hands the wheel to the graph
|
||||
"image_folder/index.js root", // the copy question, a window over the page
|
||||
"image_folder/index.js root", // the folder list, a window over the page
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user