LGraphCanvas.js

import DragAndScale from "./DragAndScale";
import { getFileExtension } from "./utils/file";
import ContextMenu from "./ContextMenu";
import { isValidConnection } from "./utils/function";
import LGraphNode from "./LGraphNode";
import {
    distance, isInsideRectangle, overlapBounding, clamp,
} from "./utils/math";
import LGraphGroup from "./LGraphGroup";
import * as registry from "./utils/registry";
import defaultConfig from "./utils/defaultConfig";
import getTime from "./utils/time";

const temp = new Float32Array(4);
const tempVC2 = new Float32Array(2);
const tempArea = new Float32Array(4);
const marginArea = new Float32Array(4);
const linkBounding = new Float32Array(4);
const tempA = new Float32Array(2);
const tempB = new Float32Array(2);

/**
 * 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 {HTMLCanvasElement} 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 }
 */
export default class LGraphCanvas {
    constructor(canvas, graph, 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.title_text_font = `${defaultConfig.NODE_TEXT_SIZE}px Arial`;
        this.inner_text_font = `normal ${defaultConfig.NODE_SUBTEXT_SIZE}px Arial`;
        this.node_title_color = defaultConfig.NODE_TITLE_COLOR;
        this.default_link_color = defaultConfig.LINK_COLOR;
        this.default_connection_color = {
            input_off: "#778",
            input_on: "#7F7",
            output_off: "#778",
            output_on: "#7F7",
        };

        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.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.allow_searchbox = true;
        this.allow_reconnect_links = false; // allows to change a connection with having to redo it
        // again

        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 if the mouse
        // does anything
        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 = defaultConfig.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 = [];

        // link canvas and graph
        if (graph) {
            graph.attachCanvas(this);
        }

        this.setCanvas(canvas);
        this.clear();

        if (!options.skip_render) {
            this.startRendering();
        }

        this.autoresize = options.autoresize;
    }

    static DEFAULT_BACKGROUND_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=";

    static link_type_colors = {
        "-1": defaultConfig.EVENT_LINK_COLOR,
        number: "#AAA",
        node: "#DCA",
    }

    static gradients = {}

