Files
2026-08-24 16:10:45 +03:00

819 lines
39 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: "RaykoStudio.CropImage",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "RSCropImage") 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 for image...";
node.buttons = [];
node.imageWidth = 0;
node.imageHeight = 0;
node._cropRect = { x: 0, y: 0, width: 256, height: 256 };
node._overlayCanvas = null;
const cropDataWidget = node.widgets?.find(w => w.name === "crop_data");
if (cropDataWidget) {
cropDataWidget.hidden = true;
cropDataWidget.serializeValue = () => {
return node.properties?.crop_rect || "{}";
};
}
const modeWidget = node.widgets?.find(w => w.name === "multiple_mode");
const ofWidget = node.widgets?.find(w => w.name === "multiple_of");
if (modeWidget && ofWidget) {
const applyDisabled = () => {
ofWidget.disabled = !modeWidget.value;
node.setDirtyCanvas(true, true);
};
let _modeValue = modeWidget.value;
Object.defineProperty(modeWidget, 'value', {
get: function() { return _modeValue; },
set: function(v) {
const wasOn = _modeValue;
_modeValue = v;
applyDisabled();
if (!wasOn && v && node.imageReady) {
applyAlignment();
updateCropRect();
}
},
configurable: true
});
const originalOfCallback = ofWidget.callback;
ofWidget.callback = function(value) {
if (originalOfCallback) originalOfCallback.apply(this, arguments);
if (modeWidget.value && node.imageReady) {
applyAlignment();
updateCropRect();
}
};
applyDisabled();
}
node.addButton = (label, color, callback) => {
node.buttons.push({
label: label, color: color, callback: callback,
x: 0, y: 0, w: 0, h: 30, hover: false
});
};
node.addButton("✔️ ACCEPT", "#28a745", () => node.sendDecision("approve"));
node.addButton("🔄 RESET", "#2196F3", () => node.sendDecision("reject"));
node.addButton("❌ CANCEL", "#dc3545", () => node.sendDecision("cancel"));
node.setSize([500, 650]);
let _syncRunning = false;
let _lastRect = null;
let _lastScale = 1;
let _isSelecting = false;
let _isDragging = false;
let _isResizing = false;
let _resizeHandle = null;
let _startX = 0;
let _startY = 0;
let _startRect = null;
if (node.properties?.crop_rect && node.properties.crop_rect !== "{}") {
try {
const r = JSON.parse(node.properties.crop_rect);
if (r.x !== undefined) node._cropRect = r;
} catch (e) {}
}
const clampCropRect = (rect) => {
if (!node.imageReady || !node.imageWidth || !node.imageHeight) return rect;
const imgW = node.imageWidth;
const imgH = node.imageHeight;
let w = Math.max(1, Math.min(rect.width, imgW));
let h = Math.max(1, Math.min(rect.height, imgH));
let x = Math.max(0, Math.min(rect.x, imgW - w));
let y = Math.max(0, Math.min(rect.y, imgH - h));
w = Math.min(w, imgW - x);
h = Math.min(h, imgH - y);
return { x, y, width: w, height: h };
};
const applyClamp = () => {
const clamped = clampCropRect(node._cropRect);
node._cropRect.x = clamped.x;
node._cropRect.y = clamped.y;
node._cropRect.width = clamped.width;
node._cropRect.height = clamped.height;
};
const alignToMultiple = (rect, m, imgW, imgH) => {
if (!m || m < 1 || !imgW || !imgH) return rect;
const origX = rect.x;
const origY = rect.y;
const origW = Math.max(1, rect.width);
const origH = Math.max(1, rect.height);
const centerX = origX + origW / 2;
const centerY = origY + origH / 2;
let newW = Math.ceil(origW / m) * m;
let newH = Math.ceil(origH / m) * m;
const maxW = ((imgW - origX) / m) | 0;
if (maxW < 1 || (maxW * m) < m) {
newW = Math.floor(origW / m) * m;
} else if (origX + newW > imgW) {
newW = maxW * m;
}
const maxH = ((imgH - origY) / m) | 0;
if (maxH < 1 || (maxH * m) < m) {
newH = Math.floor(origH / m) * m;
} else if (origY + newH > imgH) {
newH = maxH * m;
}
if (newW < m) newW = m;
if (newH < m) newH = m;
let newX = Math.round(centerX - newW / 2);
let newY = Math.round(centerY - newH / 2);
newX = Math.max(0, Math.min(newX, imgW - newW));
newY = Math.max(0, Math.min(newY, imgH - newH));
return { x: newX, y: newY, width: newW, height: newH };
};
const applyAlignment = () => {
if (!modeWidget || !ofWidget || !modeWidget.value || !node.imageReady) return;
const m = parseInt(ofWidget.value);
if (!m) return;
const aligned = alignToMultiple(node._cropRect, m, node.imageWidth, node.imageHeight);
node._cropRect.x = aligned.x;
node._cropRect.y = aligned.y;
node._cropRect.width = aligned.width;
node._cropRect.height = aligned.height;
};
const roundRect = (rect) => ({
x: Math.round(rect.x),
y: Math.round(rect.y),
width: Math.round(rect.width),
height: Math.round(rect.height),
});
const isNodeFullyVisible = () => {
if (!app.canvas) return false;
const ds = app.canvas.ds;
const scale = ds.scale;
const canvasEl = app.canvas.canvas;
const canvasRect = canvasEl.getBoundingClientRect();
const nodeLeft = node.pos[0];
const nodeTop = node.pos[1];
const nodeRight = node.pos[0] + node.size[0];
const nodeBottom = node.pos[1] + node.size[1];
const viewportLeft = -ds.offset[0];
const viewportTop = -ds.offset[1];
const viewportRight = viewportLeft + canvasRect.width / scale;
const viewportBottom = viewportTop + canvasRect.height / scale;
return nodeLeft >= viewportLeft &&
nodeTop >= viewportTop &&
nodeRight <= viewportRight &&
nodeBottom <= viewportBottom;
};
const updateCropRect = () => {
const jsonStr = JSON.stringify(roundRect(node._cropRect));
node.properties = node.properties || {};
node.properties.crop_rect = jsonStr;
if (cropDataWidget) cropDataWidget.value = jsonStr;
drawOverlay();
node.setDirtyCanvas(true, true);
};
const calculateImageRect = () => {
if (!app.canvas || !node.imageReady) return null;
const ds = app.canvas.ds;
const scale = ds.scale;
const canvasEl = app.canvas.canvas;
const canvasRect = canvasEl.getBoundingClientRect();
const graphX = node.pos[0];
const graphY = node.pos[1];
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 = 60;
const outputSlotsHeight = 40;
const startY = titleBarHeight + widgetsTotalHeight + padding + outputSlotsHeight;
const availableHeight = node.size[1] - startY - footerHeight - padding;
const availableWidth = node.size[0] - (padding * 2);
if (availableWidth <= 0 || availableHeight <= 0) return null;
let drawW = availableWidth;
let drawH = availableHeight;
let contentScale = 1;
if (node.imageWidth > 0 && node.imageHeight > 0) {
const imgRatio = node.imageWidth / node.imageHeight;
if (availableWidth / availableHeight > imgRatio) {
drawH = availableHeight;
drawW = drawH * imgRatio;
} else {
drawW = availableWidth;
drawH = drawW / imgRatio;
}
contentScale = drawW / node.imageWidth;
}
const drawX = nodeScreenX + (padding * scale) + ((availableWidth - drawW) * scale / 2);
const drawY = nodeScreenY + (startY * scale) + ((availableHeight - drawH) * scale / 2);
return {
left: drawX,
top: drawY,
width: drawW * scale,
height: drawH * scale,
scale: contentScale
};
};
const updateOverlayVisibility = () => {
if (!node._overlayCanvas) return;
const currentGraph = app.canvas.graph || node.graph;
const rootGraph = app.graph;
const isInSubgraph = currentGraph !== rootGraph;
if (isInSubgraph) {
node._overlayCanvas.style.zIndex = '-1';
node._overlayCanvas.style.display = 'none';
node._overlayCanvas.style.pointerEvents = 'none';
} else {
node._overlayCanvas.style.zIndex = '900';
node._overlayCanvas.style.display = node.imageReady ? 'block' : 'none';
node._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 (node._overlayCanvas) requestAnimationFrame(syncLoop);
else _syncRunning = false;
};
requestAnimationFrame(syncLoop);
};
const syncPosition = () => {
if (!node._overlayCanvas) return;
const imgRect = calculateImageRect();
if (!imgRect) return;
const currentScale = app.canvas.ds.scale;
const scaleChanged = Math.abs(_lastScale - currentScale) > 0.01;
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 ||
scaleChanged;
if (hasChanged) {
_lastScale = currentScale;
_lastRect = { ...imgRect };
node._overlayCanvas.style.left = `${imgRect.left}px`;
node._overlayCanvas.style.top = `${imgRect.top}px`;
node._overlayCanvas.style.width = `${imgRect.width}px`;
node._overlayCanvas.style.height = `${imgRect.height}px`;
const screenScale = imgRect.scale * currentScale;
node._overlayCanvas.dataset.screenScale = screenScale;
drawOverlay();
}
};
const createOverlayCanvas = () => {
if (node._overlayCanvas) return;
node._overlayCanvas = document.createElement("canvas");
updateOverlayVisibility();
node._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(node._overlayCanvas);
node._overlayCanvas.addEventListener("mousedown", (e) => {
e.preventDefault();
e.stopPropagation();
const rect = node._overlayCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const screenScale = parseFloat(node._overlayCanvas.dataset.screenScale || "1");
const imgX = x / screenScale;
const imgY = y / screenScale;
const cropX = node._cropRect.x * screenScale;
const cropY = node._cropRect.y * screenScale;
const cropW = node._cropRect.width * screenScale;
const cropH = node._cropRect.height * screenScale;
const handleSize = 10;
const handles = {
'tl': { x: cropX, y: cropY },
'tr': { x: cropX + cropW, y: cropY },
'bl': { x: cropX, y: cropY + cropH },
'br': { x: cropX + cropW, y: cropY + cropH }
};
let clickedHandle = null;
for (const [handle, pos] of Object.entries(handles)) {
if (Math.abs(x - pos.x) < handleSize && Math.abs(y - pos.y) < handleSize) {
clickedHandle = handle;
break;
}
}
if (clickedHandle) {
_isResizing = true;
_resizeHandle = clickedHandle;
} else if (x >= cropX && x <= cropX + cropW && y >= cropY && y <= cropY + cropH) {
_isDragging = true;
} else {
_isSelecting = true;
node._cropRect = { x: imgX, y: imgY, width: 0, height: 0 };
}
_startX = x;
_startY = y;
_startRect = { ...node._cropRect };
node._overlayCanvas.setPointerCapture(e.pointerId || 1);
});
node._overlayCanvas.addEventListener("mousemove", (e) => {
if (!_isSelecting && !_isDragging && !_isResizing) return;
const rect = node._overlayCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const screenScale = parseFloat(node._overlayCanvas.dataset.screenScale || "1");
const deltaX = (x - _startX) / screenScale;
const deltaY = (y - _startY) / screenScale;
if (_isSelecting) {
const x1 = _startRect.x;
const y1 = _startRect.y;
const x2 = x1 + deltaX;
const y2 = y1 + deltaY;
node._cropRect.x = Math.max(0, Math.min(x1, x2));
node._cropRect.y = Math.max(0, Math.min(y1, y2));
node._cropRect.width = Math.max(32, Math.abs(x2 - x1));
node._cropRect.height = Math.max(32, Math.abs(y2 - y1));
} else if (_isDragging) {
node._cropRect.x = Math.max(0, _startRect.x + deltaX);
node._cropRect.y = Math.max(0, _startRect.y + deltaY);
} else if (_isResizing) {
const handle = _resizeHandle;
let newX = _startRect.x;
let newY = _startRect.y;
let newW = _startRect.width;
let newH = _startRect.height;
if (handle === 'tl' || handle === 'bl') {
newW = _startRect.width - deltaX;
newX = _startRect.x + deltaX;
if (newW >= 32 && newX >= 0) {
node._cropRect.width = newW;
node._cropRect.x = newX;
}
}
if (handle === 'tr' || handle === 'br') {
newW = _startRect.width + deltaX;
if (newW >= 32) {
node._cropRect.width = newW;
}
}
if (handle === 'tl' || handle === 'tr') {
newH = _startRect.height - deltaY;
newY = _startRect.y + deltaY;
if (newH >= 32 && newY >= 0) {
node._cropRect.height = newH;
node._cropRect.y = newY;
}
}
if (handle === 'bl' || handle === 'br') {
newH = _startRect.height + deltaY;
if (newH >= 32) {
node._cropRect.height = newH;
}
}
}
applyClamp();
updateCropRect();
});
node._overlayCanvas.addEventListener("mouseup", (e) => {
_isSelecting = false;
_isDragging = false;
_isResizing = false;
_resizeHandle = null;
if (modeWidget && ofWidget && modeWidget.value && node.imageReady) {
applyAlignment();
updateCropRect();
}
node._overlayCanvas.releasePointerCapture(e.pointerId || 1);
});
startSyncLoop();
};
const drawOverlay = () => {
if (!node._overlayCanvas) return;
const width = parseFloat(node._overlayCanvas.style.width || "0");
const height = parseFloat(node._overlayCanvas.style.height || "0");
if (width <= 0 || height <= 0) return;
const dpr = window.devicePixelRatio || 1;
node._overlayCanvas.width = width * dpr;
node._overlayCanvas.height = height * dpr;
const ctx = node._overlayCanvas.getContext("2d");
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, width, height);
const screenScale = parseFloat(node._overlayCanvas.dataset.screenScale || "1");
const cropX = node._cropRect.x * screenScale;
const cropY = node._cropRect.y * screenScale;
const cropW = Math.max(1, node._cropRect.width * screenScale);
const cropH = Math.max(1, node._cropRect.height * screenScale);
ctx.fillStyle = "rgba(0, 200, 100, 0.25)";
ctx.fillRect(cropX, cropY, cropW, cropH);
ctx.strokeStyle = "#00c864";
ctx.lineWidth = 2;
ctx.strokeRect(cropX, cropY, cropW, cropH);
const handleSize = 8;
ctx.fillStyle = "#00c864";
ctx.fillRect(cropX - handleSize/2, cropY - handleSize/2, handleSize, handleSize);
ctx.fillRect(cropX + cropW - handleSize/2, cropY - handleSize/2, handleSize, handleSize);
ctx.fillRect(cropX - handleSize/2, cropY + cropH - handleSize/2, handleSize, handleSize);
ctx.fillRect(cropX + cropW - handleSize/2, cropY + cropH - handleSize/2, handleSize, handleSize);
};
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 = 60;
const outputSlotsHeight = 40;
const startY = titleBarHeight + widgetsTotalHeight + padding + outputSlotsHeight;
const availableHeight = h - startY - footerHeight - padding;
const availableWidth = w - (padding * 2);
const cropSizeText = `${Math.round(node._cropRect.width)} x ${Math.round(node._cropRect.height)}`;
const isMultipleOn = modeWidget && modeWidget.value && ofWidget;
const multipleSuffix = isMultipleOn ? ` (×${parseInt(ofWidget.value)})` : "";
const fullText = cropSizeText + multipleSuffix;
const statusLineY = titleBarHeight + widgetsTotalHeight + 30;
ctx.font = "14px Arial";
ctx.textBaseline = "top";
const mainWidth = ctx.measureText(cropSizeText).width;
if (isMultipleOn) {
const fullWidth = ctx.measureText(fullText).width;
let drawX = (w - fullWidth) / 2;
ctx.fillStyle = "#aaaaaa";
ctx.textAlign = "left";
ctx.fillText(cropSizeText, drawX, statusLineY);
ctx.fillStyle = "#FF0000";
ctx.fillText(multipleSuffix, drawX + mainWidth, statusLineY);
} else {
ctx.fillStyle = "#aaaaaa";
ctx.textAlign = "center";
ctx.fillText(cropSizeText, w / 2, statusLineY);
}
if (this.imageReady && this.image) {
const imgRatio = this.imageWidth / this.imageHeight;
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);
}
ctx.fillStyle = "#aaaaaa";
ctx.font = "11px Arial";
ctx.textAlign = "center";
ctx.fillText(this.currentStatus, w / 2, h - 60);
const btnH = 28;
const btnY = h - 40;
const btnW = (w - 50) / 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;
for (let btn of this.buttons) {
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";
}
}
};
const onResize = node.onResize;
node.onResize = function(size) {
if (onResize) onResize.apply(this, arguments);
};
const onMove = node.onMove;
node.onMove = function() {
if (onMove) onMove.apply(this, arguments);
};
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.sendDecision = async function(decision) {
const currentCropData = JSON.stringify(roundRect(node._cropRect));
node.properties = node.properties || {};
node.properties.crop_rect = currentCropData;
if (cropDataWidget) cropDataWidget.value = currentCropData;
this.currentStatus = `Sending ${decision}...`;
this.setDirtyCanvas(true, true);
if (decision === "cancel") {
try {
await api.interrupt();
} catch (e) {}
}
if (decision === "reject") {
node._cropRect = { x: 0, y: 0, width: 256, height: 256 };
applyClamp();
applyAlignment();
updateCropRect();
}
try {
const resp = await fetch("/rayko/rscrop/decision", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
node_id: this.id.toString(),
decision: decision,
crop_data: currentCropData
})
});
if (resp.ok) {
if (decision === "approve") {
this.currentStatus = "✅ Approved! Processing...";
} else if (decision === "reject") {
this.currentStatus = "🔄 Reset. Select & 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 (node._overlayCanvas) {
node._overlayCanvas.remove();
node._overlayCanvas = null;
}
if (node.image) {
node.image.src = "";
node.image = null;
}
node.imageReady = false;
fetch("/rayko/rscrop/cleanup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
node_id: this.id.toString()
})
}).catch(() => {});
};
api.addEventListener("rayko.rscrop.show", (event) => {
const { node_id, image_url, image_width, image_height, crop_data } = event.detail;
const currentId = node.id;
if (String(node_id) === String(currentId)) {
node.image.src = image_url + "&t=" + Date.now();
node.image.onload = () => {
node.imageReady = true;
node.imageWidth = image_width || node.image.width;
node.imageHeight = image_height || node.image.height;
if (crop_data) {
try {
const r = JSON.parse(crop_data);
if (r.x !== undefined) {
node._cropRect = { x: r.x, y: r.y, width: r.width, height: r.height };
node.properties = node.properties || {};
node.properties.crop_rect = crop_data;
if (cropDataWidget) cropDataWidget.value = crop_data;
}
} catch (e) {
console.error("[RS Crop 🦊] Error parsing crop_data from event:", e);
}
} else if (node.properties?.crop_rect && node.properties.crop_rect !== "{}") {
try {
const r = JSON.parse(node.properties.crop_rect);
if (r.x !== undefined) node._cropRect = r;
} catch (e) {}
}
applyClamp();
applyAlignment();
updateOverlayVisibility();
setTimeout(() => {
_lastRect = null;
_lastScale = 1;
syncPosition();
node.setDirtyCanvas(true, true);
}, 100);
};
if (node.size[1] < 650) node.setSize([node.size[0], 650]);
node.currentStatus = crop_data ? "🎯 Auto-cropped by mask. APPROVE!" : "🖱️ Drag to select crop area, then APPROVE!";
if (!isNodeFullyVisible()) {
app.canvas.centerOnNode(node);
}
node.setDirtyCanvas(true, true);
}
});
createOverlayCanvas();
} catch (error) {
console.error("[RS Crop 🦊] Critical Error:", error);
}
};
},
setup() {
}
});