46 lines
1.7 KiB
TypeScript
46 lines
1.7 KiB
TypeScript
'use client'
|
|
|
|
import { emitter, useScene } from '@pascal-app/core'
|
|
import { useEffect, useRef } from 'react'
|
|
import { computeSceneBoundsXZ } from '../lib/scene-bounds'
|
|
|
|
/**
|
|
* Auto-frame the camera onto a freshly loaded scene.
|
|
*
|
|
* Motivation: when the MCP `setScene` tool (or any other entry point) swaps
|
|
* the scene graph while the default camera is pointing at empty space, the
|
|
* user sees a black viewport. This hook subscribes to the core scene store
|
|
* and, whenever `nodes` transitions from empty → non-empty, computes the
|
|
* XZ bounds of the new scene and emits `camera-controls:fit-scene`. The
|
|
* `<CustomCameraControls />` component picks up that event and frames the
|
|
* camera onto the bounds.
|
|
*
|
|
* Mount in exactly ONE component (the Editor). It holds no state of its own;
|
|
* the subscription is torn down on unmount.
|
|
*/
|
|
export function useAutoFrame(): void {
|
|
// Track the previous node count so we can detect the empty → non-empty edge.
|
|
const wasEmptyRef = useRef(true)
|
|
|
|
useEffect(() => {
|
|
// Initialise from current store state so a remount after a setScene
|
|
// doesn't re-frame an already-populated scene.
|
|
wasEmptyRef.current = Object.keys(useScene.getState().nodes).length === 0
|
|
|
|
const unsubscribe = useScene.subscribe((state) => {
|
|
const isEmpty = Object.keys(state.nodes).length === 0
|
|
const wasEmpty = wasEmptyRef.current
|
|
wasEmptyRef.current = isEmpty
|
|
|
|
// Only react to empty → non-empty transitions. Normal edits keep both
|
|
// flags false; a `clearScene()` goes non-empty → empty and is ignored.
|
|
if (!wasEmpty || isEmpty) return
|
|
|
|
const bounds = computeSceneBoundsXZ(state.nodes)
|
|
emitter.emit('camera-controls:fit-scene', bounds ? { bounds } : {})
|
|
})
|
|
|
|
return unsubscribe
|
|
}, [])
|
|
}
|