Full Code of niknah/quick-connections for AI

main b48c64ce4bdc cached
23 files
1.1 MB
295.6k tokens
418 symbols
1 requests
Download .txt
Showing preview only (1,191K chars total). Download the full file or copy to clipboard to get everything.
Repository: niknah/quick-connections
Branch: main
Commit: b48c64ce4bdc
Files: 23
Total size: 1.1 MB

Directory structure:
gitextract_7ufcrmuf/

├── .editorconfig
├── .github/
│   └── workflows/
│       └── publish.yml
├── .gitignore
├── README.md
├── __init__.py
├── docs/
│   ├── CircuitBoardLines.js
│   ├── QuickConnection.js
│   ├── links/
│   │   ├── litegraph.css
│   │   └── litegraph.js
│   ├── quick_conn.html
│   └── quick_conn.js
├── example/
│   ├── comfyui_quick_conn.html
│   ├── comfyui_quick_conn.js
│   ├── make_docs.sh
│   ├── quick_conn.html
│   ├── quick_conn.js
│   └── run_example.sh
├── imgs/
│   └── SpeedUpMp4ToGif.sh
├── js/
│   ├── CircuitBoardLines.js
│   ├── QuickConnection.js
│   └── quick_conn_start.js
├── package.json
└── pyproject.toml

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
[*]
indent_style = tab
indent_size = 2


================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish to Comfy registry
on:
  workflow_dispatch:
  push:
    branches:
      - main
      - master
    paths:
      - "pyproject.toml"

permissions:
  issues: write

jobs:
  publish-node:
    name: Publish Custom Node to registry
    runs-on: ubuntu-latest
    if: ${{ github.repository_owner == 'niknah' }}
    steps:
      - name: Check out code
        uses: actions/checkout@v4
      - name: Publish Custom Node
        uses: Comfy-Org/publish-node-action@v1
        with:
          ## Add your own personal access token to your Github Repository secrets and reference it here.
          personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}


================================================
FILE: .gitignore
================================================
# example/links is made by example/run_example.sh
example/links
node_modules
__pycache__


================================================
FILE: README.md
================================================

Adds circuit board connections, quick connections to ComfyUI

![Example](imgs/CircuitBoardExample.webp)
![Example](imgs/CreateSimple.gif)


