Browse Source

Front end performance improvements

* Minor refactoring in frontend
luis 7 năm trước cách đây
mục cha
commit
e11429ddd4

+ 2 - 1
browser/vue-flow/src/assets/default-theme.css

@@ -19,9 +19,10 @@
   --link-hover: #f00;
   --selector-background: rgba(0, 0, 200, 0.1);
   --selector-color: var(--primary);
+  --transition-speed-fast: 0.2s;
   --transition-speed: 0.4s;
   --transition-speed-slow: 0.8s;
-  --node-selection: rgba(0, 0, 155, 0.5);
+  --node-selection: rgba(0, 0, 155, 1);
   --border-color: rgba(50, 50, 50, 0.3);
   --border-color-lighter: rgba(150, 150, 150, 0.17);
 

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

@@ -289,6 +289,13 @@ would be good to have the same by right clicking in other subjects:
 
 ## Changelog
 
+18/02/2018
+
+* **frontend**: Refactored nodes and links to improve performance with lots of elements
+  * Cleaned up some areas while refactoring, less code
+  * Remove dependencies on $refs and DOM elements, nodes now calculate size based on monospace font
+  * moved some display logic to respective areas, as nodeProps from editor to node
+
 15/02/2018
 
 * **Selection**: Added clone using ctrl + drag will clone selected nodes

+ 1 - 1
browser/vue-flow/src/assets/lines-theme.css

@@ -19,7 +19,7 @@
   --link-hover: #f00;
   --selector-background: rgba(0, 0, 200, 0.1);
   --selector-color: var(--primary);
-  --node-selection: rgba(0, 180, 230, 0.5);
+  --node-selection: rgba(0, 187, 238, 0.7);
 
   /*--transition-speed: 0.3s;*/
 

+ 54 - 177
browser/vue-flow/src/components/flow/editor.js

