editor.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. import {mapGetters, mapActions} from 'vuex'
  2. import FlowNode from './node'
  3. import FlowLink from './link'
  4. import FlowTriggerLink from './link-trigger'
  5. import FlowPanZoom from './panzoom'
  6. import HxContextMenu from '@/components/shared/hx-contextmenu'
  7. import SvgDefs from './svgdefswrapper'
  8. import utils from '@/utils/utils'
  9. export default {
  10. name: 'FlowManager',
  11. components: {FlowNode, FlowLink, FlowTriggerLink, FlowPanZoom, HxContextMenu, SvgDefs},
  12. props: {
  13. 'width': {type: String, default: '800px'},
  14. 'height': {type: String, default: '600px'}
  15. },
  16. data () {
  17. // const cloned = JSON.parse(JSON.stringify(this.value)) // initial?
  18. return {
  19. panzoom: { x: 0, y: 0, zoom: 1 },
  20. // Shared state
  21. // nodeData: { nodes: [], links: [], triggers: [] },
  22. dragging: null,
  23. linking: false,
  24. triggerLinking: false,
  25. pointerLink: {active: false, props: {}, src: {}},
  26. pointerTriggerLink: {active: false, props: {}, src: {}},
  27. selector: null,
  28. nodeSelection: {}, // map of true false
  29. stickySockets: false,
  30. stickyTriggers: false,
  31. nodeActivity: true
  32. }
  33. },
  34. computed: {
  35. ...mapGetters('flow', ['registry', 'activity', 'nodeData', 'nodeById']),
  36. outputNode () {
  37. const n = this.nodeData.nodes.find(n => n.src === 'Output')
  38. return !!n
  39. },
  40. viewClasses () {
  41. return {
  42. 'flow-linking': this.linking || this.stickySockets,
  43. 'flow-triggers': this.triggerLinking || this.stickyTriggers,
  44. 'activity': this.dragging || this.pointerLink.active,
  45. 'flow-node--activity': this.nodeActivity,
  46. 'selecting': !!this.selector
  47. }
  48. },
  49. nodeProps () {
  50. return (node) => {
  51. let highlight = {}
  52. if (this.pointerLink.active && this.pointerLink.src.nodeId !== node.id) {
  53. if (this.pointerLink.src.in !== undefined) {
  54. highlight = {type: 'socket-out', dtype: this.pointerLink.src.type}
  55. } else {
  56. highlight = {type: 'socket-in', dtype: this.pointerLink.src.type}
  57. }
  58. }
  59. if (this.pointerTriggerLink.active && this.pointerTriggerLink.src.nodeId !== node.id) {
  60. highlight = {type: (this.pointerTriggerLink.src.dir === 'in') ? 'trigger-in' : 'trigger-out'}
  61. }
  62. const nodeClass = this.registry[node.src]
  63. if (!nodeClass) {
  64. this.NODE_REMOVE([node])
  65. return
  66. }
  67. return {
  68. transform: `translate(${node.x} ${node.y})`,
  69. id: node.id,
  70. // Combine this into one
  71. match: highlight,
  72. dragging: this.dragging && !!this.dragging[node.id],
  73. activity: this.activity && this.activity.nodes && this.activity.nodes[node.id]
  74. }
  75. }
  76. },
  77. linkProps () {
  78. return (link) => {
  79. if (!this.$refs.nodes) return
  80. // For size .x .y
  81. const nodeFrom = this.nodeById(link.from)
  82. const nodeTo = this.nodeById(link.to)
  83. const refFrom = this.$refs.nodes.find(n => n.id === link.from)
  84. const refTo = this.$refs.nodes.find(n => n.id === link.to)
  85. if (!refFrom || !refTo) { // delete link
  86. return {}
  87. }
  88. const fromOutput = refFrom.outputPos(0) // only 1 output
  89. const toInput = refTo.inputPos(link.in)
  90. return {
  91. x1: nodeFrom.x + fromOutput.x,
  92. y1: nodeFrom.y + fromOutput.y,
  93. x2: nodeTo.x + toInput.x,
  94. y2: nodeTo.y + toInput.y,
  95. status: this.activity.nodes[nodeFrom.id] && this.activity.nodes[nodeFrom.id].status
  96. }
  97. }
  98. },
  99. triggerProps () {
  100. return (trigger) => {
  101. if (!this.$refs.nodes) return
  102. // For size .x .y
  103. const nodeFrom = this.nodeById(trigger.from)
  104. const nodeTo = this.nodeById(trigger.to)
  105. const refFrom = this.$refs.nodes.find(n => n.id === trigger.from)
  106. const refTo = this.$refs.nodes.find(n => n.id === trigger.to)
  107. if (!refFrom || !refTo) { // delete trigger
  108. return {}
  109. }
  110. const fromOutput = {
  111. x: 0,
  112. y: refFrom.bodyProps.height / 2
  113. }
  114. const toInput = {
  115. x: 0,
  116. y: -refTo.bodyProps.height / 2
  117. }
  118. return {
  119. x1: nodeFrom.x + fromOutput.x,
  120. y1: nodeFrom.y + fromOutput.y,
  121. x2: nodeTo.x + toInput.x,
  122. y2: nodeTo.y + toInput.y,
  123. status: this.activity.nodes[nodeFrom.id] && this.activity.nodes[nodeFrom.id].status
  124. }
  125. }
  126. }
  127. },
  128. mounted () {
  129. this.$nextTick(() => {
  130. this.$forceUpdate()
  131. })
  132. document.addEventListener('keydown', this.keyDown)
  133. document.addEventListener('keyup', this.keyUp)
  134. },
  135. beoreDestroy () {
  136. document.removeEventListener('keydown', this.keyDown)
  137. document.removeEventListener('keyup', this.keyUp)
  138. },
  139. methods: {
  140. ...mapActions('flow', [
  141. 'NOTIFICATION_ADD',
  142. 'DOCUMENT_SYNC',
  143. 'NODE_RAISE', 'NODE_UPDATE', 'NODE_ADD', 'NODE_REMOVE', 'NODE_INSPECT', 'NODE_PROCESS', 'NODE_TRAIN',
  144. 'LINK_ADD', 'LINK_REMOVE',
  145. 'TRIGGER_ADD', 'TRIGGER_REMOVE' ]),
  146. keyDown (ev) {
  147. if (document.activeElement && document.activeElement.matches('input,textarea')) { return }
  148. if (ev.shiftKey) {
  149. this.linking = true
  150. this.triggerLinking = true
  151. }
  152. let single = null
  153. const selectionIds = Object.keys(this.nodeSelection)
  154. if (selectionIds.length === 1) { single = this.nodeSelection[selectionIds[0]] }
  155. switch (ev.key) {
  156. case 'Enter':
  157. if (!single) { return }
  158. this.nodeInspect(single, true)
  159. break
  160. case 'Delete':
  161. if (!this.nodeSelection) { return }
  162. console.log('Removing nodes:', this.nodeSelection)
  163. this.NODE_REMOVE(this.nodeSelection)
  164. // for (let k in this.nodeSelection) {
  165. // this.nodeRemove(this.nodeSelection[k])
  166. // }
  167. break
  168. case 'a':
  169. if (ev.ctrlKey) {
  170. ev.preventDefault()
  171. ev.stopPropagation()
  172. this.nodeSelection = {}
  173. for (let n of this.nodeData.nodes) {
  174. this.nodeSelection[n.id] = n
  175. }
  176. }
  177. break
  178. }
  179. },
  180. keyUp (ev) {
  181. if (!ev.shiftKey) {
  182. this.linking = false
  183. this.triggerLinking = false
  184. }
  185. },
  186. panzoomReset () {
  187. this.panzoom = {x: 0, y: 0, zoom: 1}
  188. },
  189. // XXX: Shrink this function
  190. // and create some LinkAdd method
  191. socketPointerDown (nodeId, ev, socket) {
  192. if (ev.button !== 0) return
  193. const nodeRef = this.$refs.nodes.find(n => n.id === nodeId)
  194. const node = this.nodeById(nodeId)
  195. const isInput = socket.in !== undefined
  196. const socketPos = isInput ? nodeRef.inputPos(socket.in) : nodeRef.outputPos(socket.out)
  197. // const p = this.transformedPoint(ev.clientX, ev.clientY)
  198. this.pointerLink.props = {
  199. x1: node.x + socketPos.x,
  200. y1: node.y + socketPos.y,
  201. x2: node.x + socketPos.x,
  202. y2: node.y + socketPos.y
  203. }
  204. this.pointerLink.active = true
  205. if (isInput) {
  206. this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].inputs[socket.in].type, in: socket.in}
  207. } else {
  208. this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].output.type, out: 0}
  209. }
  210. utils.createDrag({
  211. drag: (ev) => {
  212. const p = this.transformedPoint(ev.clientX, ev.clientY)
  213. if (isInput) {
  214. this.pointerLink.props.x1 = p.x
  215. this.pointerLink.props.y1 = p.y
  216. } else {
  217. this.pointerLink.props.x2 = p.x
  218. this.pointerLink.props.y2 = p.y
  219. }
  220. },
  221. drop: (ev) => {
  222. this.pointerLink.active = false
  223. if (ev.target.matches('.flow-pan-zoom__transformer')) {
  224. if (isInput) {
  225. console.error('LINK: Invalid target')
  226. return
  227. }
  228. const pt = this.transformedPoint(ev.x, ev.y)
  229. this.createPortal(nodeId, pt.x, pt.y)
  230. return
  231. // Create a thing here
  232. // if origin socket is an output only
  233. // Empty drop, try to create a portal here
  234. }
  235. // find Parent
  236. var curTarget = ev.target
  237. for (; curTarget.hasAttribute !== undefined && curTarget !== document.body; curTarget = curTarget.parentNode) {
  238. if (curTarget.hasAttribute('data-nodeid')) {
  239. break
  240. }
  241. }
  242. if (!curTarget.hasAttribute || curTarget === document.body) {
  243. console.error('LINK: target is not a socket')
  244. return
  245. }
  246. const targetNodeId = curTarget.getAttribute('data-nodeid')
  247. const targetIn = curTarget.getAttribute('data-in')
  248. const targetOut = curTarget.getAttribute('data-out')
  249. let link
  250. // target is input
  251. if (targetIn && !isInput) {
  252. link = {
  253. from: nodeId,
  254. to: targetNodeId,
  255. in: parseInt(targetIn)
  256. }
  257. } else if (targetOut && isInput) {
  258. link = {
  259. from: targetNodeId,
  260. to: nodeId,
  261. in: socket.in
  262. }
  263. }
  264. // No link
  265. if (!link) {
  266. console.error('LINK: input same direction (in->in/out->out)')
  267. return
  268. }
  269. const nodeFrom = this.nodeById(link.from)
  270. const nodeTo = this.nodeById(link.to)
  271. const output = this.registry[nodeFrom.src].output.type
  272. const input = this.registry[nodeTo.src].inputs[link.in].type
  273. // Type checking
  274. if (!(output === 'interface {}' || output === input || input === 'interface {}')) {
  275. console.error('LINK: Invalid type')
  276. return
  277. }
  278. // Input already exists, replace
  279. const existingInput = this.nodeData.links.find(l => l.to === link.to && l.in === link.in)
  280. if (existingInput) {
  281. // REMOVE LINK
  282. this.LINK_REMOVE(existingInput)
  283. }
  284. this.LINK_ADD(link)
  285. }})
  286. },
  287. triggerPointerDown (nodeId, ev, dir) {
  288. if (ev.button !== 0) return
  289. const nodeRef = this.$refs.nodes.find(n => n.id === nodeId)
  290. const node = this.nodeById(nodeId)
  291. const isInput = (dir === 'in')
  292. const triggerPos = {
  293. x: 0,
  294. y: nodeRef.bodyProps.height / 2 * (isInput ? -1 : 1)
  295. }
  296. this.pointerTriggerLink.src = {nodeId: node.id, dir: dir}
  297. this.pointerTriggerLink.props = {
  298. x1: node.x + triggerPos.x,
  299. y1: node.y + triggerPos.y,
  300. x2: node.x + triggerPos.x,
  301. y2: node.y + triggerPos.y
  302. }
  303. this.pointerTriggerLink.active = true
  304. utils.createDrag({
  305. drag: (ev) => {
  306. const p = this.transformedPoint(ev.clientX, ev.clientY)
  307. if (isInput) {
  308. this.pointerTriggerLink.props.x1 = p.x
  309. this.pointerTriggerLink.props.y1 = p.y
  310. } else {
  311. this.pointerTriggerLink.props.x2 = p.x
  312. this.pointerTriggerLink.props.y2 = p.y
  313. }
  314. },
  315. drop: (ev) => {
  316. this.pointerTriggerLink.active = false
  317. // find Parent
  318. var curTarget = ev.target
  319. for (; curTarget.hasAttribute !== undefined && curTarget !== document.body; curTarget = curTarget.parentNode) {
  320. if (curTarget.hasAttribute('data-nodeid')) {
  321. break
  322. }
  323. }
  324. if (!curTarget.hasAttribute || curTarget === document.body) {
  325. console.error('TRIGGER: target is not a socket')
  326. return
  327. }
  328. const targetNodeId = curTarget.getAttribute('data-nodeid')
  329. const targetDir = curTarget.getAttribute('data-dir')
  330. if (targetNodeId === node.id) {
  331. console.error('TRIGGER: cannot link to self')
  332. return
  333. }
  334. let trigger
  335. // target is input
  336. if (targetDir === 'in') {
  337. trigger = {
  338. from: nodeId,
  339. to: targetNodeId
  340. }
  341. } else {
  342. trigger = {
  343. from: targetNodeId,
  344. to: nodeId
  345. }
  346. }
  347. this.TRIGGER_ADD(trigger)
  348. }
  349. })
  350. },
  351. // Is this used?
  352. nodeInspect (tnode, force) {
  353. this.$emit('nodeInspect', tnode, force)
  354. },
  355. // HERE
  356. nodePointerDown (ev, i) {
  357. document.activeElement && document.activeElement.blur()
  358. const tnode = this.nodeData.nodes[i]
  359. if (ev.button === 1) {
  360. this.NODE_REMOVE([tnode])
  361. // this.nodeRemove(tnode)
  362. // remove related links
  363. return
  364. }
  365. if (ev.button !== 0) return // first button
  366. if (ev.shiftKey) {
  367. if (this.registry[tnode.src].output) {
  368. this.socketPointerDown(tnode.id, ev, {out: 0})
  369. }
  370. return
  371. }
  372. this.nodeInspect(tnode)
  373. // Switch selection
  374. if (!this.nodeSelection[tnode.id] && !ev.ctrlKey) this.nodeSelection = {}
  375. this.nodeSelection[tnode.id] = tnode
  376. // we can handle with nodeId and a search
  377. this.NODE_RAISE(this.nodeSelection)
  378. let curP = this.transformedPoint(ev.x, ev.y)
  379. this.dragging = this.nodeSelection
  380. utils.createDrag({
  381. drag: (ev) => {
  382. if (this.nodeSelection === undefined) {
  383. console.error('Well something went wrong')
  384. }
  385. const dragP = this.transformedPoint(ev.x, ev.y)
  386. const nodeUpdate = []
  387. for (let k in this.nodeSelection) {
  388. // const n = this.nodeData.nodes.find(n => n.id === k)
  389. const n = this.nodeById(k)
  390. // create new nodes
  391. nodeUpdate.push({
  392. ...n,
  393. x: n.x + dragP.x - curP.x,
  394. y: n.y + dragP.y - curP.y
  395. })
  396. }
  397. this.NODE_UPDATE(nodeUpdate)
  398. // this.sendFlowEvent('nodeUpdate', this.nodeSelection)
  399. curP = dragP
  400. },
  401. drop: (ev) => {
  402. // snap?
  403. this.dragging = null
  404. // Snapping
  405. const dragP = this.transformedPoint(ev.x, ev.y)
  406. const nodeUpdate = []
  407. for (let k in this.nodeSelection) {
  408. // const n = this.nodeData.nodes.find(n => n.id === k)
  409. const n = this.nodeById(k)
  410. // create new nodes
  411. nodeUpdate.push({
  412. ...n,
  413. x: Math.round((n.x + dragP.x - curP.x) / 10) * 10,
  414. y: Math.round((n.y + dragP.y - curP.y) / 10) * 10
  415. })
  416. }
  417. // Updating nodes
  418. this.NODE_UPDATE(nodeUpdate)
  419. this.DOCUMENT_SYNC()
  420. }
  421. })
  422. },
  423. /// ////////////
  424. // NODE CREATOR FUNC
  425. //
  426. // STORE
  427. nodeAdd (src, x = 100, y = 100) {
  428. if (src === 'Output') {
  429. const n = this.nodeData.nodes.find(n => n.src === src)
  430. if (n) {
  431. this.NOTIFICATION_ADD('Output node already exists')
  432. return
  433. }
  434. }
  435. const newNode = {
  436. id: utils.guid(),
  437. x: x,
  438. y: y,
  439. defaultInputs: {},
  440. label: src,
  441. color: this.registry[src].style && this.registry[src].style.color, /* NEW 12/02/2018 */
  442. src: src
  443. }
  444. // Setup Props
  445. if (this.registry[src].props) {
  446. for (let k in this.registry[src].props) {
  447. newNode.prop || (newNode.prop = {})
  448. newNode.prop[k] = ''
  449. }
  450. }
  451. this.NODE_ADD(newNode)
  452. },
  453. managerDrop (ev) {
  454. ev.preventDefault()
  455. const reg = ev.dataTransfer.getData('text')
  456. if (this.registry[reg] === undefined) {
  457. console.error('Registry: Drop src not found in registry')
  458. return
  459. }
  460. const pt = this.transformedPoint(ev.x, ev.y)
  461. this.nodeAdd(reg, pt.x, pt.y)
  462. },
  463. viewPointerDown (ev) {
  464. if (ev.button !== 0) return
  465. ev.preventDefault()
  466. const p = this.transformedPoint(ev.x, ev.y)
  467. this.selector = {x: p.x, y: p.y, width: 0, height: 0}
  468. utils.createDrag({
  469. drag: (evd) => {
  470. // transform again in case we changed zoom/pan
  471. const p = this.transformedPoint(ev.x, ev.y)
  472. const p2 = this.transformedPoint(evd.x, evd.y)
  473. const nwidth = p2.x - p.x
  474. const nheight = p2.y - p.y
  475. this.selector = {
  476. x: nwidth < 0 ? p2.x : p.x,
  477. y: nheight < 0 ? p2.y : p.y,
  478. width: nwidth < 0 ? -nwidth : nwidth,
  479. height: nheight < 0 ? -nheight : nheight
  480. }
  481. },
  482. drop: (ev) => {
  483. if (!ev.shiftKey) this.nodeSelection = {}
  484. for (let n in this.nodeData.nodes) {
  485. const node = this.nodeData.nodes[n]
  486. if (node.x > this.selector.x && node.x < (this.selector.x + this.selector.width) &&
  487. node.y > this.selector.y && node.y < (this.selector.y + this.selector.height)
  488. ) {
  489. this.nodeSelection[node.id] = node
  490. }
  491. }
  492. this.selector = null
  493. }})
  494. },
  495. documentProcess () {
  496. const n = this.nodeData.nodes.find(n => n.src === 'Output')
  497. this.NODE_PROCESS(n.id)
  498. },
  499. createPortal (nodeID, x, y) {
  500. // Find nodeID
  501. let node = this.nodeData.nodes.find(n => n.id === nodeID)
  502. if (!node) {
  503. this.NOTIFICATION_ADD('invalid node ID' + nodeID)
  504. return
  505. }
  506. // If nodeID is a portal we recurse back
  507. if (node.src === 'Portal From') {
  508. const nodefromId = node.prop['portal from']
  509. this.createPortal(nodefromId, x, y)
  510. return
  511. }
  512. x = x || node.x + 10
  513. y = y || node.y + 10
  514. // Special node
  515. const portalNode = {
  516. id: utils.guid(),
  517. x: x,
  518. y: y, // Downthere/improve this
  519. defaultInputs: {},
  520. label: node.label,
  521. color: node.color,
  522. prop: {'portal from': nodeID},
  523. src: 'Portal From'
  524. }
  525. this.NODE_ADD(portalNode)
  526. },
  527. // HELPERS depending on svg ref
  528. createSVGPoint (x, y) {
  529. const p = this.$refs.svg.createSVGPoint()
  530. p.x = x; p.y = y
  531. return p
  532. },
  533. transformedPoint (x, y, abs) {
  534. const svgRect = this.$refs.svg.getBoundingClientRect()
  535. if (!abs) {
  536. x -= svgRect.left
  537. y -= svgRect.top
  538. }
  539. return this.$refs.panzoom.transformedPoint(this.createSVGPoint(x, y))
  540. }
  541. }
  542. }