diff --git a/apps/web/src/components/viewers/transform.ts b/apps/web/src/components/viewers/transform.ts index f3c579b..61c1066 100644 --- a/apps/web/src/components/viewers/transform.ts +++ b/apps/web/src/components/viewers/transform.ts @@ -7,8 +7,10 @@ import type { TTSObject } from '@tts/shared'; * * TTS is left-handed with Y up and +Z toward the player; three.js is * right-handed with Y up. Converting by reflecting the Z axis (negating Z in - * position and every rotation angle) maps a TTS placement onto the three.js - * scene while preserving the physical layout. + * position) maps a TTS placement onto the three.js scene while preserving the + * physical layout. The rotation is conjugated by the reflection: `R_three = + * M·R_tts·M` with `M = diag(1,1,-1)`, which negates `rotX`/`rotY` but leaves + * `rotZ` unchanged. * * Our viewer meshes are authored for inspection (standing up, extruded along * Z, sized in arbitrary units), so each class also gets a base-size correction @@ -43,11 +45,12 @@ const CORRECTION: Record = { /** * Rotation that lays an extruded mesh flat. The tile/token/card meshes extrude - * along +Z with their face in the XY plane (standing up); rotating +90° about - * X puts the face up (+Y), matching how objects lie on a TTS table. Custom - * models are authored standing up already, so they need no correction. + * along +Z with their face in the XY plane (standing up); rotating −90° about + * X maps the +Z face normal to +Y, so the face points up like an object lying + * on a TTS table. Custom models are authored standing up already, so they need + * no correction. */ -const LAY_FLAT: [number, number, number] = [Math.PI / 2, 0, 0]; +const LAY_FLAT: [number, number, number] = [-Math.PI / 2, 0, 0]; const FLAT_CLASSES = new Set([ 'Card', @@ -78,7 +81,8 @@ export function objectPlacement(object: TTSObject): ObjectPlacement | null { const corr = CORRECTION[object.Name] ?? 1; return { position: [t.posX, t.posY, -t.posZ], - rotation: [-rad(t.rotX), -rad(t.rotY), -rad(t.rotZ)], + // Reflection conjugates the rotation: Rx/Ry negate, Rz keeps its sign. + rotation: [-rad(t.rotX), -rad(t.rotY), rad(t.rotZ)], scale: [t.scaleX * corr, t.scaleY * corr, t.scaleZ * corr], layFlat: FLAT_CLASSES.has(object.Name) ? LAY_FLAT : [0, 0, 0], };