editor.js 18 KB

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