"use client"; import { useEffect, useId, useMemo, useRef } from 'react'; import { ComputerActor } from '../desktop/ComputerActor'; import type { ComputerMotion } from '../desktop/computer-motion.mjs'; import type { RoomMotion } from './motion/room-motion.mjs'; import { DEFAULT_CHARACTER, DEFAULT_EFFECTS, DEFAULT_PALETTE, LAYERS } from './catalog'; import { instancePrefix, placement, resolveObjects } from './model'; import { RoomObject } from './RoomObject'; import type { RoomSceneProps } from './types'; import './room.css'; /** Full vector scene. React owns composition; one imperative clock owns motion attributes. */ export function RoomScene({ objects, additions, character, effects, palette, paused = false, drawerOpen = 0, replay = 0, attitudeEnabled = false, className = '', decorative = true, onCharacterComplete, pointerTarget, label = 'A warm repair office with a patient vintage computer', onError, children, }: RoomSceneProps) { const generatedId = useId(); const idPrefix = useMemo(() => instancePrefix(generatedId), [generatedId]); const svg = useRef(null); const room = useRef(null); const computer = useRef(null); const items = useMemo(() => resolveObjects(objects, additions), [objects, additions]); const colors = useMemo(() => ({ ...DEFAULT_PALETTE, ...palette }), [palette]); const motionEffects = useMemo(() => ({ ...DEFAULT_EFFECTS, ...effects }), [effects]); const actor = character === false ? null : { ...DEFAULT_CHARACTER, ...character }; const latest = useRef({ paused, motionEffects, drawerOpen, onError }); latest.current = { paused, motionEffects, drawerOpen, onError }; useEffect(() => { let cancelled = false; let local: RoomMotion | null = null; void import('./motion/room-motion.mjs').then(({ RoomMotion: Controller }) => { if (cancelled || !svg.current) return; local = new Controller(svg.current, { effects: latest.current.motionEffects, paused: latest.current.paused, drawerOpen: latest.current.drawerOpen, onFrame: delta => computer.current?.advance(delta), }); room.current = local; }).catch(error => { if (!cancelled) latest.current.onError?.(error); }); return () => { cancelled = true; local?.destroy(); if (room.current === local) room.current = null; }; }, []); useEffect(() => { room.current?.refresh(); }, [items, character]); useEffect(() => { room.current?.setEffects(motionEffects); }, [motionEffects]); useEffect(() => { room.current?.setDrawerOpen(drawerOpen); }, [drawerOpen]); useEffect(() => { if (paused) room.current?.pause(); else room.current?.resume(); }, [paused]); const slot = (parent: string | null) => actor?.visible && actor.parent === parent ? ( ) : null; return ( {!decorative && {label}} {LAYERS.map(layer => ( {items.filter(item => item.layer === layer && item.parent === null).map(item => ( ))} {layer === 'character' && slot(null)} ))} {children} ); }