diff --git a/package.json b/package.json index 7db23ed..a91f878 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.17.6", + "version": "0.17.7", "description": "Set of custom nodes for ComfyUI providing additional image processing capabilities", "author": "Raykosan", "license": "MIT", diff --git a/pyproject.toml b/pyproject.toml index 21a67f7..40ee9a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI_RaykoStudio" description = "Set of custom nodes for ComfyUI providing additional image processing capabilities" -version = "0.17.6" +version = "0.17.7" license = { file = "LICENSE" } dependencies = ["torch>=1.7.0", "numpy>=1.19.0", "Pillow>=8.0.0", "freetype-py>=2.5.1"] diff --git a/web/spline_mask.js b/web/spline_mask.js index 7abaf87..8ee421e 100644 --- a/web/spline_mask.js +++ b/web/spline_mask.js @@ -8,636 +8,610 @@ app.registerExtension({ if (nodeData.name !== "RaykoSplineMask") return; const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + const node = this; + + // Инициализация данных (как в Styles Loader) + node.data = { + selected_image: "", + spline_coords: "[]" + }; + + // Настройка размеров + node.targetWidth = 450; + node.setSize([node.targetWidth, 600]); + + // Находим скрытые виджеты + const imageWidget = node.widgets?.find(w => w.name === "image"); + const coordsWidget = node.widgets?.find(w => w.name === "coordinates"); + + // Скрываем виджеты (как в Styles Loader) + if (imageWidget) { + imageWidget.hidden = true; + if (imageWidget.element) { + imageWidget.element.style.display = "none"; + } + } + if (coordsWidget) { + coordsWidget.hidden = true; + if (coordsWidget.element) { + coordsWidget.element.style.display = "none"; + } + } + + // Загружаем сохраненные данные из виджетов + if (imageWidget && imageWidget.value) { + node.data.selected_image = imageWidget.value; + } + if (coordsWidget && coordsWidget.value) { + node.data.spline_coords = coordsWidget.value; + } + + // Переопределяем serializeValue для виджетов (как в Styles Loader) + if (imageWidget) { + imageWidget.serializeValue = () => { + return node.data.selected_image; + }; + } + if (coordsWidget) { + coordsWidget.serializeValue = () => { + return node.data.spline_coords; + }; + } + + // Состояние + node.image = new Image(); + node.imageLoaded = false; + node.imageLoading = false; + node.buttons = []; + node.imageList = []; + + // Кнопки + node.buttons = [ + { label: "🎨 IMAGE", color: "#2196F3", callback: () => node.showImageSelector(), hover: false }, + { label: "🖼️ UPLOAD", color: "#4CAF50", callback: () => node.triggerFileUpload(), hover: false }, + { label: "🔴 CLEAR", color: "#dc3545", callback: () => node.clearPoints(), hover: false } + ]; + + let _points = []; try { - onNodeCreated?.apply(this, arguments); - const node = this; - - node.image = new Image(); - node.imageReady = false; - node.currentStatus = "Ready"; - node.buttons = []; - node.imageList = []; - node.selectedImage = ""; - - const coordsWidget = node.widgets?.find(w => w.name === "coordinates"); + const savedPoints = JSON.parse(node.data.spline_coords); + if (Array.isArray(savedPoints)) _points = savedPoints; + } catch (e) {} + + let _overlayCanvas = null; + let _syncRunning = false; + let _lastRect = null; + + // Функция синхронизации (как в Styles Loader) + const syncData = () => { + node.data.spline_coords = JSON.stringify(_points); if (coordsWidget) { - coordsWidget.hidden = true; - coordsWidget.serializeValue = () => { - return node.properties?.spline_coords || "[]"; - }; + coordsWidget.value = node.data.spline_coords; } + // Отмечаем изменение графа + if (node.graph) { + node.graph.changeTracker?.dispatchEvent(new Event("change")); + } + drawOverlay(); + node.setDirtyCanvas(true, true); + }; + + // Загрузка изображения + node.loadImage = function(imagePath) { + if (!imagePath || node.imageLoading) return; - const imageWidget = node.widgets?.find(w => w.name === "image"); + node.imageLoading = true; + node.data.selected_image = imagePath; if (imageWidget) { - imageWidget.hidden = true; - if (node.properties?.selected_image) { - imageWidget.value = node.properties.selected_image; + imageWidget.value = imagePath; + } + + let filename = imagePath; + let subfolder = ""; + if (imagePath.includes("/")) { + const parts = imagePath.split("/"); + subfolder = parts[0]; + filename = parts.slice(1).join("/"); + } + + let imgUrl = `/view?filename=${encodeURIComponent(filename)}&type=input`; + if (subfolder) imgUrl += `&subfolder=${encodeURIComponent(subfolder)}`; + imgUrl += `&t=${Date.now()}`; + + const img = new Image(); + img.onload = () => { + node.image = img; + node.imageLoaded = true; + node.imageLoading = false; + if (_overlayCanvas) _overlayCanvas.style.display = "block"; + _lastRect = null; + syncPosition(); + node.setDirtyCanvas(true, true); + + // Отмечаем изменение графа + if (node.graph) { + node.graph.changeTracker?.dispatchEvent(new Event("change")); } - } - - node.padding = 10; - node.targetWidth = 450; - node.buttonPositions = []; - - node.buttons = [ - { label: "🎨 IMAGE", color: "#2196F3", callback: () => node.showImageSelector(), hover: false }, - { label: "🖼️ UPLOAD IMAGE", color: "#4CAF50", callback: () => node.triggerFileUpload(), hover: false }, - { label: "🔴 CLEAR POINTS", color: "#dc3545", callback: () => node.clearPoints(), hover: false } - ]; - - node.setSize([node.targetWidth, 600]); - - let _points = []; - let _overlayCanvas = null; - let _syncRunning = false; - let _lastRect = null; - - if (node.properties?.spline_coords && node.properties.spline_coords !== "[]") { - try { - const p = JSON.parse(node.properties.spline_coords); - if (Array.isArray(p)) _points = p; - } catch (e) {} - } - - if (node.properties?.selected_image && node.properties.selected_image !== "") { - node.loadImage(node.properties.selected_image); - } - - const updateCoords = () => { - const jsonStr = JSON.stringify(_points); - node.properties = node.properties || {}; - node.properties.spline_coords = jsonStr; - if (coordsWidget) coordsWidget.value = jsonStr; - drawOverlay(); + }; + img.onerror = () => { + node.imageLoaded = false; + node.imageLoading = false; node.setDirtyCanvas(true, true); }; - - node.loadImage = function(imagePath) { - if (!imagePath || imagePath === "") { - console.log("[SPLINE 🦊] No image path provided"); - return; - } - - console.log("[SPLINE 🦊] Loading image: ", imagePath); - - node.selectedImage = imagePath; - node.properties = node.properties || {}; - node.properties.selected_image = imagePath; - - if (imageWidget) { - imageWidget.value = imagePath; - console.log("[SPLINE 🦊] Widget value set to: ", imagePath); - } - - let filename = imagePath; - let subfolder = ""; - - if (imagePath.includes("/")) { - const parts = imagePath.split("/"); - subfolder = parts[0]; - filename = parts.slice(1).join("/"); - } - - let imgUrl = `/view?filename=${encodeURIComponent(filename)}&type=input`; - if (subfolder && subfolder !== "") { - imgUrl += `&subfolder=${encodeURIComponent(subfolder)}`; - } - - console.log("[SPLINE 🦊] Image URL: ", imgUrl); - - node.image.src = imgUrl + "&t=" + Date.now(); - node.image.onload = () => { - console.log("[SPLINE 🦊] Image loaded successfully: ", node.image.width, "x", node.image.height); - node.imageReady = true; - if (_overlayCanvas) _overlayCanvas.style.display = "block"; - _lastRect = null; - syncPosition(); - node.setDirtyCanvas(true, true); - }; - node.image.onerror = (err) => { - console.error("[SPLINE 🦊] Image load error: ", err); - console.error("[SPLINE 🦊] Failed URL: ", imgUrl); - node.imageReady = false; - node.setDirtyCanvas(true, true); - }; - node.currentStatus = "🎨 Draw mask on image"; - }; + img.src = imgUrl; + }; + + // ========== DnD: Функция загрузки из файла (переиспользуем логику UPLOAD) ========== + const uploadFileAndLoad = async (file) => { + if (!file || !file.type.startsWith('image/')) { + console.log("[SPLINE 🦊] Not an image file"); + return false; + } - node.showImageSelector = function() { - const self = this; - - const existingMenu = document.querySelector('.spline-image-menu'); - if (existingMenu) existingMenu.remove(); - - fetch("/rayko/spline/images") - .then(response => response.json()) - .then(data => { - self.imageList = data.images || []; - console.log("[SPLINE 🦊] Found images: ", self.imageList.length); - - if (!self.imageList.length) { - const menu = document.createElement("div"); - menu.className = 'spline-image-menu'; - menu.textContent = "No images found in input folder!"; - menu.style.cssText = ` - position: fixed; - background: #1a1a1a; - border: 1px solid #444; - border-radius: 6px; - padding: 15px; - z-index: 10001; - box-shadow: 0 4px 20px rgba(0,0,0,0.5); - color: #888; - `; - self.positionMenu(menu, 0); - document.body.appendChild(menu); - setTimeout(() => menu.remove(), 2000); - return; - } - - const menu = document.createElement("div"); - menu.className = 'spline-image-menu'; - 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; - `; - - self.imageList.forEach(imgPath => { - const item = document.createElement("div"); - const displayName = imgPath.split('/').pop(); - item.textContent = displayName; - item.title = imgPath; - item.style.cssText = ` - padding: 10px 15px; - cursor: pointer; - color: #ddd; - font-size: 12px; - border-bottom: 1px solid #333; - `; - if (imgPath === self.selectedImage) { - item.style.background = "#2a4a2a"; - item.style.color = "#4CAF50"; - } - item.onmouseover = () => { - if (imgPath !== self.selectedImage) { - item.style.background = "#333"; - } - }; - item.onmouseout = () => { - if (imgPath !== self.selectedImage) { - item.style.background = "#1a1a1a"; - } - }; - item.onclick = (e) => { - e.stopPropagation(); - e.preventDefault(); - console.log("[SPLINE 🦊] Selected image: ", imgPath); - self.loadImage(imgPath); - menu.remove(); - }; - menu.appendChild(item); - }); - - self.positionMenu(menu, 0); - document.body.appendChild(menu); - - const closeHandler = (e) => { - if (!menu.contains(e.target)) { - menu.remove(); - document.removeEventListener("mousedown", closeHandler); - } - }; - setTimeout(() => { - document.addEventListener("mousedown", closeHandler); - }, 100); - }) - .catch(err => { - console.error("[SPLINE 🦊] Error fetching images: ", err); - alert("Error loading image list!"); - }); - }; + const formData = new FormData(); + formData.append('image', file); + formData.append('subfolder', ''); + formData.append('type', 'input'); - node.positionMenu = function(menu, buttonIndex) { - const canvasEl = app.canvas?.canvas || document.querySelector("canvas"); - if (!canvasEl || !this.pos) { - menu.style.left = "250px"; - menu.style.top = "200px"; - return; + try { + const response = await fetch('/upload/image', { method: 'POST', body: formData }); + if (response.ok) { + const result = await response.json(); + const imageName = result.name || result.filename; + const subfolder = result.subfolder || ''; + const finalName = subfolder ? `${subfolder}/${imageName}` : imageName; + node.loadImage(finalName); + return true; } - - const canvasRect = canvasEl.getBoundingClientRect(); - const ds = app.canvas.ds; - - const btnY = this.size[1] - 45; - const btnW = (this.size[0] - 50) / 3; - const btnX = 15 + (buttonIndex * (btnW + 5)); - - const nodeScreenX = canvasRect.left + ((this.pos[0] + ds.offset[0]) * ds.scale); - const nodeScreenY = canvasRect.top + ((this.pos[1] + ds.offset[1]) * ds.scale); - - const menuX = nodeScreenX + btnX; - const menuY = nodeScreenY + btnY + 30; - - menu.style.left = menuX + "px"; - menu.style.top = menuY + "px"; - }; + } catch (err) { + console.error("[SPLINE 🦊] Upload error:", err); + } + return false; + }; + // ========== DnD: конец ========== + + // Остальные методы + node.showImageSelector = function() { + const self = this; + const existingMenu = document.querySelector('.spline-image-menu'); + if (existingMenu) existingMenu.remove(); - node.triggerFileUpload = function() { - const self = this; - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = 'image/*'; - fileInput.style.display = 'none'; - document.body.appendChild(fileInput); - - fileInput.addEventListener('change', async (e) => { - const file = e.target.files[0]; - if (!file) { - fileInput.remove(); + fetch("/rayko/spline/images") + .then(response => response.json()) + .then(data => { + self.imageList = data.images || []; + if (!self.imageList.length) { + alert("No images found!"); return; } - console.log("[SPLINE 🦊] Uploading file: ", file.name); + const menu = document.createElement("div"); + menu.className = 'spline-image-menu'; + 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; + `; - const formData = new FormData(); - formData.append('image', file); - formData.append('subfolder', ''); - formData.append('type', 'input'); + self.imageList.forEach(imgPath => { + const item = document.createElement("div"); + const displayName = imgPath.split('/').pop(); + item.textContent = displayName; + item.style.cssText = ` + padding: 10px 15px; + cursor: pointer; + color: #ddd; + font-size: 12px; + border-bottom: 1px solid #333; + background: ${imgPath === self.data.selected_image ? '#333' : '#1a1a1a'}; + `; + item.onmouseover = () => item.style.background = "#444"; + item.onmouseout = () => item.style.background = imgPath === self.data.selected_image ? '#333' : "#1a1a1a"; + item.onclick = (e) => { + e.stopPropagation(); + self.loadImage(imgPath); + menu.remove(); + }; + menu.appendChild(item); + }); - try { - const response = await fetch('/upload/image', { - method: 'POST', - body: formData - }); - - if (response.ok) { - const result = await response.json(); - console.log("[SPLINE 🦊] Upload result: ", result); - - const imageName = result.name || result.filename; - const subfolder = result.subfolder || ''; - - let finalName = imageName; - if (subfolder && subfolder !== '') { - finalName = `${subfolder}/${imageName}`; - } - - if (finalName) { - console.log("[SPLINE 🦊] Loading uploaded image: ", finalName); - self.loadImage(finalName); - } - } else { - const errText = await response.text(); - console.error("[SPLINE 🦊] Upload failed: ", errText); - alert("Upload failed: " + errText); - } - } catch (err) { - console.error("[SPLINE 🦊] Upload error: ", err); - alert("Upload error: " + err.message); - } finally { - fileInput.remove(); - } - }); - - fileInput.click(); - }; + self.positionMenu(menu, 0); + document.body.appendChild(menu); + + const closeHandler = (e) => { + if (!menu.contains(e.target)) menu.remove(); + }; + setTimeout(() => document.addEventListener("mousedown", closeHandler), 100); + }) + .catch(err => console.error(err)); + }; + + node.positionMenu = function(menu, buttonIndex) { + const canvasEl = app.canvas?.canvas; + if (!canvasEl || !this.pos) { + menu.style.left = "250px"; + menu.style.top = "200px"; + return; + } - node.clearPoints = function() { - _points = []; - updateCoords(); - }; + const canvasRect = canvasEl.getBoundingClientRect(); + const ds = app.canvas.ds; + const btnY = this.size[1] - 45; + const btnW = (this.size[0] - 50) / 3; + const btnX = 15 + (buttonIndex * (btnW + 5)); - const calculateImageRect = () => { - if (!app.canvas) return null; - - const ds = app.canvas.ds; - const canvasEl = app.canvas.canvas; - const scale = ds.scale; - - const graphX = node.pos[0]; - const graphY = node.pos[1]; - - const canvasRect = canvasEl.getBoundingClientRect(); - const nodeScreenX = canvasRect.left + ((graphX + ds.offset[0]) * scale); - const nodeScreenY = canvasRect.top + ((graphY + ds.offset[1]) * scale); - - let widgetsTotalHeight = 0; - if (node.widgets) { - for (const w of node.widgets) { - if (!w.hidden && w.type !== "button") { - widgetsTotalHeight += (w.computeSize ? w.computeSize(node.size[0])[1] : 20); - } - } + const nodeScreenX = canvasRect.left + ((this.pos[0] + ds.offset[0]) * ds.scale); + const nodeScreenY = canvasRect.top + ((this.pos[1] + ds.offset[1]) * ds.scale); + + menu.style.left = (nodeScreenX + btnX) + "px"; + menu.style.top = (nodeScreenY + btnY + 30) + "px"; + }; + + node.triggerFileUpload = function() { + const self = this; + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = 'image/*'; + fileInput.onchange = async (e) => { + const file = e.target.files[0]; + if (!file) return; + await uploadFileAndLoad(file); + fileInput.remove(); + }; + fileInput.click(); + }; + + node.clearPoints = function() { + _points = []; + syncData(); + }; + + // Расчет позиции изображения + const calculateImageRect = () => { + if (!app.canvas) return null; + + const ds = app.canvas.ds; + const scale = ds.scale; + const graphX = node.pos[0]; + const graphY = node.pos[1]; + const canvasRect = app.canvas.canvas.getBoundingClientRect(); + + const nodeScreenX = canvasRect.left + ((graphX + ds.offset[0]) * scale); + const nodeScreenY = canvasRect.top + ((graphY + ds.offset[1]) * scale); + + let widgetsHeight = 0; + if (node.widgets) { + for (const w of node.widgets) { + if (!w.hidden) widgetsHeight += 20; } - - const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30; - const padding = 10; - const footerHeight = 50; - - const availableHeight = (node.size[1] * scale) - (titleBarHeight * scale) - (widgetsTotalHeight * scale) - (footerHeight * scale) - (padding * 2 * scale); - const availableWidth = (node.size[0] * scale) - (padding * 2 * scale); - - if (availableWidth <= 0 || availableHeight <= 0) return null; - - let drawW = availableWidth; - let drawH = availableHeight; - let contentScale = 1; - - if (node.imageReady && node.image) { - const imgRatio = node.image.width / node.image.height; - - if (availableWidth / availableHeight > imgRatio) { - drawH = availableHeight; - drawW = drawH * imgRatio; - } else { - drawW = availableWidth; - drawH = drawW / imgRatio; - } - - contentScale = drawW / node.image.width; - } - - const drawX = nodeScreenX + (padding * scale) + ((availableWidth - drawW) / 2); - const drawY = nodeScreenY + (titleBarHeight * scale) + (widgetsTotalHeight * scale) + (padding * scale) + ((availableHeight - drawH) / 2); - - return { - left: drawX, - top: drawY, - width: drawW, - height: drawH, - scale: contentScale - }; - }; + } - const startSyncLoop = () => { - if (_syncRunning) return; - _syncRunning = true; - const syncLoop = () => { - if (!_syncRunning) return; - syncPosition(); - if (_overlayCanvas) requestAnimationFrame(syncLoop); - else _syncRunning = false; - }; - requestAnimationFrame(syncLoop); - }; - - const syncPosition = () => { - if (!_overlayCanvas) return; - const imgRect = calculateImageRect(); - if (!imgRect) return; - - const hasChanged = !_lastRect || - Math.abs(_lastRect.left - imgRect.left) > 0.5 || - Math.abs(_lastRect.top - imgRect.top) > 0.5 || - Math.abs(_lastRect.width - imgRect.width) > 0.5 || - Math.abs(_lastRect.height - imgRect.height) > 0.5; - - if (hasChanged) { - _lastRect = { ...imgRect }; - _overlayCanvas.style.left = `${imgRect.left}px`; - _overlayCanvas.style.top = `${imgRect.top}px`; - _overlayCanvas.style.width = `${imgRect.width}px`; - _overlayCanvas.style.height = `${imgRect.height}px`; - _overlayCanvas.dataset.scale = imgRect.scale; - drawOverlay(); - } - }; + const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30; + const padding = 10; + const footerHeight = 50; - const createOverlayCanvas = () => { - if (_overlayCanvas) return; - _overlayCanvas = document.createElement("canvas"); - _overlayCanvas.style.cssText = ` - position: fixed !important; z-index: 1001 !important; - pointer-events: auto !important; cursor: crosshair !important; - background: transparent !important; touch-action: none; - border: 1px dashed #00FF00 !important; box-sizing: border-box !important; - display: none; - `; - _overlayCanvas.dataset.nodeType = "RaykoSplineMask"; - document.body.appendChild(_overlayCanvas); - - _overlayCanvas.addEventListener("mousedown", (e) => { - e.preventDefault(); - e.stopPropagation(); - const rect = _overlayCanvas.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - const scale = parseFloat(_overlayCanvas.dataset.scale || "1"); - const imgX = x / scale; - const imgY = y / scale; - - if (e.button === 2 || e.ctrlKey) { - let removed = false; - for (let i = _points.length - 1; i >= 0; i--) { - const dist = Math.hypot(_points[i].x - imgX, _points[i].y - imgY); - if (dist < 15 / scale) { - _points.splice(i, 1); - removed = true; - break; - } - } - if (removed) updateCoords(); - } else if (e.button === 0) { - if (_points.length >= 3) { - const distToFirst = Math.hypot(_points[0].x - imgX, _points[0].y - imgY); - if (distToFirst < 20 / scale) { - updateCoords(); - return; - } - } - _points.push({ x: imgX, y: imgY }); - updateCoords(); - } - }); - startSyncLoop(); - }; - - const drawOverlay = () => { - if (!_overlayCanvas || !_lastRect) return; - const width = parseFloat(_overlayCanvas.style.width || "0"); - const height = parseFloat(_overlayCanvas.style.height || "0"); - if (width <= 0 || height <= 0) return; - - const dpr = window.devicePixelRatio || 1; - _overlayCanvas.width = width * dpr; - _overlayCanvas.height = height * dpr; - const ctx = _overlayCanvas.getContext("2d"); - ctx.scale(dpr, dpr); - ctx.clearRect(0, 0, width, height); - - const scale = parseFloat(_overlayCanvas.dataset.scale || "1"); - if (_points.length >= 1) { + const availableHeight = (node.size[1] * scale) - (titleBarHeight * scale) - (widgetsHeight * scale) - (footerHeight * scale) - (padding * 2 * scale); + const availableWidth = (node.size[0] * scale) - (padding * 2 * scale); + + if (availableWidth <= 0 || availableHeight <= 0) return null; + + let drawW = availableWidth; + let drawH = availableHeight; + let contentScale = 1; + + if (node.imageLoaded && node.image) { + const imgRatio = node.image.width / node.image.height; + if (availableWidth / availableHeight > imgRatio) { + drawH = availableHeight; + drawW = drawH * imgRatio; + } else { + drawW = availableWidth; + drawH = drawW / imgRatio; + } + contentScale = drawW / node.image.width; + } + + const drawX = nodeScreenX + (padding * scale) + ((availableWidth - drawW) / 2); + const drawY = nodeScreenY + (titleBarHeight * scale) + (widgetsHeight * scale) + (padding * scale) + ((availableHeight - drawH) / 2); + + return { left: drawX, top: drawY, width: drawW, height: drawH, scale: contentScale }; + }; + + const syncPosition = () => { + if (!_overlayCanvas || !node.imageLoaded) return; + const imgRect = calculateImageRect(); + if (!imgRect) return; + + const hasChanged = !_lastRect || + Math.abs(_lastRect.left - imgRect.left) > 0.5 || + Math.abs(_lastRect.top - imgRect.top) > 0.5; + + if (hasChanged) { + _lastRect = { ...imgRect }; + _overlayCanvas.style.left = `${imgRect.left}px`; + _overlayCanvas.style.top = `${imgRect.top}px`; + _overlayCanvas.style.width = `${imgRect.width}px`; + _overlayCanvas.style.height = `${imgRect.height}px`; + _overlayCanvas.dataset.scale = imgRect.scale; + drawOverlay(); + } + }; + + const drawOverlay = () => { + if (!_overlayCanvas || !_lastRect || !node.imageLoaded) return; + const width = parseFloat(_overlayCanvas.style.width); + const height = parseFloat(_overlayCanvas.style.height); + if (width <= 0 || height <= 0) return; + + const dpr = window.devicePixelRatio || 1; + _overlayCanvas.width = width * dpr; + _overlayCanvas.height = height * dpr; + const ctx = _overlayCanvas.getContext("2d"); + ctx.scale(dpr, dpr); + ctx.clearRect(0, 0, width, height); + + const scale = parseFloat(_overlayCanvas.dataset.scale || "1"); + if (_points.length >= 1) { + ctx.beginPath(); + ctx.moveTo(_points[0].x * scale, _points[0].y * scale); + for (let i = 1; i < _points.length; i++) { + ctx.lineTo(_points[i].x * scale, _points[i].y * scale); + } + if (_points.length >= 3) { + ctx.closePath(); + ctx.fillStyle = "rgba(0, 255, 0, 0.3)"; + ctx.fill(); + } + ctx.strokeStyle = "#0f0"; + ctx.lineWidth = 2; + ctx.stroke(); + for (const p of _points) { ctx.beginPath(); - ctx.moveTo(_points[0].x * scale, _points[0].y * scale); - for (let i = 1; i < _points.length; i++) { - ctx.lineTo(_points[i].x * scale, _points[i].y * scale); - } - if (_points.length >= 3) { - ctx.closePath(); - ctx.fillStyle = "rgba(0, 255, 0, 0.3)"; - ctx.fill(); - } - ctx.strokeStyle = "#0f0"; - ctx.lineWidth = 2; + ctx.arc(p.x * scale, p.y * scale, 4, 0, Math.PI * 2); + ctx.fillStyle = "#f00"; + ctx.fill(); + ctx.strokeStyle = "#fff"; + ctx.lineWidth = 1; ctx.stroke(); - for (const p of _points) { - ctx.beginPath(); - ctx.arc(p.x * scale, p.y * scale, 4, 0, Math.PI * 2); - ctx.fillStyle = "#f00"; - ctx.fill(); - ctx.strokeStyle = "#fff"; - ctx.lineWidth = 1; - ctx.stroke(); - } } - }; + } + }; + + const createOverlayCanvas = () => { + if (_overlayCanvas) return; + _overlayCanvas = document.createElement("canvas"); + _overlayCanvas.style.cssText = ` + position: fixed !important; + z-index: 1001 !important; + pointer-events: auto !important; + cursor: crosshair !important; + background: transparent !important; + border: 1px dashed #00FF00 !important; + display: none; + `; + document.body.appendChild(_overlayCanvas); - node.onDrawForeground = function(ctx) { - if (!this.flags.collapsed) { - const [w, h] = this.size; - - let widgetsTotalHeight = 0; - if (this.widgets) { - for (const widget of this.widgets) { - if (!widget.hidden && widget.type !== "button") { - widgetsTotalHeight += (widget.computeSize ? widget.computeSize(w)[1] : 20); - } + // ========== DnD: Обработчики Drag & Drop ========== + _overlayCanvas.addEventListener("dragover", (e) => { + e.preventDefault(); + e.stopPropagation(); + // Визуальная обратная связь + _overlayCanvas.style.border = "2px solid #4CAF50"; + _overlayCanvas.style.backgroundColor = "rgba(76, 175, 80, 0.15)"; + }); + + _overlayCanvas.addEventListener("dragleave", (e) => { + e.preventDefault(); + e.stopPropagation(); + // Возвращаем исходный стиль + _overlayCanvas.style.border = "1px dashed #00FF00"; + _overlayCanvas.style.backgroundColor = "transparent"; + }); + + _overlayCanvas.addEventListener("drop", async (e) => { + e.preventDefault(); + e.stopPropagation(); + // Возвращаем исходный стиль + _overlayCanvas.style.border = "1px dashed #00FF00"; + _overlayCanvas.style.backgroundColor = "transparent"; + + const file = e.dataTransfer.files[0]; + if (file && file.type.startsWith('image/')) { + console.log("[SPLINE 🦊] DnD file:", file.name); + await uploadFileAndLoad(file); + } else if (file) { + console.log("[SPLINE 🦊] Not an image file:", file.type); + } + }); + // ========== DnD: конец ========== + + _overlayCanvas.addEventListener("mousedown", (e) => { + e.preventDefault(); + if (!node.imageLoaded) return; + + const rect = _overlayCanvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const scale = parseFloat(_overlayCanvas.dataset.scale || "1"); + const imgX = x / scale; + const imgY = y / scale; + + if (e.button === 2 || e.ctrlKey) { + for (let i = _points.length - 1; i >= 0; i--) { + const dist = Math.hypot(_points[i].x - imgX, _points[i].y - imgY); + if (dist < 15 / scale) { + _points.splice(i, 1); + syncData(); + break; } } - - const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30; - const padding = 10; - const footerHeight = 50; - const btnH = 28; - const btnY = h - 45; - const btnW = (w - 50) / 3; - - const startY = titleBarHeight + widgetsTotalHeight + padding; - const availableHeight = h - startY - footerHeight - padding; - const availableWidth = w - (padding * 2); - - if (this.imageReady && this.image) { - const imgRatio = this.image.width / this.image.height; - let drawW, drawH; - - if (availableWidth / availableHeight > imgRatio) { - drawH = availableHeight; - drawW = drawH * imgRatio; - } else { - drawW = availableWidth; - drawH = drawW / imgRatio; + } else if (e.button === 0) { + if (_points.length >= 3) { + const distToFirst = Math.hypot(_points[0].x - imgX, _points[0].y - imgY); + if (distToFirst < 20 / scale) { + syncData(); + return; } - - const drawX = padding + (availableWidth - drawW) / 2; - const drawY = startY + (availableHeight - drawH) / 2; - - ctx.drawImage(this.image, drawX, drawY, drawW, drawH); - - ctx.strokeStyle = "#444"; - ctx.lineWidth = 1; - ctx.strokeRect(drawX, drawY, drawW, drawH); - } else { - ctx.fillStyle = "#222"; - ctx.fillRect(padding, startY, availableWidth, availableHeight); - ctx.fillStyle = "#555"; - ctx.font = "14px Arial"; - ctx.textAlign = "center"; - ctx.fillText("Select or Upload Image...", w / 2, startY + availableHeight / 2); - } - - this.buttonPositions = []; - - for (let i = 0; i < this.buttons.length; i++) { - let btn = this.buttons[i]; - btn.x = 15 + (i * (btnW + 5)); - btn.y = btnY; - btn.w = btnW; - btn.h = btnH; - - this.buttonPositions.push({ x: btn.x, y: btn.y, w: btn.w, h: btn.h }); - - ctx.fillStyle = btn.hover ? "#444" : "#2a2a2a"; - ctx.beginPath(); - if (ctx.roundRect) ctx.roundRect(btn.x, btn.y, btn.w, btn.h, 6); - else ctx.rect(btn.x, btn.y, btn.w, btn.h); - ctx.fill(); - ctx.lineWidth = 1; - ctx.strokeStyle = btn.color; - ctx.stroke(); - ctx.fillStyle = btn.color; - ctx.font = "bold 11px Arial"; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(btn.label, btn.x + btn.w / 2, btn.y + btn.h / 2); - if (btn.hover) app.canvas.canvas.style.cursor = "pointer"; } + _points.push({ x: imgX, y: imgY }); + syncData(); } - }; + }); - const onResize = node.onResize; - node.onResize = function(size) { - if (onResize) onResize.apply(this, arguments); - _lastRect = null; + const syncLoop = () => { + syncPosition(); + if (_overlayCanvas && node.imageLoaded) requestAnimationFrame(syncLoop); }; + syncLoop(); + }; + + // Отрисовка ноды + node.onDrawForeground = function(ctx) { + if (this.flags.collapsed) return; - const onMove = node.onMove; - node.onMove = function() { - if (onMove) onMove.apply(this, arguments); - _lastRect = null; - }; - - node.onMouseMove = function(event, pos, graphPos) { - const [x, y] = pos; - let needsRedraw = false; - for (let btn of this.buttons) { - const isOver = x >= btn.x && x <= btn.x + btn.w && y >= btn.y && y <= btn.y + btn.h; - if (btn.hover !== isOver) { btn.hover = isOver; needsRedraw = true; } + const [w, h] = this.size; + let widgetsHeight = 0; + if (this.widgets) { + for (const w of this.widgets) { + if (!w.hidden) widgetsHeight += 20; } - if (needsRedraw) this.setDirtyCanvas(true, false); - }; + } - node.onMouseDown = function(event, pos, graphPos) { - const [x, y] = pos; - for (let btn of this.buttons) { - if (x >= btn.x && x <= btn.x + btn.w && y >= btn.y && y <= btn.y + btn.h) { - btn.callback(); - return true; - } - } - return false; - }; + const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30; + const padding = 10; + const footerHeight = 50; + const btnH = 28; + const btnY = h - 45; + const btnW = (w - 50) / 3; - node.onRemoved = function() { - console.log(`[SPLINE 🦊] Node ${this.id} removed, cleaning up...`); + const startY = titleBarHeight + widgetsHeight + padding; + const availableHeight = h - startY - footerHeight - padding; + const availableWidth = w - (padding * 2); + + // Рисуем изображение + if (this.imageLoaded && this.image) { + const imgRatio = this.image.width / this.image.height; + let drawW, drawH; - _syncRunning = false; - if (_overlayCanvas) { - _overlayCanvas.remove(); - _overlayCanvas = null; + if (availableWidth / availableHeight > imgRatio) { + drawH = availableHeight; + drawW = drawH * imgRatio; + } else { + drawW = availableWidth; + drawH = drawW / imgRatio; } - const existingMenu = document.querySelector('.spline-image-menu'); - if (existingMenu) existingMenu.remove(); - }; + const drawX = padding + (availableWidth - drawW) / 2; + const drawY = startY + (availableHeight - drawH) / 2; + + ctx.drawImage(this.image, drawX, drawY, drawW, drawH); + ctx.strokeStyle = "#444"; + ctx.strokeRect(drawX, drawY, drawW, drawH); + } else { + ctx.fillStyle = "#222"; + ctx.fillRect(padding, startY, availableWidth, availableHeight); + ctx.fillStyle = "#555"; + ctx.font = "14px Arial"; + ctx.textAlign = "center"; + ctx.fillText("Select Image...", w / 2, startY + availableHeight / 2); + } - createOverlayCanvas(); - - } catch (error) { - console.error("[SPLINE 🦊] Critical Error: ", error); - console.error(error.stack); + // Рисуем кнопки + for (let i = 0; i < this.buttons.length; i++) { + const btn = this.buttons[i]; + btn.x = 15 + (i * (btnW + 5)); + btn.y = btnY; + btn.w = btnW; + btn.h = btnH; + + ctx.fillStyle = btn.hover ? "#444" : "#2a2a2a"; + ctx.fillRect(btn.x, btn.y, btn.w, btn.h); + ctx.strokeStyle = btn.color; + ctx.strokeRect(btn.x, btn.y, btn.w, btn.h); + ctx.fillStyle = btn.color; + ctx.font = "bold 11px Arial"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(btn.label, btn.x + btn.w / 2, btn.y + btn.h / 2); + } + }; + + // Обработка кликов по кнопкам + node.onMouseMove = function(event, pos) { + const [x, y] = pos; + let changed = false; + for (const btn of this.buttons) { + const isOver = x >= btn.x && x <= btn.x + btn.w && y >= btn.y && y <= btn.y + btn.h; + if (btn.hover !== isOver) { + btn.hover = isOver; + changed = true; + } + } + if (changed) this.setDirtyCanvas(true, false); + }; + + node.onMouseDown = function(event, pos) { + const [x, y] = pos; + for (const btn of this.buttons) { + if (x >= btn.x && x <= btn.x + btn.w && y >= btn.y && y <= btn.y + btn.h) { + btn.callback(); + return true; + } + } + return false; + }; + + node.onResize = function() { + _lastRect = null; + syncPosition(); + }; + + node.onMove = function() { + _lastRect = null; + syncPosition(); + }; + + // Восстановление при загрузке + const onConfigure = node.onConfigure; + node.onConfigure = function(o) { + if (onConfigure) onConfigure.apply(this, arguments); + if (imageWidget && imageWidget.value) { + node.loadImage(imageWidget.value); + } + if (coordsWidget && coordsWidget.value) { + try { + _points = JSON.parse(coordsWidget.value); + if (!Array.isArray(_points)) _points = []; + syncData(); + } catch (e) {} + } + }; + + // Очистка + node.onRemoved = function() { + if (_overlayCanvas) _overlayCanvas.remove(); + const menu = document.querySelector('.spline-image-menu'); + if (menu) menu.remove(); + }; + + // Создаем overlay и загружаем изображение + createOverlayCanvas(); + if (node.data.selected_image) { + node.loadImage(node.data.selected_image); } + + return result; }; }, - - setup() { - // Empty - all handlers inside onNodeCreated - } + + setup() {} }); \ No newline at end of file