Files

657 lines
26 KiB
JavaScript

import { app } from "../../scripts/app.js";
app.registerExtension({
name: "Comfy.NodeGhost",
async setup() {
const canvas = app.canvas;
if (!canvas) return;
let customColorValue = localStorage.getItem("Comfy.NodeGhost.CustomColor") || "#00eeff";
let ghost = null;
let isDragging = false;
let startX = 0;
let startY = 0;
let dragThreshold = 5;
let draggingNodes = [];
let initialNodePositions = [];
let draggingGroups = [];
let initialGroupPositions = [];
let extraNodes = [];
let initialExtraPositions = [];
let ghostOffset = { x: 0, y: 0 };
app.ui.settings.addSetting({
id: "Comfy.NodeGhost.CustomColorPicker",
name: "📝 Pick Custom Color",
type: (name, setter, value) => {
const button = document.createElement("button");
button.innerHTML = " ";
button.style.cssText = "padding: 0; cursor: pointer; width: 100%; min-width: 100px; height: 30px; border: 2px solid #555; border-radius: 4px; display: block;";
button.style.background = customColorValue;
button.title = "Click to choose custom color";
button.onclick = () => {
const input = document.createElement('input');
input.type = 'color';
input.value = customColorValue;
input.onchange = (e) => {
customColorValue = e.target.value;
localStorage.setItem("Comfy.NodeGhost.CustomColor", customColorValue);
app.ui.settings.setSettingValue("Comfy.NodeGhost.BorderColor", customColorValue);
button.style.background = customColorValue;
document.body.removeChild(input);
};
input.style.position = 'absolute';
input.style.opacity = '0';
input.style.pointerEvents = 'none';
document.body.appendChild(input);
input.click();
};
return button;
}
});
app.ui.settings.addSetting({
id: "Comfy.NodeGhost.BorderColor",
name: "Ghost Border Color",
type: "combo",
defaultValue: "#999999",
options: [
{ value: "#999999", text: "ComfyUI Gray" },
{ value: "#ffffff", text: "White" },
{ value: "#00eeff", text: "Cyan" },
{ value: "#ff6e6e", text: "Red" },
{ value: "#8bc34a", text: "Green" },
{ value: "#ffeb3b", text: "Yellow" },
{ value: "#ff9800", text: "Orange" },
{ value: "#9c27b0", text: "Purple" },
],
onChange: (value) => {
if (value === "#999999") {
customColorValue = "#999999";
}
}
});
app.ui.settings.addSetting({
id: "Comfy.NodeGhost.BorderStyle",
name: "Ghost Border Style",
type: "combo",
defaultValue: "solid",
options: ["solid", "dashed", "dotted", "double", "none"],
});
app.ui.settings.addSetting({
id: "Comfy.NodeGhost.RespectPinned",
name: "Respect Pinned Nodes 📌",
type: "boolean",
defaultValue: false,
});
app.ui.settings.addSetting({
id: "Comfy.NodeGhost.Enabled",
name: "Enable Ghost Dragging 👻",
type: "boolean",
defaultValue: true,
});
const styleId = "cncs-ghost-styles";
if (!document.getElementById(styleId)) {
const s = document.createElement("style");
s.id = styleId;
s.textContent = `
.cncs-ghost-phantom {
position: fixed !important;
border-radius: 0px !important;
pointer-events: none !important;
z-index: 9999 !important;
box-sizing: border-box !important;
display: none;
}
.cncs-ghost-node {
position: absolute !important;
border-radius: 0px !important;
pointer-events: none !important;
box-sizing: border-box !important;
}
`;
document.head.appendChild(s);
}
const isEnabled = () => app.ui.settings.getSettingValue("Comfy.NodeGhost.Enabled");
const pointerDownHandler = (e) => {
if (!isEnabled() || e.button !== 0) return;
if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) return;
draggingNodes = [];
draggingGroups = [];
extraNodes = [];
initialExtraPositions = [];
const path = e.composedPath();
if (!path.includes(canvas.canvas)) return;
const isClickingUI = path.some(el => {
if (!el.classList) return false;
const classList = Array.from(el.classList);
return classList.some(c =>
c.includes('modal') ||
c.includes('dialog') ||
c.includes('menu') ||
c.includes('popup') ||
c.includes('settings') ||
c.includes('comfy-')
);
});
if (isClickingUI) return;
const graph = canvas.graph;
if (!graph) return;
const nodePos = canvas.convertEventToCanvasOffset(e);
const node = graph.getNodeOnPos(nodePos[0], nodePos[1]);
if (node) {
const localX = nodePos[0] - node.pos[0];
const localY = nodePos[1] - node.pos[1];
const cornerMargin = 20;
const inTopLeftCorner = localX < cornerMargin && localY < cornerMargin;
const inTopRightCorner = localX > (node.size[0] - cornerMargin) && localY < cornerMargin;
const inBottomLeftCorner = localX < cornerMargin && localY > (node.size[1] - cornerMargin);
const inBottomRightCorner = localX > (node.size[0] - cornerMargin) && localY > (node.size[1] - cornerMargin);
if (inTopLeftCorner || inTopRightCorner || inBottomLeftCorner || inBottomRightCorner) return;
if (node.widgets && node.widgets.length > 0) {
for (let widget of node.widgets) {
if (node.getWidgetOnPos && node.getWidgetOnPos(nodePos[0], nodePos[1])) return;
}
}
const slotMargin = 15;
if (localX < slotMargin || localX > (node.size[0] - slotMargin)) return;
if (canvas.isOverNodeInput && canvas.isOverNodeInput(node, nodePos[0], nodePos[1])) return;
if (canvas.isOverNodeOutput && canvas.isOverNodeOutput(node, nodePos[0], nodePos[1])) return;
const selectedNodes = canvas.selected_nodes || {};
if (selectedNodes[node.id]) {
draggingNodes = Object.values(selectedNodes).filter(n => n.graph === graph);
} else {
draggingNodes = [node];
}
if (draggingNodes.length > 0) {
initialNodePositions = draggingNodes.map(n => ({ x: n.pos[0], y: n.pos[1] }));
startX = e.clientX;
startY = e.clientY;
isDragging = false;
}
} else {
const group = [...(graph._groups || [])].reverse().find(g => {
return nodePos[0] >= g.pos[0] && nodePos[0] <= g.pos[0] + g.size[0] &&
nodePos[1] >= g.pos[1] - 30 && nodePos[1] <= g.pos[1] + g.size[1];
});
if (group) {
const localX = nodePos[0] - group.pos[0];
const localY = nodePos[1] - group.pos[1];
const cornerSize = 25;
const resizable = localX > group.size[0] - cornerSize && localY > group.size[1] - cornerSize;
if (resizable) return;
draggingGroups = [group];
initialGroupPositions = [{ x: group.pos[0], y: group.pos[1] }];
// Use LiteGraph's native method to find nodes inside the group
if (group.recomputeInsideNodes) {
group.recomputeInsideNodes();
}
const groupNodes = group._nodes || [];
// Include selected nodes that are outside the group
const selectedNodes = canvas.selected_nodes || {};
if (Object.keys(selectedNodes).length > 0) {
draggingNodes = Object.values(selectedNodes).filter(n =>
n.graph === graph && !groupNodes.some(gn => gn.id === n.id)
);
initialNodePositions = draggingNodes.map(n => ({ x: n.pos[0], y: n.pos[1] }));
}
extraNodes = groupNodes.filter(gn => !draggingNodes.some(dn => dn.id === gn.id));
initialExtraPositions = extraNodes.map(en => ({ x: en.pos[0], y: en.pos[1] }));
startX = e.clientX;
startY = e.clientY;
isDragging = false;
}
}
};
const createGhostElement = (rect, nodes, groups, groupMinX, groupMinY, scale) => {
const el = document.createElement("div");
el.className = "cncs-ghost-phantom";
el.style.width = rect.width + "px";
el.style.height = rect.height + "px";
el.style.left = "0px";
el.style.top = "0px";
const borderStyle = app.ui.settings.getSettingValue("Comfy.NodeGhost.BorderStyle") || "dashed";
let borderColor = app.ui.settings.getSettingValue("Comfy.NodeGhost.BorderColor") || "#999999";
const borderWidth = borderStyle === "double" ? "4px" : "2px";
if (!borderColor.startsWith("#")) {
borderColor = customColorValue;
}
el.style.borderStyle = borderStyle;
el.style.borderColor = borderColor;
el.style.borderWidth = borderWidth;
const rgb = hexToRgb(borderColor);
if (rgb) {
const darkerR = Math.floor(rgb.r * 0.7);
const darkerG = Math.floor(rgb.g * 0.7);
const darkerB = Math.floor(rgb.b * 0.7);
el.style.background = `rgba(${darkerR}, ${darkerG}, ${darkerB}, 0.3)`;
}
const respectPinned = app.ui.settings.getSettingValue("Comfy.NodeGhost.RespectPinned");
// Sub-ghosts for nodes
nodes.forEach(n => {
const sub = document.createElement("div");
sub.className = "cncs-ghost-node";
sub.style.borderStyle = borderStyle;
sub.style.borderWidth = borderWidth;
const isPinned = n.flags && n.flags.pinned;
if (isPinned) {
sub.style.borderColor = "#ff3333";
sub.style.background = "rgba(100, 0, 0, 0.4)";
} else {
sub.style.borderColor = borderColor;
if (rgb) {
const darkerR = Math.floor(rgb.r * 0.7);
const darkerG = Math.floor(rgb.g * 0.7);
const darkerB = Math.floor(rgb.b * 0.7);
sub.style.background = `rgba(${darkerR}, ${darkerG}, ${darkerB}, 0.2)`;
}
}
el.appendChild(sub);
});
// Sub-ghosts for groups
groups.forEach(g => {
const sub = document.createElement("div");
sub.className = "cncs-ghost-node";
sub.style.borderStyle = borderStyle;
sub.style.borderWidth = borderWidth;
sub.style.borderColor = borderColor;
if (rgb) {
const darkerR = Math.floor(rgb.r * 0.7);
const darkerG = Math.floor(rgb.g * 0.7);
const darkerB = Math.floor(rgb.b * 0.7);
sub.style.background = `rgba(${darkerR}, ${darkerG}, ${darkerB}, 0.2)`;
}
el.appendChild(sub);
});
document.body.appendChild(el);
return el;
};
const hexToRgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
};
let rafId = null;
let lastMoveEvent = null;
const pointerMoveHandler = (e) => {
if (draggingNodes.length === 0 && draggingGroups.length === 0) return;
if (!isDragging) {
const dist = Math.sqrt(Math.pow(e.clientX - startX, 2) + Math.pow(e.clientY - startY, 2));
if (dist > dragThreshold) {
isDragging = true;
if (canvas.node_dragged) canvas.node_dragged = null;
}
}
if (isDragging) {
e.stopImmediatePropagation();
e.preventDefault();
draggingNodes.forEach((n, i) => {
if (initialNodePositions[i]) {
n.pos[0] = initialNodePositions[i].x;
n.pos[1] = initialNodePositions[i].y;
}
});
draggingGroups.forEach((g, i) => {
if (initialGroupPositions[i]) {
g.pos[0] = initialGroupPositions[i].x;
g.pos[1] = initialGroupPositions[i].y;
}
});
extraNodes.forEach((n, i) => {
if (initialExtraPositions[i]) {
n.pos[0] = initialExtraPositions[i].x;
n.pos[1] = initialExtraPositions[i].y;
}
});
}
lastMoveEvent = e;
if (rafId) return;
rafId = requestAnimationFrame(() => {
rafId = null;
if (!lastMoveEvent || !isDragging) return;
const evt = lastMoveEvent;
draggingNodes.forEach((n, i) => {
if (initialNodePositions[i]) {
n.pos[0] = initialNodePositions[i].x;
n.pos[1] = initialNodePositions[i].y;
}
});
draggingGroups.forEach((g, i) => {
if (initialGroupPositions[i]) {
g.pos[0] = initialGroupPositions[i].x;
g.pos[1] = initialGroupPositions[i].y;
}
});
extraNodes.forEach((n, i) => {
if (initialExtraPositions[i]) {
n.pos[0] = initialExtraPositions[i].x;
n.pos[1] = initialExtraPositions[i].y;
}
});
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
const getNodeWidth = (node) => {
const isCollapsed = node.flags && node.flags.collapsed;
if (isCollapsed) {
if (node._collapsed_width) return node._collapsed_width;
const title = node.getTitle ? node.getTitle() : (node.title || "");
const minWidth = LiteGraph.NODE_COLLAPSED_WIDTH || 100;
const textWidth = title.length * 7 + 40;
return Math.max(minWidth, textWidth);
}
return node.size[0];
};
draggingNodes.forEach(n => {
const isCollapsed = n.flags && n.flags.collapsed;
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT || 30;
const w = getNodeWidth(n);
const effectiveY = n.pos[1] - titleHeight;
const effectiveBottom = n.pos[1] + (isCollapsed ? 0 : n.size[1]);
minX = Math.min(minX, n.pos[0]);
minY = Math.min(minY, effectiveY);
maxX = Math.max(maxX, n.pos[0] + w);
maxY = Math.max(maxY, effectiveBottom);
});
draggingGroups.forEach(g => {
const titleHeight = 30;
const effectiveY = g.pos[1] - titleHeight;
minX = Math.min(minX, g.pos[0]);
minY = Math.min(minY, effectiveY);
maxX = Math.max(maxX, g.pos[0] + g.size[0]);
maxY = Math.max(maxY, g.pos[1] + g.size[1]);
});
const ds = canvas.ds;
const scale = ds.scale;
const width = (maxX - minX) * scale;
const height = (maxY - minY) * scale;
if (!ghost) {
ghost = createGhostElement({ width, height }, draggingNodes, draggingGroups, minX, minY, scale);
ghost.style.display = "block";
const rect = canvas.canvas.getBoundingClientRect();
const screenMinX = (minX + ds.offset[0]) * scale + rect.left;
const screenMinY = (minY + ds.offset[1]) * scale + rect.top;
ghostOffset.x = evt.clientX - screenMinX;
ghostOffset.y = evt.clientY - screenMinY;
}
const targetScreenLeft = evt.clientX - ghostOffset.x;
const targetScreenTop = evt.clientY - ghostOffset.y;
const rect = canvas.canvas.getBoundingClientRect();
let graphX = (targetScreenLeft - rect.left) / scale - ds.offset[0];
let graphY = (targetScreenTop - rect.top) / scale - ds.offset[1];
if (canvas.align_to_grid || true) {
const gridSize = LiteGraph.CANVAS_GRID_SIZE || 10;
graphX = Math.round(graphX / gridSize) * gridSize;
graphY = Math.round(graphY / gridSize) * gridSize;
}
ghost.style.width = width + "px";
ghost.style.height = height + "px";
const respectPinned = app.ui.settings.getSettingValue("Comfy.NodeGhost.RespectPinned");
const children = Array.from(ghost.children);
let ghostIdx = 0;
draggingNodes.forEach((n, i) => {
const child = children[ghostIdx++];
if (!child) return;
const isCollapsed = n.flags && n.flags.collapsed;
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT || 30;
const subW = getNodeWidth(n);
const subH = isCollapsed ? titleHeight : (n.size[1] + titleHeight);
const isNodePinned = respectPinned && (n.flags && n.flags.pinned);
child.style.width = (subW * scale) + "px";
child.style.height = (subH * scale) + "px";
if (isNodePinned) {
child.style.left = ((initialNodePositions[i].x - graphX) * scale) + "px";
child.style.top = (((initialNodePositions[i].y - titleHeight) - graphY) * scale) + "px";
} else {
child.style.left = ((initialNodePositions[i].x - minX) * scale) + "px";
child.style.top = (((initialNodePositions[i].y - titleHeight) - minY) * scale) + "px";
}
child.style.display = "block";
});
draggingGroups.forEach((g, i) => {
const child = children[ghostIdx++];
if (!child) return;
const titleHeight = 30;
child.style.width = (g.size[0] * scale) + "px";
child.style.height = ((g.size[1] + titleHeight) * scale) + "px";
child.style.left = ((initialGroupPositions[i].x - minX) * scale) + "px";
child.style.top = (((initialGroupPositions[i].y - titleHeight) - minY) * scale) + "px";
child.style.display = "block";
});
const finalScreenLeft = (graphX + ds.offset[0]) * scale + rect.left;
const finalScreenTop = (graphY + ds.offset[1]) * scale + rect.top;
ghost.style.left = finalScreenLeft + "px";
ghost.style.top = finalScreenTop + "px";
});
};
const pointerUpHandler = (e) => {
if (rafId) {
cancelAnimationFrame(rafId);
rafId = null;
}
lastMoveEvent = null;
if (isDragging && ghost) {
const ds = canvas.ds;
const scale = ds.scale;
const rect = canvas.canvas.getBoundingClientRect();
const targetScreenLeft = e.clientX - ghostOffset.x;
const targetScreenTop = e.clientY - ghostOffset.y;
let finalGraphX = (targetScreenLeft - rect.left) / scale - ds.offset[0];
let finalGraphY = (targetScreenTop - rect.top) / scale - ds.offset[1];
if (canvas.align_to_grid || true) {
const gridSize = LiteGraph.CANVAS_GRID_SIZE || 10;
finalGraphX = Math.round(finalGraphX / gridSize) * gridSize;
finalGraphY = Math.round(finalGraphY / gridSize) * gridSize;
}
let minInitialX = Infinity, minInitialY = Infinity;
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT || 30;
draggingNodes.forEach((n, i) => {
const effectiveY = initialNodePositions[i].y - titleHeight;
minInitialX = Math.min(minInitialX, initialNodePositions[i].x);
minInitialY = Math.min(minInitialY, effectiveY);
});
draggingGroups.forEach((g, i) => {
const effectiveY = initialGroupPositions[i].y - 30;
minInitialX = Math.min(minInitialX, initialGroupPositions[i].x);
minInitialY = Math.min(minInitialY, effectiveY);
});
const DeltaX = finalGraphX - minInitialX;
const DeltaY = finalGraphY - minInitialY;
const respectPinned = app.ui.settings.getSettingValue("Comfy.NodeGhost.RespectPinned");
let nodesMoved = false;
draggingNodes.forEach((n, i) => {
const isNodePinned = respectPinned && (n.flags && n.flags.pinned);
if (isNodePinned) return;
const newX = initialNodePositions[i].x + DeltaX;
const newY = initialNodePositions[i].y + DeltaY;
n.pos[0] = newX;
n.pos[1] = newY;
if (n.onNodeMoved) n.onNodeMoved();
nodesMoved = true;
});
draggingGroups.forEach((g, i) => {
g.pos[0] = initialGroupPositions[i].x + DeltaX;
g.pos[1] = initialGroupPositions[i].y + DeltaY;
nodesMoved = true;
});
extraNodes.forEach((n, i) => {
const isNodePinned = respectPinned && (n.flags && n.flags.pinned);
if (isNodePinned) return;
n.pos[0] = initialExtraPositions[i].x + DeltaX;
n.pos[1] = initialExtraPositions[i].y + DeltaY;
if (n.onNodeMoved) n.onNodeMoved();
nodesMoved = true;
});
if (nodesMoved) canvas.setDirty(true, true);
}
if (ghost) {
ghost.remove();
ghost = null;
}
draggingNodes = [];
initialNodePositions = [];
draggingGroups = [];
initialGroupPositions = [];
isDragging = false;
};
const cancelDrag = () => {
if (rafId) {
cancelAnimationFrame(rafId);
rafId = null;
}
lastMoveEvent = null;
if (isDragging) {
if (draggingNodes.length > 0) {
draggingNodes.forEach((n, i) => {
if (initialNodePositions[i]) {
n.pos[0] = initialNodePositions[i].x;
n.pos[1] = initialNodePositions[i].y;
}
});
}
if (draggingGroups.length > 0) {
draggingGroups.forEach((g, i) => {
if (initialGroupPositions[i]) {
g.pos[0] = initialGroupPositions[i].x;
g.pos[1] = initialGroupPositions[i].y;
}
});
}
if (extraNodes.length > 0) {
extraNodes.forEach((n, i) => {
if (initialExtraPositions[i]) {
n.pos[0] = initialExtraPositions[i].x;
n.pos[1] = initialExtraPositions[i].y;
}
});
}
canvas.setDirty(true, true);
}
if (ghost) {
ghost.remove();
ghost = null;
}
draggingNodes = [];
initialNodePositions = [];
draggingGroups = [];
initialGroupPositions = [];
extraNodes = [];
initialExtraPositions = [];
isDragging = false;
};
const keyDownHandler = (e) => {
if (e.key === "Escape" && isDragging) {
cancelDrag();
e.stopPropagation();
e.preventDefault();
}
};
window.addEventListener("pointerdown", pointerDownHandler, true);
window.addEventListener("pointermove", pointerMoveHandler, true);
window.addEventListener("pointerup", pointerUpHandler, true);
window.addEventListener("keydown", keyDownHandler, true);
}
});