123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 |
- import {mapGetters, mapActions} from 'vuex'
- import FlowNode from './node'
- import FlowLink from './link'
- import FlowTriggerLink from './link-trigger'
- import FlowPanZoom from './panzoom'
- import HxContextMenu from '@/components/shared/hx-contextmenu'
- import SvgDefs from './svgdefswrapper'
- import utils from '@/utils/utils'
- export default {
- name: 'FlowManager',
- components: {FlowNode, FlowLink, FlowTriggerLink, FlowPanZoom, HxContextMenu, SvgDefs},
- props: {
- '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 },
- // Shared state
- // nodeData: { nodes: [], links: [], triggers: [] },
- dragging: null,
- linking: false,
- triggerLinking: false,
- pointerLink: {active: false, props: {}, src: {}},
- pointerTriggerLink: {active: false, props: {}, src: {}},
- selector: null,
- nodeSelection: {}, // map of true false
- stickySockets: false,
- stickyTriggers: false,
- nodeActivity: true
- }
- },
- computed: {
- ...mapGetters('flow', ['registry', 'activity', 'nodeData', 'nodeById']),
- outputNode () {
- const n = this.nodeData.nodes.find(n => n.src === 'Output')
- return !!n
- },
- viewClasses () {
- return {
- 'flow-linking': this.linking || this.stickySockets,
- 'flow-triggers': this.triggerLinking || this.stickyTriggers,
- 'activity': this.dragging || this.pointerLink.active,
- 'flow-node--activity': this.nodeActivity,
- 'selecting': !!this.selector
- }
- },
- nodeProps () {
- return (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,
- // Combine this into one
- match: highlight,
- dragging: this.dragging && !!this.dragging[node.id],
- activity: this.activity && this.activity.nodes && this.activity.nodes[node.id]
- }
- }
- },
- linkProps () {
- return (link) => {
- if (!this.$refs.nodes) return
- // For size .x .y
- const nodeFrom = this.nodeById(link.from)
- const nodeTo = this.nodeById(link.to)
- const refFrom = this.$refs.nodes.find(n => n.id === link.from)
- const refTo = this.$refs.nodes.find(n => n.id === link.to)
- if (!refFrom || !refTo) { // delete link
- 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
- }
- }
- },
- triggerProps () {
- return (trigger) => {
- if (!this.$refs.nodes) return
- // For size .x .y
- const nodeFrom = this.nodeById(trigger.from)
- const nodeTo = this.nodeById(trigger.to)
- const refFrom = this.$refs.nodes.find(n => n.id === trigger.from)
- const refTo = this.$refs.nodes.find(n => n.id === trigger.to)
- if (!refFrom || !refTo) { // delete trigger
- return {}
- }
- const fromOutput = {
- x: 0,
- y: refFrom.bodyProps.height / 2
- }
- const toInput = {
- x: 0,
- y: -refTo.bodyProps.height / 2
- }
- 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
- }
- }
- }
- },
- mounted () {
- this.$nextTick(() => {
- this.$forceUpdate()
- })
- document.addEventListener('keydown', this.keyDown)
- document.addEventListener('keyup', this.keyUp)
- },
- beoreDestroy () {
- document.removeEventListener('keydown', this.keyDown)
- document.removeEventListener('keyup', this.keyUp)
- },
- methods: {
- ...mapActions('flow', [
- 'NOTIFICATION_ADD',
- 'DOCUMENT_SYNC',
- 'NODE_RAISE', 'NODE_UPDATE', 'NODE_ADD', 'NODE_REMOVE', 'NODE_INSPECT', 'NODE_PROCESS', 'NODE_TRAIN',
- 'LINK_ADD', 'LINK_REMOVE',
- 'TRIGGER_ADD', 'TRIGGER_REMOVE' ]),
- keyDown (ev) {
- if (document.activeElement && document.activeElement.matches('input,textarea')) { return }
- if (ev.shiftKey) {
- this.linking = true
- this.triggerLinking = true
- }
- let single = null
- const selectionIds = Object.keys(this.nodeSelection)
- if (selectionIds.length === 1) { single = this.nodeSelection[selectionIds[0]] }
- switch (ev.key) {
- case 'Enter':
- if (!single) { return }
- this.nodeInspect(single)
- break
- case 'Delete':
- if (!this.nodeSelection) { return }
- console.log('Removing nodes:', this.nodeSelection)
- this.NODE_REMOVE(this.nodeSelection)
- // for (let k in this.nodeSelection) {
- // this.nodeRemove(this.nodeSelection[k])
- // }
- break
- case 'a':
- if (ev.ctrlKey) {
- ev.preventDefault()
- ev.stopPropagation()
- this.nodeSelection = {}
- for (let n of this.nodeData.nodes) {
- this.nodeSelection[n.id] = n
- }
- }
- break
- }
- },
- keyUp (ev) {
- if (!ev.shiftKey) {
- this.linking = false
- this.triggerLinking = false
- }
- },
- panzoomReset () {
- this.panzoom = {x: 0, y: 0, 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.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
- }
- this.pointerLink.active = true
- if (isInput) {
- this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].inputs[socket.in].type, in: socket.in}
- } else {
- this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].output.type, out: 0}
- }
- utils.createDrag({
- 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
- }
- },
- drop: (ev) => {
- this.pointerLink.active = false
- // find Parent
- var curTarget = ev.target
- for (; curTarget.hasAttribute !== undefined && curTarget !== document.body; curTarget = curTarget.parentNode) {
- if (curTarget.hasAttribute('data-nodeid')) {
- break
- }
- }
- if (!curTarget.hasAttribute || curTarget === document.body) {
- console.error('LINK: target is not a socket')
- return
- }
- const targetNodeId = curTarget.getAttribute('data-nodeid')
- const targetIn = curTarget.getAttribute('data-in')
- const targetOut = curTarget.getAttribute('data-out')
- 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.nodeById(link.from)
- const nodeTo = this.nodeById(link.to)
- const output = this.registry[nodeFrom.src].output.type
- const input = this.registry[nodeTo.src].inputs[link.in].type
- // Type checking
- if (!(output === 'interface {}' || output === input || input === 'interface {}')) {
- console.error('LINK: Invalid type')
- return
- }
- // Input already exists, replace
- const existingInput = this.nodeData.links.find(l => l.to === link.to && l.in === link.in)
- if (existingInput) {
- // REMOVE LINK
- this.LINK_REMOVE(existingInput)
- }
- this.LINK_ADD(link)
- }})
- },
- 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
- }
- 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
- }
- },
- drop: (ev) => {
- this.pointerTriggerLink.active = false
- // find Parent
- var curTarget = ev.target
- for (; curTarget.hasAttribute !== undefined && curTarget !== document.body; curTarget = curTarget.parentNode) {
- if (curTarget.hasAttribute('data-nodeid')) {
- break
- }
- }
- if (!curTarget.hasAttribute || curTarget === document.body) {
- console.error('TRIGGER: target is not a socket')
- return
- }
- const targetNodeId = curTarget.getAttribute('data-nodeid')
- const targetDir = curTarget.getAttribute('data-dir')
- if (targetNodeId === node.id) {
- console.error('TRIGGER: cannot link to self')
- return
- }
- let trigger
- // target is input
- if (targetDir === 'in') {
- trigger = {
- from: nodeId,
- to: targetNodeId
- }
- } else {
- trigger = {
- from: targetNodeId,
- to: nodeId
- }
- }
- this.TRIGGER_ADD(trigger)
- }
- })
- },
- nodeInspect (tnode, force) {
- this.$emit('nodeInspect', tnode, force)
- },
- // HERE
- nodePointerDown (ev, i) {
- document.activeElement && document.activeElement.blur()
- const tnode = this.nodeData.nodes[i]
- if (ev.button === 1) {
- this.NODE_REMOVE([tnode])
- // this.nodeRemove(tnode)
- // remove related links
- return
- }
- if (ev.button !== 0) return // first button
- if (ev.shiftKey) {
- if (this.registry[tnode.src].output) {
- this.socketPointerDown(tnode.id, ev, {out: 0})
- }
- return
- }
- // this.NODE_INSPECT(tnode.id)
- this.nodeInspect(tnode)
- // Switch selection
- if (!this.nodeSelection[tnode.id] && !ev.ctrlKey) this.nodeSelection = {}
- this.nodeSelection[tnode.id] = tnode
- // we can handle with nodeId and a search
- this.NODE_RAISE(this.nodeSelection)
- let curP = this.transformedPoint(ev.x, ev.y)
- this.dragging = this.nodeSelection
- utils.createDrag({
- drag: (ev) => {
- if (this.nodeSelection === undefined) {
- console.error('Well something went wrong')
- }
- const dragP = this.transformedPoint(ev.x, ev.y)
- const nodeUpdate = []
- for (let k in this.nodeSelection) {
- // const n = this.nodeData.nodes.find(n => n.id === k)
- const n = this.nodeById(k)
- // create new nodes
- nodeUpdate.push({
- ...n,
- x: n.x + dragP.x - curP.x,
- y: n.y + dragP.y - curP.y
- })
- }
- this.NODE_UPDATE(nodeUpdate)
- // this.sendFlowEvent('nodeUpdate', this.nodeSelection)
- curP = dragP
- },
- drop: (ev) => {
- // snap?
- this.dragging = null
- // Snapping
- const dragP = this.transformedPoint(ev.x, ev.y)
- const nodeUpdate = []
- for (let k in this.nodeSelection) {
- // const n = this.nodeData.nodes.find(n => n.id === k)
- const n = this.nodeById(k)
- // create new nodes
- nodeUpdate.push({
- ...n,
- x: Math.round((n.x + dragP.x - curP.x) / 10) * 10,
- y: Math.round((n.y + dragP.y - curP.y) / 10) * 10
- })
- }
- // Updating nodes
- this.NODE_UPDATE(nodeUpdate)
- this.DOCUMENT_SYNC()
- }
- })
- },
- /// ////////////
- // NODE CREATOR FUNC
- //
- // STORE
- nodeAdd (src, x = 100, y = 100) {
- if (src === 'Output') {
- const n = this.nodeData.nodes.find(n => n.src === src)
- if (n) {
- this.NOTIFICATION_ADD('Output node already exists')
- return
- }
- }
- const newNode = {
- id: utils.guid(),
- x: x,
- y: y,
- defaultInputs: {},
- label: src,
- color: this.registry[src].style && this.registry[src].style.color, /* NEW 12/02/2018 */
- src: src
- }
- // Setup Props
- if (this.registry[src].props) {
- for (let k in this.registry[src].props) {
- newNode.prop || (newNode.prop = {})
- newNode.prop[k] = ''
- }
- }
- this.NODE_ADD(newNode)
- },
- 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)
- },
- 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({
- 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 nwidth = p2.x - p.x
- const nheight = p2.y - p.y
- this.selector = {
- x: nwidth < 0 ? p2.x : p.x,
- y: nheight < 0 ? p2.y : p.y,
- width: nwidth < 0 ? -nwidth : nwidth,
- height: nheight < 0 ? -nheight : nheight
- }
- },
- drop: (ev) => {
- 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)
- ) {
- this.nodeSelection[node.id] = node
- }
- }
- this.selector = null
- }})
- },
- documentProcess () {
- const n = this.nodeData.nodes.find(n => n.src === 'Output')
- this.NODE_PROCESS(n.id)
- },
- createPortal (nodeID) {
- // Find nodeID
- let node = this.nodeData.nodes.find(n => n.id === nodeID)
- if (!node) {
- this.NOTIFICATION_ADD('invalid node ID' + nodeID)
- return
- }
- // Special node
- const portalNode = {
- id: utils.guid(),
- x: node.x + 10,
- y: node.y + 100, // Downthere/improve this
- defaultInputs: {},
- label: node.label,
- color: node.color,
- prop: {'portal from': nodeID},
- src: 'Portal From'
- }
- this.NODE_ADD(portalNode)
- },
- // 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))
- }
- }
- }
|