Pārlūkot izejas kodu

Frontend changes

* **UI/UX**: removed SVGMatrix favouring CSSMatrix and manual matrix calculations                                                         ~
   Area now rotates around a point, although this is not really useful                                                                     ~
* **UI/UX**: improved selection behaviour                                                                                                 ~
* **UI/UX**: Added context menu for editor area, removed some buttons from top button bar    <Paste>
luis 7 gadi atpakaļ
vecāks
revīzija
ab8c821441

+ 7 - 3
browser/vue-flow/package.json

@@ -6,21 +6,25 @@
   "license": "MIT",
   "private": true,
   "scripts": {
-    "docker":
-      "docker build --rm -t hexasoftware.com:5000/flow-proto -f ./docker/Dockerfile .",
+    "docker": "docker build --rm -t hexasoftware.com:5000/flow-proto -f ./docker/Dockerfile .",
     "docker-pull": "docker push hexasoftware.com:5000/flow-proto",
     "dev": "cross-env NODE_ENV=development webpack-dev-server --hot",
     "build": "cross-env NODE_ENV=production webpack --hide-modules"
   },
   "dependencies": {
     "github-markdown-css": "^2.10.0",
+    "rematrix": "^0.2.2",
     "vue": "^2.5.11",
     "vue-router": "^3.0.1",
     "vue2-smooth-scroll": "^1.0.1",
     "vuex": "^3.0.1",
     "vuex-router-sync": "^5.0.0"
   },