    /**
     * clears all the data inside
     *
     * @method clear
     * @memberOf LGraphCanvas
     */
    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_node = 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.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
     * @param {boolean=} skipClear
     * @memberOf LGraphCanvas
     */
    setGraph(graph, skipClear) {
        if (this.graph === graph) {
            return;
        }

        if (!skipClear) 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
     * @memberOf LGraphCanvas
     */
    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
     * @memberOf LGraphCanvas
     */
    openSubgraph(graph) {
        if (!graph) {
            throw new Error("graph cannot be null");
        }

        if (this.graph === graph) {
            throw new Error("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
     * @memberOf LGraphCanvas
     */
    closeSubgraph() {
        if (!this._graph_stack || this._graph_stack.length === 0) {
            return;
        }
        const subgraphNode = this.graph._subgraph_node;
        const graph = this._graph_stack.pop();
        this.selected_nodes = {};
        this.highlighted_links = {};
        graph.attachCanvas(this);
        this.setDirty(true, true);
        if (subgraphNode) {
            this.centerOnNode(subgraphNode);
            this.selectNodes([subgraphNode]);
        }
    }

    /**
     * returns the visualy active graph (in case there are more in the stack)
     * @method getCurrentGraph
     * @return {LGraph} the active graph
     * @memberOf LGraphCanvas
     */
    getCurrentGraph() {
        return this.graph;
    }

    /**
     * assigns a canvas
     *
     * @method setCanvas
     * @param {HTMLCanvasElement | string | HTMLElement} canvas assigns a canvas
     *  (also accepts the ID of the element (not a selector))
     * @param {boolean} skipEvents
     * @memberOf LGraphCanvas
     */
    setCanvas(canvas, skipEvents) {
        if (canvas?.constructor === String) {
            canvas = document.getElementById(canvas);
            if (!canvas) {
                throw new Error("Error creating LiteGraph canvas: Canvas not found");
            }
        }

        if (canvas === this.canvas) {
            return;
        }

        if (!canvas && this.canvas) {
            // maybe detach events from old_canvas
            if (!skipEvents) {
                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;
        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 new Error(`Element supplied for LGraphCanvas must be a <canvas> element, you passed a ${
                    canvas.localName}`);
            }
            throw new Error("This browser doesn't support Canvas");
        }

        this.ctx = canvas.getContext("2d");
        if (this.ctx == null) {
            if (!canvas.webgl_enabled) {
                console.warn(
                    "This canvas seems to be WebGL, enabling WebGL renderer",
                );
            }
            this.enableWebGL();
        }

        // input:  (move and up could be unbinded)
        this._mousemove_callback = this.processMouseMove.bind(this);
        this._mouseup_callback = this.processMouseUp.bind(this);

        if (!skipEvents) this.bindEvents();
    }

    _doNothing(e) {
        e.preventDefault();
        return false;
    }

    _doReturnTrue(e) {
        e.preventDefault();
        return true;
    }

    /**
     * binds mouse, keyboard, touch and drag events to the canvas
     * @method bindEvents
     * @memberOf LGraphCanvas
     * */
    bindEvents() {
        if (this._events_binded) {
            console.warn("LGraphCanvas: events already binded");
            return;
        }

        const { canvas } = this;

        const refWindow = this.getCanvasWindow();
        const { document } = refWindow; // hack used when moving canvas between windows

        this._mousedown_callback = this.processMouseDown.bind(this);
        this._mousewheel_callback = this.processMouseWheel.bind(this);

        canvas.addEventListener("mousedown", this._mousedown_callback, true); // down do not need
        // to store the binded
        canvas.addEventListener("mousemove", this._mousemove_callback);
        canvas.addEventListener("mousewheel", this._mousewheel_callback);

        canvas.addEventListener("contextmenu", this._doNothing);
        canvas.addEventListener("DOMMouseScroll", this._mousewheel_callback);

        canvas.addEventListener("touchstart", this.touchHandler, true);
        canvas.addEventListener("touchmove", this.touchHandler, true);
        canvas.addEventListener("touchend", this.touchHandler, true);
        canvas.addEventListener("touchcancel", this.touchHandler, true);

        // Keyboard ******************
        this._key_callback = this.processKey.bind(this);

        canvas.addEventListener("keydown", this._key_callback, true);
        document.addEventListener("keyup", this._key_callback, true); // in document, otherwise it
        // doesn't fire keyup

        // Dropping Stuff over nodes ************************************
        this._ondrop_callback = this.processDrop.bind(this);

        canvas.addEventListener("dragover", this._doNothing, false);
        canvas.addEventListener("dragend", this._doNothing, false);
        canvas.addEventListener("drop", this._ondrop_callback, false);
        canvas.addEventListener("dragenter", this._doReturnTrue, false);

        this._events_binded = true;
    }

    /**
     * unbinds mouse events from the canvas
     * @method unbindEvents
     * @memberOf LGraphCanvas
     * */
    unbindEvents() {
        if (!this._events_binded) {
            console.warn("LGraphCanvas: no events binded");
            return;
        }

        const refWindow = this.getCanvasWindow();
        const { document } = refWindow;

        this.canvas.removeEventListener("mousedown", this._mousedown_callback);
        this.canvas.removeEventListener(
            "mousewheel",
            this._mousewheel_callback,
        );
        this.canvas.removeEventListener(
            "DOMMouseScroll",
            this._mousewheel_callback,
        );
        this.canvas.removeEventListener("keydown", this._key_callback);
        document.removeEventListener("keyup", this._key_callback);
        this.canvas.removeEventListener("contextmenu", this._doNothing);
        this.canvas.removeEventListener("drop", this._ondrop_callback);
        this.canvas.removeEventListener("dragenter", this._doReturnTrue);

        this.canvas.removeEventListener("touchstart", this.touchHandler);
        this.canvas.removeEventListener("touchmove", this.touchHandler);
        this.canvas.removeEventListener("touchend", this.touchHandler);
        this.canvas.removeEventListener("touchcancel", this.touchHandler);

        this._mousedown_callback = null;
        this._mousewheel_callback = null;
        this._key_callback = null;
        this._ondrop_callback = null;

        this._events_binded = false;
    }

    /**
     * this function allows to render the canvas using WebGL instead of Canvas2D
     * this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for
     * webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL
     * @method enableWebGL
     * @memberOf LGraphCanvas
     * */
    enableWebGL() {
        if (!GL) throw new Error("litegl.js must be included to use a WebGL canvas");
        if (!enableWebGLCanvas) throw new Error("webglCanvas.js must be included to use this feature");

        this.ctx = enableWebGLCanvas(this.canvas);
        this.gl = this.ctx;
        this.ctx.webgl = true;
        this.bgcanvas = this.canvas;
        this.bgctx = this.gl;
        this.canvas.webgl_enabled = true;
    }

    /**
     * marks as dirty the canvas, this way it will be rendered again
     *
     * @class LGraphCanvas
     * @method setDirty
     * @param {boolean} [fgcanvas] if the foreground canvas is dirty (the one containing the nodes)
     * @param {boolean} [bgcanvas] if the background canvas is dirty (the one containing the wires)
     * @memberOf LGraphCanvas
     */
    setDirty(fgcanvas, bgcanvas) {
        if (fgcanvas) this.dirty_canvas = true;
        if (bgcanvas) this.dirty_bgcanvas = true;
    }

    /**
     * Used to attach the canvas in a popup
     *
     * @method getCanvasWindow
     * @return {Window} returns the window where the canvas is attached (the DOM root node)
     * @memberOf LGraphCanvas
     */
    getCanvasWindow() {
        if (!this.canvas) return window;
        const doc = this.canvas.ownerDocument;
        return doc.defaultView;
    }

    /**
     * starts rendering the content of the canvas when needed
     *
     * @method startRendering
     * @memberOf LGraphCanvas
     */
    startRendering() {
        if (this.is_rendering) return;

        this.is_rendering = true;
        this.renderFrame();
    }

    /**
     * render a frame
     *
     * @method renderFrame
     * @memberOf LGraphCanvas
     */
    renderFrame() {
        if (!this.pause_rendering) this.draw();

        const window = this.getCanvasWindow();
        if (this.is_rendering) window.requestAnimationFrame(() => this.renderFrame());
    }

    /**
     * stops rendering the content of the canvas (to save resources)
     *
     * @method stopRendering
     * @memberOf LGraphCanvas
     */
    stopRendering() {
        this.is_rendering = false;
    }

    /* LiteGraphCanvas input */

    /**
     * used to block future mouse events (because of im gui)
     *
     * @method blockClick
     * @memberOf LGraphCanvas
     */
    blockClick() {
        this.block_click = true;
        this.last_mouseclick = 0;
    }

    processMouseDown(e) {
        if (this.set_canvas_dirty_on_mouse_event) this.dirty_canvas = true;

        if (!this.graph) return;

        this.adjustMouseEvent(e);

        const refWindow = this.getCanvasWindow();
        LGraphCanvas.active_canvas = this;

        // move mouse move event to the window in case it drags outside of the canvas
        this.canvas.removeEventListener("mousemove", this._mousemove_callback);
        refWindow.document.addEventListener("mousemove", this._mousemove_callback, true); // catch for the entire window
        refWindow.document.addEventListener("mouseup", this._mouseup_callback, true);

        const node = this.graph.getNodeOnPos(e.canvasX, e.canvasY, this.visible_nodes, 5);
        let skipAction = false;
        const now = getTime();
        const isDoubleClick = now - this.last_mouseclick < 300;
        this.mouse[0] = e.localX;
        this.mouse[1] = e.localY;
        this.graph_mouse[0] = e.canvasX;
        this.graph_mouse[1] = e.canvasY;
        this.last_click_position = [this.mouse[0], this.mouse[1]];

        this.canvas.focus();

        ContextMenu.closeAllContextMenus(refWindow);

        if (this.onMouse) {
            if (this.onMouse(e)) return;
        }

        // left button mouse
        if (e.which === 1) {
            if (e.ctrlKey) {
                this.dragging_rectangle = new Float32Array(4);
                this.dragging_rectangle[0] = e.canvasX;
                this.dragging_rectangle[1] = e.canvasY;
                this.dragging_rectangle[2] = 1;
                this.dragging_rectangle[3] = 1;
                skipAction = true;
            }

            let clickingCanvasBg = false;

            // when clicked on top of a node
            // and it is not interactive
            if (node && this.allow_interaction && !skipAction && !this.read_only) {
                if (!this.live_mode && !node.flags.pinned) {
                    this.bringToFront(node);
                } // if it wasn't selected?

                // not dragging mouse to connect two slots
                if (!this.connecting_node && !node.flags.collapsed && !this.live_mode) {
                // Search for corner for resize
                    if (!skipAction
                    && node.resizable
                    && isInsideRectangle(
                        e.canvasX,
                        e.canvasY,
                        node.pos[0] + node.size[0] - 5,
                        node.pos[1] + node.size[1] - 5,
                        1010,
                    )
                    ) {
                        this.graph.beforeChange();
                        this.resizing_node = node;
                        this.canvas.style.cursor = "se-resize";
                        skipAction = true;
                    } else {
                    // search for outputs
                        if (node.outputs) {
                            for (let i = 0, l = node.outputs.length; i < l; i++) {
                                const output = node.outputs[i];
                                const linkPos = node.getConnectionPos(false, i);
                                if (isInsideRectangle(
                                    e.canvasX,
                                    e.canvasY,
                                    linkPos[0] - 15,
                                    linkPos[1] - 10,
                                    30,
                                    20,
                                )) {
                                    this.connecting_node = node;
                                    this.connecting_output = output;
                                    this.connecting_pos = node.getConnectionPos(false, i);
                                    this.connecting_slot = i;
                                    if (e.shiftKey) {
                                        node.disconnectOutput(i);
                                    }

                                    if (isDoubleClick) {
                                        if (node.onOutputDblClick) {
                                            node.onOutputDblClick(i, e);
                                        }
                                    } else if (node.onOutputClick) {
                                        node.onOutputClick(i, e);
                                    }

                                    skipAction = true;
                                    break;
                                }
                            }
                        }

                        // search for inputs
                        if (node.inputs) {
                            for (let i = 0, l = node.inputs.length; i < l; i++) {
                                const input = node.inputs[i];
                                const linkPos = node.getConnectionPos(true, i);
                                if (isInsideRectangle(
                                    e.canvasX,
                                    e.canvasY,
                                    linkPos[0] - 15,
                                    linkPos[1] - 10,
                                    30, 20,
                                )) {
                                    if (isDoubleClick) {
                                        if (node.onInputDblClick) {
                                            node.onInputDblClick(i, e);
                                        }
                                    } else if (node.onInputClick) {
                                        node.onInputClick(i, e);
                                    }

                                    if (input.link) {
                                        const linkInfo = this.graph.links[
                                            input.link
                                        ]; // before disconnecting
                                        node.disconnectInput(i);

                                        if (
                                            this.allow_reconnect_links
                                            || e.shiftKey
                                        ) {
                                            this.connecting_node = this.graph._nodes_by_id[
                                                linkInfo.origin_id
                                            ];
                                            this.connecting_slot = linkInfo.origin_slot;
                                            this.connecting_output = this.connecting_node.outputs[
                                                this.connecting_slot
                                            ];

                                            this.connecting_pos = this.connecting_node
                                                .getConnectionPos(false, this.connecting_slot);
                                        }

                                        this.dirty_bgcanvas = true;
                                        skipAction = true;
                                    }
                                }
                            }
                        }
                    } // not resizing
                }

                // it wasn't clicked on the links boxes
                if (!skipAction) {
                    let blockDragNote = false;
                    const pos = [e.canvasX - node.pos[0], e.canvasY - node.pos[1]];

                    // widgets
                    const widget = this.processNodeWidgets(node, this.graph_mouse, e);
                    if (widget) {
                        blockDragNote = true;
                        this.node_widget = [node, widget];
                    }

                    // double clicking
                    if (isDoubleClick && this.selected_nodes[node.id]) {
                        // double click node
                        if (node.onDblClick) {
                            node.onDblClick(e, pos, this);
                        }
                        this.processNodeDblClicked(node);
                        blockDragNote = true;
                    }

                    // if do not capture mouse
                    if (node.onMouseDown && node.onMouseDown(e, pos, this)) {
                        blockDragNote = true;
                    } else {
                        // open subgraph button
                        if (node.subgraph && !node.skip_subgraph_button) {
                            if (!node.flags.collapsed && pos[0]
                                > node.size[0] - defaultConfig.NODE_TITLE_HEIGHT
                                && pos[1] < 0) {
                                setTimeout(() => {
                                    this.openSubgraph(node.subgraph);
                                }, 10);
                            }
                        }

                        if (this.live_mode) {
                            clickingCanvasBg = true;
                            blockDragNote = true;
                        }
                    }

                    if (!blockDragNote) {
                        if (this.allow_dragnodes) {
                            this.graph.beforeChange();
                            this.node_dragged = node;
                        }
                        if (!this.selected_nodes[node.id]) {
                            this.processNodeSelected(node, e);
                        }
                    }

                    this.dirty_canvas = true;
                }
            } else {
                // search for link connector
                if (!this.read_only) {
                    for (const link of this.visible_links) {
                        const center = link._pos;
                        if (
                            !center
                            || e.canvasX < center[0] - 4
                            || e.canvasX > center[0] + 4
                            || e.canvasY < center[1] - 4
                            || e.canvasY > center[1] + 4
                        ) {
                            continue;
                        }
                        // link clicked
                        this.showLinkMenu(link, e);
                        this.over_link_center = null; // clear tooltip
                        break;
                    }
                }

                this.selected_group = this.graph.getGroupOnPos(e.canvasX, e.canvasY);
                this.selected_group_resizing = false;
                if (this.selected_group && !this.read_only) {
                    if (e.ctrlKey) this.dragging_rectangle = null;

                    const dist = distance([e.canvasX, e.canvasY],
                        [this.selected_group.pos[0] + this.selected_group.size[0],
                            this.selected_group.pos[1] + this.selected_group.size[1]]);
                    if (dist * this.ds.scale < 10) {
                        this.selected_group_resizing = true;
                    } else {
                        this.selected_group.recomputeInsideNodes();
                    }
                }

                if (isDoubleClick && !this.read_only && this.allow_searchbox) {
                    this.showSearchBox(e);
                }

                clickingCanvasBg = true;
            }

            if (!skipAction && clickingCanvasBg && this.allow_dragcanvas) {
                this.dragging_canvas = true;
            }
        } else if (e.which === 2) {
            // middle button
        } else if (e.which === 3) {
            // right button
            if (!this.read_only) this.processContextMenu(node, e);
        }

        // TODO
        // if(this.node_selected != prev_selected)
        //	this.onNodeSelectionChange(this.node_selected);

        this.last_mouse[0] = e.localX;
        this.last_mouse[1] = e.localY;
        this.last_mouseclick = getTime();
        this.last_mouse_dragging = true;

        /*
    if( (this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null)
    this.draw();
    */

        this.graph.change();

        // this is to ensure to defocus(blur) if a text input element is on focus
        if (
            !refWindow.document.activeElement
            || (refWindow.document.activeElement.nodeName.toLowerCase()
            !== "input"
            && refWindow.document.activeElement.nodeName.toLowerCase()
            !== "textarea")) {
            e.preventDefault();
        }
        e.stopPropagation();

        if (this.onMouseDown) {
            this.onMouseDown(e);
        }

        return false;
    }

    /**
     * Called when a mouse move event has to be processed
     * @method processMouseMove
     * @memberOf LGraphCanvas
     * */
    processMouseMove(e) {
        if (this.autoresize) this.resize();

        if (this.set_canvas_dirty_on_mouse_event) this.dirty_canvas = true;

        if (!this.graph) return;

        LGraphCanvas.active_canvas = this;
        this.adjustMouseEvent(e);
        const mouse = [e.localX, e.localY];
        this.mouse[0] = mouse[0];
        this.mouse[1] = mouse[1];
        const delta = [
            mouse[0] - this.last_mouse[0],
            mouse[1] - this.last_mouse[1],
        ];
        this.last_mouse = mouse;
        this.graph_mouse[0] = e.canvasX;
        this.graph_mouse[1] = e.canvasY;

        if (this.block_click) {
            e.preventDefault();
            return false;
        }

        e.dragging = this.last_mouse_dragging;

        if (this.node_widget) {
            this.processNodeWidgets(this.node_widget[0], this.graph_mouse, e, this.node_widget[1]);
            this.dirty_canvas = true;
        }

        if (this.dragging_rectangle) {
            this.dragging_rectangle[2] = e.canvasX - this.dragging_rectangle[0];
            this.dragging_rectangle[3] = e.canvasY - this.dragging_rectangle[1];
            this.dirty_canvas = true;
        } else if (this.selected_group && !this.read_only) {
            // moving/resizing a group
            if (this.selected_group_resizing) {
                this.selected_group.size = [
                    e.canvasX - this.selected_group.pos[0],
                    e.canvasY - this.selected_group.pos[1],
                ];
            } else {
                const deltax = delta[0] / this.ds.scale;
                const deltay = delta[1] / this.ds.scale;
                this.selected_group.move(deltax, deltay, e.ctrlKey);
                if (this.selected_group._nodes.length) this.dirty_canvas = true;
            }
            this.dirty_bgcanvas = true;
        } else if (this.dragging_canvas) {
            this.ds.offset[0] += delta[0] / this.ds.scale;
            this.ds.offset[1] += delta[1] / this.ds.scale;
            this.dirty_canvas = true;
            this.dirty_bgcanvas = true;
        } else if (this.allow_interaction && !this.read_only) {
            if (this.connecting_node) this.dirty_canvas = true;

            // get node over
            const node = this.graph.getNodeOnPos(e.canvasX, e.canvasY, this.visible_nodes);

            // remove mouseover flag
            for (const _node of this.graph._nodes) {
                if (_node.mouseOver && node !== _node) {
                    // mouse leave
                    _node.mouseOver = false;
                    if (this.node_over && this.node_over.onMouseLeave) {
                        this.node_over.onMouseLeave(e);
                    }
                    this.node_over = null;
                    this.dirty_canvas = true;
                }
            }

            // mouse over a node
            if (node) {
                if (node.redraw_on_mouse) this.dirty_canvas = true;

                // this.canvas.style.cursor = "move";
                if (!node.mouseOver) {
                    // mouse enter
                    node.mouseOver = true;
                    this.node_over = node;
                    this.dirty_canvas = true;

                    if (node.onMouseEnter) node.onMouseEnter(e);
                }

                // in case the node wants to do something
                if (node.onMouseMove) {
                    node.onMouseMove(e, [e.canvasX - node.pos[0], e.canvasY - node.pos[1]], this);
                }

                // if dragging a link
                if (this.connecting_node) {
                    const pos = this._highlight_input || [0, 0];

                    // on top of input
                    if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) {
                        // mouse on top of the corner box, don't know what to do
                    } else {
                        // check if I have a slot below de mouse
                        const slot = this.isOverNodeInput(node, e.canvasX, e.canvasY, pos);
                        if (slot !== -1 && node.inputs[slot]) {
                            const slotType = node.inputs[slot].type;
                            if (isValidConnection(this.connecting_output.type, slotType)) {
                                this._highlight_input = pos;
                            }
                        } else this._highlight_input = null;
                    }
                }

                // Search for corner
                if (this.canvas) {
                    if (isInsideRectangle(
                        e.canvasX,
                        e.canvasY,
                        node.pos[0] + node.size[0] - 5,
                        node.pos[1] + node.size[1] - 5,
                        5,
                        5,
                    )) {
                        this.canvas.style.cursor = "se-resize";
                    } else this.canvas.style.cursor = "crosshair";
                }
            } else { // not over a node
                // search for link connector
                let overLink = null;
                for (const link of this.visible_links) {
                    const center = link._pos;
                    if (!center
                        || e.canvasX < center[0] - 4
                        || e.canvasX > center[0] + 4
                        || e.canvasY < center[1] - 4
                        || e.canvasY > center[1] + 4) {
                        continue;
                    }
                    overLink = link;
                    break;
                }
                if (overLink !== this.over_link_center) {
                    this.over_link_center = overLink;
                    this.dirty_canvas = true;
                }

                if (this.canvas) this.canvas.style.cursor = "";
            } // end

            // send event to node if capturing input (used with widgets that allow drag outside of
            // the area of the node)
            if (this.node_capturing_input && this.node_capturing_input !== node && this.node_capturing_input.onMouseMove) {
                this.node_capturing_input.onMouseMove(e, [e.canvasX - this.node_capturing_input.pos[0], e.canvasY - this.node_capturing_input.pos[1]], this);
            }

            // node being dragged
            if (this.node_dragged && !this.live_mode) {
                for (const nKeys of Object.keys(this.selected_nodes)) {
                    const n = this.selected_nodes[nKeys];
                    n.pos[0] += delta[0] / this.ds.scale;
                    n.pos[1] += delta[1] / this.ds.scale;
                }

                this.dirty_canvas = true;
                this.dirty_bgcanvas = true;
            }

            if (this.resizing_node && !this.live_mode) {
                // convert mouse to node space
                const desiredSize = [
                    e.canvasX - this.resizing_node.pos[0],
                    e.canvasY - this.resizing_node.pos[1],
                ];
                const minSize = this.resizing_node.computeSize();
                desiredSize[0] = Math.max(minSize[0], desiredSize[0]);
                desiredSize[1] = Math.max(minSize[1], desiredSize[1]);
                this.resizing_node.setSize(desiredSize);

                this.canvas.style.cursor = "se-resize";
                this.dirty_canvas = true;
                this.dirty_bgcanvas = true;
            }
        }

        e.preventDefault();
        return false;
    }

    /**
     * Called when a mouse up event has to be processed
     * @method processMouseUp
     * @memberOf LGraphCanvas
     * */
    processMouseUp(e) {
        if (this.set_canvas_dirty_on_mouse_event) this.dirty_canvas = true;

        if (!this.graph) return;

        const window = this.getCanvasWindow();
        const { document } = window;
        LGraphCanvas.active_canvas = this;

        // restore the mousemove event back to the canvas
        document.removeEventListener("mousemove", this._mousemove_callback, true);
        this.canvas.addEventListener("mousemove", this._mousemove_callback, true);
        document.removeEventListener("mouseup", this._mouseup_callback, true);

        this.adjustMouseEvent(e);
        const now = getTime();
        e.click_time = now - this.last_mouseclick;
        this.last_mouse_dragging = false;
        this.last_click_position = null;

        if (this.block_click) this.block_click = false;
        // used to avoid sending twice a click in a immediate button

        if (e.which === 1) {
            if (this.node_widget) this.processNodeWidgets(this.node_widget[0], this.graph_mouse, e);

            // left button
            this.node_widget = null;

            if (this.selected_group) {
                const diffx = this.selected_group.pos[0]
                    - Math.round(this.selected_group.pos[0]);
                const diffy = this.selected_group.pos[1]
                    - Math.round(this.selected_group.pos[1]);

                this.selected_group.move(diffx, diffy, e.ctrlKey);

                this.selected_group.pos[0] = Math.round(this.selected_group.pos[0]);
                this.selected_group.pos[1] = Math.round(this.selected_group.pos[1]);

                if (this.selected_group._nodes.length) this.dirty_canvas = true;
                this.selected_group = null;
            }
            this.selected_group_resizing = false;

            if (this.dragging_rectangle) {
                if (this.graph) {
                    const nodes = this.graph._nodes;
                    const nodeBounding = new Float32Array(4);
                    this.deselectAllNodes();
                    // compute bounding and flip if left to right
                    const w = Math.abs(this.dragging_rectangle[2]);
                    const h = Math.abs(this.dragging_rectangle[3]);
                    const startx = this.dragging_rectangle[2] < 0
                        ? this.dragging_rectangle[0] - w
                        : this.dragging_rectangle[0];
                    const starty = this.dragging_rectangle[3] < 0
                        ? this.dragging_rectangle[1] - h
                        : this.dragging_rectangle[1];
                    this.dragging_rectangle[0] = startx;
                    this.dragging_rectangle[1] = starty;
                    this.dragging_rectangle[2] = w;
                    this.dragging_rectangle[3] = h;

                    // test against all nodes (not visible because the rectangle maybe start outside
                    const toSelect = [];

                    for (const node of nodes) {
                        node.getBounding(nodeBounding);
                        if (
                            !overlapBounding(
                                this.dragging_rectangle,
                                nodeBounding,
                            )
                        ) {
                            continue;
                        } // out of the visible area
                        toSelect.push(node);
                    }
                    if (toSelect.length) {
                        this.selectNodes(toSelect);
                    }
                }
                this.dragging_rectangle = null;
            } else if (this.connecting_node) {
                // dragging a connection
                this.dirty_canvas = true;
                this.dirty_bgcanvas = true;

                const node = this.graph.getNodeOnPos(
                    e.canvasX,
                    e.canvasY,
                    this.visible_nodes,
                );

                // node below mouse
                if (node) {
                    if (
                        this.connecting_output.type === defaultConfig.EVENT
                        && this.isOverNodeBox(node, e.canvasX, e.canvasY)
                    ) {
                        this.connecting_node.connect(this.connecting_slot, node, defaultConfig.EVENT);
                    } else {
                        // slot below mouse? connect
                        const slot = this.isOverNodeInput(node, e.canvasX, e.canvasY);
                        if (slot !== -1) {
                            this.connecting_node.connect(this.connecting_slot, node, slot);
                        } else {
                            // not on top of an input
                            const input = node.getInputInfo(0);
                            // auto connect
                            if (this.connecting_output.type === defaultConfig.EVENT) {
                                this.connecting_node.connect(
                                    this.connecting_slot, node,
                                    defaultConfig.EVENT,
                                );
                            } else if (
                                input
                                && !input.link
                                && isValidConnection(
                                    input.type && this.connecting_output.type,
                                )
                            ) {
                                this.connecting_node.connect(this.connecting_slot, node, 0);
                            }
                        }
                    }
                }

                this.connecting_output = null;
                this.connecting_pos = null;
                this.connecting_node = null;
                this.connecting_slot = -1;
            } else if (this.resizing_node) {
                this.dirty_canvas = true;
                this.dirty_bgcanvas = true;
                this.graph.afterChange(this.resizing_node);
                this.resizing_node = null;
            } else if (this.node_dragged) {
                // node being dragged?
                const node = this.node_dragged;
                if (
                    node
                    && e.click_time < 300
                    && isInsideRectangle(
                        e.canvasX,
                        e.canvasY,
                        node.pos[0],
                        node.pos[1] - defaultConfig.NODE_TITLE_HEIGHT,
                        defaultConfig.NODE_TITLE_HEIGHT,
                        defaultConfig.NODE_TITLE_HEIGHT,
                    )
                ) {
                    node.collapse();
                }

                this.dirty_canvas = true;
                this.dirty_bgcanvas = true;
                this.node_dragged.pos[0] = Math.round(this.node_dragged.pos[0]);
                this.node_dragged.pos[1] = Math.round(this.node_dragged.pos[1]);
                if (this.graph.config.align_to_grid) {
                    this.node_dragged.alignToGrid();
                }
                if (this.onNodeMoved) this.onNodeMoved(this.node_dragged);
                this.graph.afterChange(this.node_dragged);
                this.node_dragged = null;
            } else {
                // get node over
                const node = this.graph.getNodeOnPos(
                    e.canvasX,
                    e.canvasY,
                    this.visible_nodes,
                );

                if (!node && e.click_time < 300) {
                    this.deselectAllNodes();
                }

                this.dirty_canvas = true;
                this.dragging_canvas = false;

                if (this.node_over && this.node_over.onMouseUp) {
                    this.node_over.onMouseUp(e, [e.canvasX - this.node_over.pos[0], e.canvasY - this.node_over.pos[1]], this);
                }
                if (
                    this.node_capturing_input
                    && this.node_capturing_input.onMouseUp
                ) {
                    this.node_capturing_input.onMouseUp(e, [
                        e.canvasX - this.node_capturing_input.pos[0],
                        e.canvasY - this.node_capturing_input.pos[1],
                    ]);
                }
            }
        } else if (e.which === 2) {
            // middle button
            // trace("middle");
            this.dirty_canvas = true;
            this.dragging_canvas = false;
        } else if (e.which === 3) {
            // right button
            // trace("right");
            this.dirty_canvas = true;
            this.dragging_canvas = false;
        }

        this.graph.change();

        e.stopPropagation();
        e.preventDefault();
        return false;
    }

    /**
     * Called when a mouse wheel event has to be processed
     * @method processMouseWheel
     * @memberOf LGraphCanvas
     * */
    processMouseWheel(e) {
        if (!this.graph || !this.allow_dragcanvas) {
            return;
        }

        const delta = e.wheelDeltaY ?? e.detail * -60;

        this.adjustMouseEvent(e);

        let { scale } = this.ds;

        if (delta > 0) {
            scale *= 1.1;
        } else if (delta < 0) {
            scale *= 1 / 1.1;
        }

        // this.setZoom( scale, [ e.localX, e.localY ] );
        this.ds.changeScale(scale, [e.localX, e.localY]);

        this.graph.change();

        e.preventDefault();
        return false; // prevent default
    }

    /**
     * returns true if a position (in graph space) is on top of a node little corner box
     * @method isOverNodeBox
     * @memberOf LGraphCanvas
     * */
    isOverNodeBox(node, canvasx, canvasy) {
        const titleHeight = defaultConfig.NODE_TITLE_HEIGHT;
        return !!isInsideRectangle(
            canvasx,
            canvasy,
            node.pos[0] + 2,
            node.pos[1] + 2 - titleHeight,
            titleHeight - 4,
            titleHeight - 4,
        );
    }

    /**
     * returns true if a position (in graph space) is on top of a node input slot
     * @method isOverNodeInput
     * @memberOf LGraphCanvas
     * */
    isOverNodeInput(
        node,
        canvasx,
        canvasy,
        slotPos,
    ) {
        if (node.inputs) {
            for (let i = 0, l = node.inputs.length; i < l; ++i) {
                const linkPos = node.getConnectionPos(true, i);
                let isInside = false;
                if (node.horizontal) {
                    isInside = isInsideRectangle(
                        canvasx,
                        canvasy,
                        linkPos[0] - 5,
                        linkPos[1] - 10,
                        10,
                        20,
                    );
                } else {
                    isInside = isInsideRectangle(
                        canvasx,
                        canvasy,
                        linkPos[0] - 10,
                        linkPos[1] - 5,
                        40,
                        10,
                    );
                }
                if (isInside) {
                    if (slotPos) {
                        slotPos[0] = linkPos[0];
                        slotPos[1] = linkPos[1];
                    }
                    return i;
                }
            }
        }
        return -1;
    }

    /**
     * process a key event
     * @method processKey
     * @memberOf LGraphCanvas
     * */
    processKey(e) {
        if (!this.graph) return;

        let blockDefault = false;

        if (e.target.localName === "input") {
            return;
        }

        if (e.type === "keydown") {
            if (e.keyCode === 32) {
                // esc
                this.dragging_canvas = true;
                blockDefault = true;
            }

            // select all Control A
            if (e.keyCode === 65 && e.ctrlKey) {
                this.selectNodes();
                blockDefault = true;
            }

            if (e.code === "KeyC" && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
                // copy
                if (this.selected_nodes) {
                    this.copyToClipboard();
                    blockDefault = true;
                }
            }

            if (e.code === "KeyV" && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
                // paste
                this.pasteFromClipboard();
            }

            // delete or backspace
            if ((e.keyCode === 46 || e.keyCode === 8)
                && (e.target.localName !== "input" && e.target.localName !== "textarea")) {
                this.deleteSelectedNodes();
                blockDefault = true;
            }

            // collapse
            // ...

            // TODO
            if (this.selected_nodes) {
                for (var i in this.selected_nodes) {
                    if (this.selected_nodes[i].onKeyDown) {
                        this.selected_nodes[i].onKeyDown(e);
                    }
                }
            }
        } else if (e.type == "keyup") {
            if (e.keyCode == 32) {
                this.dragging_canvas = false;
            }

            if (this.selected_nodes) {
                for (var i in this.selected_nodes) {
                    if (this.selected_nodes[i].onKeyUp) {
                        this.selected_nodes[i].onKeyUp(e);
                    }
                }
            }
        }

        this.graph.change();

        if (blockDefault) {
            e.preventDefault();
            e.stopImmediatePropagation();
            return false;
        }
    }

    pasteFromClipboard() {
        const data = localStorage.getItem("litegrapheditor_clipboard");
        if (!data) return;

        this.graph.beforeChange();

        // create nodes
        const clipboardInfo = JSON.parse(data);
        const nodes = [];
        for (const node_data of clipboardInfo.nodes) {
            const node = LGraphNode.createNode(node_data.type);
            if (node) {
                node.configure(node_data);
                node.pos[0] += 5;
                node.pos[1] += 5;
                this.graph.add(node);
                nodes.push(node);
            }
        }

        for (const link_info of clipboardInfo.links) {
            const origin_node = nodes[link_info[0]];
            const target_node = nodes[link_info[2]];
            if (origin_node && target_node) origin_node.connect(link_info[1], target_node, link_info[3]);
            else console.warn("Warning, nodes missing on pasting");
        }

        this.selectNodes(nodes);

        this.graph.afterChange();
    }

    copyToClipboard() {
        const clipboardInfo = {
            nodes: [],
            links: [],
        };
        let index = 0;
        const selectedNodesArray = [];

        // eslint-disable-next-line guard-for-in, no-restricted-syntax
        for (const i in this.selected_nodes) {
            const node = this.selected_nodes[i];
            node.relative_id = index;
            selectedNodesArray.push(node);
            index += 1;
        }

        for (const node of selectedNodesArray) {
            const cloned = node.clone();
            if (!cloned) {
                console.warn(`node type not found: ${node.type}`);
                continue;
            }
            clipboardInfo.nodes.push(cloned.serialize());
            if (node.inputs && node.inputs.length) {
                for (let j = 0; j < node.inputs.length; ++j) {
                    const input = node.inputs[j];
                    if (!input || input.link == null) {
                        continue;
                    }
                    const link_info = this.graph.links[input.link];
                    if (!link_info) {
                        continue;
                    }
                    const target_node = this.graph.getNodeById(
                        link_info.origin_id,
                    );
                    if (!target_node || !this.selected_nodes[target_node.id]) {
                        // improve this by allowing connections to non-selected nodes
                        continue;
                    } // not selected
                    clipboardInfo.links.push([
                        target_node._relative_id,
                        link_info.origin_slot, // j,
                        node._relative_id,
                        link_info.target_slot,
                    ]);
                }
            }
        }

        localStorage.setItem("litegrapheditor_clipboard", JSON.stringify(clipboardInfo));
    }

    /**
     * process a item drop event on top the canvas
     * @method processDrop
     * @memberOf LGraphCanvas
     * */
    processDrop(e) {
        e.preventDefault();
        this.adjustMouseEvent(e);

        const pos = [e.canvasX, e.canvasY];
        const node = this.graph ? this.graph.getNodeOnPos(pos[0], pos[1]) : null;

        if (!node) {
            let r = null;
            if (this.onDropItem) r = this.onDropItem(e);
            if (!r) {
                this.checkDropItem(e);
            }
            return;
        }

        if (node.onDropFile || node.onDropData) {
            const { files } = e.dataTransfer;
            if (files && files.length) {
                for (const file of files) {
                    const filename = file.name;
                    // console.log(file);

                    if (node.onDropFile) {
                        node.onDropFile(file);
                    }

                    if (node.onDropData) {
                        // prepare reader
                        const reader = new FileReader();
                        reader.onload = (event) => {
                            // console.log(event.target);
                            const data = event.target.result;
                            node.onDropData(data, filename, file);
                        };

                        // read data
                        const type = file.type.split("/")[0];
                        if (type === "text" || type === "") {
                            reader.readAsText(file);
                        } else if (type === "image") {
                            reader.readAsDataURL(file);
                        } else {
                            reader.readAsArrayBuffer(file);
                        }
                    }
                }
            }
        }

        if (node.onDropItem) {
            if (node.onDropItem(e)) {
                return true;
            }
        }

        if (this.onDropItem) {
            return this.onDropItem(e);
        }

        return false;
    }

    checkDropItem(e) {
        if (e.dataTransfer.files.length) {
            const file = e.dataTransfer.files[0];
            const ext = getFileExtension(file.name).toLowerCase();
            const nodetype = defaultConfig.node_types_by_file_extension[ext];
            if (nodetype) {
                this.graph.beforeChange();
                const node = LGraphNode.createNode(nodetype.type);
                node.pos = [e.canvasX, e.canvasY];
                this.graph.add(node);
                if (node.onDropFile) {
                    node.onDropFile(file);
                }
                this.graph.afterChange();
            }
        }
    }

    processNodeDblClicked(n) {
        if (this.onShowNodePanel) this.onShowNodePanel(n);
        else this.showShowNodePanel(n);

        if (this.onNodeDblClicked) this.onNodeDblClicked(n);

        this.setDirty(true);
    }

    processNodeSelected(node, e) {
        this.selectNode(node, e && e.shiftKey);
        if (this.onNodeSelected) {
            this.onNodeSelected(node);
        }
    }

    /**
     * selects a given node (or adds it to the current selection)
     * @method selectNode
     * @param {LGraphNode} node
     * @param {boolean} addToCurrentSelection
     * @memberOf LGraphCanvas
     * */
    selectNode(node, addToCurrentSelection) {
        if (node == null) {
            this.deselectAllNodes();
        } else {
            this.selectNodes([node], addToCurrentSelection);
        }
    }

    /**
     * selects several nodes (or adds them to the current selection)
     * @method selectNodes
     * @memberOf LGraphCanvas
     * */
    selectNodes(nodes = this.graph._nodes, addToCurrentSelection) {
        if (!addToCurrentSelection) this.deselectAllNodes();

        for (const node of nodes) {
            if (node.is_selected) continue;

            if (!node.is_selected && node.onSelected) node.onSelected();
            node.is_selected = true;
            this.selected_nodes[node.id] = node;

            if (node.inputs) {
                for (const input of node.inputs) this.highlighted_links[input.link] = true;
            }

            if (node.outputs) {
                for (const out of node.outputs) {
                    if (out.links) {
                        for (const link of out.links) this.highlighted_links[link] = true;
                    }
                }
            }
        }

        if (this.onSelectionChange) this.onSelectionChange(this.selected_nodes);

        this.setDirty(true);
    }

    /**
     * removes a node from the current selection
     * @method deselectNode
     * @memberOf LGraphCanvas
     * */
    deselectNode(node) {
        if (!node.is_selected) return;
        if (node.onDeselected) {
            node.onDeselected();
        }
        node.is_selected = false;

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

        // remove highlighted
        if (node.inputs) {
            for (const input of node.inputs) delete this.highlighted_links[input.link];
        }
        if (node.outputs) {
            for (const out of node.outputs) {
                if (out.links) {
                    for (const link of out.links) delete this.highlighted_links[link];
                }
            }
        }
    }

    /**
     * removes all nodes from the current selection
     * @method deselectAllNodes
     * @memberOf LGraphCanvas
     * */
    deselectAllNodes() {
        if (!this.graph) return;
        for (const node of this.graph._nodes) {
            if (!node.is_selected) {
                continue;
            }
            if (node.onDeselected) {
                node.onDeselected();
            }
            node.is_selected = false;
            if (this.onNodeDeselected) {
                this.onNodeDeselected(node);
            }
        }
        this.selected_nodes = {};
        this.current_node = null;
        this.highlighted_links = {};
        if (this.onSelectionChange) this.onSelectionChange(this.selected_nodes);
        this.setDirty(true);
    }

    /**
     * deletes all nodes in the current selection from the graph
     * @method deleteSelectedNodes
     * @memberOf LGraphCanvas
     * */
    deleteSelectedNodes() {
        this.graph.beforeChange();

        // eslint-disable-next-line guard-for-in, no-restricted-syntax
        for (const i in this.selected_nodes) {
            const node = this.selected_nodes[i];

            if (node.block_delete) continue;

            // autoconnect when possible (very basic, only takes into account first input-output)
            if (node.inputs
                && node.inputs.length
                && node.outputs
                && node.outputs.length
                && isValidConnection(node.inputs[0].type, node.outputs[0].type)
                && node.inputs[0].link
                && node.outputs[0].links
                && node.outputs[0].links.length) {
                const inputLink = node.graph.links[node.inputs[0].link];
                const outputLink = node.graph.links[node.outputs[0].links[0]];
                const inputNode = node.getInputNode(0);
                const outputNode = node.getOutputNodes(0)[0];
                if (inputNode && outputNode) {
                    inputNode.connect(inputLink.origin_slot, outputNode, outputLink.target_slot);
                }
            }
            this.graph.remove(node);
            if (this.onNodeDeselected) this.onNodeDeselected(node);
        }

        this.selected_nodes = {};
        this.current_node = null;
        this.highlighted_links = {};
        this.setDirty(true);
        this.graph.afterChange();
    }

    /**
     * centers the camera on a given node
     * @method centerOnNode
     * @memberOf LGraphCanvas
     * */
    centerOnNode(node) {
        this.ds.offset[0] = -node.pos[0]
            - node.size[0] * 0.5
            + (this.canvas.width * 0.5) / this.ds.scale;
        this.ds.offset[1] = -node.pos[1]
            - node.size[1] * 0.5
            + (this.canvas.height * 0.5) / this.ds.scale;
        this.setDirty(true, true);
    }

    /**
     * adds some useful properties to a mouse event, like the position in graph coordinates
     * @method adjustMouseEvent
     * @memberOf LGraphCanvas
     * */
    adjustMouseEvent(e) {
        if (this.canvas) {
            const b = this.canvas.getBoundingClientRect();
            e.localX = e.clientX - b.left;
            e.localY = e.clientY - b.top;
        } else {
            e.localX = e.clientX;
            e.localY = e.clientY;
        }

        e.deltaX = e.localX - this.last_mouse_position[0];
        e.deltaY = e.localY - this.last_mouse_position[1];

        this.last_mouse_position[0] = e.localX;
        this.last_mouse_position[1] = e.localY;

        e.canvasX = e.localX / this.ds.scale - this.ds.offset[0];
        e.canvasY = e.localY / this.ds.scale - this.ds.offset[1];
    }

    /**
     * changes the zoom level of the graph (default is 1), you can pass also a place used to pivot
     * the zoom
     * @method setZoom
     * @memberOf LGraphCanvas
     * */
    setZoom(value, zoomingCenter) {
        this.ds.changeScale(value, zoomingCenter);
        this.dirty_canvas = true;
        this.dirty_bgcanvas = true;
    }

    /**
     * converts a coordinate from graph coordinates to canvas2D coordinates
     * @method convertOffsetToCanvas
     * @memberOf LGraphCanvas
     * */
    convertOffsetToCanvas(pos) {
        return this.ds.convertOffsetToCanvas(pos);
    }

    /**
     * converts a coordinate from Canvas2D coordinates to graph space
     * @method convertCanvasToOffset
     * @memberOf LGraphCanvas
     * */
    convertCanvasToOffset(pos, out) {
        return this.ds.convertCanvasToOffset(pos, out);
    }

    /**
     * converts event coordinates from canvas2D to graph coordinates
     * @method convertEventToCanvasOffset
     * @param e
     * @returns {Array}
     * @memberOf LGraphCanvas
     */
    convertEventToCanvasOffset(e) {
        const rect = this.canvas.getBoundingClientRect();
        return this.convertCanvasToOffset([e.clientX - rect.left, e.clientY - rect.top]);
    }

    /**
     * brings a node to front (above all other nodes)
     * @method bringToFront
     * @param {LGraphNode} node
     * @memberOf LGraphCanvas
     * */
    bringToFront(node) {
        const i = this.graph._nodes.indexOf(node);
        if (i === -1) {
            return;
        }

        this.graph._nodes.splice(i, 1);
        this.graph._nodes.push(node);
    }

    /**
     * sends a node to the back (below all other nodes)
     * @method sendToBack
     * @param {LGraphNode} node
     * @memberOf LGraphCanvas
     * */
    sendToBack(node) {
        const i = this.graph._nodes.indexOf(node);
        if (i === -1) {
            return;
        }

        this.graph._nodes.splice(i, 1);
        this.graph._nodes.unshift(node);
    }

    /**
     * checks which nodes are visible (inside the camera area)
     * @method computeVisibleNodes
     * @param {LGraphNode[]} [nodes]
     * @param {LGraphNode[]} [out]
     * @return {LGraphNode[]}
     * @memberOf LGraphCanvas
     * */
    computeVisibleNodes(nodes, out = []) {
        const visibleNodes = out;
        nodes = this.graph._nodes;
        visibleNodes.length = 0;
        for (const n of nodes) {
            // skip rendering nodes in live mode
            if (this.live_mode && !n.onDrawBackground && !n.onDrawForeground) {
                continue;
            }

            if (!overlapBounding(this.visible_area, n.getBounding(temp))) {
                continue;
            } // out of the visible area

            visibleNodes.push(n);
        }
        return visibleNodes;
    }

    /**
     * renders the whole canvas content, by rendering in two separated canvas, one containing the
     * background grid and the connections, and one containing the nodes)
     * @method draw
     * @param {boolean} [force_canvas]
     * @param {boolean} [force_bgcanvas]
     * @memberOf LGraphCanvas
     * */
    draw(force_canvas, force_bgcanvas) {
        if (!this.canvas || this.canvas.width === 0 || this.canvas.height === 0) return;

        // fps counting
        const now = getTime();
        this.render_time = (now - this.last_draw_time) * 0.001;
        this.last_draw_time = now;

        if (this.graph) this.ds.computeVisibleArea();

        if (
            this.dirty_bgcanvas
            || force_bgcanvas
            || this.always_render_background
            || (this.graph
            && this.graph._last_trigger_time
            && now - this.graph._last_trigger_time < 1000)
        ) this.drawBackCanvas();

        if (this.dirty_canvas || force_canvas) this.drawFrontCanvas();

        this.fps = this.render_time ? 1.0 / this.render_time : 0;
        this.frame += 1;
    }

    /**
     * draws the front canvas (the one containing all the nodes)
     * @method drawFrontCanvas
     * @memberOf LGraphCanvas
     * */
    drawFrontCanvas() {
        this.dirty_canvas = false;

        if (!this.ctx) this.ctx = this.bgcanvas.getContext("2d");
        const { ctx } = this;
        if (!ctx) return;

        if (ctx.start2D) {
            ctx.start2D();
        }

        const { canvas } = this;

        // reset in case of error
        ctx.restore();
        ctx.setTransform(1, 0, 0, 1, 0, 0);

        // clip dirty area if there is one, otherwise work in full canvas
        if (this.dirty_area) {
            ctx.save();
            ctx.beginPath();
            ctx.rect(
                this.dirty_area[0],
                this.dirty_area[1],
                this.dirty_area[2],
                this.dirty_area[3],
            );
            ctx.clip();
        }

        if (this.clear_background) ctx.clearRect(0, 0, canvas.width, canvas.height);

        // draw bg canvas
        if (this.bgcanvas === this.canvas) {
            this.drawBackCanvas();
        } else {
            ctx.drawImage(this.bgcanvas, 0, 0);
        }

        // rendering
        if (this.onRender) this.onRender(canvas, ctx);

        // info widget
        if (this.show_info) this.renderInfo(ctx);

        if (this.graph) {
            // apply transformations
            ctx.save();
            this.ds.toCanvasContext(ctx);

            // draw nodes
            let drawnNodes = 0;
            const visibleNodes = this.computeVisibleNodes(null, this.visible_nodes);

            for (const node of visibleNodes) {
                // transform coords system
                ctx.save();
                ctx.translate(node.pos[0], node.pos[1]);

                // Draw
                this.drawNode(node, ctx);
                drawnNodes += 1;

                // Restore
                ctx.restore();
            }

            // on top (debug)
            if (this.render_execution_order) this.drawExecutionOrder(ctx);

            // connections ontop?
            if (this.graph.config.links_ontop && !this.live_mode) this.drawConnections(ctx);

            // current connection (the one being dragged by the mouse)
            if (this.connecting_pos) {
                ctx.lineWidth = this.connections_width;
                let linkColor = null;
                switch (this.connecting_output.type) {
                    case defaultConfig.EVENT:
                        linkColor = defaultConfig.EVENT_LINK_COLOR;
                        break;
                    default:
                        linkColor = defaultConfig.CONNECTING_LINK_COLOR;
                }

                // the connection being dragged by the mouse
                this.renderLink(
                    ctx,
                    this.connecting_pos,
                    [this.graph_mouse[0], this.graph_mouse[1]],
                    null,
                    false,
                    null,
                    linkColor,
                    this.connecting_output.dir
                    || (this.connecting_node.horizontal ? defaultConfig.DOWN : defaultConfig.RIGHT),
                    defaultConfig.CENTER,
                );

                ctx.beginPath();
                if (
                    this.connecting_output.type === defaultConfig.EVENT
                    || this.connecting_output.shape === defaultConfig.BOX_SHAPE
                ) {
                    ctx.rect(
                        this.connecting_pos[0] - 6 + 0.5,
                        this.connecting_pos[1] - 5 + 0.5,
                        14,
                        10,
                    );
                } else {
                    ctx.arc(
                        this.connecting_pos[0],
                        this.connecting_pos[1],
                        4,
                        0,
                        Math.PI * 2,
                    );
                }
                ctx.fill();

                ctx.fillStyle = "#ffcc00";
                if (this._highlight_input) {
                    ctx.beginPath();
                    ctx.arc(
                        this._highlight_input[0],
                        this._highlight_input[1],
                        6,
                        0,
                        Math.PI * 2,
                    );
                    ctx.fill();
                }
            }

            // the selection rectangle
            if (this.dragging_rectangle) {
                ctx.strokeStyle = "#FFF";
                ctx.strokeRect(
                    this.dragging_rectangle[0],
                    this.dragging_rectangle[1],
                    this.dragging_rectangle[2],
                    this.dragging_rectangle[3],
                );
            }

            // on top of link center
            if (this.over_link_center && this.render_link_tooltip) {
                this.drawLinkTooltip(ctx, this.over_link_center);
            } else if (this.onDrawLinkTooltip) {
                this.onDrawLinkTooltip(ctx, null);
            }

            // custom info
            if (this.onDrawForeground) {
                this.onDrawForeground(ctx, this.visible_rect);
            }

            ctx.restore();
        }

        // draws panel in the corner
        if (this._graph_stack && this._graph_stack.length) this.drawSubgraphPanel(ctx);

        if (this.onDrawOverlay) this.onDrawOverlay(ctx);

        if (this.dirty_area) ctx.restore();

        if (ctx.finish2D) ctx.finish2D();
    }

    /**
     * draws the panel in the corner that shows subgraph properties
     * @method drawSubgraphPanel
     * @memberOf LGraphCanvas
     * */
    drawSubgraphPanel(ctx) {
        const subgraph = this.graph;
        const subnode = subgraph._subgraph_node;
        if (!subnode) {
            console.warn("subgraph without subnode");
            return;
        }

        const num = subnode.inputs ? subnode.inputs.length : 0;
        const w = 300;
        const h = Math.floor(defaultConfig.NODE_SLOT_HEIGHT * 1.6);

        ctx.fillStyle = "#111";
        ctx.globalAlpha = 0.8;
        ctx.beginPath();
        ctx.roundRect(10, 10, w, (num + 1) * h + 50, 8);
        ctx.fill();
        ctx.globalAlpha = 1;

        ctx.fillStyle = "#888";
        ctx.font = "14px Arial";
        ctx.textAlign = "left";
        ctx.fillText("Graph Inputs", 20, 34);

        if (this.drawButton(w - 20, 20, 20, 20, "X", "#151515")) {
            this.closeSubgraph();
            return;
        }

        let y = 50;
        ctx.font = "20px Arial";
        if (subnode.inputs) {
            for (const input of subnode.inputs) {
                if (input.not_subgraph_input) continue;

                // input button clicked
                if (this.drawButton(20, y + 2, w - 20, h - 2)) {
                    const type = subnode.constructor.input_node_type || "graph/input";
                    this.graph.beforeChange();
                    const newnode = createNode(type);
                    if (newnode) {
                        subgraph.add(newnode);
                        this.block_click = false;
                        this.last_click_position = null;
                        this.selectNodes([newnode]);
                        this.node_dragged = newnode;
                        this.dragging_canvas = false;
                        newnode.setProperty("name", input.name);
                        newnode.setProperty("type", input.type);
                        this.node_dragged.pos[0] = this.graph_mouse[0] - 5;
                        this.node_dragged.pos[1] = this.graph_mouse[1] - 5;
                        this.graph.afterChange();
                    } else {
                        console.error("graph input node not found:", type);
                    }
                }

                ctx.fillStyle = "#9C9";
                ctx.beginPath();
                ctx.arc(w - 16, y + h * 0.5, 5, 0, 2 * Math.PI);
                ctx.fill();

                ctx.fillStyle = "#AAA";
                ctx.fillText(input.name, 50, y + h * 0.75);
                const tw = ctx.measureText(input.name);
                ctx.fillStyle = "#777";
                ctx.fillText(input.type, 50 + tw.width + 10, y + h * 0.75);

                y += h;
            }
        }

        // add + button
        if (this.drawButton(20, y + 2, w - 20, h - 2, "+", "#151515", "#222")) {
            this.showSubgraphPropertiesDialog(subnode);
        }
    }

    /**
     * Draws a button into the canvas overlay and computes if it was clicked using the immediate
     * gui paradigm
     * @method drawButton
     * @param x
     * @param y
     * @param w
     * @param h
     * @param text
     * @param [bgcolor]
     * @param [hovercolor]
     * @param [textcolor]
     * @returns {*|boolean}
     * @memberOf LGraphCanvas
     */
    drawButton(x, y, w, h, text, bgcolor = defaultConfig.NODE_DEFAULT_COLOR, hovercolor = "#555", textcolor = defaultConfig.NODE_TEXT_COLOR) {
        const { ctx } = this;

        let pos = this.mouse;
        const hover = isInsideRectangle(pos[0], pos[1], x, y, w, h);
        pos = this.last_click_position;
        const clicked = pos && isInsideRectangle(pos[0], pos[1], x, y, w, h);

        ctx.fillStyle = hover ? hovercolor : bgcolor;
        if (clicked) ctx.fillStyle = "#AAA";
        ctx.beginPath();
        ctx.roundRect(x, y, w, h, 4);
        ctx.fill();

        if (text) {
            if (text.constructor === String) {
                ctx.fillStyle = textcolor;
                ctx.textAlign = "center";
                // eslint-disable-next-line
                ctx.font = `${(h * 0.65) | 0}px Arial`;
                ctx.fillText(text, x + w * 0.5, y + h * 0.75);
                ctx.textAlign = "left";
            }
        }

        if (clicked) this.blockClick();
        return clicked && !this.block_click;
    }

    isAreaClicked(x, y, w, h, holdClick) {
        const pos = this.last_click_position;
        const clicked = pos && isInsideRectangle(pos[0], pos[1], x, y, w, h);
        if (clicked && holdClick) this.blockClick();
        return clicked && !this.block_click;
    }

    /**
     * draws some useful stats in the corner of the canvas
     * @method renderInfo
     * @memberOf LGraphCanvas
     * */
    renderInfo(ctx, x = 10, y = this.canvas.height - 80) {
        ctx.save();
        ctx.translate(x, y);

        ctx.font = "10px Arial";
        ctx.fillStyle = "#888";
        if (this.graph) {
            ctx.fillText(`T: ${this.graph.globaltime.toFixed(2)}s`, 5, 13);
            ctx.fillText(`I: ${this.graph.iteration}`, 5, 13 * 2);
            ctx.fillText(`N: ${this.graph._nodes.length} [${this.visible_nodes.length}]`, 5, 13 * 3);
            ctx.fillText(`V: ${this.graph._version}`, 5, 13 * 4);
            ctx.fillText(`FPS:${this.fps.toFixed(2)}`, 5, 13 * 5);
        } else {
            ctx.fillText("No graph selected", 5, 13);
        }
        ctx.restore();
    }

    /**
     * draws the back canvas (the one containing the background and the connections)
     * @method drawBackCanvas
     * @memberOf LGraphCanvas
     * */
    drawBackCanvas() {
        const canvas = this.bgcanvas;
        if (canvas.width !== this.canvas.width || canvas.height !== this.canvas.height) {
            canvas.width = this.canvas.width;
            canvas.height = this.canvas.height;
        }

        if (!this.bgctx) this.bgctx = this.bgcanvas.getContext("2d");
        const ctx = this.bgctx;
        if (ctx.start) ctx.start();

        // clear
        if (this.clear_background) ctx.clearRect(0, 0, canvas.width, canvas.height);

        if (this._graph_stack && this._graph_stack.length) {
            ctx.save();
            const subgraphNode = this.graph._subgraph_node;
            ctx.strokeStyle = subgraphNode.bgcolor;
            ctx.lineWidth = 10;
            ctx.strokeRect(1, 1, canvas.width - 2, canvas.height - 2);
            ctx.lineWidth = 1;
            ctx.font = "40px Arial";
            ctx.textAlign = "center";
            ctx.fillStyle = subgraphNode.bgcolor || "#AAA";
            let title = "";

            for (const g of this._graph_stack) {
                title += `${g._subgraph_node.getTitle()} >> `;
            }

            ctx.fillText(
                title + subgraphNode.getTitle(),
                canvas.width * 0.5,
                40,
            );
            ctx.restore();
        }

        let bgAlreadyPainted = false;
        if (this.onRenderBackground) {
            bgAlreadyPainted = this.onRenderBackground(canvas, ctx);
        }

        // reset in case of error
        ctx.restore();
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        this.visible_links.length = 0;

        if (this.graph) {
            // apply transformations
            ctx.save();
            this.ds.toCanvasContext(ctx);

            // render BG
            if (this.background_image && this.ds.scale > 0.5 && !bgAlreadyPainted) {
                ctx.globalAlpha = this.zoom_modify_alpha
                    ? (1.0 - 0.5 / this.ds.scale) * this.editor_alpha
                    : this.editor_alpha;

                ctx.imageSmoothingEnabled = false;
                ctx.mozImageSmoothingEnabled = false;
                ctx.imageSmoothingEnabled = false;
                if (
                    !this._bg_img
                    || this._bg_img.id !== this.background_image
                ) {
                    this._bg_img = new Image();
                    this._bg_img.id = this.background_image;
                    this._bg_img.src = this.background_image;
                    this._bg_img.onload = () => this.draw(true, true);
                }

                let pattern = null;
                if (this._pattern == null && this._bg_img.width > 0) {
                    pattern = ctx.createPattern(this._bg_img, "repeat");
                    this._pattern_img = this._bg_img;
                    this._pattern = pattern;
                } else {
                    pattern = this._pattern;
                }
                if (pattern) {
                    ctx.fillStyle = pattern;
                    ctx.fillRect(
                        this.visible_area[0],
                        this.visible_area[1],
                        this.visible_area[2],
                        this.visible_area[3],
                    );
                    ctx.fillStyle = "transparent";
                }

                ctx.globalAlpha = 1.0;
                ctx.imageSmoothingEnabled = true;
                ctx.mozImageSmoothingEnabled = true;
                ctx.imageSmoothingEnabled = true;
            }

            // groups
            if (this.graph._groups.length && !this.live_mode) this.drawGroups(canvas, ctx);

            if (this.onDrawBackground) this.onDrawBackground(ctx, this.visible_area);

            // bg
            if (this.render_canvas_border) {
                ctx.strokeStyle = "#235";
                ctx.strokeRect(0, 0, canvas.width, canvas.height);
            }

            if (this.render_connections_shadows) {
                ctx.shadowColor = "#000";
                ctx.shadowOffsetX = 0;
                ctx.shadowOffsetY = 0;
                ctx.shadowBlur = 6;
            } else {
                ctx.shadowColor = "rgba(0,0,0,0)";
            }

            // draw connections
            if (!this.live_mode) this.drawConnections(ctx);

            ctx.shadowColor = "rgba(0,0,0,0)";

            ctx.restore();
        }

        if (ctx.finish) ctx.finish();

        this.dirty_bgcanvas = false;
        this.dirty_canvas = true; // to force to repaint the front canvas with the bgcanvas
    }

    /**
     * draws the given node inside the canvas
     * @method drawNode
     * @memberOf LGraphCanvas
     * */
    drawNode(node, ctx) {
        let glow = false;
        this.current_node = node;

        const color = node.color || node.constructor.color || defaultConfig.NODE_DEFAULT_COLOR;
        let bgcolor = node.bgcolor || node.constructor.bgcolor || defaultConfig.NODE_DEFAULT_BGCOLOR;

        // shadow and glow
        if (node.mouseOver) glow = true;

        const lowQuality = this.ds.scale < 0.6; // zoomed out

        // only render if it forces it to do it
        if (this.live_mode) {
            if (!node.flags.collapsed) {
                ctx.shadowColor = "transparent";
                if (node.onDrawForeground) {
                    node.onDrawForeground(ctx, this, this.canvas);
                }
            }
            return;
        }

        ctx.globalAlpha = this.editor_alpha;

        if (this.render_shadows && !lowQuality) {
            ctx.shadowColor = defaultConfig.DEFAULT_SHADOW_COLOR;
            ctx.shadowOffsetX = 2 * this.ds.scale;
            ctx.shadowOffsetY = 2 * this.ds.scale;
            ctx.shadowBlur = 3 * this.ds.scale;
        } else {
            ctx.shadowColor = "transparent";
        }

        // custom draw collapsed method (draw after shadows because they are affected)
        if (node.flags.collapsed
            && node.onDrawCollapsed
            && node.onDrawCollapsed(ctx, this) == true
        ) {
            return;
        }

        // clip if required (mask)
        const shape = node._shape || defaultConfig.BOX_SHAPE;
        const size = tempVC2;
        tempVC2.set(node.size);
        const { horizontal } = node; // || node.flags.horizontal;

        if (node.flags.collapsed) {
            ctx.font = this.inner_text_font;
            const title = node.getTitle ? node.getTitle() : node.title;
            if (title) {
                node._collapsed_width = Math.min(
                    node.size[0],
                    ctx.measureText(title).width
                    + defaultConfig.NODE_TITLE_HEIGHT * 2,
                ); // LiteGraph.NODE_COLLAPSED_WIDTH;
                size[0] = node._collapsed_width;
                size[1] = 0;
            }
        }

        if (node.clip_area) {
            // Start clipping
            ctx.save();
            ctx.beginPath();
            if (shape === defaultConfig.BOX_SHAPE) ctx.rect(0, 0, size[0], size[1]);
            else if (shape === defaultConfig.ROUND_SHAPE) ctx.roundRect(0, 0, size[0], size[1], 10);
            else if (shape === defaultConfig.CIRCLE_SHAPE) {
                ctx.arc(
                    size[0] * 0.5,
                    size[1] * 0.5,
                    size[0] * 0.5,
                    0,
                    Math.PI * 2,
                );
            }
            ctx.clip();
        }

        // draw shape
        if (node.has_errors) bgcolor = "red";
        this.drawNodeShape(
            node,
            ctx,
            size,
            color,
            bgcolor,
            node.is_selected,
            node.mouseOver,
        );
        ctx.shadowColor = "transparent";

        // draw foreground
        if (node.onDrawForeground) {
            node.onDrawForeground(ctx, this, this.canvas);
        }

        // connection slots
        ctx.textAlign = horizontal ? "center" : "left";
        ctx.font = this.inner_text_font;

        const renderText = !lowQuality;

        const outSlot = this.connecting_output;
        ctx.lineWidth = 1;

        let maxY = 0;
        const slotPos = new Float32Array(2); // to reuse

        // render inputs and outputs
        if (!node.flags.collapsed) {
            // input connection slots
            if (node.inputs) {
                for (let i = 0; i < node.inputs.length; i++) {
                    const slot = node.inputs[i];

                    ctx.globalAlpha = this.editor_alpha;
                    // change opacity of incompatible slots when dragging a connection
                    if (this.connecting_node
                        && !isValidConnection(slot.type, outSlot.type)) {
                        ctx.globalAlpha = 0.4 * this.editor_alpha;
                    }

                    ctx.fillStyle = slot.link
                        ? slot.color_on
                        || this.default_connection_color.input_on
                        : slot.color_off
                        || this.default_connection_color.input_off;

                    const pos = node.getConnectionPos(true, i, slotPos);
                    pos[0] -= node.pos[0];
                    pos[1] -= node.pos[1];
                    if (maxY < pos[1] + defaultConfig.NODE_SLOT_HEIGHT * 0.5) {
                        maxY = pos[1] + defaultConfig.NODE_SLOT_HEIGHT * 0.5;
                    }

                    ctx.beginPath();

                    if (slot.type === defaultConfig.EVENT || slot.shape === defaultConfig.BOX_SHAPE) {
                        if (horizontal) ctx.rect(pos[0] - 5 + 0.5, pos[1] - 8 + 0.5, 10, 14);
                        else ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10);
                    } else if (slot.shape === defaultConfig.ARROW_SHAPE) {
                        ctx.moveTo(pos[0] + 8, pos[1] + 0.5);
                        ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5);
                        ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5);
                        ctx.closePath();
                    } else if (lowQuality) {
                        ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8);
                    } else {
                        ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2);
                    }
                    ctx.fill();

                    // render name
                    if (renderText) {
                        const text = slot.label ? slot.label : slot.name;
                        if (text) {
                            ctx.fillStyle = defaultConfig.NODE_TEXT_COLOR;
                            if (horizontal || slot.dir === defaultConfig.UP) {
                                ctx.fillText(text, pos[0], pos[1] - 10);
                            } else {
                                ctx.fillText(text, pos[0] + 10, pos[1] + 5);
                            }
                        }
                    }
                }
            }