@@ -23,21 +23,17 @@ export default {
       linking: false,
       triggerLinking: false,
 
-      pointerLink: {active: false, props: {}, src: {}},
-      pointerTriggerLink: {active: false, props: {}, src: {}},
+      pointerLink: {active: false, from: null, to: null, pointer: null, src: {}},
+      pointerTriggerLink: {active: false, from: null, to: null, pointer: null, src: {}},
 
       selector: null,
-
       stickySockets: false,
       stickyTriggers: false,
-      nodeActivity: true,
-      nodeProps: [],
-      linkProps: []
-      // cacheNodeProps: [],
+      nodeActivity: true
     }
   },
   computed: {
-    ...mapGetters('flow', ['registry', 'activity', 'nodeData', 'nodeById', 'nodeSelection', 'nodeIdxCache']),
+    ...mapGetters('flow', ['registry', 'activity', 'nodeData', 'nodeById', 'nodeSelection']),
     outputNode () {
       const n = this.nodeData.nodes.find(n => n.src === 'Output')
       return !!n
@@ -51,11 +47,24 @@ export default {
         'selecting': !!this.selector
       }
     },
-    /* nodeProps () {
-      return (node) => {
-        return this.nodePropsBuild(node)
+    matchHighlight () {
+      let highlight = {}
+      if (this.pointerLink) {
+        if (this.pointerLink.active) {
+          if (this.pointerLink.src.in !== undefined) {
+            highlight = {type: 'socket-out', dtype: this.pointerLink.src.type}
+          } else {
+            highlight = {type: 'socket-in', dtype: this.pointerLink.src.type}
+          }
+        }
       }
-    }, */
+      if (this.pointerTriggerLink) {
+        if (this.pointerTriggerLink.active) {
+          highlight = {type: (this.pointerTriggerLink.src.dir === 'in') ? 'trigger-in' : 'trigger-out'}
+        }
+      }
+      return highlight
+    },
     triggerProps () {
       return (trigger) => {
         if (!this.$refs.nodes) return
@@ -89,26 +98,14 @@ export default {
     }
   },
   watch: {
-    'nodeData.nodes' (val, oldVal) {
-      console.log('Nodes updated')
-      this.nodePropsUpdate()
-    },
-    'nodeData.links' () {
-      console.log('Updating links')
-      this.linkPropsUpdate()
-    },
     'activity' () {
-      _debounce(this.activityUpdate, 1500)
+      this.activityUpdate()
     }
   },
-  created () {
-    this.nodePropsUpdate()
-  },
   mounted () {
     console.log('Mounted')
     this.$nextTick(() => {
       this.$forceUpdate()
-      this.$nextTick(() => { this.linkPropsUpdate() })
     })
     document.addEventListener('keydown', this.keyDown)
     document.addEventListener('keyup', this.keyUp)
@@ -126,98 +123,9 @@ export default {
       'LINK_ADD', 'LINK_REMOVE',
       'TRIGGER_ADD', 'TRIGGER_REMOVE' ]),
 
-    activityUpdate () {
+    activityUpdate: _debounce(function () {
       console.log('Activity updated')
-      this.linkPropsUpdate()
-    },
-    nodePropsBuild (node) {
-      let highlight = {}
-      if (this.pointerLink.active && this.pointerLink.src.nodeId !== node.id) {
-        if (this.pointerLink.src.in !== undefined) {
-          highlight = {type: 'socket-out', dtype: this.pointerLink.src.type}
-        } else {
-          highlight = {type: 'socket-in', dtype: this.pointerLink.src.type}
-        }
-      }
-      if (this.pointerTriggerLink.active && this.pointerTriggerLink.src.nodeId !== node.id) {
-        highlight = {type: (this.pointerTriggerLink.src.dir === 'in') ? 'trigger-in' : 'trigger-out'}
-      }
-
-      const nodeClass = this.registry[node.src]
-      if (!nodeClass) {
-        this.NODE_REMOVE([node])
-        return
-      }
-      return {
-        transform: `translate(${node.x} ${node.y})`,
-        id: node.id,
-        node: node,
-        // Combine this into one
-        match: highlight,
-        selected: !!this.nodeSelection[node.id],
-        dragging: this.dragging && !!this.dragging[node.id],
-        activity: this.activity && this.activity.nodes && this.activity.nodes[node.id]
-      }
-    },
-
-    nodePropsUpdate (n) {
-      // Find node ID and update it only
-      if (n) {
-        const idx = this.nodeIdxCache[n.id]
-        this.$set(this.nodeProps, idx, this.nodePropsBuild(n))
-        return
-      }
-
-      const ret = []
-      for (let node of this.nodeData.nodes) {
-        ret.push(this.nodePropsBuild(node))
-      }
-      this.nodeProps = ret
-    },
-    // propCaches
-    linkPropsUpdate (link, idx) {
-      if (!this.$refs.nodes) {
-        setTimeout(() => {
-          this.linkPropsUpdate() // Retry until we have refs
-        }, 200)
-        return
-      }
-      const linkProps = (link) => {
-        if (!this.$refs.nodes) return [] // empty we need $refs.nodes
-        // For size .x .y
-        const nodeFrom = this.nodeById(link.from)
-        const nodeTo = this.nodeById(link.to)
-
-        const refFrom = this.$refs.nodes.find(n => n.node.id === link.from)
-        const refTo = this.$refs.nodes.find(n => n.node.id === link.to)
-        if (!refFrom || !refTo) { // delete link
-          // No ref yet
-          return {}
-        }
-
-        const fromOutput = refFrom.outputPos(0) // only 1 output
-        const toInput = refTo.inputPos(link.in)
-
-        return {
-          x1: nodeFrom.x + fromOutput.x,
-          y1: nodeFrom.y + fromOutput.y,
-          x2: nodeTo.x + toInput.x,
-          y2: nodeTo.y + toInput.y,
-          status: this.activity.nodes[nodeFrom.id] && this.activity.nodes[nodeFrom.id].status
-        }
-      }
-      if (link) {
-        if (idx === undefined)idx = this.linkProps.findIndex(l => l === link)
-        this.$set(this.linkProps, idx, linkProps(link))
-        return
-      }
-      const ret = []
-      for (let link of this.nodeData.links) {
-        const p = linkProps(link)
-        ret.push(p)
-      }
-      this.linkProps = ret
-    },
+    }, 1500),
 
     keyDown (ev) {
       if (document.activeElement && document.activeElement.matches('input,textarea')) { return }
@@ -259,19 +167,17 @@ export default {
     // and create some LinkAdd method
     socketPointerDown (nodeId, ev, socket) {
       if (ev.button !== 0) return
-      const nodeRef = this.$refs.nodes.find(n => n.node.id === nodeId)
       const node = this.nodeById(nodeId)
       const isInput = socket.in !== undefined
-      const socketPos = isInput ? nodeRef.inputPos(socket.in) : nodeRef.outputPos(socket.out)
 
-      // const p = this.transformedPoint(ev.clientX, ev.clientY)
-      this.pointerLink.props = {
-        x1: node.x + socketPos.x,
-        y1: node.y + socketPos.y,
-        x2: node.x + socketPos.x,
-        y2: node.y + socketPos.y
+      const p = this.transformedPoint(ev.clientX, ev.clientY)
+      this.pointerLink.pointer = p
+      if (isInput) {
+        this.pointerLink.to = nodeId
+        this.pointerLink.in = socket.in
+      } else {
+        this.pointerLink.from = nodeId
       }
-
       if (isInput) {
         this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].inputs[socket.in].type, in: socket.in}
       } else {
@@ -281,21 +187,13 @@ export default {
       utils.createDrag({
         dragStart: (ev) => {
           this.pointerLink.active = true
-          this.nodePropsUpdate()
         },
         drag: (ev) => {
           const p = this.transformedPoint(ev.clientX, ev.clientY)
-          if (isInput) {
-            this.pointerLink.props.x1 = p.x
-            this.pointerLink.props.y1 = p.y
-          } else {
-            this.pointerLink.props.x2 = p.x
-            this.pointerLink.props.y2 = p.y
-          }
+          this.pointerLink.pointer = p
         },
         drop: (ev) => {
           this.pointerLink.active = false
-          this.nodePropsUpdate()
           if (ev.target.matches('.flow-pan-zoom__transformer')) {
             if (isInput) {
               console.error('LINK: Invalid target')
@@ -368,35 +266,20 @@ export default {
     },
     triggerPointerDown (nodeId, ev, dir) {
       if (ev.button !== 0) return
-
-      const nodeRef = this.$refs.nodes.find(n => n.id === nodeId)
-      const node = this.nodeById(nodeId)
-
       const isInput = (dir === 'in')
-      const triggerPos = {
-        x: 0,
-        y: nodeRef.bodyProps.height / 2 * (isInput ? -1 : 1)
-      }
-      this.pointerTriggerLink.src = {nodeId: node.id, dir: dir}
 
-      this.pointerTriggerLink.props = {
-        x1: node.x + triggerPos.x,
-        y1: node.y + triggerPos.y,
-        x2: node.x + triggerPos.x,
-        y2: node.y + triggerPos.y
+      if (isInput) {
+        this.pointerTriggerLink.to = nodeId
+      } else {
+        this.pointerTriggerLink.from = nodeId
       }
-
+      this.pointerTriggerLink.src = {nodeId: nodeId, dir: dir}
+      this.pointerTriggerLink.pointer = this.transformedPoint(ev.clientX, ev.clientY)
       this.pointerTriggerLink.active = true
+
       utils.createDrag({
         drag: (ev) => {
-          const p = this.transformedPoint(ev.clientX, ev.clientY)
-          if (isInput) {
-            this.pointerTriggerLink.props.x1 = p.x
-            this.pointerTriggerLink.props.y1 = p.y
-          } else {
-            this.pointerTriggerLink.props.x2 = p.x
-            this.pointerTriggerLink.props.y2 = p.y
-          }
+          this.pointerTriggerLink.pointer = this.transformedPoint(ev.clientX, ev.clientY)
         },
         drop: (ev) => {
           this.pointerTriggerLink.active = false
@@ -414,11 +297,10 @@ export default {
           }
           const targetNodeId = curTarget.getAttribute('data-nodeid')
           const targetDir = curTarget.getAttribute('data-dir')
-          if (targetNodeId === node.id) {
+          if (targetNodeId === nodeId) {
             console.error('TRIGGER: cannot link to self')
             return
           }
-
           let trigger
           // target is input
           if (targetDir === 'in') {
@@ -489,7 +371,8 @@ export default {
             for (let k in this.nodeSelection) {
               const n = this.nodeById(k)
               if (n.src === 'Output') { // Do not clone output
-                continue
+                this.NOTIFICATION_ADD('cannot clone \'Output\' node')
+                return
               }
               const newNode = JSON.parse(JSON.stringify(n))
               newNode.id = utils.guid()
@@ -500,7 +383,6 @@ export default {
             // Clone links if inside the selection
             for (let k in this.nodeSelection) {
               const links = this.nodeData.links.filter(l => l.from === k)
-              if (!links) break
               for (let l of links) {
                 if (this.nodeSelection[l.to]) { // Link is inside
                   const link = {
@@ -511,6 +393,17 @@ export default {
                   this.LINK_ADD(link)
                 }
               }
+              // Trigger too
+              const triggers = this.nodeData.triggers.filter(t => t.from === k)
+              for (let t of triggers) {
+                if (this.nodeSelection[t.to]) {
+                  const trigger = {
+                    from: nodeMap[k],
+                    to: nodeMap[t.to]
+                  }
+                  this.TRIGGER_ADD(trigger)
+                }
+              }
             }
             // Check inner links
             this.select(newNodes)
@@ -526,13 +419,6 @@ export default {
               x: n.x + dragP.x - curP.x,
               y: n.y + dragP.y - curP.y
             }
-            // XXX: temporary code
-            this.nodePropsUpdate(cloneNode)
-            // Find links too
-            this.nodeData.links.forEach((link, idx) => {
-              if (link.from !== k && link.to !== k) return true
-              this.$nextTick(() => { this.linkPropsUpdate(link, idx) })
-            })
 
             nodeUpdate.push(cloneNode)
           }
@@ -556,13 +442,6 @@ export default {
               // x: Math.round((n.x + dragP.x - curP.x) / 10) * 10,
               // y: Math.round((n.y + dragP.y - curP.y) / 10) * 10
             }
-            // XXX: temporary code
-            this.nodePropsUpdate(cloneNode)
-            // Place this in the nodeProps itself
-            this.nodeData.links.forEach((link, idx) => {
-              if (link.from !== k && link.to !== k) return true
-              this.$nextTick(() => { this.linkPropsUpdate(link, idx) })
-            })
 
             nodeUpdate.push(cloneNode)
           }
@@ -651,7 +530,6 @@ export default {
           }
           this.select(nodesToSelect, selectionAdd)
           this.selector = null
-          this.nodePropsUpdate()
         },
         noDrag: (ev) => {
           if (!ev.shiftKey) this.select()
@@ -700,7 +578,6 @@ export default {
       if (nodes) {
         this.NODE_SELECTION_ADD(nodes)
       }
-      this.nodePropsUpdate()
     },
     // HELPERS depending on svg ref
     createSVGPoint (x, y) {

+ 15 - 14
browser/vue-flow/src/components/flow/editor.vue

@@ -19,22 +19,24 @@
         v-model="panzoom">
 
         <flow-link
-          v-for="(l,i) in linkProps"
-          :key="'link' + i"
-          v-bind="l"
-          @mousedown.middle="LINK_REMOVE(nodeData.links[i])"
+          v-for="(l,i) in nodeData.links"
+          :key="'link' + l.to + l.in"
+          :link="l"
+          @mousedown.middle="LINK_REMOVE(l)"
         />
         <flow-trigger-link
-          v-for="(trigger,i) in nodeData.triggers"
-          :key="'trigger'+i"
-          label="WIP"
-          v-bind="triggerProps(trigger)"
-          @mousedown.middle="TRIGGER_REMOVE(trigger)"
+          v-for="(t,i) in nodeData.triggers"
+          :key="'trigger'+t.to+i"
+          label="error/finish"
+          :trigger="t"
+          @mousedown.middle="TRIGGER_REMOVE(t)"
         />
         <flow-node
           ref="nodes"
-          v-for="n of nodeProps"
-          v-bind="n"
+          v-for="n of nodeData.nodes"
+          :node="n"
+          :match="matchHighlight"
+          :dragging="dragging"
           :key="n.id"
           @nodePointerDown.prevent="nodePointerDown($event,n.id)"
           @socketPointerDown="socketPointerDown(n.id,...arguments)"
@@ -46,14 +48,13 @@
 
         <!-- mouse links-->
         <flow-link
-          :pointer="true"
           v-if="pointerLink.active"
-          v-bind="pointerLink.props"
+          :link="pointerLink"
         />
         <flow-trigger-link
           :pointer="true"
           v-if="pointerTriggerLink.active"
-          v-bind="pointerTriggerLink.props"
+          :trigger="pointerTriggerLink"
         />
         <rect
           class="flow-selector"

+ 46 - 13
browser/vue-flow/src/components/flow/link-trigger.vue

@@ -1,13 +1,12 @@
 <template>
   <g
     class="flow-trigger-link"
-    :class="{'flow-trigger-link--pointer':pointer}"
+    :class="{'flow-trigger-link--pointer':trigger.pointer}"
     @mousedown="$emit('mousedown',$event)">
     <path
       class="flow-trigger-link__area"
       :d="path"
     />
-
     <path
       class="flow-trigger-link__visible"
       :d="path"
@@ -15,30 +14,59 @@
     <text
       class="flow-trigger-link__label"
       v-if="label"
-      :x="5+x1+(x2-x1)/2"
-      :y="y1+(y2-y1)/2"
+      :x="5+fromPos.x+(toPos.x-fromPos.x)/2"
+      :y="fromPos.y+(toPos.y-fromPos.y)/2"
     >
       on: {{ label }}
     </text>
   </g>
 </template>
 <script>
+import {mapGetters} from 'vuex'
+import nodeSize from './node-size'
+
 export default {
   name: 'FlowTriggerLink',
   props: {
-    x1: {type: Number, default: 0},
-    y1: {type: Number, default: 0},
-    x2: {type: Number, default: 0},
-    y2: {type: Number, default: 0},
-    pointer: {type: Boolean, default: false},
+    trigger: {type: Object, required: true},
     label: {type: String, default: null}
   },
   computed: {
+    ...mapGetters('flow', ['nodeById']),
+    fromPos () {
+      if (this.trigger.from === null) {
+        return {
+          x: this.trigger.pointer.x,
+          y: this.trigger.pointer.y
+        }
+      }
+      const node = this.nodeById(this.trigger.from)
+      const nodeDim = nodeSize.nodeDim(node)
+
+      return {
+        x: node.x,
+        y: node.y + (nodeDim.height / 2)
+      }
+    },
+    toPos () {
+      if (this.trigger.to === null) {
+        return {
+          x: this.trigger.pointer.x,
+          y: this.trigger.pointer.y - 6
+        }
+      }
+
+      const node = this.nodeById(this.trigger.to)
+      const nodeDim = nodeSize.nodeDim(node)
+      return {
+        x: node.x,
+        y: node.y + (-nodeDim.height / 2) - 12
+
+      }
+    },
     path () {
-      const x1 = this.x1
-      const y1 = this.y1
-      const x2 = this.x2
-      const y2 = this.y2 - (this.pointer ? 6 : 8)
+      let {x: x1, y: y1} = this.fromPos
+      let {x: x2, y: y2} = this.toPos
 
       let ox1, oy1, ox2, oy2
       ox1 = 0
@@ -73,6 +101,11 @@ export default {
   stroke:#333;
 }
 
+.flow-view:not(.activity)
+.flow-trigger-link *{
+  transition: all var(--transition-speed-fast);
+}
+
 .flow-trigger-link__area {
   stroke-width:20;
   stroke: transparent;

+ 83 - 19
browser/vue-flow/src/components/flow/link.vue

@@ -1,28 +1,83 @@
 <template>
-  <g class="flow-link" :class="{'flow-link--pointer':pointer}" :status="status" @mousedown="$emit('mousedown',$event)">
-    <path class="flow-link__area" :d="path" />
-    <path class="flow-link__visible" :d="path" />
-    <path v-if="status" class="flow-link__status" :d="path" />
+  <g
+    class="flow-link"
+    :class="{'flow-link--pointer':link.pointer}"
+    :status="status"
+    @mousedown="$emit('mousedown',$event)">
+    <path
+      class="flow-link__area"
+      :d="path" />
+    <path
+      class="flow-link__visible"
+      :d="path" />
+    <path
+      v-if="status"
+      class="flow-link__status"
+      :d="path" />
   </g>
 </template>
 <script>
+import {mapGetters, mapActions} from 'vuex'
+
 const curve = 100
 export default {
   name: 'FlowLink',
   props: {
-    x1: {type: Number, default: 0},
-    y1: {type: Number, default: 0},
-    x2: {type: Number, default: 0},
-    y2: {type: Number, default: 0},
-    pointer: {type: Boolean, default: false},
-    status: {type: String, default: null}
+    link: {type: Object, default: null}
   },
+  // we decide our status
   computed: {
+    ...mapGetters('flow', ['nodeById', 'activity', 'nodeData', 'nodeInputPos', 'nodeOutputPos']),
+    status () {
+      if (!this.activity) { return }
+
+      const act = this.activity.nodes[this.link.from]
+      if (!act) { return }
+      return act.status
+    },
+    fromPos () {
+      if (this.link.from === null) {
+        return {
+          x: this.link.pointer.x,
+          y: this.link.pointer.y
+        }
+      }
+      const nodeFrom = this.nodeById(this.link.from)
+      const fromOutput = this.nodeOutputPos(nodeFrom, 0)
+
+      return {
+        x: nodeFrom.x + fromOutput.x + 7,
+        y: nodeFrom.y + fromOutput.y
+      }
+    },
+    toPos () {
+      if (this.link.to === null) {
+        return {
+          x: this.link.pointer.x - 7.5,
+          y: this.link.pointer.y
+        }
+      }
+      const nodeTo = this.nodeById(this.link.to)
+      if (this.link.to === undefined) {
+        console.log('Unexpected BROKEN LINK', this.link)
+        this.LINK_REMOVE(this.link)
+        return {x: 0, y: 0}
+      }
+      const toInput = this.nodeInputPos(nodeTo, this.link.in)
+      return {
+        x: nodeTo.x + toInput.x - 19.5,
+        y: nodeTo.y + toInput.y
+      }
+    },
     path () {
-      const x1 = this.x1 + 7
-      const y1 = this.y1
-      const x2 = this.x2 - (this.pointer ? 19.5 : 19.5)
-      const y2 = this.y2
+      let {x: x1, y: y1} = this.fromPos
+      let {x: x2, y: y2} = this.toPos
+
+      // const x1 = this.x1 + 7
+      // const y1 = this.y1
+
+      // const x2 = this.x2 - (this.pointer ? 7.5 : 19.5)
+      // const y2 = this.y2
 
       const dx = x2 - x1
       const dy = y2 - y1
@@ -44,10 +99,12 @@ export default {
       let oy2 = 0
 
       if (dx < 0) {
-        ox1 = Math.min(-dx, 100)
-        ox2 = -Math.min(-dx, 100)
-        oy1 = Math.min(-dx, 150)
-        oy2 = -Math.min(-dx, 150)
+        const mx = Math.min(-dx, 100)
+        const mx2 = Math.min(-dx, 150)
+        ox1 = mx
+        ox2 = -mx
+        oy1 = mx2
+        oy2 = -mx2
         if (dy < 0) {
           oy1 = -oy1
           oy2 = -oy2
@@ -55,7 +112,6 @@ export default {
       }
       ox1 += 20
       ox2 -= 20
-
       return `
       M${x1},${y1} 
       C${x1 + ox1},${y1 + oy1} 
@@ -64,6 +120,9 @@ export default {
       L${x2} ${y2}
       `
     }
+  },
+  methods: {
+    ...mapActions('flow', ['LINK_REMOVE'])
   }
 }
 </script>
@@ -80,6 +139,11 @@ export default {
   stroke:#333;
 }
 
+.flow-view:not(.activity)
+.flow-link *{
+  transition: all var(--transition-speed-fast);
+}
+
 .flow-link__area {
   stroke-width:20;
   stroke: transparent;

+ 74 - 0
browser/vue-flow/src/components/flow/node-size.js

@@ -0,0 +1,74 @@
+import store from '@/store'
+import utils from '@/utils/utils'
+
+// This should be moved to state
+export default {
+  charSizeW: 8,
+  charSizeH: 14,
+  shapeOpts: {
+    'circle': {
+      textWrap: 'any'
+    },
+    'portal': {
+      textWrap: 'white-space',
+      eDim: {x: 0, y: 0, width: 40, height: 40}
+    },
+    default: {
+      textWrap: 'white-space'
+    }
+  },
+  shape (node) {
+    const nodeStyle = store.getters['flow/registry'][node.src].style || {}
+    return nodeStyle && nodeStyle.shape
+  },
+
+  labelDim (node) {
+    const shape = this.shape(node)
+    let wrapThreshold = 8 // initial wrap threshold
+    const opt = this.shapeOpts[shape] || this.shapeOpts.default
+
+    const label = utils.textWrap(node.label, wrapThreshold, opt.textWrap)
+
+    let charWidth = 0
+    let charHeight = label.length
+    for (let l of label) {
+      if (charWidth < l.length) {
+        charWidth = l.length
+      }
+    }
+
+    const ret = {
+      width: charWidth * this.charSizeW,
+      height: charHeight * this.charSizeH
+    }
+    return ret
+  },
+  /*
+   * @return a rect
+   */
+  nodeDim (node) {
+    const ldim = this.labelDim(node)
+    const inputs = store.getters['flow/registry'][node.src].inputs || []
+    const shape = this.shape(node)
+    let width = ldim.width + 46
+    let height = Math.max(ldim.height + 20, 60, inputs.length * 25)
+    if (shape === 'circle') {
+      width = height = Math.max(width, height)
+    }
+    const ret = {
+      x: -width / 2,
+      y: -height / 2,
+      width: width,
+      height: height
+    }
+    let extraDim = this.shapeOpts[shape] && this.shapeOpts[shape].eDim
+    if (extraDim) {
+      ret.x += extraDim.x
+      ret.y += extraDim.y
+      ret.width += extraDim.width
+      ret.height += extraDim.height
+    }
+    return ret
+  }
+
+}

+ 55 - 88
browser/vue-flow/src/components/flow/node.vue

@@ -2,10 +2,11 @@
   <g
     class="flow-node"
     :class="{
-      'flow-node--dragging':dragging,
-      'flow-node--selected': selected
+      'flow-node--dragging':dragging && dragging[node.id],
+      'flow-node--selected': !!nodeSelection[node.id]
     }"
     :status="status"
+    :transform="'translate(' + node.x + ',' + node.y +')'"
     @mousedown.stop.prevent="nodePointerDown"
     @contextmenu.capture.prevent="nodeRightClick"
     @dblclick="nodeDoubleClick"
@@ -18,7 +19,6 @@
           ref="body"
           viewBox="0 0 100 100"
           class="flow-node__body"
-          :class="{'flow-node__body--dragging':dragging}"
           v-bind="bodyProps"
           preserveAspectRatio="none"
         >
@@ -29,7 +29,6 @@
         <circle
           ref="body"
           class="flow-node__body"
-          :class="{'flow-node__body--dragging':dragging}"
           v-bind="bodyProps"
         />
       </g>
@@ -37,7 +36,6 @@
         <rect
           ref="body"
           class="flow-node__body"
-          :class="{'flow-node__body--dragging':dragging}"
           v-bind="bodyProps"
         />
       </g>
@@ -45,10 +43,10 @@
     <!-- selection square -->
     <rect
       class="flow-node__selection"
-      :x="bodyProps.x-4"
-      :y="bodyProps.y-4"
-      :width="bodyProps.width+8"
-      :height="bodyProps.height+8"
+      :x="bodyProps.x-6"
+      :y="bodyProps.y-6"
+      :width="bodyProps.width+12"
+      :height="bodyProps.height+12"
     />
 
     <!-- label -->
@@ -102,11 +100,10 @@
       class="flow-node__socket flow-node__socket--outputs"
     >
       <circle r="5" />
-
-      <rect
+      <!--<rect
         v-bind="outputLabelBGProps(0)"
         class="flow-node__socket-detail--background"
-      />
+        />-->
       <text
         ref="outputLabel"
         class="flow-node__socket-detail"
@@ -159,23 +156,17 @@
 import {mapGetters} from 'vuex'
 import FlowNodeActivity from './node-activity'
 import utils from '@/utils/utils'
+import nodeSize from './node-size'
 
-const shapeOpts = {
-  'circle': {
-    textWrap: 'any'
-  },
-  default: {
-    textWrap: 'white-space'
-  }
-}
 export default {
   name: 'FlowNode',
   components: {FlowNodeActivity},
   props: {
     'node': {type: Object, required: true},
     'match': {type: Object, default: () => {}},
-    'dragging': {type: Boolean, default: false},
-    'selected': {type: Boolean, default: false}
+    'pointerLink': {type: Object, default: null},
+    'pointerTriggerLink': {type: Object, default: null},
+    'dragging': {type: Object, default: null}
     // 'activity': {type: Object, default: () => {}},
     // 'nodeStyle': {type: Object, default: () => {}}
   },
@@ -189,11 +180,11 @@ export default {
     }
   },
   computed: {
-    ...mapGetters('flow', ['nodeData', 'registry', 'activity', 'nodeById']),
-    /* node () {
-      // cached
-      return this.nodeById(this.id)
-    }, */
+    ...mapGetters('flow', ['nodeCache', 'nodeData', 'registry', 'activity', 'nodeById', 'nodeSelection', 'nodeInputPos', 'nodeOutputPos']),
+
+    nodeDim () {
+      return this.nodeCache[this.node.id].dim
+    },
     inputs () {
       return this.registry[this.node.src].inputs || []
     },
@@ -216,7 +207,7 @@ export default {
     },
     labelWrap () {
       let wrapThreshold = 8 // initial wrap threshold
-      const opt = shapeOpts[this.style.shape] || shapeOpts.default
+      const opt = nodeSize.shapeOpts[this.style.shape] || nodeSize.shapeOpts.default
       return utils.textWrap(this.node.label, wrapThreshold, opt.textWrap)
     },
     labelProps () {
@@ -228,30 +219,18 @@ export default {
         transform: `translate(0,${-this.labelRect.height / 2})`
       }
     },
-    // Store this locally
     bodyProps () {
-      let width = this.labelRect.width + 46
-      let height = Math.max(this.labelRect.height + 20, 60, this.inputs.length * 25)
-      if (this.style.shape === 'circle') {
-        width = height = Math.max(width, height)
-      }
-
+      const nodeDim = this.nodeDim
       const rect = {
-        x: -width / 2,
-        y: -height / 2,
-        width: width,
-        height: height,
+        x: -nodeDim.width / 2,
+        y: -nodeDim.height / 2,
+        width: nodeDim.width,
+        height: nodeDim.height,
         stroke: this.color || '#777',
         fill: this.color || '#777'
       }
       if (this.style.shape === 'circle') {
-        rect.r = Math.max(width / 2, height / 2)
-      }
-      if (this.style.shape === 'portal') {
-        rect.x -= 20
-        rect.y -= 20
-        rect.width += 40
-        rect.height += 40
+        rect.r = Math.max(nodeDim.width / 2, nodeDim.height / 2)
       }
       return rect
     },
@@ -266,7 +245,7 @@ export default {
         const inp = this.inputs[i]
         const match = this.match.type === 'socket-in' && (inp.type === this.match.dtype || this.match.dtype === 'interface {}' || inp.type === 'interface {}')
 
-        const {x, y} = this.inputPos(i)
+        const {x, y} = this.nodeInputPos(this.node, i)
         ret.push({
           class: {
             'flow-node__socket--match': match,
@@ -295,7 +274,7 @@ export default {
     },
     outputProps () {
       return (i) => {
-        const {x, y} = this.outputPos(i)
+        const {x, y} = this.nodeOutputPos(this.node, i)
         const outp = this.output
         const match = this.match.type === 'socket-out' && (outp.type === this.match.dtype || this.match.dtype === 'interface {}' || outp.type === 'interface {}')
         return {
@@ -309,7 +288,6 @@ export default {
     inputLabel () {
       const ret = []
       for (var i in this.inputs) {
-        // console.log('Recalulating input label')
         let input = ''
         if (this.inputs[i].name) {
           input += this.inputs[i].name + ':'
@@ -323,17 +301,6 @@ export default {
       return (i) => {
       }
     },
-    inputLabelBGProps () {
-      return (i) => {
-        if (!this.$refs.inputLabel) {
-          this.$nextTick(this.$forceUpdate)
-          return {x: 0, y: 0, width: 0, height: 0}
-        }
-        let bbox = this.$refs.inputLabel[i].getBBox()
-
-        return {x: bbox.x - 5, y: bbox.y - 2, width: bbox.width + 10, height: bbox.height + 4}
-      }
-    },
     outputLabel () {
       return (i) => {
         var output = ''
@@ -345,15 +312,26 @@ export default {
         return output
       }
     },
+    // Backgrounds
+    inputLabelBGProps () {
+      return (i) => {
+        if (!this.$refs.inputLabel) {
+          this.$nextTick(this.$forceUpdate)
+          return
+        // {x: 0, y: 0, width: 0, height: 0}
+        }
+        let bbox = this.$refs.inputLabel[i].getBBox()
+        return {x: bbox.x - 5, y: bbox.y - 2, width: bbox.width + 10, height: bbox.height + 4}
+      }
+    },
     outputLabelBGProps () {
       return (i) => {
         if (!this.$refs.outputLabel) {
           this.$nextTick(this.$forceUpdate)
-          return {x: 0, y: 0, width: 0, height: 0}
+          return
         }
-        // console.log('Recalc label')
-        let bbox = this.$refs.outputLabel.getBBox()
 
+        let bbox = this.$refs.outputLabel.getBBox()
         return {x: bbox.x - 5, y: bbox.y - 2, width: bbox.width + 10, height: bbox.height + 4}
       }
     }
@@ -362,7 +340,6 @@ export default {
     // the only thing now that affects geometry
     'node.label' (val, oldVal) {
       if (val === oldVal) { return }
-      console.log('Label changed')
       this.$nextTick(() => {
         this.labelRect = this.$refs.label.getBBox()
       })
@@ -376,24 +353,10 @@ export default {
       this.$forceUpdate()
     })
   },
+  udpated () {
+    console.log('Node update')
+  },
   methods: {
-    inputPos (i) {
-      const ilen = this.inputs.length
-      if (ilen === 0) return {}
-      const d = this.bodyProps.height / (ilen * 2)
-      return {
-        x: this.bodyProps.x + 7,
-        y: this.bodyProps.y + d + (i * 2 * d)
-      }
-    },
-
-    outputPos (i) {
-      const rect = this.bodyProps
-      return {
-        x: rect.x + rect.width - 7,
-        y: 0
-      }
-    },
     nodePointerDown (ev) {
       this.$emit('nodePointerDown', ev)
     },
@@ -423,9 +386,13 @@ export default {
   cursor:move;
 }
 
+.flow-view:not(.activity) .flow-node {
+  transition: all var(--transition-speed-fast);
+}
+
 .flow-node__body {
   opacity:0.9;
-  transition: all var(--transition-speed);
+  transition: all var(--transition-speed-fast);
 }
 
 .flow-node[status=running] .flow-node__body{
@@ -447,7 +414,7 @@ export default {
   font-size:15px;
   opacity:0;
   fill: var(--normal);
-  transition: all var(--transition-speed);
+  transition: all var(--transition-speed-fast);
 }
 
 .flow-linking .flow-node__src-detail {
@@ -459,7 +426,7 @@ export default {
   pointer-events: none;
   stroke-width:1;
   opacity:0;
-  transition: all var(--transition-speed);
+  transition: all var(--transition-speed-fast);
 }
 
 .flow-view:not(.activity) .flow-node__socket:hover {
@@ -471,12 +438,12 @@ export default {
   opacity:1;
   stroke-width:0 !important;
   fill: #fff !default;
-  transition: all var(--transition-speed);
+  transition: all var(--transition-speed-fast);
 }
 
 .flow-node__socket-detail--background {
   stroke-width:0;
-  transition:all var(--transition-speed);
+  transition:all var(--transition-speed-fast);
 }
 
 .flow-node__socket--match {
@@ -503,7 +470,7 @@ export default {
 .flow-node__trigger {
   pointer-events:none;
   opacity:0;
-  transition: all var(--transition-speed);
+  transition: all var(--transition-speed-fast);
 }
 
 .flow-triggers .flow-node__trigger {
@@ -540,7 +507,7 @@ for hidden
   stroke: var(--node-selection);
   stroke-dasharray:2,2;
   pointer-events:none;
-  transition: all var(--transition-speed);
+  transition: all var(--transition-speed-fast);
   fill: var(--node-selection);
 }
 

+ 2 - 3
browser/vue-flow/src/components/flow/panzoom.vue

@@ -124,10 +124,9 @@ export default {
   fill:transparent;
 }
 
-.flow-pan-zoom__transformed {
+/*.flow-pan-zoom__transformed {
   transition: transform 0.15s ease;
-
-}
+}*/
 
 .flow-pan-zoom__grid {
   fill:transparent;

+ 21 - 4
browser/vue-flow/src/store/flow/getters.js

@@ -1,10 +1,27 @@
 export default {
-  nodeIdxCache: state => state.nodeIdxCache,
+  nodeCache: state => state.nodeCache,
   nodeData: state => state.nodeData,
-  nodeById: state => id => {
-    const idx = state.nodeIdxCache[id]
-    return state.nodeData.nodes[idx]
+  nodeById: state => id => state.nodeCache[id] && state.nodeCache[id].node,
+  nodeCacheById: state => id => state.nodeCache[id],
+
+  nodeDim: state => (node) => state.nodeCache[node.id].dim,
+  nodeInputPos: state => (node, i) => {
+    const inputs = state.registry[node.src].inputs || []
+    const ndim = state.nodeCache[node.id].dim
+    const d = ndim.height / (inputs.length * 2)
+    return {
+      x: ndim.x + 7,
+      y: ndim.y + d + (i * 2 * d)
+    }
+  },
+  nodeOutputPos: state => (node, i) => {
+    const ndim = state.nodeCache[node.id].dim
+    return {
+      x: ndim.x + ndim.width - 7,
+      y: 0
+    }
   },
+
   registry: state => state.registry,
   activity: state => state.activity,
 

+ 36 - 19
browser/vue-flow/src/store/flow/mutations.js

@@ -1,7 +1,32 @@
 import Vue from 'vue'
+import nodeSize from '@/components/flow/node-size'
 // import m from './mutation-types'
 import m from './mutation-types'
 
+function updateNodeCache (state, nodes) {
+  if (nodes !== undefined) {
+    for (let node of nodes) {
+      // The only thing affecting dimension now is Label
+      if (state.nodeCache[node.id].label === node.label) {
+        continue
+      }
+
+      state.nodeCache[node.id].dim = nodeSize.nodeDim(node)
+    }
+    return
+  }
+  // Rebuild Full cache
+  for (let k in state.nodeData.nodes) {
+    const node = state.nodeData.nodes[k]
+    Vue.set(state.nodeCache, node.id, {
+      idx: k,
+      node: node,
+      label: node.label, // cache label for dim
+      dim: nodeSize.nodeDim(node)
+    })
+  }
+}
+
 export default {
   [m.SESSID_UPDATE] (state, sessId) {
     state.sessId = sessId
@@ -10,11 +35,10 @@ export default {
     state.registry = registry
   },
   [m.DOCUMENT_UPDATE] (state, nodeData) {
+    // Wrong update any node independently and its cache
+    // new ones will be added
     state.nodeData = nodeData
-    // Rebuild cache
-    for (let k in state.nodeData.nodes) {
-      state.nodeIdxCache[state.nodeData.nodes[k].id] = k
-    }
+    updateNodeCache(state)
   },
   [m.ACTIVITY_UPDATE] (state, activity) {
     state.activity = activity
@@ -28,33 +52,29 @@ export default {
       state.nodeData.nodes.splice(ni, 1)
       state.nodeData.nodes.push(node) // put in last
     }
-    // Rebuild cache
-    //
-    for (let k in state.nodeData.nodes) {
-      state.nodeIdxCache[state.nodeData.nodes[k].id] = k
-    }
+    updateNodeCache(state)
   },
   [m.NODE_UPDATE] (state, nodes) {
     // If array
     for (let k in nodes) {
       const node = nodes[k]
-      const ni = state.nodeIdxCache[node.id]
+      const ni = state.nodeCache[node.id]
       if (ni === null) continue
 
-      Object.assign(state.nodeData.nodes[ni], node)
+      // Object.assign(state.nodeData.nodes[ni], node)
+      Object.assign(ni.node, node)
+
       // Vue.set(state.nodeData.nodes, ni, node)
       if (node.id === state.nodeInspect.id) {
         // Update node inspect
         state.nodeInspect = node
       }
     }
+    updateNodeCache(state, nodes)
   },
   [m.NODE_ADD] (state, nodes) {
     state.nodeData.nodes.push(...nodes)
-    // Rebuild cache
-    for (let k in state.nodeData.nodes) {
-      state.nodeIdxCache[state.nodeData.nodes[k].id] = k
-    }
+    updateNodeCache(state)
   },
   [m.NODE_REMOVE] (state, nodes) {
     for (let k in nodes) {
@@ -67,10 +87,7 @@ export default {
         .filter(l => l.from !== node.id && l.to !== node.id)
       state.nodeData.nodes.splice(ni, 1)
     }
-    // Rebuild cache
-    for (let k in state.nodeData.nodes) {
-      state.nodeIdxCache[state.nodeData.nodes[k].id] = k
-    }
+    updateNodeCache(state)
   },
   [m.NODE_INSPECT] (state, nodeId) {
     const node = state.nodeData.nodes.find(n => n.id === nodeId)

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

@@ -1,7 +1,7 @@
 export default {
   sessId: null,
 
-  nodeIdxCache: {}, // Cache node by ids to indexes
+  nodeCache: {}, // Cache indexes, dimentions
   // document
   nodeData: {
     nodes: [],