mitjajez/SONCE

View on GitHub
imports/ui/components/circuits-show.js

Summary

Maintainability
F
2 wks
Test Coverage
import './circuits-show.html';

import { Template } from 'meteor/peerlibrary:blaze-components';
import { Mongo } from 'meteor/mongo';
import { ReactiveDict } from 'meteor/reactive-dict';
import { Session } from 'meteor/session';
import { $ } from 'meteor/jquery';
import { _ } from 'meteor/underscore';
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
import { Hammer } from 'meteor/chriswessels:hammer';
import Snap from 'snapsvg';

// API
import { Elements } from '../../api/elements/elements.js';
import { Symbols } from '../../api/symbols/symbols.js';
import { Wires } from '../../api/wires/wires.js';

import { displayError } from '../lib/errors.js';
import {
  getLastPointFromPathD,
  getCanvasPoint,
} from '../lib/svg.js';

// Component used in the template
import './ruler/ruler.js';
import './elements-item.js';
import './wires-item.js';
import './command-box.js';
import './element-info.js';
//import './info-box.js';
//import './edit-menu.js';
import './element-edit-menu.js';
import './wire-edit-menu.js';
import './operations.js';
import './active-element.js';
import './active-pin.js';

import {
  insertElement,
} from '../../api/elements/methods.js';

import {
  insertWire,
  updateWireD,
  updateWireName,
  updateNetName,
  addWireEnd,
  removeWire,
} from '../../api/wires/methods.js';

