fix(tabletop): tilt cards uniformly about the y axis
Apply the same tilt to every stacked part and rotate it about its local Y axis instead of Z. Default tilt to 1 degree when a stacking strategy is present, and arc the poker deck along the surface's bottom edge.
This commit is contained in:
@@ -21,7 +21,7 @@ export function PartPlacement({ pkg, placement }: { pkg: Package; placement: Pla
|
||||
|
||||
// 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` fans it about its local Y (long) axis.
|
||||
// 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;
|
||||
@@ -31,7 +31,7 @@ export function PartPlacement({ pkg, placement }: { pkg: Package; placement: Pla
|
||||
return (
|
||||
<group position={[anchorX, anchorZ, anchorY]} rotation={[0, anchorRotation, 0]}>
|
||||
{/* The part mesh extrudes along +Z; lay it flat so its face points up. */}
|
||||
<group rotation={[-Math.PI / 2, 0, tilt * DEG_TO_RAD]}>
|
||||
<group rotation={[-Math.PI / 2, tilt * DEG_TO_RAD, 0]}>
|
||||
<PartView part={part} baseUrl={part.baseUrl} />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
@@ -46,7 +46,8 @@ describe('pointAt', () => {
|
||||
describe('stackingOffset', () => {
|
||||
it('returns no offset without a curve', () => {
|
||||
expect(stackingOffset(undefined, 0, 3)).toBe(NO_OFFSET);
|
||||
expect(stackingOffset({ limit: 5 }, 0, 3)).toBe(NO_OFFSET);
|
||||
// A stacking strategy defaults to a 1° tilt, so it's not the identity.
|
||||
expect(stackingOffset({ limit: 5 }, 0, 3)).toEqual({ x: 0, y: 0, rotation: 0, z: 0, tilt: 1 });
|
||||
});
|
||||
|
||||
it('spreads parts evenly along a straight curve', () => {
|
||||
@@ -86,12 +87,12 @@ describe('stackingOffset', () => {
|
||||
expect(offset.x).toBeCloseTo(25);
|
||||
});
|
||||
|
||||
it('tilts each part without a curve', () => {
|
||||
it('tilts every part the same amount without a curve', () => {
|
||||
const offset = stackingOffset({ tilt: 0.1 }, 2, 3);
|
||||
expect(offset).toEqual({ x: 0, y: 0, rotation: 0, z: 0, tilt: 0.2 });
|
||||
expect(offset).toEqual({ x: 0, y: 0, rotation: 0, z: 0, tilt: 0.1 });
|
||||
});
|
||||
|
||||
it('tilts parts along the curve', () => {
|
||||
it('tilts every part the same amount along the curve', () => {
|
||||
const offset = stackingOffset({ curve: 'M 0 0 L 100 0', tilt: 0.1 }, 1, 3);
|
||||
expect(offset.x).toBeCloseTo(50);
|
||||
expect(offset.tilt).toBeCloseTo(0.1);
|
||||
@@ -113,7 +114,7 @@ describe('stackingOffset', () => {
|
||||
expect(last.z).toBeCloseTo(40);
|
||||
});
|
||||
|
||||
it('returns no offset without a curve, tilt, or z ramp', () => {
|
||||
expect(stackingOffset({ limit: 5 }, 0, 3)).toBe(NO_OFFSET);
|
||||
it('returns no offset without a stacking strategy', () => {
|
||||
expect(stackingOffset(undefined, 0, 3)).toBe(NO_OFFSET);
|
||||
});
|
||||
});
|
||||
@@ -43,9 +43,10 @@ export function stackingOffset(
|
||||
const shownIndex = shown.indexOf(index);
|
||||
if (shownIndex < 0) return NO_OFFSET;
|
||||
|
||||
// `tilt` fans each shown part about its local Y (long) axis, so the stack's
|
||||
// edges stay visible. It applies even without a curve.
|
||||
const tilt = (stacking?.tilt ?? 0) * shownIndex;
|
||||
// `tilt` rotates each shown part about its local Y (long) axis by the same
|
||||
// amount. It applies even without a curve. Defaults to 1° when a stacking
|
||||
// strategy is present but doesn't specify a tilt.
|
||||
const tilt = stacking?.tilt ?? (stacking ? 1 : 0);
|
||||
|
||||
// The horizontal position along the curve (or a straight pile when there's
|
||||
// no curve), plus the normalized progress used to ramp the z height.
|
||||
|
||||
Reference in New Issue
Block a user