From 063e800515aaa1dbb673699c572a38f6686c9996 Mon Sep 17 00:00:00 2001 From: Raykosan <71210053+Raykosan@users.noreply.github.com> Date: Sun, 31 May 2026 12:30:02 +0300 Subject: [PATCH] Complete modification of the RS Collage node --- Rayko_Collage.py | 130 +++---- __init__.py | 2 +- package.json | 2 +- pyproject.toml | 2 +- web/rs_collage.js | 918 ++++++++++++++++++++++++++++------------------ 5 files changed, 620 insertions(+), 434 deletions(-) diff --git a/Rayko_Collage.py b/Rayko_Collage.py index 67544b6..b451aa2 100644 --- a/Rayko_Collage.py +++ b/Rayko_Collage.py @@ -1,18 +1,4 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright 2025-2026 Raykosan (RaykoStudio) -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - +import math import torch import numpy as np import os @@ -31,23 +17,19 @@ async def transform_handler(request): data = await request.json() node_id = str(data.get("id")) transforms = data.get("transforms") - if node_id is None or transforms is None: return web.json_response({"error": "Missing data"}, status=400) - TRANSFORM_CACHE[node_id] = { **transforms, "opacity": data.get("opacity", 1.0), "feather_type": data.get("feather_type", "None"), - "edge_radius": data.get("edge_radius", 300), - "shape_radius": data.get("shape_radius", 0), + "blur_radius": data.get("blur_radius", 50), + "blur_hardness": data.get("blur_hardness", 0), "feather_center_x": data.get("feather_center_x", 0.5), "feather_center_y": data.get("feather_center_y", 0.5) } - if node_id in PENDING_TRANSFORMS: PENDING_TRANSFORMS[node_id]["status"] = "completed" - return web.json_response({"status": "ok"}) except Exception as e: return web.json_response({"error": str(e)}, status=500) @@ -74,8 +56,10 @@ async def cleanup_handler(request): temp_dir = folder_paths.get_temp_directory() for f in os.listdir(temp_dir): if f.startswith(f"rs_collage_{node_id}_"): - try: os.remove(os.path.join(temp_dir, f)) - except: pass + try: + os.remove(os.path.join(temp_dir, f)) + except: + pass return web.json_response({"status": "ok"}) except Exception as e: return web.json_response({"error": str(e)}, status=500) @@ -88,9 +72,9 @@ class RSCollage: "overlay_image": ("IMAGE",), "background_image": ("IMAGE",), "opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), - "feather_type": (["None", "Radial In", "Radial Out", "Edge", "Shape"], {"default": "None"}), - "edge_radius": ("INT", {"default": 300, "min": 0, "max": 300, "step": 1}), - "shape_radius": ("INT", {"default": 0, "min": 0, "max": 5, "step": 1}), + "feather_type": (["None", "Radial Blur In", "Radial Blur Out", "Ellipse Blur In", "Ellipse Blur Out"], {"default": "None"}), + "blur_radius": ("INT", {"default": 50, "min": 0, "max": 100, "step": 1}), + "blur_hardness": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}), }, "optional": {"overlay_mask": ("MASK",)}, "hidden": {"unique_id": "UNIQUE_ID"} @@ -108,39 +92,47 @@ class RSCollage: arr[black, 3] = 0 return Image.fromarray(arr, 'RGBA') - def apply_radial_feather(self, img, edge_radius, cx, cy, invert=False): + def apply_radial_feather(self, img, blur_radius, blur_hardness, cx, cy, invert=False): if img.mode != 'RGBA': img = img.convert('RGBA') arr = np.array(img).astype(np.float32) h, w = arr.shape[:2] cx_px, cy_px = w * cx, h * cy y, x = np.ogrid[:h, :w] dist = np.hypot(x - cx_px, y - cy_px) - max_dist = max(np.hypot(cx_px, cy_px), np.hypot(w-cx_px, cy_px), np.hypot(cx_px, h-cy_px), np.hypot(w-cx_px, h-cy_px)) or 1 - feather_width = max((edge_radius / 300.0) * max_dist, 1.0) - mask = 1.0 - np.clip(dist / feather_width, 0.0, 1.0) + max_dist = max(math.hypot(cx_px, cy_px), math.hypot(w-1-cx_px, cy_px), math.hypot(cx_px, h-1-cy_px), math.hypot(w-1-cx_px, h-1-cy_px)) or 1.0 + ratio, hardness = blur_radius/100.0, blur_hardness/100.0 + overshoot = 1.15 + (1.0 - hardness) * 0.15 + fw = max(ratio * max_dist * overshoot, 1e-6) + norm = np.clip(dist / fw, 0.0, 1.0) + exp = 1.0 + hardness * 30.0 + mask = 1.0 - np.power(norm, exp) + if hardness >= 0.5: mask = np.where(mask < 0.02, 0.0, np.where(mask > 0.98, 1.0, mask)) if invert: mask = 1.0 - mask arr[:,:,3] *= mask return Image.fromarray(arr.astype(np.uint8), 'RGBA') - def apply_edge_feather(self, img, edge_radius): + def apply_ellipse_feather(self, img, blur_radius, blur_hardness, cx, cy, invert=False): if img.mode != 'RGBA': img = img.convert('RGBA') arr = np.array(img).astype(np.float32) h, w = arr.shape[:2] + cx_px, cy_px = w * cx, h * cy y, x = np.ogrid[:h, :w] - dist = np.minimum(np.minimum(x, w-1-x), np.minimum(y, h-1-y)) - max_dist = min(w, h) / 2.0 or 1 - feather_width = max((edge_radius / 300.0) * max_dist, 1.0) - mask = np.clip(dist / feather_width, 0.0, 1.0) + dX = max(cx_px, w-1-cx_px) or 1.0 + dY = max(cy_px, h-1-cy_px) or 1.0 + dx = np.abs(x - cx_px) / dX + dy = np.abs(y - cy_px) / dY + E = np.hypot(dx, dy) + ratio, hardness = blur_radius/100.0, blur_hardness/100.0 + overshoot = 1.15 + (1.0 - hardness) * 0.15 + fw = max(ratio * overshoot, 1e-6) + norm = np.clip(E / fw, 0.0, 1.0) + exp = 1.0 + hardness * 30.0 + mask = 1.0 - np.power(norm, exp) + if hardness >= 0.5: mask = np.where(mask < 0.02, 0.0, np.where(mask > 0.98, 1.0, mask)) + if invert: mask = 1.0 - mask arr[:,:,3] *= mask return Image.fromarray(arr.astype(np.uint8), 'RGBA') - def apply_shape_feather(self, img, shape_radius): - if shape_radius <= 0: return img - if img.mode != 'RGBA': img = img.convert('RGBA') - r, g, b, a = img.split() - a = a.filter(ImageFilter.GaussianBlur(radius=shape_radius)) - return Image.merge('RGBA', (r, g, b, a)) - def tensor2pil(self, tensor): if len(tensor.shape) == 4: if tensor.shape[0] > 1: tensor = tensor[0] @@ -164,16 +156,11 @@ class RSCollage: elif pil.mode != 'RGB': pass return torch.from_numpy(np.array(pil).astype(np.float32) / 255.0).unsqueeze(0) - def composite(self, overlay_image, background_image, opacity=1.0, feather_type="None", edge_radius=0, shape_radius=0, overlay_mask=None, unique_id=None): + def composite(self, overlay_image, background_image, opacity=1.0, feather_type="None", blur_radius=50, blur_hardness=0, overlay_mask=None, unique_id=None): unique_id = str(unique_id) if unique_id is not None else "unknown" - - if overlay_image.numel() == 0 or background_image.numel() == 0: - return (background_image,) - + if overlay_image.numel() == 0 or background_image.numel() == 0: return (background_image,) h, w = overlay_image.shape[1], overlay_image.shape[2] - if h < 8 or w < 8: - return (background_image,) - + if h < 8 or w < 8: return (background_image,) TRANSFORM_CACHE.pop(unique_id, None) bg_pil = self.tensor2pil(background_image) @@ -191,38 +178,27 @@ class RSCollage: def apply_transforms(data): rx = bg_w/2 if data.get("x") is None else float(data.get("x")) ry = bg_h/2 if data.get("y") is None else float(data.get("y")) - sc_x = float(data.get("scale_x") or 1.0) sc_y = float(data.get("scale_y") or 1.0) - rot = float(data.get("rotation") or 0) fh = bool(data.get("flip_h")) fv = bool(data.get("flip_v")) op = float(data.get("opacity") or opacity) ft = data.get("feather_type") or feather_type - er = int(data.get("edge_radius") or edge_radius) - sr = int(data.get("shape_radius") or shape_radius) + br = int(data.get("blur_radius") or blur_radius) + bh = int(data.get("blur_hardness") or blur_hardness) fcx = float(data.get("feather_center_x") or 0.5) fcy = float(data.get("feather_center_y") or 0.5) rc = rot if (fh != fv) else -rot ov = ov_pil - - if sc_x != 1.0 or sc_y != 1.0: - new_w = max(1, int(ov.width * sc_x)) - new_h = max(1, int(ov.height * sc_y)) - ov = ov.resize((new_w, new_h), Image.Resampling.LANCZOS) - - if rc != 0: - ov = ov.rotate(rc, expand=True, resample=Image.Resampling.BICUBIC, center=(ov.width//2, ov.height//2)) - ov = self.remove_black_corners(ov) + if sc_x != 1.0 or sc_y != 1.0: ov = ov.resize((max(1, int(ov.width * sc_x)), max(1, int(ov.height * sc_y))), Image.Resampling.LANCZOS) + if rc != 0: ov = ov.rotate(rc, expand=True, resample=Image.Resampling.BICUBIC, center=(ov.width//2, ov.height//2)); ov = self.remove_black_corners(ov) if fh: ov = ImageOps.mirror(ov) if fv: ov = ImageOps.flip(ov) - if ft == "Radial Out" and er > 0: ov = self.apply_radial_feather(ov, er, fcx, fcy, invert=False) - elif ft == "Radial In" and er > 0: ov = self.apply_radial_feather(ov, er, fcx, fcy, invert=True) - elif ft == "Edge" and er > 0: ov = self.apply_edge_feather(ov, er) - elif ft == "Shape" and sr > 0: ov = self.apply_shape_feather(ov, sr) + if "Radial Blur" in ft and br > 0: ov = self.apply_radial_feather(ov, br, bh, fcx, fcy, invert="Out" in ft) + elif "Ellipse Blur" in ft and br > 0: ov = self.apply_ellipse_feather(ov, br, bh, fcx, fcy, invert="Out" in ft) if op < 1.0: r,g,b,a = ov.split() @@ -236,27 +212,25 @@ class RSCollage: td = folder_paths.get_temp_directory() for f in os.listdir(td): if f.startswith(f"rs_collage_{unique_id}_"): - try: os.remove(os.path.join(td, f)) - except: pass - + try: + os.remove(os.path.join(td, f)) + except: + pass bfn = f"rs_collage_{unique_id}_{ts}_bg.png" ofn = f"rs_collage_{unique_id}_{ts}_ov.png" bg_pil.save(os.path.join(td, bfn)) ov_pil.save(os.path.join(td, ofn)) PENDING_TRANSFORMS[unique_id] = {"status": "pending"} - PromptServer.instance.send_sync("rs-collage-start", { "id": unique_id, "bg_file": bfn, "ov_file": ofn, "bg_width": bg_w, "bg_height": bg_h, "ov_width": ov_pil.width, "ov_height": ov_pil.height, "timestamp": ts, - "opacity": opacity, "feather_type": feather_type, "edge_radius": edge_radius, "shape_radius": shape_radius + "opacity": opacity, "feather_type": feather_type, "blur_radius": blur_radius, "blur_hardness": blur_hardness }) print(f"[RS Collage] Node {unique_id} waiting for user input (Apply/Cancel)...") - while unique_id not in TRANSFORM_CACHE: time.sleep(0.2) - if unique_id in PENDING_TRANSFORMS: if PENDING_TRANSFORMS[unique_id].get("status") == "cancelled": print(f"[RS Collage] Cancelled by user for node {unique_id}") @@ -266,11 +240,11 @@ class RSCollage: print(f"[RS Collage] Input received for node {unique_id}") PENDING_TRANSFORMS.pop(unique_id, None) result = apply_transforms(TRANSFORM_CACHE.pop(unique_id)) - for f in [bfn, ofn]: - try: os.remove(os.path.join(td, f)) - except: pass - + try: + os.remove(os.path.join(td, f)) + except: + pass return (result,) @classmethod diff --git a/__init__.py b/__init__.py index 5022c31..bfbc001 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.19.0" +__version__ = "0.20.0" def load_modules(): print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}") diff --git a/package.json b/package.json index 23019b2..08854e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.19.0", + "version": "0.20.0", "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 69a041c..1b0823a 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.19.0" +version = "0.20.0" 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_collage.js b/web/rs_collage.js index 7e75675..d3f1a48 100644 --- a/web/rs_collage.js +++ b/web/rs_collage.js @@ -1,124 +1,460 @@ import { app } from "../../../scripts/app.js"; import { api } from "../../../scripts/api.js"; +let activeRSCollageNode = null; + app.registerExtension({ name: "RaykoStudio.RSCollage", async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== "RSCollage") return; + const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { if (onNodeCreated) onNodeCreated.apply(this, arguments); - - this.overlay = { x:0, y:0, width:100, height:100, rotation:0, flipH:false, flipV:false }; - this.overlayRelative = { x:0.5, y:0.5, width:0.3, height:0.3, rotation:0, flipH:false, flipV:false }; - this.realOverlay = { width:0, height:0 }; - this.realBackground = { width:0, height:0 }; - - this.displayWidth = 420; this.displayHeight = 420; this.canvasPixelSize = 420; - this.viewScale = 1.0; - this.viewOffsetX = 0; this.viewOffsetY = 0; - - this.overlayImage = null; this.backgroundImage = null; - this.isEditing = false; this.isLoading = false; this.dragType = null; + + // State + this.overlay = { x: 0, y: 0, width: 100, height: 100, rotation: 0, flipH: false, flipV: false }; + this.overlayRelative = { x: 0.5, y: 0.5, width: 0.3, height: 0.3, rotation: 0, flipH: false, flipV: false }; + this.realOverlay = { width: 0, height: 0 }; + this.realBackground = { width: 0, height: 0 }; + + // View + this.displayWidth = 420; + this.displayHeight = 420; + this.canvasPixelSize = 420; + this.viewScale = 1.0; + this.viewOffsetX = 0; + this.viewOffsetY = 0; + + // Images & Editing + this.overlayImage = null; + this.backgroundImage = null; + this.isEditing = false; + this.isLoading = false; + this.dragType = null; this.dragState = null; - this.currentSessionTimestamp = null; - - this.opacity = 1.0; this.featherType = "None"; - this.edgeRadius = 150; this.shapeRadius = 0; - - this.featherCenter = { x:0.5, y:0.5 }; - this.canvasRealWidth = 0; this.canvasRealHeight = 0; - this.minWidth = 500; this.minHeight = 780; + this.currentSessionTimestamp = null; + + // Parameters + this.opacity = 1.0; + this.featherType = "None"; + this.blurRadius = 50; + this.blurHardness = 0; + this.advancedMode = false; + + // Overlay DOM + this.overlayContainer = null; + this.overlayCanvas = null; + this.overlayCtx = null; + this.overlayRenderLoop = null; + this.overlayInputs = {}; + + // Misc + this.featherCenter = { x: 0.5, y: 0.5 }; + this.canvasRealWidth = 0; + this.canvasRealHeight = 0; + this.minWidth = 500; + this.minHeight = 780; this.setSize([this.minWidth, this.minHeight]); - this.btnApplyHover = false; this.btnFlipHHover = false; this.btnFlipVHover = false; this.btnCancelHover = false; - this.featherPreviewCanvas = null; this.previewDirty = true; this.previewMaxSize = 512; + + this.btnApplyHover = false; + this.btnFlipHHover = false; + this.btnFlipVHover = false; + this.btnCancelHover = false; + + this.featherPreviewCanvas = null; + this.previewDirty = true; + this.previewMaxSize = 512; this.pendingEditorData = null; - ["opacity","feather_type","edge_radius","shape_radius"].forEach(n => { - const w = this.widgets?.find(w => w.name === n); if (w) w.hidden = true; - }); - this.addWidget("slider", "opacity", 1.0, v => { this.opacity = v; this.setDirtyCanvas(true); }, { min:0, max:1, step:0.01 }); - this.addWidget("combo", "feather_type", "None", v => { this.featherType = v; this.previewDirty = true; this.setDirtyCanvas(true); }, { values:["None","Radial In","Radial Out","Edge","Shape"] }); - this.addWidget("slider", "edge_radius", 150, v => { this.edgeRadius = v; this.previewDirty = true; this.setDirtyCanvas(true); }, { min:0, max:300, step:1 }); - this.addWidget("slider", "shape_radius", 0, v => { this.shapeRadius = v; this.previewDirty = true; this.setDirtyCanvas(true); }, { min:0, max:5, step:1 }); - - api.addEventListener("rs-collage-start", (event) => { - if (event.detail.id != this.id) return; - this.pendingEditorData = event.detail; - this.openDeferredEditor(); + // Hide default widgets + ["opacity", "feather_type", "blur_radius", "blur_hardness"].forEach(n => { + const w = this.widgets?.find(w => w.name === n); + if (w) w.hidden = true; }); - api.addEventListener("rs-collage-ready", (event) => { - if (event.detail.id != this.id) return; - this.pendingEditorData = event.detail; - }); + // Add custom widgets + this.addWidget("slider", "opacity", 1.0, v => { this.opacity = v; this.previewDirty = true; this.setDirtyCanvas(true); }, { min: 0, max: 1, step: 0.01 }); + this.addWidget("combo", "feather_type", "None", v => { this.featherType = v; this.previewDirty = true; this.setDirtyCanvas(true); }, { values: ["None", "Radial Blur In", "Radial Blur Out", "Ellipse Blur In", "Ellipse Blur Out"] }); + this.addWidget("slider", "blur_radius", 50, v => { this.blurRadius = v; this.previewDirty = true; this.setDirtyCanvas(true); }, { min: 0, max: 100, step: 1 }); + this.addWidget("slider", "blur_hardness", 0, v => { this.blurHardness = v; this.previewDirty = true; this.setDirtyCanvas(true); }, { min: 0, max: 100, step: 1 }); - api.addEventListener("interrupted", () => { - this.pendingEditorData = null; - this.isLoading = false; - this.isEditing = false; - this.dragType = null; - this.dragState = null; + // Create Overlay DOM + if (!this.overlayContainer) { + this.overlayContainer = document.createElement('div'); + this.overlayContainer.style.cssText = 'position:fixed;top:60px;left:0;right:0;bottom:0;background:rgba(10,10,10,0.96);z-index:999;display:none;flex-direction:row;align-items:stretch;font-family:system-ui,-apple-system,sans-serif;'; + + this.overlayCanvasWrapper = document.createElement('div'); + this.overlayCanvasWrapper.style.cssText = 'flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative;'; + + this.overlayCanvas = document.createElement('canvas'); + this.overlayCanvas.style.cssText = 'box-shadow:0 8px 32px rgba(0,0,0,0.7);cursor:crosshair;max-width:98%;max-height:98%;border-radius:8px;'; + this.overlayCanvasWrapper.appendChild(this.overlayCanvas); + this.overlayCtx = this.overlayCanvas.getContext('2d'); + + this.sidePanel = document.createElement('div'); + this.sidePanel.style.cssText = 'width:260px;background:#151515;border-left:1px solid #333;padding:16px;display:flex;flex-direction:column;gap:12px;box-sizing:border-box;overflow-y:auto;'; + + const makeLabel = (txt) => { + const l = document.createElement('label'); + l.textContent = txt; + l.style.cssText = 'color:#aaa;font-size:11px;font-weight:600;margin-bottom:-4px;display:block;'; + return l; + }; + const makeInput = (type, min, max, step, value) => { + const i = document.createElement('input'); + i.type = type; i.min = min; i.max = max; i.step = step; i.value = value; + i.style.cssText = 'width:100%;background:#252525;color:#eee;border:1px solid #444;border-radius:4px;padding:4px 8px;font-size:12px;outline:none;'; + return i; + }; + const makeSelect = (opts, val) => { + const s = document.createElement('select'); + opts.forEach(o => { + const op = document.createElement('option'); + op.value = o; op.textContent = o; + if (o === val) op.selected = true; + s.appendChild(op); + }); + s.style.cssText = 'width:100%;background:#252525;color:#eee;border:1px solid #444;border-radius:4px;padding:6px;font-size:12px;outline:none;'; + return s; + }; + const makeBtn = (txt, col, onClick) => { + const b = document.createElement('button'); + b.textContent = txt; + b.style.cssText = `width:100%;padding:10px;background:#222;color:${col};border:1px solid ${col};border-radius:6px;cursor:pointer;font-weight:600;font-size:12px;margin-top:4px;transition:0.15s;`; + b.onmouseenter = () => { b.style.background = '#2a2a2a'; b.style.transform = 'translateY(-1px)'; }; + b.onmouseleave = () => { b.style.background = '#222'; b.style.transform = 'none'; }; + b.onclick = (e) => { e.stopPropagation(); onClick(); }; + return b; + }; + + this.sidePanel.appendChild(makeLabel("OPACITY")); + this.overlayInputs.opacity = makeInput("range", 0, 100, 1, 100); this.sidePanel.appendChild(this.overlayInputs.opacity); + this.sidePanel.appendChild(makeLabel("FEATHER TYPE")); + this.overlayInputs.featherType = makeSelect(["None", "Radial Blur In", "Radial Blur Out", "Ellipse Blur In", "Ellipse Blur Out"], "None"); this.sidePanel.appendChild(this.overlayInputs.featherType); + this.sidePanel.appendChild(makeLabel("BLUR RADIUS")); + this.overlayInputs.blurRadius = makeInput("range", 0, 100, 1, 50); this.sidePanel.appendChild(this.overlayInputs.blurRadius); + this.sidePanel.appendChild(makeLabel("BLUR HARDNESS")); + this.overlayInputs.blurHardness = makeInput("range", 0, 100, 1, 0); this.sidePanel.appendChild(this.overlayInputs.blurHardness); + + const sync = (key, type) => { + const el = this.overlayInputs[key]; + const handler = () => { + if (key === 'opacity') this.opacity = parseFloat(el.value) / 100; + else if (type === 'float') this[key] = parseFloat(el.value); + else this[key] = parseInt(el.value); + this.previewDirty = true; + this.setDirtyCanvas(true); + }; + el.addEventListener('input', handler); + el.addEventListener('change', handler); + }; + sync('opacity', 'float'); + this.overlayInputs.featherType.addEventListener('change', () => { this.featherType = this.overlayInputs.featherType.value; this.previewDirty = true; this.setDirtyCanvas(true); }); + sync('blurRadius', 'int'); + sync('blurHardness', 'int'); + + const div = () => { const d = document.createElement('div'); d.style.cssText = 'height:1px;background:#333;margin:8px 0;'; return d; }; + this.sidePanel.appendChild(div()); + this.sidePanel.appendChild(makeBtn("↔️ FLIP HORIZONTAL", "#2196F3", () => { this.overlay.flipH = !this.overlay.flipH; this.updateRelativeFromAbsolute(); this.previewDirty = true; this.setDirtyCanvas(true); })); + this.sidePanel.appendChild(makeBtn("↕️ FLIP VERTICAL", "#2196F3", () => { this.overlay.flipV = !this.overlay.flipV; this.updateRelativeFromAbsolute(); this.previewDirty = true; this.setDirtyCanvas(true); })); + this.sidePanel.appendChild(div()); + this.btnNormal = makeBtn("↩️ NORMAL MODE", "#607D8B", () => this._toggleAdvancedMode()); this.sidePanel.appendChild(this.btnNormal); + this.btnApply = makeBtn("✔️ APPLY", "#4CAF50", () => { this.sendTransforms(); this._toggleAdvancedMode(); }); this.sidePanel.appendChild(this.btnApply); + this.btnCancel = makeBtn("❌ CANCEL", "#dc3545", () => { this.cancelEditing(); this._toggleAdvancedMode(); }); this.sidePanel.appendChild(this.btnCancel); + + this.overlayContainer.appendChild(this.overlayCanvasWrapper); + this.overlayContainer.appendChild(this.sidePanel); + document.body.appendChild(this.overlayContainer); + + this._resizeObserver = new ResizeObserver(() => this._resizeOverlayCanvas()); + this._resizeObserver.observe(this.overlayCanvasWrapper); + } + + // API Listeners + api.addEventListener("rs-collage-start", (event) => { if (event.detail.id != this.id) return; this.pendingEditorData = event.detail; this.openDeferredEditor(); }); + api.addEventListener("rs-collage-ready", (event) => { if (event.detail.id != this.id) return; this.pendingEditorData = event.detail; }); + api.addEventListener("interrupted", () => { this.pendingEditorData = null; this.isLoading = false; this.isEditing = false; this.dragType = null; this.dragState = null; this.setDirtyCanvas(true); }); + }; + + // Helpers + nodeType.prototype._syncOverlayUI = function() { + if (!this.advancedMode) return; + this.overlayInputs.opacity.value = Math.round(this.opacity * 100); + this.overlayInputs.featherType.value = this.featherType; + this.overlayInputs.blurRadius.value = this.blurRadius; + this.overlayInputs.blurHardness.value = this.blurHardness; + }; + + nodeType.prototype._resizeOverlayCanvas = function() { + if (!this.overlayCanvasWrapper || !this.overlayCanvas) return; + const w = this.overlayCanvasWrapper.clientWidth; + const h = this.overlayCanvasWrapper.clientHeight; + if (w > 0 && h > 0) { + this.overlayCanvas.width = w; + this.overlayCanvas.height = h; this.setDirtyCanvas(true); + if (this.advancedMode && this.isEditing) { + this.computeAndApplyView(); + } + } + }; + + nodeType.prototype._handleOverlayEvent = function(e, type) { + e.preventDefault(); + e.stopPropagation(); + const rect = this.overlayCanvas.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) return; + const scaleX = this.overlayCanvas.width / rect.width; + const scaleY = this.overlayCanvas.height / rect.height; + const x = (e.clientX - rect.left) * scaleX; + const y = (e.clientY - rect.top) * scaleY; + this.previewDirty = true; + if (type === 'down') this.onMouseDown(null, [x, y]); + else if (type === 'move') this.onMouseMove(null, [x, y]); + else this.onMouseUp(); + this.setDirtyCanvas(true); + }; + + nodeType.prototype._handleOverlayWheel = function(e) { + if (!this.advancedMode || !this.isEditing || activeRSCollageNode !== this) return; + e.preventDefault(); + e.stopPropagation(); + + const canvasRect = this.overlayCanvas.getBoundingClientRect(); + if (canvasRect.width === 0 || canvasRect.height === 0) return; + + const scaleX = this.overlayCanvas.width / canvasRect.width; + const scaleY = this.overlayCanvas.height / canvasRect.height; + const canvasX = (e.clientX - canvasRect.left) * scaleX; + const canvasY = (e.clientY - canvasRect.top) * scaleY; + + const { rectX, rectY } = this.getCanvasMetrics(); + const worldX = (canvasX - rectX - this.viewOffsetX) / this.viewScale; + const worldY = (canvasY - rectY - this.viewOffsetY) / this.viewScale; + + const zoomFactor = e.deltaY < 0 ? 1.15 : 0.85; + const oldScale = this.viewScale; + this.viewScale = Math.max(0.05, Math.min(10.0, this.viewScale * zoomFactor)); + + this.viewOffsetX = canvasX - rectX - (worldX * this.viewScale); + this.viewOffsetY = canvasY - rectY - (worldY * this.viewScale); + + this.previewDirty = true; + this.setDirtyCanvas(true); + }; + + nodeType.prototype._toggleAdvancedMode = function(forceClose = false) { + if (forceClose || this.advancedMode) { + this.advancedMode = false; + if (activeRSCollageNode === this) activeRSCollageNode = null; + this.overlayContainer.style.display = 'none'; + + if (this._overlayWheelHandler) { + this.overlayCanvas.removeEventListener('wheel', this._overlayWheelHandler); + delete this._overlayWheelHandler; + } + if (this._overlayMouseHandler) { + this.overlayCanvas.removeEventListener('mousedown', this._overlayMouseHandler); + this.overlayCanvas.removeEventListener('mousemove', this._overlayMouseHandler); + this.overlayCanvas.removeEventListener('mouseup', this._overlayMouseHandler); + this.overlayCanvas.removeEventListener('mouseleave', this._overlayMouseLeaveHandler); + delete this._overlayMouseHandler; + delete this._overlayMouseLeaveHandler; + } + if (this._globalKeyHandler) window.removeEventListener('keydown', this._globalKeyHandler); + if (this.overlayRenderLoop) cancelAnimationFrame(this.overlayRenderLoop); + this.setDirtyCanvas(true); + return; + } + + if (activeRSCollageNode && activeRSCollageNode !== this) { + activeRSCollageNode._toggleAdvancedMode(true); + } + + this.advancedMode = true; + activeRSCollageNode = this; + this.overlayContainer.style.display = 'flex'; + this._syncOverlayUI(); + + requestAnimationFrame(() => { + this._resizeOverlayCanvas(); + this.computeAndApplyView(); }); + + // Mouse handlers + this._overlayMouseHandler = (e) => this._handleOverlayEvent(e, e.type === 'mousedown' ? 'down' : e.type === 'mousemove' ? 'move' : 'up'); + this._overlayMouseLeaveHandler = () => this.onMouseUp(); + this.overlayCanvas.addEventListener('mousedown', this._overlayMouseHandler); + this.overlayCanvas.addEventListener('mousemove', this._overlayMouseHandler); + this.overlayCanvas.addEventListener('mouseup', this._overlayMouseHandler); + this.overlayCanvas.addEventListener('mouseleave', this._overlayMouseLeaveHandler); + + // Wheel handler (passive: false is mandatory for preventDefault) + this._overlayWheelHandler = (e) => this._handleOverlayWheel(e); + this.overlayCanvas.addEventListener('wheel', this._overlayWheelHandler, { passive: false }); + + // Keyboard + this._globalKeyHandler = (e) => { + if (e.key === 'Escape' && activeRSCollageNode === this) { + this.sendTransforms(); + this._toggleAdvancedMode(); + } + }; + window.addEventListener('keydown', this._globalKeyHandler); + + // Render loop (clears canvas before drawing to prevent phantom) + this.overlayRenderLoop = () => { + if (!this.advancedMode || !this.overlayCtx) return; + this.overlayCtx.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height); + this.drawOverlayCanvas(this.overlayCtx); + requestAnimationFrame(this.overlayRenderLoop); + }; + requestAnimationFrame(this.overlayRenderLoop); + }; + + // Dedicated overlay drawing method (separated from node drawing) + nodeType.prototype.drawOverlayCanvas = function(ctx) { + if (!this.isEditing || !this.backgroundImage) { + ctx.fillStyle = "#888"; + ctx.font = "14px Arial"; + ctx.textAlign = "center"; + ctx.fillText("Loading...", this.overlayCanvas.width / 2, this.overlayCanvas.height / 2); + return; + } + + const { rectX, rectY } = this.getCanvasMetrics(); + const useScale = this.viewScale; + const useOffsetX = this.viewOffsetX; + const useOffsetY = this.viewOffsetY; + + ctx.save(); + ctx.translate(rectX + useOffsetX, rectY + useOffsetY); + ctx.scale(useScale, useScale); + + ctx.drawImage(this.backgroundImage, -this.displayWidth / 2, -this.displayHeight / 2, this.displayWidth, this.displayHeight); + + if (this.overlayImage) { + if (this.previewDirty) this.generateFeatherPreview(); + const prev = this.featherPreviewCanvas || this.overlayImage; + + ctx.save(); + ctx.translate(this.overlay.x, this.overlay.y); + ctx.rotate(this.overlay.rotation * Math.PI / 180); + ctx.scale(this.overlay.flipH ? -1 : 1, this.overlay.flipV ? -1 : 1); + ctx.globalAlpha = this.opacity; + ctx.drawImage(prev, -this.overlay.width / 2, -this.overlay.height / 2, this.overlay.width, this.overlay.height); + ctx.globalAlpha = 1; + + ctx.shadowColor = "rgba(0,0,0,0.8)"; + ctx.shadowBlur = 4 / useScale; + ctx.strokeStyle = "#00E5FF"; + ctx.lineWidth = 2 / useScale; + ctx.strokeRect(-this.overlay.width / 2, -this.overlay.height / 2, this.overlay.width, this.overlay.height); + ctx.shadowColor = "transparent"; + ctx.shadowBlur = 0; + + const hw = this.overlay.width / 2, hh = this.overlay.height / 2; + const hs = 6 / useScale; + ctx.fillStyle = "#FF0000"; + [[hw, hh], [-hw, hh], [hw, -hh], [-hw, -hh]].forEach(([x, y]) => ctx.fillRect(x - hs / 2, y - hs / 2, hs, hs)); + [[hw, 0], [-hw, 0], [0, hh], [0, -hh]].forEach(([x, y]) => ctx.fillRect(x - hs / 2, y - hs / 2, hs, hs)); + + const rotHandleY = -hh - 40; + ctx.beginPath(); + ctx.arc(0, rotHandleY, 5 / useScale, 0, Math.PI * 2); + ctx.fillStyle = "#ff9800"; + ctx.fill(); + ctx.strokeStyle = "#fff"; + ctx.lineWidth = 1 / useScale; + ctx.stroke(); + + if (this.featherType.includes("Blur") && this.blurRadius > 0) { + const fCx = (this.featherCenter.x - 0.5) * this.overlay.width; + const fCy = (this.featherCenter.y - 0.5) * this.overlay.height; + ctx.strokeStyle = "#FF0000"; + ctx.lineWidth = 2 / useScale; + ctx.beginPath(); + ctx.moveTo(fCx - 12, fCy); + ctx.lineTo(fCx + 12, fCy); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(fCx, fCy - 12); + ctx.lineTo(fCx, fCy + 12); + ctx.stroke(); + ctx.fillStyle = "#2196F3"; + ctx.beginPath(); + ctx.arc(fCx, fCy, 4 / useScale, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = "#FFFFFF"; + ctx.lineWidth = 1 / useScale; + ctx.stroke(); + } + ctx.restore(); + } + ctx.restore(); + + ctx.fillStyle = "#ff9800"; + ctx.font = "12px Arial"; + ctx.textAlign = "left"; + ctx.fillText(`EDITING (Scale: ${(useScale * 100).toFixed(0)}%)`, this.overlayCanvas.width - 200, this.overlayCanvas.height - 20); }; nodeType.prototype.openDeferredEditor = function() { if (!this.pendingEditorData) return; const data = this.pendingEditorData; - this.overlayImage = null; this.backgroundImage = null; this.isLoading = true; + this.overlayImage = null; + this.backgroundImage = null; + this.isLoading = true; this.currentSessionTimestamp = data.timestamp; this.opacity = data.opacity !== undefined ? data.opacity : 1.0; this.featherType = data.feather_type || "None"; - this.edgeRadius = data.edge_radius || 150; - this.shapeRadius = data.shape_radius || 0; - this.featherCenter = { x:0.5, y:0.5 }; this.previewDirty = true; - - ["opacity","feather_type","edge_radius","shape_radius"].forEach(n => { - const w = this.widgets?.find(w => w.name === n); if (w) w.value = this[n]; + this.blurRadius = data.blur_radius !== undefined ? data.blur_radius : 50; + this.blurHardness = data.blur_hardness !== undefined ? data.blur_hardness : 0; + this.featherCenter = { x: 0.5, y: 0.5 }; + this.previewDirty = true; + + ["opacity", "feather_type", "blur_radius", "blur_hardness"].forEach(n => { + const w = this.widgets?.find(w => w.name === n); + if (w) w.value = this[n]; }); - this.realBackground = { width:data.bg_width, height:data.bg_height }; - this.realOverlay = { width:data.ov_width, height:data.ov_height }; + this.realBackground = { width: data.bg_width, height: data.bg_height }; + this.realOverlay = { width: data.ov_width, height: data.ov_height }; this.canvasRealWidth = this.realBackground.width; this.canvasRealHeight = this.realBackground.height; - this.updateDisplaySize(this.canvasPixelSize); - + const bgFile = data.bg_file, ovFile = data.ov_file, ts = data.timestamp; let loaded = 0; const onLoad = () => { - loaded++; + loaded++; if (loaded === 2) { requestAnimationFrame(() => { this.isLoading = false; const tS = this.canvasPixelSize * 0.5; const sM = Math.min(this.realOverlay.width, this.realOverlay.height); - let sc = tS / sM, nw = this.realOverlay.width*sc, nh = this.realOverlay.height*sc; - if (nw > this.canvasPixelSize) { nw = this.canvasPixelSize; nh = nw/(this.realOverlay.width/this.realOverlay.height); } - if (nh > this.canvasPixelSize) { nh = this.canvasPixelSize; nw = nh*(this.realOverlay.width/this.realOverlay.height); } - - this.overlayRelative = { - width: nw/this.displayWidth, height: nh/this.displayHeight, - x: 0.5, y: 0.5, rotation:0, flipH:false, flipV:false - }; - + let sc = tS / sM, nw = this.realOverlay.width * sc, nh = this.realOverlay.height * sc; + if (nw > this.canvasPixelSize) { nw = this.canvasPixelSize; nh = nw / (this.realOverlay.width / this.realOverlay.height); } + if (nh > this.canvasPixelSize) { nh = this.canvasPixelSize; nw = nh * (this.realOverlay.width / this.realOverlay.height); } + this.overlayRelative = { width: nw / this.displayWidth, height: nh / this.displayHeight, x: 0.5, y: 0.5, rotation: 0, flipH: false, flipV: false }; this.updateOverlayAbsolute(); this.computeAndApplyView(); - - this.isEditing = true; + this.isEditing = true; this.setDirtyCanvas(true); }); } }; const loadImg = (file, type) => { if (!file) { onLoad(); return; } - const img = new Image(); img.crossOrigin = "Anonymous"; + const img = new Image(); + img.crossOrigin = "Anonymous"; img.onload = () => { this[type] = img; onLoad(); }; img.onerror = () => { onLoad(); }; img.src = `/view?filename=${file}&type=temp&t=${ts}`; }; - loadImg(bgFile, "backgroundImage"); loadImg(ovFile, "overlayImage"); + loadImg(bgFile, "backgroundImage"); + loadImg(ovFile, "overlayImage"); }; nodeType.prototype.updateOverlayAbsolute = function() { @@ -143,386 +479,262 @@ app.registerExtension({ nodeType.prototype.computeAndApplyView = function() { const bgW = this.displayWidth, bgH = this.displayHeight; - const ovL = this.overlay.x - this.overlay.width/2, ovT = this.overlay.y - this.overlay.height/2; - const ovR = this.overlay.x + this.overlay.width/2, ovB = this.overlay.y + this.overlay.height/2; - const bgL = -bgW/2, bgT = -bgH/2, bgR = bgW/2, bgB = bgH/2; + const ovL = this.overlay.x - this.overlay.width / 2, ovT = this.overlay.y - this.overlay.height / 2; + const ovR = this.overlay.x + this.overlay.width / 2, ovB = this.overlay.y + this.overlay.height / 2; + const bgL = -bgW / 2, bgT = -bgH / 2, bgR = bgW / 2, bgB = bgH / 2; + const minX = Math.min(ovL, bgL), minY = Math.min(ovT, bgT), maxX = Math.max(ovR, bgR), maxY = Math.max(ovB, bgB); + const contentW = Math.max(1, maxX - minX), contentH = Math.max(1, maxY - minY); + const contentCX = (minX + maxX) / 2, contentCY = (minY + maxY) / 2; - const minX = Math.min(ovL, bgL), minY = Math.min(ovT, bgT); - const maxX = Math.max(ovR, bgR), maxY = Math.max(ovB, bgB); - const contentW = Math.max(1, maxX - minX); - const contentH = Math.max(1, maxY - minY); - const contentCX = (minX + maxX) / 2; - const contentCY = (minY + maxY) / 2; - - const availableW = this.canvasPixelSize * 0.9; - const availableH = this.canvasPixelSize * 0.9; - const scaleX = availableW / contentW; - const scaleY = availableH / contentH; - this.viewScale = Math.max(0.1, Math.min(3.0, Math.min(scaleX, scaleY))); - - this.viewOffsetX = this.canvasPixelSize / 2 - (contentCX * this.viewScale); - this.viewOffsetY = this.canvasPixelSize / 2 - (contentCY * this.viewScale); + if (this.advancedMode) { + const cw = this.overlayCanvas.width || (this.overlayCanvasWrapper ? this.overlayCanvasWrapper.clientWidth : 1000); + const ch = this.overlayCanvas.height || (this.overlayCanvasWrapper ? this.overlayCanvasWrapper.clientHeight : 800); + const availableW = cw * 0.8, availableH = ch * 0.8; + this.viewScale = Math.max(0.1, Math.min(3.0, Math.min(availableW / contentW, availableH / contentH))); + this.viewOffsetX = cw / 2 - (contentCX * this.viewScale); + this.viewOffsetY = ch / 2 - (contentCY * this.viewScale); + } else { + const availableW = this.canvasPixelSize * 0.95, availableH = this.canvasPixelSize * 0.95; + this.viewScale = Math.max(0.1, Math.min(3.0, Math.min(availableW / contentW, availableH / contentH))); + this.viewOffsetX = this.canvasPixelSize / 2 - (contentCX * this.viewScale); + this.viewOffsetY = this.canvasPixelSize / 2 - (contentCY * this.viewScale); + } }; nodeType.prototype.updateDisplaySize = function(cS) { this.canvasPixelSize = cS; const safeHeight = this.realBackground.height || 1; const bgAR = this.realBackground.width / safeHeight; - if (bgAR >= 1) { - this.displayWidth = cS; - this.displayHeight = cS / bgAR; - } else { - this.displayHeight = cS; - this.displayWidth = cS * bgAR; - } + if (bgAR >= 1) { this.displayWidth = cS; this.displayHeight = cS / bgAR; } + else { this.displayHeight = cS; this.displayWidth = cS * bgAR; } }; nodeType.prototype.generateFeatherPreview = function() { if (!this.overlayImage || this.featherType === "None") { this.featherPreviewCanvas = null; this.previewDirty = false; return; } - const isShape = this.featherType === "Shape"; - const rVal = isShape ? this.shapeRadius : this.edgeRadius; - if (rVal <= 0 && !isShape) { this.featherPreviewCanvas = null; this.previewDirty = false; return; } + const rVal = this.blurRadius; + if (rVal <= 0) { this.featherPreviewCanvas = null; this.previewDirty = false; return; } const mW = Math.min(this.realOverlay.width, this.previewMaxSize), mH = Math.min(this.realOverlay.height, this.previewMaxSize); - const sc = Math.min(1, Math.min(mW/this.realOverlay.width, mH/this.realOverlay.height)); - const w = Math.round(this.realOverlay.width*sc), h = Math.round(this.realOverlay.height*sc); - if (!this.featherPreviewCanvas || this.featherPreviewCanvas.width!==w || this.featherPreviewCanvas.height!==h) { - this.featherPreviewCanvas = document.createElement('canvas'); this.featherPreviewCanvas.width = w; this.featherPreviewCanvas.height = h; + const sc = Math.min(1, Math.min(mW / this.realOverlay.width, mH / this.realOverlay.height)); + const w = Math.round(this.realOverlay.width * sc), h = Math.round(this.realOverlay.height * sc); + if (!this.featherPreviewCanvas || this.featherPreviewCanvas.width !== w || this.featherPreviewCanvas.height !== h) { + this.featherPreviewCanvas = document.createElement('canvas'); + this.featherPreviewCanvas.width = w; this.featherPreviewCanvas.height = h; } const ctx = this.featherPreviewCanvas.getContext('2d'); - ctx.clearRect(0,0,w,h); ctx.drawImage(this.overlayImage,0,0,w,h); - const imgD = ctx.getImageData(0,0,w,h), d = imgD.data; + ctx.clearRect(0, 0, w, h); ctx.drawImage(this.overlayImage, 0, 0, w, h); + const imgD = ctx.getImageData(0, 0, w, h), d = imgD.data; const cx = this.featherCenter.x * w, cy = this.featherCenter.y * h; - if (!isShape) { - let maxDist; - if (this.featherType.includes("Radial")) maxDist = Math.hypot(Math.max(cx,w-cx), Math.max(cy,h-cy)) || 1; - else maxDist = Math.min(w,h)/2 || 1; - const featherWidth = Math.max((rVal / 300.0) * maxDist, 1.0); - for (let y=0; y=0 && nx=0 && ny= 0.5) mask = mask < 0.02 ? 0 : (mask > 0.98 ? 1 : mask); + if (isOut) mask = 1.0 - mask; d[i + 3] *= mask; + } } - ctx.putImageData(imgD,0,0); this.previewDirty = false; + ctx.putImageData(imgD, 0, 0); this.previewDirty = false; }; nodeType.prototype.getRealTransform = function() { - const dS = this.canvasRealWidth / (this.displayWidth || 1); - const absX = (this.overlay.x * dS) + (this.canvasRealWidth / 2); - const absY = (this.overlay.y * dS) + (this.canvasRealHeight / 2); - return { - x: absX, y: absY, - scale_x: (this.overlay.width * dS) / (this.realOverlay.width || 1), - scale_y: (this.overlay.height * dS) / (this.realOverlay.height || 1), - rotation: this.overlay.rotation, - flip_h: this.overlay.flipH, - flip_v: this.overlay.flipV - }; + const dS = this.canvasRealWidth / (this.displayWidth || 1); + const absX = (this.overlay.x * dS) + (this.canvasRealWidth / 2), absY = (this.overlay.y * dS) + (this.canvasRealHeight / 2); + return { x: absX, y: absY, scale_x: (this.overlay.width * dS) / (this.realOverlay.width || 1), scale_y: (this.overlay.height * dS) / (this.realOverlay.height || 1), rotation: this.overlay.rotation, flip_h: this.overlay.flipH, flip_v: this.overlay.flipV }; }; nodeType.prototype.computeScreenHandles = function(rectX, rectY, useScale, useOffsetX, useOffsetY) { - const hw = this.overlay.width / 2; - const hh = this.overlay.height / 2; - const rotationRad = this.overlay.rotation * Math.PI / 180; - const cos = Math.cos(rotationRad); - const sin = Math.sin(rotationRad); - const flipX = this.overlay.flipH ? -1 : 1; - const flipY = this.overlay.flipV ? -1 : 1; - - const handles = { - 'scale-tl': { x: -hw, y: -hh }, - 'scale-tr': { x: hw, y: -hh }, - 'scale-bl': { x: -hw, y: hh }, - 'scale-br': { x: hw, y: hh }, - 'scale-t': { x: 0, y: -hh }, - 'scale-b': { x: 0, y: hh }, - 'scale-l': { x: -hw, y: 0 }, - 'scale-r': { x: hw, y: 0 }, - 'rotate': { x: 0, y: -hh - 40 }, - 'feather-center': { - x: (this.featherCenter.x - 0.5) * this.overlay.width, - y: (this.featherCenter.y - 0.5) * this.overlay.height - } - }; - + const hw = this.overlay.width / 2, hh = this.overlay.height / 2, rot = this.overlay.rotation * Math.PI / 180; + const cos = Math.cos(rot), sin = Math.sin(rot), fx = this.overlay.flipH ? -1 : 1, fy = this.overlay.flipV ? -1 : 1; + const handles = { 'scale-tl': [-hw, -hh], 'scale-tr': [hw, -hh], 'scale-bl': [-hw, hh], 'scale-br': [hw, hh], 'scale-t': [0, -hh], 'scale-b': [0, hh], 'scale-l': [-hw, 0], 'scale-r': [hw, 0], 'rotate': [0, -hh - 40], 'feather-center': [(this.featherCenter.x - 0.5) * this.overlay.width, (this.featherCenter.y - 0.5) * this.overlay.height] }; const screenHandles = {}; - for (const [name, local] of Object.entries(handles)) { - const rx = local.x * cos - local.y * sin; - const ry = local.x * sin + local.y * cos; - const fx = rx * flipX; - const fy = ry * flipY; - const wx = this.overlay.x + fx; - const wy = this.overlay.y + fy; - const sx = rectX + useOffsetX + (wx * useScale); - const sy = rectY + useOffsetY + (wy * useScale); - screenHandles[name] = { x: sx, y: sy }; + for (const [name, loc] of Object.entries(handles)) { + const rx = loc[0] * cos - loc[1] * sin, ry = loc[0] * sin + loc[1] * cos; + screenHandles[name] = { x: rectX + useOffsetX + (this.overlay.x + rx * fx) * useScale, y: rectY + useOffsetY + (this.overlay.y + ry * fy) * useScale }; } return screenHandles; }; nodeType.prototype.sendTransforms = async function() { - const payload = { - id: String(this.id), transforms: this.getRealTransform(), opacity: this.opacity, feather_type: this.featherType, - edge_radius: Math.round(this.edgeRadius), shape_radius: Math.round(this.shapeRadius), - feather_center_x: this.featherCenter.x, feather_center_y: this.featherCenter.y - }; - try { - await api.fetchApi("/rayko/rs_collage", { method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify(payload) }); - this.isEditing = false; this.setDirtyCanvas(true); - } catch(e) { /* Silent fail */ } + const payload = { id: String(this.id), transforms: this.getRealTransform(), opacity: this.opacity, feather_type: this.featherType, blur_radius: this.blurRadius, blur_hardness: this.blurHardness, feather_center_x: this.featherCenter.x, feather_center_y: this.featherCenter.y }; + try { await api.fetchApi("/rayko/rs_collage", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); this.isEditing = false; this.setDirtyCanvas(true); } catch(e) {} }; nodeType.prototype.cancelEditing = async function() { try { await api.interrupt(); } catch(e) {} - await fetch("/rayko/rs_collage/cancel", { method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({ node_id: String(this.id) }) }); + await fetch("/rayko/rs_collage/cancel", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ node_id: String(this.id) }) }); this.isEditing = false; this.isLoading = false; this.dragType = null; this.dragState = null; this.setDirtyCanvas(true); }; - nodeType.prototype.onResize = function(size) { - if(size[0] < this.minWidth) size[0] = this.minWidth; - if(size[1] < this.minHeight) size[1] = this.minHeight; - this.setDirtyCanvas(true); + nodeType.prototype.onResize = function(size) { + if (size[0] < this.minWidth) size[0] = this.minWidth; + if (size[1] < this.minHeight) size[1] = this.minHeight; + this.setDirtyCanvas(true); }; - + nodeType.prototype.getCanvasMetrics = function() { const titleH = LiteGraph.NODE_TITLE_HEIGHT || 30; - const canvasTopPadding = 150; + const canvasTopPadding = 175; const btnAreaH = 90; - const maxCanvasH = this.size[1] - titleH - canvasTopPadding - btnAreaH; - const cSize = Math.max(300, Math.min(this.size[0] - 40, maxCanvasH)); - const rectX = (this.size[0] - cSize) / 2; - const rectY = titleH + canvasTopPadding; + let cSize, rectX, rectY; + if (this.advancedMode) { + const w = this.overlayCanvas.width || (this.overlayCanvasWrapper ? this.overlayCanvasWrapper.clientWidth : 1000); + const h = this.overlayCanvas.height || (this.overlayCanvasWrapper ? this.overlayCanvasWrapper.clientHeight : 800); + cSize = Math.min(w, h); + rectX = 0; rectY = 0; + } else { + const maxCanvasH = this.size[1] - titleH - canvasTopPadding - btnAreaH; + cSize = Math.max(300, Math.min(this.size[0] - 40, maxCanvasH)); + rectX = (this.size[0] - cSize) / 2; rectY = titleH + canvasTopPadding; + } return { cSize, rectX, rectY }; }; + // Node foreground drawing (ONLY for normal mode) nodeType.prototype.onDrawForeground = function(ctx) { + // Если включён Advanced Mode, нода НЕ рисуется. Это убирает "фантом". + if (this.advancedMode) { + ctx.clearRect(0, 0, this.size[0], this.size[1]); + return; + } + const { cSize, rectX, rectY } = this.getCanvasMetrics(); - ctx.fillStyle = "#1e1e1e"; ctx.fillRect(rectX, rectY, cSize, cSize); + ctx.fillStyle = "#1e1e1e"; ctx.fillRect(rectX, rectY, cSize, cSize); ctx.strokeStyle = "#555"; ctx.strokeRect(rectX, rectY, cSize, cSize); this.updateDisplaySize(cSize); if (!this.dragState) { this.updateOverlayAbsolute(); this.computeAndApplyView(); } - if (this.isLoading) { - ctx.fillStyle = "#888"; ctx.font = "12px Arial"; ctx.fillText("Loading...", rectX + cSize/2 - 35, rectY + cSize/2); + if (this.isLoading) { + ctx.fillStyle = "#888"; ctx.font = "12px Arial"; ctx.fillText("Loading...", rectX + cSize / 2 - 35, rectY + cSize / 2); } else if (this.isEditing && this.backgroundImage) { const useScale = this.dragState ? this.dragState.viewScale : this.viewScale; const useOffsetX = this.dragState ? this.dragState.viewOffsetX : this.viewOffsetX; const useOffsetY = this.dragState ? this.dragState.viewOffsetY : this.viewOffsetY; - - ctx.save(); - ctx.translate(rectX + useOffsetX, rectY + useOffsetY); - ctx.scale(useScale, useScale); - ctx.drawImage(this.backgroundImage, -this.displayWidth/2, -this.displayHeight/2, this.displayWidth, this.displayHeight); - + ctx.save(); ctx.translate(rectX + useOffsetX, rectY + useOffsetY); ctx.scale(useScale, useScale); + ctx.drawImage(this.backgroundImage, -this.displayWidth / 2, -this.displayHeight / 2, this.displayWidth, this.displayHeight); if (this.overlayImage) { if (this.previewDirty) this.generateFeatherPreview(); const prev = this.featherPreviewCanvas || this.overlayImage; - ctx.save(); - ctx.translate(this.overlay.x, this.overlay.y); - ctx.rotate(this.overlay.rotation * Math.PI / 180); - ctx.scale(this.overlay.flipH ? -1 : 1, this.overlay.flipV ? -1 : 1); - ctx.globalAlpha = this.opacity; - ctx.drawImage(prev, -this.overlay.width/2, -this.overlay.height/2, this.overlay.width, this.overlay.height); - ctx.globalAlpha = 1; - ctx.shadowColor = "rgba(0,0,0,0.8)"; ctx.shadowBlur = 4 / useScale; - ctx.strokeStyle = "#00E5FF"; ctx.lineWidth = 2 / useScale; - ctx.strokeRect(-this.overlay.width/2, -this.overlay.height/2, this.overlay.width, this.overlay.height); - ctx.shadowColor = "transparent"; ctx.shadowBlur = 0; - - const hw = this.overlay.width/2, hh = this.overlay.height/2, hs = 6/useScale; - ctx.fillStyle = "#FF0000"; - [[hw, hh], [-hw, hh], [hw, -hh], [-hw, -hh]].forEach(([x,y]) => ctx.fillRect(x-hs/2, y-hs/2, hs, hs)); - [[hw, 0], [-hw, 0], [0, hh], [0, -hh]].forEach(([x,y]) => ctx.fillRect(x-hs/2, y-hs/2, hs, hs)); - - const rotHandleY = -hh - 40; - ctx.beginPath(); ctx.arc(0, rotHandleY, 5/useScale, 0, Math.PI*2); ctx.fillStyle = "#ff9800"; ctx.fill(); - ctx.strokeStyle = "#fff"; ctx.lineWidth = 1/useScale; ctx.stroke(); - - if ((this.featherType === "Radial Out" || this.featherType === "Radial In") && this.edgeRadius > 0) { - const fCx = (this.featherCenter.x - 0.5) * this.overlay.width; - const fCy = (this.featherCenter.y - 0.5) * this.overlay.height; - ctx.strokeStyle = "#00CED1"; ctx.lineWidth = 2/useScale; - ctx.beginPath(); ctx.moveTo(fCx-10, fCy); ctx.lineTo(fCx+10, fCy); ctx.stroke(); - ctx.beginPath(); ctx.moveTo(fCx, fCy-10); ctx.lineTo(fCx, fCy+10); ctx.stroke(); - ctx.beginPath(); ctx.arc(fCx, fCy, 4, 0, Math.PI*2); ctx.stroke(); + ctx.save(); ctx.translate(this.overlay.x, this.overlay.y); ctx.rotate(this.overlay.rotation * Math.PI / 180); + ctx.scale(this.overlay.flipH ? -1 : 1, this.overlay.flipV ? -1 : 1); ctx.globalAlpha = this.opacity; + ctx.drawImage(prev, -this.overlay.width / 2, -this.overlay.height / 2, this.overlay.width, this.overlay.height); ctx.globalAlpha = 1; + ctx.shadowColor = "rgba(0,0,0,0.8)"; ctx.shadowBlur = 4 / useScale; ctx.strokeStyle = "#00E5FF"; ctx.lineWidth = 2 / useScale; + ctx.strokeRect(-this.overlay.width / 2, -this.overlay.height / 2, this.overlay.width, this.overlay.height); ctx.shadowColor = "transparent"; ctx.shadowBlur = 0; + const hw = this.overlay.width / 2, hh = this.overlay.height / 2, hs = 6 / useScale; ctx.fillStyle = "#FF0000"; + [[hw, hh], [-hw, hh], [hw, -hh], [-hw, -hh]].forEach(([x, y]) => ctx.fillRect(x - hs / 2, y - hs / 2, hs, hs)); + [[hw, 0], [-hw, 0], [0, hh], [0, -hh]].forEach(([x, y]) => ctx.fillRect(x - hs / 2, y - hs / 2, hs, hs)); + const rotHandleY = -hh - 40; ctx.beginPath(); ctx.arc(0, rotHandleY, 5 / useScale, 0, Math.PI * 2); ctx.fillStyle = "#ff9800"; ctx.fill(); ctx.strokeStyle = "#fff"; ctx.lineWidth = 1 / useScale; ctx.stroke(); + if (this.featherType.includes("Blur") && this.blurRadius > 0) { + const fCx = (this.featherCenter.x - 0.5) * this.overlay.width, fCy = (this.featherCenter.y - 0.5) * this.overlay.height; + ctx.strokeStyle = "#FF0000"; ctx.lineWidth = 2 / useScale; ctx.beginPath(); ctx.moveTo(fCx - 12, fCy); ctx.lineTo(fCx + 12, fCy); ctx.stroke(); + ctx.beginPath(); ctx.moveTo(fCx, fCy - 12); ctx.lineTo(fCx, fCy + 12); ctx.stroke(); + ctx.fillStyle = "#2196F3"; ctx.beginPath(); ctx.arc(fCx, fCy, 4 / useScale, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = "#FFFFFF"; ctx.lineWidth = 1 / useScale; ctx.stroke(); } ctx.restore(); } ctx.restore(); - ctx.fillStyle = "#ff9800"; ctx.font = "12px Arial"; - ctx.fillText(` EDITING (Scale: ${(useScale*100).toFixed(0)}%)`, rectX + cSize - 160, rectY + cSize - 10); - } else { - ctx.fillStyle = "#888"; ctx.font = "12px Arial"; ctx.fillText("▶ Run queue to start", rectX + cSize/2 - 65, rectY + cSize/2); + ctx.fillStyle = "#ff9800"; ctx.font = "12px Arial"; ctx.fillText(`EDITING (Scale: ${(useScale * 100).toFixed(0)}%)`, rectX + cSize - 160, rectY + cSize - 10); + } else { + ctx.fillStyle = "#888"; ctx.font = "12px Arial"; ctx.fillText("▶ Run queue to start", rectX + cSize / 2 - 65, rectY + cSize / 2); } - const btnW = (this.size[0]-50)/2, btnH = 30, gap = 10; - const y2 = this.size[1] - btnH - 15; - const y1 = y2 - btnH - gap; - const rR = (ctx,x,y,w,h,r) => { - ctx.beginPath(); ctx.moveTo(x+r,y); ctx.lineTo(x+w-r,y); - ctx.quadraticCurveTo(x+w,y,x+w,y+r); ctx.lineTo(x+w,y+h-r); ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h); - ctx.lineTo(x+r,y+h); ctx.quadraticCurveTo(x,y+h,x,y+h-r); ctx.lineTo(x,y+r); ctx.quadraticCurveTo(x,y,x+r,y); ctx.closePath(); - }; - [[15,y1," FLIP H",this.btnFlipHHover,"#2196F3"], - [15+btnW+gap,y1," FLIP V",this.btnFlipVHover,"#2196F3"], - [15,y2,"✓ APPLY",this.btnApplyHover,"#4CAF50"], - [15+btnW+gap,y2," CANCEL",this.btnCancelHover,"#dc3545"]].forEach(([bx,by,txt,hov,col]) => { - ctx.fillStyle = hov ? "#444" : "#2a2a2a"; rR(ctx,bx,by,btnW,btnH,6); ctx.fill(); ctx.strokeStyle = col; ctx.stroke(); - ctx.fillStyle = col; ctx.font = "bold 11px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(txt, bx+btnW/2, by+btnH/2); + // Normal mode buttons + const toggleBtnW = 150, toggleBtnH = 24, toggleBtnX = (this.size[0] - toggleBtnW) / 2, toggleBtnY = 20; + ctx.fillStyle = "#2a2a2a"; ctx.strokeStyle = "#2196F3"; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(toggleBtnX + 6, toggleBtnY); ctx.lineTo(toggleBtnX + toggleBtnW - 6, toggleBtnY); + ctx.quadraticCurveTo(toggleBtnX + toggleBtnW, toggleBtnY, toggleBtnX + toggleBtnW, toggleBtnY + 6); ctx.lineTo(toggleBtnX + toggleBtnW, toggleBtnY + toggleBtnH - 6); + ctx.quadraticCurveTo(toggleBtnX + toggleBtnW, toggleBtnY + toggleBtnH, toggleBtnX + toggleBtnW - 6, toggleBtnY + toggleBtnH); ctx.lineTo(toggleBtnX + 6, toggleBtnY + toggleBtnH); + ctx.quadraticCurveTo(toggleBtnX, toggleBtnY + toggleBtnH, toggleBtnX, toggleBtnY + toggleBtnH - 6); ctx.lineTo(toggleBtnX, toggleBtnY + 6); + ctx.quadraticCurveTo(toggleBtnX, toggleBtnY, toggleBtnX + 6, toggleBtnY); ctx.closePath(); ctx.fill(); ctx.stroke(); + ctx.fillStyle = "#2196F3"; ctx.font = "bold 11px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "alphabetic"; ctx.fillText("🔍 ADVANCED MODE", toggleBtnX + toggleBtnW / 2, toggleBtnY + toggleBtnH / 2 + 4); + + const btnW = (this.size[0] - 50) / 2, btnH = 30, gap = 10, y2 = this.size[1] - btnH - 15, y1 = y2 - btnH - gap; + const rR = (ctx, x, y, w, h, r) => { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); }; + [[15, y1, "↔️ FLIP H", this.btnFlipHHover, "#2196F3"], [15 + btnW + gap, y1, "️ FLIP V", this.btnFlipVHover, "#2196F3"], [15, y2, "✔️ APPLY", this.btnApplyHover, "#4CAF50"], [15 + btnW + gap, y2, "❌ CANCEL", this.btnCancelHover, "#dc3545"]].forEach(([bx, by, txt, hov, col]) => { + ctx.fillStyle = hov ? "#444" : "#2a2a2a"; rR(ctx, bx, by, btnW, btnH, 6); ctx.fill(); ctx.strokeStyle = col; ctx.stroke(); ctx.fillStyle = col; ctx.font = "bold 11px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "alphabetic"; ctx.fillText(txt, bx + btnW / 2, by + btnH / 2 + 4); }); }; nodeType.prototype.onMouseDown = function(event, pos) { + if (!this.advancedMode && pos) { + const toggleBtnW = 150, toggleBtnH = 24, toggleBtnX = (this.size[0] - toggleBtnW) / 2, toggleBtnY = 20; + if (pos[0] >= toggleBtnX && pos[0] <= toggleBtnX + toggleBtnW && pos[1] >= toggleBtnY && pos[1] <= toggleBtnY + toggleBtnH) { this._toggleAdvancedMode(); return true; } + } if (!this.isEditing || this.isLoading || !this.overlayImage) return; - const { cSize, rectX, rectY } = this.getCanvasMetrics(); const mx = pos[0], my = pos[1]; const frozenScale = this.viewScale, frozenOffsetX = this.viewOffsetX, frozenOffsetY = this.viewOffsetY; - const worldMx = (mx - rectX - frozenOffsetX) / frozenScale; - const worldMy = (my - rectY - frozenOffsetY) / frozenScale; - + const worldMx = (mx - rectX - frozenOffsetX) / frozenScale, worldMy = (my - rectY - frozenOffsetY) / frozenScale; const screenHandles = this.computeScreenHandles(rectX, rectY, frozenScale, frozenOffsetX, frozenOffsetY); - const cornerSize = 14, edgeSize = 18, rotateSize = 22, featherSize = 18; let detectedType = null, minDist = Infinity; - - const checkHandle = (name, h, threshold) => { - const dist = Math.hypot(mx - h.x, my - h.y); - if (dist < threshold && dist < minDist) { detectedType = name; minDist = dist; } - }; - - const edgeHandles = ['scale-t', 'scale-b', 'scale-l', 'scale-r']; - for (const [name, pos] of Object.entries(screenHandles)) { - const isEdge = edgeHandles.includes(name); - const threshold = name === 'rotate' ? rotateSize - : (name === 'feather-center' ? featherSize - : (isEdge ? edgeSize : cornerSize)); - checkHandle(name, pos, threshold); + const checkHandle = (name, h, threshold) => { const dist = Math.hypot(mx - h.x, my - h.y); if (dist < threshold && dist < minDist) { detectedType = name; minDist = dist; } }; + for (const [name, hPos] of Object.entries(screenHandles)) { + const isEdge = ['scale-t', 'scale-b', 'scale-l', 'scale-r'].includes(name); + const threshold = name === 'rotate' ? rotateSize : (name === 'feather-center' ? featherSize : (isEdge ? edgeSize : cornerSize)); + checkHandle(name, hPos, threshold); } - this.dragType = detectedType; if (this.dragType) { - this.dragState = { - startMouseX: worldMx, startMouseY: worldMy, - startX: this.overlay.x, startY: this.overlay.y, - startW: this.overlay.width, startH: this.overlay.height, - startRotation: this.overlay.rotation, - aspect: this.overlay.width / this.overlay.height, - featherStartX: this.featherCenter.x, featherStartY: this.featherCenter.y, - viewScale: frozenScale, viewOffsetX: frozenOffsetX, viewOffsetY: frozenOffsetY, - startDist: ['scale-tl', 'scale-tr', 'scale-bl', 'scale-br'].includes(detectedType) - ? Math.hypot(worldMx - this.overlay.x, worldMy - this.overlay.y) - : 0 - }; + this.dragState = { startMouseX: worldMx, startMouseY: worldMy, startX: this.overlay.x, startY: this.overlay.y, startW: this.overlay.width, startH: this.overlay.height, startRotation: this.overlay.rotation, aspect: this.overlay.width / this.overlay.height, featherStartX: this.featherCenter.x, featherStartY: this.featherCenter.y, viewScale: frozenScale, viewOffsetX: frozenOffsetX, viewOffsetY: frozenOffsetY, startDist: ['scale-tl', 'scale-tr', 'scale-bl', 'scale-br'].includes(detectedType) ? Math.hypot(worldMx - this.overlay.x, worldMy - this.overlay.y) : 0 }; return true; } - const dx = worldMx - this.overlay.x, dy = worldMy - this.overlay.y; const rotRad = -this.overlay.rotation * Math.PI / 180; - const localX = dx * Math.cos(rotRad) - dy * Math.sin(rotRad); - const localY = dx * Math.sin(rotRad) + dy * Math.cos(rotRad); + const localX = dx * Math.cos(rotRad) - dy * Math.sin(rotRad), localY = dx * Math.sin(rotRad) + dy * Math.cos(rotRad); const flipX = this.overlay.flipH ? -1 : 1, flipY = this.overlay.flipV ? -1 : 1; - const adjustedX = localX * flipX, adjustedY = localY * flipY; - - if (Math.abs(adjustedX) < this.overlay.width / 2 && Math.abs(adjustedY) < this.overlay.height / 2) { + if (Math.abs(localX * flipX) < this.overlay.width / 2 && Math.abs(localY * flipY) < this.overlay.height / 2) { this.dragType = 'move'; - this.dragState = { - startMouseX: worldMx, startMouseY: worldMy, - startX: this.overlay.x, startY: this.overlay.y, - startW: this.overlay.width, startH: this.overlay.height, - startRotation: this.overlay.rotation, - viewScale: frozenScale, viewOffsetX: frozenOffsetX, viewOffsetY: frozenOffsetY, - }; + this.dragState = { startMouseX: worldMx, startMouseY: worldMy, startX: this.overlay.x, startY: this.overlay.y, startW: this.overlay.width, startH: this.overlay.height, startRotation: this.overlay.rotation, viewScale: frozenScale, viewOffsetX: frozenOffsetX, viewOffsetY: frozenOffsetY }; return true; } - - const btnW = (this.size[0]-50)/2, btnH = 30, gap = 10; - const y2 = this.size[1] - btnH - 15, y1 = y2 - btnH - gap; - if (mx >= 15 && mx <= 15+btnW && my >= y1 && my <= y1+btnH) { this.overlay.flipH = !this.overlay.flipH; this.updateRelativeFromAbsolute(); this.setDirtyCanvas(true); return true; } - if (mx >= 15+btnW+gap && mx <= 15+btnW+gap+btnW && my >= y1 && my <= y1+btnH) { this.overlay.flipV = !this.overlay.flipV; this.updateRelativeFromAbsolute(); this.setDirtyCanvas(true); return true; } - if (mx >= 15 && mx <= 15+btnW && my >= y2 && my <= y2+btnH) { this.sendTransforms(); return true; } - if (mx >= 15+btnW+gap && mx <= 15+btnW+gap+btnW && my >= y2 && my <= y2+btnH) { this.cancelEditing(); return true; } + if (!this.advancedMode && pos) { + const btnW = (this.size[0] - 50) / 2, btnH = 30, gap = 10, y2 = this.size[1] - btnH - 15, y1 = y2 - btnH - gap; + if (pos[0] >= 15 && pos[0] <= 15 + btnW && pos[1] >= y1 && pos[1] <= y1 + btnH) { this.overlay.flipH = !this.overlay.flipH; this.updateRelativeFromAbsolute(); this.setDirtyCanvas(true); return true; } + if (pos[0] >= 15 + btnW + gap && pos[0] <= 15 + btnW + gap + btnW && pos[1] >= y1 && pos[1] <= y1 + btnH) { this.overlay.flipV = !this.overlay.flipV; this.updateRelativeFromAbsolute(); this.setDirtyCanvas(true); return true; } + if (pos[0] >= 15 && pos[0] <= 15 + btnW && pos[1] >= y2 && pos[1] <= y2 + btnH) { this.sendTransforms(); return true; } + if (pos[0] >= 15 + btnW + gap && pos[0] <= 15 + btnW + gap + btnW && pos[1] >= y2 && pos[1] <= y2 + btnH) { this.cancelEditing(); return true; } + } return false; }; nodeType.prototype.onMouseMove = function(event, pos) { - const { cSize, rectX, rectY } = this.getCanvasMetrics(); - const mx = pos[0], my = pos[1]; - - const btnW = (this.size[0]-50)/2, btnH = 30, gap = 10; - const y2 = this.size[1] - btnH - 15, y1 = y2 - btnH - gap; - const prev = [this.btnFlipHHover, this.btnFlipVHover, this.btnApplyHover, this.btnCancelHover]; - this.btnFlipHHover = mx >= 15 && mx <= 15+btnW && my >= y1 && my <= y1+btnH; - this.btnFlipVHover = mx >= 15+btnW+gap && mx <= 15+btnW+gap+btnW && my >= y1 && my <= y1+btnH; - this.btnApplyHover = mx >= 15 && mx <= 15+btnW && my >= y2 && my <= y2+btnH; - this.btnCancelHover = mx >= 15+btnW+gap && mx <= 15+btnW+gap+btnW && my >= y2 && my <= y2+btnH; - if (prev.some((v,i) => v !== [this.btnFlipHHover, this.btnFlipVHover, this.btnApplyHover, this.btnCancelHover][i])) this.setDirtyCanvas(true); - + if (!this.advancedMode && pos) { + const btnW = (this.size[0] - 50) / 2, btnH = 30, gap = 10, y2 = this.size[1] - btnH - 15, y1 = y2 - btnH - gap; + const prev = [this.btnFlipHHover, this.btnFlipVHover, this.btnApplyHover, this.btnCancelHover]; + this.btnFlipHHover = pos[0] >= 15 && pos[0] <= 15 + btnW && pos[1] >= y1 && pos[1] <= y1 + btnH; + this.btnFlipVHover = pos[0] >= 15 + btnW + gap && pos[0] <= 15 + btnW + gap + btnW && pos[1] >= y1 && pos[1] <= y1 + btnH; + this.btnApplyHover = pos[0] >= 15 && pos[0] <= 15 + btnW && pos[1] >= y2 && pos[1] <= y2 + btnH; + this.btnCancelHover = pos[0] >= 15 + btnW + gap && pos[0] <= 15 + btnW + gap + btnW && pos[1] >= y2 && pos[1] <= y2 + btnH; + if (prev.some((v, i) => v !== [this.btnFlipHHover, this.btnFlipVHover, this.btnApplyHover, this.btnCancelHover][i])) this.setDirtyCanvas(true); + } if (!this.dragType || !this.isEditing || this.isLoading || !this.dragState) return; - + const { rectX, rectY } = this.getCanvasMetrics(); + const mx = pos[0], my = pos[1]; const worldMx = (mx - rectX - this.dragState.viewOffsetX) / this.dragState.viewScale; const worldMy = (my - rectY - this.dragState.viewOffsetY) / this.dragState.viewScale; const dx = worldMx - this.dragState.startMouseX, dy = worldMy - this.dragState.startMouseY; - switch(this.dragType) { - case 'move': - this.overlay.x = this.dragState.startX + dx; this.overlay.y = this.dragState.startY + dy; break; - case 'rotate': { - const cx = this.overlay.x, cy = this.overlay.y; - const startAngle = Math.atan2(this.dragState.startMouseY - cy, this.dragState.startMouseX - cx); - const currentAngle = Math.atan2(worldMy - cy, worldMx - cx); - this.overlay.rotation = this.dragState.startRotation + (currentAngle - startAngle) * 180 / Math.PI; - break; - } - case 'scale-br': case 'scale-bl': case 'scale-tr': case 'scale-tl': { - const currentDist = Math.hypot(worldMx - this.overlay.x, worldMy - this.overlay.y); - const startDist = this.dragState.startDist || 1; - const scale = Math.max(0.05, currentDist / startDist); - - this.overlay.width = Math.max(40, this.dragState.startW * scale); - this.overlay.height = Math.max(40, this.dragState.startH * scale); - this.overlay.x = this.dragState.startX; - this.overlay.y = this.dragState.startY; - break; - } - case 'scale-r': case 'scale-l': case 'scale-b': case 'scale-t': { - const angleRad = this.dragState.startRotation * Math.PI / 180; - const cos = Math.cos(angleRad), sin = Math.sin(angleRad); - const localDx = dx * cos + dy * sin; - const localDy = -dx * sin + dy * cos; - - let finalW = this.dragState.startW, finalH = this.dragState.startH; - - switch(this.dragType) { - case 'scale-r': finalW += localDx; break; - case 'scale-l': finalW -= localDx; break; - case 'scale-b': finalH += localDy; break; - case 'scale-t': finalH -= localDy; break; - } - - if (finalW < 40) finalW = 40; if (finalH < 40) finalH = 40; - - this.overlay.width = finalW; this.overlay.height = finalH; - this.overlay.x = this.dragState.startX; - this.overlay.y = this.dragState.startY; - break; - } - case 'feather-center': { - const rad = -this.dragState.startRotation * Math.PI / 180; - const cos = Math.cos(rad), sin = Math.sin(rad); - const rdx = dx * cos - dy * sin, rdy = dx * sin + dy * cos; - this.featherCenter.x = Math.max(0, Math.min(1, this.dragState.featherStartX + rdx / this.dragState.startW)); - this.featherCenter.y = Math.max(0, Math.min(1, this.dragState.featherStartY + rdy / this.dragState.startH)); - this.previewDirty = true; break; - } + case 'move': this.overlay.x = this.dragState.startX + dx; this.overlay.y = this.dragState.startY + dy; break; + case 'rotate': { const cx = this.overlay.x, cy = this.overlay.y; const sA = Math.atan2(this.dragState.startMouseY - cy, this.dragState.startMouseX - cx); const cA = Math.atan2(worldMy - cy, worldMx - cx); this.overlay.rotation = this.dragState.startRotation + (cA - sA) * 180 / Math.PI; break; } + case 'scale-br': case 'scale-bl': case 'scale-tr': case 'scale-tl': { const cD = Math.hypot(worldMx - this.overlay.x, worldMy - this.overlay.y); const sD = this.dragState.startDist || 1; const sc = Math.max(0.05, cD / sD); this.overlay.width = Math.max(40, this.dragState.startW * sc); this.overlay.height = Math.max(40, this.dragState.startH * sc); this.overlay.x = this.dragState.startX; this.overlay.y = this.dragState.startY; break; } + case 'scale-r': case 'scale-l': case 'scale-b': case 'scale-t': { const aR = this.dragState.startRotation * Math.PI / 180; const c = Math.cos(aR), s = Math.sin(aR); const lDx = dx * c + dy * s, lDy = -dx * s + dy * c; let fW = this.dragState.startW, fH = this.dragState.startH; if(this.dragType === 'scale-r') fW += lDx; else if(this.dragType === 'scale-l') fW -= lDx; else if(this.dragType === 'scale-b') fH += lDy; else fH -= lDy; if(fW < 40) fW = 40; if(fH < 40) fH = 40; this.overlay.width = fW; this.overlay.height = fH; this.overlay.x = this.dragState.startX; this.overlay.y = this.dragState.startY; break; } + case 'feather-center': { const rD = -this.dragState.startRotation * Math.PI / 180; const c = Math.cos(rD), s = Math.sin(rD); const rdx = dx * c - dy * s, rdy = dx * s + dy * c; this.featherCenter.x = Math.max(0, Math.min(1, this.dragState.featherStartX + rdx / this.dragState.startW)); this.featherCenter.y = Math.max(0, Math.min(1, this.dragState.featherStartY + rdy / this.dragState.startH)); break; } } - this.setDirtyCanvas(true); + this.previewDirty = true; this.setDirtyCanvas(true); }; - nodeType.prototype.onMouseUp = function() { - if (this.dragType) { this.updateRelativeFromAbsolute(); this.computeAndApplyView(); } + nodeType.prototype.onMouseUp = function() { + if (this.dragType) { + this.updateRelativeFromAbsolute(); + if (!this.advancedMode) { + this.computeAndApplyView(); + } + } this.dragType = null; this.dragState = null; }; }