import type { ReactNode } from 'react'; import type { TTSObject } from '@tts/shared'; /** * A viewer renders a single selected object. Custom viewers can be registered * per object class (`Name`) and will take precedence over the default viewer. */ export interface ObjectViewer { /** The object class this viewer handles, e.g. `Card`, `Bag`. */ name: string; component: (props: ViewerProps) => ReactNode; } /** Props passed to every object viewer. */ export interface ViewerProps { object: TTSObject; /** Expand the 3D scene to fill its container instead of the default frame. */ fill?: boolean; } const registry = new Map(); /** Register a viewer for a specific object class. */ export function registerViewer(name: string, component: ObjectViewer['component']) { registry.set(name, component); } /** Resolve the viewer for an object, falling back to the default viewer. */ export function resolveViewer(object: TTSObject): ObjectViewer['component'] { return registry.get(object.Name) ?? DefaultViewer; } /** The default viewer: a plain inspection of the object's fields. */ export function DefaultViewer({ object }: ViewerProps) { return (
{Object.entries(object).map(([key, value]) => { // `Parent` is a back-reference added during traversal; it creates a // cycle with `ContainedObjects`, so skip it in the default view. if (key === 'Parent') return null; return (
{key}
{typeof value === 'string' || typeof value === 'number' ? ( value ) : (
                  {safeStringify(value)}
                
)}
); })}
); } /** JSON-stringify a value, replacing any circular references with a marker. */ function safeStringify(value: unknown): string { const seen = new Set(); return JSON.stringify( value, (_key, v) => { if (typeof v === 'object' && v !== null) { if (seen.has(v)) return '[Circular]'; seen.add(v); } return v; }, 2, ); }