// lib/props/registry.ts // Prop resolution. resolveProp turns a value into a live PropInstance, // dispatching across three source kinds: // 1. a PropFactory object -> createProp (sync, wrapped in a resolved promise) // 2. a registered name -> createProp from the registry // 3. a model file (.glb/.gltf) -> loadModel, wrap scene + animations // 4. otherwise a module path -> dynamic import(), use its prop export // Everything returns a Promise so callers have one code path. import * as THREE from 'three' import { createProp } from './prop.js' import { loadModel } from '../loaders/model-registry.js' import { createAnimationController } from '../animation/mixer.js' import { disposeScene } from '../core/dispose.js' import type { PropContext, PropFactory, PropInstance } from '../types.js' // An explicit, owned prop registry — prefer this over the module-global one // so name resolution is scoped to your app (unidirectional-API form). /** An owned name → prop-factory registry — prefer this over the module-global {@link registerProp}/{@link resolveProp} pair so name resolution is scoped to your app. */ export interface PropRegistry { register (name: string, factory: PropFactory): void get (name: string): PropFactory | undefined resolve (src: PropFactory | string, ctx?: PropContext): Promise } /** * Create an owned prop registry. `register` names a factory, `resolve` turns * a factory, registered name, model URL, or module path into a mounted * `PropInstance`. * * @returns A {@link PropRegistry}. * @see {@link resolveProp} for the four accepted `src` forms. */ export function createPropRegistry (): PropRegistry { const names = new Map() return { register (name, factory) { names.set(name, factory) }, get: name => names.get(name), resolve (src, ctx = {}) { return resolveWith(names, src, ctx) }, } } // module-global convenience instance, kept for back-compat. const registry = new Map() /** Register a prop factory under a name in the shared module-global registry. */ export function registerProp (name: string, factory: PropFactory): void { registry.set(name, factory) } /** Look up a prop factory by name in the shared module-global registry. */ export function getProp (name: string): PropFactory | undefined { return registry.get(name) } function isFactory (value: unknown): value is PropFactory { return typeof value === 'object' && value !== null && typeof (value as PropFactory).build === 'function' } function isModelFile (src: string): boolean { return (/\.(glb|gltf)(\?|#|$)/i).test(src) } function wrapModel (object: THREE.Object3D, clips: THREE.AnimationClip[], ctx: PropContext): PropInstance { let controller: PropInstance['controller'] if (clips.length) { controller = createAnimationController(object, clips, ctx.loop) for (const clip of clips) controller.play(clip.name, { loop: THREE.LoopRepeat }) } return { object, controller, lights: [], dispose () { controller?.dispose() disposeScene(object) }, } } /** * Resolve any prop source to a mounted `PropInstance` via the shared * module-global registry. Accepts four `src` forms: a factory object, a * registered name, a `.glb`/`.gltf` URL (loaded and wrapped, clips wired), * or a module path whose default export is a factory (dynamic import). * * @param src - Factory, registered name, model URL, or module path. * @param ctx - Prop context; pass `loop` so model clips animate. * @returns The mounted prop. */ export function resolveProp ( src: PropFactory | string, ctx: PropContext = {}, ): Promise { return resolveWith(registry, src, ctx) } async function resolveWith ( names: Map, src: PropFactory | string, ctx: PropContext, ): Promise { if (isFactory(src)) return createProp(src, ctx) const registered = names.get(src) if (registered) return createProp(registered, ctx) if (isModelFile(src)) { const model = await loadModel(src) return wrapModel(model.scene, model.animations, ctx) } // module path — expect a default or `prop` export that is a PropFactory. const mod = await import(/* @vite-ignore */ src) as Record const factory = (mod.default ?? mod.prop) as PropFactory | undefined if (!isFactory(factory)) throw new Error(`resolveProp: ${src} has no default/prop PropFactory export`) return createProp(factory, ctx) } // perf: registry + model cache (loaders) dedupe repeats. Dynamic import is one // network/module fetch the first time, then cached by the runtime.