feat: graph input

backup-1
Antonio De Lucreziis 2 years ago
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>
)
}

@ -5,7 +5,7 @@ import algorithmCode from './algorithm.rb?raw'
import { useEffect, useState } from 'preact/hooks'
export const metadata = {
group: 'Programmazione Lineare',
group: '01 - Programmazione Lineare',
title: 'Simplesso Primale Algebrico',
description: 'Algoritmo principale della programmazione lineare',
}
@ -21,8 +21,6 @@ export const View = ({}) => {
return (
<>
<h1>Simplesso Primale Algebrico</h1>
<p>TODO</p>
<Steps steps={steps} />
</>
)

@ -77,12 +77,11 @@ def run(config)
print_card "Initial" do
math <<-LATEX
\\begin{aligned}
c &= #{c.to_latex} \\\\
A &= #{mat.to_latex} \\\\
b &= #{b.to_latex} \\\\
\\mathcal B &= \\{ #{basis.map { |i| i + 1 }.join(", ")} \\}
\\end{aligned}
\\begin{array}{rlcl}
c &= #{c.to_matrix.transpose.to_latex}^T & & \\\\[5pt]
A &= #{mat.to_latex} & \\leq & #{b.to_latex} = b \\\\[28pt]
\\mathcal B &= \\{ #{basis.map { |i| i + 1 }.join(", ")} \\} &&
\\end{array}
LATEX
end
@ -131,7 +130,6 @@ def run(config)
\\end{aligned}
LATEX
end
break
end
@ -150,7 +148,7 @@ def run(config)
LATEX
end
u = Vector.basis(size: basis.size, index: h)
u = Vector.basis(size: basis.size, index: basis_inv[h])
xi = -mat_basis_inv * u
@ -172,7 +170,7 @@ def run(config)
print_card "Step" do
math <<-LATEX
\\begin{aligned}
N &= \\{ #{basis.map { |i| i + 1 }.join(", ")} \\} \\setminus \\{ #{h + 1} \\} \\\\
N &= \\{ #{(0...mat.row_count).to_a.map { |i| i + 1 }.join(", ")} \\} \\setminus \\{ #{h + 1} \\} \\\\
A_N &= #{mat_basis_op.to_latex} \\\\
d &= A_N \\xi = #{mat_basis_op.to_latex} #{xi.to_latex} = #{d.to_latex}
\\end{aligned}
@ -180,8 +178,44 @@ def run(config)
end
if d.all? { |d| d <= 0 }
print_card "Step" do
text "Problem is unbounded."
math <<-LATEX
\\begin{aligned}
A_N \\xi = #{d.to_latex} \\leq 0
\\end{aligned}
LATEX
end
break
end
# k := \min \left\{ \arg\min_{i \in N, A_i \xi > 0} \frac{b_i - A_i \bar{x}}{A_i \xi} \right\}
k = (0...mat.row_count).to_a
.select { |i| !basis.include?(i) && mat.row(i).inner_product(xi) > 0 }
.min_by { |i| (b[i] - mat.row(i).inner_product(x_bar)) / mat.row(i).inner_product(xi) }
print_card "Step" do
math <<-LATEX
\\begin{aligned}
N &= \\{ #{(0...mat.row_count).to_a.map { |i| i + 1 }.join(", ")} \\} \\setminus \\{ #{h + 1} \\} \\\\
k &= \\min \\left\\{ i \\in N : A_i \\xi > 0 \\right\\} = #{k + 1}
\\end{aligned}
LATEX
end
p basis
basis = basis - [h] + [k]
p basis
print_card "Step" do
text "Update basis."
math <<-LATEX
\\begin{aligned}
\\mathcal B &= \\{ #{basis.map { |i| i + 1 }.join(", ")} \\}
\\end{aligned}
LATEX
end
end
end

@ -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
}

@ -31,20 +31,22 @@ const AlgorithmChooserView = ({ setCurrentView }) => {
return (
<>
<h1>Algoritmi</h1>
{Object.entries(sections).map(([group, algorithms]) => (
<section>
<h2>{group}</h2>
<div class="boxes">
{algorithms.map(({ id, metadata }) => (
<NewAlgorithmBox
title={metadata.title}
description={metadata.description}
onClick={() => setCurrentView(id)}
/>
))}
</div>
</section>
))}
{Object.entries(sections)
.toSorted((a, b) => a[0].localeCompare(b[0]))
.map(([group, algorithms]) => (
<section>
<h2>{group}</h2>
<div class="boxes">
{algorithms.map(({ id, metadata }) => (
<NewAlgorithmBox
title={metadata.title}
description={metadata.description}
onClick={() => setCurrentView(id)}
/>
))}
</div>
</section>
))}
<section>
<h2>Flussi su Grafi</h2>
<div class="boxes">

@ -29,6 +29,16 @@ img {
/* Typography */
.content {
display: block;
margin: 0 auto;
max-width: 100ch;
> * + * {
margin-top: 1rem;
}
}
h1,
h2,
h3,
@ -54,10 +64,10 @@ h6 {
gap: 0.25rem;
padding: 1rem;
border-radius: 0.75rem;
background: #fff;
border: 2px solid #888;
border-radius: 0.75rem;
width: 10rem;
min-height: calc(10rem * 3 / 4);
@ -91,7 +101,7 @@ h6 {
border-radius: 0.75rem;
background: #fff;
border: 2px solid #333;
border: 2px solid #888;
min-width: 20rem;
@ -100,10 +110,88 @@ h6 {
font-size: 15px;
}
& > .state {
& > .content {
display: grid;
grid-template-columns: 1fr;
gap: 0.5rem;
font-size: 16px;
}
}
}
.graph-input {
width: 100%;
height: 30rem;
background: #fff;
border: 2px solid #333;
border-radius: 0.75rem;
position: relative;
overflow: hidden;
> * {
inset: 0;
position: absolute;
pointer-events: none;
input[type='text'] {
max-width: 5rem;
}
}
> .edge-labels {
.edge-label {
position: absolute;
z-index: 10;
left: calc(var(--x) * 1px);
top: calc(var(--y) * 1px);
transform: translate(-50%, -50%);
background: #fff;
border: 2px solid #333;
border-radius: 0.25rem;
padding: 0.25rem;
cursor: pointer;
pointer-events: all;
}
}
> .nodes {
> .node {
position: absolute;
z-index: 10;
left: calc(var(--x) * 1px);
top: calc(var(--y) * 1px);
transform: translate(-50%, -50%);
display: grid;
place-content: center;
width: 2.5rem;
height: 2.5rem;
background: #f0f0f0;
border: 2px solid #333;
border-radius: 1.25rem;
pointer-events: all;
cursor: move;
&.targeted {
color: #fff;
background: green;
}
}
}
}
@ -118,6 +206,7 @@ body {
font-family: 'Inter', sans-serif;
font-weight: 400;
font-size: 16px;
line-height: 1.5;
background: #f0f0f0;

Loading…
Cancel
Save