Model TTSObjectTransform in shared types and convert each object's position, rotation, and scale into three.js placement, with per-class base-size corrections and a lay-flat rotation for extruded meshes.
122 lines
3.8 KiB
TypeScript
122 lines
3.8 KiB
TypeScript
import { Suspense, useEffect, useMemo } from 'react';
|
|
import { useParams } from 'react-router-dom';
|
|
import { flattenObjects } from '@tts/extract';
|
|
import type { TTSObject } from '@tts/shared';
|
|
import { useModStore } from '../stores/modStore';
|
|
import { useSearchStore } from '../stores/searchStore';
|
|
import Scene from '../components/viewers/Scene';
|
|
import { TileObjectMesh } from '../components/viewers/TileViewer';
|
|
import { TokenObjectMesh } from '../components/viewers/TokenViewer';
|
|
import { CardObjectMesh } from '../components/viewers/CardViewer';
|
|
import { CustomModelMesh } from '../components/viewers/CustomModelViewer';
|
|
import { objectPlacement } from '../components/viewers/transform';
|
|
|
|
/** Object classes that carry renderable assets (mirrors `viewers/register.ts`). */
|
|
const RENDERABLE = new Set([
|
|
'Tile',
|
|
'Custom_Tile',
|
|
'Custom_Token',
|
|
'Card',
|
|
'CardCustom',
|
|
'Deck',
|
|
'DeckCustom',
|
|
'Custom_Deck',
|
|
'Custom_Model',
|
|
'Custom_Model_Bag',
|
|
'Custom_Model_Infinite_Bag',
|
|
]);
|
|
|
|
/**
|
|
* A "full setup" view: renders every loadable object in a save into a single
|
|
* shared scene, placed at its real position from the save's `Transform`.
|
|
* Geometry and materials are shared across objects (see
|
|
* `components/viewers/sharedResources.ts`).
|
|
*/
|
|
export default function FullSetupPage() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const { mod, loading, error, load } = useModStore();
|
|
const item = useSearchStore((s) => s.items.find((i) => i.id === id));
|
|
|
|
useEffect(() => {
|
|
if (id) load(id, item?.fileUrl);
|
|
}, [id, item?.fileUrl, load]);
|
|
|
|
const objects = useMemo(
|
|
() => (mod ? flattenObjects(mod) : []),
|
|
[mod],
|
|
);
|
|
|
|
const renderable = useMemo(
|
|
() => objects.filter((o) => RENDERABLE.has(o.Name)),
|
|
[objects],
|
|
);
|
|
const skipped = objects.length - renderable.length;
|
|
|
|
const placed = useMemo(
|
|
() =>
|
|
renderable
|
|
.map((object) => ({ object, placement: objectPlacement(object) }))
|
|
.filter(
|
|
(p): p is { object: TTSObject; placement: NonNullable<ReturnType<typeof objectPlacement>> } =>
|
|
p.placement !== null,
|
|
),
|
|
[renderable],
|
|
);
|
|
|
|
if (loading) return <p className="text-sm text-zinc-400">Loading mod…</p>;
|
|
if (error) return <p className="text-sm text-red-400">{error}</p>;
|
|
if (!mod) return <p className="text-sm text-zinc-500">No mod loaded.</p>;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold">Full setup</h1>
|
|
<p className="mt-1 text-sm text-zinc-400">
|
|
{objects.length} objects · {renderable.length} rendered
|
|
{skipped > 0 ? ` · ${skipped} skipped` : ''}
|
|
</p>
|
|
</div>
|
|
|
|
<Scene>
|
|
<Suspense fallback={null}>
|
|
{/* Key by index, not GUID: cards in a deck share the deck's GUID. */}
|
|
{placed.map(({ object, placement }, index) => (
|
|
<group
|
|
key={index}
|
|
position={placement.position}
|
|
rotation={placement.rotation}
|
|
scale={placement.scale}
|
|
>
|
|
<group rotation={placement.layFlat}>
|
|
<RenderObject object={object} />
|
|
</group>
|
|
</group>
|
|
))}
|
|
</Suspense>
|
|
</Scene>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** Dispatch a renderable object to the mesh component for its class. */
|
|
function RenderObject({ object }: { object: TTSObject }) {
|
|
switch (object.Name) {
|
|
case 'Tile':
|
|
case 'Custom_Tile':
|
|
return <TileObjectMesh object={object} />;
|
|
case 'Custom_Token':
|
|
return <TokenObjectMesh object={object} />;
|
|
case 'Card':
|
|
case 'CardCustom':
|
|
case 'Deck':
|
|
case 'DeckCustom':
|
|
case 'Custom_Deck':
|
|
return <CardObjectMesh object={object} />;
|
|
case 'Custom_Model':
|
|
case 'Custom_Model_Bag':
|
|
case 'Custom_Model_Infinite_Bag':
|
|
return <CustomModelMesh object={object} />;
|
|
default:
|
|
return null;
|
|
}
|
|
} |