mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-19 16:13:19 +00:00
updated rendering to hybrid: continuous when simulation active, change-based when idle
This commit is contained in:
parent
b7a20093bf
commit
db0f74110a
6 changed files with 400 additions and 82 deletions
|
|
@ -41,6 +41,7 @@ export const GraphCanvas = memo<GraphCanvasProps>(
|
|||
onTouchEnd,
|
||||
draggingNodeId,
|
||||
highlightDocumentIds,
|
||||
isSimulationActive = false,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const animationRef = useRef<number>(0)
|
||||
|
|
@ -188,8 +189,15 @@ export const GraphCanvas = memo<GraphCanvasProps>(
|
|||
// Draw enhanced edges with sophisticated styling
|
||||
ctx.lineCap = "round"
|
||||
edges.forEach((edge) => {
|
||||
const sourceNode = nodeMap.get(edge.source)
|
||||
const targetNode = nodeMap.get(edge.target)
|
||||
// Handle both string IDs and node references (d3-force mutates these)
|
||||
const sourceNode =
|
||||
typeof edge.source === "string"
|
||||
? nodeMap.get(edge.source)
|
||||
: edge.source
|
||||
const targetNode =
|
||||
typeof edge.target === "string"
|
||||
? nodeMap.get(edge.target)
|
||||
: edge.target
|
||||
|
||||
if (sourceNode && targetNode) {
|
||||
const sourceX = sourceNode.x * zoom + panX
|
||||
|
|
@ -604,7 +612,7 @@ export const GraphCanvas = memo<GraphCanvasProps>(
|
|||
ctx.globalAlpha = 1
|
||||
}, [nodes, edges, panX, panY, zoom, width, height, highlightDocumentIds])
|
||||
|
||||
// Change-based rendering instead of continuous animation
|
||||
// Hybrid rendering: continuous when simulation active, change-based when idle
|
||||
const lastRenderParams = useRef<string>("")
|
||||
|
||||
// Create a render key that changes when visual state changes
|
||||
|
|
@ -628,13 +636,28 @@ export const GraphCanvas = memo<GraphCanvasProps>(
|
|||
highlightDocumentIds,
|
||||
])
|
||||
|
||||
// Only render when something actually changed
|
||||
// Render based on simulation state
|
||||
useEffect(() => {
|
||||
if (isSimulationActive) {
|
||||
// Continuous rendering during physics simulation
|
||||
const renderLoop = () => {
|
||||
render()
|
||||
animationRef.current = requestAnimationFrame(renderLoop)
|
||||
}
|
||||
renderLoop()
|
||||
|
||||
return () => {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Change-based rendering when simulation is idle
|
||||
if (renderKey !== lastRenderParams.current) {
|
||||
lastRenderParams.current = renderKey
|
||||
render()
|
||||
}
|
||||
}, [renderKey, render])
|
||||
}, [isSimulationActive, renderKey, render])
|
||||
|
||||
// Cleanup any existing animation frames
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|||
import { GraphCanvas } from "./graph-canvas"
|
||||
import { useGraphData } from "@/hooks/use-graph-data"
|
||||
import { useGraphInteractions } from "@/hooks/use-graph-interactions"
|
||||
import { useForceSimulation } from "@/hooks/use-force-simulation"
|
||||
import { injectStyles } from "@/lib/inject-styles"
|
||||
import { Legend } from "./legend"
|
||||
import { LoadingIndicator } from "./loading-indicator"
|
||||
|
|
@ -128,6 +129,30 @@ export const MemoryGraph = ({
|
|||
memoryLimit,
|
||||
)
|
||||
|
||||
// State to trigger re-renders when simulation ticks
|
||||
const [, setSimulationTick] = useState(0)
|
||||
|
||||
// Track drag state for physics integration
|
||||
const dragStateRef = useRef<{
|
||||
nodeId: string | null
|
||||
startX: number
|
||||
startY: number
|
||||
nodeStartX: number
|
||||
nodeStartY: number
|
||||
}>({ nodeId: null, startX: 0, startY: 0, nodeStartX: 0, nodeStartY: 0 })
|
||||
|
||||
// Force simulation - only runs during interactions (drag)
|
||||
const forceSimulation = useForceSimulation(
|
||||
nodes,
|
||||
edges,
|
||||
() => {
|
||||
// On each tick, trigger a re-render
|
||||
// D3 directly mutates node.x and node.y
|
||||
setSimulationTick((prev) => prev + 1)
|
||||
},
|
||||
true, // enabled
|
||||
)
|
||||
|
||||
// Auto-fit once per unique highlight set to show the full graph for context
|
||||
const lastFittedHighlightKeyRef = useRef<string>("")
|
||||
useEffect(() => {
|
||||
|
|
@ -240,20 +265,91 @@ export const MemoryGraph = ({
|
|||
}
|
||||
}, [])
|
||||
|
||||
// Enhanced node drag start that includes nodes data
|
||||
// Physics-enabled node drag start
|
||||
const handleNodeDragStartWithNodes = useCallback(
|
||||
(nodeId: string, e: React.MouseEvent) => {
|
||||
// Find the node being dragged
|
||||
const node = nodes.find((n) => n.id === nodeId)
|
||||
if (node) {
|
||||
// Store drag start state
|
||||
dragStateRef.current = {
|
||||
nodeId,
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
nodeStartX: node.x,
|
||||
nodeStartY: node.y,
|
||||
}
|
||||
|
||||
// Pin the node at its current position (d3-force pattern)
|
||||
node.fx = node.x
|
||||
node.fy = node.y
|
||||
|
||||
// Reheat simulation immediately (like d3 reference code)
|
||||
forceSimulation.reheat()
|
||||
}
|
||||
|
||||
// Set dragging state (still need this for visual feedback)
|
||||
handleNodeDragStart(nodeId, e, nodes)
|
||||
},
|
||||
[handleNodeDragStart, nodes],
|
||||
[handleNodeDragStart, nodes, forceSimulation],
|
||||
)
|
||||
|
||||
// Enhanced node drag move that includes nodes data
|
||||
// Physics-enabled node drag move
|
||||
const handleNodeDragMoveWithNodes = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
handleNodeDragMove(e, nodes)
|
||||
if (draggingNodeId && dragStateRef.current.nodeId === draggingNodeId) {
|
||||
// Update the fixed position during drag (this is what d3 uses)
|
||||
const node = nodes.find((n) => n.id === draggingNodeId)
|
||||
if (node) {
|
||||
// Calculate new position based on drag delta
|
||||
const deltaX = (e.clientX - dragStateRef.current.startX) / zoom
|
||||
const deltaY = (e.clientY - dragStateRef.current.startY) / zoom
|
||||
|
||||
// Update subject position (matches d3 reference code pattern)
|
||||
// Only update fx/fy, let simulation handle x/y
|
||||
node.fx = dragStateRef.current.nodeStartX + deltaX
|
||||
node.fy = dragStateRef.current.nodeStartY + deltaY
|
||||
}
|
||||
}
|
||||
},
|
||||
[handleNodeDragMove, nodes],
|
||||
[nodes, draggingNodeId, zoom],
|
||||
)
|
||||
|
||||
// Physics-enabled node drag end
|
||||
const handleNodeDragEndWithPhysics = useCallback(() => {
|
||||
if (draggingNodeId) {
|
||||
// Unpin the node (allow physics to take over) - matches d3 reference code
|
||||
const node = nodes.find((n) => n.id === draggingNodeId)
|
||||
if (node) {
|
||||
node.fx = null
|
||||
node.fy = null
|
||||
}
|
||||
|
||||
// Cool down the simulation (restore target alpha to 0)
|
||||
forceSimulation.coolDown()
|
||||
|
||||
// Reset drag state
|
||||
dragStateRef.current = {
|
||||
nodeId: null,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
nodeStartX: 0,
|
||||
nodeStartY: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Call original handler to clear dragging state
|
||||
handleNodeDragEnd()
|
||||
}, [draggingNodeId, nodes, forceSimulation, handleNodeDragEnd])
|
||||
|
||||
// Physics-aware node click - let simulation continue naturally
|
||||
const handleNodeClickWithPhysics = useCallback(
|
||||
(nodeId: string) => {
|
||||
// Just call original handler to update selected node state
|
||||
// Don't stop the simulation - let it cool down naturally
|
||||
handleNodeClick(nodeId)
|
||||
},
|
||||
[handleNodeClick],
|
||||
)
|
||||
|
||||
// Navigation callbacks
|
||||
|
|
@ -460,9 +556,10 @@ export const MemoryGraph = ({
|
|||
height={containerSize.height}
|
||||
nodes={nodes}
|
||||
highlightDocumentIds={highlightsVisible ? highlightDocumentIds : []}
|
||||
isSimulationActive={forceSimulation.isActive()}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onNodeClick={handleNodeClick}
|
||||
onNodeDragEnd={handleNodeDragEnd}
|
||||
onNodeClick={handleNodeClickWithPhysics}
|
||||
onNodeDragEnd={handleNodeDragEndWithPhysics}
|
||||
onNodeDragMove={handleNodeDragMoveWithNodes}
|
||||
onNodeDragStart={handleNodeDragStartWithNodes}
|
||||
onNodeHover={handleNodeHover}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,32 @@ export const LAYOUT_CONSTANTS = {
|
|||
memoryClusterRadius: 300,
|
||||
}
|
||||
|
||||
// D3-Force simulation configuration
|
||||
export const FORCE_CONFIG = {
|
||||
// Link force (spring between connected nodes)
|
||||
linkStrength: {
|
||||
docMemory: 0.8, // Strong for doc-memory connections
|
||||
version: 1.0, // Strongest for version chains
|
||||
docDocBase: 0.3, // Base for doc-doc similarity
|
||||
},
|
||||
linkDistance: 300, // Desired spring length
|
||||
|
||||
// Charge force (repulsion between nodes)
|
||||
chargeStrength: -1000, // Negative = repulsion, higher magnitude = stronger push
|
||||
|
||||
// Collision force (prevents node overlap)
|
||||
collisionRadius: {
|
||||
document: 80, // Collision radius for document nodes
|
||||
memory: 40, // Collision radius for memory nodes
|
||||
},
|
||||
|
||||
// Simulation behavior
|
||||
alphaDecay: 0.03, // How fast simulation cools down (higher = faster cooldown)
|
||||
alphaMin: 0.001, // Threshold to stop simulation (when alpha drops below this)
|
||||
velocityDecay: 0.6, // Friction/damping (0 = no friction, 1 = instant stop) - increased for less movement
|
||||
alphaTarget: 0.3, // Target alpha when reheating (on drag start)
|
||||
}
|
||||
|
||||
// Graph view settings
|
||||
export const GRAPH_SETTINGS = {
|
||||
console: {
|
||||
|
|
|
|||
177
packages/memory-graph/src/hooks/use-force-simulation.ts
Normal file
177
packages/memory-graph/src/hooks/use-force-simulation.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useRef, useCallback } from "react"
|
||||
import * as d3 from "d3-force"
|
||||
import { FORCE_CONFIG } from "@/constants"
|
||||
import type { GraphNode, GraphEdge } from "@/types"
|
||||
|
||||
export interface ForceSimulationControls {
|
||||
/** The d3 simulation instance */
|
||||
simulation: d3.Simulation<GraphNode, GraphEdge> | null
|
||||
/** Reheat the simulation (call on drag start) */
|
||||
reheat: () => void
|
||||
/** Cool down the simulation (call on drag end) */
|
||||
coolDown: () => void
|
||||
/** Check if simulation is currently active */
|
||||
isActive: () => boolean
|
||||
/** Stop the simulation completely */
|
||||
stop: () => void
|
||||
/** Get current alpha value */
|
||||
getAlpha: () => number
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to manage d3-force simulation lifecycle
|
||||
* Simulation only runs during interactions (drag) for performance
|
||||
*/
|
||||
export function useForceSimulation(
|
||||
nodes: GraphNode[],
|
||||
edges: GraphEdge[],
|
||||
onTick: (nodes: GraphNode[]) => void,
|
||||
enabled = true,
|
||||
): ForceSimulationControls {
|
||||
const simulationRef = useRef<d3.Simulation<GraphNode, GraphEdge> | null>(null)
|
||||
|
||||
// Initialize simulation ONCE
|
||||
useEffect(() => {
|
||||
if (!enabled || nodes.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Only create simulation once
|
||||
if (!simulationRef.current) {
|
||||
const simulation = d3
|
||||
.forceSimulation<GraphNode>(nodes)
|
||||
.alphaDecay(FORCE_CONFIG.alphaDecay)
|
||||
.alphaMin(FORCE_CONFIG.alphaMin)
|
||||
.velocityDecay(FORCE_CONFIG.velocityDecay)
|
||||
.on("tick", () => {
|
||||
// Trigger re-render by calling onTick
|
||||
// D3 has already mutated node.x and node.y
|
||||
onTick([...nodes])
|
||||
})
|
||||
|
||||
// Configure forces
|
||||
// 1. Link force - spring connections between nodes
|
||||
simulation.force(
|
||||
"link",
|
||||
d3
|
||||
.forceLink<GraphNode, GraphEdge>(edges)
|
||||
.id((d) => d.id)
|
||||
.distance(FORCE_CONFIG.linkDistance)
|
||||
.strength((link) => {
|
||||
// Different strength based on edge type
|
||||
if (link.edgeType === "doc-memory") {
|
||||
return FORCE_CONFIG.linkStrength.docMemory
|
||||
}
|
||||
if (link.edgeType === "version") {
|
||||
return FORCE_CONFIG.linkStrength.version
|
||||
}
|
||||
// doc-doc: variable strength based on similarity
|
||||
return link.similarity * FORCE_CONFIG.linkStrength.docDocBase
|
||||
}),
|
||||
)
|
||||
|
||||
// 2. Charge force - repulsion between nodes
|
||||
simulation.force(
|
||||
"charge",
|
||||
d3.forceManyBody<GraphNode>().strength(FORCE_CONFIG.chargeStrength),
|
||||
)
|
||||
|
||||
// 3. Collision force - prevent node overlap
|
||||
simulation.force(
|
||||
"collide",
|
||||
d3
|
||||
.forceCollide<GraphNode>()
|
||||
.radius((d) =>
|
||||
d.type === "document"
|
||||
? FORCE_CONFIG.collisionRadius.document
|
||||
: FORCE_CONFIG.collisionRadius.memory,
|
||||
)
|
||||
.strength(0.7),
|
||||
)
|
||||
|
||||
// 4. forceX and forceY - weak centering forces (like reference code)
|
||||
simulation.force("x", d3.forceX().strength(0.05))
|
||||
simulation.force("y", d3.forceY().strength(0.05))
|
||||
|
||||
// Store reference
|
||||
simulationRef.current = simulation
|
||||
|
||||
// Stop simulation immediately after creation
|
||||
// It will only run when explicitly reheated (on drag)
|
||||
simulation.stop()
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
if (simulationRef.current) {
|
||||
simulationRef.current.stop()
|
||||
simulationRef.current = null
|
||||
}
|
||||
}
|
||||
// Only run on mount/unmount, not when nodes/edges/onTick change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [enabled])
|
||||
|
||||
// Update simulation nodes when they change
|
||||
useEffect(() => {
|
||||
if (simulationRef.current && nodes.length > 0) {
|
||||
simulationRef.current.nodes(nodes)
|
||||
}
|
||||
}, [nodes])
|
||||
|
||||
// Update simulation edges when they change
|
||||
useEffect(() => {
|
||||
if (simulationRef.current && edges.length > 0) {
|
||||
const linkForce = simulationRef.current.force<
|
||||
d3.ForceLink<GraphNode, GraphEdge>
|
||||
>("link")
|
||||
if (linkForce) {
|
||||
linkForce.links(edges)
|
||||
}
|
||||
}
|
||||
}, [edges])
|
||||
|
||||
// Reheat simulation (called on drag start)
|
||||
const reheat = useCallback(() => {
|
||||
if (simulationRef.current) {
|
||||
simulationRef.current.alphaTarget(FORCE_CONFIG.alphaTarget).restart()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Cool down simulation (called on drag end)
|
||||
const coolDown = useCallback(() => {
|
||||
if (simulationRef.current) {
|
||||
simulationRef.current.alphaTarget(0)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Check if simulation is active
|
||||
const isActive = useCallback(() => {
|
||||
if (!simulationRef.current) return false
|
||||
return simulationRef.current.alpha() > FORCE_CONFIG.alphaMin
|
||||
}, [])
|
||||
|
||||
// Stop simulation completely
|
||||
const stop = useCallback(() => {
|
||||
if (simulationRef.current) {
|
||||
simulationRef.current.stop()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Get current alpha
|
||||
const getAlpha = useCallback(() => {
|
||||
if (!simulationRef.current) return 0
|
||||
return simulationRef.current.alpha()
|
||||
}, [])
|
||||
|
||||
return {
|
||||
simulation: simulationRef.current,
|
||||
reheat,
|
||||
coolDown,
|
||||
isActive,
|
||||
stop,
|
||||
getAlpha,
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import {
|
|||
getConnectionVisualProps,
|
||||
getMagicalConnectionColor,
|
||||
} from "@/lib/similarity"
|
||||
import { useMemo } from "react"
|
||||
import { useMemo, useRef } from "react"
|
||||
import { colors, LAYOUT_CONSTANTS } from "@/constants"
|
||||
import type {
|
||||
DocumentsResponse,
|
||||
|
|
@ -23,6 +23,9 @@ export function useGraphData(
|
|||
draggingNodeId: string | null,
|
||||
memoryLimit?: number,
|
||||
) {
|
||||
// Cache nodes to preserve d3-force mutations (x, y, vx, vy, fx, fy)
|
||||
const nodeCache = useRef<Map<string, GraphNode>>(new Map())
|
||||
|
||||
return useMemo(() => {
|
||||
if (!data?.documents) return { nodes: [], edges: [] }
|
||||
|
||||
|
|
@ -115,65 +118,38 @@ export function useGraphData(
|
|||
|
||||
const customPos = nodePositions.get(doc.id)
|
||||
|
||||
documentNodes.push({
|
||||
id: doc.id,
|
||||
type: "document",
|
||||
x: customPos?.x ?? defaultX,
|
||||
y: customPos?.y ?? defaultY,
|
||||
data: doc,
|
||||
size: 58,
|
||||
color: colors.document.primary,
|
||||
isHovered: false,
|
||||
isDragging: draggingNodeId === doc.id,
|
||||
} satisfies GraphNode)
|
||||
// Check if node exists in cache (preserves d3-force mutations)
|
||||
let node = nodeCache.current.get(doc.id)
|
||||
if (node) {
|
||||
// Update existing node's data, preserve physics properties (x, y, vx, vy, fx, fy)
|
||||
node.data = doc
|
||||
node.isDragging = draggingNodeId === doc.id
|
||||
// Don't reset x/y - they're managed by d3-force
|
||||
} else {
|
||||
// Create new node with initial position
|
||||
node = {
|
||||
id: doc.id,
|
||||
type: "document",
|
||||
x: customPos?.x ?? defaultX,
|
||||
y: customPos?.y ?? defaultY,
|
||||
data: doc,
|
||||
size: 58,
|
||||
color: colors.document.primary,
|
||||
isHovered: false,
|
||||
isDragging: draggingNodeId === doc.id,
|
||||
} satisfies GraphNode
|
||||
nodeCache.current.set(doc.id, node)
|
||||
}
|
||||
|
||||
documentNodes.push(node)
|
||||
})
|
||||
|
||||
spaceIndex++
|
||||
})
|
||||
|
||||
/* 2. Gentle document collision avoidance with dampening */
|
||||
const minDocDist = LAYOUT_CONSTANTS.minDocDist
|
||||
|
||||
// Reduced iterations and gentler repulsion for smoother movement
|
||||
for (let iter = 0; iter < 2; iter++) {
|
||||
documentNodes.forEach((nodeA) => {
|
||||
documentNodes.forEach((nodeB) => {
|
||||
if (nodeA.id >= nodeB.id) return
|
||||
|
||||
// Only repel documents in the same space
|
||||
const spaceA =
|
||||
(nodeA.data as DocumentWithMemories).memoryEntries[0]
|
||||
?.spaceContainerTag ??
|
||||
(nodeA.data as DocumentWithMemories).memoryEntries[0]?.spaceId ??
|
||||
"default"
|
||||
const spaceB =
|
||||
(nodeB.data as DocumentWithMemories).memoryEntries[0]
|
||||
?.spaceContainerTag ??
|
||||
(nodeB.data as DocumentWithMemories).memoryEntries[0]?.spaceId ??
|
||||
"default"
|
||||
|
||||
if (spaceA !== spaceB) return
|
||||
|
||||
const dx = nodeB.x - nodeA.x
|
||||
const dy = nodeB.y - nodeA.y
|
||||
const dist = Math.sqrt(dx * dx + dy * dy) || 1
|
||||
|
||||
if (dist < minDocDist) {
|
||||
// Much gentler push with dampening
|
||||
const push = (minDocDist - dist) / 8
|
||||
const dampening = Math.max(0.1, Math.min(1, dist / minDocDist))
|
||||
const smoothPush = push * dampening * 0.5
|
||||
|
||||
const nx = dx / dist
|
||||
const ny = dy / dist
|
||||
nodeA.x -= nx * smoothPush
|
||||
nodeA.y -= ny * smoothPush
|
||||
nodeB.x += nx * smoothPush
|
||||
nodeB.y += ny * smoothPush
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
/* 2. Manual collision avoidance removed - now handled by d3-force simulation */
|
||||
// The initial circular layout provides good starting positions
|
||||
// D3-force will handle collision avoidance and spacing dynamically
|
||||
|
||||
allNodes.push(...documentNodes)
|
||||
|
||||
|
|
@ -220,19 +196,30 @@ export function useGraphData(
|
|||
}
|
||||
|
||||
if (!memoryNodeMap.has(memoryId)) {
|
||||
const memoryNode: GraphNode = {
|
||||
id: memoryId,
|
||||
type: "memory",
|
||||
x: finalMemX,
|
||||
y: finalMemY,
|
||||
data: memory,
|
||||
size: Math.max(
|
||||
32,
|
||||
Math.min(48, (memory.memory?.length || 50) * 0.5),
|
||||
),
|
||||
color: colors.memory.primary,
|
||||
isHovered: false,
|
||||
isDragging: draggingNodeId === memoryId,
|
||||
// Check if memory node exists in cache (preserves d3-force mutations)
|
||||
let memoryNode = nodeCache.current.get(memoryId)
|
||||
if (memoryNode) {
|
||||
// Update existing node's data, preserve physics properties
|
||||
memoryNode.data = memory
|
||||
memoryNode.isDragging = draggingNodeId === memoryId
|
||||
// Don't reset x/y - they're managed by d3-force
|
||||
} else {
|
||||
// Create new node with initial position
|
||||
memoryNode = {
|
||||
id: memoryId,
|
||||
type: "memory",
|
||||
x: finalMemX,
|
||||
y: finalMemY,
|
||||
data: memory,
|
||||
size: Math.max(
|
||||
32,
|
||||
Math.min(48, (memory.memory?.length || 50) * 0.5),
|
||||
),
|
||||
color: colors.memory.primary,
|
||||
isHovered: false,
|
||||
isDragging: draggingNodeId === memoryId,
|
||||
}
|
||||
nodeCache.current.set(memoryId, memoryNode)
|
||||
}
|
||||
memoryNodeMap.set(memoryId, memoryNode)
|
||||
allNodes.push(memoryNode)
|
||||
|
|
|
|||
|
|
@ -17,14 +17,20 @@ export interface GraphNode {
|
|||
color: string
|
||||
isHovered: boolean
|
||||
isDragging: boolean
|
||||
// D3-force simulation properties
|
||||
vx?: number // velocity x
|
||||
vy?: number // velocity y
|
||||
fx?: number | null // fixed x position (for pinning during drag)
|
||||
fy?: number | null // fixed y position (for pinning during drag)
|
||||
}
|
||||
|
||||
export type MemoryRelation = "updates" | "extends" | "derives"
|
||||
|
||||
export interface GraphEdge {
|
||||
id: string
|
||||
source: string
|
||||
target: string
|
||||
// D3-force mutates source/target from string IDs to node references during simulation
|
||||
source: string | GraphNode
|
||||
target: string | GraphNode
|
||||
similarity: number
|
||||
visualProps: {
|
||||
opacity: number
|
||||
|
|
@ -74,6 +80,8 @@ export interface GraphCanvasProps {
|
|||
draggingNodeId: string | null
|
||||
// Optional list of document IDs (customId or internal id) to highlight
|
||||
highlightDocumentIds?: string[]
|
||||
// Physics simulation state
|
||||
isSimulationActive?: boolean
|
||||
}
|
||||
|
||||
export interface MemoryGraphProps {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue