import { DancerIdentity } from "../danceCommon.js"; import { SemanticPosition, PositionKind, ShortLinesPosition, CirclePosition, CircleSide, Facing } from "../interpreterCommon.js"; import { LowLevelMove, SemanticAnimationKind } from "../lowLevelMove.js"; import { Hand } from "../rendererConstants.js"; import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, MoveInterpreterCtorArgs, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js"; import { dancerIsPair } from "../libfigure/util.js"; const moveName = "hey"; type HeyStep = { kind: "StandStill" | "Loop" | "CenterPass" | "EndsPassIn" | "EndsPassOut" | "Ricochet", endPosition: SemanticPosition, } function fixupHeyOtherPath(withoutOtherPath: Map): Map { const numSteps = withoutOtherPath.get(DancerIdentity.OnesLark)!.length; for (let i = 0; i < numSteps; i++) { for (const id of withoutOtherPath.keys()) { const lowLevelMove = withoutOtherPath.get(id)![i]; if (lowLevelMove.movementPattern.kind !== SemanticAnimationKind.PassBy || !lowLevelMove.heyStep || lowLevelMove.movementPattern.otherPath) { continue; } const heyStepKind = lowLevelMove.heyStep.kind; let foundPair = false; for (const otherId of withoutOtherPath.keys()) { const otherLowLevelMove = withoutOtherPath.get(otherId)![i]; if (id === otherId || otherLowLevelMove.movementPattern.kind !== SemanticAnimationKind.PassBy || !otherLowLevelMove.heyStep || otherLowLevelMove.movementPattern.otherPath) { continue; } const otherHeyStepKind = otherLowLevelMove.heyStep.kind; if (heyStepKind === "CenterPass" && otherHeyStepKind === "CenterPass" || (lowLevelMove.startPosition.which.leftRightSide() === otherLowLevelMove.startPosition.which.leftRightSide() && (heyStepKind === "EndsPassIn" && otherHeyStepKind === "EndsPassOut" || heyStepKind === "EndsPassOut" && otherHeyStepKind === "EndsPassIn"))) { lowLevelMove.movementPattern.otherPath = { start: { ...otherLowLevelMove.startPosition, setOffset: lowLevelMove.startPosition.setOffset, lineOffset: lowLevelMove.startPosition.lineOffset }, end: { ...otherLowLevelMove.endPosition, setOffset: lowLevelMove.endPosition.setOffset, lineOffset: lowLevelMove.endPosition.lineOffset }, } otherLowLevelMove.movementPattern.otherPath = { start: { ...lowLevelMove.startPosition, setOffset: otherLowLevelMove.startPosition.setOffset, lineOffset: otherLowLevelMove.startPosition.lineOffset }, end: { ...lowLevelMove.endPosition, setOffset: otherLowLevelMove.endPosition.setOffset, lineOffset: otherLowLevelMove.endPosition.lineOffset }, } foundPair = true; break; } } if (!foundPair && heyStepKind === "EndsPassOut") { // Then other is standing still. const pos = { ...([...withoutOtherPath.values()] .map(otherMoves => otherMoves[i]) .filter(m => m.movementPattern.kind === SemanticAnimationKind.StandStill && m.endPosition.which.leftRightSide() === lowLevelMove.endPosition.which.leftRightSide()) [0].endPosition), setOffset: lowLevelMove.startPosition.setOffset, lineOffset: lowLevelMove.startPosition.lineOffset } lowLevelMove.movementPattern.otherPath = { start: pos, end: pos }; } } for (const id of withoutOtherPath.keys()) { const lowLevelMove = withoutOtherPath.get(id)![i]; if (lowLevelMove.movementPattern.kind === SemanticAnimationKind.PassBy && !lowLevelMove.movementPattern.otherPath) { throw new Error("Failed to fill in otherPath for " + id + " on hey step " + i); } } } // Object was mutated. return withoutOtherPath; } class HeySingleVariant extends SingleVariantMoveInterpreter { heyStepToPartialLowLevelMove(heyStep: HeyStep): PartialLowLevelMove & { heyStep: HeyStep } { return { beats: this.moveInterpreter.heyPartBeats, // TODO use circle positions on ends? ... unless hey ends in a box the gnat or similar... endPosition: heyStep.endPosition, movementPattern: heyStep.kind === "StandStill" ? { kind: SemanticAnimationKind.StandStill, } : heyStep.kind === "Loop" ? { // TODO Loop should probably be its own kind? Or RotateAround? kind: SemanticAnimationKind.Linear, minRotation: this.moveInterpreter.endsShoulder === Hand.Right ? +180 : -180, } : heyStep.kind === "Ricochet" ? { // TODO This is a hack. kind: SemanticAnimationKind.PassBy, around: heyStep.endPosition.which.leftRightSide(), withHands: false, otherPath: "Swap", facing: "Start", side: this.moveInterpreter.endsShoulder, } : { kind: SemanticAnimationKind.PassBy, around: heyStep.kind === "CenterPass" ? "Center" : heyStep.endPosition.which.leftRightSide(), withHands: false, side: heyStep.kind === "CenterPass" ? this.moveInterpreter.centerShoulder : this.moveInterpreter.endsShoulder, facing: "Start", otherPath: undefined!, // Placeholder, fixup later. }, heyStep, }; } continueHey(prevStep: HeyStep, stepsLeft: number, { beenInCenter, endsInCircle, inCenterFirst }: { beenInCenter: boolean, endsInCircle: boolean, inCenterFirst: boolean }): HeyStep { // TODO Not sure why type checker requires rechecking this here. if (this.move.move !== "hey") throw new Error("Unreachable."); // Continuing hey so everyone is either passing (in center or on ends) or looping on ends. if (prevStep.endPosition.kind === PositionKind.Circle) { if (prevStep.endPosition.facing === prevStep.endPosition.which.facingAcross()) { if (stepsLeft === 0) { return { kind: "StandStill", endPosition: prevStep.endPosition, } } return { kind: "EndsPassIn", endPosition: { kind: PositionKind.ShortLines, which: prevStep.endPosition.which.isLeft() ? ShortLinesPosition.MiddleLeft : ShortLinesPosition.MiddleRight, facing: prevStep.endPosition.which.facingAcross(), setOffset: prevStep.endPosition.setOffset, lineOffset: prevStep.endPosition.lineOffset, }, } } else { if (stepsLeft === 1 && !endsInCircle) { return { kind: "Loop", endPosition: { kind: PositionKind.ShortLines, which: prevStep.endPosition.which.isLeft() ? ShortLinesPosition.FarLeft : ShortLinesPosition.FarRight, facing: prevStep.endPosition.which.facingAcross(), setOffset: prevStep.endPosition.setOffset, lineOffset: prevStep.endPosition.lineOffset, }, } } return { kind: "Loop", endPosition: { ...prevStep.endPosition, which: prevStep.endPosition.which.swapUpAndDown(), facing: prevStep.endPosition.which.facingAcross() }, } } } else if (prevStep.endPosition.kind === PositionKind.ShortLines) { const isFacingSide = prevStep.endPosition.facing === prevStep.endPosition.which.facingSide(); const inMiddle = prevStep.endPosition.which.isMiddle(); if (!inMiddle && !isFacingSide) { return { kind: "Loop", endPosition: { ...prevStep.endPosition, facing: prevStep.endPosition.which.facingSide() }, } } else if (inMiddle && isFacingSide) { return { kind: "EndsPassOut", endPosition: { ...prevStep.endPosition, kind: PositionKind.Circle, which: prevStep.endPosition.which.isLeft() ? (this.moveInterpreter.endsShoulder === Hand.Right ? CirclePosition.TopLeft : CirclePosition.BottomLeft) : (this.moveInterpreter.endsShoulder === Hand.Right ? CirclePosition.BottomRight : CirclePosition.TopRight), }, } } else if (!isFacingSide) { const rico = inCenterFirst ? beenInCenter ? this.move.parameters.rico3 : this.move.parameters.rico1 : beenInCenter ? this.move.parameters.rico4 : this.move.parameters.rico2; if (rico) { const onLeftSide = prevStep.endPosition.which.isLeft(); return { kind: "Ricochet", endPosition: { ...prevStep.endPosition, kind: PositionKind.Circle, which: CirclePosition.fromSides(prevStep.endPosition.which.leftRightSide(), // TODO might be swapped (this.moveInterpreter.endsShoulder === Hand.Left) === onLeftSide ? CircleSide.Top : CircleSide.Bottom), facing: onLeftSide ? Facing.Left : Facing.Right, } } } else { return { kind: "CenterPass", endPosition: { ...prevStep.endPosition, which: prevStep.endPosition.which.swapSides() }, } } } else { return { kind: inMiddle ? "EndsPassOut" : "EndsPassIn", endPosition: { ...prevStep.endPosition, which: prevStep.endPosition.which.swapOnSide() }, } } } else { throw new Error("Unexpected PositionKind: " + (prevStep.endPosition).kind); } } moveAsLowLevelMoves(): LowLevelMovesForAllDancers { return fixupHeyOtherPath(this.handleMove(({ id, startPos }) => { const endsInCircle = startPos.kind === PositionKind.Circle; const inCenterFirst = this.moveInterpreter.firstPassInCenter && (this.findPairOpposite(this.move.parameters.who, id) !== null) || !!this.move.parameters.who2 && (this.findPairOpposite(this.move.parameters.who2, id) !== null); let firstHeyStep: HeyStep; let startingPos: SemanticPosition; if (this.moveInterpreter.firstPassInCenter) { if (startPos.kind !== PositionKind.Circle) { throw new Error("Hey starting in center not from circle is unsupported."); } startingPos = { kind: startPos.kind, which: startPos.which, facing: startPos.which.isLeft() ? Facing.Right : Facing.Left, setOffset: startPos.setOffset, lineOffset: startPos.lineOffset, }; if (inCenterFirst) { if (this.move.parameters.rico1) { firstHeyStep = { kind: "Ricochet", endPosition: { kind: PositionKind.Circle, which: startPos.which.swapUpAndDown(), facing: startPos.which.facingOut(), setOffset: startPos.setOffset, lineOffset: startPos.lineOffset, } }; } else { firstHeyStep = { kind: "CenterPass", endPosition: { kind: PositionKind.ShortLines, which: startPos.which.isLeft() ? ShortLinesPosition.MiddleRight : ShortLinesPosition.MiddleLeft, facing: startingPos.facing, setOffset: startPos.setOffset, lineOffset: startPos.lineOffset, } }; } } else { firstHeyStep = { kind: "StandStill", endPosition: startingPos, } } } else { if (startPos.kind !== PositionKind.ShortLines) { throw new Error("Hey with first pass on ends must start approximately in short lines."); } const startFacing = startPos.which.facingSide(); startingPos = { kind: startPos.kind, which: startPos.which, facing: startFacing, setOffset: startPos.setOffset, lineOffset: startPos.lineOffset, }; firstHeyStep = { kind: startingPos.which.isMiddle() ? "EndsPassOut" : "EndsPassIn", endPosition: { ...startingPos, which: startPos.which.swapOnSide() }, } } const heySteps: HeyStep[] = [firstHeyStep]; let beenInCenter = firstHeyStep.kind === "CenterPass" || firstHeyStep.kind === "Ricochet"; for (let i = 1; i < this.moveInterpreter.heyParts; i++) { const nextHeyStep = this.continueHey(heySteps[i - 1], this.moveInterpreter.heyParts - i - 1, {beenInCenter, endsInCircle, inCenterFirst}); beenInCenter ||= nextHeyStep.kind === "CenterPass" || nextHeyStep.kind === "Ricochet"; heySteps.push(nextHeyStep); } return this.combine(heySteps.map(s => this.heyStepToPartialLowLevelMove(s)), { ...startingPos, hands: undefined }); })); } } class Hey extends MoveInterpreter { public readonly heyParts: number; public readonly heyPartBeats: number; public readonly firstPassInCenter: boolean; public readonly centerShoulder: Hand; public readonly endsShoulder: Hand; constructor(args: MoveInterpreterCtorArgs) { super(args); if (this.move.parameters.dir !== "across") { throw new Error("Unsupported hey direction: " + this.move.parameters.dir); } if (typeof this.move.parameters.until === "string") { switch (this.move.parameters.until) { case "half": this.heyParts = 4; break; case "full": this.heyParts = 8; break; // TODO Are these right? Can it sometimes be 1 or 3 instead of 2? case "less than half": this.heyParts = 2; break; case "between half and full": this.heyParts = 6; default: throw new Error("Unsupported hey 'until': " + this.move.parameters.until); } } else { // TODO Is this actually this simple? this.heyParts = this.move.parameters.until.time === 1 ? 2 : 6; //throw new Error("Unsupported hey 'until': " + this.move.parameters.until.dancer + " time " + this.move.parameters.until.time); } this.heyPartBeats = this.move.beats / this.heyParts; // TODO is this right? this.firstPassInCenter = dancerIsPair(this.move.parameters.who); this.centerShoulder = this.firstPassInCenter === this.move.parameters.shoulder ? Hand.Right : Hand.Left; this.endsShoulder = this.centerShoulder.opposite(); } buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter { return new HeySingleVariant(this, startingPos); } } moveInterpreters.set(moveName, Hey);