Files
Ewald 7a1ecafc3b Fix preview/backend WYSIWYG bugs and clean up dead code across the pack
- FrameRange: reattach interactive controls on onConfigure instead of
  leaving them permanently dead after a workflow load/undo/paste
- FrameRange: stop silently swapping an inverted trim range in the
  preview - match _timeline_indices' documented zero-frame contract
- Append: bypass no longer concatenates audio/fps from clips that
  aren't part of the bypassed output
- Append: preview loop point now accounts for bypass
- Crop: bypass now returns a full-frame bbox instead of a 2-tuple that
  broke the node's declared 3-output contract
- Channel: shuffle preview now errors like the backend when "Mask" is
  selected without a connected mask, instead of faking success
- host.ts: RAF loop keeps polling instead of stopping for good when
  the canvas is briefly detached from the DOM
- Text: fix drag miscalibration when the preview is zoomed
- Eyedropper: restore picking from any canvas (VHS/WAS previews
  included), only excluding the LiteGraph graph canvas
- video_io.py: stop swallowing comfy_api import failures
- Remove dead code: compat/comfy_v3.py (V3NodeBase), core/parameters.py,
  core/validation.py, dbl-click-reset.ts, unused MEDIA_INPUT_TYPE
  imports, an unreachable fragment in _ensure_rgba
- memory.py: narrow an overly broad except clause
- distort.py: finish the first progress bar before starting the second
- keyer.py: fix inconsistent indentation
2026-09-27 19:50:21 +02:00

160 lines
6.5 KiB
JavaScript

