graf: přidána funkce pro manuální pozicování vrcholů a perzistence pozice
This commit is contained in:
parent
bd5e5a6a48
commit
a264dbead9
6 changed files with 241 additions and 28 deletions
|
@ -11,6 +11,7 @@
|
||||||
let clicked: string[] = [];
|
let clicked: string[] = [];
|
||||||
let graph: Graph;
|
let graph: Graph;
|
||||||
let currentTask: TaskDescriptor | null = null;
|
let currentTask: TaskDescriptor | null = null;
|
||||||
|
let nodeDraggingEnabled: boolean = false;
|
||||||
|
|
||||||
function clickTask(e: CustomEvent<TaskDescriptor>) {
|
function clickTask(e: CustomEvent<TaskDescriptor>) {
|
||||||
// ukladani seznamu poslednich kliknuti
|
// ukladani seznamu poslednich kliknuti
|
||||||
|
@ -39,6 +40,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveCurrentStateWithPositions() {
|
||||||
|
tasks.positions = graph.getNodePositions();
|
||||||
|
await saveTasks(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
async function saveCurrentState() {
|
async function saveCurrentState() {
|
||||||
await saveTasks(tasks);
|
await saveTasks(tasks);
|
||||||
}
|
}
|
||||||
|
@ -106,22 +112,36 @@
|
||||||
{repulsionForce}
|
{repulsionForce}
|
||||||
on:selectTask={clickTask}
|
on:selectTask={clickTask}
|
||||||
on:preSelectTask={startHovering}
|
on:preSelectTask={startHovering}
|
||||||
bind:this={graph} />
|
bind:this={graph}
|
||||||
|
{nodeDraggingEnabled} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<div class="toolbox">
|
<div class="toolbox">
|
||||||
<div>Toolbox</div>
|
<div>Toolbox</div>
|
||||||
<div>
|
<div>
|
||||||
<button disabled={clicked.length <= 1} on:click={addEdge}>Přidat hranu {clicked[clicked.length - 2]} -> {clicked[clicked.length - 1]}</button>
|
<button disabled={clicked.length <= 1} on:click={addEdge}>Přidat hranu {clicked[clicked.length - 2]}
|
||||||
|
-> {clicked[clicked.length - 1]}</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button on:click={graph.runSimulation}>Spustit simulaci</button>
|
||||||
</div>
|
</div>
|
||||||
<div><button on:click={graph.runSimulation}>Spustit simulaci</button></div>
|
|
||||||
<div>
|
<div>
|
||||||
Repulsion force: <input type="number" bind:value={repulsionForce} name="repulsionForceInput" max="1000" min="-10000" />
|
Repulsion force: <input type="number" bind:value={repulsionForce} name="repulsionForceInput" max="1000" min="-10000" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button on:click={saveCurrentState}>Uložit aktuální stav</button>
|
<button on:click={saveCurrentState}>Uložit aktuální stav</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<button on:click={saveCurrentStateWithPositions}>Uložit aktuální stav
|
||||||
|
včetně pozic nodů</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" bind:checked={nodeDraggingEnabled} /> Povolit přesouvání
|
||||||
|
vrcholů
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="taskDetails">
|
<div class="taskDetails">
|
||||||
{#if currentTask != null}
|
{#if currentTask != null}
|
||||||
|
|
|
@ -6,11 +6,12 @@
|
||||||
import type { TasksFile, TaskDescriptor } from "./task-loader";
|
import type { TasksFile, TaskDescriptor } from "./task-loader";
|
||||||
import { createNodesAndEdges } from "./graph-types";
|
import { createNodesAndEdges } from "./graph-types";
|
||||||
import { taskForce } from "./task-force";
|
import { taskForce } from "./task-force";
|
||||||
import { zoom } from "d3";
|
|
||||||
|
|
||||||
export let tasks: TasksFile;
|
export let tasks: TasksFile;
|
||||||
let hoveredTask: null | string = null;
|
|
||||||
export let repulsionForce: number = -1000;
|
export let repulsionForce: number = -1000;
|
||||||
|
export let nodeDraggingEnabled: boolean = false;
|
||||||
|
|
||||||
|
let hoveredTask: null | string = null;
|
||||||
|
|
||||||
// Svelte automatically fills these with a reference
|
// Svelte automatically fills these with a reference
|
||||||
let container: HTMLElement;
|
let container: HTMLElement;
|
||||||
|
@ -23,7 +24,7 @@
|
||||||
let [nodes, edges] = createNodesAndEdges(tasks);
|
let [nodes, edges] = createNodesAndEdges(tasks);
|
||||||
function hack() {
|
function hack() {
|
||||||
[nodes, edges] = createNodesAndEdges(tasks, nodes, edges);
|
[nodes, edges] = createNodesAndEdges(tasks, nodes, edges);
|
||||||
runSimulation();
|
//runSimulation();
|
||||||
}
|
}
|
||||||
$: {
|
$: {
|
||||||
tasks;
|
tasks;
|
||||||
|
@ -74,12 +75,18 @@
|
||||||
nodes = nodes;
|
nodes = nodes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const zoomer = d3.zoom().scaleExtent([0.1, 2])
|
|
||||||
|
|
||||||
$: {
|
export function getNodePositions(): Map<string, [number, number]> {
|
||||||
// zoomer.extent([[-clientWidth / 2,-clientHeight / 2],[clientWidth,clientHeight]])
|
let res = new Map();
|
||||||
|
for (let n of nodes) {
|
||||||
|
if (n.x != undefined && n.y != undefined) {
|
||||||
|
res.set(n.id, [n.x, n.y]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// start simulation and center view on create
|
// start simulation and center view on create
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// set center of the SVG at (0,0)
|
// set center of the SVG at (0,0)
|
||||||
|
@ -89,10 +96,9 @@
|
||||||
function zoomed(e) {
|
function zoomed(e) {
|
||||||
svg.attr("transform", e.transform);
|
svg.attr("transform", e.transform);
|
||||||
}
|
}
|
||||||
|
const zoomer = d3.zoom().scaleExtent([0.1, 2])
|
||||||
zoomer.on("zoom", zoomed);
|
zoomer.on("zoom", zoomed);
|
||||||
d3.select(container).call(zoomer);
|
d3.select(container).call(zoomer);
|
||||||
|
|
||||||
runSimulation();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -129,7 +135,9 @@
|
||||||
{task}
|
{task}
|
||||||
on:taskClick
|
on:taskClick
|
||||||
on:click={nodeClick(task.task)}
|
on:click={nodeClick(task.task)}
|
||||||
on:hoveringChange={nodeHover(task.task)} />
|
on:hoveringChange={nodeHover(task.task)}
|
||||||
|
on:positionChange={() => { tasks = tasks; }}
|
||||||
|
draggingEnabled={nodeDraggingEnabled} />
|
||||||
{/each}
|
{/each}
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import * as d3 from "d3";
|
||||||
|
|
||||||
import { createEventDispatcher, onMount } from "svelte";
|
import { createEventDispatcher, onMount } from "svelte";
|
||||||
import type { TaskId } from "./graph-types";
|
import type { TaskId } from "./graph-types";
|
||||||
|
|
||||||
|
|
||||||
export let task: TaskId;
|
export let task: TaskId;
|
||||||
|
export let draggingEnabled: boolean = false;
|
||||||
|
|
||||||
let hovering: boolean = false;
|
let hovering: boolean = false;
|
||||||
let text_element: SVGTextElement;
|
let text_element: SVGTextElement;
|
||||||
|
|
||||||
|
@ -31,6 +34,34 @@
|
||||||
const bbox = text_element.getBBox();
|
const bbox = text_element.getBBox();
|
||||||
ellipse_rx = bbox.width / 2 + 8;
|
ellipse_rx = bbox.width / 2 + 8;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// dragging
|
||||||
|
let dragging: boolean = false;
|
||||||
|
function dragStart(e: MouseEvent) {
|
||||||
|
if (!draggingEnabled) return;
|
||||||
|
|
||||||
|
dragging = true;
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
function drag(e: MouseEvent) {
|
||||||
|
if (!draggingEnabled) return;
|
||||||
|
if (!dragging) return;
|
||||||
|
|
||||||
|
let [x, y] = d3.pointer(e);
|
||||||
|
task.x = x;
|
||||||
|
task.y = y;
|
||||||
|
eventDispatcher("positionChange");
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
function dragStop(e: MouseEvent) {
|
||||||
|
if (!draggingEnabled) return;
|
||||||
|
|
||||||
|
dragging = false;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -45,7 +76,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<g on:mouseenter={enter} on:mouseleave={leave} on:click={click}>
|
<g on:mouseenter={enter} on:mouseleave={leave} on:click={click} on:mousedown={dragStart} on:mouseup={dragStop} on:mousemove={drag}>
|
||||||
<ellipse rx={ellipse_rx} ry={20} {cx} {cy} />
|
<ellipse rx={ellipse_rx} ry={20} {cx} {cy} />
|
||||||
<text
|
<text
|
||||||
bind:this={text_element}
|
bind:this={text_element}
|
||||||
|
|
|
@ -21,11 +21,14 @@ function toMapById(nodes: TaskId[]): Map<string, TaskId> {
|
||||||
function createNodes(tasks: TasksFile, old?: TaskId[]): TaskId[] {
|
function createNodes(tasks: TasksFile, old?: TaskId[]): TaskId[] {
|
||||||
let m = (old == undefined) ? new Map<string, TaskId>() : toMapById(old);
|
let m = (old == undefined) ? new Map<string, TaskId>() : toMapById(old);
|
||||||
|
|
||||||
let res = tasks.tasks.map((t) => {
|
let res: TaskId[] = tasks.tasks.map((t) => {
|
||||||
return { id: t.id, task: t };
|
return { id: t.id, task: t };
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let t of res) {
|
for (let t of res) {
|
||||||
|
if (tasks.positions.has(t.id)) {
|
||||||
|
[t.x, t.y] = tasks.positions.get(t.id)!
|
||||||
|
}
|
||||||
if (m.has(t.id)) {
|
if (m.has(t.id)) {
|
||||||
Object.assign(t, m.get(t.id))
|
Object.assign(t, m.get(t.id))
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,20 +9,31 @@ export type TaskDescriptor = {
|
||||||
export type TasksFile = {
|
export type TasksFile = {
|
||||||
tasks: TaskDescriptor[]
|
tasks: TaskDescriptor[]
|
||||||
clusters: { [name: string]: string[] }
|
clusters: { [name: string]: string[] }
|
||||||
|
positions: Map<string, [number, number]>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TaskMap = Map<string, TaskDescriptor>;
|
export type TaskMap = Map<string, TaskDescriptor>;
|
||||||
|
|
||||||
export async function loadTasks(): Promise<TasksFile> {
|
export async function loadTasks(): Promise<TasksFile> {
|
||||||
const r = await fetch("/tasks.json")
|
const r = await fetch("/tasks.json")
|
||||||
return await r.json()
|
const j = await r.json()
|
||||||
|
if (j.positions == null)
|
||||||
|
j.positions = new Map();
|
||||||
|
else
|
||||||
|
j.positions = new Map(Object.entries(j.positions))
|
||||||
|
return j
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveTasks(tasks: TasksFile) {
|
export async function saveTasks(tasks: TasksFile) {
|
||||||
|
let p: any = {}
|
||||||
|
for (let [key, val] of tasks.positions.entries())
|
||||||
|
p[key] = val;
|
||||||
|
const data = {...tasks, positions: p}
|
||||||
|
|
||||||
// request options
|
// request options
|
||||||
const options = {
|
const options = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(tasks, null, 4),
|
body: JSON.stringify(data, null, 4),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
|
|
162
tasks.json
162
tasks.json
|
@ -158,52 +158,62 @@
|
||||||
"requires": []
|
"requires": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "29-Z3-2","type": "open-data",
|
"id": "29-Z3-2",
|
||||||
|
"type": "open-data",
|
||||||
"comment": "Písemka z angličtiny — voser implementovat, easy dřevorubecký řešení, optimálně trie, což na Z IMHO hard",
|
"comment": "Písemka z angličtiny — voser implementovat, easy dřevorubecký řešení, optimálně trie, což na Z IMHO hard",
|
||||||
"requires": []
|
"requires": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "26-Z1-4","type": "open-data",
|
"id": "26-Z1-4",
|
||||||
|
"type": "open-data",
|
||||||
"comment": "Hroch v jezeře - BFS či jiné prohledávání, počítání velikosti komponent v 2D poli, ",
|
"comment": "Hroch v jezeře - BFS či jiné prohledávání, počítání velikosti komponent v 2D poli, ",
|
||||||
"requires": []
|
"requires": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "26-Z4-4","type": "open-data",
|
"id": "26-Z4-4",
|
||||||
|
"type": "open-data",
|
||||||
"comment": "Hlídači v labyrintu - policajti hlídající na grafu, konkrétně na stromě, rekurze, technicky asi až DP",
|
"comment": "Hlídači v labyrintu - policajti hlídající na grafu, konkrétně na stromě, rekurze, technicky asi až DP",
|
||||||
"requires": []
|
"requires": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "26-Z3-4","type": "open-data",
|
"id": "26-Z3-4",
|
||||||
|
"type": "open-data",
|
||||||
"comment": "Tvar labyrintu - nejdelší cesta ve stromě, graf",
|
"comment": "Tvar labyrintu - nejdelší cesta ve stromě, graf",
|
||||||
"requires": []
|
"requires": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "29-Z1-4","type": "open-data",
|
"id": "29-Z1-4",
|
||||||
|
"type": "open-data",
|
||||||
"comment": "Zuzčin výlet — DFS (topologické pořadí)",
|
"comment": "Zuzčin výlet — DFS (topologické pořadí)",
|
||||||
"requires": []
|
"requires": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "31-Z1-2","type": "open-data",
|
"id": "31-Z1-2",
|
||||||
|
"type": "open-data",
|
||||||
"comment": "BFS (šachovnice, custom figurka, nejkratší cesta) ",
|
"comment": "BFS (šachovnice, custom figurka, nejkratší cesta) ",
|
||||||
"requires": []
|
"requires": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "31-Z3-2","type": "open-data",
|
"id": "31-Z3-2",
|
||||||
|
"type": "open-data",
|
||||||
"comment": "DFS (hledání cesty v grafu po písmenech)",
|
"comment": "DFS (hledání cesty v grafu po písmenech)",
|
||||||
"requires": []
|
"requires": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "31-Z3-3","type": "open-data",
|
"id": "31-Z3-3",
|
||||||
|
"type": "open-data",
|
||||||
"comment": "barvení bipartitního grafu (hledání partit), na vstupu hrany",
|
"comment": "barvení bipartitního grafu (hledání partit), na vstupu hrany",
|
||||||
"requires": []
|
"requires": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "26-Z4-2","type": "open-data",
|
"id": "26-Z4-2",
|
||||||
|
"type": "open-data",
|
||||||
"comment": "Sbírání vajíček - hledení mediánu, musí se to ale vymyslet, nejkratší cesta při chození tam a zpět",
|
"comment": "Sbírání vajíček - hledení mediánu, musí se to ale vymyslet, nejkratší cesta při chození tam a zpět",
|
||||||
"requires": []
|
"requires": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "26-Z3-1","type": "open-data",
|
"id": "26-Z3-1",
|
||||||
|
"type": "open-data",
|
||||||
"comment": "Zámky labyrintu - hromada ifů, vhodné možná na code review, hledání čísla z trojice takového, že je trojice aritmetrická posloupnost",
|
"comment": "Zámky labyrintu - hromada ifů, vhodné možná na code review, hledání čísla z trojice takového, že je trojice aritmetrická posloupnost",
|
||||||
"requires": []
|
"requires": []
|
||||||
}
|
}
|
||||||
|
@ -259,5 +269,135 @@
|
||||||
"Nápad": [
|
"Nápad": [
|
||||||
"26-Z4-2"
|
"26-Z4-2"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"start": [
|
||||||
|
60.9366784537015,
|
||||||
|
15.282753057729023
|
||||||
|
],
|
||||||
|
"jak-resit-ulohy": [
|
||||||
|
57.60463651559811,
|
||||||
|
85.05400883098152
|
||||||
|
],
|
||||||
|
"31-Z1-1": [
|
||||||
|
176,
|
||||||
|
112
|
||||||
|
],
|
||||||
|
"26-Z1-1": [
|
||||||
|
-86.4374292583939,
|
||||||
|
114.1704716787137
|
||||||
|
],
|
||||||
|
"26-Z2-1": [
|
||||||
|
-115,
|
||||||
|
184
|
||||||
|
],
|
||||||
|
"27-Z2-1": [
|
||||||
|
-30,
|
||||||
|
181
|
||||||
|
],
|
||||||
|
"26-Z1-2": [
|
||||||
|
-324,
|
||||||
|
235
|
||||||
|
],
|
||||||
|
"26-Z4-3": [
|
||||||
|
-220.83782675574338,
|
||||||
|
190.72741511636147
|
||||||
|
],
|
||||||
|
"29-Z3-1": [
|
||||||
|
-158,
|
||||||
|
276
|
||||||
|
],
|
||||||
|
"31-Z1-4": [
|
||||||
|
-245,
|
||||||
|
284
|
||||||
|
],
|
||||||
|
"29-Z1-1": [
|
||||||
|
154,
|
||||||
|
199
|
||||||
|
],
|
||||||
|
"29-Z2-1": [
|
||||||
|
259,
|
||||||
|
272
|
||||||
|
],
|
||||||
|
"29-Z4-3": [
|
||||||
|
164,
|
||||||
|
364
|
||||||
|
],
|
||||||
|
"26-Z2-4": [
|
||||||
|
403.46860248688955,
|
||||||
|
23.996420431821353
|
||||||
|
],
|
||||||
|
"29-Z1-3": [
|
||||||
|
-442.93377199520177,
|
||||||
|
-73.3461550905827
|
||||||
|
],
|
||||||
|
"26-Z2-2": [
|
||||||
|
-64.3269017874483,
|
||||||
|
-91.7677899309802
|
||||||
|
],
|
||||||
|
"26-Z3-3": [
|
||||||
|
450.612997715014,
|
||||||
|
-65.45002256579735
|
||||||
|
],
|
||||||
|
"26-Z4-1": [
|
||||||
|
-551.4441557449455,
|
||||||
|
-38.58561957493338
|
||||||
|
],
|
||||||
|
"29-Z3-3": [
|
||||||
|
302.6193712343388,
|
||||||
|
13.535655571354772
|
||||||
|
],
|
||||||
|
"26-Z1-3": [
|
||||||
|
39.68234662392814,
|
||||||
|
-102.30393169322402
|
||||||
|
],
|
||||||
|
"26-Z2-3": [
|
||||||
|
-320.2744362098852,
|
||||||
|
-72.1967458848867
|
||||||
|
],
|
||||||
|
"26-Z3-2": [
|
||||||
|
644.167217052611,
|
||||||
|
-0.4551191547699971
|
||||||
|
],
|
||||||
|
"29-Z3-2": [
|
||||||
|
-368.51198400620785,
|
||||||
|
1.6854832115582556
|
||||||
|
],
|
||||||
|
"26-Z1-4": [
|
||||||
|
147.37372961796666,
|
||||||
|
-89.40554252368531
|
||||||
|
],
|
||||||
|
"26-Z4-4": [
|
||||||
|
335.75328660449225,
|
||||||
|
-81.50932588628959
|
||||||
|
],
|
||||||
|
"26-Z3-4": [
|
||||||
|
-480.95284944300494,
|
||||||
|
29.587091058893556
|
||||||
|
],
|
||||||
|
"29-Z1-4": [
|
||||||
|
554.2061258687584,
|
||||||
|
-44.42093615098819
|
||||||
|
],
|
||||||
|
"31-Z1-2": [
|
||||||
|
-236.06692326455527,
|
||||||
|
-77.65095973572805
|
||||||
|
],
|
||||||
|
"31-Z3-2": [
|
||||||
|
-154.07095173801807,
|
||||||
|
-65.44506844403092
|
||||||
|
],
|
||||||
|
"31-Z3-3": [
|
||||||
|
514.5773720938831,
|
||||||
|
41.05028681292239
|
||||||
|
],
|
||||||
|
"26-Z4-2": [
|
||||||
|
-633.6757896792913,
|
||||||
|
1.4284188628810082
|
||||||
|
],
|
||||||
|
"26-Z3-1": [
|
||||||
|
234.170971842641,
|
||||||
|
-59.230241172550606
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in a new issue