diff --git a/kikotools/tools/xyz_grid/__init__.py b/kikotools/tools/xyz_grid/__init__.py index b028f64..2a8807d 100644 --- a/kikotools/tools/xyz_grid/__init__.py +++ b/kikotools/tools/xyz_grid/__init__.py @@ -1,10 +1,10 @@ """XYZ Grid nodes for ComfyUI parameter comparisons.""" -from .controller.advanced_node import XYZPlotControllerAdvanced +from .controller.simple_node import XYZPlotController from .combiner.node import ImageGridCombiner NODE_CLASS_MAPPINGS = { - "XYZPlotController": XYZPlotControllerAdvanced, + "XYZPlotController": XYZPlotController, "ImageGridCombiner": ImageGridCombiner, } @@ -13,4 +13,4 @@ NODE_DISPLAY_NAME_MAPPINGS = { "ImageGridCombiner": "Image Grid Combiner", } -__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "XYZPlotControllerAdvanced", "ImageGridCombiner"] \ No newline at end of file +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "XYZPlotController", "ImageGridCombiner"] \ No newline at end of file diff --git a/kikotools/tools/xyz_grid/controller/simple_node.py b/kikotools/tools/xyz_grid/controller/simple_node.py new file mode 100644 index 0000000..58de5b8 --- /dev/null +++ b/kikotools/tools/xyz_grid/controller/simple_node.py @@ -0,0 +1,218 @@ +"""Simplified XYZ Plot Controller using native ComfyUI widgets.""" + +from typing import Dict, List, Any, Tuple +import json + +from ..utils.constants import AxisType +from ..utils.helpers import ( + get_available_models, get_available_vaes, get_available_loras, + get_sampler_names, get_scheduler_names, parse_value_string, + create_unique_id +) + + +class XYZPlotController: + """Simplified XYZ Plot Controller with native widgets.""" + + @classmethod + def INPUT_TYPES(cls): + # For file-based parameters, we'll use a special format in the values field + axis_types = [ + "none", + "model", + "vae", + "lora", + "sampler", + "scheduler", + "cfg_scale", + "steps", + "seed", + "denoise", + "clip_skip", + "prompt" + ] + + return { + "required": { + # X Axis + "x_type": (axis_types, {"default": "none"}), + "x_values": ("STRING", { + "default": "", + "multiline": True, + "placeholder": "Enter values separated by commas or use start:stop:step notation" + }), + + # Y Axis + "y_type": (axis_types, {"default": "none"}), + "y_values": ("STRING", { + "default": "", + "multiline": True, + "placeholder": "Enter values separated by commas or use start:stop:step notation" + }), + + # Z Axis (optional) + "z_type": (axis_types, {"default": "none"}), + "z_values": ("STRING", { + "default": "", + "multiline": True, + "placeholder": "Enter values separated by commas or use start:stop:step notation" + }), + + # Control + "auto_queue": ("BOOLEAN", {"default": True}), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + } + } + + RETURN_TYPES = ("XYZ_GRID", "STRING", "INT", "FLOAT", "STRING", "INT", "FLOAT", "STRING", "INT", "FLOAT", "STRING") + RETURN_NAMES = ("grid_data", "x_string", "x_int", "x_float", "y_string", "y_int", "y_float", "z_string", "z_int", "z_float", "batch_id") + OUTPUT_NODE = True + FUNCTION = "create_grid" + CATEGORY = "ComfyAssets/XYZ Grid" + + def create_grid(self, x_type, x_values, y_type, y_values, z_type, z_values, auto_queue, unique_id=None): + """Create grid configuration.""" + + # Parse values for each axis + x_parsed = self._parse_axis_values(x_type, x_values) if x_type != "none" else [] + y_parsed = self._parse_axis_values(y_type, y_values) if y_type != "none" else [] + z_parsed = self._parse_axis_values(z_type, z_values) if z_type != "none" else [] + + # Calculate total combinations + x_count = max(1, len(x_parsed)) + y_count = max(1, len(y_parsed)) + z_count = max(1, len(z_parsed)) + total_images = x_count * y_count * z_count + + # Generate batch ID + batch_id = create_unique_id() + + # Create grid data + grid_data = { + "batch_id": batch_id, + "x_axis": { + "type": x_type, + "values": x_parsed, + "count": x_count + }, + "y_axis": { + "type": y_type, + "values": y_parsed, + "count": y_count + }, + "z_axis": { + "type": z_type, + "values": z_parsed, + "count": z_count + }, + "total_images": total_images, + "current_index": 0, + "auto_queue": auto_queue + } + + # Get current values for outputs + x_current = x_parsed[0] if x_parsed else self._get_default_value(x_type) + y_current = y_parsed[0] if y_parsed else self._get_default_value(y_type) + z_current = z_parsed[0] if z_parsed else self._get_default_value(z_type) + + # Convert to appropriate output types + x_str, x_int, x_float = self._convert_value(x_type, x_current) + y_str, y_int, y_float = self._convert_value(y_type, y_current) + z_str, z_int, z_float = self._convert_value(z_type, z_current) + + # Store grid data for execution + if hasattr(self, '_grids'): + self._grids[batch_id] = grid_data + else: + self._grids = {batch_id: grid_data} + + # Log grid info + print(f"\n[XYZ Grid] Created grid with {total_images} total combinations:") + if x_type != "none": + print(f" X axis ({x_type}): {x_count} values") + if y_type != "none": + print(f" Y axis ({y_type}): {y_count} values") + if z_type != "none": + print(f" Z axis ({z_type}): {z_count} values") + + return (grid_data, x_str, x_int, x_float, y_str, y_int, y_float, z_str, z_int, z_float, batch_id) + + def _parse_axis_values(self, axis_type: str, values_str: str) -> List[Any]: + """Parse axis values based on type.""" + if not values_str.strip(): + return [] + + # Handle range notation (start:stop:step) + if ":" in values_str and axis_type in ["cfg_scale", "steps", "seed", "denoise", "clip_skip"]: + try: + parts = values_str.split(":") + if len(parts) == 2: + start, stop = float(parts[0]), float(parts[1]) + step = 1.0 + elif len(parts) == 3: + start, stop, step = float(parts[0]), float(parts[1]), float(parts[2]) + else: + raise ValueError("Invalid range format") + + # Generate values + values = [] + current = start + while current <= stop: + if axis_type in ["steps", "seed", "clip_skip"]: + values.append(int(current)) + else: + values.append(round(current, 2)) + current += step + return values + except: + pass + + # Parse comma-separated values + if axis_type == "prompt": + # For prompts, split by newline instead of comma + return [v.strip() for v in values_str.split("\n") if v.strip()] + else: + # For everything else, split by comma + values = [v.strip() for v in values_str.split(",") if v.strip()] + + # Convert numeric types + if axis_type in ["cfg_scale", "denoise"]: + return [float(v) for v in values] + elif axis_type in ["steps", "seed", "clip_skip"]: + return [int(v) for v in values] + else: + return values + + def _get_default_value(self, axis_type: str) -> Any: + """Get default value for axis type.""" + defaults = { + "model": "", + "vae": "Automatic", + "lora": "None", + "sampler": "euler", + "scheduler": "normal", + "cfg_scale": 7.0, + "steps": 20, + "seed": 0, + "denoise": 1.0, + "clip_skip": 1, + "prompt": "" + } + return defaults.get(axis_type, "") + + def _convert_value(self, axis_type: str, value: Any) -> Tuple[str, int, float]: + """Convert value to all output types.""" + if axis_type in ["model", "vae", "lora", "sampler", "scheduler", "prompt"]: + return (str(value), 0, 0.0) + elif axis_type in ["steps", "seed", "clip_skip"]: + return ("", int(value), float(value)) + elif axis_type in ["cfg_scale", "denoise"]: + return ("", 0, float(value)) + else: + return ("", 0, 0.0) + + +# For backward compatibility +XYZPlotControllerAdvanced = XYZPlotController \ No newline at end of file diff --git a/web/xyz_plot_controller.js b/web/xyz_plot_controller.js deleted file mode 100644 index 02035ed..0000000 --- a/web/xyz_plot_controller.js +++ /dev/null @@ -1,732 +0,0 @@ -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: 360px; - 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 reference to original widgets but hide them visually - this.originalWidgets = this.widgets; - - // Hide all original widgets visually but keep them in the array - this.widgets.forEach(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'; - } - } - }); - - // 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 = () => [360, 620]; - customWidget.size = customWidget.computeSize(); - - // Ensure the element has proper height constraint - if (customWidget.element) { - customWidget.element.style.width = '360px'; - customWidget.element.style.height = '620px'; - customWidget.element.style.maxHeight = '620px'; - customWidget.element.style.overflow = 'hidden'; - } - } - - // Add our custom widget to the widgets array - this.widgets.push(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); - } - }; - } - } -}); \ No newline at end of file diff --git a/web/xyz_plot_helper.js b/web/xyz_plot_helper.js new file mode 100644 index 0000000..30f36b3 --- /dev/null +++ b/web/xyz_plot_helper.js @@ -0,0 +1,114 @@ +import { app } from "../../scripts/app.js"; + +// Helper information for each parameter type +const PARAM_HELP = { + model: "Enter model names separated by commas:\nmodel1.safetensors, model2.safetensors", + vae: "Enter VAE names separated by commas:\nAutomatic, vae1.safetensors, vae2.pt", + lora: "Enter LoRA names separated by commas:\nNone, lora1.safetensors, lora2.safetensors", + sampler: "Enter sampler names separated by commas:\neuler, euler_ancestral, dpm_2", + scheduler: "Enter scheduler names separated by commas:\nnormal, karras, exponential", + cfg_scale: "Enter values separated by commas: 5, 7.5, 10\nOr use range: 5:15:2.5", + steps: "Enter values separated by commas: 20, 30, 40\nOr use range: 10:50:10", + seed: "Enter values separated by commas: 42, 123, 456\nOr use range: 0:1000:100", + denoise: "Enter values separated by commas: 0.3, 0.5, 0.7\nOr use range: 0.2:1.0:0.2", + clip_skip: "Enter values separated by commas: 1, 2\nCommon values for SDXL", + prompt: "Enter prompts separated by new lines:\nbeautiful sunset\nmystical forest\nfuturistic city" +}; + +app.registerExtension({ + name: "ComfyAssets.XYZPlotHelper", + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "XYZPlotController") { + const onNodeCreated = nodeType.prototype.onNodeCreated; + + nodeType.prototype.onNodeCreated = function() { + if (onNodeCreated) { + onNodeCreated.apply(this, arguments); + } + + // Add widget callbacks to update help text + const updateHelp = (axisPrefix) => { + const typeWidget = this.widgets.find(w => w.name === `${axisPrefix}_type`); + const valuesWidget = this.widgets.find(w => w.name === `${axisPrefix}_values`); + + if (typeWidget && valuesWidget) { + // Store original callback + const originalCallback = typeWidget.callback; + + // Add our callback + typeWidget.callback = function(value) { + // Update placeholder with help text + if (PARAM_HELP[value]) { + valuesWidget.inputEl.placeholder = PARAM_HELP[value]; + valuesWidget.inputEl.title = PARAM_HELP[value]; + } else { + valuesWidget.inputEl.placeholder = "No parameter selected"; + valuesWidget.inputEl.title = ""; + } + + // Call original callback + if (originalCallback) { + originalCallback.call(this, value); + } + }; + + // Trigger initial update + typeWidget.callback(typeWidget.value); + } + }; + + // Setup help for all axes + updateHelp('x'); + updateHelp('y'); + updateHelp('z'); + + // Add a title to show total combinations + const originalOnConnectionsChange = this.onConnectionsChange; + this.onConnectionsChange = function(type, index, connected, link_info) { + if (originalOnConnectionsChange) { + originalOnConnectionsChange.call(this, type, index, connected, link_info); + } + + // Update title with total count + setTimeout(() => { + let totalImages = 1; + + ['x', 'y', 'z'].forEach(axis => { + const typeWidget = this.widgets.find(w => w.name === `${axis}_type`); + const valuesWidget = this.widgets.find(w => w.name === `${axis}_values`); + + if (typeWidget && valuesWidget && typeWidget.value !== 'none') { + const values = valuesWidget.value.trim(); + if (values) { + // Count values + let count = 1; + if (values.includes(':')) { + // Range notation + 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; + count = Math.floor((stop - start) / step) + 1; + } + } else if (typeWidget.value === 'prompt') { + count = values.split('\n').filter(v => v.trim()).length; + } else { + count = values.split(',').filter(v => v.trim()).length; + } + totalImages *= count; + } + } + }); + + this.title = `XYZ Plot Controller (${totalImages} images)`; + }, 100); + }; + + // Set default title + this.title = "XYZ Plot Controller (1 image)"; + }; + } + } +}); \ No newline at end of file