Source

index.js

/**
 * Base class for every component
 * @class
 * @classDesc The base class for every "complex" component, doing some of the trivial work.
 */
class Component {
  /**
   * @description Initiates a new component
   *
   * @param  {number} x       X position in the parent SVG
   * @param  {number} y       Y position in the parent SVG
   * @param  {number} width   Height, for calculations
   * @param  {number} height  Width, for calculations
   * @param  {number} scale=1 The scale of the values
   * @return {Component}      The component object
   */
  constructor(x, y, width, height, scale = 1) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.scale = scale;
    this.scaleCoords = this.scaleCoords;
    this.isComponent = true;
    this.ox = this.x; // store original x and y
    this.oy = this.y;
    this.renderContainer = null;

    this.elements = []; // the array containing every sub-element of this component:
    /*
    structure:
    [
      {
        element: a variable containing the element, can be anything
        render: a function that gets called to create the html element. Must return a valid HTML Node; parameter: the element that will be rendered
      }
    ]
    */

    this.container = document.createElementNS("http://www.w3.org/2000/svg", "svg"); // the container
    this.container.style.overflow = "visible";
    this.updateAttributes();

    return this;
  }
  /**
   * @description Makes the component invisible. Interactions shouldn't be possible anymore.
   *
   * @return {void}
   */
  hide() {
    this.container.style.display = "none";
  }
  /**
   * @description Reverts [hide]{@link Component#hide}
   *
   * @return {void}
   */
  show() {
    this.container.style.display = "initial";
  }

  /**
   * @description   Updates the attributes of the SVG element in the DOM
   *
   * @return {void} Void
   */
  updateAttributes() { // variable attributes
    this.tw = this.width * this.scale; // true width: the width when the proper scale is applied
    this.th = this.height * this.scale; // same as tw but with height

    /*if (this.scaleCoords) { // do the same for coords like for height if scalable
      this.x = this.ox * this.scale;
      this.y = this.oy * this.scale;
    } else {
      this.x = this.ox;
      this.y = this.oy;
    }*/
    this.bbox = this.container.getBBox();
    // only apply the viewBox attribute when the element is rendered
    let w = this.bbox.width * this.scale;
    let h = this.bbox.height * this.scale;
    this.container.setAttribute("width", w);
    this.container.setAttribute("height", h);
    if (this.container.getAttribute("width") == 0) this.container.removeAttribute("width");
    if (this.container.getAttribute("height") == 0) this.container.removeAttribute("height");
    if (this.container.parentElement) this.container.setAttribute("viewBox", "0 0 " + this.bbox.width + " " + this.bbox.height)
    this.container.setAttribute("preserveAspectRatio", "xMinYMin slice");

    this.container.setAttribute("x", this.x);
    this.container.setAttribute("y", this.y);
  }
  updateChildren() {
    this.elements.forEach((elem) => {
      (!elem.update) ? elem.element.update(this) : elem.update(elem.element, this);
    });
  }

  outline() {
    /*
    <filter id="inset" x="-50%" y="-50%" width="200%" height="200%">
      <feFlood flood-color="white" result="outside-color"></feFlood>
      <feMorphology in="SourceAlpha" operator="dilate" radius="1"></feMorphology>
      <feComposite in="outside-color" operator="in" result="outside-stroke"></feComposite>

      <feFlood flood-color="white" result="inside-color"></feFlood>
      <feComposite in2="SourceAlpha" operator="in" result="inside-stroke"></feComposite>

      <feMorphology in="SourceAlpha" radius="2"></feMorphology>
      <feComposite in="SourceGraphic" operator="in" result="fill-area"></feComposite>

      <feMerge>
        <feMergeNode in="outside-stroke"></feMergeNode>
        <feMergeNode in="inside-stroke"></feMergeNode>
        <feMergeNode in="fill-area"></feMergeNode>
      </feMerge>
    </filter>
    */
  }


  /**
   * @description      Moves the svg container to the top of the parent. The parent has to be specified earlier by setting .renderContainer to an HTML element
   *
   * @return {boolean} Wether moving could be done or not. Returns false, if renderContainer isn't set.
   */
  moveToTop() { // moves the current component to the top of the container
    if (!this.renderContainer) return false;
    if (this.renderContainer.querySelector("#connectors")) {
      // component is inside a svgengine
      const connectorGroup = this.renderContainer.querySelector("#connectors");
      this.renderContainer.insertBefore(this.container, connectorGroup);
      return true;
    } else {
      ((this.renderContainer.querySelector(".nodes")) ? this.renderContainer.querySelector(".nodes") : this.renderContainer).appendChild(this.container);
      return true;
    }
  }


  /**
   * @description Change the coordinates in the parent svg element
   *
   * @param  {object} pos   The new position object
   * @param  {number} pos.x X coordinate
   * @param  {number} pos.y Y coordinate
   * @return {object}       The new position
   */
  setPosition(pos) {
    this.x = pos.x;
    this.y = pos.y;
    this.updateAttributes();
    return pos;
  }

  /**
   * @description Puts together the HTML element with all it's sub element, ready to be added to a parent.
   *
   * @param  {HTMLElement} c=null (Optional) Equivalent to setting the .renderContainer property.
   * @return {HTMLElement}        Returns the HTML element, containing all the children.
   */
  createSVGElement(c = null) { // create the whole svg element and return it
    this.renderContainer = c;
    this.container.innerHTML = "";
    this.elements.forEach((elem) => { // loop through each sub-element
      const rendered = (!elem.render) ? elem.element.createSVGElement(elem.element) : elem.render(elem.element); // and call the render function
      if (Array.isArray(rendered)) {
        this.container.append(...rendered);
      } else {
        this.container.append(rendered);
      }
    });
    return this.container;
  }
  attachEngine(e) {
    this.parentSVGEngine = e;
  }

  /**
   *  @typedef {Object} Position
   *  @property {number} x - The X position of the top left corner
   *  @property {number} y - The Y position of the top left corner
   */

  /**
   * @description Move the component relative to its current position.
   *
   * @param  {number} deltaX The amount of
   * @param  {number} deltaY description
   * @return {Position}        The new position
   */
  move(deltaX, deltaY) {
    if ((!deltaX && deltaX !== 0) || ((!deltaY && deltaY !== 0))) throw "Invalid parameters! [" + this.constructor.name + ".move(deltaX, deltaY)]";
    this.x += deltaX;
    this.y += deltaY;
    this.updateAttributes();
    return { x: this.x, y: this.y };
  }

  /**
   * @description Scales the component and its children.
   *
   * @deprecated Broken in many ways, use .setViewboxScale() instead.
   * @param  {number} newScale The new scale of the component
   * @return {number}          The new scale.
   */
  setComponentScale(newScale) {
    if (!newScale) throw "Scale cannot be empty! [" + this.constructor.name + ".setComponentScale()]";
    const ratio = newScale / this.scale; // how everything has to be scaled
    if (Number.isNaN(ratio)) throw "Ratio has to be a number. [" + this.constructor.name + ".setComponentScale()]";
    this.elements.forEach((e) => {
      const el = (Array.isArray(e.element)) ? e.element : [e.element];
      el.forEach(e => {
        if (e.setComponentScale) e.setComponentScale(((e.scale) ? e.scale : this.scale) * ratio);
        if (!e.move) return;
        let dX = e.x * ratio - e.x;
        let dY = e.y * ratio - e.y;
        e.move(dX, dY);
      });
    });
    this.scale = newScale;
    this.updateAttributes();
    return this.scale;
  }

  /**
   * @description Change the scale of the element using the viewBox property.
   *
   * @param  {number} newScale The new scale of the element
   * @return {void}
   */
  setViewboxScale(newScale) {
    this.scale = newScale;
    this.updateAttributes();
  }
}
class Transform {
  static vXAnchor = {
    CENTER: "Mid",
    LEFT: "Min",
    RIGHT: "Max"
  }
  static vYAnchor = {
    MIDDLE: "Mid",
    TOP: "Min",
    BOTTOM: "Max"
  }
}
/**
 * @class
 * @classdesc The viewport object is one of the basic objects beside, circle,
 * rectangle and similar. You can use this to group elements together in a new
 * viewport and new coordinates for every sub-component.
 */
class Viewport {
  /**
   * @description Initiates the Viewport object
   *
   * @param  {number} x The x position of the viewport in the parent.
   * @param  {number} y The y position of the viewport in the parent.
   * @param  {number} scale=1 This value is only used for scaling
   * @returns {Viewport}
   * @constructor
   */
  constructor(x, y, scale=1) {
    this.x = x;
    this.y = y;
    this.scale = scale;

    this.components = [];

    this.vxa = Transform.vXAnchor.LEFT;
    this.vya = Transform.vYAnchor.TOP;

    this.container = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    this.container.style.overflow = "visible";
    this.updateAttributes();

    return this;
  }

  /**
   * @description Set the position of the viewport in the parent. This will move
   * the children as well.
   *
   * @param  {object} pos The new position of the viewport.
   * @param  {number} pos.x The new x position.
   * @param  {number} pos.y The new y position.
   * @return {void}
   */
  setPosition(pos) {
    this.x = pos.x;
    this.y = pos.y;
    this.updateAttributes();
  }
  setScale(s) {
    this.scale = s;
    this.components.forEach(c => {
      if (c.setScale) c.setScale(s);
    });
  }
  setComponentScale(newScale) {
    if (!newScale) throw "Scale cannot be empty! [" + this.constructor.name + ".setComponentScale()]";
    const ratio = newScale / this.scale; // how everything has to be scaled
    if (Number.isNaN(ratio)) throw "Ratio has to be a number. [" + this.constructor.name + ".setComponentScale()]";
    this.components.forEach((e) => {
      const el = (Array.isArray(e.element)) ? e.element : [e.element];
      el.forEach(e => {
        if (e.setComponentScale) e.setComponentScale(((e.scale) ? e.scale : this.scale) * ratio);
        if (!e.move) return;
        let dX = e.x * ratio - e.x;
        let dY = e.y * ratio - e.y;
        e.move(dX, dY);
      });
    });
    this.scale = newScale;
    this.updateAttributes();
    return this.scale;
  }
  /**
   * @description Change the scale of the element using the viewBox property.
   *
   * @param  {number} newScale The new scale of the element
   * @return {void}
   */
  setViewboxScale(newScale) {
    this.scale = newScale;
    this.updateAttributes();
  }
  /**
   * @description Move the component relative to its current position.
   *
   * @param  {number} deltaX The amount of
   * @param  {number} deltaY description
   * @return {Position}        The new position
   */
  move(deltaX, deltaY) {
    if ((!deltaX && deltaX !== 0) || ((!deltaY && deltaY !== 0))) throw "Invalid parameters! [" + this.contuctor.name + ".move(deltaX, deltaY)]";
    this.x += deltaX;
    this.y += deltaY;
    this.updateAttributes();
    return { x: this.x, y: this.y };
  }

  /**
   * @description Add a component to the viewport that will be rendered inside.
   *
   * @param {(Component|object)} c The component to add.
   * @param {function}           render=(el)=>el.createSVGElement() The function
   * that will be called to render the component. It has to return an HTMLElement
   * of any type.
   * @returns {void}
   */
  addComponent(c, render = (el) => el.createSVGElement()) {
    this.components.push({ element: c, render: render });
  }
  updateAttributes() {
    this.bbox = this.container.getBBox();
    if (this.container.parentElement) this.container.setAttribute("viewBox", "0 0 " + this.bbox.width + " " + this.bbox.height)
    this.container.setAttribute("width", this.bbox.width * this.scale);
    this.container.setAttribute("height", this.bbox.height * this.scale);
    if (this.container.getAttribute("width") == 0) this.container.removeAttribute("width");
    if (this.container.getAttribute("height") == 0) this.container.removeAttribute("height");
    this.container.setAttribute("preserveAspectRatio", "x" + this.vxa + "Y" + this.vya + " slice");

    this.container.setAttribute("x", this.x);
    this.container.setAttribute("y", this.y);
  }
  createSVGElement() {
    this.container.innerHTML = "";
    this.components.forEach((elem) => { // loop through each sub-element
      const rendered = (!elem.render) ? elem.element.createSVGElement(elem.element) : elem.render(elem.element); // and call the render function
      if (Array.isArray(rendered)) {
        this.container.append(...rendered);
      } else {
        this.container.append(rendered);
      }
    });
    return this.container;
  }
  setViewboxAnchor(vXAnchor=Transform.vXAnchor.LEFT, vYAnchor=Transform.vYAnchor.TOP) {
    this.vxa = vXAnchor;
    this.vya = vYAnchor;
    this.updateAttributes();
  }
}
class Group { // somehow the equivalent to the <g> element
  constructor() {
    this.components = [];

    this.container = document.createElementNS("http://www.w3.org/2000/svg", "g");

    return this;
  }
  setPosition(pos) {
    this.components.forEach((c) => {
      c.setPosition(pos);
    });
  }
  setScale(s) {
    this.components.forEach((c) => {
      c.setScale(s);
    });
  }
  addComponent(c, render = (el) => el.createSVGElement()) {
    this.components.push(c);
    this.container.appendChild(render(c));
  }
  createSVGElement() {
    return this.container;
  }
}
class UserInteractionManager {
  constructor() { // an instance of this is stored in the window object. See SVGEngine
    this.rclick = [];

    window.addEventListener("contextmenu", (e) => {
      let el = document.elementFromPoint(e.pageX - window.pageXOffset, e.pageY - window.pageYOffset);
      let cancel = false;
      this.rclick.filter(o => o.el == el || o.el == "global").forEach(o => {
        if (o.listener) o.listener(e);
        cancel = o.cctx || cancel;
      });
      if (cancel) e.preventDefault(); // cancel context menu
      if (cancel) return false; // browsers are weird
    });

    return this;
  }
  cancelCtxMenu(el) {
    this.rclick.push({ el, listener: null, cctx: true });
  }
  rightClickListener(el, l, cancelCtxMenu=false) {
    this.rclick.push({ el, listener: l, cctx: cancelCtxMenu });
  }
  initListeners(el, onStart, onMove, onEnd, rcl=false, maxTouches=Number.POSITIVE_INFINITY, reuseTouches=false) { // TODO: fix bugs appearing when using more than one finger
    // mouse listeners
    var isRightClick = (e) => {
      e = e || window.event;
      return (e.which == 3) || (e.button == 2);
      /*if ("which" in e) { // Gecko (Firefox), WebKit (Safari/Chrome) & Opera
        return e.which == 3;
      } else if ("button" in e) { // IE, Opera
        return e.button == 2;
      }*/
    }
    el.addEventListener("pointerdown", function(e) {
      if (e.pointerType == "touch") return;
      if (!rcl) {return onStart.call(this, e);}
      if (isRightClick(e)) onStart.call(this, e);
    });
    el.addEventListener("pointermove", function(e) {
      if (e.pointerType == "touch") return;
      if (!rcl) return onMove.call(this, e);
      if (isRightClick(e)) onStart.call(this, e);
    });
    el.addEventListener("pointerup", function(e) {
      if (e.pointerType == "touch") return;
      if (!rcl) return onEnd.call(this, e);
      if (isRightClick(e)) onEnd.call(this, e);
    });

    //if (rcl) return; // no right click on touch

    // touch implementation
    function prevent(e) {
      if (e.cancelable) {
        e.preventDefault();
        return true;
      }
      return false;
    }
    const currTouches = [];
    el.addEventListener("touchstart", (e) => {
      prevent(e);
      if (!window.OpenVSTouches) window.OpenVSTouches = [];
      const touches = e.changedTouches;
      for (var i = 0; i < touches.length; i++) {
        if (currTouches.length >= maxTouches) continue;
        if (!reuseTouches && maxTouches != Number.POSITIVE_INFINITY) {
          if (window.OpenVSTouches.findIndex(el => el.identifier == touches[i].identifier) != -1) return;
          currTouches.push(touches[i]);
          onStart(touches[i]);
          window.OpenVSTouches.push(touches[i]);
          return;
        }
        currTouches.push(touches[i]);
        window.OpenVSTouches.push(touches[i]);
        onStart(touches[i]);
      }
    });
    el.addEventListener("touchcanel", (e) => {
      prevent(e);
      const touches = e.changedTouches;
      for (var i = 0; i < touches.length; i++) {
        const touch = touches[i];
        const pos = currTouches.findIndex(el => el.identifier == touch.identifier);
        if (pos >= 0) currTouches.splice(pos, 1);
        if (pos >= 0) onEnd(e);
        const p = window.OpenVSTouches.findIndex(el => el.identifier == touch.identifier);
        if (p >= 0) window.OpenVSTouches.splice(p, 1);
      }
    });
    el.addEventListener("touchend", (e) => {
      prevent(e);
      const touches = e.changedTouches;
      for (var i = 0; i < touches.length; i++) {
        const touch = touches[i];
        const pos = currTouches.findIndex(el => el.identifier == touch.identifier);
        if (pos >= 0) currTouches.splice(pos, 1);
        if (pos >= 0) onEnd(e);
        const p = window.OpenVSTouches.findIndex(el => el.identifier == touch.identifier);
        if (p >= 0) window.OpenVSTouches.splice(p, 1);
      }
    });
    el.addEventListener("touchmove", (e) => {
      prevent(e);
      const touches = e.changedTouches;
      for (var i = 0; i < touches.length; i++) {
        const touch = touches[i];
        const pos = currTouches.findIndex(el => el.identifier == touch.identifier);
        const event = {};
        for (var key in touch) {
          event[key] = touch[key];
        }
        const movementX = touch.clientX - currTouches[pos].clientX;
        const movementY = touch.clientY - currTouches[pos].clientY;
        event.movementX = movementX;
        event.movementY = movementY;
        if (pos >= 0) {
          currTouches.splice(pos, 1, touch);
        } else {
          currTouches.push(touch);
        }
        if (pos >= 0) onMove(event);
      }
    });
  }
}

/**
 * @class
 * @classdesc The component creating the output dots, that can be used to connect to input sockets.
 * @augments  Component
 */

/**
 * The type used for OutputPlugComponents
 * @typedef {(
 *  OutputPlugComponent.Type.BOOLEAN |
 *  OutputPlugComponent.Type.INTEGER |
 *  OutputPlugComponent.Type.STRING |
 *  OutputPlugComponent.Type.CONNECTOR |
 *  OutputPlugComponent.Type.ANY
 *  )} OutputPlugComponentType
 */
class OutputPlugComponent extends Component {

