Compare commits

...

4 Commits

14 changed files with 241 additions and 40 deletions

View File

@ -140,6 +140,26 @@ function danceAsLowLevelMoves(moves: Move[], startingVariants: AllVariantsForMov
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 "
@ -156,12 +176,15 @@ function danceAsLowLevelMoves(moves: Move[], startingVariants: AllVariantsForMov
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) {

View File

@ -349,6 +349,10 @@ export class ShortLinesPosition {
return this.isLeft() ? Facing.Right : Facing.Left;
}
public isToLeftOf(otherPos: ShortLinesPosition): boolean {
return this.enumValue < otherPos.enumValue;
}
public toString() : string {
return this.enumValue.toString();
}
@ -519,4 +523,84 @@ export function handToDancerToSideInCircleFacingUpOrDown(which: CirclePosition):
? [Hand.Right, { hand: Hand.Left, to: HandTo.DancerRight }]
: [Hand.Left, { hand: Hand.Right, to: HandTo.DancerLeft }]
]);
}
export function facingAdjacent(pos: SemanticPosition, otherPos: SemanticPosition): Facing | undefined {
if (pos.kind !== otherPos.kind) {
return undefined;
}
if (pos.kind === PositionKind.ShortLines) {
if (otherPos.kind !== PositionKind.ShortLines) {
return undefined;
}
if ((pos.setOffset ?? 0) !== (otherPos.setOffset ?? 0)) {
return undefined;
}
if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) {
if (pos.which === ShortLinesPosition.FarLeft && otherPos.which === ShortLinesPosition.FarRight
&& ((pos.lineOffset ?? 0) - 1) === (otherPos.lineOffset ?? 0)) {
return Facing.Left;
} else if (pos.which === ShortLinesPosition.FarRight && otherPos.which === ShortLinesPosition.FarLeft
&& ((pos.lineOffset ?? 0) + 1) === (otherPos.lineOffset ?? 0)) {
return Facing.Right;
} else {
return undefined;
}
}
if (otherPos.which.isToLeftOf(pos.which)) {
return Facing.Left;
} else {
return Facing.Right;
}
} else if (pos.kind === PositionKind.Circle) {
if (otherPos.kind !== PositionKind.Circle) {
return undefined;
}
if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) {
// TODO
return undefined;
}
if ((pos.setOffset ?? 0) !== (otherPos.setOffset ?? 0)) {
// TODO
return undefined;
}
if (pos.which.leftRightSide() === otherPos.which.leftRightSide()) {
if (pos.which.topBottomSide() === otherPos.which.topBottomSide()) {
return undefined;
}
if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) {
return undefined;
}
if ((pos.setOffset ?? 0) === (otherPos.setOffset ?? 0)) {
return pos.which.facingUpOrDown();
} else if (((pos.setOffset ?? 0) + 1) === (otherPos.setOffset ?? 0) && pos.which.isTop()) {
return Facing.Up;
} else if (((pos.setOffset ?? 0) - 1) === (otherPos.setOffset ?? 0) && !pos.which.isTop()) {
return Facing.Down;
}
} else if (pos.which.topBottomSide() === otherPos.which.topBottomSide()) {
if ((pos.setOffset ?? 0) === (otherPos.setOffset ?? 0)) {
return undefined;
}
if ((pos.lineOffset ?? 0) !== (otherPos.lineOffset ?? 0)) {
return pos.which.facingAcross();
} else if (((pos.lineOffset ?? 0) + 1) === (otherPos.lineOffset ?? 0) && pos.which.isLeft()) {
return Facing.Left;
} else if (((pos.lineOffset ?? 0) - 1) === (otherPos.lineOffset ?? 0) && !pos.which.isLeft()) {
return Facing.Right;
}
} else {
// Opposite corners of circle.
return undefined;
}
} else {
throw new Error("Unexpected PositionKind: " + otherPos.kind);
}
}

View File

