594 lines
21 KiB
JavaScript
594 lines
21 KiB
JavaScript
import { app } from '../../../scripts/app.js'
|
|
|
|
function chainCallback(object, property, callback) {
|
|
if (object == undefined) {
|
|
console.error("Tried to add callback to non-existent object");
|
|
return;
|
|
}
|
|
if (property in object && object[property]) {
|
|
const callback_orig = object[property];
|
|
object[property] = function () {
|
|
const r = callback_orig.apply(this, arguments);
|
|
return callback.apply(this, arguments) ?? r;
|
|
};
|
|
} else {
|
|
object[property] = callback;
|
|
}
|
|
}
|
|
|
|
function drawAutoAnnotated(ctx, node, widget_width, y, H) {
|
|
const litegraph_base = LiteGraph;
|
|
const show_text = app.canvas.ds.scale >= 0.5;
|
|
const margin = 15;
|
|
|
|
const autoTextWidth = 30;
|
|
const autoTextRightMargin = 5;
|
|
|
|
ctx.textAlign = 'left';
|
|
ctx.strokeStyle = litegraph_base.WIDGET_OUTLINE_COLOR;
|
|
ctx.fillStyle = litegraph_base.WIDGET_BGCOLOR;
|
|
|
|
ctx.beginPath();
|
|
if (show_text && ctx.roundRect) {
|
|
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]);
|
|
} else {
|
|
ctx.rect(margin, y, widget_width - margin * 2, H);
|
|
}
|
|
ctx.fill();
|
|
|
|
if (show_text) {
|
|
if (!this.disabled) ctx.stroke();
|
|
const isAuto = this.isAuto === true;
|
|
|
|
ctx.save();
|
|
if (isAuto) {
|
|
ctx.fillStyle = litegraph_base.WIDGET_TEXT_COLOR;
|
|
ctx.strokeStyle = litegraph_base.WIDGET_TEXT_COLOR;
|
|
} else {
|
|
ctx.fillStyle = litegraph_base.WIDGET_SECONDARY_TEXT_COLOR;
|
|
ctx.strokeStyle = litegraph_base.WIDGET_SECONDARY_TEXT_COLOR;
|
|
}
|
|
|
|
// Position for the cog
|
|
const cogX = widget_width - autoTextRightMargin - autoTextWidth - 6;
|
|
const cogY = y + H * 0.5;
|
|
const cogRadius = 6;
|
|
const toothLength = 2;
|
|
const numTeeth = 8;
|
|
const holeRadius = 2; // Radius of the center hole
|
|
|
|
// Draw the cog
|
|
ctx.beginPath();
|
|
ctx.arc(cogX, cogY, cogRadius - toothLength, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Draw the center hole (by clearing it)
|
|
ctx.beginPath();
|
|
ctx.arc(cogX, cogY, holeRadius, 0, Math.PI * 2);
|
|
ctx.fillStyle = litegraph_base.WIDGET_BGCOLOR;
|
|
ctx.fill();
|
|
|
|
// Reset fill style for the teeth
|
|
if (isAuto) {
|
|
ctx.fillStyle = litegraph_base.WIDGET_TEXT_COLOR;
|
|
} else {
|
|
ctx.fillStyle = litegraph_base.WIDGET_SECONDARY_TEXT_COLOR;
|
|
}
|
|
|
|
// Draw teeth
|
|
ctx.beginPath();
|
|
for (let i = 0; i < numTeeth; i++) {
|
|
const angle = (i / numTeeth) * Math.PI * 2;
|
|
const innerX = cogX + (cogRadius - toothLength) * Math.cos(angle);
|
|
const innerY = cogY + (cogRadius - toothLength) * Math.sin(angle);
|
|
const outerX = cogX + cogRadius * Math.cos(angle);
|
|
const outerY = cogY + cogRadius * Math.sin(angle);
|
|
|
|
ctx.moveTo(innerX, innerY);
|
|
ctx.lineTo(outerX, outerY);
|
|
}
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
|
|
ctx.restore();
|
|
|
|
// Draw label
|
|
ctx.fillStyle = litegraph_base.WIDGET_SECONDARY_TEXT_COLOR;
|
|
const label = this.label || this.name;
|
|
if (label != null) {
|
|
ctx.fillText(label, margin * 2 + 5, y + H * 0.7);
|
|
}
|
|
|
|
// Draw value
|
|
ctx.textAlign = 'right';
|
|
const text = isAuto ? "auto" : this.displayValue();
|
|
ctx.fillStyle = isAuto ? litegraph_base.WIDGET_SECONDARY_TEXT_COLOR : litegraph_base.WIDGET_TEXT_COLOR;
|
|
ctx.fillText(text, widget_width - autoTextRightMargin - autoTextWidth - 15, y + H * 0.7);
|
|
|
|
// Draw increment/decrement buttons if not in AUTO mode and not a string widget
|
|
if (!isAuto && !this.disabled && this.config[0] !== "FVAUTOSTRING") {
|
|
// Draw decrement button (left triangle)
|
|
ctx.fillStyle = litegraph_base.WIDGET_TEXT_COLOR;
|
|
ctx.beginPath();
|
|
ctx.moveTo(margin + 16, y + 5);
|
|
ctx.lineTo(margin + 6, y + H * 0.5);
|
|
ctx.lineTo(margin + 16, y + H - 5);
|
|
ctx.fill();
|
|
|
|
// Draw increment button (right triangle)
|
|
ctx.beginPath();
|
|
ctx.moveTo(widget_width - margin - 16, y + 5);
|
|
ctx.lineTo(widget_width - margin - 6, y + H * 0.5);
|
|
ctx.lineTo(widget_width - margin - 16, y + H - 5);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
}
|
|
|
|
function mouseAutoAnnotated(event, [x, y], node) {
|
|
const widget_width = node.size[0];
|
|
const margin = 15;
|
|
const H = 20; // Widget height
|
|
|
|
const autoTextWidth = 30;
|
|
const autoTextRightMargin = 5;
|
|
|
|
const cogRadius = 6;
|
|
|
|
if (this.isAuto) {
|
|
if (event.type === "pointerup" || event.type === "mouseup") {
|
|
const cogX = widget_width - autoTextRightMargin - autoTextWidth - 6;
|
|
const cogLeftEdge = cogX - cogRadius;
|
|
const cogRightEdge = cogX + cogRadius;
|
|
|
|
if (x > cogLeftEdge && x < cogRightEdge) {
|
|
this.isAuto = false;
|
|
this.value = this.cachedValue !== undefined ? this.cachedValue : (this.options.default || 0);
|
|
|
|
if (this.callback) {
|
|
this.callback(this.value);
|
|
}
|
|
node.graph.setDirtyCanvas(true, false);
|
|
}
|
|
}
|
|
|
|
// Block ALL events in auto mode except cog clicks
|
|
event.preventDefault?.();
|
|
event.stopPropagation?.();
|
|
event.stopImmediatePropagation?.();
|
|
return true; // Always return true to indicate event was handled
|
|
}
|
|
|
|
// Determine if clicking on increment/decrement buttons
|
|
const delta = this.config[0] === "FVAUTOSTRING" ? 0 :
|
|
(x < 40 ? -1 : x > widget_width - 48 ? 1 : 0);
|
|
|
|
if (event.type === "pointerdown" || event.type === "mousedown") {
|
|
// ComfyUI appears to intercept pointerdown events, so this code path is never reached
|
|
console.log("pointerdown received (unexpected)");
|
|
return false;
|
|
} else if (event.type === "pointerup" || event.type === "mouseup") {
|
|
// Stop event propagation to prevent double handling
|
|
event.preventDefault?.();
|
|
event.stopPropagation?.();
|
|
event.stopImmediatePropagation?.();
|
|
|
|
const cogX = widget_width - autoTextRightMargin - autoTextWidth - 6;
|
|
const cogLeftEdge = widget_width - autoTextRightMargin - autoTextWidth - 6 - cogRadius;
|
|
const cogRightEdge = widget_width - autoTextRightMargin - autoTextWidth - 6 + cogRadius;
|
|
|
|
if (x > cogLeftEdge && x < cogRightEdge) {
|
|
this.isAuto = !this.isAuto;
|
|
|
|
if (this.isAuto) {
|
|
this.cachedValue = this.value;
|
|
this.value = -99999;
|
|
} else {
|
|
this.value = this.cachedValue !== undefined ? this.cachedValue : (this.options.default || 0);
|
|
}
|
|
|
|
if (this.callback) {
|
|
this.callback(this.value);
|
|
}
|
|
|
|
node.graph.setDirtyCanvas(true, false);
|
|
return true;
|
|
}
|
|
|
|
// If in auto mode and NOT clicking the cog, block all other interactions
|
|
if (this.isAuto) {
|
|
return true;
|
|
}
|
|
|
|
// Handle increment/decrement buttons if not in auto mode
|
|
if (delta !== 0 && !this.isAuto) {
|
|
if (this.config[0] === "FVAUTOCOMBO") {
|
|
const options = this.options.values || [];
|
|
if (options.length === 0) return true;
|
|
|
|
let currentIndex = -1;
|
|
for (let i = 0; i < options.length; i++) {
|
|
const optValue = typeof options[i] === 'object' ? options[i].value : options[i];
|
|
if (optValue == this.value || String(optValue) === String(this.value)) {
|
|
currentIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (currentIndex === -1) {
|
|
currentIndex = 0;
|
|
}
|
|
|
|
let newIndex = currentIndex + delta;
|
|
if (newIndex < 0) {
|
|
newIndex = options.length - 1;
|
|
} else if (newIndex >= options.length) {
|
|
newIndex = 0;
|
|
}
|
|
|
|
const newOption = options[newIndex];
|
|
this.value = typeof newOption === 'object' ? newOption.value : newOption;
|
|
|
|
if (this.callback) {
|
|
this.callback(this.value);
|
|
}
|
|
|
|
node.graph.setDirtyCanvas(true, false);
|
|
return true;
|
|
} else {
|
|
let v = parseFloat(this.value);
|
|
const increment = delta * 0.1 * (this.options.step || 1);
|
|
|
|
v += increment;
|
|
|
|
// Apply min/max constraints
|
|
if (this.options.min != null) {
|
|
v = Math.max(this.options.min, v);
|
|
}
|
|
if (this.options.max != null) {
|
|
v = Math.min(this.options.max, v);
|
|
}
|
|
|
|
// Round to precision or to integer
|
|
if (this.config[0] === "FVAUTOINT") {
|
|
v = Math.round(v);
|
|
} else if (this.options.precision !== undefined) {
|
|
const precision = Math.pow(10, this.options.precision);
|
|
v = Math.round(v * precision) / precision;
|
|
}
|
|
|
|
this.value = v;
|
|
|
|
if (this.callback) {
|
|
this.callback(this.value);
|
|
}
|
|
|
|
node.graph.setDirtyCanvas(true, false);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (delta === 0 && !this.isAuto) {
|
|
if (this.config[0] === "FVAUTOCOMBO") {
|
|
const options = this.options.values || [];
|
|
|
|
// Create menu items
|
|
const menuItems = options.map(opt => {
|
|
const value = typeof opt === 'object' ? opt.value : opt;
|
|
const label = typeof opt === 'object' ? opt.label : opt.toString();
|
|
|
|
return {
|
|
content: label,
|
|
callback: () => {
|
|
this.value = value;
|
|
|
|
if (this.callback) {
|
|
this.callback(this.value);
|
|
}
|
|
|
|
node.graph.setDirtyCanvas(true, false);
|
|
}
|
|
};
|
|
});
|
|
|
|
new LiteGraph.ContextMenu(menuItems, {
|
|
event: event,
|
|
title: null,
|
|
callback: null,
|
|
extra: node
|
|
});
|
|
|
|
return true;
|
|
} else if (this.config[0] === "FVAUTOSTRING") {
|
|
const d_callback = (v) => {
|
|
this.value = v;
|
|
|
|
if (this.callback) {
|
|
this.callback(this.value);
|
|
}
|
|
|
|
node.graph.setDirtyCanvas(true, false);
|
|
};
|
|
|
|
const dialog = app.canvas.prompt(
|
|
'Value',
|
|
this.value,
|
|
d_callback,
|
|
event
|
|
);
|
|
|
|
return true;
|
|
} else {
|
|
// For numeric widgets, show input dialog
|
|
const d_callback = (v) => {
|
|
this.value = this.parseValue?.(v) ?? Number(v);
|
|
|
|
// Apply min/max constraints
|
|
if (this.options.min != null) {
|
|
this.value = Math.max(this.options.min, this.value);
|
|
}
|
|
if (this.options.max != null) {
|
|
this.value = Math.min(this.options.max, this.value);
|
|
}
|
|
|
|
// Round to precision or to integer
|
|
if (this.config[0] === "FVAUTOINT") {
|
|
this.value = Math.round(this.value);
|
|
} else if (this.options.precision !== undefined) {
|
|
const precision = Math.pow(10, this.options.precision);
|
|
this.value = Math.round(this.value * precision) / precision;
|
|
}
|
|
|
|
if (this.callback) {
|
|
this.callback(this.value);
|
|
}
|
|
|
|
node.graph.setDirtyCanvas(true, false);
|
|
};
|
|
|
|
const dialog = app.canvas.prompt(
|
|
'Value',
|
|
this.value,
|
|
d_callback,
|
|
event
|
|
);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function makeAutoAnnotated(widget, inputData) {
|
|
const original = {
|
|
callback: widget.callback,
|
|
type: widget.type,
|
|
value: widget.value
|
|
};
|
|
|
|
// Add AUTO properties to the widget
|
|
Object.assign(widget, {
|
|
type: "BOOLEAN",
|
|
draw: drawAutoAnnotated,
|
|
mouse: mouseAutoAnnotated,
|
|
onMouse: null, // Explicitly disable original onMouse handler
|
|
|
|
isAuto: true,
|
|
cachedValue: widget.value,
|
|
config: inputData,
|
|
options: Object.assign({}, inputData[1], widget.options),
|
|
original: original, // Store original properties for reference
|
|
|
|
// Disable other potential mouse handlers with no-op functions
|
|
onClick: function () {
|
|
return false;
|
|
},
|
|
onPointerUp: function () {
|
|
return false;
|
|
},
|
|
onPointerDown: function () {
|
|
return false;
|
|
},
|
|
onMouseUp: function () {
|
|
return false;
|
|
},
|
|
onMouseDown: function () {
|
|
return false;
|
|
},
|
|
|
|
computeSize(width) {
|
|
return [width, 20];
|
|
},
|
|
displayValue: function () {
|
|
if (this.config[0] === "FVAUTOINT") {
|
|
return Math.round(this.value).toString();
|
|
}
|
|
if (this.config[0] === "FVAUTOCOMBO") {
|
|
return this.value;
|
|
}
|
|
if (this.config[0] === "FVAUTOSTRING") {
|
|
return this.value;
|
|
}
|
|
// For FLOAT values, check if it's actually an integer
|
|
if (Number.isInteger(this.value)) {
|
|
return this.value.toString();
|
|
}
|
|
return this.value.toFixed(this.options.precision || 2);
|
|
},
|
|
parseValue: function (v) {
|
|
if (this.config[0] === "FVAUTOSTRING") {
|
|
return v;
|
|
}
|
|
if (typeof v === "string") {
|
|
return parseFloat(v);
|
|
}
|
|
return v;
|
|
},
|
|
serializeValue: function () {
|
|
// Return special value for AUTO mode
|
|
return this.isAuto ? -99999 : this.value;
|
|
},
|
|
deserializeValue: function (data) {
|
|
if (data === -99999) {
|
|
this.isAuto = true;
|
|
this.value = -99999;
|
|
} else {
|
|
this.isAuto = false;
|
|
this.value = data;
|
|
this.cachedValue = data;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Override callback to handle AUTO mode
|
|
widget.callback = function (v) {
|
|
if (this.isAuto) {
|
|
return; // Don't call the original callback in AUTO mode
|
|
}
|
|
const result = original.callback?.call(this, v);
|
|
return result;
|
|
};
|
|
|
|
// Override any potential click handlers
|
|
const originalOnClick = widget.onClick;
|
|
if (originalOnClick) {
|
|
widget.onClick = function (...args) {
|
|
if (this.isAuto) {
|
|
return false;
|
|
}
|
|
return originalOnClick.call(this, ...args);
|
|
};
|
|
}
|
|
|
|
return widget;
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "FastVideo.AutoWidgets",
|
|
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
if (nodeData?.name == "VideoGenerator" || nodeData?.name === "InferenceArgs" || nodeData?.name === "VAEConfig" ||
|
|
nodeData?.name === "TextEncoderConfig" || nodeData?.name === "DITConfig") {
|
|
// Add serialization support
|
|
chainCallback(nodeType.prototype, "onSerialize", function (info) {
|
|
if (!this.widgets) {
|
|
return;
|
|
}
|
|
// Ensure widgets_values exists
|
|
if (!info.widgets_values) {
|
|
info.widgets_values = {};
|
|
}
|
|
|
|
// Store AUTO widget states in a separate property
|
|
if (!info.auto_widget_states) {
|
|
info.auto_widget_states = {};
|
|
}
|
|
|
|
// Handle AUTO widgets specially
|
|
for (const w of this.widgets) {
|
|
if (w.type === "BOOLEAN" && w.isAuto !== undefined) {
|
|
// Store the serialized value (for Python node)
|
|
info.widgets_values[w.name] = w.serializeValue();
|
|
|
|
// Store the full state (for UI restoration)
|
|
info.auto_widget_states[w.name] = {
|
|
isAuto: w.isAuto,
|
|
value: w.value,
|
|
cachedValue: w.cachedValue
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add deserialization support
|
|
chainCallback(nodeType.prototype, "onConfigure", function (info) {
|
|
if (!this.widgets) {
|
|
return;
|
|
}
|
|
|
|
// First, restore from widgets_values (for backward compatibility)
|
|
if (info.widgets_values && Array.isArray(info.widgets_values)) {
|
|
for (let i = 0; i < this.widgets.length && i < info.widgets_values.length; i++) {
|
|
const w = this.widgets[i];
|
|
const value = info.widgets_values[i];
|
|
|
|
if (w.type === "BOOLEAN" && w.isAuto !== undefined) {
|
|
w.deserializeValue(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Then, restore full state if available
|
|
if (info.auto_widget_states) {
|
|
for (const w of this.widgets) {
|
|
if (w.type === "BOOLEAN" && w.isAuto !== undefined && w.name in info.auto_widget_states) {
|
|
const state = info.auto_widget_states[w.name];
|
|
|
|
w.isAuto = state.isAuto;
|
|
w.cachedValue = state.cachedValue;
|
|
w.value = state.isAuto ? -99999 : state.value;
|
|
|
|
w.callback?.(w.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Force a redraw
|
|
this.graph?.setDirtyCanvas(true, true);
|
|
});
|
|
|
|
|
|
// Override addInput to handle AUTO widgets
|
|
chainCallback(nodeType.prototype, "onNodeCreated", function () {
|
|
// Convert any existing widgets to AUTO widgets if needed
|
|
let new_widgets = [];
|
|
const intWidgetNames = ["sp_size", "tp_size", "height", "width", "num_frames", "num_inference_steps", "flow_shift", "seed", "fps", "scale_factor",
|
|
"tile_sample_min_height", "tile_sample_min_width", "tile_sample_min_num_frames", "tile_sample_stride_height", "tile_sample_stride_width",
|
|
"tile_sample_stride_num_frames", "blend_num_frames"
|
|
]
|
|
const floatWidgetNames = ["embedded_cfg_scale", "guidance_scale"]
|
|
const comboWidgetNames = ["vae_tiling", "vae_precision", "vae_sp", "text_encoder_precision", "precision",
|
|
"load_encoder", "load_decoder", "use_tiling", "use_temporal_tiling", "use_parallel_tiling", "dit_cpu_offload"
|
|
]
|
|
const stringWidgetNames = ["prefix", "quant_config", "lora_config", "image_path"]
|
|
|
|
if (this.widgets) {
|
|
for (let w of this.widgets) {
|
|
if (intWidgetNames.includes(w.name)) {
|
|
new_widgets.push(makeAutoAnnotated(w, ["FVAUTOINT", { "default": 0 }]));
|
|
} else if (floatWidgetNames.includes(w.name)) {
|
|
new_widgets.push(makeAutoAnnotated(w, ["FVAUTOFLOAT", { "default": 0 }]));
|
|
} else if (comboWidgetNames.includes(w.name)) {
|
|
new_widgets.push(makeAutoAnnotated(w, ["FVAUTOCOMBO", { "default": 0 }]));
|
|
} else if (stringWidgetNames.includes(w.name)) {
|
|
new_widgets.push(makeAutoAnnotated(w, ["FVAUTOSTRING", { "default": "" }]));
|
|
} else {
|
|
new_widgets.push(w);
|
|
}
|
|
}
|
|
this.widgets = new_widgets;
|
|
|
|
const autoWidgets = this.widgets.filter(w => w.type === "BOOLEAN" && w.isAuto !== undefined);
|
|
}
|
|
|
|
this.graph?.setDirtyCanvas(true, true);
|
|
});
|
|
}
|
|
},
|
|
|
|
async init() {
|
|
// Force a redraw of all nodes when the extension initializes
|
|
if (app.graph) {
|
|
setTimeout(() => {
|
|
app.graph.setDirtyCanvas(true, true);
|
|
}, 1000);
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log("FastVideo.core.js loaded");
|