618 lines
21 KiB
JavaScript
618 lines
21 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { api } from "../../scripts/api.js";
|
|
import { cameraBasis, DEG2RAD, clamp, wrapYaw } from "./pano_preview_render.js";
|
|
import { createPanoInteractionController } from "./pano_interaction_controller.js";
|
|
import { renderSceneToContext2D } from "./pano_gl_viewport.js";
|
|
import { buildPreviewNodeViewParams, buildStickerSceneFromState } from "./pano_gl_scene.js";
|
|
import {
|
|
drawErpBackground,
|
|
STANDALONE_MESH_LOW,
|
|
localPosFromEvent,
|
|
} from "./pano_preview_runtime.js";
|
|
|
|
const PREVIEW_MIN_HEIGHT = 140;
|
|
const PREVIEW_MIN_WIDTH = 180;
|
|
const LEGACY_TOP_OFFSET = 40;
|
|
const LEGACY_MARGIN = 10;
|
|
|
|
function normalizeExecutionId(value) {
|
|
const raw = String(value ?? "").trim();
|
|
if (!raw) return { full: "", tail: "" };
|
|
const parts = raw.split(":");
|
|
return { full: raw, tail: String(parts[parts.length - 1] || "").trim() };
|
|
}
|
|
|
|
function executionIdMatches(value, target) {
|
|
const a = normalizeExecutionId(value);
|
|
const b = normalizeExecutionId(target);
|
|
if (!a.full || !b.full) return false;
|
|
return a.full === b.full || a.tail === b.full || a.full === b.tail || a.tail === b.tail;
|
|
}
|
|
|
|
function comfyImageEntryToUrl(entry) {
|
|
if (!entry || typeof entry !== "object") return "";
|
|
const filename = String(entry.filename || "").trim();
|
|
if (!filename) return "";
|
|
const params = new URLSearchParams();
|
|
params.set("filename", filename);
|
|
params.set("type", String(entry.type || "output"));
|
|
if (entry.subfolder) params.set("subfolder", String(entry.subfolder));
|
|
const q = `/view?${params.toString()}`;
|
|
return typeof api?.apiURL === "function" ? api.apiURL(q) : q;
|
|
}
|
|
|
|
function imageSourceFromCandidate(candidate) {
|
|
if (!candidate) return "";
|
|
if (typeof candidate === "string") return String(candidate || "").trim();
|
|
if (Array.isArray(candidate)) {
|
|
if (candidate.length === 0) return "";
|
|
if (candidate.length === 1) return imageSourceFromCandidate(candidate[0]);
|
|
const filename = typeof candidate[0] === "string" ? String(candidate[0] || "").trim() : "";
|
|
if (filename) {
|
|
return comfyImageEntryToUrl({
|
|
filename,
|
|
subfolder: String(candidate[1] || "").trim(),
|
|
type: String(candidate[2] || "output").trim() || "output",
|
|
});
|
|
}
|
|
for (const entry of candidate) {
|
|
const src = imageSourceFromCandidate(entry);
|
|
if (src) return src;
|
|
}
|
|
return "";
|
|
}
|
|
if (typeof candidate?.src === "string" && candidate.src) return candidate.src;
|
|
if (typeof candidate?.url === "string" && candidate.url) return candidate.url;
|
|
return comfyImageEntryToUrl(candidate);
|
|
}
|
|
|
|
function lookupNodeOutputEntry(nodeId) {
|
|
const store = app?.nodeOutputs;
|
|
if (!store || nodeId == null) return null;
|
|
const raw = String(nodeId);
|
|
let val = null;
|
|
if (store instanceof Map) {
|
|
val = store.get(nodeId) || store.get(raw) || store.get(Number(raw)) || null;
|
|
} else {
|
|
val = store[nodeId] || store[raw] || null;
|
|
}
|
|
if (val) return val;
|
|
if (store instanceof Map) {
|
|
for (const key of store.keys()) {
|
|
if (executionIdMatches(key, raw)) return store.get(key);
|
|
}
|
|
} else {
|
|
for (const key of Object.keys(store)) {
|
|
if (executionIdMatches(key, raw)) return store[key];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getLinkedImageUrl(node, imageInputName = "erp_image") {
|
|
const inputs = Array.isArray(node?.inputs) ? node.inputs : [];
|
|
let linkId = null;
|
|
|
|
const preferred = inputs.find((input) => String(input?.name || "") === String(imageInputName));
|
|
if (preferred?.link != null) linkId = preferred.link;
|
|
if (linkId == null) {
|
|
const anyImg = inputs.find((input) => String(input?.type || "").toUpperCase() === "IMAGE" && input?.link != null);
|
|
if (anyImg?.link != null) linkId = anyImg.link;
|
|
}
|
|
if (linkId == null) return "";
|
|
|
|
const link = node?.graph?.links?.[linkId] || app?.graph?.links?.[linkId];
|
|
if (!link) return "";
|
|
const originId = Number(link.origin_id);
|
|
if (!Number.isFinite(originId)) return "";
|
|
const originNode = app?.graph?.getNodeById?.(originId);
|
|
|
|
const outputs = lookupNodeOutputEntry(originId);
|
|
const selfOutput = lookupNodeOutputEntry(node?.id);
|
|
const candidateGroups = [
|
|
outputs?.ui?.pano_input_images,
|
|
outputs?.pano_input_images,
|
|
selfOutput?.ui?.pano_input_images,
|
|
selfOutput?.pano_input_images,
|
|
outputs?.images,
|
|
];
|
|
for (const group of candidateGroups) {
|
|
if (!Array.isArray(group)) continue;
|
|
for (const cand of group) {
|
|
const src = imageSourceFromCandidate(cand);
|
|
if (src) return src;
|
|
}
|
|
}
|
|
|
|
let appNodeImageUrls = [];
|
|
try {
|
|
appNodeImageUrls = typeof app?.getNodeImageUrls === "function" ? (app.getNodeImageUrls(originNode) || []) : [];
|
|
} catch {
|
|
appNodeImageUrls = [];
|
|
}
|
|
if (Array.isArray(appNodeImageUrls) && appNodeImageUrls.length > 0) {
|
|
for (const cand of appNodeImageUrls) {
|
|
const src = imageSourceFromCandidate(cand);
|
|
if (src) return src;
|
|
}
|
|
}
|
|
|
|
if (originNode) {
|
|
const nodeImgs = Array.isArray(originNode?.imgs) ? originNode.imgs : [];
|
|
for (const cand of nodeImgs) {
|
|
const src = imageSourceFromCandidate(cand);
|
|
if (src) return src;
|
|
}
|
|
}
|
|
|
|
const imageWidget = originNode?.widgets?.find((widget) => String(widget?.name || "").toLowerCase() === "image");
|
|
const imageName = String(imageWidget?.value || "").trim();
|
|
if (!imageName) return "";
|
|
return api.apiURL(`/view?filename=${encodeURIComponent(imageName)}&type=input&subfolder=`);
|
|
}
|
|
|
|
function drawGrid(ctx, width, height) {
|
|
ctx.strokeStyle = "rgba(255,255,255,0.16)";
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.5, 0);
|
|
ctx.lineTo(width * 0.5, height);
|
|
ctx.moveTo(0, height * 0.5);
|
|
ctx.lineTo(width, height * 0.5);
|
|
ctx.stroke();
|
|
}
|
|
|
|
function drawPreview(node, ctx, width, height, view, img) {
|
|
ctx.clearRect(0, 0, width, height);
|
|
ctx.fillStyle = "#070707";
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
if (!img || !img.complete || !(img.naturalWidth || img.width) || width <= 1 || height <= 1) {
|
|
drawGrid(ctx, width, height);
|
|
return;
|
|
}
|
|
const basis = cameraBasis(Number(view.yaw || 0), Number(view.pitch || 0), 0);
|
|
const tanHalfY = Math.tan((Number(view.fov || 100) * DEG2RAD) * 0.5);
|
|
const drawn = renderSceneToContext2D({
|
|
owner: node,
|
|
cacheKey: "standalone_preview_scene",
|
|
ctx,
|
|
width,
|
|
height,
|
|
backgroundSource: img,
|
|
backgroundRevision: [
|
|
String(img.currentSrc || img.src || ""),
|
|
Number(img.naturalWidth || img.width || 0),
|
|
Number(img.naturalHeight || img.height || 0),
|
|
].join("|"),
|
|
textures: [],
|
|
scene: buildStickerSceneFromState(null, {}),
|
|
view: buildPreviewNodeViewParams(view),
|
|
});
|
|
if (!drawn) {
|
|
drawErpBackground(node, ctx, { x: 0, y: 0, w: width, h: height }, basis, tanHalfY, img, STANDALONE_MESH_LOW);
|
|
drawGrid(ctx, width, height);
|
|
}
|
|
}
|
|
|
|
function isPointInRect(x, y, rect) {
|
|
return x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h;
|
|
}
|
|
|
|
class PreviewNodeRuntime {
|
|
constructor(node, options = {}) {
|
|
this.node = node;
|
|
this.options = options;
|
|
this.imageInputName = String(options.imageInputName || "erp_image");
|
|
this.buttonText = String(options.buttonText || "Open Preview");
|
|
this.errorText = "";
|
|
this.root = null;
|
|
this.canvas = null;
|
|
this.ctx = null;
|
|
this.widget = null;
|
|
this.resizeObserver = null;
|
|
this.rafId = 0;
|
|
this.needsDraw = false;
|
|
this.inTick = false;
|
|
this.queuedDuringTick = false;
|
|
this.img = null;
|
|
this.imgSrc = "";
|
|
this.view = { yaw: 0, pitch: 0, fov: 100 };
|
|
this.controller = createPanoInteractionController({
|
|
getView: () => this.view,
|
|
setView: (next) => {
|
|
this.view = {
|
|
yaw: wrapYaw(Number(next?.yaw || 0)),
|
|
pitch: clamp(Number(next?.pitch || 0), -89.9, 89.9),
|
|
fov: clamp(Number(next?.fov || 100), 35, 140),
|
|
};
|
|
},
|
|
onInteraction: () => this.requestDraw(),
|
|
});
|
|
this.orig = {
|
|
onDrawForeground: node.onDrawForeground,
|
|
onMouseDown: node.onMouseDown,
|
|
onMouseMove: node.onMouseMove,
|
|
onMouseUp: node.onMouseUp,
|
|
onMouseWheel: node.onMouseWheel,
|
|
onExecuted: node.onExecuted,
|
|
onConnectionsChange: node.onConnectionsChange,
|
|
onResize: node.onResize,
|
|
onRemoved: node.onRemoved,
|
|
};
|
|
this.legacyDragPointer = false;
|
|
this.tick = this.tick.bind(this);
|
|
this.onResizeDom = this.onResizeDom.bind(this);
|
|
}
|
|
|
|
attach() {
|
|
if (typeof this.node?.addDOMWidget === "function") this.attachDom();
|
|
else this.attachLegacy();
|
|
this.installCommonHooks();
|
|
this.refreshImage();
|
|
this.requestDraw();
|
|
}
|
|
|
|
installCommonHooks() {
|
|
const self = this;
|
|
this.node.onExecuted = function () {
|
|
const out = self.orig.onExecuted ? self.orig.onExecuted.apply(this, arguments) : undefined;
|
|
self.refreshImage();
|
|
return out;
|
|
};
|
|
this.node.onConnectionsChange = function () {
|
|
const out = self.orig.onConnectionsChange ? self.orig.onConnectionsChange.apply(this, arguments) : undefined;
|
|
self.refreshImage();
|
|
return out;
|
|
};
|
|
this.node.onResize = function () {
|
|
const out = self.orig.onResize ? self.orig.onResize.apply(this, arguments) : undefined;
|
|
self.requestDraw();
|
|
return out;
|
|
};
|
|
this.node.onRemoved = function () {
|
|
const out = self.orig.onRemoved ? self.orig.onRemoved.apply(this, arguments) : undefined;
|
|
self.teardown();
|
|
return out;
|
|
};
|
|
}
|
|
|
|
installErrorForeground() {
|
|
const self = this;
|
|
this.node.onDrawForeground = function (ctx) {
|
|
const out = self.orig.onDrawForeground ? self.orig.onDrawForeground.apply(this, arguments) : undefined;
|
|
if (!ctx || this.flags?.collapsed || !self.errorText) return out;
|
|
const rect = self.getLegacyPreviewRect();
|
|
ctx.save();
|
|
ctx.fillStyle = "rgba(18,18,22,0.92)";
|
|
ctx.strokeStyle = "rgba(82,82,89,1)";
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.roundRect(rect.x, rect.y, rect.w, rect.h, 8);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = "rgba(236,236,242,0.92)";
|
|
ctx.font = "600 12px sans-serif";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(self.errorText, rect.x + rect.w * 0.5, rect.y + rect.h * 0.5);
|
|
ctx.restore();
|
|
return out;
|
|
};
|
|
}
|
|
|
|
attachDom() {
|
|
try {
|
|
this.root = document.createElement("div");
|
|
this.root.className = "pano-node-preview-dom pano-node-preview--stickers";
|
|
this.root.setAttribute("data-capture-wheel", "true");
|
|
this.root.setAttribute("tabindex", "0");
|
|
this.root.style.cssText = [
|
|
"width:100%",
|
|
"height:100%",
|
|
`min-height:${PREVIEW_MIN_HEIGHT}px`,
|
|
"position:relative",
|
|
"display:block",
|
|
"overflow:hidden",
|
|
"background:#070707",
|
|
"border-radius:8px",
|
|
"border:1px solid rgba(63,63,70,1)",
|
|
].join(";");
|
|
|
|
this.canvas = document.createElement("canvas");
|
|
this.canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none;cursor:grab;";
|
|
this.root.appendChild(this.canvas);
|
|
|
|
const widgetOptions = {
|
|
serialize: false,
|
|
hideOnZoom: false,
|
|
getValue() { return ""; },
|
|
setValue() {},
|
|
getMinHeight() { return PREVIEW_MIN_HEIGHT; },
|
|
getHeight() { return PREVIEW_MIN_HEIGHT; },
|
|
onRemove: () => this.teardown(),
|
|
afterResize: () => this.requestDraw(),
|
|
};
|
|
|
|
this.widget = this.node.addDOMWidget("preview", "preview", this.root, widgetOptions);
|
|
if (this.widget) {
|
|
this.widget.serialize = false;
|
|
const prevComputeLayoutSize = typeof this.widget.computeLayoutSize === "function"
|
|
? this.widget.computeLayoutSize.bind(this.widget)
|
|
: null;
|
|
this.widget.computeLayoutSize = (targetNode) => {
|
|
const prev = prevComputeLayoutSize ? (prevComputeLayoutSize(targetNode) || {}) : {};
|
|
return {
|
|
...prev,
|
|
minHeight: Math.max(PREVIEW_MIN_HEIGHT, Number(prev.minHeight || 0)),
|
|
minWidth: Math.max(PREVIEW_MIN_WIDTH, Number(prev.minWidth || 0)),
|
|
};
|
|
};
|
|
}
|
|
|
|
this.ctx = this.canvas.getContext("2d");
|
|
this.resizeObserver = typeof ResizeObserver !== "undefined"
|
|
? new ResizeObserver(() => this.onResizeDom())
|
|
: null;
|
|
this.resizeObserver?.observe(this.root);
|
|
this.bindDomInput(this.canvas, this.root);
|
|
this.onResizeDom();
|
|
this.logProbeFrames();
|
|
} catch (err) {
|
|
this.errorText = "Preview mount failed";
|
|
this.installErrorForeground();
|
|
}
|
|
}
|
|
|
|
bindDomInput(canvas, root) {
|
|
canvas.addEventListener("pointerdown", (ev) => {
|
|
if (ev.button !== 0) return;
|
|
root.focus?.({ preventScroll: true });
|
|
canvas.setPointerCapture?.(ev.pointerId);
|
|
canvas.style.cursor = "grabbing";
|
|
this.controller.startDrag(ev.clientX, ev.clientY, ev.pointerId);
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
ev.stopImmediatePropagation?.();
|
|
});
|
|
canvas.addEventListener("pointermove", (ev) => {
|
|
if (!this.controller.state.drag.active) return;
|
|
this.controller.moveDrag(ev.clientX, ev.clientY, "pano");
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
ev.stopImmediatePropagation?.();
|
|
});
|
|
const end = (ev) => {
|
|
if (!this.controller.state.drag.active) return;
|
|
canvas.releasePointerCapture?.(ev.pointerId);
|
|
canvas.style.cursor = "grab";
|
|
this.controller.endDrag();
|
|
this.requestDraw();
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
ev.stopImmediatePropagation?.();
|
|
};
|
|
canvas.addEventListener("pointerup", end);
|
|
canvas.addEventListener("pointercancel", end);
|
|
root.addEventListener("wheel", (ev) => {
|
|
const before = Number(this.view?.fov || 100);
|
|
const changed = this.controller.applyWheelEvent(ev);
|
|
const after = Number(this.view?.fov || 100);
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
ev.stopImmediatePropagation?.();
|
|
}, { passive: false, capture: true });
|
|
}
|
|
|
|
attachLegacy() {
|
|
const self = this;
|
|
this.node.onDrawForeground = function (ctx) {
|
|
const out = self.orig.onDrawForeground ? self.orig.onDrawForeground.apply(this, arguments) : undefined;
|
|
self.drawLegacy(ctx);
|
|
return out;
|
|
};
|
|
this.node.onMouseDown = function (e, localPos) {
|
|
const p = self.localPoint(localPos);
|
|
if (!p) return self.orig.onMouseDown ? self.orig.onMouseDown.apply(this, arguments) : undefined;
|
|
const previewRect = self.getLegacyPreviewRect();
|
|
if (!isPointInRect(p.x, p.y, previewRect)) {
|
|
return self.orig.onMouseDown ? self.orig.onMouseDown.apply(this, arguments) : undefined;
|
|
}
|
|
self.legacyDragPointer = true;
|
|
self.controller.startDrag(p.x - previewRect.x, p.y - previewRect.y);
|
|
this.setDirtyCanvas?.(true, false);
|
|
return true;
|
|
};
|
|
this.node.onMouseMove = function (e, localPos) {
|
|
if (!self.legacyDragPointer || !self.controller.state.drag.active) {
|
|
return self.orig.onMouseMove ? self.orig.onMouseMove.apply(this, arguments) : undefined;
|
|
}
|
|
const p = self.localPoint(localPos);
|
|
if (!p) return true;
|
|
const previewRect = self.getLegacyPreviewRect();
|
|
self.controller.moveDrag(p.x - previewRect.x, p.y - previewRect.y, "pano");
|
|
this.setDirtyCanvas?.(true, false);
|
|
return true;
|
|
};
|
|
this.node.onMouseUp = function () {
|
|
if (!self.legacyDragPointer && !self.controller.state.drag.active) {
|
|
return self.orig.onMouseUp ? self.orig.onMouseUp.apply(this, arguments) : undefined;
|
|
}
|
|
self.legacyDragPointer = false;
|
|
self.controller.endDrag();
|
|
this.setDirtyCanvas?.(true, false);
|
|
return true;
|
|
};
|
|
this.node.onMouseWheel = function (e, arg2, canvasRef) {
|
|
const p = localPosFromEvent(this, e, arg2, canvasRef);
|
|
const previewRect = self.getLegacyPreviewRect();
|
|
if (!p || !isPointInRect(p.x, p.y, previewRect)) {
|
|
return self.orig.onMouseWheel ? self.orig.onMouseWheel.apply(this, arguments) : undefined;
|
|
}
|
|
const raw = Number(e?.deltaY ?? e?.wheelDeltaY ?? (typeof arg2 === "number" ? arg2 : 0));
|
|
const before = Number(self.view?.fov || 100);
|
|
const changed = self.controller.applyWheel(Math.sign(raw));
|
|
const after = Number(self.view?.fov || 100);
|
|
if (changed) this.setDirtyCanvas?.(true, false);
|
|
e?.preventDefault?.();
|
|
e?.stopPropagation?.();
|
|
e?.stopImmediatePropagation?.();
|
|
return true;
|
|
};
|
|
}
|
|
|
|
localPoint(localPos) {
|
|
if (!Array.isArray(localPos) || localPos.length < 2) return null;
|
|
return { x: Number(localPos[0] || 0), y: Number(localPos[1] || 0) };
|
|
}
|
|
|
|
getLegacyPreviewRect() {
|
|
const width = Math.max(80, Number(this.node?.size?.[0] || 0) - LEGACY_MARGIN * 2);
|
|
const height = Math.max(60, Number(this.node?.size?.[1] || 0) - LEGACY_TOP_OFFSET - LEGACY_MARGIN);
|
|
return { x: LEGACY_MARGIN, y: LEGACY_TOP_OFFSET, w: width, h: height };
|
|
}
|
|
|
|
drawLegacy(ctx) {
|
|
if (!ctx || this.node?.flags?.collapsed) return;
|
|
const rect = this.getLegacyPreviewRect();
|
|
if (rect.w <= 2 || rect.h <= 2) return;
|
|
|
|
ctx.save();
|
|
ctx.translate(rect.x, rect.y);
|
|
drawPreview(this.node, ctx, rect.w, rect.h, this.view, this.img);
|
|
|
|
if (this.errorText) {
|
|
ctx.fillStyle = "rgba(18,18,22,0.92)";
|
|
ctx.fillRect(0, 0, rect.w, rect.h);
|
|
ctx.fillStyle = "rgba(236,236,242,0.92)";
|
|
ctx.font = "600 12px sans-serif";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(this.errorText, rect.w * 0.5, rect.h * 0.5);
|
|
}
|
|
ctx.restore();
|
|
}
|
|
|
|
onResizeDom() {
|
|
if (!this.root || !this.canvas) return;
|
|
const rect = this.root.getBoundingClientRect();
|
|
const dpr = 1;
|
|
const width = Math.max(1, Math.round(rect.width * dpr));
|
|
const height = Math.max(1, Math.round(rect.height * dpr));
|
|
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
this.canvas.width = width;
|
|
this.canvas.height = height;
|
|
this.requestDraw();
|
|
}
|
|
}
|
|
|
|
refreshImage() {
|
|
const nextSrc = getLinkedImageUrl(this.node, this.imageInputName);
|
|
if (!nextSrc) {
|
|
this.img = null;
|
|
this.imgSrc = "";
|
|
this.requestDraw();
|
|
return;
|
|
}
|
|
if (nextSrc === this.imgSrc && this.img) return;
|
|
this.imgSrc = nextSrc;
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
if (this.imgSrc !== nextSrc) return;
|
|
this.img = img;
|
|
this.requestDraw();
|
|
};
|
|
img.onerror = () => {
|
|
if (this.imgSrc !== nextSrc) return;
|
|
this.img = null;
|
|
this.requestDraw();
|
|
};
|
|
img.src = nextSrc;
|
|
}
|
|
|
|
requestDraw() {
|
|
this.needsDraw = true;
|
|
if (this.inTick) {
|
|
this.queuedDuringTick = true;
|
|
return;
|
|
}
|
|
if (!this.rafId) this.rafId = requestAnimationFrame(this.tick);
|
|
if (!this.canvas) this.node.setDirtyCanvas?.(true, false);
|
|
}
|
|
|
|
tick(ts = performance.now()) {
|
|
this.rafId = 0;
|
|
this.inTick = true;
|
|
this.queuedDuringTick = false;
|
|
this.needsDraw = false;
|
|
const moving = this.controller.stepInertia(ts);
|
|
if (this.canvas && this.ctx) {
|
|
drawPreview(this.node, this.ctx, this.canvas.width, this.canvas.height, this.view, this.img);
|
|
if (this.errorText) {
|
|
this.ctx.fillStyle = "rgba(18,18,22,0.92)";
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
this.ctx.fillStyle = "rgba(236,236,242,0.92)";
|
|
this.ctx.font = "600 14px sans-serif";
|
|
this.ctx.textAlign = "center";
|
|
this.ctx.textBaseline = "middle";
|
|
this.ctx.fillText(this.errorText, this.canvas.width * 0.5, this.canvas.height * 0.5);
|
|
}
|
|
} else {
|
|
this.node.setDirtyCanvas?.(true, false);
|
|
}
|
|
this.inTick = false;
|
|
const shouldContinue = moving || this.needsDraw || this.queuedDuringTick;
|
|
if (shouldContinue && !this.rafId) this.rafId = requestAnimationFrame(this.tick);
|
|
}
|
|
|
|
logProbeFrames() {
|
|
if (!this.root || !this.canvas) return;
|
|
let frame = 0;
|
|
const probe = () => {
|
|
if (!this.root || !this.canvas || frame >= 3) return;
|
|
frame += 1;
|
|
requestAnimationFrame(probe);
|
|
};
|
|
requestAnimationFrame(probe);
|
|
}
|
|
|
|
teardown() {
|
|
if (this.node?.__panoPreviewNodeRuntime !== this) return;
|
|
if (this.rafId) {
|
|
cancelAnimationFrame(this.rafId);
|
|
this.rafId = 0;
|
|
}
|
|
this.resizeObserver?.disconnect?.();
|
|
this.resizeObserver = null;
|
|
try {
|
|
this.root?.remove?.();
|
|
} catch {
|
|
// ignore
|
|
}
|
|
if (Array.isArray(this.node?.widgets) && this.widget) {
|
|
try {
|
|
this.node.widgets = this.node.widgets.filter((widget) => widget !== this.widget);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
this.node.onDrawForeground = this.orig.onDrawForeground;
|
|
this.node.onMouseDown = this.orig.onMouseDown;
|
|
this.node.onMouseMove = this.orig.onMouseMove;
|
|
this.node.onMouseUp = this.orig.onMouseUp;
|
|
this.node.onMouseWheel = this.orig.onMouseWheel;
|
|
this.node.onExecuted = this.orig.onExecuted;
|
|
this.node.onConnectionsChange = this.orig.onConnectionsChange;
|
|
this.node.onResize = this.orig.onResize;
|
|
this.node.onRemoved = this.orig.onRemoved;
|
|
this.node.__panoPreviewNodeRuntime = null;
|
|
}
|
|
}
|
|
|
|
export function attachPreviewNodeRuntime(node, options = {}) {
|
|
if (!node || node.__panoPreviewNodeRuntime) return;
|
|
const runtime = new PreviewNodeRuntime(node, options);
|
|
node.__panoPreviewNodeRuntime = runtime;
|
|
runtime.attach();
|
|
}
|