From ac71c1a83b6b87ae85a971873481c7eb13b248b4 Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Thu, 14 May 2026 13:59:49 -0400 Subject: [PATCH] Fix shelf cursor tracking + bigger snap step; tint registry spawn red MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small follow-ups after the first user-visible spike test. shelf cursor stuck at origin: The placeholder cursor is a translucent slab inside a . The tool was calling setCursor(state) on every grid:move, which triggered a React re-render. R3F re-applies props on each render, and since the had no `position` prop, the implicit default [0,0,0] clobbered the imperative `position.set` from the previous tick. Result: cursor stuck at level origin instead of following the mouse. Fix: drop the unused useState entirely. Pure imperative position updates via the ref. No re-renders, no clobbering. (The legacy spawn tool gets away with the same pattern because CursorSphere buffers its position prop differently — but for the spike, the simpler model is fine.) shelf snap step: Was 0.1 (10cm) — much finer than the editor's default 0.5 grid. Bumped to 0.5 (matches the toolbar grid setting and the legacy half-meter snap pattern used by spawn/column). registry-driven spawn renderer paints red: Temporary verification marker. With NEXT_PUBLIC_USE_REGISTRY_FOR_ SPAWN=1, spawns rendered via the new path now appear in #ef4444 red. Legacy renderer stays in #22c55e green. Easy visual check for "which dispatch path is this spawn on?" Reverted in the PR that signs off spawn parity (alongside legacy file deletion). Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/nodes/src/shelf/tool.tsx | 20 ++++++++++++-------- packages/nodes/src/spawn/renderer.tsx | 8 +++++++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/nodes/src/shelf/tool.tsx b/packages/nodes/src/shelf/tool.tsx index 0d44a486..b06bd047 100644 --- a/packages/nodes/src/shelf/tool.tsx +++ b/packages/nodes/src/shelf/tool.tsx @@ -3,42 +3,43 @@ import { emitter, type GridEvent, - ShelfNode, sceneRegistry, + ShelfNode, snapPointToGrid, useScene, } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useRef } from 'react' import { type Group, Vector3 } from 'three' const worldVector = new Vector3() +const GRID_STEP = 0.5 // match the editor's default placement grid function getLevelLocalPosition(levelId: string, event: GridEvent): [number, number, number] { const levelObject = sceneRegistry.nodes.get(levelId) if (!levelObject) { - const [sx, sz] = snapPointToGrid([event.localPosition[0], event.localPosition[2]], 0.1) + const [sx, sz] = snapPointToGrid([event.localPosition[0], event.localPosition[2]], GRID_STEP) return [sx, event.localPosition[1], sz] } worldVector.set(event.position[0], event.position[1], event.position[2]) levelObject.updateWorldMatrix(true, false) levelObject.worldToLocal(worldVector) - const [sx, sz] = snapPointToGrid([worldVector.x, worldVector.z], 0.1) + const [sx, sz] = snapPointToGrid([worldVector.x, worldVector.z], GRID_STEP) return [sx, worldVector.y, sz] } const ShelfTool = () => { const activeLevelId = useViewer((state) => state.selection.levelId) - const [, setCursor] = useState<[number, number, number] | null>(null) const cursorRef = useRef(null) useEffect(() => { if (!activeLevelId) return const onGridMove = (event: GridEvent) => { - const [sx, sz] = snapPointToGrid([event.localPosition[0], event.localPosition[2]], 0.1) - const next: [number, number, number] = [sx, event.localPosition[1], sz] - setCursor(next) + // Imperative position update — no React state, so the component + // doesn't re-render. R3F-applied props would otherwise clobber the + // imperative `position.set` on the next render. + const next = getLevelLocalPosition(activeLevelId, event) cursorRef.current?.position.set(next[0], next[1], next[2]) } @@ -64,6 +65,9 @@ const ShelfTool = () => { if (!activeLevelId) return null + // Cursor preview — a translucent shelf-shaped slab. No `position` prop on + // the group; we move it imperatively via the ref so React re-renders don't + // reset it to the origin. return ( diff --git a/packages/nodes/src/spawn/renderer.tsx b/packages/nodes/src/spawn/renderer.tsx index afca500f..fe42d176 100644 --- a/packages/nodes/src/spawn/renderer.tsx +++ b/packages/nodes/src/spawn/renderer.tsx @@ -5,7 +5,13 @@ import { useNodeEvents, useViewer } from '@pascal-app/viewer' import { useMemo, useRef } from 'react' import { Color, type Group, Shape } from 'three' -const SPAWN_COLOR = new Color('#22c55e') +// TEMPORARY (Phase 2 verification): the registry-driven renderer paints +// spawns RED so you can visually tell which dispatch path is live. The +// legacy renderer in @pascal-app/viewer is still green. Revert this to +// '#22c55e' once the registry path is signed off for parity. Tracked by +// the NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN flag — if a spawn renders red +// you're on the new path; green = legacy. +const SPAWN_COLOR = new Color('#ef4444') /** * Registry-driven spawn renderer. Behaviorally identical to the legacy