187 lines
5.6 KiB
Vue
187 lines
5.6 KiB
Vue
<template>
|
|
<div class="graph">
|
|
<canvas
|
|
v-if="boundingRect"
|
|
class="graph__bg"
|
|
ref="bg"
|
|
:width="boundingRect.width"
|
|
:height="boundingRect.height"
|
|
@click="showContextMenu"
|
|
></canvas>
|
|
<Draggable
|
|
v-for="(node, index) in graph"
|
|
:key="index"
|
|
@move="updateNodePosition(node, $event)"
|
|
>
|
|
<Node
|
|
class="graph__node"
|
|
:style="{transform: `translate(${node.x}px, ${node.y}px)`}"
|
|
:node="node.node"
|
|
ref="node"
|
|
:selectedInput="selectedInput"
|
|
:selectedOutput="selectedOutput"
|
|
@outputSelected="selectedOutput = $event"
|
|
@inputSelected="selectedInput = $event"
|
|
@removeNode="removeNode"
|
|
/>
|
|
</Draggable>
|
|
<ContextMenu
|
|
:open="!!contextMenuPosition"
|
|
:position="contextMenuPosition"
|
|
:items="availableNodes"
|
|
@selected="addNode"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
import * as nodes from '../../src/nodes';
|
|
import Node from './Node.vue';
|
|
import Draggable from './Drag/Draggable.js';
|
|
import ContextMenu from './ContextMenu.vue';
|
|
|
|
export default {
|
|
components: {
|
|
ContextMenu,
|
|
Draggable,
|
|
Node,
|
|
},
|
|
props: {
|
|
graph: {
|
|
type: Array,
|
|
required: true,
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
selectedOutput: null,
|
|
selectedInput: null,
|
|
boundingRect: null,
|
|
contextMenuPosition: null,
|
|
}
|
|
},
|
|
watch: {
|
|
selectedOutput() {
|
|
this.tryConnectIO();
|
|
},
|
|
selectedInput() {
|
|
this.tryConnectIO();
|
|
},
|
|
graph: {
|
|
deep: true,
|
|
handler() {
|
|
this.drawConnections();
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
showContextMenu(event) {
|
|
const {offsetX, offsetY} = event;
|
|
if (this.contextMenuPosition) {
|
|
this.contextMenuPosition = null;
|
|
} else {
|
|
this.contextMenuPosition = {
|
|
x: offsetX,
|
|
y: offsetY,
|
|
};
|
|
}
|
|
},
|
|
updateNodePosition(node, {clientX, clientY, x0, y0}) {
|
|
const x = clientX - this.$el.offsetLeft;
|
|
const y = clientY - this.$el.offsetTop;
|
|
console.log('updateNodePosition', x, y)
|
|
node.x = x;
|
|
node.y = y;
|
|
},
|
|
tryConnectIO() {
|
|
if (this.selectedInput && this.selectedOutput) {
|
|
this.selectedInput.connect(this.selectedOutput);
|
|
this.selectedInput = null;
|
|
this.selectedOutput = null;
|
|
}
|
|
},
|
|
recomputeSize() {
|
|
this.boundingRect = this.$el.getBoundingClientRect();
|
|
},
|
|
drawConnections() {
|
|
try {
|
|
const flattenObjectArray = (ary) =>
|
|
ary.reduce((acc, nxt) => Object.assign(acc, nxt), {});
|
|
const inputs = flattenObjectArray(this.$refs.node
|
|
.map((vm) => vm.inputPositions()));
|
|
const outputs = flattenObjectArray(this.$refs.node
|
|
.map((vm) => vm.outputPositions()));
|
|
|
|
const bg = this.$refs.bg;
|
|
const ctx = bg.getContext('2d');
|
|
ctx.clearRect(0, 0, bg.width, bg.height);
|
|
ctx.strokeStyle = "#eaa11a";
|
|
ctx.lineWidth = 3;
|
|
for (let input of Object.values(inputs)) {
|
|
if (!input.output) continue;
|
|
const output = outputs[input.output.id];
|
|
ctx.beginPath();
|
|
ctx.moveTo(input.rect.left, input.rect.top);
|
|
const halfWay = {x: (input.rect.left + output.rect.right)/2, y: (input.rect.y + output.rect.y)/2};
|
|
ctx.bezierCurveTo(
|
|
Math.min(halfWay.x, input.rect.left - Math.max(Math.abs(halfWay.x - input.rect.left), 50)), input.rect.top,
|
|
Math.max(halfWay.x, output.rect.right + Math.max(Math.abs(halfWay.x - output.rect.right), 50)), output.rect.top,
|
|
// Math.min(halfWay.x, output.rect.right - (halfWay.x - output.rect.right)), output.rect.top,
|
|
//halfWay.x, output.rect.top,
|
|
output.rect.right, output.rect.top
|
|
);
|
|
ctx.stroke();
|
|
}
|
|
} catch (err) {
|
|
console.warn('drawConnection', err);
|
|
}
|
|
},
|
|
addNode({x, y, item: {node}}) {
|
|
this.graph.push({
|
|
x, y,
|
|
node: new node(),
|
|
});
|
|
this.contextMenuPosition = null;
|
|
},
|
|
removeNode(nodeToRemove) {
|
|
const index = this.graph.findIndex(({node}) => node === nodeToRemove);
|
|
console.log('nodeIndex', index);
|
|
this.graph.splice(index, 1);
|
|
}
|
|
},
|
|
computed: {
|
|
availableNodes() {
|
|
return Object.keys(nodes)
|
|
.map((name) => ({
|
|
label: name,
|
|
node: nodes[name],
|
|
}))
|
|
}
|
|
},
|
|
mounted() {
|
|
this.$nextTick(() => {
|
|
this.recomputeSize();
|
|
this.drawConnections();
|
|
})
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
.graph {
|
|
position: relative;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background: linear-gradient(#333333, #151515);
|
|
}
|
|
|
|
.graph__node {
|
|
position: absolute;
|
|
}
|
|
|
|
.graph__bg {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
}
|
|
</style>
|
|
|