  /**
   * Enum for Plug types; (BOOLEAN|NUMBER|INTEGER|CONNECTOR|ANY)
   * @enum {string}
   * @constant
   */
  static Type = {
    BOOLEAN: "bool",
    NUMBER: "num",
    INTEGER: "int",
    CONNECTOR: "connect",
    ANY: "any",
    FLOAT: "float",
    ARRAY: "arr",
    STRING: "str"
  }
  static ColorMapping = {
    bool: "#a44747",
    connect: "#ffffff",
    num: "#427fbd",
    int: "#427fbd",
    str: "#daa520",
    any: "transparent"
  }
  static TypeLabel = {
    bool: "BOOL",
    num: "NUM",
    int: "INT",
    str: "STR",
    any: ""
  }
  static ConnectorColor = {
    bool: "#a44747",
    connect: "#808080",
    num: "#427fbd",
    int: "#427fbd",
    str: "#daa520",
    any: ""
  }

  /**
   * @description Initiate the OututPlugComponent object.
   *
   * @constructs
   * @see    {@link Component.constructor}  For the base parameters (x, y, ...)
   * @param  {OutputPlugComponentType} type The type of the plug. All of the outgoing connectors will only connect to compatible sockets. Also changes in design. See OutputPlugComponent.Type for the types.
   * @param  {object} engine                The SVGEngine object that contains this plug.
   * @param  {object} node                  The object type of Node, this plug is attached to.
   * @param  {string} styleType=""          The style of the Connector. See the Connector class for more details.
   * @param  {string} label=""              The label of the plug.
   * @return {object}                       The OutputPlugComponent object.
   */
  constructor(x, y, width, height, scale, type, engine, node, styleType = "", label = "") {
    super(x, y, width, height, scale);

    this.styleType = styleType; // the style of the connector like Bezier, or Line
    this.type = type; // the type of the plug
    this.node = node; // the node the plug is attached to
    this.connected = false;
    this.interactions = window.userInteractionManager; // user interactions
    this.label = label;

    this.minConDist = 50; // the minimum distance to snap

    this.container.setAttribute("OpenVS-Node-Id", this.node.id);

    this.plugPos = { // center position of the circle
      x: 20 * this.scale + 8 * this.scale,
      y: 8 * this.scale
    }
    this.parentSVGEngine = engine;
    this.connected = [];

    this.color = OutputPlugComponent.ColorMapping[type];

    this.oCircle = new Circle(20 * this.scale, 0, 8 * this.scale, true, this.scale); // the white circle
    this.oCircle.setColor(this.color);
    this.initConnector();
    this.elements.push({ element: this.oCircle, render: (el) => el.createSVGElement() });

    this.eventElem = document.createElement("span");

    if (type !== OutputPlugComponent.Type.CONNECTOR) {
      this.initType();
      return this;
    }

    this.oT = new RoundedTriangleComponent(0, 2.5 * this.scale, 90, 1 * this.scale); // the white triangle
    this.oT.setColor("white");
    this.elements.push({ element: this.oT, render: (el) => el.createSVGElement() });

    return this;
  }
  addEventListener(event, cb) {
    return this.eventElem.addEventListener(event, (e) => {
      cb(e.detail);
    });
  }
  emit(event, data) {
    return this.eventElem.dispatchEvent(new CustomEvent(event, { detail: data }));
  }
  initType() {
    this.oCircle.setPosition({
      x: 20 * this.scale,
      y: 0
    });

    let metrics = Text.measureText(this.label);
    this.text = new Text(13 * this.scale, (2.5) * this.scale, this.label, this.scale, Text.Anchor.END, Text.VerticalAnchor.TOP);
    this.text.setColor("white");
    this.elements.push({ element: this.text });

    //let typeMetrics = Text.measureText(OutputPlugComponent.TypeLabel[this.type]);
    this.typeLabel = new Text((13 - metrics.width) * this.scale, (2) * this.scale, OutputPlugComponent.TypeLabel[this.type], this.scale, Text.Anchor.END, Text.VerticalAnchor.TOP);
    this.typeLabel.container.style.fontSize = (9 * this.scale) + "px";
    this.typeLabel.setColor(this.color);
    this.elements.push({ element: this.typeLabel });
  }

  /**
   * @description Change the type of the plug after initialization
   *
   * @param  {OutputPlugComponentType} type Type to set the plug to.
   * @return {void}
   */
  setType(type) {
    this.type = type;
    this.color = OutputPlugComponent.ColorMapping[this.type];
    this.typeLabel.setColor(this.color);
    this.typeLabel.setText(OutputPlugComponent.TypeLabel[this.type]);
    this.oCircle.setColor(this.color);
  }

  /**
   * @description Set the opacity of this plug (every child element included).
   *
   * @param  {(string|float|number)} o The CSS opacity
   * @return {void}
   */
  setOpacity(o) {
    this.opacity = o;
    this.container.style.opacity = o;
  }
  attachEngine(e) {
    this.parentSVGEngine = e;
  }
  getAbsCoords(elem) {
    const box = elem.getBoundingClientRect();

    const body = document.body;
    const docEl = document.documentElement;

    const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
    const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;

    const clientTop = docEl.clientTop || body.clientTop || 0;
    const clientLeft = docEl.clientLeft || body.clientLeft || 0;

    const top = box.top + scrollTop - clientTop;
    const left = box.left + scrollLeft - clientLeft;

    return { x: left, y: top };
  }

  /**
   * @description Get the distance between two points in a 2D system.
   *
   * @param  {number} x  X coordinate of the first point
   * @param  {number} y  Y coordinate of the first point
   * @param  {number} x1 X coordinate of the second point
   * @param  {number} y1 Y coordinate of the second point
   * @return {number}    The distance between the two points
   */
  distance(x, y, x1, y1) { // get the distance between two points x,y and x1,y1
    return Math.sqrt(Math.pow(x - x1, 2) + Math.pow(y - y1, 2));
  }

  /**
   * @description Set the type of newly created connectors
   *
   * @param  {string} type The connector type. See the connector class for possible options.
   * @return {void}
   */
  setConnectorType(type) {
    this.styleType = type;
  }
  initSnapping() {
    this.sockets = this.getSockets();
    this.connectables = [];
    this.sockets.forEach(socket => {
      const s = socket.cCircle; // connection element
      const abs = this.getAbsCoords(s.container);
      const coords = {
        x: abs.x - this.parentSVGEngine.left,
        y: abs.y - this.parentSVGEngine.top
      };
      this.connectables.push({
        socket: socket,
        coords: coords,
        distance: null
      });
    });
  }

  /**
   * @description Creates a collection of suitable sockets for connector
   * connections by searching recursively through the elements inside the parent
   * SVG engine object declared on initialization.
   *
   * @return {Array.<InputSocketComponent>} The collected sockets.
   */
  getSockets() {
    const sockets = [];
    const check = (component) => {
      if (!Array.isArray(component)) return;
      component.forEach(c => {
        if (!(c instanceof InputSocketComponent)) return;
        // InputSocket selection ruleset
        if (c.node == this.node) return; // don't allow self-connections
        if (c.type != InputSocketComponent.Type.CONNECTOR) {
          if (c.connected) return;
        }
        if (this.connected.findIndex(el => el.connectedTo.id == c.id) >= 0) return; // disable duplicate connections
        if (!Connector.typesCompatible(c.type, this.type)) return; // only connect to sockets of the same type

        sockets.push(c);
        return;
      });
    }
    const gs = (component) => { // check children of components recursively for sockets
      check(component);
      if (!component.elements) return;
      component.elements.forEach((el) => {
        if (el.element instanceof Viewport) {
          el.element.components.forEach(e => {
            gs(e.element);
          });
          return;
        }
        gs(el.element);
      });
    }
    this.parentSVGEngine.components.forEach((c) => {
      gs(c.component);
    });
    return sockets;
  }

  /**
   * @description Set the colour of the parts creating the connector,
   * like the triangle and the dot.
   *
   * @param  {string} c The CSS colour you want to switch to.
   * @return {void}
   */
  setColor(c) {
    this.color = c;
    this.oCircle.setColor(c);
    this.oT.setStroke(c)
    this.oT.setColor("transparent");
  }
  prepareSnap(connectable) {
    this.snapping = true;
    const socket = connectable.socket;
    socket.cCircle.setRadius(10 * socket.scale); // change circle props without changing position
    this.snappingSocket = connectable;
  }
  connectTo(socket) {
    this.createConnector({ clientX: socket.x, clientY: socket.y });
    this.snappingSocket = { socket: socket };
    this.snap();
  }
  snap() {
    const socket = this.snappingSocket.socket;
    let p = this.getAbsCoords(socket.cCircle.container);
    this.activeConnector.moveTo({
      x: p.x + socket.cCircle.radius * socket.scale,
      y: p.y + socket.cCircle.radius * socket.scale
    });
    this.activeConnector.connectedTo = this.snappingSocket.socket;
    this.activeConnector.connectedNode = this.snappingSocket.socket.node.id;
    socket.connected = true;
    socket.connector = this.activeConnector;
    const connector = this.activeConnector;
    connector.setMoveListener(socket.node.addEventListener("move", (_e) => {
      if (this.connected.length == 0 || !this.connected) return; // only execute if the node is currently connected
      const pos = this.getAbsCoords(socket.cCircle.container);
      pos.x += socket.cCircle.radius * socket.scale;
      pos.y += socket.cCircle.radius * socket.scale;
      connector.moveTo(pos);
    }), socket.node);
    socket.connect(this.activeConnector);
    this.connected.push(this.activeConnector);
    socket.cCircle.setRadius(8 * socket.scale); // reset socket proportions
    this.dragging = false;
  }
  createConnector(e) {
    this.activeConnector = new (ConnectorManager.getConnector(this.styleType))(this, { x: e.clientX, y: e.clientY }, this.getAbsCoords(this.oCircle.container), this.scale, OutputPlugComponent.ConnectorColor[this.type]);
    this.parentSVGEngine.element.appendChild(this.activeConnector.createSVGElement()); // don't add as a component to prevent "wobbing" while panning
    this.emit("connector", this.activeConnector);
  }
  initConnector() {
    this.dragging = false;
    this.snapping = false;
    this.mouseDown = (e) => {
      this.dragging = true;
      this.initSnapping();
      this.createConnector(e);
    }
    this.interactions.initListeners(this.oCircle.container, (e) => {
      if (this.type == OutputPlugComponent.Type.ANY) return;
      this.mouseDown(e);
    }, () => { }, () => { });
    this.node.addEventListener("move", () => {
      if (!this.activeConnector || !this.connected) return; // only execute if there is a connected connector
      const pos = this.getAbsCoords(this.oCircle.container);
      pos.x += this.oCircle.radius * this.scale;
      pos.y += this.oCircle.radius * this.scale;
      this.connected.forEach(c => {
        c.moveStartTo(pos);
      });
    });
    this.interactions.initListeners(window, () => { }, (e) => {
      // move listener
      if (!this.dragging) return;
      this.activeConnector.moveTo({ x: e.clientX, y: e.clientY });
      this.connectables.forEach((c, i) => {
        const distance = this.distance(c.coords.x, c.coords.y, e.clientX, e.clientY);
        this.connectables[i].distance = distance;
      });
      this.connectables.sort((a, b) => {
        if (a.distance < b.distance) return -1;
        if (a.distance > b.distance) return 1;
        return 0;
      });
      var reset = () => {
        const s = this.snappingSocket.socket;
        s.cCircle.setRadius(s.defRadius * s.scale);
        this.snapping = false;
      }
      if (this.connectables.length == 0) return;
      if (this.connectables[0].distance > (this.minConDist || 50)) {
        if (!this.snappingSocket) return;
        reset()
        return;
      }
      if (this.connectables != this.snappingSocket && this.snappingSocket) {
        reset();
      }
      this.prepareSnap(this.connectables[0]);
    }, () => {
      // end/cancel listener
      if (!this.dragging) return;
      if (this.snapping) return this.snap();
      this.emit("connectordestroy", this.activeConnector);
      this.activeConnector = this.activeConnector.destroy(); // destroy the connector and the variable; cause destroy returns undefined
      this.dragging = false;
    });
  }
  findEngine() { // find the html element of the engine recursively
    var checkParent = (elem) => { // unused
      const parent = elem.parentElement;
      return (parent.id.includes("ULVS-Engine_")) ? parent : checkParent(parent);
    }
    return checkParent(this.container);
  }
  setSubComponentAttributes() { // update sub-elements
    this.oCircle.setPosition({
      x: 20 * this.scale,
      y: 0
    });
    this.plugPos = {
      x: 20 * this.scale + 8 * this.scale,
      y: 8 * this.scale
    }
    this.oT.setPosition({
      x: 0,
      y: -1 * this.scale
    });
    this.oCircle.radius = 8 * this.scale;
    this.oT.setScale(1 * this.scale);
  }
  setScale(s) {
    this.scale = s;
    super.updateAttributes();
    this.setSubComponentAttributes();
  }
}

/**
 * The type used for InputSocketComponents
 * @typedef {string} InputSocketComponentType
 * @property {string} BOOLEAN
 * @property {string} INTEGER
 * @property {string} FLOAT
 * @property {string} STRING
 * @property {string} CONNECTOR
 * @property {string} ANY
 */
/**
 * Creates a new InputSocketComponent
 * @class
 * @classdesc The object for ingoing plug connections. On nodes
 * @augments Component
 */
class InputSocketComponent extends Component {

  /**
   * @enum
   * @description Valid types used for InputSocketComponents
   */
  static Type = {
    BOOLEAN: "bool",
    CONNECTOR: "connect",
    NUMBER: "num",
    INTEGER: "int",
    FLOAT: "float",
    ANY: "any",
    ARRAY: "arr",
    STRING: "str"
  }
  static ColorMapping = {
    bool: "#a44747",
    connect: "#ffffff",
    num: "#427fbd",
    int: "#427fbd",
    any: "#ffffff",
    str: "#779457",
    str: "#daa520"
  }
  static StrokeMapping = {
    bool: { stroke: "transparent", width: 1 },
    connect: { stroke: "transparent", width: 1 },
    int: { stroke: "transparent", width: 1 },
    num: { stroke: "transparent", width: 1 },
    any: { stroke: "#ffffff", width: 1 },
    str: { stroke: "transparent", width: 1 }
  }
  static TypeLabel = {
    bool: "BOOL",
    num: "NUM",
    int: "INT",
    any: "ANY",
    str: "STR"
  }
  /**
   * @description Initiates a new inputSocketComponent
   *
   * @param  {Number} x             x position in the parent svg
   * @param  {Number} y             y position in the parent svg
   * @param  {Number} width         the width of he inputsocket
   * @param  {Number} height        the height
   * @param  {Number} scale         the scale, set to 1 to ignore
   * @param  {String} type          type of the socket, see InputSocketComponent.Type for types
   * @param  {Node} node            parent node object, the socket is attached to
   * @param  {String} label         the label of the socket
   * @param  {Boolean} userInput=true Wether or not to include some kind of input component for the user to enter a value
   */
  constructor(x, y, width, height, scale, type, node, label, userInput = true) {
    super(x, y, width, height, scale);

    this.type = type; // TODO: the type of the socket
    this.node = node; // the node the socket is attached to
    this.label = label;
    this.id = uid();
    this.uInput = (InputSocketComponent.Type.ANY !== type) ? userInput : false;

    this.container.setAttribute("OpenVS-Node-Id", this.node.id);

    this.color = InputSocketComponent.ColorMapping[type];

    this.cCircle = new Circle(0, 0, 8 * this.scale, true, this.scale);
    this.cCircle.setColor(this.color);
    this.elements.push({ element: this.cCircle, render: (el) => el.createSVGElement() });
    this.defRadius = 8 * this.scale; // the default radius, used for connectors

    this.con; // the connector
    this.conCallback = null, this.deconCallback = null;
    this.changeCBs = [];
    this.dataConstant = true;
    this.storedData = null; // initiated on initType
    this.phantomTypes = []; // only important if type ANY; see Connector.typesCompatible

    if (type !== InputSocketComponent.Type.CONNECTOR) {
      this.initType();
      return this;
    }

    // only draw this when creating a connection connector
    this.cT = new RoundedTriangleComponent(22 * this.scale, 2.5 * this.scale, 90, 1 * this.scale);
    this.cT.setColor(this.color);
    this.elements.push({ element: this.cT, render: (el) => el.createSVGElement() });

    return this;
  }
  get connector() {
    return this.con;
  }
  set connector(connector) {
    return this.con = connector;
  }

  /**
   * @description Specify the function to execute when a connector gets attached.
   *
   * @param  {function} cb The callback. The function receives the connector that has been connected as argument.
   * @return {void}
   */
  setConnectionCallback(cb) {
    this.conCallback = cb;
  }
  /**
   * @description Specify the function to execute when a connector gets detached.
   *
   * @param  {function} cb The callback. The function receives the connector that has been deconnected as argument.
   * @return {void}
   */
  setDisconnectionCallback(cb) {
    this.deconCallback = cb;
  }

  /**
   * @description Add a listener to the onValueChange event
   *
   * @param  {function} cb The callback. The passed parameter contains a .prevent() method that cancel the event like the .preventDefault method
   * @return {void}
   */
  onValueChange(cb) {
    this.changeCBs.push(cb);
  }

