Politur: Robustheits-Glow-Graph-Fixes fuer D3-Simulations-Initialisierung und Performance-Optimierung

This commit is contained in:
Hitonabi
2026-07-09 12:18:18 +02:00
parent 9375b8eea8
commit 520cfd9fe4
4 changed files with 50 additions and 30 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,7 +7,7 @@
<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-CnQbeKKt.js"></script> <script type="module" crossorigin src="/assets/index-BrDDv3Dl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-1USExdfg.css"> <link rel="stylesheet" crossorigin href="/assets/index-1USExdfg.css">
</head> </head>
<body> <body>
+41 -21
View File
@@ -10,7 +10,7 @@ const CAT_LABEL: Record<string, string> = {
identity: "Identität", knowledge: "Wissen", rules: "Regeln", events: "Ereignisse", identity: "Identität", knowledge: "Wissen", rules: "Regeln", events: "Ereignisse",
} }
const AUTO = new Set(["auto", "agent", "hermes"]) const AUTO = new Set(["auto", "agent", "hermes"])
const DIM_EDGE = "rgba(15, 23, 42, 0.02)" const DIM_EDGE = "rgba(15, 23, 42, 0.01)"
const EDGE = "rgba(255, 255, 255, 0.05)" const EDGE = "rgba(255, 255, 255, 0.05)"
const EDGE_HI = "rgba(99, 102, 241, 0.85)" const EDGE_HI = "rgba(99, 102, 241, 0.85)"
@@ -27,10 +27,12 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
if (!containerRef.current) return if (!containerRef.current) return
const resizeObserver = new ResizeObserver((entries) => { const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) { for (let entry of entries) {
setDimensions({ if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
width: Math.floor(entry.contentRect.width), setDimensions({
height: Math.floor(entry.contentRect.height), width: Math.floor(entry.contentRect.width),
}) height: Math.floor(entry.contentRect.height),
})
}
} }
}) })
resizeObserver.observe(containerRef.current) resizeObserver.observe(containerRef.current)
@@ -60,10 +62,15 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
// D3 Force Engine tunen (Luftige Struktur, kein Überlappen) // D3 Force Engine tunen (Luftige Struktur, kein Überlappen)
useEffect(() => { useEffect(() => {
if (!fgRef.current) return
const fg = fgRef.current const fg = fgRef.current
fg.d3Force("charge").strength(-160) if (!fg) return
fg.d3Force("link").distance(65)
// Sicherstellen, dass D3-Kräfte existieren, bevor wir aufgerufen werden
const charge = fg.d3Force("charge")
if (charge) charge.strength(-140)
const link = fg.d3Force("link")
if (link) link.distance(60)
}, [graphData]) }, [graphData])
const activeNode = hovered || selected const activeNode = hovered || selected
@@ -75,6 +82,8 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
} }
const sel = selected ? data.nodes.find((n) => n.id === selected) ?? null : null const sel = selected ? data.nodes.find((n) => n.id === selected) ?? null : null
// Nachbarn über das statische, sichere Props-Array ermitteln
const neighbors = useMemo(() => { const neighbors = useMemo(() => {
if (!selected) return [] if (!selected) return []
const ids = new Set<string>() const ids = new Set<string>()
@@ -85,6 +94,17 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
return data.nodes.filter((n) => ids.has(n.id)) return data.nodes.filter((n) => ids.has(n.id))
}, [selected, data]) }, [selected, data])
// Nachbarmenge für extrem schnellen Lookup im Render-Loop
const connectedNodes = useMemo(() => {
if (!activeNode) return new Set<string>()
const set = new Set<string>()
data.edges.forEach((e) => {
if (e.source === activeNode) set.add(e.target)
if (e.target === activeNode) set.add(e.source)
})
return set
}, [activeNode, data])
if (!data.nodes.length) { if (!data.nodes.length) {
return ( return (
<div className="text-xs text-muted-foreground border border-dashed border-border/60 rounded-2xl p-12 text-center"> <div className="text-xs text-muted-foreground border border-dashed border-border/60 rounded-2xl p-12 text-center">
@@ -106,13 +126,16 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
graphData={graphData} graphData={graphData}
backgroundColor="rgba(0,0,0,0)" backgroundColor="rgba(0,0,0,0)"
nodeRelSize={4} nodeRelSize={4}
cooldownTicks={100}
// Hover- und Klick-Logik // Hover- und Klick-Logik
onNodeClick={(node: any) => setSelected(node.id)} onNodeClick={(node: any) => setSelected(node.id)}
onBackgroundClick={() => setSelected(null)} onBackgroundClick={() => setSelected(null)}
onNodeHover={(node: any) => { onNodeHover={(node: any) => {
setHovered(node ? node.id : null) setHovered(node ? node.id : null)
containerRef.current!.style.cursor = node ? "pointer" : "default" if (containerRef.current) {
containerRef.current.style.cursor = node ? "pointer" : "default"
}
}} }}
// Kanten-Animationen & Partikelfluss // Kanten-Animationen & Partikelfluss
@@ -139,18 +162,15 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
// Custom Canvas Rendering für glühende Knoten und 100% lesbare Text-Pillen // Custom Canvas Rendering für glühende Knoten und 100% lesbare Text-Pillen
nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => { nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
const x = node.x ?? 0
const y = node.y ?? 0
const size = 5.5 + Math.min(degree[node.id] || 0, 12) * 0.9 const size = 5.5 + Math.min(degree[node.id] || 0, 12) * 0.9
const color = CAT_COLOR[node.category] || "#64748b" const color = CAT_COLOR[node.category] || "#64748b"
const isHovered = node.id === hovered const isHovered = node.id === hovered
const isSelected = node.id === selected const isSelected = node.id === selected
// Abgedunkelter Zustand, wenn ein anderer Knoten aktiv ist // Abgedunkelter Zustand, wenn ein anderer Knoten aktiv ist und dieser kein Nachbar ist
const isDimmed = activeNode && node.id !== activeNode && !fgRef.current?.getGraphData()?.links.some((l: any) => { const isDimmed = activeNode && node.id !== activeNode && !connectedNodes.has(node.id)
const sId = typeof l.source === "object" ? l.source.id : l.source
const tId = typeof l.target === "object" ? l.target.id : l.target
return (sId === activeNode && tId === node.id) || (tId === activeNode && sId === node.id)
})
const finalColor = isDimmed ? "#1e293b" : color const finalColor = isDimmed ? "#1e293b" : color
// 1. Zeichne weichen Glow-Schatten // 1. Zeichne weichen Glow-Schatten
@@ -161,7 +181,7 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
// 2. Zeichne den Knoten-Kreis // 2. Zeichne den Knoten-Kreis
ctx.beginPath() ctx.beginPath()
ctx.arc(node.x, node.y, size, 0, 2 * Math.PI, false) ctx.arc(x, y, size, 0, 2 * Math.PI, false)
ctx.fillStyle = finalColor ctx.fillStyle = finalColor
ctx.fill() ctx.fill()
ctx.shadowBlur = 0 // Schatten zurücksetzen ctx.shadowBlur = 0 // Schatten zurücksetzen
@@ -169,7 +189,7 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
// 3. Zeichne weißen Außenring bei Interaktivität // 3. Zeichne weißen Außenring bei Interaktivität
if (isHovered || isSelected) { if (isHovered || isSelected) {
ctx.beginPath() ctx.beginPath()
ctx.arc(node.x, node.y, size + 2, 0, 2 * Math.PI, false) ctx.arc(x, y, size + 2, 0, 2 * Math.PI, false)
ctx.strokeStyle = isSelected ? "#ffffff" : "rgba(255, 255, 255, 0.7)" ctx.strokeStyle = isSelected ? "#ffffff" : "rgba(255, 255, 255, 0.7)"
ctx.lineWidth = 1.5 ctx.lineWidth = 1.5
ctx.stroke() ctx.stroke()
@@ -189,8 +209,8 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
ctx.beginPath() ctx.beginPath()
// Zeichne abgerundete Pille unter dem Knoten // Zeichne abgerundete Pille unter dem Knoten
const rx = node.x - textWidth / 2 - bpad const rx = x - textWidth / 2 - bpad
const ry = node.y + size + (5 / Math.max(0.6, globalScale)) const ry = y + size + (5 / Math.max(0.6, globalScale))
const rw = textWidth + bpad * 2 const rw = textWidth + bpad * 2
const rh = fontSize + bpad * 1.5 const rh = fontSize + bpad * 1.5
const rad = 4 / Math.max(0.6, globalScale) const rad = 4 / Math.max(0.6, globalScale)
@@ -220,7 +240,7 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
? "#475569" ? "#475569"
: (isHovered || isSelected ? "#ffffff" : "#cbd5e1") : (isHovered || isSelected ? "#ffffff" : "#cbd5e1")
ctx.fillText(label, node.x, node.y + size + (6 / Math.max(0.6, globalScale))) ctx.fillText(label, x, y + size + (6 / Math.max(0.6, globalScale)))
}} }}
/> />