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.
53 lines
2.5 KiB
TypeScript
53 lines
2.5 KiB
TypeScript
/**
|
||
* `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>
|
||
);
|
||
}
|