@ -77,6 +77,8 @@ export type SemanticAnimation = {
// If true, move in close while rotating.
close: boolean,
facing?: animation.RotationAnimationFacing,
} | {
kind: SemanticAnimationKind.Swing,
@ -292,7 +294,7 @@ function SemanticToSetPosition(semantic: SemanticPosition): DancerSetPosition {
&& (semantic.balance === BalanceWeight.Backward || semantic.balance === BalanceWeight.Forward)) {
balanceOffset = {
x: 0,
y: position.y * (
y: Math.sign(position.y) * (
semantic.balance === BalanceWeight.Forward
? -balanceAmount
: balanceAmount)
@ -352,9 +354,9 @@ function SemanticToSetPosition(semantic: SemanticPosition): DancerSetPosition {
case HandTo.DancerRight:
return OffsetPlus({ x: +1, y: 0 }, balanceHandAdjustment);
case HandTo.DancerForward:
const armLength = yAmount + (semantic.balance === BalanceWeight.Backward ? balanceAmount : 0);
const armLength = yAmount + (semantic.balance === BalanceWeight.Backward ? balanceAmount : semantic.balance === BalanceWeight.Forward ? -balanceAmount : 0);
if (hand === connection.hand) {
return { x: 0, y: +armLength/2 };
return { x: 0, y: +armLength };
} else {
return { x: dancerWidth / 2 * (hand === Hand.Left ? -1 : +1), y: +armLength };
}
@ -655,7 +657,7 @@ function animateLowLevelMoveWithoutSlide(move: LowLevelMove): animation.Animatio
width: 1,
height: 1,
},
facing: animation.RotationAnimationFacing.Forward,
facing: move.movementPattern.facing ?? animation.RotationAnimationFacing.Forward,
closer: move.movementPattern.close ? {
transitionBeats: 1,
minDistance: 1,

View File

@ -428,7 +428,7 @@ displaySettingsDiv.appendChild(debugRenderLabel);
wrapperDiv.appendChild(displaySettingsDiv);
// Default dance is Two Hearts in Time by Isaac Banner. Selected arbitrarily.
const defaultDanceTitle = "Two Hearts in Time";
const defaultDanceTitle = "March for Andrea";
const danceList = document.createElement('select');
for (const [idx, dance] of danceLibrary.dances.entries()) {

View File

@ -1,5 +1,5 @@
import { CoupleRole, DanceRole, DancerIdentity, ExtendedDancerIdentity } from "../danceCommon.js";
import { CirclePosition, CircleSideOrCenter, PositionKind, SemanticPosition } from "../interpreterCommon.js";
import { CirclePosition, CircleSideOrCenter, DancerDistance, PositionKind, SemanticPosition } from "../interpreterCommon.js";
import { Move, chooser_pairz } from "../libfigureMapper.js";
import { LowLevelMove, SemanticAnimation, SemanticAnimationKind } from "../lowLevelMove.js";
@ -44,12 +44,25 @@ export abstract class SingleVariantMoveInterpreter<T extends MoveInterpreter<N>,
constructor(moveInterpreter: T, startingPos: SemanticPositionsForAllDancers) {
this.moveInterpreter = moveInterpreter;
this.startingPos = startingPos;
if (!this.allowStartingClose()) {
for (const [id, startPos] of this.startingPos) {
if (startPos.dancerDistance && startPos.dancerDistance !== DancerDistance.Normal) {
throw new Error("Can not start " + this.move.move + " at dancerDistance " + startPos.dancerDistance);
}
}
}
}
get move() : Move & { move: N } {
return this.moveInterpreter.move;
}
allowStartingClose(): boolean {
// Swings can end close, but most moves can't start close, so do this check by default for all moves.
return false;
}
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
throw new Error("You must implement either moveAsLowLevelMoves() or moveAsVariants().");
}

View File

@ -1,39 +1,101 @@
import { BalanceWeight } from "../interpreterCommon.js";
import { BalanceWeight, Facing, HandConnection, HandTo, PositionKind, facingAdjacent } from "../interpreterCommon.js";
import { SemanticAnimationKind } from "../lowLevelMove.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, MoveInterpreterCtorArgs, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
import { Hand } from "../rendererConstants.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, MoveInterpreterCtorArgs, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, Variant, VariantCollection, moveInterpreters } from "./_moveInterpreter.js";
class BalanceSingleVariant extends SingleVariantMoveInterpreter<Balance, "balance"> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
return this.handleMove(({ startPos }) => {
// TODO Use who to determine facing?
// TODO Could be left to right, not back and forth?
// TODO How to determine hand... by next move, I guess?
private static readonly balanceOptions : [BalanceWeight, BalanceWeight | undefined, Hand | undefined][]= [
// TODO Any others?
[BalanceWeight.Forward, undefined, undefined],
[BalanceWeight.Forward, BalanceWeight.Backward, undefined],
// TODO Should be left/right/inside/outside hands?
[BalanceWeight.Forward, BalanceWeight.Backward, Hand.Left],
[BalanceWeight.Forward, BalanceWeight.Backward, Hand.Right],
[BalanceWeight.Right, BalanceWeight.Backward, undefined],
[BalanceWeight.Right, BalanceWeight.Left, undefined],
[BalanceWeight.Left, BalanceWeight.Right, undefined],
];
return this.combine([
{
beats: this.moveInterpreter.forwardBeats,
endPosition: { ...startPos, balance: BalanceWeight.Forward },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
{
beats: this.moveInterpreter.backwardBeats,
endPosition: { ...startPos, balance: BalanceWeight.Backward },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
], startPos);
});
moveAsVariants(previousMoveVariant: string): VariantCollection {
const res = new Map<string, Variant>();
for (const [firstWeight, secondWeight, hand] of BalanceSingleVariant.balanceOptions) {
// If balancing someone, need to know by which hand.
if ((hand === undefined) !== (this.move.parameters.who === "everyone")) continue;
try {
res.set((firstWeight?.toString() ?? "") + (secondWeight?.toString() ?? "") + (hand === undefined ? "" : hand.toString() + "Hand"), {
lowLevelMoves: this.moveAsLowLevelMovesWeights(firstWeight, secondWeight, hand),
previousMoveVariant
});
}
catch { }
}
return res;
}
moveAsLowLevelMovesWeights(firstWeight?: BalanceWeight, secondWeight?: BalanceWeight, hand?: Hand): LowLevelMovesForAllDancers {
if (this.move.parameters.who !== "everyone") {
return this.handlePairedMove(this.move.parameters.who, ({ startPos, withPos }) => {
// TODO Does this need to support balancing inside/outside hands? If so how to identify them?
const hands = hand === undefined ? startPos.hands : new Map<Hand, HandConnection>([[hand, {hand, to: HandTo.DancerForward}]]);
const facing = facingAdjacent(startPos, withPos);
if (facing === undefined) {
throw new Error("Not adjacent to paired dancer.");
}
const startingPos = {...startPos, facing, hands};
return this.combine([
{
beats: this.moveInterpreter.balancePartBeats,
endPosition: { ...startingPos, balance: firstWeight, hands },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
{
beats: this.moveInterpreter.balancePartBeats,
endPosition: { ...startingPos, balance: secondWeight, hands },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
], startingPos);
});
} else {
return this.handleMove(({ startPos }) => {
// TODO Use who to determine facing?
// TODO Could be left to right, not back and forth?
// TODO How to determine hand... by next move, I guess?
if (startPos.kind === PositionKind.Circle && startPos.facing === Facing.CenterOfCircle) {
if (firstWeight === BalanceWeight.Left || firstWeight === BalanceWeight.Right || firstWeight === BalanceWeight.Backward
|| secondWeight === BalanceWeight.Left || secondWeight === BalanceWeight.Right || secondWeight === BalanceWeight.Backward) {
throw new Error("Balancing left or right in a circle is unsupported.");
}
}
return this.combine([
{
beats: this.moveInterpreter.balancePartBeats,
endPosition: { ...startPos, balance: firstWeight },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
{
beats: this.moveInterpreter.balancePartBeats,
endPosition: { ...startPos, balance: secondWeight },
movementPattern: { kind: SemanticAnimationKind.Linear },
},
], startPos);
});
}
}
}
class Balance extends MoveInterpreter<"balance"> {
public readonly forwardBeats: number;
public readonly backwardBeats: number;
public readonly balancePartBeats: number;
constructor(args: MoveInterpreterCtorArgs<"balance">) {
super(args);
this.forwardBeats = this.move.beats / 2;
this.backwardBeats = this.move.beats - this.forwardBeats;
this.balancePartBeats = this.move.beats / 2;
}
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {

View File

@ -1,4 +1,4 @@
import { BalanceWeight, Facing, HandConnection, HandTo, PositionKind, SemanticPosition } from "../interpreterCommon.js";
import { BalanceWeight, Facing, HandConnection, HandTo, PositionKind, SemanticPosition, handsInCircle } from "../interpreterCommon.js";
import { Move } from "../libfigureMapper.js";
import { LowLevelMove, SemanticAnimationKind } from "../lowLevelMove.js";
import { Hand } from "../rendererConstants.js";
@ -11,14 +11,11 @@ export function balanceCircleInAndOut(move: Move, startPos: SemanticPosition, ba
}
balanceBeats ??= 4;
const balancePartBeats = balanceBeats/2;
const balancePartBeats = balanceBeats / 2;
const holdingHandsInCircle: SemanticPosition = {...startPos,
facing: Facing.CenterOfCircle,
hands: new Map<Hand, HandConnection>([
[Hand.Left, { hand: Hand.Right, to: HandTo.LeftInCircle }],
[Hand.Right, { hand: Hand.Left, to: HandTo.RightInCircle }],
]),
hands: handsInCircle,
};
const circleBalancedIn: SemanticPosition = {...holdingHandsInCircle,
balance: BalanceWeight.Forward,

View File

@ -18,6 +18,10 @@ class BoxTheGnatSingleVariant extends SingleVariantMoveInterpreter<BoxTheGnat, t
const balancePartBeats = balanceBeats / 2;
const twirlBeats = this.move.beats - balanceBeats;
if (startPos.hands && startPos.hands.get(Hand.Right) === undefined && startPos.hands.get(Hand.Left)?.hand === Hand.Left) {
throw new Error(this.move.move + " shouldn't start with holding left hands. Something went wrong.");
}
// TODO Adjust facing?
const startPosition = { ...startPos, hands: new Map<Hand, HandConnection>([[hand, { hand, to: HandTo.DancerForward }]]) };

View File

@ -1,3 +1,4 @@
import { RotationAnimationFacing } from "../animation.js";
import { Move } from "../libfigureMapper.js";
import { SemanticAnimationKind } from "../lowLevelMove.js";
import { Hand } from "../rendererConstants.js";
@ -6,6 +7,10 @@ import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpre
const moveName: Move["move"] = "butterfly whirl";
class ButterflyWhirlSingleVariant extends SingleVariantMoveInterpreter<ButterflyWhirl, typeof moveName> {
override allowStartingClose(): boolean {
return true;
}
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
return this.handleCircleMove(({ startPos }) => {
return this.combine([{
@ -14,8 +19,10 @@ class ButterflyWhirlSingleVariant extends SingleVariantMoveInterpreter<Butterfly
movementPattern: {
kind: SemanticAnimationKind.RotateAround,
around: startPos.which.leftRightSide(),
// TODO hand around isn't the same as allemande...
byHand: startPos.which.isOnLeftLookingAcross() ? Hand.Right : Hand.Left,
facing: startPos.which.isOnLeftLookingAcross()
? RotationAnimationFacing.Forward
: RotationAnimationFacing.Backward,
close: true,
minAmount: 360,
}

View File

@ -1,4 +1,4 @@
import { Facing, handsInCircle } from "../interpreterCommon.js";
import { DancerDistance, Facing, handsInCircle } from "../interpreterCommon.js";
import { Move } from "../libfigureMapper.js";
import { SemanticAnimationKind } from "../lowLevelMove.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";

View File

@ -22,6 +22,9 @@ class DownTheHallSingleVariant extends SingleVariantMoveInterpreter<DownTheHall,
}
return this.handleMove(({ startPos }) => {
const startFacing = this.move.parameters.facing === "backward" ? Facing.Up : Facing.Down;
if (startPos.facing !== startFacing && (startPos.facing === Facing.Up || startPos.facing === Facing.Down)) {
throw new Error("Started facing the wrong direction.");
}
const startWhich: ShortLinesPosition = startPos.kind === PositionKind.ShortLines
? startPos.which
// TODO Is this always the right way to convert circle to short lines?

View File

@ -1,4 +1,4 @@
import { Facing, SemanticPosition } from "../interpreterCommon.js";
import { DancerDistance, Facing, SemanticPosition } from "../interpreterCommon.js";
import { SemanticAnimationKind, LowLevelMove } from "../lowLevelMove.js";
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
import { balanceCircleInAndOut } from "./balanceTheRing.js";

View File

@ -9,7 +9,7 @@ const moveName: Move["move"] = "roll away";
class RollAwaySingleVariant extends SingleVariantMoveInterpreter<RollAway, typeof moveName> {
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
// TODO maybe can roll away in short lines?
return this.handleCirclePairedMove(this.move.parameters.who, ({ id, startPos, withPos }) => {
return this.handleCirclePairedMove(this.move.parameters.whom, ({ id, startPos, withPos }) => {
let isRoller: boolean;
switch (this.move.parameters.who) {
case "gentlespoons":

View File

@ -33,6 +33,10 @@ const swingEndValues: SwingEnd[] = [
];
class SwingSingleVariant extends SingleVariantMoveInterpreter<Swing, typeof moveName> {
override allowStartingClose(): boolean {
return true;
}
moveAsVariants(previousMoveVariant: string): VariantCollection {
const res = new Map<string, Variant>();
@ -204,10 +208,12 @@ class Swing extends MoveInterpreter<typeof moveName> {
? 2
: this.move.beats / 4;
this.swingBeats = this.move.beats - this.balancePartBeats * 2;
break;
case "meltdown":
this.balancePartBeats = 0;
this.meltdownBeats = this.move.beats >= 8 ? 4 : this.move.beats / 2;
this.swingBeats = this.move.beats - this.meltdownBeats;
break;
default:
throw new Error ("Unknown swing prefix: " + this.move.parameters.prefix);
}