  /**
   * @description Callback function for Connectors. This function gets called as soon as a connector connects.
   *
   * @param  {Connector} connector The connector that connected
   * @return {void}
   */
  connect(connector) {
    if (this.conCallback) this.conCallback(connector);
    if (!this.uInput) return;
    if (!this.box) return;
    if (this.type == InputSocketComponent.Type.BOOLEAN) {
      if (this.node.reset) this.node.reset();
    }
    this.dataConstant = false;
    this.box.container.style.display = "none";
    this.offset = 0;
    this.relocateLabels();
  }
  /**
   * @description Callback function for Connectors. This function gets called as soon as a connector disconnects.
   *
   * @param  {Connector} connector The connector that disconnected
   * @return {void}
   */
  disconnect(connector) {
    if (this.deconCallback) this.deconCallback(connector);
    if (!this.uInput) return;
    if (!this.box) return;
    if (this.type == InputSocketComponent.Type.BOOLEAN) {
      if (this.node.state) this.node.simulate(this.node.state);
    }
    this.dataConstant = true;
    this.box.container.style.display = "block";
    this.offset = this.box.tw + 4;
    this.relocateLabels();
  }
  relocateLabels() {
    let metrics = this.metrics;
    let typeMetrics = this.typeMetrics;
    this.text.setPosition({
      x: (21 + this.offset) * this.scale,
      y: (metrics.height + 3) * this.scale
    });
    this.typeLabel.setPosition({
      x: (this.offset - 21 + metrics.width + typeMetrics.width) * this.scale,
      y: (typeMetrics.height + 2) * this.scale
    });
  }
  getUserInputComponent() {
    switch (this.type) {
      case InputSocketComponent.Type.STRING:
      case InputSocketComponent.Type.NUMBER:
      case InputSocketComponent.Type.INT:
        return new SVGInput(21 * this.scale, 0, 50, 1, (data) => {
          // TODO: implement e.prevent() method
          this.changeCBs.forEach(cb => {
            cb.call(this, {prevent: () => {}, oldValue: this.storedData, newValue: data});
          });
          this.storedData = data;
        });
      case InputSocketComponent.Type.BOOLEAN:
        return new SVGCheckbox(21 * this.scale, 0, this.scale, false, (state) => {
          this.storedData = state;
          if (this.node.simulate) this.node.simulate(state); // change the opacity of the plugs
        });
      default:
        console.warn("Something went wrong!", this);
        return null;
    }
  }
  initType() {
    this.offset = 0;
    if (this.uInput) {
      this.dataConstant = true;
      this.box = this.getUserInputComponent();
      this.elements.push({ element: this.box });
      this.offset = this.box.tw + 4;
    }

    let metrics = Text.measureText(this.label);
    this.metrics = metrics;
    this.text = new Text((21 + this.offset) * this.scale, (3) * this.scale, this.label, this.scale, Text.Anchor.START, Text.VerticalAnchor.TOP);
    this.text.setColor("white");
    this.elements.push({ element: this.text, render: (el) => el.createSVGElement() });

    let typeMetrics = Text.measureText(InputSocketComponent.TypeLabel[this.type], (9 * this.scale) + "px");
    this.typeMetrics = typeMetrics;
    this.typeLabel = new Text((this.offset + 23 + metrics.width) * this.scale, (2) * this.scale, InputSocketComponent.TypeLabel[this.type], this.scale, Text.Anchor.START, Text.VerticalAnchor.TOP);
    this.typeLabel.container.style.fontSize = (9 * this.scale) + "px";
    this.typeLabel.setColor(this.color);
    this.elements.push({ element: this.typeLabel, render: (el) => el.createSVGElement() });

    this.modify(true);
  }
  modify(isInit = false) {
    switch (this.type) {
      case InputSocketComponent.Type.ANY:
        this.cCircle.setColor("transparent");
        this.cCircle.setStroke(InputSocketComponent.StrokeMapping[this.type]);
        if (isInit) this.defRadius *= 0.7;
        this.cCircle.setRadius(this.cCircle.radius * 0.7, false);
        break;
      default:

        break;
    }
  }

  /**
   * @description Changes the data type of this socket after initialization.
   *
   * @param  {InputSocketComponentType} type The new data type.
   * @return {void}
   */
  setType(type) {
    this.type = type;
    this.color = InputSocketComponent.ColorMapping[this.type];
    this.cCircle.setStroke(InputSocketComponent.StrokeMapping[this.type]);
    this.cCircle.setColor(this.color);
    this.defRadius = 8 * this.scale;
    this.typeLabel.setColor(this.color);
    this.typeLabel.setText(InputSocketComponent.TypeLabel[this.type]);
    this.modify(true);
  }
  setSubComponentAttributes() { // update sub-elements
    this.cCircle.radius = 8 * this.scale;

    if (this.text) this.text.setScale(this.scale);

    if (this.type !== InputSocketComponent.Type.CONNECTOR) return;
    this.cT.setScale(1 * this.scale);
    this.cT.setPosition({
      x: 22 * this.scale,
      y: -1 * this.scale
    });
  }
  setScale(s) {
    this.scale = s;
    super.updateAttributes();
    this.setSubComponentAttributes();
  }
  resetPhantoms() {
    let removed = this.phantomTypes.length;
    this.phantomTypes.length = 0;
    return removed;
  }
  addPhantom(...types) {
    this.phantomTypes.push(...types);
    return this.phantomTypes;
  }
}

/**
 * @class
 * @classdesc The base Node class. Every complex node should extend this.
 * @augments Component
 */
class Node extends Component {
  static ClassColor = {
    basic: "#8a5794",
    event: "#779457",
    deviceinfo: "#946148",
    console: "#588068"
  };
  /**
   * @enum
   * @typedef {(string)} NodeClass
   * @description Accepted Node categories.
   */
  static Class = {
    BASIC: "basic",
    EVENT: "event",
    DEVICEINFO: "deviceinfo",
    CONSOLE: "console"
  }
  static ClassName = {
    basic: "Basic",
    event: "Event",
    deviceinfo: "Device Info",
    console: "Console"
  }

  /**
   * @description Initiates a new Node object
   *
   * @param  {number} x         The x position in the current viewport or container.
   * @param  {number} y         The y position in the current viewport or container.
   * @param  {number} scale     The scale of the Node.
   * @param  {object} svgEngine The SVGEngine object, the node is added to.
   * @return {Node}           The new Node object.
   */
  constructor(x, y, scale, svgEngine) {
    let height = 37.5 * scale;
    let width = 200 * scale;
    super(x, y, width, height, scale);

    this.colors = {
      background: "#1d1d1d",
      header: "#8a5794"
    }

    this.id = uid();
    this.nodeIdentifier = "nil";
    this.parentSVGEngine = svgEngine;
    this.embedNode = false;

    window.openVS.nodes[this.id] = this;
    this.container.setAttribute("OpenVS-Node-Id", this.id);

    this.shadows = SVGEngine.createShadowFilter(0, 1); // create the shadow defs element
    this.shadowElement = this.shadows.element;
    this.elements.push({ element: this.shadows.element, render: (el) => el });

    this.bgRect = new Rectangle(0, 1, this.tw, this.th, true, 4);
    this.bgRect.setColor(this.colors.background);
    this.bgRect.setStroke({
      color: "black",
      width: 0.5
    });
    this.bgRect.setShadow(this.shadows.id);
    this.elements.push({ element: this.bgRect, render: (el) => el.createSVGElement() });

    this.hRect = new Rectangle(0, 0, this.tw, 33 * this.scale, false, 5);
    this.hRect.setColor(this.colors.header);
    this.hRect.setStroke({
      color: this.colors.background,
      width: 0.5
    });
    this.clip = this.hRect.createClipPath(0);
    this.hRect.setClipPath(this.clip.id);
    this.elements.push({ element: this.clip, render: (el) => el.element });
    this.elements.push({ element: this.hRect, render: (el) => el.createSVGElement() });

    this.connectors = new Viewport(0, 47.5 * this.scale);
    this.elements.push({ element: this.connectors });

    this.body = new Viewport(0, 37.5 * this.scale);
    this.elements.push({ element: this.body });

    this.sockets = [];
    this.inputSockets = [];
    this.outputPlugs = [];
    this.labels = [];
    this.plugs = [];
    this.connectors.addComponent(this.sockets, (el) => { return el.map(e => e.createSVGElement()); });
    this.connectors.addComponent(this.labels, (el) => { return el.map(e => e.createSVGElement()); });
    this.connectors.addComponent(this.plugs, (el) => { return el.map(e => e.createSVGElement()); });
    this.body.addComponent(this.inputSockets, (el) => { return el.map(e => e.createSVGElement()); });
    this.body.addComponent(this.outputPlugs, (el) => { return el.map(e => e.createSVGElement()); });
    this.connectionOffset = 0;

    this.outgoingConnectors = [];
    this.incomingConnectors = [];

    this.dragHandler = new NodeDragAttachment(() => {
      this.moveToTop();
      let move = (plug) => {
        plug.renderContainer = this.parentSVGEngine.element;
        plug.connected.forEach((connector) => {
          connector.renderContainer = this.parentSVGEngine.element;
          connector.moveToTop();
        });
      }
      let moveSocket = (socket) => {
        socket.renderContainer = this.parentSVGEngine.element;
        if (!socket.connected) return;
        socket.connector.renderContainer = this.parentSVGEngine.element;
        socket.connector.moveToTop();
      }
      this.plugs.forEach(plug => {
        move(plug);
      });
      this.outputPlugs.forEach(plug => {
        move(plug);
      });
      this.inputSockets.forEach(socket => {
        moveSocket(socket);
      });
      this.sockets.forEach(socket => {
        moveSocket(socket);
      });
    });
    this.dragHandler.attach(this);

    this.eventElem = document.createElement("span");
    this.events = {
      move: new CustomEvent("move", { detail: { node: this } })
    }

    this.renderContainer = this.parentSVGEngine.element;

    return this;
  }

  destroy() {
    this.container.remove();
    delete window.openVS.nodes[this.id];
  }

  /**
   * @description Returns an up-to-date copy of the container of the current parent SVG engine.
   * @type {HTMLElement}
   */
  get renderContainer() {
    // getter to keep return an up-to-date copy of the element by the svg engine, even if it changes
    return this.parentSVGEngine.renderElement;
  }
  set renderContainer(_i) {
    // console.warn("Don't do that! [Node.renderContainer is readonly] trying to set to '" + i +"'");
  }

  /**
   * @description Returns the identifier of the node that's also passed to the program
   * spec.
   *
   * @return {string}  The node identifier string.
   */
  get identifier() {
    return this.nodeIdentifier;
  }
  set identifier(_id) {
    console.warn("Please use Node.setId() instead of the setter.");
  }

  /**
   * @description Disconnects all incoming or outgoing connectors.
   *
   * @return {void}
   */
  clearConnections() {
    [...this.inputSockets, ...this.sockets].forEach(socket => {
      if (!socket.connected) return;
      socket.connector.disconnect();
    });
    [...this.outputPlugs, ...this.plugs].forEach(plug => {
      plug.connected.forEach(connector => {
        connector.disconnect();
      })
    })
  }
  createSVGElement() {
    if (this.embedContainer) {
      this.body.createSVGElement();
      this.connectors.createSVGElement();
      this.embedContainer.append(this.connectors.container, this.body.container);
      return this.embedContainer; // don't execute the inherited function
    }
    return super.createSVGElement();
  }
  createPreview(x=0, y=0) {
    return new NodePreview(x, y, this.scale, this);
  }
  embedBody(container, node) { // Embed all the connectors and similar in another element
    this.embedContainer = container;
    this.embedNode = node;
  }
  setConnectorType(type) {
    this.plugs.forEach((plug) => {
      plug.setConnectorType(type);
    });
    this.outputPlugs.forEach((plug) => {
      plug.setConnectorType(type);
    });
  }

  /**
   * @callback Node~EventCallback
   * @param {object} event        The event object.
   * @param {object} event.detail The data that is delivered if it is a custom event, like the "move" event.
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Event} for
   * further information about the events structure. In case of a custom event
   * like "move" see {@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent}
   */
  /**
   * @description Add an event listener to the node.
   *
   * @param  {("move")} event The event that should be listened for.
   * @param  {Node~EventCallback} cb The callback function.
   * @return {object}                The event listener.
   */
  addEventListener(event, cb) {
    return (this.embedNode.eventElem || this.eventElem).addEventListener(event, cb);
  }

  /**
   * @description Remove a given listener from the node.
   *
   * @param  {(string|"move")} event  The event of the listener that should be removed
   * @param  {object} listener The listener (returned by {@link Node#addEventListener})
   * @param  {...object} opts  Other options you might want to pass to the eventTarget; see {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener#parameters}
   * @return {void}
   */
  removeEventListener(event, listener, ...opts) {
    return (this.embedNode.eventElem || this.eventElem).removeEventListener(event, listener, ...opts);
  }
  emit(event) {
    const e = this.events[event];
    if (!e) {
      console.error("Unknown event '" + event + "'! At Node.emit()");
      return;
    }
    this.eventElem.dispatchEvent(e);
  }
  setPosition(pos) {
    super.setPosition(pos);
    this.emit("move");
  }
  update() {
    /*this.elements.forEach((e, i) => {

    });*/
  }
  setSubComponentAttributes() {
    this.bgRect.setScale(this.scale);
    this.hRect.setScale(this.scale);
    this.nText.setScale(this.scale);

    this.nText.setPosition({
      x: 5 * this.scale,
      y: 20 * this.scale
    });
    this.hText.setScale(this.scale); // class name
    let metrics = Text.measureText(this.hText.txt, "14px Times New Roman");
    this.hText.setPosition({
      x: this.tw - metrics.width * this.scale,
      y: 26 * this.scale
    });
    this.labels.forEach(label => {
      label.setScale(this.scale);
    });
    this.sockets.forEach((socket, i) => {
      socket.setPosition({
        x: -8 * this.scale,
        y: (this.th * 0.2) + 25 * this.scale + (36 * i) * this.scale
      });
      socket.setScale(this.scale);
    });
    this.plugs.forEach((plug, i) => {
      plug.setPosition({
        x: this.tw - (36 - 8) * this.scale,
        y: (this.th * 0.2) + 25 * this.scale + (36 * i) * this.scale
      })
      plug.setScale(this.scale);
    });
  }
  setScale(s) {
    this.scale = s;
    super.updateAttributes();
    this.setSubComponentAttributes();
  }

  /**
   * @description Attach an attachment to the node object.
   *
   * @param  {Attachment} at The attachment that should be attached.
   * @return {void}
   */
  addAttachment(at) { // attachments like the move listener
    this.attachments.push(at);
    at.attach(this);
  }

  /**
   * @description Change the name of the node after initialization.
   *
   * @param  {string} name The new name
   * @return {Text#setText}
   */
  setName(name) {
    this.name = name;
    if (!this.nText) {
      this.nText = new Text(5 * this.scale, 10 * this.scale, name, this.scale, Text.Anchor.START, Text.VerticalAnchor.TOP);
      this.nText.setColor("white");
      this.elements.push({ element: this.nText, render: (el) => el.createSVGElement() });
      return;
    }
    return this.nText.setText(name);
  }

  /**
   * @description Set the nodeIdentifier of this node. This id will be provided
   * in the program spec on program generation for the compiler.
   *
   * @param  {string} id The new id of the Node.
   * @return {void}
   */
  setId(id) {
    this.nodeIdentifier = id;
  }

  /**
   * @description Change the class after initialization.
   *
   * @param  {NodeClass} c The new class
   * @return {void}
   */
  setClass(c) {
    this.class = c;
    let className = Node.ClassName[c];
    let classColor = Node.ClassColor[c];
    this.hRect.setColor(classColor);
    this.hText = new Text(this.tw - (5 * this.scale), 12 * this.scale, className, 1 * this.scale, Text.Anchor.END, Text.VerticalAnchor.TOP);
    this.hText.fontSize = 12 * this.scale;
    this.hText.setColor("white");
    this.elements.push({ element: this.hText, render: (el) => el.createSVGElement() });
  }
  setConnectionOffset(delta) {
    this.connectionOffset = delta;
    this.bgRect.setHeight(this.bgRect.height + delta);
  }
  /**
   * @description Add a new program flow socket. Flow sockets/plugs are used to
   * tell ULVS which nodes run when. The flow starts at a start node and then follows
   * all connected flow connectors from node to node, resulting in a program order.
   *
   * @return {array} An array containing all of the current flow sockets.
   */
  addSocket() {
    const socket = new InputSocketComponent((-8 * this.scale), (36 * this.sockets.length + this.connectionOffset) * this.scale, 16, 34, this.scale, InputSocketComponent.Type.CONNECTOR, (this.embedNode || this));

    const currLength = Math.max(this.plugs.length, this.sockets.length);

    this.sockets.push(socket);

    if (this.sockets.length > currLength) {
      let diff = this.sockets.length - currLength;
      this.bgRect.setHeight(this.bgRect.height + (36 * diff) * this.scale);
      this.body.setPosition({ x: 0, y: this.body.y + (36 * diff + this.connectionOffset) * this.scale });
    }

    return this.sockets;
  }
  /**
   * @description Add a flow plug. Flow plugs can be used to create flow connections
   * to flow sockets.
   * @see See {@link Node#addSocket} for more info about the program flow.
   *
   * @param  {string} label A label that will be displayed left to the plug.
   * @param  {("bezier"|"line")} style The style of the connector starting from this plug.
   * @return {array}       Returns an array of all the attached plugs.
   */
  addPlug(label, style) {
    //let metrics = Text.measureText(label);
    const text = new Text(this.tw - (34) * this.scale, (38 * this.plugs.length + this.connectionOffset) * this.scale, label, this.scale, Text.Anchor.END, Text.VerticalAnchor.TOP);
    text.setColor("white");
    this.labels.push(text);

    const currLength = Math.max(this.plugs.length, this.sockets.length);

    const plug = new OutputPlugComponent(this.tw - (36 - 8) * this.scale, (36 * this.plugs.length + this.connectionOffset) * this.scale, 16, 34, this.scale, OutputPlugComponent.Type.CONNECTOR, this.parentSVGEngine, (this.embedNode || this), style);
    this.plugs.push(plug);

    plug.addEventListener("connector", (e) => {
      this.outgoingConnectors.push(e);
    });
    plug.addEventListener("connectordestroy", (e) => {
      const idx = this.outgoingConnectors.findIndex(el => el.id == e.id);
      if (idx === -1) return;
      this.outgoingConnectors.splice(idx, 1);
    });

    if (this.plugs.length > currLength) {
      let diff = this.plugs.length - currLength;
      this.bgRect.setHeight(this.bgRect.height + (36 * diff) * this.scale);
      this.body.setPosition({ x: 0, y: this.body.y + (36 * diff + this.connectionOffset) * this.scale });
    }

    return this.plugs;
  }
  /**
   * @description Adds an InputSocket. These sockets can be used for data transfer.
   * They are also typed and there are strict rules for which connector type can
   * connect to which socket type.
   *
   * @param  {InputSocketComponentType} type  The type of the socket.
   * @param  {string} label A label for the socket that will be displayed right to it.
   * @return {InputSocketComponent} The initiated input socket.
   */
  addInputSocket(type, label) {
    const socket = new InputSocketComponent((-8 * this.scale), (28 * this.inputSockets.length) * this.scale, 16 * this.scale, 34 * this.scale, this.scale, type, (this.embedNode || this), label);

    const currLength = Math.max(this.inputSockets.length, this.outputPlugs.length);
    this.inputSockets.push(socket);

    if (this.inputSockets.length > currLength) {
      let diff = this.inputSockets.length - currLength;
      this.bgRect.setHeight(this.bgRect.height + (28 * diff) * this.scale);
    }

    return socket;
  }
  /**
   * @description Adds an OutputPlug to the node. These are used to create data
   * connectors.
   * @see See {@link Node#addInputSocket} for more details on data connections.
   *
   * @param  {OutputPlugComponentType} type The type of the plug
   * @param  {string} label The label of the plug that will be displayed left to it.
   * @param  {("bezier"|"line")} style The style of the connector.
   * @return {OutputPlugComponent}       The initiated plug object
   */
  addOutputPlug(type, label, style) {
    const plug = new OutputPlugComponent(this.tw - (36 - 8) * this.scale, (28 * this.outputPlugs.length) * this.scale, 16, 34, this.scale, type, this.parentSVGEngine, (this.embedNode || this), style, label);

    plug.addEventListener("connector", (e) => {
      this.outgoingConnectors.push(e);
    });
    plug.addEventListener("connectordestroy", (e) => {
      const idx = this.outgoingConnectors.findIndex(el => el.id == e.id);
      if (idx === -1) return;
      this.outgoingConnectors.splice(idx, 1);
    });

    const currLength = Math.max(this.inputSockets.length, this.outputPlugs.length);
    this.outputPlugs.push(plug);

    if (this.outputPlugs.length > currLength) {
      let diff = this.outputPlugs.length - currLength;
      this.bgRect.setHeight(this.bgRect.height + (28 * diff) * this.scale);
    }
    return plug;
  }


