( documents: GraphApiDocument[], draggingNodeId: string | null, canvasWidth: number, canvasHeight: number, colors: GraphThemeColors, )
| 456 | } |
| 457 | |
| 458 | export function useGraphData( |
| 459 | documents: GraphApiDocument[], |
| 460 | draggingNodeId: string | null, |
| 461 | canvasWidth: number, |
| 462 | canvasHeight: number, |
| 463 | colors: GraphThemeColors, |
| 464 | ) { |
| 465 | const nodeCache = useRef<Map<string, GraphNode>>(new Map()) |
| 466 | |
| 467 | const graphData = useMemo<{ |
| 468 | nodes: GraphNode[] |
| 469 | cache: Map<string, GraphNode> |
| 470 | }>(() => { |
| 471 | if (!documents || documents.length === 0) { |
| 472 | return { nodes: [], cache: new Map<string, GraphNode>() } |
| 473 | } |
| 474 | |
| 475 | const currentIds = new Set<string>() |
| 476 | for (const doc of documents) { |
| 477 | currentIds.add(doc.id) |
| 478 | for (const mem of doc.memories) currentIds.add(mem.id) |
| 479 | } |
| 480 | |
| 481 | const previousCache = nodeCache.current |
| 482 | const nextCache = new Map<string, GraphNode>() |
| 483 | const appendPlacementNodes = Array.from(previousCache.values()).filter( |
| 484 | (node) => currentIds.has(node.id), |
| 485 | ) |
| 486 | const shouldAppendNewNodes = appendPlacementNodes.length > 0 |
| 487 | const appendBaseBounds = shouldAppendNewNodes |
| 488 | ? getNodeBounds(appendPlacementNodes) |
| 489 | : null |
| 490 | const appendSpatialGrid = |
| 491 | shouldAppendNewNodes && appendBaseBounds |
| 492 | ? buildAppendSpatialGrid(appendPlacementNodes) |
| 493 | : null |
| 494 | let appendIndex = 0 |
| 495 | const clusterAssignments = computeClusterAssignments(documents) |
| 496 | |
| 497 | const result: GraphNode[] = [] |
| 498 | // Spiral layout: documents form a compact spiral core, memories orbit |
| 499 | // around their parent documents. The force simulation then gently |
| 500 | // pushes memories outward to create the constellation/starburst effect. |
| 501 | const cx = canvasWidth / 2 |
| 502 | const cy = canvasHeight / 2 |
| 503 | const docCount = documents.length |
| 504 | // Wide spiral so documents start well-separated. The simulation |
| 505 | // refines positions but the initial spread prevents clustering. |
| 506 | const spiralScale = Math.sqrt(docCount) * 60 |
| 507 | // Golden angle (~137.5 deg) produces optimal packing in a spiral |
| 508 | const goldenAngle = Math.PI * (3 - Math.sqrt(5)) |
| 509 | |
| 510 | for (let docIdx = 0; docIdx < docCount; docIdx++) { |
| 511 | const doc = documents[docIdx] |
| 512 | const docCluster = getDocumentClusterAssignment(doc, clusterAssignments) |
| 513 | const angle = docIdx * goldenAngle |
| 514 | const radius = spiralScale * Math.sqrt((docIdx + 1) / docCount) |
| 515 | const initialX = cx + Math.cos(angle) * radius |
no test coverage detected
searching dependent graphs…