Files
ryanontheinside-ComfyUI_Rya…/web/js/widget_validation.js
T

314 lines
16 KiB
JavaScript

// Widget validation for RyanOnTheInside Scheduler nodes
import { app } from "../../../scripts/app.js";
//TODO: add validation for DrawableFeatureNode, frame count for instance.
// Register validation behavior when nodes are connected
app.registerExtension({
name: "RyanOnTheInside.WidgetValidation",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// Check if this is one of our scheduler nodes
if (!nodeData.category?.startsWith("RyanOnTheInside/FlexFeatures/Scheduling")) {
return;
}
// Store constraints from connected widgets
nodeType.prototype.targetConstraints = null;
// Add handler for when node is created (including on page load)
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
const result = onNodeCreated?.apply(this, arguments);
// Function to check connections
const checkConnections = () => {
const outputLinks = this.outputs[0]?.links || [];
for (const linkId of outputLinks) {
const link = app.graph.links[linkId];
if (!link) continue;
const targetNode = app.graph.getNodeById(link.target_id);
const targetSlot = link.target_slot;
if (targetNode?.widgets) {
const inputName = targetNode.inputs[targetSlot]?.name;
const targetWidget = targetNode.widgets.find(w => w.name === inputName);
if (targetWidget?.options || (targetWidget?.type === "converted-widget" && targetWidget.options)) {
this.targetConstraints = {
min: targetWidget.options.min,
max: targetWidget.options.max,
step: targetWidget.options.step,
isInt: this.type === "FeatureToFlexIntParam"
};
// Set initial values to match target widget's range
const lowerWidget = this.widgets.find(w => w.name === "lower_threshold");
const upperWidget = this.widgets.find(w => w.name === "upper_threshold");
if (lowerWidget && upperWidget) {
lowerWidget.value = this.targetConstraints.min;
upperWidget.value = this.targetConstraints.max;
}
this.updateWidgetConstraints();
}
}
}
};
// Check connections at different intervals to ensure graph is loaded
checkConnections();
setTimeout(checkConnections, 100);
setTimeout(checkConnections, 1000);
return result;
};
const originalOnConnectOutput = nodeType.prototype.onConnectOutput;
nodeType.prototype.onConnectOutput = function (slot, type, input, targetNode, targetSlot) {
const result = originalOnConnectOutput?.apply(this, arguments);
if (targetNode?.widgets) {
const inputName = targetNode.inputs[targetSlot]?.name;
const targetWidget = targetNode.widgets.find(w => w.name === inputName);
if (targetWidget?.options || (targetWidget?.type === "converted-widget" && targetWidget.options)) {
const options = targetWidget.options;
// console.log('Node type:', this.type);
// console.log('Target widget options:', options);
// Check if we already have constraints from another connection
if (this.targetConstraints) {
// console.log('Warning: Already connected to another widget with constraints:', this.targetConstraints);
// Option 1: Prevent multiple connections
// return false;
// Option 2: Use the most restrictive constraints
this.targetConstraints = {
min: Math.max(parseFloat(options.min), this.targetConstraints.min),
max: Math.min(parseFloat(options.max), this.targetConstraints.max),
step: Math.max(parseFloat(options.step), this.targetConstraints.step),
isInt: this.type === "FeatureToFlexIntParam"
};
// console.log('Updated to more restrictive constraints:', this.targetConstraints);
} else {
this.targetConstraints = {
min: parseFloat(options.min),
max: parseFloat(options.max),
step: parseFloat(options.step),
isInt: this.type === "FeatureToFlexIntParam"
};
}
// console.log('Target constraints:', this.targetConstraints);
// Only set initial values if current values are outside constraints
const lowerWidget = this.widgets.find(w => w.name === "lower_threshold");
const upperWidget = this.widgets.find(w => w.name === "upper_threshold");
if (lowerWidget && upperWidget) {
const currentLower = parseFloat(lowerWidget.value);
const currentUpper = parseFloat(upperWidget.value);
// console.log('Current widget values:', {
// lower: currentLower,
// upper: currentUpper
// });
// Only reset if values are outside constraints
if (currentLower < this.targetConstraints.min ||
currentLower > this.targetConstraints.max ||
currentUpper < this.targetConstraints.min ||
currentUpper > this.targetConstraints.max) {
// console.log('Resetting values to match constraints');
lowerWidget.value = this.targetConstraints.min;
upperWidget.value = this.targetConstraints.max;
}
}
this.updateWidgetConstraints();
}
}
return result;
};
nodeType.prototype.onConnectionsChange = function(slotType, slot, isConnected, link_info, output) {
// Handle disconnection
if (!isConnected && slotType === LiteGraph.OUTPUT) {
// console.log('Output disconnected, checking remaining connections');
// Reset constraints initially
this.targetConstraints = null;
// Check all remaining connections and rebuild constraints
if (this.outputs[0].links && this.outputs[0].links.length > 0) {
// console.log('Found remaining connections, rebuilding constraints');
this.outputs[0].links.forEach(linkId => {
const link = this.graph.links[linkId];
if (!link) return;
const targetNode = this.graph.getNodeById(link.target_id);
const targetSlot = link.target_slot;
if (targetNode?.widgets) {
const inputName = targetNode.inputs[targetSlot]?.name;
const targetWidget = targetNode.widgets.find(w => w.name === inputName);
if (targetWidget?.options) {
const options = targetWidget.options;
// console.log('Found target widget options:', options);
// Update constraints based on remaining connection
if (this.targetConstraints) {
// If we already have constraints, use the most restrictive ones
this.targetConstraints = {
min: Math.max(parseFloat(options.min), this.targetConstraints.min),
max: Math.min(parseFloat(options.max), this.targetConstraints.max),
step: Math.max(parseFloat(options.step), this.targetConstraints.step),
isInt: this.type === "FeatureToFlexIntParam"
};
} else {
// First remaining connection sets initial constraints
this.targetConstraints = {
min: parseFloat(options.min),
max: parseFloat(options.max),
step: parseFloat(options.step),
isInt: this.type === "FeatureToFlexIntParam"
};
}
// console.log('Updated constraints:', this.targetConstraints);
}
}
});
} else {
// console.log('No remaining connections, clearing constraints');
// Reset widgets to their default constraints if needed
const lowerWidget = this.widgets.find(w => w.name === "lower_threshold");
const upperWidget = this.widgets.find(w => w.name === "upper_threshold");
if (lowerWidget && upperWidget) {
// Reset to default widget options if they exist
if (lowerWidget.options) {
this.targetConstraints = {
min: parseFloat(lowerWidget.options.min),
max: parseFloat(upperWidget.options.max),
step: parseFloat(lowerWidget.options.step || 1),
isInt: this.type === "FeatureToFlexIntParam"
};
}
}
}
// Update widget constraints with new/cleared constraints
this.updateWidgetConstraints();
}
};
// Add method to update widget constraints
nodeType.prototype.updateWidgetConstraints = function() {
if (!this.targetConstraints) return;
// Update thresholds based on target constraints
const thresholdWidgets = ["lower_threshold", "upper_threshold"];
thresholdWidgets.forEach(name => {
const widget = this.widgets.find(w => w.name === name);
if (widget) {
if (this.targetConstraints.min !== undefined) {
widget.options.min = this.targetConstraints.min;
}
if (this.targetConstraints.max !== undefined) {
widget.options.max = this.targetConstraints.max;
}
if (this.targetConstraints.step !== undefined) {
widget.options.step = this.targetConstraints.step;
}
}
});
this.clampWidgetValues();
};
// Add method to clamp widget values
nodeType.prototype.clampWidgetValues = function() {
const lowerWidget = this.widgets.find(w => w.name === "lower_threshold");
const upperWidget = this.widgets.find(w => w.name === "upper_threshold");
if (lowerWidget && upperWidget) {
const targetMin = parseFloat(this.targetConstraints?.min ?? lowerWidget.options.min);
const targetMax = parseFloat(this.targetConstraints?.max ?? upperWidget.options.max);
const step = parseFloat(this.targetConstraints?.step ?? 1);
// Function to snap value to nearest step with decimal precision
const snapToStep = (value) => {
// Special case: if value is the minimum or maximum, don't snap it
if (value === targetMin || value === targetMax) {
// console.log(`Preserving exact value ${value} (min/max)`);
return value;
}
// For other values, snap to nearest step but ensure we don't go below minimum
const snapped = Math.max(
targetMin,
Math.round(value / step) * step
);
// console.log(`Snapping ${value} to step ${step}: ${snapped}`);
return snapped;
};
// Ensure lower threshold is less than upper threshold and both are within target constraints
let lowerValue = Math.max(targetMin, Math.min(parseFloat(lowerWidget.value), parseFloat(upperWidget.value)));
let upperValue = Math.max(parseFloat(lowerWidget.value), Math.min(parseFloat(upperWidget.value), targetMax));
// Snap values to steps
lowerValue = snapToStep(lowerValue);
upperValue = snapToStep(upperValue);
// Log values before integer rounding
// console.log('Before integer rounding:', {
// lowerValue,
// upperValue
// });
// Round if integer type, otherwise maintain decimal precision
if (this.targetConstraints?.isInt) {
lowerValue = Math.round(lowerValue);
upperValue = Math.round(upperValue);
// console.log('After integer rounding:', {
// lowerValue,
// upperValue
// });
}
lowerWidget.value = lowerValue;
upperWidget.value = upperValue;
// Log final values
// console.log('Final widget values:', {
// lower: lowerWidget.value,
// upper: upperWidget.value
// });
}
};
// Override the widget's callback to enforce constraints
const originalWidgetCallback = nodeType.prototype.onWidgetChanged;
nodeType.prototype.onWidgetChanged = function(widget, value) {
if (["lower_threshold", "upper_threshold"].includes(widget.name)) {
const step = this.targetConstraints?.step ?? 1;
// Snap to step
value = Math.round(value / step) * step;
// Round if integer type
if (this.targetConstraints?.isInt) {
value = Math.round(value);
}
widget.value = value;
this.clampWidgetValues();
}
return originalWidgetCallback?.apply(this, [widget, value]);
};
}
});