manager.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. <template>
  2. <div class="flow-container">
  3. <svg
  4. xmlns="http://www.w3.org/2000/svg"
  5. xmlns:xlink="http://www.w3.org/1999/xlink"
  6. ref="svg"
  7. class="flow-view view"
  8. :class="{
  9. 'flow-linking':linking || stickySockets,
  10. 'activity':dragging || pointerLink.active ,
  11. 'flow-detail': detailed
  12. }"
  13. @dragover.prevent
  14. @drop="managerDrop"
  15. :width="width"
  16. :height="height">
  17. <svg-defs/>
  18. <flow-pan-zoom
  19. ref="panzoom"
  20. v-model="panzoom">
  21. <!-- links below nodes -->
  22. <flow-link
  23. v-for="(link,i) in nodeData.links"
  24. :key="i"
  25. v-bind="linkProps(link)"
  26. @click="linkRemove(link)"
  27. />
  28. <!-- nodes -->
  29. <flow-node
  30. ref="nodes"
  31. v-for="(n,i) of nodeData.nodes"
  32. v-bind="nodeProps(n)"
  33. :key="'node' + n.id"
  34. :id="n.id"
  35. @nodePointerDown.prevent="nodeDragStart($event,i)"
  36. @socketPointerDown="socketPointerDown(n.id,...arguments)"
  37. @nodeDoubleClick="$emit('nodeInspect',n.id)"
  38. />
  39. <!-- mouse link-->
  40. <flow-link
  41. :pointer="true"
  42. v-if="pointerLink.active"
  43. v-bind="pointerLink.props"
  44. />
  45. </flow-pan-zoom>
  46. </svg>
  47. <div class="flow-container__control">
  48. <button @click="$emit('funcsPanelToggle')">Panel</button>
  49. <button @click="stickySockets=!stickySockets"> {{ stickySockets? 'Hide':'Show' }} sockets </button>
  50. <button @click="detailed=!detailed"> {{ detailed? 'Hide':'Show' }} detail </button>
  51. <button v-if="panzoom.x!=0 || panzoom.y!=0 || panzoom.zoom!=1" @click="panzoomReset">Reset view</button>
  52. </div>
  53. <div class="flow-container__info">
  54. x:{{ panzoom.x.toFixed(2) }} y:{{ panzoom.y.toFixed(2) }} scale:{{ panzoom.zoom.toFixed(2) }}
  55. nodes: {{ nodeData.nodes.length }}
  56. links: {{ nodeData.links.length }}
  57. </div>
  58. </div>
  59. </template>
  60. <script>
  61. import FlowNode from './node'
  62. import FlowLink from './link'
  63. import FlowPanZoom from './panzoom'
  64. import SvgDefs from './svgdefwrapper.vue'
  65. export default {
  66. name: 'FlowManager',
  67. components: {FlowNode, FlowLink, FlowPanZoom, SvgDefs},
  68. props: {
  69. 'value': {type: Object, default: () => {}},
  70. 'registry': {type: Object, default: () => {}},
  71. 'width': {type: String, default: '800px'},
  72. 'height': {type: String, default: '600px'}
  73. },
  74. data () {
  75. // const cloned = JSON.parse(JSON.stringify(this.value)) // initial?
  76. return {
  77. panzoom: { x: 0, y: 0, zoom: 1 },
  78. nodeData: { nodes: [], links: [] },
  79. dragging: null,
  80. linking: false,
  81. stickySockets: false,
  82. detailed: false,
  83. pointerLink: {active: false, props: {}, src: {}}
  84. }
  85. },
  86. computed: {
  87. nodeProps () {
  88. return (node) => {
  89. let match = {}
  90. if (this.pointerLink.active && this.pointerLink.src.nodeId !== node.id) {
  91. if (this.pointerLink.src.in !== undefined) {
  92. match = {out: this.pointerLink.src.type}
  93. } else {
  94. match = {in: this.pointerLink.src.type}
  95. }
  96. }
  97. const nodeClass = this.registry[node.src]
  98. return {
  99. transform: `translate(${node.x} ${node.y})`,
  100. id: node.id,
  101. label: node.label,
  102. inputs: nodeClass.inputs,
  103. output: nodeClass.output,
  104. match: match,
  105. dragging: this.dragging === node,
  106. nodeStyle: nodeClass.style
  107. }
  108. }
  109. },
  110. linkProps () {
  111. return (link) => {
  112. if (!this.$refs.nodes) return
  113. // For size .x .y
  114. const nodeFrom = this.nodeData.nodes.find(n => n.id === link.from)
  115. const nodeTo = this.nodeData.nodes.find(n => n.id === link.to)
  116. const refFrom = this.$refs.nodes.find(n => n.id === link.from)
  117. const refTo = this.$refs.nodes.find(n => n.id === link.to)
  118. const fromOutput = refFrom.outputPos(0) // only 1 output
  119. const toInput = refTo.inputPos(link.in)
  120. return {
  121. x1: nodeFrom.x + fromOutput.x,
  122. y1: nodeFrom.y + fromOutput.y,
  123. x2: nodeTo.x + toInput.x,
  124. y2: nodeTo.y + toInput.y
  125. }
  126. }
  127. }
  128. },
  129. watch: {
  130. value: {
  131. handler (val) {
  132. const clone = JSON.parse(JSON.stringify(this.value)) // deepClone
  133. const nodes = []
  134. for (let n of clone.nodes) {
  135. if (this.dragging && this.dragging.id === n.id) {
  136. continue
  137. }
  138. nodes.push(n)
  139. }
  140. if (this.dragging) {
  141. nodes.push(this.dragging)
  142. }
  143. this.nodeData = {
  144. nodes: nodes,
  145. links: clone.links
  146. }
  147. this.$nextTick(() => {
  148. this.$forceUpdate()
  149. })
  150. },
  151. deep: true
  152. }
  153. },
  154. mounted () {
  155. this.$nextTick(() => {
  156. this.$forceUpdate()
  157. })
  158. document.addEventListener('keydown', this.keyDown)
  159. document.addEventListener('keyup', this.keyUp)
  160. },
  161. beforeDestroy () {
  162. document.removeEventListener('keydown', this.keyDown)
  163. document.removeEventListener('keyup', this.keyUp)
  164. },
  165. methods: {
  166. keyDown (ev) {
  167. if (ev.shiftKey) {
  168. this.linking = true
  169. }
  170. },
  171. keyUp (ev) {
  172. if (!ev.shiftKey) {
  173. this.linking = false
  174. }
  175. },
  176. panzoomReset () {
  177. this.panzoom.x = 0
  178. this.panzoom.y = 0
  179. this.panzoom.zoom = 1
  180. },
  181. // XXX: Shrink this function
  182. // and create some LinkAdd method
  183. socketPointerDown (nodeId, ev, socket) {
  184. if (ev.button !== 0) return
  185. const nodeRef = this.$refs.nodes.find(n => n.id === nodeId)
  186. const node = this.nodeData.nodes.find(n => n.id === nodeId)
  187. const isInput = socket.in !== undefined
  188. const socketPos = isInput ? nodeRef.inputPos(socket.in) : nodeRef.outputPos(socket.out)
  189. // const p = this.transformedPoint(ev.clientX, ev.clientY)
  190. this.pointerLink.props = {
  191. x1: node.x + socketPos.x,
  192. y1: node.y + socketPos.y,
  193. x2: node.x + socketPos.x,
  194. y2: node.y + socketPos.y
  195. }
  196. this.pointerLink.active = true
  197. if (isInput) {
  198. this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].inputs[socket.in], in: socket.in}
  199. } else {
  200. this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].output, out: 0}
  201. }
  202. // What socket is this
  203. // Create a temporary link
  204. const drag = (ev) => {
  205. const p = this.transformedPoint(ev.clientX, ev.clientY)
  206. if (isInput) {
  207. this.pointerLink.props.x1 = p.x
  208. this.pointerLink.props.y1 = p.y
  209. } else {
  210. this.pointerLink.props.x2 = p.x
  211. this.pointerLink.props.y2 = p.y
  212. }
  213. }
  214. const drop = (ev) => {
  215. document.removeEventListener('mousemove', drag)
  216. document.removeEventListener('mouseup', drop)
  217. this.pointerLink.active = false
  218. const targetNodeId = ev.target.getAttribute('data-nodeid')
  219. const targetIn = ev.target.getAttribute('data-in')
  220. const targetOut = ev.target.getAttribute('data-out')
  221. // Not a node or same node
  222. if (targetNodeId === undefined || targetNodeId === nodeId) {
  223. console.error('LINK: target is not a socket or is node')
  224. return
  225. }
  226. let link
  227. // target is input
  228. if (targetIn && !isInput) {
  229. link = {
  230. from: nodeId,
  231. to: targetNodeId,
  232. in: parseInt(targetIn)
  233. }
  234. } else if (targetOut && isInput) {
  235. link = {
  236. from: targetNodeId,
  237. to: nodeId,
  238. in: socket.in
  239. }
  240. }
  241. // No link
  242. if (!link) {
  243. console.error('LINK: input same direction (in->in/out->out)')
  244. return
  245. }
  246. const nodeFrom = this.nodeData.nodes.find(n => n.id === link.from)
  247. const nodeTo = this.nodeData.nodes.find(n => n.id === link.to)
  248. const output = this.registry[nodeFrom.src].output
  249. const input = this.registry[nodeTo.src].inputs[link.in]
  250. // Type checking
  251. if (!(output === 'any' || output == input || input === 'any')) {
  252. console.error('LINK: Invalid type')
  253. return
  254. }
  255. // Input already exists
  256. const existingInputI = this.nodeData.links.findIndex(l => l.to === link.to && l.in === link.in)
  257. if (existingInputI !== -1) {
  258. this.nodeData.links.splice(existingInputI, 1)
  259. // console.error('LINK: already has input')
  260. // return
  261. }
  262. this.linkAdd(link)
  263. }
  264. document.addEventListener('mousemove', drag)
  265. document.addEventListener('mouseup', drop)
  266. },
  267. nodeDoubleClick (ev, i) {
  268. this.nodeModalTarget = i
  269. this.nodeModal = true
  270. },
  271. nodeDragStart (ev, i) {
  272. document.activeElement && document.activeElement.blur()
  273. var tnode = this.nodeData.nodes[i]
  274. if (ev.button === 1) {
  275. // remove related links
  276. this.nodeData.links = this.nodeData.links.filter(l => l.from !== tnode.id && l.to !== tnode.id)
  277. this.nodeData.nodes.splice(i, 1)
  278. this.$emit('input', this.nodeData)
  279. this.$emit('nodeRemove', tnode)
  280. this.$emit('shouldPersist')
  281. return
  282. }
  283. if (ev.button !== 0) return // first button
  284. if (ev.shiftKey) {
  285. this.socketPointerDown(tnode.id, ev, {out: 0})
  286. return
  287. }
  288. // we can handle with nodeId and a search
  289. this.nodeData.nodes.splice(i, 1)
  290. this.nodeData.nodes.push(tnode) // put in last
  291. this.dragging = tnode
  292. // transform CTM
  293. const delta = this.transformedPoint(ev.clientX, ev.clientY)
  294. delta.x -= tnode.x
  295. delta.y -= tnode.y
  296. const drag = (ev) => {
  297. const point = this.transformedPoint(ev.clientX, ev.clientY)
  298. tnode.x = point.x - delta.x
  299. tnode.y = point.y - delta.y
  300. // Bad possibly
  301. this.$emit('input', this.nodeData)
  302. this.$emit('nodeUpdate', tnode)
  303. }
  304. const drop = (ev) => {
  305. document.removeEventListener('mousemove', drag)
  306. document.removeEventListener('mouseup', drop)
  307. this.dragging = null
  308. this.$emit('input', this.nodeData)
  309. this.$emit('nodeUpdate', tnode)
  310. this.$emit('shouldPersist')
  311. }
  312. document.addEventListener('mousemove', drag)
  313. document.addEventListener('mouseup', drop)
  314. },
  315. nodeAdd (src, x = 100, y = 100) {
  316. const newNode = {
  317. id: guid(),
  318. x: x,
  319. y: y,
  320. label: src,
  321. src: src
  322. }
  323. this.nodeData.nodes.push(newNode)
  324. this.$emit('input', this.nodeData)
  325. this.$emit('nodeUpdate', newNode)
  326. this.$emit('shouldPersist')
  327. },
  328. linkAdd (link) {
  329. this.nodeData.links.push(link)
  330. this.$emit('input', this.nodeData)
  331. this.$emit('linkUpdate', link)
  332. this.$emit('shouldPersist')
  333. },
  334. linkRemove (link) {
  335. const i = this.nodeData.links.findIndex(l => l === link)
  336. if (i === -1) return
  337. this.nodeData.links.splice(i, 1)
  338. this.$emit('input', this.nodeData)
  339. this.$emit('linkRemove', link)
  340. this.$emit('shouldPersist')
  341. },
  342. managerDrop (ev) {
  343. ev.preventDefault()
  344. const reg = ev.dataTransfer.getData('text')
  345. if (this.registry[reg] === undefined) {
  346. console.error('Registry: Drop src not found in registry')
  347. return
  348. }
  349. const pt = this.transformedPoint(ev.x, ev.y)
  350. this.nodeAdd(reg, pt.x, pt.y)
  351. },
  352. // HELPERS depending on svg ref
  353. createSVGPoint (x, y) {
  354. const p = this.$refs.svg.createSVGPoint()
  355. p.x = x; p.y = y
  356. return p
  357. },
  358. transformedPoint (x, y, abs) {
  359. const svgRect = this.$refs.svg.getBoundingClientRect()
  360. if (!abs) {
  361. x -= svgRect.left
  362. y -= svgRect.top
  363. }
  364. return this.$refs.panzoom.transformedPoint(this.createSVGPoint(x, y))
  365. }
  366. }
  367. }
  368. // utils
  369. function guid () {
  370. function s4 () {
  371. return Math.floor((1 + Math.random()) * 0x10000)
  372. .toString(16)
  373. .substring(1)
  374. }
  375. return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
  376. s4() + '-' + s4() + s4() + s4()
  377. }
  378. </script>
  379. <style>
  380. .flow-container {
  381. display:flex;
  382. flex-flow:row;
  383. position:relative;
  384. }
  385. .flow-container__control {
  386. position:absolute;
  387. top: 20px;
  388. left: 20px;
  389. display:flex;
  390. justify-content: center;
  391. align-items: center;
  392. color: #FFF;
  393. }
  394. .flow-container__control button {
  395. display:flex;
  396. justify-content: center;
  397. margin:0;
  398. padding:14px;
  399. color:#333;
  400. }
  401. .flow-container__info {
  402. position:absolute;
  403. bottom:10px;
  404. left:10px;
  405. padding:2px;
  406. font-size:9px;
  407. }
  408. .flow-view {
  409. border:none;
  410. position:relative;
  411. fill:transparent;
  412. }
  413. </style>