universal-siteswap
Version:
A library for parsing, validating, examining and finding transitions between all types of siteswaps
499 lines • 24.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Siteswap = void 0;
var common_1 = require("./common");
var parser_1 = require("./parser");
var state_1 = require("./state");
var vanilla_siteswap_1 = require("./vanilla-siteswap");
function np(a, b) {
return "".concat(a, ",").concat(b);
}
function round3(num) {
return Math.round((num + Number.EPSILON) * 1000) / 1000;
}
function swapHand(time, period, implicitFlip) {
var loops = Math.floor(time / period);
return implicitFlip && loops % 2 === 1;
}
var Siteswap = /** @class */ (function () {
function Siteswap(jugglers, jugglerDelays) {
this.numObjects = 0;
this.numJugglers = 0;
this.period = 0;
this.maxHeight = 0;
this.maxMultiplex = 0;
this.hasSync = false;
this.hasAsync = false;
this.hasPass = false;
this.pureAsync = false;
this.isValid = false;
this.errorMessage = '';
this._jugglerDelaysInferred = false;
this._implicitFlip = false;
this.jugglers = jugglers;
this.jugglerDelays = jugglerDelays ? jugglerDelays : new Array(jugglers.length).fill(-1);
// If we have a multiple of 3 jugglers then fix and .3 and .6 throws
if (this.jugglers.length % 3 === 0) {
for (var _i = 0, _a = this.jugglers; _i < _a.length; _i++) {
var juggler = _a[_i];
for (var _b = 0, _c = juggler.beats; _b < _c.length; _b++) {
var beat = _c[_b];
for (var _d = 0, _e = [beat.LH, beat.RH]; _d < _e.length; _d++) {
var ths = _e[_d];
for (var _f = 0, ths_1 = ths; _f < ths_1.length; _f++) {
var th = ths_1[_f];
th.height = (0, common_1.fixFraction)(th.dispHeight, /*allow36=*/ true);
}
}
}
}
}
if (this.jugglers.length > 0) {
// Convert any delays of more than 1 to less and rotate the siteswap
this.pureAsync = this.jugglers.every(function (juggler) {
return juggler.beats.every(function (beat) { return beat.isAsync(); });
});
// Pure async siteswaps of odd period automatically flip sides
this._implicitFlip = this.pureAsync && this.jugglers[0].beats.length % 2 === 1;
for (var i = 0; i < this.jugglerDelays.length; i++) {
if (this.jugglerDelays[i] >= 1) {
this.jugglers[i] = this.jugglers[i].clone();
}
while (this.jugglerDelays[i] >= 1) {
var lastBeat = this.jugglers[i].beats.pop();
if (this._implicitFlip)
lastBeat = lastBeat.flip();
this.jugglers[i].beats.unshift(lastBeat);
this.jugglerDelays[i]--;
}
}
}
// Make compiler happy
this.state = new state_1.State([], true, jugglerDelays);
this.validate();
}
Siteswap.prototype.inferJugglerDelays = function () {
if (!this.jugglerDelays.every(function (x) { return x === -1; })) {
// We were given delays, don't infer them
return true;
}
this.jugglerDelays[0] = 0;
var allowed_jugglers = [0];
while (allowed_jugglers.length > 0) {
var juggler = allowed_jugglers.pop();
for (var i = 0; i < this.period; i++) {
for (var _i = 0, _a = [common_1.Hand.Right, common_1.Hand.Left]; _i < _a.length; _i++) {
var hand = _a[_i];
var pos = { juggler: juggler, time: i, hand: hand };
for (var _b = 0, _c = this.throwsAt(pos); _b < _c.length; _b++) {
var th = _c[_b];
if (th.pass) {
var to = th.passTo != null ? th.passTo : (juggler + 1) % this.numJugglers;
var land_frac = (this.jugglerDelays[juggler] + th.height) % 1;
if (this.jugglerDelays[to] === -1) {
this.jugglerDelays[to] = land_frac;
allowed_jugglers.push(to);
}
else if (Math.abs(this.jugglerDelays[to] - land_frac) > 1e-2) {
var jugglerName = (0, common_1.toLetter)(juggler, 'A');
this.errorMessage = "Cannot find consistent juggler delays: juggler ".concat(jugglerName, " has delays of ").concat(this.jugglerDelays[to], " and ").concat(land_frac);
return false;
}
}
else {
if (th.height % 1 !== 0) {
this.errorMessage = "Cannot find consistent juggler delays: self throw of height ".concat(th.height);
return false;
}
}
}
}
}
}
this._jugglerDelaysInferred = true;
return true;
};
Siteswap.prototype.throwsAt = function (position) {
var beat = this.jugglers[position.juggler].beats[position.time];
return position.hand === common_1.Hand.Right ? beat.RH : beat.LH;
};
Siteswap.prototype.validate = function () {
var _this = this;
this.isValid = false;
this.errorMessage = '';
this.numJugglers = this.jugglers.length;
if (this.numJugglers === 0) {
this.errorMessage = 'No jugglers';
return false;
}
this.period = this.jugglers[0].beats.length;
if (!this.jugglers.every(function (juggler) { return juggler.beats.length === _this.period; })) {
this.errorMessage = 'Mismatching juggler periods';
return false;
}
if (!this.inferJugglerDelays()) {
// Couldn't find consistent juggler delays
return false;
}
// Convert .3 and .6 to 1/3 and 2/3 when we have a multiple of 3 jugglers
var allow36 = this.numJugglers % 3 === 0;
this.jugglerDelays = this.jugglerDelays.map(function (d) { return (0, common_1.fixFraction)(d, allow36); });
if (this.jugglerDelays.length !== this.numJugglers) {
this.errorMessage = "Number of jugglers (".concat(this.numJugglers, ") not equal to number of delays (").concat(this.jugglerDelays.length, ")");
return false;
}
this.maxMultiplex = Math.max.apply(Math, this.jugglers.map(function (j) { return Math.max.apply(Math, j.beats.map(function (b) { return b.maxMultiplex(); })); }));
this.hasPass = this.jugglers.some(function (j) { return j.beats.some(function (b) { return b.hasPass(); }); });
this.hasSync = this.jugglers.some(function (j) { return j.beats.some(function (b) { return b.isSync(); }); });
this.hasAsync = this.jugglers.some(function (j) { return j.beats.some(function (b) { return b.isAsync(); }); });
this.maxHeight = 0;
var sum = 0;
// `check` stores the number of throws which should land for each juggler, beat and hand
var check = state_1.State.Empty(this.numJugglers, this.period);
for (var _i = 0, _a = (0, common_1.allPositions)(this.numJugglers, this.period); _i < _a.length; _i++) {
var pos = _a[_i];
for (var _b = 0, _c = this.throwsAt(pos); _b < _c.length; _b++) {
var th = _c[_b];
sum += th.height;
this.maxHeight = Math.max(this.maxHeight, th.height);
// Validate pass recipients
if (th.passTo && th.passTo >= this.numJugglers) {
var juggler = (0, common_1.toLetter)(th.passTo, 'A');
this.errorMessage = "Invalid juggler ".concat(juggler, ", there are only ").concat(this.numJugglers, " jugglers");
return false;
}
check.inc(pos);
}
}
this.numObjects = sum / this.period;
// Be a bit more lax and round if we have juggler delays
var rounded = Math.round(this.numObjects);
if (this.jugglerDelays.some(function (d) { return d !== 0; }) &&
Math.abs(this.numObjects - rounded) < 1e-2) {
this.numObjects = rounded;
}
if (this.numObjects % 1 !== 0) {
this.errorMessage = 'Invalid pattern average';
return false;
}
this.state = state_1.State.Empty(this.numJugglers, this.maxHeight);
for (var _d = 0, _e = (0, common_1.allPositions)(this.numJugglers, this.period); _d < _e.length; _d++) {
var pos = _e[_d];
for (var _f = 0, _g = this.throwsAt(pos); _f < _g.length; _f++) {
var th = _g[_f];
var landJuggler = th.landJuggler(pos.juggler, this.numJugglers);
var fractionDiff = this.jugglerDelays[pos.juggler] - this.jugglerDelays[landJuggler];
var fullLandTime = pos.time + th.height + fractionDiff;
var rounded_1 = Math.round(fullLandTime);
if (Math.abs(fullLandTime - rounded_1) < 1e-2) {
fullLandTime = rounded_1;
}
else {
this.errorMessage = "Throw ".concat(th.dispHeight, " lands at an invalid time for juggler ").concat(landJuggler);
return false;
}
var landTime = fullLandTime % this.period;
var fullLandHand = th.throwSwapsHands() ? 1 - pos.hand : pos.hand;
// We swap hands (again) if there is an implicit flip (e.g. odd period
// vanilla siteswaps) and we looped around an odd number of times.
// This isn't because the throw swapped hands but because the siteswap
// should be repeating on the other side.
var landHand = swapHand(fullLandTime, this.period, this._implicitFlip) ? 1 - fullLandHand : fullLandHand;
var landPosition = {
juggler: landJuggler,
time: landTime,
hand: landHand,
};
// Check landing position
if (check.at(landPosition) <= 0) {
this.errorMessage = "Collision at juggler ".concat(landJuggler, ", time ").concat(landTime, ", hand ").concat(landHand);
return false;
}
check.dec(landPosition);
// Add to state, first at original land time, ignoring those landing before the end of the siteswap
var curTime = fullLandTime - this.period;
var curHand = fullLandHand;
while (curTime >= 0) {
this.state.inc({ juggler: landJuggler, time: curTime, hand: curHand });
// Back one period of the siteswap
if (this._implicitFlip)
curHand = 1 - curHand;
curTime -= this.period;
}
}
}
this.state.trimZeros();
this.state.recalc();
this.isValid = true;
return true;
};
Siteswap.prototype.toString = function () {
if (this.jugglers.length === 1) {
return this.jugglers[0].toString();
}
else {
var jugglerStrings = this.jugglers.map(function (j) { return j.toString(); }).join('|');
var allow36_1 = this.numJugglers % 3 === 0;
var delays = '';
if (this.jugglerDelays.some(function (d) { return d > 0; }) && !this._jugglerDelaysInferred) {
delays = "{".concat(this.jugglerDelays.map(function (d) { return (0, common_1.unfixFraction)(d, allow36_1); }).join(','), "}");
}
return "".concat(delays, "<").concat(jugglerStrings, ">");
}
};
Siteswap.prototype.flip = function () {
return new Siteswap(this.jugglers.map(function (juggler) { return juggler.flip(); }));
};
Siteswap.prototype.toVanilla = function () {
if (!this.pureAsync || this.numJugglers !== 1) {
throw "Could not convert to vanilla siteswap: must be async and single juggler";
}
var throws = [];
for (var _i = 0, _a = this.jugglers[0].beats; _i < _a.length; _i++) {
var beat = _a[_i];
var ths = beat.LH.length > 0 ? beat.LH : beat.RH;
throws.push(ths.map(function (th) { return th.height; }));
}
return new vanilla_siteswap_1.VanillaSiteswap(throws);
};
Siteswap.prototype.toJIF = function () {
var _this = this;
var RADIUS = this.numJugglers === 1 ? 0 : 1.5;
var objectType = "club";
var jugglers = [];
var limbs = [];
var props = [];
for (var i = 0; i < this.numJugglers; i++) {
limbs.push({ juggler: i, type: 'right hand' });
limbs.push({ juggler: i, type: 'left hand' });
var angle = i * 2 * Math.PI / this.numJugglers;
jugglers.push({
name: (0, common_1.toLetter)(i, 'A'),
position: [round3(RADIUS * Math.cos(angle)), 0, round3(RADIUS * Math.sin(angle))],
lookAt: [0, 0, this.numJugglers === 1 ? 1 : 0],
});
}
for (var i = 0; i < this.numObjects; i++) {
props.push({ color: "#f45d20", type: objectType });
}
var events = [];
for (var _i = 0, _a = (0, common_1.allPositions)(this.numJugglers, this.period); _i < _a.length; _i++) {
var pos = _a[_i];
for (var _b = 0, _c = this.throwsAt(pos); _b < _c.length; _b++) {
var th = _c[_b];
if (th.height == 0) {
continue;
}
events.push({
time: pos.time + this.jugglerDelays[pos.juggler],
height: th.height,
fromHand: pos.hand,
toHand: th.throwSwapsHands() ? 1 - pos.hand : pos.hand,
fromJuggler: pos.juggler,
toJuggler: th.landJuggler(pos.juggler, this.numJugglers),
label: th.toString(),
});
}
}
events.sort(function (a, b) { return a.time - b.time; });
var jif;
var useold = true;
if (useold) { // Use old code until passist is updated properly
var throws = [];
var positions = new Map();
var timeOffset = 0;
var nextProp = 0;
var inits = [];
while (true) {
for (var _d = 0, events_1 = events; _d < events_1.length; _d++) {
var event_1 = events_1[_d];
var time = event_1["time"] + timeOffset;
var fromHand = swapHand(time, this.period, this._implicitFlip) ? 1 - event_1["fromHand"] : event_1["fromHand"];
var toHand = swapHand(time, this.period, this._implicitFlip) ? 1 - event_1["toHand"] : event_1["toHand"];
var th = {
time: time,
from: event_1["fromJuggler"] * 2 + fromHand,
to: event_1["toJuggler"] * 2 + toHand,
duration: event_1["height"],
label: event_1["label"],
};
if (positions.get(np(th.time, th.from)) == undefined) {
positions.set(np(th.time, th.from), nextProp);
inits.push([th.time, th.from, nextProp]);
nextProp += 1;
}
th.prop = positions.get(np(th.time, th.from));
positions.set(np(th.time + th.duration, th.to), positions.get(np(th.time, th.from)));
positions.set(np(th.time, th.from), undefined);
throws.push(th);
}
timeOffset += this.period;
var equal = true;
for (var _e = 0, inits_1 = inits; _e < inits_1.length; _e++) {
var _f = inits_1[_e], time = _f[0], from = _f[1], prop = _f[2];
if (positions.get(np(time + timeOffset, from)) !== prop) {
equal = false;
break;
}
}
if (equal)
break;
if (timeOffset > 500) {
/* istanbul ignore next */
throw "Could not find a repetition within 500 steps, giving up";
}
}
jif = {
"meta": {
"name": this.toString(),
"type": "General siteswap",
"description": this.toString(),
"generator": "siteswap.js",
"version": "0.01",
},
"timeStretchFactor": 1,
"jugglers": jugglers,
"limbs": limbs,
"props": props,
"throws": throws,
"repetition": { "period": timeOffset },
};
}
else { // New code which relies on passist completing things as it does in JIF edit interface
var throws = [];
for (var _g = 0, events_2 = events; _g < events_2.length; _g++) {
var event_2 = events_2[_g];
var th = {
time: event_2["time"],
from: event_2["fromJuggler"] * 2 + event_2["fromHand"],
to: event_2["toJuggler"] * 2 + event_2["toHand"],
duration: event_2["height"],
label: event_2["label"],
};
throws.push(th);
}
var limbPermutation = Array.from({ length: limbs.length }, function (_, i) { return _this._implicitFlip ? i ^ 1 : i; });
jif = {
"meta": {
"name": this.toString(),
"type": "General siteswap",
"description": this.toString(),
"generator": "siteswap.js",
"version": "0.01",
},
"timeStretchFactor": 1,
"jugglers": jugglers,
"limbs": limbs,
"props": props,
"throws": throws,
"repetition": {
"period": this.period,
"limbPermutation": limbPermutation,
},
};
}
;
return jif;
};
Siteswap.FromJif = function (jif) {
if (jif.limbs.some(function (l) { return l.type !== "right hand" && l.type !== "left hand"; })) {
throw "Conversion from JIF only implemented with right and left hands";
}
var timeStretchFactor = jif.timeStretchFactor == null ? 1 : jif.timeStretchFactor;
var jugglers = jif.jugglers.map(function (j) { return new common_1.JugglerBeats(Array.from(new Array(jif.repetition.period), function () { return []; })); });
var jugglerDelays = jif.jugglers.map(function (j) { return 0; });
var events = jif.throws;
for (var _i = 0, events_3 = events; _i < events_3.length; _i++) {
var event_3 = events_3[_i];
var fromJuggler = jif.limbs[event_3.from].juggler;
var fromHand = jif.limbs[event_3.from].type === "right hand" ? common_1.Hand.Right : common_1.Hand.Left;
var toJuggler = jif.limbs[event_3.to].juggler;
var toHand = jif.limbs[event_3.to].type === "right hand" ? common_1.Hand.Right : common_1.Hand.Left;
var eventTime = event_3.time / timeStretchFactor;
var eventDuration = event_3.duration / timeStretchFactor;
var delay = eventTime % 1;
if (delay !== 0) {
if (jugglerDelays[fromJuggler] !== 0 && jugglerDelays[fromJuggler] !== delay) {
throw "Cannot handle multiple delays for the same juggler";
}
jugglerDelays[fromJuggler] = delay;
}
var time = eventTime - jugglerDelays[fromJuggler];
var pass = fromJuggler !== toJuggler;
var passTo = pass && jugglers.length > 2 ? toJuggler : undefined;
var hasX = pass ? fromHand === toHand : (eventDuration % 2 === 0) !== (fromHand === toHand);
var th = new common_1.Throw(eventDuration, hasX, pass, passTo);
var jugglerBeat = jugglers[fromJuggler].beats[time];
if (fromHand === common_1.Hand.Right) {
jugglerBeat.RH.push(th);
}
else {
jugglerBeat.LH.push(th);
}
}
return new Siteswap(jugglers);
};
Siteswap.Parse = function (input) {
var parsed = (0, parser_1.parse)(input);
return new Siteswap(parsed[0], parsed[1]);
};
Siteswap.ParseKHSS = function (input, hands) {
if (hands === void 0) { hands = 2; }
return Siteswap.FromKHSS(vanilla_siteswap_1.VanillaSiteswap.Parse(input), hands);
};
Siteswap.FromKHSS = function (input, hands) {
if (hands === void 0) { hands = 2; }
if (input.maxMultiplex > 1) {
throw "KHSS only implemented without multiplexes";
}
if (hands % 2 !== 0) {
// A bit of a hack to deal with odd-handed siteswaps as they break that each juggler has an even rhythm,
// instead make `hands` jugglers and only use one hand from each juggler.
var doubled = [];
for (var _i = 0, _a = input.throws; _i < _a.length; _i++) {
var th = _a[_i];
doubled.push(th.map(function (c) { return c * 2; }));
doubled.push([0]);
}
// const doubled = Array.from(input).map(c => intToSS(ssToInt(c) * 2) + '0').join('');
return Siteswap.FromKHSS(new vanilla_siteswap_1.VanillaSiteswap(doubled), hands * 2);
}
var numJugglers = hands / 2;
var fix36 = numJugglers % 3 === 0;
var jugglers = [];
for (var i_1 = 0; i_1 < numJugglers; i_1++) {
jugglers.push([]);
}
var j = 0;
var i = 0;
while (i >= 0) {
var fullThrow = input.throws[i][0];
// Convert to local height and make more readable
var height = (0, common_1.unfixFraction)(fullThrow / numJugglers, fix36);
// Non multiples are passes
var isPass = fullThrow % numJugglers !== 0;
// This checks the parity of the number of times the throw height crosses the last juggler
var hasX = isPass && (fullThrow + j) % hands < hands / 2;
// If it's not a pass to the next juggler, add who it's passed to
var passTo = isPass && fullThrow % numJugglers !== 1
? (fullThrow + j) % numJugglers
: undefined;
var th = new common_1.Throw(height, hasX, isPass, passTo);
// The height use earlier is the nice one - this is the exact one for use internally
th.height = fullThrow / numJugglers;
jugglers[j].push([th]);
j = (j + 1) % numJugglers;
i = (i + 1) % input.throws.length;
if (i === 0 && j === 0) {
// We've reached the start after enough repetitions
break;
}
}
var jugglerBeats = jugglers.map(function (ths) { return new common_1.JugglerBeats(ths); });
var delays = Array.from(new Array(numJugglers), function (x, i) { return i / numJugglers; });
return new Siteswap(jugglerBeats, delays);
};
return Siteswap;
}());
exports.Siteswap = Siteswap;
//# sourceMappingURL=siteswap.js.map