import { isNode as isTextNode, syncTextWidgets } from "../nodes/text.js";
import { syncDarkColorInputUI } from "../shared/dom-styles.js";
import { findWidget, setWidgetBooleanValue, setWidgetStringValue, setWidgetStringValuesByName, setWidgetValue, widgetNumber } from "../shared/widgets.js";
function clampInt(value, fallback, min, max) {
if (!Number.isFinite(value)) return fallback;
return Math.max(min, Math.min(max, Math.round(value)));
}
function clampFloat(value, fallback, min, max) {
if (!Number.isFinite(value)) return fallback;
return Math.max(min, Math.min(max, value));
}
function attachInteractions(node, ctx) {
if (!isTextNode(node)) return;
const st = node.__imageops_state ?? null;
const root = st?.previewRoot;
if (!st || !root || root.dataset.textInteractiveHooked === "1") return;
root.dataset.textInteractiveHooked = "1";
const refresh = () => {
syncTextWidgets(node);
ctx.refreshNode(node);
};
for (const input of Array.from(root.querySelectorAll("input[data-text-field]"))) {
const field = String(input.dataset.textField ?? "");
if (field === "opacity") {
input.addEventListener("input", () => {
const value = Math.max(0, Math.min(100, Number(input.value)));
setWidgetValue(findWidget(node, "opacity"), value / 100);
refresh();
});
continue;
}
if (field === "font_path") {
input.addEventListener("change", () => {
setWidgetStringValue(findWidget(node, "font_path"), String(input.value ?? ""));
refresh();
});
continue;
}
input.addEventListener("change", () => {
const numeric = Number(input.value);
if (field === "font_size") {
setWidgetValue(findWidget(node, field), clampInt(numeric, 64, 1, 512));
} else if (field === "line_spacing") {
setWidgetValue(findWidget(node, field), clampInt(numeric, 4, 0, 256));
} else if (field === "stroke_width") {
setWidgetValue(findWidget(node, field), clampInt(numeric, 0, 0, 64));
} else if (field === "x" || field === "y") {
setWidgetValue(findWidget(node, field), clampFloat(numeric, 0.5, -2, 3));
}
refresh();
});
}
for (const input of Array.from(root.querySelectorAll("input[data-text-color]"))) {
input.addEventListener("input", () => {
const channel = String(input.dataset.textColor ?? "fill");
const widgetName = channel === "stroke" ? "stroke_color" : "color";
const fallback = channel === "stroke" ? "#000000" : "#ffffff";
const normalized = String(input.value || fallback);
setWidgetStringValuesByName(node, widgetName, normalized);
syncDarkColorInputUI(input, normalized);
refresh();
});
}
const alignSelect = root.querySelector('select[data-text-select="align"]');
alignSelect?.addEventListener("change", () => {
setWidgetStringValue(findWidget(node, "align"), String(alignSelect.value || "center"));
refresh();
});
for (const button of Array.from(root.querySelectorAll("button[data-text-toggle]"))) {
button.addEventListener("click", (event) => {
event.preventDefault();
const field = String(button.dataset.textToggle ?? "");
if (!field) return;
const widget = findWidget(node, field);
setWidgetBooleanValue(widget, !Boolean(widget?.value));
refresh();
});
}
const canvas = st.canvas;
if (canvas) {
canvas.style.cursor = "grab";
let drag = null;
canvas.addEventListener("pointerdown", (event) => {
if (event.button !== 0) return;
event.preventDefault();
event.stopPropagation();
try {
canvas.setPointerCapture(event.pointerId);
} catch {
}
drag = {
pointerId: event.pointerId,
startClientX: event.clientX,
startClientY: event.clientY,
startWX: widgetNumber(node, "x", 0.5),
startWY: widgetNumber(node, "y", 0.5)
};
canvas.style.cursor = "grabbing";
});
canvas.addEventListener("pointermove", (event) => {
if (!drag || drag.pointerId !== event.pointerId) return;
event.preventDefault();
event.stopPropagation();
const rect = canvas.getBoundingClientRect();
if (rect.width < 1 || rect.height < 1) return;
const fit = st.fitGeometry;
const zoom = Math.max(0.35, st.previewZoom ?? 1);
const scaleX = fit && canvas.width > 0 ? fit.drawWidth / canvas.width : 1;
const scaleY = fit && canvas.height > 0 ? fit.drawHeight / canvas.height : 1;
const effWidth = Math.max(1, rect.width * scaleX * zoom);
const effHeight = Math.max(1, rect.height * scaleY * zoom);
const dx = (event.clientX - drag.startClientX) / effWidth;
const dy = (event.clientY - drag.startClientY) / effHeight;
const newX = clampFloat(drag.startWX + dx, 0.5, -2, 3);
const newY = clampFloat(drag.startWY + dy, 0.5, -2, 3);
setWidgetValue(findWidget(node, "x"), Math.round(newX * 1e3) / 1e3);
setWidgetValue(findWidget(node, "y"), Math.round(newY * 1e3) / 1e3);
const xInput = root.querySelector('input[data-text-field="x"]');
const yInput = root.querySelector('input[data-text-field="y"]');
if (xInput) xInput.value = String(Math.round(newX * 1e3) / 1e3);
if (yInput) yInput.value = String(Math.round(newY * 1e3) / 1e3);
refresh();
});
const endDrag = (event) => {
if (!drag || drag.pointerId !== event.pointerId) return;
drag = null;
canvas.style.cursor = "grab";
ctx.refreshDependents(node);
};
canvas.addEventListener("pointerup", endDrag);
canvas.addEventListener("pointercancel", endDrag);
canvas.addEventListener("wheel", (event) => {
event.preventDefault();
event.stopPropagation();
const step = event.shiftKey ? 10 : 2;
const delta = event.deltaY > 0 ? -step : step;
const current = Math.max(1, Math.round(widgetNumber(node, "font_size", 64)));
const next = Math.max(1, Math.min(512, current + delta));
setWidgetValue(findWidget(node, "font_size"), next);
const sizeInput = root.querySelector('input[data-text-field="font_size"]');
if (sizeInput) sizeInput.value = String(next);
refresh();
}, { passive: false });
}
syncTextWidgets(node);
const textWidget = findWidget(node, "text");
if (textWidget && !textWidget.__imageopsTextCbHooked) {
textWidget.__imageopsTextCbHooked = true;
const origTextCb = typeof textWidget.callback === "function" ? textWidget.callback : null;
textWidget.callback = function() {
const r = origTextCb?.apply(this, arguments);
refresh();
return r;
};
}
}
export {
attachInteractions
};