examples: add math example (#1461)
* examples: add math example * docs: add math example
This commit is contained in:
@@ -18,6 +18,10 @@ export const routes: RouterOptions['routes'] = [
|
||||
path: '/snap-handle',
|
||||
component: () => import('./src/SnapHandle/SnapHandleExample.vue'),
|
||||
},
|
||||
{
|
||||
path: '/math',
|
||||
component: () => import('./src/Math/MathExample.vue'),
|
||||
},
|
||||
{
|
||||
path: '/node-resizer',
|
||||
component: () => import('./src/NodeResizer/NodeResizerExample.vue'),
|
||||
|
||||
33
examples/vite/src/Math/Icon.vue
Normal file
33
examples/vite/src/Math/Icon.vue
Normal file
@@ -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>
|
||||
31
examples/vite/src/Math/MathExample.vue
Normal file
31
examples/vite/src/Math/MathExample.vue
Normal file
@@ -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>
|
||||
31
examples/vite/src/Math/OperatorNode.vue
Normal file
31
examples/vite/src/Math/OperatorNode.vue
Normal file
@@ -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>
|
||||
76
examples/vite/src/Math/ResultNode.vue
Normal file
76
examples/vite/src/Math/ResultNode.vue
Normal file
@@ -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>
|
||||
28
examples/vite/src/Math/ValueNode.vue
Normal file
28
examples/vite/src/Math/ValueNode.vue
Normal file
@@ -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>
|
||||
34
examples/vite/src/Math/initialElements.ts
Normal file
34
examples/vite/src/Math/initialElements.ts
Normal file
@@ -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 },
|
||||
]
|
||||
123
examples/vite/src/Math/styles.css
Normal file
123
examples/vite/src/Math/styles.css
Normal file
@@ -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;
|
||||
}
|
||||
9
examples/vite/src/Math/types.ts
Normal file
9
examples/vite/src/Math/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface ValueNodeData {
|
||||
value: number
|
||||
}
|
||||
|
||||
export type Operator = '+' | '-' | '*' | '/'
|
||||
|
||||
export interface OperatorNodeData {
|
||||
operator: Operator
|
||||
}
|
||||
8
examples/vite/src/Math/utils.ts
Normal file
8
examples/vite/src/Math/utils.ts
Normal file
@@ -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