From 53db06206dec5b76083ef53b774c9bf1b4ab90a0 Mon Sep 17 00:00:00 2001 From: Raykosan <71210053+Raykosan@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:00:44 +0300 Subject: [PATCH] Added image previews to the RS Load Image node when using the INPUT button --- web/spline_mask.js | 171 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 168 insertions(+), 3 deletions(-) diff --git a/web/spline_mask.js b/web/spline_mask.js index 0e7d803..8ea9c88 100644 --- a/web/spline_mask.js +++ b/web/spline_mask.js @@ -181,14 +181,153 @@ app.registerExtension({ app.canvas.canvas.addEventListener('dragover', handleCanvasDragOver, { capture: true }); app.canvas.canvas.addEventListener('drop', handleCanvasDrop, { capture: true }); + // Переменные для превью + let _previewContainer = null; + let _previewImg = null; + let _previewTimeout = null; + let _previewCache = {}; + + const createPreviewContainer = () => { + if (_previewContainer) return; + + _previewContainer = document.createElement("div"); + _previewContainer.className = 'spline-image-preview'; + _previewContainer.style.cssText = ` + position: fixed; + width: 200px; + height: 200px; + background: #1a1a1a; + border: 1px solid #444; + border-radius: 6px; + z-index: 10002; + display: none; + align-items: center; + justify-content: center; + box-shadow: 0 4px 20px rgba(0,0,0,0.5); + opacity: 0; + transition: opacity 0.2s ease-in-out; + `; + + _previewImg = document.createElement("img"); + _previewImg.style.cssText = ` + max-width: 100%; + max-height: 100%; + object-fit: contain; + `; + + _previewContainer.appendChild(_previewImg); + document.body.appendChild(_previewContainer); + }; + + const showPreview = (imgPath, targetElement) => { + if (!_previewContainer) createPreviewContainer(); + + // Проверяем кэш + if (_previewCache[imgPath]) { + _previewImg.src = _previewCache[imgPath]; + positionPreview(targetElement); + _previewContainer.style.display = "flex"; + setTimeout(() => { + _previewContainer.style.opacity = "1"; + }, 10); + return; + } + + // Загружаем изображение + const img = new Image(); + + // Пробуем с параметрами размера + let filename = imgPath; + let subfolder = ""; + if (imgPath.includes("/")) { + const parts = imgPath.split("/"); + subfolder = parts[0]; + filename = parts.slice(1).join("/"); + } + + let imgUrl = `/view?filename=${encodeURIComponent(filename)}&type=input&width=200&height=200`; + if (subfolder) imgUrl += `&subfolder=${encodeURIComponent(subfolder)}`; + imgUrl += `&t=${Date.now()}`; + + img.onload = () => { + _previewCache[imgPath] = imgUrl; + _previewImg.src = imgUrl; + positionPreview(targetElement); + _previewContainer.style.display = "flex"; + setTimeout(() => { + _previewContainer.style.opacity = "1"; + }, 10); + }; + + img.onerror = () => { + // Fallback: пробуем без параметров размера + let fallbackUrl = `/view?filename=${encodeURIComponent(filename)}&type=input`; + if (subfolder) fallbackUrl += `&subfolder=${encodeURIComponent(subfolder)}`; + fallbackUrl += `&t=${Date.now()}`; + + const fallbackImg = new Image(); + fallbackImg.onload = () => { + _previewCache[imgPath] = fallbackUrl; + _previewImg.src = fallbackUrl; + positionPreview(targetElement); + _previewContainer.style.display = "flex"; + setTimeout(() => { + _previewContainer.style.opacity = "1"; + }, 10); + }; + fallbackImg.onerror = () => { + console.error("[SPLINE 🦊] Failed to load preview for:", imgPath); + }; + fallbackImg.src = fallbackUrl; + }; + + img.src = imgUrl; + }; + + const positionPreview = (targetElement) => { + if (!_previewContainer || !targetElement) return; + + const rect = targetElement.getBoundingClientRect(); + const previewWidth = 200; + const gap = 10; + + // Пробуем справа + let left = rect.right + gap; + let top = rect.top + (rect.height / 2) - 100; // Центрируем по вертикали + + // Проверяем, хватает ли места справа + if (left + previewWidth > window.innerWidth) { + // Если нет - показываем слева + left = rect.left - previewWidth - gap; + } + + // Проверяем границы экрана по вертикали + if (top < 0) top = 0; + if (top + 200 > window.innerHeight) top = window.innerHeight - 200; + + _previewContainer.style.left = left + "px"; + _previewContainer.style.top = top + "px"; + }; + + const hidePreview = () => { + if (!_previewContainer) return; + _previewContainer.style.opacity = "0"; + setTimeout(() => { + if (_previewContainer) { + _previewContainer.style.display = "none"; + } + }, 200); + }; + node.showImageSelector = function() { const self = this; - // Удаляем старое меню, если есть + // Удаляем старое меню и превью, если есть const existingMenu = document.querySelector('.spline-image-menu'); if (existingMenu) { existingMenu.remove(); } + hidePreview(); fetch("/rayko/spline/images") .then(response => response.json()) @@ -201,22 +340,24 @@ app.registerExtension({ const menu = document.createElement("div"); menu.className = 'spline-image-menu'; + // Убрали max-height и overflow-y отсюда, добавили flex menu.style.cssText = ` position: fixed; background: #1a1a1a; border: 1px solid #444; border-radius: 6px; - max-height: 300px; - overflow-y: auto; z-index: 10001; box-shadow: 0 4px 20px rgba(0,0,0,0.5); min-width: 200px; + display: flex; + flex-direction: column; `; // Header с кнопкой закрытия const header = document.createElement("div"); header.style.cssText = ` height: 28px; + flex-shrink: 0; /* Чтобы header не сжимался */ display: flex; justify-content: center; align-items: center; @@ -242,12 +383,19 @@ app.registerExtension({ // Обработчик клика на header header.onclick = (e) => { e.stopPropagation(); + if (_previewTimeout) clearTimeout(_previewTimeout); + hidePreview(); menu.remove(); }; // Content area для элементов списка const content = document.createElement("div"); content.className = 'menu-content'; + // Перенесли max-height и overflow-y сюда + content.style.cssText = ` + max-height: 300px; + overflow-y: auto; + `; self.imageList.forEach(imgPath => { const item = document.createElement("div"); @@ -264,8 +412,23 @@ app.registerExtension({ item.onmouseover = () => item.style.background = "#444"; item.onmouseout = () => item.style.background = imgPath === self.data.selected_image ? '#333' : "#1a1a1a"; + // Hover для превью с debounce + item.onmouseenter = () => { + if (_previewTimeout) clearTimeout(_previewTimeout); + _previewTimeout = setTimeout(() => { + showPreview(imgPath, item); + }, 250); + }; + + item.onmouseleave = () => { + if (_previewTimeout) clearTimeout(_previewTimeout); + hidePreview(); + }; + item.onclick = (e) => { e.stopPropagation(); + if (_previewTimeout) clearTimeout(_previewTimeout); + hidePreview(); self.loadImage(imgPath); menu.remove(); }; @@ -690,8 +853,10 @@ app.registerExtension({ _animationId = null; } if (_overlayCanvas) _overlayCanvas.remove(); + if (_previewContainer) _previewContainer.remove(); const menu = document.querySelector('.spline-image-menu'); if (menu) menu.remove(); + if (_previewTimeout) clearTimeout(_previewTimeout); app.canvas.canvas.removeEventListener('dragover', handleCanvasDragOver, { capture: true }); app.canvas.canvas.removeEventListener('drop', handleCanvasDrop, { capture: true });