Feat: Graph-Knoten verschieben (Drag) + Neu-anordnen-Button

Sigma-Drag-Muster (downNode/mousemovebody/mouseup) → Knoten frei ziehen; 'Neu anordnen' rechnet das
ForceAtlas2-Layout neu (aufräumen nach manuellem Verschieben).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-28 11:19:53 +02:00
parent de3c452f20
commit 88661545b4
7 changed files with 354 additions and 320 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-BizBt9ex.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-pLGAPRKv.css">
<script type="module" crossorigin src="/assets/index-CWx_Bgrw.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BFWF7uFR.css">
</head>
<body>
<div id="root"></div>
+37 -3
View File
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"
import Graph from "graphology"
import forceAtlas2 from "graphology-layout-forceatlas2"
import Sigma from "sigma"
import { Trash2, Sparkles, Share2 } from "lucide-react"
import { Trash2, Sparkles, Share2, RefreshCw } from "lucide-react"
import { type MemoryGraph } from "@/lib/api"
// Sigma.js v3 + graphology: graph-optimiertes WebGL (kein three.js-Ballast) → skaliert auf tausende
@@ -80,11 +80,33 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
},
})
sigmaRef.current = renderer
let dragged: string | null = null // gerade gezogener Knoten (für Hover/Drag-Logik)
renderer.on("clickNode", ({ node }) => setSelected(node))
renderer.on("clickStage", () => setSelected(null))
renderer.on("enterNode", ({ node }) => { active.current = node; renderer.refresh(); containerRef.current!.style.cursor = "pointer" })
renderer.on("leaveNode", () => { active.current = selectedRef.current; renderer.refresh(); containerRef.current!.style.cursor = "default" })
renderer.on("enterNode", ({ node }) => { if (!dragged) { active.current = node; renderer.refresh() } containerRef.current!.style.cursor = dragged ? "grabbing" : "grab" })
renderer.on("leaveNode", () => { if (!dragged) { active.current = selectedRef.current; renderer.refresh(); containerRef.current!.style.cursor = "default" } })
// Knoten ziehen (Sigma hat kein eingebautes Dragging → Standard-Muster mit dem Mouse-Captor).
renderer.on("downNode", (e) => {
dragged = e.node
graph.setNodeAttribute(dragged, "highlighted", true)
if (!renderer.getCustomBBox()) renderer.setCustomBBox(renderer.getBBox())
containerRef.current!.style.cursor = "grabbing"
})
const mc = renderer.getMouseCaptor()
mc.on("mousemovebody", (e) => {
if (!dragged) return
const pos = renderer.viewportToGraph(e)
graph.setNodeAttribute(dragged, "x", pos.x)
graph.setNodeAttribute(dragged, "y", pos.y)
e.preventSigmaDefault(); e.original.preventDefault(); e.original.stopPropagation()
})
mc.on("mouseup", () => {
if (dragged) graph.removeNodeAttribute(dragged, "highlighted")
dragged = null
if (containerRef.current) containerRef.current.style.cursor = "default"
})
return () => { renderer.kill(); sigmaRef.current = null; graphRef.current = null }
}, [data, degree])
@@ -96,6 +118,14 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
sigmaRef.current?.refresh()
}, [selected])
// Layout neu berechnen (nach manuellem Verschieben „aufräumen").
const relayout = () => {
const g = graphRef.current, s = sigmaRef.current
if (!g || !s) return
forceAtlas2.assign(g, { iterations: 220, settings: forceAtlas2.inferSettings(g) })
s.refresh()
}
const sel = selected ? data.nodes.find((n) => n.id === selected) ?? null : null
const neighbors = useMemo(() => {
if (!selected) return []
@@ -119,6 +149,10 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
<div className="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-3">
<div className="relative h-[calc(100vh-13rem)] min-h-[560px] rounded-2xl border border-border/60 bg-[#070a0f] overflow-hidden">
<div ref={containerRef} className="absolute inset-0" />
<button onClick={relayout} title="Knoten neu anordnen"
className="absolute right-3 top-3 z-10 flex items-center gap-1.5 rounded-lg border border-border/60 bg-background/50 px-2.5 py-1.5 text-[11px] text-muted-foreground hover:text-foreground backdrop-blur-sm transition-colors">
<RefreshCw className="h-3.5 w-3.5" /> Neu anordnen
</button>
{/* Legende */}
<div className="absolute left-3 bottom-3 flex flex-wrap gap-2 rounded-lg bg-background/40 px-2.5 py-1.5 backdrop-blur-sm">
{Object.entries(CAT_LABEL).map(([k, l]) => (