import { app } from "../../../scripts/app.js"; import { api } from "../../../scripts/api.js"; let activeNodeId = null; const BASIC_PARAMS = ['brightness', 'contrast', 'hue', 'saturation']; const HIGHLIGHT_PARAMS = [...BASIC_PARAMS, 'sharpen']; const HIDDEN_WIDGETS = ['brightness', 'contrast', 'hue', 'saturation', 'sharpen']; const DEFAULT_SLIDER_VALUES = { brightness: 0, contrast: 0, sharpen: 0, hue: 0, saturation: 0, vibrance: 0, clarity: 0, lut_intensity: 100, input_black: 0, gamma: 1.0, input_white: 255, exposure: 0, offset: 0, shadows_cyan_red: 0, shadows_magenta_green: 0, shadows_yellow_blue: 0, midtones_cyan_red: 0, midtones_magenta_green: 0, midtones_yellow_blue: 0, highlights_cyan_red: 0, highlights_magenta_green: 0, highlights_yellow_blue: 0, bw_red: 40, bw_yellow: 60, bw_green: 40, bw_cyan: 60, bw_blue: 20, bw_magenta: 80, red_red: 100, red_green: 0, red_blue: 0, green_red: 0, green_green: 100, green_blue: 0, blue_red: 0, blue_green: 0, blue_blue: 100, sc_cyan: 0, sc_magenta: 0, sc_yellow: 0, sc_black: 0, bw_enabled: false, }; const DEFAULT_ADJUSTMENTS = { brightness: 0, contrast: 0, sharpen: 0, hue: 0, saturation: 0, vibrance: 0, clarity: 0, lut_path: "", lut_intensity: 100, levels: { input_black: 0, gamma: 1.0, input_white: 255 }, exposure: { exposure: 0, offset: 0 }, color_balance: { shadows_cyan_red: 0, shadows_magenta_green: 0, shadows_yellow_blue: 0, midtones_cyan_red: 0, midtones_magenta_green: 0, midtones_yellow_blue: 0, highlights_cyan_red: 0, highlights_magenta_green: 0, highlights_yellow_blue: 0 }, black_white: { bw_red: 40, bw_yellow: 60, bw_green: 40, bw_cyan: 60, bw_blue: 20, bw_magenta: 80 }, bw_enabled: false, channel_mixer: { red_red: 100, red_green: 0, red_blue: 0, green_red: 0, green_green: 100, green_blue: 0, blue_red: 0, blue_green: 0, blue_blue: 100 }, selective_color: { color_name: "Reds", sc_cyan: 0, sc_magenta: 0, sc_yellow: 0, sc_black: 0 } }; function applyBasicAdjustmentsJS(imageData, brightness, contrast, hue, saturation) { const data = imageData.data; const len = data.length; for (let i = 0; i < len; i += 4) { let r = data[i]; let g = data[i + 1]; let b = data[i + 2]; if (brightness !== 0 || contrast !== 0) { const alpha = 1.0 + (contrast / 100.0); const beta = brightness; r = Math.min(255, Math.max(0, r * alpha + beta)); g = Math.min(255, Math.max(0, g * alpha + beta)); b = Math.min(255, Math.max(0, b * alpha + beta)); } if (hue !== 0 || saturation !== 0) { const rNorm = r / 255.0; const gNorm = g / 255.0; const bNorm = b / 255.0; const max = Math.max(rNorm, gNorm, bNorm); const min = Math.min(rNorm, gNorm, bNorm); const delta = max - min; let h = 0, s = 0, v = max; if (delta !== 0) { s = delta / max; if (max === rNorm) h = ((gNorm - bNorm) / delta) % 6; else if (max === gNorm) h = (bNorm - rNorm) / delta + 2; else h = (rNorm - gNorm) / delta + 4; h *= 60; if (h < 0) h += 360; } h = Math.floor(h); s = Math.floor(s * 255); v = Math.floor(v * 255); h = Math.floor(h / 2); h = (h + hue) % 180; if (h < 0) h += 180; s = Math.min(255, Math.max(0, Math.floor(s * (1.0 + saturation / 100.0)))); h = h * 2; s = s / 255; v = v / 255; const c = v * s; const x = c * (1 - Math.abs((h / 60) % 2 - 1)); const m = v - c; let r2 = 0, g2 = 0, b2 = 0; if (h < 60) { r2 = c; g2 = x; b2 = 0; } else if (h < 120) { r2 = x; g2 = c; b2 = 0; } else if (h < 180) { r2 = 0; g2 = c; b2 = x; } else if (h < 240) { r2 = 0; g2 = x; b2 = c; } else if (h < 300) { r2 = x; g2 = 0; b2 = c; } else { r2 = c; g2 = 0; b2 = x; } r = Math.floor((r2 + m) * 255); g = Math.floor((g2 + m) * 255); b = Math.floor((b2 + m) * 255); } data[i] = Math.min(255, Math.max(0, Math.floor(r))); data[i + 1] = Math.min(255, Math.max(0, Math.floor(g))); data[i + 2] = Math.min(255, Math.max(0, Math.floor(b))); } return imageData; } app.registerExtension({ name: "RaykoStudio.RSImageAdjustments", async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== "RS_ImageAdjustments") return; const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { if (onNodeCreated) onNodeCreated.apply(this, arguments); this.adjustments = JSON.parse(JSON.stringify(DEFAULT_ADJUSTMENTS)); this.backgroundImage = null; this.previewImage = null; this.originalImageData = null; this._tempCanvas = document.createElement('canvas'); this._tempCtx = this._tempCanvas.getContext('2d'); this.isEditing = false; this.isLoading = false; this.advancedMode = false; this.currentSessionTimestamp = null; this.pendingEditorData = null; this.renderTimeout = null; this.currentRenderAbortController = null; this._previewTimestamp = null; this._previewKey = null; this._overlayRenderLoop = null; this._ws = null; this._wsConnected = false; this._wsReconnectTimer = null; this._lastHeavyRenderTime = 0; this._isHeavyRenderPending = false; // BATCH MODE VARIABLES this.isBatchMode = false; this._batchParams = null; this.btnBatchHover = false; this._queueWasRunning = false; this._batchResetTimer = null; console.log(`[RS Adjustments] Node ${this.id} created. Registering status listener.`); this.displayWidth = 420; this.displayHeight = 420; this.canvasPixelSize = 420; this.realBackground = { width: 0, height: 0 }; this.overlayContainer = null; this.overlayCanvas = null; this.overlayCtx = null; this.overlayCanvasWrapper = null; this.sidePanel = null; this.overlayInputs = {}; this.sliderDisplays = {}; this._basicParamLabels = {}; this._allSectionsExpanded = false; this.minWidth = 500; this.minHeight = 450; this.setSize([this.minWidth, this.minHeight]); this.slidersY = 0; this.sliderRects = []; this.resetBtnRects = []; this.sliderHover = [false, false, false, false, false]; this.resetBtnHover = [false, false, false, false, false]; this.sliderDragging = -1; this.btnApplyHover = false; this.btnCancelHover = false; this.btnAdvancedHover = false; this.btnResetAllHover = false; this._syncingWidgets = false; BASIC_PARAMS.forEach(n => { const w = this.widgets?.find(w => w.name === n); if (w) { w.hidden = true; w.computeSize = () => [0, 0]; w.y = 0; const origCallback = w.callback; w.callback = (v) => { if (origCallback) origCallback.apply(w, arguments); if (!this._syncingWidgets) { this.adjustments[n] = v; this.previewImage = null; this.setDirtyCanvas(true); } }; } }); const serverParams = ['sharpen']; serverParams.forEach(n => { const w = this.widgets?.find(w => w.name === n); if (w) { w.hidden = true; w.computeSize = () => [0, 0]; w.y = 0; const origCallback = w.callback; w.callback = (v) => { if (origCallback) origCallback.apply(w, arguments); if (!this._syncingWidgets) { this.adjustments[n] = v; this.setDirtyCanvas(true); this._scheduleHeavyRender(); } }; } }); const advWidget = this.widgets?.find(w => w.name === "advanced_params"); if (advWidget) { advWidget.hidden = true; advWidget.computeSize = () => [0, 0]; advWidget.y = 0; } this._hideNativeWidgets(); this._buildOverlayUI(); this._mouseLeaveHandler = () => { if (this.sliderDragging >= 0) { this.sliderDragging = -1; this.setDirtyCanvas(true); } }; const originalOnMouseLeave = this.onMouseLeave; this.onMouseLeave = function() { if (originalOnMouseLeave) originalOnMouseLeave.apply(this, arguments); this._mouseLeaveHandler(); }; api.addEventListener("rs-adjustments-start", (event) => { if (event.detail.id != this.id) return; this.pendingEditorData = event.detail; this._openDeferredEditor(); }); api.addEventListener("interrupted", () => { this._cleanup(); }); // STATUS LISTENER FOR BATCH RESET (moved inside onNodeCreated for correct 'this') api.addEventListener("status", (event) => { const remaining = event.detail?.exec_info?.queue_remaining; console.log(`[RS Adjustments] Status event received. queue_remaining=${remaining}, this.id=${this.id}, isBatchMode=${this.isBatchMode}`); if (remaining > 0) { this._queueWasRunning = true; if (this._batchResetTimer) { clearTimeout(this._batchResetTimer); this._batchResetTimer = null; } } else if (remaining === 0 && this._queueWasRunning) { if (!this._batchResetTimer) { console.log(`[RS Adjustments] Node ${this.id}: Queue finished. Starting batch reset timer (3s)...`); this._batchResetTimer = setTimeout(() => { console.log(`[RS Adjustments] Node ${this.id}: Timer fired! Resetting batch. isBatchMode was: ${this.isBatchMode}`); this.isBatchMode = false; this._batchParams = null; this.setDirtyCanvas(true); this._batchResetTimer = null; this._queueWasRunning = false; console.log(`[RS Adjustments] Node ${this.id}: Batch reset complete. isBatchMode is now: ${this.isBatchMode}`); }, 500); } } }); }; nodeType.prototype._hideNativeWidgets = function() { if (!this.widgets) return; HIDDEN_WIDGETS.forEach(param => { const widget = this.widgets.find(w => w.name === param); if (widget) { widget.hidden = true; widget.computeSize = () => [0, 0]; widget.y = 0; } }); this.setSize([this.minWidth, this.minHeight]); this.setDirtyCanvas(true); }; nodeType.prototype._syncWidgetsFromAdjustments = function() { if (!this.widgets) return; this._syncingWidgets = true; try { HIDDEN_WIDGETS.forEach(param => { const widget = this.widgets.find(w => w.name === param); if (widget && widget.value !== this.adjustments[param]) { widget.value = this.adjustments[param]; } }); } finally { this._syncingWidgets = false; } this.setDirtyCanvas(true); }; nodeType.prototype._syncOverlayUI = function() { if (!this.advancedMode) return; BASIC_PARAMS.forEach(key => { if (this.overlayInputs[key]) { this.overlayInputs[key].value = this.adjustments[key]; } if (this.sliderDisplays[key]) { this.sliderDisplays[key].textContent = String(this.adjustments[key]); } }); if (this.overlayInputs.sharpen) { this.overlayInputs.sharpen.value = this.adjustments.sharpen || 0; } if (this.sliderDisplays.sharpen) { this.sliderDisplays.sharpen.textContent = String(this.adjustments.sharpen || 0); } }; nodeType.prototype._isParamModified = function(key, parentObj = this.adjustments) { const def = DEFAULT_SLIDER_VALUES[key]; if (def === undefined) return false; const val = parentObj[key]; if (val === undefined) return false; if (typeof def === 'number') { return Math.abs(val - def) > 0.001; } return val !== def; }; nodeType.prototype._isSectionModified = function(sectionName) { const checks = { lut: () => (this.adjustments.lut_path && this.adjustments.lut_path !== "") || this._isParamModified('lut_intensity'), advanced_tools: () => this._isParamModified('vibrance') || this._isParamModified('clarity'), levels: () => this._isParamModified('input_black', this.adjustments.levels) || this._isParamModified('gamma', this.adjustments.levels) || this._isParamModified('input_white', this.adjustments.levels), exposure: () => this._isParamModified('exposure', this.adjustments.exposure) || this._isParamModified('offset', this.adjustments.exposure), color_balance: () => [ 'shadows_cyan_red', 'shadows_magenta_green', 'shadows_yellow_blue', 'midtones_cyan_red', 'midtones_magenta_green', 'midtones_yellow_blue', 'highlights_cyan_red', 'highlights_magenta_green', 'highlights_yellow_blue' ].some(k => this._isParamModified(k, this.adjustments.color_balance)), black_white: () => ['bw_red', 'bw_yellow', 'bw_green', 'bw_cyan', 'bw_blue', 'bw_magenta'].some(k => this._isParamModified(k, this.adjustments.black_white)), channel_mixer: () => [ 'red_red', 'red_green', 'red_blue', 'green_red', 'green_green', 'green_blue', 'blue_red', 'blue_green', 'blue_blue' ].some(k => this._isParamModified(k, this.adjustments.channel_mixer)), selective_color: () => ['sc_cyan', 'sc_magenta', 'sc_yellow', 'sc_black'].some(k => this._isParamModified(k, this.adjustments.selective_color)) }; return checks[sectionName] ? checks[sectionName]() : false; }; nodeType.prototype._updateSectionHeaders = function() { if (!this._sectionRegistry) return; this._sectionRegistry.forEach(entry => { const modified = this._isSectionModified(entry.name); if (entry.label) { entry.label.style.color = modified ? '#00FF00' : '#666'; } }); }; nodeType.prototype._updateBasicParamLabels = function() { HIGHLIGHT_PARAMS.forEach(key => { const lbl = this._basicParamLabels[key]; if (lbl) { const isModified = this._isParamModified(key); lbl.style.color = isModified ? '#00FF00' : '#aaa'; } }); }; nodeType.prototype._getDefaultValueForKey = function(key) { if (DEFAULT_SLIDER_VALUES.hasOwnProperty(key)) { return DEFAULT_SLIDER_VALUES[key]; } const nestedKeys = [ 'levels', 'exposure', 'color_balance', 'black_white', 'channel_mixer', 'selective_color' ]; for (let section of nestedKeys) { if (DEFAULT_ADJUSTMENTS[section] && DEFAULT_ADJUSTMENTS[section].hasOwnProperty(key)) { return DEFAULT_ADJUSTMENTS[section][key]; } } return 0; }; nodeType.prototype._resetAllParameters = function() { const resetRecursive = (target, source) => { for (let key in source) { if (source.hasOwnProperty(key)) { if (source[key] !== null && typeof source[key] === 'object' && !Array.isArray(source[key])) { if (!target[key] || typeof target[key] !== 'object') target[key] = {}; resetRecursive(target[key], source[key]); } else { target[key] = source[key]; } } } }; resetRecursive(this.adjustments, DEFAULT_ADJUSTMENTS); Object.keys(this.overlayInputs).forEach(key => { const slider = this.overlayInputs[key]; if (!slider) return; const defaultVal = this._getDefaultValueForKey(key); slider.value = defaultVal; if (this.sliderDisplays[key]) { const isFloat = Number.isFinite(defaultVal) && !Number.isInteger(defaultVal); this.sliderDisplays[key].textContent = isFloat ? defaultVal.toFixed(2) : String(Math.round(defaultVal)); } }); if (this._bwToggle) { this._bwToggle.checked = this.adjustments.bw_enabled || false; } const colorSelect = this.overlayContainer?.querySelector('#selective-color-select'); if (colorSelect) { colorSelect.value = this.adjustments.selective_color.color_name || 'Reds'; } const lutFileInput = this.overlayContainer?.querySelector('input[type="file"][accept=".cube"]'); const lutFilenameDisplay = this.overlayContainer?.querySelector('.lut-filename-display'); if (lutFileInput) lutFileInput.value = ''; if (lutFilenameDisplay) { lutFilenameDisplay.textContent = 'File not selected'; lutFilenameDisplay.style.color = '#888'; } const lutIntensitySlider = this.overlayInputs['lut_intensity']; if (lutIntensitySlider) { lutIntensitySlider.value = this.adjustments.lut_intensity; if (this.sliderDisplays['lut_intensity']) { this.sliderDisplays['lut_intensity'].textContent = String(this.adjustments.lut_intensity); } } this._updateSectionHeaders(); this._updateBasicParamLabels(); this.previewImage = null; this.setDirtyCanvas(true); this._scheduleHeavyRender(); this._syncWidgetsFromAdjustments(); }; nodeType.prototype._buildOverlayUI = function() { this.overlayContainer = document.createElement('div'); this.overlayContainer.style.cssText = 'position:fixed;top:60px;left:0;right:0;bottom:0;background:rgba(10,10,10,0.96);z-index:999;display:none;flex-direction:row;align-items:stretch;font-family:system-ui,-apple-system,sans-serif;'; this.overlayCanvasWrapper = document.createElement('div'); this.overlayCanvasWrapper.style.cssText = 'flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative;'; this.overlayCanvas = document.createElement('canvas'); this.overlayCanvas.style.cssText = 'box-shadow:0 8px 32px rgba(0,0,0,0.7);max-width:98%;max-height:98%;border-radius:8px;'; this.overlayCanvasWrapper.appendChild(this.overlayCanvas); this.overlayCtx = this.overlayCanvas.getContext('2d'); this.sidePanel = document.createElement('div'); this.sidePanel.style.cssText = 'width:320px;background:#151515;border-left:1px solid #333;padding:12px;display:flex;flex-direction:column;gap:4px;box-sizing:border-box;overflow-y:auto;'; const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = 'position:sticky;top:0;background:#151515;z-index:10;padding:0 0 8px 0;border-bottom:1px solid #333;margin-bottom:8px;'; this.sidePanel.appendChild(buttonContainer); const makeBtn = (txt, col, onClick) => { const b = document.createElement('button'); b.textContent = txt; b.style.cssText = `width:100%;padding:10px;background:#222;color:${col};border:1px solid ${col};border-radius:6px;cursor:pointer;font-weight:600;font-size:12px;margin-top:4px;transition:0.15s;`; b.onmouseenter = () => { b.style.background = '#2a2a2a'; b.style.transform = 'translateY(-1px)'; }; b.onmouseleave = () => { b.style.background = '#222'; b.style.transform = 'none'; }; b.onclick = (e) => { e.stopPropagation(); onClick(); }; return b; }; const btnNormalMode = makeBtn("🔵 NORMAL MODE", "#2196F3", () => { this._toggleAdvancedMode(); }); buttonContainer.appendChild(btnNormalMode); const btnApply = makeBtn("✔️ APPLY", "#4CAF50", () => { this._sendAdjustments(); this._toggleAdvancedMode(); }); buttonContainer.appendChild(btnApply); const btnCancel = makeBtn("❌ CANCEL", "#dc3545", () => { this._cancelEditing(); this._toggleAdvancedMode(); }); buttonContainer.appendChild(btnCancel); const bottomRow = document.createElement('div'); bottomRow.style.cssText = 'display:flex;gap:8px;width:100%;'; const btnResetAll = makeBtn("🔄 RESET ALL", "#FF9800", () => { this._resetAllParameters(); }); btnResetAll.style.flex = '1'; bottomRow.appendChild(btnResetAll); this._expandCollapseBtn = makeBtn("▶ EXPAND ALL", "#888", () => { this._allSectionsExpanded = !this._allSectionsExpanded; const display = this._allSectionsExpanded ? 'block' : 'none'; const icon = this._allSectionsExpanded ? '▼' : '▶'; const text = this._allSectionsExpanded ? '▼ COLLAPSE ALL' : '▶ EXPAND ALL'; this._sectionRegistry.forEach(entry => { if (entry.content) entry.content.style.display = display; if (entry.chevron) entry.chevron.textContent = icon; }); this._expandCollapseBtn.textContent = text; }); this._expandCollapseBtn.style.flex = '1'; bottomRow.appendChild(this._expandCollapseBtn); buttonContainer.appendChild(bottomRow); const makeSlider = (label, key, min, max, step, isFloat = false, parentObj = this.adjustments) => { const container = document.createElement('div'); container.style.cssText = 'display:flex;flex-direction:column;gap:4px;'; const lbl = document.createElement('label'); lbl.textContent = label; lbl.style.cssText = 'color:#aaa;font-size:11px;font-weight:600;'; container.appendChild(lbl); if (HIGHLIGHT_PARAMS.includes(key)) { this._basicParamLabels[key] = lbl; } const row = document.createElement('div'); row.style.cssText = 'display:flex;align-items:center;gap:8px;'; const slider = document.createElement('input'); slider.type = 'range'; slider.min = min; slider.max = max; slider.step = step; slider.value = parentObj[key]; slider.style.cssText = 'flex:1;height:4px;background:#252525;border-radius:2px;outline:none;cursor:pointer;-webkit-appearance:none;'; const valueDisplay = document.createElement('div'); valueDisplay.textContent = isFloat ? parseFloat(parentObj[key]).toFixed(2) : String(parseInt(parentObj[key])); valueDisplay.style.cssText = 'min-width:50px;text-align:center;background:#252525;color:#4CAF50;border:1px solid #444;border-radius:4px;padding:4px 8px;font-size:12px;cursor:pointer;font-weight:600;transition:0.15s;'; valueDisplay.onclick = (e) => { e.stopPropagation(); const popup = document.createElement('div'); popup.style.cssText = 'position:fixed;z-index:10004;background:#1a1a1a;border:1px solid #444;border-radius:6px;padding:8px 12px;box-shadow:0 4px 20px rgba(0,0,0,0.5);display:flex;align-items:center;gap:8px;'; const input = document.createElement('input'); input.type = 'number'; input.value = parentObj[key]; input.min = min; input.max = max; input.step = step; input.style.cssText = 'width:100px;background:#222;color:#fff;border:1px solid #444;border-radius:4px;padding:6px 10px;font-size:12px;outline:none;'; const saveBtn = document.createElement('button'); saveBtn.textContent = 'OK'; saveBtn.style.cssText = 'background:#4CAF50;color:#fff;border:none;border-radius:4px;padding:6px 12px;font-size:12px;cursor:pointer;'; const doSave = () => { let num = isFloat ? parseFloat(input.value) : parseInt(input.value); if (isNaN(num)) num = parentObj[key]; num = Math.max(min, Math.min(max, num)); parentObj[key] = num; slider.value = num; valueDisplay.textContent = isFloat ? num.toFixed(2) : String(num); this._updateSectionHeaders(); this._updateBasicParamLabels(); const widget = this.widgets?.find(w => w.name === key); if (widget) widget.value = num; this.setDirtyCanvas(true); if (BASIC_PARAMS.includes(key)) { this.previewImage = null; } else { this._scheduleHeavyRender(); } popup.remove(); }; saveBtn.onclick = (ev) => { ev.stopPropagation(); ev.preventDefault(); doSave(); }; input.onkeydown = (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); doSave(); } }; popup.appendChild(input); popup.appendChild(saveBtn); document.body.appendChild(popup); const popupWidth = popup.offsetWidth || 180; const popupHeight = popup.offsetHeight || 40; const rect = valueDisplay.getBoundingClientRect(); let leftPos = rect.right + 8; let topPos = rect.top + (rect.height - popupHeight) / 2; if (leftPos + popupWidth > window.innerWidth - 10) { leftPos = rect.left - popupWidth - 8; } if (topPos < 10) topPos = 10; if (topPos + popupHeight > window.innerHeight - 10) { topPos = window.innerHeight - popupHeight - 10; } popup.style.left = leftPos + 'px'; popup.style.top = topPos + 'px'; setTimeout(() => { input.focus(); input.select(); }, 50); setTimeout(() => { const closeHandler = (ev) => { if (!popup.contains(ev.target)) { popup.remove(); document.removeEventListener('mousedown', closeHandler); } }; document.addEventListener('mousedown', closeHandler); }, 100); }; const resetBtn = document.createElement('button'); resetBtn.textContent = '🔄'; resetBtn.style.cssText = 'width:28px;height:28px;background:#252525;color:#888;border:1px solid #444;border-radius:4px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:0.15s;flex-shrink:0;'; resetBtn.onmouseenter = () => { resetBtn.style.background = '#2a2a2a'; resetBtn.style.color = '#fff'; resetBtn.style.borderColor = '#4CAF50'; }; resetBtn.onmouseleave = () => { resetBtn.style.background = '#252525'; resetBtn.style.color = '#888'; resetBtn.style.borderColor = '#444'; }; resetBtn.onclick = (e) => { e.stopPropagation(); const defaultVal = DEFAULT_SLIDER_VALUES[key] !== undefined ? DEFAULT_SLIDER_VALUES[key] : (isFloat ? 0.0 : 0); parentObj[key] = defaultVal; slider.value = defaultVal; valueDisplay.textContent = isFloat ? parseFloat(defaultVal).toFixed(2) : String(parseInt(defaultVal)); this._updateSectionHeaders(); this._updateBasicParamLabels(); this.setDirtyCanvas(true); if (BASIC_PARAMS.includes(key)) { this.previewImage = null; } this._scheduleHeavyRender(); }; slider.oninput = () => { const val = isFloat ? parseFloat(slider.value) : parseInt(slider.value); parentObj[key] = val; valueDisplay.textContent = isFloat ? val.toFixed(2) : String(val); this._updateSectionHeaders(); this._updateBasicParamLabels(); this.setDirtyCanvas(true); if (BASIC_PARAMS.includes(key)) { this.previewImage = null; const widget = this.widgets?.find(w => w.name === key); if (widget) widget.value = val; } else { this._scheduleHeavyRender(); } }; this.overlayInputs[key] = slider; this.sliderDisplays[key] = valueDisplay; row.appendChild(slider); row.appendChild(valueDisplay); row.appendChild(resetBtn); container.appendChild(row); return container; }; const makeSection = (title, sectionName, contentBuilder) => { const section = document.createElement('div'); section.style.cssText = 'margin-top:8px;'; const header = document.createElement('div'); header.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px 0;cursor:pointer;user-select:none;'; const chevron = document.createElement('span'); chevron.textContent = '▶'; chevron.style.cssText = 'color:#666;font-size:12px;transition:transform 200ms;flex-shrink:0;'; const lbl = document.createElement('label'); lbl.textContent = title; lbl.style.cssText = `flex:1;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;transition:color 200ms;color:#666;`; header.appendChild(chevron); header.appendChild(lbl); const content = document.createElement('div'); content.style.cssText = 'display:none;padding-left:8px;'; header.addEventListener('click', (e) => { const isOpen = content.style.display !== 'none'; content.style.display = isOpen ? 'none' : 'block'; chevron.textContent = isOpen ? '▶' : '▼'; }); section.appendChild(header); section.appendChild(content); contentBuilder(content); if (!this._sectionRegistry) this._sectionRegistry = []; this._sectionRegistry.push({ name: sectionName, label: lbl, content: content, chevron: chevron }); return section; }; const div = () => { const d = document.createElement('div'); d.style.cssText = 'height:1px;background:#333;margin:4px 0;'; return d; }; const truncateFilename = (filename) => { if (!filename) return 'File not selected'; if (filename.length <= 30) return filename; return filename.substring(0, 27) + '...'; }; this.sidePanel.appendChild(div()); this.sidePanel.appendChild(makeSlider("BRIGHTNESS", "brightness", -100, 100, 1)); this.sidePanel.appendChild(makeSlider("CONTRAST", "contrast", -100, 100, 1)); this.sidePanel.appendChild(makeSlider("SHARPEN", "sharpen", 0, 200, 1)); this.sidePanel.appendChild(makeSlider("HUE", "hue", -180, 180, 1)); this.sidePanel.appendChild(makeSlider("SATURATION", "saturation", -100, 100, 1)); this.sidePanel.appendChild(div()); const lutSection = makeSection("COLOR LOOKUP (LUT)", "lut", (content) => { const fileRow = document.createElement('div'); fileRow.style.cssText = 'display:flex;align-items:center;gap:5px;'; const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.cube'; fileInput.style.display = 'none'; const filenameDisplay = document.createElement('div'); filenameDisplay.className = 'lut-filename-display'; filenameDisplay.textContent = 'File not selected'; filenameDisplay.style.cssText = 'flex:1;padding:8px;background:#252525;color:#888;border:1px solid #444;border-radius:4px;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-height:36px;display:flex;align-items:center;cursor:pointer;'; const browseBtn = document.createElement('button'); browseBtn.textContent = 'Browse'; browseBtn.style.cssText = 'padding:8px 12px;background:#252525;color:#aaa;border:1px solid #444;border-radius:4px;cursor:pointer;font-size:11px;font-weight:600;transition:0.15s;flex-shrink:0;'; browseBtn.onmouseenter = () => { browseBtn.style.background = '#2a2a2a'; browseBtn.style.color = '#fff'; browseBtn.style.borderColor = '#4CAF50'; }; browseBtn.onmouseleave = () => { browseBtn.style.background = '#252525'; browseBtn.style.color = '#aaa'; browseBtn.style.borderColor = '#444'; }; browseBtn.onclick = (e) => { e.stopPropagation(); fileInput.click(); }; const resetBtn = document.createElement('button'); resetBtn.textContent = ''; resetBtn.style.cssText = 'width:28px;height:28px;background:#252525;color:#888;border:1px solid #444;border-radius:4px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:0.15s;flex-shrink:0;'; resetBtn.onmouseenter = () => { resetBtn.style.background = '#2a2a2a'; resetBtn.style.color = '#fff'; resetBtn.style.borderColor = '#4CAF50'; }; resetBtn.onmouseleave = () => { resetBtn.style.background = '#252525'; resetBtn.style.color = '#888'; resetBtn.style.borderColor = '#444'; }; resetBtn.onclick = (e) => { e.stopPropagation(); fileInput.value = ''; filenameDisplay.textContent = 'File not selected'; filenameDisplay.style.color = '#888'; this.adjustments.lut_path = ''; this.adjustments.lut_intensity = 100; this.previewImage = null; this.setDirtyCanvas(true); this._updateSectionHeaders(); this._scheduleHeavyRender(); }; fileInput.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; filenameDisplay.textContent = truncateFilename(file.name); filenameDisplay.style.color = '#eee'; const reader = new FileReader(); reader.onload = async (ev) => { const blob = new Blob([ev.target.result], { type: 'text/plain' }); const formData = new FormData(); formData.append('image', blob, file.name); formData.append('subfolder', 'rs_adjustments'); formData.append('type', 'temp'); try { const resp = await api.fetchApi('/upload/image', { method: 'POST', body: formData }); const data = await resp.json(); this.adjustments.lut_path = data.name; this._updateSectionHeaders(); this.previewImage = null; this.setDirtyCanvas(true); this._scheduleHeavyRender(); } catch (err) { filenameDisplay.textContent = 'File not selected'; filenameDisplay.style.color = '#888'; } }; reader.readAsText(file); }; filenameDisplay.onclick = () => fileInput.click(); fileRow.appendChild(filenameDisplay); fileRow.appendChild(browseBtn); fileRow.appendChild(resetBtn); fileRow.appendChild(fileInput); content.appendChild(fileRow); content.appendChild(div()); content.appendChild(makeSlider("INTENSITY", "lut_intensity", 0, 100, 1)); }); this.sidePanel.appendChild(lutSection); const advancedToolsSection = makeSection("ADVANCED TOOLS", "advanced_tools", (content) => { content.appendChild(makeSlider("VIBRANCE", "vibrance", -100, 100, 1)); content.appendChild(makeSlider("CLARITY", "clarity", -100, 100, 1)); }); this.sidePanel.appendChild(advancedToolsSection); const levelsSection = makeSection("LEVELS", "levels", (content) => { content.appendChild(makeSlider("INPUT BLACK", "input_black", 0, 255, 1, false, this.adjustments.levels)); content.appendChild(makeSlider("GAMMA", "gamma", 0.1, 3.0, 0.01, true, this.adjustments.levels)); content.appendChild(makeSlider("INPUT WHITE", "input_white", 0, 255, 1, false, this.adjustments.levels)); }); this.sidePanel.appendChild(levelsSection); const exposureSection = makeSection("EXPOSURE", "exposure", (content) => { content.appendChild(makeSlider("EXPOSURE", "exposure", -100, 100, 1, false, this.adjustments.exposure)); content.appendChild(makeSlider("OFFSET", "offset", -100, 100, 1, false, this.adjustments.exposure)); }); this.sidePanel.appendChild(exposureSection); const colorBalanceSection = makeSection("COLOR BALANCE", "color_balance", (content) => { const shadowsLabel = document.createElement('div'); shadowsLabel.textContent = 'SHADOWS'; shadowsLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;'; content.appendChild(shadowsLabel); content.appendChild(makeSlider("CYAN / RED", "shadows_cyan_red", -100, 100, 1, false, this.adjustments.color_balance)); content.appendChild(makeSlider("MAGENTA / GREEN", "shadows_magenta_green", -100, 100, 1, false, this.adjustments.color_balance)); content.appendChild(makeSlider("YELLOW / BLUE", "shadows_yellow_blue", -100, 100, 1, false, this.adjustments.color_balance)); const midtonesLabel = document.createElement('div'); midtonesLabel.textContent = 'MIDTONES'; midtonesLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;'; content.appendChild(midtonesLabel); content.appendChild(makeSlider("CYAN / RED", "midtones_cyan_red", -100, 100, 1, false, this.adjustments.color_balance)); content.appendChild(makeSlider("MAGENTA / GREEN", "midtones_magenta_green", -100, 100, 1, false, this.adjustments.color_balance)); content.appendChild(makeSlider("YELLOW / BLUE", "midtones_yellow_blue", -100, 100, 1, false, this.adjustments.color_balance)); const highlightsLabel = document.createElement('div'); highlightsLabel.textContent = 'HIGHLIGHTS'; highlightsLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;'; content.appendChild(highlightsLabel); content.appendChild(makeSlider("CYAN / RED", "highlights_cyan_red", -100, 100, 1, false, this.adjustments.color_balance)); content.appendChild(makeSlider("MAGENTA / GREEN", "highlights_magenta_green", -100, 100, 1, false, this.adjustments.color_balance)); content.appendChild(makeSlider("YELLOW / BLUE", "highlights_yellow_blue", -100, 100, 1, false, this.adjustments.color_balance)); }); this.sidePanel.appendChild(colorBalanceSection); const bwSection = makeSection("BLACK & WHITE", "black_white", (content) => { const toggleRow = document.createElement('div'); toggleRow.style.cssText = 'display:flex;align-items:center;gap:10px;margin:4px 0 8px 0;'; const toggleLabel = document.createElement('label'); toggleLabel.textContent = 'Enable B&W'; toggleLabel.style.cssText = 'color:#aaa;font-size:11px;font-weight:600;'; const toggleInput = document.createElement('input'); toggleInput.type = 'checkbox'; toggleInput.checked = this.adjustments.bw_enabled || false; toggleInput.style.cssText = 'width:18px;height:18px;accent-color:#4CAF50;cursor:pointer;'; this._bwToggle = toggleInput; toggleInput.onchange = () => { this.adjustments.bw_enabled = toggleInput.checked; this.previewImage = null; this.setDirtyCanvas(true); this._scheduleHeavyRender(); }; toggleRow.appendChild(toggleLabel); toggleRow.appendChild(toggleInput); content.appendChild(toggleRow); content.appendChild(makeSlider("RED", "bw_red", 0, 200, 1, false, this.adjustments.black_white)); content.appendChild(makeSlider("YELLOW", "bw_yellow", 0, 200, 1, false, this.adjustments.black_white)); content.appendChild(makeSlider("GREEN", "bw_green", 0, 200, 1, false, this.adjustments.black_white)); content.appendChild(makeSlider("CYAN", "bw_cyan", 0, 200, 1, false, this.adjustments.black_white)); content.appendChild(makeSlider("BLUE", "bw_blue", 0, 200, 1, false, this.adjustments.black_white)); content.appendChild(makeSlider("MAGENTA", "bw_magenta", 0, 200, 1, false, this.adjustments.black_white)); }); this.sidePanel.appendChild(bwSection); const channelMixerSection = makeSection("CHANNEL MIXER", "channel_mixer", (content) => { const redLabel = document.createElement('div'); redLabel.textContent = 'OUTPUT RED'; redLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;'; content.appendChild(redLabel); content.appendChild(makeSlider("RED", "red_red", -200, 200, 1, false, this.adjustments.channel_mixer)); content.appendChild(makeSlider("GREEN", "red_green", -200, 200, 1, false, this.adjustments.channel_mixer)); content.appendChild(makeSlider("BLUE", "red_blue", -200, 200, 1, false, this.adjustments.channel_mixer)); const greenLabel = document.createElement('div'); greenLabel.textContent = 'OUTPUT GREEN'; greenLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;'; content.appendChild(greenLabel); content.appendChild(makeSlider("RED", "green_red", -200, 200, 1, false, this.adjustments.channel_mixer)); content.appendChild(makeSlider("GREEN", "green_green", -200, 200, 1, false, this.adjustments.channel_mixer)); content.appendChild(makeSlider("BLUE", "green_blue", -200, 200, 1, false, this.adjustments.channel_mixer)); const blueLabel = document.createElement('div'); blueLabel.textContent = 'OUTPUT BLUE'; blueLabel.style.cssText = 'color:#888;font-size:10px;font-weight:600;margin:8px 0 4px 0;'; content.appendChild(blueLabel); content.appendChild(makeSlider("RED", "blue_red", -200, 200, 1, false, this.adjustments.channel_mixer)); content.appendChild(makeSlider("GREEN", "blue_green", -200, 200, 1, false, this.adjustments.channel_mixer)); content.appendChild(makeSlider("BLUE", "blue_blue", -200, 200, 1, false, this.adjustments.channel_mixer)); }); this.sidePanel.appendChild(channelMixerSection); const selectiveColorSection = makeSection("SELECTIVE COLOR", "selective_color", (content) => { const colorSelect = document.createElement('select'); colorSelect.id = 'selective-color-select'; colorSelect.style.cssText = 'width:100%;background:#252525;color:#eee;border:1px solid #444;border-radius:4px;padding:6px;font-size:12px;outline:none;margin-bottom:8px;'; ['Reds', 'Yellows', 'Greens', 'Cyans', 'Blues', 'Magentas', 'Whites', 'Neutrals', 'Blacks'].forEach(c => { const opt = document.createElement('option'); opt.value = c; opt.textContent = c; if (c === this.adjustments.selective_color.color_name) opt.selected = true; colorSelect.appendChild(opt); }); colorSelect.onchange = () => { this.adjustments.selective_color.color_name = colorSelect.value; this.previewImage = null; this.setDirtyCanvas(true); this._scheduleHeavyRender(); }; content.appendChild(colorSelect); content.appendChild(makeSlider("CYAN", "sc_cyan", -100, 100, 1, false, this.adjustments.selective_color)); content.appendChild(makeSlider("MAGENTA", "sc_magenta", -100, 100, 1, false, this.adjustments.selective_color)); content.appendChild(makeSlider("YELLOW", "sc_yellow", -100, 100, 1, false, this.adjustments.selective_color)); content.appendChild(makeSlider("BLACK", "sc_black", -100, 100, 1, false, this.adjustments.selective_color)); }); this.sidePanel.appendChild(selectiveColorSection); this.overlayContainer.appendChild(this.overlayCanvasWrapper); this.overlayContainer.appendChild(this.sidePanel); document.body.appendChild(this.overlayContainer); }; nodeType.prototype._scheduleBasicRender = function() { this._syncWidgetsFromAdjustments(); this.setDirtyCanvas(true); }; nodeType.prototype._scheduleHeavyRender = function() { if (this._isHeavyRenderPending) { return; } const now = Date.now(); if (now - this._lastHeavyRenderTime < 50) { return; } this._lastHeavyRenderTime = now; this._syncWidgetsFromAdjustments(); if (this._wsConnected) { this._sendPreviewViaWebSocket(); } else { this._fetchPreviewFromServer(); } }; nodeType.prototype._fetchPreviewFromServer = async function() { if (!this.isEditing || !this.backgroundImage) return; if (this.currentRenderAbortController) { this.currentRenderAbortController.abort(); } this.currentRenderAbortController = new AbortController(); const signal = this.currentRenderAbortController.signal; const payload = { node_id: String(this.id), image_file: this.pendingEditorData.bg_file, adjustments: this.adjustments }; try { const response = await api.fetchApi('/rayko/rs_adjustments/preview', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: signal }); const data = await response.json(); if (data.preview_file) { const img = new Image(); img.crossOrigin = "Anonymous"; const ts = data.timestamp; this._previewTimestamp = ts; img.onload = () => { if (this._previewTimestamp === ts) { this.previewImage = img; this.setDirtyCanvas(true); } }; img.onerror = () => {}; img.src = `/view?filename=${data.preview_file}&type=temp&t=${ts}`; } } catch (e) { if (e.name === 'AbortError') return; } }; nodeType.prototype._initWebSocket = function() { if (this._ws && this._ws.readyState === WebSocket.OPEN) { return; } const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${location.host}/rayko/rs_adjustments/ws`; this._ws = new WebSocket(wsUrl); this._ws.binaryType = 'blob'; this._ws.onopen = () => { console.log('[RS Adjustments] WebSocket connected'); this._wsConnected = true; if (this._wsReconnectTimer) { clearTimeout(this._wsReconnectTimer); this._wsReconnectTimer = null; } }; this._ws.onmessage = (event) => { if (event.data instanceof Blob) { const url = URL.createObjectURL(event.data); const img = new Image(); img.crossOrigin = "Anonymous"; img.onload = () => { this.previewImage = img; this.setDirtyCanvas(true); URL.revokeObjectURL(url); }; img.onerror = () => { URL.revokeObjectURL(url); }; img.src = url; } else if (event.data instanceof ArrayBuffer) { const blob = new Blob([event.data], { type: 'image/jpeg' }); const url = URL.createObjectURL(blob); const img = new Image(); img.onload = () => { this.previewImage = img; this.setDirtyCanvas(true); URL.revokeObjectURL(url); }; img.src = url; } }; this._ws.onclose = () => { console.log('[RS Adjustments] WebSocket disconnected'); this._wsConnected = false; if (!this._wsReconnectTimer) { this._wsReconnectTimer = setTimeout(() => { this._initWebSocket(); }, 2000); } }; this._ws.onerror = (error) => { console.error('[RS Adjustments] WebSocket error:', error); this._ws.close(); }; }; nodeType.prototype._sendPreviewViaWebSocket = function() { if (!this._ws || this._ws.readyState !== WebSocket.OPEN) { this._fetchPreviewFromServer(); return; } const payload = { node_id: String(this.id), image_file: this.pendingEditorData.bg_file, adjustments: this.adjustments }; this._ws.send(JSON.stringify(payload)); }; nodeType.prototype._openDeferredEditor = function() { // BATCH MODE: SKIP EDITOR AND AUTO-APPLY SAVED PARAMS if (this.isBatchMode && this._batchParams) { this.adjustments = JSON.parse(JSON.stringify(this._batchParams)); this._syncWidgetsFromAdjustments(); this.previewImage = null; this.setDirtyCanvas(true); this._scheduleHeavyRender(); setTimeout(() => this._sendAdjustments(), 100); return; } if (!this.pendingEditorData) return; this.backgroundImage = null; this.previewImage = null; this.originalImageData = null; this.isLoading = true; this.isEditing = false; this._hideNativeWidgets(); const data = this.pendingEditorData; this.adjustments.brightness = data.brightness || 0; this.adjustments.contrast = data.contrast || 0; this.adjustments.sharpen = data.sharpen || 0; this.adjustments.hue = data.hue || 0; this.adjustments.saturation = data.saturation || 0; try { const adv = JSON.parse(data.advanced_params || "{}"); const mergeRecursive = (target, source) => { for (let key in source) { if (source.hasOwnProperty(key)) { if (source[key] !== null && typeof source[key] === 'object' && !Array.isArray(source[key])) { if (!target[key] || typeof target[key] !== 'object') target[key] = {}; mergeRecursive(target[key], source[key]); } else { target[key] = source[key]; } } } }; mergeRecursive(this.adjustments, adv); } catch (e) {} this.realBackground = { width: data.bg_width, height: data.bg_height }; this._updateDisplaySize(this.canvasPixelSize); const bgFile = data.bg_file, ts = data.timestamp; const img = new Image(); img.crossOrigin = "Anonymous"; const imageUrl = `/view?filename=${bgFile}&type=temp&t=${ts}_${Date.now()}`; img.onload = () => { this.backgroundImage = img; this.isLoading = false; this.isEditing = true; this._tempCanvas.width = img.width; this._tempCanvas.height = img.height; this._tempCtx.drawImage(img, 0, 0); this.originalImageData = this._tempCtx.getImageData(0, 0, img.width, img.height); this.setDirtyCanvas(true, true); if (app.graph) app.graph.setDirtyCanvas(true, true); this._syncWidgetsFromAdjustments(); this._syncOverlayUI(); this._updateBasicParamLabels(); this._initWebSocket(); this._scheduleHeavyRender(); }; img.onerror = () => { this.isLoading = false; this.isEditing = false; this.setDirtyCanvas(true); }; img.src = imageUrl; }; nodeType.prototype._updateDisplaySize = function(cS) { this.canvasPixelSize = cS; const safeHeight = this.realBackground.height || 1; const bgAR = this.realBackground.width / safeHeight; if (bgAR >= 1) { this.displayWidth = cS; this.displayHeight = cS / bgAR; } else { this.displayHeight = cS; this.displayWidth = cS * bgAR; } }; nodeType.prototype._toggleAdvancedMode = function(forceClose = false) { if (forceClose || this.advancedMode) { this.advancedMode = false; if (activeNodeId === this.id) activeNodeId = null; this.overlayContainer.style.display = 'none'; if (this._overlayRenderLoop) { cancelAnimationFrame(this._overlayRenderLoop); this._overlayRenderLoop = null; } this.setDirtyCanvas(true); return; } if (activeNodeId && activeNodeId !== this.id) { const otherNode = app.graph?.nodes.find(n => n.id == activeNodeId); if (otherNode && typeof otherNode._toggleAdvancedMode === 'function') { otherNode._toggleAdvancedMode(true); } } this.advancedMode = true; activeNodeId = this.id; this.overlayContainer.style.display = 'flex'; this._syncOverlayUI(); requestAnimationFrame(() => { this._resizeOverlayCanvas(); this._updateSectionHeaders(); this._startOverlayRenderLoop(); }); }; nodeType.prototype._resizeOverlayCanvas = function() { if (!this.overlayCanvasWrapper || !this.overlayCanvas) return; const w = this.overlayCanvasWrapper.clientWidth; const h = this.overlayCanvasWrapper.clientHeight; if (w > 0 && h > 0) { this.overlayCanvas.width = w; this.overlayCanvas.height = h; this.setDirtyCanvas(true); } }; nodeType.prototype._startOverlayRenderLoop = function() { if (this._overlayRenderLoop) return; const render = () => { if (!this.advancedMode) return; this._drawOverlayCanvas(); this._overlayRenderLoop = requestAnimationFrame(render); }; this._overlayRenderLoop = requestAnimationFrame(render); }; nodeType.prototype._drawOverlayCanvas = function() { if (!this.advancedMode || !this.overlayCtx) return; const ctx = this.overlayCtx; const w = this.overlayCanvas.width; const h = this.overlayCanvas.height; ctx.clearRect(0, 0, w, h); if (this.isLoading) { ctx.fillStyle = "#888"; ctx.font = "14px Arial"; ctx.textAlign = "center"; ctx.fillText("Loading...", w / 2, h / 2); return; } if (!this.backgroundImage) return; const safeDW = this.displayWidth > 0 ? this.displayWidth : 100; const safeDH = this.displayHeight > 0 ? this.displayHeight : 100; const scale = Math.min(w / safeDW, h / safeDH); const drawW = safeDW * scale; const drawH = safeDH * scale; const drawX = (w - drawW) / 2; const drawY = (h - drawH) / 2; if (this.previewImage) { ctx.drawImage(this.previewImage, drawX, drawY, drawW, drawH); } else if (this.originalImageData) { const processed = new ImageData( new Uint8ClampedArray(this.originalImageData.data), this.originalImageData.width, this.originalImageData.height ); applyBasicAdjustmentsJS(processed, this.adjustments.brightness, this.adjustments.contrast, this.adjustments.hue, this.adjustments.saturation); this._tempCanvas.width = processed.width; this._tempCanvas.height = processed.height; this._tempCtx.putImageData(processed, 0, 0); ctx.drawImage(this._tempCanvas, drawX, drawY, drawW, drawH); } else { ctx.drawImage(this.backgroundImage, drawX, drawY, drawW, drawH); } }; nodeType.prototype._sendAdjustments = async function() { if (this.currentRenderAbortController) { this.currentRenderAbortController.abort(); this.currentRenderAbortController = null; } const payload = { id: String(this.id), adjustments: this.adjustments, is_batch: this.isBatchMode }; try { await api.fetchApi("/rayko/rs_adjustments", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); this.isEditing = false; this.setDirtyCanvas(true); } catch(e) {} }; nodeType.prototype._cancelEditing = async function() { if (this.currentRenderAbortController) { this.currentRenderAbortController.abort(); this.currentRenderAbortController = null; } clearTimeout(this.renderTimeout); try { await api.interrupt(); } catch(e) {} await fetch("/rayko/rs_adjustments/cancel", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ node_id: String(this.id) }) }); this._cleanup(); }; nodeType.prototype._cleanup = function() { this.isEditing = false; this.isLoading = false; this.backgroundImage = null; this.previewImage = null; this.originalImageData = null; this.pendingEditorData = null; this.sliderDragging = -1; if (this._overlayRenderLoop) { cancelAnimationFrame(this._overlayRenderLoop); this._overlayRenderLoop = null; } if (this._ws) { this._ws.close(); this._ws = null; this._wsConnected = false; } if (this._wsReconnectTimer) { clearTimeout(this._wsReconnectTimer); this._wsReconnectTimer = null; } this.setDirtyCanvas(true); }; nodeType.prototype.onResize = function(size) { if (size[0] < this.minWidth) size[0] = this.minWidth; const titleH = LiteGraph.NODE_TITLE_HEIGHT || 30; const canvasTopPadding = 30; const cSize = Math.max(200, size[0] - 40); this.canvasPixelSize = cSize; const slidersH = 5 * 30 + 10; const btnH = 30; const gaps = 10 + 8 + 8 + 20 + 15; const neededHeight = titleH + canvasTopPadding + cSize + slidersH + btnH + gaps; if (size[1] < neededHeight) size[1] = neededHeight; this.setDirtyCanvas(true); if (this.isEditing && this.backgroundImage) { this._updateDisplaySize(cSize); } }; nodeType.prototype._getCanvasMetrics = function() { const titleH = LiteGraph.NODE_TITLE_HEIGHT || 30; const canvasTopPadding = 30; let cSize, rectX, rectY; if (this.advancedMode) { cSize = Math.min(this.overlayCanvas.width || 1000, this.overlayCanvas.height || 800); rectX = 0; rectY = 0; } else { cSize = Math.max(200, this.size[0] - 40); rectX = (this.size[0] - cSize) / 2; rectY = titleH + canvasTopPadding; } return { cSize, rectX, rectY }; }; nodeType.prototype._roundRect = function(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); }; nodeType.prototype._drawSlider = function(ctx, label, value, min, max, x, y, w, h, isHover, isActive) { ctx.fillStyle = isHover ? "#2a2a2a" : "#252525"; ctx.strokeStyle = isActive ? "#4CAF50" : (isHover ? "#4CAF50" : "#444"); ctx.lineWidth = 1; this._roundRect(ctx, x, y, w, h, 4); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#aaa"; ctx.font = "10px sans-serif"; ctx.textAlign = "left"; ctx.textBaseline = "middle"; ctx.fillText(label, x + 8, y + h / 2); const trackX = x + 90; const trackW = w - 190; const trackY = y + h / 2; const trackH = 3; ctx.fillStyle = "#444"; ctx.fillRect(trackX, trackY - trackH / 2, trackW, trackH); const ratio = Math.max(0, Math.min(1, (value - min) / (max - min))); const fillW = trackW * ratio; ctx.fillStyle = "#4CAF50"; ctx.fillRect(trackX, trackY - trackH / 2, fillW, trackH); const handleX = trackX + fillW; const handleSize = 12; ctx.fillStyle = "#fff"; ctx.beginPath(); ctx.arc(handleX, trackY, handleSize / 2, 0, Math.PI * 2); ctx.fill(); const valueW = 50; const valueX = x + w - valueW - 38; ctx.fillStyle = "#222"; ctx.strokeStyle = "#444"; this._roundRect(ctx, valueX, y + 3, valueW, h - 6, 3); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#4CAF50"; ctx.font = "10px monospace"; ctx.textAlign = "center"; ctx.fillText(String(Math.round(value)), valueX + valueW / 2, y + h / 2); const resetBtnSize = 24; const resetBtnX = x + w - resetBtnSize - 8; const resetBtnY = y + (h - resetBtnSize) / 2; ctx.fillStyle = "#252525"; ctx.strokeStyle = "#444"; this._roundRect(ctx, resetBtnX, resetBtnY, resetBtnSize, resetBtnSize, 4); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#888"; ctx.font = "14px sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("🔄", resetBtnX + resetBtnSize / 2, resetBtnY + resetBtnSize / 2); return { x: trackX - 6, y: y, w: trackW + 12, h: h, resetBtnX, resetBtnY, resetBtnSize, valueX, valueY: y, valueW, valueH: h }; }; nodeType.prototype.onDrawForeground = function(ctx) { if (this.advancedMode) { ctx.clearRect(0, 0, this.size[0], this.size[1]); return; } const { cSize, rectX, rectY } = this._getCanvasMetrics(); ctx.fillStyle = "#1e1e1e"; ctx.fillRect(rectX, rectY, cSize, cSize); ctx.strokeStyle = "#555"; ctx.strokeRect(rectX, rectY, cSize, cSize); this._updateDisplaySize(cSize); if (this.isLoading) { ctx.fillStyle = "#888"; ctx.font = "12px Arial"; ctx.fillText("Loading...", rectX + cSize / 2 - 35, rectY + cSize / 2); } else if (this.isEditing && this.backgroundImage) { try { const safeDW = this.displayWidth > 0 ? this.displayWidth : 100; const safeDH = this.displayHeight > 0 ? this.displayHeight : 100; ctx.save(); ctx.translate(rectX, rectY); const scale = Math.min(cSize / safeDW, cSize / safeDH); const drawW = safeDW * scale; const drawH = safeDH * scale; const drawX = (cSize - drawW) / 2; const drawY = (cSize - drawH) / 2; if (this.previewImage) { ctx.drawImage(this.previewImage, drawX, drawY, drawW, drawH); } else if (this.originalImageData) { const processed = new ImageData( new Uint8ClampedArray(this.originalImageData.data), this.originalImageData.width, this.originalImageData.height ); applyBasicAdjustmentsJS(processed, this.adjustments.brightness, this.adjustments.contrast, this.adjustments.hue, this.adjustments.saturation); this._tempCanvas.width = processed.width; this._tempCanvas.height = processed.height; this._tempCtx.putImageData(processed, 0, 0); ctx.drawImage(this._tempCanvas, drawX, drawY, drawW, drawH); } else { ctx.drawImage(this.backgroundImage, drawX, drawY, drawW, drawH); } ctx.restore(); } catch (e) { console.error('[RS Adjustments] FATAL DRAW ERROR:', e); } } else { ctx.fillStyle = "#888"; ctx.font = "12px Arial"; ctx.fillText("▶ Run queue to start", rectX + cSize / 2 - 65, rectY + cSize / 2); } const btnW = 140; const btnH = 24; const gap = 10; const totalW = btnW * 2 + gap; const startX = (this.size[0] - totalW) / 2; const btnY = 20; const advBtnX = startX; ctx.fillStyle = this.btnAdvancedHover ? "#3a3a3a" : "#2a2a2a"; ctx.strokeStyle = "#2196F3"; ctx.lineWidth = 2; this._roundRect(ctx, advBtnX, btnY, btnW, btnH, 6); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#2196F3"; ctx.font = "bold 11px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "alphabetic"; ctx.fillText("✨ ADVANCED", advBtnX + btnW/2, btnY + btnH/2 + 4); const resetBtnX = startX + btnW + gap; ctx.fillStyle = this.btnResetAllHover ? "#3a3a3a" : "#2a2a2a"; ctx.strokeStyle = "#FF9800"; ctx.lineWidth = 2; this._roundRect(ctx, resetBtnX, btnY, btnW, btnH, 6); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#FF9800"; ctx.font = "bold 11px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "alphabetic"; ctx.fillText("🔄 RESET ALL", resetBtnX + btnW/2, btnY + btnH/2 + 4); const widgetX = 15; const widgetW = this.size[0] - 30; const slidersY = rectY + cSize + 10; this.slidersY = slidersY; this.sliderRects = []; this.resetBtnRects = []; const sliderH = 30; const gapSlider = 8; const sliderConfigs = [ { label: "BRIGHTNESS", key: "brightness", min: -100, max: 100 }, { label: "CONTRAST", key: "contrast", min: -100, max: 100 }, { label: "SHARPEN", key: "sharpen", min: 0, max: 200 }, { label: "HUE", key: "hue", min: -180, max: 180 }, { label: "SATURATION", key: "saturation", min: -100, max: 100 } ]; sliderConfigs.forEach((cfg, i) => { const y = slidersY + i * (sliderH + gapSlider); const rect = this._drawSlider(ctx, cfg.label, this.adjustments[cfg.key], cfg.min, cfg.max, widgetX, y, widgetW, sliderH, this.sliderHover[i], this.sliderDragging === i); this.sliderRects.push({ ...rect, key: cfg.key, min: cfg.min, max: cfg.max }); const resetBtnRect = { x: rect.resetBtnX, y: rect.resetBtnY, w: rect.resetBtnSize, h: rect.resetBtnSize, key: cfg.key }; this.resetBtnRects.push(resetBtnRect); if (this.resetBtnHover[i]) { ctx.strokeStyle = "#4CAF50"; ctx.lineWidth = 1.5; this._roundRect(ctx, resetBtnRect.x, resetBtnRect.y, resetBtnRect.w, resetBtnRect.h, 4); ctx.stroke(); } }); // THREE BUTTON ROW: BATCH | APPLY | CANCEL const buttonsY = slidersY + 5 * (sliderH + gapSlider) + 10; const btnH2 = 30; const btnGap2 = 8; const btnW3 = (this.size[0] - 46) / 3; // BATCH BUTTON const batchBtnX = 15; const isBatchActive = this.isBatchMode; ctx.fillStyle = isBatchActive ? "#2196F3" : (this.btnBatchHover ? "#3a3a3a" : "#2a2a2a"); ctx.strokeStyle = isBatchActive ? "#2196F3" : "#666"; ctx.lineWidth = isBatchActive ? 2 : 1; this._roundRect(ctx, batchBtnX, buttonsY, btnW3, btnH2, 6); ctx.fill(); ctx.stroke(); ctx.fillStyle = isBatchActive ? "#fff" : "#aaa"; ctx.font = "bold 11px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "alphabetic"; ctx.fillText(isBatchActive ? "⚪ BATCH ON" : "️🔘 BATCH OFF", batchBtnX + btnW3/2, buttonsY + btnH2/2 + 4); // APPLY BUTTON const applyBtnX = batchBtnX + btnW3 + btnGap2; ctx.fillStyle = this.btnApplyHover ? "#444" : "#2a2a2a"; this._roundRect(ctx, applyBtnX, buttonsY, btnW3, btnH2, 6); ctx.fill(); ctx.strokeStyle = "#4CAF50"; ctx.lineWidth = 2; ctx.stroke(); ctx.fillStyle = "#4CAF50"; ctx.fillText("✔️ APPLY", applyBtnX + btnW3/2, buttonsY + btnH2/2 + 4); // CANCEL BUTTON const cancelBtnX = applyBtnX + btnW3 + btnGap2; ctx.fillStyle = this.btnCancelHover ? "#444" : "#2a2a2a"; this._roundRect(ctx, cancelBtnX, buttonsY, btnW3, btnH2, 6); ctx.fill(); ctx.strokeStyle = "#dc3545"; ctx.lineWidth = 2; ctx.stroke(); ctx.fillStyle = "#dc3545"; ctx.fillText("❌ CANCEL", cancelBtnX + btnW3/2, buttonsY + btnH2/2 + 4); }; nodeType.prototype.onMouseDown = function(event, pos) { if (!this.advancedMode && pos) { const btnW = 140; const btnH = 24; const gap = 10; const totalW = btnW * 2 + gap; const startX = (this.size[0] - totalW) / 2; const btnY = 20; const advBtnX = startX; const resetBtnX = startX + btnW + gap; if (pos[0] >= advBtnX && pos[0] <= advBtnX + btnW && pos[1] >= btnY && pos[1] <= btnY + btnH) { this._toggleAdvancedMode(); return true; } if (pos[0] >= resetBtnX && pos[0] <= resetBtnX + btnW && pos[1] >= btnY && pos[1] <= btnY + btnH) { this._resetAllParameters(); return true; } } if (this.sliderRects && pos) { for (let i = 0; i < this.sliderRects.length; i++) { const rect = this.sliderRects[i]; if (rect.valueX !== undefined && pos[0] >= rect.valueX && pos[0] <= rect.valueX + rect.valueW && pos[1] >= rect.valueY && pos[1] <= rect.valueY + rect.valueH) { this._showValueInputPopup(i, event); return true; } } } if (this.sliderRects && pos) { for (let i = 0; i < this.sliderRects.length; i++) { const rect = this.sliderRects[i]; if (pos[0] >= rect.x && pos[0] <= rect.x + rect.w && pos[1] >= rect.y && pos[1] <= rect.y + rect.h) { this.sliderDragging = i; this._updateSliderFromMouse(i, pos[0]); return true; } } } if (this.resetBtnRects && pos) { for (let i = 0; i < this.resetBtnRects.length; i++) { const rect = this.resetBtnRects[i]; if (pos[0] >= rect.x && pos[0] <= rect.x + rect.w && pos[1] >= rect.y && pos[1] <= rect.y + rect.h) { const defaultVal = DEFAULT_SLIDER_VALUES[rect.key] !== undefined ? DEFAULT_SLIDER_VALUES[rect.key] : 0; this.adjustments[rect.key] = defaultVal; const widget = this.widgets?.find(w => w.name === rect.key); if (widget) widget.value = defaultVal; this.previewImage = null; this.setDirtyCanvas(true); return true; } } } if (pos && this.slidersY !== undefined) { const btnH2 = 30; const btnGap2 = 8; const btnW3 = (this.size[0] - 46) / 3; const buttonsY = this.slidersY + 5 * 38 + 10; // BATCH CLICK const batchBtnX = 15; if (pos[0] >= batchBtnX && pos[0] <= batchBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2) { this.isBatchMode = !this.isBatchMode; console.log(`[RS Adjustments] Node ${this.id}: Batch toggled to ${this.isBatchMode}`); if (!this.isBatchMode) { this._batchParams = null; } this.setDirtyCanvas(true); return true; } // APPLY CLICK const applyBtnX = batchBtnX + btnW3 + btnGap2; if (pos[0] >= applyBtnX && pos[0] <= applyBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2) { if (this.isBatchMode) { this._batchParams = JSON.parse(JSON.stringify(this.adjustments)); } this._sendAdjustments(); return true; } // CANCEL CLICK const cancelBtnX = applyBtnX + btnW3 + btnGap2; if (pos[0] >= cancelBtnX && pos[0] <= cancelBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2) { this.isBatchMode = false; this._batchParams = null; this._cancelEditing(); return true; } } return false; }; nodeType.prototype.onMouseMove = function(event, pos) { if (this.sliderDragging >= 0 && event && event.buttons === 0) { this.sliderDragging = -1; this.setDirtyCanvas(true); return; } if (this.sliderDragging >= 0 && pos) { this._updateSliderFromMouse(this.sliderDragging, pos[0]); return; } if (pos && !this.advancedMode) { const btnW = 140; const btnH = 24; const gap = 10; const totalW = btnW * 2 + gap; const startX = (this.size[0] - totalW) / 2; const btnY = 20; const advBtnX = startX; const resetBtnX = startX + btnW + gap; const prevAdv = this.btnAdvancedHover; const prevReset = this.btnResetAllHover; this.btnAdvancedHover = pos[0] >= advBtnX && pos[0] <= advBtnX + btnW && pos[1] >= btnY && pos[1] <= btnY + btnH; this.btnResetAllHover = pos[0] >= resetBtnX && pos[0] <= resetBtnX + btnW && pos[1] >= btnY && pos[1] <= btnY + btnH; if (prevAdv !== this.btnAdvancedHover || prevReset !== this.btnResetAllHover) { this.setDirtyCanvas(true); } } const prevHover = [...this.sliderHover]; const prevResetHover = [...this.resetBtnHover]; if (this.sliderRects && pos) { for (let i = 0; i < this.sliderRects.length; i++) { const rect = this.sliderRects[i]; this.sliderHover[i] = pos[0] >= rect.x && pos[0] <= rect.x + rect.w && pos[1] >= rect.y && pos[1] <= rect.y + rect.h; } } if (this.resetBtnRects && pos) { for (let i = 0; i < this.resetBtnRects.length; i++) { const rect = this.resetBtnRects[i]; this.resetBtnHover[i] = pos[0] >= rect.x && pos[0] <= rect.x + rect.w && pos[1] >= rect.y && pos[1] <= rect.y + rect.h; } } if (prevHover.some((v, i) => v !== this.sliderHover[i]) || prevResetHover.some((v, i) => v !== this.resetBtnHover[i])) { this.setDirtyCanvas(true); } if (pos && this.slidersY !== undefined) { const btnH2 = 30; const btnGap2 = 8; const btnW3 = (this.size[0] - 46) / 3; const buttonsY = this.slidersY + 5 * 38 + 10; const prevBatch = this.btnBatchHover; const batchBtnX = 15; this.btnBatchHover = pos[0] >= batchBtnX && pos[0] <= batchBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2; const prevApply = this.btnApplyHover; const prevCancel = this.btnCancelHover; const applyBtnX = batchBtnX + btnW3 + btnGap2; const cancelBtnX = applyBtnX + btnW3 + btnGap2; this.btnApplyHover = pos[0] >= applyBtnX && pos[0] <= applyBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2; this.btnCancelHover = pos[0] >= cancelBtnX && pos[0] <= cancelBtnX + btnW3 && pos[1] >= buttonsY && pos[1] <= buttonsY + btnH2; if (prevBatch !== this.btnBatchHover || prevApply !== this.btnApplyHover || prevCancel !== this.btnCancelHover) { this.setDirtyCanvas(true); } } }; nodeType.prototype.onMouseUp = function() { if (this.sliderDragging >= 0) { this.sliderDragging = -1; this.setDirtyCanvas(true); } }; nodeType.prototype._updateSliderFromMouse = function(index, mouseX) { if (!this.sliderRects || !this.sliderRects[index]) return; const rect = this.sliderRects[index]; const ratio = Math.max(0, Math.min(1, (mouseX - rect.x) / rect.w)); const value = Math.round(rect.min + ratio * (rect.max - rect.min)); this.adjustments[rect.key] = value; const widget = this.widgets?.find(w => w.name === rect.key); if (widget) widget.value = value; if (this.advancedMode) { if (this.overlayInputs[rect.key]) { this.overlayInputs[rect.key].value = value; } if (this.sliderDisplays[rect.key]) { this.sliderDisplays[rect.key].textContent = String(value); } } if (BASIC_PARAMS.includes(rect.key)) { this.previewImage = null; } else { this._scheduleHeavyRender(); } this.setDirtyCanvas(true); }; nodeType.prototype._showValueInputPopup = function(index, event) { if (!this.sliderRects || !this.sliderRects[index]) return; const rect = this.sliderRects[index]; const currentValue = this.adjustments[rect.key]; const screenX = event.clientX; const screenY = event.clientY; const popup = document.createElement('div'); popup.style.cssText = 'position:fixed;z-index:10003;background:#1a1a1a;border:1px solid #444;border-radius:6px;padding:8px 12px;box-shadow:0 4px 20px rgba(0,0,0,0.5);display:flex;align-items:center;gap:8px;'; const input = document.createElement('input'); input.type = 'number'; input.value = currentValue; input.min = rect.min; input.max = rect.max; input.step = 1; input.style.cssText = 'width:100px;background:#222;color:#fff;border:1px solid #444;border-radius:4px;padding:6px 10px;font-size:12px;outline:none;'; const saveBtn = document.createElement('button'); saveBtn.textContent = 'OK'; saveBtn.style.cssText = 'background:#4CAF50;color:#fff;border:none;border-radius:4px;padding:6px 12px;font-size:12px;cursor:pointer;'; const doSave = () => { let num = parseInt(input.value); if (isNaN(num)) num = currentValue; num = Math.max(rect.min, Math.min(rect.max, num)); this.adjustments[rect.key] = num; const widget = this.widgets?.find(w => w.name === rect.key); if (widget) widget.value = num; if (this.advancedMode) { if (this.overlayInputs[rect.key]) { this.overlayInputs[rect.key].value = num; } if (this.sliderDisplays[rect.key]) { this.sliderDisplays[rect.key].textContent = String(num); } } popup.remove(); this.previewImage = null; this.setDirtyCanvas(true); }; saveBtn.onclick = (ev) => { ev.stopPropagation(); ev.preventDefault(); doSave(); }; input.onkeydown = (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); doSave(); } }; popup.appendChild(input); popup.appendChild(saveBtn); document.body.appendChild(popup); const popupWidth = popup.offsetWidth || 180; const popupHeight = popup.offsetHeight || 40; let leftPos = screenX + 15; let topPos = screenY - popupHeight / 2; if (leftPos + popupWidth > window.innerWidth - 10) { leftPos = screenX - popupWidth - 15; } if (topPos < 10) topPos = 10; if (topPos + popupHeight > window.innerHeight - 10) { topPos = window.innerHeight - popupHeight - 10; } popup.style.left = leftPos + 'px'; popup.style.top = topPos + 'px'; setTimeout(() => { input.focus(); input.select(); }, 50); setTimeout(() => { const closeHandler = (ev) => { if (!popup.contains(ev.target)) { popup.remove(); document.removeEventListener('mousedown', closeHandler); } }; document.addEventListener('mousedown', closeHandler); }, 100); }; } });