Files

987 lines
38 KiB
JavaScript

// IMGNR-Utils/Split Screen
// Initial code
// =========================================================
// FEATURE: DRAGGABLE SPLIT SCREEN
// =========================================================
// split screen Icon by Yudhi Restu Pebriyanto from Noun Project (CC BY 3.0)
// https://thenounproject.com/icon/split-screen-6095990/
import { app } from "../../scripts/app.js";
const PREFIX = "IMGNR";
// --- STATE GLOBALS ---
let isSplit = false;
let secondaryCanvas = null;
let separatorEl = null;
let separatorLine = null;
let bgBlocker = null;
let originalMenu = null;
let originalZoomPopup = null;
let popupObserver = null;
let labelOriginal = null;
let labelClone = null;
let mirrorContainer = null;
let splitControls = null;
// [CHANGE] Removed global 'wrapper' to force ID-based lookups for safety
let currentSplitX = window.innerWidth / 2;
let pendingSplitX = window.innerWidth / 2;
let isDragging = false;
// Variables to restore state
let originalBodyBg = "";
let originalOverflow = "";
let originalMainStyle = { width: "", height: "", position: "", zIndex: "" };
// --- DOM SYNC MANAGER ---
class MirrorDOMManager {
constructor() {
this.elements = new Map();
this.container = null;
this.interactionEnabled = true;
}
init(containerEl) {
this.container = containerEl;
}
setInteraction(enabled) {
this.interactionEnabled = enabled;
const pState = enabled ? "auto" : "none";
for (const entry of this.elements.values()) {
if (entry.element) {
entry.element.style.pointerEvents = pState;
const inputs = this.getInputs(entry.element);
inputs.forEach(i => i.style.pointerEvents = pState);
}
}
}
shouldUpdate(canvas) {
return !!(canvas && canvas.ds);
}
update(canvas) {
if (!canvas || !this.container) return;
let targetNodes = [];
if (canvas.graph && canvas.graph._nodes) {
targetNodes = canvas.graph._nodes;
}
const currentFrameIds = new Set();
for (const node of targetNodes) {
if(node.widgets) {
node.widgets.forEach((widget, wIndex) => {
if (widget.element) {
const id = `${node.id}_w_${wIndex}`;
this.processWidget(canvas, node, widget, wIndex, id);
currentFrameIds.add(id);
}
});
}
}
for (const [id, data] of this.elements.entries()) {
if (!currentFrameIds.has(id)) {
if (data.element) data.element.remove();
this.elements.delete(id);
}
}
}
getInputs(el) {
const found = [];
if (el.matches && el.matches("input, textarea, select, button")) found.push(el);
if (el.querySelectorAll) {
const kids = el.querySelectorAll("input, textarea, select, button");
kids.forEach(k => found.push(k));
}
return found;
}
createClone(sourceEl, widget, node) {
const clone = sourceEl.cloneNode(true);
if (clone.id) clone.id = "clone_" + clone.id;
const pState = this.interactionEnabled ? "auto" : "none";
Object.assign(clone.style, {
position: "absolute",
transform: "",
left: "", top: "",
margin: "0",
boxSizing: "border-box",
pointerEvents: pState
});
const inputs = this.getInputs(clone);
inputs.forEach(input => {
input.style.pointerEvents = pState;
input.addEventListener("input", (e) => {
const val = e.target.value;
widget.value = val;
if (widget.element && "value" in widget.element) widget.element.value = val;
if (widget.callback) widget.callback(widget.value, app.canvas, node, app.canvas.getPointer(), e);
app.graph.setDirtyCanvas(true, true);
});
input.addEventListener("mousedown", (e) => e.stopPropagation());
});
clone.addEventListener("dblclick", (e) => {
if (!this.interactionEnabled) return;
e.stopPropagation();
e.preventDefault();
const evt = new MouseEvent("dblclick", {
bubbles: true,
cancelable: true,
view: window
});
sourceEl.dispatchEvent(evt);
});
clone.addEventListener("mousedown", (e) => e.stopPropagation());
return clone;
}
processWidget(canvas, node, widget, wIndex, id) {
const sourceEl = widget.element.closest(".dom-widget") || widget.element;
let entry = this.elements.get(id);
if (!entry) {
const clone = this.createClone(sourceEl, widget, node);
this.container.appendChild(clone);
entry = {
element: clone,
sourceElement: sourceEl,
cachedHTML: sourceEl.innerHTML
};
this.elements.set(id, entry);
}
let clone = entry.element;
const userTyping = document.activeElement && clone.contains(document.activeElement);
if (!userTyping) {
const currentHTML = sourceEl.innerHTML;
if (currentHTML !== entry.cachedHTML) {
const newClone = this.createClone(sourceEl, widget, node);
this.syncWidgetPosition(canvas, node, widget, newClone, sourceEl);
newClone.className = sourceEl.className;
clone.replaceWith(newClone);
entry.element = newClone;
entry.cachedHTML = currentHTML;
clone = newClone;
}
}
if (clone.className !== sourceEl.className) {
clone.className = sourceEl.className;
}
const inputs = this.getInputs(clone);
inputs.forEach(input => {
if (document.activeElement !== input && String(input.value) !== String(widget.value)) {
input.value = widget.value;
}
});
const sImgs = sourceEl.querySelectorAll("img");
const cImgs = clone.querySelectorAll("img");
if (sImgs.length === cImgs.length) {
sImgs.forEach((sImg, i) => {
if (cImgs[i].src !== sImg.src) cImgs[i].src = sImg.src;
});
}
this.syncWidgetPosition(canvas, node, widget, clone, sourceEl);
}
syncWidgetPosition(canvas, node, widget, el, sourceEl) {
const scale = canvas.ds.scale;
const offset = canvas.ds.offset;
const relY = widget.y || (LiteGraph.NODE_TITLE_HEIGHT + 20);
const logicalX = (node.pos[0] + 10 + offset[0]) * scale;
const logicalY = (node.pos[1] + relY + offset[1]) * scale;
if (Number.isFinite(logicalX) && Number.isFinite(logicalY)) {
el.style.transformOrigin = "0 0";
el.style.transform = `translate(${logicalX}px, ${logicalY}px) scale(${scale})`;
el.style.display = "block";
el.style.width = (node.size[0] - 20) + "px";
if (sourceEl.style.height) {
el.style.height = sourceEl.style.height;
} else {
el.style.height = "";
}
}
}
destroy() {
for (const [id, data] of this.elements.entries()) {
if (data.element) data.element.remove();
}
this.elements.clear();
}
}
const mirrorDOM = new MirrorDOMManager();
const originalDraw = LGraphCanvas.prototype.draw;
// --- PERSISTENCE HELPERS ---
function getGraphData() {
if (!app.graph) return null;
if (!app.graph.extra) app.graph.extra = {};
return app.graph.extra.IMGNR_SplitScreen || null;
}
function setGraphData(data) {
if (!app.graph) return;
if (!app.graph.extra) app.graph.extra = {};
const existing = app.graph.extra.IMGNR_SplitScreen || {};
app.graph.extra.IMGNR_SplitScreen = { ...existing, ...data };
}
function loadSavedPosition() {
const data = getGraphData();
if (data && typeof data.splitX === "number") {
return Math.max(50, Math.min(window.innerWidth - 50, data.splitX));
}
return window.innerWidth / 2;
}
function savePosition(x) { setGraphData({ splitX: Math.floor(x) }); }
function getDSValues(dsObj) {
if (!dsObj) return { scale: 1, tx: 0, ty: 0 };
if (typeof dsObj.scale === "number" && dsObj.offset) {
return { scale: dsObj.scale, tx: dsObj.offset[0], ty: dsObj.offset[1] };
}
return { scale: 1, tx: 0, ty: 0 };
}
function setDSValues(dsObj, v) {
if (!dsObj) return;
if (typeof dsObj.scale === "number" && dsObj.offset) {
dsObj.scale = v.scale;
dsObj.offset[0] = v.tx;
dsObj.offset[1] = v.ty;
}
}
function saveCameraState() {
if (!secondaryCanvas || !secondaryCanvas.ds) return;
setGraphData({ camera: getDSValues(secondaryCanvas.ds) });
}
function applyCameraState() {
if (!secondaryCanvas || !secondaryCanvas.ds) return;
const data = getGraphData();
const target = (data && data.camera) ? data.camera : getDSValues(app.canvas.ds);
setDSValues(secondaryCanvas.ds, target);
secondaryCanvas.dirty_canvas = true;
secondaryCanvas.dirty_bgcanvas = true;
}
// --- SYNC MANAGERS ---
function syncSelectionState(source, target) {
if(!source || !target) return;
target.deselectAllNodes();
if (source.selected_nodes) {
for (const id in source.selected_nodes) {
const node = app.graph.getNodeById(id);
if (node) target.selectNode(node, true);
}
}
target.setDirty(true, true);
}
const handleMainMouseUp = () => { if (isSplit && secondaryCanvas) requestAnimationFrame(() => syncSelectionState(app.canvas, secondaryCanvas)); };
const handleSecMouseUp = () => { if (isSplit && app.canvas) requestAnimationFrame(() => syncSelectionState(secondaryCanvas, app.canvas)); };
function setupInteractionSync(secCanvasEl) {
secCanvasEl.addEventListener("mouseup", handleSecMouseUp);
const mainEl = document.getElementById("graph-canvas");
if(mainEl) mainEl.addEventListener("mouseup", handleMainMouseUp);
}
function teardownInteractionSync(secCanvasEl) {
if(secCanvasEl) secCanvasEl.removeEventListener("mouseup", handleSecMouseUp);
const mainEl = document.getElementById("graph-canvas");
if(mainEl) mainEl.removeEventListener("mouseup", handleMainMouseUp);
}
// --- UI SETUP ---
function findZoomPopup() {
const candidates = document.querySelectorAll("div.absolute.right-0.z-1300");
for (const c of candidates) {
if (c.classList.contains("bottom-[62px]")) {
return c;
}
}
return null;
}
function setupMenus() {
const candidateMenus = document.querySelectorAll(".p-buttongroup");
for (const m of candidateMenus) {
if (m.classList.contains("right-0") && m.classList.contains("bottom-0")) {
originalMenu = m;
break;
}
}
popupObserver = new MutationObserver((mutations) => {
let found = false;
for (const m of mutations) {
for (const n of m.addedNodes) {
if (n.nodeType === 1 && n.classList.contains("absolute") && n.classList.contains("right-0")) {
if (n.classList.contains("bottom-[62px]")) {
originalZoomPopup = n;
found = true;
}
}
}
}
if (found) applyResizeNow();
});
popupObserver.observe(document.body, { childList: true, subtree: true });
originalZoomPopup = findZoomPopup();
}
function teardownMenus() {
if (originalMenu) {
originalMenu.style.display = "";
originalMenu.style.right = "";
originalMenu = null;
}
if (popupObserver) {
popupObserver.disconnect();
popupObserver = null;
}
if (originalZoomPopup) {
originalZoomPopup.style.right = "";
originalZoomPopup = null;
}
}
function setupToolbarButton() {
const containerSelector = ".actionbar-container .flex.gap-2.mx-2";
const addButton = (container) => {
if (container.querySelector(".imgnr-split-screen-btn")) return;
const wrapper = document.createElement("div");
wrapper.className = "comfyui-button-group imgnr-split-screen-btn";
const btn = document.createElement("button");
btn.className = "comfyui-button primary";
btn.title = "Toggle Split Screen (Shift+Click for Settings)";
btn.onclick = (e) => {
if (e.shiftKey) {
// 1. Trigger Keyboard Shortcut
window.dispatchEvent(new KeyboardEvent('keydown', {
key: ',',
ctrlKey: true,
bubbles: true,
cancelable: true,
view: window
}));
// 2. Find and Scroll/Focus IMGNR Tab (Robust for List Items)
const tryClickTab = (attempts = 0) => {
if (attempts > 20) return; // Stop after 1s
// [FIX] Target LI with role="option" as per user HTML
const candidates = document.querySelectorAll('li[role="option"]');
let target = null;
for (const el of candidates) {
const ariaLabel = el.getAttribute("aria-label") || "";
const text = el.textContent || "";
if (ariaLabel === "IMGNR" || text.includes("IMGNR")) {
target = el;
break;
}
}
if (target) {
target.click();
target.scrollIntoView({ behavior: "smooth", block: "center" });
// Flash background
target.style.transition = "background-color 0.5s";
const originalBg = target.style.backgroundColor;
target.style.backgroundColor = "rgba(8, 181, 167, 0.4)";
setTimeout(() => { target.style.backgroundColor = originalBg; }, 800);
} else {
setTimeout(() => tryClickTab(attempts + 1), 50);
}
};
tryClickTab();
} else {
toggleSplitScreen();
}
};
Object.assign(btn.style, {
display: "flex", alignItems: "center", justifyContent: "center",
width: "40px", height: "100%", padding: "0", border: "none", cursor: "pointer", background: "transparent"
});
const icon = document.createElement("img");
icon.id = "imgnr-split-screen-icon";
icon.src = new URL("./split_screen.svg", import.meta.url).href;
Object.assign(icon.style, {
width: "36px", height: "36px", objectFit: "contain", filter: "invert(0.9)", display: "block", paddingTop: "6px"
});
icon.onerror = () => { icon.style.display = "none"; btn.textContent = "||"; };
btn.appendChild(icon);
wrapper.appendChild(btn);
container.prepend(wrapper);
};
const container = document.querySelector(containerSelector);
if (container) addButton(container);
else {
const obs = new MutationObserver(() => {
const c = document.querySelector(containerSelector);
if(c) { addButton(c); obs.disconnect(); }
});
obs.observe(document.body, {childList: true, subtree: true});
}
}
// =========================================================
// LAYOUT ENGINE: MASSIVE CANVAS
// =========================================================
function forceHighDPIResize(canvasObj, logicalWidth, logicalHeight) {
const ratio = window.devicePixelRatio || 1;
const canvas = canvasObj.canvas;
canvas.width = Math.round(logicalWidth * ratio);
canvas.height = Math.round(logicalHeight * ratio);
canvas.style.width = logicalWidth + "px";
canvas.style.height = logicalHeight + "px";
const ctx = canvasObj.ctx;
if(ctx) ctx.scale(ratio, ratio);
}
function refreshSettings() {
if (!isSplit || !secondaryCanvas) return;
// 1. Interaction
const interactionEnabled = app.ui.settings.getSettingValue(`${PREFIX}.SplitScreen.EnableInteraction`, true);
secondaryCanvas.allow_interaction = interactionEnabled;
secondaryCanvas.allow_dragnodes = interactionEnabled;
mirrorDOM.setInteraction(interactionEnabled);
// 2. Background
const bgColor = app.ui.settings.getSettingValue(`${PREFIX}.SplitScreen.CloneBgColor`, "#141414");
secondaryCanvas.clear_background_color = bgColor;
const w = document.getElementById("imgnr-split-wrapper");
if (w) w.style.backgroundColor = bgColor;
// 3. Labels
const labelColor = app.ui.settings.getSettingValue(`${PREFIX}.SplitScreen.LabelColor`, "#7a7a7a");
const labelSize = app.ui.settings.getSettingValue(`${PREFIX}.SplitScreen.LabelSize`, 10);
if (labelOriginal) {
labelOriginal.style.color = labelColor;
labelOriginal.style.fontSize = labelSize + "px";
}
if (labelClone) {
labelClone.style.color = labelColor;
labelClone.style.fontSize = labelSize + "px";
}
// 4. Line Color/Width
const userColor = app.ui.settings.getSettingValue(`${PREFIX}.SplitScreen.LineColor`, "#d33");
const userWidth = app.ui.settings.getSettingValue(`${PREFIX}.SplitScreen.LineWidth`, 4);
if (separatorLine) {
separatorLine.style.background = userColor;
separatorLine.style.width = userWidth + "px";
}
secondaryCanvas.dirty_bgcanvas = true;
secondaryCanvas.dirty_canvas = true;
app.canvas.setDirty(true, true);
applyResizeNow();
}
function applyResizeNow() {
// [FIX] Always ID-lookup to ensure valid reference
const wrapper = document.getElementById("imgnr-split-wrapper");
const sep = document.getElementById("multiview-separator");
if (!wrapper || !sep) return;
if (!currentSplitX || isNaN(currentSplitX)) currentSplitX = window.innerWidth / 2;
const targetX = isDragging ? pendingSplitX : currentSplitX;
const boundaryX = Math.floor(Math.max(50, Math.min(window.innerWidth - 50, targetX)));
wrapper.style.left = boundaryX + "px";
wrapper.style.width = (window.innerWidth - boundaryX) + "px";
const secCanvasEl = document.getElementById("multiview-canvas");
if (secCanvasEl) secCanvasEl.style.left = -boundaryX + "px";
if (mirrorContainer) mirrorContainer.style.left = -boundaryX + "px";
sep.style.left = (boundaryX - 15) + "px";
const rightOffset = window.innerWidth - boundaryX;
if (originalMenu) {
originalMenu.style.right = rightOffset + "px";
originalMenu.style.borderWidth = "2px";
originalMenu.style.borderColor = "var(--component-node-widget-background)";
originalMenu.style.background = "var(--comfy-menu-bg)";
originalMenu.style.height = "40px";
originalMenu.style.padding = "var(--spacing)";
}
if (!originalZoomPopup) originalZoomPopup = findZoomPopup();
if (originalZoomPopup) {
originalZoomPopup.style.right = rightOffset + "px";
}
if (splitControls) {
splitControls.style.left = (boundaryX + 5) + "px";
}
if (labelOriginal) {
labelOriginal.style.left = (boundaryX / 2) + "px";
labelOriginal.style.transform = "translateX(-50%)";
}
if (labelClone) {
const rightW = window.innerWidth - boundaryX;
labelClone.style.left = (boundaryX + (rightW / 2)) + "px";
labelClone.style.transform = "translateX(-50%)";
}
}
function onMouseDown(e) {
isDragging = true;
document.body.style.cursor = "col-resize";
if (separatorLine) separatorLine.style.background = "#ff0000";
const mainCanvas = document.getElementById("graph-canvas");
if (mainCanvas) {
mainCanvas.style.width = "100vw";
if(app.canvas) app.canvas.resize();
}
const wrapper = document.getElementById("imgnr-split-wrapper");
if (wrapper) wrapper.style.pointerEvents = "none";
e.preventDefault();
}
function onMouseMove(e) {
if (!isDragging) return;
pendingSplitX = e.clientX;
requestAnimationFrame(applyResizeNow);
}
function onMouseUp(e) {
if (!isDragging) return;
isDragging = false;
currentSplitX = pendingSplitX;
document.body.style.cursor = "default";
savePosition(currentSplitX);
if (separatorLine) separatorLine.style.background = app.ui.settings.getSettingValue(`${PREFIX}.SplitScreen.LineColor`, "#d33");
const wrapper = document.getElementById("imgnr-split-wrapper");
if (wrapper) wrapper.style.pointerEvents = "auto";
const mainCanvas = document.getElementById("graph-canvas");
if (mainCanvas) {
const boundaryX = Math.floor(Math.max(50, Math.min(window.innerWidth - 50, currentSplitX)));
mainCanvas.style.width = boundaryX + "px";
if(app.canvas) app.canvas.resize();
}
app.canvas.dirty_canvas = true;
app.canvas.dirty_bgcanvas = true;
}
// --- SMART ZOOM LOGIC ---
function getRightPaneVisualCenter() {
const visibleWidth = window.innerWidth - currentSplitX;
return currentSplitX + (visibleWidth / 2);
}
function centerZoom(canvasInstance, targetScale) {
if(!canvasInstance || !canvasInstance.ds) return;
const ds = canvasInstance.ds;
const visualCenterX = getRightPaneVisualCenter();
const visualCenterY = window.innerHeight / 2;
const graphPointX = (visualCenterX / ds.scale) - ds.offset[0];
const graphPointY = (visualCenterY / ds.scale) - ds.offset[1];
ds.scale = targetScale;
ds.offset[0] = (visualCenterX / ds.scale) - graphPointX;
ds.offset[1] = (visualCenterY / ds.scale) - graphPointY;
canvasInstance.setDirty(true, true);
}
function fitViewToRightPane(canvasInstance) {
if(!canvasInstance) return;
const selection = canvasInstance.selected_nodes || {};
const nodes = Object.values(selection);
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
let targetNodes = nodes;
if (targetNodes.length === 0 && canvasInstance.graph && canvasInstance.graph._nodes) {
targetNodes = canvasInstance.graph._nodes;
}
if (targetNodes.length === 0) {
centerZoom(canvasInstance, 1);
return;
}
for (const n of targetNodes) {
minX = Math.min(minX, n.pos[0]);
maxX = Math.max(maxX, n.pos[0] + n.size[0]);
minY = Math.min(minY, n.pos[1]);
maxY = Math.max(maxY, n.pos[1] + n.size[1]);
}
const graphCX = (minX + maxX) / 2;
const graphCY = (minY + maxY) / 2;
const nodesW = maxX - minX;
const nodesH = maxY - minY;
const topMenuHeight = 110;
const paneW = window.innerWidth - currentSplitX;
const paneH = window.innerHeight - topMenuHeight;
const marginX = 50;
const marginY = 50;
const scaleX = (paneW - marginX * 2) / nodesW;
const scaleY = (paneH - marginY * 2) / nodesH;
let targetScale = Math.min(scaleX, scaleY);
targetScale = Math.min(Math.max(targetScale, 0.02), 4.0);
const ds = canvasInstance.ds;
ds.scale = targetScale;
const visualCX = getRightPaneVisualCenter();
const visualCY = topMenuHeight + (paneH / 2);
ds.offset[0] = (visualCX / ds.scale) - graphCX;
ds.offset[1] = (visualCY / ds.scale) - graphCY;
canvasInstance.setDirty(true, true);
}
function toggleSplitScreen() {
const mainCanvas = document.getElementById("graph-canvas");
if (!mainCanvas) return;
const icon = document.getElementById("imgnr-split-screen-icon");
isSplit = !isSplit;
if (icon) icon.style.filter = isSplit ? "invert(76%) sepia(18%) saturate(1065%) hue-rotate(184deg) brightness(101%) contrast(96%)" : "invert(0.9)";
if (isSplit) {
originalMainStyle.width = mainCanvas.style.width;
originalMainStyle.height = mainCanvas.style.height;
originalMainStyle.position = mainCanvas.style.position;
originalBodyBg = document.body.style.background;
originalOverflow = document.body.style.overflow;
const userColor = app.ui.settings.getSettingValue(`${PREFIX}.SplitScreen.LineColor`, "#d33");
const userWidth = app.ui.settings.getSettingValue(`${PREFIX}.SplitScreen.LineWidth`, 4);
const labelColor = app.ui.settings.getSettingValue(`${PREFIX}.SplitScreen.LabelColor`, "#7a7a7a");
const labelSize = app.ui.settings.getSettingValue(`${PREFIX}.SplitScreen.LabelSize`, 10);
const bgColor = app.ui.settings.getSettingValue(`${PREFIX}.SplitScreen.CloneBgColor`, "#141414");
const allowInteract = app.ui.settings.getSettingValue(`${PREFIX}.SplitScreen.EnableInteraction`, true);
currentSplitX = loadSavedPosition();
pendingSplitX = currentSplitX;
document.body.style.overflow = "hidden";
const boundaryX = Math.floor(Math.max(50, Math.min(window.innerWidth - 50, currentSplitX)));
mainCanvas.style.width = boundaryX + "px";
if(app.canvas) app.canvas.resize();
const wrapper = document.createElement("div");
wrapper.id = "imgnr-split-wrapper";
Object.assign(wrapper.style, {
position: "absolute", top: "0", right: "0", height: "100%",
overflow: "hidden", zIndex: "50", background: bgColor
});
document.body.appendChild(wrapper);
separatorEl = document.createElement("div");
separatorEl.id = "multiview-separator";
Object.assign(separatorEl.style, {
position: "absolute", top: "0", width: "30px", height: "100%",
background: "transparent", cursor: "col-resize",
zIndex: "60", userSelect: "none", display: "flex", justifyContent: "center"
});
separatorLine = document.createElement("div");
Object.assign(separatorLine.style, {
width: `${userWidth}px`, height: "100%", background: userColor, transition: "background 0.1s"
});
separatorEl.appendChild(separatorLine);
separatorEl.onmouseenter = () => { if(!isDragging && separatorLine) separatorLine.style.background = "#ff0000"; };
separatorEl.onmouseleave = () => { if(!isDragging && separatorLine) separatorLine.style.background = userColor; };
separatorEl.addEventListener("mousedown", onMouseDown);
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
document.body.appendChild(separatorEl);
const MASSIVE_PAD = 0;
const massiveCSSWidth = window.innerWidth + MASSIVE_PAD;
const massiveCSSHeight = window.innerHeight + MASSIVE_PAD;
mirrorContainer = document.createElement("div");
mirrorContainer.id = "imgnr-mirror-container";
Object.assign(mirrorContainer.style, {
position: "absolute", top: "0", left: "0",
width: massiveCSSWidth + "px", height: massiveCSSHeight + "px",
pointerEvents: "none", zIndex: "55", overflow: "visible"
});
wrapper.appendChild(mirrorContainer);
const secCanvasEl = document.createElement("canvas");
secCanvasEl.id = "multiview-canvas";
secCanvasEl.tabIndex = 1;
Object.assign(secCanvasEl.style, {
position: "absolute", top: "0", left: "0", outline: "none",
background: "transparent", margin: "0", padding: "0",
width: massiveCSSWidth + "px", height: massiveCSSHeight + "px"
});
wrapper.appendChild(secCanvasEl);
labelOriginal = document.createElement("div");
labelOriginal.textContent = "ORIGINAL";
Object.assign(labelOriginal.style, {
position: "fixed", bottom: "10px", color: labelColor, fontSize: `${labelSize}px`,
fontFamily: "Lucida Console, sans-serif", pointerEvents: "none",
padding: "5px", background: "rgba(0,0,0,0.7)", zIndex: "1001", userSelect: "none", whiteSpace: "nowrap"
});
document.body.appendChild(labelOriginal);
labelClone = document.createElement("div");
labelClone.textContent = "CLONE VIEW";
Object.assign(labelClone.style, {
position: "fixed", bottom: "10px", color: labelColor, fontSize: `${labelSize}px`,
fontFamily: "Lucida Console, sans-serif", pointerEvents: "none",
padding: "5px", background: "rgba(0,0,0,0.7)", zIndex: "1001", userSelect: "none", whiteSpace: "nowrap"
});
document.body.appendChild(labelClone);
splitControls = document.createElement("div");
splitControls.className = "p-buttongroup p-component absolute bottom-0 z-[1200] flex-row gap-1 border-[1px] border-interface-stroke bg-comfy-menu-bg p-2";
Object.assign(splitControls.style, {
position: "fixed",
bottom: "4px",
display: "flex", gap: "4px", padding: "4px",
borderRadius: "8px",
borderWidth: "2px",
borderColor: "var(--component-node-widget-background)",
background: "var(--comfy-menu-bg)",
pointerEvents: "auto",
height: "40px"
});
const createCtrlBtn = (text, iconSvg, action) => {
const b = document.createElement("button");
b.className = "p-button p-component p-button-secondary bg-comfy-menu-bg hover:bg-interface-button-hover-surface! p-0 h-8";
Object.assign(b.style, {
background: "transparent", border: "none", cursor: "pointer",
borderRadius: "8px", //color: "var(--input-text)",
display: "inline-flex", alignItems: "center", justifyContent: "center",
height: "32px", minWidth: "32px",
padding: text ? "0 12px" : "0",
fontSize: "12px", fontFamily: "sans-serif"
});
b.innerHTML = iconSvg ?
`<span class="flex items-center gap-2">${iconSvg}${text ? `<span>${text}</span>` : ''}</span>` :
`<span>${text}</span>`;
b.onmouseenter = () => b.style.background = "var(--comfy-menu-secondary-bg)";
b.onmouseleave = () => b.style.background = "transparent";
b.onclick = action;
return b;
};
const iconFit = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>`;
const btnFit = createCtrlBtn("", iconFit, () => fitViewToRightPane(secondaryCanvas));
btnFit.title = "Fit View (Center Selection)";
const iconSync = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>`;
const btnSync = createCtrlBtn("MATCH ZOOM", iconSync, () => {
if(app.canvas && secondaryCanvas) {
centerZoom(secondaryCanvas, app.canvas.ds.scale);
}
});
const div = () => {
const d = document.createElement("div");
d.className = "h-[27px] w-[1px] self-center bg-node-divider";
Object.assign(d.style, { width: "1px", height: "24px", background: "var(--border-color)", alignSelf: "center", margin: "0 2px" });
return d;
};
const btnReset = createCtrlBtn("1:1", null, () => centerZoom(secondaryCanvas, 1));
splitControls.appendChild(btnFit);
splitControls.appendChild(div());
splitControls.appendChild(btnSync);
splitControls.appendChild(div());
splitControls.appendChild(btnReset);
document.body.appendChild(splitControls);
mirrorDOM.init(mirrorContainer);
// Initialize State Correctly
mirrorDOM.setInteraction(allowInteract);
secondaryCanvas = new LGraphCanvas(secCanvasEl, app.graph);
secondaryCanvas.background_image = app.canvas.background_image;
secondaryCanvas.clear_background = true;
secondaryCanvas.clear_background_color = bgColor;
secondaryCanvas.highquality_render = true;
secondaryCanvas.render_canvas_border = false;
secondaryCanvas.allow_dragnodes = allowInteract;
secondaryCanvas.allow_interaction = allowInteract;
secondaryCanvas.read_only = false;
secondaryCanvas.zoom_modify_alpha = true;
forceHighDPIResize(secondaryCanvas, massiveCSSWidth, massiveCSSHeight);
setupInteractionSync(secCanvasEl);
secondaryCanvas.startRendering();
setTimeout(() => { secondaryCanvas.stopRendering(); }, 100);
setupMenus();
applyResizeNow();
applyCameraState();
app.canvas.draw = function(force_fg, force_bg) {
const wasDirtyCanvas = this.dirty_canvas;
const wasDirtyBg = this.dirty_bgcanvas;
originalDraw.apply(this, arguments);
if (secondaryCanvas) {
if (secondaryCanvas.graph !== app.graph) secondaryCanvas.graph = app.graph;
const wrapper = document.getElementById("imgnr-split-wrapper");
if (wrapper) {
const shift = -(wrapper.getBoundingClientRect().left);
secCanvasEl.style.left = shift + "px";
mirrorContainer.style.left = shift + "px";
}
if (wasDirtyBg || force_bg) secondaryCanvas.dirty_bgcanvas = true;
if (wasDirtyCanvas || force_fg) secondaryCanvas.dirty_canvas = true;
originalDraw.call(secondaryCanvas, force_fg, force_bg);
mirrorDOM.update(secondaryCanvas);
mirrorDOM.drawDebug && mirrorDOM.drawDebug(secondaryCanvas.ctx);
}
};
window.addEventListener("resize", onWindowResize);
app.canvas.draw(true, true);
} else {
savePosition(currentSplitX);
saveCameraState();
mirrorDOM.destroy();
teardownMenus();
teardownInteractionSync(document.getElementById("multiview-canvas"));
if (labelOriginal) { labelOriginal.remove(); labelOriginal = null; }
if (labelClone) { labelClone.remove(); labelClone = null; }
if (splitControls) { splitControls.remove(); splitControls = null; }
// [FIX] Always cleanup via ID
const wrapper = document.getElementById("imgnr-split-wrapper");
if (wrapper) wrapper.remove();
app.canvas.draw = originalDraw;
window.removeEventListener("resize", onWindowResize);
if (bgBlocker) { bgBlocker.remove(); bgBlocker = null; }
if (separatorEl) { separatorEl.remove(); separatorEl = null; separatorLine = null; }
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
mainCanvas.style.width = originalMainStyle.width || "100%";
mainCanvas.style.height = originalMainStyle.height || "100%";
mainCanvas.style.position = originalMainStyle.position || "relative";
document.body.style.overflow = originalOverflow || "";
document.body.style.background = originalBodyBg || "";
mainCanvas.removeAttribute("width");
mainCanvas.removeAttribute("height");
if (app.canvas && app.canvas.resize) {
app.canvas.resize();
}
secondaryCanvas = null;
app.canvas.draw(true, true);
}
}
function onWindowResize() {
if (app.canvas) app.canvas.resize();
if (secondaryCanvas) {
const MASSIVE_PAD = 0;
const massiveCSSWidth = window.innerWidth + MASSIVE_PAD;
const massiveCSSHeight = window.innerHeight + MASSIVE_PAD;
forceHighDPIResize(secondaryCanvas, massiveCSSWidth, massiveCSSHeight);
if (mirrorContainer) {
mirrorContainer.style.width = massiveCSSWidth + "px";
mirrorContainer.style.height = massiveCSSHeight + "px";
}
secondaryCanvas.dirty_canvas = true;
}
applyResizeNow();
}
app.registerExtension({
name: "IMGNR.SplitScreen",
setup() {
window.IMGNR_REFRESH_SPLIT_SCREEN = refreshSettings;
if (app.ui.settings.getSettingValue(`${PREFIX}.SplitScreen.Enabled`, true)) {
setupToolbarButton();
const originalLoadGraph = app.loadGraphData;
app.loadGraphData = function(graphData) {
const result = originalLoadGraph.apply(this, arguments);
if (isSplit) setTimeout(() => refreshSplitViewOnSwitch(), 50);
return result;
};
}
}
});
function refreshSplitViewOnSwitch() {
if (!isSplit || !secondaryCanvas) return;
if (secondaryCanvas.graph !== app.graph) secondaryCanvas.graph = app.graph;
applyResizeNow();
applyCameraState();
}