diff --git a/src/algorithms/flussi-su-grafi/View.jsx b/src/algorithms/flussi-su-grafi/View.jsx new file mode 100644 index 0000000..a232f72 --- /dev/null +++ b/src/algorithms/flussi-su-grafi/View.jsx @@ -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 ( +
+

Flussi su Grafi

+

Input

+ +

+ 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. +

+

Svolgimento

+

Output

+
+ ) +} diff --git a/src/algorithms/simplesso-primale-algebrico/View.jsx b/src/algorithms/simplesso-primale-algebrico/View.jsx index 0c5ee2c..cac7261 100644 --- a/src/algorithms/simplesso-primale-algebrico/View.jsx +++ b/src/algorithms/simplesso-primale-algebrico/View.jsx @@ -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 ( <>

Simplesso Primale Algebrico

-

TODO

- ) diff --git a/src/algorithms/simplesso-primale-algebrico/algorithm.rb b/src/algorithms/simplesso-primale-algebrico/algorithm.rb index 4ffc412..f1f816c 100644 --- a/src/algorithms/simplesso-primale-algebrico/algorithm.rb +++ b/src/algorithms/simplesso-primale-algebrico/algorithm.rb @@ -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 diff --git a/src/components/GraphInput.jsx b/src/components/GraphInput.jsx new file mode 100644 index 0000000..16e011f --- /dev/null +++ b/src/components/GraphInput.jsx @@ -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 ( +
+ 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) + } + }} + > +
+ + + + + + + + + + + {lines.map(({ from, to }) => ( + + ))} + + {interacting && interacting.type === 'arrow' && ( + + )} + +
+
+ {lines.map(({ midpoint: { x, y }, label }, index) => ( +
{ + 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 ? ( + + setGraph(g => { + const newEdges = [...g.edges] + newEdges[interacting.index] = { + ...g.edges[interacting.index], + label: e.target.value, + } + + return { + ...g, + edges: newEdges, + } + }) + } + /> + ) : ( + label + )} +
+ ))} +
+
+ {graph.nodes.map(({ id, label, x, y }, index) => ( +
{ + 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 ? ( + + setGraph(g => { + const newNodes = [...g.nodes] + newNodes[interacting.index] = { + ...g.nodes[interacting.index], + label: e.target.value, + } + + return { + ...g, + nodes: newNodes, + } + }) + } + /> + ) : ( + label + )} +
+ ))} +
+
+ ) +} + +// +// 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 +} diff --git a/src/main.jsx b/src/main.jsx index c330965..699d66a 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -31,20 +31,22 @@ const AlgorithmChooserView = ({ setCurrentView }) => { return ( <>

Algoritmi

- {Object.entries(sections).map(([group, algorithms]) => ( -
-

{group}

-
- {algorithms.map(({ id, metadata }) => ( - setCurrentView(id)} - /> - ))} -
-
- ))} + {Object.entries(sections) + .toSorted((a, b) => a[0].localeCompare(b[0])) + .map(([group, algorithms]) => ( +
+

{group}

+
+ {algorithms.map(({ id, metadata }) => ( + setCurrentView(id)} + /> + ))} +
+
+ ))}

Flussi su Grafi

diff --git a/src/style.css b/src/style.css index d7b3c63..2ee53a7 100644 --- a/src/style.css +++ b/src/style.css @@ -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;