diff --git a/pyproject.toml b/pyproject.toml index 5c83426..20a26bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_fill-nodes" description = "Fill-Nodes is a versatile collection of custom nodes for ComfyUI that extends functionality across multiple domains. Features include advanced image processing (pixelation, slicing, masking), visual effects generation (glitch, halftone, pixel art), comprehensive file handling (PDF creation/extraction, Google Drive integration), AI model interfaces (GPT, DALL-E, Hugging Face), utility nodes for workflow enhancement, and specialized tools for video processing, captioning, and batch operations. The pack provides both practical workflow solutions and creative tools within a unified node collection." -version = "2.28.5" +version = "2.28.6" license = {file = "LICENSE"} dependencies = [ "librosa", diff --git a/tests/test_video_canvas_navigation.mjs b/tests/test_video_canvas_navigation.mjs new file mode 100644 index 0000000..c6b2a8a --- /dev/null +++ b/tests/test_video_canvas_navigation.mjs @@ -0,0 +1,108 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +const VIDEO_NODE_URL = new URL("../web/nodes/video/", import.meta.url); + +class FakeElement { + constructor() { + this.listeners = new Map(); + this.events = []; + } + + addEventListener(name, handler) { + const handlers = this.listeners.get(name) || []; + handlers.push(handler); + this.listeners.set(name, handlers); + } + + dispatchEvent(event) { + this.events.push(event); + } + + emit(name, event) { + for (const handler of this.listeners.get(name) || []) handler(event); + } +} + +class FakeEvent { + constructor(type, init = {}) { + Object.assign(this, init); + this.type = type; + } + + preventDefault() { + this.defaultPrevented = true; + } + + stopPropagation() { + this.propagationStopped = true; + } +} + +globalThis.PointerEvent = FakeEvent; +globalThis.WheelEvent = FakeEvent; + +const source = await readFile(new URL("canvas_navigation.js", VIDEO_NODE_URL), "utf8"); +const moduleUrl = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`; +const { addCanvasNavigation } = await import(moduleUrl); + +function pointerEvent(type, overrides = {}) { + return new FakeEvent(type, { + pointerId: 4, + button: -1, + buttons: 0, + altKey: false, + ctrlKey: false, + shiftKey: false, + ...overrides, + }); +} + +test("video panels pass wheel and canvas drag gestures to LiteGraph", () => { + const element = new FakeElement(); + const graphCanvas = new FakeElement(); + const canvas = { canvas: graphCanvas, dragZoomEnabled: true, read_only: false }; + addCanvasNavigation(element, canvas); + + const wheel = new FakeEvent("wheel", { deltaY: -120 }); + element.emit("wheel", wheel); + assert.deepEqual(graphCanvas.events.map((event) => event.type), ["wheel"]); + assert.equal(wheel.defaultPrevented, true); + assert.equal(wheel.propagationStopped, true); + + element.emit("pointerdown", pointerEvent("pointerdown", { button: 0, buttons: 1 })); + assert.equal(graphCanvas.events.length, 1); + + element.emit("pointerdown", pointerEvent("pointerdown", { button: 1, buttons: 4 })); + element.emit("pointermove", pointerEvent("pointermove", { buttons: 4 })); + element.emit("pointerup", pointerEvent("pointerup", { button: 1 })); + element.emit("pointermove", pointerEvent("pointermove", { buttons: 4 })); + assert.deepEqual( + graphCanvas.events.map((event) => event.type), + ["wheel", "pointerdown", "pointermove", "pointerup"], + ); + + canvas.read_only = true; + element.emit("pointerdown", pointerEvent("pointerdown", { button: 0, buttons: 1 })); + element.emit("pointerup", pointerEvent("pointerup", { button: 0 })); + assert.deepEqual(graphCanvas.events.slice(-2).map((event) => event.type), ["pointerdown", "pointerup"]); + + canvas.read_only = false; + element.emit("pointerdown", pointerEvent("pointerdown", { + button: 0, + buttons: 1, + ctrlKey: true, + shiftKey: true, + })); + element.emit("pointercancel", pointerEvent("pointercancel", { button: 0 })); + assert.deepEqual(graphCanvas.events.slice(-2).map((event) => event.type), ["pointerdown", "pointercancel"]); +}); + +test("both FL video DOM widgets enable canvas navigation", async () => { + for (const filename of ["FL_VideoCombine.js", "FL_LoadVideo.js"]) { + const nodeSource = await readFile(new URL(filename, VIDEO_NODE_URL), "utf8"); + assert.match(nodeSource, /import \{ addCanvasNavigation \} from "\.\/canvas_navigation\.js";/); + assert.match(nodeSource, /addCanvasNavigation\(container, app\.canvas\);/); + } +}); diff --git a/web/nodes/video/FL_LoadVideo.js b/web/nodes/video/FL_LoadVideo.js index 896f7e5..23a1b27 100644 --- a/web/nodes/video/FL_LoadVideo.js +++ b/web/nodes/video/FL_LoadVideo.js @@ -1,5 +1,6 @@ import { app } from "../../../../scripts/app.js"; import { api } from "../../../../scripts/api.js"; +import { addCanvasNavigation } from "./canvas_navigation.js"; const DEFAULT_SETTINGS = { version: 1, @@ -1871,6 +1872,7 @@ app.registerExtension({ container.style.height = "100%"; container.style.minHeight = `${MIN_PANEL_HEIGHT}px`; container.style.overflow = "hidden"; + addCanvasNavigation(container, app.canvas); const domWidget = node.addDOMWidget("fl_load_video_panel", "fl-load-video", container, { getMinHeight: () => MIN_PANEL_HEIGHT, diff --git a/web/nodes/video/FL_VideoCombine.js b/web/nodes/video/FL_VideoCombine.js index b036155..2d75a40 100644 --- a/web/nodes/video/FL_VideoCombine.js +++ b/web/nodes/video/FL_VideoCombine.js @@ -1,5 +1,6 @@ import { app } from "../../../../scripts/app.js"; import { api } from "../../../../scripts/api.js"; +import { addCanvasNavigation } from "./canvas_navigation.js"; const DEFAULT_SETTINGS = { version: 1, @@ -1125,6 +1126,7 @@ app.registerExtension({ container.style.height = "100%"; container.style.minHeight = `${MIN_PANEL_HEIGHT}px`; container.style.overflow = "hidden"; + addCanvasNavigation(container, app.canvas); const domWidget = node.addDOMWidget("fl_video_combine_panel", "fl-video-combine", container, { getMinHeight: () => MIN_PANEL_HEIGHT, diff --git a/web/nodes/video/canvas_navigation.js b/web/nodes/video/canvas_navigation.js new file mode 100644 index 0000000..bbc603c --- /dev/null +++ b/web/nodes/video/canvas_navigation.js @@ -0,0 +1,42 @@ +function startsCanvasGesture(event, canvas) { + return event.button === 1 || ( + event.button === 0 + && ( + canvas.read_only + || (canvas.dragZoomEnabled && event.ctrlKey && event.shiftKey && !event.altKey) + ) + ); +} + +export function addCanvasNavigation(element, canvas) { + const graphCanvas = canvas.canvas; + let pointerId = null; + + element.addEventListener("wheel", (event) => { + graphCanvas.dispatchEvent(new WheelEvent(event.type, event)); + event.preventDefault(); + event.stopPropagation(); + }, { passive: false }); + + element.addEventListener("pointerdown", (event) => { + if (!startsCanvasGesture(event, canvas)) return; + pointerId = event.pointerId; + graphCanvas.dispatchEvent(new PointerEvent(event.type, event)); + event.preventDefault(); + event.stopPropagation(); + }); + + for (const eventName of ["pointermove", "pointerup", "pointercancel"]) { + element.addEventListener(eventName, (event) => { + if (event.pointerId !== pointerId) return; + graphCanvas.dispatchEvent(new PointerEvent(event.type, event)); + if (eventName !== "pointermove") pointerId = null; + event.preventDefault(); + event.stopPropagation(); + }); + } + + element.addEventListener("auxclick", (event) => { + if (event.button === 1) event.preventDefault(); + }); +}