Template.Circuits_show.onCreated(function circuitShowOnCreated() {
  Session.setDefault('component2add', false);

  this.state = new ReactiveDict();
  this.state.setDefault({
    width: 0,
    height: 0,
    acting: 'viewing',    // viewing | editing | adding | wiring
    active: false,        // element | wire | net | node
    selection: false,     //
    dragging: false,      // panning | dragging
    menuPosition: false,  // element center | click coords on wire
    wiringMode: 'axis-x', // ['a-x', 'a-l', 'l', 'l-a', 'a-y']
    svgOffset: false,
    dragOffset: false,
    zoom: 1,
    pan: {x:0, y:0},
    mouse: {x:0, y:0},
  });

  this.active = new ReactiveDict();
  this.active .setDefault({
    element: false, // {component: 'r', transform: {x: 0, y:0, rot:0}, visibility: 'hidden'}
    wire: false,    // {d:'M0,0', visibility: 'hidden'}
  });

  this.autorun(() => {
    new SimpleSchema({
      circuit: { type: Function },
      elements: { type: Mongo.Cursor },
      wires: { type: Mongo.Cursor },
      subscriptionsReady: { type: Boolean },
    }).validate(Template.currentData());

    if( Session.get( 'component2add' ) ){
      this.state.set('acting', 'adding');
    }
  });


// ########## SVG related ######################################################
  this.snapToGrid = (T) => {
    const gridSpace = 10;
    return {
      x: Math.round(T.x/gridSpace) * gridSpace,
      y: Math.round(T.y/gridSpace) * gridSpace,
    };
  };

  this.getCanvasCenter = () => {
    const C = Snap('.js-circuit-canvas').select('.js-circuit');
    const t = C.transform().globalMatrix.split();
    return {
      x: (this.state.get('width')/2 - t.dx) / t.scalex,
      y: (this.state.get('height')/2 - t.dy) / t.scaley,
    };
  };

  this.getEventPoint = (event, mode) => {
    const T = { x:0, y:0 };
    if (event.type.indexOf('mouse') > -1) {
      //event.type === 'mousemove'
      if(mode === 'svg') {
        const C = Snap('.js-circuit-canvas').select('.js-circuit');
        const t = C.transform().globalMatrix.split();
        T.x = (event.offsetX - t.dx) / t.scalex;
        T.y = (event.offsetY - t.dy) / t.scaley;
      }
      else {
        T.x = event.offsetX;
        T.y = event.offsetY;
      }
    }
    else if (event.pointerType === 'mouse') {
      if(mode === 'svg') {
        const C = Snap('.js-circuit-canvas').select('.js-circuit');
        const t = C.transform().globalMatrix.split();
        T.x = (event.layerX - t.dx) / t.scalex;
        T.y = (event.layerY - t.dy) / t.scaley;
      }
      else {
        T.x = event.layerX;
        T.y = event.layerY;
      }
    }
    else if(event.pointerType === 'touch'){
      if(mode === 'svg') {
        const C = Snap('.js-circuit-canvas').select('.js-circuit');
        const t = C.transform().globalMatrix.split();
        T.x = event.deltaX / t.scalex;
        T.y = event.deltaY / t.scaley;
      }
      else {
        T.x = event.deltaX;
        T.y = event.deltaY;
      }
    }
    return T;
  };


  this.setmenuPosition = (T0, mode) => {
    const T = T0;
    if(mode === 'svg') {
      const C = Snap('.js-circuit-canvas').select('.js-circuit');
      const t = C.transform().globalMatrix.split();
      T.x = T0.x / t.scalex + t.dx;
      T.y = T0.y / t.scaley + t.dy;
    }
    this.state.set('menuPosition', `translate(${T.x},${T.y})`);
  };

  this.zoom = (event) => {
    const C = Snap('.js-circuit-canvas').select('.js-circuit');
    let z = 1;
    let T = {x:0, y:0};
    const t = C.transform().globalMatrix.split();
    T.x = (this.state.get('width')/2 - t.dx) / t.scalex;
    T.y = (this.state.get('height')/2 - t.dy) / t.scaley;

    if(event.type === 'pinch') {
      // console.log('ZOOM pinch');
      // what here?
    }
    else {
      T = this.state.get('mouse');
      z = event.originalEvent.deltaY > 0 ? 0.9 : 1.1;
    }
    const out = C.transform().globalMatrix.scale(z, z, T.x, T.y).split();
    this.state.set('zoom', out.scalex );
    this.state.set('pan', {x: out.dx, y: out.dy} );
  };

  this.zoomIn = () => {
    const C = Snap('.js-circuit-canvas').select('.js-circuit');
    const T = this.getCanvasCenter();
    const z = 1.1;
    const out = C.transform().globalMatrix.scale(z, z, T.x, T.y).split();
    this.state.set('zoom', out.scalex );
    this.state.set('pan', {x: out.dx, y: out.dy} );
  };

  this.zoomIn = () => {
    const C = Snap('.js-circuit-canvas').select('.js-circuit');
    const T = this.getCanvasCenter();
    const z = 0.9;
    const out = C.transform().globalMatrix.scale(z, z, T.x, T.y).split();
    this.state.set('zoom', out.scalex );
    this.state.set('pan', {x: out.dx, y: out.dy} );
  };

  this.pan = (event) => {
    // console.log('PAN');
    const C = Snap('.js-circuit-canvas').select('.js-circuit');
    if(event.type === 'pan'){
      if(!this.state.get('dragOffset')){
        this.state.set('dragOffset', this.state.get('pan') );
      }
      const off = this.state.get('dragOffset' );
      const dT = this.getEventPoint(event, 'svg');
      if(event.srcEvent.type === 'touchmove') {
        this.state.set('pan', {x: (off.x+dT.x), y: (off.y+dT.y)} );
      }
      if(event.srcEvent.type === 'touchend') {
        this.state.set('dragOffset', false );
      }
    }
    else {
      const T = this.getEventPoint(event, 'svg');
      const T0 = this.state.get('dragOffset');
      const t =  C.transform().globalMatrix.translate(T.x - T0.x, T.y - T0.y);
//        sCircuit.transform(t);
      const out = t.split();
      this.state.set('pan', {x: out.dx*1, y: out.dy*1} );
    }
  };


// ########## wiring ###########################################################

  this.wiringModes = ['axis-x', 'axis-line', 'line', 'line-axis', 'axis-y'];

  this.getWiringMode = () => {
    return this.state.get('wiringMode');
  };

  this.setWiringMode = (mode) => {
    this.state.set('wiringMode', mode);
  };

  this.newNetPathPoint = (T) => { // ['a-x', 'a-l', 'l', 'l-a', 'a-y']
    const d = this.$('.js-active-wire').data().wire.d;
    const p = d ? getLastPointFromPathD(d) : {x:0, y:0};
//    let p = {x:0, y:0};
//    if(d) {
//      p = this.getLastPoint(d);
//    }

    let str = '';
    let dl = 0;
    switch (this.state.get('wiringMode')) {
    case 'axis-x':
      str = ` H${T.x} V${T.y}`;
      break;
    case 'axis-line':
      if( Math.abs(T.x-p.x) > Math.abs(T.y-p.y) ) {
        dl = Math.abs(T.y-p.y);
        str = T.x > p.x ? ` H${T.x-dl}` : ` H${T.x+dl}`;
        str += T.x > p.x ? ` l${dl}` : ` l${-dl}`;
        str += T.y > p.y ? `,${dl}` : `,${-dl}`;
      }
      else {
        dl = Math.abs(T.x-p.x);
        str = T.y > p.y ? ` V${T.y-dl}` : ` V${T.y+dl}`;
        str += T.x > p.x ? ` l${dl}` : ` l${-dl}`;
        str += T.y > p.y ? `,${dl}` : `,${-dl}`;
      }
      break;
    case 'line':
      str = ` L${T.x},${T.y}`;
      break;
    case 'line-axis':
      if(Math.abs(T.x-p.x) > Math.abs(T.y-p.y)){ // LH ...
        dl = Math.abs(T.y-p.y);
        str = T.x > p.x ? ` l${dl}` : ` l${-dl}`;
        str += T.y > p.y ? `,${dl}` : `,${-dl}`;
        str += ` H${T.x}`;
      }
      else { // LV
        dl = Math.abs(T.x-p.x);
        str = T.x > p.x ? ` l${dl}` : ` l${-dl}`;
        str += T.y > p.y ? `,${dl}` : `,${-dl}`;
        str += ` V${T.y}`;
      }
      break;
    case 'axis-y':
      str = ` V${T.y} H${T.x}`;
      break;
    default:
    }
    return str;
  };

  this.focusCli = () => {
    this.$('.command-box input[name="command-line"]').focus();
  };

});

