examples: add math example (#1461)
* examples: add math example * docs: add math example
This commit is contained in:
@@ -19,6 +19,7 @@ import { SnapToHandleApp, SnappableConnectionLine } from './connection-radius'
|
|||||||
import { NodeResizerApp, ResizableNode } from './node-resizer'
|
import { NodeResizerApp, ResizableNode } from './node-resizer'
|
||||||
import { ToolbarApp, ToolbarNode } from './node-toolbar'
|
import { ToolbarApp, ToolbarNode } from './node-toolbar'
|
||||||
import { LayoutApp, LayoutEdge, LayoutElements, LayoutIcon, LayoutNode, useLayout, useRunProcess, useShuffle } from './layout'
|
import { LayoutApp, LayoutEdge, LayoutElements, LayoutIcon, LayoutNode, useLayout, useRunProcess, useShuffle } from './layout'
|
||||||
|
import { MathApp, MathCSS, MathElements, MathIcon, MathOperatorNode, MathResultNode, MathValueNode } from './math'
|
||||||
|
|
||||||
export const exampleImports = {
|
export const exampleImports = {
|
||||||
basic: {
|
basic: {
|
||||||
@@ -140,4 +141,13 @@ export const exampleImports = {
|
|||||||
'@dagrejs/dagre': 'https://cdn.jsdelivr.net/npm/@dagrejs/dagre@1.1.2/+esm',
|
'@dagrejs/dagre': 'https://cdn.jsdelivr.net/npm/@dagrejs/dagre@1.1.2/+esm',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
math: {
|
||||||
|
'App.vue': MathApp,
|
||||||
|
'ValueNode.vue': MathValueNode,
|
||||||
|
'OperatorNode.vue': MathOperatorNode,
|
||||||
|
'ResultNode.vue': MathResultNode,
|
||||||
|
'Icon.vue': MathIcon,
|
||||||
|
'style.css': MathCSS,
|
||||||
|
'initial-elements.js': MathElements,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { VueFlow } from '@vue-flow/core'
|
||||||
|
import { Background } from '@vue-flow/background'
|
||||||
|
import { initialEdges, initialNodes } from './initial-elements.js'
|
||||||
|
import ValueNode from './ValueNode.vue'
|
||||||
|
import OperatorNode from './OperatorNode.vue'
|
||||||
|
import ResultNode from './ResultNode.vue'
|
||||||
|
|
||||||
|
const nodes = ref(initialNodes)
|
||||||
|
|
||||||
|
const edges = ref(initialEdges)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VueFlow class="math-flow" :nodes="nodes" :edges="edges" fit-view-on-init>
|
||||||
|
<template #node-value="props">
|
||||||
|
<ValueNode :id="props.id" :data="props.data" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #node-operator="props">
|
||||||
|
<OperatorNode :id="props.id" :data="props.data" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #node-result="props">
|
||||||
|
<ResultNode :id="props.id" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Background />
|
||||||
|
</VueFlow>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps(['name'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg v-if="name === '+'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg v-else-if="name === '-'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M19 13H5v-2h14v2z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg v-else-if="name === '*'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 12L6 6m6 6l6 6m-6-6l6-6m-6 6l-6 6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg v-else-if="name === '/'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M19 13H5v-2h14zm-7-8a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0 10a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup>
|
||||||
|
import { Handle, Position, useVueFlow } from '@vue-flow/core'
|
||||||
|
import Icon from './Icon.vue'
|
||||||
|
|
||||||
|
const props = defineProps(['id', 'data'])
|
||||||
|
|
||||||
|
const operators = ['+', '-', '*', '/']
|
||||||
|
|
||||||
|
const { updateNodeData } = useVueFlow()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="buttons nodrag">
|
||||||
|
<button
|
||||||
|
v-for="operator of operators"
|
||||||
|
:key="`${id}-${operator}-operator`"
|
||||||
|
:class="{ selected: data.operator === operator }"
|
||||||
|
@click="updateNodeData(props.id, { operator })"
|
||||||
|
>
|
||||||
|
<Icon :name="operator" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Handle type="source" :position="Position.Right" :connectable="false" />
|
||||||
|
<Handle id="target-a" type="target" :position="Position.Left" :connectable="false" />
|
||||||
|
<Handle id="target-b" type="target" :position="Position.Left" :connectable="false" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Handle, Position, useHandleConnections, useNodesData, useVueFlow } from '@vue-flow/core'
|
||||||
|
|
||||||
|
defineProps(['id'])
|
||||||
|
|
||||||
|
const mathFunctions = {
|
||||||
|
'+': (a, b) => a + b,
|
||||||
|
'-': (a, b) => a - b,
|
||||||
|
'*': (a, b) => a * b,
|
||||||
|
'/': (a, b) => a / b,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getConnectedEdges } = useVueFlow()
|
||||||
|
|
||||||
|
// Get the source connections of the result node. In this example it's only one operator node.
|
||||||
|
const sourceConnections = useHandleConnections({
|
||||||
|
// type target means all connections where *this* node is the target
|
||||||
|
// that means we go backwards in the graph to find the source of the connection(s)
|
||||||
|
type: 'target',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get the source connections of the operator node
|
||||||
|
const operatorSourceConnections = computed(() =>
|
||||||
|
getConnectedEdges(sourceConnections.value[0].source).filter((e) => e.source !== sourceConnections.value[0].source),
|
||||||
|
)
|
||||||
|
|
||||||
|
const operatorData = useNodesData(() => sourceConnections.value.map((connection) => connection.source))
|
||||||
|
|
||||||
|
const valueData = useNodesData(() => operatorSourceConnections.value.map((connection) => connection.source))
|
||||||
|
|
||||||
|
const result = computed(() => {
|
||||||
|
const currResult = operatorData.value.reduce((acc, { data }) => {
|
||||||
|
const operator = data?.operator
|
||||||
|
|
||||||
|
if (operator) {
|
||||||
|
const [a, b] = valueData.value.map(({ data }) => data?.value)
|
||||||
|
|
||||||
|
if (a && b) {
|
||||||
|
return mathFunctions[operator](a, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
// Round to 2 decimal places
|
||||||
|
return Math.round(currResult * 100) / 100
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="calculation">
|
||||||
|
<template v-for="(value, i) in valueData" :key="`${value.id}-${value.data}`">
|
||||||
|
<span>
|
||||||
|
{{ value.data?.value }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="i !== valueData.length - 1">
|
||||||
|
{{ operatorData[0].data?.operator }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span> = </span>
|
||||||
|
|
||||||
|
<span class="counter" :style="{ color: result > 0 ? '#5EC697' : '#f15a16' }">
|
||||||
|
{{ result }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
:position="Position.Left"
|
||||||
|
:connectable="false"
|
||||||
|
:style="{ background: result > 0 ? '#5EC697' : '#f15a16' }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup>
|
||||||
|
import { Handle, Position, useVueFlow } from '@vue-flow/core'
|
||||||
|
|
||||||
|
const props = defineProps(['id', 'data'])
|
||||||
|
|
||||||
|
const { updateNodeData } = useVueFlow()
|
||||||
|
|
||||||
|
function onChange(event) {
|
||||||
|
const value = Number.parseFloat(event.target.value)
|
||||||
|
|
||||||
|
if (!Number.isNaN(value)) {
|
||||||
|
updateNodeData(props.id, { value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<label :for="`${id}-input`"> </label>
|
||||||
|
<input :id="`${id}-input`" :value="data.value" type="number" class="nodrag" @change="onChange" />
|
||||||
|
|
||||||
|
<Handle type="source" :position="Position.Right" :connectable="false" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export { default as MathApp } from './App.vue?raw'
|
||||||
|
export { default as MathElements } from './initial-elements.js?raw'
|
||||||
|
export { default as MathValueNode } from './ValueNode.vue?raw'
|
||||||
|
export { default as MathOperatorNode } from './OperatorNode.vue?raw'
|
||||||
|
export { default as MathResultNode } from './ResultNode.vue?raw'
|
||||||
|
export { default as MathIcon } from './Icon.vue?raw'
|
||||||
|
export { default as MathCSS } from './style.css?inline'
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
export const initialNodes = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
type: 'value',
|
||||||
|
data: { value: 10 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
position: { x: 0, y: 100 },
|
||||||
|
type: 'value',
|
||||||
|
data: { value: 30 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
position: { x: 300, y: 35 },
|
||||||
|
type: 'operator',
|
||||||
|
data: { operator: '+' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
position: { x: 650, y: 15 },
|
||||||
|
type: 'result',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const initialEdges = [
|
||||||
|
{ id: 'e1-3', source: '1', target: '3', animated: true, targetHandle: 'target-a' },
|
||||||
|
{ id: 'e2-3', source: '2', target: '3', animated: true, targetHandle: 'target-b' },
|
||||||
|
{ id: 'e3-4', source: '3', target: '4', animated: true },
|
||||||
|
]
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
.math-flow {
|
||||||
|
background-color: #edf2f7;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__handle {
|
||||||
|
height: 24px;
|
||||||
|
width: 10px;
|
||||||
|
background: #aaa;
|
||||||
|
border-radius: 4px
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__edges path {
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-value.selected {
|
||||||
|
box-shadow: 0 0 0 2px #ec4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-value input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-value input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-value .vue-flow__handle {
|
||||||
|
background-color: #ec4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator.selected {
|
||||||
|
box-shadow: 0 0 0 2px #0EA5E9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator .buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator button {
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #4a5568;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator button:hover {
|
||||||
|
background-color: #0EA5E9;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator button.selected {
|
||||||
|
background-color: #0EA5E9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator .vue-flow__handle[data-handleid="target-a"] {
|
||||||
|
top: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator .vue-flow__handle[data-handleid="target-b"] {
|
||||||
|
top: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator .vue-flow__handle {
|
||||||
|
background-color: #0EA5E9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-result {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-result.selected {
|
||||||
|
box-shadow: 0 0 0 2px #5EC697;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-result .calculation {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
@@ -206,6 +206,7 @@ export default defineConfigWithTheme<DefaultTheme.Config>({
|
|||||||
{ text: 'Drag & Drop', link: '/examples/dnd' },
|
{ text: 'Drag & Drop', link: '/examples/dnd' },
|
||||||
{ text: 'Interactions', link: '/examples/interaction' },
|
{ text: 'Interactions', link: '/examples/interaction' },
|
||||||
{ text: 'Save & Restore', link: '/examples/save' },
|
{ text: 'Save & Restore', link: '/examples/save' },
|
||||||
|
{ text: 'Math Operation Flow', link: '/examples/math' },
|
||||||
{ text: 'Screenshot', link: '/examples/screenshot' },
|
{ text: 'Screenshot', link: '/examples/screenshot' },
|
||||||
{ text: 'Node Visibility', link: '/examples/hidden' },
|
{ text: 'Node Visibility', link: '/examples/hidden' },
|
||||||
{ text: 'Node Intersections', link: '/examples/intersection' },
|
{ text: 'Node Intersections', link: '/examples/intersection' },
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Math Operation
|
||||||
|
|
||||||
|
This example demonstrates how to use the different composables like `useHandleConnections` or `useNodesData` to create a data flow between nodes.
|
||||||
|
|
||||||
|
In this example we use nodes to create a simple math operation, where the user can input two numbers and select an operation to perform.
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<Repl example="math"></Repl>
|
||||||
|
</div>
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# Stress
|
# Stress
|
||||||
|
|
||||||
Vue Flow is built to be fast but there are limits.
|
Vue Flow is built to be fast but there are limits.
|
||||||
@@ -6,9 +5,5 @@ Vue Flow is built to be fast but there are limits.
|
|||||||
Try out how many nodes you can get to render before your browser crashes.
|
Try out how many nodes you can get to render before your browser crashes.
|
||||||
|
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<ClientOnly>
|
<Repl example="stress"></Repl>
|
||||||
<Suspense>
|
|
||||||
<Repl example="stress"></Repl>
|
|
||||||
</Suspense>
|
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,9 +4,5 @@
|
|||||||
Teleport your nodes to another dom element using Vue 3 [`Teleport`](https://vuejs.org/guide/built-ins/teleport.html).
|
Teleport your nodes to another dom element using Vue 3 [`Teleport`](https://vuejs.org/guide/built-ins/teleport.html).
|
||||||
|
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<ClientOnly>
|
<Repl example="teleport"></Repl>
|
||||||
<Suspense>
|
|
||||||
<Repl example="teleport"></Repl>
|
|
||||||
</Suspense>
|
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export const routes: RouterOptions['routes'] = [
|
|||||||
path: '/snap-handle',
|
path: '/snap-handle',
|
||||||
component: () => import('./src/SnapHandle/SnapHandleExample.vue'),
|
component: () => import('./src/SnapHandle/SnapHandleExample.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/math',
|
||||||
|
component: () => import('./src/Math/MathExample.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/node-resizer',
|
path: '/node-resizer',
|
||||||
component: () => import('./src/NodeResizer/NodeResizerExample.vue'),
|
component: () => import('./src/NodeResizer/NodeResizerExample.vue'),
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{
|
||||||
|
name: '+' | '-' | '*' | '/'
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg v-if="name === '+'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg v-else-if="name === '-'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M19 13H5v-2h14v2z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg v-else-if="name === '*'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 12L6 6m6 6l6 6m-6-6l6-6m-6 6l-6 6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg v-else-if="name === '/'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M19 13H5v-2h14zm-7-8a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0 10a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { VueFlow } from '@vue-flow/core'
|
||||||
|
import { Background } from '@vue-flow/background'
|
||||||
|
import { initialEdges, initialNodes } from './initialElements'
|
||||||
|
import ValueNode from './ValueNode.vue'
|
||||||
|
import OperatorNode from './OperatorNode.vue'
|
||||||
|
import ResultNode from './ResultNode.vue'
|
||||||
|
import './styles.css'
|
||||||
|
|
||||||
|
const nodes = ref(initialNodes)
|
||||||
|
|
||||||
|
const edges = ref(initialEdges)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VueFlow class="math-flow" :nodes="nodes" :edges="edges" fit-view-on-init>
|
||||||
|
<template #node-value="props">
|
||||||
|
<ValueNode :id="props.id" :data="props.data" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #node-operator="props">
|
||||||
|
<OperatorNode :id="props.id" :data="props.data" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #node-result="props">
|
||||||
|
<ResultNode :id="props.id" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Background />
|
||||||
|
</VueFlow>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { NodeProps } from '@vue-flow/core'
|
||||||
|
import { Handle, Position, useVueFlow } from '@vue-flow/core'
|
||||||
|
import type { Operator, OperatorNodeData } from './types'
|
||||||
|
import Icon from './Icon.vue'
|
||||||
|
|
||||||
|
const props = defineProps<Pick<NodeProps<OperatorNodeData>, 'id' | 'data'>>()
|
||||||
|
|
||||||
|
const operators: Operator[] = ['+', '-', '*', '/']
|
||||||
|
|
||||||
|
const { updateNodeData } = useVueFlow()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<label :for="`${id}-operator`">Operator</label>
|
||||||
|
|
||||||
|
<div class="buttons nodrag">
|
||||||
|
<button
|
||||||
|
v-for="operator of operators"
|
||||||
|
:key="`${id}-${operator}-operator`"
|
||||||
|
:class="{ selected: data.operator === operator }"
|
||||||
|
@click="updateNodeData(props.id, { operator })"
|
||||||
|
>
|
||||||
|
<Icon :name="operator" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Handle type="source" :position="Position.Right" :connectable="false" />
|
||||||
|
<Handle id="target-a" type="target" :position="Position.Left" :connectable="false" />
|
||||||
|
<Handle id="target-b" type="target" :position="Position.Left" :connectable="false" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { GraphNode } from '@vue-flow/core'
|
||||||
|
import { Handle, Position, useHandleConnections, useNodesData, useVueFlow } from '@vue-flow/core'
|
||||||
|
import type { OperatorNodeData, ValueNodeData } from './types'
|
||||||
|
import { mathFunctions } from './utils'
|
||||||
|
|
||||||
|
defineProps<{ id: string }>()
|
||||||
|
|
||||||
|
const { getConnectedEdges } = useVueFlow()
|
||||||
|
|
||||||
|
// Get the source connections of the result node. In this example it's only one operator node.
|
||||||
|
const sourceConnections = useHandleConnections({
|
||||||
|
// type target means all connections where *this* node is the target
|
||||||
|
// that means we go backwards in the graph to find the source of the connection(s)
|
||||||
|
type: 'target',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get the source connections of the operator node
|
||||||
|
const operatorSourceConnections = computed(() =>
|
||||||
|
getConnectedEdges(sourceConnections.value[0].source).filter((e) => e.source !== sourceConnections.value[0].source),
|
||||||
|
)
|
||||||
|
|
||||||
|
const operatorData = useNodesData<GraphNode<OperatorNodeData>>(() =>
|
||||||
|
sourceConnections.value.map((connection) => connection.source),
|
||||||
|
)
|
||||||
|
|
||||||
|
const valueData = useNodesData<GraphNode<ValueNodeData>>(() =>
|
||||||
|
operatorSourceConnections.value.map((connection) => connection.source),
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = computed(() => {
|
||||||
|
const currResult = operatorData.value.reduce((acc, { data }) => {
|
||||||
|
const operator = data?.operator
|
||||||
|
|
||||||
|
if (operator) {
|
||||||
|
const [a, b] = valueData.value.map(({ data }) => data?.value)
|
||||||
|
|
||||||
|
if (a && b) {
|
||||||
|
return mathFunctions[operator](a, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
// Round to 2 decimal places
|
||||||
|
return Math.round(currResult * 100) / 100
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="calculation">
|
||||||
|
<template v-for="(value, i) in valueData" :key="`${value.id}-${value.data}`">
|
||||||
|
<span>
|
||||||
|
{{ value.data?.value }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="i !== valueData.length - 1">
|
||||||
|
{{ operatorData[0].data?.operator }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span> = </span>
|
||||||
|
|
||||||
|
<span class="counter" :style="{ color: result > 0 ? '#5EC697' : '#f15a16' }">
|
||||||
|
{{ result }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
:position="Position.Left"
|
||||||
|
:connectable="false"
|
||||||
|
:style="{ background: result > 0 ? '#5EC697' : '#f15a16' }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { NodeProps } from '@vue-flow/core'
|
||||||
|
import { Handle, Position, useVueFlow } from '@vue-flow/core'
|
||||||
|
import type { ValueNodeData } from './types'
|
||||||
|
|
||||||
|
const props = defineProps<Pick<NodeProps<ValueNodeData>, 'id' | 'data'>>()
|
||||||
|
|
||||||
|
const { updateNodeData } = useVueFlow()
|
||||||
|
|
||||||
|
function onChange(event: Event) {
|
||||||
|
const evt = event as InputEvent
|
||||||
|
|
||||||
|
const target = evt.target as HTMLInputElement
|
||||||
|
|
||||||
|
const value = Number.parseFloat(target.value)
|
||||||
|
|
||||||
|
if (!Number.isNaN(value)) {
|
||||||
|
updateNodeData(props.id, { value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<label :for="`${id}-input`">Value</label>
|
||||||
|
<input :id="`${id}-input`" :value="data.value" type="number" class="nodrag" @change="onChange" />
|
||||||
|
|
||||||
|
<Handle type="source" :position="Position.Right" :connectable="false" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { Edge, Node } from '@vue-flow/core'
|
||||||
|
|
||||||
|
// these are some math nodes, inputs and outputs
|
||||||
|
export const initialNodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
type: 'value',
|
||||||
|
data: { value: 10 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
position: { x: 0, y: 100 },
|
||||||
|
type: 'value',
|
||||||
|
data: { value: 30 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
position: { x: 400, y: 35 },
|
||||||
|
type: 'operator',
|
||||||
|
data: { operator: '+' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
position: { x: 700, y: 40 },
|
||||||
|
type: 'result',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const initialEdges: Edge[] = [
|
||||||
|
{ id: 'e1-3', source: '1', target: '3', animated: true, targetHandle: 'target-a' },
|
||||||
|
{ id: 'e2-3', source: '2', target: '3', animated: true, targetHandle: 'target-b' },
|
||||||
|
{ id: 'e3-4', source: '3', target: '4', animated: true },
|
||||||
|
]
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
.math-flow {
|
||||||
|
background-color: #edf2f7;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__handle {
|
||||||
|
height: 24px;
|
||||||
|
width: 10px;
|
||||||
|
background: #aaa;
|
||||||
|
border-radius: 4px
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__edges path {
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-value.selected {
|
||||||
|
box-shadow: 0 0 0 2px #ec4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-value input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-value input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-value .vue-flow__handle {
|
||||||
|
background-color: #ec4899;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator.selected {
|
||||||
|
box-shadow: 0 0 0 2px #0EA5E9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator .buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator button {
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #4a5568;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator button:hover {
|
||||||
|
background-color: #0EA5E9;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator button.selected {
|
||||||
|
background-color: #0EA5E9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator .vue-flow__handle[data-handleid="target-a"] {
|
||||||
|
top: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator .vue-flow__handle[data-handleid="target-b"] {
|
||||||
|
top: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-operator .vue-flow__handle {
|
||||||
|
background-color: #0EA5E9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-result {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-result.selected {
|
||||||
|
box-shadow: 0 0 0 2px #5EC697;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__node-result .calculation {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export interface ValueNodeData {
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Operator = '+' | '-' | '*' | '/'
|
||||||
|
|
||||||
|
export interface OperatorNodeData {
|
||||||
|
operator: Operator
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { Operator } from './types'
|
||||||
|
|
||||||
|
export const mathFunctions: Record<Operator, (a: number, b: number) => number> = {
|
||||||
|
'+': (a: number, b: number) => a + b,
|
||||||
|
'-': (a: number, b: number) => a - b,
|
||||||
|
'*': (a: number, b: number) => a * b,
|
||||||
|
'/': (a: number, b: number) => a / b,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user