diff --git a/src/LGraph.ts b/src/LGraph.ts
index 56f690f24..a91102ac9 100644
--- a/src/LGraph.ts
+++ b/src/LGraph.ts
@@ -1,5 +1,6 @@
// @ts-nocheck
-import { LiteGraph, LGraphCanvas } from "./litegraph";
+import { LiteGraph } from "./litegraph";
+import { LGraphCanvas } from "./LGraphCanvas";
import { LGraphGroup } from "./LGraphGroup";
import { LGraphNode } from "./LGraphNode";
import { LLink } from "./LLink";
diff --git a/src/LGraphCanvas.ts b/src/LGraphCanvas.ts
new file mode 100644
index 000000000..54cecd1bd
--- /dev/null
+++ b/src/LGraphCanvas.ts
@@ -0,0 +1,8108 @@
+// @ts-nocheck
+import { DragAndScale } from "./DragAndScale";
+import { drawSlot, LabelPosition } from "./draw";
+import { LiteGraph, clamp } from "./litegraph";
+import { isInsideRectangle, distance, overlapBounding, LiteGraphGlobal } from "./LiteGraphGlobal";
+
+//*********************************************************************************
+// LGraphCanvas: LGraph renderer CLASS
+//*********************************************************************************
+/**
+ * This class is in charge of rendering one graph inside a canvas. And provides all the interaction required.
+ * Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked
+ *
+ * @class LGraphCanvas
+ * @constructor
+ * @param {HTMLCanvas} canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself)
+ * @param {LGraph} graph [optional]
+ * @param {Object} options [optional] { skip_rendering, autoresize, viewport }
+ */
+
+export class LGraphCanvas {
+
+ /* Interaction */
+ static #temp = new Float32Array(4);
+ static #temp_vec2 = new Float32Array(2);
+ static #tmp_area = new Float32Array(4);
+ static #margin_area = new Float32Array(4);
+ static #link_bounding = new Float32Array(4);
+ static #tempA = new Float32Array(2);
+ static #tempB = new Float32Array(2);
+
+ static DEFAULT_BACKGROUND_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=";
+
+ // TODO: Remove workaround - this should be instance-based, regardless. "-1" formerly pointed to LiteGraph.EVENT_LINK_COLOR.
+ static link_type_colors = {
+ "-1": LiteGraphGlobal.DEFAULT_EVENT_LINK_COLOR,
+ number: "#AAA",
+ node: "#DCA"
+ };
+ static gradients = {}; //cache of gradients
+
+ static search_limit = -1;
+ static node_colors = {
+ red: { color: "#322", bgcolor: "#533", groupcolor: "#A88" },
+ brown: { color: "#332922", bgcolor: "#593930", groupcolor: "#b06634" },
+ green: { color: "#232", bgcolor: "#353", groupcolor: "#8A8" },
+ blue: { color: "#223", bgcolor: "#335", groupcolor: "#88A" },
+ pale_blue: {
+ color: "#2a363b",
+ bgcolor: "#3f5159",
+ groupcolor: "#3f789e"
+ },
+ cyan: { color: "#233", bgcolor: "#355", groupcolor: "#8AA" },
+ purple: { color: "#323", bgcolor: "#535", groupcolor: "#a1309b" },
+ yellow: { color: "#432", bgcolor: "#653", groupcolor: "#b58b2a" },
+ black: { color: "#222", bgcolor: "#000", groupcolor: "#444" }
+ };
+
+ constructor(canvas, graph, options) {
+ this.options = options = options || {};
+
+ //if(graph === undefined)
+ // throw ("No graph assigned");
+ this.background_image = LGraphCanvas.DEFAULT_BACKGROUND_IMAGE;
+
+ if (canvas && canvas.constructor === String) {
+ canvas = document.querySelector(canvas);
+ }
+
+ this.ds = new DragAndScale();
+ this.zoom_modify_alpha = true; //otherwise it generates ugly patterns when scaling down too much
+ this.zoom_speed = 1.1; // in range (1.01, 2.5). Less than 1 will invert the zoom direction
+
+ this.title_text_font = "" + LiteGraph.NODE_TEXT_SIZE + "px Arial";
+ this.inner_text_font =
+ "normal " + LiteGraph.NODE_SUBTEXT_SIZE + "px Arial";
+ this.node_title_color = LiteGraph.NODE_TITLE_COLOR;
+ this.default_link_color = LiteGraph.LINK_COLOR;
+ this.default_connection_color = {
+ input_off: "#778",
+ input_on: "#7F7", //"#BBD"
+ output_off: "#778",
+ output_on: "#7F7" //"#BBD"
+ };
+ this.default_connection_color_byType = {
+ /*number: "#7F7",
+ string: "#77F",
+ boolean: "#F77",*/
+ };
+ this.default_connection_color_byTypeOff = {
+ /*number: "#474",
+ string: "#447",
+ boolean: "#744",*/
+ };
+
+ this.highquality_render = true;
+ this.use_gradients = false; //set to true to render titlebar with gradients
+ this.editor_alpha = 1; //used for transition
+ this.pause_rendering = false;
+ this.clear_background = true;
+ this.clear_background_color = "#222";
+
+ this.read_only = false; //if set to true users cannot modify the graph
+ this.render_only_selected = true;
+ this.live_mode = false;
+ this.show_info = true;
+ this.allow_dragcanvas = true;
+ this.allow_dragnodes = true;
+ this.allow_interaction = true; //allow to control widgets, buttons, collapse, etc
+ this.multi_select = false; //allow selecting multi nodes without pressing extra keys
+ this.allow_searchbox = true;
+ this.allow_reconnect_links = true; //allows to change a connection with having to redo it again
+ this.align_to_grid = false; //snap to grid
+
+ this.drag_mode = false;
+ this.dragging_rectangle = null;
+
+ this.filter = null; //allows to filter to only accept some type of nodes in a graph
+
+ this.set_canvas_dirty_on_mouse_event = true; //forces to redraw the canvas on mouse events (except move)
+ this.always_render_background = false;
+ this.render_shadows = true;
+ this.render_canvas_border = true;
+ this.render_connections_shadows = false; //too much cpu
+ this.render_connections_border = true;
+ this.render_curved_connections = false;
+ this.render_connection_arrows = false;
+ this.render_collapsed_slots = true;
+ this.render_execution_order = false;
+ this.render_title_colored = true;
+ this.render_link_tooltip = true;
+
+ this.links_render_mode = LiteGraph.SPLINE_LINK;
+
+ this.mouse = [0, 0]; //mouse in canvas coordinates, where 0,0 is the top-left corner of the blue rectangle
+ this.graph_mouse = [0, 0]; //mouse in graph coordinates, where 0,0 is the top-left corner of the blue rectangle
+ this.canvas_mouse = this.graph_mouse; //LEGACY: REMOVE THIS, USE GRAPH_MOUSE INSTEAD
+
+
+
+ //to personalize the search box
+ this.onSearchBox = null;
+ this.onSearchBoxSelection = null;
+
+ //callbacks
+ this.onMouse = null;
+ this.onDrawBackground = null; //to render background objects (behind nodes and connections) in the canvas affected by transform
+ this.onDrawForeground = null; //to render foreground objects (above nodes and connections) in the canvas affected by transform
+ this.onDrawOverlay = null; //to render foreground objects not affected by transform (for GUIs)
+ this.onDrawLinkTooltip = null; //called when rendering a tooltip
+ this.onNodeMoved = null; //called after moving a node
+ this.onSelectionChange = null; //called if the selection changes
+ this.onConnectingChange = null; //called before any link changes
+ this.onBeforeChange = null; //called before modifying the graph
+ this.onAfterChange = null; //called after modifying the graph
+
+ this.connections_width = 3;
+ this.round_radius = 8;
+
+ this.current_node = null;
+ this.node_widget = null; //used for widgets
+ this.over_link_center = null;
+ this.last_mouse_position = [0, 0];
+ this.visible_area = this.ds.visible_area;
+ this.visible_links = [];
+ this.connecting_links = null; // Explicitly null-checked
+
+ this.viewport = options.viewport || null; //to constraint render area to a portion of the canvas
+
+
+
+ //link canvas and graph
+ if (graph) {
+ graph.attachCanvas(this);
+ }
+
+ this.setCanvas(canvas, options.skip_events);
+ this.clear();
+
+ if (!options.skip_render) {
+ this.startRendering();
+ }
+
+ this.autoresize = options.autoresize;
+ }
+ static getFileExtension(url) {
+ var question = url.indexOf("?");
+ if (question != -1) {
+ url = url.substr(0, question);
+ }
+ var point = url.lastIndexOf(".");
+ if (point == -1) {
+ return "";
+ }
+ return url.substr(point + 1).toLowerCase();
+ }
+ /* this is an implementation for touch not in production and not ready
+ */
+ /*LGraphCanvas.prototype.touchHandler = function(event) {
+ //alert("foo");
+ var touches = event.changedTouches,
+ first = touches[0],
+ type = "";
+
+ switch (event.type) {
+ case "touchstart":
+ type = "mousedown";
+ break;
+ case "touchmove":
+ type = "mousemove";
+ break;
+ case "touchend":
+ type = "mouseup";
+ break;
+ default:
+ return;
+ }
+
+ //initMouseEvent(type, canBubble, cancelable, view, clickCount,
+ // screenX, screenY, clientX, clientY, ctrlKey,
+ // altKey, shiftKey, metaKey, button, relatedTarget);
+
+ // this is eventually a Dom object, get the LGraphCanvas back
+ if(typeof this.getCanvasWindow == "undefined"){
+ var window = this.lgraphcanvas.getCanvasWindow();
+ }else{
+ var window = this.getCanvasWindow();
+ }
+
+ var document = window.document;
+
+ var simulatedEvent = document.createEvent("MouseEvent");
+ simulatedEvent.initMouseEvent(
+ type,
+ true,
+ true,
+ window,
+ 1,
+ first.screenX,
+ first.screenY,
+ first.clientX,
+ first.clientY,
+ false,
+ false,
+ false,
+ false,
+ 0, //left
+ null
+ );
+ first.target.dispatchEvent(simulatedEvent);
+ event.preventDefault();
+ };*/
+ /* CONTEXT MENU ********************/
+ static onGroupAdd(info, entry, mouse_event) {
+ var canvas = LGraphCanvas.active_canvas;
+ var ref_window = canvas.getCanvasWindow();
+
+ var group = new LiteGraph.LGraphGroup();
+ group.pos = canvas.convertEventToCanvasOffset(mouse_event);
+ canvas.graph.add(group);
+ }
+ /**
+ * Determines the furthest nodes in each direction
+ * @param nodes {LGraphNode[]} the nodes to from which boundary nodes will be extracted
+ * @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}}
+ */
+ static getBoundaryNodes(nodes) {
+ let top = null;
+ let right = null;
+ let bottom = null;
+ let left = null;
+ for (const nID in nodes) {
+ const node = nodes[nID];
+ const [x, y] = node.pos;
+ const [width, height] = node.size;
+
+ if (top === null || y < top.pos[1]) {
+ top = node;
+ }
+ if (right === null || x + width > right.pos[0] + right.size[0]) {
+ right = node;
+ }
+ if (bottom === null || y + height > bottom.pos[1] + bottom.size[1]) {
+ bottom = node;
+ }
+ if (left === null || x < left.pos[0]) {
+ left = node;
+ }
+ }
+
+ return {
+ "top": top,
+ "right": right,
+ "bottom": bottom,
+ "left": left
+ };
+ }
+ /**
+ *
+ * @param {LGraphNode[]} nodes a list of nodes
+ * @param {"top"|"bottom"|"left"|"right"} direction Direction to align the nodes
+ * @param {LGraphNode?} align_to Node to align to (if null, align to the furthest node in the given direction)
+ */
+ static alignNodes(nodes, direction, align_to) {
+ if (!nodes) {
+ return;
+ }
+
+ const canvas = LGraphCanvas.active_canvas;
+ let boundaryNodes = [];
+ if (align_to === undefined) {
+ boundaryNodes = LGraphCanvas.getBoundaryNodes(nodes);
+ } else {
+ boundaryNodes = {
+ "top": align_to,
+ "right": align_to,
+ "bottom": align_to,
+ "left": align_to
+ };
+ }
+
+ for (const [_, node] of Object.entries(canvas.selected_nodes)) {
+ switch (direction) {
+ case "right":
+ node.pos[0] = boundaryNodes["right"].pos[0] + boundaryNodes["right"].size[0] - node.size[0];
+ break;
+ case "left":
+ node.pos[0] = boundaryNodes["left"].pos[0];
+ break;
+ case "top":
+ node.pos[1] = boundaryNodes["top"].pos[1];
+ break;
+ case "bottom":
+ node.pos[1] = boundaryNodes["bottom"].pos[1] + boundaryNodes["bottom"].size[1] - node.size[1];
+ break;
+ }
+ }
+
+ canvas.dirty_canvas = true;
+ canvas.dirty_bgcanvas = true;
+ }
+ static onNodeAlign(value, options, event, prev_menu, node) {
+ new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], {
+ event: event,
+ callback: inner_clicked,
+ parentMenu: prev_menu,
+ });
+
+ function inner_clicked(value) {
+ LGraphCanvas.alignNodes(LGraphCanvas.active_canvas.selected_nodes, value.toLowerCase(), node);
+ }
+ }
+ static onGroupAlign(value, options, event, prev_menu) {
+ new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], {
+ event: event,
+ callback: inner_clicked,
+ parentMenu: prev_menu,
+ });
+
+ function inner_clicked(value) {
+ LGraphCanvas.alignNodes(LGraphCanvas.active_canvas.selected_nodes, value.toLowerCase());
+ }
+ }
+ static onMenuAdd(node, options, e, prev_menu, callback) {
+
+ var canvas = LGraphCanvas.active_canvas;
+ var ref_window = canvas.getCanvasWindow();
+ var graph = canvas.graph;
+ if (!graph)
+ return;
+
+ function inner_onMenuAdded(base_category, prev_menu) {
+
+ var categories = LiteGraph.getNodeTypesCategories(canvas.filter || graph.filter).filter(function (category) { return category.startsWith(base_category); });
+ var entries = [];
+
+ categories.map(function (category) {
+
+ if (!category)
+ return;
+
+ var base_category_regex = new RegExp('^(' + base_category + ')');
+ var category_name = category.replace(base_category_regex, "").split('/')[0];
+ var category_path = base_category === '' ? category_name + '/' : base_category + category_name + '/';
+
+ var name = category_name;
+ if (name.indexOf("::") != -1) //in case it has a namespace like "shader::math/rand" it hides the namespace
+ name = name.split("::")[1];
+
+ var index = entries.findIndex(function (entry) { return entry.value === category_path; });
+ if (index === -1) {
+ entries.push({
+ value: category_path, content: name, has_submenu: true, callback: function (value, event, mouseEvent, contextMenu) {
+ inner_onMenuAdded(value.value, contextMenu);
+ }
+ });
+ }
+
+ });
+
+ var nodes = LiteGraph.getNodeTypesInCategory(base_category.slice(0, -1), canvas.filter || graph.filter);
+ nodes.map(function (node) {
+
+ if (node.skip_list)
+ return;
+
+ var entry = {
+ value: node.type, content: node.title, has_submenu: false, callback: function (value, event, mouseEvent, contextMenu) {
+
+ var first_event = contextMenu.getFirstEvent();
+ canvas.graph.beforeChange();
+ var node = LiteGraph.createNode(value.value);
+ if (node) {
+ node.pos = canvas.convertEventToCanvasOffset(first_event);
+ canvas.graph.add(node);
+ }
+ if (callback)
+ callback(node);
+ canvas.graph.afterChange();
+
+ }
+ };
+
+ entries.push(entry);
+
+ });
+
+ new LiteGraph.ContextMenu(entries, { event: e, parentMenu: prev_menu }, ref_window);
+
+ }
+
+ inner_onMenuAdded('', prev_menu);
+ return false;
+
+ }
+ static onMenuCollapseAll() { }
+ static onMenuNodeEdit() { }
+ static showMenuNodeOptionalInputs(v,
+ options,
+ e,
+ prev_menu,
+ node) {
+ if (!node) {
+ return;
+ }
+
+ var that = this;
+ var canvas = LGraphCanvas.active_canvas;
+ var ref_window = canvas.getCanvasWindow();
+
+ var options = node.optional_inputs;
+ if (node.onGetInputs) {
+ options = node.onGetInputs();
+ }
+
+ var entries = [];
+ if (options) {
+ for (var i = 0; i < options.length; i++) {
+ var entry = options[i];
+ if (!entry) {
+ entries.push(null);
+ continue;
+ }
+ var label = entry[0];
+ if (!entry[2])
+ entry[2] = {};
+
+ if (entry[2].label) {
+ label = entry[2].label;
+ }
+
+ entry[2].removable = true;
+ var data = { content: label, value: entry };
+ if (entry[1] == LiteGraph.ACTION) {
+ data.className = "event";
+ }
+ entries.push(data);
+ }
+ }
+
+ if (node.onMenuNodeInputs) {
+ var retEntries = node.onMenuNodeInputs(entries);
+ if (retEntries) entries = retEntries;
+ }
+
+ if (!entries.length) {
+ console.log("no input entries");
+ return;
+ }
+
+ var menu = new LiteGraph.ContextMenu(
+ entries,
+ {
+ event: e,
+ callback: inner_clicked,
+ parentMenu: prev_menu,
+ node: node
+ },
+ ref_window
+ );
+
+ function inner_clicked(v, e, prev) {
+ if (!node) {
+ return;
+ }
+
+ if (v.callback) {
+ v.callback.call(that, node, v, e, prev);
+ }
+
+ if (v.value) {
+ node.graph.beforeChange();
+ node.addInput(v.value[0], v.value[1], v.value[2]);
+
+ if (node.onNodeInputAdd) { // callback to the node when adding a slot
+ node.onNodeInputAdd(v.value);
+ }
+ node.setDirtyCanvas(true, true);
+ node.graph.afterChange();
+ }
+ }
+
+ return false;
+ }
+ static showMenuNodeOptionalOutputs(v,
+ options,
+ e,
+ prev_menu,
+ node) {
+ if (!node) {
+ return;
+ }
+
+ var that = this;
+ var canvas = LGraphCanvas.active_canvas;
+ var ref_window = canvas.getCanvasWindow();
+
+ var options = node.optional_outputs;
+ if (node.onGetOutputs) {
+ options = node.onGetOutputs();
+ }
+
+ var entries = [];
+ if (options) {
+ for (var i = 0; i < options.length; i++) {
+ var entry = options[i];
+ if (!entry) {
+ //separator?
+ entries.push(null);
+ continue;
+ }
+
+ if (node.flags &&
+ node.flags.skip_repeated_outputs &&
+ node.findOutputSlot(entry[0]) != -1) {
+ continue;
+ } //skip the ones already on
+ var label = entry[0];
+ if (!entry[2])
+ entry[2] = {};
+ if (entry[2].label) {
+ label = entry[2].label;
+ }
+ entry[2].removable = true;
+ var data = { content: label, value: entry };
+ if (entry[1] == LiteGraph.EVENT) {
+ data.className = "event";
+ }
+ entries.push(data);
+ }
+ }
+
+ if (this.onMenuNodeOutputs) {
+ entries = this.onMenuNodeOutputs(entries);
+ }
+ if (LiteGraph.do_add_triggers_slots) { //canvas.allow_addOutSlot_onExecuted
+ if (node.findOutputSlot("onExecuted") == -1) {
+ entries.push({ content: "On Executed", value: ["onExecuted", LiteGraph.EVENT, { nameLocked: true }], className: "event" }); //, opts: {}
+ }
+ }
+ // add callback for modifing the menu elements onMenuNodeOutputs
+ if (node.onMenuNodeOutputs) {
+ var retEntries = node.onMenuNodeOutputs(entries);
+ if (retEntries) entries = retEntries;
+ }
+
+ if (!entries.length) {
+ return;
+ }
+
+ var menu = new LiteGraph.ContextMenu(
+ entries,
+ {
+ event: e,
+ callback: inner_clicked,
+ parentMenu: prev_menu,
+ node: node
+ },
+ ref_window
+ );
+
+ function inner_clicked(v, e, prev) {
+ if (!node) {
+ return;
+ }
+
+ if (v.callback) {
+ v.callback.call(that, node, v, e, prev);
+ }
+
+ if (!v.value) {
+ return;
+ }
+
+ var value = v.value[1];
+
+ if (value &&
+ (value.constructor === Object || value.constructor === Array)) {
+ //submenu why?
+ var entries = [];
+ for (var i in value) {
+ entries.push({ content: i, value: value[i] });
+ }
+ new LiteGraph.ContextMenu(entries, {
+ event: e,
+ callback: inner_clicked,
+ parentMenu: prev_menu,
+ node: node
+ });
+ return false;
+ } else {
+ node.graph.beforeChange();
+ node.addOutput(v.value[0], v.value[1], v.value[2]);
+
+ if (node.onNodeOutputAdd) { // a callback to the node when adding a slot
+ node.onNodeOutputAdd(v.value);
+ }
+ node.setDirtyCanvas(true, true);
+ node.graph.afterChange();
+ }
+ }
+
+ return false;
+ }
+ static onShowMenuNodeProperties(value,
+ options,
+ e,
+ prev_menu,
+ node) {
+ if (!node || !node.properties) {
+ return;
+ }
+
+ var that = this;
+ var canvas = LGraphCanvas.active_canvas;
+ var ref_window = canvas.getCanvasWindow();
+
+ var entries = [];
+ for (var i in node.properties) {
+ var value = node.properties[i] !== undefined ? node.properties[i] : " ";
+ if (typeof value == "object")
+ value = JSON.stringify(value);
+ var info = node.getPropertyInfo(i);
+ if (info.type == "enum" || info.type == "combo")
+ value = LGraphCanvas.getPropertyPrintableValue(value, info.values);
+
+ //value could contain invalid html characters, clean that
+ value = LGraphCanvas.decodeHTML(value);
+ entries.push({
+ content: "" +
+ (info.label ? info.label : i) +
+ "" +
+ "" +
+ value +
+ "",
+ value: i
+ });
+ }
+ if (!entries.length) {
+ return;
+ }
+
+ var menu = new LiteGraph.ContextMenu(
+ entries,
+ {
+ event: e,
+ callback: inner_clicked,
+ parentMenu: prev_menu,
+ allow_html: true,
+ node: node
+ },
+ ref_window
+ );
+
+ function inner_clicked(v, options, e, prev) {
+ if (!node) {
+ return;
+ }
+ var rect = this.getBoundingClientRect();
+ canvas.showEditPropertyValue(node, v.value, {
+ position: [rect.left, rect.top]
+ });
+ }
+
+ return false;
+ }
+ static decodeHTML(str) {
+ var e = document.createElement("div");
+ e.innerText = str;
+ return e.innerHTML;
+ }
+ static onMenuResizeNode(value, options, e, menu, node) {
+ if (!node) {
+ return;
+ }
+
+ var fApplyMultiNode = function (node) {
+ node.size = node.computeSize();
+ if (node.onResize)
+ node.onResize(node.size);
+ };
+
+ var graphcanvas = LGraphCanvas.active_canvas;
+ if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) {
+ fApplyMultiNode(node);
+ } else {
+ for (var i in graphcanvas.selected_nodes) {
+ fApplyMultiNode(graphcanvas.selected_nodes[i]);
+ }
+ }
+
+ node.setDirtyCanvas(true, true);
+ }
+ // TODO refactor :: this is used fot title but not for properties!
+ static onShowPropertyEditor(item, options, e, menu, node) {
+ var input_html = "";
+ var property = item.property || "title";
+ var value = node[property];
+
+ // TODO refactor :: use createDialog ?
+ var dialog = document.createElement("div");
+ dialog.is_modified = false;
+ dialog.className = "graphdialog";
+ dialog.innerHTML =
+ "";
+ dialog.close = function () {
+ if (dialog.parentNode) {
+ dialog.parentNode.removeChild(dialog);
+ }
+ };
+ var title = dialog.querySelector(".name");
+ title.innerText = property;
+ var input = dialog.querySelector(".value");
+ if (input) {
+ input.value = value;
+ input.addEventListener("blur", function (e) {
+ this.focus();
+ });
+ input.addEventListener("keydown", function (e) {
+ dialog.is_modified = true;
+ if (e.keyCode == 27) {
+ //ESC
+ dialog.close();
+ } else if (e.keyCode == 13) {
+ inner(); // save
+ } else if (e.keyCode != 13 && e.target.localName != "textarea") {
+ return;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ });
+ }
+
+ var graphcanvas = LGraphCanvas.active_canvas;
+ var canvas = graphcanvas.canvas;
+
+ var rect = canvas.getBoundingClientRect();
+ var offsetx = -20;
+ var offsety = -20;
+ if (rect) {
+ offsetx -= rect.left;
+ offsety -= rect.top;
+ }
+
+ if (event) {
+ dialog.style.left = event.clientX + offsetx + "px";
+ dialog.style.top = event.clientY + offsety + "px";
+ } else {
+ dialog.style.left = canvas.width * 0.5 + offsetx + "px";
+ dialog.style.top = canvas.height * 0.5 + offsety + "px";
+ }
+
+ var button = dialog.querySelector("button");
+ button.addEventListener("click", inner);
+ canvas.parentNode.appendChild(dialog);
+
+ if (input) input.focus();
+
+ var dialogCloseTimer = null;
+ dialog.addEventListener("mouseleave", function (e) {
+ if (LiteGraph.dialog_close_on_mouse_leave)
+ if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave)
+ dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close();
+ });
+ dialog.addEventListener("mouseenter", function (e) {
+ if (LiteGraph.dialog_close_on_mouse_leave)
+ if (dialogCloseTimer) clearTimeout(dialogCloseTimer);
+ });
+
+ function inner() {
+ if (input) setValue(input.value);
+ }
+
+ function setValue(value) {
+ if (item.type == "Number") {
+ value = Number(value);
+ } else if (item.type == "Boolean") {
+ value = Boolean(value);
+ }
+ node[property] = value;
+ if (dialog.parentNode) {
+ dialog.parentNode.removeChild(dialog);
+ }
+ node.setDirtyCanvas(true, true);
+ }
+ }
+ static getPropertyPrintableValue(value, values) {
+ if (!values)
+ return String(value);
+
+ if (values.constructor === Array) {
+ return String(value);
+ }
+
+ if (values.constructor === Object) {
+ var desc_value = "";
+ for (var k in values) {
+ if (values[k] != value)
+ continue;
+ desc_value = k;
+ break;
+ }
+ return String(value) + " (" + desc_value + ")";
+ }
+ }
+ static onMenuNodeCollapse(value, options, e, menu, node) {
+ node.graph.beforeChange( /*?*/);
+
+ var fApplyMultiNode = function (node) {
+ node.collapse();
+ };
+
+ var graphcanvas = LGraphCanvas.active_canvas;
+ if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) {
+ fApplyMultiNode(node);
+ } else {
+ for (var i in graphcanvas.selected_nodes) {
+ fApplyMultiNode(graphcanvas.selected_nodes[i]);
+ }
+ }
+
+ node.graph.afterChange( /*?*/);
+ }
+ static onMenuNodePin(value, options, e, menu, node) {
+ }
+ static onMenuNodeMode(value, options, e, menu, node) {
+ new LiteGraph.ContextMenu(
+ LiteGraph.NODE_MODES,
+ { event: e, callback: inner_clicked, parentMenu: menu, node: node }
+ );
+
+ function inner_clicked(v) {
+ if (!node) {
+ return;
+ }
+ var kV = Object.values(LiteGraph.NODE_MODES).indexOf(v);
+ var fApplyMultiNode = function (node) {
+ if (kV >= 0 && LiteGraph.NODE_MODES[kV])
+ node.changeMode(kV);
+ else {
+ console.warn("unexpected mode: " + v);
+ node.changeMode(LiteGraph.ALWAYS);
+ }
+ };
+
+ var graphcanvas = LGraphCanvas.active_canvas;
+ if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) {
+ fApplyMultiNode(node);
+ } else {
+ for (var i in graphcanvas.selected_nodes) {
+ fApplyMultiNode(graphcanvas.selected_nodes[i]);
+ }
+ }
+ }
+
+ return false;
+ }
+ static onMenuNodeColors(value, options, e, menu, node) {
+ if (!node) {
+ throw "no node for color";
+ }
+
+ var values = [];
+ values.push({
+ value: null,
+ content: "No color"
+ });
+
+ for (var i in LGraphCanvas.node_colors) {
+ var color = LGraphCanvas.node_colors[i];
+ var value = {
+ value: i,
+ content: "" +
+ i +
+ ""
+ };
+ values.push(value);
+ }
+ new LiteGraph.ContextMenu(values, {
+ event: e,
+ callback: inner_clicked,
+ parentMenu: menu,
+ node: node
+ });
+
+ function inner_clicked(v) {
+ if (!node) {
+ return;
+ }
+
+ var color = v.value ? LGraphCanvas.node_colors[v.value] : null;
+
+ var fApplyColor = function (node) {
+ if (color) {
+ if (node.constructor === LiteGraph.LGraphGroup) {
+ node.color = color.groupcolor;
+ } else {
+ node.color = color.color;
+ node.bgcolor = color.bgcolor;
+ }
+ } else {
+ delete node.color;
+ delete node.bgcolor;
+ }
+ };
+
+ var graphcanvas = LGraphCanvas.active_canvas;
+ if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) {
+ fApplyColor(node);
+ } else {
+ for (var i in graphcanvas.selected_nodes) {
+ fApplyColor(graphcanvas.selected_nodes[i]);
+ }
+ }
+ node.setDirtyCanvas(true, true);
+ }
+
+ return false;
+ }
+ static onMenuNodeShapes(value, options, e, menu, node) {
+ if (!node) {
+ throw "no node passed";
+ }
+
+ new LiteGraph.ContextMenu(LiteGraph.VALID_SHAPES, {
+ event: e,
+ callback: inner_clicked,
+ parentMenu: menu,
+ node: node
+ });
+
+ function inner_clicked(v) {
+ if (!node) {
+ return;
+ }
+ node.graph.beforeChange( /*?*/); //node
+
+ var fApplyMultiNode = function (node) {
+ node.shape = v;
+ };
+
+ var graphcanvas = LGraphCanvas.active_canvas;
+ if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) {
+ fApplyMultiNode(node);
+ } else {
+ for (var i in graphcanvas.selected_nodes) {
+ fApplyMultiNode(graphcanvas.selected_nodes[i]);
+ }
+ }
+
+ node.graph.afterChange( /*?*/); //node
+ node.setDirtyCanvas(true);
+ }
+
+ return false;
+ }
+ static onMenuNodeRemove(value, options, e, menu, node) {
+ if (!node) {
+ throw "no node passed";
+ }
+
+ var graph = node.graph;
+ graph.beforeChange();
+
+
+ var fApplyMultiNode = function (node) {
+ if (node.removable === false) {
+ return;
+ }
+ graph.remove(node);
+ };
+
+ var graphcanvas = LGraphCanvas.active_canvas;
+ if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) {
+ fApplyMultiNode(node);
+ } else {
+ for (var i in graphcanvas.selected_nodes) {
+ fApplyMultiNode(graphcanvas.selected_nodes[i]);
+ }
+ }
+
+ graph.afterChange();
+ node.setDirtyCanvas(true, true);
+ }
+ static onMenuNodeToSubgraph(value, options, e, menu, node) {
+ var graph = node.graph;
+ var graphcanvas = LGraphCanvas.active_canvas;
+ if (!graphcanvas) //??
+ return;
+
+ var nodes_list = Object.values(graphcanvas.selected_nodes || {});
+ if (!nodes_list.length)
+ nodes_list = [node];
+
+ var subgraph_node = LiteGraph.createNode("graph/subgraph");
+ subgraph_node.pos = node.pos.concat();
+ graph.add(subgraph_node);
+
+ subgraph_node.buildFromNodes(nodes_list);
+
+ graphcanvas.deselectAllNodes();
+ node.setDirtyCanvas(true, true);
+ }
+ static onMenuNodeClone(value, options, e, menu, node) {
+
+ node.graph.beforeChange();
+
+ var newSelected = {};
+
+ var fApplyMultiNode = function (node) {
+ if (node.clonable === false) {
+ return;
+ }
+ var newnode = node.clone();
+ if (!newnode) {
+ return;
+ }
+ newnode.pos = [node.pos[0] + 5, node.pos[1] + 5];
+ node.graph.add(newnode);
+ newSelected[newnode.id] = newnode;
+ };
+
+ var graphcanvas = LGraphCanvas.active_canvas;
+ if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) {
+ fApplyMultiNode(node);
+ } else {
+ for (var i in graphcanvas.selected_nodes) {
+ fApplyMultiNode(graphcanvas.selected_nodes[i]);
+ }
+ }
+
+ if (Object.keys(newSelected).length) {
+ graphcanvas.selectNodes(newSelected);
+ }
+
+ node.graph.afterChange();
+
+ node.setDirtyCanvas(true, true);
+ }
+ /**
+ * clears all the data inside
+ *
+ * @method clear
+ */
+ clear() {
+ this.frame = 0;
+ this.last_draw_time = 0;
+ this.render_time = 0;
+ this.fps = 0;
+
+ //this.scale = 1;
+ //this.offset = [0,0];
+ this.dragging_rectangle = null;
+
+ this.selected_nodes = {};
+ this.selected_group = null;
+
+ this.visible_nodes = [];
+ this.node_dragged = null;
+ this.node_over = null;
+ this.node_capturing_input = null;
+ this.connecting_links = null;
+ this.highlighted_links = {};
+
+ this.dragging_canvas = false;
+
+ this.dirty_canvas = true;
+ this.dirty_bgcanvas = true;
+ this.dirty_area = null;
+
+ this.node_in_panel = null;
+ this.node_widget = null;
+
+ this.last_mouse = [0, 0];
+ this.last_mouseclick = 0;
+ this.pointer_is_down = false;
+ this.pointer_is_double = false;
+ this.visible_area.set([0, 0, 0, 0]);
+
+ if (this.onClear) {
+ this.onClear();
+ }
+ }
+ /**
+ * assigns a graph, you can reassign graphs to the same canvas
+ *
+ * @method setGraph
+ * @param {LGraph} graph
+ */
+ setGraph(graph, skip_clear) {
+ if (this.graph == graph) {
+ return;
+ }
+
+ if (!skip_clear) {
+ this.clear();
+ }
+
+ if (!graph && this.graph) {
+ this.graph.detachCanvas(this);
+ return;
+ }
+
+ graph.attachCanvas(this);
+
+ //remove the graph stack in case a subgraph was open
+ if (this._graph_stack)
+ this._graph_stack = null;
+
+ this.setDirty(true, true);
+ }
+ /**
+ * returns the top level graph (in case there are subgraphs open on the canvas)
+ *
+ * @method getTopGraph
+ * @return {LGraph} graph
+ */
+ getTopGraph() {
+ if (this._graph_stack.length)
+ return this._graph_stack[0];
+ return this.graph;
+ }
+ /**
+ * opens a graph contained inside a node in the current graph
+ *
+ * @method openSubgraph
+ * @param {LGraph} graph
+ */
+ openSubgraph(graph) {
+ if (!graph) {
+ throw "graph cannot be null";
+ }
+
+ if (this.graph == graph) {
+ throw "graph cannot be the same";
+ }
+
+ this.clear();
+
+ if (this.graph) {
+ if (!this._graph_stack) {
+ this._graph_stack = [];
+ }
+ this._graph_stack.push(this.graph);
+ }
+
+ graph.attachCanvas(this);
+ this.checkPanels();
+ this.setDirty(true, true);
+ }
+ /**
+ * closes a subgraph contained inside a node
+ *
+ * @method closeSubgraph
+ * @param {LGraph} assigns a graph
+ */
+ closeSubgraph() {
+ if (!this._graph_stack || this._graph_stack.length == 0) {
+ return;
+ }
+ var subgraph_node = this.graph._subgraph_node;
+ var graph = this._graph_stack.pop();
+ this.selected_nodes = {};
+ this.highlighted_links = {};
+ graph.attachCanvas(this);
+ this.setDirty(true, true);
+ if (subgraph_node) {
+ this.centerOnNode(subgraph_node);
+ this.selectNodes([subgraph_node]);
+ }
+ // when close sub graph back to offset [0, 0] scale 1
+ this.ds.offset = [0, 0];
+ this.ds.scale = 1;
+ }
+ /**
+ * returns the visually active graph (in case there are more in the stack)
+ * @method getCurrentGraph
+ * @return {LGraph} the active graph
+ */
+ getCurrentGraph() {
+ return this.graph;
+ }
+ /**
+ * assigns a canvas
+ *
+ * @method setCanvas
+ * @param {Canvas} assigns a canvas (also accepts the ID of the element (not a selector)
+ */
+ setCanvas(canvas, skip_events) {
+ var that = this;
+
+ if (canvas) {
+ if (canvas.constructor === String) {
+ canvas = document.getElementById(canvas);
+ if (!canvas) {
+ throw "Error creating LiteGraph canvas: Canvas not found";
+ }
+ }
+ }
+
+ if (canvas === this.canvas) {
+ return;
+ }
+
+ if (!canvas && this.canvas) {
+ //maybe detach events from old_canvas
+ if (!skip_events) {
+ this.unbindEvents();
+ }
+ }
+
+ this.canvas = canvas;
+ this.ds.element = canvas;
+
+ if (!canvas) {
+ return;
+ }
+
+ //this.canvas.tabindex = "1000";
+ canvas.className += " lgraphcanvas";
+ canvas.data = this;
+ canvas.tabindex = "1"; //to allow key events
+
+
+
+ //bg canvas: used for non changing stuff
+ this.bgcanvas = null;
+ if (!this.bgcanvas) {
+ this.bgcanvas = document.createElement("canvas");
+ this.bgcanvas.width = this.canvas.width;
+ this.bgcanvas.height = this.canvas.height;
+ }
+
+ if (canvas.getContext == null) {
+ if (canvas.localName != "canvas") {
+ throw "Element supplied for LGraphCanvas must be a