Files
tts-workshop/packages/tabletop/src/placement.tsx
T
hypercross d663f4afea feat: add facing orientation to parts
Replace the face/back boolean with a three-state facing (face, back,
standing) seeded from setup placements. Parts now orient about the
anchor at the resting face/edge, so back-down parts sit on the table
instead of below it and standing parts rest on their bottom edge.
2026-08-10 11:47:58 +08:00

53 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* `PartPlacement` — position a part on a surface location.
*
* A stable per-part component that places a part at its route's anchor (plus
* the candidate's anchor when there is one) and applies the route's stacking
* strategy via `useStacking`. Renders the part's mesh with `PartView`.
*/
import type { Package } from '@tts/bgm';
import { useStacking } from './stacking.js';
import type { Placement } from './state.js';
import { PartView } from './partView.js';
import { MM_TO_WORLD, DEG_TO_RAD, facingTransform, partDimensions } from './part.js';
export function PartPlacement({ pkg, placement }: { pkg: Package; placement: Placement }) {
const { route, candidate, piece, index, stackSize, facing } = placement;
// `piece` is `package:type#id`; the parts map is keyed by `type#id`.
const part = pkg.parts.get(piece.split(':').slice(1).join(':'));
if (!part) return null;
// A candidate inherits the route's stacking unless it overrides it.
const stacking = candidate?.stacking ?? route.stacking;
const { x, y, rotation, z, tilt } = useStacking(stacking, index, stackSize);
// Route anchors and stacking offsets are in mm; convert to world units so
// parts land on the (world-scaled) surface. `z` raises the part along the
// surface normal (world +Y); `tilt` rotates it about its local Y (long) axis.
// Angles are authored in degrees; three.js expects radians.
const anchorX = ((candidate?.x ?? route.x ?? 0) + x) * MM_TO_WORLD;
const anchorY = ((candidate?.y ?? route.y ?? 0) + y) * MM_TO_WORLD;
const anchorZ = z * MM_TO_WORLD;
// The curve tangent `rotation` is the direction of travel; the card's long
// edge ends up along (sinR, cosR) in the surface plane, so it aligns with
// the tangent (cosφ, sinφ) when R = 90° φ.
const anchorRotation =
((candidate?.rotation ?? route.rotation ?? 0) - rotation) * DEG_TO_RAD;
// The facing pivot is the center of the face/edge that rests on the table
// and should land at the anchor. Translate the mesh by the pivot, then apply
// the facing rotation and tilt about it, so the part sits on the table.
const { width, height, depth } = partDimensions(part);
const { pivot, xRotation } = facingTransform(facing, { height, depth });
return (
<group position={[anchorX, anchorZ, anchorY]} rotation={[0, anchorRotation, 0]}>
<group position={[-pivot[0], -pivot[1], -pivot[2]]}>
<group rotation={[xRotation, tilt * DEG_TO_RAD, 0]}>
<PartView part={part} baseUrl={part.baseUrl} />
</group>
</group>
</group>
);
}