contra-renderer/www/js/interpreter.ts

339 lines
15 KiB
TypeScript

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<DancerIdentity, LowLevelMove[]> {
let currentVariants = new Map<string, MoveAsLowLevelMovesArgs>(startingVariants);
let currentVariantMoves: Map<string, LowLevelMovesForAllDancers> = new Map<string, LowLevelMovesForAllDancers>(
[...startingVariants.keys()].map(name => [name, new Map<DancerIdentity, LowLevelMove[]>(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<string, MoveAsLowLevelMovesArgs>();
currentVariantMoves = new Map<string, LowLevelMovesForAllDancers>();
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<DancerIdentity, LowLevelMove[]>();
const newPositions = new Map<DancerIdentity, SemanticPosition>();
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<DancerIdentity, SemanticPosition>([...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<DancerIdentity, SemanticPosition>([...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<DancerIdentity, SemanticPosition>([...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<DancerIdentity, SemanticPosition> {
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<DancerIdentity, SemanticPosition>([...handsFourImproper.entries()].map(
([id, pos]) => ([id, {...pos, which: pos.which.circleLeft(1)}])
));
case "Becket ccw":
return new Map<DancerIdentity, SemanticPosition>([...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<common.DancerIdentity, SemanticPosition>([
[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<string, MoveAsLowLevelMovesArgs>([
["Initial", { startingPos: defaultFormation }]
]);
} else {
// Handle various different starting position that all count as "improper" like short/long waves, etc.
return new Map<string, MoveAsLowLevelMovesArgs>([
["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<DancerIdentity, LowLevelMove[]>;
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;
}