import { DanceRole, DancerIdentity, Rotation } from "./danceCommon.js"; import { Hand } from "./rendererConstants.js"; export enum PositionKind { Circle = "Circle", ShortLines = "ShortLines", }; // Which of the four corners of a circle. // Note that "Left" is the left side of the set, not left from the dancer's PoV, // so, in improper, the Robin starts in the BottomLeft. enum CirclePositionEnum { TopLeft = "TopLeft", BottomLeft = "BottomLeft", BottomRight = "BottomRight", TopRight = "TopRight", } export enum CircleSide { Top = "Top", Bottom = "Bottom", Left = "Left", Right = "Right", } export type CircleSideOrCenter = CircleSide | "Center"; export function isLeftRightCircleSide(side: CircleSideOrCenter): side is CircleSide.Left | CircleSide.Right { return side === CircleSide.Left || side === CircleSide.Right; } export function isTopBottomCircleSide(side: CircleSideOrCenter): side is CircleSide.Top | CircleSide.Bottom { return side === CircleSide.Top || side === CircleSide.Bottom; } export class CirclePosition { public static readonly TopLeft = new CirclePosition(CirclePositionEnum.TopLeft); public static readonly BottomLeft = new CirclePosition(CirclePositionEnum.BottomLeft); public static readonly BottomRight = new CirclePosition(CirclePositionEnum.BottomRight); public static readonly TopRight = new CirclePosition(CirclePositionEnum.TopRight); private readonly enumValue: CirclePositionEnum; private constructor(enumValue: CirclePositionEnum) { this.enumValue = enumValue; } public static fromSides(leftRightSide: CircleSide.Left | CircleSide.Right, topBottomSide: CircleSide.Bottom | CircleSide.Top) { return leftRightSide === CircleSide.Left ? topBottomSide === CircleSide.Top ? CirclePosition.TopLeft : CirclePosition.BottomLeft : topBottomSide === CircleSide.Top ? CirclePosition.TopRight : CirclePosition.BottomRight; } public static fromSwing(side: CircleSide, swingRole: DanceRole, facing: "In" | "Out" | "Across" | Facing.Up | Facing.Down) { if (isLeftRightCircleSide(side)) { if (facing === Facing.Up || facing === Facing.Down) { throw new Error("Ending a swing in a circle on the left/right side facing up/down doesn't make sense."); } else if (facing === "Across") { facing = "In"; } return this.fromSides(side, (side === CircleSide.Left) !== (swingRole === DanceRole.Lark) !== (facing === "In") ? CircleSide.Bottom : CircleSide.Top); } else { if (facing === Facing.Up || facing === Facing.Down) { facing = (facing === Facing.Up) === (side === CircleSide.Top) ? "Out" : "In"; } else if (facing === "Across") { throw new Error("Ending a swing in a circle on the top/bottom side facing across doesn't make sense."); } return this.fromSides( (side === CircleSide.Top) !== (swingRole === DanceRole.Lark) !== (facing === "In") ? CircleSide.Left : CircleSide.Right, side); } } private static enumValueToNumber(enumValue: CirclePositionEnum) : number { switch (enumValue) { case CirclePositionEnum.TopLeft: return 0; case CirclePositionEnum.BottomLeft: return 1; case CirclePositionEnum.BottomRight: return 2; case CirclePositionEnum.TopRight: return 3; } } private static numberToEnumValue(num: number) : CirclePositionEnum { if (num < 0 || num > 3) { num %= 4; if (num < 0) num += 4; } return [ CirclePositionEnum.TopLeft, CirclePositionEnum.BottomLeft, CirclePositionEnum.BottomRight, CirclePositionEnum.TopRight, ][num]; } private static get(enumValue: CirclePositionEnum) : CirclePosition { return [ CirclePosition.TopLeft, CirclePosition.BottomLeft, CirclePosition.BottomRight, CirclePosition.TopRight, ][this.enumValueToNumber(enumValue)]; } public circleRight(places?: number) : CirclePosition { return CirclePosition.get(CirclePosition.numberToEnumValue(CirclePosition.enumValueToNumber(this.enumValue) - (places ?? 1))); } public circleLeft(places?: number) : CirclePosition { return CirclePosition.get(CirclePosition.numberToEnumValue(CirclePosition.enumValueToNumber(this.enumValue) + (places ?? 1))); } public swapAcross() : CirclePosition { return [ CirclePosition.TopRight, CirclePosition.BottomRight, CirclePosition.BottomLeft, CirclePosition.TopLeft, ][CirclePosition.enumValueToNumber(this.enumValue)]; } public swapUpAndDown() : CirclePosition { return [ CirclePosition.BottomLeft, CirclePosition.TopLeft, CirclePosition.TopRight, CirclePosition.BottomRight, ][CirclePosition.enumValueToNumber(this.enumValue)]; } public swapOnSide(side: CircleSide) : CirclePosition { if (side === CircleSide.Bottom || side === CircleSide.Top) { return this.swapAcross(); } else { return this.swapUpAndDown(); } } public toShortLines(slideTo: Hand) : ShortLinesPosition { return slideTo === Hand.Left ? [ ShortLinesPosition.FarLeft, ShortLinesPosition.MiddleLeft, ShortLinesPosition.FarRight, ShortLinesPosition.MiddleRight, ][CirclePosition.enumValueToNumber(this.enumValue)] : [ ShortLinesPosition.MiddleLeft, ShortLinesPosition.FarLeft, ShortLinesPosition.MiddleRight, ShortLinesPosition.FarRight, ][CirclePosition.enumValueToNumber(this.enumValue)]; } public unfoldToShortLines(center: CircleSide.Bottom | CircleSide.Top) : ShortLinesPosition { return center === CircleSide.Bottom ? [ ShortLinesPosition.FarLeft, ShortLinesPosition.MiddleLeft, ShortLinesPosition.MiddleRight, ShortLinesPosition.FarRight, ][CirclePosition.enumValueToNumber(this.enumValue)] : [ ShortLinesPosition.MiddleLeft, ShortLinesPosition.FarLeft, ShortLinesPosition.FarRight, ShortLinesPosition.MiddleRight, ][CirclePosition.enumValueToNumber(this.enumValue)]; } public swapDiagonal() : CirclePosition { return this.swapAcross().swapUpAndDown(); } public facingCenterRotation() : Rotation { return (45 + 90 * CirclePosition.enumValueToNumber(this.enumValue)) % 360; } public topBottomSide() : CircleSide.Top | CircleSide.Bottom { return this.enumValue === CirclePositionEnum.TopLeft || this.enumValue === CirclePositionEnum.TopRight ? CircleSide.Top : CircleSide.Bottom; } public leftRightSide() : CircleSide.Left | CircleSide.Right { return this.enumValue === CirclePositionEnum.TopLeft || this.enumValue === CirclePositionEnum.BottomLeft ? CircleSide.Left : CircleSide.Right; } public isTop() : boolean { return this.topBottomSide() === CircleSide.Top; } public isLeft() : boolean { return this.leftRightSide() === CircleSide.Left; } public isOnLeftLookingAcross() : boolean { return this.enumValue === CirclePositionEnum.TopRight || this.enumValue === CirclePositionEnum.BottomLeft; } public isOnLeftLookingUpAndDown() : boolean { return this.enumValue === CirclePositionEnum.TopLeft || this.enumValue === CirclePositionEnum.BottomRight; } public isOnLeft(around: CircleSide, facing: Facing.Down | Facing.Up | Facing.Left | Facing.Right | Facing.CenterOfCircle | "Out") { if (facing === Facing.CenterOfCircle) { facing = facingInAround(around); } else if (facing === "Out") { facing = oppositeFacing(facingInAround(around)); } if (around === CircleSide.Bottom || around === CircleSide.Top) { if (facing === Facing.Left || facing === Facing.Right) { throw new Error("Cannot face " + facing + " and have a left/right side around " + around); } return (around === CircleSide.Bottom) !== (facing === Facing.Down) !== (this.isLeft()); } else { if (facing === Facing.Up || facing === Facing.Down) { throw new Error("Cannot face " + facing + " and have a left/right side around " + around); } return (around === CircleSide.Left) !== (facing === Facing.Right) !== (this.isTop()); } } public facingAcross() : Facing.Left | Facing.Right { return this.isLeft() ? Facing.Right : Facing.Left; } public facingOut() : Facing.Left | Facing.Right { return this.isLeft() ? Facing.Left : Facing.Right; } public facingUpOrDown() : Facing.Up | Facing.Down { return this.isTop() ? Facing.Down : Facing.Up; } public toString() : string { return this.enumValue.toString(); } } enum ShortLinesPositionEnum { FarLeft = "FarLeft", MiddleLeft = "MiddleLeft", MiddleRight = "MiddleRight", FarRight = "FarRight", } export class ShortLinesPosition { public static readonly FarLeft = new ShortLinesPosition(ShortLinesPositionEnum.FarLeft); public static readonly MiddleLeft = new ShortLinesPosition(ShortLinesPositionEnum.MiddleLeft); public static readonly MiddleRight = new ShortLinesPosition(ShortLinesPositionEnum.MiddleRight); public static readonly FarRight = new ShortLinesPosition(ShortLinesPositionEnum.FarRight); private readonly enumValue: ShortLinesPositionEnum; private constructor(enumValue: ShortLinesPositionEnum) { this.enumValue = enumValue; } public static fromSide(side: CircleSide.Left | CircleSide.Right, which: "Far" | "Middle") { if (side === CircleSide.Left) { return which === "Far" ? ShortLinesPosition.FarLeft : ShortLinesPosition.MiddleLeft; } else { return which === "Far" ? ShortLinesPosition.FarRight : ShortLinesPosition.MiddleRight; } } public static fromSwing(side: "Center" | CircleSide.Left | CircleSide.Right, swingRole: DanceRole, facing: Facing.Up | Facing.Down) { if (side === "Center") { return this.fromSide( (swingRole === DanceRole.Lark) !== (facing === Facing.Down) ? CircleSide.Right : CircleSide.Left, "Middle"); } else { return this.fromSide(side, (side === CircleSide.Left) !== (swingRole === DanceRole.Lark) !== (facing === Facing.Down) ? "Far" : "Middle"); } } private static enumValueToNumber(enumValue: ShortLinesPositionEnum) : number { switch (enumValue) { case ShortLinesPositionEnum.FarLeft: return 0; case ShortLinesPositionEnum.MiddleLeft: return 1; case ShortLinesPositionEnum.MiddleRight: return 2; case ShortLinesPositionEnum.FarRight: return 3; } } private static numberToEnumValue(num: number) : ShortLinesPositionEnum { if (num < 0 || num > 3) { num %= 4; if (num < 0) num += 4; } return [ ShortLinesPositionEnum.FarLeft, ShortLinesPositionEnum.MiddleLeft, ShortLinesPositionEnum.MiddleRight, ShortLinesPositionEnum.FarRight, ][num]; } private static get(enumValue: ShortLinesPositionEnum) : ShortLinesPosition { return [ ShortLinesPosition.FarLeft, ShortLinesPosition.MiddleLeft, ShortLinesPosition.MiddleRight, ShortLinesPosition.FarRight, ][this.enumValueToNumber(enumValue)]; } public swapOnSide() : ShortLinesPosition { return [ ShortLinesPosition.MiddleLeft, ShortLinesPosition.FarLeft, ShortLinesPosition.FarRight, ShortLinesPosition.MiddleRight, ][ShortLinesPosition.enumValueToNumber(this.enumValue)]; } public swapSides() : ShortLinesPosition { return [ ShortLinesPosition.FarRight, ShortLinesPosition.MiddleRight, ShortLinesPosition.MiddleLeft, ShortLinesPosition.FarLeft, ][ShortLinesPosition.enumValueToNumber(this.enumValue)]; } public shift(dir: Hand, facing: Facing.Up | Facing.Down): ShortLinesPosition { const { newPos, wrap } = this.shiftWithWrap(dir, facing); if (wrap) { throw new Error("Invalid shift: " + this + " facing " + facing + " to " + dir + "."); } return newPos; } public shiftWithWrap(dir: Hand, facing: Facing.Up | Facing.Down): { newPos: ShortLinesPosition, wrap?: -1 | 1 } { const shift = (dir === Hand.Left) === (facing === Facing.Down) ? -1 : +1; const newNum = ShortLinesPosition.enumValueToNumber(this.enumValue) + shift; const newPos = ShortLinesPosition.get(ShortLinesPosition.numberToEnumValue(newNum)) if (newNum < 0) { return { newPos, wrap: -1 }; } else if (newNum > 3) { return { newPos, wrap: +1 }; } else { return { newPos }; } } public isMiddle() : boolean { return this.enumValue === ShortLinesPositionEnum.MiddleRight || this.enumValue === ShortLinesPositionEnum.MiddleLeft; } public leftRightSide() : CircleSide.Left | CircleSide.Right { return this.enumValue === ShortLinesPositionEnum.FarLeft || this.enumValue === ShortLinesPositionEnum.MiddleLeft ? CircleSide.Left : CircleSide.Right; } public isLeft() : boolean { return this.leftRightSide() === CircleSide.Left; } // Of the two positions on the same leftRightSide() is this the one further to the left? public isLeftOfSide() : boolean { return this.enumValue === ShortLinesPositionEnum.FarLeft || this.enumValue === ShortLinesPositionEnum.MiddleRight; } public facingSide() : Facing.Left | Facing.Right { return this.isLeft() === this.isMiddle() ? Facing.Left : Facing.Right; } public facingAcross() : Facing.Left | Facing.Right { return this.isLeft() ? Facing.Right : Facing.Left; } public isToLeftOf(otherPos: ShortLinesPosition): boolean { return this.enumValue < otherPos.enumValue; } public toString() : string { return this.enumValue.toString(); } } export enum Facing { CenterOfCircle = "CenterOfCircle", // for star left/right LeftInCircle = "LeftInCircle", RightInCircle = "RightInCircle", Up = "Up", Down = "Down", Left = "Left", Right = "Right", } export function oppositeFacing(facing: Facing.Up | Facing.Down) : Facing.Up | Facing.Down; export function oppositeFacing(facing: Facing.Left | Facing.Right) : Facing.Left | Facing.Right; export function oppositeFacing(facing: Facing.LeftInCircle | Facing.RightInCircle) : Facing.LeftInCircle | Facing.RightInCircle; export function oppositeFacing(facing: Facing.Up | Facing.Down | Facing.Left | Facing.Right) : Facing.Up | Facing.Down | Facing.Left | Facing.Right; export function oppositeFacing(facing: Facing.Up | Facing.Down | Facing.Left | Facing.Right | Facing.LeftInCircle | Facing.RightInCircle) : Facing { switch (facing) { case Facing.LeftInCircle: return Facing.RightInCircle; case Facing.RightInCircle: return Facing.LeftInCircle; case Facing.Up: return Facing.Down; case Facing.Down: return Facing.Up; case Facing.Left: return Facing.Right; case Facing.Right: return Facing.Left; } } export function isFacingUpOrDown(facing: Facing | "Across"): facing is Facing.Up | Facing.Down { return facing === Facing.Up || facing === Facing.Down; } export function facingInAround(side: CircleSide) : Facing.Down | Facing.Up | Facing.Left | Facing.Right { switch (side) { case CircleSide.Bottom: return Facing.Up; break; case CircleSide.Top: return Facing.Down; break; case CircleSide.Left: return Facing.Right; break; case CircleSide.Right: return Facing.Left; break; } } export enum StarGrip { HandsAcross = "HandsAcross", WristGrip = "WristGrip", } export enum HandTo { LeftInCircle = "LeftInCircle", RightInCircle = "RightInCircle", AcrossCircle = "AcrossCircle", DiagonalAcrossCircle = "DiagonalAcrossCircle", LeftDiagonalAcrossCircle = "LeftDiagonalAcrossCircle", RightDiagonalAcrossCircle = "RightDiagonalAcrossCircle", DancerForward = "DancerForward", DancerLeft = "DancerLeft", DancerRight = "DancerRight", } export interface HandConnection { to: HandTo, hand: Hand, } export enum BalanceWeight { Forward = "Forward", Backward = "Backward", Left = "Left", Right = "Right", } export enum DancerDistance { Normal = "Normal", Compact = "Compact", // allemande, etc. // Swings asymmetrical, but some dances may have the Lark do a swing as a Robin or vice versa, // especially any dance with Larks Swing or Robins Swing. SwingLark = "SwingLark", SwingRobin = "SwingRobin", } export enum LongLines { // Walked forward into center. Forward = "Forward", // Only a little offset (has walked almost all the way from the other side after a give and take). Near = "Near", // Actually in center. May be slightly offset for wavy lines. Center = "Center", } export type SemanticPosition = { kind: PositionKind.Circle, setOffset?: number, lineOffset?: number, which: CirclePosition, facing: Facing, hands?: Map, balance?: BalanceWeight, dancerDistance?: DancerDistance, longLines?: LongLines, } | { kind: PositionKind.ShortLines, setOffset?: number, lineOffset?: number, which: ShortLinesPosition, facing: Facing, hands?: Map, balance?: BalanceWeight, dancerDistance?: DancerDistance, longLines?: undefined, }; export const handsInCircle = new Map([ [Hand.Left, { to: HandTo.LeftInCircle, hand: Hand.Right, }], [Hand.Right, { to: HandTo.RightInCircle, hand: Hand.Left, }], ]); export const handsFourImproper: Map = new Map([ [DancerIdentity.OnesLark, { kind: PositionKind.Circle, which: CirclePosition.TopLeft, facing: Facing.CenterOfCircle, hands: handsInCircle, }], [DancerIdentity.OnesRobin, { kind: PositionKind.Circle, which: CirclePosition.TopRight, facing: Facing.CenterOfCircle, hands: handsInCircle, }], [DancerIdentity.TwosLark, { kind: PositionKind.Circle, which: CirclePosition.BottomRight, facing: Facing.CenterOfCircle, hands: handsInCircle, }], [DancerIdentity.TwosRobin, { kind: PositionKind.Circle, which: CirclePosition.BottomLeft, facing: Facing.CenterOfCircle, hands: handsInCircle, }], ]); export function handsInLongLines(wavy: boolean) { return new Map([ [Hand.Left, { hand: wavy ? Hand.Right : Hand.Left, to: HandTo.DancerLeft }], [Hand.Right, { hand: wavy ? Hand.Left : Hand.Right, to: HandTo.DancerRight }], ]); } export function handsInShortLine({ which, facing, wavy }: { which: ShortLinesPosition; facing: Facing.Up | Facing.Down; wavy: boolean; }): Map { return which.isMiddle() ? handsInLongLines(wavy) : new Map([ which.isLeft() === (facing === Facing.Up) ? [Hand.Left, { hand: wavy ? Hand.Right : Hand.Left, to: HandTo.DancerLeft }] : [Hand.Right, { hand: wavy ? Hand.Left : Hand.Right, to: HandTo.DancerRight }] ]); } export function handsInLine(args: { wavy: boolean, which: ShortLinesPosition | CirclePosition, facing?: Facing }) { if (args.which instanceof ShortLinesPosition && (args.facing === Facing.Up || args.facing === Facing.Down)) { return handsInShortLine({ wavy: args.wavy, which: args.which, facing: args.facing }); } else { return handsInLongLines(args.wavy); } } export function handToDancerToSideInCircleFacingAcross(which: CirclePosition): Map { return new Map([ which.isOnLeftLookingAcross() ? [Hand.Right, { hand: Hand.Left, to: HandTo.DancerRight }] : [Hand.Left, { hand: Hand.Right, to: HandTo.DancerLeft }] ]); } export function handToDancerToSideInCircleFacingUpOrDown(which: CirclePosition): Map { return new Map([ which.isOnLeftLookingUpAndDown() ? [Hand.Right, { hand: Hand.Left, to: HandTo.DancerRight }] : [Hand.Left, { hand: Hand.Right, to: HandTo.DancerLeft }] ]); } export function facingAdjacent(pos: SemanticPosition, otherPos: SemanticPosition): Facing | undefined { if (pos.kind !== otherPos.kind) { return undefined; } if (pos.kind === PositionKind.ShortLines) { if (otherPos.kind !== PositionKind.ShortLines) { return undefined; } if ((pos.setOffset ?? 0) !== (otherPos.setOffset ?? 0)) { return undefined; } if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) { if (pos.which === ShortLinesPosition.FarLeft && otherPos.which === ShortLinesPosition.FarRight && ((pos.lineOffset ?? 0) - 1) === (otherPos.lineOffset ?? 0)) { return Facing.Left; } else if (pos.which === ShortLinesPosition.FarRight && otherPos.which === ShortLinesPosition.FarLeft && ((pos.lineOffset ?? 0) + 1) === (otherPos.lineOffset ?? 0)) { return Facing.Right; } else { return undefined; } } if (otherPos.which.isToLeftOf(pos.which)) { return Facing.Left; } else { return Facing.Right; } } else if (pos.kind === PositionKind.Circle) { if (otherPos.kind !== PositionKind.Circle) { return undefined; } if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) { // TODO return undefined; } if ((pos.setOffset ?? 0) !== (otherPos.setOffset ?? 0)) { // TODO return undefined; } if (pos.which.leftRightSide() === otherPos.which.leftRightSide()) { if (pos.which.topBottomSide() === otherPos.which.topBottomSide()) { return undefined; } if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) { return undefined; } if ((pos.setOffset ?? 0) === (otherPos.setOffset ?? 0)) { return pos.which.facingUpOrDown(); } else if (((pos.setOffset ?? 0) + 1) === (otherPos.setOffset ?? 0) && pos.which.isTop()) { return Facing.Up; } else if (((pos.setOffset ?? 0) - 1) === (otherPos.setOffset ?? 0) && !pos.which.isTop()) { return Facing.Down; } } else if (pos.which.topBottomSide() === otherPos.which.topBottomSide()) { if ((pos.setOffset ?? 0) === (otherPos.setOffset ?? 0)) { return undefined; } if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) { return pos.which.facingAcross(); } else if (((pos.lineOffset ?? 0) + 1) === (otherPos.lineOffset ?? 0) && pos.which.isLeft()) { return Facing.Left; } else if (((pos.lineOffset ?? 0) - 1) === (otherPos.lineOffset ?? 0) && !pos.which.isLeft()) { return Facing.Right; } } else { // Opposite corners of circle. return undefined; } } else { throw new Error("Unexpected PositionKind: " + otherPos.kind); } } export function facingRequireAdjacent(pos: SemanticPosition, otherPos: SemanticPosition, moveName: string): Facing { const res = facingAdjacent(pos, otherPos); if (!res) { throw new Error("Cannot " + moveName + " when not adjacent to paired dancer."); } return res; }