            // output connection slots
            if (this.connecting_node) {
                ctx.globalAlpha = 0.4 * this.editor_alpha;
            }

            ctx.textAlign = horizontal ? "center" : "right";
            ctx.strokeStyle = "black";
            if (node.outputs) {
                for (let i = 0; i < node.outputs.length; i++) {
                    const slot = node.outputs[i];

                    const pos = node.getConnectionPos(false, i, slotPos);
                    pos[0] -= node.pos[0];
                    pos[1] -= node.pos[1];
                    if (maxY < pos[1] + defaultConfig.NODE_SLOT_HEIGHT * 0.5) {
                        maxY = pos[1] + defaultConfig.NODE_SLOT_HEIGHT * 0.5;
                    }

                    ctx.fillStyle = slot.links && slot.links.length
                        ? slot.color_on
                        || this.default_connection_color.output_on
                        : slot.color_off
                        || this.default_connection_color.output_off;
                    ctx.beginPath();
                    // ctx.rect( node.size[0] - 14,i*14,10,10);

                    if (
                        slot.type === defaultConfig.EVENT
                        || slot.shape === defaultConfig.BOX_SHAPE
                    ) {
                        if (horizontal) {
                            ctx.rect(
                                pos[0] - 5 + 0.5,
                                pos[1] - 8 + 0.5,
                                10,
                                14,
                            );
                        } else {
                            ctx.rect(
                                pos[0] - 6 + 0.5,
                                pos[1] - 5 + 0.5,
                                14,
                                10,
                            );
                        }
                    } else if (slot.shape === defaultConfig.ARROW_SHAPE) {
                        ctx.moveTo(pos[0] + 8, pos[1] + 0.5);
                        ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5);
                        ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5);
                        ctx.closePath();
                    } else if (lowQuality) {
                        ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8);
                    } else {
                        ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2);
                    }

                    ctx.fill();
                    if (!lowQuality) ctx.stroke();

                    // render output name
                    if (renderText) {
                        const text = slot.label != null ? slot.label : slot.name;
                        if (text) {
                            ctx.fillStyle = defaultConfig.NODE_TEXT_COLOR;
                            if (horizontal || slot.dir === defaultConfig.DOWN) {
                                ctx.fillText(text, pos[0], pos[1] - 8);
                            } else {
                                ctx.fillText(text, pos[0] - 10, pos[1] + 5);
                            }
                        }
                    }
                }
            }

            ctx.textAlign = "left";
            ctx.globalAlpha = 1;

            if (node.widgets) {
                let widgetsY = maxY;
                if (horizontal || node.widgets_up) widgetsY = 2;
                if (node.widgets_start_y) widgetsY = node.widgets_start_y;
                this.drawNodeWidgets(
                    node,
                    widgetsY,
                    ctx,
                    this.node_widget && this.node_widget[0] === node ? this.node_widget[1] : null,
                );
            }
        } else if (this.render_collapsed_slots) {
            // if collapsed
            let inputSlot = null;
            let outputSlot = null;
            let storedSlot;

            // get first connected slot to render
            if (node.inputs) {
                for (const slot of node.inputs) {
                    if (slot.link == null) continue;
                    inputSlot = slot;
                    storedSlot = slot;
                    break;
                }
            }
            if (node.outputs) {
                for (const slot of node.outputs) {
                    if (!slot.links || !slot.links.length) continue;
                    outputSlot = slot;
                    storedSlot = slot;
                }
            }

            if (inputSlot) {
                let x = 0;
                let y = defaultConfig.NODE_TITLE_HEIGHT * -0.5; // center
                if (horizontal) {
                    x = node._collapsed_width * 0.5;
                    y = -defaultConfig.NODE_TITLE_HEIGHT;
                }
                ctx.fillStyle = "#686";
                ctx.beginPath();
                if (storedSlot.type === defaultConfig.EVENT
                    || storedSlot.shape === defaultConfig.BOX_SHAPE) {
                    ctx.rect(x - 7 + 0.5, y - 4, 14, 8);
                } else if (storedSlot.shape === defaultConfig.ARROW_SHAPE) {
                    ctx.moveTo(x + 8, y);
                    ctx.lineTo(x + -4, y - 4);
                    ctx.lineTo(x + -4, y + 4);
                    ctx.closePath();
                } else ctx.arc(x, y, 4, 0, Math.PI * 2);
                ctx.fill();
            }

            if (outputSlot) {
                let x = node._collapsed_width;
                let y = defaultConfig.NODE_TITLE_HEIGHT * -0.5; // center
                if (horizontal) {
                    x = node._collapsed_width * 0.5;
                    y = 0;
                }
                ctx.fillStyle = "#686";
                ctx.strokeStyle = "black";
                ctx.beginPath();
                if (
                    storedSlot.type === defaultConfig.EVENT
                    || storedSlot.shape === defaultConfig.BOX_SHAPE
                ) {
                    ctx.rect(x - 7 + 0.5, y - 4, 14, 8);
                } else if (slot.shape === defaultConfig.ARROW_SHAPE) {
                    ctx.moveTo(x + 6, y);
                    ctx.lineTo(x - 6, y - 4);
                    ctx.lineTo(x - 6, y + 4);
                    ctx.closePath();
                } else ctx.arc(x, y, 4, 0, Math.PI * 2);
                ctx.fill();
                // ctx.stroke();
            }
        }

        if (node.clip_area) ctx.restore();

        ctx.globalAlpha = 1.0;
    }

    // used by this.over_link_center
    drawLinkTooltip(ctx, link) {
        const pos = link._pos;
        ctx.fillStyle = "black";
        ctx.beginPath();
        ctx.arc(pos[0], pos[1], 3, 0, Math.PI * 2);
        ctx.fill();

        if (link.data == null) return;

        if (this.onDrawLinkTooltip && this.onDrawLinkTooltip(ctx, link, this)) return;

        const { data } = link;
        let text;

        if (data.constructor === Number) text = data.toFixed(2);
        else if (data.constructor === String) text = `"${data}"`;
        else if (data.constructor === Boolean) text = String(data);
        else if (data.toToolTip) text = data.toToolTip();
        else text = `[${data.constructor.name}]`;

        if (!text) return;
        text = text.substr(0, 30); // avoid weird

        ctx.font = "14px Courier New";
        const info = ctx.measureText(text);
        const w = info.width + 20;
        const h = 24;
        ctx.shadowColor = "black";
        ctx.shadowOffsetX = 2;
        ctx.shadowOffsetY = 2;
        ctx.shadowBlur = 3;
        ctx.fillStyle = "#454";
        ctx.beginPath();
        ctx.roundRect(pos[0] - w * 0.5, pos[1] - 15 - h, w, h, 3, 3);
        ctx.moveTo(pos[0] - 10, pos[1] - 15);
        ctx.lineTo(pos[0] + 10, pos[1] - 15);
        ctx.lineTo(pos[0], pos[1] - 5);
        ctx.fill();
        ctx.shadowColor = "transparent";
        ctx.textAlign = "center";
        ctx.fillStyle = "#CEC";
        ctx.fillText(text, pos[0], pos[1] - 15 - h * 0.3);
    }

    /**
     * draws the shape of the given node in the canvas
     * @method drawNodeShape
     * @memberOf LGraphCanvas
     * */
    drawNodeShape(
        node,
        ctx,
        size,
        fgcolor,
        bgcolor,
        selected,
        mouseHover,
    ) {
        // bg rect
        ctx.strokeStyle = fgcolor;
        ctx.fillStyle = bgcolor;

        const titleHeight = defaultConfig.NODE_TITLE_HEIGHT;
        const lowQuality = this.ds.scale < 0.5;

        // render node area depending on shape
        const shape = node._shape || node.constructor.shape || defaultConfig.ROUND_SHAPE;

        const { title_mode } = node.constructor;

        let renderTitle = true;
        if (title_mode === defaultConfig.TRANSPARENT_TITLE) renderTitle = false;
        else if (title_mode === defaultConfig.AUTOHIDE_TITLE && mouseHover) renderTitle = true;

        const area = tempArea;
        area[0] = 0; // x
        area[1] = renderTitle ? -titleHeight : 0; // y
        area[2] = size[0] + 1; // w
        area[3] = renderTitle ? size[1] + titleHeight : size[1]; // h

        const oldAlpha = ctx.globalAlpha;

        ctx.beginPath();
        if (shape === defaultConfig.BOX_SHAPE || lowQuality) {
            ctx.fillRect(area[0], area[1], area[2], area[3]);
        } else if (shape === defaultConfig.ROUND_SHAPE || shape === defaultConfig.CARD_SHAPE) {
            ctx.roundRect(
                area[0],
                area[1],
                area[2],
                area[3],
                this.round_radius,
                shape === defaultConfig.CARD_SHAPE ? 0 : this.round_radius,
            );
        } else if (shape === defaultConfig.CIRCLE_SHAPE) {
            ctx.arc(
                size[0] * 0.5,
                size[1] * 0.5,
                size[0] * 0.5,
                0,
                Math.PI * 2,
            );
        }
        ctx.fill();

        // separator
        if (!node.flags.collapsed) {
            ctx.shadowColor = "transparent";
            ctx.fillStyle = "rgba(0,0,0,0.2)";
            ctx.fillRect(0, -1, area[2], 2);
        }
        ctx.shadowColor = "transparent";

        if (node.onDrawBackground) node.onDrawBackground(ctx, this, this.canvas, this.graph_mouse);

        // title bg (remember, it is rendered ABOVE the node)
        if (renderTitle || title_mode === defaultConfig.TRANSPARENT_TITLE) {
            // title bar
            if (node.onDrawTitleBar) {
                node.onDrawTitleBar(ctx, titleHeight, size, this.ds.scale, fgcolor);
            } else if (
                title_mode !== defaultConfig.TRANSPARENT_TITLE
                && (node.constructor.title_color || this.render_title_colored)
            ) {
                const titleColor = node.constructor.title_color || fgcolor;

                if (node.flags.collapsed) {
                    ctx.shadowColor = defaultConfig.DEFAULT_SHADOW_COLOR;
                }

                //* gradient test
                if (this.use_gradients) {
                    let grad = LGraphCanvas.gradients[titleColor];
                    if (!grad) {
                        grad = ctx.createLinearGradient(0, 0, 400, 0);
                        LGraphCanvas.gradients[titleColor] = grad;
                        grad.addColorStop(0, titleColor);
                        grad.addColorStop(1, "#000");
                    }
                    ctx.fillStyle = grad;
                } else {
                    ctx.fillStyle = titleColor;
                }

                // ctx.globalAlpha = 0.5 * old_alpha;
                ctx.beginPath();
                if (shape === defaultConfig.BOX_SHAPE || lowQuality) {
                    ctx.rect(0, -titleHeight, size[0] + 1, titleHeight);
                } else if (shape === defaultConfig.ROUND_SHAPE || shape === defaultConfig.CARD_SHAPE) {
                    ctx.roundRect(
                        0,
                        -titleHeight,
                        size[0] + 1,
                        titleHeight,
                        this.round_radius,
                        node.flags.collapsed ? this.round_radius : 0,
                    );
                }
                ctx.fill();
                ctx.shadowColor = "transparent";
            }

            // title box
            const boxSize = 10;
            if (node.onDrawTitleBox) {
                node.onDrawTitleBox(ctx, titleHeight, size, this.ds.scale);
            } else if ([defaultConfig.ROUND_SHAPE, defaultConfig.CIRCLE_SHAPE, defaultConfig.CARD_SHAPE].includes(shape)) {
                if (lowQuality) {
                    ctx.fillStyle = "black";
                    ctx.beginPath();
                    ctx.arc(
                        titleHeight * 0.5,
                        titleHeight * -0.5,
                        boxSize * 0.5 + 1,
                        0,
                        Math.PI * 2,
                    );
                    ctx.fill();
                }

                ctx.fillStyle = node.boxcolor || defaultConfig.NODE_DEFAULT_BOXCOLOR;
                if (lowQuality) ctx.fillRect(titleHeight * 0.5 - boxSize * 0.5, titleHeight * -0.5 - boxSize * 0.5, boxSize, boxSize);
                else {
                    ctx.beginPath();
                    ctx.arc(
                        titleHeight * 0.5,
                        titleHeight * -0.5,
                        boxSize * 0.5,
                        0,
                        Math.PI * 2,
                    );
                    ctx.fill();
                }
            } else {
                if (lowQuality) {
                    ctx.fillStyle = "black";
                    ctx.fillRect(
                        (titleHeight - boxSize) * 0.5 - 1,
                        (titleHeight + boxSize) * -0.5 - 1,
                        boxSize + 2,
                        boxSize + 2,
                    );
                }
                ctx.fillStyle = node.boxcolor || defaultConfig.NODE_DEFAULT_BOXCOLOR;
                ctx.fillRect(
                    (titleHeight - boxSize) * 0.5,
                    (titleHeight + boxSize) * -0.5,
                    boxSize,
                    boxSize,
                );
            }
            ctx.globalAlpha = oldAlpha;

            // title text
            if (node.onDrawTitleText) {
                node.onDrawTitleText(
                    ctx,
                    titleHeight,
                    size,
                    this.ds.scale,
                    this.title_text_font,
                    selected,
                );
            }
            if (!lowQuality) {
                ctx.font = this.title_text_font;
                const title = String(node.getTitle());
                if (title) {
                    if (selected) ctx.fillStyle = defaultConfig.NODE_SELECTED_TITLE_COLOR;
                    else ctx.fillStyle = node.constructor.title_text_color || this.node_title_color;
                    if (node.flags.collapsed) {
                        ctx.textAlign = "left";
                        const measure = ctx.measureText(title);
                        ctx.fillText(
                            title.substr(0, 20), // avoid urls too long
                            titleHeight, // + measure.width * 0.5,
                            defaultConfig.NODE_TITLE_TEXT_Y - titleHeight,
                        );
                        ctx.textAlign = "left";
                    } else {
                        ctx.textAlign = "left";
                        ctx.fillText(
                            title,
                            titleHeight,
                            defaultConfig.NODE_TITLE_TEXT_Y - titleHeight,
                        );
                    }
                }
            }

            // subgraph box
            if (!node.flags.collapsed && node.subgraph && !node.skip_subgraph_button) {
                const w = defaultConfig.NODE_TITLE_HEIGHT;
                const x = node.size[0] - w;
                const over = isInsideRectangle(
                    this.graph_mouse[0] - node.pos[0],
                    this.graph_mouse[1] - node.pos[1],
                    x + 2,
                    -w + 2,
                    w - 4,
                    w - 4,
                );
                ctx.fillStyle = over ? "#888" : "#555";
                if (shape === defaultConfig.BOX_SHAPE || lowQuality) {
                    ctx.fillRect(x + 2, -w + 2, w - 4, w - 4);
                } else {
                    ctx.beginPath();
                    ctx.roundRect(x + 2, -w + 2, w - 4, w - 4, 4);
                    ctx.fill();
                }
                ctx.fillStyle = "#333";
                ctx.beginPath();
                ctx.moveTo(x + w * 0.2, -w * 0.6);
                ctx.lineTo(x + w * 0.8, -w * 0.6);
                ctx.lineTo(x + w * 0.5, -w * 0.3);
                ctx.fill();
            }

            // custom title render
            if (node.onDrawTitle) node.onDrawTitle(ctx);
        }

        // render selection marker
        if (selected) {
            if (node.onBounding) node.onBounding(area);

            if (title_mode === defaultConfig.TRANSPARENT_TITLE) {
                area[1] -= titleHeight;
                area[3] += titleHeight;
            }
            ctx.lineWidth = 1;
            ctx.globalAlpha = 0.8;
            ctx.beginPath();
            if (shape === defaultConfig.BOX_SHAPE) {
                ctx.rect(
                    -6 + area[0],
                    -6 + area[1],
                    12 + area[2],
                    12 + area[3],
                );
            } else if (
                shape === defaultConfig.ROUND_SHAPE
                || (shape === defaultConfig.CARD_SHAPE && node.flags.collapsed)
            ) {
                ctx.roundRect(
                    -6 + area[0],
                    -6 + area[1],
                    12 + area[2],
                    12 + area[3],
                    this.round_radius * 2,
                );
            } else if (shape === defaultConfig.CARD_SHAPE) {
                ctx.roundRect(
                    -6 + area[0],
                    -6 + area[1],
                    12 + area[2],
                    12 + area[3],
                    this.round_radius * 2,
                    2,
                );
            } else if (shape === defaultConfig.CIRCLE_SHAPE) {
                ctx.arc(
                    size[0] * 0.5,
                    size[1] * 0.5,
                    size[0] * 0.5 + 6,
                    0,
                    Math.PI * 2,
                );
            }
            ctx.strokeStyle = defaultConfig.NODE_BOX_OUTLINE_COLOR;
            ctx.stroke();
            ctx.strokeStyle = fgcolor;
            ctx.globalAlpha = 1;
        }
    }

    /**
     * draws every connection visible in the canvas
     * OPTIMIZE THIS: pre-catch connections position instead of recomputing them every time
     * @method drawConnections
     * @memberOf LGraphCanvas
     * */
    drawConnections(ctx) {
        const now = getTime();
        const { visible_area } = this;
        marginArea[0] = visible_area[0] - 20;
        marginArea[1] = visible_area[1] - 20;
        marginArea[2] = visible_area[2] + 40;
        marginArea[3] = visible_area[3] + 40;

        // draw connections
        ctx.lineWidth = this.connections_width;

        ctx.fillStyle = "#AAA";
        ctx.strokeStyle = "#AAA";
        ctx.globalAlpha = this.editor_alpha;
        // for every node
        const nodes = this.graph._nodes;
        for (const node of nodes) {
            // for every input (we render just inputs because it is easier as every slot can only
            // have one input)
            if (!node.inputs || !node.inputs.length) {
                continue;
            }

            for (let i = 0; i < node.inputs.length; ++i) {
                const input = node.inputs[i];
                if (!input || input.link == null) continue;

                const linkId = input.link;
                const link = this.graph.links[linkId];
                if (!link) continue;

                // find link info
                const startNode = this.graph.getNodeById(link.origin_id);
                if (!startNode) continue;

                const startNodeSlot = link.origin_slot;
                let startNodeSlotPos = null;
                if (startNodeSlot === -1) {
                    startNodeSlotPos = [
                        startNode.pos[0] + 10,
                        startNode.pos[1] + 10,
                    ];
                } else {
                    startNodeSlotPos = startNode.getConnectionPos(
                        false,
                        startNodeSlot,
                        tempA,
                    );
                }

                const endNodeSlotPos = node.getConnectionPos(true, i, tempB);

                // compute link bounding
                linkBounding[0] = startNodeSlotPos[0];
                linkBounding[1] = startNodeSlotPos[1];
                linkBounding[2] = endNodeSlotPos[0] - startNodeSlotPos[0];
                linkBounding[3] = endNodeSlotPos[1] - startNodeSlotPos[1];

                if (linkBounding[2] < 0) {
                    linkBounding[0] += linkBounding[2];
                    linkBounding[2] = Math.abs(linkBounding[2]);
                }
                if (linkBounding[3] < 0) {
                    linkBounding[1] += linkBounding[3];
                    linkBounding[3] = Math.abs(linkBounding[3]);
                }

                // skip links outside of the visible area of the canvas
                if (!overlapBounding(linkBounding, marginArea)) {
                    continue;
                }

                const startSlot = startNode.outputs[startNodeSlot];
                const endSlot = node.inputs[i];
                if (!startSlot || !endSlot) continue;
                const startDir = startSlot.dir
                    || (startNode.horizontal ? defaultConfig.DOWN : defaultConfig.RIGHT);
                const endDir = endSlot.dir
                    || (node.horizontal ? defaultConfig.UP : defaultConfig.LEFT);

                this.renderLink(
                    ctx,
                    startNodeSlotPos,
                    endNodeSlotPos,
                    link,
                    false,
                    0,
                    null,
                    startDir,
                    endDir,
                );

                // event triggered rendered on top
                if (link && link._last_time && now - link._last_time < 1000) {
                    const f = 2.0 - (now - link._last_time) * 0.002;
                    const tmp = ctx.globalAlpha;
                    ctx.globalAlpha = tmp * f;
                    this.renderLink(
                        ctx,
                        startNodeSlotPos,
                        endNodeSlotPos,
                        link,
                        true,
                        f,
                        "white",
                        startDir,
                        endDir,
                    );
                    ctx.globalAlpha = tmp;
                }
            }
        }
        ctx.globalAlpha = 1;
    }

    /**
     * draws a link between two points
     * @method renderLink
     * @param {vec2} a start pos
     * @param {vec2} b end pos
     * @param {Object} link the link object with all the link info
     * @param {boolean} skipBorder ignore the shadow of the link
     * @param {boolean} flow show flow animation (for events)
     * @param {string} color the color for the link
     * @param {number} startDir the direction enum
     * @param {number} endDir the direction enum
     * @param {number} numSubline number of sublines (useful to represent vec3 or rgb)
     * @memberOf LGraphCanvas
     * */
    renderLink(
        ctx,
        a,
        b,
        link,
        skipBorder,
        flow,
        color,
        startDir,
        endDir,
        numSubline,
    ) {
        if (link) this.visible_links.push(link);

        // choose color
        if (!color && link) color = link.color || LGraphCanvas.link_type_colors[link.type];
        if (!color) color = this.default_link_color;
        if (link != null && this.highlighted_links[link.id]) color = "#FFF";

        startDir = startDir || defaultConfig.RIGHT;
        endDir = endDir || defaultConfig.LEFT;

        const dist = distance(a, b);

        if (this.render_connections_border && this.ds.scale > 0.6) {
            ctx.lineWidth = this.connections_width + 4;
        }
        ctx.lineJoin = "round";
        numSubline = numSubline || 1;
        if (numSubline > 1) ctx.lineWidth = 0.5;

        // begin line shape
        ctx.beginPath();
        for (let i = 0; i < numSubline; i += 1) {
            const offsety = (i - (numSubline - 1) * 0.5) * 5;

            if (this.links_render_mode === defaultConfig.SPLINE_LINK) {
                ctx.moveTo(a[0], a[1] + offsety);
                let startOffsetX = 0;
                let startOffsetY = 0;
                let endOffsetX = 0;
                let endOffsetY = 0;
                switch (startDir) {
                    case defaultConfig.LEFT:
                        startOffsetX = dist * -0.25;
                        break;
                    case defaultConfig.RIGHT:
                        startOffsetX = dist * 0.25;
                        break;
                    case defaultConfig.UP:
                        startOffsetY = dist * -0.25;
                        break;
                    case defaultConfig.DOWN:
                        startOffsetY = dist * 0.25;
                        break;
                    default:
                        break;
                }
                switch (endDir) {
                    case defaultConfig.LEFT:
                        endOffsetX = dist * -0.25;
                        break;
                    case defaultConfig.RIGHT:
                        endOffsetX = dist * 0.25;
                        break;
                    case defaultConfig.UP:
                        endOffsetY = dist * -0.25;
                        break;
                    case defaultConfig.DOWN:
                        endOffsetY = dist * 0.25;
                        break;
                    default:
                        break;
                }
                ctx.bezierCurveTo(
                    a[0] + startOffsetX,
                    a[1] + startOffsetY + offsety,
                    b[0] + endOffsetX,
                    b[1] + endOffsetY + offsety,
                    b[0],
                    b[1] + offsety,
                );
            } else if (this.links_render_mode === defaultConfig.LINEAR_LINK) {
                ctx.moveTo(a[0], a[1] + offsety);
                let startOffsetX = 0;
                let startOffsetY = 0;
                let endOffsetX = 0;
                let endOffsetY = 0;
                switch (startDir) {
                    case defaultConfig.LEFT:
                        startOffsetX = -1;
                        break;
                    case defaultConfig.RIGHT:
                        startOffsetX = 1;
                        break;
                    case defaultConfig.UP:
                        startOffsetY = -1;
                        break;
                    case defaultConfig.DOWN:
                        startOffsetY = 1;
                        break;
                    default:
                        break;
                }
                switch (endDir) {
                    case defaultConfig.LEFT:
                        endOffsetX = -1;
                        break;
                    case defaultConfig.RIGHT:
                        endOffsetX = 1;
                        break;
                    case defaultConfig.UP:
                        endOffsetY = -1;
                        break;
                    case defaultConfig.DOWN:
                        endOffsetY = 1;
                        break;
                    default:
                        break;
                }
                const l = 15;
                ctx.lineTo(
                    a[0] + startOffsetX * l,
                    a[1] + startOffsetY * l + offsety,
                );
                ctx.lineTo(
                    b[0] + endOffsetX * l,
                    b[1] + endOffsetY * l + offsety,
                );
                ctx.lineTo(b[0], b[1] + offsety);
            } else if (this.links_render_mode === defaultConfig.STRAIGHT_LINK) {
                ctx.moveTo(a[0], a[1]);
                let startX = a[0];
                let startY = a[1];
                let endX = b[0];
                let endY = b[1];

                if (startDir === defaultConfig.RIGHT) startX += 10;
                else startY += 10;
                if (endDir === defaultConfig.LEFT) endX -= 10;
                else endY -= 10;

                ctx.lineTo(startX, startY);
                ctx.lineTo((startX + endX) * 0.5, startY);
                ctx.lineTo((startX + endX) * 0.5, endY);
                ctx.lineTo(endX, endY);
                ctx.lineTo(b[0], b[1]);
            } else return;
        }

        // rendering the outline of the connection can be a little bit slow
        if (this.render_connections_border && this.ds.scale > 0.6 && !skipBorder) {
            ctx.strokeStyle = "rgba(0,0,0,0.5)";
            ctx.stroke();
        }

        ctx.lineWidth = this.connections_width;
        ctx.fillStyle = color;
        ctx.strokeStyle = color;
        ctx.stroke();
        // end line shape

        const posConnectionPoint = this.computeConnectionPoint(a, b, 0.5, startDir, endDir);
        if (link && link._pos) {
            link._pos[0] = posConnectionPoint[0];
            link._pos[1] = posConnectionPoint[1];
        }

        // render arrow in the middle
        if (this.ds.scale >= 0.6 && this.highquality_render && endDir !== defaultConfig.CENTER) {
            // render arrow
            if (this.render_connection_arrows) {
                // compute two points in the connection
                const posA = this.computeConnectionPoint(a, b, 0.25, startDir, endDir);
                const posB = this.computeConnectionPoint(a, b, 0.26, startDir, endDir);
                const posC = this.computeConnectionPoint(a, b, 0.75, startDir, endDir);
                const posD = this.computeConnectionPoint(a, b, 0.76, startDir, endDir);

                // compute the angle between them so the arrow points in the right direction
                let angleA = 0;
                let angleB = 0;
                if (this.render_curved_connections) {
                    angleA = -Math.atan2(posB[0] - posA[0], posB[1] - posA[1]);
                    angleB = -Math.atan2(posD[0] - posC[0], posD[1] - posC[1]);
                } else angleB = angleA = b[1] > a[1] ? 0 : Math.PI;

                // render arrow
                ctx.save();
                ctx.translate(posA[0], posA[1]);
                ctx.rotate(angleA);
                ctx.beginPath();
                ctx.moveTo(-5, -3);
                ctx.lineTo(0, 7);
                ctx.lineTo(5, -3);
                ctx.fill();
                ctx.restore();
                ctx.save();
                ctx.translate(posC[0], posC[1]);
                ctx.rotate(angleB);
                ctx.beginPath();
                ctx.moveTo(-5, -3);
                ctx.lineTo(0, 7);
                ctx.lineTo(5, -3);
                ctx.fill();
                ctx.restore();
            }

            // circle
            ctx.beginPath();
            ctx.arc(posConnectionPoint[0], posConnectionPoint[1], 5, 0, Math.PI * 2);
            ctx.fill();
        }

        // render flowing points
        if (flow) {
            ctx.fillStyle = color;
            for (let i = 0; i < 5; ++i) {
                const f = (getTime() * 0.001 + i * 0.2) % 1;
                const pos = this.computeConnectionPoint(a, b, f, startDir, endDir);
                ctx.beginPath();
                ctx.arc(pos[0], pos[1], 5, 0, 2 * Math.PI);
                ctx.fill();
            }
        }
    }

    /**
     * returns the link center point based on curvature
     * @method computeConnectionPoint
     * @param a
     * @param b
     * @param t
     * @param [startDir]
     * @param [endDir]
     * @returns {number[]}
     * @memberOf LGraphCanvas
     */
    computeConnectionPoint(a, b, t, startDir = defaultConfig.RIGHT, endDir = defaultConfig.LEFT) {
        const dist = distance(a, b);
        const p0 = a;
        const p1 = [a[0], a[1]];
        const p2 = [b[0], b[1]];
        const p3 = b;

        switch (startDir) {
            case defaultConfig.LEFT:
                p1[0] += dist * -0.25;
                break;
            case defaultConfig.RIGHT:
                p1[0] += dist * 0.25;
                break;
            case defaultConfig.UP:
                p1[1] += dist * -0.25;
                break;
            case defaultConfig.DOWN:
                p1[1] += dist * 0.25;
                break;
            default:
                break;
        }
        switch (endDir) {
            case defaultConfig.LEFT:
                p2[0] += dist * -0.25;
                break;
            case defaultConfig.RIGHT:
                p2[0] += dist * 0.25;
                break;
            case defaultConfig.UP:
                p2[1] += dist * -0.25;
                break;
            case defaultConfig.DOWN:
                p2[1] += dist * 0.25;
                break;
            default:
                break;
        }

        const c1 = (1 - t) * (1 - t) * (1 - t);
        const c2 = 3 * ((1 - t) * (1 - t)) * t;
        const c3 = 3 * (1 - t) * (t * t);
        const c4 = t * t * t;

        const x = c1 * p0[0] + c2 * p1[0] + c3 * p2[0] + c4 * p3[0];
        const y = c1 * p0[1] + c2 * p1[1] + c3 * p2[1] + c4 * p3[1];
        return [x, y];
    }

    drawExecutionOrder(ctx) {
        ctx.shadowColor = "transparent";
        ctx.globalAlpha = 0.25;

        ctx.textAlign = "center";
        ctx.strokeStyle = "white";
        ctx.globalAlpha = 0.75;

        const { visible_nodes } = this;
        for (const node of visible_nodes) {
            ctx.fillStyle = "black";
            ctx.fillRect(
                node.pos[0] - defaultConfig.NODE_TITLE_HEIGHT,
                node.pos[1] - defaultConfig.NODE_TITLE_HEIGHT,
                defaultConfig.NODE_TITLE_HEIGHT,
                defaultConfig.NODE_TITLE_HEIGHT,
            );
            if (node.order === 0) {
                ctx.strokeRect(
                    node.pos[0] - defaultConfig.NODE_TITLE_HEIGHT + 0.5,
                    node.pos[1] - defaultConfig.NODE_TITLE_HEIGHT + 0.5,
                    defaultConfig.NODE_TITLE_HEIGHT,
                    defaultConfig.NODE_TITLE_HEIGHT,
                );
            }
            ctx.fillStyle = "#FFF";
            ctx.fillText(
                node.order,
                node.pos[0] + defaultConfig.NODE_TITLE_HEIGHT * -0.5,
                node.pos[1] - 6,
            );
        }
        ctx.globalAlpha = 1;
    }

    /**
     * draws the widgets stored inside a node
     * @method drawNodeWidgets
     * @memberOf LGraphCanvas
     * */
    drawNodeWidgets(node, posY, ctx, active_widget) {
        if (!node.widgets || !node.widgets.length) return 0;
        const width = node.size[0];
        const { widgets } = node;
        posY += 2;
        const H = defaultConfig.NODE_WIDGET_HEIGHT;
        const showText = this.ds.scale > 0.5;
        ctx.save();
        ctx.globalAlpha = this.editor_alpha;
        const outlineColor = defaultConfig.WIDGET_OUTLINE_COLOR;
        const backgroundColor = defaultConfig.WIDGET_BGCOLOR;
        const textColor = defaultConfig.WIDGET_TEXT_COLOR;
        const secondaryTextColor = defaultConfig.WIDGET_SECONDARY_TEXT_COLOR;
        const margin = 15;

        for (const w of widgets) {
            let y = posY;
            if (w.y) y = w.y;
            w.last_y = y;
            ctx.strokeStyle = outlineColor;
            ctx.fillStyle = "#222";
            ctx.textAlign = "left";
            // ctx.lineWidth = 2;
            if (w.disabled) ctx.globalAlpha *= 0.5;
            const widgetWidth = w.width || width;

            switch (w.type) {
                case "button":
                    if (w.clicked) {
                        ctx.fillStyle = "#AAA";
                        w.clicked = false;
                        this.dirty_canvas = true;
                    }
                    ctx.fillRect(margin, y, widgetWidth - margin * 2, H);
                    if (showText && !w.disabled) ctx.strokeRect(margin, y, widgetWidth - margin * 2, H);
                    if (showText) {
                        ctx.textAlign = "center";
                        ctx.fillStyle = textColor;
                        ctx.fillText(w.name, widgetWidth * 0.5, y + H * 0.7);
                    }
                    break;
                case "toggle":
                    ctx.textAlign = "left";
                    ctx.strokeStyle = outlineColor;
                    ctx.fillStyle = backgroundColor;
                    ctx.beginPath();

                    if (showText) ctx.roundRect(margin, posY, widgetWidth - margin * 2, H, H * 0.5);
                    else ctx.rect(margin, posY, widgetWidth - margin * 2, H);

                    ctx.fill();
                    if (showText && !w.disabled) ctx.stroke();
                    ctx.fillStyle = w.value ? "#89A" : "#333";
                    ctx.beginPath();
                    ctx.arc(widgetWidth - margin * 2, y + H * 0.5, H * 0.36, 0, Math.PI * 2);
                    ctx.fill();
                    if (showText) {
                        ctx.fillStyle = secondaryTextColor;
                        if (w.name) ctx.fillText(w.name, margin * 2, y + H * 0.7);
                        ctx.fillStyle = w.value ? textColor : secondaryTextColor;
                        ctx.textAlign = "right";
                        ctx.fillText(
                            w.value
                                ? w.options.on || "true"
                                : w.options.off || "false",
                            widgetWidth - 40,
                            y + H * 0.7,
                        );
                    }
                    break;
                case "slider":
                    ctx.fillStyle = backgroundColor;
                    ctx.fillRect(margin, y, widgetWidth - margin * 2, H);
                    var range = w.options.max - w.options.min;
                    var nvalue = (w.value - w.options.min) / range;
                    ctx.fillStyle = active_widget === w ? "#89A" : "#678";
                    ctx.fillRect(margin, y, nvalue * (widgetWidth - margin * 2), H);
                    if (showText && !w.disabled) ctx.strokeRect(margin, y, widgetWidth - margin * 2, H);
                    if (w.marker) {
                        const marker_nvalue = (w.marker - w.options.min) / range;
                        ctx.fillStyle = "#AA9";
                        ctx.fillRect(margin + marker_nvalue * (widgetWidth - margin * 2), y, 2, H);
                    }
                    if (showText) {
                        ctx.textAlign = "center";
                        ctx.fillStyle = textColor;
                        ctx.fillText(
                            `${w.name}  ${Number(w.value)
                                .toFixed(3)}`,
                            widgetWidth * 0.5,
                            y + H * 0.7,
                        );
                    }
                    break;
                case "number":
                case "combo":
                    ctx.textAlign = "left";
                    ctx.strokeStyle = outlineColor;
                    ctx.fillStyle = backgroundColor;
                    ctx.beginPath();
                    if (showText) ctx.roundRect(margin, posY, widgetWidth - margin * 2, H, H * 0.5);
                    else ctx.rect(margin, posY, widgetWidth - margin * 2, H);
                    ctx.fill();
                    if (showText) {
                        if (!w.disabled) ctx.stroke();
                        ctx.fillStyle = textColor;
                        if (!w.disabled) {
                            ctx.beginPath();
                            ctx.moveTo(margin + 16, posY + 5);
                            ctx.lineTo(margin + 6, posY + H * 0.5);
                            ctx.lineTo(margin + 16, posY + H - 5);
                            ctx.fill();
                            ctx.beginPath();
                            ctx.moveTo(widgetWidth - margin - 16, posY + 5);
                            ctx.lineTo(widgetWidth - margin - 6, posY + H * 0.5);
                            ctx.lineTo(widgetWidth - margin - 16, posY + H - 5);
                            ctx.fill();
                        }
                        ctx.fillStyle = secondaryTextColor;
                        ctx.fillText(w.name, margin * 2 + 5, y + H * 0.7);
                        ctx.fillStyle = textColor;
                        ctx.textAlign = "right";
                        if (w.type === "number") {
                            ctx.fillText(
                                Number(w.value)
                                    .toFixed(
                                        w.options.precision
                                            ? w.options.precision
                                            : 3,
                                    ),
                                widgetWidth - margin * 2 - 20,
                                y + H * 0.7,
                            );
                        } else {
                            let v = w.value;
                            if (w.options.values) {
                                let { values } = w.options;
                                if (values.constructor === Function) values = values();
                                if (values && values.constructor !== Array) v = values[w.value];
                            }
                            ctx.fillText(
                                v,
                                widgetWidth - margin * 2 - 20,
                                y + H * 0.7,
                            );
                        }
                    }
                    break;
                case "string":
                case "text":
                    ctx.textAlign = "left";
                    ctx.strokeStyle = outlineColor;
                    ctx.fillStyle = backgroundColor;
                    ctx.beginPath();

                    if (showText) ctx.roundRect(margin, posY, widgetWidth - margin * 2, H, H * 0.5);
                    else ctx.rect(margin, posY, widgetWidth - margin * 2, H);

                    ctx.fill();
                    if (showText) {
                        if (!w.disabled) ctx.stroke();
                        ctx.save();
                        ctx.beginPath();
                        ctx.rect(margin, posY, widgetWidth - margin * 2, H);
                        ctx.clip();

                        // ctx.stroke();
                        ctx.fillStyle = secondaryTextColor;
                        if (w.name) ctx.fillText(w.name, margin * 2, y + H * 0.7);
                        ctx.fillStyle = textColor;
                        ctx.textAlign = "right";
                        ctx.fillText(String(w.value)
                            .substr(0, 30), widgetWidth - margin * 2, y + H * 0.7); // 30 chars max
                        ctx.restore();
                    }
                    break;
                default:
                    if (w.draw) w.draw(ctx, node, widgetWidth, y, H);
                    break;
            }
            posY += (w.computeSize ? w.computeSize(widgetWidth)[1] : H) + 4;
            ctx.globalAlpha = this.editor_alpha;
        }
        ctx.restore();
        ctx.textAlign = "left";
    }

    /**
     * process an event on widgets
     * @method processNodeWidgets
     * @memberOf LGraphCanvas
     * */
    processNodeWidgets(node, pos, event, activeWidget) {
        if (!node.widgets || !node.widgets.length) return null;

        const x = pos[0] - node.pos[0];
        const y = pos[1] - node.pos[1];
        const width = node.size[0];
        const refWindow = this.getCanvasWindow();

        for (const w of node.widgets) {
            if (!w || w.disabled) continue;
            const widgetHeight = w.computeSize ? w.computeSize(width)[1] : defaultConfig.NODE_WIDGET_HEIGHT;
            const widgetWidth = w.width || width;
            // outside
            if (w !== activeWidget
                && (x < 6 || x > widgetWidth - 12 || y < w.last_y || y > w.last_y + widgetHeight)) {
                continue;
            }

            const oldValue = w.value;

            // if ( w == active_widget || (x > 6 && x < widget_width - 12 && y > w.last_y && y <
            // w.last_y + widget_height) ) { inside widget
            switch (w.type) {
                case "button":
                    if (event.type === "mousemove") {
                        break;
                    }
                    if (w.callback) {
                        setTimeout(() => w.callback(w, this, node, pos, event), 20);
                    }
                    w.clicked = true;
                    this.dirty_canvas = true;
                    break;
                case "slider":
                    const range = w.options.max - w.options.min;
                    const nvalue = clamp((x - 15) / (widgetWidth - 30), 0, 1);
                    w.value = w.options.min + (w.options.max - w.options.min) * nvalue;
                    if (w.callback) {
                        setTimeout(() => innerValueChange(w, w.value), 20);
                    }
                    this.dirty_canvas = true;
                    break;
                case "number":
                case "combo":
                    const oldValue = w.value;
                    if (event.type === "mousemove" && w.type === "number") {
                        w.value += event.deltaX * 0.1 * (w.options.step || 1);
                        if (w.options.min && w.value < w.options.min) w.value = w.options.min;
                        if (w.options.max && w.value > w.options.max) w.value = w.options.max;
                    } else if (event.type === "mousedown") {
                        let { values } = w.options;
                        if (values && values.constructor === Function) {
                            values = w.options.values(w, node);
                        }
                        let valuesList = [];

                        if (w.type !== "number") valuesList = values.constructor === Array ? values : Object.keys(values);

                        const delta = x < 40 ? -1 : x > widgetWidth - 40 ? 1 : 0;
                        if (w.type === "number") {
                            w.value += delta * 0.1 * (w.options.step || 1);
                            if (w.options.min != null && w.value < w.options.min) {
                                w.value = w.options.min;
                            }
                            if (w.options.max != null && w.value > w.options.max) {
                                w.value = w.options.max;
                            }
                        } else if (delta) { // clicked in arrow, used for combos
                            let index = -1;
                            this.last_mouseclick = 0; // avoids dobl click event
                            if (values.constructor === Object) {
                                const indexInValues = valuesList.indexOf(String(w.value));
                                index = (indexInValues === -1) ? 0 : indexInValues + delta;
                            } else {
                                index = valuesList.indexOf(w.value) + delta;
                            }
                            if (index >= valuesList.length) {
                                index = valuesList.length - 1;
                            }
                            if (index < 0) {
                                index = 0;
                            }
                            if (values.constructor === Array) {
                                w.value = values[index];
                            } else {
                                w.value = valuesList[index];
                                console.log(w.value);
                            }
                        } else { // combo clicked
                            const textValues = values !== valuesList
                                ? Object.keys(values)
                                : values;
                            const menu = new ContextMenu(textValues, {
                                scale: Math.max(1, this.ds.scale),
                                event,
                                className: "dark",
                                callback: (v) => {
                                    w.value = values != valuesList ? textValues.indexOf(v) : v;
                                    innerValueChange(w, v);
                                    this.dirty_canvas = true;
                                    return false;
                                },
                            },
                            refWindow);
                        }
                    } else if (event.type === "mouseup" && w.type === "number") {
                        const delta = x < 40 ? -1 : x > widgetWidth - 40 ? 1 : 0;
                        if (event.click_time < 200 && delta == 0) {
                            this.prompt("Value", w.value, (v) => {
                                w.value = Number(v);
                                innerValueChange(w, w.value);
                            }, event);
                        }
                    }

                    if (oldValue !== w.value) {
                        setTimeout(
                            () => {
                                innerValueChange(this, this.value);
                            },
                            20,
                        );
                    }
                    this.dirty_canvas = true;
                    break;
                case "toggle":
                    if (event.type === "mousedown") {
                        w.value = !w.value;
                        setTimeout(() => {
                            innerValueChange(w, w.value);
                        }, 20);
                    }
                    break;
                case "string":
                case "text":
                    if (event.type === "mousedown") {
                        this.prompt("Value", w.value, (v) => {
                            w.value = v;
                            innerValueChange(w, v);
                        }, event, w.options ? w.options.multiline : false);
                    }
                    break;
                default:
                    if (w.mouse) {
                        this.dirty_canvas = w.mouse(event, [x, y], node);
                    }
                    break;
            } // end switch

            // value changed
            if (oldValue !== w.value) {
                if (node.onWidgetChanged) node.onWidgetChanged(w.name, w.value, oldValue, w);
                if (node.graph.on_change) node.graph.on_change(node.graph);
                node.graph._version++;
            }

            return w;
        }

        const that = this;
        function innerValueChange(widget, value) {
            widget.value = value;
            if (widget.options && widget.options.property && node.properties[widget.options.property]) {
                node.setProperty(widget.options.property, value);
            }
            if (widget.callback) {
                widget.callback(widget.value, that, node, pos, event);
            }
            if (node.onWidgetChanged) node.onWidgetChanged(widget.name, widget.value, oldValue, widget);
            if (node.graph.on_change) node.graph.on_change(node.graph);
            node.graph._version++;
        }

        return null;
    }

    /**
     * draws every group area in the background
     * @method drawGroups
     * @memberOf LGraphCanvas
     * */
    drawGroups(canvas, ctx) {
        if (!this.graph) return;

        const groups = this.graph._groups;

        ctx.save();
        ctx.globalAlpha = 0.5 * this.editor_alpha;

        for (const group of groups) {
            if (!overlapBounding(this.visible_area, group._bounding)) {
                continue;
            } // out of the visible area

            ctx.fillStyle = group.color || "#335";
            ctx.strokeStyle = group.color || "#335";
            const pos = group._pos;
            const size = group._size;
            ctx.globalAlpha = 0.25 * this.editor_alpha;
            ctx.beginPath();
            ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], size[1]);
            ctx.fill();
            ctx.globalAlpha = this.editor_alpha;
            ctx.stroke();

            ctx.beginPath();
            ctx.moveTo(pos[0] + size[0], pos[1] + size[1]);
            ctx.lineTo(pos[0] + size[0] - 10, pos[1] + size[1]);
            ctx.lineTo(pos[0] + size[0], pos[1] + size[1] - 10);
            ctx.fill();

            const fontSize = group.font_size || defaultConfig.DEFAULT_GROUP_FONT_SIZE;
            ctx.font = `${fontSize}px Arial`;
            ctx.fillText(group.title, pos[0] + 4, pos[1] + fontSize);
        }

        ctx.restore();
    }

    adjustNodesSize() {
        const nodes = this.graph._nodes;
        for (const node of nodes) node.size = node.computeSize();
        this.setDirty(true, true);
    }

    /**
     * resizes the canvas to a given size, if no size is passed, then it tries to fill the
     * parentNode
     * @method resize
     * @memberOf LGraphCanvas
     * */
    resize(width, height) {
        if (!width && !height) {
            const parent = this.canvas.parentNode;
            width = parent.offsetWidth;
            height = parent.offsetHeight;
        }

        if (this.canvas.width === width && this.canvas.height === height) {
            return;
        }

        this.canvas.width = width;
        this.canvas.height = height;
        this.bgcanvas.width = this.canvas.width;
        this.bgcanvas.height = this.canvas.height;
        this.setDirty(true, true);
    }

    /**
     * switches to live mode (node shapes are not rendered, only the content)
     * this feature was designed when graphs where meant to create user interfaces
     * @method switchLiveMode
     * @memberOf LGraphCanvas
     * */
    switchLiveMode(transition) {
        if (!transition) {
            this.live_mode = !this.live_mode;
            this.dirty_canvas = true;
            this.dirty_bgcanvas = true;
            return;
        }

        const delta = this.live_mode ? 1.1 : 0.9;
        if (this.live_mode) {
            this.live_mode = false;
            this.editor_alpha = 0.1;
        }

        const t = setInterval(() => {
            this.editor_alpha *= delta;
            this.dirty_canvas = true;
            this.dirty_bgcanvas = true;

            if (delta < 1 && this.editor_alpha < 0.01) {
                clearInterval(t);
                if (delta < 1) {
                    this.live_mode = true;
                }
            }
            if (delta > 1 && this.editor_alpha > 0.99) {
                clearInterval(t);
                this.editor_alpha = 1;
            }
        }, 1);
    }

    /**
     * @method onNodeSelectionChange
     * @param node
     * @todo Need create event
     * @memberOf LGraphCanvas
     */
    onNodeSelectionChange(node) {
        // disabled
    }

    /**
     * @method touchHandler
     * @param {TouchEvent} event
     * @memberOf LGraphCanvas
     */
    touchHandler(event) {
        // alert("foo");
        const touches = event.changedTouches;
        const first = touches[0];
        let 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);

        const window = this.getCanvasWindow();
        const { document } = window;

        const 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();
    }

    /**
     * @method onGroupAdd
     * @param info
     * @param entry
     * @param {MouseEvent} mouseEvent
     * @memberOf LGraphCanvas
     */
    static onGroupAdd(info, entry, mouseEvent) {
        const canvas = LGraphCanvas.active_canvas;

        const group = new LGraphGroup();
        group.pos = canvas.convertEventToCanvasOffset(mouseEvent);
        canvas.graph.add(group);
    }

    static onMenuAdd(node, options, e, previousMenu, callback) {
        const canvas = LGraphCanvas.active_canvas;
        const refWindow = canvas.getCanvasWindow();
        const { graph } = canvas;
        if (!graph) return;

        function inner_onMenuAdded(base_category, prev_menu) {
            const categories = registry.getNodeTypesCategories(canvas.filter || graph.filter)
                .filter((category) => category.startsWith(base_category));
            const entries = [];

            categories.forEach((category) => {
                if (!category) {
                    return;
                }

                const base_category_regex = new RegExp(`^(${base_category})`);
                const category_name = category.replace(base_category_regex, "").split("/")[0];
                const category_path = base_category === "" ? `${category_name}/` : `${base_category + category_name}/`;

                let name = category_name;
                if (name.indexOf("::") != -1) {
                    name = name.split("::")[1];
                }

                const index = entries.findIndex((entry) => entry.value === category_path);
                if (index === -1) {
                    entries.push({
                        value: category_path,
                        content: name,
                        has_submenu: true,
                        callback(value, event, mouseEvent, contextMenu) {
                            inner_onMenuAdded(value.value, contextMenu);
                        },
                    });
                }
            });

            const nodes = registry.getNodeTypesInCategory(base_category.slice(0, -1), canvas.filter || graph.filter);
            nodes.forEach((node) => {
                if (node.skip_list) return;

                const entry = {
                    value: node.type,
                    content: node.title,
                    has_submenu: false,
                    callback(value, event, mouseEvent, contextMenu) {
                        const first_event = contextMenu.getFirstEvent();
                        canvas.graph.beforeChange();
                        const node = LGraphNode.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 ContextMenu(entries, {
                event: e,
                parentMenu: prev_menu,
            }, refWindow);
        }

        inner_onMenuAdded("", previousMenu);
        return false;
    }

    /**
     * @method onMenuCollapseAll
     * @todo Need create event
     * @memberOf LGraphCanvas
     */
    static onMenuCollapseAll() {
    }

    /**
     * @method onMenuNodeEdit
     * @todo Need create event
     * @memberOf LGraphCanvas
     */
    static onMenuNodeEdit() {
    }

    static showMenuNodeOptionalInputs(v, optionsParam, e, previousMenu, node) {
        if (!node) return;

        const that = this;
        const canvas = LGraphCanvas.active_canvas;
        const refWindow = canvas.getCanvasWindow();

        let options = node.optional_inputs;
        if (node.onGetInputs) options = node.onGetInputs();

        let entries = [];
        if (options) {
            for (const entry of options) {
                if (!entry) {
                    entries.push(null);
                    continue;
                }
                let label = entry[0];
                if (entry[2] && entry[2].label) {
                    label = entry[2].label;
                }
                const data = {
                    content: label,
                    value: entry,
                };
                if (entry[1] === defaultConfig.ACTION) {
                    data.className = "event";
                }
                entries.push(data);
            }
        }

        if (this.onMenuNodeInputs) entries = this.onMenuNodeInputs(entries);

        if (!entries.length) {
            console.log("no input entries");
            return;
        }

        const menu = new ContextMenu(
            entries,
            {
                event: e,
                callback: innerClicked,
                parentMenu: previousMenu,
                node,
            },
            refWindow,
        );

        function innerClicked(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]);
                node.setDirtyCanvas(true, true);
                node.graph.afterChange();
            }
        }

        return false;
    }

    static showMenuNodeOptionalOutputs(v, optionsParam, e, previousMenu, node) {
        if (!node) return;

        const that = this;
        const canvas = LGraphCanvas.active_canvas;
        const refWindow = canvas.getCanvasWindow();

        let options = node.optional_outputs;
        if (node.onGetOutputs) {
            options = node.onGetOutputs();
        }

        let entries = [];
        if (options) {
            for (const entry of options) {
                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
                let label = entry[0];
                if (entry[2] && entry[2].label) label = entry[2].label;
                const data = {
                    content: label,
                    value: entry,
                };
                if (entry[1] === defaultConfig.EVENT) data.className = "event";
                entries.push(data);
            }
        }

        if (this.onMenuNodeOutputs) entries = this.onMenuNodeOutputs(entries);

        if (!entries.length) return;

        const menu = new ContextMenu(
            entries,
            {
                event: e,
                callback: innerClicked,
                parentMenu: previousMenu,
                node,
            },
            refWindow,
        );

        function innerClicked(v, e, prev) {
            if (!node) return;

            if (v.callback) v.callback.call(that, node, v, e, prev);

            if (!v.value) {
                return;
            }

            const value = v.value[1];

            if (
                value
                && (value.constructor === Object || value.constructor === Array)
            ) {
                // submenu why?
                const entries = [];
                for (const i in value) {
                    entries.push({
                        content: i,
                        value: value[i],
                    });
                }
                new ContextMenu(entries, {
                    event: e,
                    callback: innerClicked,
                    parentMenu: previousMenu,
                    node,
                });
                return false;
            }
            node.graph.beforeChange();
            node.addOutput(v.value[0], v.value[1], v.value[2]);
            node.setDirtyCanvas(true, true);
            node.graph.afterChange();
        }

        return false;
    }

    static onShowMenuNodeProperties(value, options, e, previousMenu, node) {
        if (!node || !node.properties) {
            return;
        }

        const canvas = LGraphCanvas.active_canvas;
        const refWindow = canvas.getCanvasWindow();

        const entries = [];
        // eslint-disable-next-line
        for (const i in node.properties) {
            let value = node.properties[i] ? node.properties[i] : " ";
            if (typeof value === "object") value = JSON.stringify(value);
            const 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:
                    `<span class="property_name">${
                        info.label ? info.label : i
                    }</span>`
                    + `<span class="property_value">${
                        value
                    }</span>`,
                value: i,
            });
        }
        if (!entries.length) {
            return;
        }

        const menu = new ContextMenu(
            entries,
            {
                event: e,
                callback: innerClicked,
                parentMenu: previousMenu,
                allow_html: true,
                node,
            },
            refWindow,
        );

        function innerClicked(v) {
            if (!node) {
                return;
            }
            const rect = this.getBoundingClientRect();
            canvas.showEditPropertyValue(node, v.value, {
                position: [rect.left, rect.top],
            });
        }

        return false;
    }

    static decodeHTML(str) {
        const e = document.createElement("div");
        e.innerText = str;
        return e.innerHTML;
    }

    static onResizeNode(value, options, e, menu, node) {
        if (!node) return;
        node.size = node.computeSize();
        if (node.onResize) node.onResize(node.size);
        node.setDirtyCanvas(true, true);
    }

    showLinkMenu(link, e) {
        const that = this;
        const options = ["Add Node", null, "Delete"];
        const menu = new ContextMenu(options, {
            event: e,
            title: link.data != null ? link.data.constructor.name : null,
            callback: innerClicked,
        });

        function innerClicked(v, options, e) {
            switch (v) {
                case "Add Node":
                    LGraphCanvas.onMenuAdd(null, null, e, menu, (node) => {
                        console.log("node autoconnect");
                        const nodeLeft = that.graph.getNodeById(link.origin_id);
                        const nodeRight = that.graph.getNodeById(link.target_id);
                        if (!node.inputs
                            || !node.inputs.length
                            || !node.outputs
                            || !node.outputs.length) return;
                        if (nodeLeft.outputs[link.origin_slot].type === node.inputs[0].type && node.outputs[0].type === nodeRight.inputs[0].type) {
                            nodeLeft.connect(link.origin_slot, node, 0);
                            node.connect(0, nodeRight, link.target_slot);
                            node.pos[0] -= node.size[0] * 0.5;
                        }
                    });
                    break;
                case "Delete":
                    that.graph.removeLink(link.id);
                    break;
                default:
            }
        }

        return false;
    }

    static onShowPropertyEditor(item, options, e, menu, node) {
        const property = item.property || "title";
        const value = node[property];

        const dialog = document.createElement("div");
        dialog.className = "graphdialog";
        dialog.innerHTML = "<span class='name'></span><input autofocus type='text' class='value'/><button>OK</button>";

        const title = dialog.querySelector(".name");
        title.innerText = property;

        const input = dialog.querySelector(".value");
        if (input) {
            input.value = value;
            input.addEventListener("blur", (e) => {
                input.focus();
            });
            input.addEventListener("keydown", (e) => {
                if (e.keyCode !== 13 && e.target.localName !== "textarea") return;
                setValue(input.value);
                e.preventDefault();
                e.stopPropagation();
            });
        }

        const graphcanvas = LGraphCanvas.active_canvas;
        const { canvas } = graphcanvas;

        const rect = canvas.getBoundingClientRect();
        let offsetx = -20;
        let offsety = -20;
        if (rect) {
            offsetx -= rect.left;
            offsety -= rect.top;
        }

        if (e) {
            dialog.style.left = `${e.clientX + offsetx}px`;
            dialog.style.top = `${e.clientY + offsety}px`;
        } else {
            dialog.style.left = `${canvas.width * 0.5 + offsetx}px`;
            dialog.style.top = `${canvas.height * 0.5 + offsety}px`;
        }

        const button = dialog.querySelector("button");
        button.addEventListener("click", () => setValue(input.value));
        canvas.parentNode.appendChild(dialog);

        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.remove();
            }
            node.setDirtyCanvas(true, true);
        }
    }

    prompt(title = "", value, callback, event, multiline) {
        const that = this;

        let modified = false;

        const dialog = document.createElement("div");
        dialog.className = "graphdialog rounded";
        if (multiline) {
            dialog.innerHTML = "<span class='name'></span> <textarea autofocus class='value'></textarea><button class='rounded'>OK</button>";
        } else {
            dialog.innerHTML = "<span class='name'></span> <input autofocus type='text' class='value'/><button class='rounded'>OK</button>";
        }
        dialog.close = () => {
            this.prompt_box = null;
            if (dialog.parentNode) dialog.remove();
        };

        if (this.ds.scale > 1) {
            dialog.style.transform = `scale(${this.ds.scale})`;
        }

        dialog.addEventListener("mouseleave", (e) => {
            if (!modified) dialog.close();
        });

        if (this.prompt_box) {
            this.prompt_box.close();
        }
        this.prompt_box = dialog;

        const first = null;
        const timeout = null;
        const selected = null;

        const nameElement = dialog.querySelector(".name");
        nameElement.innerText = title;
        const valueElement = dialog.querySelector(".value");
        valueElement.value = value;

        const input = valueElement;
        input.addEventListener("keydown", (e) => {
            modified = true;
            if (e.keyCode === 27) dialog.close();
            else if (e.keyCode === 13 && e.target.localName !== "textarea") {
                if (callback) {
                    callback(input.value);
                }
                dialog.close();
            } else {
                return;
            }
            e.preventDefault();
            e.stopPropagation();
        });

        const button = dialog.querySelector("button");
        button.addEventListener("click", () => {
            if (callback) callback(input.value);
            this.setDirty(true);
            dialog.close();
        });

        const graphcanvas = LGraphCanvas.active_canvas;
        const { canvas } = graphcanvas;

        const rect = canvas.getBoundingClientRect();
        let offsetx = -20;
        let 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`;
        }

        canvas.parentNode.appendChild(dialog);
        setTimeout(() => input.focus(), 10);

        return dialog;
    }

    static search_limit = -1

    showSearchBox = function (event) {
        const that = this;
        const graphcanvas = LGraphCanvas.active_canvas;
        const { canvas } = graphcanvas;
        const rootDocument = canvas.ownerDocument || document;

        const dialog = document.createElement("div");
        dialog.className = "litegraph litesearchbox graphdialog rounded";
        dialog.innerHTML = "<span class='name'>Search</span> <input autofocus type='text' class='value rounded'/><div class='helper'></div>";
        dialog.close = () => {
            this.search_box = null;
            rootDocument.body.focus();
            rootDocument.body.style.overflow = "";

            setTimeout(() => {
                this.canvas.focus();
            }, 20); // important, if canvas loses focus keys wont be captured
            if (dialog.parentNode) {
                dialog.remove();
            }
        };

        let timeoutClose = null;

        if (this.ds.scale > 1) dialog.style.transform = `scale(${this.ds.scale})`;

        dialog.addEventListener("mouseenter", () => {
            if (timeoutClose) {
                clearTimeout(timeoutClose);
                timeoutClose = null;
            }
        });

        dialog.addEventListener("mouseleave", () => {
            // dialog.close();
            timeoutClose = setTimeout(() => dialog.close(), 500);
        });

        if (this.search_box) this.search_box.close();
        this.search_box = dialog;

        const helper = dialog.querySelector(".helper");

        let first = null;
        let timeout = null;
        let selected = null;

        const input = dialog.querySelector("input");
        if (input) {
            input.addEventListener("blur", () => input.focus());
            input.addEventListener("keydown", (e) => {
                if (e.keyCode === 38) {
                    // UP
                    changeSelection(false);
                } else if (e.keyCode === 40) {
                    // DOWN
                    changeSelection(true);
                } else if (e.keyCode === 27) {
                    // ESC
                    dialog.close();
                } else if (e.keyCode === 13) {
                    if (selected) {
                        select(selected.innerHTML);
                    } else if (first) {
                        select(first);
                    } else {
                        dialog.close();
                    }
                } else {
                    if (timeout) {
                        clearInterval(timeout);
                    }
                    timeout = setTimeout(refreshHelper, 10);
                    return;
                }
                e.preventDefault();
                e.stopPropagation();
                e.stopImmediatePropagation();
                return true;
            });
        }

        if (rootDocument.fullscreenElement) rootDocument.fullscreenElement.appendChild(dialog);
        else {
            rootDocument.body.appendChild(dialog);
            rootDocument.body.style.overflow = "hidden";
        }

        // compute best position
        const rect = canvas.getBoundingClientRect();

        const left = (event ? event.clientX : (rect.left + rect.width * 0.5)) - 80;
        const top = (event ? event.clientY : (rect.top + rect.height * 0.5)) - 20;
        dialog.style.left = `${left}px`;
        dialog.style.top = `${top}px`;

        // To avoid out of screen problems
        if (event.layerY > (rect.height - 200)) {
            helper.style.maxHeight = `${rect.height - event.layerY - 20}px`;
        }

        input.focus();

        function select(name) {
            if (name) {
                if (that.onSearchBoxSelection) {
                    that.onSearchBoxSelection(name, event, graphcanvas);
                } else {
                    const extra = defaultConfig.searchbox_extras[name.toLowerCase()];
                    if (extra) {
                        name = extra.type;
                    }

                    graphcanvas.graph.beforeChange();
                    const node = LGraphNode.createNode(name);
                    if (node) {
                        node.pos = graphcanvas.convertEventToCanvasOffset(
                            event,
                        );
                        graphcanvas.graph.add(node);
                    }

                    if (extra && extra.data) {
                        if (extra.data.properties) {
                            // eslint-disable-next-line
                            for (const i in extra.data.properties) {
                                node.addProperty(i, extra.data.properties[i]);
                            }
                        }
                        if (extra.data.inputs) {
                            node.inputs = [];
                            // eslint-disable-next-line
                            for (const i in extra.data.inputs) {
                                node.addOutput(
                                    extra.data.inputs[i][0],
                                    extra.data.inputs[i][1],
                                );
                            }
                        }
                        if (extra.data.outputs) {
                            node.outputs = [];
                            // eslint-disable-next-line
                            for (const i in extra.data.outputs) {
                                node.addOutput(
                                    extra.data.outputs[i][0],
                                    extra.data.outputs[i][1],
                                );
                            }
                        }
                        if (extra.data.title) node.title = extra.data.title;
                        if (extra.data.json) node.configure(extra.data.json);

                        graphcanvas.graph.afterChange();
                    }
                }
            }

            dialog.close();
        }

        function changeSelection(forward) {
            const prev = selected;
            if (selected) selected.classList.remove("selected");
            if (!selected) {
                selected = forward
                    ? helper.childNodes[0]
                    : helper.childNodes[helper.childNodes.length];
            } else {
                selected = forward
                    ? selected.nextSibling
                    : selected.previousSibling;
                if (!selected) selected = prev;
            }
            if (!selected) return;
            selected.classList.add("selected");
            selected.scrollIntoView({
                block: "end",
                behavior: "smooth",
            });
        }

        function refreshHelper() {
            timeout = null;
            let str = input.value;
            first = null;
            helper.innerHTML = "";
            if (!str) return;

            if (that.onSearchBox) {
                const list = that.onSearchBox(helper, str, graphcanvas);
                if (list) {
                    for (const l of list) addResult(l);
                }
            } else {
                let c = 0;
                str = str.toLowerCase();
                const filter = graphcanvas.filter || graphcanvas.graph.filter;

                // extras
                // eslint-disable-next-line
                for (const i in defaultConfig.searchbox_extras) {
                    const extra = defaultConfig.searchbox_extras[i];
                    if (extra.desc.toLowerCase().indexOf(str) === -1) {
                        continue;
                    }
                    const ctor = defaultConfig.registered_node_types[extra.type];
                    if (ctor && ctor.filter !== filter) continue;
                    addResult(extra.desc, "searchbox_extra");
                    if (LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit) {
                        break;
                    }
                }

                const keys = Object.keys(defaultConfig.registered_node_types); // types
                const filtered = keys.filter((type) => {
                    const ctor = defaultConfig.registered_node_types[type];
                    if (filter && ctor.filter !== filter) return false;
                    return type.toLowerCase().indexOf(str) !== -1;
                });

                for (const filteredItem of filtered) {
                    addResult(filteredItem);
                    if (LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit) {
                        break;
                    }
                }
            }

            function addResult(type, className) {
                const help = document.createElement("div");
                if (!first) first = type;
                help.innerText = type;
                help.dataset.type = escape(type);
                help.className = "litegraph lite-search-item";
                if (className) help.className += ` ${className}`;
                help.addEventListener("click", () => {
                    select(unescape(help.dataset.type));
                });
                helper.appendChild(help);
            }
        }

        return dialog;
    }

    showEditPropertyValue(node, property, options = {}) {
        if (!node || node.properties[property] === undefined) return;

        const info = node.getPropertyInfo(property);
        const { type } = info;

        let inputHTML = "";

        if (["sring", "number", "array", "object"].includes(type)) {
            inputHTML = "<input autofocus type='text' class='value'/>";
        } else if (["enum", "combo"].includes(type) && info.values) {
            inputHTML = "<select autofocus type='text' class='value'>";
            // eslint-disable-next-line
            for (const i in info.values) {
                let value = i;
                if (info.values.constructor === Array) value = info.values[i];

                inputHTML += `<option value="${value}" ${value == node.properties[property] ? "selected" : ""}>${info.values[i]}</option>`;
            }
            inputHTML += "</select>";
        } else if (type === "boolean") {
            inputHTML = `<input autofocus type="checkbox" class="value" ${
                node.properties[property] ? "checked" : ""
            }/>`;
        } else {
            console.warn(`unknown type: ${type}`);
            return;
        }

        const dialog = this.createDialog(
            `<span class="name">${
                info.label ? info.label : property
            }</span>${
                inputHTML
            }<button>OK</button>`,
            options,
        );

        if (["enum", "combo"].includes(type) && info.values) {
            const input = dialog.querySelector("select");
            input.addEventListener("change", (e) => {
                setValue(e.target.value);
            });
        } else if (type === "boolean") {
            const input = dialog.querySelector("input");
            if (input) {
                input.addEventListener("click", () => setValue(!!input.checked));
            }
        } else {
            const input = dialog.querySelector("input");
            if (input) {
                input.addEventListener("blur", () => { input.focus(); });

                let v = node.properties[property] ? node.properties[property] : "";
                if (type !== "string") {
                    v = JSON.stringify(v);
                }

                input.value = v;
                input.addEventListener("keydown", (e) => {
                    if (e.keyCode != 13) return;
                    setValue(input.value);
                    e.preventDefault();
                    e.stopPropagation();
                });
            }
        }

        const button = dialog.querySelector("button");
        button.addEventListener("click", () => setValue(input.value));
        function setValue(value) {
            if (info
                && info.values
                && info.values.constructor === Object
                && info.values[value]) value = info.values[value];

            if (typeof node.properties[property] === "number") {
                value = Number(value);
            }
            if (["array", "object"].includes(type)) {
                value = JSON.parse(value);
            }
            node.properties[property] = value;
            if (node.graph) {
                node.graph._version++;
            }
            if (node.onPropertyChanged) {
                node.onPropertyChanged(property, value);
            }
            if (options.onclose) options.onclose();
            dialog.close();
            node.setDirtyCanvas(true, true);
        }

        return dialog;
    }

    createDialog(html, options = {}) {
        const dialog = document.createElement("div");
        dialog.className = "graphdialog";
        dialog.innerHTML = html;

        const rect = this.canvas.getBoundingClientRect();
        let offsetx = -20;
        let offsety = -20;
        if (rect) {
            offsetx -= rect.left;
            offsety -= rect.top;
        }

        if (options.position) {
            offsetx += options.position[0];
            offsety += options.position[1];
        } else if (options.event) {
            offsetx += options.event.clientX;
            offsety += options.event.clientY;
        } // centered
        else {
            offsetx += this.canvas.width * 0.5;
            offsety += this.canvas.height * 0.5;
        }

        dialog.style.left = `${offsetx}px`;
        dialog.style.top = `${offsety}px`;

        this.canvas.parentNode.appendChild(dialog);

        dialog.close = () => {
            if (dialog.parentNode) dialog.remove();
        };

        return dialog;
    }

    createPanel(title, options = {}) {
        const refWindow = options.window || window;

        const root = document.createElement("div");
        root.className = "litegraph dialog";
        root.innerHTML = "<div class='dialog-header'><span class='dialog-title'></span></div><div class='dialog-content'></div><div class='dialog-footer'></div>";
        root.header = root.querySelector(".dialog-header");

        if (options.width) root.style.width = options.width + (options.width.constructor === Number ? "px" : "");
        if (options.height) root.style.height = options.height + (options.height.constructor === Number ? "px" : "");
        if (options.closable) {
            const close = document.createElement("span");
            close.innerHTML = "&#10005;";
            close.classList.add("close");
            close.addEventListener("click", () => root.close());
            root.header.appendChild(close);
        }
        root.title_element = root.querySelector(".dialog-title");
        root.title_element.innerText = title;
        root.content = root.querySelector(".dialog-content");
        root.footer = root.querySelector(".dialog-footer");

        root.close = () => root.remove();

        root.clear = () => root.content.innerHTML = "";

        root.addHTML = (code, classname, onFooter) => {
            const elem = document.createElement("div");
            if (classname) elem.className = classname;
            elem.innerHTML = code;
            if (onFooter) root.footer.appendChild(elem);
            else root.content.appendChild(elem);
            return elem;
        };

        root.addButton = (name, callback, options) => {
            const elem = document.createElement("button");
            elem.innerText = name;
            elem.options = options;
            elem.classList.add("btn");
            elem.addEventListener("click", callback);
            root.footer.appendChild(elem);
            return elem;
        };

        root.addSeparator = () => {
            const elem = document.createElement("div");
            elem.className = "separator";
            root.content.appendChild(elem);
        };

        root.addWidget = (type, name, value, options = {}, callback) => {
            type = type.toLowerCase();
            value = String(value);
            let strValue = type === "number" ? new Number(value).toFixed(3) : value.toString();
            const elem = document.createElement("div");
            elem.className = "property";
            elem.innerHTML = "<span class='property_name'></span><span class='property_value'></span>";
            elem.querySelector(".property_name").innerText = name;
            const valueElement = elem.querySelector(".property_value");
            valueElement.innerText = strValue;
            elem.dataset.property = name;
            elem.dataset.type = options.type || type;
            elem.options = options;
            elem.value = strValue;

            if (type === "boolean") {
                elem.classList.add("boolean");
                if (value) elem.classList.add("bool-on");
                elem.addEventListener("click", () => {
                    // var v = node.properties[this.dataset["property"]];
                    // node.setProperty(this.dataset["property"],!v); this.innerText = v ? "true" :
                    // "false";
                    const propname = elem.dataset.property;
                    this.value = !elem.value;
                    this.classList.toggle("bool-on");
                    this.querySelector(".property_value").innerText = elem.value ? "true" : "false";
                    innerChange(propname, elem.value);
                });
            } else if (["string", "number"].includes(type)) {
                valueElement.setAttribute("contenteditable", true);
                valueElement.addEventListener("keydown", (e) => {
                    if (e.code === "Enter") {
                        e.preventDefault();
                        valueElement.blur();
                    }
                });
                valueElement.addEventListener("blur", () => {
                    let v = valueElement.innerText;
                    const propname = valueElement.parentNode.dataset.property;
                    const proptype = valueElement.parentNode.dataset.type;
                    if (proptype === "number") v = Number(v);
                    innerChange(propname, v);
                });
            } else if (["enum", "combo"].includes(type)) strValue = LGraphCanvas.getPropertyPrintableValue(value, options.values);
            valueElement.innerText = strValue;

            valueElement.addEventListener("click", (event) => {
                const values = options.values || [];
                const propname = valueElement.parentNode.dataset.property;
                const menu = new ContextMenu(values, {
                    event,
                    className: "dark",
                    callback: (v, option, event) => {
                        this.innerText = v;
                        innerChange(propname, v);
                        return false;
                    },
                },
                refWindow);
            });

            root.content.appendChild(elem);

            function innerChange(name, value) {
                console.log("change", name, value);
                // that.dirty_canvas = true;
                if (options.callback) options.callback(name, value);
                if (callback) callback(name, value);
            }

            return elem;
        };

        return root;
    }

    static getPropertyPrintableValue(value, values) {
        if (!values) return String(value);
        if (values.constructor === Array) return String(value);

        if (values.constructor === Object) {
            let desc_value = "";
            for (const k in values) {
                if (values[k] !== value) continue;
                desc_value = k;
                break;
            }
            return `${String(value)} (${desc_value})`;
        }
    }

    showShowNodePanel = function (node) {
        window.SELECTED_NODE = node;
        let panel = document.querySelector("#node-panel");
        if (panel) panel.close();
        const refWindow = this.getCanvasWindow();
        panel = this.createPanel(node.title || "", {
            closable: true,
            window: refWindow,
        });
        panel.id = "node-panel";
        panel.node = node;
        panel.classList.add("settings");
        const that = this;
        const graphcanvas = this;

        const inner_refresh = () => {
            panel.content.innerHTML = ""; // clear
            panel.addHTML(`<span class="node_type">${node.type}</span><span class="node_desc">${node.constructor.desc || ""}</span><span class="separator"></span>`);

            panel.addHTML("<h3>Properties</h3>");

            for (const i in node.properties) {
                const value = node.properties[i];
                const info = node.getPropertyInfo(i);

                if (node.onAddPropertyToPanel && node.onAddPropertyToPanel(i, panel)) continue;

                panel.addWidget(info.widget || info.type, i, value, info, (name, value) => {
                    graphcanvas.graph.beforeChange(node);
                    node.setProperty(name, value);
                    graphcanvas.graph.afterChange();
                    graphcanvas.dirty_canvas = true;
                });
            }

            panel.addSeparator();

            if (node.onShowCustomPanelInfo) node.onShowCustomPanelInfo(panel);
            panel.addButton("Delete", () => {
                if (node.block_delete) return;
                node.graph.remove(node);
                panel.close();
            })
                .classList
                .add("delete");
        };

        function inner_showCodePad(node, propname) {
            panel.style.top = "calc( 50% - 250px)";
            panel.style.left = "calc( 50% - 400px)";
            panel.style.width = "800px";
            panel.style.height = "500px";

            panel.content.innerHTML = "<textarea class='code'></textarea>";
            const textarea = panel.content.querySelector("textarea");
            textarea.value = node.properties[propname];
            textarea.addEventListener("keydown", (e) => {
                if (e.code === "Enter" && e.ctrlKey) {
                    console.log("Assigned");
                    node.setProperty(propname, textarea.value);
                }
            });
            textarea.style.height = "calc(100% - 40px)";

            const assign = that.createButton("Assign", null, () => {
                node.setProperty(propname, textarea.value);
            });
            panel.content.appendChild(assign);
            const button = that.createButton("Close", null, () => {
                panel.style.height = "";
                inner_refresh();
            });
            button.style.float = "right";
            panel.content.appendChild(button);
        }

        inner_refresh();

        this.canvas.parentNode.appendChild(panel);
    }

    showSubgraphPropertiesDialog(node) {
        console.log("showing subgraph properties dialog");

        const old_panel = this.canvas.parentNode.querySelector(".subgraph_dialog");
        if (old_panel) old_panel.close();

        const panel = this.createPanel("Subgraph Inputs", {
            closable: true,
            width: 500,
        });
        panel.node = node;
        panel.classList.add("subgraph_dialog");

        function inner_refresh() {
            panel.clear();

            // show currents
            if (node.inputs) {
                for (const input of node.inputs) {
                    if (input.not_subgraph_input) continue;
                    const html = "<button>&#10005;</button> <span class='bullet_icon'></span><span class='name'></span><span class='type'></span>";
                    const elem = panel.addHTML(html, "subgraph_property");
                    elem.dataset.name = input.name;
                    elem.dataset.slot = i;
                    elem.querySelector(".name").innerText = input.name;
                    elem.querySelector(".type").innerText = input.type;
                    elem.querySelector("button")
                        .addEventListener("click", () => {
                            node.removeInput(Number(elem.parentNode.dataset.slot));
                            inner_refresh();
                        });
                }
            }
        }

        // add extra
        const html = " + <span class='label'>Name</span><input class='name'/><span class='label'>Type</span><input class='type'/><button>+</button>";
        const elem = panel.addHTML(html, "subgraph_property extra", true);
        elem.querySelector("button")
            .addEventListener("click", function (e) {
                const elem = this.parentNode;
                const name = elem.querySelector(".name").value;
                const type = elem.querySelector(".type").value;
                if (!name || node.findInputSlot(name) !== -1) return;
                node.addInput(name, type);
                elem.querySelector(".name").value = "";
                elem.querySelector(".type").value = "";
                inner_refresh();
            });

        inner_refresh();
        this.canvas.parentNode.appendChild(panel);
        return panel;
    }

    checkPanels() {
        if (!this.canvas) return;
        const panels = this.canvas.parentNode.querySelectorAll(".litegraph.dialog");
        for (const panel of panels) {
            if (!panel.node) continue;
            if (!panel.node.graph || panel.graph !== this.graph) panel.close();
        }
    }

    static onMenuNodeCollapse(value, options, e, menu, node) {
        node.graph.beforeChange(node);
        node.collapse();
        node.graph.afterChange(node);
    }

    static onMenuNodePin(value, options, e, menu, node) {
        node.pin();
    }

    static onMenuNodeMode = function (value, options, e, menu, node) {
        new ContextMenu(
            ["Always", "On Event", "On Trigger", "Never"],
            {
                event: e,
                callback: (v) => {
                    if (!node) {
                        return;
                    }
                    switch (v) {
                        case "On Event":
                            node.mode = defaultConfig.ON_EVENT;
                            break;
                        case "On Trigger":
                            node.mode = defaultConfig.ON_TRIGGER;
                            break;
                        case "Never":
                            node.mode = defaultConfig.NEVER;
                            break;
                        case "Always":
                        default:
                            node.mode = defaultConfig.ALWAYS;
                            break;
                    }
                },
                parentMenu: menu,
                node,
            },
        );
        return false;
    }

    static onMenuNodeColors(value, options, e, menu, node) {
        if (!node) throw new Error("no node for color");
        const values = [];
        values.push({
            value: null,
            content:
                "<span style='display: block; padding-left: 4px;'>No color</span>",
        });

        // eslint-disable-next-line
        for (const i in LGraphCanvas.node_colors) {
            const color = LGraphCanvas.node_colors[i];
            values.push({
                value: i,
                content: `<span style="display: block; color: #999; padding-left: 4px; border-left: 8px solid ${color.color}; background-color:${color.bgcolor}">${i}</span>`,
            });
        }
        new ContextMenu(values, {
            event: e,
            callback: (v) => {
                if (!node) {
                    return;
                }

                const color = v.value ? LGraphCanvas.node_colors[v.value] : null;
                if (color) {
                    if (node.constructor.name === "LGraphGroup") {
                        node.color = color.groupcolor;
                    } else {
                        node.color = color.color;
                        node.bgcolor = color.bgcolor;
                    }
                } else {
                    delete node.color;
                    delete node.bgcolor;
                }
                node.setDirtyCanvas(true, true);
            },
            parentMenu: menu,
            node,
        });

        return false;
    }

    static onMenuNodeShapes(value, options, e, menu, node) {
        if (!node) {
            throw new Error("no node passed");
        }

        new ContextMenu(defaultConfig.VALID_SHAPES, {
            event: e,
            callback: (v) => {
                if (!node) return;
                node.graph.beforeChange(node);
                node.shape = v;
                node.graph.afterChange(node);
                node.setDirtyCanvas(true);
            },
        }, {
            parentMenu: menu,
            node,
        });

        return false;
    }

    static onMenuNodeRemove(value, options, e, menu, node) {
        if (!node) throw new Error("no node passed");
        if (node.removable === false) return;

        const { graph } = node;
        graph.beforeChange();
        graph.remove(node);
        graph.afterChange();
        node.setDirtyCanvas(true, true);
    }

    static onMenuNodeToSubgraph(value, options, e, menu, node) {
        const { graph } = node;
        const graphcanvas = LGraphCanvas.active_canvas;
        if (!graphcanvas) return;

        let nodesList = Object.values(graphcanvas.selected_nodes || {});
        if (!nodesList.length) nodesList = [node];

        const subgraphNode = LGraphNode.createNode("graph/subgraph");
        subgraphNode.pos = node.pos.concat();
        graph.add(subgraphNode);

        subgraphNode.buildFromNodes(nodesList);

        graphcanvas.deselectAllNodes();
        node.setDirtyCanvas(true, true);
    }

    static onMenuNodeClone(value, options, e, menu, node) {
        if (node.clonable === false) return;
        const newnode = node.clone();
        if (!newnode) return;
        newnode.pos = [node.pos[0] + 5, node.pos[1] + 5];

        node.graph.beforeChange();
        node.graph.add(newnode);
        node.graph.afterChange();

        node.setDirtyCanvas(true, true);
    }

    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",
        },
    }

    getCanvasMenuOptions() {
        let options = null;
        if (this.getMenuOptions) {
            options = this.getMenuOptions();
        } else {
            options = [
                {
                    content: "Add Node",
                    has_submenu: true,
                    callback: LGraphCanvas.onMenuAdd,
                },
                {
                    content: "Add Group",
                    callback: LGraphCanvas.onGroupAdd,
                },
                // {content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll }
            ];

            if (this._graph_stack && this._graph_stack.length > 0) {
                options.push(null, {
                    content: "Close subgraph",
                    callback: this.closeSubgraph.bind(this),
                });
            }
        }

        if (this.getExtraMenuOptions) {
            const extra = this.getExtraMenuOptions(this, options);
            if (extra) options = options.concat(extra);
        }

        return options;
    }

    getNodeMenuOptions(node) {
        let options = null;

        if (node.getMenuOptions) options = node.getMenuOptions(this);
        else {
            options = [
                {
                    content: "Inputs",
                    has_submenu: true,
                    disabled: true,
                    callback: LGraphCanvas.showMenuNodeOptionalInputs,
                },
                {
                    content: "Outputs",
                    has_submenu: true,
                    disabled: true,
                    callback: LGraphCanvas.showMenuNodeOptionalOutputs,
                },
                null,
                {
                    content: "Properties",
                    has_submenu: true,
                    callback: LGraphCanvas.onShowMenuNodeProperties,
                },
                null,
                {
                    content: "Title",
                    callback: LGraphCanvas.onShowPropertyEditor,
                },
                {
                    content: "Mode",
                    has_submenu: true,
                    callback: LGraphCanvas.onMenuNodeMode,
                },
                {
                    content: "Resize",
                    callback() {
                        if (node.resizable) {
                            return LGraphCanvas.onResizeNode;
                        }
                    },
                },
                {
                    content: "Collapse",
                    callback: LGraphCanvas.onMenuNodeCollapse,
                },
                {
                    content: "Pin",
                    callback: LGraphCanvas.onMenuNodePin,
                },
                {
                    content: "Colors",
                    has_submenu: true,
                    callback: LGraphCanvas.onMenuNodeColors,
                },
                {
                    content: "Shapes",
                    has_submenu: true,
                    callback: LGraphCanvas.onMenuNodeShapes,
                },
                null,
            ];
        }

        if (node.onGetInputs) {
            const inputs = node.onGetInputs();
            if (inputs && inputs.length) options[0].disabled = false;
        }

        if (node.onGetOutputs) {
            const outputs = node.onGetOutputs();
            if (outputs && outputs.length) options[1].disabled = false;
        }

        if (node.getExtraMenuOptions) {
            const extra = node.getExtraMenuOptions(this, options);
            if (extra) {
                extra.push(null);
                options = extra.concat(options);
            }
        }

        if (node.clonable) {
            options.push({
                content: "Clone",
                callback: LGraphCanvas.onMenuNodeClone,
            });
        }

        options.push(null, {
            content: "Remove",
            disabled: !(node.removable !== false && !node.block_delete),
            callback: LGraphCanvas.onMenuNodeRemove,
        });

        if (node.graph && node.graph.onGetNodeMenuOptions) {
            node.graph.onGetNodeMenuOptions(options, node);
        }

        return options;
    }

    getGroupMenuOptions() {
        return [
            {
                content: "Title",
                callback: LGraphCanvas.onShowPropertyEditor,
            },
            {
                content: "Color",
                has_submenu: true,
                callback: LGraphCanvas.onMenuNodeColors,
            },
            {
                content: "Font size",
                property: "font_size",
                type: "Number",
                callback: LGraphCanvas.onShowPropertyEditor,
            },
            null,
            {
                content: "Remove",
                callback: LGraphCanvas.onMenuNodeRemove,
            },
        ];
    }

    processContextMenu(node, event) {
        const that = this;
        const canvas = LGraphCanvas.active_canvas;
        const refWindow = canvas.getCanvasWindow();

        let menuInfo = null;
        const options = {
            event,
            callback: inner_option_clicked,
            extra: node,
        };

        if (node) options.title = node.type;

        // check if mouse is in input
        let slot = null;
        if (node) {
            slot = node.getSlotInPosition(event.canvasX, event.canvasY);
            LGraphCanvas.active_node = node;
        }

        if (slot) {
            // on slot
            menuInfo = [];
            if (node.getSlotMenuOptions) menuInfo = node.getSlotMenuOptions(slot);
            else {
                if (slot && slot.output && slot.output.links && slot.output.links.length) {
                    menuInfo.push({
                        content: "Disconnect Links",
                        slot,
                    });
                }
                const _slot = slot.input || slot.output;
                menuInfo.push(_slot.locked ? "Cannot remove" : { content: "Remove Slot", slot });
                menuInfo.push(_slot.nameLocked ? "Cannot rename" : { content: "Rename Slot", slot });
            }
            options.title = (slot.input ? slot.input.type : slot.output.type) || "*";
            if (slot.input && slot.input.type === defaultConfig.ACTION) options.title = "Action";
            if (slot.output && slot.output.type === defaultConfig.EVENT) options.title = "Event";
        } else if (node) {
            menuInfo = this.getNodeMenuOptions(node);
        } else {
            menuInfo = this.getCanvasMenuOptions();
            const group = this.graph.getGroupOnPos(event.canvasX, event.canvasY);
            if (group) {
                // on group
                menuInfo.push(null, {
                    content: "Edit Group",
                    has_submenu: true,
                    submenu: {
                        title: "Group",
                        extra: group,
                        options: this.getGroupMenuOptions(group),
                    },
                });
            }
        }

        // show menu
        if (!menuInfo) return;

        const menu = new ContextMenu(menuInfo, options, refWindow);

        function inner_option_clicked(v, options, e) {
            if (!v) {
                return;
            }

            if (v.content === "Remove Slot") {
                const info = v.slot;
                if (info.input) node.removeInput(info.slot);
                else if (info.output) node.removeOutput(info.slot);
            } else if (v.content === "Disconnect Links") {
                const info = v.slot;
                if (info.output) node.disconnectOutput(info.slot);
                else if (info.input) node.disconnectInput(info.slot);
            } else if (v.content === "Rename Slot") {
                const info = v.slot;
                const slotInfo = info.input
                    ? node.getInputInfo(info.slot) : node.getOutputInfo(info.slot);
                const dialog = that.createDialog(
                    "<span class='name'>Name</span><input autofocus type='text'/><button>OK</button>",
                    options,
                );
                const input = dialog.querySelector("input");
                if (input && slotInfo) input.value = slotInfo.label || "";
                dialog.querySelector("button")
                    .addEventListener("click", () => {
                        if (input.value) {
                            if (slotInfo) slotInfo.label = input.value;
                            that.setDirty(true);
                        }
                        dialog.close();
                    });
            }
        }
    }
}