feat(tabletop): add tilt and zStart/zEnd stacking options
Replace the per-part lift with a local Y-axis tilt that fans the stack, and add zStart/zEnd to ramp the stack's height across the curve so it arches in 3D. Update the poker deck and docs accordingly.
This commit is contained in:
@@ -16,48 +16,77 @@ export interface StackOffset {
|
||||
y: number;
|
||||
/** Rotation in radians. */
|
||||
rotation: number;
|
||||
/** Vertical (surface-normal) offset from the anchor, in mm. */
|
||||
z: number;
|
||||
/** Rotation in radians about the card's local Y (long) axis. */
|
||||
tilt: number;
|
||||
}
|
||||
|
||||
/** The identity offset: no stacking applied. */
|
||||
export const NO_OFFSET: StackOffset = { x: 0, y: 0, rotation: 0 };
|
||||
export const NO_OFFSET: StackOffset = { x: 0, y: 0, rotation: 0, z: 0, tilt: 0 };
|
||||
|
||||
/**
|
||||
* Compute the offset/rotation for the piece at `index` of a `stackSize`-piece
|
||||
* stack, given the route's stacking strategy. Returns `NO_OFFSET` when there's
|
||||
* no curve or the stack is empty.
|
||||
* no curve, no tilt, and no z profile, or the stack is empty.
|
||||
*/
|
||||
export function stackingOffset(
|
||||
stacking: Stacking | undefined,
|
||||
index: number,
|
||||
stackSize: number,
|
||||
): StackOffset {
|
||||
if (!stacking?.curve || stackSize <= 0) return NO_OFFSET;
|
||||
if (stackSize <= 0) return NO_OFFSET;
|
||||
|
||||
// `limit` selects which pieces are shown; the offset is computed over the
|
||||
// shown span. `0` (or absent) shows all.
|
||||
const shown = applyLimit(stacking.limit, stackSize);
|
||||
const shown = applyLimit(stacking?.limit, stackSize);
|
||||
const shownIndex = shown.indexOf(index);
|
||||
if (shownIndex < 0) return NO_OFFSET;
|
||||
|
||||
const curve = parsePath(stacking.curve);
|
||||
const length = curve.length;
|
||||
if (length <= 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;
|
||||
|
||||
// Step length: curve length / max(steps, # parts − 1). A single part sits
|
||||
// at the start of the curve.
|
||||
const steps = stacking.steps ?? 1;
|
||||
const span = Math.max(steps, shown.length - 1);
|
||||
const step = length / span;
|
||||
// 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.
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let rotation = 0;
|
||||
let u = shown.length > 1 ? shownIndex / (shown.length - 1) : 0;
|
||||
|
||||
// Alignment: how far the whole span is inset from the curve's start.
|
||||
const spanLength = step * (shown.length - 1);
|
||||
let start = 0;
|
||||
if (stacking.align === 'end') start = length - spanLength;
|
||||
else if (stacking.align === 'center') start = (length - spanLength) / 2;
|
||||
if (stacking?.curve) {
|
||||
const curve = parsePath(stacking.curve);
|
||||
const length = curve.length;
|
||||
if (length > 0) {
|
||||
// Step length: curve length / max(steps, # parts − 1). A single part
|
||||
// sits at the start of the curve.
|
||||
const steps = stacking.steps ?? 1;
|
||||
const span = Math.max(steps, shown.length - 1);
|
||||
const step = length / span;
|
||||
|
||||
const distance = start + shownIndex * step;
|
||||
const { x, y, angle } = pointAt(curve, distance);
|
||||
return { x, y, rotation: angle };
|
||||
// Alignment: how far the whole span is inset from the curve's start.
|
||||
const spanLength = step * (shown.length - 1);
|
||||
let start = 0;
|
||||
if (stacking.align === 'end') start = length - spanLength;
|
||||
else if (stacking.align === 'center') start = (length - spanLength) / 2;
|
||||
|
||||
const distance = start + shownIndex * step;
|
||||
const point = pointAt(curve, distance);
|
||||
x = point.x;
|
||||
y = point.y;
|
||||
rotation = point.angle;
|
||||
u = distance / length;
|
||||
}
|
||||
}
|
||||
|
||||
// The z height ramps linearly from `zStart` to `zEnd` across the curve's
|
||||
// span, lifting the stack in 3D.
|
||||
const zStart = stacking?.zStart ?? 0;
|
||||
const zEnd = stacking?.zEnd ?? 0;
|
||||
const z = zStart + (zEnd - zStart) * u;
|
||||
|
||||
if (x === 0 && y === 0 && rotation === 0 && z === 0 && tilt === 0) return NO_OFFSET;
|
||||
return { x, y, rotation, z, tilt };
|
||||
}
|
||||
|
||||
/** The stacking hook: memoized `stackingOffset` for a piece. */
|
||||
|
||||
Reference in New Issue
Block a user