diff --git a/ss.png b/ss.png new file mode 100644 index 0000000..f4d0db0 Binary files /dev/null and b/ss.png differ diff --git a/ss2.png b/ss2.png new file mode 100644 index 0000000..79bcc5c Binary files /dev/null and b/ss2.png differ diff --git a/web/xyz_plot_controller.js b/web/xyz_plot_controller.js index bcfa5b0..3ef6388 100644 --- a/web/xyz_plot_controller.js +++ b/web/xyz_plot_controller.js @@ -1,428 +1,101 @@ import { app } from "../../scripts/app.js"; +import { ComfyWidgets } from "../../scripts/widgets.js"; -console.log("[XYZ Plot Controller] Extension loading..."); - -app.registerExtension({ - name: "ComfyAssets.XYZPlotController", - - async beforeRegisterNodeDef(nodeType, nodeData, app) { - console.log("[XYZ Plot Controller] beforeRegisterNodeDef called for:", nodeData.name); - - if (nodeData.name === "XYZPlotController") { - console.log("[XYZ Plot Controller] Configuring XYZPlotController node..."); - const onNodeCreated = nodeType.prototype.onNodeCreated; - - nodeType.prototype.onNodeCreated = function() { - console.log("[XYZ Plot Controller] onNodeCreated called"); - - if (onNodeCreated) { - onNodeCreated.apply(this, arguments); - } - - // Store reference to node - this.parameterHelpers = createParameterHelpers(); - this.setupComplete = false; - - // Setup UI after a delay to ensure widgets are ready - setTimeout(() => { - console.log("[XYZ Plot Controller] Setting up enhanced UI..."); - if (!this.setupComplete) { - setupEnhancedUI(this); - this.setupComplete = true; - } - }, 100); - }; - - // Handle widget changes - const onWidgetChange = nodeType.prototype.onWidgetChange; - nodeType.prototype.onWidgetChange = function(name, value, oldValue, widget) { - if (onWidgetChange) { - onWidgetChange.call(this, name, value, oldValue, widget); - } - - // Handle axis type changes - if (name.endsWith('_axis_type')) { - const axisPrefix = name.split('_')[0]; - const valueWidgetName = `${axisPrefix}_values`; - console.log(`[XYZ Plot Controller] Axis type changed: ${name} = ${value}`); - updateValueWidget(this, valueWidgetName, value); - } - - // Update total count for any value change - if (name.includes('_values') || name.includes('_axis_type')) { - updateImageCount(this); - } - }; - } - } -}); - -// Parameter information -const parameterInfo = { - none: { description: "No parameter selected" }, +// Parameter definitions +const PARAM_INFO = { + none: { + description: "No parameter selected", + hasOptions: false + }, model: { description: "Checkpoint/Model files", - example: "Click 'Select Models' to choose multiple checkpoints", - hasOptions: true + hasOptions: true, + inputNames: ['ckpt_name', 'checkpoint', 'model_name', 'unet_name'] }, vae: { description: "VAE models", - example: "Click 'Select Vaes' to choose VAEs\n'Automatic' uses the model's built-in VAE", - hasOptions: true + hasOptions: true, + inputNames: ['vae_name', 'vae'], + addDefault: "Automatic" }, lora: { description: "LoRA models", - example: "Click 'Select Loras' to choose LoRAs\n'None' disables LoRA", - hasOptions: true + hasOptions: true, + inputNames: ['lora_name', 'lora'], + addDefault: "None" }, sampler: { description: "Sampling algorithms", - example: "Click 'Select Samplers' to choose sampling methods", - hasOptions: true + hasOptions: true, + inputNames: ['sampler_name', 'sampler'] }, scheduler: { description: "Noise schedulers", - example: "Click 'Select Schedulers' to choose scheduler types", - hasOptions: true + hasOptions: true, + inputNames: ['scheduler', 'scheduler_name'] }, cfg_scale: { - description: "CFG Scale (Classifier-Free Guidance)", - example: "Examples:\n5, 7.5, 10, 12.5\n5:15:2.5 (range from 5 to 15, step 2.5)" + description: "CFG Scale values", + example: "Examples: 5, 7.5, 10\nRange: 5:15:2.5" }, steps: { - description: "Denoising steps", - example: "Examples:\n20, 30, 40, 50\n10:50:10 (range)" - }, - clip_skip: { - description: "CLIP layers to skip", - example: "Common values: 1, 2\nSDXL typically uses 1 or 2" + description: "Sampling steps", + example: "Examples: 20, 30, 40\nRange: 10:50:10" }, seed: { - description: "Random seed", - example: "Examples:\n42, 123, 456\n0:1000:100 (range)" + description: "Random seeds", + example: "Examples: 42, 123, 456\nRange: 0:1000:100" }, denoise: { description: "Denoising strength", - example: "Examples:\n0.3, 0.5, 0.7, 1.0\n0.2:1.0:0.2 (range)" + example: "Examples: 0.3, 0.5, 0.7\nRange: 0.2:1.0:0.2" }, flux_guidance: { description: "Flux guidance strength", - example: "Examples:\n1.0, 2.0, 3.5, 5.0\n1:5:0.5 (range)" + example: "Examples: 1.0, 3.5, 5.0\nRange: 1:5:0.5" }, prompt: { - description: "Text prompts (one per line)", - example: "Enter different prompts, one per line:\n\na beautiful sunset\na mystical forest\na futuristic city" + description: "Text prompts", + example: "Enter one prompt per line:\n\na beautiful sunset\na mystical forest" } }; -// Generic function to find options from any node type -async function findOptionsForParameter(paramType) { +// Get options for parameter types +async function getParameterOptions(paramType) { + if (!PARAM_INFO[paramType]?.hasOptions) return []; + try { - console.log(`[XYZ Plot Controller] Finding options for ${paramType}`); const resp = await fetch('/object_info'); const data = await resp.json(); - // Map parameter types to common input names - const parameterMappings = { - model: ['ckpt_name', 'checkpoint', 'model_name', 'unet_name'], - vae: ['vae_name', 'vae'], - lora: ['lora_name', 'lora'], - sampler: ['sampler_name', 'sampler'], - scheduler: ['scheduler', 'scheduler_name'], - // Add more mappings as needed - }; - - const possibleNames = parameterMappings[paramType] || [paramType]; + const inputNames = PARAM_INFO[paramType].inputNames || [paramType]; let allOptions = []; - // Search through all nodes for matching inputs + // 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)) { - // Check if this input name matches what we're looking for - if (possibleNames.includes(inputName)) { - // Check if it's a list (array as first element) - if (Array.isArray(inputDef[0])) { - console.log(`[XYZ Plot Controller] Found ${paramType} options in ${nodeName}.${inputName}`); - allOptions = [...new Set([...allOptions, ...inputDef[0]])]; - } + if (inputNames.includes(inputName) && Array.isArray(inputDef[0])) { + allOptions = [...new Set([...allOptions, ...inputDef[0]])]; } } } } - console.log(`[XYZ Plot Controller] Total ${paramType} options found:`, allOptions.length); + // Add default option if specified + if (PARAM_INFO[paramType].addDefault) { + allOptions.unshift(PARAM_INFO[paramType].addDefault); + } + return allOptions; } catch (e) { - console.error(`[XYZ Plot Controller] Error finding options for ${paramType}:`, e); + console.error(`Error fetching ${paramType} options:`, e); return []; } } -function createParameterHelpers() { - return { - model: { - getOptions: async () => { - const options = await findOptionsForParameter('model'); - return options.length > 0 ? options : []; - } - }, - vae: { - getOptions: async () => { - const options = await findOptionsForParameter('vae'); - // Add "Automatic" as first option for VAEs - return ["Automatic", ...options]; - } - }, - lora: { - getOptions: async () => { - const options = await findOptionsForParameter('lora'); - // Add "None" as first option for LoRAs - return ["None", ...options]; - } - }, - sampler: { - getOptions: async () => { - const options = await findOptionsForParameter('sampler'); - return options.length > 0 ? options : - ["euler", "euler_ancestral", "heun", "dpm_2", "dpm_2_ancestral", - "lms", "dpm_fast", "dpm_adaptive", "dpmpp_2s_ancestral", - "dpmpp_sde", "dpmpp_2m", "dpmpp_2m_sde", "ddim", "uni_pc"]; - } - }, - scheduler: { - getOptions: async () => { - const options = await findOptionsForParameter('scheduler'); - return options.length > 0 ? options : - ["normal", "karras", "exponential", "sgm_uniform", "simple", "ddim_uniform"]; - } - } - }; -} - -function setupEnhancedUI(node) { - console.log("[XYZ Plot Controller] setupEnhancedUI called for node:", node); - - // Add custom UI container - const container = document.createElement("div"); - container.style.cssText = ` - padding: 10px; - background: #1e1e1e; - border-radius: 6px; - margin: 10px 5px; - border: 1px solid #333; - `; - - // Add info display - const infoDiv = document.createElement("div"); - infoDiv.id = "xyz-info-display"; - infoDiv.style.cssText = ` - background: #2a2a2a; - padding: 8px; - border-radius: 4px; - margin-bottom: 10px; - font-size: 12px; - color: #ccc; - display: none; - `; - container.appendChild(infoDiv); - - // Add total count display - const countDiv = document.createElement("div"); - countDiv.id = "xyz-count-display"; - countDiv.style.cssText = ` - background: #2a2a2a; - padding: 8px; - border-radius: 4px; - text-align: center; - font-weight: bold; - color: #4ecdc4; - `; - countDiv.textContent = "Total Images: 0"; - container.appendChild(countDiv); - - console.log("[XYZ Plot Controller] Adding DOM widget..."); - - // Add widget - try { - node.addDOMWidget("xyz_ui", "div", container); - console.log("[XYZ Plot Controller] DOM widget added successfully"); - } catch (error) { - console.error("[XYZ Plot Controller] Error adding DOM widget:", error); - } - - // Setup axis handlers - ['x', 'y', 'z'].forEach(axis => { - const typeWidget = node.widgets.find(w => w.name === `${axis}_axis_type`); - const valueWidget = node.widgets.find(w => w.name === `${axis}_values`); - - console.log(`[XYZ Plot Controller] Setting up ${axis} axis - type: ${typeWidget?.value}`); - - if (typeWidget && valueWidget) { - // Initial setup - if (typeWidget.value && typeWidget.value !== 'none') { - updateValueWidget(node, `${axis}_values`, typeWidget.value); - } - - // Force widget callback setup - if (!typeWidget._xyz_initialized) { - typeWidget._xyz_initialized = true; - const originalCallback = typeWidget.callback; - typeWidget.callback = function(value) { - console.log(`[XYZ Plot Controller] ${axis}_axis_type changed to: ${value}`); - updateValueWidget(node, `${axis}_values`, value); - updateImageCount(node); - if (originalCallback) { - originalCallback.call(this, value); - } - }; - } - } - }); - - // Initial count update - updateImageCount(node); -} - -function updateValueWidget(node, widgetName, paramType) { - console.log(`[XYZ Plot Controller] updateValueWidget called: ${widgetName} -> ${paramType}`); - - const widget = node.widgets.find(w => w.name === widgetName); - if (!widget) { - console.error(`[XYZ Plot Controller] Widget ${widgetName} not found`); - return; - } - - // Always remove existing custom elements first - const existingDiv = node.element?.querySelector(`#${widgetName}-custom`); - if (existingDiv) { - console.log(`[XYZ Plot Controller] Removing existing button for ${widgetName}`); - existingDiv.remove(); - } - - const info = parameterInfo[paramType]; - if (!info || paramType === 'none') { - // Hide info display if no parameter selected - const infoDiv = node.element?.querySelector('#xyz-info-display'); - if (infoDiv) { - infoDiv.style.display = 'none'; - } - return; - } - - // Update info display - const infoDiv = node.element?.querySelector('#xyz-info-display'); - if (infoDiv) { - infoDiv.style.display = 'block'; - infoDiv.innerHTML = ` -
- ${paramType.toUpperCase()} -
-
${info.description}
- ${info.example || ''} - `; - } - - // Add select button for options-based parameters - if (info.hasOptions && node.parameterHelpers[paramType]) { - console.log(`[XYZ Plot Controller] Creating select button for ${paramType}`); - - // Create new button - let customDiv = document.createElement("div"); - customDiv.id = `${widgetName}-custom`; - customDiv.style.cssText = ` - margin-top: 5px; - margin-bottom: 15px; - padding-bottom: 10px; - `; - - const selectBtn = document.createElement("button"); - // Better pluralization - const buttonLabels = { - model: "Select Models", - vae: "Select VAEs", - lora: "Select LoRAs", - sampler: "Select Samplers", - scheduler: "Select Schedulers" - }; - selectBtn.textContent = `📁 ${buttonLabels[paramType] || `Select ${paramType}s`}`; - selectBtn.style.cssText = ` - padding: 6px 12px; - background: #4a90e2; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 12px; - width: 100%; - box-shadow: 0 2px 4px rgba(0,0,0,0.2); - transition: all 0.2s; - `; - - selectBtn.onmouseenter = () => { - selectBtn.style.background = "#357abd"; - selectBtn.style.transform = "translateY(-1px)"; - selectBtn.style.boxShadow = "0 3px 6px rgba(0,0,0,0.3)"; - }; - - selectBtn.onmouseleave = () => { - selectBtn.style.background = "#4a90e2"; - selectBtn.style.transform = "translateY(0)"; - selectBtn.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)"; - }; - - selectBtn.onclick = async (e) => { - e.preventDefault(); - e.stopPropagation(); - console.log(`[XYZ Plot Controller] Getting ${paramType} options...`); - const options = await node.parameterHelpers[paramType].getOptions(); - console.log(`[XYZ Plot Controller] Got ${options.length} ${paramType} options:`, options); - showSelectDialog(options, widget, paramType); - }; - - customDiv.appendChild(selectBtn); - - // Insert after widget - const widgetElement = widget.inputEl || widget.element; - console.log(`[XYZ Plot Controller] Widget element for ${widgetName}:`, widgetElement); - console.log(`[XYZ Plot Controller] Widget parent:`, widgetElement?.parentElement); - - if (widgetElement && widgetElement.parentElement) { - widgetElement.parentElement.appendChild(customDiv); - console.log(`[XYZ Plot Controller] Button added for ${paramType}`); - - // Trigger node resize - if (node.computeSize) { - node.computeSize(); - } - node.setDirtyCanvas(true, true); - } else { - console.error(`[XYZ Plot Controller] Could not find widget element for ${widgetName}`); - } - } - - // Add validation for numeric types - if (['cfg_scale', 'steps', 'clip_skip', 'seed', 'denoise', 'flux_guidance'].includes(paramType)) { - if (widget.inputEl) { - widget.inputEl.addEventListener('input', () => { - const value = widget.inputEl.value.trim(); - let isValid = true; - - if (value) { - const rangePattern = /^\d+(\.\d+)?:\d+(\.\d+)?(:\d+(\.\d+)?)?$/; - const listPattern = /^[\d\.\s,]+$/; - isValid = rangePattern.test(value) || listPattern.test(value); - } - - widget.inputEl.style.borderColor = isValid ? "#4ecdc4" : "#ff6b6b"; - }); - } - } -} - -function showSelectDialog(options, widget, paramType) { - console.log(`[XYZ Plot Controller] showSelectDialog called with ${options.length} options for ${paramType}`); - +// Create multi-select dialog +function showMultiSelectDialog(options, currentValues, paramType, onApply) { // Create overlay const overlay = document.createElement("div"); overlay.style.cssText = ` @@ -446,6 +119,7 @@ function showSelectDialog(options, widget, paramType) { border-radius: 8px; padding: 20px; max-width: 500px; + width: 90%; max-height: 70vh; overflow: hidden; display: flex; @@ -455,17 +129,25 @@ function showSelectDialog(options, widget, paramType) { // Title const title = document.createElement("h3"); - const titleLabels = { - model: "Select Models", - vae: "Select VAEs", - lora: "Select LoRAs", - sampler: "Select Samplers", - scheduler: "Select Schedulers" - }; - title.textContent = titleLabels[paramType] || `Select ${paramType} values`; + 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 = ` @@ -478,16 +160,15 @@ function showSelectDialog(options, widget, paramType) { max-height: 400px; `; - // Get current values - const currentValues = widget.value.split(',').map(v => v.trim()).filter(v => v); + const checkboxes = []; + const labels = []; // Create checkboxes - const checkboxes = []; options.forEach(option => { const label = document.createElement("label"); label.style.cssText = ` display: block; - padding: 8px 10px; + padding: 8px; cursor: pointer; color: #ddd; transition: background 0.2s; @@ -509,20 +190,25 @@ function showSelectDialog(options, widget, paramType) { label.appendChild(checkbox); label.appendChild(document.createTextNode(option)); - // Hover effect - label.onmouseenter = () => { - label.style.background = "rgba(78, 205, 196, 0.1)"; - }; - label.onmouseleave = () => { - label.style.background = "transparent"; - }; + 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 = ` @@ -532,6 +218,38 @@ function showSelectDialog(options, widget, paramType) { 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 = ` @@ -558,11 +276,12 @@ function showSelectDialog(options, widget, paramType) { const selected = checkboxes .filter(cb => cb.checked) .map(cb => cb.value); - widget.value = selected.join(", "); - widget.callback?.(widget.value); + onApply(selected); document.body.removeChild(overlay); }; + buttonContainer.appendChild(selectAllBtn); + buttonContainer.appendChild(clearBtn); buttonContainer.appendChild(cancelBtn); buttonContainer.appendChild(applyBtn); dialog.appendChild(buttonContainer); @@ -570,22 +289,280 @@ function showSelectDialog(options, widget, paramType) { overlay.appendChild(dialog); document.body.appendChild(overlay); - console.log("[XYZ Plot Controller] Dialog added to document body"); + // Focus search box + searchBox.focus(); } -function updateImageCount(node) { - const countDiv = node.element?.querySelector('#xyz-count-display'); - if (!countDiv) return; - - let xCount = 1, yCount = 1, zCount = 1; - - ['x', 'y', 'z'].forEach(axis => { - const typeWidget = node.widgets.find(w => w.name === `${axis}_axis_type`); - const valueWidget = node.widgets.find(w => w.name === `${axis}_values`); +// Create the enhanced UI +function createEnhancedUI(node) { + const container = document.createElement("div"); + container.className = "xyz-controller-ui"; + container.innerHTML = ` + - if (typeWidget?.value && typeWidget.value !== 'none' && valueWidget?.value) { - const values = valueWidget.value.trim(); - if (values) { + +
+
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.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.widgets.find(w => w.name === `${axis}_values`); + if (valueWidget) { + valueWidget.value = valuesTextarea.value; + valueWidget.callback?.(valuesTextarea.value); + } + updateTotalCount(); + }); + }); + + valuesTextarea.addEventListener('input', () => { + const valueWidget = 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(':'); @@ -606,21 +583,81 @@ function updateImageCount(node) { 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'; } - }); - - const total = xCount * yCount * zCount; - countDiv.textContent = `Total Images: ${total}`; - - // Update color based on count - if (total > 100) { - countDiv.style.color = "#ff6b6b"; - countDiv.textContent += " ⚠️"; - } else if (total > 50) { - countDiv.style.color = "#ffaa00"; - } else { - countDiv.style.color = "#4ecdc4"; } + + // Initialize handlers + ['x', 'y', 'z'].forEach(axis => setupAxisHandlers(axis)); + + // Sync with existing widget values + function syncFromWidgets() { + ['x', 'y', 'z'].forEach(axis => { + const typeWidget = node.widgets.find(w => w.name === `${axis}_axis_type`); + const valueWidget = node.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; } -console.log("[XYZ Plot Controller] Extension module loaded successfully"); \ No newline at end of file +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); + } + + // Hide all original widgets + this.widgets.forEach(w => { + if (w.element) { + w.element.style.display = 'none'; + } + if (w.inputEl) { + w.inputEl.style.display = 'none'; + } + }); + + // Create custom UI + const ui = createEnhancedUI(this); + this.addDOMWidget("xyz_enhanced_ui", "div", ui, { + serialize: false, + hideOnZoom: false + }); + + // Set node size + this.size = [400, 850]; + }; + } + } +}); \ No newline at end of file diff --git a/web/xyz_plot_controller_old.js b/web/xyz_plot_controller_old.js new file mode 100644 index 0000000..bcfa5b0 --- /dev/null +++ b/web/xyz_plot_controller_old.js @@ -0,0 +1,626 @@ +import { app } from "../../scripts/app.js"; + +console.log("[XYZ Plot Controller] Extension loading..."); + +app.registerExtension({ + name: "ComfyAssets.XYZPlotController", + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + console.log("[XYZ Plot Controller] beforeRegisterNodeDef called for:", nodeData.name); + + if (nodeData.name === "XYZPlotController") { + console.log("[XYZ Plot Controller] Configuring XYZPlotController node..."); + const onNodeCreated = nodeType.prototype.onNodeCreated; + + nodeType.prototype.onNodeCreated = function() { + console.log("[XYZ Plot Controller] onNodeCreated called"); + + if (onNodeCreated) { + onNodeCreated.apply(this, arguments); + } + + // Store reference to node + this.parameterHelpers = createParameterHelpers(); + this.setupComplete = false; + + // Setup UI after a delay to ensure widgets are ready + setTimeout(() => { + console.log("[XYZ Plot Controller] Setting up enhanced UI..."); + if (!this.setupComplete) { + setupEnhancedUI(this); + this.setupComplete = true; + } + }, 100); + }; + + // Handle widget changes + const onWidgetChange = nodeType.prototype.onWidgetChange; + nodeType.prototype.onWidgetChange = function(name, value, oldValue, widget) { + if (onWidgetChange) { + onWidgetChange.call(this, name, value, oldValue, widget); + } + + // Handle axis type changes + if (name.endsWith('_axis_type')) { + const axisPrefix = name.split('_')[0]; + const valueWidgetName = `${axisPrefix}_values`; + console.log(`[XYZ Plot Controller] Axis type changed: ${name} = ${value}`); + updateValueWidget(this, valueWidgetName, value); + } + + // Update total count for any value change + if (name.includes('_values') || name.includes('_axis_type')) { + updateImageCount(this); + } + }; + } + } +}); + +// Parameter information +const parameterInfo = { + none: { description: "No parameter selected" }, + model: { + description: "Checkpoint/Model files", + example: "Click 'Select Models' to choose multiple checkpoints", + hasOptions: true + }, + vae: { + description: "VAE models", + example: "Click 'Select Vaes' to choose VAEs\n'Automatic' uses the model's built-in VAE", + hasOptions: true + }, + lora: { + description: "LoRA models", + example: "Click 'Select Loras' to choose LoRAs\n'None' disables LoRA", + hasOptions: true + }, + sampler: { + description: "Sampling algorithms", + example: "Click 'Select Samplers' to choose sampling methods", + hasOptions: true + }, + scheduler: { + description: "Noise schedulers", + example: "Click 'Select Schedulers' to choose scheduler types", + hasOptions: true + }, + cfg_scale: { + description: "CFG Scale (Classifier-Free Guidance)", + example: "Examples:\n5, 7.5, 10, 12.5\n5:15:2.5 (range from 5 to 15, step 2.5)" + }, + steps: { + description: "Denoising steps", + example: "Examples:\n20, 30, 40, 50\n10:50:10 (range)" + }, + clip_skip: { + description: "CLIP layers to skip", + example: "Common values: 1, 2\nSDXL typically uses 1 or 2" + }, + seed: { + description: "Random seed", + example: "Examples:\n42, 123, 456\n0:1000:100 (range)" + }, + denoise: { + description: "Denoising strength", + example: "Examples:\n0.3, 0.5, 0.7, 1.0\n0.2:1.0:0.2 (range)" + }, + flux_guidance: { + description: "Flux guidance strength", + example: "Examples:\n1.0, 2.0, 3.5, 5.0\n1:5:0.5 (range)" + }, + prompt: { + description: "Text prompts (one per line)", + example: "Enter different prompts, one per line:\n\na beautiful sunset\na mystical forest\na futuristic city" + } +}; + +// Generic function to find options from any node type +async function findOptionsForParameter(paramType) { + try { + console.log(`[XYZ Plot Controller] Finding options for ${paramType}`); + const resp = await fetch('/object_info'); + const data = await resp.json(); + + // Map parameter types to common input names + const parameterMappings = { + model: ['ckpt_name', 'checkpoint', 'model_name', 'unet_name'], + vae: ['vae_name', 'vae'], + lora: ['lora_name', 'lora'], + sampler: ['sampler_name', 'sampler'], + scheduler: ['scheduler', 'scheduler_name'], + // Add more mappings as needed + }; + + const possibleNames = parameterMappings[paramType] || [paramType]; + let allOptions = []; + + // Search through all nodes for matching inputs + for (const [nodeName, nodeData] of Object.entries(data)) { + if (nodeData.input?.required) { + for (const [inputName, inputDef] of Object.entries(nodeData.input.required)) { + // Check if this input name matches what we're looking for + if (possibleNames.includes(inputName)) { + // Check if it's a list (array as first element) + if (Array.isArray(inputDef[0])) { + console.log(`[XYZ Plot Controller] Found ${paramType} options in ${nodeName}.${inputName}`); + allOptions = [...new Set([...allOptions, ...inputDef[0]])]; + } + } + } + } + } + + console.log(`[XYZ Plot Controller] Total ${paramType} options found:`, allOptions.length); + return allOptions; + } catch (e) { + console.error(`[XYZ Plot Controller] Error finding options for ${paramType}:`, e); + return []; + } +} + +function createParameterHelpers() { + return { + model: { + getOptions: async () => { + const options = await findOptionsForParameter('model'); + return options.length > 0 ? options : []; + } + }, + vae: { + getOptions: async () => { + const options = await findOptionsForParameter('vae'); + // Add "Automatic" as first option for VAEs + return ["Automatic", ...options]; + } + }, + lora: { + getOptions: async () => { + const options = await findOptionsForParameter('lora'); + // Add "None" as first option for LoRAs + return ["None", ...options]; + } + }, + sampler: { + getOptions: async () => { + const options = await findOptionsForParameter('sampler'); + return options.length > 0 ? options : + ["euler", "euler_ancestral", "heun", "dpm_2", "dpm_2_ancestral", + "lms", "dpm_fast", "dpm_adaptive", "dpmpp_2s_ancestral", + "dpmpp_sde", "dpmpp_2m", "dpmpp_2m_sde", "ddim", "uni_pc"]; + } + }, + scheduler: { + getOptions: async () => { + const options = await findOptionsForParameter('scheduler'); + return options.length > 0 ? options : + ["normal", "karras", "exponential", "sgm_uniform", "simple", "ddim_uniform"]; + } + } + }; +} + +function setupEnhancedUI(node) { + console.log("[XYZ Plot Controller] setupEnhancedUI called for node:", node); + + // Add custom UI container + const container = document.createElement("div"); + container.style.cssText = ` + padding: 10px; + background: #1e1e1e; + border-radius: 6px; + margin: 10px 5px; + border: 1px solid #333; + `; + + // Add info display + const infoDiv = document.createElement("div"); + infoDiv.id = "xyz-info-display"; + infoDiv.style.cssText = ` + background: #2a2a2a; + padding: 8px; + border-radius: 4px; + margin-bottom: 10px; + font-size: 12px; + color: #ccc; + display: none; + `; + container.appendChild(infoDiv); + + // Add total count display + const countDiv = document.createElement("div"); + countDiv.id = "xyz-count-display"; + countDiv.style.cssText = ` + background: #2a2a2a; + padding: 8px; + border-radius: 4px; + text-align: center; + font-weight: bold; + color: #4ecdc4; + `; + countDiv.textContent = "Total Images: 0"; + container.appendChild(countDiv); + + console.log("[XYZ Plot Controller] Adding DOM widget..."); + + // Add widget + try { + node.addDOMWidget("xyz_ui", "div", container); + console.log("[XYZ Plot Controller] DOM widget added successfully"); + } catch (error) { + console.error("[XYZ Plot Controller] Error adding DOM widget:", error); + } + + // Setup axis handlers + ['x', 'y', 'z'].forEach(axis => { + const typeWidget = node.widgets.find(w => w.name === `${axis}_axis_type`); + const valueWidget = node.widgets.find(w => w.name === `${axis}_values`); + + console.log(`[XYZ Plot Controller] Setting up ${axis} axis - type: ${typeWidget?.value}`); + + if (typeWidget && valueWidget) { + // Initial setup + if (typeWidget.value && typeWidget.value !== 'none') { + updateValueWidget(node, `${axis}_values`, typeWidget.value); + } + + // Force widget callback setup + if (!typeWidget._xyz_initialized) { + typeWidget._xyz_initialized = true; + const originalCallback = typeWidget.callback; + typeWidget.callback = function(value) { + console.log(`[XYZ Plot Controller] ${axis}_axis_type changed to: ${value}`); + updateValueWidget(node, `${axis}_values`, value); + updateImageCount(node); + if (originalCallback) { + originalCallback.call(this, value); + } + }; + } + } + }); + + // Initial count update + updateImageCount(node); +} + +function updateValueWidget(node, widgetName, paramType) { + console.log(`[XYZ Plot Controller] updateValueWidget called: ${widgetName} -> ${paramType}`); + + const widget = node.widgets.find(w => w.name === widgetName); + if (!widget) { + console.error(`[XYZ Plot Controller] Widget ${widgetName} not found`); + return; + } + + // Always remove existing custom elements first + const existingDiv = node.element?.querySelector(`#${widgetName}-custom`); + if (existingDiv) { + console.log(`[XYZ Plot Controller] Removing existing button for ${widgetName}`); + existingDiv.remove(); + } + + const info = parameterInfo[paramType]; + if (!info || paramType === 'none') { + // Hide info display if no parameter selected + const infoDiv = node.element?.querySelector('#xyz-info-display'); + if (infoDiv) { + infoDiv.style.display = 'none'; + } + return; + } + + // Update info display + const infoDiv = node.element?.querySelector('#xyz-info-display'); + if (infoDiv) { + infoDiv.style.display = 'block'; + infoDiv.innerHTML = ` +
+ ${paramType.toUpperCase()} +
+
${info.description}
+ ${info.example || ''} + `; + } + + // Add select button for options-based parameters + if (info.hasOptions && node.parameterHelpers[paramType]) { + console.log(`[XYZ Plot Controller] Creating select button for ${paramType}`); + + // Create new button + let customDiv = document.createElement("div"); + customDiv.id = `${widgetName}-custom`; + customDiv.style.cssText = ` + margin-top: 5px; + margin-bottom: 15px; + padding-bottom: 10px; + `; + + const selectBtn = document.createElement("button"); + // Better pluralization + const buttonLabels = { + model: "Select Models", + vae: "Select VAEs", + lora: "Select LoRAs", + sampler: "Select Samplers", + scheduler: "Select Schedulers" + }; + selectBtn.textContent = `📁 ${buttonLabels[paramType] || `Select ${paramType}s`}`; + selectBtn.style.cssText = ` + padding: 6px 12px; + background: #4a90e2; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + width: 100%; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + transition: all 0.2s; + `; + + selectBtn.onmouseenter = () => { + selectBtn.style.background = "#357abd"; + selectBtn.style.transform = "translateY(-1px)"; + selectBtn.style.boxShadow = "0 3px 6px rgba(0,0,0,0.3)"; + }; + + selectBtn.onmouseleave = () => { + selectBtn.style.background = "#4a90e2"; + selectBtn.style.transform = "translateY(0)"; + selectBtn.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)"; + }; + + selectBtn.onclick = async (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log(`[XYZ Plot Controller] Getting ${paramType} options...`); + const options = await node.parameterHelpers[paramType].getOptions(); + console.log(`[XYZ Plot Controller] Got ${options.length} ${paramType} options:`, options); + showSelectDialog(options, widget, paramType); + }; + + customDiv.appendChild(selectBtn); + + // Insert after widget + const widgetElement = widget.inputEl || widget.element; + console.log(`[XYZ Plot Controller] Widget element for ${widgetName}:`, widgetElement); + console.log(`[XYZ Plot Controller] Widget parent:`, widgetElement?.parentElement); + + if (widgetElement && widgetElement.parentElement) { + widgetElement.parentElement.appendChild(customDiv); + console.log(`[XYZ Plot Controller] Button added for ${paramType}`); + + // Trigger node resize + if (node.computeSize) { + node.computeSize(); + } + node.setDirtyCanvas(true, true); + } else { + console.error(`[XYZ Plot Controller] Could not find widget element for ${widgetName}`); + } + } + + // Add validation for numeric types + if (['cfg_scale', 'steps', 'clip_skip', 'seed', 'denoise', 'flux_guidance'].includes(paramType)) { + if (widget.inputEl) { + widget.inputEl.addEventListener('input', () => { + const value = widget.inputEl.value.trim(); + let isValid = true; + + if (value) { + const rangePattern = /^\d+(\.\d+)?:\d+(\.\d+)?(:\d+(\.\d+)?)?$/; + const listPattern = /^[\d\.\s,]+$/; + isValid = rangePattern.test(value) || listPattern.test(value); + } + + widget.inputEl.style.borderColor = isValid ? "#4ecdc4" : "#ff6b6b"; + }); + } + } +} + +function showSelectDialog(options, widget, paramType) { + console.log(`[XYZ Plot Controller] showSelectDialog called with ${options.length} options for ${paramType}`); + + // 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; + 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"); + const titleLabels = { + model: "Select Models", + vae: "Select VAEs", + lora: "Select LoRAs", + sampler: "Select Samplers", + scheduler: "Select Schedulers" + }; + title.textContent = titleLabels[paramType] || `Select ${paramType} values`; + title.style.cssText = "margin: 0 0 15px 0; color: #4ecdc4; font-size: 16px;"; + dialog.appendChild(title); + + // 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; + `; + + // Get current values + const currentValues = widget.value.split(',').map(v => v.trim()).filter(v => v); + + // Create checkboxes + const checkboxes = []; + options.forEach(option => { + const label = document.createElement("label"); + label.style.cssText = ` + display: block; + padding: 8px 10px; + 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)); + + // Hover effect + label.onmouseenter = () => { + label.style.background = "rgba(78, 205, 196, 0.1)"; + }; + label.onmouseleave = () => { + label.style.background = "transparent"; + }; + + checkboxes.push(checkbox); + optionsContainer.appendChild(label); + }); + + dialog.appendChild(optionsContainer); + + // Buttons + const buttonContainer = document.createElement("div"); + buttonContainer.style.cssText = ` + margin-top: 15px; + display: flex; + gap: 10px; + justify-content: flex-end; + `; + + 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); + widget.value = selected.join(", "); + widget.callback?.(widget.value); + document.body.removeChild(overlay); + }; + + buttonContainer.appendChild(cancelBtn); + buttonContainer.appendChild(applyBtn); + dialog.appendChild(buttonContainer); + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + + console.log("[XYZ Plot Controller] Dialog added to document body"); +} + +function updateImageCount(node) { + const countDiv = node.element?.querySelector('#xyz-count-display'); + if (!countDiv) return; + + let xCount = 1, yCount = 1, zCount = 1; + + ['x', 'y', 'z'].forEach(axis => { + const typeWidget = node.widgets.find(w => w.name === `${axis}_axis_type`); + const valueWidget = node.widgets.find(w => w.name === `${axis}_values`); + + if (typeWidget?.value && typeWidget.value !== 'none' && valueWidget?.value) { + const values = valueWidget.value.trim(); + if (values) { + // 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; + countDiv.textContent = `Total Images: ${total}`; + + // Update color based on count + if (total > 100) { + countDiv.style.color = "#ff6b6b"; + countDiv.textContent += " ⚠️"; + } else if (total > 50) { + countDiv.style.color = "#ffaa00"; + } else { + countDiv.style.color = "#4ecdc4"; + } +} + +console.log("[XYZ Plot Controller] Extension module loaded successfully"); \ No newline at end of file