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); } }