Files
2026-11-09 20:14:27 +03:00

929 lines
40 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
app.registerExtension({
name: "RaykoSplineMask",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
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;
node.data = {
selected_image: "",
spline_coords: "[]"
};
// Чуть увеличиваем высоту, чтобы вместить 4 слота + виджеты + кнопки
node.targetWidth = 320;
node.setSize([320, 440]);
const imageWidget = node.widgets?.find(w => w.name === "image");
const coordsWidget = node.widgets?.find(w => w.name === "coordinates");
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;
if (imageWidget) {
imageWidget.serializeValue = () => node.data.selected_image;
}
if (coordsWidget) {
coordsWidget.serializeValue = () => node.data.spline_coords;
}
node.image = new Image();
node.imageLoaded = false;
node.imageLoading = false;
node.buttons = [];
node.imageList = [];
node.fillFullMask = function() {
if (!node.imageLoaded || !node.image) return;
const w = node.image.width;
const h = node.image.height;
_points = [
{ x: 0, y: 0 },
{ x: w, y: 0 },
{ x: w, y: h },
{ x: 0, y: h }
];
syncData();
};
node.buttons = [
{ label: "📂 INPUT", color: "#2196F3", callback: () => node.showImageSelector(), hover: false },
{ label: "🖥️ LOAD", color: "#4CAF50", callback: () => node.triggerFileUpload(), hover: false },
{ label: "✔️ FULL", color: "#6ABF70", callback: () => node.fillFullMask(), hover: false },
{ label: "❌ CLEAR", color: "#dc3545", callback: () => node.clearPoints(), hover: false }
];
let _points = [];
try {
const savedPoints = JSON.parse(node.data.spline_coords);
if (Array.isArray(savedPoints)) _points = savedPoints;
} catch (e) {}
let _overlayCanvas = null;
let _animationId = null;
let _lastRect = null;
const updateOverlayVisibility = () => {
if (!_overlayCanvas) return;
const currentGraph = app.canvas.graph || node.graph;
const rootGraph = app.graph;
const isInSubgraph = currentGraph !== rootGraph;
if (isInSubgraph) {
_overlayCanvas.style.zIndex = '-1';
_overlayCanvas.style.display = 'none';
_overlayCanvas.style.pointerEvents = 'none';
} else {
_overlayCanvas.style.zIndex = '900';
_overlayCanvas.style.display = node.imageLoaded ? 'block' : 'none';
_overlayCanvas.style.pointerEvents = 'auto';
}
};
const handleGraphChange = () => updateOverlayVisibility();
api.addEventListener('graph_changed', handleGraphChange);
api.addEventListener('graphConfigured', handleGraphChange);
node._subgraphListeners = [
{ event: 'graph_changed', handler: handleGraphChange },
{ event: 'graphConfigured', handler: handleGraphChange }
];
const syncData = () => {
node.data.spline_coords = JSON.stringify(_points);
if (coordsWidget) 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;
node.imageLoading = true;
node.data.selected_image = imagePath;
if (imageWidget) 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;
updateOverlayVisibility();
_lastRect = null;
syncPosition();
node.setDirtyCanvas(true, true);
if (node.graph) node.graph.changeTracker?.dispatchEvent(new Event("change"));
};
img.onerror = () => {
node.imageLoaded = false;
node.imageLoading = false;
updateOverlayVisibility();
node.setDirtyCanvas(true, true);
};
img.src = imgUrl;
};
const uploadFileAndLoad = async (file) => {
if (!file || !file.type.startsWith('image/')) {
console.log("[SPLINE 🦊] Not an image file");
return false;
}
const formData = new FormData();
formData.append('image', file);
formData.append('subfolder', '');
formData.append('type', 'input');
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;
}
} catch (err) {
console.error("[SPLINE 🦊] Upload error:", err);
}
return false;
};
const handleCanvasDrop = (e) => {
if (!e.dataTransfer || !e.dataTransfer.files.length) return;
const rect = app.canvas.canvas.getBoundingClientRect();
const ds = app.canvas.ds;
const nodeX = rect.left + ((node.pos[0] + ds.offset[0]) * ds.scale);
const nodeY = rect.top + ((node.pos[1] + ds.offset[1]) * ds.scale);
const nodeW = node.size[0] * ds.scale;
const nodeH = node.size[1] * ds.scale;
if (e.clientX >= nodeX && e.clientX <= nodeX + nodeW &&
e.clientY >= nodeY && e.clientY <= nodeY + nodeH) {
e.preventDefault();
e.stopPropagation();
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) {
console.log("[SPLINE 🦊] Canvas intercepted drop for node:", file.name);
uploadFileAndLoad(file);
}
}
};
const handleCanvasDragOver = (e) => {
if (!e.dataTransfer || !e.dataTransfer.files.length) return;
const rect = app.canvas.canvas.getBoundingClientRect();
const ds = app.canvas.ds;
const nodeX = rect.left + ((node.pos[0] + ds.offset[0]) * ds.scale);
const nodeY = rect.top + ((node.pos[1] + ds.offset[1]) * ds.scale);
const nodeW = node.size[0] * ds.scale;
const nodeH = node.size[1] * ds.scale;
if (e.clientX >= nodeX && e.clientX <= nodeX + nodeW &&
e.clientY >= nodeY && e.clientY <= nodeY + nodeH) {
e.preventDefault();
e.stopPropagation();
}
};
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 = () => {
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())
.then(data => {
self.imageList = data.images || [];
if (!self.imageList.length) {
alert("No images found!");
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;
z-index: 10001;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
min-width: 200px;
display: flex;
flex-direction: column;
`;
const header = document.createElement("div");
header.style.cssText = `
height: 28px;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
border-bottom: 1px solid #333;
cursor: pointer;
background: #2a2a2a;
`;
header.onmouseover = () => header.style.background = "#444";
header.onmouseout = () => header.style.background = "#2a2a2a";
const closeText = document.createElement("span");
closeText.textContent = "CLOSE ✕";
closeText.style.cssText = `
color: #dc3545;
font-size: 11px;
font-weight: bold;
font-family: Arial, sans-serif;
`;
header.appendChild(closeText);
menu.appendChild(header);
header.onclick = (e) => {
e.stopPropagation();
if (_previewTimeout) clearTimeout(_previewTimeout);
hidePreview();
menu.remove();
};
const content = document.createElement("div");
content.className = 'menu-content';
content.style.cssText = `
max-height: 300px;
overflow-y: auto;
`;
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.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();
};
content.appendChild(item);
});
menu.appendChild(content);
self.positionMenu(menu, 0);
document.body.appendChild(menu);
})
.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;
}
const canvasRect = canvasEl.getBoundingClientRect();
const ds = app.canvas.ds;
const scale = ds.scale;
const nodeScreenX = canvasRect.left + ((this.pos[0] + ds.offset[0]) * scale);
const nodeScreenY = canvasRect.top + ((this.pos[1] + ds.offset[1]) * scale);
const padding = 15;
const gap = 10;
const btnW = (this.size[0] - 2 * padding - 3 * gap) / 4;
const btnH = 28;
const btnYOffset = 45;
const btnX = padding + (buttonIndex * (btnW + gap));
const btnGraphY = this.size[1] - btnYOffset;
const screenLeft = nodeScreenX + (btnX * scale);
const screenTop = nodeScreenY + (btnGraphY * scale) + (btnH * scale);
menu.style.left = screenLeft + "px";
menu.style.top = (screenTop + 5) + "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) {
widgetsHeight += 20;
}
}
const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30;
// Оставляем стандартные отступы, чтобы картинка занимала всю ширину
const leftRightPadding = 10;
const topPadding = 18;
const bottomPadding = 10;
const footerHeight = 60; // Выделяем место под кнопки в самом низу
const availableHeight = (node.size[1] * scale) - (titleBarHeight * scale) - (widgetsHeight * scale) - (footerHeight * scale) - (topPadding * scale) - (bottomPadding * scale);
const availableWidth = (node.size[0] * scale) - (leftRightPadding * 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 + (leftRightPadding * scale) + ((availableWidth - drawW) / 2);
const drawY = nodeScreenY + (titleBarHeight * scale) + (widgetsHeight * scale) + (topPadding * scale) + ((availableHeight - drawH) / 2);
return { left: drawX, top: drawY, width: drawW, height: drawH, scale: contentScale };
};
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 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.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");
updateOverlayVisibility();
_overlayCanvas.style.cssText = `
position: fixed !important;
z-index: 900 !important;
pointer-events: auto !important;
cursor: crosshair !important;
background: transparent !important;
border: 1px dashed #00FF00 !important;
display: none;
`;
document.body.appendChild(_overlayCanvas);
_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 🦊] Overlay DnD file:", file.name);
await uploadFileAndLoad(file);
} else if (file) {
console.log("[SPLINE 🦊] Not an image file:", file.type);
}
});
_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;
}
}
} 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;
}
}
_points.push({ x: imgX, y: imgY });
syncData();
}
});
const updateLoop = () => {
if (!_overlayCanvas) return;
updateOverlayVisibility();
syncPosition();
_animationId = requestAnimationFrame(updateLoop);
};
updateLoop();
};
node.onDrawForeground = function(ctx) {
if (this.flags.collapsed) return;
const [w, h] = this.size;
let widgetsHeight = 0;
if (this.widgets) {
for (const w of this.widgets) {
widgetsHeight += 20;
}
}
const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30;
const leftRightPadding = 10;
const topPadding = 18;
const bottomPadding = 10;
const footerHeight = 60; // Выделяем место под кнопки
const btnH = 28;
const btnY = h - 45;
const btnPadding = 15;
const btnGap = 10;
const btnW = (w - 2 * btnPadding - 3 * btnGap) / 4;
if (this.imageLoaded && this.image) {
ctx.fillStyle = "#888";
ctx.font = "15px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.fillText(
`${this.image.width} × ${this.image.height}`,
w / 2,
titleBarHeight + widgetsHeight - 13
);
}
const startY = titleBarHeight + widgetsHeight + topPadding;
const availableHeight = h - startY - footerHeight - bottomPadding;
const availableWidth = w - (leftRightPadding * 2);
if (this.imageLoaded && 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;
}
const drawX = leftRightPadding + (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(leftRightPadding, startY, availableWidth, availableHeight);
ctx.fillStyle = "#555";
ctx.font = "14px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Select Image...", w / 2, startY + availableHeight / 2);
}
for (let i = 0; i < this.buttons.length; i++) {
const btn = this.buttons[i];
btn.x = btnPadding + (i * (btnW + btnGap));
btn.y = btnY;
btn.w = btnW;
btn.h = btnH;
const radius = 4;
const x = btn.x, y = btn.y, w = btn.w, h = btn.h;
let fillColor = btn.color;
if (btn.label === "✔️ FULL" && btn.hover) {
fillColor = "#4CAF50";
}
ctx.fillStyle = btn.hover ? "#444" : "#2a2a2a";
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + w - radius, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
ctx.lineTo(x + w, y + h - radius);
ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
ctx.lineTo(x + radius, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = fillColor;
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = fillColor;
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(size) {
if (size[0] < 320) size[0] = 320;
if (size[1] < 440) size[1] = 440;
_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) {}
}
};
const originalOnRemoved = node.onRemoved;
node.onRemoved = function() {
if (_animationId) {
cancelAnimationFrame(_animationId);
_animationId = null;
}
if (node._subgraphListeners) {
node._subgraphListeners.forEach(({ event, handler }) => {
api.removeEventListener(event, handler);
});
node._subgraphListeners = 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 });
if (originalOnRemoved) originalOnRemoved.apply(this, arguments);
};
createOverlayCanvas();
if (node.data.selected_image) {
node.loadImage(node.data.selected_image);
}
return result;
};
},
setup() {}
});