|
@@ -0,0 +1,297 @@
|
|
|
+<template>
|
|
|
+ <div class="flow-container">
|
|
|
+ <div>
|
|
|
+ fn:
|
|
|
+ <button
|
|
|
+ :key="i"
|
|
|
+ v-for="(r,k,i) of nodeData.registry"
|
|
|
+ @click="nodeAdd(r)">
|
|
|
+ {{ k }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <svg
|
|
|
+ ref="svg"
|
|
|
+ class="view"
|
|
|
+ :width="width"
|
|
|
+ :height="height">
|
|
|
+ <flow-pan-zoom
|
|
|
+ ref="panzoom"
|
|
|
+ v-model="nodeData.panzoom">
|
|
|
+ <flow-link
|
|
|
+ v-for="(l,i) in nodeData.links"
|
|
|
+ :key="i"
|
|
|
+ v-bind="linkProps(l)"
|
|
|
+ @click="removeLink(i)"
|
|
|
+ />
|
|
|
+ <!-- mouse link-->
|
|
|
+ <flow-link
|
|
|
+ v-if="pointerLink.active"
|
|
|
+ v-bind="pointerLink.props"
|
|
|
+ />
|
|
|
+ <flow-node
|
|
|
+ ref="nodes"
|
|
|
+ v-for="(n,i) of nodeData.nodes"
|
|
|
+ v-bind="nodeProps(n)"
|
|
|
+ :key="'node' + n.id"
|
|
|
+ :id="n.id"
|
|
|
+ @nodePointerDown.prevent="nodeDragStart($event,i)"
|
|
|
+ @socketPointerDown="socketPointerDown(n.id,...arguments)"
|
|
|
+ />
|
|
|
+ </flow-pan-zoom>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+<script>
|
|
|
+import nodeData from '../nodedata'
|
|
|
+import FlowNode from './node'
|
|
|
+import FlowLink from './link'
|
|
|
+import FlowPanZoom from './panzoom'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'FlowManager',
|
|
|
+ components: {FlowNode, FlowLink, FlowPanZoom},
|
|
|
+ props: {
|
|
|
+ 'width': {type: String, default: '800px'},
|
|
|
+ 'height': {type: String, default: '600px'}
|
|
|
+ },
|
|
|
+ data () {
|
|
|
+ return {
|
|
|
+ nodeData: nodeData,
|
|
|
+ pointerLink: {active: false, props: {}, src: {}}
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ nodeProps () {
|
|
|
+ return (node) => {
|
|
|
+ let match = {}
|
|
|
+ if (this.pointerLink.active && this.pointerLink.src.nodeId !== node.id) {
|
|
|
+ if (this.pointerLink.src.in !== undefined) {
|
|
|
+ match = {out: this.pointerLink.src.type}
|
|
|
+ } else {
|
|
|
+ match = {in: this.pointerLink.src.type}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const nodeClass = this.nodeData.registry[node.src]
|
|
|
+ return {
|
|
|
+ transform: `translate(${node.x} ${node.y})`,
|
|
|
+ id: node.id,
|
|
|
+ label: node.label,
|
|
|
+ type: nodeClass.type,
|
|
|
+ inputs: nodeClass.inputs,
|
|
|
+ output: nodeClass.output,
|
|
|
+ match: match,
|
|
|
+ color: nodeClass.style && nodeClass.style.color,
|
|
|
+ textColor: nodeClass.style && nodeClass.style.textColor
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ linkProps () {
|
|
|
+ return (link) => {
|
|
|
+ if (!this.$refs.nodes) return
|
|
|
+ // For size .x .y
|
|
|
+ const nodeFrom = this.nodeData.nodes.find(n => n.id === link.from)
|
|
|
+ const nodeTo = this.nodeData.nodes.find(n => n.id === link.to)
|
|
|
+
|
|
|
+ const refFrom = this.$refs.nodes.find(n => n.id === link.from)
|
|
|
+ const refTo = this.$refs.nodes.find(n => n.id === link.to)
|
|
|
+
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ },
|
|
|
+ mounted () {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.$forceUpdate()
|
|
|
+ })
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // XXX: Shrink this function
|
|
|
+ socketPointerDown (nodeId, e, socket) {
|
|
|
+ const nodeRef = this.$refs.nodes.find(n => n.id === nodeId)
|
|
|
+ const node = this.nodeData.nodes.find(n => n.id === nodeId)
|
|
|
+
|
|
|
+ const isInput = socket.in !== undefined
|
|
|
+ const socketPos = isInput ? nodeRef.inputPos(socket.in) : nodeRef.outputPos(socket.out)
|
|
|
+
|
|
|
+ const p = this.transformedPoint(e.clientX, e.clientY)
|
|
|
+ if (isInput) {
|
|
|
+ this.pointerLink.props = {
|
|
|
+ x1: node.x + socketPos.x,
|
|
|
+ y1: node.y + socketPos.y,
|
|
|
+ x2: p.x,
|
|
|
+ y2: p.y
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.pointerLink.props = {
|
|
|
+ x1: p.x,
|
|
|
+ y1: p.y,
|
|
|
+ x2: node.x + socketPos.x,
|
|
|
+ y2: node.y + socketPos.y
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.pointerLink.active = true
|
|
|
+ if (isInput) {
|
|
|
+ this.pointerLink.src = {nodeId: nodeId, type: this.nodeData.registry[node.src].inputs[socket.in], in: socket.in}
|
|
|
+ } else {
|
|
|
+ this.pointerLink.src = {nodeId: nodeId, type: this.nodeData.registry[node.src].output, out: 0}
|
|
|
+ }
|
|
|
+
|
|
|
+ // What socket is this
|
|
|
+ // Create a temporary link
|
|
|
+ const drag = (e) => {
|
|
|
+ const p = this.transformedPoint(e.clientX, e.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
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const drop = (e) => {
|
|
|
+ document.removeEventListener('mousemove', drag)
|
|
|
+ document.removeEventListener('mouseup', drop)
|
|
|
+ this.pointerLink.active = false
|
|
|
+
|
|
|
+ const targetNodeId = e.target.getAttribute('data-nodeid')
|
|
|
+ const targetIn = e.target.getAttribute('data-in')
|
|
|
+ const targetOut = e.target.getAttribute('data-out')
|
|
|
+ // Not a node or same node
|
|
|
+ if (targetNodeId === undefined || targetNodeId === nodeId) {
|
|
|
+ console.error('LINK: target is not a socket/ same socket')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ let link
|
|
|
+ // target is input
|
|
|
+ if (targetIn && !isInput) {
|
|
|
+ link = {
|
|
|
+ from: nodeId,
|
|
|
+ to: targetNodeId,
|
|
|
+ in: parseInt(targetIn)
|
|
|
+ }
|
|
|
+ } else if (targetOut && isInput) {
|
|
|
+ link = {
|
|
|
+ from: targetNodeId,
|
|
|
+ to: nodeId,
|
|
|
+ in: socket.in
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // No link
|
|
|
+ if (!link) {
|
|
|
+ console.error('LINK: input same direction (in->in/out->out)')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const nodeFrom = this.nodeData.nodes.find(n => n.id === link.from)
|
|
|
+ const nodeTo = this.nodeData.nodes.find(n => n.id === link.to)
|
|
|
+ // Type checking
|
|
|
+ if (this.nodeData.registry[ nodeFrom.src ].output !==
|
|
|
+ this.nodeData.registry[ nodeTo.src ].inputs[link.in]) {
|
|
|
+ console.error('LINK: Invalid type')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Input already exists
|
|
|
+ const existingInputI = this.nodeData.links.findIndex(l => l.to === link.to && l.in === link.in)
|
|
|
+ if (existingInputI !== -1) {
|
|
|
+ this.nodeData.links.splice(existingInputI, 1)
|
|
|
+ // console.error('LINK: already has input')
|
|
|
+ // return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.nodeData.links.push(link)
|
|
|
+ }
|
|
|
+ document.addEventListener('mousemove', drag)
|
|
|
+ document.addEventListener('mouseup', drop)
|
|
|
+ },
|
|
|
+ nodeDragStart (e, i) {
|
|
|
+ e.preventDefault()
|
|
|
+ var tnode = this.nodeData.nodes[i]
|
|
|
+ if (e.button === 1) {
|
|
|
+ // remove related links
|
|
|
+ this.nodeData.links = this.nodeData.links.filter(l => l.from !== tnode.id && l.to !== tnode.id)
|
|
|
+ this.nodeData.nodes.splice(i, 1)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (e.button !== 0) return // first button
|
|
|
+ // we can handle with nodeId and a search
|
|
|
+ this.nodeData.nodes.splice(i, 1)
|
|
|
+ this.nodeData.nodes.push(tnode) // put in last
|
|
|
+
|
|
|
+ // transform CTM
|
|
|
+ const delta = this.transformedPoint(e.clientX, e.clientY)
|
|
|
+ delta.x -= tnode.x
|
|
|
+ delta.y -= tnode.y
|
|
|
+
|
|
|
+ const drag = (e) => {
|
|
|
+ const point = this.transformedPoint(e.clientX, e.clientY)
|
|
|
+ tnode.x = point.x - delta.x
|
|
|
+ tnode.y = point.y - delta.y
|
|
|
+ }
|
|
|
+ const drop = (e) => {
|
|
|
+ document.removeEventListener('mousemove', drag)
|
|
|
+ document.removeEventListener('mouseup', drop)
|
|
|
+ }
|
|
|
+ document.addEventListener('mousemove', drag)
|
|
|
+ document.addEventListener('mouseup', drop)
|
|
|
+ },
|
|
|
+ nodeAdd (k) {
|
|
|
+ console.log('Adding:', k)
|
|
|
+ this.nodeData.nodes.push({
|
|
|
+ id: guid(),
|
|
|
+ x: 100,
|
|
|
+ y: 100,
|
|
|
+ label: k,
|
|
|
+ src: k
|
|
|
+ })
|
|
|
+ },
|
|
|
+ removeLink (i) {
|
|
|
+ this.nodeData.links.splice(i, 1)
|
|
|
+ },
|
|
|
+ createSVGPoint (x, y) {
|
|
|
+ const p = this.$refs.svg.createSVGPoint()
|
|
|
+ p.x = x; p.y = y
|
|
|
+ return p
|
|
|
+ },
|
|
|
+ transformedPoint (x, y, abs) {
|
|
|
+ const svgRect = this.$refs.svg.getBoundingClientRect()
|
|
|
+ if (!abs) {
|
|
|
+ x -= svgRect.x
|
|
|
+ y -= svgRect.y
|
|
|
+ }
|
|
|
+ return this.$refs.panzoom.transformedPoint(this.createSVGPoint(x, y))
|
|
|
+ }
|
|
|
+ // helper
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+function guid () {
|
|
|
+ function s4 () {
|
|
|
+ return Math.floor((1 + Math.random()) * 0x10000)
|
|
|
+ .toString(16)
|
|
|
+ .substring(1)
|
|
|
+ }
|
|
|
+ return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
|
|
|
+ s4() + '-' + s4() + s4() + s4()
|
|
|
+}
|
|
|
+</script>
|
|
|
+<style>
|
|
|
+.flow-container {
|
|
|
+ display:flex;
|
|
|
+ flex-flow:column;
|
|
|
+}
|
|
|
+svg.view {
|
|
|
+ border:none;
|
|
|
+ position:relative;
|
|
|
+}
|
|
|
+
|
|
|
+</style>
|