Files
FunnyFinger a92aaa7159 updated to v2.0.0
added new features such as dynamic sum adjustment and sum-lock dynamics
2025-04-19 20:22:11 +03:00

1150 lines
48 KiB
JavaScript

import { app } from "../../scripts/app.js";
// Define the hard cap for sliders maximum
const SLIDERS_MAX_HARD_CAP = 2.0;
const REFERENCE_AVG_BUFFER = 0.01; // Buffer to prevent sliders_max hitting exactly reference_avg
// --- Helper Functions ---
const findWidgetByName = (node, name) => {
return node.widgets ? node.widgets.find((w) => w.name === name) : null;
};
const getWidgetValue = (widget) => {
const val = widget?.value;
const num = Number(val);
return isNaN(num) ? 0 : num;
};
// Function to check if node is our Slider Stacker using multiple identification methods
function isSliderStackerNode(node) {
// Check the display title
const nodeTitle = node.getTitle ? node.getTitle() : node.title;
if (nodeTitle === "Slider Stacker") return true;
// Check internal type name
if (node.type === "Slider Stacker (DSS)") return true;
// Check properties that are unique to our node
if (findWidgetByName(node, "sliders_count") &&
findWidgetByName(node, "sliders_max") &&
findWidgetByName(node, "slider_name_1")) {
return true;
}
return false;
}
// Intercepts widget value changes
function interceptWidgetValue(node, widgetName, setterCallback) {
const widget = findWidgetByName(node, widgetName);
if (!widget) {
// console.error(`Slider Stacker: Could not find widget ${widgetName} to intercept.`);
return;
}
let internalValueTracker = widget.value;
let originalDescriptor = Object.getOwnPropertyDescriptor(widget, 'value');
if (!originalDescriptor) {
let proto = Object.getPrototypeOf(widget);
while (proto && !originalDescriptor && proto !== Object.prototype) {
originalDescriptor = Object.getOwnPropertyDescriptor(proto, 'value');
proto = Object.getPrototypeOf(proto);
}
}
Object.defineProperty(widget, 'value', {
configurable: true,
enumerable: true,
get() {
return originalDescriptor?.get ? originalDescriptor.get.call(widget) : internalValueTracker;
},
set(newValue) {
// --- Special Check for Locked Sum ---
// If this specific widget is 'sliders_sum' and the lock is on, prevent user changes.
if (widgetName === 'sliders_sum') {
const lockWidget = findWidgetByName(node, "lock_sum");
if (lockWidget?.value && !node._isUpdatingInternally) {
// Only block manual user changes while allowing internal updates
// Visually revert input if possible (optional, helps reinforce lock)
if (widget.inputEl) {
const actualSum = getCurrentSum(node);
const precision = widget.options?.precision ?? 2;
widget.inputEl.value = actualSum.toFixed(precision);
}
// Log attempt to change locked sum
// console.log("Manual change to locked sum widget blocked");
// Prevent setting the value or calling the callback
return;
}
}
// --- Regular Update Logic ---
if (node._isUpdatingInternally) {
if (originalDescriptor?.set) { originalDescriptor.set.call(widget, newValue); } else { internalValueTracker = newValue; }
if (widget.inputEl) {
try { widget.inputEl.value = Number(newValue).toFixed(widget.options?.precision ?? 2); } catch (e) { /* Ignore */ }
}
return;
}
const precision = widget.options?.precision ?? 2;
let clampedNewValue = Math.max(widget.options?.min ?? -Infinity, Math.min(Number(newValue) || 0, widget.options?.max ?? Infinity));
clampedNewValue = parseFloat(clampedNewValue.toFixed(precision));
if (originalDescriptor?.set) { originalDescriptor.set.call(widget, clampedNewValue); }
internalValueTracker = clampedNewValue;
if(widget.inputEl) {
try { widget.inputEl.value = internalValueTracker.toFixed(precision); } catch(e) {/* Ignore */}
}
// Only call the callback if the value actually changed (or if it's not locked sum)
// Note: setterCallback might still be called even if value is same due to float precision?
// Let's call it regardless for now, the callback can handle its own logic.
setterCallback(clampedNewValue);
}
});
}
// --- Sum Lock Logic ---
// Moved applySumLockVisuals definition BEFORE its first use
function applySumLockVisuals(sumWidget, locked) {
if (sumWidget?.inputEl) {
sumWidget.inputEl.style.pointerEvents = locked ? 'none' : '';
sumWidget.inputEl.style.opacity = locked ? '0.6' : '';
// Add readOnly property toggle
sumWidget.inputEl.readOnly = locked;
}
}
// --- Sum Widget Limit Calculation & Update ---
// Calculate the lowest possible sum given the original deltas
function calculateMinimumPossibleSum(node) {
const sliderCountWidget = findWidgetByName(node, "sliders_count");
if (!sliderCountWidget) return 0;
const count = getWidgetValue(sliderCountWidget);
let sum = 0;
// For minimum sum, only positive deltas contribute
for (let i = 1; i <= count; i++) {
const delta = node.originalDeltas?.[i] || 0;
if (delta > 0) {
sum += delta;
}
}
return sum;
}
// Calculate the maximum possible sum when maintaining original deltas
function calculateMaximumPossibleSum(node) {
const sliderCountWidget = findWidgetByName(node, "sliders_count");
if (!sliderCountWidget || !node.originalDeltas) return 0;
const count = getWidgetValue(sliderCountWidget);
let maxDelta = 0;
let totalDeltas = 0;
let activeSliderCount = 0;
// Find maximum delta and count active sliders
for (let i = 1; i <= count; i++) {
const wtWidget = findWidgetByName(node, `(${i})`);
if (wtWidget) {
const delta = node.originalDeltas[i] || 0;
maxDelta = Math.max(maxDelta, delta);
totalDeltas += delta;
activeSliderCount++;
}
}
// Calculate the maximum possible sum:
if (activeSliderCount > 0) {
const maxBaseValue = SLIDERS_MAX_HARD_CAP - maxDelta;
return (maxBaseValue * activeSliderCount) + totalDeltas;
}
return 0;
}
// Updates the min/max limits of the sliders_sum widget, incorporating the lock state
function updateSumWidgetLimits(node) {
const sliderCountWidget = findWidgetByName(node, "sliders_count");
const sumWidget = findWidgetByName(node, "sliders_sum");
const lockWidget = findWidgetByName(node, "lock_sum");
if (!sliderCountWidget || !sumWidget || !lockWidget) return;
const count = getWidgetValue(sliderCountWidget);
const currentSum = getWidgetValue(sumWidget);
const isLocked = lockWidget?.value || false;
const precision = sumWidget.options?.precision ?? 2;
const epsilon = Math.pow(10, -precision) / 2; // Small value based on precision
node._isUpdatingInternally = true;
// Apply visual indicators for lock state but don't restrict the sum widget value
if (isLocked) {
// Apply lock visuals but keep actual sum limits
const absoluteMinSum = calculateMinimumPossibleSum(node);
const absoluteMaxSum = calculateMaximumPossibleSum(node);
let minValue = Math.max(0, absoluteMinSum);
let maxValue = absoluteMaxSum;
// Update the widget options with dynamic limits
sumWidget.options.min = parseFloat(minValue.toFixed(precision));
sumWidget.options.max = parseFloat(maxValue.toFixed(precision));
// Apply visual indicators for lock state
applySumLockVisuals(sumWidget, true);
} else {
// Remove lock: Calculate dynamic limits based on deltas
const absoluteMinSum = calculateMinimumPossibleSum(node);
const absoluteMaxSum = calculateMaximumPossibleSum(node);
let minValue = Math.max(0, absoluteMinSum);
let maxValue = absoluteMaxSum;
// Update the widget options with dynamic limits
sumWidget.options.min = parseFloat(minValue.toFixed(precision));
sumWidget.options.max = parseFloat(maxValue.toFixed(precision));
applySumLockVisuals(sumWidget, false);
// Update reference_sum when not locked
node.reference_sum = currentSum;
// Clamp current value if it falls outside the new dynamic range
if (currentSum < minValue) {
sumWidget.value = minValue;
} else if (currentSum > maxValue) {
sumWidget.value = maxValue;
}
}
node._isUpdatingInternally = false;
// Ensure the input element reflects potential clamping/option changes
if (sumWidget.inputEl) {
try { sumWidget.inputEl.value = getWidgetValue(sumWidget).toFixed(precision); } catch(e) {}
}
}
// Updates the displayed value of the sliders_sum widget
function updateTotalSumWidget(node) {
const sumWidget = findWidgetByName(node, "sliders_sum");
const lockWidget = findWidgetByName(node, "lock_sum");
if (!sumWidget) return;
// Always calculate and display the real sum of all sliders
const sliderCountWidget = findWidgetByName(node, "sliders_count");
const count = getWidgetValue(sliderCountWidget);
let currentSum = 0;
for (let i = 1; i <= count; i++) {
const wtWidget = findWidgetByName(node, `(${i})`);
if (wtWidget) {
currentSum += getWidgetValue(wtWidget);
}
}
// Always update the sum widget with the actual sum regardless of lock state
// Use direct assignment to bypass the interceptor's lock check
node._isUpdatingInternally = true;
if (sumWidget.inputEl) {
const precision = sumWidget.options?.precision ?? 2;
try {
sumWidget.inputEl.value = currentSum.toFixed(precision);
} catch(e) {/* Ignore */}
}
// Update the internal value directly
let originalDescriptor = Object.getOwnPropertyDescriptor(sumWidget, 'value');
if (originalDescriptor?.set) {
originalDescriptor.set.call(sumWidget, currentSum);
} else {
sumWidget.value = currentSum;
}
node._isUpdatingInternally = false;
// Update reference_sum only when not locked
if (!lockWidget?.value) {
node.reference_sum = currentSum;
}
// Update sum widget limits
updateSumWidgetLimits(node);
if (node.graph) {
node.graph.setDirtyCanvas(true, true);
}
}
// Calculate the maximum base value that can be used without any slider exceeding the hard cap
function calculateMaxBaseValue(activeSliders) {
if (!activeSliders || activeSliders.length === 0) return 0;
let maxPossibleBaseValue = SLIDERS_MAX_HARD_CAP;
for (const slider of activeSliders) {
const maxBaseForThisSlider = SLIDERS_MAX_HARD_CAP - (slider.delta || 0);
maxPossibleBaseValue = Math.min(maxPossibleBaseValue, maxBaseForThisSlider);
}
return Math.max(0, maxPossibleBaseValue);
}
// Stores the relative differences between sliders
function storeDeltaValues(node) {
if (!node.sliderDeltas) node.sliderDeltas = {};
if (!node.originalDeltas) node.originalDeltas = {}; // Store a copy that won't be modified
const sliderCountWidget = findWidgetByName(node, "sliders_count");
if (!sliderCountWidget) return;
const count = getWidgetValue(sliderCountWidget);
// Find the minimum value among all active sliders
let minValue = Number.MAX_VALUE;
const activeSliders = [];
for (let i = 1; i <= count; i++) {
const wtWidget = findWidgetByName(node, `(${i})`);
if (wtWidget) {
const value = getWidgetValue(wtWidget);
minValue = Math.min(minValue, value);
activeSliders.push({
index: i,
widget: wtWidget,
value: value
});
}
}
if (activeSliders.length === 0) return;
if (minValue === Number.MAX_VALUE) minValue = 0; // Handle case where all values might be non-numeric somehow
// If min value is very small, treat it as zero
if (minValue < 0.001) minValue = 0;
// Store the delta for each slider from the minimum value
for (const slider of activeSliders) {
node.sliderDeltas[slider.index] = slider.value - minValue;
node.originalDeltas[slider.index] = slider.value - minValue; // Store unchanging copy
}
// Also store the minimum value and current sum for future calculations
node.baseSliderValue = minValue;
node.lastTotalSum = getCurrentSum(node);
}
// Gets the current sum of slider values
function getCurrentSum(node) {
const sliderCountWidget = findWidgetByName(node, "sliders_count");
if (!sliderCountWidget) return 0;
const count = getWidgetValue(sliderCountWidget);
let totalSum = 0;
for (let i = 1; i <= count; i++) {
const wtWidget = findWidgetByName(node, `(${i})`);
if (wtWidget) {
totalSum += getWidgetValue(wtWidget);
}
}
return totalSum;
}
// Updates individual sliders based on a change in the total sum
function updateSlidersBasedOnDeltas(node, newSum) {
if (!node.sliderDeltas || !node.originalDeltas || node._isUpdatingInternally) return;
// Mark that we're updating via sum to prevent delta recalculation
node._isUpdatingSumSlider = true;
node._isUpdatingInternally = true;
const sliderCountWidget = findWidgetByName(node, "sliders_count");
const lockWidget = findWidgetByName(node, "lock_sum");
if (!sliderCountWidget) {
node._isUpdatingInternally = false;
node._isUpdatingSumSlider = false;
return;
}
const count = getWidgetValue(sliderCountWidget);
// Use reference_sum instead of newSum when toggle is on
const targetSum = lockWidget?.value ? node.reference_sum : newSum;
// Collect active sliders with their deltas and calculate min/max possible sum
const activeSliders = [];
let positiveDeltas = 0;
let totalDelta = 0;
for (let i = 1; i <= count; i++) {
const wtWidget = findWidgetByName(node, `(${i})`);
if (wtWidget) {
const delta = node.originalDeltas[i] || 0;
// Track positive deltas for minimum sum calculation
if (delta > 0) {
positiveDeltas += delta;
}
activeSliders.push({
index: i,
widget: wtWidget,
delta: delta,
min: wtWidget.options?.min ?? 0.0,
max: SLIDERS_MAX_HARD_CAP // Use hard cap as the absolute maximum
});
totalDelta += delta;
}
}
if (activeSliders.length === 0) {
node._isUpdatingInternally = false;
node._isUpdatingSumSlider = false;
return;
}
// Calculate the maximum base value that ensures no slider exceeds the hard cap
const maxBaseValue = calculateMaxBaseValue(activeSliders);
// Calculate absolute min and max possible sums
const minPossibleSum = positiveDeltas;
const maxPossibleSum = (maxBaseValue * activeSliders.length) + totalDelta;
// Ensure the target sum is within valid bounds
const clampedTargetSum = Math.max(minPossibleSum, Math.min(maxPossibleSum, targetSum));
// Special case: If we're at or near minimum sum
if (Math.abs(clampedTargetSum - minPossibleSum) < 0.01) {
for (const slider of activeSliders) {
const targetValue = Math.max(0, slider.delta);
slider.widget.value = targetValue;
}
}
// Special case: If we're at or near maximum sum
else if (Math.abs(clampedTargetSum - maxPossibleSum) < 0.01) {
for (const slider of activeSliders) {
const targetValue = maxBaseValue + slider.delta;
slider.widget.value = Math.min(targetValue, SLIDERS_MAX_HARD_CAP);
}
}
// Normal case: Calculate base value
else {
const baseValue = (clampedTargetSum - totalDelta) / activeSliders.length;
for (const slider of activeSliders) {
const targetValue = baseValue + slider.delta;
const constrainedValue = Math.max(slider.min, Math.min(targetValue, slider.max));
slider.widget.value = constrainedValue;
}
}
// Update max strength to maintain consistency
const maxStrengthWidget = findWidgetByName(node, "sliders_max");
if (maxStrengthWidget) {
const newCalculatedMax = recalculateCurrentMaxStrength(node);
maxStrengthWidget.value = newCalculatedMax;
updateNormalization(node, newCalculatedMax);
}
// Update the widget limits
updateSumWidgetLimits(node);
// Verify we haven't exceeded the maximum possible sum and correct if needed
const currentSumAfterUpdate = getCurrentSum(node);
if (currentSumAfterUpdate > maxPossibleSum + 0.01) {
// console.warn(`Sum exceeded maximum: ${currentSumAfterUpdate} > ${maxPossibleSum}, correcting...`);
const scaleFactor = maxPossibleSum / currentSumAfterUpdate;
for (const slider of activeSliders) {
const currentValue = getWidgetValue(slider.widget);
slider.widget.value = currentValue * scaleFactor;
}
}
// Keep track of the sum but never recalculate deltas during sum adjustment
node.lastTotalSum = getCurrentSum(node);
// Clear flags
node._isUpdatingInternally = false;
node._isUpdatingSumSlider = false;
if (node.graph) {
node.graph.setDirtyCanvas(true, true);
}
}
// --- Max Strength and Normalization Logic ---
function recalculateCurrentMaxStrength(node) {
const sliderCountWidget = findWidgetByName(node, "sliders_count");
const count = getWidgetValue(sliderCountWidget);
let currentMaxValue = 0;
for (let i = 1; i <= count; i++) {
const wtWidget = findWidgetByName(node, `(${i})`);
if (wtWidget) {
currentMaxValue = Math.max(currentMaxValue, getWidgetValue(wtWidget));
}
}
const maxStrengthWidget = findWidgetByName(node, "sliders_max");
const overallMax = maxStrengthWidget?.options?.max ?? 2.0;
const overallMin = maxStrengthWidget?.options?.min ?? 0.0;
return Math.max(overallMin, Math.min(currentMaxValue, overallMax));
}
function updateNormalization(node, currentMaxStrength) {
if (!node.normalizedSliderWeights) node.normalizedSliderWeights = {};
const sliderCountWidget = findWidgetByName(node, "sliders_count");
const maxSliders = sliderCountWidget?.options?.max || 50;
for (let i = 1; i <= maxSliders; i++) {
const wtWidget = findWidgetByName(node, `(${i})`);
if (wtWidget) {
const currentWt = getWidgetValue(wtWidget);
// Handle division by zero or near-zero
const norm = (currentMaxStrength > 0.0001) ? (currentWt / currentMaxStrength) : 0;
node.normalizedSliderWeights[i] = norm;
}
}
}
function updateAllSliderDisplays(node, newMaxStrength) {
if (!node.normalizedSliderWeights || node._isUpdatingInternally) return;
node._isUpdatingInternally = true;
const sliderCountWidget = findWidgetByName(node, "sliders_count");
const count = getWidgetValue(sliderCountWidget);
for (let i = 1; i <= count; i++) {
const wtWidget = findWidgetByName(node, `(${i})`);
if (wtWidget) {
const normalizedValue = node.normalizedSliderWeights[i] || 0;
let newDisplayValue = normalizedValue * newMaxStrength;
const widgetMax = wtWidget.options?.max ?? 2.0;
const widgetMin = wtWidget.options?.min ?? 0.0;
newDisplayValue = Math.max(widgetMin, Math.min(newDisplayValue, widgetMax, newMaxStrength));
if (Math.abs(getWidgetValue(wtWidget) - newDisplayValue) > 0.001) {
wtWidget.value = newDisplayValue;
}
}
}
if (node.graph) {
node.graph.setDirtyCanvas(true, true);
}
node._isUpdatingInternally = false;
}
// Calculate the current average of active sliders
function calculateCurrentAverage(node) {
const sliderCountWidget = findWidgetByName(node, "sliders_count");
if (!sliderCountWidget) return 0;
const count = getWidgetValue(sliderCountWidget);
let totalSum = 0;
let activeSliderCount = 0;
for (let i = 1; i <= count; i++) {
const wtWidget = findWidgetByName(node, `(${i})`);
if (wtWidget) {
totalSum += getWidgetValue(wtWidget);
activeSliderCount++;
}
}
return activeSliderCount > 0 ? totalSum / activeSliderCount : 0;
}
// Updates the internal state for sliders_avg and reference_avg
function updateAverageState(node, updateContext = null) {
const lockWidget = findWidgetByName(node, "lock_sum");
const currentAvg = calculateCurrentAverage(node);
node.sliders_avg = currentAvg;
// Update reference_avg unless lock is ON AND the update came from sliders_max
if (!(lockWidget?.value && updateContext === 'sliders_max')) {
node.reference_avg = currentAvg;
}
// console.log(`Avg updated: ${node.sliders_avg.toFixed(3)}, Ref Avg: ${node.reference_avg.toFixed(3)}, Context: ${updateContext}`);
}
// --- ComfyUI Extension ---
app.registerExtension({
name: "Comfy.DynamicSlidersStack.DynamicBehavior",
nodeCreated(node) {
if (!isSliderStackerNode(node)) return;
// Initialize node state
node.normalizedSliderWeights = {};
node.sliderDeltas = {};
node.originalDeltas = {}; // Add immutable copy of deltas
node.baseSliderValue = 0;
node.lastTotalSum = 0;
node.reference_sum = 0; // Reference sum for locked mode
node.sliders_avg = 0; // NEW: Initialize sliders average
node.reference_avg = 0; // NEW: Initialize reference average
node._isUpdatingInternally = false;
node._isUpdatingSumSlider = false;
// Get key widgets
const maxStrengthWidget = findWidgetByName(node, "sliders_max");
const sliderCountWidget = findWidgetByName(node, "sliders_count");
const sumWidget = findWidgetByName(node, "sliders_sum");
const lockWidget = findWidgetByName(node, "lock_sum");
if (!maxStrengthWidget || !sliderCountWidget || !sumWidget || !lockWidget) {
// console.error("Slider Stacker Dynamic.js: Setup failed, required widgets not found (max, count, sum, lock).");
return;
}
// --- Initialize ---
setTimeout(() => {
// Set up initial max strength
const initialMax = recalculateCurrentMaxStrength(node);
if (Math.abs(initialMax - getWidgetValue(maxStrengthWidget)) > 0.001) {
maxStrengthWidget.value = initialMax;
}
// Initialize normalization and slider displays
updateNormalization(node, initialMax);
updateAllSliderDisplays(node, initialMax);
// Store initial delta values between sliders
storeDeltaValues(node);
// Initialize reference_sum with the actual sum
const initialSum = getCurrentSum(node);
node.reference_sum = initialSum;
// Initial sum calculation and limits update
updateTotalSumWidget(node);
// NEW: Initial average calculation
updateAverageState(node);
}, 150);
// --- Set up Widget Interceptors ---
// Intercept sliders_max changes
interceptWidgetValue(node, "sliders_max", (newMax) => {
const lockWidget = findWidgetByName(node, "lock_sum");
let actualAchievedMax; // Variable to store the max value after adjustments
// Epsilon for general comparison still needed if separate from buffer
const epsilon = 0.0001;
if (lockWidget?.value) {
// --- Locked Sum Behavior ---
// Clamp the requested max based on reference average + BUFFER and hard cap
const effectiveMax = Math.max(node.reference_avg + REFERENCE_AVG_BUFFER, Math.min(newMax, SLIDERS_MAX_HARD_CAP));
// Call the new locked update function
updateSlidersForMaxChangeLockedSum(node, effectiveMax);
// After locked update, check the actual max achieved
actualAchievedMax = recalculateCurrentMaxStrength(node);
// Correct the sliders_max widget if the effectiveMax couldn't be fully reached
// Or if the user tried to drag below reference_avg + BUFFER
const valueToSet = Math.max(node.reference_avg + REFERENCE_AVG_BUFFER, actualAchievedMax); // Ensure widget never shows less than ref_avg + BUFFER
if (Math.abs(getWidgetValue(maxStrengthWidget) - valueToSet) > 0.001) { // Use a slightly larger tolerance for setting widget display
node._isUpdatingInternally = true;
maxStrengthWidget.value = valueToSet;
node._isUpdatingInternally = false;
}
} else {
// --- Unlocked Sum Behavior (Original) ---
updateAllSliderDisplays(node, newMax); // Use the original function
actualAchievedMax = recalculateCurrentMaxStrength(node);
// Correct the widget if requested max wasn't reached
if (Math.abs(actualAchievedMax - newMax) > 0.001) {
node._isUpdatingInternally = true;
maxStrengthWidget.value = actualAchievedMax;
node._isUpdatingInternally = false;
}
}
// --- Common Updates (Run for both locked and unlocked) ---
// Deltas might need recalculation based on the new slider values
storeDeltaValues(node);
updateTotalSumWidget(node); // Update sum display (should be constant if locked)
updateAverageState(node, 'sliders_max'); // Update average state with context
});
// Intercept sliders_count changes
interceptWidgetValue(node, "sliders_count", (newCount) => {
const currentMaxStrength = getWidgetValue(maxStrengthWidget);
updateNormalization(node, currentMaxStrength);
updateAllSliderDisplays(node, currentMaxStrength);
storeDeltaValues(node);
updateTotalSumWidget(node);
updateAverageState(node); // NEW: Update average state
});
// Intercept sliders_sum changes
interceptWidgetValue(node, "sliders_sum", (newSum) => {
// When toggle is off, update sliders based on the new sum
// When toggle is on, use the reference_sum instead
if (!lockWidget.value) {
// If unlocked, update sliders AND reference_sum based on the new sum
updateSlidersBasedOnDeltas(node, newSum);
node.reference_sum = newSum;
} else {
// If locked, completely ignore manual changes and revert to actual sum
// Set a timeout to revert the widget to show the actual sum after user input
setTimeout(() => {
// Get current actual sum
const actualSum = getCurrentSum(node);
// Force update the widget to show actual sum
node._isUpdatingInternally = true;
sumWidget.value = actualSum;
node._isUpdatingInternally = false;
// Make sure the input element displays the correct value
if (sumWidget.inputEl) {
try {
const precision = sumWidget.options?.precision ?? 2;
sumWidget.inputEl.value = actualSum.toFixed(precision);
} catch(e) {}
}
}, 0);
// Use reference_sum for any slider updates
updateSlidersBasedOnDeltas(node, node.reference_sum);
}
// Always update limits/visuals after a change attempt
updateSumWidgetLimits(node);
updateAverageState(node); // NEW: Update average state after sum changes
});
// Intercept lock_sum - Trigger limit/visual update
interceptWidgetValue(node, "lock_sum", (isLocked) => {
// If locking (changing from unlocked to locked), store the current sum as reference
if (isLocked) {
// Store the current calculated sum as the reference value when locking
node.reference_sum = getCurrentSum(node);
}
// Update the sum widget limits and appearance based on the new lock state
updateSumWidgetLimits(node);
// Always update total sum widget to reflect actual calculated sum
updateTotalSumWidget(node);
updateAverageState(node); // NEW: Update average state after lock change
if (node.graph) node.graph.setDirtyCanvas(true, true); // Redraw to show visual changes
});
const maxSlidersIntercept = sliderCountWidget?.options?.max || 50;
for (let i = 1; i <= maxSlidersIntercept; i++) {
interceptWidgetValue(node, `(${i})`, (newWt) => {
// Skip delta update if we're updating via the sum slider
if (node._isUpdatingSumSlider) {
updateTotalSumWidget(node); // Still update sum display
return;
}
// Skip delta recalculation and inverse adjustment if we're updating internally
if (node._isUpdatingInternally) {
return;
}
// Get current value for comparison
const sliderWidget = findWidgetByName(node, `(${i})`);
const currentValue = getWidgetValue(sliderWidget);
// Check if toggle is on and we need to maintain constant sum
const lockWidget = findWidgetByName(node, "lock_sum");
if (lockWidget?.value) {
// In lock mode, redistribute changes to maintain constant sum
// Apply redistribution and get constrained new value
const constrainedNewValue = redistributeSliderChange(node, i, newWt, currentValue);
// If constrained value is different than the attempted value, update the widget
if (Math.abs(constrainedNewValue - newWt) > 0.0001) {
// Need to update the widget with constrained value, but avoid triggering another intercept
node._isUpdatingInternally = true;
sliderWidget.value = constrainedNewValue;
node._isUpdatingInternally = false;
// Update max strength and normalization to reflect the new state
const currentMaxStrengthWidget = findWidgetByName(node, "sliders_max");
const newCalculatedMax = recalculateCurrentMaxStrength(node);
updateNormalization(node, newCalculatedMax);
if (Math.abs(newCalculatedMax - getWidgetValue(currentMaxStrengthWidget)) > 0.001) {
node._isUpdatingInternally = true;
currentMaxStrengthWidget.value = newCalculatedMax;
node._isUpdatingInternally = false;
}
// Force update of sum widget with correct sum
node._isUpdatingInternally = true;
updateTotalSumWidget(node);
node._isUpdatingInternally = false;
updateAverageState(node); // NEW: Update average after internal sum update
// If lock is on, verify and correct sum as a final step
if (lockWidget?.value) {
verifyAndCorrectSum(node);
}
return;
}
}
const currentMaxStrengthWidget = findWidgetByName(node, "sliders_max");
const currentMaxDisplayed = getWidgetValue(currentMaxStrengthWidget);
const newCalculatedMax = recalculateCurrentMaxStrength(node);
updateNormalization(node, newCalculatedMax); // Update normalization with potentially new max
// Update master slider display if the calculated max changed
if (Math.abs(newCalculatedMax - currentMaxDisplayed) > 0.001) {
node._isUpdatingInternally = true;
currentMaxStrengthWidget.value = newCalculatedMax;
node._isUpdatingInternally = false;
} else {
// If master didn't change, still ensure other sliders respect the current max
updateAllSliderDisplays(node, currentMaxDisplayed);
}
// Only recalculate deltas when sliders are directly changed
storeDeltaValues(node);
// Force update of sum widget with correct sum regardless of lock state
node._isUpdatingInternally = true;
updateTotalSumWidget(node); // Update sum display
node._isUpdatingInternally = false;
updateAverageState(node); // NEW: Update average after direct slider change (non-locked)
// If lock is on, verify and correct sum as a final step
if (lockWidget?.value) {
verifyAndCorrectSum(node);
}
});
}
}
});
// console.log("Loaded Dynamic Sliders Stack Dynamic Behavior Script V6.0 (Code Cleanup)");
// Redistributes slider change inversely to maintain constant sum
function redistributeSliderChange(node, changedSliderIndex, newValue, oldValue) {
if (!node.reference_sum) return newValue;
// Get necessary widgets
const sliderCountWidget = findWidgetByName(node, "sliders_count");
if (!sliderCountWidget) return newValue;
const count = getWidgetValue(sliderCountWidget);
// Calculate the delta that needs to be distributed
const delta = newValue - oldValue;
// If delta is negligible, no need to redistribute
if (Math.abs(delta) < 0.0001) return newValue;
// Find all other active sliders and collect their values
const otherSliders = [];
for (let i = 1; i <= count; i++) {
if (i === changedSliderIndex) continue;
const sliderWidget = findWidgetByName(node, `(${i})`);
if (sliderWidget) {
otherSliders.push({
index: i,
widget: sliderWidget,
value: getWidgetValue(sliderWidget)
});
}
}
// If no other sliders, we can't redistribute
if (otherSliders.length === 0) return oldValue;
// Calculate the exact change per slider using the formula:
// If slider_x increases by X, other sliders decrease by X/(sliders_count-1)
const changePerSlider = -delta / otherSliders.length;
// First, check if any slider would hit its min/max limit with the proposed change
let limitingSlider = null;
let maxPossibleChangePerSlider = Infinity;
for (const slider of otherSliders) {
const newSliderValue = slider.value + changePerSlider;
// Check against min (0) constraint
if (newSliderValue < 0) {
const possibleChange = -slider.value; // How much this slider can decrease
if (possibleChange > -0.0001) { // If slider is already near 0
limitingSlider = slider;
maxPossibleChangePerSlider = 0;
} else if (Math.abs(possibleChange) < Math.abs(maxPossibleChangePerSlider)) {
limitingSlider = slider;
maxPossibleChangePerSlider = possibleChange;
}
}
// Check against max (2.0) constraint
else if (newSliderValue > SLIDERS_MAX_HARD_CAP) {
const possibleChange = SLIDERS_MAX_HARD_CAP - slider.value; // How much this slider can increase
if (possibleChange < 0.0001) { // If slider is already near max
limitingSlider = slider;
maxPossibleChangePerSlider = 0;
} else if (Math.abs(possibleChange) < Math.abs(maxPossibleChangePerSlider)) {
limitingSlider = slider;
maxPossibleChangePerSlider = possibleChange;
}
}
}
// If we found a limiting slider, constrain the primary slider's movement
let constrainedChangePerSlider = changePerSlider;
let constrainedDelta = delta;
if (limitingSlider && Math.abs(maxPossibleChangePerSlider) < Math.abs(changePerSlider)) {
constrainedChangePerSlider = maxPossibleChangePerSlider;
constrainedDelta = -constrainedChangePerSlider * otherSliders.length;
}
// Calculate the constrained new value for the primary slider
const constrainedNewValue = oldValue + constrainedDelta;
// Now apply the SAME exact change to ALL other sliders
for (const slider of otherSliders) {
const targetValue = slider.value + constrainedChangePerSlider;
const clampedValue = Math.max(0, Math.min(SLIDERS_MAX_HARD_CAP, targetValue));
// Apply the change
node._isUpdatingInternally = true;
slider.widget.value = clampedValue;
node._isUpdatingInternally = false;
}
// Verify and correct to ensure exact reference_sum compliance
verifyAndCorrectSum(node);
// Return the constrained new value for the main slider
return constrainedNewValue;
}
// Adjust sliders to fix tiny precision-based deviations from reference_sum
function verifyAndCorrectSum(node) {
if (!node.reference_sum) return;
const lockWidget = findWidgetByName(node, "lock_sum");
if (!lockWidget?.value) return; // Only verify when lock is on
const actualSum = getCurrentSum(node);
const diff = actualSum - node.reference_sum;
// If the difference is negligible, no need to correct
if (Math.abs(diff) < 0.0001) return;
// Get all active sliders
const sliderCountWidget = findWidgetByName(node, "sliders_count");
if (!sliderCountWidget) return;
const count = getWidgetValue(sliderCountWidget);
const activeSliders = [];
// Collect all slider values
for (let i = 1; i <= count; i++) {
const sliderWidget = findWidgetByName(node, `(${i})`);
if (sliderWidget) {
const value = getWidgetValue(sliderWidget);
activeSliders.push({
index: i,
widget: sliderWidget,
value: value
});
}
}
if (activeSliders.length === 0) return;
// Distribute the correction evenly across all sliders
const correctionPerSlider = -diff / activeSliders.length;
// Apply the correction to all sliders
node._isUpdatingInternally = true;
for (const slider of activeSliders) {
const newValue = slider.value + correctionPerSlider;
const constrainedValue = Math.max(0, Math.min(SLIDERS_MAX_HARD_CAP, newValue));
slider.widget.value = constrainedValue;
}
node._isUpdatingInternally = false;
// Update average state after correction
updateAverageState(node); // NEW: Update average after sum correction
}
// NEW: Adjusts sliders when sliders_max changes and sum is locked
function updateSlidersForMaxChangeLockedSum(node, requestedNewMax) {
if (node._isUpdatingInternally) return;
const sliderCountWidget = findWidgetByName(node, "sliders_count");
if (!sliderCountWidget) return;
const count = getWidgetValue(sliderCountWidget);
if (count <= 1) return; // Need at least 2 sliders to make this adjustment meaningful
const reference_sum = node.reference_sum;
const reference_avg = node.reference_avg;
const epsilon = 0.0001;
// Clamp the requested max value initially based on reference_avg + BUFFER and hard cap
const initiallyClampedMax = Math.max(reference_avg + REFERENCE_AVG_BUFFER, Math.min(requestedNewMax, SLIDERS_MAX_HARD_CAP));
// Collect active sliders, their values, and find the current max value/slider
const sliders = [];
let currentMaxSliderValue = -Infinity;
let maxSliderIndex = -1;
for (let i = 1; i <= count; i++) {
const widget = findWidgetByName(node, `(${i})`);
if (widget) {
const currentValue = getWidgetValue(widget);
sliders.push({ index: i, widget: widget, currentValue: currentValue });
if (currentValue > currentMaxSliderValue) {
currentMaxSliderValue = currentValue;
maxSliderIndex = i;
}
}
}
if (sliders.length <= 1 || maxSliderIndex === -1) return; // Need at least 2 sliders
// Calculate the ideal change for the highest slider based on the clamped request
const idealDeltaH = initiallyClampedMax - currentMaxSliderValue;
// If the ideal change is negligible, do nothing
if (Math.abs(idealDeltaH) < epsilon) {
// Even if no change, ensure the widget reflects the current max (or ref_avg + BUFFER)
const maxStrengthWidget = findWidgetByName(node, "sliders_max");
const valueToSet = Math.max(reference_avg + REFERENCE_AVG_BUFFER, currentMaxSliderValue);
if(maxStrengthWidget && Math.abs(getWidgetValue(maxStrengthWidget) - valueToSet) > epsilon) {
node._isUpdatingInternally = true;
maxStrengthWidget.value = valueToSet;
node._isUpdatingInternally = false;
}
return;
}
node._isUpdatingInternally = true;
// Total change needed for all other sliders to compensate
const idealDeltaO_total = -idealDeltaH;
// Calculate deviations from average for *other* sliders and their sum
let totalDeviationO = 0;
const otherSliders = [];
sliders.forEach(s => {
if (s.index !== maxSliderIndex) {
s.deviation = s.currentValue - reference_avg;
totalDeviationO += s.deviation;
otherSliders.push(s);
}
});
// --- Find the most constraining factor ---
let maxAllowedDeltaH = idealDeltaH; // Start with the ideal change
let limitingFactorFound = false;
if (otherSliders.length > 0) {
// Handle case where all other sliders are at the average (avoid division by zero)
if (Math.abs(totalDeviationO) < epsilon) {
const changePerSlider = idealDeltaO_total / otherSliders.length;
for (const s of otherSliders) {
let deltaI_max;
if (changePerSlider < 0 && s.currentValue + changePerSlider < 0) { // Hitting zero
deltaI_max = -s.currentValue;
} else if (changePerSlider > 0 && s.currentValue + changePerSlider > SLIDERS_MAX_HARD_CAP) { // Hitting max
deltaI_max = SLIDERS_MAX_HARD_CAP - s.currentValue;
} else {
continue; // This slider doesn't limit the change
}
// Calculate the corresponding max deltaH for this slider
const currentDeltaH_max = -deltaI_max * otherSliders.length;
if (Math.abs(currentDeltaH_max) < Math.abs(maxAllowedDeltaH)) {
maxAllowedDeltaH = currentDeltaH_max;
limitingFactorFound = true;
}
}
} else {
// Proportional distribution
for (const s of otherSliders) {
const proportion = s.deviation / totalDeviationO;
const idealDeltaI = idealDeltaO_total * proportion;
let deltaI_max;
if (idealDeltaI < 0 && s.currentValue + idealDeltaI < 0) { // Hitting zero
deltaI_max = -s.currentValue;
} else if (idealDeltaI > 0 && s.currentValue + idealDeltaI > SLIDERS_MAX_HARD_CAP) { // Hitting max
deltaI_max = SLIDERS_MAX_HARD_CAP - s.currentValue;
} else {
continue; // This slider doesn't limit the change
}
// Calculate the corresponding max deltaH for this slider
// deltaI_max = (-deltaH_max) * proportion => deltaH_max = -deltaI_max / proportion
if (Math.abs(proportion) > epsilon) { // Avoid division by zero if proportion is tiny
const currentDeltaH_max = -deltaI_max / proportion;
// We only care if this limit is stricter (smaller magnitude) than the current maxAllowedDeltaH
// And has the same sign as the idealDeltaH
if (Math.sign(currentDeltaH_max) === Math.sign(idealDeltaH) && Math.abs(currentDeltaH_max) < Math.abs(maxAllowedDeltaH)) {
maxAllowedDeltaH = currentDeltaH_max;
limitingFactorFound = true;
}
}
}
}
}
// Use the constrained change
const constrainedDeltaH = limitingFactorFound ? maxAllowedDeltaH : idealDeltaH;
const constrainedDeltaO_total = -constrainedDeltaH;
const finalHighestSliderValue = currentMaxSliderValue + constrainedDeltaH;
// --- Apply the constrained changes ---
let finalValues = {};
finalValues[maxSliderIndex] = finalHighestSliderValue;
// Distribute the constrained change among other sliders
if (otherSliders.length > 0) {
if (Math.abs(totalDeviationO) < epsilon) {
const changePerSlider = constrainedDeltaO_total / otherSliders.length;
otherSliders.forEach(s => {
finalValues[s.index] = s.currentValue + changePerSlider;
});
} else {
otherSliders.forEach(s => {
const proportion = s.deviation / totalDeviationO;
const deltaI = constrainedDeltaO_total * proportion;
finalValues[s.index] = s.currentValue + deltaI;
});
}
}
// Apply changes with clamping (as a safeguard)
sliders.forEach(s => {
const clampedValue = Math.max(0, Math.min(SLIDERS_MAX_HARD_CAP, finalValues[s.index]));
if (Math.abs(s.widget.value - clampedValue) > epsilon) {
s.widget.value = clampedValue;
}
});
// --- Update sliders_max widget directly ---
const maxStrengthWidget = findWidgetByName(node, "sliders_max");
const finalWidgetValue = Math.max(reference_avg + REFERENCE_AVG_BUFFER, Math.min(finalHighestSliderValue, SLIDERS_MAX_HARD_CAP)); // Clamp final value, ensuring min is ref_avg + BUFFER
if (maxStrengthWidget && Math.abs(getWidgetValue(maxStrengthWidget) - finalWidgetValue) > epsilon) {
maxStrengthWidget.value = finalWidgetValue;
}
node._isUpdatingInternally = false;
// Final check to enforce sum constraint strictly
verifyAndCorrectSum(node);
if (node.graph) {
node.graph.setDirtyCanvas(true, true);
}
}