import * as animation from "./animation.js"; import { CoupleRole, DanceRole, DancerIdentity } from "./danceCommon.js"; import * as common from "./danceCommon.js"; import { Hand, setDistance } from "./rendererConstants.js"; import { nameLibFigureParameters, Move, chooser_pairz } from "./libfigureMapper.js"; import { LowLevelMove, SemanticAnimation, SemanticAnimationKind, animateFromLowLevelMoves } from "./lowLevelMove.js"; import { BalanceWeight, CirclePosition, CircleSide, CircleSideOrCenter, DancerDistance, Facing, HandConnection, HandTo, LongLines, PositionKind, SemanticPosition, ShortLinesPosition, StarGrip, handToDancerToSideInCircleFacingAcross, handsFourImproper, handsInCircle, handsInLine, handsInShortLine, oppositeFacing } from "./interpreterCommon.js"; import { ContraDBDance } from "./danceLibrary.js"; import { AllVariantsForMoveArgs, ErrorMoveInterpreter, LowLevelMovesForAllDancers, MoveAsLowLevelMovesArgs, SemanticPositionsForAllDancers, Variant, VariantCollection, errorMoveInterpreterCtor, moveInterpreters } from "./moves/_moveInterpreter.js"; // Import all individual move files. // ls [a-z]*.js | sed 's@.*@import "./moves/\0";@' import "./moves/allemande.js"; import "./moves/balance.js"; import "./moves/balanceTheRing.js"; import "./moves/boxCirculate.js"; import "./moves/boxTheGnat.js"; import "./moves/butterflyWhirl.js"; import "./moves/californiaTwirl.js"; import "./moves/chain.js"; import "./moves/circle.js"; import "./moves/custom.js"; import "./moves/doSiDo.js"; import "./moves/downTheHall.js"; import "./moves/formAnOceanWave.js"; import "./moves/formLongWaves.js"; import "./moves/giveTake.js"; import "./moves/hey.js"; import "./moves/longLines.js"; import "./moves/madRobin.js"; import "./moves/passBy.js"; import "./moves/passThrough.js"; import "./moves/petronella.js"; import "./moves/promenade.js"; import "./moves/pullByDancers.js"; import "./moves/revolvingDoor.js"; import "./moves/rightLeftThrough.js"; import "./moves/rollAway.js"; import "./moves/roryOMore.js"; import "./moves/slice.js"; import "./moves/slideAlongSet.js"; import "./moves/star.js"; import "./moves/starPromenade.js"; import "./moves/swing.js"; import "./moves/turnAlone.js"; import "./moves/upTheHall.js"; // TODO Get rid of nextMove here once variants are actually supported by Swing. function allVariantsForMove({ move, nextMove, startingVariants, numProgessions }: { move: Move; nextMove: Move; startingVariants: AllVariantsForMoveArgs; numProgessions: number; }): VariantCollection { const moveInterpreter = moveInterpreters.get(move.move) ?? errorMoveInterpreterCtor; return new moveInterpreter({ move, nextMove, numProgessions }).allVariantsForMove(startingVariants); } function danceAsLowLevelMoves(moves: Move[], startingVariants: AllVariantsForMoveArgs): Map { let currentVariants = new Map(startingVariants); let currentVariantMoves: Map = new Map( [...startingVariants.keys()].map(name => [name, new Map(DancerIdentity.all().map(id => [id, []]))])); let numProgessions = 0; for (let i = 0; i < moves.length; i++) { const move = moves[i]; const nextMove = i === moves.length - 1 ? moves[0] : moves[i + 1]; function updateVariants(newVariants: VariantCollection) { let previousVariants = currentVariants; let previousVariantMoves = currentVariantMoves; try { currentVariants = new Map(); currentVariantMoves = new Map(); variant: for (const [name, variant] of newVariants) { const oldMoves = previousVariantMoves.get(variant.previousMoveVariant); const oldPos = previousVariants.get(variant.previousMoveVariant); if (!oldMoves || !oldPos) { throw new Error("Referenced unknown previous variant: " + variant.previousMoveVariant); } const newMoves: LowLevelMovesForAllDancers = new Map(); const newPositions = new Map(); for (const [id, newMoveList] of variant.lowLevelMoves.entries()) { const prevEnd = oldPos.startingPos.get(id)!; const newStart = newMoveList[0].startPosition; if ((prevEnd.setOffset ?? 0) != (newStart.setOffset ?? 0) || (prevEnd.lineOffset ?? 0) != (newStart.lineOffset ?? 0) || prevEnd.kind != newStart.kind || prevEnd.which != newStart.which || prevEnd.balance != newStart.balance || prevEnd.dancerDistance != newStart.dancerDistance) { // TODO Should this be considered a bug? // TODO Require more properties to match? facing? probably not hands. //throw new Error("Moves start/end did not line up."); continue variant; } newMoves.set(id, [...oldMoves.get(id)!, ...newMoveList]); newPositions.set(id, newMoveList.at(-1)!.endPosition); } currentVariants.set(name, { startingPos: newPositions }); currentVariantMoves.set(name, newMoves); } } catch (ex) { currentVariants = previousVariants; currentVariantMoves = previousVariantMoves; throw ex; } if (currentVariantMoves.size === 0) { currentVariants = previousVariants; currentVariantMoves = previousVariantMoves; throw new Error("All variants were eliminated as possibilities."); } } try { if (i > 0 && move.beats === 0 && move.move === "slide along set") { const slideLeft: boolean = move.parameters.slide; for (const [name, currentPos] of currentVariants.entries()) { for (const [id, currPos] of currentPos.startingPos.entries()) { const slideAmount = (currPos.which.leftRightSide() === CircleSide.Left) === slideLeft ? +0.5 : -0.5; const setOffset = (currPos.setOffset ?? 0) + slideAmount; currentPos.startingPos.set(id, { ...currPos, setOffset }); const prevMove = currentVariantMoves.get(name)!.get(id)!.at(-1)!; prevMove.movementPattern.setSlideAmount = slideAmount; prevMove.endPosition.setOffset = setOffset; } } } else { const newMoves = allVariantsForMove({ move, nextMove, startingVariants: currentVariants, numProgessions }); if (newMoves.size === 0) { updateVariants(new ErrorMoveInterpreter({ move, nextMove, numProgessions }, "ERROR: " + move.move + " has no valid starting variants.").allVariantsForMove(currentVariants)); } else { updateVariants(newMoves); } if (currentVariantMoves.size === 0) { throw new Error("Variants selection somehow went down to zero?"); } } } catch (ex) { if (currentVariantMoves.size === 0) throw ex; // catch exception so something can be displayed const errorMessage: string = ex instanceof Error ? ex.message : ex; const errorVariants = new ErrorMoveInterpreter({ move, nextMove, numProgessions }, errorMessage).allVariantsForMove(currentVariants); updateVariants(errorVariants); } if (move.progression) numProgessions++; } // If there's multiple variants, check if there's fewer that flow into the first move. if (currentVariantMoves.size !== 1) { let newMoves: VariantCollection | undefined; try { newMoves = allVariantsForMove({ move: moves[0], nextMove: moves[1], startingVariants: currentVariants, numProgessions }); } catch { newMoves = undefined; } if (newMoves) { const firstMoveVariants = [...newMoves.values()].map(v => v.previousMoveVariant); if (firstMoveVariants.length > 0) { for (const variant of [...currentVariantMoves.keys()]) { if (!firstMoveVariants.includes(variant)) { currentVariantMoves.delete(variant); } } } } } const res = [...currentVariantMoves.values()].at(-1)!; if (currentVariantMoves.size !== 1) { res.get(DancerIdentity.OnesRobin)![0].interpreterError = "Expected exactly one variant. Found " + currentVariantMoves.size + ": " + [...currentVariantMoves.keys()].join(", "); } try { const progression = animateFromLowLevelMoves(res).progression; const progressionInSets = progression.y / setDistance; // fixup end positions to match start of next move for (const [id, lowLevelMoves] of res.entries()) { for (let i = 0; i < lowLevelMoves.length - 1; i++) { if (!lowLevelMoves[i].endPosition) throw "endPosition is undefined"; lowLevelMoves[i].endPosition = lowLevelMoves[i + 1].startPosition; if (!lowLevelMoves[i].endPosition) throw "endPosition is undefined now"; // TODO Exactly what is the StandStill fixup needed for? Can it be handled in other ways? Should it be rotation only? /* if (lowLevelMoves[i].movementPattern.kind === SemanticAnimationKind.StandStill) { lowLevelMoves[i].startPosition = lowLevelMoves[i].endPosition; if (i > 0) { lowLevelMoves[i - 1].endPosition = lowLevelMoves[i].startPosition; } } */ } // If progression isn't detected properly, do nothing. if (progressionInSets === 0) { lowLevelMoves[lowLevelMoves.length - 1].interpreterError = "No progression detected. Not lining up end with start of dance."; } else { const startPos = lowLevelMoves[0].startPosition; lowLevelMoves[lowLevelMoves.length - 1].endPosition = { ...lowLevelMoves[0].startPosition, // progressed setOffset: (startPos.setOffset ?? 0) + (id.coupleRole == CoupleRole.Ones ? 1 : -1) * progressionInSets, }; } } return res; } catch (ex) { res.get(DancerIdentity.OnesLark)![0].interpreterError = "Error detecting progression: " + (ex instanceof Error ? ex.message : ex); return res; } } function improperShortWaves(slideTo: Hand) { return new Map([...handsFourImproper.entries()].map( ([id, pos]) => ([id, { kind: PositionKind.ShortLines, which: pos.which.toShortLines(slideTo), facing: pos.which.facingUpOrDown(), }]) )); } function improperUnfoldToShortLines(center: CircleSide.Bottom | CircleSide.Top) { return new Map([...handsFourImproper.entries()].map( ([id, pos]) => ([id, { kind: PositionKind.ShortLines, which: pos.which.unfoldToShortLines(center), facing: center === CircleSide.Top ? Facing.Down : Facing.Up, }]) )); } function improperLongWaves(facingOut: DanceRole) { return new Map([...handsFourImproper.entries()].map( ([id, pos]) => ([id, { ...pos, facing: id.danceRole === facingOut ? pos.which.facingOut() : pos.which.facingAcross(), hands: handsInLine({ wavy: true, which: pos.which, facing: undefined }) }]) )); } function StartingPosForFormation(formation: common.StartFormation, dance?: ContraDBDance): Map { const preamble = dance?.preamble ?? ""; if (preamble.includes("Starts in short waves, right hands to neighbors")) { return improperShortWaves(Hand.Left); } else if (preamble.includes("face down the hall in lines of 4, 1s splitting the 2s")) { return improperUnfoldToShortLines(CircleSide.Top); } else if (preamble.includes("Long waves, larks face out")) { return improperLongWaves(DanceRole.Lark); } switch (formation) { case "improper": return handsFourImproper; case "Becket": return new Map([...handsFourImproper.entries()].map( ([id, pos]) => ([id, {...pos, which: pos.which.circleLeft(1)}]) )); case "Becket ccw": return new Map([...handsFourImproper.entries()].map( ([id, pos]) => ([id, {...pos, which: pos.which.circleRight(1)}]) )); case "Sawtooth Becket": // Dancers start becket, then slide one person to the right. https://contradb.com/dances/848 // TODO Not sure this is right. return new Map([ [DancerIdentity.OnesLark, { kind: PositionKind.Circle, which: CirclePosition.BottomLeft, facing: Facing.CenterOfCircle, hands: handsInCircle, }], [DancerIdentity.OnesRobin, { kind: PositionKind.Circle, which: CirclePosition.TopLeft, 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.TopRight, facing: Facing.CenterOfCircle, hands: handsInCircle, setOffset: 1, }], ]); } } function startingVariantsForFormation(formation: common.StartFormation, dance?: ContraDBDance) : AllVariantsForMoveArgs { const defaultFormation = StartingPosForFormation(formation, dance); if (formation !== "improper" || defaultFormation !== handsFourImproper) { return new Map([ ["Initial", { startingPos: defaultFormation }] ]); } else { // Handle various different starting position that all count as "improper" like short/long waves, etc. return new Map([ ["InitialCircle", { startingPos: handsFourImproper }], ["InitialShortWavesLeft", { startingPos: improperShortWaves(Hand.Left) }], ["InitialShortWavesRight", { startingPos: improperShortWaves(Hand.Right) }], ["InitialShortLinesDown", { startingPos: improperUnfoldToShortLines(CircleSide.Top) }], ["InitialShortLinesUp", { startingPos: improperUnfoldToShortLines(CircleSide.Bottom) }], ["InitialLongWavesLarksOut", { startingPos: improperLongWaves(DanceRole.Lark) }], ["InitialLongWavesRobinsOut", { startingPos: improperLongWaves(DanceRole.Robin) }], ]); } } export let mappedDance: Move[]; export let interpretedDance: Map; export let interpretedAnimation: animation.Animation; export function loadDance(dance: ContraDBDance): animation.Animation { mappedDance = dance.figures.map(nameLibFigureParameters); interpretedDance = danceAsLowLevelMoves(mappedDance, startingVariantsForFormation(dance.start_type, dance)); interpretedAnimation = animateFromLowLevelMoves(interpretedDance); return interpretedAnimation; }