* To install, go to [ComfyUI manager](https://github.com/ltdrdata/ComfyUI-Manager) -> look up "quick-connections"
* [Demo](https://niknah.github.io/quick-connections/quick_conn.html?nodebug=1)

## How to use...


* Don't block the output / input areas.
* Give it some room.  If the connections have no room to move, it'll get confused.
* There is no insert dot in the middle of the connection.  Drag the output to an empty spot, add a new node, and drag the output of the new node.  Sorry.
* Can be disabled in options under "Quick connections", "Circuit Board lines"


## Can be used with litegraph too.  See example folder(examples won't work in windows because it doesn't support symlinks, use WSL).
To run examples...
```
npm install
python -m http.server
# visit in browser: http://localhost:8000/example/quick_conn.html
```


## Changelog

* 2026-04-04 v1.0.29: Added adjustable numbers for line / node spacing
* 2026-03-30 v1.0.27: Catch errors and continue, so we don't stop any other things that are supposed to happen [#31]
* 2026-03-28 v1.0.26: Fix for show/hide links button not working [#31]
* 2026-03-10 v1.0.25: Missing round corners some times.  Funny line when two nodes are close together.
* 2025-08-29 v1.0.21: Changes to get subgraphs working.
* 2025-08-26 v1.0.20: Disable quick-connections in subgraphs, not working.
* 2025-08-23 v1.0.19: Possible crashes in subgraph node fix. https://github.com/niknah/quick-connections/issues/25
* 2025-07-15 v1.0.17: Draw connections in subgraph mode.  https://github.com/niknah/quick-connections/issues/24
* 2025-07-04 v1.0.16: Problem with the enable/disable toggle not working.  https://github.com/niknah/quick-connections/issues/19
* 2024-11-03: It defaults to mostly 90 or 45 degree lines now.  This can be changed in the options back to the old way(connect any angle when nothing is blocking).


================================================
FILE: __init__.py
================================================

# Set the web directory, any .js file in that directory will be loaded by the frontend as a frontend extension
WEB_DIRECTORY = "./js"

NODE_CLASS_MAPPINGS = {
}




================================================
FILE: docs/CircuitBoardLines.js
================================================
/* eslint max-classes-per-file: 0 */
/* eslint no-tabs: 0 */
/* eslint no-underscore-dangle:0 */
/* eslint import/prefer-default-export:0 */
/* eslint prefer-rest-params:0 */
/* eslint curly:0 */
/* eslint no-plusplus:0 */
/* global LiteGraph */
/* global LGraphCanvas */

/**
 * @preserve
 * Fast, destructive implemetation of Liang-Barsky line clipping algorithm.
 * It clips a 2D segment by a rectangle.
 * @author Alexander Milevski <info@w8r.name>
 * @license MIT
 */
const EPSILON = 1e-6;
const INSIDE = 1;
const OUTSIDE = 0;

function clipT(num, denom, c) {
	/* eslint-disable one-var,no-param-reassign,prefer-destructuring,operator-linebreak */
	/* eslint-disable one-var-declaration-per-line,nonblock-statement-body-position,curly */
	const tE = c[0], tL = c[1];
	if (Math.abs(denom) < EPSILON)
		return num < 0;
	const t = num / denom;
	if (denom > 0) {
		if (t > tL)
			return 0;
		if (t > tE)
			c[0] = t;
	} else {
		if (t < tE)
			return 0;
		if (t < tL)
			c[1] = t;
	}
	return 1;
}
/**
 * @param	{Point} a
 * @param	{Point} b
 * @param	{BoundingBox} box [xmin, ymin, xmax, ymax]
 * @param	{Point?} [da]
 * @param	{Point?} [db]
 * @return {number}
 */
function liangBarsky(a, b, box, da, db) {
	/* eslint-disable one-var,no-param-reassign,prefer-destructuring,operator-linebreak */
	/* eslint-disable one-var-declaration-per-line */
	const x1 = a[0], y1 = a[1];
	const x2 = b[0], y2 = b[1];
	const dx = x2 - x1;
	const dy = y2 - y1;
	if (da === undefined || db === undefined) {
		da = a;
		db = b;
	} else {
		da[0] = a[0];
		da[1] = a[1];
		db[0] = b[0];
		db[1] = b[1];
	}
	if (Math.abs(dx) < EPSILON &&
		Math.abs(dy) < EPSILON &&
		x1 >= box[0] &&
		x1 <= box[2] &&
		y1 >= box[1] &&
		y1 <= box[3]) {
		return INSIDE;
	}
	const c = [0, 1];
	if (clipT(box[0] - x1, dx, c) &&
		clipT(x1 - box[2], -dx, c) &&
		clipT(box[1] - y1, dy, c) &&
		clipT(y1 - box[3], -dy, c)) {
		const tE = c[0], tL = c[1];
		if (tL < 1) {
			db[0] = x1 + tL * dx;
			db[1] = y1 + tL * dy;
		}
		if (tE > 0) {
			da[0] += tE * dx;
			da[1] += tE * dy;
		}
		return INSIDE;
	}
	return OUTSIDE;
}

class MapLinks {
	constructor(canvas) {
		this.canvas = canvas;
		this.nodesByRight = [];
		this.nodesById = [];
		this.lastPathId = 10000000;
		this.paths = [];
		this.lineSpace = Math.floor(LiteGraph.NODE_SLOT_HEIGHT / 2);
		this.maxDirectLineDistance = Number.MAX_SAFE_INTEGER;
		this.debug = false;
	}

	isInsideNode(xy) {
		for (let i = 0; i < this.nodesByRight.length; ++i) {
			const nodeI = this.nodesByRight[i];
			if (nodeI.node.isPointInside(xy[0], xy[1])) {
				return nodeI.node;
			}
		}
		return null;
	}

	findClippedNode(outputXY, inputXY) {
		let closestDistance = Number.MAX_SAFE_INTEGER;
		let closest = null;

		for (let i = 0; i < this.nodesByRight.length; ++i) {
			const node = this.nodesByRight[i];
			const clipA = [-1, -1]; // outputXY.slice();
			const clipB = [-1, -1]; // inputXY.slice();
			const clipped = liangBarsky(
				outputXY,
				inputXY,
				node.area,
				clipA,
				clipB,
			);

			if (clipped === INSIDE) {
				const centerX = (node.area[0] + ((node.area[2] - node.area[0]) / 2));
				const centerY = (node.area[1] + ((node.area[3] - node.area[1]) / 2));
				const dist = Math.sqrt(((centerX - outputXY[0]) ** 2) + ((centerY - outputXY[1]) ** 2));
				if (dist < closestDistance) {
					closest = {
						start: clipA,
						end: clipB,
						node,
					};
					closestDistance = dist;
				}
			}
		}
		return { clipped: closest, closestDistance };
	}

	testPath(path) {
		const len1 = (path.length - 1);
		for (let p = 0; p < len1; ++p) {
			const { clipped } = this.findClippedNode(path[p], path[p + 1]);
			if (clipped) {
				return clipped;
			}
		}
		return null;
	}

	mapFinalLink(outputXY, inputXY) {
		const { clipped } = this.findClippedNode(outputXY, inputXY);
		if (!clipped) {
			const dist = Math.sqrt(((outputXY[0] - inputXY[0]) ** 2) + ((outputXY[1] - inputXY[1]) ** 2));
			if (dist < this.maxDirectLineDistance) {
				// direct, nothing blocking us
				return { path: [outputXY, inputXY] };
			}
		}

		const horzDistance = inputXY[0] - outputXY[0];
		const vertDistance = inputXY[1] - outputXY[1];
		const horzDistanceAbs = Math.abs(horzDistance);
		const vertDistanceAbs = Math.abs(vertDistance);

		if (horzDistanceAbs > vertDistanceAbs) {
			// we should never go left anyway,
			// because input slot is always on left and output is always on right
			const goingLeft = inputXY[0] < outputXY[0];
			const pathStraight45 = [
				[outputXY[0], outputXY[1]],
				[inputXY[0] - (goingLeft ? -vertDistanceAbs : vertDistanceAbs), outputXY[1]],
				[inputXY[0], inputXY[1]],
			];
			// __/
			//
			// __
			//   \
			if (!this.testPath(pathStraight45)) {
				return { path: pathStraight45 };
			}

			const path45Straight = [
				[outputXY[0], outputXY[1]],
				[outputXY[0] + (goingLeft ? -vertDistanceAbs : vertDistanceAbs), inputXY[1]],
				[inputXY[0], inputXY[1]],
			];
			// \__
			//
			//  __
			// /
			if (!this.testPath(path45Straight)) {
				return { path: path45Straight };
			}
		} else {
			// move vert
			const goingUp = inputXY[1] < outputXY[1];
			const pathStraight45 = [
				[outputXY[0], outputXY[1]],
				[outputXY[0], inputXY[1] + (goingUp ? horzDistanceAbs : -horzDistanceAbs)],
				[inputXY[0], inputXY[1]],
			];
			// |
			// |
			//  \
			if (!this.testPath(pathStraight45)) {
				return { path: pathStraight45 };
			}

			const path45Straight = [
				[outputXY[0], outputXY[1]],
				[inputXY[0], outputXY[1] - (goingUp ? horzDistanceAbs : -horzDistanceAbs)],
				[inputXY[0], inputXY[1]],
			];
			// \
			//  |
			//  |
			if (!this.testPath(path45Straight)) {
				return { path: path45Straight };
			}
		}

		const path90Straight = [
			[outputXY[0], outputXY[1]],
			[outputXY[0], inputXY[1]],
			[inputXY[0], inputXY[1]],
		];
		// |_
		const clippedVert = this.testPath(path90Straight);
		if (!clippedVert) {
			return { path: path90Straight };
		}

		const pathStraight90 = [
			[outputXY[0], outputXY[1]],
			[inputXY[0], outputXY[1]],
			[inputXY[0], inputXY[1]],
		];
		// _
		//  |
		//
		// _|
		const clippedHorz = this.testPath(pathStraight90);
		if (!clippedHorz) {
			// add to lines area in destination node?
			// targetNodeInfo.linesArea[0] -= this.lineSpace;
			return { path: pathStraight90 };
		}
		return {
			clippedHorz,
			clippedVert,
		};
	}

	mapLink(outputXY, inputXY, targetNodeInfo, isBlocked /* , lastDirection */) {
		const { clippedHorz, clippedVert, path } = this.mapFinalLink(outputXY, inputXY);
		if (path) {
			return path;
		}

		const horzDistance = inputXY[0] - outputXY[0];
		const vertDistance = inputXY[1] - outputXY[1];
		const horzDistanceAbs = Math.abs(horzDistance);
		const vertDistanceAbs = Math.abs(vertDistance);

		let blockedNodeId;
		// let blockedArea;
		let pathAvoidNode;
		let lastPathLocation;
		let linesArea;

		let thisDirection = null;
		// if (lastDirection !== 'horz' && horzDistanceAbs > vertDistanceAbs) {
		if (horzDistanceAbs > vertDistanceAbs) {
			// horz then vert to avoid blocking node
			blockedNodeId = clippedHorz.node.node.id;
			// blockedArea = clippedHorz.node.area;
			linesArea = clippedHorz.node.linesArea;
			const horzEdge = horzDistance <= 0
				? (linesArea[2])
				: (linesArea[0] - 1);
			pathAvoidNode = [
				[outputXY[0], outputXY[1]],
				[horzEdge, outputXY[1]],
			];

			if (horzDistance <= 0) {
				linesArea[2] += this.lineSpace;
			} else {
				linesArea[0] -= this.lineSpace;
			}

			const vertDistanceViaBlockTop =
				Math.abs(inputXY[1] - linesArea[1]) +
				Math.abs(linesArea[1] - outputXY[1]);
			const vertDistanceViaBlockBottom =
				Math.abs(inputXY[1] - linesArea[3]) +
				Math.abs(linesArea[3] - outputXY[1]);

			lastPathLocation = [
				horzEdge,
				vertDistanceViaBlockTop <= vertDistanceViaBlockBottom ?
					(linesArea[1])
					: (linesArea[3]),
			];
			const unblockNotPossible1 = this.testPath([...pathAvoidNode, lastPathLocation]);
			if (unblockNotPossible1) {
				lastPathLocation = [
					horzEdge,
					vertDistanceViaBlockTop > vertDistanceViaBlockBottom ?
						(linesArea[1])
						: (linesArea[3]),
				];
			}
			if (lastPathLocation[1] < outputXY[1]) {
				linesArea[1] -= this.lineSpace;
				lastPathLocation[1] -= 1;
			} else {
				linesArea[3] += this.lineSpace;
				lastPathLocation[1] += 1;
			}
			thisDirection = 'vert';
		// } else if (lastDirection !== 'vert') {
		} else {
			// vert then horz to avoid blocking node
			blockedNodeId = clippedVert.node.node.id;
			// blockedArea = clippedVert.node.area;
			linesArea = clippedVert.node.linesArea;
			// Special +/- 1 here because of the way it's calculated
			const vertEdge =
				vertDistance <= 0
					? (linesArea[3] + 1)
					: (linesArea[1] - 1);
			pathAvoidNode = [
				[outputXY[0], outputXY[1]],
				[outputXY[0], vertEdge],
			];
			if (vertDistance <= 0) {
				linesArea[3] += this.lineSpace;
			} else {
				linesArea[1] -= this.lineSpace;
			}

			const horzDistanceViaBlockLeft =
				Math.abs(inputXY[0] - linesArea[0]) +
				Math.abs(linesArea[0] - outputXY[0]);
			const horzDistanceViaBlockRight =
				Math.abs(inputXY[0] - linesArea[2]) +
				Math.abs(linesArea[2] - outputXY[0]);

			lastPathLocation = [
				horzDistanceViaBlockLeft <= horzDistanceViaBlockRight ?
					(linesArea[0] - 1)
					: (linesArea[2]),
				vertEdge,
			];
			const unblockNotPossible1 = this.testPath([...pathAvoidNode, lastPathLocation]);
			if (unblockNotPossible1) {
				lastPathLocation = [
					horzDistanceViaBlockLeft > horzDistanceViaBlockRight ?
						(linesArea[0])
						: (linesArea[2]),
					vertEdge,
				];
			}
			if (lastPathLocation[0] < outputXY[0]) {
				linesArea[0] -= this.lineSpace;
				// lastPathLocation[0] -= 1; //this.lineSpace;
			} else {
				linesArea[2] += this.lineSpace;
				// lastPathLocation[0] += 1; //this.lineSpace;
			}
			thisDirection = 'horz';
			//		} else {
			//			console.log('blocked will not go backwards', outputXY, inputXY);
			//			return [outputXY, inputXY];
		}

		// console.log('is blocked check',isBlocked, blockedNodeId);
		if (isBlocked[blockedNodeId] > 3) {
			// Blocked too many times, let's return the direct path
			console.log('Too many blocked', outputXY, inputXY); // eslint-disable-line no-console
			return [outputXY, inputXY];
		}
		if (isBlocked[blockedNodeId])
			++isBlocked[blockedNodeId];
		else
			isBlocked[blockedNodeId] = 1;
		// console.log('pathavoid', pathAvoidNode);
		const nextPath = this.mapLink(
			lastPathLocation,
			inputXY,
			targetNodeInfo,
			isBlocked,
			thisDirection,
		);
		return [...pathAvoidNode, lastPathLocation, ...nextPath.slice(1)];
	}

	expandSourceNodeLinesArea(sourceNodeInfo, path) {
		if (path.length < 3) {
			return false;
		}

		const linesArea = sourceNodeInfo.linesArea;
		if (path[1][0] === path[2][0]) {
			// first link is going vertical
			// while (path[1][0] > linesArea[2])
			linesArea[2] += this.lineSpace;
		}
		return true;
	}

	// expand left side of target node if we're going up there vertically.
	expandTargetNodeLinesArea(targetNodeInfo, path) {
		if (path.length < 2) {
			return false;
		}

		const linesArea = targetNodeInfo.linesArea;
		const path2Len = path.length - 2;
		if (path[path2Len - 1][0] === path[path2Len][0]) {
			// first link is going vertical
			// while (path[path2Len][0] < linesArea[0])
			linesArea[0] -= this.lineSpace;
		}
		return true;
	}

	getNodeOnPos(xy) {
		for (let i = 0; i < this.nodesByRight.length; ++i) {
			const nodeI = this.nodesByRight[i];
			const { linesArea } = nodeI;
			if (xy[0] >= linesArea[0]
				&& xy[1] >= linesArea[1]
				&& xy[0] < linesArea[2]
				&& xy[1] < linesArea[3]
			) {
				return nodeI;
			}
		}
		return null;
	}

	mapLinks(nodesByExecution) {
		if (!this.canvas.graph.links) {
			console.error('Missing graph.links', this.canvas.graph); // eslint-disable-line no-console
			return;
		}

		const startCalcTime = new Date().getTime();
		this.links = [];
		this.lastPathId = 1000000;
		this.nodesByRight = [];
		this.nodesById = {};
		this.nodesByRight = nodesByExecution.map((node) => {
			const barea = new Float32Array(4);
			node.getBounding(barea);
			const area = [
				barea[0],
				barea[1],
				barea[0] + barea[2],
				barea[1] + barea[3],
			];
			const linesArea = Array.from(area);
			linesArea[0] -= 5;
			linesArea[1] -= 1;
			linesArea[2] += 3;
			linesArea[3] += 3;
			const obj = {
				node,
				area,
				linesArea,
			};
			this.nodesById[node.id] = obj;
			return obj;
		});
		//
		//		this.nodesByRight.sort(
		//			(a, b) => (a.area[1]) - (b.area[1]),
		//		);

		this.nodesByRight.filter((nodeI) => {
			const { node } = nodeI;
			if (!node.outputs) {
				return false;
			}
			node.outputs.filter((output, slot) => {
				if (!output.links) {
					return false;
				}

				const linkPos = new Float32Array(2);
				const outputXYConnection = node.getConnectionPos(false, slot, linkPos);
				const outputNodeInfo = this.nodesById[node.id];
				let outputXY = Array.from(outputXYConnection);
				output.links.filter((linkId) => {
					outputXY[0] = outputNodeInfo.linesArea[2];
					const link = this.canvas.graph.links[linkId];
					if (!link) {
						return false;
					}
					const targetNode = this.canvas.graph.getNodeById(link.target_id);
					if (!targetNode) {
						return false;
					}

					const inputLinkPos = new Float32Array(2);
					const inputXYConnection = targetNode.getConnectionPos(
						true,
						link.target_slot,
						inputLinkPos,
					);
					const inputXY = Array.from(inputXYConnection);
					const nodeInfo = this.nodesById[targetNode.id];
					inputXY[0] = nodeInfo.linesArea[0] - 1;

					const inputBlockedByNode =
						this.getNodeOnPos(inputXY);
					const outputBlockedByNode =
						this.getNodeOnPos(outputXY);

					let path = null;
					// console.log('blocked', inputBlockedByNode, outputBlockedByNode,
					//	'inputXY', inputXY, 'outputXY', outputXY);
					if (!inputBlockedByNode && !outputBlockedByNode) {
						const pathFound = this.mapLink(outputXY, inputXY, nodeInfo, {}, null);
						if (pathFound && pathFound.length > 2) {
							// mapLink() may have expanded the linesArea,
							// lets put it back into the inputXY so the line is straight
							path = [outputXYConnection, ...pathFound, inputXYConnection];
							this.expandTargetNodeLinesArea(nodeInfo, path);
						}
					}
					if (!path) {
						path = [outputXYConnection, outputXY, inputXY, inputXYConnection];
					}
					this.expandSourceNodeLinesArea(nodeI, path);
					this.paths.push({
						path,
						node,
						targetNode,
						slot,
					});
					outputXY = [
						outputXY[0] + this.lineSpace,
						outputXY[1],
					];
					return false;
				});
				return false;
			});
			return false;
		});
		this.lastCalculate = new Date().getTime();
		this.lastCalcTime = this.lastCalculate - startCalcTime;

		if (this.debug)
			console.log('last calc time', this.lastCalcTime); // eslint-disable-line no-console
		// console.log('nodesbyright', this.nodesByRight);
		// Uncomment this to test timeout on draws
		// this.lastCalcTime = 250;
	}

	drawLinks(ctx) {
		if (!this.canvas.default_connection_color_byType || !this.canvas.default_connection_color) {
			console.error('Missing canvas.default_connection_color_byType', this.canvas); // eslint-disable-line no-console
			return;
		}
		if (this.debug)
			console.log('paths', this.paths); // eslint-disable-line no-console

		ctx.save();
		const currentNodeIds = this.canvas.selected_nodes || {};
		const corners = [];
		this.paths.filter((pathI) => {
			const path = pathI.path;
			const connection = pathI.node.outputs[pathI.slot];
			if (path.length <= 1) {
				return false;
			}
			ctx.beginPath();
			const slotColor =
				this.canvas.default_connection_color_byType[connection.type]
				|| this.canvas.default_connection_color.input_on;

			if (currentNodeIds[pathI.node.id] || currentNodeIds[pathI.targetNode.id]) {
				ctx.strokeStyle = 'white';
			} else {
				ctx.strokeStyle = slotColor;
			}
			ctx.lineWidth = 3;
			const cornerRadius = this.lineSpace;

			let isPrevDotRound = false;
			for (let p = 0; p < path.length; ++p) {
				const pos = path[p];

				if (p === 0) {
					ctx.moveTo(pos[0], pos[1]);
				}
				const prevPos = pos;
				const cornerPos = path[p + 1];
				const nextPos = path[p + 2];

				let drawn = false;
				if (nextPos) {
					const xDiffBefore = cornerPos[0] - prevPos[0];
					const yDiffBefore = cornerPos[1] - prevPos[1];
					const xDiffAfter = nextPos[0] - cornerPos[0];
					const yDiffAfter = nextPos[1] - cornerPos[1];
					const isBeforeStraight = xDiffBefore === 0 || yDiffBefore === 0;
					const isAfterStraight = xDiffAfter === 0 || yDiffAfter === 0;
					// up/down -> left/right
					if (
						(isBeforeStraight || isAfterStraight)
					) {
						const beforePos = [
							cornerPos[0],
							cornerPos[1],
						];
						const afterPos = [
							cornerPos[0],
							cornerPos[1],
						];

						if (isBeforeStraight) {
							const xSignBefore = Math.sign(xDiffBefore);
							const ySignBefore = Math.sign(yDiffBefore);
							beforePos[0] = cornerPos[0] - cornerRadius * xSignBefore;
							beforePos[1] = cornerPos[1] - cornerRadius * ySignBefore;
						}
						if (isAfterStraight) {
							const xSignAfter = Math.sign(xDiffAfter);
							const ySignAfter = Math.sign(yDiffAfter);
							afterPos[0] = cornerPos[0] + cornerRadius * xSignAfter;
							afterPos[1] = cornerPos[1] + cornerRadius * ySignAfter;
						}

						if (isPrevDotRound
							&& Math.abs(isPrevDotRound[0] - beforePos[0]) <= cornerRadius
							&& Math.abs(isPrevDotRound[1] - beforePos[1]) <= cornerRadius
						) {
							// if two rounded corners are too close, draw a straight line so it doesn't look funny
							ctx.lineTo(cornerPos[0], cornerPos[1]);
							// ctx.lineTo(beforePos[0], beforePos[1]);
							// ctx.lineTo(afterPos[0], afterPos[1]);
						} else {
							ctx.lineTo(beforePos[0], beforePos[1]);
							corners.push(cornerPos);
							ctx.quadraticCurveTo(cornerPos[0], cornerPos[1], afterPos[0], afterPos[1]);
						}
						isPrevDotRound = beforePos;
						drawn = true;
					}
				}
				if (p > 0 && !drawn) {
					if (!isPrevDotRound) {
						ctx.lineTo(pos[0], pos[1]);
					}
					isPrevDotRound = false;
				}
			}

			ctx.stroke();
			ctx.closePath();
			return false;
		});

		if (this.debug) {
			corners.filter((corn) => {
				ctx.strokeStyle = '#ff00ff';
				ctx.beginPath();
				ctx.arc(corn[0], corn[1], 1, 0, 2 * Math.PI);
				ctx.stroke();
				return false;
			});

			this.nodesByRight.filter((nodeI) => {
				ctx.lineWidth = 1;
				ctx.strokeStyle = '#000080';
				ctx.beginPath();
				ctx.rect(
					nodeI.area[0],
					nodeI.area[1],
					nodeI.area[2] - nodeI.area[0],
					nodeI.area[3] - nodeI.area[1],
				);
				ctx.stroke();
				ctx.closePath();

				ctx.strokeStyle = '#0000a0';
				ctx.beginPath();
				ctx.rect(
					nodeI.linesArea[0],
					nodeI.linesArea[1],
					nodeI.linesArea[2] - nodeI.linesArea[0],
					nodeI.linesArea[3] - nodeI.linesArea[1],
				);
				ctx.stroke();
				ctx.closePath();
				return false;
			});
		}

		ctx.restore();
	}
}

class EyeButton {
	constructor() {
		this.hidden = null;
	}

	static getEyeButton() {
		const eyeButtons = document.querySelectorAll('.pi-eye,.pi-eye-slash');
		if (eyeButtons.length > 1) {
			console.log('found too many eye buttons', eyeButtons); // eslint-disable-line no-console
		}
		return eyeButtons[0];
	}

	check() {
		const eyeButton = EyeButton.getEyeButton();
		if (!eyeButton) {
			return;
		}
		const hidden = eyeButton.classList.contains('pi-eye-slash');
		if (this.hidden !== hidden) {
			this.hidden = hidden;
			if (this.onChange) {
				this.onChange(hidden);
			}
		}
	}

	listenEyeButton(onChange) {
		this.onChange = onChange;
		const eyeButton = EyeButton.getEyeButton();
		if (!eyeButton) {
			setTimeout(() => this.listenEyeButton(onChange), 1000);
			return;
		}
		const eyeDom = eyeButton.parentNode;
		eyeDom.addEventListener('click', () => this.check());
		eyeDom.addEventListener('keyup', () => this.check());
		eyeDom.addEventListener('mouseup', () => this.check());
	}
}

export class CircuitBoardLines {
	constructor() {
		this.canvas = null;
		this.mapLinks = null;
		this.enabled = true;
		this.eyeHidden = false;
		this.maxDirectLineDistance = Number.MAX_SAFE_INTEGER;
	}

	setEnabled(e) { this.enabled = e; }

	isShow() { return this.enabled && !this.eyeHidden; }

	recalcMapLinksTimeout() {
		// calculate paths when user is idle...
		if (!this.skipNextRecalcTimeout) {
			if (this.recalcTimeout) {
				clearTimeout(this.recalcTimeout);
				this.recalcTimeout = null;
			}

			this.recalcTimeout = setTimeout(() => {
				this.recalcTimeout = null;
				this.recalcMapLinks();
				this.redraw();
			}, this.mapLinks.lastCalcTime * 2);
		}
		this.skipNextRecalcTimeout = false;
	}

	redraw() {
		if (this.lastDrawTimeout) {
			clearTimeout(this.lastDrawTimeout);
			this.lastDrawTimeout = null;
		}

		this.lastDrawTimeout = setTimeout(() => {
			this.lastDrawTimeout = null;
			window.requestAnimationFrame(() => {
				console.log('redraw timeout'); // eslint-disable-line no-console
				this.canvas.setDirty(true, true);
				this.skipNextRecalcTimeout = true;
				this.canvas.draw(true, true);
			});
		}, 0);
	}

	recalcMapLinksCheck() {
		if (this.mapLinks) {
			if (this.mapLinks.lastCalcTime > 100) {
				this.recalcMapLinksTimeout();
				return false;
			}
		}
		this.recalcMapLinks();
		return true;
	}

	recalcMapLinks() {
		this.mapLinks = new MapLinks(this.canvas);
		this.mapLinks.maxDirectLineDistance = this.maxDirectLineDistance;
		this.mapLinks.debug = this.debug;
		const nodesByExecution = this.canvas.graph.computeExecutionOrder() || [];
		this.mapLinks.mapLinks(nodesByExecution);
	}

	drawConnections(
		ctx,
	) {
		if (!this.canvas || !this.canvas.graph) {
			return false;
		}

		try {
			this.recalcMapLinksCheck();

			this.mapLinks.drawLinks(ctx);

			if (this.canvas.subgraph) {
				this.drawSubgraphConnections(ctx, this.canvas.graph, this.canvas.subgraph);
			}
		} catch(e) {
			console.error('drawConnections crash', e);
		} finally {
			this.lastDrawConnections = new Date().getTime();
		}

		return true;
	}

	drawSubgraphConnections(
		ctx,
		graph,
		subgraph,
	) {
		for (const output of subgraph.inputNode.slots) { // eslint-disable-line no-restricted-syntax
			if (!output.linkIds.length) {
				continue;
			}

			// find link info
			for (const linkId of output.linkIds) { // eslint-disable-line no-restricted-syntax
				const resolved = LiteGraph.LLink.resolve(linkId, graph);
				if (!resolved) continue;

				const { link, inputNode, input } = resolved;
				if (!inputNode || !input)
					continue;

				const endPos = inputNode.getInputPos(link.target_slot);

				const startDir = input.dir || LiteGraph.RIGHT;
				const endDir = input.dir || LiteGraph.LEFT;

				this.canvas.renderLink(
					ctx,
					output.pos,
					endPos,
					link,
					false,
					0,
					null,
					startDir,
					endDir,
				);
			}
		}

		for (const input of subgraph.outputNode.slots) { // eslint-disable-line no-restricted-syntax
			if (!input.linkIds.length) continue;

			// find link info
			const resolved = LiteGraph.LLink.resolve(input.linkIds[0], graph);
			if (!resolved) continue;

			const { link, outputNode, output } = resolved;
			if (!outputNode || !output) continue;

			const startPos = outputNode.getOutputPos(link.origin_slot);

			const startDir = output.dir || LiteGraph.RIGHT;
			const endDir = input.dir || LiteGraph.LEFT;

			this.canvas.renderLink(
				ctx,
				startPos,
				input.pos,
				link,
				false,
				0,
				null,
				startDir,
				endDir,
			);
		}
	}

	init() {
		const oldDrawConnections = LGraphCanvas.prototype.drawConnections;
		const t = this;
		LGraphCanvas.prototype.drawConnections = function drawConnections(
			ctx,
		) {
			if (t.canvas && t.isShow()) {
				return t.drawConnections(
					ctx,
				);
			}
			return oldDrawConnections.apply(this, arguments);
		};
		this.eyeButton = new EyeButton();
		this.eyeButton.listenEyeButton((hidden) => {
			this.eyeHidden = hidden;
		});
	}

	initOverrides(canvas) {
		this.canvas = canvas;
	}
}


================================================
FILE: docs/QuickConnection.js
================================================
/* eslint no-tabs: 0 */
/* eslint import/prefer-default-export:0 */
/* eslint no-underscore-dangle:0 */
/* eslint no-plusplus:0 */
/* eslint prefer-rest-params:0 */
/* eslint operator-linebreak:0 */
/* eslint no-unneeded-ternary:0 */
/* global LGraphCanvas */
/* global LiteGraph */

export class QuickConnection {
	constructor() {
		this.insideConnection = null;
		this.enabled = false;
		// use inputs that already have a link to them.
		this.useInputsWithLinks = false;
		this.release_link_on_empty_shows_menu = true;
		this.connectDotOnly = true;
		this.doNotAcceptType = /^\*$/;
		this.boxAlpha = 0.7;
		this.boxBackground = '#000';
	}

	init() {
		const origProcessMouseDown = LGraphCanvas.prototype.processMouseDown;
		const t = this;
		this.acceptingNodes = null;
		LGraphCanvas.prototype.processMouseDown = function mouseDown() {
			t.pointerDown();
			const ret = origProcessMouseDown.apply(this, arguments);
			return ret;
		};

		const origProcessMouseUp = LGraphCanvas.prototype.processMouseUp;
		LGraphCanvas.prototype.processMouseUp = function mouseUp() {
			if (!t.enabled) {
				return origProcessMouseUp.apply(this, arguments);
			}

			// Let's not popup the release on empty spot menu if we've released the mouse on a dot
			const origReleaseLink = LiteGraph.release_link_on_empty_shows_menu;
			const origShowConnectionMenu = t.canvas.showConnectionMenu;

			let ret = null;
			try {
				if (t.pointerUp()) {
					if (!t.isComfyUI) {
						LiteGraph.release_link_on_empty_shows_menu = false;
					} else {
						t.canvas.showConnectionMenu = () => {};
					}
					t.release_link_on_empty_shows_menu = false;
				}
				ret = origProcessMouseUp.apply(this, arguments);
			} finally {
				if (!t.release_link_on_empty_shows_menu) {
					if (!t.isComfyUI) {
						LiteGraph.release_link_on_empty_shows_menu = origReleaseLink;
					} else {
						t.canvas.showConnectionMenu = origShowConnectionMenu;
						t.canvas.linkConnector.reset();
					}
					t.release_link_on_empty_shows_menu = true;
				}
			}
			return ret;
		};

		// ComfyUI has it's own version of litegraph.js
		// https://github.com/Comfy-Org/litegraph.js
	}

	initListeners(canvas) {
		this.enabled = true;
		this.graph = canvas.graph;
		this.canvas = canvas;
		if (!this.canvas.canvas) {
			console.error('no canvas', this.canvas); // eslint-disable-line no-console
		} else {
			this.canvas.canvas.addEventListener('litegraph:canvas', (e) => {
				const { detail } = e;
				if (!this.release_link_on_empty_shows_menu
					&& detail && detail.subType === 'empty-release'
				) {
					e.stopPropagation();
				}
			});
		}

		this.isComfyUI = this.canvas.connecting_links !== undefined ? true : false;

		this.addOnCanvas('onDrawOverlay', (ctx) => this.onDrawOverlay(ctx));
	}

	getCurrentConnection() {
		if (this.isComfyUI) {
			const connectingLink =
				(this.canvas.connecting_links
					&& this.canvas.connecting_links.length > 0
				) ?
					this.canvas.connecting_links[0] : null;
			if (connectingLink) {
				return {
					node: connectingLink.node,
					slot: connectingLink.slot,
					input: connectingLink.input,
					output: connectingLink.output,
				};
			}
		} else if (this.canvas.connecting_node) {
			return {
				node: this.canvas.connecting_node,
				input: this.canvas.connecting_input,
				slot: this.canvas.connecting_slot,
				output: this.canvas.connecting_output,
			};
		}
		return null;
	}

	pointerDown() {
		this.acceptingNodes = null;
		return false;
	}

	pointerUp() {
		this.acceptingNodes = null;
		const connectionInfo = this.getCurrentConnection();

		if (this.insideConnection && connectionInfo) {
			if (connectionInfo.input) {
				this.insideConnection.node.connect(
					this.insideConnection.connection_slot_index,
					connectionInfo.node,
					connectionInfo.slot,
				);
			} else {
				connectionInfo.node.connect(
					connectionInfo.slot,
					this.insideConnection.node,
					this.insideConnection.connection_slot_index,
				);
			}
			return true;
		}
		return false;
	}

	findAcceptingNodes(fromConnection, fromNode, findInput) {
		const accepting = [];
		if (this.doNotAcceptType.exec(fromConnection.type)) {
			// Too many connections are available if we area a * connection
			return accepting;
		}
		const addToAccepting = (arr, node) => {
			// eslint-disable-next-line eqeqeq
			if (node.mode == 4) {
				// bypassed
				return;
			}
			if (node.id === fromNode.id) {
				// Don't connect to myself
				return;
			}
			for (let c = 0; c < arr.length; ++c) {
				const input = arr[c];
				if (!input.link || this.useInputsWithLinks) {
					const accept = LiteGraph.isValidConnection(
						input.type,
						fromConnection.type,
					);
					if (accept && !this.doNotAcceptType.exec(input.type)) {
						accepting.push({
							node,
							connection: input,
							connection_slot_index: c,
						});
					}
				}
			}
		};

		const nodes = this.graph._nodes;
		for (let i = 0; i < nodes.length; ++i) {
			const node = nodes[i];
			if (node.inputs && findInput) {
				addToAccepting(node.inputs, node);
			}
			if (node.outputs && !findInput) {
				addToAccepting(node.outputs, node);
			}
		}

		accepting.sort((a, b) => a.node.pos[1] - b.node.pos[1]);
		return accepting;
	}

	addOnCanvas(name, func) {
		const obj = this.canvas;
		const oldFunc = obj[name];
		obj[name] = function callFunc() {
			if (oldFunc) {
				oldFunc.apply(obj, arguments);
			}
			return func.apply(obj, arguments);
		};
	}

	onDrawOverlay(ctx) {
		if (!this.enabled) {
			return;
		}
		if (!this.canvas || !this.canvas.graph_mouse) {
			console.error('no canvas or mouse yet', this.canvas); // eslint-disable-line no-console
			return;
		}

		this.insideConnection = null;

		const connectionInfo = this.getCurrentConnection();

		if (connectionInfo) {
			const {
				node, input, output, slot,
			} = connectionInfo;
			if (!input && !output) {
				return;
			}

			ctx.save();
			this.canvas.ds.toCanvasContext(ctx);

			const slotPos = new Float32Array(2);

			const isInput = input ? true : false;
			const connecting = isInput ? input : output;
			const connectionSlot = slot;

			const pos = node.getConnectionPos(isInput, connectionSlot, slotPos);

			if (!this.acceptingNodes) {
				this.acceptingNodes = this.findAcceptingNodes(
					connecting,
					// this.canvas.connecting_node,
					node,
					!isInput,
				);
			}

			const mouseX = this.canvas.graph_mouse[0];
			const mouseY = this.canvas.graph_mouse[1];

			// const hasNodeTooltip = document.querySelector('.node-tooltip');

			const buttonShift = [
				isInput ? -32 : +32,
				// force for now so the dots don't move around when the tooltip pops up.
				// No need to avoid tool tip if we're using the input,
				//	tool tip is visible to the right of dot
				isInput ? 0 : LiteGraph.NODE_SLOT_HEIGHT,
				/*
				(true || this.acceptingNodes.length === 1 || hasNodeTooltip)
					? 0
					: (
						((-this.acceptingNodes.length * LiteGraph.NODE_SLOT_HEIGHT) / 2)
						+ (LiteGraph.NODE_SLOT_HEIGHT / 2)
					),
					*/
			];
			const linkPos = [
				pos[0] + buttonShift[0],
				pos[1] + buttonShift[1],
			];

			let scale = 1 / this.canvas.ds.scale;
			if (scale < 1.0) {
				scale = 1.0;
			}

			const linkCloseArea = [
				linkPos[0] - (LiteGraph.NODE_SLOT_HEIGHT * 6 * scale),
				linkPos[1] - LiteGraph.NODE_SLOT_HEIGHT,
				LiteGraph.NODE_SLOT_HEIGHT * 8 * scale,
				LiteGraph.NODE_SLOT_HEIGHT * (this.acceptingNodes.length + 1) * scale,
			];
			if (!isInput) {
				linkCloseArea[0] = linkPos[0] - ((LiteGraph.NODE_SLOT_HEIGHT * 2) * scale);
			}

			const isInsideClosePosition = LiteGraph.isInsideRectangle(
				mouseX,
				mouseY,
				linkCloseArea[0],
				linkCloseArea[1],
				linkCloseArea[2],
				linkCloseArea[3],
			);
			let boxRect = null;
			const textsToDraw = [];

			// const oldFillStyle = ctx.fillStyle;
			if (isInsideClosePosition) {
				const oldFont = ctx.font;
				let font = oldFont;
				const fontM = /([0-9]+)px/.exec(font);
				if (!fontM) {
					fontM[1] = 'px';
					font += ' 12px';
				}
				if (fontM) {
					const fontSize = parseInt(fontM[1], 10) * scale;
					ctx.font = font.replace(/[0-9]+px/, `${fontSize}px`);
				}
				this.acceptingNodes.filter((acceptingNode) => {
					const textxy = [
						linkPos[0] + (isInput ? -LiteGraph.NODE_SLOT_HEIGHT : LiteGraph.NODE_SLOT_HEIGHT),
						linkPos[1],
					];

					const acceptingText = `${acceptingNode.connection.name} @${acceptingNode.node.title}`;
					const textBox = ctx.measureText(acceptingText);
					const box = [
						textxy[0],
						textxy[1] - textBox.fontBoundingBoxAscent,
						textBox.width,
						LiteGraph.NODE_SLOT_HEIGHT,
						// (textBox.fontBoundingBoxAscent + textBox.fontBoundingBoxDescent),
					];

					let textAlign;
					if (!isInput) {
						textAlign = 'left';
					} else {
						box[0] -= textBox.width;
						textAlign = 'right';
					}

					const rRect = [
						box[0] - 8 * scale,
						box[1] - 4 * scale,
						box[2] + 16 * scale,
						box[3], // + 5 * scale,
					];
					if (!boxRect) {
						boxRect = rRect.slice(0);
					} else {
						if (boxRect[0] > rRect[0]) {
							// eslint-disable-next-line prefer-destructuring
							boxRect[0] = rRect[0];
						}
						if (boxRect[2] < rRect[2]) {
							// eslint-disable-next-line prefer-destructuring
							boxRect[2] = rRect[2];
						}
						boxRect[3] += rRect[3];
					}

					textsToDraw.push({
						x: textxy[0],
						y: textxy[1],
						acceptingText,
						textAlign,
					});

					let isInsideRect;
					if (this.connectDotOnly) {
						isInsideRect = LiteGraph.isInsideRectangle(
							mouseX,
							mouseY,
							linkPos[0] - ((LiteGraph.NODE_SLOT_HEIGHT / 2) * scale),
							linkPos[1] - ((LiteGraph.NODE_SLOT_HEIGHT / 2) * scale),
							LiteGraph.NODE_SLOT_HEIGHT * scale,
							LiteGraph.NODE_SLOT_HEIGHT * scale,
						);
					} else {
						isInsideRect = LiteGraph.isInsideRectangle(
							mouseX,
							mouseY,
							isInput ? box[0] : (linkPos[0] - (LiteGraph.NODE_SLOT_HEIGHT / 2)),
							linkPos[1] - 10,
							isInput ?
								((linkPos[0] - box[0]) + LiteGraph.NODE_SLOT_HEIGHT / 2)
								: (rRect[2] + LiteGraph.NODE_SLOT_HEIGHT / 2),
							rRect[3],
						);
					}

					if (isInsideRect && !this.insideConnection) {
						this.insideConnection = acceptingNode;
						ctx.fillStyle = LiteGraph.EVENT_LINK_COLOR; // "#ffcc00";
						// highlight destination if mouseover
						ctx.beginPath();
						ctx.arc(
							linkPos[0],
							linkPos[1],
							6 * scale,
							0,
							Math.PI * 2,
						);
						ctx.fill();
						ctx.closePath();

						ctx.beginPath();

						ctx.strokeStyle = '#6a6';
						ctx.setLineDash([5, 10]);
						ctx.lineWidth = 3;

						const aNode = acceptingNode.node;
						const destPos = new Float32Array(2);
						aNode.getConnectionPos(!isInput, acceptingNode.connection_slot_index, destPos);
						ctx.moveTo(pos[0], pos[1]);

						ctx.lineTo(destPos[0], destPos[1]);
						ctx.stroke();
						ctx.closePath();
					} else {
						const slotColor =
							this.canvas.default_connection_color_byType[acceptingNode.connection.type]
							|| this.canvas.default_connection_color.input_on;

						ctx.fillStyle = slotColor || this.canvas.default_connection_color.input_on;
						ctx.beginPath();

						ctx.arc(linkPos[0], linkPos[1], 4 * scale, 0, Math.PI * 2);
						ctx.fill();
						ctx.closePath();
					}

					linkPos[1] += LiteGraph.NODE_SLOT_HEIGHT * scale;
					return false;
				});

				if (boxRect) {
					ctx.beginPath();
					ctx.fillStyle = this.boxBackground;
					const oldAlpha = ctx.globalAlpha;
					ctx.globalAlpha = this.boxAlpha;
					ctx.roundRect(
						boxRect[0],
						boxRect[1],
						boxRect[2],
						boxRect[3],
						5,
					);
					ctx.fill();
					ctx.closePath();
					ctx.globalAlpha = oldAlpha;
				}

				ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
				textsToDraw.filter((textToDraw) => {
					ctx.textAlign = textToDraw.textAlign;
					ctx.fillText(textToDraw.acceptingText, textToDraw.x, textToDraw.y);
					return true;
				});

				ctx.font = oldFont;
			}
			ctx.restore();
		}
	}
}


================================================
FILE: docs/links/litegraph.css
================================================
/* this CSS contains only the basic CSS needed to run the app and use it */

.lgraphcanvas {
    /*cursor: crosshair;*/
    user-select: none;
    -moz-user-select: none;
    -webkit-user-select: none;
	outline: none;
    font-family: Tahoma, sans-serif;
}

.lgraphcanvas * {
    box-sizing: border-box;
}

.litegraph.litecontextmenu {
    font-family: Tahoma, sans-serif;
    position: fixed;
    top: 100px;
    left: 100px;
    min-width: 100px;
    color: #aaf;
    padding: 0;
    box-shadow: 0 0 10px black !important;
    background-color: #2e2e2e !important;
	z-index: 10;
}

.litegraph.litecontextmenu.dark {
    background-color: #000 !important;
}

.litegraph.litecontextmenu .litemenu-title img {
    margin-top: 2px;
    margin-left: 2px;
    margin-right: 4px;
}

.litegraph.litecontextmenu .litemenu-entry {
    margin: 2px;
    padding: 2px;
}

.litegraph.litecontextmenu .litemenu-entry.submenu {
    background-color: #2e2e2e !important;
}

.litegraph.litecontextmenu.dark .litemenu-entry.submenu {
    background-color: #000 !important;
}

.litegraph .litemenubar ul {
    font-family: Tahoma, sans-serif;
    margin: 0;
    padding: 0;
}

.litegraph .litemenubar li {
    font-size: 14px;
    color: #999;
    display: inline-block;
    min-width: 50px;
    padding-left: 10px;
    padding-right: 10px;
    user-select: none;
    -moz-user-select: none;
    -webkit-user-select: none;
    cursor: pointer;
}

.litegraph .litemenubar li:hover {
    background-color: #777;
    color: #eee;
}

.litegraph .litegraph .litemenubar-panel {
    position: absolute;
    top: 5px;
    left: 5px;
    min-width: 100px;
    background-color: #444;
    box-shadow: 0 0 3px black;
    padding: 4px;
    border-bottom: 2px solid #aaf;
    z-index: 10;
}

.litegraph .litemenu-entry,
.litemenu-title {
    font-size: 12px;
    color: #aaa;
    padding: 0 0 0 4px;
    margin: 2px;
    padding-left: 2px;
    -moz-user-select: none;
    -webkit-user-select: none;
    user-select: none;
    cursor: pointer;
}

.litegraph .litemenu-entry .icon {
    display: inline-block;
    width: 12px;
    height: 12px;
    margin: 2px;
    vertical-align: top;
}

.litegraph .litemenu-entry.checked .icon {
    background-color: #aaf;
}

.litegraph .litemenu-entry .more {
    float: right;
    padding-right: 5px;
}

.litegraph .litemenu-entry.disabled {
    opacity: 0.5;
    cursor: default;
}

.litegraph .litemenu-entry.separator {
    display: block;
    border-top: 1px solid #333;
    border-bottom: 1px solid #666;
    width: 100%;
    height: 0px;
    margin: 3px 0 2px 0;
    background-color: transparent;
    padding: 0 !important;
    cursor: default !important;
}

.litegraph .litemenu-entry.has_submenu {
    border-right: 2px solid cyan;
}

.litegraph .litemenu-title {
    color: #dde;
    background-color: #111;
    margin: 0;
    padding: 2px;
    cursor: default;
}

.litegraph .litemenu-entry:hover:not(.disabled):not(.separator) {
    background-color: #444 !important;
    color: #eee;
    transition: all 0.2s;
}

.litegraph .litemenu-entry .property_name {
    display: inline-block;
    text-align: left;
    min-width: 80px;
    min-height: 1.2em;
}

.litegraph .litemenu-entry .property_value {
    display: inline-block;
    background-color: rgba(0, 0, 0, 0.5);
    text-align: right;
    min-width: 80px;
    min-height: 1.2em;
    vertical-align: middle;
    padding-right: 10px;
}

.litegraph.litesearchbox {
    font-family: Tahoma, sans-serif;
    position: absolute;
    background-color: rgba(0, 0, 0, 0.5);
    padding-top: 4px;
}

.litegraph.litesearchbox input,
.litegraph.litesearchbox select {
    margin-top: 3px;
    min-width: 60px;
    min-height: 1.5em;
    background-color: black;
    border: 0;
    color: white;
    padding-left: 10px;
    margin-right: 5px;
}

.litegraph.litesearchbox .name {
    display: inline-block;
    min-width: 60px;
    min-height: 1.5em;
    padding-left: 10px;
}

.litegraph.litesearchbox .helper {
    overflow: auto;
    max-height: 200px;
    margin-top: 2px;
}

.litegraph.lite-search-item {
    font-family: Tahoma, sans-serif;
    background-color: rgba(0, 0, 0, 0.5);
    color: white;
    padding-top: 2px;
}

.litegraph.lite-search-item.not_in_filter{
    /*background-color: rgba(50, 50, 50, 0.5);*/
    /*color: #999;*/
    color: #B99;
    font-style: italic;
}

.litegraph.lite-search-item.generic_type{
    /*background-color: rgba(50, 50, 50, 0.5);*/
    /*color: #DD9;*/
    color: #999;
    font-style: italic;
}

.litegraph.lite-search-item:hover,
.litegraph.lite-search-item.selected {
    cursor: pointer;
    background-color: white;
    color: black;
}

/* DIALOGs ******/

.litegraph .dialog {
    position: absolute;
    top: 50%;
    left: 50%;
    margin-top: -150px;
    margin-left: -200px;

    background-color: #2A2A2A;

    min-width: 400px;
    min-height: 200px;
	box-shadow: 0 0 4px #111;
    border-radius: 6px;
}

.litegraph .dialog.settings {
	left: 10px;
	top: 10px;
	height: calc( 100% - 20px );
	margin: auto;
    max-width: 50%;
}

.litegraph .dialog.centered {
    top: 50px;
    left: 50%;
    position: absolute;
    transform: translateX(-50%);
    min-width: 600px;
    min-height: 300px;
    height: calc( 100% - 100px );
	margin: auto;
}

.litegraph .dialog .close {
    float: right;
	margin: 4px;
	margin-right: 10px;
	cursor: pointer;
	font-size: 1.4em;
}

.litegraph .dialog .close:hover {
	color: white;
}

.litegraph .dialog .dialog-header {
	color: #AAA;
	border-bottom: 1px solid #161616;
}

.litegraph .dialog .dialog-header { height: 40px; }
.litegraph .dialog .dialog-footer { height: 50px; padding: 10px; border-top: 1px solid #1a1a1a;}

.litegraph .dialog .dialog-header .dialog-title {
    font: 20px "Arial";
    margin: 4px;
    padding: 4px 10px;
    display: inline-block;
}

.litegraph .dialog .dialog-content, .litegraph .dialog .dialog-alt-content {
    height: calc(100% - 90px);
    width: 100%;
	min-height: 100px;
    display: inline-block;
	color: #AAA;
    /*background-color: black;*/
    overflow: auto;
}

.litegraph .dialog .dialog-content h3 {
	margin: 10px;
}

.litegraph .dialog .dialog-content .connections {
	flex-direction: row;
}

.litegraph .dialog .dialog-content .connections .connections_side {
	width: calc(50% - 5px);
	min-height: 100px;
	background-color: black;
	display: flex;
}

.litegraph .dialog .node_type {
	font-size: 1.2em;
	display: block;
	margin: 10px;
}

.litegraph .dialog .node_desc {
	opacity: 0.5;
	display: block;
	margin: 10px;
}

.litegraph .dialog .separator {
	display: block;
	width: calc( 100% - 4px );
	height: 1px;
	border-top: 1px solid #000;
	border-bottom: 1px solid #333;
	margin: 10px 2px;
	padding: 0;
}

.litegraph .dialog .property {
	margin-bottom: 2px;
	padding: 4px;
}

.litegraph .dialog .property:hover {
	background: #545454;
}

.litegraph .dialog .property_name {
	color: #737373;
    display: inline-block;
    text-align: left;
    vertical-align: top;
    width: 160px;
	padding-left: 4px;
	overflow: hidden;
    margin-right: 6px;
}

.litegraph .dialog .property:hover .property_name {
    color: white;
}

.litegraph .dialog .property_value {
    display: inline-block;
    text-align: right;
	color: #AAA;
	background-color: #1A1A1A;
    /*width: calc( 100% - 122px );*/
    max-width: calc( 100% - 162px );
    min-width: 200px;
	max-height: 300px;
    min-height: 20px;
    padding: 4px;
	padding-right: 12px;
	overflow: hidden;
	cursor: pointer;
	border-radius: 3px;
}

.litegraph .dialog .property_value:hover {
	color: white;
}

.litegraph .dialog .property.boolean .property_value {
	padding-right: 30px;
    color: #A88;
    /*width: auto;
    float: right;*/
}

.litegraph .dialog .property.boolean.bool-on .property_name{
    color: #8A8;
}
.litegraph .dialog .property.boolean.bool-on .property_value{
    color: #8A8;
}

.litegraph .dialog .btn {
	border: 0;
	border-radius: 4px;
    padding: 4px 20px;
    margin-left: 0px;
    background-color: #060606;
    color: #8e8e8e;
}

.litegraph .dialog .btn:hover {
    background-color: #111;
    color: #FFF;
}

.litegraph .dialog .btn.delete:hover {
    background-color: #F33;
    color: black;
}

.litegraph .subgraph_property {
	padding: 4px;
}

.litegraph .subgraph_property:hover {
	background-color: #333;
}

.litegraph .subgraph_property.extra {
    margin-top: 8px;
}

.litegraph .subgraph_property span.name {
	font-size: 1.3em;
	padding-left: 4px;
}

.litegraph .subgraph_property span.type {
	opacity: 0.5;
	margin-right: 20px;
	padding-left: 4px;
}

.litegraph .subgraph_property span.label {
	display: inline-block;
	width: 60px;
	padding:  0px 10px;
}

.litegraph .subgraph_property input {
	width: 140px;
	color: #999;
	background-color: #1A1A1A;
	border-radius: 4px;
	border: 0;
	margin-right: 10px;
	padding: 4px;
	padding-left: 10px;
}

.litegraph .subgraph_property button {
	background-color: #1c1c1c;
	color: #aaa;
	border: 0;
	border-radius: 2px;
	padding: 4px 10px;
	cursor: pointer;
}

.litegraph .subgraph_property.extra {
	color: #ccc;
}

.litegraph .subgraph_property.extra input {
	background-color: #111;
}

.litegraph .bullet_icon {
	margin-left: 10px;
	border-radius: 10px;
	width: 12px;
	height: 12px;
	background-color: #666;
	display: inline-block;
	margin-top: 2px;
	margin-right: 4px;
    transition: background-color 0.1s ease 0s;
    -moz-transition: background-color 0.1s ease 0s;
}

.litegraph .bullet_icon:hover {
	background-color: #698;
	cursor: pointer;
} 

/* OLD */

.graphcontextmenu {
    padding: 4px;
    min-width: 100px;
}

.graphcontextmenu-title {
    color: #dde;
    background-color: #222;
    margin: 0;
    padding: 2px;
    cursor: default;
}

.graphmenu-entry {
    box-sizing: border-box;
    margin: 2px;
    padding-left: 20px;
    user-select: none;
    -moz-user-select: none;
    -webkit-user-select: none;
    transition: all linear 0.3s;
}

.graphmenu-entry.event,
.litemenu-entry.event {
    border-left: 8px solid orange;
    padding-left: 12px;
}

.graphmenu-entry.disabled {
    opacity: 0.3;
}

.graphmenu-entry.submenu {
    border-right: 2px solid #eee;
}

.graphmenu-entry:hover {
    background-color: #555;
}

.graphmenu-entry.separator {
    background-color: #111;
    border-bottom: 1px solid #666;
    height: 1px;
    width: calc(100% - 20px);
    -moz-width: calc(100% - 20px);
    -webkit-width: calc(100% - 20px);
}

.graphmenu-entry .property_name {
    display: inline-block;
    text-align: left;
    min-width: 80px;
    min-height: 1.2em;
}

.graphmenu-entry .property_value,
.litemenu-entry .property_value {
    display: inline-block;
    background-color: rgba(0, 0, 0, 0.5);
    text-align: right;
    min-width: 80px;
    min-height: 1.2em;
    vertical-align: middle;
    padding-right: 10px;
}

.graphdialog {
    position: absolute;
    top: 10px;
    left: 10px;
    min-height: 2em;
    background-color: #333;
    font-size: 1.2em;
    box-shadow: 0 0 10px black !important;
	z-index: 10;
}

.graphdialog.rounded {
    border-radius: 12px;
    padding-right: 2px;
}

.graphdialog .name {
    display: inline-block;
    min-width: 60px;
    min-height: 1.5em;
    padding-left: 10px;
}

.graphdialog input,
.graphdialog textarea,
.graphdialog select {
    margin: 3px;
    min-width: 60px;
    min-height: 1.5em;
    background-color: black;
    border: 0;
    color: white;
    padding-left: 10px;
    outline: none;
}

.graphdialog textarea {
	min-height: 150px;
}

.graphdialog button {
    margin-top: 3px;
    vertical-align: top;
    background-color: #999;
	border: 0;
}

.graphdialog button.rounded,
.graphdialog input.rounded {
    border-radius: 0 12px 12px 0;
}

.graphdialog .helper {
    overflow: auto;
    max-height: 200px;
}

.graphdialog .help-item {
    padding-left: 10px;
}

.graphdialog .help-item:hover,
.graphdialog .help-item.selected {
    cursor: pointer;
    background-color: white;
    color: black;
}

.litegraph .dialog {
    min-height: 0;
}
.litegraph .dialog .dialog-content {
display: block;
}
.litegraph .dialog .dialog-content .subgraph_property {
padding: 5px;
}
.litegraph .dialog .dialog-footer {
margin: 0;
}
.litegraph .dialog .dialog-footer .subgraph_property {
margin-top: 0;
display: flex;
align-items: center;
padding: 5px;
}
.litegraph .dialog .dialog-footer .subgraph_property .name {
flex: 1;
}
.litegraph .graphdialog {
display: flex;
align-items: center;
border-radius: 20px;
padding: 4px 10px;
position: fixed;
}
.litegraph .graphdialog .name {
padding: 0;
min-height: 0;
font-size: 16px;
vertical-align: middle;
}
.litegraph .graphdialog .value {
font-size: 16px;
min-height: 0;
margin: 0 10px;
padding: 2px 5px;
}
.litegraph .graphdialog input[type="checkbox"] {
width: 16px;
height: 16px;
}
.litegraph .graphdialog button {
padding: 4px 18px;
border-radius: 20px;
cursor: pointer;
}
  


================================================
FILE: docs/links/litegraph.js
================================================
//packer version


(function(global) {
    // *************************************************************
    //   LiteGraph CLASS                                     *******
    // *************************************************************

    /**
     * The Global Scope. It contains all the registered node classes.
     *
     * @class LiteGraph
     * @constructor
     */

    var LiteGraph = (global.LiteGraph = {
        VERSION: 0.4,

        CANVAS_GRID_SIZE: 10,

        NODE_TITLE_HEIGHT: 30,
        NODE_TITLE_TEXT_Y: 20,
        NODE_SLOT_HEIGHT: 20,
        NODE_WIDGET_HEIGHT: 20,
        NODE_WIDTH: 140,
        NODE_MIN_WIDTH: 50,
        NODE_COLLAPSED_RADIUS: 10,
        NODE_COLLAPSED_WIDTH: 80,
        NODE_TITLE_COLOR: "#999",
        NODE_SELECTED_TITLE_COLOR: "#FFF",
        NODE_TEXT_SIZE: 14,
        NODE_TEXT_COLOR: "#AAA",
        NODE_SUBTEXT_SIZE: 12,
        NODE_DEFAULT_COLOR: "#333",
        NODE_DEFAULT_BGCOLOR: "#353535",
        NODE_DEFAULT_BOXCOLOR: "#666",
        NODE_DEFAULT_SHAPE: "box",
        NODE_BOX_OUTLINE_COLOR: "#FFF",
        DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)",
        DEFAULT_GROUP_FONT: 24,

        WIDGET_BGCOLOR: "#222",
        WIDGET_OUTLINE_COLOR: "#666",
        WIDGET_TEXT_COLOR: "#DDD",
        WIDGET_SECONDARY_TEXT_COLOR: "#999",

        LINK_COLOR: "#9A9",
        EVENT_LINK_COLOR: "#A86",
        CONNECTING_LINK_COLOR: "#AFA",

        MAX_NUMBER_OF_NODES: 1000, //avoid infinite loops
        DEFAULT_POSITION: [100, 100], //default node position
        VALID_SHAPES: ["default", "box", "round", "card"], //,"circle"

        //shapes are used for nodes but also for slots
        BOX_SHAPE: 1,
        ROUND_SHAPE: 2,
        CIRCLE_SHAPE: 3,
        CARD_SHAPE: 4,
        ARROW_SHAPE: 5,
        GRID_SHAPE: 6, // intended for slot arrays

        //enums
        INPUT: 1,
        OUTPUT: 2,

        EVENT: -1, //for outputs
        ACTION: -1, //for inputs

        NODE_MODES: ["Always", "On Event", "Never", "On Trigger"], // helper, will add "On Request" and more in the future
        NODE_MODES_COLORS:["#666","#422","#333","#224","#626"], // use with node_box_coloured_by_mode
        ALWAYS: 0,
        ON_EVENT: 1,
        NEVER: 2,
        ON_TRIGGER: 3,

        UP: 1,
        DOWN: 2,
        LEFT: 3,
        RIGHT: 4,
        CENTER: 5,

        LINK_RENDER_MODES: ["Straight", "Linear", "Spline"], // helper
        STRAIGHT_LINK: 0,
        LINEAR_LINK: 1,
        SPLINE_LINK: 2,

        NORMAL_TITLE: 0,
        NO_TITLE: 1,
        TRANSPARENT_TITLE: 2,
        AUTOHIDE_TITLE: 3,
        VERTICAL_LAYOUT: "vertical", // arrange nodes vertically

        proxy: null, //used to redirect calls
        node_images_path: "",

        debug: false,
        catch_exceptions: true,
        throw_errors: true,
        allow_scripts: false, //if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits
        use_deferred_actions: true, //executes actions during the graph execution flow
        registered_node_types: {}, //nodetypes by string
        node_types_by_file_extension: {}, //used for dropping files in the canvas
        Nodes: {}, //node types by classname
		Globals: {}, //used to store vars between graphs

        searchbox_extras: {}, //used to add extra features to the search box
        auto_sort_node_types: false, // [true!] If set to true, will automatically sort node types / categories in the context menus
		
		node_box_coloured_when_on: false, // [true!] this make the nodes box (top left circle) coloured when triggered (execute/action), visual feedback
        node_box_coloured_by_mode: false, // [true!] nodebox based on node mode, visual feedback
        
        dialog_close_on_mouse_leave: true, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false
        dialog_close_on_mouse_leave_delay: 500,
        
        shift_click_do_break_link_from: false, // [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys
        click_do_break_link_to: false, // [false!]prefer false, way too easy to break links
        
        search_hide_on_mouse_leave: true, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false
        search_filter_enabled: false, // [true!] enable filtering slots type in the search widget, !requires auto_load_slot_types or manual set registered_slot_[in/out]_types and slot_types_[in/out]
        search_show_all_on_open: true, // [true!] opens the results list when opening the search widget
        
        auto_load_slot_types: false, // [if want false, use true, run, get vars values to be statically set, than disable] nodes types and nodeclass association with node types need to be calculated, if dont want this, calculate once and set registered_slot_[in/out]_types and slot_types_[in/out]
        
		// set these values if not using auto_load_slot_types
        registered_slot_in_types: {}, // slot types for nodeclass
        registered_slot_out_types: {}, // slot types for nodeclass
        slot_types_in: [], // slot types IN
        slot_types_out: [], // slot types OUT
        slot_types_default_in: [], // specify for each IN slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search
		slot_types_default_out: [], // specify for each OUT slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search
		
		alt_drag_do_clone_nodes: false, // [true!] very handy, ALT click to clone and drag the new node

		do_add_triggers_slots: false, // [true!] will create and connect event slots when using action/events connections, !WILL CHANGE node mode when using onTrigger (enable mode colors), onExecuted does not need this
		
		allow_multi_output_for_events: true, // [false!] being events, it is strongly reccomended to use them sequentially, one by one

		middle_click_slot_add_default_node: false, //[true!] allows to create and connect a ndoe clicking with the third button (wheel)
		
		release_link_on_empty_shows_menu: false, //[true!] dragging a link to empty space will open a menu, add from list, search or defaults
		
        pointerevents_method: "mouse", // "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now)
        // TODO implement pointercancel, gotpointercapture, lostpointercapture, (pointerover, pointerout if necessary)

        ctrl_shift_v_paste_connect_unselected_outputs: false, //[true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected with the inputs of the newly pasted nodes

        // if true, all newly created nodes/links will use string UUIDs for their id fields instead of integers.
        // use this if you must have node IDs that are unique across all graphs and subgraphs.
        use_uuids: false,

        /**
         * Register a node class so it can be listed when the user wants to create a new one
         * @method registerNodeType
         * @param {String} type name of the node and path
         * @param {Class} base_class class containing the structure of a node
         */

        registerNodeType: function(type, base_class) {
            if (!base_class.prototype) {
                throw "Cannot register a simple object, it must be a class with a prototype";
            }
            base_class.type = type;

            if (LiteGraph.debug) {
                console.log("Node registered: " + type);
            }

            const classname = base_class.name;

            const pos = type.lastIndexOf("/");
            base_class.category = type.substring(0, pos);

            if (!base_class.title) {
                base_class.title = classname;
            }

            //extend class
            for (var i in LGraphNode.prototype) {
                if (!base_class.prototype[i]) {
                    base_class.prototype[i] = LGraphNode.prototype[i];
                }
            }

            const prev = this.registered_node_types[type];
            if(prev) {
                console.log("replacing node type: " + type);
            }
            if( !Object.prototype.hasOwnProperty.call( base_class.prototype, "shape") ) {
                Object.defineProperty(base_class.prototype, "shape", {
                    set: function(v) {
                        switch (v) {
                            case "default":
                                delete this._shape;
                                break;
                            case "box":
                                this._shape = LiteGraph.BOX_SHAPE;
                                break;
                            case "round":
                                this._shape = LiteGraph.ROUND_SHAPE;
                                break;
                            case "circle":
                                this._shape = LiteGraph.CIRCLE_SHAPE;
                                break;
                            case "card":
                                this._shape = LiteGraph.CARD_SHAPE;
                                break;
                            default:
                                this._shape = v;
                        }
                    },
                    get: function() {
                        return this._shape;
                    },
                    enumerable: true,
                    configurable: true
                });
                

                //used to know which nodes to create when dragging files to the canvas
                if (base_class.supported_extensions) {
                    for (let i in base_class.supported_extensions) {
                        const ext = base_class.supported_extensions[i];
                        if(ext && ext.constructor === String) {
                            this.node_types_by_file_extension[ ext.toLowerCase() ] = base_class;
                        }
                    }
                }
            }

            this.registered_node_types[type] = base_class;
            if (base_class.constructor.name) {
                this.Nodes[classname] = base_class;
            }
            if (LiteGraph.onNodeTypeRegistered) {
                LiteGraph.onNodeTypeRegistered(type, base_class);
            }
            if (prev && LiteGraph.onNodeTypeReplaced) {
                LiteGraph.onNodeTypeReplaced(type, base_class, prev);
            }

            //warnings
            if (base_class.prototype.onPropertyChange) {
                console.warn(
                    "LiteGraph node class " +
                        type +
                        " has onPropertyChange method, it must be called onPropertyChanged with d at the end"
                );
            }
            
            // TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types
            if (this.auto_load_slot_types) {
                new base_class(base_class.title || "tmpnode");
            }
        },

        /**
         * removes a node type from the system
         * @method unregisterNodeType
         * @param {String|Object} type name of the node or the node constructor itself
         */
        unregisterNodeType: function(type) {
            const base_class =
                type.constructor === String
                    ? this.registered_node_types[type]
                    : type;
            if (!base_class) {
                throw "node type not found: " + type;
            }
            delete this.registered_node_types[base_class.type];
            if (base_class.constructor.name) {
                delete this.Nodes[base_class.constructor.name];
            }
        },

        /**
        * Save a slot type and his node
        * @method registerSlotType
        * @param {String|Object} type name of the node or the node constructor itself
        * @param {String} slot_type name of the slot type (variable type), eg. string, number, array, boolean, ..
        */
        registerNodeAndSlotType: function(type, slot_type, out){
            out = out || false;
            const base_class =
                type.constructor === String &&
                this.registered_node_types[type] !== "anonymous"
                    ? this.registered_node_types[type]
                    : type;

            const class_type = base_class.constructor.type;

            let allTypes = [];
            if (typeof slot_type === "string") {
                allTypes = slot_type.split(",");
            } else if (slot_type == this.EVENT || slot_type == this.ACTION) {
                allTypes = ["_event_"];
            } else {
                allTypes = ["*"];
            }

            for (let i = 0; i < allTypes.length; ++i) {
                let slotType = allTypes[i];
                if (slotType === "") {
                    slotType = "*";
                }
                const registerTo = out
                    ? "registered_slot_out_types"
                    : "registered_slot_in_types";
                if (this[registerTo][slotType] === undefined) {
                    this[registerTo][slotType] = { nodes: [] };
                }
                if (!this[registerTo][slotType].nodes.includes(class_type)) {
                    this[registerTo][slotType].nodes.push(class_type);
                }

                // check if is a new type
                if (!out) {
                    if (!this.slot_types_in.includes(slotType.toLowerCase())) {
                        this.slot_types_in.push(slotType.toLowerCase());
                        this.slot_types_in.sort();
                    }
                } else {
                    if (!this.slot_types_out.includes(slotType.toLowerCase())) {
                        this.slot_types_out.push(slotType.toLowerCase());
                        this.slot_types_out.sort();
                    }
                }
            }
        },
        
        /**
         * Create a new nodetype by passing an object with some properties
         * like onCreate, inputs:Array, outputs:Array, properties, onExecute
         * @method buildNodeClassFromObject
         * @param {String} name node name with namespace (p.e.: 'math/sum')
         * @param {Object} object methods expected onCreate, inputs, outputs, properties, onExecute
         */
         buildNodeClassFromObject: function(
            name,
            object
        ) {
            var ctor_code = "";
            if(object.inputs)
            for(var i=0; i < object.inputs.length; ++i)
            {
                var _name = object.inputs[i][0];
                var _type = object.inputs[i][1];
                if(_type && _type.constructor === String)
                    _type = '"'+_type+'"';
                ctor_code += "this.addInput('"+_name+"',"+_type+");\n";
            }
            if(object.outputs)
            for(var i=0; i < object.outputs.length; ++i)
            {
                var _name = object.outputs[i][0];
                var _type = object.outputs[i][1];
                if(_type && _type.constructor === String)
                    _type = '"'+_type+'"';
                ctor_code += "this.addOutput('"+_name+"',"+_type+");\n";
            }
            if(object.properties)
            for(var i in object.properties)
            {
                var prop = object.properties[i];
                if(prop && prop.constructor === String)
                    prop = '"'+prop+'"';
                ctor_code += "this.addProperty('"+i+"',"+prop+");\n";
            }
            ctor_code += "if(this.onCreate)this.onCreate()";
            var classobj = Function(ctor_code);
            for(var i in object)
                if(i!="inputs" && i!="outputs" && i!="properties")
                    classobj.prototype[i] = object[i];
            classobj.title = object.title || name.split("/").pop();
            classobj.desc = object.desc || "Generated from object";
            this.registerNodeType(name, classobj);
            return classobj;
        },
        
        /**
         * Create a new nodetype by passing a function, it wraps it with a proper class and generates inputs according to the parameters of the function.
         * Useful to wrap simple methods that do not require properties, and that only process some input to generate an output.
         * @method wrapFunctionAsNode
         * @param {String} name node name with namespace (p.e.: 'math/sum')
         * @param {Function} func
         * @param {Array} param_types [optional] an array containing the type of every parameter, otherwise parameters will accept any type
         * @param {String} return_type [optional] string with the return type, otherwise it will be generic
         * @param {Object} properties [optional] properties to be configurable
         */
        wrapFunctionAsNode: function(
            name,
            func,
            param_types,
            return_type,
            properties
        ) {
            var params = Array(func.length);
            var code = "";
            if(param_types !== null) //null means no inputs
            {
                var names = LiteGraph.getParameterNames(func);
                for (var i = 0; i < names.length; ++i) {
                    var type = 0;
                    if(param_types)
                    {
                        //type = param_types[i] != null ? "'" + param_types[i] + "'" : "0";
                        if( param_types[i] != null && param_types[i].constructor === String )
                            type = "'" + param_types[i] + "'" ;
                        else if( param_types[i] != null )
                            type = param_types[i];
                    } 
                    code +=
                        "this.addInput('" +
                        names[i] +
                        "'," +
                        type +
                        ");\n";
                }
            }
            if(return_type !== null) //null means no output
            code +=
                "this.addOutput('out'," +
                (return_type != null ? (return_type.constructor === String ? "'" + return_type + "'" : return_type) : 0) +
                ");\n";
            if (properties) {
                code +=
                    "this.properties = " + JSON.stringify(properties) + ";\n";
            }
            var classobj = Function(code);
            classobj.title = name.split("/").pop();
            classobj.desc = "Generated from " + func.name;
            classobj.prototype.onExecute = function onExecute() {
                for (var i = 0; i < params.length; ++i) {
                    params[i] = this.getInputData(i);
                }
                var r = func.apply(this, params);
                this.setOutputData(0, r);
            };
            this.registerNodeType(name, classobj);
            return classobj;
        },

        /**
         * Removes all previously registered node's types
         */
        clearRegisteredTypes: function() {
            this.registered_node_types = {};
            this.node_types_by_file_extension = {};
            this.Nodes = {};
            this.searchbox_extras = {};
        },

        /**
         * Adds this method to all nodetypes, existing and to be created
         * (You can add it to LGraphNode.prototype but then existing node types wont have it)
         * @method addNodeMethod
         * @param {Function} func
         */
        addNodeMethod: function(name, func) {
            LGraphNode.prototype[name] = func;
            for (var i in this.registered_node_types) {
                var type = this.registered_node_types[i];
                if (type.prototype[name]) {
                    type.prototype["_" + name] = type.prototype[name];
                } //keep old in case of replacing
                type.prototype[name] = func;
            }
        },

        /**
         * Create a node of a given type with a name. The node is not attached to any graph yet.
         * @method createNode
         * @param {String} type full name of the node class. p.e. "math/sin"
         * @param {String} name a name to distinguish from other nodes
         * @param {Object} options to set options
         */

        createNode: function(type, title, options) {
            var base_class = this.registered_node_types[type];
            if (!base_class) {
                if (LiteGraph.debug) {
                    console.log(
                        'GraphNode type "' + type + '" not registered.'
                    );
                }
                return null;
            }

            var prototype = base_class.prototype || base_class;

            title = title || base_class.title || type;

            var node = null;

            if (LiteGraph.catch_exceptions) {
                try {
                    node = new base_class(title);
                } catch (err) {
                    console.error(err);
                    return null;
                }
            } else {
                node = new base_class(title);
            }

            node.type = type;

            if (!node.title && title) {
                node.title = title;
            }
            if (!node.properties) {
                node.properties = {};
            }
            if (!node.properties_info) {
                node.properties_info = [];
            }
            if (!node.flags) {
                node.flags = {};
            }
            if (!node.size) {
                node.size = node.computeSize();
				//call onresize?
            }
            if (!node.pos) {
                node.pos = LiteGraph.DEFAULT_POSITION.concat();
            }
            if (!node.mode) {
                node.mode = LiteGraph.ALWAYS;
            }

            //extra options
            if (options) {
                for (var i in options) {
                    node[i] = options[i];
                }
            }

			// callback
            if ( node.onNodeCreated ) {
                node.onNodeCreated();
            }
            
            return node;
        },

        /**
         * Returns a registered node type with a given name
         * @method getNodeType
         * @param {String} type full name of the node class. p.e. "math/sin"
         * @return {Class} the node class
         */
        getNodeType: function(type) {
            return this.registered_node_types[type];
        },

        /**
         * Returns a list of node types matching one category
         * @method getNodeType
         * @param {String} category category name
         * @return {Array} array with all the node classes
         */

        getNodeTypesInCategory: function(category, filter) {
            var r = [];
            for (var i in this.registered_node_types) {
                var type = this.registered_node_types[i];
                if (type.filter != filter) {
                    continue;
                }

                if (category == "") {
                    if (type.category == null) {
                        r.push(type);
                    }
                } else if (type.category == category) {
                    r.push(type);
                }
            }

            if (this.auto_sort_node_types) {
                r.sort(function(a,b){return a.title.localeCompare(b.title)});
            }

            return r;
        },

        /**
         * Returns a list with all the node type categories
         * @method getNodeTypesCategories
         * @param {String} filter only nodes with ctor.filter equal can be shown
         * @return {Array} array with all the names of the categories
         */
        getNodeTypesCategories: function( filter ) {
            var categories = { "": 1 };
            for (var i in this.registered_node_types) {
				var type = this.registered_node_types[i];
                if ( type.category && !type.skip_list )
                {
					if(type.filter != filter)
						continue;
                    categories[type.category] = 1;
                }
            }
            var result = [];
            for (var i in categories) {
                result.push(i);
            }
            return this.auto_sort_node_types ? result.sort() : result;
        },

        //debug purposes: reloads all the js scripts that matches a wildcard
        reloadNodes: function(folder_wildcard) {
            var tmp = document.getElementsByTagName("script");
            //weird, this array changes by its own, so we use a copy
            var script_files = [];
            for (var i=0; i < tmp.length; i++) {
                script_files.push(tmp[i]);
            }

            var docHeadObj = document.getElementsByTagName("head")[0];
            folder_wildcard = document.location.href + folder_wildcard;

            for (var i=0; i < script_files.length; i++) {
                var src = script_files[i].src;
                if (
                    !src ||
                    src.substr(0, folder_wildcard.length) != folder_wildcard
                ) {
                    continue;
                }

                try {
                    if (LiteGraph.debug) {
                        console.log("Reloading: " + src);
                    }
                    var dynamicScript = document.createElement("script");
                    dynamicScript.type = "text/javascript";
                    dynamicScript.src = src;
                    docHeadObj.appendChild(dynamicScript);
                    docHeadObj.removeChild(script_files[i]);
                } catch (err) {
                    if (LiteGraph.throw_errors) {
                        throw err;
                    }
                    if (LiteGraph.debug) {
                        console.log("Error while reloading " + src);
                    }
                }
            }

            if (LiteGraph.debug) {
                console.log("Nodes reloaded");
            }
        },

        //separated just to improve if it doesn't work
        cloneObject: function(obj, target) {
            if (obj == null) {
                return null;
            }
            var r = JSON.parse(JSON.stringify(obj));
            if (!target) {
                return r;
            }

            for (var i in r) {
                target[i] = r[i];
            }
            return target;
        },

        /*
         * https://gist.github.com/jed/982883?permalink_comment_id=852670#gistcomment-852670
         */
        uuidv4: function() {
            return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,a=>(a^Math.random()*16>>a/4).toString(16));
        },

        /**
         * Returns if the types of two slots are compatible (taking into account wildcards, etc)
         * @method isValidConnection
         * @param {String} type_a
         * @param {String} type_b
         * @return {Boolean} true if they can be connected
         */
        isValidConnection: function(type_a, type_b) {
			if (type_a=="" || type_a==="*") type_a = 0;
			if (type_b=="" || type_b==="*") type_b = 0;
            if (
                !type_a //generic output
                || !type_b // generic input
                || type_a == type_b //same type (is valid for triggers)
                || (type_a == LiteGraph.EVENT && type_b == LiteGraph.ACTION)
            ) {
                return true;
            }

            // Enforce string type to handle toLowerCase call (-1 number not ok)
            type_a = String(type_a);
            type_b = String(type_b);
            type_a = type_a.toLowerCase();
            type_b = type_b.toLowerCase();

            // For nodes supporting multiple connection types
            if (type_a.indexOf(",") == -1 && type_b.indexOf(",") == -1) {
                return type_a == type_b;
            }

            // Check all permutations to see if one is valid
            var supported_types_a = type_a.split(",");
            var supported_types_b = type_b.split(",");
            for (var i = 0; i < supported_types_a.length; ++i) {
                for (var j = 0; j < supported_types_b.length; ++j) {
                    if(this.isValidConnection(supported_types_a[i],supported_types_b[j])){
					//if (supported_types_a[i] == supported_types_b[j]) {
                        return true;
                    }
                }
            }

            return false;
        },

        /**
         * Register a string in the search box so when the user types it it will recommend this node
         * @method registerSearchboxExtra
         * @param {String} node_type the node recommended
         * @param {String} description text to show next to it
         * @param {Object} data it could contain info of how the node should be configured
         * @return {Boolean} true if they can be connected
         */
        registerSearchboxExtra: function(node_type, description, data) {
            this.searchbox_extras[description.toLowerCase()] = {
                type: node_type,
                desc: description,
                data: data
            };
        },

        /**
         * Wrapper to load files (from url using fetch or from file using FileReader)
         * @method fetchFile
         * @param {String|File|Blob} url the url of the file (or the file itself)
         * @param {String} type an string to know how to fetch it: "text","arraybuffer","json","blob"
         * @param {Function} on_complete callback(data)
         * @param {Function} on_error in case of an error
         * @return {FileReader|Promise} returns the object used to 
         */
		fetchFile: function( url, type, on_complete, on_error ) {
			var that = this;
			if(!url)
				return null;

			type = type || "text";
			if( url.constructor === String )
			{
				if (url.substr(0, 4) == "http" && LiteGraph.proxy) {
					url = LiteGraph.proxy + url.substr(url.indexOf(":") + 3);
				}
				return fetch(url)
				.then(function(response) {
					if(!response.ok)
						 throw new Error("File not found"); //it will be catch below
					if(type == "arraybuffer")
						return response.arrayBuffer();
					else if(type == "text" || type == "string")
						return response.text();
					else if(type == "json")
						return response.json();
					else if(type == "blob")
						return response.blob();
				})
				.then(function(data) {
					if(on_complete)
						on_complete(data);
				})
				.catch(function(error) {
					console.error("error fetching file:",url);
					if(on_error)
						on_error(error);
				});
			}
			else if( url.constructor === File || url.constructor === Blob)
			{
				var reader = new FileReader();
				reader.onload = function(e)
				{
					var v = e.target.result;
					if( type == "json" )
						v = JSON.parse(v);
					if(on_complete)
						on_complete(v);
				}
				if(type == "arraybuffer")
					return reader.readAsArrayBuffer(url);
				else if(type == "text" || type == "json")
					return reader.readAsText(url);
				else if(type == "blob")
					return reader.readAsBinaryString(url);
			}
			return null;
		}
    });

    //timer that works everywhere
    if (typeof performance != "undefined") {
        LiteGraph.getTime = performance.now.bind(performance);
    } else if (typeof Date != "undefined" && Date.now) {
        LiteGraph.getTime = Date.now.bind(Date);
    } else if (typeof process != "undefined") {
        LiteGraph.getTime = function() {
            var t = process.hrtime();
            return t[0] * 0.001 + t[1] * 1e-6;
        };
    } else {
        LiteGraph.getTime = function getTime() {
            return new Date().getTime();
        };
    }

    //*********************************************************************************
    // LGraph CLASS
    //*********************************************************************************

    /**
     * LGraph is the class that contain a full graph. We instantiate one and add nodes to it, and then we can run the execution loop.
	 * supported callbacks:
		+ onNodeAdded: when a new node is added to the graph
		+ onNodeRemoved: when a node inside this graph is removed
		+ onNodeConnectionChange: some connection has changed in the graph (connected or disconnected)
     *
     * @class LGraph
     * @constructor
     * @param {Object} o data from previous serialization [optional]
     */

    function LGraph(o) {
        if (LiteGraph.debug) {
            console.log("Graph created");
        }
        this.list_of_graphcanvas = null;
        this.clear();

        if (o) {
            this.configure(o);
        }
    }

    global.LGraph = LiteGraph.LGraph = LGraph;

    //default supported types
    LGraph.supported_types = ["number", "string", "boolean"];

    //used to know which types of connections support this graph (some graphs do not allow certain types)
    LGraph.prototype.getSupportedTypes = function() {
        return this.supported_types || LGraph.supported_types;
    };

    LGraph.STATUS_STOPPED = 1;
    LGraph.STATUS_RUNNING = 2;

    /**
     * Removes all nodes from this graph
     * @method clear
     */

    LGraph.prototype.clear = function() {
        this.stop();
        this.status = LGraph.STATUS_STOPPED;

        this.last_node_id = 0;
        this.last_link_id = 0;

        this._version = -1; //used to detect changes

        //safe clear
        if (this._nodes) {
            for (var i = 0; i < this._nodes.length; ++i) {
                var node = this._nodes[i];
                if (node.onRemoved) {
                    node.onRemoved();
                }
            }
        }

        //nodes
        this._nodes = [];
        this._nodes_by_id = {};
        this._nodes_in_order = []; //nodes sorted in execution order
        this._nodes_executable = null; //nodes that contain onExecute sorted in execution order

        //other scene stuff
        this._groups = [];

        //links
        this.links = {}; //container with all the links

        //iterations
        this.iteration = 0;

        //custom data
        this.config = {};
		this.vars = {};
		this.extra = {}; //to store custom data

        //timing
        this.globaltime = 0;
        this.runningtime = 0;
        this.fixedtime = 0;
        this.fixedtime_lapse = 0.01;
        this.elapsed_time = 0.01;
        this.last_update_time = 0;
        this.starttime = 0;

        this.catch_errors = true;

        this.nodes_executing = [];
        this.nodes_actioning = [];
        this.nodes_executedAction = [];
        
        //subgraph_data
        this.inputs = {};
        this.outputs = {};

        //notify canvas to redraw
        this.change();

        this.sendActionToCanvas("clear");
    };

    /**
     * Attach Canvas to this graph
     * @method attachCanvas
     * @param {GraphCanvas} graph_canvas
     */

    LGraph.prototype.attachCanvas = function(graphcanvas) {
        if (graphcanvas.constructor != LGraphCanvas) {
            throw "attachCanvas expects a LGraphCanvas instance";
        }
        if (graphcanvas.graph && graphcanvas.graph != this) {
            graphcanvas.graph.detachCanvas(graphcanvas);
        }

        graphcanvas.graph = this;

        if (!this.list_of_graphcanvas) {
            this.list_of_graphcanvas = [];
        }
        this.list_of_graphcanvas.push(graphcanvas);
    };

    /**
     * Detach Canvas from this graph
     * @method detachCanvas
     * @param {GraphCanvas} graph_canvas
     */
    LGraph.prototype.detachCanvas = function(graphcanvas) {
        if (!this.list_of_graphcanvas) {
            return;
        }

        var pos = this.list_of_graphcanvas.indexOf(graphcanvas);
        if (pos == -1) {
            return;
        }
        graphcanvas.graph = null;
        this.list_of_graphcanvas.splice(pos, 1);
    };

    /**
     * Starts running this graph every interval milliseconds.
     * @method start
     * @param {number} interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate
     */

    LGraph.prototype.start = function(interval) {
        if (this.status == LGraph.STATUS_RUNNING) {
            return;
        }
        this.status = LGraph.STATUS_RUNNING;

        if (this.onPlayEvent) {
            this.onPlayEvent();
        }

        this.sendEventToAllNodes("onStart");

        //launch
        this.starttime = LiteGraph.getTime();
        this.last_update_time = this.starttime;
        interval = interval || 0;
        var that = this;

		//execute once per frame
        if ( interval == 0 && typeof window != "undefined" && window.requestAnimationFrame ) {
            function on_frame() {
                if (that.execution_timer_id != -1) {
                    return;
                }
                window.requestAnimationFrame(on_frame);
				if(that.onBeforeStep)
					that.onBeforeStep();
                that.runStep(1, !that.catch_errors);
				if(that.onAfterStep)
					that.onAfterStep();
            }
            this.execution_timer_id = -1;
            on_frame();
        } else { //execute every 'interval' ms
            this.execution_timer_id = setInterval(function() {
                //execute
				if(that.onBeforeStep)
					that.onBeforeStep();
                that.runStep(1, !that.catch_errors);
				if(that.onAfterStep)
					that.onAfterStep();
            }, interval);
        }
    };

    /**
     * Stops the execution loop of the graph
     * @method stop execution
     */

    LGraph.prototype.stop = function() {
        if (this.status == LGraph.STATUS_STOPPED) {
            return;
        }

        this.status = LGraph.STATUS_STOPPED;

        if (this.onStopEvent) {
            this.onStopEvent();
        }

        if (this.execution_timer_id != null) {
            if (this.execution_timer_id != -1) {
                clearInterval(this.execution_timer_id);
            }
            this.execution_timer_id = null;
        }

        this.sendEventToAllNodes("onStop");
    };

    /**
     * Run N steps (cycles) of the graph
     * @method runStep
     * @param {number} num number of steps to run, default is 1
     * @param {Boolean} do_not_catch_errors [optional] if you want to try/catch errors 
     * @param {number} limit max number of nodes to execute (used to execute from start to a node)
     */

    LGraph.prototype.runStep = function(num, do_not_catch_errors, limit ) {
        num = num || 1;

        var start = LiteGraph.getTime();
        this.globaltime = 0.001 * (start - this.starttime);

        //not optimal: executes possible pending actions in node, problem is it is not optimized
        //it is done here as if it was done in the later loop it wont be called in the node missed the onExecute
        
        //from now on it will iterate only on executable nodes which is faster
        var nodes = this._nodes_executable
            ? this._nodes_executable
            : this._nodes;
        if (!nodes) {
            return;
        }

		limit = limit || nodes.length;

        if (do_not_catch_errors) {
            //iterations
            for (var i = 0; i < num; i++) {
                for (var j = 0; j < limit; ++j) {
                    var node = nodes[j];
                    if(LiteGraph.use_deferred_actions && node._waiting_actions && node._waiting_actions.length)
                        node.executePendingActions();
                    if (node.mode == LiteGraph.ALWAYS && node.onExecute) {
                        //wrap node.onExecute();
						node.doExecute();
                    }
                }

                this.fixedtime += this.fixedtime_lapse;
                if (this.onExecuteStep) {
                    this.onExecuteStep();
                }
            }

            if (this.onAfterExecute) {
                this.onAfterExecute();
            }
        } else { //catch errors
            try {
                //iterations
                for (var i = 0; i < num; i++) {
                    for (var j = 0; j < limit; ++j) {
                        var node = nodes[j];
                        if(LiteGraph.use_deferred_actions && node._waiting_actions && node._waiting_actions.length)
                            node.executePendingActions();
                        if (node.mode == LiteGraph.ALWAYS && node.onExecute) {
                            node.onExecute();
                        }
                    }

                    this.fixedtime += this.fixedtime_lapse;
                    if (this.onExecuteStep) {
                        this.onExecuteStep();
                    }
                }

                if (this.onAfterExecute) {
                    this.onAfterExecute();
                }
                this.errors_in_execution = false;
            } catch (err) {
                this.errors_in_execution = true;
                if (LiteGraph.throw_errors) {
                    throw err;
                }
                if (LiteGraph.debug) {
                    console.log("Error during execution: " + err);
                }
                this.stop();
            }
        }

        var now = LiteGraph.getTime();
        var elapsed = now - start;
        if (elapsed == 0) {
            elapsed = 1;
        }
        this.execution_time = 0.001 * elapsed;
        this.globaltime += 0.001 * elapsed;
        this.iteration += 1;
        this.elapsed_time = (now - this.last_update_time) * 0.001;
        this.last_update_time = now;
        this.nodes_executing = [];
        this.nodes_actioning = [];
        this.nodes_executedAction = [];
    };

    /**
     * Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than
     * nodes with only inputs.
     * @method updateExecutionOrder
     */
    LGraph.prototype.updateExecutionOrder = function() {
        this._nodes_in_order = this.computeExecutionOrder(false);
        this._nodes_executable = [];
        for (var i = 0; i < this._nodes_in_order.length; ++i) {
            if (this._nodes_in_order[i].onExecute) {
                this._nodes_executable.push(this._nodes_in_order[i]);
            }
        }
    };

    //This is more internal, it computes the executable nodes in order and returns it
    LGraph.prototype.computeExecutionOrder = function(
        only_onExecute,
        set_level
    ) {
        var L = [];
        var S = [];
        var M = {};
        var visited_links = {}; //to avoid repeating links
        var remaining_links = {}; //to a

        //search for the nodes without inputs (starting nodes)
        for (var i = 0, l = this._nodes.length; i < l; ++i) {
            var node = this._nodes[i];
            if (only_onExecute && !node.onExecute) {
                continue;
            }

            M[node.id] = node; //add to pending nodes

            var num = 0; //num of input connections
            if (node.inputs) {
                for (var j = 0, l2 = node.inputs.length; j < l2; j++) {
                    if (node.inputs[j] && node.inputs[j].link != null) {
                        num += 1;
                    }
                }
            }

            if (num == 0) {
                //is a starting node
                S.push(node);
                if (set_level) {
                    node._level = 1;
                }
            } //num of input links
            else {
                if (set_level) {
                    node._level = 0;
                }
                remaining_links[node.id] = num;
            }
        }

        while (true) {
            if (S.length == 0) {
                break;
            }

            //get an starting node
            var node = S.shift();
            L.push(node); //add to ordered list
            delete M[node.id]; //remove from the pending nodes

            if (!node.outputs) {
                continue;
            }

            //for every output
            for (var i = 0; i < node.outputs.length; i++) {
                var output = node.outputs[i];
                //not connected
                if (
                    output == null ||
                    output.links == null ||
                    output.links.length == 0
                ) {
                    continue;
                }

                //for every connection
                for (var j = 0; j < output.links.length; j++) {
                    var link_id = output.links[j];
                    var link = this.links[link_id];
                    if (!link) {
                        continue;
                    }

                    //already visited link (ignore it)
                    if (visited_links[link.id]) {
                        continue;
                    }

                    var target_node = this.getNodeById(link.target_id);
                    if (target_node == null) {
                        visited_links[link.id] = true;
                        continue;
                    }

                    if (
                        set_level &&
                        (!target_node._level ||
                            target_node._level <= node._level)
                    ) {
                        target_node._level = node._level + 1;
                    }

                    visited_links[link.id] = true; //mark as visited
                    remaining_links[target_node.id] -= 1; //reduce the number of links remaining
                    if (remaining_links[target_node.id] == 0) {
                        S.push(target_node);
                    } //if no more links, then add to starters array
                }
            }
        }

        //the remaining ones (loops)
        for (var i in M) {
            L.push(M[i]);
        }

        if (L.length != this._nodes.length && LiteGraph.debug) {
            console.warn("something went wrong, nodes missing");
        }

        var l = L.length;

        //save order number in the node
        for (var i = 0; i < l; ++i) {
            L[i].order = i;
        }

        //sort now by priority
        L = L.sort(function(A, B) {
            var Ap = A.constructor.priority || A.priority || 0;
            var Bp = B.constructor.priority || B.priority || 0;
            if (Ap == Bp) {
                //if same priority, sort by order
                return A.order - B.order;
            }
            return Ap - Bp; //sort by priority
        });

        //save order number in the node, again...
        for (var i = 0; i < l; ++i) {
            L[i].order = i;
        }

        return L;
    };

    /**
     * Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively.
     * It doesn't include the node itself
     * @method getAncestors
     * @return {Array} an array with all the LGraphNodes that affect this node, in order of execution
     */
    LGraph.prototype.getAncestors = function(node) {
        var ancestors = [];
        var pending = [node];
        var visited = {};

        while (pending.length) {
            var current = pending.shift();
            if (!current.inputs) {
                continue;
            }
            if (!visited[current.id] && current != node) {
                visited[current.id] = true;
                ancestors.push(current);
            }

            for (var i = 0; i < current.inputs.length; ++i) {
                var input = current.getInputNode(i);
                if (input && ancestors.indexOf(input) == -1) {
                    pending.push(input);
                }
            }
        }

        ancestors.sort(function(a, b) {
            return a.order - b.order;
        });
        return ancestors;
    };

    /**
     * Positions every node in a more readable manner
     * @method arrange
     */
    LGraph.prototype.arrange = function (margin, layout) {
        margin = margin || 100;

        const nodes = this.computeExecutionOrder(false, true);
        const columns = [];
        for (let i = 0; i < nodes.length; ++i) {
            const node = nodes[i];
            const col = node._level || 1;
            if (!columns[col]) {
                columns[col] = [];
            }
            columns[col].push(node);
        }

        let x = margin;

        for (let i = 0; i < columns.length; ++i) {
            const column = columns[i];
            if (!column) {
                continue;
            }
            let max_size = 100;
            let y = margin + LiteGraph.NODE_TITLE_HEIGHT;
            for (let j = 0; j < column.length; ++j) {
                const node = column[j];
                node.pos[0] = (layout == LiteGraph.VERTICAL_LAYOUT) ? y : x;
                node.pos[1] = (layout == LiteGraph.VERTICAL_LAYOUT) ? x : y;
                const max_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 1 : 0;
                if (node.size[max_size_index] > max_size) {
                    max_size = node.size[max_size_index];
                }
                const node_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 0 : 1;
                y += node.size[node_size_index] + margin + LiteGraph.NODE_TITLE_HEIGHT;
            }
            x += max_size + margin;
        }

        this.setDirtyCanvas(true, true);
    };

    /**
     * Returns the amount of time the graph has been running in milliseconds
     * @method getTime
     * @return {number} number of milliseconds the graph has been running
     */
    LGraph.prototype.getTime = function() {
        return this.globaltime;
    };

    /**
     * Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant
     * @method getFixedTime
     * @return {number} number of milliseconds the graph has been running
     */

    LGraph.prototype.getFixedTime = function() {
        return this.fixedtime;
    };

    /**
     * Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct
     * if the nodes are using graphical actions
     * @method getElapsedTime
     * @return {number} number of milliseconds it took the last cycle
     */

    LGraph.prototype.getElapsedTime = function() {
        return this.elapsed_time;
    };

    /**
     * Sends an event to all the nodes, useful to trigger stuff
     * @method sendEventToAllNodes
     * @param {String} eventname the name of the event (function to be called)
     * @param {Array} params parameters in array format
     */
    LGraph.prototype.sendEventToAllNodes = function(eventname, params, mode) {
        mode = mode || LiteGraph.ALWAYS;

        var nodes = this._nodes_in_order ? this._nodes_in_order : this._nodes;
        if (!nodes) {
            return;
        }

        for (var j = 0, l = nodes.length; j < l; ++j) {
            var node = nodes[j];

            if (
                node.constructor === LiteGraph.Subgraph &&
                eventname != "onExecute"
            ) {
                if (node.mode == mode) {
                    node.sendEventToAllNodes(eventname, params, mode);
                }
                continue;
            }

            if (!node[eventname] || node.mode != mode) {
                continue;
            }
            if (params === undefined) {
                node[eventname]();
            } else if (params && params.constructor === Array) {
                node[eventname].apply(node, params);
            } else {
                node[eventname](params);
            }
        }
    };

    LGraph.prototype.sendActionToCanvas = function(action, params) {
        if (!this.list_of_graphcanvas) {
            return;
        }

        for (var i = 0; i < this.list_of_graphcanvas.length; ++i) {
            var c = this.list_of_graphcanvas[i];
            if (c[action]) {
                c[action].apply(c, params);
            }
        }
    };

    /**
     * Adds a new node instance to this graph
     * @method add
     * @param {LGraphNode} node the instance of the node
     */

    LGraph.prototype.add = function(node, skip_compute_order) {
        if (!node) {
            return;
        }

        //groups
        if (node.constructor === LGraphGroup) {
            this._groups.push(node);
            this.setDirtyCanvas(true);
            this.change();
            node.graph = this;
            this._version++;
            return;
        }

        //nodes
        if (node.id != -1 && this._nodes_by_id[node.id] != null) {
            console.warn(
                "LiteGraph: there is already a node with this ID, changing it"
            );
            if (LiteGraph.use_uuids) {
                node.id = LiteGraph.uuidv4();
            }
            else {
                node.id = ++this.last_node_id;
            }
        }

        if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) {
            throw "LiteGraph: max number of nodes in a graph reached";
        }

        //give him an id
        if (LiteGraph.use_uuids) {
            if (node.id == null || node.id == -1)
                node.id = LiteGraph.uuidv4();
        }
        else {
            if (node.id == null || node.id == -1) {
                node.id = ++this.last_node_id;
            } else if (this.last_node_id < node.id) {
                this.last_node_id = node.id;
            }
        }

        node.graph = this;
        this._version++;

        this._nodes.push(node);
        this._nodes_by_id[node.id] = node;

        if (node.onAdded) {
            node.onAdded(this);
        }

        if (this.config.align_to_grid) {
            node.alignToGrid();
        }

        if (!skip_compute_order) {
            this.updateExecutionOrder();
        }

        if (this.onNodeAdded) {
            this.onNodeAdded(node);
        }

        this.setDirtyCanvas(true);
        this.change();

        return node; //to chain actions
    };

    /**
     * Removes a node from the graph
     * @method remove
     * @param {LGraphNode} node the instance of the node
     */

    LGraph.prototype.remove = function(node) {
        if (node.constructor === LiteGraph.LGraphGroup) {
            var index = this._groups.indexOf(node);
            if (index != -1) {
                this._groups.splice(index, 1);
            }
            node.graph = null;
            this._version++;
            this.setDirtyCanvas(true, true);
            this.change();
            return;
        }

        if (this._nodes_by_id[node.id] == null) {
            return;
        } //not found

        if (node.ignore_remove) {
            return;
        } //cannot be removed

		this.beforeChange(); //sure? - almost sure is wrong

        //disconnect inputs
        if (node.inputs) {
            for (var i = 0; i < node.inputs.length; i++) {
                var slot = node.inputs[i];
                if (slot.link != null) {
                    node.disconnectInput(i);
                }
            }
        }

        //disconnect outputs
        if (node.outputs) {
            for (var i = 0; i < node.outputs.length; i++) {
                var slot = node.outputs[i];
                if (slot.links != null && slot.links.length) {
                    node.disconnectOutput(i);
                }
            }
        }

        //node.id = -1; //why?

        //callback
        if (node.onRemoved) {
            node.onRemoved();
        }

        node.graph = null;
        this._version++;

        //remove from canvas render
        if (this.list_of_graphcanvas) {
            for (var i = 0; i < this.list_of_graphcanvas.length; ++i) {
                var canvas = this.list_of_graphcanvas[i];
                if (canvas.selected_nodes[node.id]) {
                    delete canvas.selected_nodes[node.id];
                }
                if (canvas.node_dragged == node) {
                    canvas.node_dragged = null;
                }
            }
        }

        //remove from containers
        var pos = this._nodes.indexOf(node);
        if (pos != -1) {
            this._nodes.splice(pos, 1);
        }
        delete this._nodes_by_id[node.id];

        if (this.onNodeRemoved) {
            this.onNodeRemoved(node);
        }

		//close panels
		this.sendActionToCanvas("checkPanels");

        this.setDirtyCanvas(true, true);
		this.afterChange(); //sure? - almost sure is wrong
        this.change();

        this.updateExecutionOrder();
    };

    /**
     * Returns a node by its id.
     * @method getNodeById
     * @param {Number} id
     */

    LGraph.prototype.getNodeById = function(id) {
        if (id == null) {
            return null;
        }
        return this._nodes_by_id[id];
    };

    /**
     * Returns a list of nodes that matches a class
     * @method findNodesByClass
     * @param {Class} classObject the class itself (not an string)
     * @return {Array} a list with all the nodes of this type
     */
    LGraph.prototype.findNodesByClass = function(classObject, result) {
        result = result || [];
        result.length = 0;
        for (var i = 0, l = this._nodes.length; i < l; ++i) {
            if (this._nodes[i].constructor === classObject) {
                result.push(this._nodes[i]);
            }
        }
        return result;
    };

    /**
     * Returns a list of nodes that matches a type
     * @method findNodesByType
     * @param {String} type the name of the node type
     * @return {Array} a list with all the nodes of this type
     */
    LGraph.prototype.findNodesByType = function(type, result) {
        var type = type.toLowerCase();
        result = result || [];
        result.length = 0;
        for (var i = 0, l = this._nodes.length; i < l; ++i) {
            if (this._nodes[i].type.toLowerCase() == type) {
                result.push(this._nodes[i]);
            }
        }
        return result;
    };

    /**
     * Returns the first node that matches a name in its title
     * @method findNodeByTitle
     * @param {String} name the name of the node to search
     * @return {Node} the node or null
     */
    LGraph.prototype.findNodeByTitle = function(title) {
        for (var i = 0, l = this._nodes.length; i < l; ++i) {
            if (this._nodes[i].title == title) {
                return this._nodes[i];
            }
        }
        return null;
    };

    /**
     * Returns a list of nodes that matches a name
     * @method findNodesByTitle
     * @param {String} name the name of the node to search
     * @return {Array} a list with all the nodes with this name
     */
    LGraph.prototype.findNodesByTitle = function(title) {
        var result = [];
        for (var i = 0, l = this._nodes.length; i < l; ++i) {
            if (this._nodes[i].title == title) {
                result.push(this._nodes[i]);
            }
        }
        return result;
    };

    /**
     * Returns the top-most node in this position of the canvas
     * @method getNodeOnPos
     * @param {number} x the x coordinate in canvas space
     * @param {number} y the y coordinate in canvas space
     * @param {Array} nodes_list a list with all the nodes to search from, by default is all the nodes in the graph
     * @return {LGraphNode} the node at this position or null
     */
    LGraph.prototype.getNodeOnPos = function(x, y, nodes_list, margin) {
        nodes_list = nodes_list || this._nodes;
		var nRet = null;
        for (var i = nodes_list.length - 1; i >= 0; i--) {
            var n = nodes_list[i];
            if (n.isPointInside(x, y, margin)) {
                // check for lesser interest nodes (TODO check for overlapping, use the top)
				/*if (typeof n == "LGraphGroup"){
					nRet = n;
				}else{*/
					return n;
				/*}*/
            }
        }
        return nRet;
    };

    /**
     * Returns the top-most group in that position
     * @method getGroupOnPos
     * @param {number} x the x coordinate in canvas space
     * @param {number} y the y coordinate in canvas space
     * @return {LGraphGroup} the group or null
     */
    LGraph.prototype.getGroupOnPos = function(x, y) {
        for (var i = this._groups.length - 1; i >= 0; i--) {
            var g = this._groups[i];
            if (g.isPointInside(x, y, 2, true)) {
                return g;
            }
        }
        return null;
    };

    /**
     * Checks that the node type matches the node type registered, used when replacing a nodetype by a newer version during execution
     * this replaces the ones using the old version with the new version
     * @method checkNodeTypes
     */
    LGraph.prototype.checkNodeTypes = function() {
        var changes = false;
        for (var i = 0; i < this._nodes.length; i++) {
            var node = this._nodes[i];
            var ctor = LiteGraph.registered_node_types[node.type];
            if (node.constructor == ctor) {
                continue;
            }
            console.log("node being replaced by newer version: " + node.type);
            var newnode = LiteGraph.createNode(node.type);
            changes = true;
            this._nodes[i] = newnode;
            newnode.configure(node.serialize());
            newnode.graph = this;
            this._nodes_by_id[newnode.id] = newnode;
            if (node.inputs) {
                newnode.inputs = node.inputs.concat();
            }
            if (node.outputs) {
                newnode.outputs = node.outputs.concat();
            }
        }
        this.updateExecutionOrder();
    };

    // ********** GLOBALS *****************

    LGraph.prototype.onAction = function(action, param, options) {
        this._input_nodes = this.findNodesByClass(
            LiteGraph.GraphInput,
            this._input_nodes
        );
        for (var i = 0; i < this._input_nodes.length; ++i) {
            var node = this._input_nodes[i];
            if (node.properties.name != action) {
                continue;
            }
            //wrap node.onAction(action, param);
            node.actionDo(action, param, options);
            break;
        }
    };

    LGraph.prototype.trigger = function(action, param) {
        if (this.onTrigger) {
            this.onTrigger(action, param);
        }
    };

    /**
     * Tell this graph it has a global graph input of this type
     * @method addGlobalInput
     * @param {String} name
     * @param {String} type
     * @param {*} value [optional]
     */
    LGraph.prototype.addInput = function(name, type, value) {
        var input = this.inputs[name];
        if (input) {
            //already exist
            return;
        }

		this.beforeChange();
        this.inputs[name] = { name: name, type: type, value: value };
        this._version++;
		this.afterChange();

        if (this.onInputAdded) {
            this.onInputAdded(name, type);
        }

        if (this.onInputsOutputsChange) {
            this.onInputsOutputsChange();
        }
    };

    /**
     * Assign a data to the global graph input
     * @method setGlobalInputData
     * @param {String} name
     * @param {*} data
     */
    LGraph.prototype.setInputData = function(name, data) {
        var input = this.inputs[name];
        if (!input) {
            return;
        }
        input.value = data;
    };

    /**
     * Returns the current value of a global graph input
     * @method getInputData
     * @param {String} name
     * @return {*} the data
     */
    LGraph.prototype.getInputData = function(name) {
        var input = this.inputs[name];
        if (!input) {
            return null;
        }
        return input.value;
    };

    /**
     * Changes the name of a global graph input
     * @method renameInput
     * @param {String} old_name
     * @param {String} new_name
     */
    LGraph.prototype.renameInput = function(old_name, name) {
        if (name == old_name) {
            return;
        }

        if (!this.inputs[old_name]) {
            return false;
        }

        if (this.inputs[name]) {
            console.error("there is already one input with that name");
            return false;
        }

        this.inputs[name] = this.inputs[old_name];
        delete this.inputs[old_name];
        this._version++;

        if (this.onInputRenamed) {
            this.onInputRenamed(old_name, name);
        }

        if (this.onInputsOutputsChange) {
            this.onInputsOutputsChange();
        }
    };

    /**
     * Changes the type of a global graph input
     * @method changeInputType
     * @param {String} name
     * @param {String} type
     */
    LGraph.prototype.changeInputType = function(name, type) {
        if (!this.inputs[name]) {
            return false;
        }

        if (
            this.inputs[name].type &&
            String(this.inputs[name].type).toLowerCase() ==
                String(type).toLowerCase()
        ) {
            return;
        }

        this.inputs[name].type = type;
        this._version++;
        if (this.onInputTypeChanged) {
            this.onInputTypeChanged(name, type);
        }
    };

    /**
     * Removes a global graph input
     * @method removeInput
     * @param {String} name
     * @param {String} type
     */
    LGraph.prototype.removeInput = function(name) {
        if (!this.inputs[name]) {
            return false;
        }

        delete this.inputs[name];
        this._version++;

        if (this.onInputRemoved) {
            this.onInputRemoved(name);
        }

        if (this.onInputsOutputsChange) {
            this.onInputsOutputsChange();
        }
        return true;
    };

    /**
     * Creates a global graph output
     * @method addOutput
     * @param {String} name
     * @param {String} type
     * @param {*} value
     */
    LGraph.prototype.addOutput = function(name, type, value) {
        this.outputs[name] = { name: name, type: type, value: value };
        this._version++;

        if (this.onOutputAdded) {
            this.onOutputAdded(name, type);
        }

        if (this.onInputsOutputsChange) {
            this.onInputsOutputsChange();
        }
    };

    /**
     * Assign a data to the global output
     * @method setOutputData
     * @param {String} name
     * @param {String} value
     */
    LGraph.prototype.setOutputData = function(name, value) {
        var output = this.outputs[name];
        if (!output) {
            return;
        }
        output.value = value;
    };

    /**
     * Returns the current value of a global graph output
     * @method getOutputData
     * @param {String} name
     * @return {*} the data
     */
    LGraph.prototype.getOutputData = function(name) {
        var output = this.outputs[name];
        if (!output) {
            return null;
        }
        return output.value;
    };

    /**
     * Renames a global graph output
     * @method renameOutput
     * @param {String} old_name
     * @param {String} new_name
     */
    LGraph.prototype.renameOutput = function(old_name, name) {
        if (!this.outputs[old_name]) {
            return false;
        }

        if (this.outputs[name]) {
            console.error("there is already one output with that name");
            return false;
        }

        this.outputs[name] = this.outputs[old_name];
        delete this.outputs[old_name];
        this._version++;

        if (this.onOutputRenamed) {
            this.onOutputRenamed(old_name, name);
        }

        if (this.onInputsOutputsChange) {
            this.onInputsOutputsChange();
        }
    };

    /**
     * Changes the type of a global graph output
     * @method changeOutputType
     * @param {String} name
     * @param {String} type
     */
    LGraph.prototype.changeOutputType = function(name, type) {
        if (!this.outputs[name]) {
            return false;
        }

        if (
            this.outputs[name].type &&
            String(this.outputs[name].type).toLowerCase() ==
                String(type).toLowerCase()
        ) {
            return;
        }

        this.outputs[name].type = type;
        this._version++;
        if (this.onOutputTypeChanged) {
            this.onOutputTypeChanged(name, type);
        }
    };

    /**
     * Removes a global graph output
     * @method removeOutput
     * @param {String} name
     */
    LGraph.prototype.removeOutput = function(name) {
        if (!this.outputs[name]) {
            return false;
        }
        delete this.outputs[name];
        this._version++;

        if (this.onOutputRemoved) {
            this.onOutputRemoved(name);
        }

        if (this.onInputsOutputsChange) {
            this.onInputsOutputsChange();
        }
        return true;
    };

    LGraph.prototype.triggerInput = function(name, value) {
        var nodes = this.findNodesByTitle(name);
        for (var i = 0; i < nodes.length; ++i) {
            nodes[i].onTrigger(value);
        }
    };

    LGraph.prototype.setCallback = function(name, func) {
        var nodes = this.findNodesByTitle(name);
        for (var i = 0; i < nodes.length; ++i) {
            nodes[i].setTrigger(func);
        }
    };

	//used for undo, called before any change is made to the graph
    LGraph.prototype.beforeChange = function(info) {
        if (this.onBeforeChange) {
            this.onBeforeChange(this,info);
        }
        this.sendActionToCanvas("onBeforeChange", this);
    };

	//used to resend actions, called after any change is made to the graph
    LGraph.prototype.afterChange = function(info) {
        if (this.onAfterChange) {
            this.onAfterChange(this,info);
        }
        this.sendActionToCanvas("onAfterChange", this);
    };

    LGraph.prototype.connectionChange = function(node, link_info) {
        this.updateExecutionOrder();
        if (this.onConnectionChange) {
            this.onConnectionChange(node);
        }
        this._version++;
        this.sendActionToCanvas("onConnectionChange");
    };

    /**
     * returns if the graph is in live mode
     * @method isLive
     */

    LGraph.prototype.isLive = function() {
        if (!this.list_of_graphcanvas) {
            return false;
        }

        for (var i = 0; i < this.list_of_graphcanvas.length; ++i) {
            var c = this.list_of_graphcanvas[i];
            if (c.live_mode) {
                return true;
            }
        }
        return false;
    };

    /**
     * clears the triggered slot animation in all links (stop visual animation)
     * @method clearTriggeredSlots
     */
    LGraph.prototype.clearTriggeredSlots = function() {
        for (var i in this.links) {
            var link_info = this.links[i];
            if (!link_info) {
                continue;
            }
            if (link_info._last_time) {
                link_info._last_time = 0;
            }
        }
    };

    /* Called when something visually changed (not the graph!) */
    LGraph.prototype.change = function() {
        if (LiteGraph.debug) {
            console.log("Graph changed");
        }
        this.sendActionToCanvas("setDirty", [true, true]);
        if (this.on_change) {
            this.on_change(this);
        }
    };

    LGraph.prototype.setDirtyCanvas = function(fg, bg) {
        this.sendActionToCanvas("setDirty", [fg, bg]);
    };

    /**
     * Destroys a link
     * @method removeLink
     * @param {Number} link_id
     */
    LGraph.prototype.removeLink = function(link_id) {
        var link = this.links[link_id];
        if (!link) {
            return;
        }
        var node = this.getNodeById(link.target_id);
        if (node) {
            node.disconnectInput(link.target_slot);
        }
    };

    //save and recover app state ***************************************
    /**
     * Creates a Object containing all the info about this graph, it can be serialized
     * @method serialize
     * @return {Object} value of the node
     */
    LGraph.prototype.serialize = function() {
        var nodes_info = [];
        for (var i = 0, l = this._nodes.length; i < l; ++i) {
            nodes_info.push(this._nodes[i].serialize());
        }

        //pack link info into a non-verbose format
        var links = [];
        for (var i in this.links) {
            //links is an OBJECT
            var link = this.links[i];
            if (!link.serialize) {
                //weird bug I havent solved yet
                console.warn(
                    "weird LLink bug, link info is not a LLink but a regular object"
                );
                var link2 = new LLink();
                for (var j in link) { 
                    link2[j] = link[j];
                }
                this.links[i] = link2;
                link = link2;
            }

            links.push(link.serialize());
        }

        var groups_info = [];
        for (var i = 0; i < this._groups.length; ++i) {
            groups_info.push(this._groups[i].serialize());
        }

        var data = {
            last_node_id: this.last_node_id,
            last_link_id: this.last_link_id,
            nodes: nodes_info,
            links: links,
            groups: groups_info,
            config: this.config,
			extra: this.extra,
            version: LiteGraph.VERSION
        };

		if(this.onSerialize)
			this.onSerialize(data);

        return data;
    };

    /**
     * Configure a graph from a JSON string
     * @method configure
     * @param {String} str configure a graph from a JSON string
     * @param {Boolean} returns if there was any error parsing
     */
    LGraph.prototype.configure = function(data, keep_old) {
        if (!data) {
            return;
        }

        if (!keep_old) {
            this.clear();
        }

        var nodes = data.nodes;

        //decode links info (they are very verbose)
        if (data.links && data.links.constructor === Array) {
            var links = [];
            for (var i = 0; i < data.links.length; ++i) {
                var link_data = data.links[i];
				if(!link_data) //weird bug
				{
					console.warn("serialized graph link data contains errors, skipping.");
					continue;
				}
                var link = new LLink();
                link.configure(link_data);
                links[link.id] = link;
            }
            data.links = links;
        }

        //copy all stored fields
        for (var i in data) {
			if(i == "nodes" || i == "groups" ) //links must be accepted
				continue;
            this[i] = data[i];
        }

        var error = false;

        //create nodes
        this._nodes = [];
        if (nodes) {
            for (var i = 0, l = nodes.length; i < l; ++i) {
                var n_info = nodes[i]; //stored info
                var node = LiteGraph.createNode(n_info.type, n_info.title);
                if (!node) {
                    if (LiteGraph.debug) {
                        console.log(
                            "Node not found or has errors: " + n_info.type
                        );
                    }

                    //in case of error we create a replacement node to avoid losing info
                    node = new LGraphNode();
                    node.last_serialization = n_info;
                    node.has_errors = true;
                    error = true;
                    //continue;
                }

                node.id = n_info.id; //id it or it will create a new id
                this.add(node, true); //add before configure, otherwise configure cannot create links
            }

            //configure nodes afterwards so they can reach each other
            for (var i = 0, l = nodes.length; i < l; ++i) {
                var n_info = nodes[i];
                var node = this.getNodeById(n_info.id);
                if (node) {
                    node.configure(n_info);
                }
            }
        }

        //groups
        this._groups.length = 0;
        if (data.groups) {
            for (var i = 0; i < data.groups.length; ++i) {
                var group = new LiteGraph.LGraphGroup();
                group.configure(data.groups[i]);
                this.add(group);
            }
        }

        this.updateExecutionOrder();

		this.extra = data.extra || {};

		if(this.onConfigure)
			this.onConfigure(data);

        this._version++;
        this.setDirtyCanvas(true, true);
        return error;
    };

    LGraph.prototype.load = function(url, callback) {
        var that = this;

		//from file
		if(url.constructor === File || url.constructor === Blob)
		{
			var reader = new FileReader();
			reader.addEventListener('load', function(event) {
				var data = JSON.parse(event.target.result);
				that.configure(data);
				if(callback)
					callback();
			});
			
			reader.readAsText(url);
			return;
		}

		//is a string, then an URL
        var req = new XMLHttpRequest();
        req.open("GET", url, true);
        req.send(null);
        req.onload = function(oEvent) {
            if (req.status !== 200) {
                console.error("Error loading graph:", req.status, req.response);
                return;
            }
            var data = JSON.parse( req.response );
            that.configure(data);
			if(callback)
				callback();
        };
        req.onerror = function(err) {
            console.error("Error loading graph:", err);
        };
    };

    LGraph.prototype.onNodeTrace = function(node, msg, color) {
        //TODO
    };

    //this is the class in charge of storing link information
    function LLink(id, type, origin_id, origin_slot, target_id, target_slot) {
        this.id = id;
        this.type = type;
        this.origin_id = origin_id;
        this.origin_slot = origin_slot;
        this.target_id = target_id;
        this.target_slot = target_slot;

        this._data = null;
        this._pos = new Float32Array(2); //center
    }

    LLink.prototype.configure = function(o) {
        if (o.constructor === Array) {
            this.id = o[0];
            this.origin_id = o[1];
            this.origin_slot = o[2];
            this.target_id = o[3];
            this.target_slot = o[4];
            this.type = o[5];
        } else {
            this.id = o.id;
            this.type = o.type;
            this.origin_id = o.origin_id;
            this.origin_slot = o.origin_slot;
            this.target_id = o.target_id;
            this.target_slot = o.target_slot;
        }
    };

    LLink.prototype.serialize = function() {
        return [
            this.id,
            this.origin_id,
            this.origin_slot,
            this.target_id,
            this.target_slot,
            this.type
        ];
    };

    LiteGraph.LLink = LLink;

    // *************************************************************
    //   Node CLASS                                          *******
    // *************************************************************

    /*
	title: string
	pos: [x,y]
	size: [x,y]

	input|output: every connection
		+  { name:string, type:string, pos: [x,y]=Optional, direction: "input"|"output", links: Array });

	general properties:
		+ clip_area: if you render outside the node, it will be clipped
		+ unsafe_execution: not allowed for safe execution
		+ skip_repeated_outputs: when adding new outputs, it wont show if there is one already connected
		+ resizable: if set to false it wont be resizable with the mouse
		+ horizontal: slots are distributed horizontally
		+ widgets_start_y: widgets start at y distance from the top of the node
	
	flags object:
		+ collapsed: if it is collapsed

	supported callbacks:
		+ onAdded: when added to graph (warning: this is called BEFORE the node is configured when loading)
		+ onRemoved: when removed from graph
		+ onStart:	when the graph starts playing
		+ onStop:	when the graph stops playing
		+ onDrawForeground: render the inside widgets inside the node
		+ onDrawBackground: render the background area inside the node (only in edit mode)
		+ onMouseDown
		+ onMouseMove
		+ onMouseUp
		+ onMouseEnter
		+ onMouseLeave
		+ onExecute: execute the node
		+ onPropertyChanged: when a property is changed in the panel (return true to skip default behaviour)
		+ onGetInputs: returns an array of possible inputs
		+ onGetOutputs: returns an array of possible outputs
		+ onBounding: in case this node has a bigger bounding than the node itself (the callback receives the bounding as [x,y,w,h])
		+ onDblClick: double clicked in the node
		+ onInputDblClick: input slot double clicked (can be used to automatically create a node connected)
		+ onOutputDblClick: output slot double clicked (can be used to automatically create a node connected)
		+ onConfigure: called after the node has been configured
		+ onSerialize: to add extra info when serializing (the callback receives the object that should be filled with the data)
		+ onSelected
		+ onDeselected
		+ onDropItem : DOM item dropped over the node
		+ onDropFile : file dropped over the node
		+ onConnectInput : if returns false the incoming connection will be canceled
		+ onConnectionsChange : a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info )
		+ onAction: action slot triggered
		+ getExtraMenuOptions: to add option to context menu
*/

    /**
     * Base Class for all the node type classes
     * @class LGraphNode
     * @param {String} name a name for the node
     */

    function LGraphNode(title) {
        this._ctor(title);
    }

    global.LGraphNode = LiteGraph.LGraphNode = LGraphNode;

    LGraphNode.prototype._ctor = function(title) {
        this.title = title || "Unnamed";
        this.size = [LiteGraph.NODE_WIDTH, 60];
        this.graph = null;

        this._pos = new Float32Array(10, 10);

        Object.defineProperty(this, "pos", {
            set: function(v) {
                if (!v || v.length < 2) {
                    return;
                }
                this._pos[0] = v[0];
                this._pos[1] = v[1];
            },
            get: function() {
                return this._pos;
            },
            enumerable: true
        });

        if (LiteGraph.use_uuids) {
            this.id = LiteGraph.uuidv4();
        }
        else {
            this.id = -1; //not know till not added
        }
        this.type = null;

        //inputs available: array of inputs
        this.inputs = [];
        this.outputs = [];
        this.connections = [];

        //local data
        this.properties = {}; //for the values
        this.properties_info = []; //for the info

        this.flags = {};
    };

    /**
     * configure a node from an object containing the serialized info
     * @method configure
     */
    LGraphNode.prototype.configure = function(info) {
        if (this.graph) {
            this.graph._version++;
        }
        for (var j in info) {
            if (j == "properties") {
                //i don't want to clone properties, I want to reuse the old container
                for (var k in info.properties) {
                    this.properties[k] = info.properties[k];
                    if (this.onPropertyChanged) {
                        this.onPropertyChanged( k, info.properties[k] );
                    }
                }
                continue;
            }

            if (info[j] == null) {
                continue;
            } else if (typeof info[j] == "object") {
                //object
                if (this[j] && this[j].configure) {
                    this[j].configure(info[j]);
                } else {
                    this[j] = LiteGraph.cloneObject(info[j], this[j]);
                }
            } //value
            else {
                this[j] = info[j];
            }
        }

        if (!info.title) {
            this.title = this.constructor.title;
        }

		if (this.inputs) {
			for (var i = 0; i < this.inputs.length; ++i) {
				var input = this.inputs[i];
				var link_info = this.graph ? this.graph.links[input.link] : null;
				if (this.onConnectionsChange)
					this.onConnectionsChange( LiteGraph.INPUT, i, true, link_info, input ); //link_info has been created now, so its updated

				if( this.onInputAdded )
					this.onInputAdded(input);

			}
		}

		if (this.outputs) {
			for (var i = 0; i < this.outputs.length; ++i) {
				var output = this.outputs[i];
				if (!output.links) {
					continue;
				}
				for (var j = 0; j < output.links.length; ++j) {
					var link_info = this.graph 	? this.graph.links[output.links[j]] : null;
					if (this.onConnectionsChange)
						this.onConnectionsChange( LiteGraph.OUTPUT, i, true, link_info, output ); //link_info has been created now, so its updated
				}

				if( this.onOutputAdded )
					this.onOutputAdded(output);
			}
        }

		if( this.widgets )
		{
			for (var i = 0; i < this.widgets.length; ++i)
			{
				var w = this.widgets[i];
				if(!w)
					continue;
				if(w.options && w.options.property && (this.properties[ w.options.property ] != undefined))
					w.value = JSON.parse( JSON.stringify( this.properties[ w.options.property ] ) );
			}
			if (info.widgets_values) {
				for (var i = 0; i < info.widgets_values.length; ++i) {
					if (this.widgets[i]) {
						this.widgets[i].value = info.widgets_values[i];
					}
				}
			}
		}

        if (this.onConfigure) {
            this.onConfigure(info);
        }
    };

    /**
     * serialize the content
     * @method serialize
     */

    LGraphNode.prototype.serialize = function() {
        //create serialization object
        var o = {
            id: this.id,
            type: this.type,
            pos: this.pos,
            size: this.size,
            flags: LiteGraph.cloneObject(this.flags),
			order: this.order,
            mode: this.mode
        };

        //special case for when there were errors
        if (this.constructor === LGraphNode && this.last_serialization) {
            return this.last_serialization;
        }

        if (this.inputs) {
            o.inputs = this.inputs;
        }

        if (this.outputs) {
            //clear outputs last data (because data in connections is never serialized but stored inside the outputs info)
            for (var i = 0; i < this.outputs.length; i++) {
                delete this.outputs[i]._data;
            }
            o.outputs = this.outputs;
        }

        if (this.title && this.title != this.constructor.title) {
            o.title = this.title;
        }

        if (this.properties) {
            o.properties = LiteGraph.cloneObject(this.properties);
        }

        if (this.widgets && this.serialize_widgets) {
            o.widgets_values = [];
            for (var i = 0; i < this.widgets.length; ++i) {
				if(this.widgets[i])
	                o.widgets_values[i] = this.widgets[i].value;
				else
					o.widgets_values[i] = null;
            }
        }

        if (!o.type) {
            o.type = this.constructor.type;
        }

        if (this.color) {
            o.color = this.color;
        }
        if (this.bgcolor) {
            o.bgcolor = this.bgcolor;
        }
        if (this.boxcolor) {
            o.boxcolor = this.boxcolor;
        }
        if (this.shape) {
            o.shape = this.shape;
        }

        if (this.onSerialize) {
            if (this.onSerialize(o)) {
                console.warn(
                    "node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter"
                );
            }
        }

        return o;
    };

    /* Creates a clone of this node */
    LGraphNode.prototype.clone = function() {
        var node = LiteGraph.createNode(this.type);
        if (!node) {
            return null;
        }

        //we clone it because serialize returns shared containers
        var data = LiteGraph.cloneObject(this.serialize());

        //remove links
        if (data.inputs) {
            for (var i = 0; i < data.inputs.length; ++i) {
                data.inputs[i].link = null;
            }
        }

        if (data.outputs) {
            for (var i = 0; i < data.outputs.length; ++i) {
                if (data.outputs[i].links) {
                    data.outputs[i].links.length = 0;
                }
            }
        }

        delete data["id"];

        if (LiteGraph.use_uuids) {
            data["id"] = LiteGraph.uuidv4()
        }

        //remove links
        node.configure(data);

        return node;
    };

    /**
     * serialize and stringify
     * @method toString
     */

    LGraphNode.prototype.toString = function() {
        return JSON.stringify(this.serialize());
    };
    //LGraphNode.prototype.deserialize = function(info) {} //this cannot be done from within, must be done in LiteGraph

    /**
     * get the title string
     * @method getTitle
     */

    LGraphNode.prototype.getTitle = function() {
        return this.title || this.constructor.title;
    };

    /**
     * sets the value of a property
     * @method setProperty
     * @param {String} name
     * @param {*} value
     */
    LGraphNode.prototype.setProperty = function(name, value) {
        if (!this.properties) {
            this.properties = {};
        }
		if( value === this.properties[name] )
			return;
		var prev_value = this.properties[name];
        this.properties[name] = value;
        if (this.onPropertyChanged) {
            if( this.onPropertyChanged(name, value, prev_value) === false ) //abort change
				this.properties[name] = prev_value;
        }
		if(this.widgets) //widgets could be linked to properties
			for(var i = 0; i < this.widgets.length; ++i)
			{
				var w = this.widgets[i];
				if(!w)
					continue;
				if(w.options.property == name)
				{
					w.value = value;
					break;
				}
			}
    };

    // Execution *************************
    /**
     * sets the output data
     * @method setOutputData
     * @param {number} slot
     * @param {*} data
     */
    LGraphNode.prototype.setOutputData = function(slot, data) {
        if (!this.outputs) {
            return;
        }

        //this maybe slow and a niche case
        //if(slot && slot.constructor === String)
        //	slot = this.findOutputSlot(slot);

        if (slot == -1 || slot >= this.outputs.length) {
            return;
        }

        var output_info = this.outputs[slot];
        if (!output_info) {
            return;
        }

        //store data in the output itself in case we want to debug
        output_info._data = data;

        //if there are connections, pass the data to the connections
        if (this.outputs[slot].links) {
            for (var i = 0; i < this.outputs[slot].links.length; i++) {
                var link_id = this.outputs[slot].links[i];
				var link = this.graph.links[link_id];
				if(link)
					link.data = data;
            }
        }
    };

    /**
     * sets the output data type, useful when you want to be able to overwrite the data type
     * @method setOutputDataType
     * @param {number} slot
     * @param {String} datatype
     */
    LGraphNode.prototype.setOutputDataType = function(slot, type) {
        if (!this.outputs) {
            return;
        }
        if (slot == -1 || slot >= this.outputs.length) {
            return;
        }
        var output_info = this.outputs[slot];
        if (!output_info) {
            return;
        }
        //store data in the output itself in case we want to debug
        output_info.type = type;

        //if there are connections, pass the data to the connections
        if (this.outputs[slot].links) {
            for (var i = 0; i < this.outputs[slot].links.length; i++) {
                var link_id = this.outputs[slot].links[i];
                this.graph.links[link_id].type = type;
            }
        }
    };

    /**
     * Retrieves the input data (data traveling through the connection) from one slot
     * @method getInputData
     * @param {number} slot
     * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link
     * @return {*} data or if it is not connected returns undefined
     */
    LGraphNode.prototype.getInputData = function(slot, force_update) {
        if (!this.inputs) {
            return;
        } //undefined;

        if (slot >= this.inputs.length || this.inputs[slot].link == null) {
            return;
        }

        var link_id = this.inputs[slot].link;
        var link = this.graph.links[link_id];
        if (!link) {
            //bug: weird case but it happens sometimes
            return null;
        }

        if (!force_update) {
            return link.data;
        }

        //special case: used to extract data from the incoming connection before the graph has been executed
        var node = this.graph.getNodeById(link.origin_id);
        if (!node) {
            return link.data;
        }

        if (node.updateOutputData) {
            node.updateOutputData(link.origin_slot);
        } else if (node.onExecute) {
            node.onExecute();
        }

        return link.data;
    };

    /**
     * Retrieves the input data type (in case this supports multiple input types)
     * @method getInputDataType
     * @param {number} slot
     * @return {String} datatype in string format
     */
    LGraphNode.prototype.getInputDataType = function(slot) {
        if (!this.inputs) {
            return null;
        } //undefined;

        if (slot >= this.inputs.length || this.inputs[slot].link == null) {
            return null;
        }
        var link_id = this.inputs[slot].link;
        var link = this.graph.links[link_id];
        if (!link) {
            //bug: weird case but it happens sometimes
            return null;
        }
        var node = this.graph.getNodeById(link.origin_id);
        if (!node) {
            return link.type;
        }
        var output_info = node.outputs[link.origin_slot];
        if (output_info) {
            return output_info.type;
        }
        return null;
    };

    /**
     * Retrieves the input data from one slot using its name instead of slot number
     * @method getInputDataByName
     * @param {String} slot_name
     * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link
     * @return {*} data or if it is not connected returns null
     */
    LGraphNode.prototype.getInputDataByName = function(
        slot_name,
        force_update
    ) {
        var slot = this.findInputSlot(slot_name);
        if (slot == -1) {
            return null;
        }
        return this.getInputData(slot, force_update);
    };

    /**
     * tells you if there is a connection in one input slot
     * @method isInputConnected
     * @param {number} slot
     * @return {boolean}
     */
    LGraphNode.prototype.isInputConnected = function(slot) {
        if (!this.inputs) {
            return false;
        }
        return slot < this.inputs.length && this.inputs[slot].link != null;
    };

    /**
     * tells you info about an input connection (which node, type, etc)
     * @method getInputInfo
     * @param {number} slot
     * @return {Object} object or null { link: id, name: string, type: string or 0 }
     */
    LGraphNode.prototype.getInputInfo = function(slot) {
        if (!this.inputs) {
            return null;
        }
        if (slot < this.inputs.length) {
            return this.inputs[slot];
        }
        return null;
    };

    /**
     * Returns the link info in the connection of an input slot
     * @method getInputLink
     * @param {number} slot
     * @return {LLink} object or null
     */
    LGraphNode.prototype.getInputLink = function(slot) {
        if (!this.inputs) {
            return null;
        }
        if (slot < this.inputs.length) {
            var slot_info = this.inputs[slot];
			return this.graph.links[ slot_info.link ];
        }
        return null;
    };

    /**
     * returns the node connected in the input slot
     * @method getInputNode
     * @param {number} slot
     * @return {LGraphNode} node or null
     */
    LGraphNode.prototype.getInputNode = function(slot) {
        if (!this.inputs) {
            return null;
        }
        if (slot >= this.inputs.length) {
            return null;
        }
        var input = this.inputs[slot];
        if (!input || input.link === null) {
            return null;
        }
        var link_info = this.graph.links[input.link];
        if (!link_info) {
            return null;
        }
        return this.graph.getNodeById(link_info.origin_id);
    };

    /**
     * returns the value of an input with this name, otherwise checks if there is a property with that name
     * @method getInputOrProperty
     * @param {string} name
     * @return {*} value
     */
    LGraphNode.prototype.getInputOrProperty = function(name) {
        if (!this.inputs || !this.inputs.length) {
            return this.properties ? this.properties[name] : null;
        }

        for (var i = 0, l = this.inputs.length; i < l; ++i) {
            var input_info = this.inputs[i];
            if (name == input_info.name && input_info.link != null) {
                var link = this.graph.links[input_info.link];
                if (link) {
                    return link.data;
                }
            }
        }
        return this.properties[name];
    };

    /**
     * tells you the last output data that went in that slot
     * @method getOutputData
     * @param {number} slot
     * @return {Object}  object or null
     */
    LGraphNode.prototype.getOutputData = function(slot) {
        if (!this.outputs) {
            return null;
        }
        if (slot >= this.outputs.length) {
            return null;
        }

        var info = this.outputs[slot];
        return info._data;
    };

    /**
     * tells you info about an output connection (which node, type, etc)
     * @method getOutputInfo
     * @param {number} slot
     * @return {Object}  object or null { name: string, type: string, links: [ ids of links in number ] }
     */
    LGraphNode.prototype.getOutputInfo = function(slot) {
        if (!this.outputs) {
            return null;
        }
        if (slot < this.outputs.length) {
            return this.outputs[slot];
        }
        return null;
    };

    /**
     * tells you if there is a connection in one output slot
     * @method isOutputConnected
     * @param {number} slot
     * @return {boolean}
     */
    LGraphNode.prototype.isOutputConnected = function(slot) {
        if (!this.outputs) {
            return false;
        }
        return (
            slot < this.outputs.length &&
            this.outputs[slot].links &&
            this.outputs[slot].links.length
        );
    };

    /**
     * tells you if there is any connection in the output slots
     * @method isAnyOutputConnected
     * @return {boolean}
     */
    LGraphNode.prototype.isAnyOutputConnected = function() {
        if (!this.outputs) {
            return false;
        }
        for (var i = 0; i < this.outputs.length; ++i) {
            if (this.outputs[i].links && this.outputs[i].links.length) {
                return true;
            }
        }
        return false;
    };

    /**
     * retrieves all the nodes connected to this output slot
     * @method getOutputNodes
     * @param {number} slot
     * @return {array}
     */
    LGraphNode.prototype.getOutputNodes = function(slot) {
        if (!this.outputs || this.outputs.length == 0) {
            return null;
        }

        if (slot >= this.outputs.length) {
            return null;
        }

        var output = this.outputs[slot];
        if (!output.links || output.links.length == 0) {
            return null;
        }

        var r = [];
        for (var i = 0; i < output.links.length; i++) {
            var link_id = output.links[i];
            var link = this.graph.links[link_id];
            if (link) {
                var target_node = this.graph.getNodeById(link.target_id);
                if (target_node) {
                    r.push(target_node);
                }
            }
        }
        return r;
    };

    LGraphNode.prototype.addOnTriggerInput = function(){
        var trigS = this.findInputSlot("onTrigger");
        if (trigS == -1){ //!trigS || 
            var input = this.addInput("onTrigger", LiteGraph.EVENT, {optional: true, nameLocked: true});
            return this.findInputSlot("onTrigger");
        }
        return trigS;
    }
    
    LGraphNode.prototype.addOnExecutedOutput = function(){
        var trigS = this.findOutputSlot("onExecuted");
        if (trigS == -1){ //!trigS || 
            var output = this.addOutput("onExecuted", LiteGraph.ACTION, {optional: true, nameLocked: true});
            return this.findOutputSlot("onExecuted");
        }
        return trigS;
    }
    
    LGraphNode.prototype.onAfterExecuteNode = function(param, options){
        var trigS = this.findOutputSlot("onExecuted");
        if (trigS != -1){
            
            //console.debug(this.id+":"+this.order+" triggering slot onAfterExecute");
            //console.debug(param);
            //console.debug(options);
            this.triggerSlot(trigS, param, null, options);
            
        }
    }    
    
    LGraphNode.prototype.changeMode = function(modeTo){
        switch(modeTo){
            case LiteGraph.ON_EVENT:
                // this.addOnExecutedOutput();
                break;
                
            case LiteGraph.ON_TRIGGER:
                this.addOnTriggerInput();
                this.addOnExecutedOutput();
                break;
                
            case LiteGraph.NEVER:
                break;
                
            case LiteGraph.ALWAYS:
                break;
                
            case LiteGraph.ON_REQUEST:
                break;
            
            default:
                return false;
                break;
        }
        this.mode = modeTo;
        return true;
    };

    /**
     * Triggers the execution of actions that were deferred when the action was triggered
     * @method executePendingActions
     */    
    LGraphNode.prototype.executePendingActions = function() {
        if(!this._waiting_actions || !this._waiting_actions.length)
            return;
        for(var i = 0; i < this._waiting_actions.length;++i)
        {
            var p = this._waiting_actions[i];
            this.onAction(p[0],p[1],p[2],p[3],p[4]);
        }        
        this._waiting_actions.length = 0;
    }

    
    /**
     * Triggers the node code execution, place a boolean/counter to mark the node as being executed
     * @method doExecute
     * @param {*} param
     * @param {*} options
     */
    LGraphNode.prototype.doExecute = function(param, options) {
        options = options || {};
        if (this.onExecute){
            
            // enable this to give the event an ID
			if (!options.action_call) options.action_call = this.id+"_exec_"+Math.floor(Math.random()*9999);
            
            this.graph.nodes_executing[this.id] = true; //.push(this.id);

            this.onExecute(param, options);
            
            this.graph.nodes_executing[this.id] = false; //.pop();
            
            // save execution/action ref
            this.exec_version = this.graph.iteration;
            if(options && options.action_call){
                this.action_call = options.action_call; // if (param)
                this.graph.nodes_executedAction[this.id] = options.action_call;
            }
        }
        else {
        }
        this.execute_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event
        if(this.onAfterExecuteNode) this.onAfterExecuteNode(param, options); // callback
    };
    
    /**
     * Triggers an action, wrapped by logics to control execution flow
     * @method actionDo
     * @param {String} action name
     * @param {*} param
     */
    LGraphNode.prototype.actionDo = function(action, param, options, action_slot ) {
        options = options || {};
        if (this.onAction){
            
			// enable this to give the event an ID
            if (!options.action_call) options.action_call = this.id+"_"+(action?action:"action")+"_"+Math.floor(Math.random()*9999);
            
            this.graph.nodes_actioning[this.id] = (action?action:"actioning"); //.push(this.id);
            
            this.onAction(action, param, options, action_slot);
            
            this.graph.nodes_actioning[this.id] = false; //.pop();
            
            // save execution/action ref
            if(options && options.action_call){
                this.action_call = options.action_call; // if (param)
                this.graph.nodes_executedAction[this.id] = options.action_call;
            }
        }
        this.action_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event
        if(this.onAfterExecuteNode) this.onAfterExecuteNode(param, options);
    };
    
    /**
     * Triggers an event in this node, this will trigger any output with the same name
     * @method trigger
     * @param {String} event name ( "on_play", ... ) if action is equivalent to false then the event is send to all
     * @param {*} param
     */
    LGraphNode.prototype.trigger = function(action, param, options) {
        if (!this.outputs || !this.outputs.length) {
            return;
        }

        if (this.graph)
            this.graph._last_trigger_time = LiteGraph.getTime();

        for (var i = 0; i < this.outputs.length; ++i) {
            var output = this.outputs[i];
            if ( !output || output.type !== LiteGraph.EVENT || (action && output.name != action) )
                continue;
            this.triggerSlot(i, param, null, options);
        }
    };

    /**
     * Triggers a slot event in this node: cycle output slots and launch execute/action on connected nodes
     * @method triggerSlot
     * @param {Number} slot the index of the output slot
     * @param {*} param
     * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot
     */
    LGraphNode.prototype.triggerSlot = function(slot, param, link_id, options) {
        options = options || {};
        if (!this.outputs) {
            return;
        }

		if(slot == null)
		{
			console.error("slot must be a number");
			return;
		}

		if(slot.constructor !== Number)
			console.warn("slot must be a number, use node.trigger('name') if you want to use a string");

        var output = this.outputs[slot];
        if (!output) {
            return;
        }

        var links = output.links;
        if (!links || !links.length) {
            return;
        }

        if (this.graph) {
            this.graph._last_trigger_time = LiteGraph.getTime();
        }

        //for every link attached here
        for (var k = 0; k < links.length; ++k) {
            var id = links[k];
            if (link_id != null && link_id != id) {
                //to skip links
                continue;
            }
            var link_info = this.graph.links[links[k]];
            if (!link_info) {
                //not connected
                continue;
            }
            link_info._last_time = LiteGraph.getTime();
            var node = this.graph.getNodeById(link_info.target_id);
            if (!node) {
                //node not found?
                continue;
            }

            //used to mark events in graph
            var target_connection = node.inputs[link_info.target_slot];

			if (node.mode === LiteGraph.ON_TRIGGER)
			{
				// generate unique trigger ID if not present
				if (!options.action_call) options.action_call = this.id+"_trigg_"+Math.floor(Math.random()*9999);
                if (node.onExecute) {
                    // -- wrapping node.onExecute(param); --
                    node.doExecute(param, options);
                }
			}
			else if (node.onAction) {
                // generate unique action ID if not present
				if (!options.action_call) options.action_call = this.id+"_act_"+Math.floor(Math.random()*9999);
                //pass the action name
                var target_connection = node.inputs[link_info.target_slot];

                //instead of executing them now, it will be executed in the next graph loop, to ensure data flow
                if(LiteGraph.use_deferred_actions && node.onExecute)
                {
                    if(!node._waiting_actions)
                        node._waiting_actions = [];
                    node._waiting_actions.push([target_connection.name, param, options, link_info.target_slot]);
                }
                else
                {
                    // wrap node.onAction(target_connection.name, param);
                    node.actionDo( target_connection.name, param, options, link_info.target_slot );
                }
            }
        }
    };

    /**
     * clears the trigger slot animation
     * @method clearTriggeredSlot
     * @param {Number} slot the index of the output slot
     * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot
     */
    LGraphNode.prototype.clearTriggeredSlot = function(slot, link_id) {
        if (!this.outputs) {
            return;
        }

        var output = this.outputs[slot];
        if (!output) {
            return;
        }

        var links = output.links;
        if (!links || !links.length) {
            return;
        }

        //for every link attached here
        for (var k = 0; k < links.length; ++k) {
            var id = links[k];
            if (link_id != null && link_id != id) {
                //to skip links
                continue;
            }
            var link_info = this.graph.links[links[k]];
            if (!link_info) {
                //not connected
                continue;
            }
            link_info._last_time = 0;
        }
    };

    /**
     * changes node size and triggers callback
     * @method setSize
     * @param {vec2} size
     */
    LGraphNode.prototype.setSize = function(size)
	{
		this.size = size;
		if(this.onResize)
			this.onResize(this.size);
	}

    /**
     * add a new property to this node
     * @method addProperty
     * @param {string} name
     * @param {*} default_value
     * @param {string} type string defining the output type ("vec3","number",...)
     * @param {Object} extra_info this can be used to have special properties of the property (like values, etc)
     */
    LGraphNode.prototype.addProperty = function(
        name,
        default_value,
        type,
        extra_info
    ) {
        var o = { name: name, type: type, default_value: default_value };
        if (extra_info) {
            for (var i in extra_info) {
                o[i] = extra_info[i];
            }
        }
        if (!this.properties_info) {
            this.properties_info = [];
        }
        this.properties_info.push(o);
        if (!this.properties) {
            this.properties = {};
        }
        this.properties[name] = default_value;
        return o;
    };

    //connections

    /**
     * add a new output slot to use in this node
     * @method addOutput
     * @param {string} name
     * @param {string} type string defining the output type ("vec3","number",...)
     * @param {Object} extra_info this can be used to have special properties of an output (label, special color, position, etc)
     */
    LGraphNode.prototype.addOutput = function(name, type, extra_info) {
        var output = { name: name, type: type, links: null };
        if (extra_info) {
            for (var i in extra_info) {
                output[i] = extra_info[i];
            }
        }

        if (!this.outputs) {
            this.outputs = [];
        }
        this.outputs.push(output);
        if (this.onOutputAdded) {
            this.onOutputAdded(output);
        }
        
        if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,type,true);
        
        this.setSize( this.computeSize() );
        this.setDirtyCanvas(true, true);
        return output;
    };

    /**
     * add a new output slot to use in this node
     * @method addOutputs
     * @param {Array} array of triplets like [[name,type,extra_info],[...]]
     */
    LGraphNode.prototype.addOutputs = function(array) {
        for (var i = 0; i < array.length; ++i) {
            var info = array[i];
            var o = { name: info[0], type: info[1], link: null };
            if (array[2]) {
                for (var j in info[2]) {
                    o[j] = info[2][j];
                }
            }

            if (!this.outputs) {
                this.outputs = [];
            }
            this.outputs.push(o);
            if (this.onOutputAdded) {
                this.onOutputAdded(o);
            }
            
            if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,info[1],true);
            
        }

        this.setSize( this.computeSize() );
        this.setDirtyCanvas(true, true);
    };

    /**
     * remove an existing output slot
     * @method removeOutput
     * @param {number} slot
     */
    LGraphNode.prototype.removeOutput = function(slot) {
        this.disconnectOutput(slot);
        this.outputs.splice(slot, 1);
        for (var i = slot; i < this.outputs.length; ++i) {
            if (!this.outputs[i] || !this.outputs[i].links) {
                continue;
            }
            var links = this.outputs[i].links;
            for (var j = 0; j < links.length; ++j) {
                var link = this.graph.links[links[j]];
                if (!link) {
                    continue;
                }
                link.origin_slot -= 1;
            }
        }

        this.setSize( this.computeSize() );
        if (this.onOutputRemoved) {
            this.onOutputRemoved(slot);
        }
        this.setDirtyCanvas(true, true);
    };

    /**
     * add a new input slot to use in this node
     * @method addInput
     * @param {string} name
     * @param {string} type string defining the input type ("vec3","number",...), it its a generic one use 0
     * @param {Object} extra_info this can be used to have special properties of an input (label, color, position, etc)
     */
    LGraphNode.prototype.addInput = function(name, type, extra_info) {
        type = type || 0;
        var input = { name: name, type: type, link: null };
        if (extra_info) {
            for (var i in extra_info) {
                input[i] = extra_info[i];
            }
        }

        if (!this.inputs) {
            this.inputs = [];
        }

        this.inputs.push(input);
        this.setSize( this.computeSize() );

        if (this.onInputAdded) {
            this.onInputAdded(input);
		}
        
        LiteGraph.registerNodeAndSlotType(this,type);

        this.setDirtyCanvas(true, true);
        return input;
    };

    /**
     * add several new input slots in this node
     * @method addInputs
     * @param {Array} array of triplets like [[name,type,extra_info],[...]]
     */
    LGraphNode.prototype.addInputs = function(array) {
        for (var i = 0; i < array.length; ++i) {
            var info = array[i];
            var o = { name: info[0], type: info[1], link: null };
            if (array[2]) {
                for (var j in info[2]) {
                    o[j] = info[2][j];
                }
            }

            if (!this.inputs) {
                this.inputs = [];
            }
            this.inputs.push(o);
            if (this.onInputAdded) {
                this.onInputAdded(o);
            }
            
            LiteGraph.registerNodeAndSlotType(this,info[1]);
        }

        this.setSize( this.computeSize() );
        this.setDirtyCanvas(true, true);
    };

    /**
     * remove an existing input slot
     * @method removeInput
     * @param {number} slot
     */
    LGraphNode.prototype.removeInput = function(slot) {
        this.disconnectInput(slot);
        var slot_info = this.inputs.splice(slot, 1);
        for (var i = slot; i < this.inputs.length; ++i) {
            if (!this.inputs[i]) {
                continue;
            }
            var link = this.graph.links[this.inputs[i].link];
            if (!link) {
                continue;
            }
            link.target_slot -= 1;
        }
        this.setSize( this.computeSize() );
        if (this.onInputRemoved) {
            this.onInputRemoved(slot, slot_info[0] );
        }
        this.setDirtyCanvas(true, true);
    };

    /**
     * add an special connection to this node (used for special kinds of graphs)
     * @method addConnection
     * @param {string} name
     * @param {string} type string defining the input type ("vec3","number",...)
     * @param {[x,y]} pos position of the connection inside the node
     * @param {string} direction if is input or output
     */
    LGraphNode.prototype.addConnection = function(name, type, pos, direction) {
        var o = {
            name: name,
            type: type,
            pos: pos,
            direction: direction,
            links: null
        };
        this.connections.push(o);
        return o;
    };

    /**
     * computes the minimum size of a node according to its inputs and output slots
     * @method computeSize
     * @param {vec2} minHeight
     * @return {vec2} the total size
     */
    LGraphNode.prototype.computeSize = function(out) {
        if (this.constructor.size) {
            return this.constructor.size.concat();
        }

        var rows = Math.max(
            this.inputs ? this.inputs.length : 1,
            this.outputs ? this.outputs.length : 1
        );
        var size = out || new Float32Array([0, 0]);
        rows = Math.max(rows, 1);
        var font_size = LiteGraph.NODE_TEXT_SIZE; //although it should be graphcanvas.inner_text_font size

        var title_width = compute_text_size(this.title);
        var input_width = 0;
        var output_width = 0;

        if (this.inputs) {
            for (var i = 0, l = this.inputs.length; i < l; ++i) {
                var input = this.inputs[i];
                var text = input.label || input.name || "";
                var text_width = compute_text_size(text);
                if (input_width < text_width) {
                    input_width = text_width;
                }
            }
        }

        if (this.outputs) {
            for (var i = 0, l = this.outputs.length; i < l; ++i) {
                var output = this.outputs[i];
                var text = output.label || output.name || "";
                var text_width = compute_text_size(text);
                if (output_width < text_width) {
                    output_width = text_width;
                }
            }
        }

        size[0] = Math.max(input_width + output_width + 10, title_width);
        size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH);
        if (this.widgets && this.widgets.length) {
            size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH * 1.5);
        }

        size[1] = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT;

        var widgets_height = 0;
        if (this.widgets && this.widgets.length) {
            for (var i = 0, l = this.widgets.length; i < l; ++i) {
                if (this.widgets[i].computeSize)
                    widgets_height += this.widgets[i].computeSize(size[0])[1] + 4;
                else
                    widgets_height += LiteGraph.NODE_WIDGET_HEIGHT + 4;
            }
            widgets_height += 8;
        }

        //compute height using widgets height
        if( this.widgets_up )
            size[1] = Math.max( size[1], widgets_height );
        else if( this.widgets_start_y != null )
            size[1] = Math.max( size[1], widgets_height + this.widgets_start_y );
        else
            size[1] += widgets_height;

        function compute_text_size(text) {
            if (!text) {
                return 0;
            }
            return font_size * text.length * 0.6;
        }

        if (
            this.constructor.min_height &&
            size[1] < this.constructor.min_height
        ) {
            size[1] = this.constructor.min_height;
        }

        size[1] += 6; //margin

        return size;
    };

    /**
     * returns all the info available about a property of this node.
     *
     * @method getPropertyInfo
     * @param {String} property name of the property
     * @return {Object} the object with all the available info
    */
    LGraphNode.prototype.getPropertyInfo = function( property )
	{
        var info = null;

		//there are several ways to define info about a property
		//legacy mode
		if (this.properties_info) {
            for (var i = 0; i < this.properties_info.length; ++i) {
                if (this.properties_info[i].name == property) {
                    info = this.properties_info[i];
                    break;
                }
            }
        }
		//litescene mode using the constructor
		if(this.constructor["@" + property])
			info = this.constructor["@" + property];

		if(this.constructor.widgets_info && this.constructor.widgets_info[property])
			info = this.constructor.widgets_info[property];

		//litescene mode using the constructor
		if (!info && this.onGetPropertyInfo) {
            info = this.onGetPropertyInfo(property);
        }

        if (!info)
            info = {};
		if(!info.type)
			info.type = typeof this.properties[property];
		if(info.widget == "combo")
			info.type = "enum";

		return info;
	}

    /**
     * Defines a widget inside the node, it will be rendered on top of the node, you can control lots of properties
     *
     * @method addWidget
     * @param {String} type the widget type (could be "number","string","combo"
     * @param {String} name the text to show on the widget
     * @param {String} value the default value
     * @param {Function|String} callback function to call when it changes (optionally, it can be the name of the property to modify)
     * @param {Object} options the object that contains special properties of this widget 
     * @return {Object} the created widget object
     */
    LGraphNode.prototype.addWidget = function( type, name, value, callback, options )
	{
        if (!this.widgets) {
            this.widgets = [];
        }

		if(!options && callback && callback.constructor === Object)
		{
			options = callback;
			callback = null;
		}

		if(options && options.constructor === String) //options can be the property name
			options = { property: options };

		if(callback && callback.constructor === String) //callback can be the property name
		{
			if(!options)
				options = {};
			options.property = callback;
			callback = null;
		}

		if(callback && callback.constructor !== Function)
		{
			console.warn("addWidget: callback must be a function");
			callback = null;
		}

        var w = {
            type: type.toLowerCase(),
            name: name,
            value: value,
            callback: callback,
            options: options || {}
        };

        if (w.options.y !== undefined) {
            w.y = w.options.y;
        }

        if (!callback && !w.options.callback && !w.options.property) {
            console.warn("LiteGraph addWidget(...) without a callback or property assigned");
        }
        if (type == "combo" && !w.options.values) {
            throw "LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }";
        }
        this.widgets.push(w);
		this.setSize( this.computeSize() );
        return w;
    };

    LGraphNode.prototype.addCustomWidget = function(custom_widget) {
        if (!this.widgets) {
            this.widgets = [];
        }
        this.widgets.push(custom_widget);
        return custom_widget;
    };

    /**
     * returns the bounding of the object, used for rendering purposes
     * @method getBounding
     * @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage
     * @param compute_outer {boolean?} [optional] set to true to include the shadow and connection points in the bounding calculation
     * @return {Float32Array[4]} the bounding box in format of [topleft_cornerx, topleft_cornery, width, height]
     */
    LGraphNode.prototype.getBounding = function(out, compute_outer) {
        out = out || new Float32Array(4);
        const nodePos = this.pos;
        const isCollapsed = this.flags.collapsed;
        const nodeSize = this.size;
        
        let left_offset = 0;
        // 1 offset due to how nodes are rendered
        let right_offset =  1 ;
        let top_offset = 0;
        let bottom_offset = 0;
        
        if (compute_outer) {
            // 4 offset for collapsed node connection points
            left_offset = 4;
            // 6 offset for right shadow and collapsed node connection points
            right_offset = 6 + left_offset;
            // 4 offset for collapsed nodes top connection points
            top_offset = 4;
            // 5 offset for bottom shadow and collapsed node connection points
            bottom_offset = 5 + top_offset;
        }
        
        out[0] = nodePos[0] - left_offset;
        out[1] = nodePos[1] - LiteGraph.NODE_TITLE_HEIGHT - top_offset;
        out[2] = isCollapsed ?
            (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + right_offset :
            nodeSize[0] + right_offset;
        out[3] = isCollapsed ?
            LiteGraph.NODE_TITLE_HEIGHT + bottom_offset :
            nodeSize[1] + LiteGraph.NODE_TITLE_HEIGHT + bottom_offset;

        if (this.onBounding) {
            this.onBounding(out);
        }
        return out;
    };

    /**
     * checks if a point is inside the shape of a node
     * @method isPointInside
     * @param {number} x
     * @param {number} y
     * @return {boolean}
     */
    LGraphNode.prototype.isPointInside = function(x, y, margin, skip_title) {
        margin = margin || 0;

        var margin_top = this.graph && this.graph.isLive() ? 0 : LiteGraph.NODE_TITLE_HEIGHT;
        if (skip_title) {
            margin_top = 0;
        }
        if (this.flags && this.flags.collapsed) {
            //if ( distance([x,y], [this.pos[0] + this.size[0]*0.5, this.pos[1] + this.size[1]*0.5]) < LiteGraph.NODE_COLLAPSED_RADIUS)
            if (
                isInsideRectangle(
                    x,
                    y,
                    this.pos[0] - margin,
                    this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT - margin,
                    (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) +
                        2 * margin,
                    LiteGraph.NODE_TITLE_HEIGHT + 2 * margin
                )
            ) {
                return true;
            }
        } else if (
            this.pos[0] - 4 - margin < x &&
            this.pos[0] + this.size[0] + 4 + margin > x &&
            this.pos[1] - margin_top - margin < y &&
            this.pos[1] + this.size[1] + margin > y
        ) {
            return true;
        }
        return false;
    };

    /**
     * checks if a point is inside a node slot, and returns info about which slot
     * @method getSlotInPosition
     * @param {number} x
     * @param {number} y
     * @return {Object} if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] }
     */
    LGraphNode.prototype.getSlotInPosition = function(x, y) {
        //search for inputs
        var link_pos = new Float32Array(2);
        if (this.inputs) {
            for (var i = 0, l = this.inputs.length; i < l; ++i) {
                var input = this.inputs[i];
                this.getConnectionPos(true, i, link_pos);
                if (
                    isInsideRectangle(
                        x,
                        y,
                        link_pos[0] - 10,
                        link_pos[1] - 5,
                        20,
                        10
                    )
                ) {
                    return { input: input, slot: i, link_pos: link_pos };
                }
            }
        }

        if (this.outputs) {
            for (var i = 0, l = this.outputs.length; i < l; ++i) {
                var output = this.outputs[i];
                this.getConnectionPos(false, i, link_pos);
                if (
                    isInsideRectangle(
                        x,
                        y,
                        link_pos[0] - 10,
                        link_pos[1] - 5,
                        20,
                        10
                    )
                ) {
                    return { output: output, slot: i, link_pos: link_pos };
                }
            }
        }

        return null;
    };

    /**
     * returns the input slot with a given name (used for dynamic slots), -1 if not found
     * @method findInputSlot
     * @param {string} name the name of the slot
     * @param {boolean} returnObj if the obj itself wanted
     * @return {number_or_object} the slot (-1 if not found)
     */
    LGraphNode.prototype.findInputSlot = function(name,  returnObj) {
        if (!this.inputs) {
            return -1;
        }
        for (var i = 0, l = this.inputs.length; i < l; ++i) {
            if (name == this.inputs[i].name) {
                return !returnObj ? i : this.inputs[i];
            }
        }
        return -1;
    };

    /**
     * returns the output slot with a given name (used for dynamic slots), -1 if not found
     * @method findOutputSlot
     * @param {string} name the name of the slot
     * @param {boolean} returnObj if the obj itself wanted
     * @return {number_or_object} the slot (-1 if not found)
     */
    LGraphNode.prototype.findOutputSlot = function(name, returnObj) {
        returnObj = returnObj || false;
        if (!this.outputs) {
            return -1;
        }
        for (var i = 0, l = this.outputs.length; i < l; ++i) {
            if (name == this.outputs[i].name) {
                return !returnObj ? i : this.outputs[i];
            }
        }
        return -1;
    };
    
    // TODO refactor: USE SINGLE findInput/findOutput functions! :: merge options
    
    /**
     * returns the first free input slot
     * @method findInputSlotFree
     * @param {object} options
     * @return {number_or_object} the slot (-1 if not found)
     */
    LGraphNode.prototype.findInputSlotFree = function(optsIn) {
        var optsIn = optsIn || {};
        var optsDef = {returnObj: false
                        ,typesNotAccepted: []
                      };
        var opts = Object.assign(optsDef,optsIn);
        if (!this.inputs) {
            return -1;
        }
        for (var i = 0, l = this.inputs.length; i < l; ++i) {
            if (this.inputs[i].link && this.inputs[i].link != null) {
                continue;
            }
            if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.inputs[i].type)){
                continue;
            }
            return !opts.returnObj ? i : this.inputs[i];
        }
        return -1;
    };

    /**
     * returns the first output slot free
     * @method findOutputSlotFree
     * @param {object} options
     * @return {number_or_object} the slot (-1 if not found)
     */
    LGraphNode.prototype.findOutputSlotFree = function(optsIn) {
        var optsIn = optsIn || {};
        var optsDef = { returnObj: false
                        ,typesNotAccepted: []
                      };
        var opts = Object.assign(optsDef,optsIn);
        if (!this.outputs) {
            return -1;
        }
        for (var i = 0, l = this.outputs.length; i < l; ++i) {
            if (this.outputs[i].links && this.outputs[i].links != null) {
                continue;
            }
            if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.outputs[i].type)){
                continue;
            }
            return !opts.returnObj ? i : this.outputs[i];
        }
        return -1;
    };
    
    /**
     * findSlotByType for INPUTS
     */
    LGraphNode.prototype.findInputSlotByType = function(type, returnObj, preferFreeSlot, doNotUseOccupied) {
        return this.findSlotByType(true, type, returnObj, preferFreeSlot, doNotUseOccupied);
    };

    /**
     * findSlotByType for OUTPUTS
     */
    LGraphNode.prototype.findOutputSlotByType = function(type, returnObj, preferFreeSlot, doNotUseOccupied) {
        return this.findSlotByType(false, type, returnObj, preferFreeSlot, doNotUseOccupied);
    };
    
    /**
     * returns the output (or input) slot with a given type, -1 if not found
     * @method findSlotByType
     * @param {boolean} input uise inputs instead of outputs
     * @param {string} type the type of the slot
     * @param {boolean} returnObj if the obj itself wanted
     * @param {boolean} preferFreeSlot if we want a free slot (if not found, will return the first of the type anyway)
     * @return {number_or_object} the slot (-1 if not found)
     */
    LGraphNode.prototype.findSlotByType = function(input, type, returnObj, preferFreeSlot, doNotUseOccupied) {
        input = input || false;
        returnObj = returnObj || false;
        preferFreeSlot = preferFreeSlot || false;
        doNotUseOccupied = doNotUseOccupied || false;
        var aSlots = input ? this.inputs : this.outputs;
        if (!aSlots) {
            return -1;
        }
		// !! empty string type is considered 0, * !!
		if (type == "" || type == "*") type = 0; 
        for (var i = 0, l = aSlots.length; i < l; ++i) {
            var tFound = false;
            var aSource = (type+"").toLowerCase().split(",");
            var aDest = aSlots[i].type=="0"||aSlots[i].type=="*"?"0":aSlots[i].type;
			aDest = (aDest+"").toLowerCase().split(",");
            for(var sI=0;sI<aSource.length;sI++){
                for(var dI=0;dI<aDest.length;dI++){
					if (aSource[sI]=="_event_") aSource[sI] = LiteGraph.EVENT;
					if (aDest[sI]=="_event_") aDest[sI] = LiteGraph.EVENT;
					if (aSource[sI]=="*") aSource[sI] = 0;
					if (aDest[sI]=="*") aDest[sI] = 0;
					if (aSource[sI] == aDest[dI]) {
                        if (preferFreeSlot && aSlots[i].links && aSlots[i].links !== null) continue;
                        return !returnObj ? i : aSlots[i];
                    }
                }
            }
        }
        // if didnt find some, stop checking for free slots
        if (preferFreeSlot && !doNotUseOccupied){
            for (var i = 0, l = aSlots.length; i < l; ++i) {
                var tFound = false;
                var aSource = (type+"").toLowerCase().split(",");
                var aDest = aSlots[i].type=="0"||aSlots[i].type=="*"?"0":aSlots[i].type;
				aDest = (aDest+"").toLowerCase().split(",");
                for(var sI=0;sI<aSource.length;sI++){
                    for(var dI=0;dI<aDest.length;dI++){
						if (aSource[sI]=="*") aSource[sI] = 0;
						if (aDest[sI]=="*") aDest[sI] = 0;
                        if (aSource[sI] == aDest[dI]) {
                            return !returnObj ? i : aSlots[i];
                        }
                    }
                }
            }
        }
        return -1;
    };

    /**
     * connect this node output to the input of another node BY TYPE
     * @method connectByType
     * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
     * @param {LGraphNode} node the target node
     * @param {string} target_type the input slot type of the target node
     * @return {Object} the link_info is created, otherwise null
     */
    LGraphNode.prototype.connectByType = function(slot, target_node, target_slotType, optsIn) {
        var optsIn = optsIn || {};
        var optsDef = { createEventInCase: true
					   	,firstFreeIfOutputGeneralInCase: true
                        ,generalTypeInCase: true
                      };
        var opts = Object.assign(optsDef,optsIn);
        if (target_node && target_node.constructor === Number) {
            target_node = this.graph.getNodeById(target_node);
        }
        var target_slot = target_node.findInputSlotByType(target_slotType, false, true);
        if (target_slot >= 0 && target_slot !== null){
            //console.debug("CONNbyTYPE type "+target_slotType+" for "+target_slot)
            return this.connect(slot, target_node, target_slot);
        }else{
            //console.log("type "+target_slotType+" not found or not free?")
            if (opts.createEventInCase && target_slotType == LiteGraph.EVENT){
                // WILL CREATE THE onTrigger IN SLOT
				//console.debug("connect WILL CREATE THE onTrigger "+target_slotType+" to "+target_node);
                return this.connect(slot, target_node, -1);
            }
			// connect to the first general output slot if not found a specific type and 
            if (opts.generalTypeInCase){
                var target_slot = target_node.findInputSlotByType(0, false, true, true);
				//console.debug("connect TO a general type (*, 0), if not found the specific type ",target_slotType," to ",target_node,"RES_SLOT:",target_slot);
                if (target_slot >= 0){
                    return this.connect(slot, target_node, target_slot);
                }
            }
            // connect to the first free input slot if not found a specific type and this output is general
            if (opts.firstFreeIfOutputGeneralInCase && (target_slotType == 0 || target_slotType == "*" || target_slotType == "")){
                var target_slot = target_node.findInputSlotFree({typesNotAccepted: [LiteGraph.EVENT] });
				//console.debug("connect TO TheFirstFREE ",target_slotType," to ",target_node,"RES_SLOT:",target_slot);
                if (target_slot >= 0){
					return this.connect(slot, target_node, target_slot);
                }
            }
			
			console.debug("no way to connect type: ",target_slotType," to targetNODE ",target_node);
			//TODO filter
			
            return null;
        }
    }
    
    /**
     * connect this node input to the output of another node BY TYPE
     * @method connectByType
     * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
     * @param {LGraphNode} node the target node
     * @param {string} target_type the output slot type of the target node
     * @return {Object} the link_info is created, otherwise null
     */
    LGraphNode.prototype.connectByTypeOutput = function(slot, source_node, source_slotType, optsIn) {
        var optsIn = optsIn || {};
        var optsDef = { createEventInCase: true
                        ,firstFreeIfInputGeneralInCase: true
                        ,generalTypeInCase: true
                      };
        var opts = Object.assign(optsDef,optsIn);
        if (source_node && source_node.constructor === Number) {
            source_node = this.graph.getNodeById(source_node);
        }
        var source_slot = source_node.findOutputSlotByType(source_slotType, false, true);
        if (source_slot >= 0 && source_slot !== null){
            //console.debug("CONNbyTYPE OUT! type "+source_slotType+" for "+source_slot)
            return source_node.connect(source_slot, this, slot);
        }else{
            
            // connect to the first general output slot if not found a specific type and 
            if (opts.generalTypeInCase){
                var source_slot = source_node.findOutputSlotByType(0, false, true, true);
                if (source_slot >= 0){
                    return source_node.connect(source_slot, this, slot);
                }
            }
            
            if (opts.createEventInCase && source_slotType == LiteGraph.EVENT){
                // WILL CREATE THE onExecuted OUT SLOT
				if (LiteGraph.do_add_triggers_slots){
					var source_slot = source_node.addOnExecutedOutput();
					return source_node.connect(source_slot, this, slot);
				}
            }
            // connect to the first free output slot if not found a specific type and this input is general
            if (opts.firstFreeIfInputGeneralInCase && (source_slotType == 0 || source_slotType == "*" || source_slotType == "")){
                var source_slot = source_node.findOutputSlotFree({typesNotAccepted: [LiteGraph.EVENT] });
                if (source_slot >= 0){
                    return source_node.connect(source_slot, this, slot);
                }
            }
            
			console.debug("no way to connect byOUT type: ",source_slotType," to sourceNODE ",source_node);
			//TODO filter
			
            //console.log("type OUT! "+source_slotType+" not found or not free?")
            return null;
        }
    }
    
    /**
     * connect this node output to the input of another node
     * @method connect
     * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
     * @param {LGraphNode} node the target node
     * @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger)
     * @return {Object} the link_info is created, otherwise null
     */
    LGraphNode.prototype.connect = function(slot, target_node, target_slot) {
        target_slot = target_slot || 0;

        if (!this.graph) {
            //could be connected before adding it to a graph
            console.log(
                "Connect: Error, node doesn't belong to any graph. Nodes must be added first to a graph before connecting them."
            ); //due to link ids being associated with graphs
            return null;
        }

        //seek for the output slot
        if (slot.constructor === String) {
            slot = this.findOutputSlot(slot);
            if (slot == -1) {
                if (LiteGraph.debug) {
                    console.log("Connect: Error, no slot of name " + slot);
                }
                return null;
            }
        } else if (!this.outputs || slot >= this.outputs.length) {
            if (LiteGraph.debug) {
                console.log("Connect: Error, slot number not found");
            }
            return null;
        }

        if (target_node && target_node.constructor === Number) {
            target_node = this.graph.getNodeById(target_node);
        }
        if (!target_node) {
            throw "target node is null";
        }

        //avoid loopback
        if (target_node == this) {
            return null;
        }

        //you can specify the slot by name
        if (target_slot.constructor === String) {
            target_slot = target_node.findInputSlot(target_slot);
            if (target_slot == -1) {
                if (LiteGraph.debug) {
                    console.log(
                        "Connect: Error, no slot of name " + target_slot
                    );
                }
                return null;
            }
        } else if (target_slot === LiteGraph.EVENT) {
            
            if (LiteGraph.do_add_triggers_slots){
	            //search for first slot with event? :: NO this is done outside
				//console.log("Connect: Creating triggerEvent");
	            // force mode
	            target_node.changeMode(LiteGraph.ON_TRIGGER);
	            target_slot = target_node.findInputSlot("onTrigger");
        	}else{
            	return null; // -- break --
			}
        } else if (
            !target_node.inputs ||
            target_slot >= target_node.inputs.length
        ) {
            if (LiteGraph.debug) {
                console.log("Connect: Error, slot number not found");
            }
            return null;
        }

		var changed = false;

        var input = target_node.inputs[target_slot];
        var link_info = null;
        var output = this.outputs[slot];
        
        if (!this.outputs[slot]){
            /*console.debug("Invalid slot passed: "+slot);
            console.debug(this.outputs);*/
            return null;
        }

        // allow target node to change slot
        if (target_node.onBeforeConnectInput) {
            // This way node can choose another slot (or make a new one?)
            target_slot = target_node.onBeforeConnectInput(target_slot); //callback
        }

		//check target_slot and check connection types
        if (target_slot===false || target_slot===null || !LiteGraph.isValidConnection(output.type, input.type))
		{
	        this.setDirtyCanvas(false, true);
			if(changed)
		        this.graph.connectionChange(this, link_info);
			return null;
		}else{
			//console.debug("valid connection",output.type, input.type);
		}

        //allows nodes to block connection, callback
        if (target_node.onConnectInput) {
            if ( target_node.onConnectInput(target_slot, output.type, output, this, slot) === false ) {
                return null;
            }
        }
        if (this.onConnectOutput) { // callback
            if ( this.onConnectOutput(slot, input.type, input, target_node, target_slot) === false ) {
                return null;
            }
        }

        //if there is something already plugged there, disconnect
        if (target_node.inputs[target_slot] && target_node.inputs[target_slot].link != null) {
			this.graph.beforeChange();
            target_node.disconnectInput(target_slot, {doProcessChange: false});
			changed = true;
        }
        if (output.links !== null && output.links.length){
            switch(output.type){
                case LiteGraph.EVENT:
                    if (!LiteGraph.allow_multi_output_for_events){
                        this.graph.beforeChange();
                        this.disconnectOutput(slot, false, {doProcessChange: false}); // Input(target_slot, {doProcessChange: false});
                        changed = true;
                    }
                break;
                default:
                break;
            }
        }

        var nextId
        if (LiteGraph.use_uuids)
           
Download .txt
gitextract_7ufcrmuf/

├── .editorconfig
├── .github/
│   └── workflows/
│       └── publish.yml
├── .gitignore
├── README.md
├── __init__.py
├── docs/
│   ├── CircuitBoardLines.js
│   ├── QuickConnection.js
│   ├── links/
│   │   ├── litegraph.css
│   │   └── litegraph.js
│   ├── quick_conn.html
│   └── quick_conn.js
├── example/
│   ├── comfyui_quick_conn.html
│   ├── comfyui_quick_conn.js
│   ├── make_docs.sh
│   ├── quick_conn.html
│   ├── quick_conn.js
│   └── run_example.sh
├── imgs/
│   └── SpeedUpMp4ToGif.sh
├── js/
│   ├── CircuitBoardLines.js
│   ├── QuickConnection.js
│   └── quick_conn_start.js
├── package.json
└── pyproject.toml
Download .txt
SYMBOL INDEX (418 symbols across 9 files)

FILE: docs/CircuitBoardLines.js
  constant EPSILON (line 18) | const EPSILON = 1e-6;
  constant INSIDE (line 19) | const INSIDE = 1;
  constant OUTSIDE (line 20) | const OUTSIDE = 0;
  function clipT (line 22) | function clipT(num, denom, c) {
  function liangBarsky (line 50) | function liangBarsky(a, b, box, da, db) {
  class MapLinks (line 93) | class MapLinks {
    method constructor (line 94) | constructor(canvas) {
    method isInsideNode (line 105) | isInsideNode(xy) {
    method findClippedNode (line 115) | findClippedNode(outputXY, inputXY) {
    method testPath (line 148) | testPath(path) {
    method mapFinalLink (line 159) | mapFinalLink(outputXY, inputXY) {
    method mapLink (line 263) | mapLink(outputXY, inputXY, targetNodeInfo, isBlocked /* , lastDirectio...
    method expandSourceNodeLinesArea (line 408) | expandSourceNodeLinesArea(sourceNodeInfo, path) {
    method expandTargetNodeLinesArea (line 423) | expandTargetNodeLinesArea(targetNodeInfo, path) {
    method getNodeOnPos (line 438) | getNodeOnPos(xy) {
    method mapLinks (line 453) | mapLinks(nodesByExecution) {
    method drawLinks (line 573) | drawLinks(ctx) {
  class EyeButton (line 718) | class EyeButton {
    method constructor (line 719) | constructor() {
    method getEyeButton (line 723) | static getEyeButton() {
    method check (line 731) | check() {
    method listenEyeButton (line 745) | listenEyeButton(onChange) {
  class CircuitBoardLines (line 759) | class CircuitBoardLines {
    method constructor (line 760) | constructor() {
    method setEnabled (line 768) | setEnabled(e) { this.enabled = e; }
    method isShow (line 770) | isShow() { return this.enabled && !this.eyeHidden; }
    method recalcMapLinksTimeout (line 772) | recalcMapLinksTimeout() {
    method redraw (line 789) | redraw() {
    method recalcMapLinksCheck (line 806) | recalcMapLinksCheck() {
    method recalcMapLinks (line 817) | recalcMapLinks() {
    method drawConnections (line 825) | drawConnections(
    method drawSubgraphConnections (line 849) | drawSubgraphConnections(
    method init (line 916) | init() {
    method initOverrides (line 935) | initOverrides(canvas) {

FILE: docs/QuickConnection.js
  class QuickConnection (line 11) | class QuickConnection {
    method constructor (line 12) | constructor() {
    method init (line 24) | init() {
    method initListeners (line 73) | initListeners(canvas) {
    method getCurrentConnection (line 95) | getCurrentConnection() {
    method pointerDown (line 121) | pointerDown() {
    method pointerUp (line 126) | pointerUp() {
    method findAcceptingNodes (line 149) | findAcceptingNodes(fromConnection, fromNode, findInput) {
    method addOnCanvas (line 198) | addOnCanvas(name, func) {
    method onDrawOverlay (line 209) | onDrawOverlay(ctx) {

FILE: docs/links/litegraph.js
  function LGraph (line 837) | function LGraph(o) {
  function on_frame (line 997) | function on_frame() {
  function LLink (line 2378) | function LLink(id, type, origin_id, origin_slot, target_id, target_slot) {
  function LGraphNode (line 2482) | function LGraphNode(title) {
  function compute_text_size (line 3742) | function compute_text_size(text) {
  function LGraphGroup (line 4992) | function LGraphGroup(title) {
  function DragAndScale (line 5095) | function DragAndScale(element, skip_events) {
  function LGraphCanvas (line 5327) | function LGraphCanvas(canvas, graph, options) {
  function renderFrame (line 5891) | function renderFrame() {
  function inner_clicked (line 10205) | function inner_clicked(v, option, event) {
  function inner_value_change (line 10277) | function inner_value_change(widget, value) {
  function inner_clicked (line 10577) | function inner_clicked(value) {
  function inner_clicked (line 10589) | function inner_clicked(value) {
  function inner_onMenuAdded (line 10602) | function inner_onMenuAdded(base_category ,prev_menu){
  function inner_clicked (line 10734) | function inner_clicked(v, e, prev) {
  function inner_clicked (line 10839) | function inner_clicked(v, e, prev) {
  function inner_clicked (line 10938) | function inner_clicked(v, options, e, prev) {
  function inner_clicked (line 10999) | function inner_clicked(v,options,e) {
  function inner_clicked (line 11256) | function inner_clicked(v,options,e) {
  function inner (line 11370) | function inner() {
  function setValue (line 11374) | function setValue(value) {
  function select (line 11754) | function select(name) {
  function changeSelection (line 11872) | function changeSelection(forward) {
  function refreshHelper (line 11896) | function refreshHelper() {
  function inner (line 12179) | function inner() {
  function setValue (line 12183) | function setValue(value) {
  function inner_clicked (line 12478) | function inner_clicked(v, option, event) {
  function innerChange (line 12490) | function innerChange(name, value)
  function inner_refresh (line 12572) | function inner_refresh(){
  function inner_refresh (line 12651) | function inner_refresh()
  function inner_refresh (line 12792) | function inner_refresh()
  function inner_refresh (line 12848) | function inner_refresh() {
  function addOutput (line 12880) | function addOutput() {
  function inner_clicked (line 12940) | function inner_clicked(v) {
  function inner_clicked (line 13001) | function inner_clicked(v) {
  function inner_clicked (line 13048) | function inner_clicked(v) {
  function inner_option_clicked (line 13449) | function inner_option_clicked(v, options, e) {
  function compareObjects (line 13521) | function compareObjects(a, b) {
  function distance (line 13531) | function distance(a, b) {
  function colorToString (line 13538) | function colorToString(c) {
  function isInsideRectangle (line 13553) | function isInsideRectangle(x, y, left, top, width, height) {
  function growBounding (line 13562) | function growBounding(bounding, x, y) {
  function isInsideBounding (line 13578) | function isInsideBounding(p, bb) {
  function overlapBounding (line 13592) | function overlapBounding(a, b) {
  function hex2num (line 13613) | function hex2num(hex) {
  function num2hex (line 13635) | function num2hex(triplet) {
  function ContextMenu (line 13664) | function ContextMenu(values, options) {
  function inner_over (line 13897) | function inner_over(e) {
  function inner_onclick (line 13907) | function inner_onclick(e) {
  function CurveEditor (line 14119) | function CurveEditor( points )
  function clamp (line 14401) | function clamp(v, a, b) {
  function Time (line 14433) | function Time() {
  function Subgraph (line 14449) | function Subgraph() {
  function GraphInput (line 14911) | function GraphInput() {
  function GraphOutput (line 15072) | function GraphOutput() {
  function ConstantNumber (line 15205) | function ConstantNumber() {
  function ConstantBoolean (line 15239) | function ConstantBoolean() {
  function ConstantString (line 15269) | function ConstantString() {
  function ConstantObject (line 15301) | function ConstantObject() {
  function ConstantFile (line 15316) | function ConstantFile() {
  function JSONParse (line 15416) | function JSONParse() {
  function ConstantData (line 15457) | function ConstantData() {
  function ConstantArray (line 15492) | function ConstantArray() {
  function SetArray (line 15541) | function SetArray()
  function ArrayElement (line 15567) | function ArrayElement() {
  function TableElement (line 15589) | function TableElement() {
  function ObjectProperty (line 15620) | function ObjectProperty() {
  function ObjectKeys (line 15658) | function ObjectKeys() {
  function SetObject (line 15677) | function SetObject()
  function MergeObjects (line 15704) | function MergeObjects() {
  function Variable (line 15735) | function Variable() {
  function length (line 15791) | function length(v) {
  function length (line 15804) | function length(v) {
  function DownloadData (line 15817) | function DownloadData() {
  function Watch (line 15880) | function Watch() {
  function Cast (line 15927) | function Cast() {
  function Console (line 15943) | function Console() {
  function Alert (line 15989) | function Alert() {
  function NodeScript (line 16017) | function NodeScript() {
  function GenericCompare (line 16101) | function GenericCompare() {
  function LogEvent (line 16204) | function LogEvent() {
  function TriggerEvent (line 16219) | function TriggerEvent() {
  function Sequence (line 16250) | function Sequence() {
  function WaitAll (line 16292) | function WaitAll() {
  function Stepper (line 16349) | function Stepper() {
  function FilterEvent (line 16416) | function FilterEvent() {
  function EventBranch (line 16459) | function EventBranch() {
  function EventCounter (line 16483) | function EventCounter() {
  function DelayEvent (line 16538) | function DelayEvent() {
  function TimerEvent (line 16589) | function TimerEvent() {
  function SemaphoreEvent (line 16660) | function SemaphoreEvent() {
  function OnceEvent (line 16695) | function OnceEvent() {
  function DataStore (line 16722) | function DataStore() {
  function WidgetButton (line 16767) | function WidgetButton() {
  function WidgetToggle (line 16850) | function WidgetToggle() {
  function WidgetNumber (line 16922) | function WidgetNumber() {
  function WidgetCombo (line 17040) | function WidgetCombo() {
  function WidgetKnob (line 17080) | function WidgetKnob() {
  function WidgetSliderGUI (line 17241) | function WidgetSliderGUI() {
  function WidgetHSlider (line 17278) | function WidgetHSlider() {
  function WidgetProgress (line 17362) | function WidgetProgress() {
  function WidgetText (line 17392) | function WidgetText() {
  function WidgetPanel (line 17489) | function WidgetPanel() {
  function GamepadInput (line 17563) | function GamepadInput() {
  function Converter (line 17919) | function Converter() {
  function Bypass (line 17994) | function Bypass() {
  function ToNumber (line 18010) | function ToNumber() {
  function MathRange (line 18025) | function MathRange() {
  function MathRand (line 18108) | function MathRand() {
  function MathNoise (line 18148) | function MathNoise() {
  function MathSpikes (line 18218) | function MathSpikes() {
  function MathClamp (line 18259) | function MathClamp() {
  function MathLerp (line 18297) | function MathLerp() {
  function MathAbs (line 18335) | function MathAbs() {
  function MathFloor (line 18355) | function MathFloor() {
  function MathFrac (line 18375) | function MathFrac() {
  function MathSmoothStep (line 18395) | function MathSmoothStep() {
  function MathScale (line 18425) | function MathScale() {
  function Gate (line 18445) | function Gate() {
  function MathAverageFilter (line 18464) | function MathAverageFilter() {
  function MathTendTo (line 18516) | function MathTendTo() {
  function MathOperation (line 18544) | function MathOperation() {
  function MathCompare (line 18673) | function MathCompare() {
  function MathCondition (line 18768) | function MathCondition() {
  function MathBranch (line 18845) | function MathBranch() {
  function MathAccumulate (line 18875) | function MathAccumulate() {
  function MathTrigonometry (line 18902) | function MathTrigonometry() {
  function MathFormula (line 18989) | function MathFormula() {
  function Math3DVec2ToXY (line 19071) | function Math3DVec2ToXY() {
  function Math3DXYToVec2 (line 19092) | function Math3DXYToVec2() {
  function Math3DVec3ToXYZ (line 19121) | function Math3DVec3ToXYZ() {
  function Math3DXYZToVec3 (line 19144) | function Math3DXYZToVec3() {
  function Math3DVec4ToXYZW (line 19178) | function Math3DVec4ToXYZW() {
  function Math3DXYZWToVec4 (line 19203) | function Math3DXYZWToVec4() {
  function Math3DMat4 (line 19253) | function Math3DMat4()
  function Math3DOperation (line 19316) | function Math3DOperation() {
  function Math3DVec3Scale (line 19426) | function Math3DVec3Scale() {
  function Math3DVec3Length (line 19456) | function Math3DVec3Length() {
  function Math3DVec3Normalize (line 19475) | function Math3DVec3Normalize() {
  function Math3DVec3Lerp (line 19500) | function Math3DVec3Lerp() {
  function Math3DVec3Dot (line 19533) | function Math3DVec3Dot() {
  function Math3DQuaternion (line 19560) | function Math3DQuaternion() {
  function Math3DRotation (line 19591) | function Math3DRotation() {
  function MathEulerToQuat (line 19619) | function MathEulerToQuat() {
  function MathQuatToEuler (line 19644) | function MathQuatToEuler() {
  function Math3DRotateVec3 (line 19666) | function Math3DRotateVec3() {
  function Math3DMultQuat (line 19693) | function Math3DMultQuat() {
  function Math3DQuatSlerp (line 19719) | function Math3DQuatSlerp() {
  function Math3DRemapRange (line 19756) | function Math3DRemapRange() {
  function toString (line 19827) | function toString(a) {
  function compare (line 19844) | function compare(a, b) {
  function concatenate (line 19855) | function concatenate(a, b) {
  function contains (line 19872) | function contains(a, b) {
  function toUpperCase (line 19886) | function toUpperCase(a) {
  function split (line 19900) | function split(str, separator) {
  function toFixed (line 19927) | function toFixed(a) {
  function StringToTable (line 19943) | function StringToTable() {
  function Selector (line 19977) | function Selector() {
  function Sequence (line 20021) | function Sequence() {
  function logicAnd (line 20060) | function logicAnd(){
  function logicOr (line 20086) | function logicOr(){
  function logicNot (line 20112) | function logicNot(){
  function logicCompare (line 20126) | function logicCompare(){
  function logicBranch (line 20155) | function logicBranch(){
  function GraphicsPlot (line 20179) | function GraphicsPlot() {
  function GraphicsImage (line 20253) | function GraphicsImage() {
  function ColorPalette (line 20351) | function ColorPalette() {
  function ImageFrame (line 20426) | function ImageFrame() {
  function ImageFade (line 20477) | function ImageFade() {
  function ImageCrop (line 20536) | function ImageCrop() {
  function CanvasNode (line 20619) | function CanvasNode() {
  function DrawImageNode (line 20656) | function DrawImageNode() {
  function DrawRectangleNode (line 20686) | function DrawRectangleNode() {
  function ImageVideo (line 20721) | function ImageVideo() {
  function ImageWebcam (line 20910) | function ImageWebcam() {
  function onFailSoHard (line 20940) | function onFailSoHard(e) {
  function LGraphTexture (line 21085) | function LGraphTexture() {
  function LGraphTexturePreview (line 21464) | function LGraphTexturePreview() {
  function LGraphTextureSave (line 21513) | function LGraphTextureSave() {
  function LGraphTextureOperation (line 21560) | function LGraphTextureOperation() {
  function LGraphTextureShader (line 21824) | function LGraphTextureShader() {
  function LGraphTextureScaleOffset (line 22040) | function LGraphTextureScaleOffset() {
  function LGraphTextureWarp (line 22157) | function LGraphTextureWarp() {
  function LGraphTextureToViewport (line 22290) | function LGraphTextureToViewport() {
  function LGraphTextureCopy (line 22477) | function LGraphTextureCopy() {
  function LGraphTextureDownsample (line 22562) | function LGraphTextureDownsample() {
  function LGraphTextureResize (line 22690) | function LGraphTextureResize() {
  function LGraphTextureAverage (line 22752) | function LGraphTextureAverage() {
  function LGraphTextureMinMax (line 22908) | function LGraphTextureMinMax() {
  function LGraphTextureTemporalSmooth (line 23034) | function LGraphTextureTemporalSmooth() {
  function LGraphTextureLinearAvgSmooth (line 23116) | function LGraphTextureLinearAvgSmooth() {
  function LGraphImageToTexture (line 23242) | function LGraphImageToTexture() {
  function LGraphTextureLUT (line 23294) | function LGraphTextureLUT() {
  function LGraphTextureEncode (line 23414) | function LGraphTextureEncode() {
  function LGraphTextureChannels (line 23537) | function LGraphTextureChannels() {
  function LGraphChannelsTexture (line 23644) | function LGraphChannelsTexture() {
  function LGraphTextureColor (line 23761) | function LGraphTextureColor() {
  function LGraphTextureGradient (line 23860) | function LGraphTextureGradient() {
  function LGraphTextureMix (line 23973) | function LGraphTextureMix() {
  function LGraphTextureEdges (line 24092) | function LGraphTextureEdges() {
  function LGraphTextureDepthRange (line 24195) | function LGraphTextureDepthRange() {
  function LGraphTextureLinearDepth (line 24337) | function LGraphTextureLinearDepth() {
  function LGraphTextureBlur (line 24433) | function LGraphTextureBlur() {
  function FXGlow (line 24556) | function FXGlow()
  function LGraphTextureGlow (line 24787) | function LGraphTextureGlow() {
  function LGraphTextureKuwaharaFilter (line 24947) | function LGraphTextureKuwaharaFilter() {
  function LGraphTextureXDoGFilter (line 25137) | function LGraphTextureXDoGFilter() {
  function LGraphTextureWebcam (line 25277) | function LGraphTextureWebcam() {
  function onFailSoHard (line 25308) | function onFailSoHard(e) {
  function LGraphLensFX (line 25466) | function LGraphLensFX() {
  function LGraphTextureFromData (line 25604) | function LGraphTextureFromData() {
  function LGraphTextureCurve (line 25652) | function LGraphTextureCurve() {
  function LGraphExposition (line 25868) | function LGraphExposition() {
  function LGraphToneMapping (line 25946) | function LGraphToneMapping() {
  function LGraphTexturePerlin (line 26110) | function LGraphTexturePerlin() {
  function LGraphTextureCanvas2D (line 26296) | function LGraphTextureCanvas2D() {
  function LGraphTextureMatte (line 26440) | function LGraphTextureMatte() {
  function LGraphCubemapToTexture2D (line 26535) | function LGraphCubemapToTexture2D() {
  function parseGLSLDescriptions (line 26623) | function parseGLSLDescriptions()
  function registerShaderNode (line 26649) | function registerShaderNode( type, node_ctor )
  function getShaderNodeVarName (line 26708) | function getShaderNodeVarName( node, name )
  function getInputLinkID (line 26713) | function getInputLinkID( node, slot )
  function getOutputLinkID (line 26729) | function getOutputLinkID( node, slot )
  function LGShaderContext (line 26892) | function LGShaderContext()
  function LGraphShaderGraph (line 27089) | function LGraphShaderGraph() {
  function shaderNodeFromFunction (line 27325) | function shaderNodeFromFunction( classname, params, return_type, code )
  function LGraphShaderUniform (line 27333) | function LGraphShaderUniform() {
  function LGraphShaderAttribute (line 27387) | function LGraphShaderAttribute() {
  function LGraphShaderSampler2D (line 27426) | function LGraphShaderSampler2D() {
  function LGraphShaderConstant (line 27467) | function LGraphShaderConstant()
  function LGraphShaderVec2 (line 27569) | function LGraphShaderVec2()
  function LGraphShaderVec3 (line 27625) | function LGraphShaderVec3()
  function LGraphShaderVec4 (line 27690) | function LGraphShaderVec4()
  function LGraphShaderFragColor (line 27760) | function LGraphShaderFragColor() {
  function LGraphShaderOperation (line 27812) | function LGraphShaderOperation()
  function LGraphShaderFunc (line 27885) | function LGraphShaderFunc()
  function LGraphShaderSnippet (line 27986) | function LGraphShaderSnippet()
  function LGraphShaderRand (line 28063) | function LGraphShaderRand()
  function LGraphShaderNoise (line 28086) | function LGraphShaderNoise()
  function LGraphShaderTime (line 28267) | function LGraphShaderTime()
  function LGraphShaderDither (line 28289) | function LGraphShaderDither()
  function LGraphShaderRemap (line 28333) | function LGraphShaderRemap()
  function generateGeometryId (line 28420) | function generateGeometryId() {
  function LGraphPoints3D (line 28424) | function LGraphPoints3D() {
  function findRandomTriangle (line 28756) | function findRandomTriangle( areas, f )
  function LGraphPointsToInstances (line 28911) | function LGraphPointsToInstances() {
  function LGraphGeometryTransform (line 29050) | function LGraphGeometryTransform() {
  function LGraphGeometryPolygon (line 29175) | function LGraphGeometryPolygon() {
  function LGraphGeometryExtrude (line 29249) | function LGraphGeometryExtrude() {
  function LGraphGeometryEval (line 29338) | function LGraphGeometryEval() {
  function LGraphConnectPoints (line 29526) | function LGraphConnectPoints() {
  function LGraphToGeometry (line 29615) | function LGraphToGeometry() {
  function LGraphGeometryToMesh (line 29653) | function LGraphGeometryToMesh() {
  function LGraphRenderMesh (line 29722) | function LGraphRenderMesh() {
  function LGraphGeometryPrimitive (line 29826) | function LGraphGeometryPrimitive() {
  function LGraphRenderPoints (line 29900) | function LGraphRenderPoints() {
  function LGraphFXLens (line 30319) | function LGraphFXLens() {
  function LGraphFXBokeh (line 30560) | function LGraphFXBokeh() {
  function LGraphFXGeneric (line 30806) | function LGraphFXGeneric() {
  function LGraphFXVigneting (line 31006) | function LGraphFXVigneting() {
  function MIDIEvent (line 31105) | function MIDIEvent(data) {
  function MIDIInterface (line 31432) | function MIDIInterface(on_ready, on_error) {
  function LGMIDIIn (line 31567) | function LGMIDIIn() {
  function LGMIDIOut (line 31693) | function LGMIDIOut() {
  function LGMIDIShow (line 31762) | function LGMIDIShow() {
  function LGMIDIFilter (line 31809) | function LGMIDIFilter() {
  function LGMIDIEvent (line 31917) | function LGMIDIEvent() {
  function LGMIDICC (line 32091) | function LGMIDICC() {
  function LGMIDIGenerator (line 32116) | function LGMIDIGenerator() {
  function LGMIDITranspose (line 32207) | function LGMIDITranspose() {
  function LGMIDIQuantize (line 32251) | function LGMIDIQuantize() {
  function LGMIDIFromFile (line 32328) | function LGMIDIFromFile() {
  function LGMIDIPlay (line 32444) | function LGMIDIPlay() {
  function LGMIDIKeys (line 32503) | function LGMIDIKeys() {
  function now (line 32683) | function now() {
  function onError (line 32922) | function onError(err) {
  function LGAudioSource (line 32934) | function LGAudioSource() {
  function inner (line 33137) | function inner(buffer) {
  function LGAudioMediaSource (line 33180) | function LGAudioMediaSource() {
  function onFailSoHard (line 33253) | function onFailSoHard(err) {
  function LGAudioAnalyser (line 33331) | function LGAudioAnalyser() {
  function LGAudioGain (line 33416) | function LGAudioGain() {
  function LGAudioConvolver (line 33448) | function LGAudioConvolver() {
  function inner (line 33506) | function inner(buffer) {
  function LGAudioDynamicsCompressor (line 33518) | function LGAudioDynamicsCompressor() {
  function LGAudioWaveShaper (line 33570) | function LGAudioWaveShaper() {
  function LGAudioMixer (line 33603) | function LGAudioMixer() {
  function LGAudioADSR (line 33675) | function LGAudioADSR() {
  function LGAudioDelay (line 33733) | function LGAudioDelay() {
  function LGAudioBiquadFilter (line 33759) | function LGAudioBiquadFilter() {
  function LGAudioOscillatorNode (line 33814) | function LGAudioOscillatorNode() {
  function LGAudioVisualization (line 33891) | function LGAudioVisualization() {
  function LGAudioBandSignal (line 33963) | function LGAudioBandSignal() {
  function LGAudioScript (line 34014) | function LGAudioScript() {
  function LGAudioDestination (line 34135) | function LGAudioDestination() {
  function LGWebSocket (line 34149) | function LGWebSocket() {
  function LGSillyClient (line 34297) | function LGSillyClient() {
  function HTTPRequestNode (line 34511) | function HTTPRequestNode() {

FILE: docs/quick_conn.js
  function addNodes (line 34) | function addNodes() {
  function addNodes2 (line 94) | function addNodes2() {

FILE: example/comfyui_quick_conn.js
  class Watch (line 22) | class Watch extends LGraphNode {
    method constructor (line 23) | constructor() {
    method onExecute (line 32) | onExecute() {
    method getTitle (line 38) | getTitle() {
    method toString (line 45) | toString(o) {
    method onDrawBackground (line 62) | onDrawBackground(/* ctx */) {
  class ConstantNumber (line 71) | class ConstantNumber extends LGraphNode {
    method constructor (line 72) | constructor() {
    method onExecute (line 83) | onExecute() {
    method getTitle (line 87) | getTitle() {
    method setValue (line 94) | setValue(v) {
    method onDrawBackground (line 98) | onDrawBackground(/* ctx */) {

FILE: example/quick_conn.js
  function addNodes (line 33) | function addNodes() {
  function addNodes2 (line 93) | function addNodes2() {

FILE: js/CircuitBoardLines.js
  constant EPSILON (line 17) | const EPSILON = 1e-6;
  constant INSIDE (line 18) | const INSIDE = 1;
  constant OUTSIDE (line 19) | const OUTSIDE = 0;
  function clipT (line 21) | function clipT(num, denom, c) {
  function liangBarsky (line 48) | function liangBarsky(a, b, box, da, db) {
  class MapLinks (line 89) | class MapLinks {
    method constructor (line 90) | constructor(canvas, config) {
    method isInsideNode (line 101) | isInsideNode(xy) {
    method findClippedNode (line 112) | findClippedNode(outputXY, inputXY) {
    method testPath (line 145) | testPath(path) {
    method mapFinalLink (line 156) | mapFinalLink(outputXY, inputXY) {
    method mapLink (line 260) | mapLink(outputXY, inputXY, targetNodeInfo, isBlocked /* , lastDirectio...
    method expandSourceNodeLinesArea (line 407) | expandSourceNodeLinesArea(sourceNodeInfo, path) {
    method expandTargetNodeLinesArea (line 422) | expandTargetNodeLinesArea(targetNodeInfo, path) {
    method getNodeOnPos (line 438) | getNodeOnPos(xy) {
    method mapLinks (line 454) | mapLinks(nodesByExecution) {
    method drawLinks (line 616) | drawLinks(ctx) {
  class SubgraphSlotProxy (line 760) | class SubgraphSlotProxy {
    method constructor (line 761) | constructor(slot) {
    method links (line 765) | get links() {
  class SubgraphInOutNodeProxy (line 770) | class SubgraphInOutNodeProxy {
    method constructor (line 771) | constructor(subgraphNode, isInput) {
    method id (line 780) | get id() {
    method outputs (line 784) | get outputs() {
    method getSlotPosition (line 792) | getSlotPosition(slot /* , isInput */) {
    method getInputPos (line 796) | getInputPos(slot) {
    method getOutputPos (line 800) | getOutputPos(slot) {
    method getBounding (line 804) | getBounding(area) {
  class EyeButton (line 813) | class EyeButton {
    method constructor (line 814) | constructor() {
    method getEyeButton (line 818) | static getEyeButton() {
    method check (line 826) | check() {
    method listenEyeButton (line 840) | listenEyeButton(onChange) {
  class CircuitBoardLines (line 854) | class CircuitBoardLines {
    method constructor (line 855) | constructor() {
    method cleanFloat (line 867) | static cleanFloat(n, def) {
    method cleanInteger (line 875) | static cleanInteger(n, def) {
    method setEnabled (line 879) | setEnabled(e) { this.enabled = e; }
    method isShow (line 881) | isShow() {
    method recalcMapLinksTimeout (line 889) | recalcMapLinksTimeout() {
    method redraw (line 906) | redraw() {
    method recalcMapLinksCheck (line 923) | recalcMapLinksCheck() {
    method recalcMapLinks (line 934) | recalcMapLinks() {
    method drawConnections (line 954) | drawConnections(
    method init (line 973) | init() {
    method initOverrides (line 997) | initOverrides(canvas) {

FILE: js/QuickConnection.js
  class QuickConnection (line 10) | class QuickConnection {
    method constructor (line 11) | constructor() {
    method mouseUp (line 25) | mouseUp(origThis, origFunc, args) {
    method init (line 60) | init() {
    method setGraphMouseFromClientXY (line 79) | setGraphMouseFromClientXY(clientX, clientY) {
    method initListeners (line 91) | initListeners(canvas) {
    method getCurrentConnection (line 191) | getCurrentConnection() {
    method pointerDown (line 218) | pointerDown() {
    method pointerUp (line 223) | pointerUp() {
    method findAcceptingNodes (line 264) | findAcceptingNodes(fromConnection, fromNode, findInput) {
    method addOnCanvas (line 315) | addOnCanvas(name, func) {
    method onDrawOverlay (line 326) | onDrawOverlay(ctx) {

FILE: js/quick_conn_start.js
  method init (line 62) | init() {
  method init (line 211) | init() {
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,294K chars).
[
  {
    "path": ".editorconfig",
    "chars": 39,
    "preview": "[*]\nindent_style = tab\nindent_size = 2\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 660,
    "preview": "name: Publish to Comfy registry\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n      - master\n    paths:\n  "
  },
  {
    "path": ".gitignore",
    "chars": 89,
    "preview": "# example/links is made by example/run_example.sh\nexample/links\nnode_modules\n__pycache__\n"
  },
  {
    "path": "README.md",
    "chars": 2000,
    "preview": "\nAdds circuit board connections, quick connections to ComfyUI\n\n![Example](imgs/CircuitBoardExample.webp)\n![Example](imgs"
  },
  {
    "path": "__init__.py",
    "chars": 164,
    "preview": "\n# Set the web directory, any .js file in that directory will be loaded by the frontend as a frontend extension\nWEB_DIRE"
  },
  {
    "path": "docs/CircuitBoardLines.js",
    "chars": 24163,
    "preview": "/* eslint max-classes-per-file: 0 */\n/* eslint no-tabs: 0 */\n/* eslint no-underscore-dangle:0 */\n/* eslint import/prefer"
  },
  {
    "path": "docs/QuickConnection.js",
    "chars": 12098,
    "preview": "/* eslint no-tabs: 0 */\n/* eslint import/prefer-default-export:0 */\n/* eslint no-underscore-dangle:0 */\n/* eslint no-plu"
  },
  {
    "path": "docs/links/litegraph.css",
    "chars": 12820,
    "preview": "/* this CSS contains only the basic CSS needed to run the app and use it */\n\n.lgraphcanvas {\n    /*cursor: crosshair;*/\n"
  },
  {
    "path": "docs/links/litegraph.js",
    "chars": 1074483,
    "preview": "//packer version\n\n\n(function(global) {\n    // *************************************************************\n    //   Lit"
  },
  {
    "path": "docs/quick_conn.html",
    "chars": 602,
    "preview": "<html>\n<head>\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"links/litegraph.css\">\n\t<script type=\"text/javascript\" src=\"l"
  },
  {
    "path": "docs/quick_conn.js",
    "chars": 3428,
    "preview": "/* global LGraphCanvas */\n/* global LGraph */\n/* global LiteGraph */\n/* eslint camelcase:0 */\n/* eslint import/extension"
  },
  {
    "path": "example/comfyui_quick_conn.html",
    "chars": 486,
    "preview": "<html>\n<head>\n\t<!--\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"litegraph.css\">\n\t<script type=\"text/javascript\" src=\"l"
  },
  {
    "path": "example/comfyui_quick_conn.js",
    "chars": 2916,
    "preview": "/* eslint camelcase:0 */\n/* eslint no-else-return:0 */\n/* eslint max-classes-per-file:0 */\n\nimport {\n\tLGraphNode,\n\tLGrap"
  },
  {
    "path": "example/make_docs.sh",
    "chars": 148,
    "preview": "\nmkdir -p docs/links\ncp -auL example/quick_conn.* js/CircuitBoardLines.js js/QuickConnection.js docs/\ncp -auL example/li"
  },
  {
    "path": "example/quick_conn.html",
    "chars": 602,
    "preview": "<html>\n<head>\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"links/litegraph.css\">\n\t<script type=\"text/javascript\" src=\"l"
  },
  {
    "path": "example/quick_conn.js",
    "chars": 3370,
    "preview": "/* global LGraphCanvas */\n/* global LGraph */\n/* global LiteGraph */\n/* eslint camelcase:0 */\n\nimport { QuickConnection "
  },
  {
    "path": "example/run_example.sh",
    "chars": 453,
    "preview": "\n\nmkdir -p example/links\npushd example/links\nln -s ../../node_modules/@comfyorg/litegraph/dist/css/litegraph.css comfyui"
  },
  {
    "path": "imgs/SpeedUpMp4ToGif.sh",
    "chars": 130,
    "preview": "\nffmpeg -y -i \"$1\" -filter_complex \"[0:v]fps=10,split[a][b];[b]palettegen[p];[a][p]paletteuse,setpts=0.5*PTS[v]\" -map '["
  },
  {
    "path": "js/CircuitBoardLines.js",
    "chars": 25449,
    "preview": "/* eslint max-classes-per-file: 0 */\n/* eslint no-tabs: 0 */\n/* eslint no-underscore-dangle:0 */\n/* eslint prefer-rest-p"
  },
  {
    "path": "js/QuickConnection.js",
    "chars": 15928,
    "preview": "/* eslint no-tabs: 0 */\n/* eslint no-underscore-dangle:0 */\n/* eslint no-plusplus:0 */\n/* eslint prefer-rest-params:0 */"
  },
  {
    "path": "js/quick_conn_start.js",
    "chars": 5990,
    "preview": "/* eslint quotes:0 */\n/* eslint prefer-spread:0 */\n/* global LiteGraph */\n/* eslint operator-linebreak:0 */\n\nimport { ap"
  },
  {
    "path": "package.json",
    "chars": 337,
    "preview": "{\n\t\"type\": \"module\",\n\t\"scripts\": {\n\t\t\"example\": \"bash example/run_example.sh\",\n\t\t\"docs\": \"bash example/make_docs.sh\",\n\t\t"
  },
  {
    "path": "pyproject.toml",
    "chars": 358,
    "preview": "[project]\nname = \"quick-connections\"\ndescription = \"Quick connections, Circuit board connections\"\nversion = \"1.0.29\"\nlic"
  }
]

About this extraction

This page contains the full source code of the niknah/quick-connections GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (1.1 MB), approximately 295.6k tokens, and a symbol index with 418 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!