  /**
   * @description Add a user input to the body of the node. The user can enter
   * values that will be used in the compiling process as constants. Temporary deprecated
   * @deprecated Temporary deprecated as user inputs are included in input sockets
   * @param  {InputSocketComponentType} type     The data type of the input.
   * @param  {string} label="" The label of thé input that will be displayed next to it.
   * @return {object}          The input object depending on the data type.
   */
  addUserInput(type, label = "") {
    const object = new ({
      [InputSocketComponent.Type.STRING]: SVGInput
    }[type])((10 * this.scale), (28 * this.inputSockets.length) * this.scale, 64 * this.scale, 1, label);
    object.dataConstant = true; // mark for compiler
    const currLength = Math.max(this.inputSockets.length, this.outputPlugs.length);
    this.inputSockets.push(object);

    if (this.inputSockets.length > currLength) {
      let diff = this.inputSockets.length - currLength;
      this.bgRect.setHeight(this.bgRect.height + (28 * diff) * this.scale);
    }

    return object;
  }
}
class NodePreview extends Component {
  constructor(x, y, scale, node) {
    const measures = {
      header: {
        height: 28 / 4,
        radius: 2
      },
      background: {
        width: 200 / 4,
        height: 76 / 4,
        radius: 2
      }
    }
    let height = measures.background.height * scale;
    let width = measures.background.width * scale;
    super(x, y, width, height, scale);

    this.colors = {
      background: "#1d1d1d",
      header: "#8a5794"
    }

    this.node = node;
    this.class = node.class;

    this.bgRect = new Rectangle(0, 1, this.tw, this.th, true, measures.background.radius);
    this.bgRect.setColor(this.colors.background);
    this.bgRect.setStroke({
      color: "black",
      width: 0.5
    });
    this.elements.push({ element: this.bgRect });

    this.hRect = new Rectangle(0, 0, this.tw, measures.header.height * this.scale, false, measures.header.radius);
    this.hRect.setColor(Node.ClassColor[this.class]);
    this.hRect.setStroke({
      color: Node.ClassColor[this.class],
      width: 0.5
    });
    this.clip = this.hRect.createClipPath(0);
    this.hRect.setClipPath(this.clip.id);
    this.elements.push({ element: this.clip, render: (el) => el.element });
    this.elements.push({ element: this.hRect });

    this.body = new Viewport(0, measures.header.height + 6);
    this.elements.push({ element: this.body });

    console.log(node.plugs, node.sockets, node.inputSockets, node.outputPlugs);

    const spacing = 8;

    node.sockets.forEach((_socket, i) => {
      let indicator = new Circle(0, spacing * i, 2.5, false, this.scale);
      indicator.setColor("white");
      this.body.addComponent(indicator);
    });
    node.inputSockets.forEach((socket, i) => {
      let offset = Math.max(spacing * node.sockets.length, spacing * node.plugs.length) - 3.5; // remove 3.5 pixels as there too much spacing
      let indicator = new Rectangle(-2.75, offset + 4.5 * i, 5.5 * this.scale, 3, true, 2);
      indicator.setColor(InputSocketComponent.ColorMapping[socket.type])
      this.body.addComponent(indicator);
    });

    node.plugs.forEach((_plug, i) => {
      let indicator = new Circle(measures.background.width, spacing * i, 2.5, false, this.scale);
      indicator.setColor("white");
      this.body.addComponent(indicator);
    });
    node.outputPlugs.forEach((plug, i) => {
      let offset = Math.max(spacing * node.sockets.length, spacing * node.plugs.length) - 3.5; // remove 3.5 pixels as there too much spacing
      let indicator = new Rectangle(-2.75 + measures.background.width, offset + 4.5 * i, 5.5 * this.scale, 3, true, 2);
      indicator.setColor(OutputPlugComponent.ColorMapping[plug.type])
      this.body.addComponent(indicator);
    });

    let offset = measures.header.height + 3.5;
    let flowOff = Math.max(offset + node.sockets.length * spacing, offset + node.plugs.length * spacing); // offset caused by flow sockets/inputs
    console.log(flowOff);

    let sockets = flowOff + node.inputSockets.length * 4.5; // add a bit of spacing
    let plugs = flowOff + node.outputPlugs.length * 4.5;
    let corrected = Math.max(sockets, plugs, measures.background.height) + 2.5;
    console.log(measures, plugs, sockets);
    this.bgRect.setHeight(corrected);

    return this;
  }
}
class MenuItem extends Component {
  ht = "";
  title = "";
  callback = () => {};

  constructor() {
    super(0, 0, 143, 20, 1);

    return this;
  }
  setTitle(t) {
    this.title = t;
    return this;
  }
  setHoverTitle(t) {
    this.ht = t;
    return this;
  }
  setCallback(cb) {
    this.callback = cb;
    return this;
  }
}
class ContextMenu extends Component {
  constructor() {
    super(0, 0, 143, 20, 1);

    this.hide(); // only reveal when right-clicking

    this.items = [];
    this.body = new ScrollComponent(0, 0, this.width * 5, this.height, this.scale);

    return this;
  }
  addItem(i) {
    this.items.push(i);
    this.body.addComponent(i);
  }
}
class ConditionNode extends Node {
  constructor(x, y, scale, svgEngine, type = "") {
    super(x, y, scale, svgEngine);

    this.setId("OpenVS-Base-Basic-Condition");
    this.setName("If");
    this.setClass(Node.Class.BASIC);

    this.addSocket(); // connector in/out-puts
    this.addPlug("Met", type); // if block
    this.addPlug("Not met", type); // else block
    // data inputs
    this.addInputSocket(InputSocketComponent.Type.BOOLEAN, "Condition", type);

    this.forceBranch = true;
    this.internalBranch = true;

    this.state = null;

    return this;
  }
  simulate(state) {
    this.state = state;
    this.labels[1 - state].setColor("white");
    this.labels[state].setColor("rgba(255, 255, 255, 0.3)");
    this.plugs[1 - state].setOpacity(1);
    this.plugs[state].setOpacity(0.3);
  }
  reset() {
    this.labels[0].setColor("white");
    this.labels[1].setColor("white");
    this.plugs[0].setOpacity(1);
    this.plugs[1].setOpacity(1);
  }
}
class WhileLoopNode extends Node {
  constructor(x, y, scale, svgEngine, type="") {
    super(x, y, scale, svgEngine);

    this.setId("OpenVS-Base-Loop-While");
    this.setName("While");
    this.setClass(Node.Class.BASIC);

    this.addSocket();
    this.addPlug("", type);
    this.addPlug("Body", type);

    this.addInputSocket(InputSocketComponent.Type.BOOLEAN, "Condition");

    this.forceBranch = true;
    this.internalBranch = true;
  }
}
class VariableWriteNode extends Node {
  constructor(x, y, scale, svgEngine, type="") {
    super(x, y, scale, svgEngine);

    this.setId("OpenVS-Base-Variable-Write");
    this.setName("Write Variable");
    this.setClass(Node.Class.BASIC);

    this.addSocket();
    this.addPlug("", type);

    const name = this.addInputSocket(InputSocketComponent.Type.STRING, "Name");
    name.setConnectionCallback((e) => {
    });
    this.addInputSocket(InputSocketComponent.Type.STRING, "Value");
  }
}
class VariableReadNode extends Node { // TODO: implement type selection
  constructor(x, y, scale, svgEngine, type="") {
    super(x, y, scale, svgEngine);

    this.setId("OpenVS-Base-Variable-Read");
    this.setName("Read Variable");
    this.setClass(Node.Class.BASIC);

    const name = this.addInputSocket(InputSocketComponent.Type.STRING, "Name");
    name.onValueChange((e) => {
      const input = svgEngine.getVariable(e.value);
      this.value.setType(input.type || OutputPlugComponent.Type.ANY);
    });
    this.value = this.addOutputPlug(OutputPlugComponent.Type.ANY, "Value", type); // TODO: implement variable registry registry
  }
}
// TODO: Group nodes inside their classes
class ConsoleLogNode extends Node {
  constructor(x, y, scale, svgEngine, type = "") {
    super(x, y, scale, svgEngine);

    this.setId("OpenVS-Base-Console-Log");
    this.setName("Log");
    this.setClass(Node.Class.CONSOLE);

    this.addSocket();
    this.addPlug("", type);

    this.addInputSocket(InputSocketComponent.Type.ANY, "Object");

    return this;
  }
}
class NodeClass {
  constructor() {
    this.nodes = [];

    return this;
  }

  addNode(id, name, c, custom = () => { }) {
    nodes.push({
      id: id,
      name: name,
      class: c,
      custom: custom
    });
  }

  // TODO: Finish
}
class IsMobileNode extends Node {
  constructor(x, y, scale, svgEngine, type = "") {
    super(x, y, scale, svgEngine);

    this.setId("OpenVS-Base-DInfo-Mobile");
    this.setName("Is Mobile");
    this.setClass(Node.Class.DEVICEINFO);

    this.addOutputPlug(OutputPlugComponent.Type.BOOLEAN, "Is Mobile", type);

    return this;
  }
}
class ScreenSizeNode extends Node {
  constructor(x, y, scale, svgEngine, type = "") {
    super(x, y, scale, svgEngine);

    this.setId("OpenVS-Base-DInfo-SSize");
    this.setName("Screen Size");
    this.setClass(Node.Class.DEVICEINFO);

    this.addOutputPlug(OutputPlugComponent.Type.INTEGER, "Pixels X", type);
    this.addOutputPlug(OutputPlugComponent.Type.INTEGER, "Pixels Y", type);

    return this;
  }
}
class AdditionNode extends Node {
  constructor(x, y, scale, svgEngine, type = "", embed = null, embedNode = null) {
    super(x, y, scale, svgEngine);

    this.setId("OpenVS-Base-Basic-Add");
    this.setName("Add (Math)");
    this.setClass(Node.Class.BASIC);

    if (embed) this.embedBody(embed, embedNode);

    this.addInputSocket(InputSocketComponent.Type.NUMBER, "A", type);
    this.addInputSocket(InputSocketComponent.Type.NUMBER, "B", type);

    this.addOutputPlug(OutputPlugComponent.Type.NUMBER, "Result", type);

    return this;
  }
}
class GeneralAdditionNode extends Node {
  constructor(x, y, scale, svgEngine, type = "", embed = null, embedNode = null) {
    super(x, y, scale, svgEngine);

    this.setId("OpenVS-Base-Baisc-GAdd");
    this.setName("Add");
    this.setClass(Node.Class.BASIC);

    if (embed) this.embedBody(embed, embedNode);

    this.a = this.addInputSocket(InputSocketComponent.Type.ANY, "A", type)
    this.a.setConnectionCallback((connector) => {
      this.a.setType(connector.plug.type);
      this.updatePlug();
      /*this.b.resetPhantoms();
      this.b.addPhantom(...this.typeLogic(connector.plug.type));*/
    });
    this.a.setDisconnectionCallback((_c) => {
      this.a.setType(InputSocketComponent.Type.ANY);
      this.updatePlug();
    })
    this.b = this.addInputSocket(InputSocketComponent.Type.ANY, "B", type)
    this.b.setConnectionCallback((connector) => {
      this.b.setType(connector.plug.type);
      this.updatePlug();
    });
    this.b.setDisconnectionCallback((_c) => {
      this.b.setType(InputSocketComponent.Type.ANY);
      this.updatePlug();
    })

    this.plug = this.addOutputPlug(OutputPlugComponent.Type.ANY, "Result", type);

    return this;
  }
  updatePlug() {
    console.log(this.a.type, this.b.type);
    this.plug.setType(this.resultType(this.a.type, this.b.type));
  }
  resultType(...types) {
    const map = {
      [OutputPlugComponent.Type.BOOLEAN]: -2,
      [OutputPlugComponent.Type.INTEGER]: -1,
      [OutputPlugComponent.Type.FLOAT]: 0,
      [OutputPlugComponent.Type.NUMBER]: 1,
      [OutputPlugComponent.Type.STRING]: 2,
      [OutputPlugComponent.Type.ARRAY]: 3,
      [OutputPlugComponent.Type.ANY]: 4,
    }
    let highest = null;
    types.forEach(el => {
      if (highest == null) return highest = el;
      if (map[el] > map[highest]) return highest = el;
    });
    return highest;
  }
}
class MultiplicationNode extends Node {
  constructor(x, y, scale, svgEngine, type = "", embed = null, embedNode = null) {
    super(x, y, scale, svgEngine);

    this.setId("OpenVS-Base-Basic-Multiply");
    this.setName("Multiply (Math)");
    this.setClass(Node.Class.BASIC);

    if (embed) this.embedBody(embed, embedNode);

    this.addInputSocket(InputSocketComponent.Type.NUMBER, "A", type);
    this.addInputSocket(InputSocketComponent.Type.NUMBER, "B", type);

    this.addOutputPlug(OutputPlugComponent.Type.NUMBER, "Product", type);

    return this;
  }
}
class MathNode extends Node {
  constructor(x, y, scale, svgEngine, type = "") {
    super(x, y, scale, svgEngine);

    this.setId("OpenVS-Base-Basic-Math");
    this.setName("Add (Math)"); // default math operation
    this.setClass(Node.Class.BASIC);

    this.cStyle = type; // connector design

    this.setConnectionOffset(28 * this.scale);

    this.opSelect = new SVGSelect(10 * this.scale, 42 * this.scale, 180, this.scale, (data) => this.switched(data), (s) => this.mTop(s));
    this.opSelect.addItem("Add (Math)", "add-concat");
    this.opSelect.addItem("Multiply (Math)", "multiply");
    this.opSelect.selected.container.innerHTML = "Add (Math)";
    // select has to be rendered last

    this.embeds = new Viewport(0, 30 * this.scale);
    this.elements.push({ element: this.embeds });

    const config = { attributes: true, childList: true, subtree: true };
    const callback = (mutationList) => {
      for (var mutation of mutationList) {
        if (mutation.type == "attributes" || mutation.type == "childList") {
          this.bgRect.setHeight((45 + this.embeds.y) * this.scale + this.embeds.container.getBBox().height);
        }
      }
    }
    this.elements.push({ element: this.opSelect });

    const observer = new MutationObserver(callback);
    observer.observe(this.embeds.container, config);

    this.init = true;
    this.setupBody("add-concat");

    return this;
  }
  createSVGElement() {
    return super.createSVGElement();
  }
  transfer(node) {
    this.plugs = node.plugs;
    this.sockets = node.sockets;
    this.outputPlugs = node.outputPlugs;
    this.inputSockets = node.inputSockets;
    this.labels = node.labels;
  }
  setupBody(id) {
    this.clearConnections();
    this.embeds.container.innerHTML = "";
    switch (id) {
      case "add-concat":
        const addition = new AdditionNode(0, 0, this.scale, this.parentSVGEngine, this.cStyle, this.embeds.container, this);
        this.elements.push({
          element: addition, render: (el) => {
            el.createSVGElement();
          }
        });
        this.transfer(addition);
        console.log(this);
        if (!this.init) return addition.createSVGElement();
        this.init = false;
        break;
      case "multiply":
        const mult = new MultiplicationNode(0, 0, this.scale, this.parentSVGEngine, this.cStyle, this.embeds.container, this);
        this.elements.push({
          element: mult, render: (el) => {
            el.createSVGElement();
          }
        });
        this.transfer(mult);
        if (!this.init) return mult.createSVGElement();
        this.init = false;
        break;
      default:
        console.warn("Suspicious case detected 🤨: ", id);
        break;
    }
  }
  switched(item) {
    this.type = item.selected;
    this.setName(item.label);
    this.setupBody(item.selected);
  }
  mTop(state) {
    if (!state) return; // only run if it is expanding
    /*this.opSelect.renderContainer = this.parentSVGEngine.element;
    console.log(this);
    this.opSelect.moveToTop();*/ // don't do this... not good
  }
}
class StartEventNode extends Node {
  constructor(x, y, scale, svgEngine, type = "") {
    super(x, y, scale, svgEngine);

    this.setId("OpenVS-Base-Event-Start");

    this.setName("Start");
    this.setClass(Node.Class.EVENT);

    this.addPlug("Start", type);

    return this;
  }
}
class Attachment {
  constructor() {
    this.node = null;

    return this;
  }
  attach(node) {
    this.node = node;
  }
}
class NodeDragAttachment extends Attachment {
  constructor(onStart = null) {
    super();

    this.onStart = onStart;

    this.dragging = false;
    this.mouseStartPos = {}; // the mouse position when you start dragging to calc the offset
    this.mouseElemOffset = {}; // offset of the mouse position to the element

    this.interactions = window.userInteractionManager;

    return this;
  }
  attach(node) {
    super.attach(node);
    this.interactions.initListeners(node.hRect.elem, (e) => {
      // mousedown
      if (this.onStart) this.onStart(e);
      this.mouseStartPos = {
        x: e.clientX,
        y: e.clientY
      };
      this.nodeStartPos = {
        x: this.node.x,
        y: this.node.y
      }
      this.dragging = true;
    }, () => { }, () => {
      // mouseup
      this.mouseStartPos = {};
      this.dragging = false;
    }, false, 1);
    this.interactions.initListeners(window, () => { }, (e) => {
      // mousemove
      if (!this.dragging) return;
      let xDiff = e.clientX - this.mouseStartPos.x;
      let yDiff = e.clientY - this.mouseStartPos.y;
      let x = this.nodeStartPos.x + xDiff;
      let y = this.nodeStartPos.y + yDiff;
      this.node.setPosition({ x: x, y: y });
    }, () => { });
  }
}
class Connector extends Component {
  static typesCompatible(input, output) {
    // input == the socket
    // output == the plug
    // Do NOT allow connections of data and flow connectors
    if (input == InputSocketComponent.Type.ANY && output !== OutputPlugComponent.Type.CONNECTOR) return true;
    if (input == output) return true;
    const compatible = {
      [InputSocketComponent.Type.NUMBER]: [
        InputSocketComponent.Type.INTEGER,
        InputSocketComponent.Type.FLOAT
      ]
    };
    if (!compatible[input]) return false;
    if (compatible[input].includes(output)) return true;
    return false;
  }
  switchType(type) {
    const desired = new (ConnectorManager.getConnector(type))(this.plug, this.currMousePos, this.absCoords, this.scale, this.color);
    desired.moveTo(this.currMousePos);
    desired.moveStartTo(this.startPos);
    desired.elements.forEach(e => {
      this.container.appendChild(e.render(e.element));
    });
    this.desired = desired;
    this.sCircle.container.remove();
    this.line.container.remove();
    this.eCircle.container.remove();
  }
  constructor(plug, mousePos, absPlugCoords, scale) {
    const startPos = absPlugCoords; // component stuff
    let x = startPos.x + 8 * scale;
    let y = startPos.y + 8 * scale;
    let width = mousePos.x - x;
    let height = mousePos.y - y;
    super(x, y, width, height, scale);

    window.openVS.connectors.push(this); // TODO: z-index stuff, you know what to do

    this.eventElem = document.createElement("span");

    this.plug = plug;
    this.currMousePos = mousePos;
    this.absCoords = absPlugCoords;
    this.id = uid();
    this.startPos = { x: this.x, y: this.y };
    this.moveListener = null;
    this.moveTarget = null;

    return this;
  }
  addEventListener(event, cb) {
    return this.eventElem.addEventListener(event, cb);
  }
  emit(event, data) {
    return this.eventElem.dispatchEvent(new CustomEvent(event, { detail: data }));
  }
  destroy() {
    this.container.remove();
    const idx = window.openVS.connectors.findIndex(e => e.id == this.id);
    if (idx == -1) return;
    window.openVS.connectors.splice(idx, 1);
    return;
  }
  moveTo(mousePos) {
    if (this.desired) return this.desired.moveTo(mousePos);
    this.currMousePos = mousePos;
  }
  moveStartTo(mousePos) {
    if (this.desired) return this.desired.moveStartTo(mousePos);
    this.startPos = mousePos;
    this.setPosition(mousePos); // relocate svg container
  }
  attachStartListener(el) {
    this.plug.interactions.initListeners(el, (e) => {
      this.plug.mouseDown(e);
    }, () => { }, () => { });
  }
  disconnect() {
    this.connectedTo.connected = false;
    this.connectedTo.disconnect(this);
    this.connectedTo = { id: null };
    this.connectedNode = null;
    this.plug.connected.splice(this.plug.connected.findIndex(el => el.id == this.id), 1);
    this.destroy();
  }
  attachMoveListener(el) { // the event to relocate the connector
    this.plug.interactions.initListeners(el, () => {
      this.plug.dragging = true; // the moving and destroying is done in the plugcomponent
      this.plug.snapping = false;
      this.plug.connected.splice(this.plug.connected.findIndex(el => el.id == this.id), 1);
      this.plug.initSnapping();
      this.plug.activeConnector = this;
      if (!this.connectedTo) return console.log("Weird stuff happening here. <Connector>(Line 1612)");
      this.connectedTo.connected = false;
      this.connectedTo.disconnect(this);
      this.moveTarget.removeEventListener("move", this.moveListener);
      this.connectedTo = { id: null };
      this.connectedNode = null;
    }, () => { }, () => { });
  }
  setMoveListener(l, t) {
    this.moveListener = l;
    this.moveTarget = t;
  }
}
class BezierConnector extends Connector {
  constructor(plug, mousePos, absPlugCoords, scale, color) {
    super(plug, mousePos, absPlugCoords, scale);

    this.color = color;

    this.sCircle = new Circle(0, 0, 6 * this.scale, false, this.scale); // the circle connected to the output plug
    this.sCircle.setColor(this.color);
    this.elements.push({ element: this.sCircle, render: (el) => el.createSVGElement() });
    super.attachStartListener(this.sCircle.container);

    const end = {
      x: this.currMousePos.x - this.x,
      y: this.currMousePos.y - this.y
    }

    this.group = new Group();
    super.attachMoveListener(this.group.container);
    this.elements.push({ element: this.group, render: (el) => el.createSVGElement() });

    this.pathBuilder = new PathBuilder();
    this.pathBuilder.moveTo(0, 0);
    this.pathBuilder.cubicCurve(end.x / 2, 0, end.x / 2, end.y, end.x, end.y);
    this.d = this.pathBuilder.build();
    this.line = new Path();
    this.line.path = this.d;
    this.line.setColor("transparent");
    this.line.setStroke({
      stroke: this.color,
      width: 3 * this.scale
    });
    this.group.addComponent(this.line);

    this.eCircle = new Circle(end.x, end.y, 6 * this.scale, false, this.scale);
    this.eCircle.setColor(this.color);
    this.group.addComponent(this.eCircle);
  }
  update() {
    const end = {
      x: this.currMousePos.x - this.x,
      y: this.currMousePos.y - this.y
    }

    this.pathBuilder.clear();
    this.pathBuilder.moveTo(0, 0);
    this.pathBuilder.cubicCurve(end.x / 2, 0, end.x / 2, end.y, end.x, end.y);
    this.line.path = this.pathBuilder.build();
    this.eCircle.setPosition({ x: end.x, y: end.y });
  }
  moveTo(mousePos) {
    super.moveTo(mousePos);

    this.update()
  }
  moveStartTo(mousePos) {
    super.moveStartTo(mousePos);

    this.update();
  }
}
class LineConnector extends Connector {
  constructor(plug, mousePos, absPlugCoords, scale, color) {
    super(plug, mousePos, absPlugCoords, scale);

    this.color = color;

    this.sCircle = new Circle(0, 0, 6 * this.scale, false, this.scale); // the circle connected to the output plug
    this.sCircle.setColor(this.color);
    this.elements.push({ element: this.sCircle, render: (el) => el.createSVGElement() });
    super.attachStartListener(this.sCircle.container);

    this.group = new Group();
    this.elements.push({ element: this.group, render: (el) => el.createSVGElement() });
    super.attachMoveListener(this.group.container);

    this.line = new Line(0, 0, this.currMousePos.x - this.x, this.currMousePos.y - this.y, 3 * this.scale);
    this.line.setColor(this.color);
    this.group.addComponent(this.line);

    this.eCircle = new Circle(this.currMousePos.x - this.x, this.currMousePos.y - this.y, 6 * this.scale, false, this.scale);
    this.eCircle.setColor(this.color);
    this.group.addComponent(this.eCircle);

    return this;
  }
  update() {
    const mousePos = this.mousePos;
    this.line.setPosition({ x: 0, y: 0 }, { x: mousePos.x - this.x, y: mousePos.y - this.y });
    this.eCircle.setPosition({ x: mousePos.x - this.x, y: mousePos.y - this.y });
  }
  moveTo(mousePos) {
    super.moveTo(mousePos);

    this.mousePos = mousePos;
    this.update(mousePos);
  }
  moveStartTo(mousePos) {
    super.moveStartTo(mousePos);

    this.update();
  }
}
class ConnectorManager {
  static BEZIER = BezierConnector;
  static LINE = LineConnector;

