juanmard/icestudio

View on GitHub
services/graph.js

Summary

Maintainability
F
2 wks
Test Coverage
/* eslint-disable new-cap */

angular
  .module('icestudio')
  .service(
    'graph',
    function (
      $rootScope,
      joint,
      blocks,
      utils,
      common,
      gettextCatalog,
      window
    ) {
      'use strict';

      function _tcStr(str, args) {
        return gettextCatalog.getString(str, args);
      }

      var z = {index: 100};
      var graph = null;
      var paper = null;
      var selection = null;
      var selectionView = null;
      var commandManager = null;
      var mousePosition = {x: 0, y: 0};
      var gridsize = 8;
      var state = {pan: {x: 0, y: 0}, zoom: 1.0};

      var self = this;

      const ZOOM_MAX = 2.1;
      const ZOOM_MIN = 0.3;
      const ZOOM_SENS = 0.3;

      this.breadcrumbs = [{name: '', type: '', id: ''}];

      function _updateTitle() {
        let title =
          'Icestudio: ' +
          (self.breadcrumbs[0] ? self.breadcrumbs[0].name : 'undefined');
        if (self.breadcrumbs.length > 1) {
          for (const val of self.breadcrumbs.slice(1)) {
            title += ' / ' + val.name;
          }
        }
        document.title = title;
      }

      this.popTitle = function () {
        self.breadcrumbs.pop();
        _updateTitle();
        return self.breadcrumbs.slice(-1)[0];
      };

      this.resetTitle = function (name) {
        this.breadcrumbs = [{name: name, type: ''}];
        _updateTitle();
        utils.rootScopeSafeApply();
      };

      this.addingDraggableBlock = false;

      this.getState = function () {
        // Clone state
        return utils.clone(state);
      };

      this.setState = function (_state) {
        if (!_state) {
          _state = {
            pan: {
              x: 0,
              y: 0,
            },
            zoom: 1.0,
          };
        }
        this.panAndZoom.zoom(_state.zoom);
        this.panAndZoom.pan(_state.pan);
      };

      this.resetView = function () {
        this.setState(null);
      };

      this.fitContent = function () {
        if (!this.isEmpty()) {
          // Target box
          var margin = 40;
          var menuFooterHeight = 93;
          var winWidth = window.get().width;
          var winHeight = window.get().height;

          var tbox = {
            x: margin,
            y: margin,
            width: winWidth - 2 * margin,
            height: winHeight - menuFooterHeight - 2 * margin,
          };
          // Source box
          var sbox = V(paper.viewport).bbox(true, paper.svg);
          sbox = {
            x: sbox.x * state.zoom,
            y: sbox.y * state.zoom,
            width: sbox.width * state.zoom,
            height: sbox.height * state.zoom,
          };
          var scale;
          if (tbox.width / sbox.width > tbox.height / sbox.height) {
            scale = tbox.height / sbox.height;
          } else {
            scale = tbox.width / sbox.width;
          }
          if (state.zoom * scale > 1) {
            scale = 1 / state.zoom;
          }
          var target = {
            x: tbox.x + tbox.width / 2,
            y: tbox.y + tbox.height / 2,
          };
          var source = {
            x: sbox.x + sbox.width / 2,
            y: sbox.y + sbox.height / 2,
          };
          this.setState({
            pan: {
              x: target.x - source.x * scale,
              y: target.y - source.y * scale,
            },
            zoom: state.zoom * scale,
          });
          $('.joint-paper.joint-theme-default>svg').attr('height', winHeight);
          $('.joint-paper.joint-theme-default>svg').attr('width', winWidth);
        } else {
          this.resetView();
        }
      };

      this.createPaper = function (element) {
        graph = new joint.dia.Graph();

        paper = new joint.dia.Paper({
          el: element,
          width: 3000,
          height: 3000,
          model: graph,
          gridSize: gridsize,
          clickThreshold: 6,
          snapLinks: {radius: 16},
          linkPinning: false,
          embeddingMode: false,
          //markAvailable: true,
          getState: this.getState,
          defaultLink: new joint.shapes.ice.Wire(),
          // guard: function(evt, view) vg
          //   // FALSE means the event isn't guarded.
          //   return false;
          // },
          validateMagnet: function (cellView, magnet) {
            // Prevent to start wires from an input port
            return magnet.getAttribute('type') === 'output';
          },
          validateConnection: function (
            cellViewS,
            magnetS,
            cellViewT,
            magnetT,
            end,
            linkView
          ) {
            // Prevent output-output links
            if (
              magnetS &&
              magnetS.getAttribute('type') === 'output' &&
              magnetT &&
              magnetT.getAttribute('type') === 'output'
            ) {
              if (magnetS !== magnetT) {
                // Show warning if source and target blocks are different
                warning(_tcStr('Invalid connection'));
              }
              return false;
            }
            // Ensure right -> left connections
            if (magnetS && magnetS.getAttribute('pos') === 'right') {
              if (magnetT && magnetT.getAttribute('pos') !== 'left') {
                warning(_tcStr('Invalid connection'));
                return false;
              }
            }
            // Ensure bottom -> top connections
            if (magnetS && magnetS.getAttribute('pos') === 'bottom') {
              if (magnetT && magnetT.getAttribute('pos') !== 'top') {
                warning(_tcStr('Invalid connection'));
                return false;
              }
            }
            var i;
            var links = graph.getLinks();
            for (i in links) {
              var link = links[i];
              var linkIView = link.findView(paper);
              if (linkView === linkIView) {
                //Skip the wire the user is drawing
                continue;
              }
              // Prevent multiple input links
              if (
                cellViewT.model.id === link.get('target').id &&
                magnetT.getAttribute('port') === link.get('target').port
              ) {
                warning(_tcStr('Invalid multiple input connections'));
                return false;
              }
              // Prevent to connect a pull-up if other blocks are connected
              if (
                cellViewT.model.get('pullup') &&
                cellViewS.model.id === link.get('source').id
              ) {
                warning(
                  _tcStr(
                    'Invalid <i>Pull up</i> connection:<br>block already connected'
                  )
                );
                return false;
              }
              // Prevent to connect other blocks if a pull-up is connected
              if (
                linkIView.targetView.model.get('pullup') &&
                cellViewS.model.id === link.get('source').id
              ) {
                warning(
                  _tcStr(
                    'Invalid block connection:<br><i>Pull up</i> already connected'
                  )
                );
                return false;
              }
            }
            // Ensure input -> pull-up connections
            if (cellViewT.model.get('pullup')) {
              var ret = cellViewS.model.get('blockType') === 'basic.input';
              if (!ret) {
                warning(
                  _tcStr(
                    'Invalid <i>Pull up</i> connection:<br>only <i>Input</i> blocks allowed'
                  )
                );
              }
              return ret;
            }
            // Prevent different size connections
            var tsize;
            var lsize = linkView.model.get('size');
            var portId = magnetT.getAttribute('port');
            var tLeftPorts = cellViewT.model.get('leftPorts');
            for (i in tLeftPorts) {
              var port = tLeftPorts[i];
              if (portId === port.id) {
                tsize = port.size;
                break;
              }
            }
            tsize = tsize || 1;
            lsize = lsize || 1;
            if (tsize !== lsize) {
              warning(
                _tcStr('Invalid connection: {{a}} → {{b}}', {
                  a: lsize,
                  b: tsize,
                })
              );
              return false;
            }
            // Prevent loop links
            return magnetS !== magnetT;
          },
        });

        // Command Manager

        commandManager = new joint.dia.CommandManager({
          paper: paper,
          graph: graph,
        });

        // Selection View

        selection = new Backbone.Collection();
        selectionView = new joint.ui.SelectionView({
          paper: paper,
          graph: graph,
          model: selection,
          state: state,
        });

        paper.options.enabled = true;
        paper.options.warningTimer = false;

        function warning(message) {
          if (!paper.options.warningTimer) {
            paper.options.warningTimer = true;
            alertify.warning(message, 5);
            setTimeout(function () {
              paper.options.warningTimer = false;
            }, 4000);
          }
        }

        var targetElement = element[0];

        this.panAndZoom = svgPanZoom(targetElement.childNodes[2], {
          fit: false,
          center: false,
          zoomEnabled: true,
          panEnabled: false,
          zoomScaleSensitivity: ZOOM_SENS,
          dblClickZoomEnabled: false,
          minZoom: ZOOM_MIN,
          maxZoom: ZOOM_MAX,
          eventsListenerElement: targetElement,
          onZoom: function (scale) {
            state.zoom = scale;
            // Close expanded combo
            if (document.activeElement.className === 'select2-search__field') {
              $('select').select2('close');
            }
            updateCellBoxes();
          },
          onPan: function (newPan) {
            state.pan = newPan;
            graph.trigger('state', state);
            updateCellBoxes();
          },
        });

        function updateCellBoxes() {
          var cells = graph.getCells();
          selectionView.options.state = state;

          for (var i = 0, len = cells.length; i < len; i++) {
            if (!cells[i].isLink()) {
              cells[i].attributes.state = state;
              var elementView = paper.findViewByModel(cells[i]);
              // Pan blocks
              elementView.updateBox();
              // Pan selection boxes
              selectionView.updateBox(elementView.model);
            }
          }
        }
        // Events

        $('body').mousemove(function (event) {
          mousePosition = {
            x: event.pageX,
            y: event.pageY,
          };
        });

        selectionView.on('selection-box:pointerdown', function (/*evt*/) {
          // Move selection to top view
          if (hasSelection()) {
            selection.each(function (cell) {
              var cellView = paper.findViewByModel(cell);
              if (!cellView.model.isLink()) {
                if (cellView.$box.css('z-index') < z.index) {
                  cellView.$box.css('z-index', ++z.index);
                }
              }
            });
          }
        });

        selectionView.on('selection-box:pointerclick', function (evt) {
          if (self.addingDraggableBlock) {
            // Set new block's position
            self.addingDraggableBlock = false;
            processReplaceBlock(selection.at(0));
            disableSelected();
            updateWiresOnObstacles();
            graph.trigger('batch:stop');
          } else {
            // Toggle selected cell
            if (evt.shiftKey) {
              var cell = selection.get($(evt.target).data('model'));
              selection.reset(selection.without(cell));
              selectionView.destroySelectionBox(cell);
            }
          }
        });

        paper.on('cell:pointerclick', function (cellView, evt, x, y) {
          if (!checkInsideViewBox(cellView, x, y)) {
            return;
          }
          // If Shift is pressed, we are updating the selection. Else new selection.
          if (!evt.shiftKey) {
            selectionView.cancelSelection();
          }
          if (!paper.options.enabled || cellView.model.isLink()) {
            return;
          }
          // Disable current focus
          document.activeElement.blur();
          if (utils.hasLeftButton(evt)) {
            selection.add(cellView.model);
            selectionView.createSelectionBox(cellView.model);
          }
        });

        paper.on('cell:pointerdblclick', function (cellView, evt, x, y) {
          if (x && y && !checkInsideViewBox(cellView, x, y)) {
            return;
          }
          selectionView.cancelSelection();
          if (evt.shiftKey) {
            // Allow dblClick only if Shift is not pressed
            return;
          }

          var type = cellView.model.get('blockType');
          var blockId = cellView.model.get('id');

          if (type.indexOf('basic.') !== -1) {
            // Edit basic blocks
            if (paper.options.enabled) {
              blocks.editBasic(type, cellView, addCell);
            }
          } else if (common.allDependencies[type]) {
            if (common.isEditingSubmodule) {
              alertify.warning(
                _tcStr(
                  'To enter on "edit mode" of deeper block, you need to finish current "edit mode", lock the keylock to do it.'
                )
              );
              return;
            }
            // Navigate inside generic blocks
            z.index = 1;
            var project = common.allDependencies[type];

            utils.startWait();
            $rootScope.navigateProject(
              self.breadcrumbs.length === 1,
              project,
              type,
              blockId,
              true
            );
            self.breadcrumbs.push({
              name: project.package.name || '#',
              type: type,
              id: blockId,
            });
            _updateTitle();
            utils.rootScopeSafeApply();
          }
        });

        function checkInsideViewBox(view, x, y) {
          if (!x || !y) {
            return false;
          }
          var $box = $(view.$box[0]);
          var position = $box.position();
          var rbox = g.rect(
            position.left,
            position.top,
            $box.width(),
            $box.height()
          );
          return rbox.containsPoint({
            x: x * state.zoom + state.pan.x,
            y: y * state.zoom + state.pan.y,
          });
        }

        paper.on('blank:pointerdown', function (evt, x, y) {
          // Disable current focus
          document.activeElement.blur();

          if (utils.hasLeftButton(evt)) {
            if (utils.hasCtrl(evt)) {
              if (!self.isEmpty()) {
                self.panAndZoom.enablePan();
              }
            } else if (paper.options.enabled) {
              selectionView.startSelecting(evt, x, y);
            }
          } else if (utils.hasRightButton(evt)) {
            if (!self.isEmpty()) {
              self.panAndZoom.enablePan();
            }
          }
        });

        paper.on('blank:pointerup', function (/*cellView, evt*/) {
          self.panAndZoom.disablePan();
        });

        paper.on('cell:mouseover', function (cellView, evt) {
          // Move selection to top view if !mousedown
          if (!utils.hasButtonPressed(evt)) {
            if (!cellView.model.isLink()) {
              if (cellView.$box.css('z-index') < z.index) {
                cellView.$box.css('z-index', ++z.index);
              }
            }
          }
        });

        paper.on('cell:pointerup', function (cellView /*, evt*/) {
          graph.trigger('batch:start');
          processReplaceBlock(cellView.model);
          graph.trigger('batch:stop');
          if (paper.options.enabled) {
            updateWiresOnObstacles();
          }
        });

        paper.on('cell:pointermove', function (cellView /*, evt*/) {
          debounceDisableReplacedBlock(cellView.model);
        });

        selectionView.on('selection-box:pointermove', function (/*evt*/) {
          if (self.addingDraggableBlock && hasSelection()) {
            debounceDisableReplacedBlock(selection.at(0));
          }
        });

        function processReplaceBlock(upperBlock) {
          var lowerBlock = findLowerBlock(upperBlock);
          replaceBlock(upperBlock, lowerBlock);
        }

        function findLowerBlock(upperBlock) {
          if (
            upperBlock.get('type') === 'ice.Wire' ||
            upperBlock.get('type') === 'ice.Info'
          ) {
            return;
          }
          var blocks = graph.findModelsUnderElement(upperBlock);
          // There is at least one model under the upper block
          if (blocks.length === 0) {
            return;
          }
          // Get the first model found
          var lowerBlock = blocks[0];
          if (
            lowerBlock.get('type') === 'ice.Wire' ||
            lowerBlock.get('type') === 'ice.Info'
          ) {
            return;
          }
          var validReplacements = {
            'ice.Generic': [
              'ice.Generic',
              'ice.Code',
              'ice.Input',
              'ice.Output',
            ],
            'ice.Code': ['ice.Generic', 'ice.Code', 'ice.Input', 'ice.Output'],
            'ice.Input': ['ice.Generic', 'ice.Code'],
            'ice.Output': ['ice.Generic', 'ice.Code'],
            'ice.Constant': ['ice.Constant', 'ice.Memory'],
            'ice.Memory': ['ice.Constant', 'ice.Memory'],
          }[lowerBlock.get('type')];
          // Check if the upper block is a valid replacement
          if (validReplacements.indexOf(upperBlock.get('type')) === -1) {
            return;
          }
          return lowerBlock;
        }

        function replaceBlock(upperBlock, lowerBlock) {
          if (lowerBlock) {
            // 1. Compute portsMap between the upperBlock and the lowerBlock
            var portsMap = computeAllPortsMap(upperBlock, lowerBlock);
            // 2. Reconnect the wires from the lowerBlock to the upperBlock
            var wires = graph.getConnectedLinks(lowerBlock);
            _.each(wires, function (wire) {
              // Replace wire's source
              replaceWireConnection(wire, 'source');
              // Replace wire's target
              replaceWireConnection(wire, 'target');
            });
            // 3. Move the upperModel to be centered with the lowerModel
            var lowerBlockSize = lowerBlock.get('size');
            var upperBlockSize = upperBlock.get('size');
            var lowerBlockType = lowerBlock.get('type');
            var lowerBlockPosition = lowerBlock.get('position');
            if (
              lowerBlockType === 'ice.Constant' ||
              lowerBlockType === 'ice.Memory'
            ) {
              // Center x, Bottom y
              upperBlock.set('position', {
                x:
                  lowerBlockPosition.x +
                  (lowerBlockSize.width - upperBlockSize.width) / 2,
                y:
                  lowerBlockPosition.y +
                  lowerBlockSize.height -
                  upperBlockSize.height,
              });
            } else if (lowerBlockType === 'ice.Input') {
              // Right x, Center y
              upperBlock.set('position', {
                x:
                  lowerBlockPosition.x +
                  lowerBlockSize.width -
                  upperBlockSize.width,
                y:
                  lowerBlockPosition.y +
                  (lowerBlockSize.height - upperBlockSize.height) / 2,
              });
            } else if (lowerBlockType === 'ice.Output') {
              // Left x, Center y
              upperBlock.set('position', {
                x: lowerBlockPosition.x,
                y:
                  lowerBlockPosition.y +
                  (lowerBlockSize.height - upperBlockSize.height) / 2,
              });
            } else {
              // Center x, Center y
              upperBlock.set('position', {
                x:
                  lowerBlockPosition.x +
                  (lowerBlockSize.width - upperBlockSize.width) / 2,
                y:
                  lowerBlockPosition.y +
                  (lowerBlockSize.height - upperBlockSize.height) / 2,
              });
            }
            // 4. Remove the lowerModel
            lowerBlock.remove();
            prevLowerBlock = null;
          }

          function replaceWireConnection(wire, connectorType) {
            var connector = wire.get(connectorType);
            if (
              connector.id === lowerBlock.get('id') &&
              portsMap[connector.port]
            ) {
              wire.set(connectorType, {
                id: upperBlock.get('id'),
                port: portsMap[connector.port],
              });
            }
          }
        }

        function computeAllPortsMap(upperBlock, lowerBlock) {
          var portsMap = {};
          // Compute the ports for each side: left, right and top.
          // If there are ports with the same name they are ordered
          // by position, from 0 to n.
          //
          //                   Top ports 0 ·· n
          //                   _____|__|__|_____
          //  Left ports 0  --|                 |--  0 Right ports
          //             ·  --|      BLOCK      |--  ·
          //             ·  --|                 |--  ·
          //             n    |_________________|    n
          //                        |  |  |
          //                   Bottom port 0 -- n

          _.merge(
            portsMap,
            computePortsMap(upperBlock, lowerBlock, 'leftPorts')
          );
          _.merge(
            portsMap,
            computePortsMap(upperBlock, lowerBlock, 'rightPorts')
          );
          _.merge(
            portsMap,
            computePortsMap(upperBlock, lowerBlock, 'topPorts')
          );
          _.merge(
            portsMap,
            computePortsMap(upperBlock, lowerBlock, 'bottomPorts')
          );

          return portsMap;
        }

        function computePortsMap(upperBlock, lowerBlock, portType) {
          var portsMap = {};
          var usedUpperPorts = [];
          var upperPorts = upperBlock.get(portType);
          var lowerPorts = lowerBlock.get(portType);

          _.each(lowerPorts, function (lowerPort) {
            var matchedPorts = _.filter(upperPorts, function (upperPort) {
              return (
                lowerPort.name === upperPort.name &&
                lowerPort.size === upperPort.size &&
                !_.includes(usedUpperPorts, upperPort)
              );
            });
            if (matchedPorts && matchedPorts.length > 0) {
              portsMap[lowerPort.id] = matchedPorts[0].id;
              usedUpperPorts = usedUpperPorts.concat(matchedPorts[0]);
            }
          });

          if (_.isEmpty(portsMap)) {
            // If there is no match replace the connections if the
            // port's size matches ignoring the port's name.
            var n = Math.min(upperPorts.length, lowerPorts.length);
            for (var i = 0; i < n; i++) {
              if (lowerPorts[i].size === upperPorts[i].size) {
                portsMap[lowerPorts[i].id] = upperPorts[i].id;
              }
            }
          }

          return portsMap;
        }

        var prevLowerBlock = null;

        function disableReplacedBlock(lowerBlock) {
          if (prevLowerBlock) {
            // Unhighlight previous lower block
            var prevLowerBlockView = paper.findViewByModel(prevLowerBlock);
            prevLowerBlockView.$box.removeClass('block-disabled');
            prevLowerBlockView.$el.removeClass('block-disabled');
          }
          if (lowerBlock) {
            // Highlight new lower block
            var lowerBlockView = paper.findViewByModel(lowerBlock);
            lowerBlockView.$box.addClass('block-disabled');
            lowerBlockView.$el.addClass('block-disabled');
          }
          prevLowerBlock = lowerBlock;
        }

        const nodeDebounce = require('lodash.debounce');

        // Debounce `pointermove` handler to improve the performance
        var debounceDisableReplacedBlock = nodeDebounce(function (upperBlock) {
          var lowerBlock = findLowerBlock(upperBlock);
          disableReplacedBlock(lowerBlock);
        }, 100);

        graph.on('add change:source change:target', function (cell) {
          if (cell.isLink() && cell.get('source').id) {
            // Link connected
            var target = cell.get('target');
            if (target.id) {
              // Connected to a port
              cell.attributes.lastTarget = target;
              updatePortDefault(target, false);
            } else {
              // Moving the wire connection
              target = cell.get('lastTarget');
              updatePortDefault(target, true);
            }
          }
        });

        graph.on('remove', function (cell) {
          if (cell.isLink()) {
            // Link removed
            var target = cell.get('target');
            if (!target.id) {
              target = cell.get('lastTarget');
            }
            updatePortDefault(target, true);
          }
        });

        function updatePortDefault(target, value) {
          if (target) {
            var i, port;
            var block = graph.getCell(target.id);
            if (block) {
              var data = block.get('data');
              if (data && data.ports && data.ports.in) {
                for (i in data.ports.in) {
                  port = data.ports.in[i];
                  if (port.name === target.port && port.default) {
                    port.default.apply = value;
                    break;
                  }
                }
                paper.findViewByModel(block.id).updateBox();
              }
            }
          }
        }
        // Initialize state
        graph.trigger('state', state);
      };

      function updateWiresOnObstacles() {
        var cells = graph.getCells();

        //_.each(cells, function (cell) {
        for (var i = 0, n = cells.length; i < n; i++) {
          if (cells[i].isLink()) {
            paper.findViewByModel(cells[i]).update();
          }
        }
      }

      this.setBoardRules = function (rules) {
        var cells = graph.getCells();
        common.set('boardRules', rules);

        for (var i = 0, n = cells.length; i < n; i++) {
          if (!cells[i].isLink()) {
            cells[i].attributes.rules = rules;
            var cellView = paper.findViewByModel(cells[i]);
            cellView.updateBox();
          }
        }
      };

      this.undo = function () {
        if (!this.addingDraggableBlock) {
          disableSelected();
          commandManager.undo();
          updateWiresOnObstacles();
        }
      };

      this.redo = function () {
        if (!this.addingDraggableBlock) {
          disableSelected();
          commandManager.redo();
          updateWiresOnObstacles();
        }
      };

      this.clearAll = function () {
        graph.clear();
        this.appEnable(true);
        selectionView.cancelSelection();
      };

      this.appEnable = function (value) {
        paper.options.enabled = value;
        var ael, i;
        if (value) {
          /* In the new javascript context of nwjs, angular can't change classes over the dom in this way,
                for this we need to update directly , but for the moment we maintain angular too to maintain model synced */
          angular.element('#menu').removeClass('is-disabled');
          angular.element('.paper').removeClass('looks-disabled');
          angular.element('.banner').addClass('hidden');
          ael = document.getElementById('menu');
          if (typeof ael !== 'undefined') {
            ael.classList.remove('is-disabled');
          }
          ael = document.getElementsByClassName('paper');
          if (typeof ael !== 'undefined' && ael.length > 0) {
            for (i = 0; i < ael.length; i++) {
              ael[i].classList.remove('looks-disabled');
            }
          }
        } else {
          angular.element('#menu').addClass('is-disabled');
          angular.element('.paper').addClass('looks-disabled');
          ael = document.getElementById('menu');
          if (typeof ael !== 'undefined') {
            ael.classList.add('is-disabled');
          }
          ael = document.getElementsByClassName('paper');
          if (typeof ael !== 'undefined' && ael.length > 0) {
            for (i = 0; i < ael.length; i++) {
              ael[i].classList.add('looks-disabled');
            }
          }
        }

        var cells = graph.getCells();
        _.each(cells, function (cell) {
          var cellView = paper.findViewByModel(cell.id);
          cellView.options.interactive = value;
          if (cell.get('type') !== 'ice.Generic') {
            if (value) {
              cellView.$el.removeClass('disable-graph');
            } else {
              cellView.$el.addClass('disable-graph');
            }
          } else if (cell.get('type') !== 'ice.Wire') {
            if (value) {
              cellView.$el.find('.port-body').removeClass('disable-graph');
            } else {
              cellView.$el.find('.port-body').addClass('disable-graph');
            }
          }
        });
      };

      this.createBlock = function (type, block) {
        blocks.newGeneric(type, block, function (cell) {
          self.addDraggableCell(cell);
        });
      };

      this.createBasicBlock = function (type) {
        blocks.newBasic(type, function (cells) {
          self.addDraggableCells(cells);
        });
      };

      this.addDraggableCell = function (cell) {
        this.addingDraggableBlock = true;
        var menuHeight = $('#menu').height();
        cell.set('position', {
          x:
            Math.round(
              ((mousePosition.x - state.pan.x) / state.zoom -
                cell.get('size').width / 2) /
                gridsize
            ) * gridsize,
          y:
            Math.round(
              ((mousePosition.y - state.pan.y - menuHeight) / state.zoom -
                cell.get('size').height / 2) /
                gridsize
            ) * gridsize,
        });
        graph.trigger('batch:start');
        addCell(cell);
        disableSelected();
        var opt = {transparent: true, initooltip: false};
        selection.add(cell);
        selectionView.createSelectionBox(cell, opt);
        selectionView.startAddingSelection({
          clientX: mousePosition.x,
          clientY: mousePosition.y,
        });
      };

      this.addDraggableCells = function (cells) {
        this.addingDraggableBlock = true;
        var menuHeight = $('#menu').height();
        if (cells.length > 0) {
          var firstCell = cells[0];
          var offset = {
            x:
              Math.round(
                ((mousePosition.x - state.pan.x) / state.zoom -
                  firstCell.get('size').width / 2) /
                  gridsize
              ) *
                gridsize -
              firstCell.get('position').x,
            y:
              Math.round(
                ((mousePosition.y - state.pan.y - menuHeight) / state.zoom -
                  firstCell.get('size').height / 2) /
                  gridsize
              ) *
                gridsize -
              firstCell.get('position').y,
          };
          _.each(cells, function (cell) {
            var position = cell.get('position');
            cell.set('position', {
              x: position.x + offset.x,
              y: position.y + offset.y,
            });
          });
          graph.trigger('batch:start');
          addCells(cells);
          disableSelected();
          var opt = {transparent: true};
          _.each(cells, function (cell) {
            selection.add(cell);
            selectionView.createSelectionBox(cell, opt);
          });
          selectionView.startAddingSelection({
            clientX: mousePosition.x,
            clientY: mousePosition.y,
          });
        }
      };

      this.toJSON = function () {
        return graph.toJSON();
      };

      this.getCells = function () {
        return graph.getCells();
      };

      this.setCells = function (cells) {
        graph.attributes.cells.models = cells;
      };

      this.selectBoard = function (board, reset) {
        graph.startBatch('change');
        graph.trigger('board', {
          data: {
            previous: common.selectedBoard,
            next: board,
          },
        });
        utils.selectBoard(board.name);
        if (reset) {
          resetBlocks();
        }
        graph.stopBatch('change');
      };

      this.setBlockInfo = function (values, newValues, blockId) {
        if (common.allDependencies === undefined) {
          return false;
        }

        graph.startBatch('change');
        // Trigger info event
        var data = {
          previous: values,
          next: newValues,
        };
        graph.trigger('info', {data: data});

        common.allDependencies[blockId].package.name = newValues[0];
        common.allDependencies[blockId].package.version = newValues[1];
        common.allDependencies[blockId].package.description = newValues[2];
        common.allDependencies[blockId].package.author = newValues[3];
        common.allDependencies[blockId].package.image = newValues[4];

        graph.stopBatch('change');
      };

      this.setInfo = function (values, newValues, project) {
        graph.startBatch('change');
        // Trigger info event
        var data = {
          previous: values,
          next: newValues,
        };
        graph.trigger('info', {data: data});
        project.set('package', {
          name: newValues[0],
          version: newValues[1],
          description: newValues[2],
          author: newValues[3],
          image: newValues[4],
        });
        graph.stopBatch('change');
      };

      this.selectLanguage = function (language) {
        graph.startBatch('change');
        // Trigger lang event
        var data = {
          previous: common.get('language'),
          next: language,
        };
        graph.trigger('lang', {data: data});
        language = utils.setLocale(language);
        graph.stopBatch('change');
        return language;
      };

      function resetBlocks() {
        var data, connectedLinks;
        var cells = graph.getCells();
        _.each(cells, function (cell) {
          if (cell.isLink()) {
            return;
          }
          var type = cell.get('blockType');
          if (type === 'basic.input' || type === 'basic.output') {
            // Reset choices in all Input / blocks
            var view = paper.findViewByModel(cell.id);
            cell.set(
              'choices',
              type === 'basic.input'
                ? common.pinoutInputHTML
                : common.pinoutOutputHTML
            );
            view.clearValues();
            view.applyChoices();
          } else if (type === 'basic.code') {
            // Reset rules in Code block ports
            data = utils.clone(cell.get('data'));
            connectedLinks = graph.getConnectedLinks(cell);
            if (data && data.ports && data.ports.in) {
              _.each(data.ports.in, function (port) {
                var connected = false;
                _.each(connectedLinks, function (connectedLink) {
                  if (connectedLink.get('target').port === port.name) {
                    connected = true;
                    return false;
                  }
                });
                port.default = utils.hasInputRule(port.name, !connected);
                cell.set('data', data);
                paper.findViewByModel(cell.id).updateBox();
              });
            }
          } else if (type.indexOf('basic.') === -1) {
            // Reset rules in Generic block ports
            var block = common.allDependencies[type];
            data = {ports: {in: []}};
            connectedLinks = graph.getConnectedLinks(cell);
            if (block.design.graph.blocks) {
              _.each(block.design.graph.blocks, function (item) {
                if (item.type === 'basic.input' && !item.data.range) {
                  var connected = false;
                  _.each(connectedLinks, function (connectedLink) {
                    if (connectedLink.get('target').port === item.id) {
                      connected = true;
                      return false;
                    }
                  });
                  data.ports.in.push({
                    name: item.id,
                    default: utils.hasInputRule(
                      (item.data.clock ? 'clk' : '') || item.data.name,
                      !connected
                    ),
                  });
                }
                cell.set('data', data);
                paper.findViewByModel(cell.id).updateBox();
              });
            }
          }
        });
      }

      this.resetCommandStack = function () {
        commandManager.reset();
      };

      this.cutSelected = function () {
        if (hasSelection()) {
          utils.copyToClipboard(selection, graph);
          this.removeSelected();
        }
      };

      this.copySelected = function () {
        if (hasSelection()) {
          utils.copyToClipboard(selection, graph);
        }
      };

      this.pasteSelected = function () {
        if (
          document.activeElement.tagName === 'A' ||
          document.activeElement.tagName === 'BODY'
        ) {
          utils.pasteFromClipboard(function (object) {
            if (object.version === common.VERSION) {
              self.appendDesign(object.design, object.dependencies);
            }
          });
        }
      };
      this.pasteAndCloneSelected = function () {
        if (
          document.activeElement.tagName === 'A' ||
          document.activeElement.tagName === 'BODY'
        ) {
          utils.pasteFromClipboard(function (object) {
            if (object.version === common.VERSION) {
              var hash = {};
              // We will clone all dependencies
              if (
                typeof object.dependencies !== false &&
                object.dependencies !== false &&
                object.dependencies !== null
              ) {
                var dependencies = utils.clone(object.dependencies);
                object.dependencies = {};
                var hId = false;

                for (var dep in dependencies) {
                  dependencies[dep].package.name =
                    dependencies[dep].package.name + ' CLONE';
                  var dat = new Date();
                  var seq = dat.getTime();
                  var oldversion = dependencies[dep].package.version.replace(
                    /(.*)(-c\d*)/,
                    '$1'
                  );
                  dependencies[dep].package.version = oldversion + '-c' + seq;

                  hId = utils.dependencyID(dependencies[dep]);
                  object.dependencies[hId] = dependencies[dep];
                  hash[dep] = hId;
                }

                //reassign dependencies

                object.design.graph.blocks = object.design.graph.blocks.map(
                  function (e) {
                    if (
                      typeof e.type !== 'undefined' &&
                      typeof hash[e.type] !== 'undefined'
                    ) {
                      e.type = hash[e.type];
                    }
                    return e;
                  }
                );
              }
              self.appendDesign(object.design, object.dependencies);
            }
          });
        }
      };

      this.selectAll = function () {
        disableSelected();
        var cells = graph.getCells();
        _.each(cells, function (cell) {
          if (!cell.isLink()) {
            selection.add(cell);
            selectionView.createSelectionBox(cell);
          }
        });
      };

      function hasSelection() {
        return selection && selection.length > 0;
      }

      this.removeSelected = function () {
        if (hasSelection()) {
          graph.removeCells(selection.models);
          selectionView.cancelSelection();
          updateWiresOnObstacles();
        }
      };

      function disableSelected() {
        if (hasSelection()) {
          selectionView.cancelSelection();
        }
      }

      var stepValue = 8;

      this.stepLeft = function () {
        performStep({x: -stepValue, y: 0});
      };

      this.stepUp = function () {
        performStep({x: 0, y: -stepValue});
      };

      this.stepRight = function () {
        performStep({x: stepValue, y: 0});
      };

      this.stepDown = function () {
        performStep({x: 0, y: stepValue});
      };

      var stepCounter = 0;
      var stepTimer = null;
      var stepGroupingInterval = 500;
      var allowStep = true;
      var allosStepInterval = 200;

      function performStep(offset) {
        if (selection && allowStep) {
          allowStep = false;
          // Check consecutive-change interval
          if (Date.now() - stepCounter < stepGroupingInterval) {
            clearTimeout(stepTimer);
          } else {
            graph.startBatch('change');
          }
          // Move a step
          step(offset);
          // Launch timer
          stepTimer = setTimeout(function () {
            graph.stopBatch('change');
          }, stepGroupingInterval);
          // Reset counter
          stepCounter = Date.now();
          setTimeout(function () {
            allowStep = true;
          }, allosStepInterval);
        }
      }

      function step(offset) {
        var processedWires = {};
        // Translate blocks
        selection.each(function (cell) {
          cell.translate(offset.x, offset.y);
          selectionView.updateBox(cell);
          // Translate link vertices
          var connectedWires = graph.getConnectedLinks(cell);
          _.each(connectedWires, function (wire) {
            if (processedWires[wire.id]) {
              return;
            }

            var vertices = wire.get('vertices');
            if (vertices && vertices.length) {
              var newVertices = [];
              _.each(vertices, function (vertex) {
                newVertices.push({
                  x: vertex.x + offset.x,
                  y: vertex.y + offset.y,
                });
              });
              wire.set('vertices', newVertices);
            }

            processedWires[wire.id] = true;
          });
        });
      }

      this.isEmpty = function () {
        return graph.getCells().length === 0;
      };

      this.isEnabled = function () {
        return paper ? paper.options.enabled : false;
      };

      this.loadDesign = function (design, opt, callback) {
        if (
          design &&
          design.graph &&
          design.graph.blocks &&
          design.graph.wires
        ) {
          opt = opt || {};
          utils.startWait();
          commandManager.stopListening();
          self.clearAll();
          var cells = graphToCells(design.graph, opt);
          graph.addCells(cells);
          self.setState(design.state);
          self.appEnable(!opt.disabled);
          if (!opt.disabled) {
            commandManager.listen();
          }
          if (callback) {
            callback();
          }
          setTimeout(function () {
            updateWiresOnObstacles();
            utils.endWait();
          }, 0);
          return true;
        }
        return false;
      };

      function graphToCells(_graph, opt) {
        // Options:
        // - new: assign a new id to all the cells
        // - reset: clear I/O blocks values
        // - disabled: set disabled flag to the blocks
        // - offset: apply an offset to all the cells
        // - originalPinout: if reset is true (conversion), this variable
        //   contains the pinout for the previous board.

        var cell;
        var cells = [];
        var blocksMap = {};
        opt = opt || {};
        // Blocks
        var isMigrated = false;

        function getBlocksFromLib(id) {
          for (var dep in common.allDependencies) {
            if (id === dep) {
              return common.allDependencies[dep].design.graph.blocks;
            }
          }
          return false;
        }
        function outputExists(oid, blks) {
          var founded = false;
          for (var i = 0; i < blks.length; i++) {
            if (blks[i].id === oid) {
              return true;
            }
          }
          return founded;
        }
        /* Check if wire source exists (block+port) */
        function wireExists(wre, blk, edge) {
          var founded = false;
          var blk2 = false;

          for (var i = 0; i < blk.length; i++) {
            if (wre[edge].block === blk[i].id) {
              founded = i;
              break;
            }
          }
          if (founded !== false) {
            switch (blk[founded].type) {
              case 'basic.memory':
              case 'basic.constant':
              case 'basic.outputLabel':
              case 'basic.inputLabel':
              case 'basic.code':
              case 'basic.input':
              case 'basic.output':
                founded = true;
                break;

              default:
                /* Generic type, look into the library */
                blk2 = getBlocksFromLib(blk[i].type);
                founded = outputExists(wre[edge].port, blk2);
            }
          }
          return founded;
        }

        // Wires
        var todelete = [];
        for (var i = 0; i < _graph.wires.length; i++) {
          if (
            wireExists(_graph.wires[i], _graph.blocks, 'source') &&
            wireExists(_graph.wires[i], _graph.blocks, 'target')
          ) {
            continue;
          }
          todelete.push(i);
        }
        var tempw = [];
        for (var z = 0; z < _graph.wires.length; z++) {
          if (todelete.indexOf(z) === -1) {
            tempw.push(_graph.wires[z]);
          }
        }
        _graph.wires = utils.clone(tempw);

        _.each(_graph.blocks, function (blockInstance) {
          if (
            blockInstance.type !== false &&
            blockInstance.type.indexOf('basic.') !== -1
          ) {
            if (
              opt.reset &&
              (blockInstance.type === 'basic.input' ||
                blockInstance.type === 'basic.output')
            ) {
              var pins = blockInstance.data.pins;

              // - if conversion from one board to other is in progress,
              //   now is based on pin names, an improvement could be
              //   through hash tables with assigned pins previously
              //   selected by icestudio developers
              var replaced = false;
              for (var i in pins) {
                replaced = false;
                if (typeof opt.designPinout !== 'undefined') {
                  for (var opin = 0; opin < opt.designPinout.length; opin++) {
                    if (
                      String(opt.designPinout[opin].name) ===
                      String(pins[i].name)
                    ) {
                      replaced = true;
                    } else {
                      let prefix = String(pins[i].name).replace(/[0-9]/g, '');
                      if (String(opt.designPinout[opin].name) === prefix) {
                        replaced = true;
                      }
                    }

                    if (replaced === true) {
                      pins[i].name = opt.designPinout[opin].name;
                      pins[i].value = opt.designPinout[opin].value;
                      opin = opt.designPinout.length;
                      replaced = true;
                      isMigrated = true;
                    }
                  }
                }
                if (replaced === false) {
                  pins[i].name = '';
                  pins[i].value = '0';
                }
              }
            }
            cell = blocks.loadBasic(blockInstance, opt.disabled);
          } else {
            if (blockInstance.type in common.allDependencies) {
              cell = blocks.loadGeneric(
                blockInstance,
                common.allDependencies[blockInstance.type],
                opt.disabled
              );
            }
          }

          blocksMap[cell.id] = cell;
          if (opt.new) {
            var oldId = cell.id;
            cell = cell.clone();
            blocksMap[oldId] = cell;
          }
          if (opt.offset) {
            cell.translate(opt.offset.x, opt.offset.y);
          }
          updateCellAttributes(cell);
          cells.push(cell);
        });

        if (isMigrated) {
          alertify.warning(
            _tcStr(
              "If you have blank IN/OUT pins, it's because there is no equivalent in this board"
            )
          );
        }

        _.each(_graph.wires, function (wireInstance) {
          var source = blocksMap[wireInstance.source.block];
          var target = blocksMap[wireInstance.target.block];
          if (opt.offset) {
            var newVertices = [];
            var vertices = wireInstance.vertices;
            if (vertices && vertices.length) {
              _.each(vertices, function (vertex) {
                newVertices.push({
                  x: vertex.x + opt.offset.x,
                  y: vertex.y + opt.offset.y,
                });
              });
            }
            wireInstance.vertices = newVertices;
          }
          cell = blocks.loadWire(wireInstance, source, target);
          if (opt.new) {
            cell = cell.clone();
          }
          updateCellAttributes(cell);
          cells.push(cell);
        });

        return cells;
      }

      this.appendDesign = function (design, dependencies) {
        if (
          design &&
          dependencies &&
          design.graph &&
          design.graph.blocks &&
          design.graph.wires
        ) {
          selectionView.cancelSelection();
          // Merge dependencies
          for (var type in dependencies) {
            if (!(type in common.allDependencies)) {
              common.allDependencies[type] = dependencies[type];
            }
          }

          // Append graph cells: blocks and wires
          // - assign new UUIDs to the cells
          // - add the graph in the mouse position
          var origin = graphOrigin(design.graph);
          var menuHeight = $('#menu').height();
          var opt = {
            new: true,
            disabled: false,
            reset: design.board !== common.selectedBoard.name,
            offset: {
              x:
                Math.round(
                  ((mousePosition.x - state.pan.x) / state.zoom - origin.x) /
                    gridsize
                ) * gridsize,
              y:
                Math.round(
                  ((mousePosition.y - state.pan.y - menuHeight) / state.zoom -
                    origin.y) /
                    gridsize
                ) * gridsize,
            },
          };
          var cells = graphToCells(design.graph, opt);
          graph.addCells(cells);

          // Select pasted elements
          _.each(cells, function (cell) {
            if (!cell.isLink()) {
              var cellView = paper.findViewByModel(cell);
              if (cellView.$box.css('z-index') < z.index) {
                cellView.$box.css('z-index', ++z.index);
              }
              selection.add(cell);
              selectionView.createSelectionBox(cell);
            }
          });
        }
      };

      function graphOrigin(graph) {
        var origin = {x: Infinity, y: Infinity};
        _.each(graph.blocks, function (block) {
          var position = block.position;
          if (position.x < origin.x) {
            origin.x = position.x;
          }
          if (position.y < origin.y) {
            origin.y = position.y;
          }
        });
        return origin;
      }

      function updateCellAttributes(cell) {
        cell.attributes.state = state;
        cell.attributes.rules = common.get('boardRules');
        //cell.attributes.zindex = z.index;
      }

      function addCell(cell) {
        if (cell) {
          updateCellAttributes(cell);
          graph.addCell(cell);
          if (!cell.isLink()) {
            var cellView = paper.findViewByModel(cell);
            if (cellView.$box.css('z-index') < z.index) {
              cellView.$box.css('z-index', ++z.index);
            }
          }
        }
      }

      function addCells(cells) {
        _.each(cells, function (cell) {
          updateCellAttributes(cell);
        });
        graph.addCells(cells);
        _.each(cells, function (cell) {
          if (!cell.isLink()) {
            var cellView = paper.findViewByModel(cell);
            if (cellView.$box.css('z-index') < z.index) {
              cellView.$box.css('z-index', ++z.index);
            }
          }
        });
      }

      this.resetCodeErrors = function () {
        var cells = graph.getCells();
        return new Promise(function (resolve) {
          _.each(cells, function (cell) {
            var cellView;
            if (cell.get('type') === 'ice.Code') {
              cellView = paper.findViewByModel(cell);
              cellView.$box
                .find('.code-content')
                .removeClass('highlight-error');
              $('.sticker-error', cellView.$box).remove();
              cellView.clearAnnotations();
            } else if (
              cell.get('type') === 'ice.Generic' ||
              cell.get('type') === 'ice.Constant'
            ) {
              cellView = paper.findViewByModel(cell);
              $('.sticker-error', cellView.$box).remove();
              cellView.$box
                .remove('.sticker-error')
                .removeClass('highlight-error');
            }
          });
          resolve();
        });
      };

      $(document).on('codeError', function (evt, codeError) {
        var cells = graph.getCells();
        _.each(cells, function (cell) {
          var blockId, cellView;
          if (
            (codeError.blockType === 'code' &&
              cell.get('type') === 'ice.Code') ||
            (codeError.blockType === 'constant' &&
              cell.get('type') === 'ice.Constant')
          ) {
            blockId = utils.digestId(cell.id);
          } else if (
            codeError.blockType === 'generic' &&
            cell.get('type') === 'ice.Generic'
          ) {
            blockId = utils.digestId(cell.attributes.blockType);
          }
          if (codeError.blockId === blockId) {
            cellView = paper.findViewByModel(cell);
            if (codeError.type === 'error') {
              $('.sticker-error', cellView.$box).remove();
              if (cell.get('type') === 'ice.Code') {
                cellView.$box
                  .find('.code-content')
                  .addClass('highlight-error')
                  .append(
                    '<div class="sticker-error error-code-editor"></div>'
                  );
              } else {
                cellView.$box
                  .addClass('highlight-error')
                  .append('<div class="sticker-error"></div>');
              }
            }
            if (cell.get('type') === 'ice.Code') {
              cellView.setAnnotation(codeError);
            }
          }
        });
      });
    }
  );