-  "browserslist": ["> 1%", "last 2 versions", "not ie <= 8"],
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 8"
+  ],
   "devDependencies": {
     "babel-core": "^6.26.0",
     "babel-eslint": "^8.2.1",

+ 7 - 0
browser/vue-flow/src/assets/doc/readme.md

@@ -305,6 +305,13 @@ nodes to be processed in an action having possible multiple nodes per action
 
 ## Changelog
 
+27/02/2018
+
+* **UI/UX**: removed SVGMatrix favouring CSSMatrix and manual matrix calculations
+  Area now rotates around a point, although this is not really useful
+* **UI/UX**: improved selection behaviour
+* **UI/UX**: Added context menu for editor area, removed some buttons from top button bar
+
 25/02/2018
 
 * **UI/UX**: Added image visualization if the content result is a dataurl

+ 66 - 27
browser/vue-flow/src/components/flow/editor.js

@@ -7,7 +7,9 @@ import FlowModalData from './modal-data' // NEW 15/02/2018
 import HxContextMenu from '@/components/shared/hx-contextmenu'
 import SvgDefs from './svgdefswrapper'
 import utils from '@/utils/utils'
+import mat from '@/utils/mat'
 
+const affIdentity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
 export default {
   name: 'FlowManager',
   components: {FlowNode, FlowLink, FlowTriggerLink, FlowPanZoom, FlowModalData, HxContextMenu, SvgDefs},
@@ -17,7 +19,8 @@ export default {
   },
   data () {
     return {
-      panzoom: { x: 0, y: 0, zoom: 1 },
+      // panzoom: { x: 0, y: 0, zoom: 1 },
+      panzoomCTM: affIdentity,
       dragging: null,
       linking: false,
       triggerLinking: false,
@@ -33,6 +36,14 @@ export default {
   },
   computed: {
     ...mapGetters('flow', ['registry', 'activity', 'nodeData', 'nodeById', 'nodeSelection']),
+    panzoomIdentity () {
+      for (let i = 0; i < 16; i++) {
+        if (this.panzoomCTM[i] !== affIdentity[i]) {
+          return false
+        }
+      }
+      return true
+    },
     outputNode () {
       const n = this.nodeData.nodes.find(n => n.src === 'Output')
       return !!n
@@ -81,7 +92,7 @@ export default {
       'NOTIFICATION_ADD',
       'DOCUMENT_SYNC',
       'NODE_RAISE', 'NODE_UPDATE', 'NODE_ADD', 'NODE_REMOVE', 'NODE_INSPECT', 'NODE_PROCESS', 'NODE_TRAIN',
-      'NODE_SELECTION_ADD', 'NODE_SELECTION_SET', 'NODE_SELECTION_CLEAR',
+      'NODE_SELECTION_ADD', 'NODE_SELECTION_SET', 'NODE_SELECTION_REMOVE', 'NODE_SELECTION_CLEAR',
       'LINK_ADD', 'LINK_REMOVE',
       'TRIGGER_ADD', 'TRIGGER_REMOVE' ]),
 
@@ -108,7 +119,7 @@ export default {
           if (ev.ctrlKey) {
             ev.preventDefault()
             ev.stopPropagation()
-            this.select(this.nodeData.nodes)
+            this.NODE_SELECTION_SET(this.nodeData.nodes)
           }
           break
       }
@@ -120,7 +131,7 @@ export default {
       }
     },
     panzoomReset () {
-      this.panzoom = {x: 0, y: 0, zoom: 1}
+      this.panzoomCTM = affIdentity
     },
     // XXX: Shrink this function
     // and create some LinkAdd method
@@ -289,10 +300,10 @@ export default {
     nodeInspect (nodeId, force) {
       this.$emit('nodeInspect', nodeId, force)
     },
-
     nodePointerDown (ev, nodeId) {
       document.activeElement && document.activeElement.blur()
       const tnode = this.nodeById(nodeId)
+      // Confirm
       if (ev.button === 1) {
         this.NODE_REMOVE([tnode])
         return
@@ -305,19 +316,36 @@ export default {
         return
       }
       this.nodeInspect(tnode.id)
-
-      let selectionAdd = true
-      // Switch selection
-      if (!this.nodeSelection[tnode.id] && !ev.ctrlKey) {
-        selectionAdd = false
+      let selectionToggle = false
+      let prevSelection = Object.assign({}, this.nodeSelection)
+      let wasSelected = !!this.nodeSelection[tnode.id]
+      // Basically clear selection and selection the new one
+      // if the clicked node isn't selected
+      if (!wasSelected) {
+        this.NODE_SELECTION_SET([tnode])
+      }
+      if (ev.ctrlKey) {
+        selectionToggle = true
       }
-      this.select([tnode], selectionAdd)
       this.NODE_RAISE(this.nodeSelection)
 
       let curP = this.transformedPoint(ev.x, ev.y)
       let clone = false
       if (ev.ctrlKey && Object.keys(this.nodeSelection).length > 0) clone = true
       utils.createDrag({
+        noDrag: (ev) => {
+          if (selectionToggle) {
+            if (wasSelected) {
+              this.NODE_SELECTION_REMOVE([tnode])
+            } else {
+              prevSelection[tnode.id] = tnode
+              this.NODE_SELECTION_SET(prevSelection)
+            }
+            return
+          }
+          this.NODE_SELECTION_SET([tnode])
+        },
+
         drag: (ev) => {
           if (!ev.ctrlKey) clone = false
           /// /////////// IMPORTANT NEW ////////////////
@@ -368,7 +396,7 @@ export default {
               }
             }
             // Check inner links
-            this.select(newNodes)
+            this.NODE_SELECTION_SET(newNodes)
           }
           // DRAG operation
           this.dragging = this.nodeSelection
@@ -410,9 +438,6 @@ export default {
           // Updating nodes
           this.NODE_UPDATE(nodeUpdate)
           this.DOCUMENT_SYNC()
-        },
-        noDrag: (ev) => {
-          this.dragging = null
         }
       })
     },
@@ -448,9 +473,9 @@ export default {
       this.NODE_ADD([newNode])
     },
     nodeProcess (nodeId) {
-      const n = this.nodeById(nodeId)
-      this.nodeInspect(n.id, true)
-      this.NODE_PROCESS(n.id)
+      // const n = this.nodeById(nodeId)
+      this.nodeInspect(nodeId, true)
+      this.NODE_PROCESS([nodeId])
       // this.NODE_SELECTION_SET([n])
       // this.nodeSelectionProcess()
     },
@@ -469,14 +494,15 @@ export default {
     },
     viewPointerDown (ev) {
       if (ev.button !== 0) return
+      var svgElRect = this.$refs.svg.getBoundingClientRect()
       ev.preventDefault()
-      const p = this.transformedPoint(ev.x, ev.y)
+      const p = {x: ev.x - svgElRect.left, y: ev.y - svgElRect.top}// this.transformedPoint(ev.x, ev.y)
       this.selector = {x: p.x, y: p.y, width: 0, height: 0}
       utils.createDrag({
         drag: (evd) => {
           // transform again in case we changed zoom/pan
-          const p = this.transformedPoint(ev.x, ev.y)
-          const p2 = this.transformedPoint(evd.x, evd.y)
+          // const p = {x: ev.x, y: ev.y} // this.transformedPoint(ev.x, ev.y)
+          const p2 = {x: evd.x - svgElRect.left, y: evd.y - svgElRect.top} // this.transformedPoint(evd.x, evd.y)
           const nwidth = p2.x - p.x
           const nheight = p2.y - p.y
           this.selector = {
@@ -492,18 +518,26 @@ export default {
           const nodesToSelect = []
           for (let n in this.nodeData.nodes) {
             const node = this.nodeData.nodes[n]
-            if (node.x > this.selector.x && node.x < (this.selector.x + this.selector.width) &&
-              node.y > this.selector.y && node.y < (this.selector.y + this.selector.height)
+            // Transform backNodes
+            var nodeP = [node.x, node.y, 0]
+            nodeP = mat.multiplyPoint(this.panzoomCTM, nodeP)
+
+            if (nodeP[0] > this.selector.x && nodeP[0] < (this.selector.x + this.selector.width) &&
+              nodeP[1] > this.selector.y && nodeP[1] < (this.selector.y + this.selector.height)
             ) {
               nodesToSelect.push(node)
               // Add to selection
             }
           }
-          this.select(nodesToSelect, selectionAdd)
+          if (selectionAdd) {
+            this.NODE_SELECTION_ADD(nodesToSelect)
+          } else {
+            this.NODE_SELECTION_SET(nodesToSelect)
+          }
           this.selector = null
         },
         noDrag: (ev) => {
-          if (!ev.shiftKey) this.select()
+          if (!ev.shiftKey) this.NODE_SELECTION_CLEAR()
         }
       })
     },
@@ -540,14 +574,19 @@ export default {
 
       this.NODE_ADD([portalNode])
     },
-    select (nodes, add) {
+
+    /* select (nodes, add) {
+      if (!nodes) {
+        this.NODE_SELECTION_CLEAR()
+        return
+      }
       if (!add) {
         this.NODE_SELECTION_CLEAR()
       }
       if (nodes) {
         this.NODE_SELECTION_ADD(nodes)
       }
-    },
+    }, */
     // HELPERS depending on svg ref
     createSVGPoint (x, y) {
       const p = this.$refs.svg.createSVGPoint()

+ 35 - 18
browser/vue-flow/src/components/flow/editor.vue

@@ -10,13 +10,15 @@
       @dragover.prevent
       @drop="managerDrop"
       @mousedown="viewPointerDown"
+      @contextmenu.capture.prevent="$refs.menu.open($event,{ctx:'editor'})"
       :width="width"
       :height="height">
       <svg-defs/>
 
       <flow-pan-zoom
         ref="panzoom"
-        v-model="panzoom">
+        v-model="panzoomCTM"
+      >
 
         <flow-link
           v-for="l of nodeData.links"
@@ -42,7 +44,7 @@
           @socketPointerDown="socketPointerDown(n.id,...arguments)"
           @triggerPointerDown="triggerPointerDown(n.id,...arguments)"
           @nodeDoubleClick="$emit('nodeDoubleClick',n.id)"
-          @nodeRightClick="$refs.menu.open($event,n)"
+          @nodeRightClick="$refs.menu.open($event,{ctx:'node',node:n})"
           @activityPointerDown="$emit('activityPointerDown',n.id)"
         />
 
@@ -56,25 +58,24 @@
           v-if="pointerTriggerLink.active"
           :trigger="pointerTriggerLink"
         />
-        <rect
-          class="flow-selector"
-          :class="{'flow-selector--selecting':(selector)?true:false}"
-          v-bind="selector"/>
       </flow-pan-zoom>
+      <rect
+        class="flow-selector"
+        :class="{'flow-selector--selecting':(selector)?true:false}"
+        v-bind="selector"/>
+
     </svg>
     <div class="flow-container__control">
       <button disabled>Deploy</button>
       <button @click="stickySockets=!stickySockets"> {{ stickySockets? 'Hide':'Show' }} details </button>
+      <!--
       <button @click="stickyTriggers=!stickyTriggers"> {{ stickyTriggers? 'Hide':'Show' }} triggers </button>
-      <button @click="nodeActivity=!nodeActivity"> {{ nodeActivity? 'Hide':'Show' }} activity </button>
+      <button @click="nodeActivity=!nodeActivity"> {{ nodeActivity? 'Hide':'Show' }} activity </button>-->
       <button @click="$emit('documentSave', nodeData)"> Save </button> <!-- should disable until confirmation -->
-      <button v-if="panzoom.x!=0 || panzoom.y!=0 || panzoom.zoom!=1" @click="panzoomReset">
-        Reset view
-      </button>
+      <button v-if="!panzoomIdentity" @click="panzoomReset"> Reset view </button>
       <button v-if="selectionCount>0" @click="nodeSelectionProcess" class="primary-inverse">Process</button>
     </div>
     <div class="flow-container__info">
-      x:{{ panzoom.x.toFixed(2) }} y:{{ panzoom.y.toFixed(2) }} scale:{{ panzoom.zoom.toFixed(2) }} |
       {{ nodeData.nodes.length }} nodes
       {{ nodeData.links.length }} links
       {{ nodeData.triggers.length }} triggers
@@ -82,10 +83,9 @@
     </div>
     <hx-context-menu ref="menu">
       <template slot-scope="d" >
-        <div class="flow-node__context-menu">
-
+        <div v-if="d.userData && d.userData.ctx === 'node'" class="flow-node__context-menu">
           <!-- for the selection -->
-          <div v-if="d.userData && nodeSelection[d.userData.id]">
+          <div v-if="nodeSelection[d.userData.node.id]">
             <div class="hover item" @click="nodeSelectionProcess">Run ({{ selectionCount }})</div>
             <div class="hover item" @click="$emit('nodeViewData')">View ({{ selectionCount }})</div>
             <hr>
@@ -93,15 +93,32 @@
           </div>
           <!-- for the context -->
           <div v-else>
-            <div class="hover item" @click="nodeProcess(d.userData.id)">Run</div>
+            <div class="hover item" @click="nodeProcess(d.userData.node.id)">Run</div>
             <hr>
-            <div class="hover item" @click="nodeRemove(d.userData.id)">Delete</div>
+            <div class="hover item" @click="nodeRemove(d.userData.node.id)">Delete</div>
           </div>
 
           <hr>
-          <div class="hover item" @click="NODE_TRAIN(d.userData.id)">Train(temporary)</div>
+          <div class="hover item" @click="NODE_TRAIN(d.userData.node.id)">Train(temporary)</div>
           <hr>
-          <div class="hover item" @click="nodeInspect(d.userData.id,true)">Inspect</div>
+          <div class="hover item" @click="nodeInspect(d.userData.node.id,true)">Inspect</div>
+        </div>
+
+        <div v-if="d.userData && d.userData.ctx === 'editor'" class="flow-node__context-menu">
+          <div v-if="selectionCount>0">
+            <div class="hover item" @click="nodeSelectionProcess">Run ({{ selectionCount }})</div>
+            <div class="hover item" @click="$emit('nodeViewData')">View ({{ selectionCount }})</div>
+            <hr>
+            <div class="hover item" @click="NODE_REMOVE(nodeSelection)">Delete ({{ selectionCount }})</div>
+            <hr>
+          </div>
+          <!-- editor settings -->
+          <div>
+            <div class="hover item" @click="stickySockets=!stickySockets"> {{ stickySockets? 'Hide':'Show' }} details </div>
+            <div class="hover item" @click="stickyTriggers=!stickyTriggers"> {{ stickyTriggers? 'Hide':'Show' }} triggers </div>
+            <div class="hover item" @click="nodeActivity=!nodeActivity"> {{ nodeActivity? 'Hide':'Show' }} activity </div>
+          </div>
+
         </div>
       </template>
     </hx-context-menu>

+ 3 - 1
browser/vue-flow/src/components/flow/node-activity.vue

@@ -211,10 +211,12 @@ export default {
   animation: shake 1s infinite linear;
 }
 
+/*
 .flow-node__activity--finish-data {
   pointer-events: unset;
   cursor:pointer;
-}
+  }
+  */
 
 .flow-node__activity--finish-data
 .flow-node__activity-icon > * {

+ 67 - 25
browser/vue-flow/src/components/flow/panzoom.vue

@@ -1,11 +1,11 @@
 <template>
   <g>
-    <rect
+    <!--<rect
       class="flow-pan-zoom__grid"
       width="100%"
       height="100%"
       fill="url(#grid)"
-    />
+      />-->
     <rect
       ref="transformer"
       class="flow-pan-zoom__transformer"
@@ -23,14 +23,16 @@
 </template>
 <script>
 import utils from '@/utils/utils'
+import matrix from '@/utils/mat'
 
 export default {
   name: 'FlowPanZoom',
   props: {
-    value: {type: Object, default: () => { return {x: 0, y: 0, zoom: 1} }}
+    value: {type: Array, default: matrix.identity}
   },
   data () {
     return {
+      transform: matrix.identity(),
       zoom: this.value.zoom,
       x: this.value.x,
       y: this.value.y,
@@ -39,26 +41,28 @@ export default {
   },
   computed: {
     transformProps () {
-      const transString = 'matrix(' + [
-        this.zoom, 0, 0,
-        this.zoom, this.x, this.y
-      ].join(' ') + ')'
+      const transString = 'matrix3d(' + this.transform.join(',') + ')'
+      /* const transString = 'matrix(' + [
+        this.zoom, 0,
+        0, this.zoom,
+        this.x, this.y
+      ].join(',') + ')' */
 
       return {
-        transform: transString,
-        class: 'flow-pan-zoom__transform ' + this.moving ? 'moving' : ''
+        style: 'transform: ' + transString + ';'
+        // transform: transString
       }
     }
   },
 
   watch: {
     value: {
-      handler () {
-        this.zoom = this.value.zoom
+      handler (val) {
+        this.transform = val
+        /* this.zoom = this.value.zoom
         this.x = this.value.x
-        this.y = this.value.y
-      },
-      deep: true
+        this.y = this.value.y */
+      }
     }
   },
   mounted () {
@@ -75,10 +79,17 @@ export default {
       if (ev.target !== this.$refs.transformer) return
       ev.stopPropagation()
 
+      let mouseP = [ev.x, ev.y, 0]
+      let offsetP = matrix.multiplyPoint(matrix.inverse(this.transform), mouseP)
+
       utils.createDrag({
         drag: (ev) => {
           this.moving = true
-          this.update(this.x + ev.movementX, this.y + ev.movementY)
+          let mouseP = [ev.x, ev.y, 0]
+          const curP = matrix.multiplyPoint(matrix.inverse(this.transform), mouseP)
+          const delta = [curP[0] - offsetP[0], curP[1] - offsetP[1]]
+          const trans = matrix.translate(delta[0], delta[1])
+          this.update(matrix.multiply(this.transform, trans))
         },
         drop: (ev) => {
           this.moving = false
@@ -86,12 +97,38 @@ export default {
       })
     },
     wheel (ev) {
+      ev.preventDefault()
+      const elRect = this.$refs.transformer.getBoundingClientRect()
+
+      let mElPos = [ev.x - elRect.left, ev.y - elRect.top, 0]
+      let mPos = matrix.multiplyPoint(matrix.inverse(this.transform), mElPos)
+      let mat = matrix.multiply(
+        this.transform,
+        matrix.translate(mPos[0], mPos[1])
+      )
+      // Try rotation
+      if (ev.ctrlKey) {
+        const rotStep = ev.shiftKey ? 8 : 4
+        const rot = ev.deltaY > 0 ? rotStep : -rotStep
+        mat = matrix.multiply(mat, matrix.rotate(rot))
+      } else {
+        const scaleStep = ev.shiftKey ? 0.8 : 0.9
+        let scale = ev.deltaY > 0 ? scaleStep : 1 / scaleStep
+        mat = matrix.multiply(mat, matrix.scale(scale))
+      }
+
+      mat = matrix.multiply(mat, matrix.translate(-mPos[0], -mPos[1]))
+
+      this.update(mat)
+      // this.$emit('input', {x: this.transform[12], y: this.transform[13], zoom: this.zoom})
+
+      /*
       ev.preventDefault()
       let deltaY = (ev.deltaY > 0) ? 1 : -1
       deltaY *= (ev.shiftKey) ? 0.3 : 0.07
-      const svgRect = this.$refs.transformer.getBoundingClientRect()
-      const oX = ev.clientX - svgRect.left
-      const oY = ev.clientY - svgRect.top
+      const elRect = this.$refs.transformer.getBoundingClientRect()
+      const oX = ev.clientX - elRect.left
+      const oY = ev.clientY - elRect.top
       const z = Math.max(this.zoom - (deltaY * this.zoom), 0.1)
 
       var curX = this.x
@@ -102,17 +139,22 @@ export default {
       var ny = scaleD * (curY - oY) + oY
 
       this.update(nx, ny, z)
+      */
     },
-    update (x, y, zoom) {
-      if (x) this.x = x
-      if (y) this.y = y
-      if (zoom) this.zoom = zoom
+    update (transform) {
+      this.transform = transform
 
-      this.$emit('input', {x: this.x, y: this.y, zoom: this.zoom})
+      this.$emit('input', transform)
     },
     transformedPoint (point) {
-      const ctm = this.$refs.transform.getCTM()
-      return point.matrixTransform(ctm.inverse())
+      const p = [point.x, point.y, 0]
+      const newP = matrix.multiplyPoint(matrix.inverse(this.transform), p)
+      return {
+        x: newP[0],
+        y: newP[1]
+      }
+      // const ctm = this.$refs.transform.getCTM()
+      // return point.matrixTransform(ctm.inverse())
     }
   }
 }

+ 136 - 0
browser/vue-flow/src/components/flow/panzoom.vue.bak

@@ -0,0 +1,136 @@
+<template>
+  <g>
+    <!--<rect
+      class="flow-pan-zoom__grid"
+      width="100%"
+      height="100%"
+      fill="url(#grid)"
+      />-->
+    <rect
+      ref="transformer"
+      class="flow-pan-zoom__transformer"
+      width="100%"
+      height="100%"
+      @mousedown="dragStart"/>
+    <g
+      class="flow-pan-zoom__transformed"
+      ref="transform"
+      v-bind="transformProps">
+      <slot/>
+    </g>
+  </g>
+
+</template>
+<script>
+import utils from '@/utils/utils'
+
+export default {
+  name: 'FlowPanZoom',
+  props: {
+    value: {type: Object, default: () => { return {x: 0, y: 0, zoom: 1} }}
+  },
+  data () {
+    return {
+      zoom: this.value.zoom,
+      x: this.value.x,
+      y: this.value.y,
+      moving: false
+    }
+  },
+  computed: {
+    transformProps () {
+      const transString = 'matrix(' + [
+        this.zoom, 0,
+        0, this.zoom,
+        this.x, this.y
+      ].join(',') + ')'
+
+      return {
+        transform: transString
+      }
+    }
+  },
+
+  watch: {
+    value: {
+      handler () {
+        this.zoom = this.value.zoom
+        this.x = this.value.x
+        this.y = this.value.y
+      },
+      deep: true
+    }
+  },
+  mounted () {
+    this.$el.addEventListener('wheel', this.wheel)
+  },
+  beforeDestroy () {
+    this.$el.removeEventListener('wheel', this.wheel)
+  },
+  methods: {
+    // panStart
+    dragStart (ev) {
+      document.activeElement && document.activeElement.blur()
+      if (!(ev.button === 1 || (ev.button === 0 && ev.ctrlKey))) return // first button
+      if (ev.target !== this.$refs.transformer) return
+      ev.stopPropagation()
+
+      utils.createDrag({
+        drag: (ev) => {
+          this.moving = true
+          this.update(this.x + ev.movementX, this.y + ev.movementY)
+        },
+        drop: (ev) => {
+          this.moving = false
+        }
+      })
+    },
+    wheel (ev) {
+      ev.preventDefault()
+      let deltaY = (ev.deltaY > 0) ? 1 : -1
+      deltaY *= (ev.shiftKey) ? 0.3 : 0.07
+      const elRect = this.$refs.transformer.getBoundingClientRect()
+      const oX = ev.clientX - elRect.left
+      const oY = ev.clientY - elRect.top
+      const z = Math.max(this.zoom - (deltaY * this.zoom), 0.1)
+
+      var curX = this.x
+      var curY = this.y
+      var scaleD = z / this.zoom // delta
+
+      var nx = scaleD * (curX - oX) + oX
+      var ny = scaleD * (curY - oY) + oY
+
+      this.update(nx, ny, z)
+    },
+    update (x, y, zoom) {
+      if (x) this.x = x
+      if (y) this.y = y
+      if (zoom) this.zoom = zoom
+
+      this.$emit('input', {x: this.x, y: this.y, zoom: this.zoom})
+    },
+    transformedPoint (point) {
+      const ctm = this.$refs.transform.getCTM()
+      return point.matrixTransform(ctm.inverse())
+    }
+  }
+}
+
+</script>
+
+<style>
+.flow-pan-zoom__transformer {
+  fill:transparent;
+}
+
+.flow-pan-zoom__transformed {
+  /*transition: transform 0.45s ease;*/
+}
+
+.flow-pan-zoom__grid {
+  fill:transparent;
+  pointer-events:none;
+}
+
+</style>

+ 1 - 1
browser/vue-flow/src/store/flow/actions.js

@@ -44,7 +44,7 @@ export default {
     ctx.commit(m.NODE_SELECTION_SET, nodes)
   },
   [m.NODE_SELECTION_REMOVE] (ctx, nodes) {
-    ctx.commit(m.NODE_SELECTION_REMOVED, nodes)
+    ctx.commit(m.NODE_SELECTION_REMOVE, nodes)
   },
   [m.NODE_SELECTION_ADD] (ctx, nodes) {
     ctx.commit(m.NODE_SELECTION_ADD, nodes)

+ 4 - 2
browser/vue-flow/src/store/flow/mutations.js

@@ -37,9 +37,11 @@ export default {
   [m.DOCUMENT_UPDATE] (state, nodeData) {
     // Wrong update any node independently and its cache
     // new ones will be added
+    // sanitize data somehow
+    nodeData.nodes = nodeData.nodes.filter(n => n !== null)
+
     state.nodeData = nodeData
-    updateNodeCache(state)
-    // Delete selected unexistent nodes
+    updateNodeCache(state) // Rebuild full
     for (let k in state.nodeSelection) {
       if (!state.nodeCache[k]) {
         Vue.delete(state.nodeSelection, k)

+ 23 - 0
browser/vue-flow/src/utils/mat.js

@@ -0,0 +1,23 @@
+import * as Rematrix from 'rematrix'
+
+// Point
+export default {
+  identity: Rematrix.identity,
+  multiply: Rematrix.multiply,
+  inverse: Rematrix.inverse,
+
+  translate: Rematrix.translate,
+  rotate: Rematrix.rotate,
+  scale: Rematrix.scale,
+  multiplyPoint (mat, point) {
+    const newPoint = new Array(4)
+    // assuming point[3] == 1
+    newPoint[0] =
+    mat[0] * point[0] + mat[4] * point[1] + mat[8] * point[2] + mat[12]
+    newPoint[1] =
+    mat[1] * point[0] + mat[5] * point[1] + mat[9] * point[2] + mat[13]
+    newPoint[2] =
+    mat[2] * point[0] + mat[6] * point[1] + mat[10] * point[2] + mat[14]
+    return newPoint
+  }
+}

+ 4 - 4
browser/vue-flow/yarn.lock

@@ -3572,10 +3572,6 @@ lodash.cond@^4.3.0:
   version "4.5.2"
   resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
 
-lodash.debounce@^4.0.8:
-  version "4.0.8"
-  resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
-
 lodash.memoize@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -4912,6 +4908,10 @@ relateurl@0.2.x:
   version "0.2.7"
   resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
 
+rematrix@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/rematrix/-/rematrix-0.2.2.tgz#c96a050260782db9908904885991256e23beda5d"
+
 remove-trailing-separator@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"