  static getConnector(type) {
    switch (type) {
      case "bezier":
        return ConnectorManager.BEZIER;
      case "line":
        return ConnectorManager.LINE;
      default:
        return ConnectorManager.BEZIER;
    }
  }

  constructor() {
    return this;
  }
}

/**
 * @typedef {string} VerticalTextAnchor
 * @property {string} TOP Align the top edge to the y coordinate
 * @property {string} MIDDLE Align the text in the middle of the y coordinate.
 * @property {string} BOTTOM Align the bottom of the text to the y coordinate.
 * @see See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/dominant-baseline}
 * for the documentation on HTML level.
 */
/**
 * @typedef {string} HorizontalTextAnchor
 * @property {string} START Align the start of the text to the x coordinate
 * @property {string} MIDDLE Aligne the middle of the text to the x coordinate
 * @property {string} END Align the end of the text to the x coordinate
 * @see See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor}
 * for an explanation of it.
 */
/**
 * @class
 * @classdesc A basic component to display text.
 */
class Text {
  /**
   * @enum {HorizontalTextAnchor}
   * @description Valid values for horizontal anchoring of the text.
   */
  static Anchor = {
    START: "start",
    MIDDLE: "middle",
    END: "end"
  }
  /**
   * @enum {VerticalTextAnchor}
   * @description Valid values for vertical anchoring of the text.
   */
  static VerticalAnchor = {
    TOP: "hanging",
    MIDDLE: "middle",
    BOTTOM: "auto"
  }
  /**
   * @description Initiates a new Text object.
   *
   * @param  {number} x                                  The x position in the parent container or viewport
   * @param  {number} y                                  The y position in the parent container or viewport
   * @param  {string} text                               The actual text content of the Text element.
   * @param  {number} scale                              The scale of the text element
   * @param  {HorizontalTextAnchor} anchor=Text.Anchor.START Aligns the text horizontally.
   * @param  {VerticalTextAnchor} vAnchor=Text.VerticalAnchor.BOTTOM Aligns the text vertically.
   * @return {Text}                                      The new Text object.
   *
   * @see See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor}
   * for more information about the horizontal anchor and {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/dominant-baseline}
   * for the vertical anchor.
   */
  constructor(x, y, text, scale, anchor = Text.Anchor.START, vAnchor = Text.VerticalAnchor.BOTTOM) {
    this.x = x;
    this.y = y;
    this.txt = text;
    this.scale = scale;
    this.anchor = anchor;
    this.vAnchor = vAnchor;
    this.isComponent = true;

    this.container = document.createElementNS("http://www.w3.org/2000/svg", "text");
    this.container.style.userSelect = "text";
    this.updateAttributes();
  }
  set fontSize(s) {
    this.fs = s;
    this.container.style.fontSize = s + "px";
  }
  get fontSize() {
    return this.fs;
  }
  static getCSSStyle(el, prop) {
    return window.getComputedStyle(el, null).getPropertyValue(prop);
  }
  static getCanvasFont(el = document.body) {
    const fontWeight = Text.getCSSStyle(el, 'font-weight') || 'normal';
    const fontSize = Text.getCSSStyle(el, 'font-size') || '16px';
    const fontFamily = Text.getCSSStyle(el, 'font-family') || 'Times New Roman';

    return `${fontWeight} ${fontSize} ${fontFamily}`;
  }
  static measureText(text, font = Text.getCanvasFont()) {
    window.openvs_canvas = window.openvs_canvas || (window.openvs_canvas = document.createElement("canvas"));
    const context = window.openvs_canvas.getContext("2d");
    context.font = font;
    const metrics = context.measureText(text);
    metrics.fontHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
    metrics.height = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
    return metrics;
  }
  setColor(color) {
    this.color = color;
    this.updateAttributes();
  }
  updateAttributes() {
    const text = this.container;
    text.innerHTML = this.txt;
    //    text.style.fontSize = parseInt(text.style.fontSize.replace("px", "")) * this.scale + "px";
    text.style.textAnchor = this.anchor;
    text.style.dominantBaseline = this.vAnchor; // the vertical alignment
    text.setAttribute("x", this.x);
    text.setAttribute("y", this.y);
    if (this.color) text.setAttribute("fill", this.color);
    if (this.scale) text.style.transform = "scale(" + this.scale + ")";
  }
  setPosition(pos) {
    this.x = pos.x;
    this.y = pos.y;
  }
  setScale(scale) {
    this.scale = scale
    this.updateAttributes();
  }
  setComponentScale(newScale) {
    this.setScale(newScale);
  }

