import { DancerIdentity } from "../danceCommon.js"; import { SemanticPosition, PositionKind, ShortLinesPosition, CirclePosition, CircleSide, Facing } from "../interpreterCommon.js"; import { Move } from "../libfigureMapper.js"; import { LowLevelMove, SemanticAnimationKind } from "../lowLevelMove.js"; import { Hand } from "../rendererConstants.js"; import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js"; import { dancerIsPair } from "../libfigure/util.js"; const moveName: Move["move"] = "hey"; class HeySingleVariant extends SingleVariantMoveInterpreter { moveAsLowLevelMoves(): LowLevelMovesForAllDancers { // Needed for inner functions... that probably should be methods. const move = this.move; type HeyStep = { kind: "StandStill" | "Loop" | "CenterPass" | "EndsPassIn" | "EndsPassOut" | "Ricochet", endPosition: SemanticPosition, } if (this.move.parameters.dir !== "across") { throw new Error("Unsupported hey direction: " + this.move.parameters.dir); } let heyParts: number; switch (this.move.parameters.until) { case "half": heyParts = 4; break; case "full": heyParts = 8; break; default: throw new Error("Unsupported hey 'until': " + this.move.parameters.until); } const heyPartBeats: number = this.move.beats / heyParts; // TODO is this right? const firstPassInCenter: boolean = dancerIsPair(this.move.parameters.who); const centerShoulder = firstPassInCenter === this.move.parameters.shoulder ? Hand.Right : Hand.Left; const endsShoulder = centerShoulder.opposite(); 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; } return fixupHeyOtherPath(this.handleMove(({ id, startPos }) => { const endsInCircle = startPos.kind === PositionKind.Circle; function heyStepToPartialLowLevelMove(heyStep: HeyStep): PartialLowLevelMove & { heyStep: HeyStep } { return { beats: 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: 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: endsShoulder, } : { kind: SemanticAnimationKind.PassBy, around: heyStep.kind === "CenterPass" ? "Center" : heyStep.endPosition.which.leftRightSide(), withHands: false, side: heyStep.kind === "CenterPass" ? centerShoulder : endsShoulder, facing: "Start", otherPath: undefined!, // Placeholder, fixup later. }, heyStep, }; } function continueHey(prevStep: HeyStep, stepsLeft: number, beenInCenter: boolean): HeyStep { // TODO Not sure why type checker requires rechecking this here. if (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() ? (endsShoulder === Hand.Right ? CirclePosition.TopLeft : CirclePosition.BottomLeft) : (endsShoulder === Hand.Right ? CirclePosition.BottomRight : CirclePosition.TopRight), }, } } else if (!isFacingSide) { const rico = inCenterFirst ? beenInCenter ? move.parameters.rico3 : move.parameters.rico1 : beenInCenter ? move.parameters.rico4 : 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 (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); } } const inCenterFirst = 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 (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 < heyParts; i++) { const isLast = i === heyParts - 1; const nextHeyStep = continueHey(heySteps[i - 1], heyParts - i - 1, beenInCenter); beenInCenter ||= nextHeyStep.kind === "CenterPass" || nextHeyStep.kind === "Ricochet"; heySteps.push(nextHeyStep); } return this.combine(heySteps.map(heyStepToPartialLowLevelMove), { ...startingPos, hands: undefined }); })); } } class Hey extends MoveInterpreter { buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter { return new HeySingleVariant(this, startingPos); } } moveInterpreters.set(moveName, Hey);