929 lines
40 KiB
JavaScript
929 lines
40 KiB
JavaScript
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() {}
|
||
});
|