Files

793 lines
33 KiB
JavaScript

// Graph integration for virtual image links and multi-image prompt transport.
// @ts-ignore
import { app } from "../../../scripts/app.js";
import { addLayerForgeImageInputLink, getLayerForgeImageInputLinks, getLayerForgeImageInputSlot, LAYERFORGE_MAX_IMAGE_INPUTS, removeLayerForgeImageInputLink, } from "../utils/multi_image_input_utils.js";
export const canvasNodeInstances = new Map();
let layerForgeQuickCreateMenu = null;
let layerForgeQuickCreateCanvas = null;
let layerForgeQuickCreateCleanup = null;
let layerForgeQuickCreatePending = false;
let layerForgeQuickCreateToken = 0;
let layerForgeLastCapturedDropAt = 0;
let layerForgeSuppressNativeDropUntil = 0;
export const isLayerForgeTransportInput = (name) => /^input_image_\d+$/i.test(String(name || ""));
const getLayerForgeGraphLink = (node, linkId) => {
const graph = node?.graph || app.graph;
if (!graph || linkId == null)
return null;
for (const links of [graph.links, graph._links]) {
if (!links)
continue;
if (typeof links.get === 'function') {
const link = links.get(linkId) ?? links.get(String(linkId));
if (link)
return link;
}
const link = links[linkId] ?? links[String(linkId)];
if (link)
return link;
}
return null;
};
const getLayerForgeSlotIndex = (slots, rawSlot) => {
if (!Array.isArray(slots))
return -1;
if (typeof rawSlot === 'number')
return slots[rawSlot] ? rawSlot : -1;
for (const key of ['slot_index', 'slot', 'index']) {
const value = rawSlot?.[key];
if (typeof value === 'number' && slots[value])
return value;
}
if (rawSlot) {
const directIndex = slots.indexOf(rawSlot);
if (directIndex >= 0)
return directIndex;
const name = typeof rawSlot === 'string' ? rawSlot : rawSlot?.name;
if (name)
return slots.findIndex(slot => slot?.name === name);
}
return -1;
};
const getLayerForgePendingConnectorLink = (canvas) => {
const renderLinks = canvas?.linkConnector?.renderLinks;
const link = renderLinks?.[0] || renderLinks?.at?.(0);
if (!link)
return null;
const endpointNode = link.node
|| link.fromNode
|| link.originNode
|| link.sourceNode
|| link.toNode
|| link.targetNode
|| link.inputNode
|| link.outputNode;
const endpointSlot = link.fromSlot ?? link.slot ?? link.output ?? link.input ?? link.toSlot ?? {};
if (!endpointNode)
return null;
const inputIndex = getLayerForgeSlotIndex(endpointNode.inputs, endpointSlot);
const outputIndex = getLayerForgeSlotIndex(endpointNode.outputs, endpointSlot);
const toType = String(link.toType || link.targetType || link.targetSlotType || '').toLowerCase();
let direction = toType.includes('output') ? 'from_input' : 'from_output';
if (inputIndex >= 0 && outputIndex < 0)
direction = 'from_input';
if (outputIndex >= 0 && inputIndex < 0)
direction = 'from_output';
if (direction === 'from_input') {
const input = endpointNode.inputs?.[inputIndex] || endpointSlot;
const inputName = String(input?.name || '');
if (endpointNode?.comfyClass !== 'LayerForgeNode'
&& endpointNode?.type !== 'LayerForgeNode')
return null;
if (inputName !== 'input_image' && !isLayerForgeTransportInput(inputName))
return null;
return {
direction,
targetNode: endpointNode,
targetSlot: inputIndex,
};
}
const output = endpointNode.outputs?.[outputIndex] || endpointSlot || {};
return {
direction,
sourceNode: endpointNode,
sourceSlot: Math.max(0, outputIndex),
sourceType: String(output?.type || output?.datatype || output?.name || 'IMAGE'),
};
};
const getLayerForgePointerGraphPosition = (canvas, event) => {
if (Number.isFinite(event?.canvasX) && Number.isFinite(event?.canvasY)) {
return [event.canvasX, event.canvasY];
}
const rect = canvas?.canvas?.getBoundingClientRect?.();
const scale = canvas?.ds?.scale || 1;
const offset = canvas?.ds?.offset || [0, 0];
if (rect && Number.isFinite(event?.clientX) && Number.isFinite(event?.clientY)) {
return [
(event.clientX - rect.left) / scale - offset[0],
(event.clientY - rect.top) / scale - offset[1],
];
}
return [0, 0];
};
const getLayerForgeConnectionPosition = (node, isInput, slotIndex) => {
const normalize = (point) => {
if (!point || !Number.isFinite(Number(point[0])) || !Number.isFinite(Number(point[1])))
return null;
return [Number(point[0]), Number(point[1])];
};
const modernPosition = normalize(isInput
? node?.getInputPos?.(slotIndex)
: node?.getOutputPos?.(slotIndex));
if (modernPosition)
return modernPosition;
try {
if (typeof node?.getConnectionPos === 'function') {
const output = [0, 0];
const legacyPosition = normalize(node.getConnectionPos(isInput, slotIndex, output)) || normalize(output);
if (legacyPosition)
return legacyPosition;
}
}
catch {
// Fall through to stable LiteGraph geometry for older frontend builds.
}
const position = node?.pos || [0, 0];
const size = node?.size || [160, 0];
const slotY = Number(position[1] || 0) + 40 + Math.max(0, slotIndex) * 20;
return [
Number(position[0] || 0) + (isInput ? 0 : Number(size[0] || 160)),
slotY,
];
};
const getLayerForgeVirtualLinkGeometry = (targetNode, link) => {
const graph = targetNode?.graph || app.graph;
const sourceNode = graph?.getNodeById?.(Number(link?.source_id));
if (!sourceNode)
return null;
const inputSlot = getLayerForgeImageInputSlot(targetNode);
const inputIndex = Math.max(0, targetNode?.inputs?.indexOf(inputSlot) ?? 0);
const source = getLayerForgeConnectionPosition(sourceNode, false, Number(link?.source_slot) || 0);
const target = getLayerForgeConnectionPosition(targetNode, true, inputIndex);
if (!source || !target)
return null;
const midpoint = [
(source[0] + target[0]) / 2,
(source[1] + target[1]) / 2,
];
return { source, target, midpoint, sourceNode };
};
const getLayerForgeVirtualLinkColor = (link) => {
const colors = globalThis.LGraphCanvas?.link_type_colors || {};
const rawType = String(link?.source_type || 'IMAGE');
for (const candidate of [rawType, rawType.toUpperCase(), rawType.toLowerCase()]) {
if (colors[candidate])
return colors[candidate];
}
return '#5aa9f0';
};
const drawLayerForgeVirtualLinks = (canvas, context) => {
const graph = canvas?.graph || app.graph;
if (!graph?._nodes || canvas.links_render_mode === globalThis.LiteGraph?.HIDDEN_LINK)
return;
for (const targetNode of graph._nodes) {
if (targetNode?.comfyClass !== 'LayerForgeNode' && targetNode?.type !== 'LayerForgeNode')
continue;
const links = getLayerForgeImageInputLinks(targetNode);
links.forEach((link, index) => {
const geometry = getLayerForgeVirtualLinkGeometry(targetNode, link);
if (!geometry)
return;
const highlighted = Boolean(targetNode.selected || geometry.sourceNode.selected);
const color = highlighted ? '#ffffff' : getLayerForgeVirtualLinkColor(link);
const width = Number(canvas.connections_width) || 3;
const controlOffset = 80;
context.save();
context.lineJoin = 'round';
context.shadowBlur = 0;
context.shadowColor = 'transparent';
context.beginPath();
context.moveTo(geometry.source[0], geometry.source[1]);
context.bezierCurveTo(geometry.source[0] + controlOffset, geometry.source[1], geometry.target[0] - controlOffset, geometry.target[1], geometry.target[0], geometry.target[1]);
context.lineWidth = width + 4;
context.strokeStyle = canvas.render_connections_border !== false && !canvas.low_quality
? 'rgba(0, 0, 0, 0.5)'
: 'transparent';
if (context.strokeStyle !== 'transparent')
context.stroke();
context.beginPath();
context.moveTo(geometry.source[0], geometry.source[1]);
context.bezierCurveTo(geometry.source[0] + controlOffset, geometry.source[1], geometry.target[0] - controlOffset, geometry.target[1], geometry.target[0], geometry.target[1]);
context.lineWidth = width;
context.strokeStyle = color;
context.stroke();
if (canvas.linkMarkerShape !== 0 && (canvas.ds?.scale ?? 1) >= 0.6 && canvas.highquality_render !== false) {
context.beginPath();
context.arc(geometry.midpoint[0], geometry.midpoint[1], 5, 0, Math.PI * 2);
context.fillStyle = color;
context.fill();
context.fillStyle = highlighted ? '#222' : '#fff';
context.font = 'bold 7px sans-serif';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText(String(index + 1), geometry.midpoint[0], geometry.midpoint[1] + 0.3);
}
context.restore();
});
}
};
const getLayerForgeGraphPosition = (canvas, event) => {
try {
canvas?.adjustMouseEvent?.(event);
}
catch {
// Older LiteGraph builds may not expose adjustMouseEvent.
}
if (Array.isArray(canvas?.graph_mouse))
return [canvas.graph_mouse[0], canvas.graph_mouse[1]];
if (Number.isFinite(event?.canvasX) && Number.isFinite(event?.canvasY)) {
return [event.canvasX, event.canvasY];
}
const rect = canvas?.canvas?.getBoundingClientRect?.();
const scale = canvas?.ds?.scale || 1;
const offset = canvas?.ds?.offset || [0, 0];
if (rect && Number.isFinite(event?.clientX) && Number.isFinite(event?.clientY)) {
return [
(event.clientX - rect.left) / scale - offset[0],
(event.clientY - rect.top) / scale - offset[1],
];
}
return [0, 0];
};
const hitTestLayerForgeVirtualLinks = (graph, x, y) => {
let best = null;
for (const targetNode of graph?._nodes || []) {
if (targetNode?.comfyClass !== 'LayerForgeNode' && targetNode?.type !== 'LayerForgeNode')
continue;
getLayerForgeImageInputLinks(targetNode).forEach((link, index) => {
const geometry = getLayerForgeVirtualLinkGeometry(targetNode, link);
if (!geometry)
return;
const distance = Math.hypot(x - geometry.midpoint[0], y - geometry.midpoint[1]);
if (distance <= 18 && (!best || distance < best.distance)) {
best = {
targetNode,
index,
point: geometry.midpoint,
distance,
};
}
});
}
return best;
};
const getLayerForgeClientPosition = (canvas, point) => {
const rect = canvas?.canvas?.getBoundingClientRect?.();
if (!rect)
return null;
const scale = canvas?.ds?.scale || 1;
const offset = canvas?.ds?.offset || [0, 0];
return {
x: rect.left + (point[0] + offset[0]) * scale,
y: rect.top + (point[1] + offset[1]) * scale,
};
};
const openLayerForgeVirtualLinkMenu = (canvas, hit, event) => {
const ContextMenu = globalThis.LiteGraph?.ContextMenu;
if (typeof ContextMenu !== 'function')
return;
const clientPoint = getLayerForgeClientPosition(canvas, hit.point);
const clientX = Number.isFinite(event?.clientX) ? event.clientX : clientPoint?.x || 0;
const clientY = Number.isFinite(event?.clientY) ? event.clientY : clientPoint?.y || 0;
const PointerEventConstructor = globalThis.PointerEvent;
const MouseEventConstructor = globalThis.MouseEvent;
let menuEvent;
try {
const EventConstructor = PointerEventConstructor || MouseEventConstructor;
menuEvent = EventConstructor
? new EventConstructor('pointerdown', {
clientX,
clientY,
bubbles: true,
cancelable: true,
})
: { clientX, clientY };
}
catch {
menuEvent = { clientX, clientY };
}
let menuInstance = null;
const closeMenu = () => {
menuInstance?.close?.();
menuInstance?.remove?.();
};
menuInstance = new ContextMenu([
{
content: 'Remove connection',
callback: () => {
if (removeLayerForgeImageInputLink(hit.targetNode, hit.index)) {
hit.targetNode.setDirtyCanvas?.(true, true);
canvas?.setDirty?.(true, true);
canvas?.graph?.setDirtyCanvas?.(true, true);
app.graph?.change?.();
}
closeMenu();
},
},
], { event: menuEvent });
};
const clearLayerForgeTemporaryConnector = (canvas) => {
const connector = canvas?.linkConnector;
connector?.reset?.();
if (Array.isArray(connector?.renderLinks))
connector.renderLinks.length = 0;
canvas?.setDirty?.(true, true);
(canvas?.graph || app.graph)?.setDirtyCanvas?.(true, true);
};
const createLayerForgeLoadImageNode = (canvas, targetNode, position) => {
const graph = canvas?.graph || app.graph;
const LiteGraph = globalThis.LiteGraph;
if (!graph || typeof LiteGraph?.createNode !== 'function' || !targetNode)
return false;
if (getLayerForgeImageInputLinks(targetNode).length >= LAYERFORGE_MAX_IMAGE_INPUTS)
return false;
const node = LiteGraph.createNode('LoadImage');
if (!node)
return false;
node.pos = [position[0], position[1]];
graph.add(node);
const imageOutputIndex = Math.max(0, node.outputs?.findIndex((output) => {
const type = String(output?.type || output?.datatype || output?.name || '').toUpperCase();
return type.includes('IMAGE');
}) ?? 0);
const outputPosition = getLayerForgeConnectionPosition(node, false, imageOutputIndex);
if (outputPosition) {
node.pos = [
Number(node.pos?.[0] || 0) + position[0] - outputPosition[0],
Number(node.pos?.[1] || 0) + position[1] - outputPosition[1],
];
}
const output = node.outputs?.[imageOutputIndex];
addLayerForgeImageInputLink(targetNode, {
source_id: Number(node.id),
source_slot: imageOutputIndex,
source_type: String(output?.type || 'IMAGE'),
});
node.setDirtyCanvas?.(true, true);
targetNode.setDirtyCanvas?.(true, true);
graph.setDirtyCanvas?.(true, true);
app.graph?.change?.();
return true;
};
const getLayerForgeQuickCreateMenuEvent = (detail) => {
const clientX = Number(detail?.clientX) || 0;
const clientY = Number(detail?.clientY) || 0;
const PointerEventConstructor = globalThis.PointerEvent;
const MouseEventConstructor = globalThis.MouseEvent;
try {
const EventConstructor = PointerEventConstructor || MouseEventConstructor;
return EventConstructor
? new EventConstructor('pointerdown', {
clientX,
clientY,
bubbles: true,
cancelable: true,
})
: { clientX, clientY };
}
catch {
return { clientX, clientY };
}
};
const closeLayerForgeNativeNodeSearchSoon = () => {
const documentObject = globalThis.document;
if (!documentObject)
return;
const close = () => {
const container = documentObject.querySelector?.('.node-search-box-dialog-mask, .invisible-dialog-root, .comfy-vue-node-search-container');
const input = container?.querySelector?.('input')
|| documentObject.querySelector?.('input[id^="comfy-vue-node-search-box-input-"]');
if (!container && !input)
return;
const KeyboardEventConstructor = globalThis.KeyboardEvent;
if (typeof KeyboardEventConstructor !== 'function')
return;
const init = {
key: 'Escape',
code: 'Escape',
keyCode: 27,
which: 27,
bubbles: true,
cancelable: true,
};
input?.dispatchEvent?.(new KeyboardEventConstructor('keydown', init));
container?.dispatchEvent?.(new KeyboardEventConstructor('keydown', init));
documentObject.dispatchEvent?.(new KeyboardEventConstructor('keydown', init));
};
for (const delay of [0, 16, 50, 120])
setTimeout(close, delay);
};
const openLayerForgeQuickCreateMenu = (canvas, targetNode, detail) => {
const ContextMenu = globalThis.LiteGraph?.ContextMenu;
if (typeof ContextMenu !== 'function' || !targetNode)
return;
layerForgeQuickCreateMenu?.close?.();
layerForgeQuickCreateMenu?.remove?.();
layerForgeQuickCreateMenu = null;
const position = [
Number(detail?.canvasX) || 0,
Number(detail?.canvasY) || 0,
];
const finish = () => {
clearLayerForgeTemporaryConnector(canvas);
layerForgeQuickCreatePending = false;
layerForgeQuickCreateMenu?.close?.();
layerForgeQuickCreateMenu?.remove?.();
layerForgeQuickCreateMenu = null;
};
const menuInstance = new ContextMenu([
{
content: 'Load image',
callback: () => {
createLayerForgeLoadImageNode(canvas, targetNode, position);
finish();
},
},
], { event: getLayerForgeQuickCreateMenuEvent(detail) });
layerForgeQuickCreateMenu = menuInstance;
menuInstance.controller?.signal?.addEventListener?.('abort', () => {
if (layerForgeQuickCreateMenu !== menuInstance)
return;
clearLayerForgeTemporaryConnector(canvas);
layerForgeQuickCreatePending = false;
layerForgeQuickCreateMenu = null;
}, { once: true });
};
const getLayerForgeTargetInputState = (pending) => {
const targetNode = pending?.targetNode;
const input = getLayerForgeImageInputSlot(targetNode);
return {
inputLinkId: input?.link ?? null,
virtualLinkCount: getLayerForgeImageInputLinks(targetNode).length,
};
};
const scheduleLayerForgeQuickCreateMenu = (canvas, event, pending) => {
if (pending?.direction !== 'from_input' || !pending.targetNode)
return false;
const token = ++layerForgeQuickCreateToken;
const [canvasX, canvasY] = getLayerForgePointerGraphPosition(canvas, event);
const detail = {
clientX: event?.clientX,
clientY: event?.clientY,
canvasX,
canvasY,
originalEvent: event,
};
const linkSnapshot = { ...pending };
const before = getLayerForgeTargetInputState(linkSnapshot);
const graph = canvas?.graph || app.graph;
const beforeGraphVersion = Number(graph?._version) || 0;
const releaseConnectorHold = holdLayerForgeConnectorReset(canvas);
layerForgeQuickCreatePending = true;
layerForgeSuppressNativeDropUntil = performance.now() + 1000;
closeLayerForgeNativeNodeSearchSoon();
const checkConnected = () => {
if (token !== layerForgeQuickCreateToken)
return true;
const current = getLayerForgeTargetInputState(linkSnapshot);
const graphVersion = Number(graph?._version) || 0;
return current.inputLinkId != null
|| current.virtualLinkCount > before.virtualLinkCount
|| graphVersion > beforeGraphVersion;
};
const openIfStillEmpty = () => {
if (token !== layerForgeQuickCreateToken) {
releaseConnectorHold?.();
return;
}
if (checkConnected()) {
releaseConnectorHold?.();
layerForgeQuickCreatePending = false;
clearLayerForgeTemporaryConnector(canvas);
return;
}
releaseConnectorHold?.();
layerForgeQuickCreatePending = false;
openLayerForgeQuickCreateMenu(canvas, linkSnapshot.targetNode, detail);
};
setTimeout(openIfStillEmpty, 70);
return true;
};
const holdLayerForgeConnectorReset = (canvas) => {
const events = canvas?.linkConnector?.events;
if (!events)
return null;
const preventReset = (event) => event?.preventDefault?.();
events.addEventListener?.('reset', preventReset, { once: true });
return () => events.removeEventListener?.('reset', preventReset, { once: true });
};
const hasLayerForgePendingConnection = (canvas) => Boolean(canvas?.connecting_node
|| canvas?.connectingNode
|| canvas?.connecting_input
|| canvas?.connectingInput
|| canvas?.linkConnector?.renderLinks?.length);
const shouldSuppressLayerForgeNativeDrop = (type) => (type === 'dropped-on-canvas'
&& Boolean(layerForgeQuickCreateMenu || layerForgeQuickCreatePending)
&& performance.now() < layerForgeSuppressNativeDropUntil);
const primeLayerForgeInputDropSuppression = (canvas) => {
const pending = getLayerForgePendingConnectorLink(canvas);
if (pending?.direction !== 'from_input')
return false;
layerForgeQuickCreatePending = true;
layerForgeSuppressNativeDropUntil = performance.now() + 1000;
return true;
};
const installLayerForgeQuickCreateCapture = (canvas) => {
const events = canvas?.linkConnector?.events;
if (!canvas?.canvas || !events)
return false;
if (canvas === layerForgeQuickCreateCanvas && canvas.__layerForgeQuickCreateInstalled)
return true;
layerForgeQuickCreateCleanup?.();
layerForgeQuickCreateCleanup = null;
layerForgeQuickCreateCanvas = canvas;
canvas.__layerForgeQuickCreateInstalled = true;
const handler = (event) => {
if (layerForgeQuickCreateMenu
|| event?.target?.closest?.('.litecontextmenu'))
return;
if (event?.button > 0 || performance.now() - layerForgeLastCapturedDropAt < 80)
return;
const pending = getLayerForgePendingConnectorLink(canvas);
if (!pending)
return;
const [x, y] = getLayerForgePointerGraphPosition(canvas, event);
if (pending.direction === 'from_output') {
const target = (canvas.graph?._nodes || []).find((node) => {
if (node?.comfyClass !== 'LayerForgeNode' && node?.type !== 'LayerForgeNode')
return false;
const inputSlot = getLayerForgeImageInputSlot(node);
const inputIndex = Math.max(0, node.inputs?.indexOf(inputSlot) ?? 0);
const dot = getLayerForgeConnectionPosition(node, true, inputIndex);
return dot && Math.hypot(x - dot[0], y - dot[1]) <= 18;
});
if (!target || !pending.sourceNode || target === pending.sourceNode)
return;
const added = addLayerForgeImageInputLink(target, {
source_id: Number(pending.sourceNode.id),
source_slot: Number(pending.sourceSlot) || 0,
source_type: pending.sourceType || 'IMAGE',
});
if (!added)
return;
layerForgeLastCapturedDropAt = performance.now();
event.preventDefault?.();
event.stopPropagation?.();
event.stopImmediatePropagation?.();
clearLayerForgeTemporaryConnector(canvas);
return;
}
if (scheduleLayerForgeQuickCreateMenu(canvas, event, pending)) {
layerForgeLastCapturedDropAt = performance.now();
closeLayerForgeNativeNodeSearchSoon();
event.preventDefault?.();
event.stopPropagation?.();
event.stopImmediatePropagation?.();
}
};
const pointerTargets = [
globalThis.window,
globalThis.document,
canvas.canvas,
];
for (const target of pointerTargets) {
target?.addEventListener?.('pointerup', handler, true);
target?.addEventListener?.('mouseup', handler, true);
}
const originalDispatch = typeof events.dispatch === 'function' ? events.dispatch : null;
const originalDispatchEvent = typeof events.dispatchEvent === 'function' ? events.dispatchEvent : null;
let wrappedDispatch = null;
let wrappedDispatchEvent = null;
if (originalDispatch) {
wrappedDispatch = function dispatchWithLayerForgeDropGuard(type, detail) {
if (type === 'before-drop-links')
primeLayerForgeInputDropSuppression(canvas);
if (shouldSuppressLayerForgeNativeDrop(type))
return false;
return originalDispatch.call(events, type, detail);
};
events.dispatch = wrappedDispatch;
}
if (originalDispatchEvent) {
wrappedDispatchEvent = function dispatchEventWithLayerForgeDropGuard(event) {
if (event?.type === 'before-drop-links')
primeLayerForgeInputDropSuppression(canvas);
if (shouldSuppressLayerForgeNativeDrop(event?.type)) {
event?.preventDefault?.();
event?.stopPropagation?.();
return false;
}
return originalDispatchEvent.call(events, event);
};
events.dispatchEvent = wrappedDispatchEvent;
}
const beforeDropLinksHandler = () => {
primeLayerForgeInputDropSuppression(canvas);
};
const droppedOnCanvasHandler = (event) => {
if (shouldSuppressLayerForgeNativeDrop(event?.type)) {
event?.preventDefault?.();
event?.stopPropagation?.();
event?.stopImmediatePropagation?.();
}
};
events.addEventListener?.('before-drop-links', beforeDropLinksHandler, { capture: true });
events.addEventListener?.('dropped-on-canvas', droppedOnCanvasHandler, { capture: true });
layerForgeQuickCreateCleanup = () => {
for (const target of pointerTargets) {
target?.removeEventListener?.('pointerup', handler, true);
target?.removeEventListener?.('mouseup', handler, true);
}
events.removeEventListener?.('before-drop-links', beforeDropLinksHandler, { capture: true });
events.removeEventListener?.('dropped-on-canvas', droppedOnCanvasHandler, { capture: true });
if (wrappedDispatch && events.dispatch === wrappedDispatch)
events.dispatch = originalDispatch;
if (wrappedDispatchEvent && events.dispatchEvent === wrappedDispatchEvent) {
events.dispatchEvent = originalDispatchEvent;
}
canvas.__layerForgeQuickCreateInstalled = false;
if (layerForgeQuickCreateCanvas === canvas)
layerForgeQuickCreateCanvas = null;
layerForgeQuickCreatePending = false;
};
return true;
};
export const installLayerForgeVirtualWirePatch = () => {
const canvas = app.canvas;
if (!canvas)
return;
installLayerForgeQuickCreateCapture(canvas);
if (canvas.__layerForgeVirtualWirePatched || typeof canvas.drawConnections !== 'function')
return;
const originalDrawConnections = canvas.drawConnections;
canvas.__layerForgeVirtualWirePatched = true;
canvas.drawConnections = function drawConnectionsWithLayerForgeLinks(context) {
const result = originalDrawConnections.apply(this, arguments);
const connectionContext = context || this.bgctx || this.ctx;
const onConnectionLayer = connectionContext?.canvas === this?.bgcanvas
|| connectionContext === this?.bgctx
|| !this?.bgcanvas;
if (connectionContext && onConnectionLayer) {
drawLayerForgeVirtualLinks(this, connectionContext);
}
return result;
};
const originalProcessMouseDown = canvas.processMouseDown;
canvas.processMouseDown = function processMouseDownWithLayerForgeLinks(event) {
if (!hasLayerForgePendingConnection(this)) {
const [x, y] = getLayerForgeGraphPosition(this, event);
const hit = hitTestLayerForgeVirtualLinks(this.graph || app.graph, x, y);
if (hit) {
openLayerForgeVirtualLinkMenu(this, hit, event);
event?.preventDefault?.();
event?.stopImmediatePropagation?.();
return true;
}
}
return originalProcessMouseDown?.apply(this, arguments);
};
const linkPointerHandler = (event) => {
if (hasLayerForgePendingConnection(canvas))
return;
const [x, y] = getLayerForgeGraphPosition(canvas, event);
const hit = hitTestLayerForgeVirtualLinks(canvas.graph || app.graph, x, y);
if (!hit)
return;
openLayerForgeVirtualLinkMenu(canvas, hit, event);
event.preventDefault?.();
event.stopPropagation?.();
event.stopImmediatePropagation?.();
};
canvas.canvas?.addEventListener?.('pointerdown', linkPointerHandler, true);
};
const convertLayerForgeImageConnection = (node, inputIndex, linkInfo = null) => {
if (!node || node.__layerForgeVirtualWireClearing)
return false;
const input = node.inputs?.[inputIndex];
if (!input || (String(input.name || '') !== 'input_image' && !isLayerForgeTransportInput(input.name))) {
return false;
}
const graph = node.graph || app.graph;
const linkId = input.link ?? linkInfo?.id ?? linkInfo?.link_id ?? linkInfo?.linkId;
const nativeLink = getLayerForgeGraphLink(node, linkId) || linkInfo;
if (!nativeLink)
return false;
const sourceId = Number(nativeLink.origin_id ?? nativeLink.originId ?? nativeLink.from_id ?? nativeLink.fromId);
const sourceSlot = Number(nativeLink.origin_slot ?? nativeLink.originSlot ?? nativeLink.from_slot ?? nativeLink.fromSlot ?? 0);
if (!Number.isFinite(sourceId) || !Number.isFinite(sourceSlot) || sourceId === Number(node.id))
return false;
const sourceNode = graph?.getNodeById?.(sourceId);
const sourceType = sourceNode?.outputs?.[sourceSlot]?.type
|| nativeLink.type
|| 'IMAGE';
addLayerForgeImageInputLink(node, {
source_id: sourceId,
source_slot: sourceSlot,
source_type: String(sourceType),
});
node.__layerForgeVirtualWireClearing = true;
try {
if (input.link != null && typeof node.disconnectInput === 'function') {
node.disconnectInput(inputIndex);
}
else if (linkId != null && typeof graph?.removeLink === 'function') {
graph.removeLink(linkId);
}
if (node.inputs?.[inputIndex])
node.inputs[inputIndex].link = null;
}
finally {
node.__layerForgeVirtualWireClearing = false;
}
node.setDirtyCanvas?.(true, true);
graph?.setDirtyCanvas?.(true, true);
app.graph?.change?.();
return true;
};
export const scheduleLayerForgeImageConnectionConversion = (node, inputIndex, linkInfo = null) => {
setTimeout(() => convertLayerForgeImageConnection(node, inputIndex, linkInfo), 0);
if (!linkInfo)
setTimeout(() => convertLayerForgeImageConnection(node, inputIndex), 50);
};
export const pruneLayerForgeTransportInputs = (node) => {
if (!Array.isArray(node?.inputs))
return;
for (let index = node.inputs.length - 1; index >= 0; index -= 1) {
const input = node.inputs[index];
if (!isLayerForgeTransportInput(input?.name))
continue;
if (input.link != null)
convertLayerForgeImageConnection(node, index);
if (input.link != null)
continue;
if (typeof node.removeInput === 'function')
node.removeInput(index);
else
node.inputs.splice(index, 1);
}
};
export const installLayerForgeMultiImagePromptPatch = () => {
const appWithPrompt = app;
const originalGraphToPrompt = appWithPrompt.graphToPrompt;
if (typeof originalGraphToPrompt !== 'function' || originalGraphToPrompt.__layerForgeMultiImagePatched)
return;
const graphToPrompt = async function (...args) {
const promptData = await originalGraphToPrompt.apply(this, args);
const output = promptData?.output;
if (!output)
return promptData;
for (const node of app.graph?._nodes || []) {
if (node?.comfyClass !== 'LayerForgeNode' && node?.type !== 'LayerForgeNode')
continue;
const promptNode = output[String(node.id)];
if (!promptNode)
continue;
promptNode.inputs || (promptNode.inputs = {});
for (let index = 1; index <= LAYERFORGE_MAX_IMAGE_INPUTS; index += 1) {
delete promptNode.inputs[`input_image_${index}`];
}
const links = getLayerForgeImageInputLinks(node)
.filter(link => output[String(link.source_id)]);
if (links.length === 0)
continue;
delete promptNode.inputs.input_image;
links.forEach((link, index) => {
promptNode.inputs[`input_image_${index + 1}`] = [String(link.source_id), link.source_slot];
});
}
return promptData;
};
graphToPrompt.__layerForgeMultiImagePatched = true;
appWithPrompt.graphToPrompt = graphToPrompt;
};