Sweeping update. New features, more maintainable. See readme and comments for more info.
334 lines
16 KiB
JavaScript
334 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;
|
|
nodeType.prototype.hasInitialized = false;
|
|
|
|
// 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 = () => {
|
|
if (this.hasInitialized) {
|
|
return; // Skip if we've already initialized
|
|
}
|
|
|
|
const outputLinks = this.outputs[0]?.links || [];
|
|
let combinedConstraints = null;
|
|
|
|
// First pass: build combined constraints from all connections
|
|
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)) {
|
|
const currentConstraints = {
|
|
min: parseFloat(targetWidget.options.min),
|
|
max: parseFloat(targetWidget.options.max),
|
|
step: parseFloat(targetWidget.options.step),
|
|
isInt: this.type === "FeatureToFlexIntParam"
|
|
};
|
|
|
|
if (combinedConstraints) {
|
|
// Use most restrictive constraints
|
|
combinedConstraints = {
|
|
min: Math.max(currentConstraints.min, combinedConstraints.min),
|
|
max: Math.min(currentConstraints.max, combinedConstraints.max),
|
|
step: Math.max(currentConstraints.step, combinedConstraints.step),
|
|
isInt: this.type === "FeatureToFlexIntParam"
|
|
};
|
|
} else {
|
|
combinedConstraints = currentConstraints;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only proceed if we found any constraints
|
|
if (combinedConstraints) {
|
|
this.targetConstraints = combinedConstraints;
|
|
this.updateWidgetConstraints();
|
|
this.hasInitialized = true;
|
|
}
|
|
};
|
|
|
|
// 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;
|
|
|
|
// Check if we already have constraints from another connection
|
|
if (this.targetConstraints) {
|
|
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 {
|
|
this.targetConstraints = {
|
|
min: parseFloat(options.min),
|
|
max: parseFloat(options.max),
|
|
step: parseFloat(options.step),
|
|
isInt: this.type === "FeatureToFlexIntParam"
|
|
};
|
|
}
|
|
|
|
// Only set initial values if they are undefined, null, or outside the valid range
|
|
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);
|
|
|
|
// Only reset individual values if they are invalid
|
|
if (currentLower === undefined || currentLower === null ||
|
|
currentLower < this.targetConstraints.min ||
|
|
currentLower > this.targetConstraints.max) {
|
|
lowerWidget.value = this.targetConstraints.min;
|
|
}
|
|
|
|
if (currentUpper === undefined || currentUpper === null ||
|
|
currentUpper < this.targetConstraints.min ||
|
|
currentUpper > this.targetConstraints.max) {
|
|
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) {
|
|
// Store current values before resetting constraints
|
|
const lowerWidget = this.widgets.find(w => w.name === "lower_threshold");
|
|
const upperWidget = this.widgets.find(w => w.name === "upper_threshold");
|
|
const currentValues = {
|
|
lower: lowerWidget ? lowerWidget.value : null,
|
|
upper: upperWidget ? upperWidget.value : null
|
|
};
|
|
|
|
// Reset constraints initially
|
|
this.targetConstraints = null;
|
|
|
|
// Check all remaining connections and rebuild constraints
|
|
if (this.outputs[0].links && this.outputs[0].links.length > 0) {
|
|
let combinedConstraints = null;
|
|
|
|
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 currentConstraints = {
|
|
min: parseFloat(targetWidget.options.min),
|
|
max: parseFloat(targetWidget.options.max),
|
|
step: parseFloat(targetWidget.options.step),
|
|
isInt: this.type === "FeatureToFlexIntParam"
|
|
};
|
|
|
|
if (combinedConstraints) {
|
|
combinedConstraints = {
|
|
min: Math.max(currentConstraints.min, combinedConstraints.min),
|
|
max: Math.min(currentConstraints.max, combinedConstraints.max),
|
|
step: Math.max(currentConstraints.step, combinedConstraints.step),
|
|
isInt: this.type === "FeatureToFlexIntParam"
|
|
};
|
|
} else {
|
|
combinedConstraints = currentConstraints;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if (combinedConstraints) {
|
|
this.targetConstraints = combinedConstraints;
|
|
|
|
// Restore previous values if they're within the new constraints
|
|
if (lowerWidget && upperWidget) {
|
|
if (currentValues.lower !== null &&
|
|
currentValues.lower >= this.targetConstraints.min &&
|
|
currentValues.lower <= this.targetConstraints.max) {
|
|
lowerWidget.value = currentValues.lower;
|
|
}
|
|
if (currentValues.upper !== null &&
|
|
currentValues.upper >= this.targetConstraints.min &&
|
|
currentValues.upper <= this.targetConstraints.max) {
|
|
upperWidget.value = currentValues.upper;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// If no connections remain, keep the widgets but remove constraints
|
|
this.targetConstraints = null;
|
|
}
|
|
|
|
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) {
|
|
// Store current value before updating options
|
|
const currentValue = widget.value;
|
|
|
|
// Update widget options
|
|
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;
|
|
}
|
|
|
|
// Only reset the value if it's invalid
|
|
if (currentValue === null || currentValue === undefined ||
|
|
currentValue < this.targetConstraints.min ||
|
|
currentValue > this.targetConstraints.max) {
|
|
widget.value = name === "lower_threshold" ?
|
|
this.targetConstraints.min :
|
|
this.targetConstraints.max;
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
// 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]);
|
|
};
|
|
}
|
|
});
|