From d582c8649b0c27585a07deecfdc2bd80b5571331 Mon Sep 17 00:00:00 2001 From: Raykosan <71210053+Raykosan@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:07:41 +0300 Subject: [PATCH] Modification of the RS Crop Image node --- Rayko_Crop_Image.py | 25 +++--- __init__.py | 2 +- package.json | 2 +- pyproject.toml | 2 +- web/rs_crop.js | 211 ++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 209 insertions(+), 33 deletions(-) diff --git a/Rayko_Crop_Image.py b/Rayko_Crop_Image.py index 79c8e0a..1d27ff2 100644 --- a/Rayko_Crop_Image.py +++ b/Rayko_Crop_Image.py @@ -36,6 +36,14 @@ class RSCropImage: inputs = { "required": { "image": ("IMAGE",), + "multiple_mode": ("BOOLEAN", { + "default": False, + "label_on": "ON", + "label_off": "OFF", + }), + "multiple_of": (["4", "8", "16", "32", "64"], { + "default": "8", + }), }, "optional": { "crop_data": ("STRING", {"multiline": False, "default": "{}"}), @@ -54,7 +62,8 @@ class RSCropImage: CATEGORY = "🦊 RaykoStudio" OUTPUT_NODE = True - def crop_image(self, image, crop_data="{}", unique_id=None, prompt_id=None): + def crop_image(self, image, multiple_mode=False, multiple_of="8", + crop_data="{}", unique_id=None, prompt_id=None): unique_id = str(unique_id) if unique_id else "unknown" if SERVER_AVAILABLE and unique_id: @@ -120,10 +129,10 @@ class RSCropImage: except Exception: crop_dict = {} - x = int(crop_dict.get("x", 0)) - y = int(crop_dict.get("y", 0)) - width = int(crop_dict.get("width", 512)) - height = int(crop_dict.get("height", 512)) + x = round(crop_dict.get("x", 0)) + y = round(crop_dict.get("y", 0)) + width = round(crop_dict.get("width", 512)) + height = round(crop_dict.get("height", 512)) if image is None or len(image) == 0: h, w = 512, 512 @@ -180,19 +189,15 @@ if SERVER_AVAILABLE: PENDING_DECISIONS[node_id]["status"] = "approved" if crop_data: PENDING_DECISIONS[node_id]["crop_data"] = crop_data - elif decision == "reject": PENDING_DECISIONS[node_id]["status"] = "rejected" if crop_data is not None: PENDING_DECISIONS[node_id]["crop_data"] = crop_data - elif decision == "cancel": PENDING_DECISIONS[node_id]["status"] = "cancelled" - return web.Response(status=200, text="Decision recorded") else: return web.Response(status=404, text=f"Node {node_id} not waiting") - except Exception as e: return web.Response(status=500, text=str(e)) @@ -201,13 +206,11 @@ if SERVER_AVAILABLE: try: data = await request.json() node_id = str(data.get("node_id")) - if node_id in PENDING_DECISIONS: PENDING_DECISIONS[node_id]["status"] = "removed" return web.Response(status=200, text="Cleanup recorded") else: return web.Response(status=200, text="Node not found") - except Exception as e: return web.Response(status=500, text=str(e)) diff --git a/__init__.py b/__init__.py index fe10c33..2e06525 100644 --- a/__init__.py +++ b/__init__.py @@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'} REQUIRED_MODULES: Set[str] = set() PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio" -__version__ = "0.20.1" +__version__ = "0.20.2" def load_modules(): print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}") diff --git a/package.json b/package.json index 5e6bd3e..890ca43 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.20.1", + "version": "0.20.2", "description": "Set of custom nodes for ComfyUI providing additional image processing capabilities", "author": "Raykosan", "license": "Apache License 2.0", diff --git a/pyproject.toml b/pyproject.toml index 3be882f..dfaf7b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI_RaykoStudio" description = "Set of custom nodes for ComfyUI providing additional image processing capabilities" -version = "0.20.1" +version = "0.20.2" license = { file = "LICENSE" } dependencies = ["torch>=1.7.0", "numpy>=1.19.0", "Pillow>=8.0.0", "freetype-py>=2.5.1"] diff --git a/web/rs_crop.js b/web/rs_crop.js index f3ffe84..60a71c9 100644 --- a/web/rs_crop.js +++ b/web/rs_crop.js @@ -30,6 +30,42 @@ app.registerExtension({ }; } + 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, @@ -61,8 +97,115 @@ app.registerExtension({ } 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(node._cropRect); + const jsonStr = JSON.stringify(roundRect(node._cropRect)); node.properties = node.properties || {}; node.properties.crop_rect = jsonStr; if (cropDataWidget) cropDataWidget.value = jsonStr; @@ -303,6 +446,7 @@ app.registerExtension({ } } + applyClamp(); updateCropRect(); }); @@ -311,6 +455,12 @@ app.registerExtension({ _isDragging = false; _isResizing = false; _resizeHandle = null; + + if (modeWidget && ofWidget && modeWidget.value && node.imageReady) { + applyAlignment(); + updateCropRect(); + } + node._overlayCanvas.releasePointerCapture(e.pointerId || 1); }); @@ -350,10 +500,6 @@ app.registerExtension({ 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); - - ctx.fillStyle = "#ffffff"; - ctx.font = "bold 12px Arial"; - ctx.fillText(`${Math.round(node._cropRect.width)} x ${Math.round(node._cropRect.height)}`, cropX + 5, cropY + 20); }; node.onDrawForeground = function(ctx) { @@ -378,6 +524,34 @@ app.registerExtension({ 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; @@ -407,10 +581,10 @@ app.registerExtension({ ctx.fillText("Waiting for Image...", w / 2, startY + availableHeight / 2); } - ctx.fillStyle = "#888"; + ctx.fillStyle = "#aaaaaa"; ctx.font = "11px Arial"; ctx.textAlign = "center"; - ctx.fillText(this.currentStatus, w / 2, h - 50); + ctx.fillText(this.currentStatus, w / 2, h - 60); const btnH = 28; const btnY = h - 40; @@ -470,7 +644,7 @@ app.registerExtension({ }; node.sendDecision = async function(decision) { - const currentCropData = JSON.stringify(node._cropRect); + const currentCropData = JSON.stringify(roundRect(node._cropRect)); node.properties = node.properties || {}; node.properties.crop_rect = currentCropData; @@ -482,11 +656,13 @@ app.registerExtension({ if (decision === "cancel") { try { await api.interrupt(); - } catch (e) { console.error("Interrupt failed:", e); } + } catch (e) {} } if (decision === "reject") { node._cropRect = { x: 0, y: 0, width: 256, height: 256 }; + applyClamp(); + applyAlignment(); updateCropRect(); } @@ -531,7 +707,7 @@ app.registerExtension({ body: JSON.stringify({ node_id: this.id.toString() }) - }).catch(err => {}); + }).catch(() => {}); }; api.addEventListener("rayko.rscrop.show", (event) => { @@ -541,25 +717,20 @@ app.registerExtension({ if (String(node_id) === String(currentId)) { node.image.src = image_url + "&t=" + Date.now(); node.image.onload = () => { - console.log("[RS Crop 🦊] Image loaded, showing overlay"); node.imageReady = true; node.imageWidth = image_width || node.image.width; node.imageHeight = image_height || node.image.height; if (node._overlayCanvas) { node._overlayCanvas.style.display = "block"; - console.log("[RS Crop 🦊] Overlay display set to block"); } if (node.properties?.crop_rect) { try { const r = JSON.parse(node.properties.crop_rect); - if (r.x >= node.imageWidth) r.x = 0; - if (r.y >= node.imageHeight) r.y = 0; - if (r.x + r.width > node.imageWidth) r.width = Math.max(64, node.imageWidth - r.x); - if (r.y + r.height > node.imageHeight) r.height = Math.max(64, node.imageHeight - r.y); - node.properties.crop_rect = JSON.stringify(r); - node._cropRect = r; + if (r.x !== undefined) node._cropRect = r; + applyClamp(); + applyAlignment(); } catch (e) {} } @@ -573,7 +744,9 @@ app.registerExtension({ if (node.size[1] < 650) node.setSize([node.size[0], 650]); node.currentStatus = "🖱️ Drag to select crop area, then APPROVE!"; - app.canvas.centerOnNode(node); + if (!isNodeFullyVisible()) { + app.canvas.centerOnNode(node); + } node.setDirtyCanvas(true, true); } });