refactor(web): code-split the three.js stack out of the main bundle
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import { useTexture } from '@react-three/drei';
|
||||
import { useMemo } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import type { TTSObject } from '@tts/shared';
|
||||
import {
|
||||
circleShape,
|
||||
extrudeShapeParts,
|
||||
hexShape,
|
||||
rectShape,
|
||||
roundedRectShape,
|
||||
scaleShape,
|
||||
type ExtrudedGeometry,
|
||||
} from '@tts/mesh';
|
||||
import { assetUrl } from '@tts/http';
|
||||
import { flipTexture } from './flipTexture';
|
||||
import {
|
||||
getSharedGeometry,
|
||||
getSharedMaterial,
|
||||
objectTint,
|
||||
tintKey,
|
||||
tintedColor,
|
||||
} from './sharedResources';
|
||||
|
||||
/** `CustomTile.Type` enum from Tabletop Simulator. */
|
||||
const TileType = {
|
||||
Box: 0,
|
||||
Hex: 1,
|
||||
Circle: 2,
|
||||
Rounded: 3,
|
||||
} as const;
|
||||
|
||||
const TILE_SIZE = 2;
|
||||
|
||||
/**
|
||||
* The tile mesh for an object, exported so the full-setup view can compose it
|
||||
* into a shared scene.
|
||||
*/
|
||||
export function TileObjectMesh({ object }: { object: TTSObject }) {
|
||||
const url = object.CustomImage?.ImageURL ?? object.CustomImage?.ImageSecondaryURL;
|
||||
const thickness = object.CustomImage?.CustomTile?.Thickness ?? 0.2;
|
||||
const type = object.CustomImage?.CustomTile?.Type ?? TileType.Box;
|
||||
const stretch = object.CustomImage?.CustomTile?.Stretch ?? true;
|
||||
|
||||
return (
|
||||
<TileMesh
|
||||
url={url}
|
||||
thickness={thickness}
|
||||
type={type}
|
||||
stretch={stretch}
|
||||
tint={objectTint(object)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Rendered inside the Canvas so `useTexture` can access the R3F store.
|
||||
// Exported so the full-setup view can compose it into a shared scene.
|
||||
export function TileMesh({
|
||||
url,
|
||||
thickness,
|
||||
type,
|
||||
stretch,
|
||||
tint,
|
||||
}: {
|
||||
url?: string;
|
||||
thickness: number;
|
||||
type: number;
|
||||
stretch: boolean;
|
||||
tint: THREE.Color;
|
||||
}) {
|
||||
const texture: THREE.Texture | null = url ? useTexture(assetUrl(url)) : null;
|
||||
|
||||
// Build the extruded geometry from the tile shape. When `stretch` is false
|
||||
// and a texture is available, scale the shape to the image's aspect ratio so
|
||||
// the tile matches the source proportions instead of being square. Shared
|
||||
// across tiles with the same shape so the full-setup view reuses geometry.
|
||||
const { front, back, walls } = useMemo(() => {
|
||||
const img = texture?.image as HTMLImageElement;
|
||||
const aspect = stretch ? img.width / img.height : 1;
|
||||
const shape = tileShape(type, aspect);
|
||||
const parts = extrudeShapeParts(shape, { height: thickness });
|
||||
const key = `tile:${type}:${aspect}:${thickness}`;
|
||||
return {
|
||||
front: getSharedGeometry(key + ':front', () => toGeometry(parts.front)),
|
||||
back: getSharedGeometry(key + ':back', () => toGeometry(parts.back)),
|
||||
walls: getSharedGeometry(key + ':walls', () => toGeometry(parts.walls)),
|
||||
};
|
||||
}, [type, thickness, stretch, texture]);
|
||||
|
||||
// The back face maps with the same planar UVs as the front, so flip it
|
||||
// left/right to avoid a mirrored texture when viewed from behind.
|
||||
const backMap = useMemo(() => (texture ? flipTexture(texture) : null), [texture]);
|
||||
|
||||
// Shared materials: the front/back carry the tile texture (or a neutral
|
||||
// color when absent); the walls are a solid white, matching TTS tinting.
|
||||
// The tint is baked into the color and the cache key so tinted variants
|
||||
// don't collide.
|
||||
const tintK = tintKey(tint);
|
||||
const faceKey = `tile-face:${url ?? 'none'}:${tintK}`;
|
||||
const faceMat = getSharedMaterial(faceKey, {
|
||||
color: tintedColor(texture ? new THREE.Color('#ffffff') : new THREE.Color('#52525b'), tint),
|
||||
map: texture ?? undefined,
|
||||
roughness: 0.8,
|
||||
});
|
||||
const backMat = getSharedMaterial(faceKey + ':back', {
|
||||
color: tintedColor(texture ? new THREE.Color('#ffffff') : new THREE.Color('#52525b'), tint),
|
||||
map: backMap ?? undefined,
|
||||
roughness: 0.8,
|
||||
});
|
||||
const wallMat = getSharedMaterial(`tile-wall:${tintK}`, {
|
||||
color: tintedColor(new THREE.Color('#ffffff'), tint),
|
||||
roughness: 0.8,
|
||||
});
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Front face carries the tile texture. */}
|
||||
<mesh geometry={front} material={faceMat} />
|
||||
{/* Back face, flipped so it isn't mirrored. */}
|
||||
<mesh geometry={back} material={backMat} />
|
||||
{/* Sides are a solid white, matching TTS tile tinting. */}
|
||||
<mesh geometry={walls} material={wallMat} />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/** Convert raw extruded arrays into a three.js `BufferGeometry`. */
|
||||
function toGeometry(extruded: ExtrudedGeometry) {
|
||||
const { positions, normals, uvs, indices } = extruded;
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
geo.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
|
||||
geo.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
|
||||
geo.setIndex(new THREE.BufferAttribute(indices, 1));
|
||||
return geo;
|
||||
}
|
||||
|
||||
/** Build the 2D footprint for a tile type, scaled to a target aspect ratio. */
|
||||
function tileShape(type: number, aspect: number) {
|
||||
// Base shape is square (1x1); scale x to the aspect ratio so the tile is
|
||||
// `aspect` wide and 1 tall (or keep 1x1 when the aspect is 1).
|
||||
const sx = aspect;
|
||||
const sy = 1;
|
||||
switch (type) {
|
||||
case TileType.Hex:
|
||||
return scaleShape(hexShape(TILE_SIZE / 2), sx, sy);
|
||||
case TileType.Circle:
|
||||
return scaleShape(circleShape(TILE_SIZE / 2), sx, sy);
|
||||
case TileType.Rounded:
|
||||
return scaleShape(roundedRectShape(TILE_SIZE, TILE_SIZE, 0.08), sx, sy);
|
||||
case TileType.Box:
|
||||
default:
|
||||
return scaleShape(rectShape(TILE_SIZE, TILE_SIZE), sx, sy);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user