forked from perelman/contra-renderer
439 lines
16 KiB
TypeScript
439 lines
16 KiB
TypeScript
import { CoupleRole, DanceRole, DancerIdentity, ExtendedDancerIdentity } from "../danceCommon.js";
|
|
import { BalanceWeight, CirclePosition, CircleSideOrCenter, DancerDistance, LongLines, PositionKind, SemanticPosition } from "../interpreterCommon.js";
|
|
import { Move, chooser_pairz } from "../libfigureMapper.js";
|
|
import { LowLevelMove, SemanticAnimation, SemanticAnimationKind } from "../lowLevelMove.js";
|
|
|
|
type MoveName = string & Move["move"];
|
|
export type MoveInterpreterCtor<N extends MoveName> = new (args: MoveInterpreterCtorArgs<N>) => MoveInterpreter<N>;
|
|
export const moveInterpreters: Map<MoveName, MoveInterpreterCtor<MoveName>> = new Map<MoveName, MoveInterpreterCtor<MoveName>>();
|
|
|
|
export interface MoveInterpreterCtorArgs<N extends MoveName> {
|
|
move: Move & { move: N };
|
|
nextMove?: Move;
|
|
numProgessions: number;
|
|
}
|
|
export type SemanticPositionsForAllDancers = Map<DancerIdentity, SemanticPosition>;
|
|
export interface MoveAsLowLevelMovesArgs {
|
|
startingPos: SemanticPositionsForAllDancers;
|
|
}
|
|
export type LowLevelMovesForAllDancers = Map<DancerIdentity, LowLevelMove[]>;
|
|
export interface Variant {
|
|
previousMoveVariant: string,
|
|
lowLevelMoves: LowLevelMovesForAllDancers,
|
|
};
|
|
export type VariantCollection = Map<string, Variant>;
|
|
|
|
export type AllVariantsForMoveArgs = Map<string, MoveAsLowLevelMovesArgs>;
|
|
|
|
export type PartialLowLevelMove = {
|
|
remarks?: string,
|
|
beats: number,
|
|
startPosition?: SemanticPosition,
|
|
endPosition: SemanticPosition,
|
|
movementPattern: SemanticAnimation,
|
|
};
|
|
|
|
export interface ISingleVariantMoveInterpreter {
|
|
moveAsVariants: (previousMoveVariant: string | undefined) => VariantCollection;
|
|
};
|
|
|
|
export abstract class SingleVariantMoveInterpreter<T extends MoveInterpreter<N>, N extends MoveName> implements ISingleVariantMoveInterpreter {
|
|
protected readonly moveInterpreter: T;
|
|
protected readonly startingPos: SemanticPositionsForAllDancers;
|
|
|
|
constructor(moveInterpreter: T, startingPos: SemanticPositionsForAllDancers) {
|
|
this.moveInterpreter = moveInterpreter;
|
|
this.startingPos = startingPos;
|
|
|
|
const checkDistance = !this.moveInterpreter.allowStartingClose();
|
|
const checkBalance = !this.moveInterpreter.allowStartingBalance();
|
|
const checkLongLines = !this.moveInterpreter.allowStartingLongLines();
|
|
if (checkBalance || checkDistance || checkLongLines) {
|
|
for (const [id, startPos] of this.startingPos) {
|
|
if (checkBalance && startPos.balance) {
|
|
throw new Error("Can not start " + this.move.move + " with balance weight " + startPos.balance);
|
|
}
|
|
if (checkDistance && startPos.dancerDistance && startPos.dancerDistance !== DancerDistance.Normal) {
|
|
throw new Error("Can not start " + this.move.move + " at dancerDistance " + startPos.dancerDistance);
|
|
}
|
|
if (checkLongLines && startPos.longLines) {
|
|
throw new Error("Can not start " + this.move.move + " at long lines " + startPos.longLines);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
get move() : Move & { move: N } {
|
|
return this.moveInterpreter.move;
|
|
}
|
|
|
|
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
|
throw new Error("You must implement either moveAsLowLevelMoves() or moveAsVariants().");
|
|
}
|
|
|
|
// TODO It would make more sense for previousMoveVariant to get passed into the constructor...
|
|
// ... but that requires touching every subclass, so it's an annoying refactor.
|
|
moveAsVariants(previousMoveVariant: string): VariantCollection {
|
|
return new Map<string, Variant>([
|
|
["default", { lowLevelMoves: this.moveAsLowLevelMoves(), previousMoveVariant }]
|
|
]);
|
|
}
|
|
|
|
static append(moves: LowLevelMove[],
|
|
newMove: LowLevelMove | PartialLowLevelMove
|
|
| ((prevEnd: SemanticPosition) => PartialLowLevelMove)): LowLevelMove[] {
|
|
const lastMove = moves.at(-1)!;
|
|
const prevEnd = lastMove.endPosition;
|
|
if (typeof newMove === 'function') {
|
|
newMove = newMove(prevEnd);
|
|
}
|
|
|
|
if (!newMove.startPosition) {
|
|
newMove.startPosition = prevEnd;
|
|
}
|
|
moves.push({
|
|
...newMove,
|
|
startPosition: newMove.startPosition ?? prevEnd,
|
|
move: lastMove.move,
|
|
startBeat: lastMove.startBeat + lastMove.beats,
|
|
});
|
|
|
|
return moves;
|
|
}
|
|
|
|
combine(moves: ((LowLevelMove | PartialLowLevelMove
|
|
| ((prevEnd: SemanticPosition) => PartialLowLevelMove))[]),
|
|
startPos?: SemanticPosition): LowLevelMove[] {
|
|
const res: LowLevelMove[] = [];
|
|
if (moves.length === 0) return res;
|
|
|
|
let firstMove = moves[0];
|
|
if ('move' in firstMove) {
|
|
res.push(firstMove);
|
|
} else {
|
|
if (typeof firstMove === 'function') {
|
|
firstMove = firstMove(startPos!);
|
|
}
|
|
|
|
res.push({...firstMove,
|
|
move: this.move,
|
|
startBeat: 0,
|
|
startPosition: firstMove.startPosition ?? startPos!,
|
|
});
|
|
}
|
|
|
|
for (const move of moves.slice(1)) {
|
|
SingleVariantMoveInterpreter.append(res, move);
|
|
}
|
|
|
|
if (res[0].startPosition === undefined) {
|
|
throw new Error("combine() called without a startPosition.");
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
findPairOpposite(who: chooser_pairz, id: DancerIdentity): ExtendedDancerIdentity | null {
|
|
const pos = this.getPosFor(id.asExtendedDancerIdentity());
|
|
|
|
const inSameSet = (proposedId: ExtendedDancerIdentity) => {
|
|
const proposedPos = this.getPosFor(proposedId);
|
|
return {
|
|
...proposedId,
|
|
// Get the same role dancer in the set the dancer is currently in.
|
|
relativeSet: proposedId.relativeSet + (pos.setOffset - proposedPos.setOffset)
|
|
}
|
|
}
|
|
|
|
switch (who) {
|
|
case "partners":
|
|
return id.partner().asExtendedDancerIdentity();
|
|
case "neighbors":
|
|
case "next neighbors":
|
|
case "3rd neighbors":
|
|
// TODO This isn't quite right... especially if it's "next" to intentionally progress...
|
|
return inSameSet(id.neighbor().asExtendedDancerIdentity());
|
|
// These three might get used when not with neighbors?
|
|
case "gentlespoons":
|
|
case "ladles":
|
|
case "same roles":
|
|
if (who === "gentlespoons" && id.danceRole === DanceRole.Robin
|
|
|| who === "ladles" && id.danceRole === DanceRole.Lark) {
|
|
return null;
|
|
}
|
|
return inSameSet(id.oppositeSameRole().asExtendedDancerIdentity());
|
|
case "ones":
|
|
if (id.coupleRole === CoupleRole.Twos) return null;
|
|
return id.partner().asExtendedDancerIdentity();
|
|
case "twos":
|
|
if (id.coupleRole === CoupleRole.Ones) return null;
|
|
return id.partner().asExtendedDancerIdentity();
|
|
case "shadows":
|
|
throw new Error("Not sure shadow is consistently the same.");
|
|
case "first corners":
|
|
case "second corners":
|
|
throw new Error("Contra corners are unsupported.");
|
|
default:
|
|
throw new Error("Unsupported who: " + who);
|
|
}
|
|
}
|
|
|
|
getPosFor(id: ExtendedDancerIdentity): SemanticPosition & { setOffset: number, lineOffset: number } {
|
|
const basePos = this.startingPos.get(id.setIdentity)!;
|
|
return {...basePos,
|
|
setOffset: (basePos.setOffset ?? 0) + id.relativeSet,
|
|
lineOffset: (basePos.lineOffset ?? 0) + id.relativeLine,
|
|
};
|
|
}
|
|
|
|
handleMove(dancerFunc: ((arg: { id: DancerIdentity, startPos: SemanticPosition }) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
|
|
const res = new Map<DancerIdentity, LowLevelMove[]>();
|
|
let anyProgressed = false;
|
|
for (const [id, startPos] of this.startingPos.entries()) {
|
|
const lowLevelMoves = dancerFunc({ id, startPos });
|
|
|
|
if (this.move.progression) {
|
|
const startingPos: SemanticPosition = lowLevelMoves.at(0)?.startPosition!;
|
|
const endPos: SemanticPosition = lowLevelMoves.at(-1)?.endPosition!;
|
|
if (startingPos.setOffset !== endPos.setOffset) {
|
|
anyProgressed = true;
|
|
}
|
|
}
|
|
|
|
res.set(id, lowLevelMoves);
|
|
}
|
|
|
|
if (this.move.progression && !anyProgressed) {
|
|
for (const [id, lowLevelMoves] of res.entries()) {
|
|
const startingPos: SemanticPosition = lowLevelMoves.at(0)?.startPosition!;
|
|
const endPos: SemanticPosition = lowLevelMoves.at(-1)?.endPosition!;
|
|
if (startingPos.setOffset === endPos.setOffset && endPos.kind === PositionKind.Circle) {
|
|
const endSetOffset = (endPos.setOffset ?? 0) + (endPos.which.isTop() ? -0.5 : +0.5);
|
|
const endWhich = endPos.which.swapUpAndDown();
|
|
lowLevelMoves[lowLevelMoves.length - 1] = {
|
|
...lowLevelMoves[lowLevelMoves.length - 1],
|
|
endPosition: {
|
|
...endPos,
|
|
setOffset: endSetOffset,
|
|
which: endWhich,
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
handleCircleMove(dancerFunc: ((arg: { id: DancerIdentity, startPos: SemanticPosition & { kind: PositionKind.Circle } }) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
|
|
return this.handleMove(({ id, startPos }) => {
|
|
if (startPos.kind !== PositionKind.Circle) {
|
|
throw new Error(this.move.move + " must start in a circle, but " + id + " is at " + startPos);
|
|
}
|
|
|
|
return dancerFunc({ id, startPos });
|
|
});
|
|
}
|
|
|
|
handlePairedMove(who: chooser_pairz, dancerFunc: ((arg: {
|
|
id: DancerIdentity,
|
|
startPos: SemanticPosition,
|
|
withPos: SemanticPosition & { setOffset: number, lineOffset: number },
|
|
withId: ExtendedDancerIdentity,
|
|
around: CircleSideOrCenter,
|
|
}) => LowLevelMove[]), meanwhileFunc?: ((arg: {
|
|
id: DancerIdentity,
|
|
startPos: SemanticPosition,
|
|
}) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
|
|
return this.handleMove(({ id, startPos }) => {
|
|
const withId = this.findPairOpposite(who, id);
|
|
if (!withId) {
|
|
if (meanwhileFunc) {
|
|
return meanwhileFunc({ id, startPos });
|
|
} else {
|
|
return this.combine([{
|
|
beats: this.move.beats,
|
|
startPosition: { ...startPos, hands: undefined },
|
|
endPosition: { ...startPos, hands: undefined },
|
|
// TODO Not sure this is actually a good default...
|
|
movementPattern: { kind: SemanticAnimationKind.StandStill },
|
|
}]);
|
|
}
|
|
}
|
|
const withPos = this.getPosFor(withId);
|
|
const setDifference = withPos.setOffset - (startPos.setOffset ?? 0);
|
|
let startPosAdjusted = startPos;
|
|
if (setDifference !== 0) {
|
|
// TODO Can move be with a different short line or just a different circle?
|
|
// PassBy can probably be with the next short line...
|
|
if (startPos.kind === PositionKind.Circle && (setDifference === 1 || setDifference === -1)) {
|
|
startPosAdjusted = {
|
|
...startPos,
|
|
setOffset: (startPos.setOffset ?? 0) + setDifference / 2,
|
|
which: startPos.which.swapUpAndDown(),
|
|
}
|
|
} else {
|
|
throw new Error("Not near dancer to " + this.move.move + " with.");
|
|
}
|
|
}
|
|
const startWhich = startPosAdjusted.which;
|
|
// TODO Can swing be across the set (top or bottom)?
|
|
const around = withPos.which.leftRightSide() === startWhich.leftRightSide()
|
|
? startWhich.leftRightSide()
|
|
: withPos.kind === PositionKind.Circle
|
|
? (startWhich instanceof CirclePosition && withPos.which.topBottomSide() === startWhich.topBottomSide()
|
|
? startWhich.topBottomSide()
|
|
: "Center")
|
|
: "Center";
|
|
|
|
return dancerFunc({ id, startPos: startPosAdjusted, withId, withPos, around });
|
|
});
|
|
}
|
|
|
|
handleCirclePairedMove(who: chooser_pairz, dancerFunc: ((arg: {
|
|
id: DancerIdentity,
|
|
startPos: SemanticPosition & { kind: PositionKind.Circle },
|
|
withPos: SemanticPosition & { setOffset: number, lineOffset: number },
|
|
withId: ExtendedDancerIdentity,
|
|
around: CircleSideOrCenter,
|
|
}) => LowLevelMove[]), meanwhileFunc?: ((arg: {
|
|
id: DancerIdentity,
|
|
startPos: SemanticPosition & { kind: PositionKind.Circle },
|
|
}) => LowLevelMove[])): Map<DancerIdentity, LowLevelMove[]> {
|
|
return this.handlePairedMove(who, ({ id, startPos, withId, withPos, around }) => {
|
|
if (startPos.kind !== PositionKind.Circle) {
|
|
throw new Error(this.move.move + " must start in a circle, but " + id + " is at " + startPos);
|
|
}
|
|
|
|
return dancerFunc({ id, startPos, withId, withPos, around });
|
|
}, meanwhileFunc ? ({id, startPos}) => {
|
|
if (startPos.kind !== PositionKind.Circle) {
|
|
throw new Error(this.move.move + " must start in a circle, but " + id + " is at " + startPos);
|
|
}
|
|
|
|
return meanwhileFunc({id, startPos});
|
|
} : undefined);
|
|
}
|
|
|
|
errorStandStill(error?: string) {
|
|
return this.handleMove(({ startPos }) => {
|
|
return [{
|
|
interpreterError: error ?? ("UNKNOWN MOVE '" + this.move.move + "': standing still"),
|
|
move: this.move,
|
|
startBeat: 0,
|
|
beats: this.move.beats,
|
|
startPosition: startPos,
|
|
endPosition: startPos,
|
|
movementPattern: {
|
|
kind: SemanticAnimationKind.StandStill,
|
|
},
|
|
}];
|
|
});
|
|
}
|
|
}
|
|
|
|
export abstract class MoveInterpreter<N extends MoveName> {
|
|
public readonly move: Move & { move: N };
|
|
public readonly nextMove?: Move;
|
|
public readonly numProgressions: number;
|
|
|
|
constructor({ move, nextMove, numProgessions }: MoveInterpreterCtorArgs<N>) {
|
|
this.move = move;
|
|
this.nextMove = nextMove; // TODO Should be able to get rid of this using variants.
|
|
this.numProgressions = numProgessions;
|
|
}
|
|
|
|
allowStartingClose(): boolean {
|
|
// Swings can end close, but most moves can't start close, so do this check by default for all moves.
|
|
return false;
|
|
}
|
|
|
|
allowStartingBalance(): boolean {
|
|
// Make moves that support balance beforehand explicitly opt-in.
|
|
return false;
|
|
}
|
|
|
|
allowStartingLongLines(): boolean {
|
|
// Long lines is used for tweaking positioning; most moves don't know about it.
|
|
return false;
|
|
}
|
|
|
|
abstract buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter;
|
|
|
|
// TODO Better name?
|
|
moveAsVariants({ startingPos }: MoveAsLowLevelMovesArgs, previousMoveVariant: string): VariantCollection {
|
|
return this.buildSingleVariantMoveInterpreter(startingPos).moveAsVariants(previousMoveVariant);
|
|
}
|
|
|
|
allVariantsForMove(args: AllVariantsForMoveArgs): VariantCollection {
|
|
const res = new Map<string, Variant>();
|
|
let error;
|
|
|
|
for (const [variantName, variantArgs] of args.entries()) {
|
|
let newVariants: VariantCollection;
|
|
try {
|
|
newVariants = this.moveAsVariants(variantArgs, variantName);
|
|
} catch (ex) {
|
|
// TODO Maybe have a way to distinguish invalid start from error processing move?
|
|
error = ex;
|
|
// If this variant can't be processed, just continue.
|
|
continue;
|
|
}
|
|
|
|
for (const [newVariantName, variant] of newVariants) {
|
|
let combinedVariantName: string;
|
|
if (args.size === 1) {
|
|
combinedVariantName = newVariantName;
|
|
} else if (newVariants.size === 1) {
|
|
combinedVariantName = variantName;
|
|
} else {
|
|
combinedVariantName = newVariantName + "_from_" + variantName;
|
|
}
|
|
|
|
res.set(combinedVariantName, variant);
|
|
}
|
|
}
|
|
|
|
if (res.size === 0) throw error;
|
|
else if (res.size > 1) {
|
|
// TODO Try to reduce variants if possible.
|
|
// XXX TODO Simple hack: perfer starting improper in a circle.
|
|
if ([...res.values()].find(v => v.previousMoveVariant === "InitialCircle")) {
|
|
for (const key of [...res.entries()].filter(([k, v]) => v.previousMoveVariant !== "InitialCircle").map(([k, v]) => k)) {
|
|
res.delete(key);
|
|
}
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
}
|
|
|
|
class ErrorSingleVariantMoveInterpreter extends SingleVariantMoveInterpreter<ErrorMoveInterpreter, MoveName> {
|
|
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
|
return this.errorStandStill(this.moveInterpreter.error);
|
|
}
|
|
}
|
|
export class ErrorMoveInterpreter extends MoveInterpreter<MoveName> {
|
|
public readonly error?: string;
|
|
|
|
constructor(args: MoveInterpreterCtorArgs<MoveName>, error?: string) {
|
|
super(args);
|
|
this.error = error;
|
|
}
|
|
|
|
override allowStartingBalance(): boolean {
|
|
return true;
|
|
}
|
|
override allowStartingClose(): boolean {
|
|
return true;
|
|
}
|
|
override allowStartingLongLines(): boolean {
|
|
return true;
|
|
}
|
|
|
|
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
|
|
//throw new Error("Method not implemented.");
|
|
return new ErrorSingleVariantMoveInterpreter(this, startingPos);
|
|
}
|
|
}
|
|
export const errorMoveInterpreterCtor: MoveInterpreterCtor<MoveName> = ErrorMoveInterpreter;Error |