123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437 |
- <template>
- <div class="flow-container">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- xmlns:xlink="http://www.w3.org/1999/xlink"
- ref="svg"
- class="flow-view view"
- :class="{
- 'flow-linking':linking || stickySockets,
- 'activity':dragging || pointerLink.active ,
- 'flow-detail': detailed
- }"
- @dragover.prevent
- @drop="managerDrop"
- :width="width"
- :height="height">
- <svg-defs/>
- <flow-pan-zoom
- ref="panzoom"
- v-model="panzoom">
- <!-- links below nodes -->
- <flow-link
- v-for="(link,i) in nodeData.links"
- :key="i"
- v-bind="linkProps(link)"
- @click="linkRemove(link)"
- />
- <!-- nodes -->
- <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)"
- @nodeDoubleClick="$emit('nodeInspect',n.id)"
- />
- <!-- mouse link-->
- <flow-link
- :pointer="true"
- v-if="pointerLink.active"
- v-bind="pointerLink.props"
- />
- </flow-pan-zoom>
- </svg>
- <div class="flow-container__control">
- <button @click="$emit('funcsPanelToggle')">Panel</button>
- <button @click="stickySockets=!stickySockets"> {{ stickySockets? 'Hide':'Show' }} sockets </button>
- <button @click="detailed=!detailed"> {{ detailed? 'Hide':'Show' }} detail </button>
- <button v-if="panzoom.x!=0 || panzoom.y!=0 || panzoom.zoom!=1" @click="panzoomReset">Reset view</button>
- </div>
- <div class="flow-container__info">
- x:{{ panzoom.x.toFixed(2) }} y:{{ panzoom.y.toFixed(2) }} scale:{{ panzoom.zoom.toFixed(2) }}
- nodes: {{ nodeData.nodes.length }}
- links: {{ nodeData.links.length }}
- </div>
- </div>
- </template>
- <script>
- import FlowNode from './node'
- import FlowLink from './link'
- import FlowPanZoom from './panzoom'
- import SvgDefs from './svgdefwrapper.vue'
- export default {
- name: 'FlowManager',
- components: {FlowNode, FlowLink, FlowPanZoom, SvgDefs},
- props: {
- 'value': {type: Object, default: () => {}},
- 'registry': {type: Object, default: () => {}},
- 'width': {type: String, default: '800px'},
- 'height': {type: String, default: '600px'}
- },
- data () {
- // const cloned = JSON.parse(JSON.stringify(this.value)) // initial?
- return {
- panzoom: { x: 0, y: 0, zoom: 1 },
- nodeData: { nodes: [], links: [] },
- dragging: null,
- linking: false,
- stickySockets: false,
- detailed: false,
- 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.registry[node.src]
- return {
- transform: `translate(${node.x} ${node.y})`,
- id: node.id,
- label: node.label,
- inputs: nodeClass.inputs,
- output: nodeClass.output,
- match: match,
- dragging: this.dragging === node,
- nodeStyle: nodeClass.style
- }
- }
- },
- 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
- }
- }
- }
- },
- watch: {
- value: {
- handler (val) {
- const clone = JSON.parse(JSON.stringify(this.value)) // deepClone
- const nodes = []
- for (let n of clone.nodes) {
- if (this.dragging && this.dragging.id === n.id) {
- continue
- }
- nodes.push(n)
- }
- if (this.dragging) {
- nodes.push(this.dragging)
- }
- this.nodeData = {
- nodes: nodes,
- links: clone.links
- }
- this.$nextTick(() => {
- this.$forceUpdate()
- })
- },
- deep: true
- }
- },
- mounted () {
- this.$nextTick(() => {
- this.$forceUpdate()
- })
- document.addEventListener('keydown', this.keyDown)
- document.addEventListener('keyup', this.keyUp)
- },
- beforeDestroy () {
- document.removeEventListener('keydown', this.keyDown)
- document.removeEventListener('keyup', this.keyUp)
- },
- methods: {
- keyDown (ev) {
- if (ev.shiftKey) {
- this.linking = true
- }
- },
- keyUp (ev) {
- if (!ev.shiftKey) {
- this.linking = false
- }
- },
- panzoomReset () {
- this.panzoom.x = 0
- this.panzoom.y = 0
- this.panzoom.zoom = 1
- },
- // XXX: Shrink this function
- // and create some LinkAdd method
- socketPointerDown (nodeId, ev, socket) {
- if (ev.button !== 0) return
- 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(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
- }
- this.pointerLink.active = true
- if (isInput) {
- this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].inputs[socket.in], in: socket.in}
- } else {
- this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].output, out: 0}
- }
- // What socket is this
- // Create a temporary link
- const 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
- }
- }
- const drop = (ev) => {
- document.removeEventListener('mousemove', drag)
- document.removeEventListener('mouseup', drop)
- this.pointerLink.active = false
- const targetNodeId = ev.target.getAttribute('data-nodeid')
- const targetIn = ev.target.getAttribute('data-in')
- const targetOut = ev.target.getAttribute('data-out')
- // Not a node or same node
- if (targetNodeId === undefined || targetNodeId === nodeId) {
- console.error('LINK: target is not a socket or is node')
- 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)
- const output = this.registry[nodeFrom.src].output
- const input = this.registry[nodeTo.src].inputs[link.in]
- // Type checking
- if (!(output === 'any' || output == input || input === 'any')) {
- 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.linkAdd(link)
- }
- document.addEventListener('mousemove', drag)
- document.addEventListener('mouseup', drop)
- },
- nodeDoubleClick (ev, i) {
- this.nodeModalTarget = i
- this.nodeModal = true
- },
- nodeDragStart (ev, i) {
- document.activeElement && document.activeElement.blur()
- var tnode = this.nodeData.nodes[i]
- if (ev.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)
- this.$emit('input', this.nodeData)
- this.$emit('nodeRemove', tnode)
- this.$emit('shouldPersist')
- return
- }
- if (ev.button !== 0) return // first button
- if (ev.shiftKey) {
- this.socketPointerDown(tnode.id, ev, {out: 0})
- return
- }
- // we can handle with nodeId and a search
- this.nodeData.nodes.splice(i, 1)
- this.nodeData.nodes.push(tnode) // put in last
- this.dragging = tnode
- // transform CTM
- const delta = this.transformedPoint(ev.clientX, ev.clientY)
- delta.x -= tnode.x
- delta.y -= tnode.y
- const drag = (ev) => {
- const point = this.transformedPoint(ev.clientX, ev.clientY)
- tnode.x = point.x - delta.x
- tnode.y = point.y - delta.y
- // Bad possibly
- this.$emit('input', this.nodeData)
- this.$emit('nodeUpdate', tnode)
- }
- const drop = (ev) => {
- document.removeEventListener('mousemove', drag)
- document.removeEventListener('mouseup', drop)
- this.dragging = null
- this.$emit('input', this.nodeData)
- this.$emit('nodeUpdate', tnode)
- this.$emit('shouldPersist')
- }
- document.addEventListener('mousemove', drag)
- document.addEventListener('mouseup', drop)
- },
- nodeAdd (src, x = 100, y = 100) {
- const newNode = {
- id: guid(),
- x: x,
- y: y,
- label: src,
- src: src
- }
- this.nodeData.nodes.push(newNode)
- this.$emit('input', this.nodeData)
- this.$emit('nodeUpdate', newNode)
- this.$emit('shouldPersist')
- },
- linkAdd (link) {
- this.nodeData.links.push(link)
- this.$emit('input', this.nodeData)
- this.$emit('linkUpdate', link)
- this.$emit('shouldPersist')
- },
- linkRemove (link) {
- const i = this.nodeData.links.findIndex(l => l === link)
- if (i === -1) return
- this.nodeData.links.splice(i, 1)
- this.$emit('input', this.nodeData)
- this.$emit('linkRemove', link)
- this.$emit('shouldPersist')
- },
- managerDrop (ev) {
- ev.preventDefault()
- const reg = ev.dataTransfer.getData('text')
- if (this.registry[reg] === undefined) {
- console.error('Registry: Drop src not found in registry')
- return
- }
- const pt = this.transformedPoint(ev.x, ev.y)
- this.nodeAdd(reg, pt.x, pt.y)
- },
- // HELPERS depending on svg ref
- 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.left
- y -= svgRect.top
- }
- return this.$refs.panzoom.transformedPoint(this.createSVGPoint(x, y))
- }
- }
- }
- // utils
- 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:row;
- position:relative;
- }
- .flow-container__control {
- position:absolute;
- top: 20px;
- left: 20px;
- display:flex;
- justify-content: center;
- align-items: center;
- color: #FFF;
- }
- .flow-container__control button {
- display:flex;
- justify-content: center;
- margin:0;
- padding:14px;
- color:#333;
- }
- .flow-container__info {
- position:absolute;
- bottom:10px;
- left:10px;
- padding:2px;
- font-size:9px;
- }
- .flow-view {
- border:none;
- position:relative;
- fill:transparent;
- }
- </style>
|