import { app } from "../../scripts/app.js"; import { ComfyWidgets } from "../../scripts/widgets.js"; // Parameter definitions const PARAM_INFO = { none: { description: "No parameter selected", hasOptions: false }, model: { description: "Checkpoint/Model files", hasOptions: true, inputNames: ['ckpt_name', 'checkpoint', 'model_name', 'unet_name'] }, vae: { description: "VAE models", hasOptions: true, inputNames: ['vae_name', 'vae'], addDefault: "Automatic" }, lora: { description: "LoRA models", hasOptions: true, inputNames: ['lora_name', 'lora'], addDefault: "None" }, sampler: { description: "Sampling algorithms", hasOptions: true, inputNames: ['sampler_name', 'sampler'] }, scheduler: { description: "Noise schedulers", hasOptions: true, inputNames: ['scheduler', 'scheduler_name'] }, cfg_scale: { description: "CFG Scale values", example: "Examples: 5, 7.5, 10\nRange: 5:15:2.5" }, steps: { description: "Sampling steps", example: "Examples: 20, 30, 40\nRange: 10:50:10" }, seed: { description: "Random seeds", example: "Examples: 42, 123, 456\nRange: 0:1000:100" }, denoise: { description: "Denoising strength", example: "Examples: 0.3, 0.5, 0.7\nRange: 0.2:1.0:0.2" }, flux_guidance: { description: "Flux guidance strength", example: "Examples: 1.0, 3.5, 5.0\nRange: 1:5:0.5" }, prompt: { description: "Text prompts", example: "Enter one prompt per line:\n\na beautiful sunset\na mystical forest" } }; // Get options for parameter types async function getParameterOptions(paramType) { if (!PARAM_INFO[paramType]?.hasOptions) return []; try { const resp = await fetch('/object_info'); const data = await resp.json(); const inputNames = PARAM_INFO[paramType].inputNames || [paramType]; let allOptions = []; // Search through all nodes for (const [nodeName, nodeData] of Object.entries(data)) { if (nodeData.input?.required) { for (const [inputName, inputDef] of Object.entries(nodeData.input.required)) { if (inputNames.includes(inputName) && Array.isArray(inputDef[0])) { allOptions = [...new Set([...allOptions, ...inputDef[0]])]; } } } } // Add default option if specified if (PARAM_INFO[paramType].addDefault) { allOptions.unshift(PARAM_INFO[paramType].addDefault); } return allOptions; } catch (e) { console.error(`Error fetching ${paramType} options:`, e); return []; } } // Create multi-select dialog function showMultiSelectDialog(options, currentValues, paramType, onApply) { // Create overlay const overlay = document.createElement("div"); overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.7); z-index: 10000; display: flex; align-items: center; justify-content: center; `; // Create dialog const dialog = document.createElement("div"); dialog.style.cssText = ` background: #1e1e1e; border: 2px solid #444; border-radius: 8px; padding: 20px; max-width: 500px; width: 90%; max-height: 70vh; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 10px 30px rgba(0,0,0,0.5); `; // Title const title = document.createElement("h3"); title.textContent = `Select ${paramType.charAt(0).toUpperCase() + paramType.slice(1)}s`; title.style.cssText = "margin: 0 0 15px 0; color: #4ecdc4; font-size: 16px;"; dialog.appendChild(title); // Search box const searchBox = document.createElement("input"); searchBox.type = "text"; searchBox.placeholder = "Search..."; searchBox.style.cssText = ` padding: 8px; margin-bottom: 10px; background: #2a2a2a; border: 1px solid #444; border-radius: 4px; color: #fff; font-size: 14px; `; dialog.appendChild(searchBox); // Options container const optionsContainer = document.createElement("div"); optionsContainer.style.cssText = ` flex: 1; overflow-y: auto; border: 1px solid #444; border-radius: 4px; padding: 10px; background: #2a2a2a; max-height: 400px; `; const checkboxes = []; const labels = []; // Create checkboxes options.forEach(option => { const label = document.createElement("label"); label.style.cssText = ` display: block; padding: 8px; cursor: pointer; color: #ddd; transition: background 0.2s; border-radius: 4px; margin-bottom: 2px; `; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.value = option; checkbox.checked = currentValues.includes(option); checkbox.style.cssText = ` margin-right: 10px; width: 16px; height: 16px; cursor: pointer; `; label.appendChild(checkbox); label.appendChild(document.createTextNode(option)); label.onmouseenter = () => label.style.background = "rgba(78, 205, 196, 0.1)"; label.onmouseleave = () => label.style.background = "transparent"; checkboxes.push(checkbox); labels.push(label); optionsContainer.appendChild(label); }); dialog.appendChild(optionsContainer); // Search functionality searchBox.oninput = () => { const searchTerm = searchBox.value.toLowerCase(); labels.forEach((label, i) => { const text = options[i].toLowerCase(); label.style.display = text.includes(searchTerm) ? "block" : "none"; }); }; // Buttons const buttonContainer = document.createElement("div"); buttonContainer.style.cssText = ` margin-top: 15px; display: flex; gap: 10px; justify-content: flex-end; `; const selectAllBtn = document.createElement("button"); selectAllBtn.textContent = "Select All"; selectAllBtn.style.cssText = ` padding: 8px 15px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer; `; selectAllBtn.onclick = () => { checkboxes.forEach(cb => { if (cb.parentElement.style.display !== "none") { cb.checked = true; } }); }; const clearBtn = document.createElement("button"); clearBtn.textContent = "Clear All"; clearBtn.style.cssText = ` padding: 8px 15px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer; `; clearBtn.onclick = () => { checkboxes.forEach(cb => cb.checked = false); }; const cancelBtn = document.createElement("button"); cancelBtn.textContent = "Cancel"; cancelBtn.style.cssText = ` padding: 8px 15px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer; `; cancelBtn.onclick = () => document.body.removeChild(overlay); const applyBtn = document.createElement("button"); applyBtn.textContent = "Apply"; applyBtn.style.cssText = ` padding: 8px 15px; background: #4a90e2; color: white; border: none; border-radius: 4px; cursor: pointer; `; applyBtn.onclick = () => { const selected = checkboxes .filter(cb => cb.checked) .map(cb => cb.value); onApply(selected); document.body.removeChild(overlay); }; buttonContainer.appendChild(selectAllBtn); buttonContainer.appendChild(clearBtn); buttonContainer.appendChild(cancelBtn); buttonContainer.appendChild(applyBtn); dialog.appendChild(buttonContainer); overlay.appendChild(dialog); document.body.appendChild(overlay); // Focus search box searchBox.focus(); } // Create the enhanced UI function createEnhancedUI(node) { const container = document.createElement("div"); container.className = "xyz-controller-ui"; container.style.cssText = ` width: 100%; height: 100%; max-height: 620px; overflow-y: auto; overflow-x: hidden; box-sizing: border-box; `; container.innerHTML = `
X AXIS
Parameter:
Y AXIS
Parameter:
Z AXIS (Optional)
Parameter:
Total Images: 0
`; // Setup event handlers function setupAxisHandlers(axis) { const typeSelect = container.querySelector(`#${axis}_axis_type`); const valuesTextarea = container.querySelector(`#${axis}_values`); const selectBtn = container.querySelector(`#${axis}_select_btn`); const infoBox = container.querySelector(`#${axis}_info`); typeSelect.addEventListener('change', async () => { const paramType = typeSelect.value; const info = PARAM_INFO[paramType]; // Update widget values const typeWidget = (node.originalWidgets || node.widgets).find(w => w.name === `${axis}_axis_type`); if (typeWidget) { typeWidget.value = paramType; typeWidget.callback?.(paramType); } if (!info || paramType === 'none') { selectBtn.style.display = 'none'; infoBox.style.display = 'none'; valuesTextarea.placeholder = 'Values will appear here...'; return; } // Show info infoBox.style.display = 'block'; infoBox.innerHTML = ` ${info.description} ${info.example ? `
${info.example}
` : ''} `; // Show/hide select button if (info.hasOptions) { selectBtn.style.display = 'block'; selectBtn.textContent = `📁 Select ${paramType.charAt(0).toUpperCase() + paramType.slice(1)}s`; valuesTextarea.placeholder = 'Click the button above to select values...'; } else { selectBtn.style.display = 'none'; valuesTextarea.placeholder = 'Enter values...'; } updateTotalCount(); }); selectBtn.addEventListener('click', async () => { const paramType = typeSelect.value; const options = await getParameterOptions(paramType); const currentValues = valuesTextarea.value.split(',').map(v => v.trim()).filter(v => v); showMultiSelectDialog(options, currentValues, paramType, (selected) => { valuesTextarea.value = selected.join(', '); const valueWidget = (node.originalWidgets || node.widgets).find(w => w.name === `${axis}_values`); if (valueWidget) { valueWidget.value = valuesTextarea.value; valueWidget.callback?.(valuesTextarea.value); } updateTotalCount(); }); }); valuesTextarea.addEventListener('input', () => { const valueWidget = (node.originalWidgets || node.widgets).find(w => w.name === `${axis}_values`); if (valueWidget) { valueWidget.value = valuesTextarea.value; valueWidget.callback?.(valuesTextarea.value); } updateTotalCount(); }); } function updateTotalCount() { let xCount = 1, yCount = 1, zCount = 1; ['x', 'y', 'z'].forEach(axis => { const typeSelect = container.querySelector(`#${axis}_axis_type`); const valuesTextarea = container.querySelector(`#${axis}_values`); if (typeSelect.value !== 'none' && valuesTextarea.value.trim()) { const values = valuesTextarea.value.trim(); // Check for range syntax if (values.includes(':')) { const parts = values.split(':'); if (parts.length >= 2) { const start = parseFloat(parts[0]); const stop = parseFloat(parts[1]); const step = parts[2] ? parseFloat(parts[2]) : 1; const count = Math.floor((stop - start) / step) + 1; if (axis === 'x') xCount = count; else if (axis === 'y') yCount = count; else if (axis === 'z') zCount = count; } } else { // Count comma-separated values const count = values.split(',').filter(v => v.trim()).length; if (axis === 'x') xCount = count; else if (axis === 'y') yCount = count; else if (axis === 'z') zCount = count; } } }); const total = xCount * yCount * zCount; const totalDiv = container.querySelector('#xyz_total_count'); totalDiv.textContent = `Total Images: ${total}`; totalDiv.className = 'xyz-total-count'; if (total > 100) { totalDiv.className += ' danger'; totalDiv.textContent += ' ⚠️'; } else if (total > 50) { totalDiv.className += ' warning'; } } // Initialize handlers ['x', 'y', 'z'].forEach(axis => setupAxisHandlers(axis)); // Sync with existing widget values function syncFromWidgets() { ['x', 'y', 'z'].forEach(axis => { const widgets = node.originalWidgets || node.widgets; const typeWidget = widgets.find(w => w.name === `${axis}_axis_type`); const valueWidget = widgets.find(w => w.name === `${axis}_values`); if (typeWidget) { const typeSelect = container.querySelector(`#${axis}_axis_type`); typeSelect.value = typeWidget.value; typeSelect.dispatchEvent(new Event('change')); } if (valueWidget) { const valuesTextarea = container.querySelector(`#${axis}_values`); valuesTextarea.value = valueWidget.value; } }); updateTotalCount(); } setTimeout(syncFromWidgets, 100); return container; } app.registerExtension({ name: "ComfyAssets.XYZPlotControllerV2", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === "XYZPlotController") { const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { if (onNodeCreated) { onNodeCreated.apply(this, arguments); } // Set initial size immediately - adjusted for compact layout this.size = [380, 680]; // Store original widgets for later access but hide them completely this.originalWidgets = this.widgets.slice(); // Remove all original widgets from display this.widgets = this.widgets.filter(w => { // Hide DOM elements if (w.element) { w.element.style.display = 'none'; if (w.element.parentElement) { w.element.parentElement.style.display = 'none'; } } if (w.inputEl) { w.inputEl.style.display = 'none'; if (w.inputEl.parentElement) { w.inputEl.parentElement.style.display = 'none'; } } // Keep only non-visual widgets return false; }); // Set initial size this.size = [380, 680]; // Create custom UI immediately const ui = createEnhancedUI(this); // Create the DOM widget const customWidget = this.addDOMWidget("xyz_enhanced_ui", "div", ui, { serialize: false, hideOnZoom: false }); // Ensure widget has proper size if (customWidget) { customWidget.computeSize = () => [this.size[0] - 20, this.size[1] - 60]; customWidget.size = customWidget.computeSize(); // Ensure the element has proper height constraint if (customWidget.element) { customWidget.element.style.maxHeight = (this.size[1] - 60) + 'px'; customWidget.element.style.overflow = 'hidden'; } } // Update widgets array this.widgets = [customWidget]; // Schedule a resize to ensure proper display setTimeout(() => { this.setSize(this.size); this.setDirtyCanvas(true, true); }, 0); }; // Override onResize to handle widget sizing const onResize = nodeType.prototype.onResize; nodeType.prototype.onResize = function(size) { if (onResize) { onResize.apply(this, arguments); } // Update widget size when node is resized const customWidget = this.widgets?.find(w => w.name === "xyz_enhanced_ui"); if (customWidget) { customWidget.size = [size[0] - 20, size[1] - 60]; if (customWidget.element) { customWidget.element.style.width = customWidget.size[0] + 'px'; customWidget.element.style.maxHeight = customWidget.size[1] + 'px'; customWidget.element.style.height = 'auto'; customWidget.element.style.overflow = 'hidden'; } this.setDirtyCanvas(true); } }; } } });