782 lines
37 KiB
JavaScript
782 lines
37 KiB
JavaScript
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", "#dc3545", () => node.sendDecision("reject"));
|
||
node.addButton("❌ CANCEL", "#666666", () => 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 startSyncLoop = () => {
|
||
if (_syncRunning) return;
|
||
_syncRunning = true;
|
||
const syncLoop = () => {
|
||
if (!_syncRunning) return;
|
||
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;
|
||
|
||
node._overlayCanvas.style.display = "block";
|
||
drawOverlay();
|
||
}
|
||
};
|
||
|
||
const createOverlayCanvas = () => {
|
||
if (node._overlayCanvas) return;
|
||
node._overlayCanvas = document.createElement("canvas");
|
||
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._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();
|
||
|
||
if (node._overlayCanvas) {
|
||
node._overlayCanvas.style.display = "block";
|
||
}
|
||
|
||
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() {
|
||
}
|
||
}); |