  move(deltaX, deltaY) {
    if ((!deltaX && deltaX !== 0) || ((!deltaY && deltaY !== 0))) throw "Invalid parameters! [" + this.constructor.name + ".move(deltaX, deltaY)]";
    this.x += deltaX;
    this.y += deltaY;
    this.updateAttributes();
    return { x: this.x, y: this.y };
  }
  /**
   * @description Set the text content of this element.
   *
   * @param  {string} t The new text content
   * @return {void}
   */
  setText(t) {
    this.txt = t;
    this.container.innerHTML = t;
  }
  createSVGElement() {
    return this.container;
  }
}
class Line {
  constructor(x, y, x1, y1, width) {
    this.x = x;
    this.y = y;
    this.x1 = x1; // end position of the current connector
    this.y1 = y1;
    this.width = width;
    this.isComponent = true;

    this.d = ""; // path instructions
    this.color = "";

    this.container = document.createElementNS("http://www.w3.org/2000/svg", "path");
    this.updateAttributes();

    return this;
  }
  setColor(color) {
    this.color = color;
    this.updateAttributes();
  }
  setPosition(pos1, pos2) {
    this.x = pos1.x;
    this.y = pos1.y;
    this.x1 = pos2.x;
    this.y1 = pos2.y;
    this.updateAttributes();
  }
  updateAttributes() {
    const path = this.container;
    this.d = "M " + this.x + " " + this.y + " "; // start position
    this.d += "L " + this.x1 + " " + this.y1; // end position
    path.setAttribute("d", this.d);
    path.setAttribute("stroke-width", this.width);
    if (this.color) path.setAttribute("stroke", this.color);
  }
  createSVGElement() {
    return this.container;
  }
}
class Path {
  constructor() {
    this.container = document.createElementNS("http://www.w3.org/2000/svg", "path");

    return this;
  }
  set path(d) {
    this.d = d;
    this.updateAttributes();
  }
  get path() {
    return this.d;
  }
  setStroke(opts) {
    this.stroke = opts.stroke;
    this.strokeWidth = opts.width;
    this.updateAttributes();
  }
  setColor(c) {
    this.fill = c;
    this.updateAttributes();
  }
  updateAttributes() {
    this.container.setAttribute("stroke", this.stroke);
    this.container.setAttribute("stroke-width", this.strokeWidth);
    this.container.setAttribute("fill", this.fill);
    this.container.setAttribute("d", this.d);
  }
  createSVGElement() {
    return this.container
  }
}
class PathBuilder {
  constructor() {
    this.instructions = [];

    return this;
  }
  build() {
    return this.instructions.reduce((prev, curr) => {
      if (typeof prev != "string") {
        return prev.command + prev.content + curr.command + curr.content;
      }
      return prev + curr.command + curr.content;
    });
  }
  uid() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2);
  }
  moveTo(x, y, relative = false) {
    const instruction = {
      command: (relative) ? "m" : "M",
      content: " " + x + " " + y,
      id: this.uid()
    }
    this.instructions.push(instruction);
    return instruction.id;
  }
  lineTo(x, y, relative = false) {
    const instruction = {
      command: (relative) ? "l" : "L",
      content: " " + x + " " + y,
      id: this.uid()
    }
    this.instructions.push(instruction);
    return instruction.id;
  }
  cubicCurve(x1, y1, x2, y2, x, y, relative = false) {
    const instruction = {
      command: (relative) ? "c" : "C",
      content: " " + x1 + " " + y1 + " " + x2 + " " + y2 + " " + x + " " + y,
      id: this.uid()
    }
    this.instructions.push(instruction);
    return instruction.id;
  }
  closePath() {
    const instruction = {
      command: "Z",
      content: "",
      id: this.uid()
    };
    this.instructions.push(instruction);
    return instruction.id;
  }
  getInstruction(id) {
    return this.instructions.filter(el => el.id == id)[0];
  }
  clear() {
    this.instructions = [];
  }
}
class Circle {
  constructor(x, y, radius, cornerCoords, scale=1) {
    this.x = (cornerCoords) ? x + radius : x;
    this.y = (cornerCoords) ? y + radius : y;
    this.ox = x; // original x and y
    this.oy = y;
    this.r = radius;
    this.cornerCoords = cornerCoords;
    this.isComponent = true;
    this.scale = scale;

    this.container = document.createElementNS("http://www.w3.org/2000/svg", "circle");
    this.updateAttributes();

    return this;
  }
  addEventListener(event, cb) {
    this.container.addEventListener(event, cb);
  }
  set radius(r) {
    this.r = r;
    if (!this.cornerCoords) return this.updateAttributes();
    this.x = this.ox + r;
    this.y = this.oy + r;
    return this.updateAttributes();
  }
  get radius() {
    return this.r;
  }
  setRadius(r, changePos = false) {
    this.r = r;
    if (!changePos || !this.cornerCoords) return this.updateAttributes();
    this.x = this.ox + r;
    this.y = this.oy + r;
    return this.updateAttributes();
  }
  setPosition(pos) {
    this.x = (this.cornerCoords) ? pos.x + this.r : pos.x;
    this.y = (this.cornerCoords) ? pos.y + this.r : pos.y;
    this.updateAttributes();
  }
  setColor(color) {
    this.color = color;
    this.updateAttributes();
  }
  setStroke(opts) {
    this.stroke = opts.stroke;
    this.strokeWidth = opts.width;
    this.updateAttributes();
  }
  update(parent) {
    this.scale = parent.scale;
    this.updateAttributes();
  }
  updateAttributes() {
    const circle = this.container;
    circle.setAttribute("cx", this.x);
    circle.setAttribute("cy", this.y);
    circle.setAttribute("r", this.r);
    if (this.stroke) circle.setAttribute("stroke", this.stroke);
    if (this.strokeWidth) circle.setAttribute("stroke-width", this.strokeWidth);
    if (this.color) circle.setAttribute("fill", this.color);
  }
  setComponentScale(newScale) {
    const ratio = newScale / this.scale;
    this.r = this.r * ratio;
    if (this.cornercoords) {
      this.x = this.ox + this.r;
      this.y = this.oy + this.r;
    }
    this.updateAttributes();
  }
  createSVGElement() {
    return this.container;
  }
}
class Rectangle {
  constructor(x, y, width, height, rounded = false, radius = 0) {
    this.x = x;
    this.y = y;
    this.height = height;
    this.width = width;
    this.oheight = JSON.parse(JSON.stringify(height)); // copy, don't reference the values
    this.owidth = JSON.parse(JSON.stringify(width));
    this.rounded = rounded;
    this.isComponent = true;

    this.radius = radius;
    this.oradius = JSON.parse(JSON.stringify(radius));
    this.rx = (radius !== 0) ? radius : 0.3;
    this.ry = (radius !== 0) ? radius : 0.3;

    this.shadow = null;
    this.visible = true;

    this.eventElem = document.createElement("span");
    this.clickEvent = new Event("click");

    const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
    this.elem = rect;

    return this;
  }
  setVisible(visible) {
    this.visible = visible;
    this.elem.style.display = (visible) ? "block" : "none";
  }
  setPosition(pos) {
    this.x = pos.x;
    this.y = pos.y;
    this.updateAttributes();
  }
  setColor(color) {
    this.color = color;
  }
  setStroke(opts) {
    this.stroke = opts.color;
    this.strokeWidth = opts.width;
    this.updateAttributes();
  }
  setShadow(shadow) {
    this.shadow = shadow;
  }
  setClipPath(path) {
    this.clipPath = path;
  }
  addEventListener(event, cb) {
    //return this.eventElem.addEventListener(event, cb);
    return this.elem.addEventListener(event, cb);
  }
  createClipPath(yOffset) {
    const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");

    const path = document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
    let id = "rounded" + (new Date()).getTime();
    path.id = id;
    defs.appendChild(path);

    const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
    rect.setAttribute("x", 0);
    rect.setAttribute("y", yOffset);
    rect.setAttribute("width", this.width);
    rect.setAttribute("height", this.height + this.radius);
    if (rect.getAttribute("width") == 0) rect.removeAttribute("width");
    if (rect.getAttribute("height") == 0) rect.removeAttribute("height");
    rect.setAttribute("rx", this.radius);
    rect.setAttribute("ry", this.radius);
    path.appendChild(rect);

    return { element: defs, id: id };
  }
  setHeight(h) {
    this.height = (this.scale) ? h * this.scale : h;
    this.oheight = h;
    this.updateAttributes();
  }
  setWidth(w) {
    this.width = (this.scale) ? w * this.scale : w;
    this.owidth = w;
    this.updateAttributes();
  }
  updateAttributes() {
    const rect = this.elem;
    rect.setAttribute("x", this.x);
    rect.setAttribute("y", this.y);
    rect.setAttribute("width", this.width);
    rect.setAttribute("height", this.height);
    if (this.color) rect.setAttribute("fill", this.color);
    if (this.stroke) rect.setAttribute("stroke", this.stroke);
    if (this.strokeWidth) rect.setAttribute("stroke-width", this.strokeWidth);
    if (this.shadow) rect.setAttribute("filter", "url(#" + this.shadow + ")");
    if (this.clipPath) {
      rect.setAttribute("clip-path", "url(#" + this.clipPath + ")");
      return rect;
    }
    if (!this.rounded) return rect;
    rect.setAttribute("rx", this.rx);
    rect.setAttribute("ry", this.ry);
  }
  createSVGElement() {
    this.updateAttributes();
    /*rect.onclick = () => {
      this.eventElem.dispatchEvent(this.clickEvent);
    }*/
    return this.elem;
  }
  updateClip() {
    const e = document.querySelector("#" + this.clipPath); // get the clipPath element
    e.setAttribute("width", this.width);
    e.setAttribute("height", this.height + this.rx);

    const rect = e.children[0];
    rect.setAttribute("rx", this.rx);
    rect.setAttribute("ry", this.ry);
    rect.setAttribute("width", this.width);
    rect.setAttribute("height", this.height + this.rx);
  }
  setComponentScale(scale) {
    this.scale = scale;
    this.height = this.oheight * scale; // oheight, owidth, etc to use the original values
    this.width = this.owidth * scale;
    this.radius = this.oradius * scale;
    this.rx = (this.radius !== 0) ? this.radius : 0.3;
    this.ry = (this.radius !== 0) ? this.radius : 0.3;
    if (this.clipPath) {
      this.updateClip();
    }
    this.updateAttributes();
  }
}
class SVGInput extends Component {
  constructor(x, y, width, scale, cb = () => { }) {
    super(x, y, width, 18, scale);

    this.htmlContainer = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
    this.htmlContainer.setAttribute("width", this.tw);
    this.htmlContainer.setAttribute("height", this.th);
    this.elements.push({ element: this.htmlContainer, render: (el) => el });

    this.input = document.createElement("input");
    this.input.classList.add("openvs_graphics_input");
    this.htmlContainer.appendChild(this.input);

    this.storedData = "";
    this.cb = cb;

    var lastValue = "";
    var reset = false;
    this.input.onblur = () => {
      if (reset) { reset = false; return this.input.value = lastValue; }
      this.storedData = this.input.value;
      this.cb(this.input.value);
    }
    this.input.onfocus = () => {
      lastValue = this.input.value;
    }
    this.input.onkeyup = (e) => {
      if (!(e.code === "Enter" || e.code === "Escape")) return;
      if (e.code === "Escape") reset = true;
      this.input.blur();
    }

    return this;
  }
}
class SVGSelect extends Component {
  constructor(x, y, width, scale, cb = null, ccb = null) {
    super(x, y, width, 18, scale);

    this.callback = cb;
    this.ccb = ccb; // ccb == (called when the dropdown is opened)
    this.maxHeight = (18 * 5 + 4) * this.scale

    this.dropRect = new Rectangle(0, 0, this.tw, this.th, true, 3);
    this.dropRect.setColor("#121212");
    this.dropRect.setStroke({
      color: "#0f0f0f",
      width: 1
    });
    this.dropRect.elem.style.overflow = "scroll";
    this.dropRect.elem.style.transition = "0.2s";
    this.elements.push({ element: this.dropRect });

    this.body = new ScrollComponent(0, this.th, this.tw, 0, scale);
    this.body.rect.style.transition = "0.2s";
    this.elements.push({ element: this.body });

    this.bgRect = new Rectangle(0, 0, this.tw, this.th, true, 3);
    this.bgRect.setColor("#121212");
    this.bgRect.setStroke({
      color: "#0f0f0f",
      width: 1
    });
    this.bgRect.elem.id = "bgRect";
    this.elements.push({ element: this.bgRect });

    this.selected = new Text(3, 3.5, "", this.scale, Text.Anchor.START, Text.VerticalAnchor.TOP);
    this.selected.container.style.fontSize = (14 * this.scale) + "px";
    this.selected.container.style.userSelect = "none";
    this.selected.container.id = uid();
    this.selected.setColor("#808080");
    this.elements.push({ element: this.selected });

    this.builder = new PathBuilder();
    this.builder.moveTo(this.tw - 10, 7);
    this.builder.lineTo(3, 4, true);
    this.builder.lineTo(3, -4, true);
    this.path = new Path();
    this.path.path = this.builder.build();
    this.path.setColor("transparent");
    this.path.setStroke({
      stroke: "#808080",
      width: 1
    });
    this.default = this.builder.build();
    this.path.container.style.strokeLinejoin = "round";
    this.path.container.style.strokeLinecap = "round";
    this.path.container.style.transition = "0.2s";
    this.path.container.id = "arrow";
    this.elements.push({ element: this.path });

    // the d attribute for the upside-down arrow onclick with animation
    this.builder.clear();
    this.builder.moveTo(this.tw - 10, 11);
    this.builder.lineTo(3, -4, true);
    this.builder.lineTo(3, 4, true);
    this.flipped = this.builder.build();

    this.expanded = 0;
    this.resetPlaceholder = false; // true == placeholder will stay the same
    this.items = [];

    this.container.addEventListener("pointerup", (e) => {
      if (e.target.id == this.bgRect.elem.id || e.target.id == this.path.container.id || e.target.id == this.selected.container.id) {
        this.toggle();
      }
    });

    return this;
  }
  addItem(label, id, cb = null) {
    const text = new Text(3, (18 * (this.items.length + 1) - 3) * this.scale, label, this.scale, Text.Anchor.START, Text.VerticalAnchor.BOTTOM);
    text.setColor("#808080");
    this.body.addComponent(text);
    this.items.push({ id: id, elem: text, cb: cb });
    text.container.addEventListener("pointerup", (e) => {
      const data = {
        selected: id,
        label: label,
        target: e.target,
        clientPos: {
          x: e.clientX,
          y: e.clientY
        }
      };
      if (cb) cb.call(this, data);
      if (this.callback) this.callback.call(this, data);
      if (!this.resetPlaceholder) this.selected.setText(label);
      this.expanded = 0;
      this.collapse();
    });
  }
  get dynamicPlaceholder() {
    return !this.resetPlaceholder;
  }

  /**
   * @description change wether to change the placeholder of the select to the selected item on click
   * @member
   * @param  {boolean} bool If this is set to true, then the default placeholder will be exchanged with the content of the item that was clicked.
   * @return {void}
   */
  set dynamicPlaceholder(bool) {
    this.resetPlaceholder = !bool;
  }
  toggle() {
    if (this.expanded) {
      this.collapse();
    } else {
      this.expand();
    }
    this.expanded = 1 - this.expanded; // math magic :D
    this.ccb.call(this, this.expanded);
  }
  expand() {
    this.bgRect.setStroke({
      color: "black",
      width: 1
    });
    this.path.container.style.d = 'path("' + this.flipped + '")';

    const h = Math.min(this.maxHeight, (18 * (this.items.length + 1)) * this.scale);
    console.log(h, this.dropRect, this.body);
    this.dropRect.setHeight(h);
    this.body.setHeight(h - this.th);
  }
  collapse() {
    this.bgRect.setStroke({
      color: "#0f0f0f",
      width: 1
    });
    this.path.container.style.d = 'path("' + this.default + '")';
    this.dropRect.setHeight(this.th);
    this.body.setHeight(0);
  }
}
class ScrollComponent extends Component {
  constructor(x, y, width, height, scale) {
    super(x, y, width, height, scale);

    this.defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
    this.elements.push({ element: this.defs, render: (el) => el });

    this.cPath = document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
    this.cPath.id = uid();
    this.defs.appendChild(this.cPath);
    this.rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
    this.rect.setAttribute("x", 0);
    this.rect.setAttribute("y", 0);
    this.rect.setAttribute("width", width);
    this.rect.setAttribute("height", height);
    this.cPath.appendChild(this.rect);

    this.container.style.overflow = "visible";
    this.container.setAttribute("clip-path", "url(#" + this.cPath.id + ")");

    this.content = new Viewport(0, 0);
    this.baseScrollDelta = 150;
    this.container.addEventListener("wheel", (e) => {
      if (e.cancelable) e.preventDefault();
      let sY = Math.min(this.scrollY + (this.th * 0.2) * (this.baseScrollDelta / e.deltaY), this.th - (this.content.container.getBBox().height - this.th));
      this.scrollTo(0, (sY < 0) ? 0 : sY);
    });
    this.tPosY;
    this.container.addEventListener("touchstart", (e) => {
      this.tPosY = e.changedTouches[0].clientY;
    });
    this.container.addEventListener("touchmove", (e) => {
      if (e.cancelable) e.preventDefault();
      let newPos = e.changedTouches[0].clientY;
      if (this.tPosY > newPos) {
        let sY = Math.min(this.scrollY + ((this.content.container.getBBox().height - this.th) * 0.2), this.th - (this.content.container.getBBox().height - this.th));
        this.scrollTo(0, (sY < 0) ? 0 : sY);
      } else {
        let sY = Math.min(this.scrollY + ((this.content.container.getBBox().height - this.th) * -0.2), this.th - (this.content.container.getBBox().height - this.th));
        this.scrollTo(0, (sY < 0) ? 0 : sY);
      }
    });
    this.elements.push({ element: this.content });

    const scrollHeight = this.th;
    this.scrollBar = new Rectangle(this.tw - 2 - 5, 0, 5, scrollHeight, true, 2);
    this.scrollBar.setColor("#060606");
    this.elements.push({ element: this.scrollBar });

    this.scrollY = 0;

    return this;
  }
  scrollTo(x, y) {
    this.scrollY = y;
    this.content.setPosition({ x: -1 * x, y: -1 * y });

    // move scroll bar
    let factor = y / (this.content.container.getBBox().height - this.th);
    this.scrollBar.setPosition({ x: this.scrollBar.x, y: y * factor });
  }
  setHeight(h) {
    this.height = h;
    this.th = h * this.scale;
    this.rect.setAttribute("height", h);

    // height * (percentage of the overlapping content towards the current height)
    const height = (this.th * ((this.content.container.getBBox().height - this.th) / 100));
    this.scrollBar.setHeight(height);
  }
  setWidth(w) {
    this.width = w;
    this.tw = w * this.scale;
    this.rect.setAttribute("width", w);
  }
  addComponent(c, render = (el) => el.createSVGElement()) {
    this.content.addComponent(c, render);
  }
}
class SVGCheckbox extends Component {
  constructor(x, y, scale, checked = false, clickCallback = () => { }) {
    super(x, y, 18, 18, scale);

    this.checked = checked;
    this.toggled = (checked) ? 1 : 0;
    this.clickCallback = clickCallback;

    // the box
    this.bgRect = new Rectangle(0, 0, this.tw, this.th, true, 3);
    this.bgRect.setColor("#121212");
    this.bgRect.setStroke({
      color: "#0f0f0f",
      width: 1
    });
    this.elements.push({ element: this.bgRect });

    // checkmark
    this.builder = new PathBuilder();
    this.builder.moveTo(3 * this.scale, this.th / 2);
    this.builder.lineTo(this.tw / 2 - 1 * this.scale, this.th - (3 * this.scale));
    this.builder.lineTo(this.tw - (3 * this.scale), 3 * this.scale);
    this.mark = new Path();
    this.mark.path = this.builder.build();
    this.mark.setColor("transparent");
    this.mark.setStroke({
      stroke: "#747474",
      width: 2
    });
    this.mark.container.style.display = (checked) ? "block" : "none";
    this.elements.push({ element: this.mark });

    // "clickability":
    this.container.addEventListener("pointerdown", () => { // include touch and mouse clicks
      this.toggle(this.clickCallback);
    });

    return this;
  }
  toggle(cb) {
    this.toggled = 1 - this.toggled;
    this.checked = (this.toggled) ? true : false;
    this.mark.container.style.display = (this.toggled) ? "block" : "none";
    cb(this.toggled); // call the callback function with the current state
  }
  check() {
    this.mark.container.style.display = "block";
    this.checked = true;
  }
  uncheck() {
    this.mark.container.style.display = "none";
    this.checked = false;
  }
  setScale(s) {
    this.scale = s;
    this.updateAttributes();
  }
  updateAttributes() {
    super.updateAttributes();
    if (!this.builder) return; // happens when the super constructor is called
    this.builder.clear();
    this.builder.moveTo(3 * this.scale, this.th / 2);
    this.builder.lineTo(this.tw / 2 - 1 * this.scale, this.th - (3 * this.scale));
    this.builder.lineTo(this.tw - (3 * this.scale), 3 * this.scale);
    this.mark.path = this.builder.build();
  }
}
class HTMLCheckbox {
  constructor(x, y, scale) {
    this.x = x;
    this.y = y;
    this.scale = scale;

    this.container = document.createElement("input");
    this.container.type = "checkbox";
    this.container.classList.add("openvs_graphics_checkbox");
    this.container.style.position = "absolute";
    this.updateAttributes();

    this.style = document.createElement("style");
    document.head.appendChild(this.style);

    return this;
  }
  createStyleDeclaration() {
    var style = ".openvs_graphics_checkbox:checked:after {\n";
    style += "font-size: " + (14 * this.scale) + "px;\n";
    style += "left: " + (3 * this.scale) + "px;\n";
    style += "}";
    return style;
  }
  updateAttributes() {
    this.container.style.top = this.y + "px";
    this.container.style.left = this.x + "px";
    if (this.scale) {
      this.style.innerHTML = this.createStyleDeclaration();
      this.container.style.padding = (9 * this.scale) + "px";
    }
  }
  setPosition(pos) {
    this.x = pos.x;
    this.y = pos.y;
    this.updateAttributes();
  }
  setScale(s) {
    this.scale = s;
    this.updateAttributes();
  }
  createSVGElement() {
    return this.container;
  }
}
class RasterBackground {
  constructor(x, y, width, height, zoom) {
    this.colors = {
      background: "#141414",
      dot: "#1c1c1c"
    }

    // TODO: Spam dragging while slow movement bug fix!

    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.zoom = zoom;
    this.interactions = window.userInteractionManager;

    this.container = document.createElementNS("http://www.w3.org/2000/svg", "g");

    this.bg = new Rectangle(0, 0, this.width, this.height);
    this.bg.setColor(this.colors.background);

    //this.baseDist = this.width / 23;
    this.dotRad = 5 * 0.675;

    // panning support
    this.bgPos = { // pan position
      x: 0,
      y: 0
    };
    this.mouseStartPos = {
      x: 0,
      y: 0
    };
    this.dragging = false;
    this.toggleDragging = false;

    return this;
  }
  attach(engine) { // called on attaching to an SVGEngine
    this.engine = engine;
  }
  pan(xDiff, yDiff, cXDiff, cYDiff) { // cDiff == the changes since the last event
    let dotDiffX = xDiff % this.distance; // the difference a single dot has to move
    let dotDiffY = yDiff % this.distance;

    /*this.dots.forEach((dot) => {
      dot.setPosition({ x: dot.ox + dotDiffX, y: dot.oy + dotDiffY });
    });*/

    if (!this.engine) return;
    this.engine.components.forEach(d => {
      let component = d.component;
      component.setPosition({ x: component.x + cXDiff, y: component.y + cYDiff });
    });
  }
  initPanning() {
    this.interactions.cancelCtxMenu(this.container.children[0]);
    this.interactions.initListeners(this.container, (e) => {
      // mousedown
      if (this.toggleDragging) return;
      if (e.button || e.button == 0) if (e.button != 2) return;
      this.dragging = true;
      this.mouseStartPos = {
        x: e.clientX,
        y: e.clientY
      };
    }, () => { }, (_e) => { }, false);
    this.interactions.initListeners(window, () => { }, (e) => {
      // mousemove on windows to prevent glitching when noving mouse over other elements
      if (!this.dragging) return;
      let xDiff = e.clientX - this.mouseStartPos.x;
      let yDiff = e.clientY - this.mouseStartPos.y;
      this.bgPos.x += xDiff;
      this.bgPos.y += yDiff;
      this.pan(xDiff, yDiff, e.movementX, e.movementY);
    }, () => {
      // mouseup
      this.dragging = false;
      this.mouseStartPos = { x: 0, y: 0 };
    }, false)

    // middle click panning
    this.interactions.initListeners(this.engine.body.container, (e) => {
      if (e.button !== 1) return;
      /*this.renderContainer = this.engine.element;
      this.toggleDragging = 1 - this.toggleDragging; // actually toggle the value
      this.mouseStartPos = {
        x: e.clientX,
        y: e.clientY
      };*/
      this.dragging = true;
      this.mouseStartPos = {
        x: e.clientX,
        y: e.clientY
      };
    }, (e) => {
      /*if (!this.toggleDragging) return;
      let xDiff = e.clientX - this.mouseStartPos.x;
      let yDiff = e.clientY - this.mouseStartPos.y;
      this.bgPos.x += xDiff;
      this.bgPos.y += yDiff;
      this.pan(xDiff, yDiff, e.movementX, e.movementY);*/
    }, () => {}, false);
  }
  createDots() {
    this.distance = 35; //this.baseDist * this.zoom;
    this.rad = this.dotRad //* this.zoom; don't scale this as this is done by the svg engine

    this.dots = [];
    this.columns = Math.ceil(this.width / this.distance);
    this.rows = Math.ceil(this.height / this.distance);

    return;
    for (let i = 0; i < this.columns; i++) {
      for (let j = 0; j < this.rows; j++) {
        const circle = new Circle(this.distance * (i), this.distance * (j), this.rad, false);
        circle.setColor(this.colors.dot);
        this.dots.push(circle);
      }
    }
  }
  createSVGElement() {
    this.container.innerHTML = "";
    this.createDots();
    this.container.append(this.bg.createSVGElement());
    this.container.append(...this.dots.map(el => el.createSVGElement()));
    this.initPanning();
    return this.container;
  }
  dispatchEvent(type, data) {
    return this[type + "Event"](data);
  }
  scaleEvent(data) {
    this.zoom = data;
    this.width /= data;
    this.height /= data;
    this.createSVGElement();
  }
}

