forked from perelman/contra-renderer
360 lines
15 KiB
TypeScript
360 lines
15 KiB
TypeScript
import { DancerIdentity } from "../danceCommon.js";
|
|
import { SemanticPosition, PositionKind, ShortLinesPosition, CirclePosition, CircleSide, Facing } from "../interpreterCommon.js";
|
|
import { LowLevelMove, SemanticAnimationKind } from "../lowLevelMove.js";
|
|
import { Hand } from "../rendererConstants.js";
|
|
import { ISingleVariantMoveInterpreter, LowLevelMovesForAllDancers, MoveInterpreter, MoveInterpreterCtorArgs, PartialLowLevelMove, SemanticPositionsForAllDancers, SingleVariantMoveInterpreter, moveInterpreters } from "./_moveInterpreter.js";
|
|
import { dancerIsPair } from "../libfigure/util.js";
|
|
|
|
const moveName = "hey";
|
|
|
|
type HeyStep = {
|
|
kind: "StandStill" | "Loop" | "CenterPass" | "EndsPassIn" | "EndsPassOut" | "Ricochet",
|
|
endPosition: SemanticPosition,
|
|
}
|
|
|
|
function fixupHeyOtherPath(withoutOtherPath: Map<DancerIdentity, (LowLevelMove & { heyStep?: HeyStep })[]>): Map<DancerIdentity, LowLevelMove[]> {
|
|
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;
|
|
}
|
|
|
|
class HeySingleVariant extends SingleVariantMoveInterpreter<Hey, typeof moveName> {
|
|
heyStepToPartialLowLevelMove(heyStep: HeyStep): PartialLowLevelMove & { heyStep: HeyStep } {
|
|
return {
|
|
beats: this.moveInterpreter.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: this.moveInterpreter.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: this.moveInterpreter.endsShoulder,
|
|
} : {
|
|
kind: SemanticAnimationKind.PassBy,
|
|
around: heyStep.kind === "CenterPass" ? "Center" : heyStep.endPosition.which.leftRightSide(),
|
|
withHands: false,
|
|
side: heyStep.kind === "CenterPass" ? this.moveInterpreter.centerShoulder : this.moveInterpreter.endsShoulder,
|
|
facing: "Start",
|
|
otherPath: undefined!, // Placeholder, fixup later.
|
|
},
|
|
heyStep,
|
|
};
|
|
}
|
|
|
|
continueHey(prevStep: HeyStep, stepsLeft: number,
|
|
{ beenInCenter, endsInCircle, inCenterFirst }:
|
|
{ beenInCenter: boolean, endsInCircle: boolean, inCenterFirst: boolean }): HeyStep {
|
|
// TODO Not sure why type checker requires rechecking this here.
|
|
if (this.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()
|
|
? (this.moveInterpreter.endsShoulder === Hand.Right ? CirclePosition.TopLeft : CirclePosition.BottomLeft)
|
|
: (this.moveInterpreter.endsShoulder === Hand.Right ? CirclePosition.BottomRight : CirclePosition.TopRight),
|
|
},
|
|
}
|
|
}
|
|
else if (!isFacingSide) {
|
|
const rico = inCenterFirst
|
|
? beenInCenter
|
|
? this.move.parameters.rico3
|
|
: this.move.parameters.rico1
|
|
: beenInCenter
|
|
? this.move.parameters.rico4
|
|
: this.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
|
|
(this.moveInterpreter.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: " + (<any>prevStep.endPosition).kind);
|
|
}
|
|
}
|
|
|
|
moveAsLowLevelMoves(): LowLevelMovesForAllDancers {
|
|
return fixupHeyOtherPath(this.handleMove(({ id, startPos }) => {
|
|
const endsInCircle = startPos.kind === PositionKind.Circle;
|
|
|
|
const inCenterFirst = this.moveInterpreter.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 (this.moveInterpreter.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 < this.moveInterpreter.heyParts; i++) {
|
|
const nextHeyStep = this.continueHey(heySteps[i - 1], this.moveInterpreter.heyParts - i - 1, {beenInCenter, endsInCircle, inCenterFirst});
|
|
beenInCenter ||= nextHeyStep.kind === "CenterPass" || nextHeyStep.kind === "Ricochet";
|
|
heySteps.push(nextHeyStep);
|
|
}
|
|
return this.combine(heySteps.map(s => this.heyStepToPartialLowLevelMove(s)), { ...startingPos, hands: undefined });
|
|
}));
|
|
}
|
|
}
|
|
|
|
class Hey extends MoveInterpreter<typeof moveName> {
|
|
public readonly heyParts: number;
|
|
public readonly heyPartBeats: number;
|
|
public readonly firstPassInCenter: boolean;
|
|
public readonly centerShoulder: Hand;
|
|
public readonly endsShoulder: Hand;
|
|
|
|
constructor(args: MoveInterpreterCtorArgs<typeof moveName>) {
|
|
super(args);
|
|
|
|
if (this.move.parameters.dir !== "across") {
|
|
throw new Error("Unsupported hey direction: " + this.move.parameters.dir);
|
|
}
|
|
|
|
if (typeof this.move.parameters.until === "string") {
|
|
switch (this.move.parameters.until) {
|
|
case "half":
|
|
this.heyParts = 4;
|
|
break;
|
|
case "full":
|
|
this.heyParts = 8;
|
|
break;
|
|
// TODO Are these right? Can it sometimes be 1 or 3 instead of 2?
|
|
case "less than half":
|
|
this.heyParts = 2;
|
|
break;
|
|
case "between half and full":
|
|
this.heyParts = 6;
|
|
default:
|
|
throw new Error("Unsupported hey 'until': " + this.move.parameters.until);
|
|
}
|
|
}
|
|
else {
|
|
// TODO Is this actually this simple?
|
|
this.heyParts = this.move.parameters.until.time === 1 ? 2 : 6;
|
|
//throw new Error("Unsupported hey 'until': " + this.move.parameters.until.dancer + " time " + this.move.parameters.until.time);
|
|
}
|
|
this.heyPartBeats = this.move.beats / this.heyParts;
|
|
// TODO is this right?
|
|
this.firstPassInCenter = dancerIsPair(this.move.parameters.who);
|
|
this.centerShoulder = this.firstPassInCenter === this.move.parameters.shoulder ? Hand.Right : Hand.Left;
|
|
this.endsShoulder = this.centerShoulder.opposite();
|
|
}
|
|
|
|
buildSingleVariantMoveInterpreter(startingPos: SemanticPositionsForAllDancers): ISingleVariantMoveInterpreter {
|
|
return new HeySingleVariant(this, startingPos);
|
|
}
|
|
}
|
|
|
|
moveInterpreters.set(moveName, Hey); |