Fill the viewport with a fixed sidebar of highlightable type-filter icons over an independently scrolling tree, and let the viewer expand into a full-height scene.
75 lines
2.4 KiB
TypeScript
75 lines
2.4 KiB
TypeScript
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<string, ObjectViewer['component']>();
|
|
|
|
/** 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 (
|
|
<dl className="grid grid-cols-1 gap-2 text-sm sm:grid-cols-2">
|
|
{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 (
|
|
<div key={key} className="rounded-lg border border-zinc-800 bg-zinc-900 p-3">
|
|
<dt className="font-mono text-xs uppercase text-zinc-500">{key}</dt>
|
|
<dd className="mt-1 wrap-break-words text-zinc-200">
|
|
{typeof value === 'string' || typeof value === 'number' ? (
|
|
value
|
|
) : (
|
|
<pre className="whitespace-pre-wrap font-mono text-xs">
|
|
{safeStringify(value)}
|
|
</pre>
|
|
)}
|
|
</dd>
|
|
</div>
|
|
);
|
|
})}
|
|
</dl>
|
|
);
|
|
}
|
|
|
|
/** JSON-stringify a value, replacing any circular references with a marker. */
|
|
function safeStringify(value: unknown): string {
|
|
const seen = new Set<object>();
|
|
return JSON.stringify(
|
|
value,
|
|
(_key, v) => {
|
|
if (typeof v === 'object' && v !== null) {
|
|
if (seen.has(v)) return '[Circular]';
|
|
seen.add(v);
|
|
}
|
|
return v;
|
|
},
|
|
2,
|
|
);
|
|
}
|