/**
 * @class
 * @classdesc A Button Element
 * @augments Component
 */
class SVGButton extends Component {
  text = "Button";
  constructor(x, y, width, height, scale, text) {
    super(x, y, width, height, scale);

    this.text = text;

    // text color: #808080

    this.bgrd = new Rectangle(x, y, width, height, true);
    this.bgrd.setColor("#121212");
    this.bgrd.setStroke({ stroke: "#0f0f0f", width: 1 })
    this.elements.push({ element: this.bgrd });

    this.text = new Text()
  }
}

class OpenVSPlugin extends Component {
  engine = null;
  constructor(x, y, width=0, height=0, scale=1) {
    super(x, y, width, height, scale);
  }
  attach(engine) {
    // this.setScale(engine.scale);
    this.engine = engine;
    this.engine.addComponent(this);
  }
}
class SelectionPlugin extends OpenVSPlugin {
  constructor() {
    super(0, 0);

    this.bgrd = new Rectangle(0, 0, 2, 2, true, 2.5);
    this.bgrd.setColor("rgba(37, 150, 190, 0.6)");
    this.bgrd.setStroke({ color: "#1a6c89", width: 1 });
    this.bgrd.setVisible(false);
    this.elements.push({ element: this.bgrd });

    this.selecting = false;
    this.mouseStartPos = {
      x: 0,
      y: 0
    }

    this.interactions = window.userInteractionManager;
  }
  computeDimensions(width, height) { // needed because svg rects can't have negative heights and widths (obviously)
    return {
      width: Math.abs(width),
      height: Math.abs(height),
      x: (width < 0) ? this.mouseStartPos.x - Math.abs(width) : this.mouseStartPos.x,
      y: (height < 0) ? this.mouseStartPos.y - Math.abs(height) : this.mouseStartPos.y,
    }
  }
  attach(...args) {
    super.attach(...args);
    this.interactions.initListeners(this.engine.element, (e) => {
      if (e.target != this.engine.background.bg.elem) return;
      if (e.button != 0) return;
      this.renderContainer = this.engine.element;
      this.selecting = true;
      this.mouseStartPos = {
        x: e.clientX,
        y: e.clientY
      };
      this.bgrd.setVisible(true);
      this.setPosition({ x: this.mouseStartPos.x, y: this.mouseStartPos.y })
      this.moveToTop();
    }, () => { }, (_e) => {
      this.selecting = false;
      this.mouseStartPos = { x: 0, y: 0 };
      this.bgrd.setVisible(false);
      this.bgrd.setWidth(2);
      this.bgrd.setHeight(2);
    }, false);
    this.interactions.initListeners(window, () => { }, (e) => {
      if (!this.selecting) return;
      const currPosX = e.clientX;
      const currPosY = e.clientY;

      const dims = this.computeDimensions(currPosX - this.mouseStartPos.x, currPosY - this.mouseStartPos.y);
      this.bgrd.setWidth(dims.width);
      this.bgrd.setHeight(dims.height);
      this.setPosition(dims);
    }, () => { }, false);
  }
}

/**
 * @class
 * @classdesc The object managing the graphics and components
 */
class SVGEngine {
  variables = new Map()
  registry = null;
  plugins = [];
  constructor() {
    this.element = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    this.element.setAttribute("height", window.innerHeight);
    this.element.setAttribute("width", window.innerWidth);
    this.element.style.touchAction = "none";
    this.element.style.userSelect = "none";
    this.element.id = "ULVS-Engine_" + (new Date()).getTime();
    document.body.appendChild(this.element);

    this.scale = 1;
    this.outline = null;

    // separated container for connector elements so they stay on top
    /*this.connectorContainer = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    this.connectorContainer.id = "connectors";
    this.element.appendChild(this.connectorContainer);*/

    // TODO: optimize panning; sub-viewport that just gets moved around (maybe)

    this.width = window.innerWidth;
    this.height = window.innerHeight;

    // setup global variables
    window.openVS = {
      nodes: {

      },
      connectors: [

      ]
    }

    this.body = new Viewport(0, 0, this.scale);
    this.body.container.classList.add("nodes");
    this.body.setViewboxAnchor(Transform.vXAnchor.CENTER, Transform.vYAnchor.MIDDLE);
    this.element.appendChild(this.body.createSVGElement());

    this.components = [];
    this.interfaces = [];
    this.scale = 1;
    this.maxZoom = {
      in: 1.7,
      out: 0.4
    }

    window.uid = () => {
      return Date.now().toString(36) + Math.random().toString(36).substr(2);
    }

    this.top = SVGEngine.getAbsCoords(this.element).y;
    this.left = SVGEngine.getAbsCoords(this.element).x;

    this.connTypeToggle = 1;

    if (!window.userInteractionManager) {
      Object.defineProperty(window, "userInteractionManager", {
        get: function() {
          if (this.openVS.readyIManager) return this.openVS.readyIManager;
          this.openVS.readyIManager = new UserInteractionManager();
          return this.openVS.readyIManager;
        }
      });
    }

    this.generateStyles();

    return this;
  }
  addPlugin(plugin) {
    this.plugins.push(plugin);
    plugin.attach(this);
  }
  setNodeRegistry(r) {
    this.registry = r;
  }

  create(elem) {
    return document.createElementNS("http://www.w3.org/2000/svg", elem);
  }
  get outlineFilter() {
    if (this.outline) return this.outline;
    const filter = this.create("filter");
    filter.id = "OVSOutline" + uid();
    filter.setAttribute("x", "-50%");
    filter.setAttribute("y", "-50%");
    filter.setAttribute("width", "200%");
    filter.setAttribute("height", "200%");
    this.element.appendChild(filter);
    this.outline = filter.id;

    const outCol = this.create("feFlood");
    outCol.setAttribute("flood-color", "white");
    outCol.setAttribute("result", "outside-color");
    filter.appendChild(outCol);

    const morph1 = this.create("feMorphology");
    morph1.setAttribute("in", "SourceAlpha");
    morph1.setAttribute("operator", "dilate");
    morph1.setAttribute("radius", "1");
    filter.appendChild(morph1);

    const com1 = this.create("feComposite");
    com1.setAttribute("in", "outside-color");
    com1.setAttribute("operator", "in");
    com1.setAttribute("result", "outside-stroke");
    filter.appendChild(com1);

    const inCol = this.create("feFlood");
    inCol.setAttribute("flood-color", "white");
    inCol.setAttribute("result", "inside-color");
    filter.appendChild(inCol);

    const com2 = this.create("feComposite");
    com2.setAttribute("in2", "SourceAlpha");
    com2.setAttribute("operator", "in");
    com2.setAttribute("result", "inside-stroke");
    filter.appendChild(com2);

    const morph2 = this.create("feMorphology");
    morph2.setAttribute("in", "SourceAlpha");
    morph2.setAttribute("radius", "2");
    filter.appendChild(morph2);

    const com3 = this.create("feComposite");
    com3.setAttribute("in", "SourceGraphic");
    com3.setAttribute("operator", "in");
    com3.setAttribute("result", "fill-area");
    filter.appendChild(com3);

    const merge = this.create("feMerge");
    filter.appendChild(merge);

    const m1 = this.create("feMergeNode");
    m1.setAttribute("in", "outside-stroke");
    merge.appendChild(m1);

    const m2 = this.create("feMergeNode");
    m2.setAttribute("in", "inside-stroke");
    merge.appendChild(m2);

    const m3 = this.create("feMergeNode");
    m3.setAttribute("in", "fill-area");
    merge.appendChild(m3);

    return this.outline;
  }

  addUI(interf) {
    interf.attachEngine(this);
    this.interfaces.push(interf);
  }

  registerVariable(name) {
    if (this.variables.includes(name)) return false;
    this.variables.push(name);
    return true;
  }

  exportProgram() {
    const mapFlow = (start) => {
      console.log(start);
      if (start.plugs.filter(p => p.connected.length > 0).length == 0) return [start];

      let components = start.plugs.filter(p => p.connected.length > 0).map(pc => {
        return pc.connected.map(c => mapFlow(c.connectedTo.node)).flat(1);
      }).flat(1);

      return [start, ...components];
    }
    console.log(this.components);
    var flows = [];
    var additional = [];
    // collect involved nodes
    this.components.filter(e => e.component.nodeIdentifier == "OpenVS-Base-Event-Start").forEach(s => {
      const flow = mapFlow(s.component)

      flow.forEach(c => {
        additional.push(...c.inputSockets.filter(i => i.connected).map(i => i.con.plug.node))
      });

      flows.push(flow);
    });

    for (let i = 0, off = 0; i < additional.length; i++) { // remove duplicates from data source pool
      if (additional.filter(a => a.id == additional[i + off].id).length != 1) {
        additional.splice(i + off, 1);
        off -= 1;
      }
    }

    // map flow connections
    flows.map(f => {
      return f.map(c => {
        c.flowPlugs = Array(c.plugs.length);
        c.plugs.forEach((p, i) => {
          c.flowPlugs[i] = p.connected.map(con => {
            return {
              connectorId: con.id,
              conTo: con.connectedTo.node.id,
              targetPort: con.connectedTo.node.sockets.findIndex(s => s.id == con.connectedTo.id),
            }
          });
        });
        return c;
      });
    });

    console.log(additional);

    // map data connections
    additional = additional.map(n => {
      n.dataPlugs = Array(n.outputPlugs.length);
      n.outputPlugs.forEach((p, i) => {
        n.dataPlugs[i] = p.connected.map(con => {
          return {
            conTo: con.connectedTo.node.id,
            targetPort: con.connectedTo.node.inputSockets.findIndex(s => s.id == con.connectedTo.id),
            connectorId: con.id
          }
        });
      });
      return n;
    });

    const simplify = (node) => {
      return {
        x: node.x,
        y: node.y,
        scale: node.scale,
        flowPlugs: node.flowPlugs,
        dataPlugs: node.dataPlugs,
        identifier: node.nodeIdentifier,
        node: node.constructor.name,
        uid: node.id
      }
    }
    flows = flows.map(f => f.map(n => simplify(n)));
    additional = additional.map(n => simplify(n));

    console.log("flows", flows);
    console.log("additional", additional);

    return {
      flows,
      additional
    };
  }
  importProgram(exported) {
    const nodes = new Map();
    const spawnNode = (n) => {
      if (!this.registry.getNodeClass(n.node)) return;
      const node = new (this.registry.getNodeClass(n.node))(n.x, n.y, n.scale, this, "bezier");
      nodes.set(n.uid, node);
      this.addComponent(node);
    }
    exported.flows.forEach(f => {
      f.forEach(n => {
        spawnNode(n);
      });
      f.forEach(n => {
        if (!nodes.has(n.uid)) return;
        const node = nodes.get(n.uid);
        n.flowPlugs.forEach((p, i) => {
          p.forEach(c => {
            if (!nodes.has(c.conTo)) return;
            const target = nodes.get(c.conTo);
            node.plugs[i].connectTo(target.sockets[c.targetPort]);
          });
        });
      });
    });
    exported.additional.forEach(n => {
      spawnNode(n);
    });
    exported.additional.forEach(n => {
      if (!nodes.has(n.uid)) return;
      const node = nodes.get(n.uid);
      n.dataPlugs.forEach((p, i) => {
        p.forEach(c => {
          if (!nodes.has(c.conTo)) return;
          const target = nodes.get(c.conTo);
          node.outputPlugs[i].connectTo(target.inputSockets[c.targetPort]);
        });
      });
    });
  }
  clearWorkspace() {
    for (let id in window.openVS.nodes) {
      window.openVS.nodes[id].destroy();
    }
    let cons = window.openVS.connectors.slice();
    cons.forEach(c => c.destroy());
  }

