diff --git a/packages/memory-graph/src/__tests__/spatial-index.test.ts b/packages/memory-graph/src/__tests__/spatial-index.test.ts index 46d7beab..23f72f09 100644 --- a/packages/memory-graph/src/__tests__/spatial-index.test.ts +++ b/packages/memory-graph/src/__tests__/spatial-index.test.ts @@ -54,6 +54,33 @@ describe("SpatialIndex", () => { expect(result).toBe(true) }) + it("hash guard keeps stale objects for a new generation at the same positions", () => { + const idx = new SpatialIndex() + const genA = [makeNode("a", 100, 100)] + idx.rebuild(genA) + + const genB = [makeNode("a", 100, 100)] + expect(idx.rebuild(genB)).toBe(false) + expect(idx.queryPoint(100, 100)).toBe(genA[0]) + }) + + it("force rebuild swaps the grid to the new node generation", () => { + const idx = new SpatialIndex() + const genA = [makeNode("a", 100, 100)] + idx.rebuild(genA) + + const genB = [makeNode("a", 100, 100)] + expect(idx.rebuild(genB, true)).toBe(true) + expect(idx.queryPoint(100, 100)).toBe(genB[0]) + }) + + it("force rebuild still refreshes the hash for subsequent unforced calls", () => { + const idx = new SpatialIndex() + const nodes = [makeNode("a", 100, 100)] + idx.rebuild(nodes, true) + expect(idx.rebuild(nodes)).toBe(false) + }) + it("rebuild detects sub-pixel movements (10x granularity)", () => { const idx = new SpatialIndex() const nodes = [makeNode("a", 100.0, 100.0)] diff --git a/packages/memory-graph/src/canvas/hit-test.ts b/packages/memory-graph/src/canvas/hit-test.ts index ac20bca7..7f4d8bd6 100644 --- a/packages/memory-graph/src/canvas/hit-test.ts +++ b/packages/memory-graph/src/canvas/hit-test.ts @@ -5,9 +5,10 @@ export class SpatialIndex { private cellSize = 200 private lastHash = 0 - rebuild(nodes: GraphNode[]): boolean { + // Hash only covers ids/positions, so pass force when the node array identity changes. + rebuild(nodes: GraphNode[], force = false): boolean { const hash = this.computeHash(nodes) - if (hash === this.lastHash) return false + if (!force && hash === this.lastHash) return false this.lastHash = hash this.grid.clear() diff --git a/packages/memory-graph/src/components/graph-canvas.tsx b/packages/memory-graph/src/components/graph-canvas.tsx index 2f072c9e..1e6d00ba 100644 --- a/packages/memory-graph/src/components/graph-canvas.tsx +++ b/packages/memory-graph/src/components/graph-canvas.tsx @@ -97,13 +97,13 @@ export const GraphCanvas = memo(function GraphCanvas({ simulation, } - // Rebuild nodeMap + spatial index when nodes change + // Force the spatial rebuild: a new node generation can keep the same ids/positions useEffect(() => { const map = nodeMapRef.current map.clear() for (const n of nodes) map.set(n.id, n) inputRef.current?.syncNodes(map) - spatialRef.current.rebuild(nodes) + spatialRef.current.rebuild(nodes, true) renderNeeded.current = true }, [nodes]) diff --git a/packages/memory-graph/src/components/memory-graph.tsx b/packages/memory-graph/src/components/memory-graph.tsx index b62b10d9..df8e7936 100644 --- a/packages/memory-graph/src/components/memory-graph.tsx +++ b/packages/memory-graph/src/components/memory-graph.tsx @@ -20,7 +20,6 @@ import type { GraphApiDocument, GraphNode, MemoryNodeData, - GraphThemeColors, MemoryGraphProps, ResolvedMemoryGraphLabels, } from "../types" @@ -85,14 +84,7 @@ export function MemoryGraph({ ) const hoverPopoverZIndex = layering?.hoverPopoverZIndex ?? DEFAULT_HOVER_POPOVER_Z_INDEX - const resolvedColors = useGraphTheme(colorOverrides) - const colors = useMemo( - () => - colorOverrides - ? { ...resolvedColors, ...colorOverrides } - : resolvedColors, - [resolvedColors, colorOverrides], - ) + const colors = useGraphTheme(colorOverrides) const [containerSize, setContainerSize] = useState({ width: 0, height: 0 }) const [containerBounds, setContainerBounds] = useState(null)