Fix shelf cursor tracking + bigger snap step; tint registry spawn red

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 <group ref>.
  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 <group> 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) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-14 13:59:49 -04:00
co-authored by Claude Opus 4.7
parent 88546d26f0
commit ac71c1a83b
2 changed files with 19 additions and 9 deletions
+12 -8
View File
@@ -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<Group>(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 (
<group ref={cursorRef}>
<mesh position={[0, 0.9, 0]}>
+7 -1
View File
@@ -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