  /**
   * @description Calling this function will generate an object containing all the
   * instructions a compiler should need. The data can be stringified for storage.
   *  **Work in Progress**
   *
   * @return {object}  The program specification.
   */
  generateProgramSpec() {
    // TODO: document program specification structure
    // find the start nodes
    const starts = this.components.filter(el => {
      return el.component instanceof StartEventNode;
    }).map(el => el.component);

    var follow = (f) => {
      // TODO: Stop recursion on visual loop
      let n = f[f.length - 1];

      // create new branch for every **connected** flow plug (if there is more than one)
      const sub = []; // new subBranches
      if (n.plugs.filter(p => p.connected.length != 0).length == 0) return f;

      const branches = [];
      n.plugs.forEach(plug => {
        if (plug.connected.length == 0) return branches.push([]);
        branches.push(...plug.connected);
      });
      if (branches.length == 1 && !n.forceBranch) {
        f.push(branches[0].connectedTo.node);
        return follow(f);
      }

      branches.forEach(connected => {
        if (connected.length == 0) return sub.push(connected);
        sub.push(follow([connected.connectedTo.node]));
      });
      if (!n.internalBranch) {
        f.push({
          id: "Connector-Branch-Split",
          branchCount: sub.length,
          inputs: [],
          outputs: [],
          uuid: "nil"
        });
      }
      f.push({
        id: "OVS-Branch",
        branches: sub
      });
      return f;
    }

    const flow = [];
    console.log(flow);
    starts.forEach(s => {
      flow.push(follow([s])); // create the basic flow order
    });

    console.log(flow);

    var traceDataSource = (component) => {
      let found = [component];
      component.inputSockets.filter(i => i.connected).forEach(i => {
        found.push(...traceDataSource(this.components.find(c => {
          return c.component.id == i.con.plug.node.id;
        }).component));
      });
      return found;
    }

    const additional = [];
    var mapComponent = (fc) => {
      if (!fc.inputSockets) fc.inputSockets = [];
      const is = fc.inputSockets.map(i => {
        if (i.connected) {
          const dataSource = this.components.filter(c => {
            return c.component.id == i.con.plug.node.id
          })[0].component;
          if (!dataSource) console.warn("This is not right.");
          let dependencies = traceDataSource(dataSource);
          dependencies.forEach(d => {
            if (additional.findIndex(e => e.id == d.id) == -1) additional.push(d);
          })
          if (additional.findIndex(e => e.id == dataSource.id) == -1) additional.push(dataSource);
        }
        const pid = (i.connected) ? i.con.plug.node.outputPlugs.findIndex(p => p == i.con.plug) : null;
        // TODO: add data sources to main flow
        return {
          inputSource: (i.connected) ? i.con.plug.node.id : null,
          required: i.required, // TODO: implement
          type: i.type,
          dataConstant: i.dataConstant,
          dataValue: i.storedData,
          portId: pid
        }
      });
      if (!fc.outputPlugs) fc.outputPlugs = [];
      const os = fc.outputPlugs.map(o => {
        return {
          type: o.type,
        }
      });
      return {
        is, os
      }
    }

    const basic = flow.slice();
    flow.length = 0;
    flow.push(...basic.map(flowBranch => { // convert the complex data to object spec
      return flowBranch.map(f => { // fc == flow component
        var mc = (fc) => {
          var m = (component) => {
            const d = mapComponent(component);
            return {
              id: component.identifier,
              inputs: d.is,
              outputs: d.os,
              uuid: component.id
            }
          }
          if (fc.id == "OVS-Branch") {
            return {
              id: fc.id,
              branches: fc.branches.map(branch => {
                return (!branch) ? branch : branch.map(c => {
                  return mc(c);
                });
              })
            };
          }
          return (fc.id != "Connector-Branch-Split") ? m(fc) : fc;
        }
        return mc(f);
      });
    }));
    const a = additional.map(el => {
      const d = mapComponent(el);
      d.uuid = el.id;
      d.id = el.identifier;
      return d;
    });
    return {
      flow,
      additional: a
    }
  }
  get renderElement() {
    return this.element;
  }
  toggleConnectorType() {
    this.connTypeToggle = 1 - this.connTypeToggle;
    let type = (this.connTypeToggle === 1) ? "bezier" : "line";
    this.components.forEach((c) => {
      if (!c.component instanceof Node) return;
      c.component.setConnectorType(type);
    });
  }
  generateStyles() {
    this.style = document.createElement("style");

    this.style.innerHTML = "@font-face {\n";
    this.style.innerHTML += "  font-family: LibreFranklin_" + this.element.id + ";\n";
    this.style.innerHTML += "  src: url('./assets/LibreFranklin-VariableFont_wght.ttf');\n";
    this.style.innerHTML += "}\n";

    this.style.innerHTML += "#" + this.element.id + " * {\n";
    this.style.innerHTML += "  font-family: LibreFranklin_" + this.element.id + ";\n";
    this.style.innerHTML += "  font-size: 96%;\n";
    this.style.innerHTML += "}";

    document.head.appendChild(this.style);
  }
  static getAbsCoords(elem) {
    const box = elem.getBoundingClientRect();

    const body = document.body;
    const docEl = document.documentElement;

    const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
    const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;

    const clientTop = docEl.clientTop || body.clientTop || 0;
    const clientLeft = docEl.clientLeft || body.clientLeft || 0;

    const top = box.top + scrollTop - clientTop;
    const left = box.left + scrollLeft - clientLeft;

    return { x: left, y: top };
  }
  static createShadowFilter(dx = 3, dy = 3, x = "-50%", y = "-50%", deviation = 3) {
    const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");

    const filter = document.createElementNS("http://www.w3.org/2000/svg", "filter");
    let id = "shadow" + (new Date()).getTime();
    filter.id = id;
    filter.setAttribute("x", x);
    filter.setAttribute("y", y);
    filter.setAttribute("width", "200%");
    filter.setAttribute("height", "200%");
    defs.appendChild(filter);

    const offset = document.createElementNS("http://www.w3.org/2000/svg", "feOffset");
    offset.setAttribute("dx", dx);
    offset.setAttribute("dy", dy);
    offset.setAttribute("in", "SourceAlpha");
    offset.setAttribute("result", "offset");
    filter.appendChild(offset);

    const blur = document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur");
    blur.setAttribute("in", "offset");
    blur.setAttribute("stdDeviation", deviation);
    blur.setAttribute("result", "blur");
    filter.appendChild(blur);

    const blend = document.createElementNS("http://www.w3.org/2000/svg", "feBlend");
    blend.setAttribute("in", "SourceGraphic");
    blend.setAttribute("in2", "blur");
    blend.setAttribute("mode", "normal");
    filter.appendChild(blend);

    return { element: defs, id: id };
  }
  setBackground(bgrd) {
    this.background = bgrd;
    bgrd.attach(this);
    this.body.container.prepend(bgrd.createSVGElement());
  }
  zoomOut(delta=0.2) {
    if (this.scale - delta < this.maxZoom.out) return;
    this.scale -= delta;
    this.zoom();
  }
  zoomIn(delta=0.2) {
    if (this.scale - delta > this.maxZoom.in) return;
    this.scale += delta;
    this.zoom();
  }
  zoom() {
    this.body.setViewboxScale(this.scale);
    if (!this.background) return this.scale;
    this.background.bg.setWidth(this.background.width * (1 / this.scale));
    this.background.bg.setHeight(this.background.height * (1 / this.scale));
    this.background.dispatchEvent("scale", this.scale);
    return this.scale;
  }
  addComponent(c, render = (el) => el.createSVGElement()) {
    this.components.push({ component: c, render: render });
    if (c.attachEngine) c.attachEngine(this);
    this.body.container.appendChild(render(c));
  }
}
class RoundedTriangle {
  constructor(borderRadius = 2, width) {
    this.width = width;
    this.height = (width / 2) * Math.sqrt(3);
    this.cd = borderRadius; // corner distance, distance from the corner of the triangle where the curve starts
    this.strokeWidth = this.cd * 10;

    this.builder = new PathBuilder();
    this.builder.moveTo(this.cd / 2, this.height - this.cd / 2);
    this.builder.lineTo(this.width - this.cd / 2, this.height - this.cd / 2);
    this.builder.lineTo(this.width / 2, this.cd / 2);
    this.builder.closePath();

    this.path = document.createElementNS("http://www.w3.org/2000/svg", "path");
    this.path.setAttribute("d", this.builder.build());
    this.path.setAttribute("stroke-width", this.strokeWidth);
    this.path.style.strokeLinejoin = "round";
  }
}
class RoundedTriangleComponent extends RoundedTriangle {
  constructor(x, y, rot, scale) {
    super(0.6, 13);
    this.x = x;
    this.y = y;
    this.rot = rot;
    this.scale = scale;
    this.isComponent = true;

    this.container = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    this.container.style.overflow = "visible";
    this.container.appendChild(this.path);
    this.color = "white";
    this.stroke = "white";
    this.updateAttributes();

    return this;
  }
  setOpacity(o) {
    this.opacity = 0;
    this.container.style.opacity = o;
  }
  setScale(s) {
    this.scale = s;
    this.updateAttributes();
  }
  setPosition(pos) {
    this.x = pos.x;
    this.y = pos.y;
    this.updateAttributes();
  }
  setColor(color) {
    this.color = color;
    this.updateAttributes();
  }
  setStroke(color) {
    this.stroke = color;
    this.updateAttributes();
  }
  updateAttributes() {
    const path = this.path;
    const svg = this.container;

    svg.setAttribute("x", this.x);
    svg.setAttribute("y", this.y);

    if (this.color) path.setAttribute("fill", this.color);
    if (this.stroke) path.setAttribute("stroke", this.stroke);
    if (this.rot || this.scale) path.setAttribute("transform", ((this.scale) ? "scale(" + this.scale + ") " : " ") + ((this.rot) ? "rotate(" + this.rot + "," + (this.width / 2) + "," + (this.height / 2) + ")" : ""));
  }
  createSVGElement() {
    return this.container;
  }
  setScale(scale) {
    this.scale = scale;
    this.updateAttributes();
  }
}

const builder = new PathBuilder();
builder.moveTo(20, 120);
builder.lineTo(120, 120);
builder.lineTo(70, 20);
builder.closePath();

const path = new Path();
path.path = builder.build();
path.setColor("white");
path.setStroke({
  stroke: "white",
  width: 10
});
path.container.style.strokeLinejoin = "round";
//engine.element.appendChild(path.container);

document.body.style.height = window.innerHeight + "px";
document.body.style.width = window.innerWidth + "px";

const engine = new SVGEngine();
const bgrd = new RasterBackground(0, 0, engine.width, engine.height, 1);
engine.setBackground(bgrd);

const rect = new Rectangle(120, 120, 50, 50, true);
rect.setColor("black");
rect.setStroke({
  color: "black",
  width: 1
});

const condition = new ConditionNode(375, 124, 1, engine, "bezier");
engine.addComponent(condition);

const condition1 = new ConditionNode(704, 125, 1, engine, "bezier");
engine.addComponent(condition1);

const addition = new AdditionNode(361, 381, 1, engine, "bezier");
engine.addComponent(addition);

const screen = new ScreenSizeNode(55, 347, 1, engine, "bezier");
engine.addComponent(screen);

const math = new MathNode(100, 100, 1, engine, "bezier");
engine.addComponent(math);

const add = new GeneralAdditionNode(100, 250, 1, engine, "bezier");
engine.addComponent(add);

const log = new ConsoleLogNode(100, 100, 1, engine, "bezier");
engine.addComponent(log);

const read = new VariableReadNode(200, 400, 1, engine, "bezier");
engine.addComponent(read);

const device = new IsMobileNode(55, 224, 1, engine, "bezier");
engine.addComponent(device);

const start = new StartEventNode(56, 56, 1, engine, "bezier");
engine.addComponent(start);

// engine.addComponent(condition.createPreview(350, 300));

class NodeRegistry {
  constructor() {
    this.classes = [];
    this.nodeClasses = [];

    return this;
  }
  addNodes(...nodes) {
    nodes.forEach((node) => this.addNode(node));
  }
  /**
   * @description adds a node to the registry
   *
   * @param  {object} node The config object of the data to add.
   * @param  {Class}  node.nodeClass The node to add
   * @param  {string} node.name The name of the node in the menu
   * @param  {string} node.class The class of the node
   * @return {void}
   */
  addNode(node) {
    const index = this.classes.findIndex(e => e.className == node.class);
    if (index === -1) throw "Unknown Class [" + this.constructor.name + ".addNode]";
    this.classes[index].nodes.push(node);
    this.nodeClasses.push(node.nodeClass);
  }
  /**
   * @description Add a new class to the registry
   *
   * @param  {object} c The config of the new class
   * @param  {string} c.className The id of the class
   * @param  {string} c.name The display name of the class
   * @param  {string} c.color The color of the class
   * @return {void}
   */
  addClassConfig(c) {
    if (!c) throw "Config cannot be null [" + this.constructor.name + ".addClassConfig]";
    c.nodes = [];
    this.classes.push(c);
  }

  /**
   * @description Get the class of a node by name
   *
   * @param  {string} name The class name of the node
   * @return {Class}  The class of the corresponding node
   */
  getNodeClass(name) {
    if (!name) throw "Class name cannot be null [" + this.constructor.name + ".getNodeClass]";
    return this.nodeClasses.find(el => el.name === name);
  }
}
/**
 * @class
 * @classdesc This class renders and administrates the block storages, where you can drag new nodes from.
 */
class UiBlockShelf {
  /**
   * @description Initiates the ui component.
   *
   * @param  {NodeRegistry} nodeReg=Node The registry object containign the available nodes.
   * @return {object}             The initiated object
   */
  constructor(nodeReg) {
    this.registry = nodeReg;
    this.engine = null;

    this.container = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    this.container.setAttribute("x", 0);
    this.container.setAttribute("y", 0);
    this.container.setAttribute("width", 0); // change on attachEngine
    this.container.setAttribute("height", 0);

    this.shadow = SVGEngine.createShadowFilter(0, 1); // create the shadow defs element
    this.shadowElement = this.shadow.element;
    this.container.appendChild(this.shadowElement);

    this.bg = new Rectangle(10, 10, 0, 0, true, 4)
    this.bg.elem.style.maxHeight = "calc(100% - 20px)";
    this.bg.setShadow(this.shadow.id);
    this.bg.setColor("#121212");
    this.bg.setStroke({
      stroke: "#0f0f0f",
      width: 2
    });
    this.container.appendChild(this.bg.createSVGElement());

    this.body = new ScrollComponent(12, 12, 0, 0, 1);

    return this;
  }

  spawn(id, x, y=10) {
    x = x || this.tw + 10;
    let node = new (this.registry.getNodeClass(id))(x, y, this.engine.scale, this.engine, "bezier");
    this.engine.addComponent(node);
  }

  attachEngine(engine) {
    // TODO: visual previews of the blocks
    this.engine = engine;

    this.container.setAttribute("width", this.engine.width); // change on attachEngine
    this.container.setAttribute("height", this.engine.height);

    this.body.setWidth(this.engine.width * 0.2 - 4);
    this.body.setHeight(this.engine.height - 24);

    this.bg.setWidth(this.engine.width * 0.2);
    this.engine.element.appendChild(this.container);

    const ui = this;
    this.registry.classes.forEach((config, i) => {
      let y = (18 * i + 2 * i) * this.engine.scale;
      let select = new SVGSelect(0, y, this.engine.width * 0.2 - 5, 1, () => { }, function(_data) {
        let sel = this; // anonymous function scoped inside SVGSelect
        sel.renderContainer = ui.body.container;
        sel.moveToTop();
      });
      select.selected.container.innerHTML = config.name;
      select.dynamicPlaceholder = false;
      config.nodes.forEach(n => {
        select.addItem(n.name, n.nodeClass.name, (d) => {
          this.spawn(d.selected, d.clientPos.x - 25, d.clientPos.y - 15);
        });
      });
      this.body.addComponent(select);
    });
    let classCount = this.registry.classes.length;
    let height = Math.min(18 * this.engine.scale * classCount + ((classCount + 1) * 2), this.engine.height - 20);
    this.bg.setHeight(height);
    //this.bg.elem.after(this.body.createSVGElement());
    this.container.appendChild(this.body.createSVGElement());
  }
}

const reg = new NodeRegistry();
reg.addClassConfig({
  className: Node.Class.BASIC,
  color: Node.ClassColor[Node.Class.BASIC],
  name: Node.ClassName[Node.Class.BASIC]
});
reg.addClassConfig({
  className: Node.Class.CONSOLE,
  color: Node.ClassColor[Node.Class.CONSOLE],
  name: Node.ClassName[Node.Class.CONSOLE]
});
reg.addClassConfig({
  className: Node.Class.EVENT,
  color: Node.ClassColor[Node.Class.EVENT],
  name: Node.ClassName[Node.Class.EVENT]
});
reg.addClassConfig({
  className: Node.Class.DEVICEINFO,
  color: Node.ClassColor[Node.Class.DEVICEINFO],
  name: Node.ClassName[Node.Class.DEVICEINFO]
});

reg.addNode({
  nodeClass: StartEventNode,
  class: Node.Class.EVENT,
  name: "Start"
});
reg.addNode({
  nodeClass: ConditionNode,
  class: Node.Class.BASIC,
  name: "Condition"
});
reg.addNode({
  nodeClass: ConsoleLogNode,
  class: Node.Class.CONSOLE,
  name: "Console.log()"
});
reg.addNode({
  nodeClass: AdditionNode,
  class: Node.Class.BASIC,
  name: "Add (math)"
});
reg.addNode({
  nodeClass: IsMobileNode,
  class: Node.Class.DEVICEINFO,
  name: "Is Mobile?"
});
reg.addNode({
  nodeClass: ScreenSizeNode,
  class: Node.Class.DEVICEINFO,
  name: "Screen Size"
});
reg.addNode({
  nodeClass: VariableWriteNode,
  class: Node.Class.BASIC,
  name: "Write Variable"
});
reg.addNode({
  nodeClass: VariableReadNode,
  class: Node.Class.BASIC,
  name: "Read Variable"
});
reg.addNode({
  nodeClass: WhileLoopNode,
  class: Node.Class.BASIC,
  name: "While Loop"
});

const shelf = new UiBlockShelf(reg);
engine.addUI(shelf);
engine.setNodeRegistry(reg);
console.log(shelf);

document.body.style.overflow = "hidden";

const compiler = new Worker("worker.js");
compiler.onmessage = (e) => console.log(e.data);

function compile() {
  compiler.postMessage(engine.generateProgramSpec());
}

window.addEventListener("mousewheel", (e) => {
  // disabled cause not working
  /*e.preventDefault();
  if (e.deltaY > 0) {
    // zoom out
    engine.zoomOut(0.2);
  } else {
    // zoom in
    engine.zoomIn(0.2);
  }*/
});

const program = '{"flows":[[{"x":56,"y":56,"scale":1,"flowPlugs":[[]],"identifier":"OpenVS-Base-Event-Start","node":"StartEventNode","uid":"lfzek7nqbner9zzlelm"}],[{"x":292,"y":327,"scale":1,"flowPlugs":[[]],"identifier":"OpenVS-Base-Event-Start","node":"StartEventNode","uid":"lfzek7o21or7ngng3x"}],[{"x":292,"y":327,"scale":1,"flowPlugs":[[{"connectorId":"lfzek7pfcee54lkp25d","conTo":"lfzek7o7e6ut8y5z8xe","targetPort":0}]],"identifier":"OpenVS-Base-Event-Start","node":"StartEventNode","uid":"lfzek7o4jkkr5nnxth"},{"x":611,"y":395,"scale":1,"flowPlugs":[[{"connectorId":"lfzek7piu5dvls0z6t","conTo":"lfzek7oor3z716aj4di","targetPort":0}],[{"connectorId":"lfzek7pk70c2dszlgnw","conTo":"lfzek7osmwa3a8fdknj","targetPort":0}]],"identifier":"OpenVS-Base-Basic-Condition","node":"ConditionNode","uid":"lfzek7o7e6ut8y5z8xe"},{"x":336,"y":371,"scale":1,"flowPlugs":[[]],"identifier":"OpenVS-Base-Console-Log","node":"ConsoleLogNode","uid":"lfzek7oor3z716aj4di"},{"x":940,"y":396,"scale":1,"flowPlugs":[[{"connectorId":"lfzek7pmkutx5ras9b","conTo":"lfzek7oxc5tmrgrkj0f","targetPort":0}],[]],"identifier":"OpenVS-Base-Basic-Condition","node":"ConditionNode","uid":"lfzek7osmwa3a8fdknj"},{"x":854,"y":585,"scale":1,"flowPlugs":[[],[{"connectorId":"lfzekezv3atkkynyic8","conTo":"lfzek7mlpgv03i8ok3k","targetPort":0}]],"identifier":"OpenVS-Base-Loop-While","node":"WhileLoopNode","uid":"lfzek7oxc5tmrgrkj0f"},{"x":704,"y":125,"scale":1,"flowPlugs":[[],[]],"identifier":"OpenVS-Base-Basic-Condition","node":"ConditionNode","uid":"lfzek7mlpgv03i8ok3k"}]],"additional":[{"x":291,"y":495,"scale":1,"dataPlugs":[[{"conTo":"lfzek7o7e6ut8y5z8xe","targetPort":0,"connectorId":"lfzek7psb56lpjf9xy6"},{"conTo":"lfzek7osmwa3a8fdknj","targetPort":0,"connectorId":"lfzek7pv806psp96h3j"},{"conTo":"lfzek7oor3z716aj4di","targetPort":0,"connectorId":"lfzek7pzdt1tqou12cq"},{"conTo":"lfzek7oxc5tmrgrkj0f","targetPort":0,"connectorId":"lfzek9awh3k7ti4ewfi"},{"conTo":"lfzek7mlpgv03i8ok3k","targetPort":0,"connectorId":"lfzekhsn49lejbf5jko"}]],"identifier":"OpenVS-Base-DInfo-Mobile","node":"IsMobileNode","uid":"lfzek7pnwpwr7urca89"},{"x":291,"y":495,"scale":1,"dataPlugs":[[{"conTo":"lfzek7o7e6ut8y5z8xe","targetPort":0,"connectorId":"lfzek7psb56lpjf9xy6"},{"conTo":"lfzek7osmwa3a8fdknj","targetPort":0,"connectorId":"lfzek7pv806psp96h3j"},{"conTo":"lfzek7oor3z716aj4di","targetPort":0,"connectorId":"lfzek7pzdt1tqou12cq"},{"conTo":"lfzek7oxc5tmrgrkj0f","targetPort":0,"connectorId":"lfzek9awh3k7ti4ewfi"},{"conTo":"lfzek7mlpgv03i8ok3k","targetPort":0,"connectorId":"lfzekhsn49lejbf5jko"}]],"identifier":"OpenVS-Base-DInfo-Mobile","node":"IsMobileNode","uid":"lfzek7pnwpwr7urca89"}]}';

engine.importProgram(JSON.parse(program));

engine.addPlugin(new SelectionPlugin());

//module.exports = engine;