Files
2026-08-24 16:14:01 +03:00

583 lines
27 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. 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";
let queueWasRunning = false;
let batchResetTimer = null;
let allNodes = [];
app.registerExtension({
name: "RaykoIntermediateSplineMask",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "RaykoIntermediateSplineMask") return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
try {
onNodeCreated?.apply(this, arguments);
const node = this;
node.image = new Image();
node.imageReady = false;
node.currentStatus = "Waiting...";
node.buttons = [];
node.batchMode = false;
node.hasPreset = false;
const coordsWidget = node.widgets?.find(w => w.name === "coordinates");
if (coordsWidget) {
coordsWidget.hidden = true;
coordsWidget.serializeValue = () => {
return node.properties?.spline_coords || "[]";
};
}
node.addButton = (label, color, callback, isBatch = false) => {
node.buttons.push({
label: label, color: color, callback: callback,
x: 0, y: 0, w: 0, h: 30, hover: false, isBatch: isBatch
});
};
node.addButton("⚙️ BATCH", "#2196F3", () => node.toggleBatchMode(), true);
node.addButton("✔️ ACCEPT", "#28a745", () => node.sendDecision("approve"));
node.addButton("🔴 CLEAR", "#dc3545", () => node.sendDecision("reject"));
node.addButton("❌ CANCEL", "#dc3545", () => node.sendDecision("cancel"));
node.setSize([450, 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) {}
}
const updateCoords = () => {
const jsonStr = JSON.stringify(_points);
node.properties = node.properties || {};
node.properties.spline_coords = jsonStr;
if (coordsWidget) coordsWidget.value = jsonStr;
drawOverlay();
};
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 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 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.imageReady ? '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 startSyncLoop = () => {
if (_syncRunning) return;
_syncRunning = true;
const syncLoop = () => {
if (!_syncRunning) return;
updateOverlayVisibility();
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 createOverlayCanvas = () => {
if (_overlayCanvas) return;
_overlayCanvas = document.createElement("canvas");
_overlayCanvas.className = "rayko-inspline-overlay";
updateOverlayVisibility();
_overlayCanvas.style.cssText = `
position: fixed !important; z-index: 900 !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;
`;
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) {
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();
}
}
};
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);
}
}
}
const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30;
const padding = 10;
const footerHeight = 50;
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;
}
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("Waiting for Image...", w / 2, startY + availableHeight / 2);
}
const btnH = 28;
const btnY = h - 45;
const btnW = (w - 60) / 4 + 3;
this.buttons[0].x = 15; this.buttons[0].y = btnY; this.buttons[0].w = btnW; this.buttons[0].h = btnH;
this.buttons[1].x = 20 + btnW; this.buttons[1].y = btnY; this.buttons[1].w = btnW; this.buttons[1].h = btnH;
this.buttons[2].x = 25 + (btnW * 2); this.buttons[2].y = btnY; this.buttons[2].w = btnW; this.buttons[2].h = btnH;
this.buttons[3].x = 30 + (btnW * 3); this.buttons[3].y = btnY; this.buttons[3].w = btnW; this.buttons[3].h = btnH;
for (let btn of this.buttons) {
let btnColor = btn.color;
if (btn.isBatch && node.batchMode) {
btnColor = "#ff9800";
btn.label = "️️ BATCH ON";
} else if (btn.isBatch) {
btn.label = "️️ BATCH";
}
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 = btnColor;
ctx.stroke();
ctx.fillStyle = btnColor;
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";
}
}
};
const onResize = node.onResize;
node.onResize = function(size) {
if (onResize) onResize.apply(this, arguments);
if (size[0] < 450) size[0] = 450;
_lastRect = null;
};
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; }
}
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;
}
}
};
node.toggleBatchMode = async function() {
node.batchMode = !node.batchMode;
if (node.batchMode) {
node.hasPreset = false;
}
this.setDirtyCanvas(true, true);
try {
await fetch("/rayko/inspline/batch_toggle", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
node_id: this.id.toString(),
enabled: node.batchMode
})
});
} catch(e) {
console.error("[InSPLINE 🦊] Batch toggle failed:", e);
}
};
node.sendDecision = async function(decision) {
const currentCoords = node.properties?.spline_coords || "[]";
this.currentStatus = `Sending ${decision}...`;
this.setDirtyCanvas(true, true);
if (decision === "cancel") {
try {
await api.interrupt();
} catch (e) { console.error("Interrupt failed:", e); }
node.batchMode = false;
node.hasPreset = false;
try {
await fetch("/rayko/inspline/clear_preset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ node_id: this.id.toString() })
});
} catch(e) {}
}
if (decision === "reject") {
_points = [];
updateCoords();
}
try {
const resp = await fetch("/rayko/inspline/decision", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
node_id: this.id.toString(),
decision: decision,
coordinates: currentCoords,
batch_mode: node.batchMode
})
});
if (resp.ok) {
if (decision === "approve") {
if (node.batchMode) {
node.hasPreset = true;
} else {
this.currentStatus = "✅ Approved! Processing...";
}
} else if (decision === "reject") {
this.currentStatus = "🔄 Mask cleared. Draw & Approve!";
} else {
this.currentStatus = "❌ Cancelled.";
}
} else {
this.currentStatus = "Error: " + resp.statusText;
}
} catch (err) {
this.currentStatus = "Error: Connection Failed";
}
this.setDirtyCanvas(true, true);
};
node.onRemoved = function() {
_syncRunning = false;
if (node._subgraphListeners) {
node._subgraphListeners.forEach(({ event, handler }) => {
api.removeEventListener(event, handler);
});
node._subgraphListeners = null;
}
if (_overlayCanvas) {
_overlayCanvas.remove();
_overlayCanvas = null;
}
fetch("/rayko/inspline/clear_preset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
node_id: this.id.toString()
})
}).catch(err => {
console.error("[InSPLINE 🦊] Cleanup signal failed:", err);
});
};
createOverlayCanvas();
const originalOnRendered = app.canvas.onRendered;
app.canvas.onRendered = function() {
if (originalOnRendered) originalOnRendered.apply(this, arguments);
if (_overlayCanvas && node.imageReady) {
_lastRect = null;
syncPosition();
}
};
allNodes.push({node});
} catch (error) {
console.error("[InSPLINE 🦊] Critical Error:", error);
}
};
},
setup() {
api.addEventListener("rayko.inspline.show", (event) => {
const { node_id, image_url } = event.detail;
const node = app.graph.getNodeById(node_id);
if (node) {
node.image.src = image_url + "&t=" + Date.now();
node.image.onload = () => {
node.imageReady = true;
const overlay = document.querySelector('.rayko-inspline-overlay');
if (overlay) {
updateOverlayVisibility();
}
setTimeout(() => {
_lastRect = null;
syncPosition();
}, 100);
node.setDirtyCanvas(true, true);
};
if (node.size[1] < 600) node.setSize([node.size[0], 600]);
node.currentStatus = " Draw mask, then APPROVE!";
app.canvas.centerOnNode(node);
node.setDirtyCanvas(true, true);
}
});
api.addEventListener("status", (e) => {
const remaining = e.detail?.exec_info?.queue_remaining;
if (remaining > 0) {
queueWasRunning = true;
if (batchResetTimer) { clearTimeout(batchResetTimer); batchResetTimer = null; }
} else if (remaining === 0 && queueWasRunning) {
queueWasRunning = false;
if (!batchResetTimer) {
batchResetTimer = setTimeout(() => {
allNodes.forEach(({node}) => {
if (node && (node.batchMode || node.hasPreset)) {
node.batchMode = false;
node.hasPreset = false;
node.setDirtyCanvas(true, true);
fetch("/rayko/inspline/clear_preset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ node_id: String(node.id) })
}).catch(err => console.error("Failed to clear preset:", err));
}
});
batchResetTimer = null;
}, 2000);
}
}
});
}
});