From ebfd387474d40ec37b200bb1c9d71b307c204b50 Mon Sep 17 00:00:00 2001 From: Temult <146690928+Temult@users.noreply.github.com> Date: Thu, 17 Apr 2025 04:34:14 -0400 Subject: [PATCH] Major overhaul --- TWanSigmaGraph.py | 24 +- __init__.py | 2 +- js/TWanSigmaGraphWidget.js | 1343 +++++++++++++----------------------- 3 files changed, 475 insertions(+), 894 deletions(-) diff --git a/TWanSigmaGraph.py b/TWanSigmaGraph.py index 86295f5..0e62bf6 100644 --- a/TWanSigmaGraph.py +++ b/TWanSigmaGraph.py @@ -3,7 +3,7 @@ import json import torch import math # Import math for isnan -class SigmaGraphNode: +class TWanSigmaGraph: """ A ComfyUI node that generates a sigma schedule tensor based on user-defined points edited via a custom graph widget in the UI. It also outputs the @@ -56,18 +56,18 @@ class SigmaGraphNode: isinstance(p['y'], (int, float)) and not math.isnan(p['y']) and not math.isinf(p['y']): valid_points.append({"x": float(p['x']), "y": float(p['y'])}) # Ensure float type else: - print(f"[SigmaGraphNode Warning] Ignoring invalid point data: {p}") + print(f"[TWanSigmaGraph Warning] Ignoring invalid point data: {p}") points = valid_points if len(points) != len(points_data): - print("[SigmaGraphNode Warning] Some points in graph_data were invalid and ignored.") + print("[TWanSigmaGraph Warning] Some points in graph_data were invalid and ignored.") except (json.JSONDecodeError, ValueError, TypeError) as e: - print(f"[SigmaGraphNode Warning] Invalid graph_data input: {e}. Using default points.") + print(f"[TWanSigmaGraph Warning] Invalid graph_data input: {e}. Using default points.") return default_points_list # Return default on parse error if not points: - print("[SigmaGraphNode Warning] No valid points found after filtering. Using default points.") + print("[TWanSigmaGraph Warning] No valid points found after filtering. Using default points.") return default_points_list # Ensure boundary points exist using a tolerance @@ -78,12 +78,12 @@ class SigmaGraphNode: # Find point closest to x=0 to determine y-value, or default to 1.0 start_y = min(points, key=lambda p: abs(p['x'] - 0.0))['y'] if points else 1.0 points.append({"x": 0.0, "y": start_y}) - print("[SigmaGraphNode Info] Added missing start point (x=0).") + print("[TWanSigmaGraph Info] Added missing start point (x=0).") if not has_end: # Find point closest to x=1 to determine y-value, or default to 0.0 end_y = min(points, key=lambda p: abs(p['x'] - 1.0))['y'] if points else 0.0 points.append({"x": 1.0, "y": end_y}) - print("[SigmaGraphNode Info] Added missing end point (x=1).") + print("[TWanSigmaGraph Info] Added missing end point (x=1).") # Sort points by x-coordinate points.sort(key=lambda p: p["x"]) @@ -98,12 +98,12 @@ class SigmaGraphNode: unique_points.append(points[i]) last_x = points[i]['x'] else: - print(f"[SigmaGraphNode Warning] Removing duplicate point near x={points[i]['x']}.") + print(f"[TWanSigmaGraph Warning] Removing duplicate point near x={points[i]['x']}.") # Ensure minimum number of points (e.g., 2 for interpolation) if len(unique_points) < 2: - print("[SigmaGraphNode Warning] Not enough unique points after cleanup. Using default points.") + print("[TWanSigmaGraph Warning] Not enough unique points after cleanup. Using default points.") return default_points_list return unique_points @@ -162,9 +162,9 @@ class SigmaGraphNode: # Convert the final list of sigma values to a PyTorch tensor sigmas_tensor = torch.tensor(sigma_values, dtype=torch.float32) - # print(f"[SigmaGraphNode Debug] Calculated Sigmas ({len(sigma_values)} values, first 5): {sigmas_tensor[:5]}...") + # print(f"[TWanSigmaGraph Debug] Calculated Sigmas ({len(sigma_values)} values, first 5): {sigmas_tensor[:5]}...") return (sigmas_tensor, steps,) # --- Node Registration --- -NODE_CLASS_MAPPINGS = { "SigmaGraphNode": SigmaGraphNode } -NODE_DISPLAY_NAME_MAPPINGS = { "SigmaGraphNode": "Sigma Schedule Graph" } +NODE_CLASS_MAPPINGS = { "TWanSigmaGraph": TWanSigmaGraph } +NODE_DISPLAY_NAME_MAPPINGS = { "TWanSigmaGraph": "Sigma Schedule Graph" } diff --git a/__init__.py b/__init__.py index 1bc5a29..e727dd8 100644 --- a/__init__.py +++ b/__init__.py @@ -3,7 +3,7 @@ from .TWanSigmaGraph import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS WEB_DIRECTORY = "./js" -NODE_DISPLAY_NAME_MAPPINGS = {} +# NODE_DISPLAY_NAME_MAPPINGS = {} __all__ = [ "NODE_CLASS_MAPPINGS", diff --git a/js/TWanSigmaGraphWidget.js b/js/TWanSigmaGraphWidget.js index e0227af..7e2e221 100644 --- a/js/TWanSigmaGraphWidget.js +++ b/js/TWanSigmaGraphWidget.js @@ -1,911 +1,492 @@ -// SigmaGraphWidget.js -// Refactored to use the addDOMWidget API -console.log(window.LiteGraph); -console.log(window.app); -console.log(window.ui); -console.log("[SigmaGraphWidget] top-level: file loaded successfully"); +// TWanSigmaGraphWidget.js – v2.24 +// ------------------------------- +// Companion widget for the TWanSigmaGraph node. Draws a bi‑ +// directional sigma schedule, preview text, density controls, +// save/load slots, undo, and an ℹ️ info popup. import { app } from "/../scripts/app.js"; import { $el } from "/../scripts/ui.js"; -//import { LiteGraph } from "/scripts/litegraph.js"; -/** Node and widget constants */ -const nodeClassName = "SigmaGraphNode"; -const graphDataWidgetName = "graph_data"; // Name of the hidden STRING widget -const stepsWidgetName = "steps"; -const customWidgetName = "sigma_graph_ui"; // Name for our new custom DOM widget +/*—— Constants ——*/ +const NODE_CLASS = "TWanSigmaGraph"; +const GRAPH_DATA_NAME = "graph_data"; +const STEPS_NAME = "steps"; +const CANVAS_TYPE = "SIGMA_GRAPH_CANVAS"; +const MIN_POINTS = 3; +const NUM_SLOTS = 8; +const LS_KEY = "sigma_graph_saveSlots"; +const UNDO_LIMIT = 1; +const MIN_NODE_WIDTH = 180; +const GRAB_THRESHOLD = 0.08; +const POINT_RADIUS = 6; +const POINT_COLOR = "#1E88E5"; -/** How many preset slots we allow for saving/loading graph shapes. */ -const numPresetSlots = 9; -/** Minimum number of points allowed on the graph */ -const minGraphPoints = 3; +/*—— Helper Functions ——*/ -// --- Helper Functions (now mostly standalone) --- +/** Linear‑interpolate pts→sigma array of length=steps */ +function calcSigmas(pts, steps) { + steps = Math.max(1, steps | 0); + const p = (pts || []).slice().sort((a, b) => a.x - b.x); + if (p.length < 2) return Array(steps).fill(1.0); -/** - * Calculates sigma values for the text preview based on points and steps. - */ -function calculateSigmasJS(points, steps) { - const sigmaValues = []; - steps = Math.max(1, Math.floor(steps)); + if (!p.some((pt) => Math.abs(pt.x) < 1e-6)) + p.unshift({ x: 0, y: p[0].y }); + if (!p.some((pt) => Math.abs(pt.x - 1) < 1e-6)) + p.push({ x: 1, y: p[p.length - 1].y }); - if (!points || points.length < 1) { - for (let i = 0; i < steps; i++) { sigmaValues.push(1.0); } - return sigmaValues; - } - - if (steps === 1) { - const startPoint = points.find((p) => Math.abs(p.x - 0.0) < 1e-6); - return [startPoint ? Math.max(0.001, startPoint.y) : (points[0]?.y ?? 1.0)]; - } - - let localPoints = JSON.parse(JSON.stringify(points)); // Deep copy - if (!Array.isArray(localPoints) || localPoints.length < 2) { - const yVal = localPoints[0]?.y ?? 1.0; - for (let i = 0; i < steps; i++) { - sigmaValues.push(Math.max(0.001, yVal - (yVal * i) / (steps - 1))); - } - return sigmaValues; - } - - localPoints.sort((a, b) => a.x - b.x); - if (localPoints[0].x > 1e-6) { // Tolerance for boundary check - localPoints.unshift({ x: 0.0, y: localPoints[0].y }); - } - if (localPoints[localPoints.length - 1].x < 1.0 - 1e-6) { // Tolerance - localPoints.push({ x: 1.0, y: localPoints[localPoints.length - 1].y }); - } - - let currentPointIndex = 0; + const out = []; + let idx = 0; for (let i = 0; i < steps; i++) { - const stepProgress = steps === 1 ? 0 : i / (steps - 1); - while ( - currentPointIndex < localPoints.length - 2 && - localPoints[currentPointIndex + 1].x < stepProgress - 1e-6 // Tolerance - ) { - currentPointIndex++; - } - const p1 = localPoints[currentPointIndex]; - const p2 = localPoints[currentPointIndex + 1] || p1; - let sigma; - const x_diff = p2.x - p1.x; - if (x_diff <= 1e-6) { - sigma = p1.y; - } else { - const clampedProgress = Math.max(p1.x, Math.min(p2.x, stepProgress)); - const interpFactor = (clampedProgress - p1.x) / x_diff; - sigma = p1.y + interpFactor * (p2.y - p1.y); - } - sigmaValues.push(Math.max(0.001, sigma)); + const t = steps === 1 ? 0 : i / (steps - 1); + while (idx < p.length - 2 && p[idx + 1].x < t) idx++; + const [p1, p2] = [p[idx], p[idx + 1]]; + const dx = p2.x - p1.x; + let y = dx < 1e-6 + ? (Math.abs(t - p2.x) < Math.abs(t - p1.x) ? p2.y : p1.y) + : p1.y + ((t - p1.x) / dx) * (p2.y - p1.y); + out.push(Math.max(0.001, Math.round(y * 100) / 100)); } - return sigmaValues; + return out; } -/** - * Generates `numTargetPoints` points by interpolating along `existingPoints`. - */ -function interpolatePoints(existingPoints, numTargetPoints) { - if (numTargetPoints < 2 || !existingPoints || existingPoints.length < 2) { - return [ { x: 0, y: 1 }, { x: 1, y: 0 } ]; - } - const newPoints = []; - const sortedExisting = [...existingPoints].sort((a, b) => a.x - b.x); - if (sortedExisting[0].x > 1e-6) { - sortedExisting.unshift({ x: 0.0, y: sortedExisting[0].y }); - } - if (sortedExisting[sortedExisting.length - 1].x < 1.0 - 1e-6) { - sortedExisting.push({ x: 1.0, y: sortedExisting[sortedExisting.length - 1].y }); - } +/** Parse either JSON‑[{x,y}] or comma‑list "y,y,y" */ +function strToPts(str) { + try { + const arr = JSON.parse(str); + if ( + Array.isArray(arr) && + arr.every((o) => typeof o.x === "number" && typeof o.y === "number") + ) return arr; + } catch { /* ignore */ } - let currentSegmentIndex = 0; - for (let i = 0; i < numTargetPoints; i++) { - const targetX = numTargetPoints === 1 ? 0 : i / (numTargetPoints - 1); - while ( - currentSegmentIndex < sortedExisting.length - 2 && - sortedExisting[currentSegmentIndex + 1].x < targetX - 1e-6 // Tolerance - ) { - currentSegmentIndex++; - } - const p1 = sortedExisting[currentSegmentIndex]; - const p2 = sortedExisting[currentSegmentIndex + 1] || p1; - let interpolatedY; - const segmentXDiff = p2.x - p1.x; - if (segmentXDiff <= 1e-6) { - interpolatedY = p1.y; - } else { - const clampedTargetX = Math.max(p1.x, Math.min(p2.x, targetX)); - const interpFactor = (clampedTargetX - p1.x) / segmentXDiff; - interpolatedY = p1.y + interpFactor * (p2.y - p1.y); - } - newPoints.push({ x: targetX, y: Math.max(0.001, interpolatedY) }); - } - - const uniqueNewPoints = []; - const seenX = new Set(); - for (const p of newPoints) { - let isDuplicate = false; - for(const sx of seenX) { if (Math.abs(p.x - sx) < 1e-6) { isDuplicate = true; break; } } - if (!isDuplicate) { uniqueNewPoints.push(p); seenX.add(p.x); } - } - if (uniqueNewPoints.length > 0) { - if (!uniqueNewPoints.some(p => Math.abs(p.x - 0.0) < 1e-6)) - uniqueNewPoints.unshift({x: 0, y: newPoints[0].y}); - if (!uniqueNewPoints.some(p => Math.abs(p.x - 1.0) < 1e-6)) - uniqueNewPoints.push({x: 1, y: newPoints[newPoints.length-1].y}); - } else { - return [ { x: 0, y: 1 }, { x: 1, y: 0 } ]; - } - return uniqueNewPoints.sort((a,b) => a.x - b.x); + const nums = str + .split(/[^0-9.+-]+/) + .map(parseFloat) + .filter((n) => !isNaN(n)); + if (nums.length < 2) { + const y = nums[0] || 1; + return [{ x: 0, y }, { x: 1, y: nums[0] != null ? y : 0 }]; + } + return nums.map((y, i) => ({ x: i / (nums.length - 1), y: Math.round(y * 100) / 100 })); } -/** - * Redraws the curve on the canvas. - */ -function drawCurve(widgetState) { - const canvasEl = widgetState.canvasEl; - if (!canvasEl || !widgetState._points) return; - const rect = canvasEl.getBoundingClientRect(); - if (rect.width <= 0 || rect.height <= 0) return; +/** Convert [{x,y}] → "y, y, y" */ +function ptsToStr(pts, prec = 2) { + return pts.map((p) => p.y.toFixed(prec)).join(", "); +} - const dpr = window.devicePixelRatio || 1; - const displayWidth = Math.round(rect.width); - const displayHeight = Math.round(rect.height); - if (canvasEl.width !== displayWidth * dpr || canvasEl.height !== displayHeight * dpr) { - canvasEl.width = displayWidth * dpr; - canvasEl.height = displayHeight * dpr; - canvasEl.style.width = `${displayWidth}px`; - canvasEl.style.height = `${displayHeight}px`; +/*—— Main Widget Setup ——*/ + +function setup(node) { + // prevent double init + if (node._sigmaSetupDone) return; + node._sigmaSetupDone = true; + + // 1) locate the hidden textarea + const gw = node.widgets.find((w) => w.name === GRAPH_DATA_NAME); + if (!gw || !gw.element) return; + const ta = gw.element; + + // 2) style it + ta.style.minHeight = "10px"; + ta.style.boxSizing = "border-box"; + ta.style.resize = "vertical"; + + // 3) per‑node storage key + const storageKey = `TWanSigmaGraph_last_${node.id}`; + + // 4) try load from cache + let initialPts = null; + const cached = localStorage.getItem(storageKey); + if (cached) { + try { + initialPts = strToPts(cached); + ta.value = ptsToStr(initialPts); + gw.value = JSON.stringify(initialPts); + } catch { + initialPts = null; + } } - const ctx = canvasEl.getContext("2d"); - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - ctx.clearRect(0, 0, displayWidth, displayHeight); - - ctx.strokeStyle = "#444"; - ctx.lineWidth = 0.5; - ctx.beginPath(); - for (let i = 1; i < 10; i++) { - const vx = (i * displayWidth) / 10; - const vy = (i * displayHeight) / 10; - ctx.moveTo(vx, 0); - ctx.lineTo(vx, displayHeight); - ctx.moveTo(0, vy); - ctx.lineTo(displayWidth, vy); + // 5) if no cache → four‐point default + if (!initialPts) { + const defaultText = "1.00, 0.67, 0.33, 0.00"; + ta.value = defaultText; + initialPts = strToPts(defaultText); + gw.value = JSON.stringify(initialPts); } - ctx.stroke(); - const sortedPoints = [...widgetState._points].sort((a, b) => a.x - b.x); - if (sortedPoints.length < 2) return; - ctx.strokeStyle = "white"; - ctx.lineWidth = 1.5; - ctx.beginPath(); - sortedPoints.forEach((pt, idx) => { - const px = pt.x * displayWidth; - const py = (1 - pt.y) * displayHeight; - if (idx === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); + // 6) Build UI: container, canvas, preview + const wrap = $el("div", { + style: { + width: "100%", position: "relative", display: "flex", + flexDirection: "column", flexGrow: "1", minHeight: "180px", + paddingBottom: "20px", minWidth: `${MIN_NODE_WIDTH}px` + } }); - ctx.stroke(); + const canvas = $el("canvas", { + style: { + width: "100%", flexGrow: "1", minHeight: "125px", + background: "#282828", border: "1px solid #555", + borderRadius: "4px", cursor: "crosshair" + } + }); + wrap.appendChild(canvas); - widgetState._points.forEach((pt, idx) => { - const ratio = widgetState._points.length > 1 ? idx / (widgetState._points.length - 1) : 0; - const startColor = { r: 61, g: 3, b: 0 }; - const endColor = { r: 255, g: 0, b: 0 }; - const r = Math.round(startColor.r + ratio * (endColor.r - startColor.r)); - const g = Math.round(startColor.g + ratio * (endColor.g - startColor.g)); - const b = Math.round(startColor.b + ratio * (endColor.b - startColor.b)); - ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; - const px = pt.x * displayWidth; - const py = (1 - pt.y) * displayHeight; + const preview = $el("textarea", { + placeholder: "Sigmas preview…", + readOnly: true, + style: { + width: "100%", height: '55px', fontFamily: "monospace", + background: "#181818", color: "#ccc", + border: "1px solid #555", borderRadius: "4px", + boxSizing: "border-box", padding: "4px", resize: "none", + marginTop: "4px" + } + }); + wrap.appendChild(preview); + + node.addDOMWidget(CANVAS_TYPE, "custom", wrap, { + getHeight: () => Math.max(180, wrap.scrollHeight) + }); + + // enforce min width on resize + if (node.size[0] < MIN_NODE_WIDTH) { + node.setSize([MIN_NODE_WIDTH, node.size[1]]); + } + const prevOnResize = node.onResize; + node.onResize = function() { + prevOnResize?.apply(this, arguments); + if (this.size[0] < MIN_NODE_WIDTH) { + this.setSize([MIN_NODE_WIDTH, this.size[1]]); + } + }; + + /*—— Density Controls (+ / –) ——*/ + const addBtn = $el("button", { + textContent: "+", title: "Double density", + style: { + position: "absolute", top: "4px", right: "40px", + width: "24px", height: "24px", + borderRadius: "3px", border: "1px solid #555", + background: "#64B5F6", color: "#fff", cursor: "pointer" + } + }); + const delBtn = $el("button", { + textContent: "−", title: "Halve density", + style: { + position: "absolute", top: "4px", right: "10px", + width: "24px", height: "24px", + borderRadius: "3px", border: "1px solid #555", + background: "#EF9A9A", color: "#fff", cursor: "pointer" + } + }); + wrap.appendChild(addBtn); + wrap.appendChild(delBtn); + + /*—— Slots & Presets ——*/ + const slotBar = $el("div", { + style: { display: "flex", gap: "4px", marginTop: "4px", alignItems: "center" } + }); + wrap.appendChild(slotBar); + + const slotsData = JSON.parse(localStorage.getItem(LS_KEY) || "[]"); + let recordMode = false; + const recBtn = $el("button", { + textContent: "💾", title: "Toggle save mode", + style: { minWidth: "28px", cursor: "pointer" } + }); + slotBar.appendChild(recBtn); + + const slotBtns = []; + for (let i = 0; i < NUM_SLOTS; i++) { + const b = $el("button", { + textContent: `${i + 1}`, + disabled: !slotsData[i] && !recordMode, + style: { + flex: "1", minWidth: "20px", + opacity: slotsData[i]||recordMode ? "1" : "0.3", + cursor: slotsData[i]||recordMode ? "pointer" : "not-allowed" + } + }); + slotBar.appendChild(b); + slotBtns.push(b); + + b.onclick = () => { + if (recordMode) { + slotsData[i] = gw.value; + localStorage.setItem(LS_KEY, JSON.stringify(slotsData)); + recBtn.click(); + } else if (slotsData[i]) { + let pts; + try { pts = JSON.parse(slotsData[i]); } + catch { pts = strToPts(slotsData[i]); } + applyPoints(pts); + } + }; + } + + recBtn.onclick = () => { + recordMode = !recordMode; + recBtn.style.background = recordMode ? "#4caf50" : ""; + slotBtns.forEach((b, i) => { + const ok = slotsData[i] || recordMode; + b.disabled = !ok; + b.style.opacity = ok ? "1" : "0.3"; + b.style.cursor = ok ? "pointer" : "not-allowed"; + }); + }; + + /*—— Info Popup ——*/ + const infoBtn = $el("button", { + textContent: "ℹ️", title: "Instructions", + style: { + flex: "0 0 auto", minWidth: "10px", + opacity: "0.6", cursor: "pointer", + background: "transparent", border: "none", + color: "#ccc" + } + }); + const advBtn = $el("button", { + textContent: "§", title: "Advanced (WIP)", disabled: true, + style: { + flex: "0 0 auto", minWidth: "10px", + opacity: "0.4", cursor: "not-allowed", + background: "transparent", border: "none", + color: "#666" + } + }); + slotBar.appendChild(infoBtn); + slotBar.appendChild(advBtn); + + const overlay = $el("div", { + style: { + position: "absolute", + top: "15%", + left: "50%", + transform: "translate(-50%, -50%)", + width: "240px", + background: "#222", + color: "#ccc", + border: "1px solid #555", + borderRadius: "6px", + padding: "8px 12px", + fontSize: "11px", + lineHeight: "1.4", + textAlign: "center", + display: "none", + zIndex: "9999", + pointerEvents: "auto" + } + }); + overlay.innerHTML = ` + Sigma Schedule Editor
+ Bidirectional editor for sigma schedules.
+ Use + / – to adjust density.
+ Expand node to preview sigmas.
+ Special thanks to huchenlei! + `; + wrap.appendChild(overlay); + + infoBtn.addEventListener("click", (e) => { + e.stopPropagation(); + overlay.style.display = + overlay.style.display === "none" ? "block" : "none"; + }); + wrap.addEventListener("click", () => { + if (overlay.style.display === "block") { + overlay.style.display = "none"; + } + }); + + /*—— Undo & Draw Logic ——*/ + let undoStack = []; + function pushUndo(state) { + undoStack.unshift(state); + if (undoStack.length > UNDO_LIMIT) undoStack.pop(); + } + + function applyPoints(pts) { + pushUndo(gw.value); + const clean = pts.map((p) => ({ + x: p.x, + y: Math.round(p.y * 100) / 100 + })); + gw.value = JSON.stringify(clean); + ta.value = ptsToStr(clean); + + // persist to cache + localStorage.setItem(storageKey, gw.value); + + draw(clean); + } + + const ctx = canvas.getContext("2d"); + function draw(overridePts) { + const pts = overridePts || strToPts(gw.value); + const dpr = window.devicePixelRatio || 1; + const w = canvas.clientWidth, h = canvas.clientHeight; + canvas.width = w * dpr; + canvas.height = h * dpr; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.clearRect(0, 0, w, h); + + // grid + ctx.strokeStyle = "#444"; + ctx.lineWidth = 0.5; ctx.beginPath(); - ctx.arc(px, py, 4, 0, Math.PI * 2); - ctx.fill(); + for (let i = 1; i < 10; i++) { + const x = (i * w) / 10, y = (i * h) / 10; + ctx.moveTo(x, 0); ctx.lineTo(x, h); + ctx.moveTo(0, y); ctx.lineTo(w, y); + } + ctx.stroke(); + + // curve & handles + if (pts.length >= 2) { + ctx.strokeStyle = "#fff"; + ctx.lineWidth = 1.5; + ctx.beginPath(); + pts.forEach((p, i) => { + const px = p.x * w, py = (1 - p.y) * h; + i ? ctx.lineTo(px, py) : ctx.moveTo(px, py); + }); + ctx.stroke(); + + pts.forEach((p) => { + const px = p.x * w, py = (1 - p.y) * h; + ctx.fillStyle = POINT_COLOR; + ctx.beginPath(); + ctx.arc(px, py, POINT_RADIUS, 0, 2 * Math.PI); + ctx.fill(); + }); + } + + // update text preview + const stepsW = node.widgets.find((w) => w.name === STEPS_NAME).value | 0; + preview.value = calcSigmas(pts, stepsW) + .map((v) => v.toFixed(2)) + .join(", "); + } + + /*—— Event Bindings ——*/ + let debounce; + ta.addEventListener("input", () => { + clearTimeout(debounce); + debounce = setTimeout(() => { + if (ta.value.trim()) draw(strToPts(ta.value)); + }, 200); }); + ta.addEventListener("blur", () => { + if (ta.value.trim()) applyPoints(strToPts(ta.value)); + }); + ta.addEventListener("keydown", (e) => { + if ((e.ctrlKey||e.metaKey) && e.key==="z" && undoStack.length) { + e.preventDefault(); + const prev = undoStack.shift(); + gw.value = prev; + const pts = strToPts(prev); + ta.value = ptsToStr(pts); + draw(pts); + } + }); + + addBtn.onclick = () => { + const pts = strToPts(gw.value); + const n = pts.length + 1; + applyPoints(calcSigmas(pts, n).map((y,i)=>({ x:i/(n-1), y }))); + }; + delBtn.onclick = () => { + const pts = strToPts(gw.value); + if (pts.length > MIN_POINTS) { + const n = pts.length - 1; + applyPoints(calcSigmas(pts, n).map((y,i)=>({ x:i/(n-1), y }))); + } + }; + + // pointer interactions: drag, dblclick add, RMB delete + let dragIdx = -1; + canvas.onpointerdown = (e) => { + const rect = canvas.getBoundingClientRect(); + const x = (e.clientX-rect.left)/rect.width; + const y = 1 - (e.clientY-rect.top)/rect.height; + const pts = strToPts(gw.value); + + if (e.button===2) { + const idx = pts.findIndex(p=>Math.hypot(p.x-x,p.y-y)0 && idxMIN_POINTS) { + pushUndo(gw.value); + pts.splice(idx,1); + applyPoints(pts); + } + e.preventDefault(); + return; + } + + if (e.detail===2) { + pushUndo(gw.value); + pts.push({ x, y }); + pts.sort((a,b)=>a.x-b.x); + applyPoints(pts); + return; + } + + dragIdx = pts.findIndex(p=>Math.hypot(p.x-x,p.y-y)=0) e.target.setPointerCapture(e.pointerId); + }; + canvas.onpointermove = (e) => { + const rect = canvas.getBoundingClientRect(); + const x = (e.clientX-rect.left)/rect.width; + const y = 1 - (e.clientY-rect.top)/rect.height; + const pts = strToPts(gw.value); + + if (dragIdx>=0) { + const ny = Math.min(1, Math.max(0, y)); + pts[dragIdx] = { x:pts[dragIdx].x, y:ny }; + applyPoints(pts); + } + + const over = pts.some(p=>Math.hypot(p.x-x,p.y-y)=0||over ? "pointer":"crosshair"; + }; + canvas.onpointerup = (e) => { + if (dragIdx>=0) { + e.target.releasePointerCapture(e.pointerId); + dragIdx = -1; + } + }; + canvas.oncontextmenu = (e) => e.preventDefault(); + + // recompute on step‐count change + const sw = node.widgets.find((w) => w.name === STEPS_NAME); + if (sw) sw.callback = () => applyPoints(strToPts(ta.value)); + + // initial draw + applyPoints(strToPts(ta.value)); + new ResizeObserver(() => draw()).observe(canvas); } -/** - * Updates the sigma text area display. - */ -function updateSigmaDisplay(widgetState) { - if (!widgetState.sigmaDisplayEl || !widgetState.stepsWidget || !widgetState._points) return; - const steps = widgetState.stepsWidget.value; - const sigmas = calculateSigmasJS(widgetState._points, steps); - widgetState.sigmaDisplayEl.value = sigmas.map((s) => s.toFixed(3)).join(", "); -} - -/** - * Updates the enabled state of the remove (-) button. - */ -function updateRemoveButtonState(widgetState) { - if (!widgetState.removeButtonEl || !widgetState._points) return; - const canRemove = widgetState._points.length > minGraphPoints; - widgetState.removeButtonEl.disabled = !canRemove; - widgetState.removeButtonEl.style.opacity = canRemove ? "1" : "0.5"; - widgetState.removeButtonEl.style.cursor = canRemove ? "pointer" : "not-allowed"; -} - -// --- Main Extension Registration --- +/*—— Registration ——*/ app.registerExtension({ - name: "sigma_graph.widget", - async beforeRegisterNodeDef(nodeType, nodeData, appInstance) { - if (nodeData.name !== nodeClassName) return; - - // Patch onConfigure to set up the custom widget - const originalOnConfigure = nodeType.prototype.onConfigure; - nodeType.prototype.onConfigure = function (info) { - originalOnConfigure?.apply(this, arguments); - if (this.widgets?.find(w => w.name === customWidgetName)) { return; } - console.log(`[SigmaGraphWidget] ${this.id}: Patched onConfigure running - setting up widget.`); - - const dataWidget = this.widgets.find((w) => w.name === graphDataWidgetName); - const stepsWidget = this.widgets.find((w) => w.name === stepsWidgetName); - if (!dataWidget) { console.error(`[SigmaGraphWidget] ${this.id}: Could not find data widget '${graphDataWidgetName}'!`); return; } - if (!stepsWidget) { console.error(`[SigmaGraphWidget] ${this.id}: Could not find steps widget '${stepsWidgetName}'!`); } - - if (dataWidget.inputEl) { dataWidget.inputEl.style.display = "none"; } - try { - const widgetIndex = this.widgets.findIndex(w => w === dataWidget); - if (widgetIndex !== -1 && this.inputs && this.inputs[widgetIndex]) { - const slotElement = this.graphcanvas?.canvas?.querySelector(`.node_${this.id}_slot_${widgetIndex}`); - if (slotElement?.previousElementSibling?.tagName === 'LABEL') { - slotElement.previousElementSibling.style.display = 'none'; - } - } - } catch(e) { console.warn("[SigmaGraphWidget] Error trying to hide label:", e); } - - // Build the root element for the widget UI - const rootElement = $el("div", { - className: "sigma-graph-widget-container", - style: { width: "100%", height: "100%", padding: "5px", boxSizing: "border-box", display: "flex", flexDirection: "column", gap: "10px" }, - }); - - // Initialize widget state and store key elements in widgetState object - const widgetState = { - _points: [], - _saveSlots: new Array(numPresetSlots).fill(null), - _saveSlotButtons: [], - _recordMode: false, - _draggingIndex: -1, - _rightClickIndex: -1, - rootElement: rootElement, - node: this, - dataWidget: dataWidget, - stepsWidget: stepsWidget, - sigmaDisplayEl: null, - canvasContainerEl: null, - canvasEl: null, - presetContainerEl: null, - addButtonEl: null, - removeButtonEl: null, - refreshButton: null, // We'll assign the refresh button here - originalStepsCallback: stepsWidget ? stepsWidget.callback : null, - saveTimeout: null, - boundMouseUpHandler: null, + name: "TWanSigmaGraph.widget", + beforeRegisterNodeDef(nt, nd) { + if (nd.name === NODE_CLASS) { + const orig = nt.prototype.onConfigure; + nt.prototype.onConfigure = function(info) { + orig?.apply(this, arguments); + setup(this); }; + } + }, +}); - const saveStateToDataWidget = () => { - clearTimeout(widgetState.saveTimeout); - widgetState.saveTimeout = setTimeout(() => { - widgetState._points.sort((a, b) => a.x - b.x); - const serialized = JSON.stringify(widgetState._points); - if (widgetState.dataWidget.value !== serialized) { - widgetState.dataWidget.value = serialized; - } - }, 100); - }; - - // --- Build the UI --- - - // Sigma display and button grid container - const sigmaDisplayContainer = $el("div", { style: { display: "flex", flexDirection: "row", gap: "5px", alignItems: "stretch" } }); - const sigmaDisplay = $el("textarea", { - placeholder: "Sigma values...", - style: { - flexGrow: "1", - minHeight: "5px", - fontSize: "0.9em", - background: "#181818", - color: "#ccc", - border: "1px solid #555", - borderRadius: "3px", - boxSizing: "border-box", - resize: "auto", - fontFamily: "monospace", - padding: "2px 4px", - } - }); - widgetState.sigmaDisplayEl = sigmaDisplay; - sigmaDisplayContainer.appendChild(sigmaDisplay); - - const buttonGridContainer = $el("div", { style: { display: "flex", flexDirection: "row", gap: "1px", height: "45px" } }); - sigmaDisplayContainer.appendChild(buttonGridContainer); - rootElement.appendChild(sigmaDisplayContainer); - - // Left column for refresh & advanced buttons - const leftButtonColumn = $el("div", { style: { display: "flex", flexDirection: "column", alignItems: "stretch", width: "20px", borderRadius: "3px", overflow: "hidden", border: "1px solid #555", background: "#444" } }); - buttonGridContainer.appendChild(leftButtonColumn); - - // Create refreshButton and attach it to widgetState.refreshButton - widgetState.refreshButton = $el("button", { - textContent: "🔄", - style: { - flex: "1 1 auto", - display: "flex", - alignItems: "center", - justifyContent: "center", - fontSize: "13px", - border: "none", - borderBottom: "1px solid #555", - background: "transparent", - color: "#ccc", - cursor: "pointer", - lineHeight: "normal", - padding: "0" - }, - title: "Refresh text display from current graph/steps" - }); - leftButtonColumn.appendChild(widgetState.refreshButton); - - // Create advancedButton (for demonstration purposes) - const advancedButton = $el("button", { - textContent: "⚙️", - style: { - flex: "1 1 auto", - display: "flex", - alignItems: "center", - justifyContent: "center", - fontSize: "13px", - border: "none", - background: "transparent", - color: "#666", - cursor: "not-allowed", - lineHeight: "normal", - padding: "0" - }, - title: "Advanced Mode (Not Implemented)" - }); - leftButtonColumn.appendChild(advancedButton); - - // Right column for info & apply buttons - const rightButtonColumn = $el("div", { style: { display: "flex", flexDirection: "column", alignItems: "stretch", width: "20px", borderRadius: "3px", overflow: "hidden", border: "1px solid #555", background: "#444" } }); - buttonGridContainer.appendChild(rightButtonColumn); - - const infoButton = $el("button", { - textContent: "ℹ", - style: { - flex: "1 1 auto", - display: "flex", - alignItems: "center", - justifyContent: "center", - fontSize: "13px", - border: "none", - borderBottom: "1px solid #555", - background: "transparent", - color: "#ccc", - cursor: "pointer", - lineHeight: "normal", - padding: "0" - }, - title: "Show instructions about this node" - }); - rightButtonColumn.appendChild(infoButton); - - const applyButton = $el("button", { - textContent: "▼", - style: { - flex: "1 1 auto", - display: "flex", - alignItems: "center", - justifyContent: "center", - fontSize: "13px", - border: "none", - background: "transparent", - color: "#ccc", - cursor: "pointer", - lineHeight: "normal", - padding: "0" - }, - title: "Apply sigma values from the text area to the graph points" - }); - rightButtonColumn.appendChild(applyButton); - - // Info overlay element - const infoOverlay = $el("div", { - style: { - position: "absolute", - top: "55%", - left: "50%", - transform: "translate(-50%, -50%)", - width: "240px", - background: "#222", - color: "#ccc", - border: "1px solid #555", - borderRadius: "6px", - padding: "8px 12px", - fontSize: "11px", - lineHeight: "1.4", - textAlign: "center", - display: "none", - zIndex: "9999", - pointerEvents: "none" - } - }); - infoOverlay.innerHTML = `
- Sigma Schedule Editor
- Bidirectional editor for sigma schedules.
- 🔄: Refresh text from graph/steps.
- ▼: Apply text to graph points.
- Drag, double-click, right-click to edit points.
- Use +/- on graph to adjust density.
- Special thanks to huchenlei for js help!! -
`; - rootElement.appendChild(infoOverlay); - - // Canvas container and canvas element - const canvasContainer = $el("div", { - style: { - position: "relative", - width: "100%", - flexGrow: "1", - minHeight: "50px", - boxSizing: "border-box", - cursor: "crosshair", - overflow: "hidden" - } - }); - const canvas = $el("canvas", { - style: { - display: "block", - width: "100%", - height: "100%", - background: "#282828", - border: "2px solid #555", - borderRadius: "3px", - boxSizing: "border-box" - } - }); - widgetState.canvasContainerEl = canvasContainer; - widgetState.canvasEl = canvas; - canvasContainer.appendChild(canvas); - rootElement.appendChild(canvasContainer); - - // Add and remove buttons for graph points - const addButton = $el("button", { - textContent: "+", - style: { - position: "absolute", - top: "4px", - right: "30px", - width: "22px", - height: "22px", - fontSize: "16px", - lineHeight: "20px", - textAlign: "center", - padding: "0", - background: "#4CAF50", - color: "white", - border: "1px solid #388E3C", - borderRadius: "3px", - cursor: "pointer", - zIndex: "10" - }, - title: "Add points (increase density)" - }); - canvasContainer.appendChild(addButton); - widgetState.addButtonEl = addButton; - - const removeBtn = $el("button", { - textContent: "-", - style: { - position: "absolute", - top: "4px", - right: "4px", - width: "22px", - height: "22px", - fontSize: "18px", - lineHeight: "18px", - textAlign: "center", - padding: "0", - background: "#f44336", - color: "white", - border: "1px solid #d32f2f", - borderRadius: "3px", - cursor: "pointer", - zIndex: "10" - }, - title: "Remove points (decrease density)" - }); - canvasContainer.appendChild(removeBtn); - widgetState.removeButtonEl = removeBtn; - - // Preset container and save mode button - const presetContainer = $el("div", { - style: { display: "inline-flex", flexDirection: "row", alignItems: "center", gap: "5px", marginTop: "0px" } - }); - widgetState.presetContainerEl = presetContainer; - rootElement.appendChild(presetContainer); - - const saveModeButton = $el("button", { - textContent: "💾", - style: { - width: "30px", - height: "30px", - borderRadius: "3px", - border: "1px solid #555", - background: "#96322f", - color: "#fff", - fontSize: "15px", - cursor: "pointer", - padding: "0", - lineHeight: "30px", - textAlign: "center" - }, - title: "Toggle Save Mode (then click a slot to save). Otherwise, click to load." - }); - presetContainer.appendChild(saveModeButton); - - // Load existing preset slots from localStorage - const savedSlotsJSON = localStorage.getItem("sigma_graph_saveSlots"); - if (savedSlotsJSON) { - try { - const savedSlots = JSON.parse(savedSlotsJSON); - if (Array.isArray(savedSlots) && savedSlots.length === numPresetSlots) { - widgetState._saveSlots = savedSlots; - } - } catch (err) { - console.error("Error parsing saved slots:", err); - } - } - - const updateSaveSlotVisibility = () => { - widgetState._saveSlotButtons.forEach((btn, idx) => { - if (widgetState._recordMode) { - btn.style.visibility = "visible"; - btn.style.backgroundColor = "#4caf50"; - } else { - if (widgetState._saveSlots[idx]) { - btn.style.visibility = "visible"; - btn.style.backgroundColor = "#273648"; - } else { - btn.style.visibility = "hidden"; - } - } - }); - saveModeButton.style.background = widgetState._recordMode ? "#4caf50" : "#333"; - }; - - for (let i = 0; i < numPresetSlots; i++) { - const slotContainer = $el("div", { style: { width: "30px", height: "30px", display: "inline-block" } }); - const slotButton = $el("button", { - textContent: `${i + 1}`, - style: { width: "100%", height: "100%", boxSizing: "border-box", padding: "0", borderRadius: "3px", border: "1px solid #555", background: "#333", color: "#fff", fontSize: "15px", cursor: "pointer" }, - title: `Slot #${i + 1}` - }); - slotButton.addEventListener("click", () => { - if (widgetState._recordMode) { - widgetState._saveSlots[i] = JSON.stringify(widgetState._points); - localStorage.setItem("sigma_graph_saveSlots", JSON.stringify(widgetState._saveSlots)); - widgetState._recordMode = false; - updateSaveSlotVisibility(); - } else { - if (widgetState._saveSlots[i]) { - try { - const loadedPoints = JSON.parse(widgetState._saveSlots[i]); - customWidget.value = loadedPoints; - } catch (e) { - console.error(`Error parsing slot ${i + 1}:`, e); - } - } - } - }); - widgetState._saveSlotButtons.push(slotButton); - slotContainer.appendChild(slotButton); - presetContainer.appendChild(slotContainer); - } - updateSaveSlotVisibility(); - saveModeButton.addEventListener("click", () => { - widgetState._recordMode = !widgetState._recordMode; - updateSaveSlotVisibility(); - }); - - // --- Define Options for addDOMWidget --- - const widgetOptions = { - setValue: (value) => { - let parsedPoints = []; - try { - if (typeof value === 'string') { - parsedPoints = JSON.parse(value || '[]'); - } else if (Array.isArray(value)) { - parsedPoints = value; - } else { - throw new Error("Invalid value type"); - } - if (!Array.isArray(parsedPoints)) - throw new Error("Parsed value is not an array"); - } catch (e) { - console.error("[SigmaGraphWidget] setValue error:", e); - parsedPoints = [ { x: 0, y: 1 }, { x: 1, y: 0 } ]; - } - if (parsedPoints.length < 1) - parsedPoints = [ { x: 0, y: 1 }, { x: 1, y: 0 } ]; - if (!parsedPoints.some((p) => Math.abs(p.x - 0.0) < 1e-6)) - parsedPoints.push({ x: 0, y: 1 }); - if (!parsedPoints.some((p) => Math.abs(p.x - 1.0) < 1e-6)) - parsedPoints.push({ x: 1, y: 0 }); - parsedPoints.sort((a, b) => a.x - b.x); - const uniquePoints = []; - const seenX = new Set(); - for(const p of parsedPoints) { - let isDuplicate = false; - for(const sx of seenX) { - if (Math.abs(p.x - sx) < 1e-6) { isDuplicate = true; break; } - } - if (!isDuplicate) { uniquePoints.push(p); seenX.add(p.x); } - } - parsedPoints = uniquePoints; - while (parsedPoints.length < minGraphPoints) { - const mid = Math.floor(parsedPoints.length / 2); - const p1 = parsedPoints[mid-1]; - const p2 = parsedPoints[mid]; - parsedPoints.splice(mid, 0, { x: (p1.x+p2.x)/2, y: (p1.y+p2.y)/2 }); - } - widgetState._points = parsedPoints; - drawCurve(widgetState); - updateSigmaDisplay(widgetState); - updateRemoveButtonState(widgetState); - saveStateToDataWidget(); - }, - getValue: () => { - widgetState._points.sort((a, b) => a.x - b.x); - return JSON.stringify(widgetState._points); - }, - afterResize: () => { - requestAnimationFrame(() => drawCurve(widgetState)); - }, - }; - - // --- Add the Custom DOM Widget --- - const customWidget = this.addDOMWidget(customWidgetName, "custom", rootElement, widgetOptions); - if (!customWidget) { - console.error(`[SigmaGraphWidget] ${this.id}: Failed to add DOM widget!`); - return; - } - - // --- Attach Event Listeners --- - widgetState.refreshButton.addEventListener("click", () => { updateSigmaDisplay(widgetState); }); - advancedButton.addEventListener("click", () => { /* No action */ }); - infoButton.addEventListener("click", () => { infoOverlay.style.display = (infoOverlay.style.display === "none") ? "block" : "none"; }); - applyButton.addEventListener("click", () => { - const textValue = widgetState.sigmaDisplayEl.value.trim(); - if (!textValue) return; - const parts = textValue.split(",").map(s => s.trim()).filter(s => s !== ""); - const parsedSigmas = parts.map(Number); - if (parsedSigmas.some(isNaN) || parsedSigmas.length === 0) return; - let pointsChanged = false; - widgetState._points.sort((a, b) => a.x - b.x); - widgetState._points.forEach((pt) => { - let targetIndex; - if (parsedSigmas.length === 1) targetIndex = 0; - else { - targetIndex = Math.round(pt.x * (parsedSigmas.length - 1)); - targetIndex = Math.max(0, Math.min(parsedSigmas.length - 1, targetIndex)); - } - const newY = Math.max(0.001, parsedSigmas[targetIndex]); - if (pt.y !== newY) { pt.y = newY; pointsChanged = true; } - }); - if (pointsChanged) { - drawCurve(widgetState); - saveStateToDataWidget(); - widgetState.node.setDirtyCanvas(true, true); - } - }); - addButton.addEventListener("click", () => { - const numCurrent = widgetState._points.length; - const numToAdd = Math.max(1, Math.floor(numCurrent * 0.5)); - widgetState._points = interpolatePoints(widgetState._points, numCurrent + numToAdd); - drawCurve(widgetState); - updateSigmaDisplay(widgetState); - updateRemoveButtonState(widgetState); - saveStateToDataWidget(); - }); - removeBtn.addEventListener("click", () => { - const numCurrent = widgetState._points.length; - if (numCurrent <= minGraphPoints) return; - const numToRemove = Math.max(1, Math.floor(numCurrent * 0.5)); - let targetNumPoints = Math.max(minGraphPoints, numCurrent - numToRemove); - const actualNumToRemove = numCurrent - targetNumPoints; - if (actualNumToRemove <= 0) return; - const candidates = []; - const sortedPoints = [...widgetState._points].sort((a, b) => a.x - b.x); - for (let i = 1; i < sortedPoints.length - 1; i++) { - const p_prev = sortedPoints[i - 1]; - const p_curr = sortedPoints[i]; - const p_next = sortedPoints[i + 1]; - const x_diff = p_next.x - p_prev.x; - let interpolatedY; - if (x_diff <= 1e-6) interpolatedY = p_prev.y; - else { - const ratio = (p_curr.x - p_prev.x) / x_diff; - interpolatedY = p_prev.y + ratio * (p_next.y - p_prev.y); - } - candidates.push({ point: p_curr, distance: Math.abs(p_curr.y - interpolatedY) }); - } - candidates.sort((a, b) => a.distance - b.distance); - const pointsToRemove = new Set(candidates.slice(0, actualNumToRemove).map(c => c.point)); - widgetState._points = widgetState._points.filter(pt => !pointsToRemove.has(pt)); - drawCurve(widgetState); - updateSigmaDisplay(widgetState); - updateRemoveButtonState(widgetState); - saveStateToDataWidget(); - }); - - widgetState.boundMouseUpHandler = () => { - if (widgetState._draggingIndex >= 0) { - widgetState._draggingIndex = -1; - if (widgetState.canvasEl) widgetState.canvasEl.style.cursor = "crosshair"; - saveStateToDataWidget(); - } - }; - document.addEventListener("mouseup", widgetState.boundMouseUpHandler, true); - - canvas.addEventListener("mousedown", (evt) => { - if (evt.target === addButton || evt.target === removeBtn) return; - evt.stopPropagation(); - const rect = widgetState.canvasEl.getBoundingClientRect(); - const x = evt.clientX - rect.left; - const y = evt.clientY - rect.top; - const clickRadiusSq = 64; - widgetState._draggingIndex = -1; - widgetState._rightClickIndex = -1; - let closestDistSq = Infinity; - let clickedIndex = -1; - widgetState._points.forEach((pt, i) => { - const px = pt.x * rect.width; - const py = (1 - pt.y) * rect.height; - const distSq = (px - x) ** 2 + (py - y) ** 2; - if (distSq < clickRadiusSq && distSq < closestDistSq) { - closestDistSq = distSq; - clickedIndex = i; - } - }); - if (clickedIndex !== -1) { - if (evt.button === 0) { - widgetState._draggingIndex = clickedIndex; - widgetState.canvasEl.style.cursor = "grabbing"; - } else if (evt.button === 2) { - widgetState._rightClickIndex = clickedIndex; - } - } - }); - canvas.addEventListener("mousemove", (evt) => { - if (evt.target === addButton || evt.target === removeBtn) return; - const rect = widgetState.canvasEl.getBoundingClientRect(); - const x = evt.clientX - rect.left; - const y = evt.clientY - rect.top; - if (widgetState._draggingIndex >= 0) { - evt.stopPropagation(); - let nx = x / rect.width; - let ny = 1 - y / rect.height; - nx = Math.max(0, Math.min(1, nx)); - ny = Math.max(0, Math.min(1, ny)); - const point = widgetState._points[widgetState._draggingIndex]; - if (point.x === 0 || point.x === 1) nx = point.x; - widgetState._points[widgetState._draggingIndex] = { x: nx, y: ny }; - drawCurve(widgetState); - updateSigmaDisplay(widgetState); - saveStateToDataWidget(); - } else { - let hovering = false; - const hoverRadiusSq = 64; - widgetState._points.forEach((pt) => { - const px = pt.x * rect.width; - const py = (1 - pt.y) * rect.height; - if ((px - x) ** 2 + (py - y) ** 2 < hoverRadiusSq) - hovering = true; - }); - widgetState.canvasEl.style.cursor = hovering ? "grab" : "crosshair"; - } - }); - canvas.addEventListener("dblclick", (evt) => { - if (evt.target === addButton || evt.target === removeBtn) return; - evt.stopPropagation(); - const rect = widgetState.canvasEl.getBoundingClientRect(); - let nx = (evt.clientX - rect.left) / rect.width; - let ny = 1 - (evt.clientY - rect.top) / rect.height; - nx = Math.max(0, Math.min(1, nx)); - ny = Math.max(0, Math.min(1, ny)); - widgetState._points.push({ x: nx, y: ny }); - drawCurve(widgetState); - updateSigmaDisplay(widgetState); - updateRemoveButtonState(widgetState); - saveStateToDataWidget(); - }); - canvas.addEventListener("contextmenu", (evt) => { - if (evt.target === addButton || evt.target === removeBtn) { evt.preventDefault(); return; } - evt.preventDefault(); - evt.stopPropagation(); - if (widgetState._rightClickIndex >= 0) { - const pt = widgetState._points[widgetState._rightClickIndex]; - if (widgetState._points.length <= minGraphPoints) { /* Show a message if needed */ } - else if (pt.x > 1e-6 && pt.x < 1.0 - 1e-6) { - widgetState._points.splice(widgetState._rightClickIndex, 1); - drawCurve(widgetState); - updateSigmaDisplay(widgetState); - updateRemoveButtonState(widgetState); - saveStateToDataWidget(); - } else { /* Do not remove boundary points */ } - } - widgetState._rightClickIndex = -1; - }); - - // Steps Widget Hook - if (stepsWidget) { - stepsWidget.callback = (value) => { - if (widgetState.originalStepsCallback) { widgetState.originalStepsCallback.call(stepsWidget, value); } - updateSigmaDisplay(widgetState); - }; - } - - // Resize Observer for canvas - widgetState.resizeObserver = new ResizeObserver(() => { - requestAnimationFrame(() => drawCurve(widgetState)); - }); - widgetState.resizeObserver.observe(canvasContainer); - - // --- Initial Setup --- - requestAnimationFrame(() => { - console.log(`[SigmaGraphWidget] ${this.id}: Running initial setup in rAF.`); - if (customWidget?.value !== undefined) { - customWidget.value = dataWidget.value; - } else { - console.error(`[SigmaGraphWidget] ${this.id}: customWidget or customWidget.value missing for initial load.`); - let initialPoints = []; - try { - initialPoints = JSON.parse(dataWidget.value || '[]'); - } catch(e) { - initialPoints = [{x:0,y:1},{x:1,y:0}]; - } - widgetState._points = initialPoints; - drawCurve(widgetState); - updateSigmaDisplay(widgetState); - updateRemoveButtonState(widgetState); - } - this.setSize(this.computeSize()); - this.setDirtyCanvas(true, true); - if (this.graph) { this.graph.setDirtyCanvas(true, true); } - }); - - // --- Cleanup on Removal --- - const originalOnRemoved = this.onRemoved; - this.onRemoved = () => { - console.log(`[SigmaGraphWidget] ${this.id}: Cleaning up.`); - if (widgetState.boundMouseUpHandler) { document.removeEventListener("mouseup", widgetState.boundMouseUpHandler, true); } - if (widgetState.resizeObserver) { widgetState.resizeObserver.disconnect(); widgetState.resizeObserver = null; } - if (stepsWidget && widgetState.originalStepsCallback) { stepsWidget.callback = widgetState.originalStepsCallback; } - if (originalOnRemoved) { originalOnRemoved.apply(this, arguments); } - }; - - // Mark node dirty after adding the widget - this.setDirtyCanvas(true, true); - }; // End of onConfigure patch - - // --- Node Appearance --- - nodeType.prototype.resizable = true; - const originalComputeSize = nodeType.prototype.computeSize; - nodeType.prototype.computeSize = function(out) { - const size = originalComputeSize ? originalComputeSize.apply(this, [null]) : [this.constructor.NODE_WIDTH || 140, 0]; - const customWidgetContentHeight = 225; - size[0] = Math.max(290, size[0]); - let baseHeight = this.constructor.title_height || 20; - if (this.inputs) { - this.inputs.forEach(input => { if (input.name !== graphDataWidgetName) { baseHeight += 20; } }); - } - if (this.outputs) { baseHeight += this.outputs.length * 20; } - size[1] = baseHeight + customWidgetContentHeight; - size[1] = Math.max(280, size[1]); - if (out) { out[0] = size[0]; out[1] = size[1]; } - return size; - }; - nodeType.prototype.onResize = function() { /* No-op */ }; - }, // End of beforeRegisterNodeDef -}); // End of app.registerExtension +if (app.on) { + app.on("nodeAdded", (n) => { if (n.type === NODE_CLASS) setup(n); }); +} else { + setInterval(() => { + (app.graph?._nodes||[]).forEach((n) => { + if (n.type === NODE_CLASS) setup(n); + }); + }, 100); +}