Template.Circuits_show.onRendered(function circuitShowOnRendered() {
  const instance = Template.instance();
  const b = $( '.js-circuit-canvas' )[0].getBoundingClientRect();
  instance.state.set({
    width: b.width,
    height: b.height,
  });
  $(window).resize(function() {
    const b = $( '.js-circuit-canvas' )[0].getBoundingClientRect();
    instance.state.set({
      width: b.width,
      height: b.height,
    });
  });
});


Template.Circuits_show.helpers({
  mouse: () => Template.instance().state.get('mouse'),
  zoom: () => Template.instance().state.get('zoom'),
  pan: () => Template.instance().state.get('pan'),
  viewZoom() {
    const zoom = Template.instance().state.get('zoom');
    return `${(zoom*100).toFixed()}`;
  },
  viewPan() {
    const pan = Template.instance().state.get('pan');
    return `${(pan.x).toFixed()},${(pan.y).toFixed()}`;
  },

  actingClass: () => Template.instance().state.get('acting'),
  active: () => Template.instance().state.get('active'),
  selection: () => Template.instance().state.get('selection'),

  viewing: () => Template.instance().state.equals('acting', 'viewing'),
  adding: () => Template.instance().state.equals('acting', 'adding'),
  editing: () => Template.instance().state.equals('acting', 'editing'),
  wiring: () => Template.instance().state.equals('acting', 'wiring'),
  wiringMode: () => Template.instance().state.get('wiringMode'),

  selectedElement: () => Template.instance().state.equals('active', 'element'),
  selectedWire: () => Template.instance().state.equals('active', 'wire') ||
                      Template.instance().state.equals('active', 'net'),

  cancel: () => {
    const state = Template.instance().state;
    return !state.equals('acting', 'viewing') || !state.equals('active', false);
  },
  rulerArgs() {
    const instance = Template.instance();
    return {
      width: instance.state.get('width'),
      height: instance.state.get('height'),
      zoom: instance.state.get('zoom'),
      pan: instance.state.get('pan'),
//      mouse: instance.state.get('mouse'),
    };
  },
  elementArgs(element) {
    const instance = Template.instance();
    return {
      element,
      'symbol': Symbols.findOne({'key': element.symbol}),
      'editing': instance.state.equals('acting', 'editing'),
      'selected': instance.state.equals('selection', element._id),
      setSelected(select) {
        instance.state.set('selection', select ? element._id : false);
        instance.state.set('active', select ? 'element' : false);
      },
    };
  },
  wireArgs(wire) {
    const instance = Template.instance();
    return {
      wire,
      wiring: instance.state.equals('acting', 'wiring'),
      selected: instance.state.equals('selection', wire._id),
      setSelected(select) {
        instance.state.set('selection', select ? wire._id : false);
        instance.state.set('active', select ? 'wire' : false);
      },
    };
  },
  elementMenuArgs() {
    const instance = Template.instance();
    const eid = instance.state.get('selection');
    return {
      element: Elements.findOne({ _id: eid}),
      cid: this.circuit()._id,
      active: instance.state.get('active'),       // migrate to MenuArgs
      selection: instance.state.get('selection'),
      menuPosition: instance.state.get('menuPosition'),
      setSelected(doSelect) {
        instance.state.set('selection', doSelect ? eid : false);
        instance.state.set('active', doSelect ? 'element' : false);
      },
    };
  },
  wireMenuArgs() {
    const instance = Template.instance();
    const wid = instance.state.get('selection');
    return {
      cid: this.circuit()._id,
      active: instance.state.get('active'),       // migrate to MenuArgs
      selection: instance.state.get('selection'),
      menuPosition: instance.state.get('menuPosition'),
      setSelected(select) {
        instance.state.set('selection', select ? wid : false);
        instance.state.set('active', select ? 'wire' : false);
      },
    };
  },

  configureHammer () {
//    return function (hammer, instance) {
    return function (hammer) {
      const twoFingerSwipe = new Hammer.Swipe({
        event: '2fingerswipe', /* prefix for custom swipe events, e.g. 2fingerswipeleft, 2fingerswiperight */
        pointers: 2,
        velocity: 0.5,
      });
      hammer.add([twoFingerSwipe]);

      return hammer;
    };
  },
  templateGestures: {
    'pan .js-circuit-canvas'(event, instance) {
      instance.pan(event);
    },
    'pinch .js-circuit-canvas'(event, instance) {
      instance.zoom(event);
    },
    'tap .js-select-element'(event, instance) {
      // console.log('tap .js-select-element');
      this.selected ? this.setSelected(false) : this.setSelected(true);
      const p = this.element.transform;
      instance.setmenuPosition({x:p.x, y:p.y}, 'svg');
      instance.focusCli();
    },

    'doubletap .js-select-element'(event, instance) {
      // console.log('doubletap .js-select-element');
      this.selected ? this.setSelected(false) : this.setSelected(true);
      const p = this.element.transform;
      instance.setmenuPosition({x:p.x, y:p.y}, 'svg');
      instance.state.set('acting', 'editing');
      instance.focusCli();
    },
  },
});


