cubing
Version:
A collection of JavaScript cubing libraries.
1,696 lines (1,664 loc) • 189 kB
JavaScript
import {
DEGREES_PER_RADIAN,
FreshListenerManager,
HTMLElementShim,
HintFaceletProp,
ManagedCustomElement,
NO_VALUE,
RenderScheduler,
SimpleTwistyPropSource,
StaleDropper,
Twisty3DVantage,
TwistyPropDerived,
TwistyPropSource,
bulk3DCode,
cssStyleSheetShim,
customElementsShim,
hintFaceletStyles,
rawRenderPooled,
setCameraFromOrbitCoordinates,
setTwistyDebug
} from "../chunks/chunk-DQGYYYHZ.js";
import {
countAnimatedLeaves,
countLeavesInExpansionForSimultaneousMoveIndexer,
countMetricMoves
} from "../chunks/chunk-ZU7PSGX4.js";
import {
puzzles
} from "../chunks/chunk-FLK6AZKB.js";
import {
cube3x3x3,
customPGPuzzleLoader,
getPartialAppendOptionsForPuzzleSpecificSimplifyOptions,
getPieceStickeringMask
} from "../chunks/chunk-FUHYAW74.js";
import "../chunks/chunk-RINY3U6G.js";
import {
Alg,
AlgBuilder,
Conjugate,
Grouping,
LineComment,
Move,
Newline,
Pause,
TraversalDownUp,
TraversalUp,
direct,
directedGenerator,
endCharIndexKey,
experimentalAppendMove,
functionFromTraversal,
offsetMod,
startCharIndexKey
} from "../chunks/chunk-O6HEZXGY.js";
// src/cubing/twisty/controllers/indexer/AlgDuration.ts
function defaultDurationForAmount(amount) {
switch (Math.abs(amount)) {
case 0:
return 0;
case 1:
return 1e3;
case 2:
return 1500;
default:
return 2e3;
}
}
var AlgDuration = class extends TraversalUp {
// TODO: Pass durationForAmount as Down type instead?
constructor(durationForAmount = defaultDurationForAmount) {
super();
this.durationForAmount = durationForAmount;
}
traverseAlg(alg) {
let total = 0;
for (const algNode of alg.childAlgNodes()) {
total += this.traverseAlgNode(algNode);
}
return total;
}
traverseGrouping(grouping) {
return grouping.amount * this.traverseAlg(grouping.alg);
}
traverseMove(move) {
return this.durationForAmount(move.amount);
}
traverseCommutator(commutator) {
return 2 * (this.traverseAlg(commutator.A) + this.traverseAlg(commutator.B));
}
traverseConjugate(conjugate) {
return 2 * this.traverseAlg(conjugate.A) + this.traverseAlg(conjugate.B);
}
traversePause(_pause) {
return this.durationForAmount(1);
}
traverseNewline(_newline) {
return this.durationForAmount(1);
}
traverseLineComment(_comment) {
return this.durationForAmount(0);
}
};
// src/cubing/twisty/controllers/indexer/SimpleAlgIndexer.ts
var SimpleAlgIndexer = class {
constructor(kpuzzle, alg) {
this.kpuzzle = kpuzzle;
this.moves = new Alg(alg.experimentalExpand());
}
moves;
// TODO: Allow custom `durationFn`.
durationFn = new AlgDuration(
defaultDurationForAmount
);
getAnimLeaf(index) {
return Array.from(this.moves.childAlgNodes())[index];
}
indexToMoveStartTimestamp(index) {
const alg = new Alg(Array.from(this.moves.childAlgNodes()).slice(0, index));
return this.durationFn.traverseAlg(alg);
}
timestampToIndex(timestamp) {
let cumulativeTime = 0;
let i;
for (i = 0; i < this.numAnimatedLeaves(); i++) {
cumulativeTime += this.durationFn.traverseMove(this.getAnimLeaf(i));
if (cumulativeTime >= timestamp) {
return i;
}
}
return i;
}
patternAtIndex(index) {
return this.kpuzzle.defaultPattern().applyTransformation(this.transformationAtIndex(index));
}
transformationAtIndex(index) {
let pattern = this.kpuzzle.identityTransformation();
for (const move of Array.from(this.moves.childAlgNodes()).slice(0, index)) {
pattern = pattern.applyMove(move);
}
return pattern;
}
algDuration() {
return this.durationFn.traverseAlg(this.moves);
}
numAnimatedLeaves() {
return countAnimatedLeaves(this.moves);
}
moveDuration(index) {
return this.durationFn.traverseMove(this.getAnimLeaf(index));
}
};
// src/cubing/twisty/controllers/indexer/tree/AlgWalker.ts
var AlgWalkerDecoration = class {
constructor(moveCount, duration, forward, backward, children = []) {
this.moveCount = moveCount;
this.duration = duration;
this.forward = forward;
this.backward = backward;
this.children = children;
}
};
var DecoratorConstructor = class extends TraversalUp {
constructor(kpuzzle) {
super();
this.kpuzzle = kpuzzle;
this.identity = kpuzzle.identityTransformation();
this.dummyLeaf = new AlgWalkerDecoration(
0,
0,
this.identity,
this.identity,
[]
);
}
identity;
dummyLeaf;
durationFn = new AlgDuration(
defaultDurationForAmount
);
cache = {};
traverseAlg(alg) {
let moveCount = 0;
let duration = 0;
let transformation = this.identity;
const child = [];
for (const algNode of alg.childAlgNodes()) {
const apd = this.traverseAlgNode(algNode);
moveCount += apd.moveCount;
duration += apd.duration;
if (transformation === this.identity) {
transformation = apd.forward;
} else {
transformation = transformation.applyTransformation(apd.forward);
}
child.push(apd);
}
return new AlgWalkerDecoration(
moveCount,
duration,
transformation,
transformation.invert(),
child
);
}
traverseGrouping(grouping) {
const dec = this.traverseAlg(grouping.alg);
return this.mult(dec, grouping.amount, [dec]);
}
traverseMove(move) {
const key = move.toString();
let r2 = this.cache[key];
if (r2) {
return r2;
}
const transformation = this.kpuzzle.moveToTransformation(move);
r2 = new AlgWalkerDecoration(
1,
this.durationFn.traverseAlgNode(move),
transformation,
transformation.invert()
);
this.cache[key] = r2;
return r2;
}
traverseCommutator(commutator) {
const decA = this.traverseAlg(commutator.A);
const decB = this.traverseAlg(commutator.B);
const AB = decA.forward.applyTransformation(decB.forward);
const ApBp = decA.backward.applyTransformation(decB.backward);
const ABApBp = AB.applyTransformation(ApBp);
const dec = new AlgWalkerDecoration(
2 * (decA.moveCount + decB.moveCount),
2 * (decA.duration + decB.duration),
ABApBp,
ABApBp.invert(),
[decA, decB]
);
return this.mult(dec, 1, [dec, decA, decB]);
}
traverseConjugate(conjugate) {
const decA = this.traverseAlg(conjugate.A);
const decB = this.traverseAlg(conjugate.B);
const AB = decA.forward.applyTransformation(decB.forward);
const ABAp = AB.applyTransformation(decA.backward);
const dec = new AlgWalkerDecoration(
2 * decA.moveCount + decB.moveCount,
2 * decA.duration + decB.duration,
ABAp,
ABAp.invert(),
[decA, decB]
);
return this.mult(dec, 1, [dec, decA, decB]);
}
traversePause(pause) {
if (pause.experimentalNISSGrouping) {
return this.dummyLeaf;
}
return new AlgWalkerDecoration(
1,
this.durationFn.traverseAlgNode(pause),
this.identity,
this.identity
);
}
traverseNewline(_newline) {
return this.dummyLeaf;
}
traverseLineComment(_comment) {
return this.dummyLeaf;
}
mult(apd, n, child) {
const absn = Math.abs(n);
const st = apd.forward.selfMultiply(n);
return new AlgWalkerDecoration(
apd.moveCount * absn,
apd.duration * absn,
st,
st.invert(),
child
);
}
};
var WalkerDown = class {
constructor(apd, back) {
this.apd = apd;
this.back = back;
}
};
var AlgWalker = class extends TraversalDownUp {
constructor(kpuzzle, algOrAlgNode, apd) {
super();
this.kpuzzle = kpuzzle;
this.algOrAlgNode = algOrAlgNode;
this.apd = apd;
this.i = -1;
this.dur = -1;
this.goalIndex = -1;
this.goalDuration = -1;
this.move = void 0;
this.back = false;
this.moveDuration = 0;
this.st = this.kpuzzle.identityTransformation();
this.root = new WalkerDown(this.apd, false);
}
move;
moveDuration;
back;
st;
root;
i;
dur;
goalIndex;
goalDuration;
moveByIndex(loc) {
if (this.i >= 0 && this.i === loc) {
return this.move !== void 0;
}
return this.dosearch(loc, Infinity);
}
moveByDuration(dur) {
if (this.dur >= 0 && this.dur < dur && this.dur + this.moveDuration >= dur) {
return this.move !== void 0;
}
return this.dosearch(Infinity, dur);
}
dosearch(loc, dur) {
this.goalIndex = loc;
this.goalDuration = dur;
this.i = 0;
this.dur = 0;
this.move = void 0;
this.moveDuration = 0;
this.back = false;
this.st = this.kpuzzle.identityTransformation();
const r2 = this.algOrAlgNode.is(Alg) ? this.traverseAlg(this.algOrAlgNode, this.root) : this.traverseAlgNode(this.algOrAlgNode, this.root);
return r2;
}
traverseAlg(alg, wd) {
if (!this.firstcheck(wd)) {
return false;
}
let i = wd.back ? alg.experimentalNumChildAlgNodes() - 1 : 0;
for (const algNode of directedGenerator(
alg.childAlgNodes(),
wd.back ? -1 /* Backwards */ : 1 /* Forwards */
)) {
if (this.traverseAlgNode(
algNode,
new WalkerDown(wd.apd.children[i], wd.back)
)) {
return true;
}
i += wd.back ? -1 : 1;
}
return false;
}
traverseGrouping(grouping, wd) {
if (!this.firstcheck(wd)) {
return false;
}
const back = this.domult(wd, grouping.amount);
return this.traverseAlg(
grouping.alg,
new WalkerDown(wd.apd.children[0], back)
);
}
traverseMove(move, wd) {
if (!this.firstcheck(wd)) {
return false;
}
this.move = move;
this.moveDuration = wd.apd.duration;
this.back = wd.back;
return true;
}
traverseCommutator(commutator, wd) {
if (!this.firstcheck(wd)) {
return false;
}
const back = this.domult(wd, 1);
if (back) {
return this.traverseAlg(
commutator.B,
new WalkerDown(wd.apd.children[2], !back)
) || this.traverseAlg(
commutator.A,
new WalkerDown(wd.apd.children[1], !back)
) || this.traverseAlg(
commutator.B,
new WalkerDown(wd.apd.children[2], back)
) || this.traverseAlg(commutator.A, new WalkerDown(wd.apd.children[1], back));
} else {
return this.traverseAlg(
commutator.A,
new WalkerDown(wd.apd.children[1], back)
) || this.traverseAlg(
commutator.B,
new WalkerDown(wd.apd.children[2], back)
) || this.traverseAlg(
commutator.A,
new WalkerDown(wd.apd.children[1], !back)
) || this.traverseAlg(
commutator.B,
new WalkerDown(wd.apd.children[2], !back)
);
}
}
traverseConjugate(conjugate, wd) {
if (!this.firstcheck(wd)) {
return false;
}
const back = this.domult(wd, 1);
if (back) {
return this.traverseAlg(
conjugate.A,
new WalkerDown(wd.apd.children[1], !back)
) || this.traverseAlg(
conjugate.B,
new WalkerDown(wd.apd.children[2], back)
) || this.traverseAlg(conjugate.A, new WalkerDown(wd.apd.children[1], back));
} else {
return this.traverseAlg(
conjugate.A,
new WalkerDown(wd.apd.children[1], back)
) || this.traverseAlg(
conjugate.B,
new WalkerDown(wd.apd.children[2], back)
) || this.traverseAlg(conjugate.A, new WalkerDown(wd.apd.children[1], !back));
}
}
traversePause(pause, wd) {
if (!this.firstcheck(wd)) {
return false;
}
this.move = pause;
this.moveDuration = wd.apd.duration;
this.back = wd.back;
return true;
}
traverseNewline(_newline, _wd) {
return false;
}
traverseLineComment(_lineComment, _wd) {
return false;
}
firstcheck(wd) {
if (wd.apd.moveCount + this.i <= this.goalIndex && wd.apd.duration + this.dur < this.goalDuration) {
return this.keepgoing(wd);
}
return true;
}
domult(wd, amount) {
let back = wd.back;
if (amount === 0) {
return back;
}
if (amount < 0) {
back = !back;
amount = -amount;
}
const base = wd.apd.children[0];
const full = Math.min(
Math.floor((this.goalIndex - this.i) / base.moveCount),
Math.ceil((this.goalDuration - this.dur) / base.duration - 1)
);
if (full > 0) {
this.keepgoing(new WalkerDown(base, back), full);
}
return back;
}
keepgoing(wd, mul = 1) {
this.i += mul * wd.apd.moveCount;
this.dur += mul * wd.apd.duration;
if (mul !== 1) {
if (wd.back) {
this.st = this.st.applyTransformation(
wd.apd.backward.selfMultiply(mul)
);
} else {
this.st = this.st.applyTransformation(wd.apd.forward.selfMultiply(mul));
}
} else {
if (wd.back) {
this.st = this.st.applyTransformation(wd.apd.backward);
} else {
this.st = this.st.applyTransformation(wd.apd.forward);
}
}
return false;
}
};
// src/cubing/twisty/controllers/indexer/tree/chunkAlgs.ts
var MIN_CHUNKING_THRESHOLD = 16;
function chunkifyAlg(alg, chunkMaxLength) {
const mainAlgBuilder = new AlgBuilder();
const chunkAlgBuilder = new AlgBuilder();
for (const algNode of alg.childAlgNodes()) {
chunkAlgBuilder.push(algNode);
if (chunkAlgBuilder.experimentalNumAlgNodes() >= chunkMaxLength) {
mainAlgBuilder.push(new Grouping(chunkAlgBuilder.toAlg()));
chunkAlgBuilder.reset();
}
}
mainAlgBuilder.push(new Grouping(chunkAlgBuilder.toAlg()));
return mainAlgBuilder.toAlg();
}
var ChunkAlgs = class extends TraversalUp {
traverseAlg(alg) {
const algLength = alg.experimentalNumChildAlgNodes();
if (algLength < MIN_CHUNKING_THRESHOLD) {
return alg;
}
return chunkifyAlg(alg, Math.ceil(Math.sqrt(algLength)));
}
traverseGrouping(grouping) {
return new Grouping(
this.traverseAlg(grouping.alg),
grouping.amount
// TODO
);
}
traverseMove(move) {
return move;
}
traverseCommutator(commutator) {
return new Conjugate(
this.traverseAlg(commutator.A),
this.traverseAlg(commutator.B)
);
}
traverseConjugate(conjugate) {
return new Conjugate(
this.traverseAlg(conjugate.A),
this.traverseAlg(conjugate.B)
);
}
traversePause(pause) {
return pause;
}
traverseNewline(newline) {
return newline;
}
traverseLineComment(comment) {
return comment;
}
};
var chunkAlgs = functionFromTraversal(ChunkAlgs);
// src/cubing/twisty/controllers/indexer/tree/TreeAlgIndexer.ts
var TreeAlgIndexer = class {
constructor(kpuzzle, alg) {
this.kpuzzle = kpuzzle;
const deccon = new DecoratorConstructor(this.kpuzzle);
const chunkedAlg = chunkAlgs(alg);
this.decoration = deccon.traverseAlg(chunkedAlg);
this.walker = new AlgWalker(this.kpuzzle, chunkedAlg, this.decoration);
}
decoration;
walker;
getAnimLeaf(index) {
if (this.walker.moveByIndex(index)) {
if (!this.walker.move) {
throw new Error("`this.walker.mv` missing");
}
const move = this.walker.move;
if (this.walker.back) {
return move.invert();
}
return move;
}
return null;
}
indexToMoveStartTimestamp(index) {
if (this.walker.moveByIndex(index) || this.walker.i === index) {
return this.walker.dur;
}
throw new Error(`Out of algorithm: index ${index}`);
}
indexToMovesInProgress(index) {
if (this.walker.moveByIndex(index) || this.walker.i === index) {
return this.walker.dur;
}
throw new Error(`Out of algorithm: index ${index}`);
}
patternAtIndex(index, startPattern) {
this.walker.moveByIndex(index);
return (startPattern ?? this.kpuzzle.defaultPattern()).applyTransformation(
this.walker.st
);
}
// TransformAtIndex does not reflect the start pattern; it only reflects
// the change from the start pattern to the current move index. If you
// want the actual pattern, use patternAtIndex.
transformationAtIndex(index) {
this.walker.moveByIndex(index);
return this.walker.st;
}
numAnimatedLeaves() {
return this.decoration.moveCount;
}
timestampToIndex(timestamp) {
this.walker.moveByDuration(timestamp);
return this.walker.i;
}
algDuration() {
return this.decoration.duration;
}
moveDuration(index) {
this.walker.moveByIndex(index);
return this.walker.moveDuration;
}
};
// src/cubing/twisty/model/props/viewer/BackViewProp.ts
var backViewLayouts = {
none: true,
// default
"side-by-side": true,
"top-right": true
};
var BackViewProp = class extends SimpleTwistyPropSource {
getDefaultValue() {
return "auto";
}
};
// src/cubing/twisty/views/2D/TwistyAnimatedSVG.ts
var xmlns = "http://www.w3.org/2000/svg";
var DATA_COPY_ID_ATTRIBUTE = "data-copy-id";
var svgCounter = 0;
function nextSVGID() {
svgCounter += 1;
return `svg${svgCounter.toString()}`;
}
var colorMaps = {
dim: {
white: "#dddddd",
orange: "#884400",
limegreen: "#008800",
red: "#660000",
"rgb(34, 102, 255)": "#000088",
// TODO
yellow: "#888800",
"rgb(102, 0, 153)": "rgb(50, 0, 76)",
purple: "#3f003f"
},
oriented: "#44ddcc",
ignored: "#555555",
invisible: "#00000000"
};
var TwistyAnimatedSVG = class {
constructor(kpuzzle, svgSource, experimentalStickeringMask, showUnknownOrientations = false) {
this.kpuzzle = kpuzzle;
this.showUnknownOrientations = showUnknownOrientations;
if (!svgSource) {
throw new Error(`No SVG definition for puzzle type: ${kpuzzle.name()}`);
}
this.svgID = nextSVGID();
this.wrapperElement = document.createElement("div");
this.wrapperElement.classList.add("svg-wrapper");
this.wrapperElement.innerHTML = svgSource;
const svgElem = this.wrapperElement.querySelector("svg");
if (!svgElem) {
throw new Error("Could not get SVG element");
}
this.svgElement = svgElem;
if (xmlns !== svgElem.namespaceURI) {
throw new Error("Unexpected XML namespace");
}
svgElem.style.maxWidth = "100%";
svgElem.style.maxHeight = "100%";
this.gradientDefs = document.createElementNS(xmlns, "defs");
svgElem.insertBefore(this.gradientDefs, svgElem.firstChild);
for (const orbitDefinition of kpuzzle.definition.orbits) {
for (let idx = 0; idx < orbitDefinition.numPieces; idx++) {
for (let orientation = 0; orientation < orbitDefinition.numOrientations; orientation++) {
const id = this.elementID(
orbitDefinition.orbitName,
idx,
orientation
);
const elem = this.elementByID(id);
let originalColor = elem?.style.fill;
if (experimentalStickeringMask) {
(() => {
const a = experimentalStickeringMask.orbits;
if (!a) {
return;
}
const orbitStickeringMask = a[orbitDefinition.orbitName];
if (!orbitStickeringMask) {
return;
}
const pieceStickeringMask = orbitStickeringMask.pieces[idx];
if (!pieceStickeringMask) {
return;
}
const faceletStickeringMasks = pieceStickeringMask.facelets[orientation];
if (!faceletStickeringMasks) {
return;
}
const stickeringMask = typeof faceletStickeringMasks === "string" ? faceletStickeringMasks : faceletStickeringMasks?.mask;
const colorMap = colorMaps[stickeringMask];
if (typeof colorMap === "string") {
originalColor = colorMap;
} else if (colorMap) {
originalColor = colorMap[originalColor];
}
})();
} else {
originalColor = elem?.style.fill;
}
this.originalColors[id] = originalColor;
this.gradients[id] = this.newGradient(id, originalColor);
this.gradientDefs.appendChild(this.gradients[id]);
elem?.setAttribute("style", `fill: url(#grad-${this.svgID}-${id})`);
}
}
}
for (const hintElem of Array.from(
svgElem.querySelectorAll(`[${DATA_COPY_ID_ATTRIBUTE}]`)
)) {
const id = hintElem.getAttribute(DATA_COPY_ID_ATTRIBUTE);
hintElem.setAttribute("style", `fill: url(#grad-${this.svgID}-${id})`);
}
if (this.showUnknownOrientations) {
this.drawPattern(this.kpuzzle.defaultPattern());
}
}
wrapperElement;
svgElement;
gradientDefs;
originalColors = {};
gradients = {};
svgID;
drawPattern(pattern, nextPattern, fraction) {
this.draw(pattern, nextPattern, fraction);
}
// TODO: save definition in the constructor?
draw(pattern, nextPattern, fraction) {
const nextTransformation = nextPattern?.experimentalToTransformation();
if (!pattern) {
throw new Error("Distinguishable pieces are not handled for SVG yet!");
}
for (const orbitDefinition of pattern.kpuzzle.definition.orbits) {
const currentPatternOrbit = pattern.patternData[orbitDefinition.orbitName];
const nextTransformationOrbit = nextTransformation ? nextTransformation.transformationData[orbitDefinition.orbitName] : null;
for (let idx = 0; idx < orbitDefinition.numPieces; idx++) {
for (let orientation = 0; orientation < orbitDefinition.numOrientations; orientation++) {
const id = this.elementID(
orbitDefinition.orbitName,
idx,
orientation
);
const fromCur = this.elementID(
orbitDefinition.orbitName,
currentPatternOrbit.pieces[idx],
(orbitDefinition.numOrientations - currentPatternOrbit.orientation[idx] + orientation) % orbitDefinition.numOrientations
);
let singleColor = false;
if (nextTransformationOrbit) {
const fromNext = this.elementID(
orbitDefinition.orbitName,
nextTransformationOrbit.permutation[idx],
(orbitDefinition.numOrientations - nextTransformationOrbit.orientationDelta[idx] + orientation) % orbitDefinition.numOrientations
);
if (fromCur === fromNext) {
singleColor = true;
}
fraction = fraction || 0;
const easedBackwardsPercent = 100 * (1 - fraction * fraction * (2 - fraction * fraction));
this.gradients[id].children[0].setAttribute(
"stop-color",
this.originalColors[fromCur]
);
this.gradients[id].children[0].setAttribute(
"offset",
`${Math.max(easedBackwardsPercent - 5, 0)}%`
);
this.gradients[id].children[1].setAttribute(
"offset",
`${Math.max(easedBackwardsPercent - 5, 0)}%`
);
this.gradients[id].children[2].setAttribute(
"offset",
`${easedBackwardsPercent}%`
);
this.gradients[id].children[3].setAttribute(
"offset",
`${easedBackwardsPercent}%`
);
this.gradients[id].children[3].setAttribute(
"stop-color",
this.originalColors[fromNext]
);
} else {
singleColor = true;
}
if (singleColor) {
if (this.showUnknownOrientations && currentPatternOrbit.orientationMod?.[idx] === 1) {
this.gradients[id].children[0].setAttribute("stop-color", "#000");
this.gradients[id].children[0].setAttribute("offset", "5%");
this.gradients[id].children[1].setAttribute("offset", "5%");
this.gradients[id].children[2].setAttribute("offset", "20%");
this.gradients[id].children[3].setAttribute("offset", "20%");
this.gradients[id].children[3].setAttribute(
"stop-color",
this.originalColors[fromCur]
);
} else {
this.gradients[id].children[0].setAttribute(
"stop-color",
this.originalColors[fromCur]
);
this.gradients[id].children[0].setAttribute("offset", "100%");
this.gradients[id].children[1].setAttribute("offset", "100%");
this.gradients[id].children[2].setAttribute("offset", "100%");
this.gradients[id].children[3].setAttribute("offset", "100%");
}
}
}
}
}
}
newGradient(id, originalColor) {
const grad = document.createElementNS(
xmlns,
"radialGradient"
);
grad.setAttribute("id", `grad-${this.svgID}-${id}`);
grad.setAttribute("r", "70.7107%");
const stopDefs = [
{ offset: 0, color: originalColor },
{ offset: 0, color: "black" },
{ offset: 0, color: "black" },
{ offset: 0, color: originalColor }
];
for (const stopDef of stopDefs) {
const stop = document.createElementNS(xmlns, "stop");
stop.setAttribute("offset", `${stopDef.offset}%`);
stop.setAttribute("stop-color", stopDef.color);
stop.setAttribute("stop-opacity", "1");
grad.appendChild(stop);
}
return grad;
}
elementID(orbitName, idx, orientation) {
return `${orbitName}-l${idx}-o${orientation}`;
}
elementByID(id) {
return this.wrapperElement.querySelector(`#${id}`);
}
};
// src/cubing/twisty/views/ClassListManager.ts
var ClassListManager = class {
// The prefix should ideally end in a dash.
constructor(elem, prefix, validSuffixes) {
this.elem = elem;
this.prefix = prefix;
this.validSuffixes = validSuffixes;
}
#currentClassName = null;
// Does nothing if there was no value.
clearValue() {
if (this.#currentClassName) {
this.elem.contentWrapper.classList.remove(this.#currentClassName);
}
this.#currentClassName = null;
}
// Returns if the value changed
setValue(suffix) {
if (!this.validSuffixes.includes(suffix)) {
throw new Error(`Invalid suffix: ${suffix}`);
}
const newClassName = `${this.prefix}${suffix}`;
const changed = this.#currentClassName !== newClassName;
if (changed) {
this.clearValue();
this.elem.contentWrapper.classList.add(newClassName);
this.#currentClassName = newClassName;
}
return changed;
}
};
// src/cubing/twisty/model/helpers.ts
function arrayEquals(a, b) {
if (a === b) {
return true;
}
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
function arrayEqualsCompare(a, b, compare) {
if (a === b) {
return true;
}
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!compare(a[i], b[i])) {
return false;
}
}
return true;
}
function modIntoRange(v, rangeMin, rangeMax) {
return offsetMod(v, rangeMax - rangeMin, rangeMin);
}
// src/cubing/twisty/controllers/AnimationTypes.ts
function directionScalar(direction) {
return direction;
}
// src/cubing/twisty/controllers/TwistyAnimationController.ts
var CatchUpHelper = class {
// TODO
constructor(model) {
this.model = model;
model.tempoScale.addFreshListener((tempoScale) => {
this.tempoScale = tempoScale;
});
}
catchingUp = false;
pendingFrame = false;
tempoScale = 1;
scheduler = new RenderScheduler(
this.animFrame.bind(this)
);
start() {
if (!this.catchingUp) {
this.lastTimestamp = performance.now();
}
this.catchingUp = true;
this.pendingFrame = true;
this.scheduler.requestAnimFrame();
}
stop() {
this.catchingUp = false;
this.scheduler.cancelAnimFrame();
}
catchUpMs = 500;
lastTimestamp = 0;
animFrame(timestamp) {
this.scheduler.requestAnimFrame();
const delta = this.tempoScale * (timestamp - this.lastTimestamp) / this.catchUpMs;
this.lastTimestamp = timestamp;
this.model.catchUpMove.set(
(async () => {
const previousCatchUpMove = await this.model.catchUpMove.get();
if (previousCatchUpMove.move === null) {
return previousCatchUpMove;
}
const amount = previousCatchUpMove.amount + delta;
if (amount >= 1) {
this.pendingFrame = true;
this.stop();
this.model.timestampRequest.set("end");
return {
move: null,
amount: 0
};
}
this.pendingFrame = false;
return {
move: previousCatchUpMove.move,
amount
};
})()
);
}
};
var TwistyAnimationController = class {
constructor(model, delegate) {
this.delegate = delegate;
this.model = model;
this.lastTimestampPromise = this.#effectiveTimestampMilliseconds();
this.model.playingInfo.addFreshListener(this.onPlayingProp.bind(this));
this.catchUpHelper = new CatchUpHelper(this.model);
this.model.catchUpMove.addFreshListener(this.onCatchUpMoveProp.bind(this));
}
// TODO: #private?
playing = false;
direction = 1 /* Forwards */;
catchUpHelper;
model;
lastDatestamp = 0;
lastTimestampPromise;
scheduler = new RenderScheduler(
this.animFrame.bind(this)
);
// TODO: Do we need this?
async onPlayingProp(playingInfo) {
if (playingInfo.playing !== this.playing) {
playingInfo.playing ? this.play(playingInfo) : this.pause();
}
}
// TODO: Do we need this?
async onCatchUpMoveProp(catchUpMove) {
const catchingUp = catchUpMove.move !== null;
if (catchingUp !== this.catchUpHelper.catchingUp) {
catchingUp ? this.catchUpHelper.start() : this.catchUpHelper.stop();
}
this.scheduler.requestAnimFrame();
}
async #effectiveTimestampMilliseconds() {
return (await this.model.detailedTimelineInfo.get()).timestamp;
}
// TODO: Return the animation we've switched to.
jumpToStart(options) {
this.model.timestampRequest.set("start");
this.pause();
if (options?.flash) {
this.delegate.flash();
}
}
// TODO: Return the animation we've switched to.
jumpToEnd(options) {
this.model.timestampRequest.set("end");
this.pause();
if (options?.flash) {
this.delegate.flash();
}
}
// TODO: Return the playing info we've switched to.
playPause() {
if (this.playing) {
this.pause();
} else {
this.play();
}
}
// TODO: bundle playing direction, and boundary into `toggle`.
play(options) {
void (async () => {
const direction = options?.direction ?? 1 /* Forwards */;
const coarseTimelineInfo = await this.model.coarseTimelineInfo.get();
if (options?.autoSkipToOtherEndIfStartingAtBoundary ?? true) {
if (direction === 1 /* Forwards */ && coarseTimelineInfo.atEnd) {
this.model.timestampRequest.set("start");
this.delegate.flash();
}
if (direction === -1 /* Backwards */ && coarseTimelineInfo.atStart) {
this.model.timestampRequest.set("end");
this.delegate.flash();
}
}
this.model.playingInfo.set({
playing: true,
direction,
untilBoundary: options?.untilBoundary ?? "entire-timeline" /* EntireTimeline */,
loop: options?.loop ?? false
});
this.playing = true;
this.lastDatestamp = performance.now();
this.lastTimestampPromise = this.#effectiveTimestampMilliseconds();
this.scheduler.requestAnimFrame();
})();
}
pause() {
this.playing = false;
this.scheduler.cancelAnimFrame();
this.model.playingInfo.set({
playing: false,
untilBoundary: "entire-timeline" /* EntireTimeline */
});
}
#animFrameEffectiveTimestampStaleDropper = new StaleDropper();
async animFrame(frameDatestamp) {
if (this.playing) {
this.scheduler.requestAnimFrame();
}
const lastDatestamp = this.lastDatestamp;
const freshenerResult = await this.#animFrameEffectiveTimestampStaleDropper.queue(
Promise.all([
this.model.playingInfo.get(),
this.lastTimestampPromise,
this.model.timeRange.get(),
this.model.tempoScale.get(),
this.model.currentMoveInfo.get()
])
);
const [playingInfo, lastTimestamp, timeRange, tempoScale, currentMoveInfo] = freshenerResult;
if (!playingInfo.playing) {
this.playing = false;
return;
}
let end = currentMoveInfo.earliestEnd;
if (currentMoveInfo.currentMoves.length === 0 || playingInfo.untilBoundary === "entire-timeline" /* EntireTimeline */) {
end = timeRange.end;
}
let start = currentMoveInfo.latestStart;
if (currentMoveInfo.currentMoves.length === 0 || playingInfo.untilBoundary === "entire-timeline" /* EntireTimeline */) {
start = timeRange.start;
}
let delta = (frameDatestamp - lastDatestamp) * directionScalar(this.direction) * tempoScale;
delta = Math.max(delta, 1);
delta *= playingInfo.direction;
let newTimestamp = lastTimestamp + delta;
let newSmartTimestampRequest = null;
if (newTimestamp >= end) {
if (playingInfo.loop) {
newTimestamp = modIntoRange(
newTimestamp,
timeRange.start,
timeRange.end
);
} else {
if (newTimestamp === timeRange.end) {
newSmartTimestampRequest = "end";
} else {
newTimestamp = end;
}
this.playing = false;
this.model.playingInfo.set({
playing: false
});
}
} else if (newTimestamp <= start) {
if (playingInfo.loop) {
newTimestamp = modIntoRange(
newTimestamp,
timeRange.start,
timeRange.end
);
} else {
if (newTimestamp === timeRange.start) {
newSmartTimestampRequest = "start";
} else {
newTimestamp = start;
}
this.playing = false;
this.model.playingInfo.set({
playing: false
});
}
}
this.lastDatestamp = frameDatestamp;
this.lastTimestampPromise = Promise.resolve(
newTimestamp
);
this.model.timestampRequest.set(newSmartTimestampRequest ?? newTimestamp);
}
};
// src/cubing/twisty/controllers/TwistyPlayerController.ts
var TwistyPlayerController = class {
constructor(model, delegate) {
this.model = model;
this.animationController = new TwistyAnimationController(model, delegate);
}
animationController;
jumpToStart(options) {
this.animationController.jumpToStart(options);
}
jumpToEnd(options) {
this.animationController.jumpToEnd(options);
}
togglePlay(play) {
if (typeof play === "undefined") {
this.animationController.playPause();
}
play ? this.animationController.play() : this.animationController.pause();
}
async visitTwizzleLink() {
const a = document.createElement("a");
a.href = await this.model.twizzleLink();
a.target = "_blank";
a.click();
}
};
// src/cubing/twisty/model/props/viewer/ControlPanelProp.ts
var controlsLocations = {
"bottom-row": true,
// default
none: true
};
var ControlPanelProp = class extends SimpleTwistyPropSource {
getDefaultValue() {
return "auto";
}
};
// src/cubing/twisty/views/TwistyViewerWrapper.css.ts
var twistyViewerWrapperCSS = new cssStyleSheetShim();
twistyViewerWrapperCSS.replaceSync(
`
:host {
width: 384px;
height: 256px;
display: grid;
}
.wrapper {
width: 100%;
height: 100%;
display: grid;
overflow: hidden;
}
.wrapper > * {
width: 100%;
height: 100%;
overflow: hidden;
}
.wrapper.back-view-side-by-side {
grid-template-columns: 1fr 1fr;
}
.wrapper.back-view-top-right {
grid-template-columns: 3fr 1fr;
grid-template-rows: 1fr 3fr;
}
.wrapper.back-view-top-right > :nth-child(1) {
grid-row: 1 / 3;
grid-column: 1 / 3;
}
.wrapper.back-view-top-right > :nth-child(2) {
grid-row: 1 / 2;
grid-column: 2 / 3;
}
`
);
// src/cubing/twisty/views/2D/Twisty2DPuzzle.css.ts
var twisty2DSVGCSS = new cssStyleSheetShim();
twisty2DSVGCSS.replaceSync(
`
:host {
width: 384px;
height: 256px;
display: grid;
}
.wrapper {
width: 100%;
height: 100%;
display: grid;
overflow: hidden;
}
.svg-wrapper,
twisty-2d-svg,
svg {
width: 100%;
height: 100%;
display: grid;
min-height: 0;
}
svg {
animation: fade-in 0.25s ease-in;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.hint-facelets-none .hint-facelet {
display: none;
}
`
);
// src/cubing/twisty/views/2D/Twisty2DPuzzle.ts
var Twisty2DPuzzle = class extends ManagedCustomElement {
// TODO: pull when needed.
constructor(model, kpuzzle, svgSource, options, puzzleLoader) {
super();
this.model = model;
this.kpuzzle = kpuzzle;
this.svgSource = svgSource;
this.options = options;
this.puzzleLoader = puzzleLoader;
this.addCSS(twisty2DSVGCSS);
this.resetSVG();
this.#freshListenerManager.addListener(
this.model.puzzleID,
(puzzleID) => {
if (puzzleLoader?.id !== puzzleID) {
this.disconnect();
}
}
);
this.#freshListenerManager.addListener(
this.model.twistySceneModel.hintFacelet,
(hintFacelet) => {
this.setHintFacelet(hintFacelet);
}
);
this.#freshListenerManager.addListener(
this.model.legacyPosition,
this.onPositionChange.bind(this)
);
if (this.options?.experimentalStickeringMask) {
this.experimentalSetStickeringMask(
this.options.experimentalStickeringMask
);
}
}
svgWrapper;
scheduler = new RenderScheduler(this.render.bind(this));
#cachedPosition = null;
#freshListenerManager = new FreshListenerManager();
disconnect() {
this.#freshListenerManager.disconnect();
}
onPositionChange(position) {
try {
if (position.movesInProgress.length > 0) {
const move = position.movesInProgress[0].move;
let partialMove = move;
if (position.movesInProgress[0].direction === -1 /* Backwards */) {
partialMove = move.invert();
}
const newPattern = position.pattern.applyMove(partialMove);
this.svgWrapper?.draw(
position.pattern,
newPattern,
position.movesInProgress[0].fraction
);
} else {
this.svgWrapper?.draw(position.pattern);
this.#cachedPosition = position;
}
} catch (e) {
console.warn(
"Bad position (this doesn't necessarily mean something is wrong). Pre-emptively disconnecting:",
this.puzzleLoader?.id,
e
);
this.disconnect();
}
}
scheduleRender() {
this.scheduler.requestAnimFrame();
}
experimentalSetStickeringMask(stickeringMask) {
this.resetSVG(stickeringMask);
}
// TODO: do this without constructing a new SVG.
resetSVG(stickeringMask) {
if (this.svgWrapper) {
this.removeElement(this.svgWrapper.wrapperElement);
}
if (!this.kpuzzle) {
return;
}
this.svgWrapper = new TwistyAnimatedSVG(
this.kpuzzle,
this.svgSource,
stickeringMask
);
this.addElement(this.svgWrapper.wrapperElement);
if (this.#cachedPosition) {
this.onPositionChange(this.#cachedPosition);
}
}
hintFaceletsClassListManager = new ClassListManager(
this,
"hint-facelets-",
Object.keys(hintFaceletStyles)
);
setHintFacelet(hintFacelet) {
this.hintFaceletsClassListManager.setValue(
hintFacelet === "auto" ? "floating" : hintFacelet
);
}
render() {
}
};
customElementsShim.define("twisty-2d-puzzle", Twisty2DPuzzle);
// src/cubing/twisty/views/2D/Twisty2DPuzzleWrapper.ts
var Twisty2DPuzzleWrapper = class {
constructor(model, schedulable, puzzleLoader, effectiveVisualization) {
this.model = model;
this.schedulable = schedulable;
this.puzzleLoader = puzzleLoader;
this.effectiveVisualization = effectiveVisualization;
void this.twisty2DPuzzle();
this.#freshListenerManager.addListener(
this.model.twistySceneModel.stickeringMask,
async (stickeringMask) => {
(await this.twisty2DPuzzle()).experimentalSetStickeringMask(
stickeringMask
);
}
);
}
#freshListenerManager = new FreshListenerManager();
disconnect() {
this.#freshListenerManager.disconnect();
}
// TODO: Hook this up nicely.
scheduleRender() {
}
#cachedTwisty2DPuzzle = null;
// TODO: Stale dropper?
async twisty2DPuzzle() {
return this.#cachedTwisty2DPuzzle ??= (async () => {
const svgPromise = this.effectiveVisualization === "experimental-2D-LL-face" ? this.puzzleLoader.llFaceSVG() : this.effectiveVisualization === "experimental-2D-LL" ? this.puzzleLoader.llSVG() : this.puzzleLoader.svg();
return new Twisty2DPuzzle(
this.model,
await this.puzzleLoader.kpuzzle(),
await svgPromise,
{},
this.puzzleLoader
);
})();
}
};
// src/cubing/twisty/views/2D/Twisty2DSceneWrapper.ts
var Twisty2DSceneWrapper = class extends ManagedCustomElement {
constructor(model, effectiveVisualization) {
super();
this.model = model;
this.effectiveVisualization = effectiveVisualization;
}
#freshListenerManager = new FreshListenerManager();
disconnect() {
this.#freshListenerManager.disconnect();
}
async connectedCallback() {
this.addCSS(twistyViewerWrapperCSS);
if (this.model) {
this.#freshListenerManager.addListener(
this.model.twistyPlayerModel.puzzleLoader,
this.onPuzzleLoader.bind(this)
);
}
}
#cachedScene;
async scene() {
return this.#cachedScene ??= (async () => new (await bulk3DCode()).ThreeScene())();
}
scheduleRender() {
this.#currentTwisty2DPuzzleWrapper?.scheduleRender();
}
#currentTwisty2DPuzzleWrapper;
currentTwisty2DPuzzleWrapper() {
return this.#currentTwisty2DPuzzleWrapper;
}
// #oldTwisty3DPuzzleWrappers: Twisty3DPuzzleWrapper[] = []; // TODO: Animate these out.
async setCurrentTwisty2DPuzzleWrapper(twisty2DPuzzleWrapper) {
const old = this.#currentTwisty2DPuzzleWrapper;
this.#currentTwisty2DPuzzleWrapper = twisty2DPuzzleWrapper;
old?.disconnect();
const twisty2DPuzzlePromise = twisty2DPuzzleWrapper.twisty2DPuzzle();
this.contentWrapper.textContent = "";
this.addElement(await twisty2DPuzzlePromise);
}
async onPuzzleLoader(puzzleLoader) {
this.#currentTwisty2DPuzzleWrapper?.disconnect();
const twisty2DPuzzleWrapper = new Twisty2DPuzzleWrapper(
this.model.twistyPlayerModel,
this,
puzzleLoader,
this.effectiveVisualization
);
void this.setCurrentTwisty2DPuzzleWrapper(twisty2DPuzzleWrapper);
}
};
customElementsShim.define("twisty-2d-scene-wrapper", Twisty2DSceneWrapper);
// src/cubing/twisty/views/InitialValueTracker.ts
var InitialValueTracker = class {
// @ts-expect-error: We do initialize this synchronously (assuming no one has tampered with the `Promise` constructor).
#resolve;
// @ts-expect-error: We do initialize this synchronously (assuming no one has tampered with the `Promise` constructor).
reject;
// TODO: AbortController?
promise;
constructor() {
this.promise = new Promise((resolve, reject) => {
this.#resolve = resolve;
this.reject = reject;
});
}
handleNewValue(t) {
this.#resolve(t);
}
};
// src/cubing/twisty/views/3D/Twisty3DPuzzleWrapper.ts
var Twisty3DPuzzleWrapper = class extends EventTarget {
constructor(model, schedulable, puzzleLoader, visualizationStrategy) {
super();
this.model = model;
this.schedulable = schedulable;
this.puzzleLoader = puzzleLoader;
this.visualizationStrategy = visualizationStrategy;
void this.twisty3DPuzzle();
this.#freshListenerManager.addListener(
this.model.puzzleLoader,
(puzzleLoader2) => {
if (this.puzzleLoader.id !== puzzleLoader2.id) {
this.disconnect();
}
}
);
this.#freshListenerManager.addListener(
this.model.legacyPosition,
async (position) => {
try {
(await this.twisty3DPuzzle()).onPositionChange(position);
this.scheduleRender();
} catch {
this.disconnect();
}
}
);
this.#freshListenerManager.addListener(
this.model.twistySceneModel.hintFacelet,
async (hintFaceletStyle) => {
(await this.twisty3DPuzzle()).experimentalUpdateOptions({
hintFacelets: hintFaceletStyle === "auto" ? "floating" : hintFaceletStyle
});
this.scheduleRender();
}
);
this.#freshListenerManager.addListener(
this.model.twistySceneModel.foundationDisplay,
async (foundationDisplay) => {
(await this.twisty3DPuzzle()).experimentalUpdateOptions({
showFoundation: foundationDisplay !== "none"
});
this.scheduleRender();
}
);
this.#freshListenerManager.addListener(
this.model.twistySceneModel.stickeringMask,
async (stickeringMask) => {
const twisty3D = await this.twisty3DPuzzle();
twisty3D.setStickeringMask(stickeringMask);
this.scheduleRender();
}
);
this.#freshListenerManager.addListener(
this.model.twistySceneModel.faceletScale,
async (faceletScale) => {
(await this.twisty3DPuzzle()).experimentalUpdateOptions({
faceletScale
});
this.scheduleRender();
}
);
this.#freshListenerManager.addListener(
this.model.twistySceneModel.hintFaceletsElevation,
async (hintFaceletsElevation) => {
(await this.twisty3DPuzzle()).experimentalUpdateOptions({
hintFaceletsElevation
});
this.scheduleRender();
}
);
this.#freshListenerManager.addMultiListener3(
[
this.model.twistySceneModel.stickeringMask,
this.model.twistySceneModel.foundationStickerSprite,
this.model.twistySceneModel.hintStickerSprite
],
async (inputs) => {
if ("experimentalUpdateTexture" in await this.twisty3DPuzzle()) {
(await this.twisty3DPuzzle()).experimentalUpdateTexture(
inputs[0].specialBehaviour === "picture",
inputs[1],
inputs[2]
);
this.scheduleRender();
}
}
);
}
#freshListenerManager = new FreshListenerManager();
disconnect() {
this.#freshListenerManager.disconnect();
}
scheduleRender() {
this.schedulable.scheduleRender();
this.dispatchEvent(new CustomEvent("render-scheduled"));
}
#cachedTwisty3DPuzzle = null;
async twisty3DPuzzle() {
return this.#cachedTwisty3DPuzzle ??= (async () => {
const proxyPromise = bulk3DCode();
if (this.puzzleLoader.id === "3x3x3" && this.visualizationStrategy === "Cube3D") {
const [
foundationSprite,
hintSprite,
experimentalStickeringMask,
initialHintFaceletsAnimation,
faceletScale,
hintFaceletsElevation
] = await Promise.all([
this.model.twistySceneModel.foundationStickerSprite.get(),
this.model.twistySceneModel.hintStickerSprite.get(),
this.model.twistySceneModel.stickeringMask.get(),
this.model.twistySceneModel.initialHintFaceletsAnimation.get(),
this.model.twistySceneModel.faceletScale.get(),
this.model.twistySceneModel.hintFaceletsElevation.get()
]);
return (await proxyPromise).cube3DShim(
() => this.schedulable.scheduleRender(),
{
foundationSprite,
hintSprite,
experimentalStickeringMask,
initialHintFaceletsAnimation,
faceletScale,
hintFaceletsElevation
}
);
} else {
const [hintFacelets, foundationSprite, hintSprite, faceletScale] = await Promise.all([
this.model.twistySceneModel.hintFacelet.get(),
this.model.twistySceneModel.foundationStickerSprite.get(),
this.model.twistySceneModel.hintStickerSprite.get(),
this.model.twistySceneModel.faceletScale.get()
]);
const pg3d = (await proxyPromise).pg3dShim(
() => this.schedulable.scheduleRender(),
this.puzzleLoader,
hintFacelets === "auto" ? "floating" : hintFacelets,
faceletScale,
this.puzzleLoader.id === "kilominx"
// TODO: generalize to other puzzles
);
pg3d.then(
(p) => p.experimentalUpdateTexture(
true,
foundationSprite ?? void 0,
hintSprite ?? void 0
)
);
return pg3d;
}
})();
}
async raycastMove(raycasterPromise, transformations) {
const puzzle = await this.twisty3DPuzzle();
if (!("experimentalGetControlTargets" in puzzle)) {
console.info("not PG3D! skipping raycast");
return;
}
const targets = puzzle.experimentalGetControlTargets();
const [raycaster, movePressCancelOptions] = await Promise.all([
raycasterPromise,
this.model.twistySceneModel.movePressCancelOptions.get()
]);
const intersects = raycaster.intersectObjects(targets);
if (intersects.length > 0) {
const closestMove = puzzle.getClosestMoveToAxis(
intersects[0].point,
transformations
);
if (closestMove) {
this.model.experimentalAddMove(closestMove.move, {
cancel: movePressCancelOptions
});
} else {
console.info("Skipping move!");
}
}
}
};
// src/cubing/twisty/views/3D/Twisty3DSceneWrapper.ts
var Twisty3DSceneWrapper = class extends ManagedCustomElement {
constructor(model) {
super();
this.model = model;
}
// @ts-expect-error TypeScript type inference appears to be borked: ts(2322)
#backViewClassListMana