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="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BizBt9ex.js"></script>
|
<script type="module" crossorigin src="/assets/index-CWx_Bgrw.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-pLGAPRKv.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BFWF7uFR.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"
|
|||||||
import Graph from "graphology"
|
import Graph from "graphology"
|
||||||
import forceAtlas2 from "graphology-layout-forceatlas2"
|
import forceAtlas2 from "graphology-layout-forceatlas2"
|
||||||
import Sigma from "sigma"
|
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"
|
import { type MemoryGraph } from "@/lib/api"
|
||||||
|
|
||||||
// Sigma.js v3 + graphology: graph-optimiertes WebGL (kein three.js-Ballast) → skaliert auf tausende
|
// 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
|
sigmaRef.current = renderer
|
||||||
|
let dragged: string | null = null // gerade gezogener Knoten (für Hover/Drag-Logik)
|
||||||
|
|
||||||
renderer.on("clickNode", ({ node }) => setSelected(node))
|
renderer.on("clickNode", ({ node }) => setSelected(node))
|
||||||
renderer.on("clickStage", () => setSelected(null))
|
renderer.on("clickStage", () => setSelected(null))
|
||||||
renderer.on("enterNode", ({ node }) => { active.current = node; renderer.refresh(); containerRef.current!.style.cursor = "pointer" })
|
renderer.on("enterNode", ({ node }) => { if (!dragged) { active.current = node; renderer.refresh() } containerRef.current!.style.cursor = dragged ? "grabbing" : "grab" })
|
||||||
renderer.on("leaveNode", () => { active.current = selectedRef.current; renderer.refresh(); containerRef.current!.style.cursor = "default" })
|
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 }
|
return () => { renderer.kill(); sigmaRef.current = null; graphRef.current = null }
|
||||||
}, [data, degree])
|
}, [data, degree])
|
||||||
@@ -96,6 +118,14 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
|
|||||||
sigmaRef.current?.refresh()
|
sigmaRef.current?.refresh()
|
||||||
}, [selected])
|
}, [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 sel = selected ? data.nodes.find((n) => n.id === selected) ?? null : null
|
||||||
const neighbors = useMemo(() => {
|
const neighbors = useMemo(() => {
|
||||||
if (!selected) return []
|
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="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 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" />
|
<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 */}
|
{/* 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">
|
<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]) => (
|
{Object.entries(CAT_LABEL).map(([k, l]) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user