fix: NodeGraph crashed on render — stale sourceImageB64 reference

The multi-source refactor dropped the global sourceImageB64 prop but
left two references in the eyedropper-sample useEffect, which threw
a ReferenceError on first render and broke the entire Pipeline view.

Replaced the single-canvas eyedropper machinery with a per-Source-node
canvas Map keyed by node id. The effect now iterates every Source in
the graph, decodes its current `nodePreviews[id]` b64 into a hidden
canvas (skipping already-decoded ones via a `_b64` cache key), and
prunes entries for Sources that have been deleted. sampleColorAt()
looks up the canvas matching the clicked Source's id.

Verified: npm run build clean.
This commit is contained in:
Mitchell Hansen
2026-05-08 22:36:34 -07:00
parent 2d2b76757f
commit 356ddd230d

View File

@@ -131,19 +131,34 @@ export default function NodeGraph({ graph, onChange, nodePreviews, nodeWidth = 2
const sampleNodeIdRef = useRef(null)
sampleNodeIdRef.current = sampleNodeId
// Offscreen canvas holding decoded source image pixels for eyedropper sampling
const sourceSampleCanvasRef = useRef(null)
// Per-Source offscreen canvases for eyedropper sampling. Each Source
// node's preview b64 is decoded once into a hidden canvas keyed by node
// id; sampleColorAt() looks up the canvas matching the clicked Source.
const sourceSampleCanvasesRef = useRef(new Map())
useEffect(() => {
if (!sourceImageB64) { sourceSampleCanvasRef.current = null; return }
const map = sourceSampleCanvasesRef.current
const sourceNodes = (graph?.nodes ?? []).filter(n => n.kind === 'Source')
// Drop entries for Sources that no longer exist.
for (const id of Array.from(map.keys())) {
if (!sourceNodes.some(n => n.id === id)) map.delete(id)
}
// Decode each Source's current preview b64 into a fresh canvas.
for (const n of sourceNodes) {
const b64 = nodePreviews?.[n.id]
if (!b64) { map.delete(n.id); continue }
const existing = map.get(n.id)
if (existing && existing._b64 === b64) continue // already decoded
const img = new Image()
img.onload = () => {
const c = document.createElement('canvas')
c.width = img.naturalWidth; c.height = img.naturalHeight
c.getContext('2d').drawImage(img, 0, 0)
sourceSampleCanvasRef.current = c
c._b64 = b64
map.set(n.id, c)
}
img.src = `data:image/jpeg;base64,${sourceImageB64}`
}, [sourceImageB64])
img.src = `data:image/jpeg;base64,${b64}`
}
}, [graph, nodePreviews])
// ── Wheel zoom — zoom to cursor, computed from refs to handle rapid scroll ──
const onWheel = useCallback(e => {
@@ -351,7 +366,7 @@ export default function NodeGraph({ graph, onChange, nodePreviews, nodeWidth = 2
// Sample a pixel from the source image at the given click position (screen coords).
function sampleColorAt(e, sourceNode) {
const c = sourceSampleCanvasRef.current
const c = sourceSampleCanvasesRef.current.get(sourceNode.id)
if (!c) return
const r = canvasRef.current.getBoundingClientRect()
const wx = (e.clientX - r.left - panRef.current.x) / zoomRef.current