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 = ` - - - -