From 1370b8e8c169ca76bf50697546fa282dec5cf073 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Wed, 30 Jul 2025 15:17:54 +0800 Subject: [PATCH] feat: implement drag-and-drop reordering for LoRA entries and enhance keyboard navigation. Fixes #302 --- web/comfyui/lora_loader.js | 32 +-- web/comfyui/lora_stacker.js | 32 +-- web/comfyui/loras_widget.js | 67 ++++- web/comfyui/loras_widget_components.js | 81 +++++++ web/comfyui/loras_widget_events.js | 322 ++++++++++++++++++++++++- web/comfyui/loras_widget_utils.js | 70 +++++- web/comfyui/utils.js | 43 ++++ web/comfyui/wanvideo_lora_select.js | 33 +-- 8 files changed, 582 insertions(+), 98 deletions(-) diff --git a/web/comfyui/lora_loader.js b/web/comfyui/lora_loader.js index a5e40c41..958f4878 100644 --- a/web/comfyui/lora_loader.js +++ b/web/comfyui/lora_loader.js @@ -4,39 +4,11 @@ import { LORA_PATTERN, collectActiveLorasFromChain, updateConnectedTriggerWords, - chainCallback + chainCallback, + mergeLoras } from "./utils.js"; import { addLorasWidget } from "./loras_widget.js"; -function mergeLoras(lorasText, lorasArr) { - const result = []; - let match; - - // Reset pattern index before using - LORA_PATTERN.lastIndex = 0; - - // Parse text input and create initial entries - while ((match = LORA_PATTERN.exec(lorasText)) !== null) { - const name = match[1]; - const modelStrength = Number(match[2]); - // Extract clip strength if provided, otherwise use model strength - const clipStrength = match[3] ? Number(match[3]) : modelStrength; - - // Find if this lora exists in the array data - const existingLora = lorasArr.find(l => l.name === name); - - result.push({ - name: name, - // Use existing strength if available, otherwise use input strength - strength: existingLora ? existingLora.strength : modelStrength, - active: existingLora ? existingLora.active : true, - clipStrength: existingLora ? existingLora.clipStrength : clipStrength, - }); - } - - return result; -} - app.registerExtension({ name: "LoraManager.LoraLoader", diff --git a/web/comfyui/lora_stacker.js b/web/comfyui/lora_stacker.js index 81d2338e..1309ec66 100644 --- a/web/comfyui/lora_stacker.js +++ b/web/comfyui/lora_stacker.js @@ -4,39 +4,11 @@ import { getActiveLorasFromNode, collectActiveLorasFromChain, updateConnectedTriggerWords, - chainCallback + chainCallback, + mergeLoras } from "./utils.js"; import { addLorasWidget } from "./loras_widget.js"; -function mergeLoras(lorasText, lorasArr) { - const result = []; - let match; - - // Reset pattern index before using - LORA_PATTERN.lastIndex = 0; - - // Parse text input and create initial entries - while ((match = LORA_PATTERN.exec(lorasText)) !== null) { - const name = match[1]; - const modelStrength = Number(match[2]); - // Extract clip strength if provided, otherwise use model strength - const clipStrength = match[3] ? Number(match[3]) : modelStrength; - - // Find if this lora exists in the array data - const existingLora = lorasArr.find(l => l.name === name); - - result.push({ - name: name, - // Use existing strength if available, otherwise use input strength - strength: existingLora ? existingLora.strength : modelStrength, - active: existingLora ? existingLora.active : true, - clipStrength: existingLora ? existingLora.clipStrength : clipStrength, - }); - } - - return result; -} - app.registerExtension({ name: "LoraManager.LoraStacker", diff --git a/web/comfyui/loras_widget.js b/web/comfyui/loras_widget.js index 066e388b..8f317c34 100644 --- a/web/comfyui/loras_widget.js +++ b/web/comfyui/loras_widget.js @@ -1,5 +1,4 @@ -import { app } from "../../scripts/app.js"; -import { createToggle, createArrowButton, PreviewTooltip } from "./loras_widget_components.js"; +import { createToggle, createArrowButton, PreviewTooltip, createDragHandle, updateEntrySelection } from "./loras_widget_components.js"; import { parseLoraValue, formatLoraValue, @@ -11,7 +10,7 @@ import { CONTAINER_PADDING, EMPTY_CONTAINER_HEIGHT } from "./loras_widget_utils.js"; -import { initDrag, createContextMenu, initHeaderDrag } from "./loras_widget_events.js"; +import { initDrag, createContextMenu, initHeaderDrag, initReorderDrag, handleKeyboardNavigation } from "./loras_widget_events.js"; export function addLorasWidget(node, name, opts, callback) { // Create container for loras @@ -42,6 +41,30 @@ export function addLorasWidget(node, name, opts, callback) { // Create preview tooltip instance const previewTooltip = new PreviewTooltip(); + // Selection state - only one LoRA can be selected at a time + let selectedLora = null; + + // Function to select a LoRA + const selectLora = (loraName) => { + selectedLora = loraName; + // Update visual feedback for all entries + container.querySelectorAll('.comfy-lora-entry').forEach(entry => { + const entryLoraName = entry.dataset.loraName; + updateEntrySelection(entry, entryLoraName === selectedLora); + }); + }; + + // Add keyboard event listener to container + container.addEventListener('keydown', (e) => { + if (handleKeyboardNavigation(e, selectedLora, widget, renderLoras, selectLora)) { + e.stopPropagation(); + } + }); + + // Make container focusable for keyboard events + container.tabIndex = 0; + container.style.outline = 'none'; + // Function to render loras from data const renderLoras = (value, widget) => { // Clear existing content @@ -185,6 +208,26 @@ export function addLorasWidget(node, name, opts, callback) { marginBottom: "4px", }); + // Store lora name and active state in dataset for selection + loraEl.dataset.loraName = name; + loraEl.dataset.active = active; + + // Add click handler for selection + loraEl.addEventListener('click', (e) => { + // Skip if clicking on interactive elements + if (e.target.closest('.comfy-lora-toggle') || + e.target.closest('input') || + e.target.closest('.comfy-lora-arrow') || + e.target.closest('.comfy-lora-drag-handle')) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + selectLora(name); + container.focus(); // Focus container for keyboard events + }); + // Add double-click handler to toggle clip entry loraEl.addEventListener('dblclick', (e) => { // Skip if clicking on toggle or strength control areas @@ -220,6 +263,12 @@ export function addLorasWidget(node, name, opts, callback) { } }); + // Create drag handle for reordering + const dragHandle = createDragHandle(); + + // Initialize reorder drag functionality + initReorderDrag(dragHandle, name, widget, renderLoras); + // Create toggle for this lora const toggle = createToggle(active, (newActive) => { // Update this lora's active state @@ -416,6 +465,7 @@ export function addLorasWidget(node, name, opts, callback) { minWidth: "0", // Allow shrinking }); + leftSection.appendChild(dragHandle); // Add drag handle first leftSection.appendChild(toggle); leftSection.appendChild(nameEl); @@ -424,6 +474,9 @@ export function addLorasWidget(node, name, opts, callback) { container.appendChild(loraEl); + // Update selection state + updateEntrySelection(loraEl, name === selectedLora); + // If expanded, show the clip entry if (isExpanded) { totalVisibleEntries++; @@ -444,6 +497,10 @@ export function addLorasWidget(node, name, opts, callback) { marginTop: "-2px" }); + // Store the same lora name in clip entry dataset + clipEl.dataset.loraName = name; + clipEl.dataset.active = active; + // Create clip name display const clipNameEl = document.createElement("div"); clipNameEl.textContent = "[clip] " + name; @@ -601,7 +658,7 @@ export function addLorasWidget(node, name, opts, callback) { }); // Calculate height based on number of loras and fixed sizes - const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (Math.min(totalVisibleEntries, 10) * LORA_ENTRY_HEIGHT); + const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (Math.min(totalVisibleEntries, 12) * LORA_ENTRY_HEIGHT); updateWidgetHeight(container, calculatedHeight, defaultHeight, node); }; @@ -685,6 +742,8 @@ export function addLorasWidget(node, name, opts, callback) { widget.onRemove = () => { container.remove(); previewTooltip.cleanup(); + // Remove keyboard event listener + container.removeEventListener('keydown', handleKeyboardNavigation); }; return { minWidth: 400, minHeight: defaultHeight, widget }; diff --git a/web/comfyui/loras_widget_components.js b/web/comfyui/loras_widget_components.js index 4a4448e0..f2a18bf7 100644 --- a/web/comfyui/loras_widget_components.js +++ b/web/comfyui/loras_widget_components.js @@ -78,6 +78,87 @@ export function createArrowButton(direction, onClick) { return button; } +// Function to create drag handle +export function createDragHandle() { + const handle = document.createElement("div"); + handle.className = "comfy-lora-drag-handle"; + handle.innerHTML = "≡"; + handle.title = "Drag to reorder LoRA"; + + Object.assign(handle.style, { + width: "16px", + height: "16px", + display: "flex", + alignItems: "center", + justifyContent: "center", + cursor: "grab", + userSelect: "none", + fontSize: "14px", + color: "rgba(226, 232, 240, 0.6)", + transition: "all 0.2s ease", + marginRight: "8px", + flexShrink: "0" + }); + + // Add hover effect + handle.onmouseenter = () => { + handle.style.color = "rgba(226, 232, 240, 0.9)"; + handle.style.transform = "scale(1.1)"; + }; + + handle.onmouseleave = () => { + handle.style.color = "rgba(226, 232, 240, 0.6)"; + handle.style.transform = "scale(1)"; + }; + + // Change cursor when dragging + handle.onmousedown = () => { + handle.style.cursor = "grabbing"; + }; + + return handle; +} + +// Function to create drop indicator +export function createDropIndicator() { + const indicator = document.createElement("div"); + indicator.className = "comfy-lora-drop-indicator"; + + Object.assign(indicator.style, { + position: "absolute", + left: "0", + right: "0", + height: "3px", + backgroundColor: "rgba(66, 153, 225, 0.9)", + borderRadius: "2px", + opacity: "0", + transition: "opacity 0.2s ease", + boxShadow: "0 0 6px rgba(66, 153, 225, 0.8)", + zIndex: "10", + pointerEvents: "none" + }); + + return indicator; +} + +// Function to update entry selection state +export function updateEntrySelection(entryEl, isSelected) { + const baseColor = entryEl.dataset.active === 'true' ? + "rgba(45, 55, 72, 0.7)" : "rgba(35, 40, 50, 0.5)"; + const selectedColor = entryEl.dataset.active === 'true' ? + "rgba(66, 153, 225, 0.3)" : "rgba(66, 153, 225, 0.2)"; + + if (isSelected) { + entryEl.style.backgroundColor = selectedColor; + entryEl.style.border = "1px solid rgba(66, 153, 225, 0.6)"; + entryEl.style.boxShadow = "0 0 0 1px rgba(66, 153, 225, 0.3)"; + } else { + entryEl.style.backgroundColor = baseColor; + entryEl.style.border = "1px solid transparent"; + entryEl.style.boxShadow = "none"; + } +} + // Function to create menu item export function createMenuItem(text, icon, onClick) { const menuItem = document.createElement('div'); diff --git a/web/comfyui/loras_widget_events.js b/web/comfyui/loras_widget_events.js index 0c1e56b4..e03fa08f 100644 --- a/web/comfyui/loras_widget_events.js +++ b/web/comfyui/loras_widget_events.js @@ -1,6 +1,7 @@ import { api } from "../../scripts/api.js"; -import { createMenuItem } from "./loras_widget_components.js"; -import { parseLoraValue, formatLoraValue, syncClipStrengthIfCollapsed, saveRecipeDirectly, copyToClipboard, showToast } from "./loras_widget_utils.js"; +import { app } from "../../scripts/app.js"; +import { createMenuItem, createDropIndicator } from "./loras_widget_components.js"; +import { parseLoraValue, formatLoraValue, syncClipStrengthIfCollapsed, saveRecipeDirectly, copyToClipboard, showToast, moveLoraByDirection, getDropTargetIndex } from "./loras_widget_utils.js"; // Function to handle strength adjustment via dragging export function handleStrengthDrag(name, initialStrength, initialX, event, widget, isClipStrength = false) { @@ -227,6 +228,223 @@ export function initHeaderDrag(headerEl, widget, renderFunction) { }); } +// Function to initialize drag-and-drop for reordering +export function initReorderDrag(dragHandle, loraName, widget, renderFunction) { + let isDragging = false; + let draggedElement = null; + let dropIndicator = null; + let container = null; + let scale = 1; + + dragHandle.addEventListener('mousedown', (e) => { + e.preventDefault(); + e.stopPropagation(); + + isDragging = true; + draggedElement = dragHandle.closest('.comfy-lora-entry'); + container = draggedElement.parentElement; + + // Add dragging class and visual feedback + draggedElement.classList.add('comfy-lora-dragging'); + draggedElement.style.opacity = '0.5'; + draggedElement.style.transform = 'scale(0.98)'; + + // Create single drop indicator with absolute positioning + dropIndicator = createDropIndicator(); + + // Make container relatively positioned for absolute indicator + const originalPosition = container.style.position; + container.style.position = 'relative'; + container.appendChild(dropIndicator); + + // Store original position for cleanup + container._originalPosition = originalPosition; + + // Add global cursor style + document.body.style.cursor = 'grabbing'; + + // Store workflow scale for accurate positioning + scale = app.canvas.ds.scale; + }); + + document.addEventListener('mousemove', (e) => { + if (!isDragging || !draggedElement || !dropIndicator) return; + + const targetIndex = getDropTargetIndex(container, e.clientY); + const entries = container.querySelectorAll('.comfy-lora-entry, .comfy-lora-clip-entry'); + + if (targetIndex === 0) { + // Show at top + const firstEntry = entries[0]; + if (firstEntry) { + const rect = firstEntry.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + dropIndicator.style.top = `${(rect.top - containerRect.top - 2) / scale}px`; + dropIndicator.style.opacity = '1'; + } + } else if (targetIndex < entries.length) { + // Show between entries + const targetEntry = entries[targetIndex]; + if (targetEntry) { + const rect = targetEntry.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + dropIndicator.style.top = `${(rect.top - containerRect.top - 2) / scale}px`; + dropIndicator.style.opacity = '1'; + } + } else { + // Show at bottom + const lastEntry = entries[entries.length - 1]; + if (lastEntry) { + const rect = lastEntry.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + dropIndicator.style.top = `${(rect.bottom - containerRect.top + 2) / scale}px`; + dropIndicator.style.opacity = '1'; + } + } + }); + + document.addEventListener('mouseup', (e) => { + if (!isDragging || !draggedElement) return; + + const targetIndex = getDropTargetIndex(container, e.clientY); + + // Get current LoRA data + const lorasData = parseLoraValue(widget.value); + const currentIndex = lorasData.findIndex(l => l.name === loraName); + + if (currentIndex !== -1 && currentIndex !== targetIndex) { + // Calculate actual target index (excluding clip entries from count) + const loraEntries = container.querySelectorAll('.comfy-lora-entry'); + let actualTargetIndex = targetIndex; + + // Adjust target index if it's beyond the number of actual LoRA entries + if (actualTargetIndex > loraEntries.length) { + actualTargetIndex = loraEntries.length; + } + + // Move the LoRA + const newLoras = [...lorasData]; + const [moved] = newLoras.splice(currentIndex, 1); + newLoras.splice(actualTargetIndex > currentIndex ? actualTargetIndex - 1 : actualTargetIndex, 0, moved); + + widget.value = formatLoraValue(newLoras); + + if (widget.callback) { + widget.callback(widget.value); + } + + // Re-render + if (renderFunction) { + renderFunction(widget.value, widget); + } + } + + // Cleanup + isDragging = false; + if (draggedElement) { + draggedElement.classList.remove('comfy-lora-dragging'); + draggedElement.style.opacity = ''; + draggedElement.style.transform = ''; + draggedElement = null; + } + + if (dropIndicator && container) { + container.removeChild(dropIndicator); + // Restore original position + container.style.position = container._originalPosition || ''; + delete container._originalPosition; + dropIndicator = null; + } + + // Reset cursor + document.body.style.cursor = ''; + container = null; + }); +} + +// Function to handle keyboard navigation +export function handleKeyboardNavigation(event, selectedLora, widget, renderFunction, selectLora) { + if (!selectedLora) return false; + + const lorasData = parseLoraValue(widget.value); + let handled = false; + + // Check for Ctrl/Cmd modifier for reordering + if (event.ctrlKey || event.metaKey) { + switch (event.key) { + case 'ArrowUp': + event.preventDefault(); + const newLorasUp = moveLoraByDirection(lorasData, selectedLora, 'up'); + widget.value = formatLoraValue(newLorasUp); + if (widget.callback) widget.callback(widget.value); + if (renderFunction) renderFunction(widget.value, widget); + handled = true; + break; + + case 'ArrowDown': + event.preventDefault(); + const newLorasDown = moveLoraByDirection(lorasData, selectedLora, 'down'); + widget.value = formatLoraValue(newLorasDown); + if (widget.callback) widget.callback(widget.value); + if (renderFunction) renderFunction(widget.value, widget); + handled = true; + break; + + case 'Home': + event.preventDefault(); + const newLorasTop = moveLoraByDirection(lorasData, selectedLora, 'top'); + widget.value = formatLoraValue(newLorasTop); + if (widget.callback) widget.callback(widget.value); + if (renderFunction) renderFunction(widget.value, widget); + handled = true; + break; + + case 'End': + event.preventDefault(); + const newLorasBottom = moveLoraByDirection(lorasData, selectedLora, 'bottom'); + widget.value = formatLoraValue(newLorasBottom); + if (widget.callback) widget.callback(widget.value); + if (renderFunction) renderFunction(widget.value, widget); + handled = true; + break; + } + } else { + // Normal navigation without Ctrl/Cmd + switch (event.key) { + case 'ArrowUp': + event.preventDefault(); + const currentIndex = lorasData.findIndex(l => l.name === selectedLora); + if (currentIndex > 0) { + selectLora(lorasData[currentIndex - 1].name); + } + handled = true; + break; + + case 'ArrowDown': + event.preventDefault(); + const currentIndexDown = lorasData.findIndex(l => l.name === selectedLora); + if (currentIndexDown < lorasData.length - 1) { + selectLora(lorasData[currentIndexDown + 1].name); + } + handled = true; + break; + + case 'Delete': + case 'Backspace': + event.preventDefault(); + const filtered = lorasData.filter(l => l.name !== selectedLora); + widget.value = formatLoraValue(filtered); + if (widget.callback) widget.callback(widget.value); + if (renderFunction) renderFunction(widget.value, widget); + selectLora(null); // Clear selection + handled = true; + break; + } + } + + return handled; +} + // Function to create context menu export function createContextMenu(x, y, loraName, widget, previewTooltip, renderFunction) { // Hide preview tooltip first @@ -398,6 +616,94 @@ export function createContextMenu(x, y, loraName, widget, previewTooltip, render } ); + // Move Up option with arrow up icon + const moveUpOption = createMenuItem( + 'Move Up', + '', + () => { + menu.remove(); + document.removeEventListener('click', closeMenu); + + const lorasData = parseLoraValue(widget.value); + const newLoras = moveLoraByDirection(lorasData, loraName, 'up'); + widget.value = formatLoraValue(newLoras); + + if (widget.callback) { + widget.callback(widget.value); + } + + if (renderFunction) { + renderFunction(widget.value, widget); + } + } + ); + + // Move Down option with arrow down icon + const moveDownOption = createMenuItem( + 'Move Down', + '', + () => { + menu.remove(); + document.removeEventListener('click', closeMenu); + + const lorasData = parseLoraValue(widget.value); + const newLoras = moveLoraByDirection(lorasData, loraName, 'down'); + widget.value = formatLoraValue(newLoras); + + if (widget.callback) { + widget.callback(widget.value); + } + + if (renderFunction) { + renderFunction(widget.value, widget); + } + } + ); + + // Move to Top option with chevrons up icon + const moveTopOption = createMenuItem( + 'Move to Top', + '', + () => { + menu.remove(); + document.removeEventListener('click', closeMenu); + + const lorasData = parseLoraValue(widget.value); + const newLoras = moveLoraByDirection(lorasData, loraName, 'top'); + widget.value = formatLoraValue(newLoras); + + if (widget.callback) { + widget.callback(widget.value); + } + + if (renderFunction) { + renderFunction(widget.value, widget); + } + } + ); + + // Move to Bottom option with chevrons down icon + const moveBottomOption = createMenuItem( + 'Move to Bottom', + '', + () => { + menu.remove(); + document.removeEventListener('click', closeMenu); + + const lorasData = parseLoraValue(widget.value); + const newLoras = moveLoraByDirection(lorasData, loraName, 'bottom'); + widget.value = formatLoraValue(newLoras); + + if (widget.callback) { + widget.callback(widget.value); + } + + if (renderFunction) { + renderFunction(widget.value, widget); + } + } + ); + // Add separator const separator1 = document.createElement('div'); Object.assign(separator1.style, { @@ -412,9 +718,21 @@ export function createContextMenu(x, y, loraName, widget, previewTooltip, render borderTop: '1px solid rgba(255, 255, 255, 0.1)', }); + // Add separator for order options + const orderSeparator = document.createElement('div'); + Object.assign(orderSeparator.style, { + margin: '4px 0', + borderTop: '1px solid rgba(255, 255, 255, 0.1)', + }); + menu.appendChild(viewOnCivitaiOption); menu.appendChild(deleteOption); menu.appendChild(separator1); + menu.appendChild(moveUpOption); + menu.appendChild(moveDownOption); + menu.appendChild(moveTopOption); + menu.appendChild(moveBottomOption); + menu.appendChild(orderSeparator); menu.appendChild(copyNotesOption); menu.appendChild(copyTriggerWordsOption); menu.appendChild(separator2); diff --git a/web/comfyui/loras_widget_utils.js b/web/comfyui/loras_widget_utils.js index d5dc30df..1b4193b3 100644 --- a/web/comfyui/loras_widget_utils.js +++ b/web/comfyui/loras_widget_utils.js @@ -3,7 +3,7 @@ import { app } from "../../scripts/app.js"; // Fixed sizes for component calculations export const LORA_ENTRY_HEIGHT = 40; // Height of a single lora entry export const CLIP_ENTRY_HEIGHT = 40; // Height of a clip entry -export const HEADER_HEIGHT = 40; // Height of the header section +export const HEADER_HEIGHT = 32; // Height of the header section export const CONTAINER_PADDING = 12; // Top and bottom padding export const EMPTY_CONTAINER_HEIGHT = 100; // Height when no loras are present @@ -164,3 +164,71 @@ export function showToast(message, type = 'info') { } } } + +/** + * Move a LoRA to a new position in the array + * @param {Array} loras - Array of LoRA objects + * @param {number} fromIndex - Current index of the LoRA + * @param {number} toIndex - Target index for the LoRA + * @returns {Array} - New array with LoRA moved + */ +export function moveLoraInArray(loras, fromIndex, toIndex) { + const newLoras = [...loras]; + const [removed] = newLoras.splice(fromIndex, 1); + newLoras.splice(toIndex, 0, removed); + return newLoras; +} + +/** + * Move a LoRA by name to a specific position + * @param {Array} loras - Array of LoRA objects + * @param {string} loraName - Name of the LoRA to move + * @param {string} direction - 'up', 'down', 'top', 'bottom' + * @returns {Array} - New array with LoRA moved + */ +export function moveLoraByDirection(loras, loraName, direction) { + const currentIndex = loras.findIndex(l => l.name === loraName); + if (currentIndex === -1) return loras; + + let newIndex; + switch (direction) { + case 'up': + newIndex = Math.max(0, currentIndex - 1); + break; + case 'down': + newIndex = Math.min(loras.length - 1, currentIndex + 1); + break; + case 'top': + newIndex = 0; + break; + case 'bottom': + newIndex = loras.length - 1; + break; + default: + return loras; + } + + if (newIndex === currentIndex) return loras; + return moveLoraInArray(loras, currentIndex, newIndex); +} + +/** + * Get the drop target index based on mouse position + * @param {HTMLElement} container - The container element + * @param {number} clientY - Mouse Y position + * @returns {number} - Target index for dropping + */ +export function getDropTargetIndex(container, clientY) { + const entries = container.querySelectorAll('.comfy-lora-entry'); + let targetIndex = entries.length; + + for (let i = 0; i < entries.length; i++) { + const rect = entries[i].getBoundingClientRect(); + if (clientY < rect.top + rect.height / 2) { + targetIndex = i; + break; + } + } + + return targetIndex; +} diff --git a/web/comfyui/utils.js b/web/comfyui/utils.js index 5acf395a..8d34ea5d 100644 --- a/web/comfyui/utils.js +++ b/web/comfyui/utils.js @@ -183,4 +183,47 @@ export function updateConnectedTriggerWords(node, loraNames) { }) }).catch(err => console.error("Error fetching trigger words:", err)); } +} + +export function mergeLoras(lorasText, lorasArr) { + // Parse lorasText into a map: name -> {strength, clipStrength} + const parsedLoras = {}; + let match; + LORA_PATTERN.lastIndex = 0; + while ((match = LORA_PATTERN.exec(lorasText)) !== null) { + const name = match[1]; + const modelStrength = Number(match[2]); + const clipStrength = match[3] ? Number(match[3]) : modelStrength; + parsedLoras[name] = { strength: modelStrength, clipStrength }; + } + + // Build result array in the order of lorasArr + const result = []; + const usedNames = new Set(); + + for (const lora of lorasArr) { + if (parsedLoras[lora.name]) { + result.push({ + name: lora.name, + strength: lora.strength !== undefined ? lora.strength : parsedLoras[lora.name].strength, + active: lora.active !== undefined ? lora.active : true, + clipStrength: lora.clipStrength !== undefined ? lora.clipStrength : parsedLoras[lora.name].clipStrength, + }); + usedNames.add(lora.name); + } + } + + // Add any new loras from lorasText that are not in lorasArr, in their text order + for (const name in parsedLoras) { + if (!usedNames.has(name)) { + result.push({ + name, + strength: parsedLoras[name].strength, + active: true, + clipStrength: parsedLoras[name].clipStrength, + }); + } + } + + return result; } \ No newline at end of file diff --git a/web/comfyui/wanvideo_lora_select.js b/web/comfyui/wanvideo_lora_select.js index 144530de..9712d9cf 100644 --- a/web/comfyui/wanvideo_lora_select.js +++ b/web/comfyui/wanvideo_lora_select.js @@ -2,41 +2,12 @@ import { app } from "../../scripts/app.js"; import { LORA_PATTERN, getActiveLorasFromNode, - collectActiveLorasFromChain, updateConnectedTriggerWords, - chainCallback + chainCallback, + mergeLoras } from "./utils.js"; import { addLorasWidget } from "./loras_widget.js"; -function mergeLoras(lorasText, lorasArr) { - const result = []; - let match; - - // Reset pattern index before using - LORA_PATTERN.lastIndex = 0; - - // Parse text input and create initial entries - while ((match = LORA_PATTERN.exec(lorasText)) !== null) { - const name = match[1]; - const modelStrength = Number(match[2]); - // Extract clip strength if provided, otherwise use model strength - const clipStrength = match[3] ? Number(match[3]) : modelStrength; - - // Find if this lora exists in the array data - const existingLora = lorasArr.find(l => l.name === name); - - result.push({ - name: name, - // Use existing strength if available, otherwise use input strength - strength: existingLora ? existingLora.strength : modelStrength, - active: existingLora ? existingLora.active : true, - clipStrength: existingLora ? existingLora.clipStrength : clipStrength, - }); - } - - return result; -} - app.registerExtension({ name: "LoraManager.WanVideoLoraSelect",