Browse Source

graf: přidána funkce pro manuální pozicování vrcholů a perzistence pozice

mj-deploy
Vašek Šraier 4 years ago
parent
commit
a264dbead9
  1. 26
      frontend/src/Editor.svelte
  2. 26
      frontend/src/Graph.svelte
  3. 35
      frontend/src/GraphNode.svelte
  4. 5
      frontend/src/graph-types.ts
  5. 15
      frontend/src/task-loader.ts
  6. 160
      tasks.json

26
frontend/src/Editor.svelte

@ -11,6 +11,7 @@
let clicked: string[] = [];
let graph: Graph;
let currentTask: TaskDescriptor | null = null;
let nodeDraggingEnabled: boolean = false;
function clickTask(e: CustomEvent<TaskDescriptor>) {
// ukladani seznamu poslednich kliknuti
@ -39,6 +40,11 @@
}
}
async function saveCurrentStateWithPositions() {
tasks.positions = graph.getNodePositions();
await saveTasks(tasks);
}
async function saveCurrentState() {
await saveTasks(tasks);
}
@ -106,22 +112,36 @@
{repulsionForce}
on:selectTask={clickTask}
on:preSelectTask={startHovering}
bind:this={graph} />
bind:this={graph}
{nodeDraggingEnabled} />
</div>
</div>
<div class="right">
<div class="toolbox">
<div>Toolbox</div>
<div>
<button disabled={clicked.length <= 1} on:click={addEdge}>Přidat hranu {clicked[clicked.length - 2]} -&gt; {clicked[clicked.length - 1]}</button>
<button disabled={clicked.length <= 1} on:click={addEdge}>Přidat hranu {clicked[clicked.length - 2]}
-&gt; {clicked[clicked.length - 1]}</button>
</div>
<div>
<button on:click={graph.runSimulation}>Spustit simulaci</button>
</div>
<div><button on:click={graph.runSimulation}>Spustit simulaci</button></div>
<div>
Repulsion force: <input type="number" bind:value={repulsionForce} name="repulsionForceInput" max="1000" min="-10000" />
</div>
<div>
<button on:click={saveCurrentState}>Uložit aktuální stav</button>
</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 class="taskDetails">
{#if currentTask != null}

26
frontend/src/Graph.svelte

@ -6,11 +6,12 @@
import type { TasksFile, TaskDescriptor } from "./task-loader";
import { createNodesAndEdges } from "./graph-types";
import { taskForce } from "./task-force";
import { zoom } from "d3";
export let tasks: TasksFile;
let hoveredTask: null | string = null;
export let repulsionForce: number = -1000;
export let nodeDraggingEnabled: boolean = false;
let hoveredTask: null | string = null;
// Svelte automatically fills these with a reference
let container: HTMLElement;
@ -23,7 +24,7 @@
let [nodes, edges] = createNodesAndEdges(tasks);
function hack() {
[nodes, edges] = createNodesAndEdges(tasks, nodes, edges);
runSimulation();
//runSimulation();
}
$: {
tasks;
@ -74,11 +75,17 @@
nodes = nodes;
}
}
const zoomer = d3.zoom().scaleExtent([0.1, 2])
$: {
// zoomer.extent([[-clientWidth / 2,-clientHeight / 2],[clientWidth,clientHeight]])
export function getNodePositions(): Map<string, [number, number]> {
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
onMount(() => {
@ -89,10 +96,9 @@
function zoomed(e) {
svg.attr("transform", e.transform);
}
const zoomer = d3.zoom().scaleExtent([0.1, 2])
zoomer.on("zoom", zoomed);
d3.select(container).call(zoomer);
runSimulation();
});
</script>
@ -129,7 +135,9 @@
{task}
on:taskClick
on:click={nodeClick(task.task)}
on:hoveringChange={nodeHover(task.task)} />
on:hoveringChange={nodeHover(task.task)}
on:positionChange={() => { tasks = tasks; }}
draggingEnabled={nodeDraggingEnabled} />
{/each}
</g>
</g>

35
frontend/src/GraphNode.svelte

@ -1,9 +1,12 @@
<script lang="ts">
import * as d3 from "d3";
import { createEventDispatcher, onMount } from "svelte";
import type { TaskId } from "./graph-types";
export let task: TaskId;
export let draggingEnabled: boolean = false;
let hovering: boolean = false;
let text_element: SVGTextElement;
@ -31,6 +34,34 @@
const bbox = text_element.getBBox();
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>
<style>
@ -45,7 +76,7 @@
}
</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} />
<text
bind:this={text_element}

5
frontend/src/graph-types.ts

@ -21,11 +21,14 @@ function toMapById(nodes: TaskId[]): Map<string, TaskId> {
function createNodes(tasks: TasksFile, old?: TaskId[]): TaskId[] {
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 };
});
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)) {
Object.assign(t, m.get(t.id))
}

15
frontend/src/task-loader.ts

@ -9,20 +9,31 @@ export type TaskDescriptor = {
export type TasksFile = {
tasks: TaskDescriptor[]
clusters: { [name: string]: string[] }
positions: Map<string, [number, number]>
}
export type TaskMap = Map<string, TaskDescriptor>;
export async function loadTasks(): Promise<TasksFile> {
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) {
let p: any = {}
for (let [key, val] of tasks.positions.entries())
p[key] = val;
const data = {...tasks, positions: p}
// request options
const options = {
method: 'POST',
body: JSON.stringify(tasks, null, 4),
body: JSON.stringify(data, null, 4),
headers: {
'Content-Type': 'application/json'
}

160
tasks.json

@ -158,52 +158,62 @@
"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",
"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, ",
"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",
"requires": []
},
{
"id": "26-Z3-4","type": "open-data",
"id": "26-Z3-4",
"type": "open-data",
"comment": "Tvar labyrintu - nejdelší cesta ve stromě, graf",
"requires": []
},
{
"id": "29-Z1-4","type": "open-data",
"id": "29-Z1-4",
"type": "open-data",
"comment": "Zuzčin výlet — DFS (topologické pořadí)",
"requires": []
},
{
"id": "31-Z1-2","type": "open-data",
"id": "31-Z1-2",
"type": "open-data",
"comment": "BFS (šachovnice, custom figurka, nejkratší cesta) ",
"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)",
"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",
"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",
"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",
"requires": []
}
@ -259,5 +269,135 @@
"Nápad": [
"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
]
}
}