Parcourir la source

Visual improvements, Backend portals

* Easy portal creation by dragging an output socket to an empty area
* Added animation to selection to make it noticeable
* Implemented selection add (shift selecting)
* Improved backend code for portals and activity
luis il y a 7 ans
Parent
commit
5af23c1c36

+ 3 - 0
browser/vue-flow/src/assets/dark-theme.css

@@ -18,6 +18,7 @@
   --link-hover: var(--primary);
   --selector-background: rgba(200, 150, 50, 0.1);
   --selector-color: var(--primary);
+  --node-selection: var(--primary);
 }
 
 .dark .primary-inverse {
@@ -26,6 +27,8 @@
 
 .dark .flow-node__body {
   fill: transparent;
+  stroke-width: 3;
+  opacity: 0.5;
 }
 
 .dark .flow-node__socket-detail--background {

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

@@ -19,8 +19,13 @@
   --link-hover: #f00;
   --selector-background: rgba(0, 0, 200, 0.1);
   --selector-color: var(--primary);
-  --transition-speed: 0.3s;
-  --transition-speed-slow: 0.7s;
+  --transition-speed: 0.1s;
+  --transition-speed-slow: 0.3s;
+  --node-selection: darkblue;
+
+  /*--transition-speed: 0.3s;*/
+
+  /*--transition-speed-slow: 0.7s;*/
 }
 
 .vertical_sep {

+ 4 - 3
browser/vue-flow/src/assets/doc/appinfo.md

@@ -1,8 +1,8 @@
 #### Editor
 
-* **Collaboration**: Using the same url address, others can join the session
-* **Pan**: Drag with Middle mouse or `Ctrl`+left mouse button
-* **Zoom**: Mouse wheel up and down to zoom in and out
+* **Collaboration**: Invite others to session by sharing the url
+* **Pan**: Drag with Middle mouse or `ctrl`+left mouse button
+* **Zoom**: Mouse wheel up and down to zoom in and out, use `shift` will zoom faster
 * **Reset**: Reset view by pressing on the reset button
 
 #### Flow
@@ -15,5 +15,6 @@
 * **Links**: Press `shift` and Drag from a node/socket to a socket highlighted in green
 * **Links(alternative)**: Toggle socket visualisation in the panel and Drag from a socket to a socket highlighted in green
 * **Remove Link**: Simple click on the link when it turns red
+* **Create a portal**: Drag an output socket from a node to an empty space check <a href="readme#portals" target="_blank">portals</a> on readme for further information
 
 <small>&copy; Luis Figueiredo (luisf@hexasoftware.com)</small>

+ 19 - 16
browser/vue-flow/src/assets/doc/readme.md

@@ -164,10 +164,12 @@ Same as f.Var passing the variable name as a property of the node
 
 Same as f.SetVar passing the variable name as a property of the node
 
-#### Portal From
+#### Portals
 
 UI Only: Portals are helper nodes that allows to connect areas of UI without
-crossing any links, right click in a node and choose create portal
+crossing any links, right click in a node and choose create portal or drag a
+link from an output socket to an empty area, this will create a portal from the
+node
 
 ---
 
@@ -177,29 +179,30 @@ crossing any links, right click in a node and choose create portal
 
 ### UX/UI
 
-* UX/UI: Create Undo behaviour
-* UX/UI: Special visualisers/nodes to display information (images, datatables, graphs)
-* UX/UI: Ability to group nodes into a single box exposing inputs and outputs
-* UX/UI: Implement touch
-* UX/UI: Drop link in node to link to the next compatible input
-* ~~UX/UI: Portals to clean graph crossing~~ (testing)
-* UX/UI: `Shift` key to merge group selections
+* UX/UI: Create Undo behaviour, Possibly easy since this is using vuex (centralized state management)
+* UX/UI: Special visualisers/nodes to display information (images, datatables, graphs);
+* UX/UI: Ability to group nodes into a single box exposing inputs and outputs;
+* UX/UI: Implement touch;
+* UX/UI: Drop link in node to link to the next compatible input;
+* UX/UI: Find a way to highlight/linked portals for easy user reference
+* ~~UX/UI: Portals to clean graph crossing~~ (testing);
+* ~~UX/UI: `Shift` key to merge group selections~~;
 
 ### Packages
 
-* FlowPkg: Create training mechanism
-* FlowPkg: Machine learning examples
-* FlowPkg: Generate go code to maximize call performance
+* FlowPkg: Create training mechanism;
+* FlowPkg: Machine learning examples;
+* FlowPkg: Generate go code to maximize call performance;
 
 ### Backend
 
-* Backend: Export/Deploy a model to a training cluster
-* Backend: Expose the model possibly via an Rest API for easy function access
+* Backend: Export/Deploy a model to a training cluster;
+* Backend: Expose the model possibly via an Rest API for easy function access;
 
 ### Other
 
-* Collaboration: Better concurrent editing/message passing
-* Flow: Proper documentation
+* Collaboration: Better concurrent editing/message passing;
+* Flow: Proper documentation;
 
 ## Ideas
 

+ 32 - 7
browser/vue-flow/src/components/flow/editor.js

@@ -173,7 +173,7 @@ export default {
       switch (ev.key) {
         case 'Enter':
           if (!single) { return }
-          this.nodeInspect(single)
+          this.nodeInspect(single, true)
           break
         case 'Delete':
           if (!this.nodeSelection) { return }
@@ -244,8 +244,22 @@ export default {
         drop: (ev) => {
           this.pointerLink.active = false
 
-          // find Parent
+          console.log('taerget', ev.target)
+          if (ev.target.matches('.flow-pan-zoom__transformer')) {
+            if (isInput) {
+              console.error('Invalid target')
+              return
+            }
+
+            const pt = this.transformedPoint(ev.x, ev.y)
+            this.createPortal(nodeId, pt.x, pt.y)
+            return
+            // Create a thing here
+            // if origin socket is an output only
+            // Empty drop, try to create a portal here
+          }
 
+          // find Parent
           var curTarget = ev.target
           for (; curTarget.hasAttribute !== undefined && curTarget !== document.body; curTarget = curTarget.parentNode) {
             if (curTarget.hasAttribute('data-nodeid')) {
@@ -372,6 +386,7 @@ export default {
       })
     },
 
+    // Is this used?
     nodeInspect (tnode, force) {
       this.$emit('nodeInspect', tnode, force)
     },
@@ -393,7 +408,6 @@ export default {
         }
         return
       }
-      // this.NODE_INSPECT(tnode.id)
       this.nodeInspect(tnode)
 
       // Switch selection
@@ -493,7 +507,6 @@ export default {
     viewPointerDown (ev) {
       if (ev.button !== 0) return
       ev.preventDefault()
-      this.nodeSelection = {}
       const p = this.transformedPoint(ev.x, ev.y)
       this.selector = {x: p.x, y: p.y, width: 0, height: 0}
       utils.createDrag({
@@ -511,6 +524,7 @@ export default {
           }
         },
         drop: (ev) => {
+          if (!ev.shiftKey) this.nodeSelection = {}
           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) &&
@@ -526,18 +540,29 @@ export default {
       const n = this.nodeData.nodes.find(n => n.src === 'Output')
       this.NODE_PROCESS(n.id)
     },
-    createPortal (nodeID) {
+
+    createPortal (nodeID, x, y) {
       // Find nodeID
       let node = this.nodeData.nodes.find(n => n.id === nodeID)
       if (!node) {
         this.NOTIFICATION_ADD('invalid node ID' + nodeID)
         return
       }
+      // If nodeID is a portal we recurse back
+      if (node.src === 'Portal From') {
+        const nodefromId = node.prop['portal from']
+        this.createPortal(nodefromId, x, y)
+        return
+      }
+
+      x = x || node.x + 10
+      y = y || node.y + 10
+      console.log('X,Y:', x, y, 'nodexy', node.x, node.y)
       // Special node
       const portalNode = {
         id: utils.guid(),
-        x: node.x + 10,
-        y: node.y + 100, // Downthere/improve this
+        x: x,
+        y: y, // Downthere/improve this
         defaultInputs: {},
         label: node.label,
         color: node.color,

+ 4 - 4
browser/vue-flow/src/components/flow/editor.vue

@@ -85,12 +85,12 @@
       <template slot-scope="d" >
         <div class="flow-node__context-menu">
           <div class="hover" @click="NODE_PROCESS(d.userData.id)">Run</div>
-          <div class="hover" @click="NODE_TRAIN(d.userData.id)">Train(temporary)</div>
-          <div class="hover" @click="NODE_REMOVE([d.userData])">Delete</div>
-          <hr>
+          <div class="hover" @click="nodeInspect(d.userData,true)">Inspect</div>
           <div class="hover" @click="createPortal(d.userData.id)">Create Portal</div>
           <hr>
-          <div class="hover" @click="nodeInspect(d.userData,true)">Inspect</div>
+          <div class="hover" @click="NODE_TRAIN(d.userData.id)">Train(temporary)</div>
+          <hr>
+          <div class="hover" @click="NODE_REMOVE([d.userData])">Delete</div>
         </div>
       </template>
     </hx-context-menu>

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

@@ -13,60 +13,38 @@
 
     <!-- shape -->
     <template>
-      <!--<svg
-        v-if="style.shape == 'portal'"
-        ref="body"
-        viewBox="0 0 100 100"
-        class="flow-node__body"
-        :class="{'flow-node__body--dragging':dragging}"
-        preserveAspectRation="xMinYMax"
-        v-bind="bodyProps"
-      >
-        <path d="
-        M 50 0
-        L 100 50
-        L 50 100
-        L 0 50
-        Z"/>
-        <!--<path d="
-      M 10 0
-      l 80 0
-      l 17 50
-      l -17 50
-      l -80 0
-      l -17 -50
-      Z " />-->
-      <!--c 10 -20, 10 -80, 0 -100-->
-      <!--</svg>-->
-      <rect
-        v-if="style.shape == 'portal'"
-        ref="body"
-        class="flow-node__body"
-        :class="{'flow-node__body--dragging':dragging}"
-        v-bind="bodyProps"
-        rx="25"
-        ry="25"
-      />
-
-      <circle
-        v-else-if="style.shape == 'circle'"
-        ref="body"
-        class="flow-node__body"
-        :class="{'flow-node__body--dragging':dragging}"
-        v-bind="bodyProps"
-      />
-      <rect
-        v-else
-        ref="body"
-        class="flow-node__body"
-        :class="{'flow-node__body--dragging':dragging}"
-        v-bind="bodyProps"
-      />
+      <g v-if="style.shape == 'portal'" >
+        <svg
+          ref="body"
+          viewBox="0 0 100 100"
+          class="flow-node__body"
+          :class="{'flow-node__body--dragging':dragging}"
+          v-bind="bodyProps"
+          preserveAspectRatio="none"
+        >
+          <path d=" M 50 0 L 100 50 L 50 100 L 0 50 Z"/>
+        </svg>
+      </g>
+      <g v-else-if="style.shape == 'circle'">
+        <circle
+          ref="body"
+          class="flow-node__body"
+          :class="{'flow-node__body--dragging':dragging}"
+          v-bind="bodyProps"
+        />
+      </g>
+      <g v-else>
+        <rect
+          ref="body"
+          class="flow-node__body"
+          :class="{'flow-node__body--dragging':dragging}"
+          v-bind="bodyProps"
+        />
+      </g>
     </template>
     <!-- selection square -->
     <rect
       class="flow-node__selection"
-      stroke-dasharray="7,3"
       :x="bodyProps.x-4"
       :y="bodyProps.y-4"
       :width="bodyProps.width+8"
@@ -165,7 +143,7 @@
     <text
       class="flow-node__src-detail"
       x="0"
-      :y="-bodyProps.height/2 - 5"
+      :y="-bodyProps.height/2 - 6"
       text-anchor="middle">
       {{ node.src }}
     </text>
@@ -266,10 +244,10 @@ export default {
         rect.r = Math.max(width / 2, height / 2)
       }
       if (this.style.shape === 'portal') {
-        /* rect.x -= 20
+        rect.x -= 20
         rect.y -= 20
         rect.width += 40
-        rect.height += 40 */
+        rect.height += 40
       }
       return rect
     },
@@ -433,7 +411,7 @@ export default {
 }
 
 .flow-linking .flow-node__src-detail {
-  opacity:0.7;
+  opacity:1;
 }
 
 /* sockets */
@@ -519,13 +497,20 @@ for hidden
 .flow-node .flow-node__selection {
   opacity:0;
   stroke-width:2;
-  stroke: var(--primary-lighter);
+  stroke: var(--node-selection);
+  stroke-dasharray:2,2;
   pointer-events:none;
   transition: all var(--transition-speed);
 }
 
 .flow-node--selected .flow-node__selection {
   opacity:0.9;
+  animation: flow-node--selected__dash 3s linear infinite;
+}
+
+@keyframes flow-node--selected__dash {
+  from { stroke-dashoffset:100;}
+  to { stroke-dashoffset: 0; }
 }
 
 </style>

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

@@ -13,6 +13,7 @@
       height="100%"
       @mousedown="dragStart"/>
     <g
+      class="flow-pan-zoom__transformed"
       ref="transform"
       v-bind="transformProps">
       <slot/>
@@ -87,11 +88,11 @@ export default {
     wheel (ev) {
       ev.preventDefault()
       let deltaY = (ev.deltaY > 0) ? 1 : -1
-      deltaY *= (ev.shiftKey) ? 0.01 : 0.07
+      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 z = this.zoom - (deltaY * this.zoom)
+      const z = Math.max(this.zoom - (deltaY * this.zoom), 0.1)
 
       var curX = this.x
       var curY = this.y
@@ -123,6 +124,11 @@ export default {
   fill:transparent;
 }
 
+.flow-pan-zoom__transformed {
+  transition: transform 0.15s ease;
+
+}
+
 .flow-pan-zoom__grid {
   fill:transparent;
   pointer-events:none;

+ 11 - 5
go/src/flow/flowserver/flowbuilder/builder.go

@@ -34,14 +34,17 @@ func New(r *registry.R) *FlowBuilder {
 	}
 }
 
-// GetOpID fetches operation ID
-func (fb *FlowBuilder) GetOpID(op flow.Operation) string {
+// GetOpIDs fetches operation IDs
+// with portals we can have several ids pointing to same operation
+func (fb *FlowBuilder) GetOpIDs(op flow.Operation) []string {
+	var ret []string
+
 	for k, v := range fb.OperationMap {
 		if op == v {
-			return k
+			ret = append(ret, k)
 		}
 	}
-	return ""
+	return ret
 }
 
 // Load document from json into builder
@@ -102,7 +105,10 @@ func (fb *FlowBuilder) Build(ID string) flow.Operation {
 		if n == nil {
 			return f.ErrOp(fmt.Errorf("Invalid portal, id: %v", nID))
 		}
-		return fb.Build(nID)
+		// Fetch existing or build new
+		op = fb.Build(nID)
+		fb.OperationMap[node.ID] = op
+		return op
 	case "Input":
 		inputID, err := strconv.Atoi(node.Prop["input"])
 		if err != nil {

+ 34 - 27
go/src/flow/flowserver/session.go

@@ -230,38 +230,45 @@ func (s *FlowSession) NodeProcess(c *websocket.Conn, data []byte) error {
 			Any: func(name string, triggerOp flow.Operation, triggerTime time.Time, extra ...flow.Data) {
 				s.Lock()
 				defer s.Unlock()
-				nodeID := builder.GetOpID(triggerOp)
 
-				act, ok := s.nodeActivity[nodeID]
-				if !ok {
-					act = &NodeActivity{ID: nodeID}
-					s.nodeActivity[nodeID] = act
-				}
-				status := ""
-				switch name {
-				case "Wait":
-					status = "waiting"
-					act.StartTime = time.Time{}
-					act.EndTime = time.Time{}
-				case "Start":
-					status = "running"
-					act.EndTime = time.Time{}
-					act.StartTime = triggerTime
-				case "Finish":
-					status = "finish"
-					act.EndTime = triggerTime
-					if nodeID == ID { // only load data from requested node
-						act.Data = extra[0]
+				nodeIDs := builder.GetOpIDs(triggerOp)
+				updated := true
+				for _, nodeID := range nodeIDs {
+					act, ok := s.nodeActivity[nodeID]
+					if !ok {
+						act = &NodeActivity{ID: nodeID}
+						s.nodeActivity[nodeID] = act
+					}
+					status := ""
+					switch name {
+					case "Wait":
+						status = "waiting"
+						act.StartTime = time.Time{}
+						act.EndTime = time.Time{}
+					case "Start":
+						status = "running"
+						act.EndTime = time.Time{}
+						act.StartTime = triggerTime
+					case "Finish":
+						status = "finish"
+						act.EndTime = triggerTime
+						if nodeID == ID { // only load data from requested node
+							act.Data = extra[0]
+						}
+					case "Error":
+						status = "error"
+						act.EndTime = triggerTime
+						act.Error = fmt.Sprint(extra[0])
+					}
+					if act.Status == status {
+						continue
 					}
-				case "Error":
-					status = "error"
-					act.EndTime = triggerTime
-					act.Error = fmt.Sprint(extra[0])
+					updated = true
+					act.Status = status
 				}
-				if act.Status == status {
+				if !updated {
 					return
 				}
-				act.Status = status
 				s.broadcast(nil, s.activity())
 
 			},