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:
+312
File diff suppressed because one or more lines are too long
-312
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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>
|
||||
|
||||
@@ -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]) => (
|
||||
|
||||
Reference in New Issue
Block a user