Template.Circuits_show.events({
  // COMMON --------------------------------------------------------------------
  'click .js-view-circuit' (event, instance) {
    event.preventDefault();
    instance.state.set('acting', 'viewing');
  },
  'click .js-edit-circuit' (event, instance) {
    event.preventDefault();
    instance.state.set('acting', 'editing');
  },
  'click .js-wire-circuit' (event, instance) {
    event.preventDefault();
    instance.state.set('acting', 'wiring');
  },
  'click .js-cancel-selection' (event, instance) {
    instance.state.set('selection', false);
    instance.state.set('active', false);
  },

  'wheel .js-circuit-canvas'(event, instance) {
    const T = instance.state.get('mouse');
    instance.zoom(event);
  },
  'click .js-zoom-out' (event, instance) {
    instance.zoomOut;
  },
  'click .js-zoom-in' (event, instance) {
    instance.zoomIn();
  },
  'focus input[name=zoom]' (event, instance) {
    instance.$('input[name=zoom]').val('');
  },
  'blur input[name=zoom]' (event, instance) {
    instance.$('input[name=zoom]').val(instance.state.zoom);
  },
  'submit .js-set-zoom' (event, instance) {
    event.preventDefault();
    const val = instance.$('input[name=zoom]').val();
    const zoom = parseInt(val, 10);
    if(!isNaN(zoom)) {
      // console.log( zoom/100 );
    }
  },

  'mousedown .js-circuit-canvas'(event, instance){
    event.preventDefault();
    if( instance.$(event.target).is('svg.js-circuit-canvas') ){
      instance.state.set('dragging', 'panning'); //panning | moving
    }
    else if ( instance.$(event.target).is('.js-select-element') ) {
      // copy element to js-active-element
      instance.state.set('dragging', 'moving'); //panning | moving
      // instance.$('js-active-element').attr('visibility', 'visable');
    }
    const T = instance.state.get('mouse');
    instance.state.set('dragOffset', T);
  },

  'mouseup .js-circuit-canvas'(event, instance){
    event.preventDefault();
    instance.state.set('dragging', false);
  },

  'mousemove .js-circuit-canvas'(event, instance) {
    event.preventDefault();
    const T = {
      x: event.pageX - $(event.currentTarget).offset().left,
      y: event.pageY - $(event.currentTarget).offset().top,
    };

    const pan = instance.state.get('pan');
    const zoom = instance.state.get('zoom');
    const p = getCanvasPoint(T, pan, zoom);
    instance.state.set('mouse', p );

    if( instance.state.equals('dragging', 'panning') ){
      instance.pan(event);
    }
    if( instance.state.equals('dragging', 'moving') ){
      // console.log('to MOVE');
//      instance.move(event);
    }
  },

  // SELECTING -----------------------------------------------------------------
  'click .js-select-element'(event, instance) {
    event.preventDefault();
    this.selected ? this.setSelected(false) : this.setSelected(true);
    const T = this.element.transform;
    instance.state.set('menuPosition', `translate(${T.x},${T.y})`);
  },

  'dblclick .js-select-element'(event, instance) {
    event.preventDefault();
    this.selected ? this.setSelected(false) : this.setSelected(true);
    const T = this.element.transform;
    instance.state.set('menuPosition', `translate(${T.x},${T.y})`);
    instance.state.set('acting', 'editing');
    instance.focusCli();
  },

  'click .viewing .js-wire, click .editing .js-wire' (event, instance) {
    event.preventDefault();
    const T = instance.state.get('mouse');
    const p = instance.snapToGrid(T);
    instance.state.set('menuPosition', `translate(${p.x},${p.y})`);
    this.setSelected(true);
  },

  // ADDING --------------------------------------------------------------------
  'mouseenter .adding .js-circuit-canvas'(event, instance) {
    const newElement = Session.get('component2add');
    const symbolsSVG = Session.get('symbolsSVG');
    const $active = instance.$('.js-active-element');
    const $data = $active.data();
    $data.element = {
      component: newElement.key,
      type: newElement.type,
      symbol: newElement.symbol,
      cid: this.circuit()._id,
      pins: newElement.pins,
    };
    $data.element.pins.forEach( (pin) => {
      pin.net = 'open';
    });

    if(!$data.element.transform){
      $data.element.transform = {x:0, y:0, rot:0};
    }

    const s = Snap('.js-active-element').select('use');
    s.attr({
      'xlink:href': `${symbolsSVG}#${newElement.symbol}`,
    });
    $active.attr({'visibility': 'visible'});
    instance.$('input[name=command-line]').focus();
  },

  'mousemove .adding .js-circuit-canvas'(event, instance) {
    const T = instance.state.get('mouse');
    const p = instance.snapToGrid(T);
    const $active = instance.$('.js-active-element');
    const t = $active.data().element.transform;
    t.x = p.x;
    t.y = p.y;
    $active.attr({
      'transform': `translate(${t.x},${t.y}) rotate(${t.rot})`,
    });
  },

  'click .adding .js-circuit-canvas'(event, instance) {
    const newElement = instance.$('.js-active-element').data().element;
    // console.log( newElement );
    insertElement.call( newElement, displayError);
  },

  'keyup .adding input[name=command-line]' (event, instance) {
    // console.log( 'keyup .adding' );
    if (event.which === 82) { // r
      // console.log('--> r ... rotate');
      const $active = instance.$('.js-active-element');
      const t = $active.data().element.transform;
      t.rot += 90;
      if(t.rot > 360) t.rot = 0;
      instance.$('input[name=command-line]').val('');
    }
  },


  // WIRING --------------------------------------------------------------------
  'mouseenter .js-pin' (event, instance) {
    const $pin = instance.$(event.currentTarget);
    const cx = $pin.attr('cx');
    const cy = $pin.attr('cy');
    const $data = $pin.data();
    const mx = new Snap.Matrix();
    const t = this.element.transform;
    mx.translate(t.x, t.y);
    if( t.rot ) mx.rotate(t.rot);
    const p = {x: mx.x(cx, cy), y: mx.y(cx, cy)};

    const $active = instance.$('.js-active-pin');
    $active.data({
      'e': $data.element,
      'p': $data.id,
    });
    $active.attr({
      'id': $pin.attr('id'),
      'cx': p.x,
      'cy': p.y,
      'visibility': 'visable',
    });
  },

  'mouseleave .js-active-pin' (event, instance) {
    const $pin = instance.$('.js-active-pin');
    $pin.attr({'visibility': 'hidden'});
  },

  'click .wiring .js-active-pin' (event, instance) {
    const $pin = instance.$( '.js-active-pin' );
    const p = {
      id: $pin.attr('id'),
      x: $pin.attr('cx'),
      y: $pin.attr('cy'),
    };
    const pData = $pin.data();
    const $wire = instance.$('.js-active-wire');
    const w = $wire.data().wire;
    // console.log( `CLICK on PIN ${p.id}` );

    if (instance.state.get('selection') &&
    instance.state.equals('active', 'wire') ) {
      // end this wire
      if( instance.state.equals('startWire', p.id) ) {
        // remove all about this wire
        // console.log( '--> Cancel wiring' );
        if( !instance.state.equals('selection', '.js-active-wire')) {
          removeWire.call({
            wid: instance.state.get('selection'),
          }, displayError);
        }
      }
      else {
        // console.log( '--> Stop wiring' );
        w.d += instance.newNetPathPoint(p);
        w.ends.push({e: pData.e, p: `${pData.p}`});
        if( instance.state.equals('selection', '.js-active-wire') ){
          // insert new wire
          insertWire.call({
            name: w.name,
            d:    w.d,
            ends: w.ends.map( function(end) { return {e: `${end.e}`, p: `${end.p}`}; }),
            cid:  w.cid,
          }, displayError);
        }
        else {
          // update exist wire
          const wid = instance.state.get('selection');
          updateWireD.call({ wid, newD: w.d }, displayError);
          addWireEnd.call({
            wid,
            newEnd: {e: pData.e, p: `${pData.p}`},
          }, displayError);
        }
      }
      // instance.stopWire();
      $wire.attr({ 'visibility': 'hidden', 'd': '' });
      $wire.data().wire = {};
      instance.state.set('active', false);
      instance.state.set('selection', false);
      instance.state.set('startWire', false);
    }
    else {
      // start new wire
      instance.state.set('startWire', p.id);
      $wire.data().wire = {
        ends: [{e: pData.e, p: `${pData.p}`}],
        cid: instance.data.circuit()._id,
        d: `M${p.x},${p.y}`,
      };
      $wire.attr({ visibility: 'visible' });
      instance.state.set('active', 'wire');
      instance.state.set('selection', '.js-active-wire');
      instance.focusCli();
      // console.log( '  --> Start wiring' );
    }
  },
  'click .wiring .js-circuit-canvas' (event, instance) {
    if(instance.$(event.currentTarget).is('.js-circuit-canvas') &&
    !instance.state.equals('startWire', false) ) {
      console.log( 'CLICK on SVG ACTIVE WIRE' ); // testing
    }
  },

  'click .wiring .js-active-wire'(event, instance){
    // console.log( 'CLICK on ACTIVE WIRE' );
    const T = instance.state.get('mouse');
    const p = instance.snapToGrid(T);
    const w = instance.$('.js-active-wire').data().wire;
    w.d += instance.newNetPathPoint(p);
    if ( instance.state.equals('selection', '.js-active-wire') ) {
      const wid = insertWire.call({
        d: w.d,
        ends: w.ends.map( (end) => { return { e: end.e, p: `${end.p}` }; }),
        cid: w.cid,
      }, displayError);
      instance.state.set('selection', wid);
      w.name = Wires.findOne(wid).name;
    }
    else {
      const wid = instance.state.get('selection');
      updateWireD.call( {wid, newD:w.d}, displayError);
    }
    instance.focusCli();
  },

  'click .wiring .js-wire'(event, instance) {
    const name = instance.$(event.currentTarget).attr('name');
    const T = instance.state.get('mouse');
    const p = instance.snapToGrid(T);
    // console.log( `CLICK on WIRE ${name}` );
    const $wire = instance.$('.js-active-wire');
    const $wData = $wire.data();
    const w = $wData.wire;

    if (instance.state.get('selection') &&
    instance.state.equals('active', 'wire') ) {
      // end this wire
      // console.log( '--> Stop wiring' );
      // const start = instance.state.get('startWire');
      // console.log( `start: ${start}`);
      // console.log( `selection: ${instance.state.get('selection')}`);
      // console.log( `name: ${name}`);
      // console.log( `w.name: ${w.name}`);


      if ( !instance.state.equals('selection', '.js-active-wire') &&
      w.name !== name) {
        // console.log(`CONFLICT: ${w.name} !== ${name}; merge to?'`);
        // TODO: prompt here
        updateNetName.call({
          cid: w.cid, net:w.name, newName: name,
        }, displayError);
        w.name = name;
      }

      w.d += instance.newNetPathPoint(p);
      if ( instance.state.equals('selection', '.js-active-wire') ){
        // instance.insertWire(wire);
        w.ends.push({e: 'node', p: 'new'});
        w.name = name;
        insertWire.call({
          name: w.name,
          d: w.d,
          ends: w.ends.map( function(end) { return {e: `${end.e}`, p: `${end.p}`}; }),
          cid: w.cid,
        }, displayError);
      }
      else {
        // instance.endWire(d,end);
        const wid = instance.state.get('selection');
        updateWireD.call({ wid, newD: w.d}, displayError);
        addWireEnd.call({
          wid,
          newEnd: { e: 'node', p: 'new' },
        }, displayError);
        updateWireName.call({
          wid,
          newName: w.name,
        }, displayError);

      }
      // instance.stopWire();
      $wire.attr({ 'visibility': 'hidden', 'd': '' });
      $wire.data().wire = {};
      instance.state.set('active', false);
      instance.state.set('selection', false);
      instance.state.set('startWire', false);
    }
    else {
      // start new wire
      // console.log( '  --> Start wiring' );
      instance.state.set('startWire', name);
      $wData.wire = {
        name,
        ends: [{e: 'node', p: 'new'}],
        cid: instance.data.circuit()._id,
        d: `M${p.x},${p.y}`,
      };
      $wire.attr({ visibility: 'visible' });
      instance.state.set('active', 'wire');
      instance.state.set('selection', '.js-active-wire');
      instance.focusCli();
    }
  },

  'mousemove .wiring .js-circuit-canvas'(event, instance){
    const $wire = instance.$('.js-active-wire');
    if ( $wire.attr('visibility') === 'visible') {
      const T = instance.state.get('mouse');
      const p = instance.snapToGrid(T);
      $wire.attr('d', $wire.data().wire.d + instance.newNetPathPoint(p));
    }
  },

  'keyup .wiring input[name=command-line]' (event, instance) {
    // console.log( 'keydown .wiring' );
    const $cli = instance.$('input[name=command-line]');
    if (event.which === 87) {
      // console.log('--> w ... wiring mode');
      const modes = instance.wiringModes;
      const mode = instance.getWiringMode();
      let iMode = _.indexOf(modes, mode) + 1;
      if(iMode === modes.length){
        iMode = 0;
      }
      instance.setWiringMode( modes[iMode] );
    }
    // ESC
    if (event.which === 27) {
      event.preventDefault();
      // console.log( '--> ESC');
      const $active = instance.$('.js-active-wire');
      $active.attr({
        'visibility': 'hidden',
        'd': '',
      });
      $active.data().wire = {};
      const wid = instance.state.get('selection');
      if(wid && !instance.state.equals('selection', '.js-active-wire')) {
        removeWire.call({ wid }, displayError);
      }
      instance.state.set('active', false);
      instance.state.set('selection', false);
      instance.state.set('startWire', false);
    }
    $cli.val('');
  },

  // CANCELING -----------------------------------------------------------------
  'mousedown .js-cancel, click .js-cancel'(event, instance) {
    event.preventDefault();
    if(Session.get( 'component2add' )){
      Session.set( 'component2add', false );
      instance.state.set('acting', 'viewing');
      instance.$('.js-active-element').attr({'visibility': 'hidden'});
    }
    else if ( instance.state.get('active') || instance.state.get('selection')) {
      instance.state.set('active', false);
      instance.state.set('selection', false);
    }
    else {
      instance.state.set('acting', 'viewing');
    }
  },

  // This is for the mobile dropdown
  'change .js-action'(event, instance) {
    const target = event.target;
    if ($(target).val() === 'edit') {
      instance.editCircuit();
    } else if ($(target).val() === 'delete') {
      instance.deleteCircuit();
    } else {
      instance.toggleCircuitPrivacy();
    }

    target.selectedIndex = 0;
  },

  'click .js-edit'(event, instance) {
    // console.log( 'CLICK EDITING' );
    instance.state.set('acting', 'editing');
  },
});