mirror of https://github.com/aziis98/ro-vis
feat: graph input
parent
566c24eeea
commit
35759b6d1d
@ -0,0 +1,59 @@
|
|||||||
|
// import { evalRuby } from '../../ruby.js'
|
||||||
|
// import algorithmCode from './algorithm.rb?raw'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
|
import { GraphInput, useGraph } from '../../components/GraphInput.jsx'
|
||||||
|
|
||||||
|
const [id1, id2, id3] = Array(3)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => crypto.randomUUID(9).split('-')[0])
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
group: '02 - Flussi su Grafi',
|
||||||
|
title: 'Flussi su Grafi',
|
||||||
|
description: 'Algoritmo per Flussi su Grafi tramite PL',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const View = ({}) => {
|
||||||
|
const [graph, setGraph] = useGraph({
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: id1,
|
||||||
|
label: '1',
|
||||||
|
x: 100 + Math.random() * 500,
|
||||||
|
y: 100 + Math.random() * 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: id2,
|
||||||
|
label: '2',
|
||||||
|
x: 100 + Math.random() * 500,
|
||||||
|
y: 100 + Math.random() * 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: id3,
|
||||||
|
label: '3',
|
||||||
|
x: 100 + Math.random() * 500,
|
||||||
|
y: 100 + Math.random() * 300,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{ from: id1, to: id2, label: 'a' },
|
||||||
|
{ from: id1, to: id3, label: 'b' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="content">
|
||||||
|
<h1>Flussi su Grafi</h1>
|
||||||
|
<h2>Input</h2>
|
||||||
|
<GraphInput graph={graph} setGraph={setGraph} />
|
||||||
|
<p>
|
||||||
|
Doppio click per aggiungere un nodo o modificarne uno esistente. Trascina per
|
||||||
|
spostare i nodi. Inizia a trascinare da un nodo tenendo Ctrl premuto per creare un
|
||||||
|
arco.
|
||||||
|
</p>
|
||||||
|
<h2>Svolgimento</h2>
|
||||||
|
<h2>Output</h2>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,348 @@
|
|||||||
|
import _ from 'lodash'
|
||||||
|
import { useState, useEffect } from 'preact/hooks'
|
||||||
|
|
||||||
|
export const useGraph = (initialGraph = { nodes: [], edges: [] }) => {
|
||||||
|
return useState(initialGraph)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GraphInput = ({ graph, setGraph }) => {
|
||||||
|
const dict = _.keyBy(graph.nodes, 'id')
|
||||||
|
|
||||||
|
const lines = graph.edges.map(({ from, to, label }) => {
|
||||||
|
return shrinkLine({
|
||||||
|
from: dict[from],
|
||||||
|
to: dict[to],
|
||||||
|
midpoint: computeMidpoint(dict[from], dict[to]),
|
||||||
|
label,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const [interacting, setInteracting] = useState(false)
|
||||||
|
// console.log(interacting)
|
||||||
|
|
||||||
|
const onMouseMove = e => {
|
||||||
|
if (interacting && interacting.type === 'drag') {
|
||||||
|
const deltaX = e.x - interacting.initialDragPos.x
|
||||||
|
const deltaY = e.y - interacting.initialDragPos.y
|
||||||
|
|
||||||
|
setGraph(g => {
|
||||||
|
const newNodes = [...g.nodes]
|
||||||
|
newNodes[interacting.index] = {
|
||||||
|
...g.nodes[interacting.index],
|
||||||
|
x: interacting.initialPos.x + deltaX,
|
||||||
|
y: interacting.initialPos.y + deltaY,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...g,
|
||||||
|
nodes: newNodes,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (interacting && interacting.type === 'arrow') {
|
||||||
|
setInteracting(i => ({
|
||||||
|
...i,
|
||||||
|
x: i.initialPos.x + e.x - i.initialDragPos.x,
|
||||||
|
y: i.initialPos.y + e.y - i.initialDragPos.y,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addNode = (x, y) => {
|
||||||
|
setGraph(g => ({
|
||||||
|
...g,
|
||||||
|
nodes: [
|
||||||
|
...g.nodes,
|
||||||
|
{
|
||||||
|
id: crypto.randomUUID(9).split('-')[0],
|
||||||
|
label: '?',
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="graph-input"
|
||||||
|
onDblclick={e =>
|
||||||
|
e.target.classList.contains('graph-input') && addNode(e.offsetX, e.offsetY)
|
||||||
|
}
|
||||||
|
onClick={e =>
|
||||||
|
e.target.classList.contains('graph-input') &&
|
||||||
|
(interacting?.type === 'edit-node' || interacting?.type === 'edit-edge') &&
|
||||||
|
setInteracting(false)
|
||||||
|
}
|
||||||
|
onMouseMove={e => onMouseMove(e)}
|
||||||
|
onMouseUp={() => {
|
||||||
|
if (interacting?.type === 'arrow' && interacting.target !== null) {
|
||||||
|
setGraph(g => ({
|
||||||
|
...g,
|
||||||
|
edges: [
|
||||||
|
...g.edges,
|
||||||
|
{
|
||||||
|
from: g.nodes[interacting.index].id,
|
||||||
|
to: g.nodes[interacting.target].id,
|
||||||
|
label: '?',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interacting?.type === 'arrow' || interacting?.type === 'drag') {
|
||||||
|
setInteracting(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="edges">
|
||||||
|
<svg width="100%" height="100%">
|
||||||
|
<defs>
|
||||||
|
<marker
|
||||||
|
id="arrowhead"
|
||||||
|
markerWidth="6"
|
||||||
|
markerHeight="6"
|
||||||
|
refX="5" // Adjust position of the arrowhead
|
||||||
|
refY="3"
|
||||||
|
orient="auto"
|
||||||
|
markerUnits="strokeWidth"
|
||||||
|
>
|
||||||
|
<polygon points="0 0, 6 3, 0 6" fill="black" />
|
||||||
|
</marker>
|
||||||
|
<marker
|
||||||
|
id="arrowhead-green"
|
||||||
|
markerWidth="10"
|
||||||
|
markerHeight="10"
|
||||||
|
refX="9" // Adjust position of the arrowhead
|
||||||
|
refY="3"
|
||||||
|
orient="auto"
|
||||||
|
markerUnits="strokeWidth"
|
||||||
|
>
|
||||||
|
<polygon points="0 0, 10 3, 0 6" fill="green" />
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{lines.map(({ from, to }) => (
|
||||||
|
<line
|
||||||
|
x1={from.x}
|
||||||
|
y1={from.y}
|
||||||
|
x2={to.x}
|
||||||
|
y2={to.y}
|
||||||
|
stroke="black"
|
||||||
|
stroke-width="2"
|
||||||
|
marker-end="url(#arrowhead)"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{interacting && interacting.type === 'arrow' && (
|
||||||
|
<line
|
||||||
|
x1={graph.nodes[interacting.index].x}
|
||||||
|
y1={graph.nodes[interacting.index].y}
|
||||||
|
x2={interacting.x}
|
||||||
|
y2={interacting.y}
|
||||||
|
stroke="green"
|
||||||
|
stroke-width="2"
|
||||||
|
marker-end="url(#arrowhead-green)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="edge-labels">
|
||||||
|
{lines.map(({ midpoint: { x, y }, label }, index) => (
|
||||||
|
<div
|
||||||
|
class="edge-label"
|
||||||
|
style={{ '--x': x, '--y': y }}
|
||||||
|
onDblclick={e => {
|
||||||
|
setInteracting({
|
||||||
|
type: 'edit-edge',
|
||||||
|
index,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (
|
||||||
|
(e.key === 'Enter' || e.key === 'Escape') &&
|
||||||
|
interacting?.type === 'edit-edge'
|
||||||
|
) {
|
||||||
|
setInteracting(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{interacting?.type === 'edit-edge' && interacting.index === index ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={label}
|
||||||
|
onInput={e =>
|
||||||
|
setGraph(g => {
|
||||||
|
const newEdges = [...g.edges]
|
||||||
|
newEdges[interacting.index] = {
|
||||||
|
...g.edges[interacting.index],
|
||||||
|
label: e.target.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...g,
|
||||||
|
edges: newEdges,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
label
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div class="nodes">
|
||||||
|
{graph.nodes.map(({ id, label, x, y }, index) => (
|
||||||
|
<div
|
||||||
|
class={[
|
||||||
|
'node',
|
||||||
|
interacting?.type === 'arrow' &&
|
||||||
|
interacting.target === index &&
|
||||||
|
'targeted',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
style={{ '--x': x, '--y': y }}
|
||||||
|
onMouseDown={e => {
|
||||||
|
if (interacting) return
|
||||||
|
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
setInteracting({
|
||||||
|
type: 'arrow',
|
||||||
|
index,
|
||||||
|
initialPos: { x, y },
|
||||||
|
initialDragPos: { x: e.x, y: e.y },
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
target: null,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setInteracting({
|
||||||
|
type: 'drag',
|
||||||
|
index,
|
||||||
|
initialPos: { x, y },
|
||||||
|
initialDragPos: { x: e.x, y: e.y },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseMove={e => {
|
||||||
|
if (interacting?.type === 'arrow' && interacting.index !== index) {
|
||||||
|
setInteracting(i => ({ ...i, target: index }))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
if (interacting && interacting.type === 'arrow') {
|
||||||
|
setInteracting(i => ({ ...i, target: null }))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDblclick={e => {
|
||||||
|
setInteracting({
|
||||||
|
type: 'edit-node',
|
||||||
|
index,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (
|
||||||
|
(e.key === 'Enter' || e.key === 'Escape') &&
|
||||||
|
interacting?.type === 'edit-node'
|
||||||
|
) {
|
||||||
|
setInteracting(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{interacting?.type === 'edit-node' && interacting.index === index ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={label}
|
||||||
|
onInput={e =>
|
||||||
|
setGraph(g => {
|
||||||
|
const newNodes = [...g.nodes]
|
||||||
|
newNodes[interacting.index] = {
|
||||||
|
...g.nodes[interacting.index],
|
||||||
|
label: e.target.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...g,
|
||||||
|
nodes: newNodes,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
label
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Utils
|
||||||
|
//
|
||||||
|
|
||||||
|
const shrinkLine = (line, offset = 28) => {
|
||||||
|
const dx = line.to.x - line.from.x
|
||||||
|
const dy = line.to.y - line.from.y
|
||||||
|
|
||||||
|
const length = Math.sqrt(dx * dx + dy * dy)
|
||||||
|
const ratio = offset / length
|
||||||
|
|
||||||
|
const newStart = {
|
||||||
|
x: line.from.x + dx * ratio,
|
||||||
|
y: line.from.y + dy * ratio,
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEnd = {
|
||||||
|
x: line.to.x - dx * ratio,
|
||||||
|
y: line.to.y - dy * ratio,
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...line, from: newStart, to: newEnd }
|
||||||
|
}
|
||||||
|
|
||||||
|
const computeMidpoint = (start, end) => {
|
||||||
|
return {
|
||||||
|
x: (start.x + end.x) / 2,
|
||||||
|
y: (start.y + end.y) / 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const useCtrlClick = () => {
|
||||||
|
const [isCtrlClick, setIsCtrlClick] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeydown = event => {
|
||||||
|
setIsCtrlClick(event.ctrlKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeydown)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeydown)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return isCtrlClick
|
||||||
|
}
|
||||||
|
|
||||||
|
const useShiftClick = () => {
|
||||||
|
const [isShiftClick, setIsShiftClick] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeydown = event => {
|
||||||
|
setIsShiftClick(event.shiftKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeydown)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeydown)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return isShiftClick
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue