@vscubing/cubing
Version:
A collection of JavaScript cubing libraries.
1,511 lines (1,458 loc) • 208 kB
JavaScript
import {
DEGREES_PER_RADIAN,
FreshListenerManager,
HTMLElementShim,
HintFaceletProp,
ManagedCustomElement,
NO_VALUE,
RenderScheduler,
SimpleTwistyPropSource,
StaleDropper,
Twisty3DVantage,
TwistyPropDerived,
TwistyPropSource,
bulk3DCode,
cssStyleSheetShim,
customElementsShim,
rawRenderPooled,
setCameraFromOrbitCoordinates,
setTwistyDebug
} from "../chunks/chunk-KDDGYTE3.js";
import {
countAnimatedLeaves,
countLeavesInExpansionForSimultaneousMoveIndexer,
countMetricMoves
} from "../chunks/chunk-6FV4UEXR.js";
import {
cube3x3x3,
puzzles
} from "../chunks/chunk-VJGNDN7F.js";
import {
PuzzleStickering,
StickeringManager,
customPGPuzzleLoader,
getPartialAppendOptionsForPuzzleSpecificSimplifyOptions,
getPieceStickeringMask
} from "../chunks/chunk-TMJHT7ZO.js";
import {
KPattern
} from "../chunks/chunk-S7T73XHS.js";
import {
Alg,
AlgBuilder,
Conjugate,
Grouping,
LineComment,
Move,
Newline,
Pause,
TraversalDownUp,
TraversalUp,
direct,
directedGenerator,
endCharIndexKey,
experimentalAppendMove,
functionFromTraversal,
offsetMod,
startCharIndexKey
} from "../chunks/chunk-2PLBXHXN.js";
// src/cubing/twisty/controllers/AnimationTypes.ts
function directionScalar(direction) {
return direction;
}
var BoundaryType = /* @__PURE__ */ ((BoundaryType2) => {
BoundaryType2["Move"] = "move";
BoundaryType2["EntireTimeline"] = "entire-timeline";
return BoundaryType2;
})(BoundaryType || {});
// 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/TwistyAnimationController.ts
var CatchUpHelper = class {
constructor(model) {
this.model = model;
model.tempoScale.addFreshListener((tempoScale) => {
this.tempoScale = tempoScale;
});
}
catchingUp = false;
pendingFrame = false;
tempoScale;
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`.
async play(options) {
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/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/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; }
}
`
);
// 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.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);
}
}
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;
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 = null;
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
);
this.setCurrentTwisty2DPuzzleWrapper(twisty2DPuzzleWrapper);
}
};
customElementsShim.define("twisty-2d-scene-wrapper", Twisty2DSceneWrapper);
// 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/views/InitialValueTracker.ts
var InitialValueTracker = class {
#resolve;
reject;
// TODO: AbortController?
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;
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 (e) {
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.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
] = await Promise.all([
this.model.twistySceneModel.foundationStickerSprite.get(),
this.model.twistySceneModel.hintStickerSprite.get(),
this.model.twistySceneModel.stickeringMask.get(),
this.model.twistySceneModel.initialHintFaceletsAnimation.get()
]);
return (await proxyPromise).cube3DShim(
() => this.schedulable.scheduleRender(),
{
foundationSprite,
hintSprite,
experimentalStickeringMask,
initialHintFaceletsAnimation
}
);
} 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;
}
#backViewClassListManager = new ClassListManager(this, "back-view-", [
"auto",
"none",
"side-by-side",
"top-right"
]);
#freshListenerManager = new FreshListenerManager();
disconnect() {
this.#freshListenerManager.disconnect();
}
async connectedCallback() {
this.addCSS(twistyViewerWrapperCSS);
const vantage = new Twisty3DVantage(this.model, this);
this.addVantage(vantage);
if (this.model) {
this.#freshListenerManager.addMultiListener(
[this.model.puzzleLoader, this.model.visualizationStrategy],
this.onPuzzle.bind(this)
);
this.#freshListenerManager.addListener(
this.model.backView,
this.onBackView.bind(this)
);
}
this.scheduleRender();
}
#backViewVantage = null;
setBackView(backView) {
const shouldHaveBackView = ["side-by-side", "top-right"].includes(backView);
const hasBackView = this.#backViewVantage !== null;
this.#backViewClassListManager.setValue(backView);
if (shouldHaveBackView) {
if (!hasBackView) {
this.#backViewVantage = new Twisty3DVantage(this.model, this, {
backView: true
});
this.addVantage(this.#backViewVantage);
this.scheduleRender();
}
} else {
if (this.#backViewVantage) {
this.removeVantage(this.#backViewVantage);
this.#backViewVantage = null;
}
}
}
onBackView(backView) {
this.setBackView(backView);
}
async onPress(e) {
const twisty3DPuzzleWrapper = this.#currentTwisty3DPuzzleWrapper;
if (!twisty3DPuzzleWrapper) {
console.info("no wrapper; skipping scene wrapper press!");
return;
}
const raycasterPromise = (async () => {
const [camera, { ThreeRaycaster, ThreeVector2 }] = await Promise.all([
e.detail.cameraPromise,
(async () => {
const { ThreeRaycaster: ThreeRaycaster2, ThreeVector2: ThreeVector22 } = await bulk3DCode();
return { ThreeRaycaster: ThreeRaycaster2, ThreeVector2: ThreeVector22 };
})()
]);
const raycaster = new ThreeRaycaster();
const mouse = new ThreeVector2(
e.detail.pressInfo.normalizedX,
e.detail.pressInfo.normalizedY
);
raycaster.setFromCamera(mouse, camera);
return raycaster;
})();
twisty3DPuzzleWrapper.raycastMove(raycasterPromise, {
invert: !e.detail.pressInfo.rightClick,
depth: e.detail.pressInfo.keys.ctrlOrMetaKey ? "rotation" : e.detail.pressInfo.keys.shiftKey ? "secondSlice" : "none"
});
}
#cachedScene;
async scene() {
return this.#cachedScene ??= (async () => new (await bulk3DCode()).ThreeScene())();
}
#vantages = /* @__PURE__ */ new Set();
addVantage(vantage) {
vantage.addEventListener("press", this.onPress.bind(this));
this.#vantages.add(vantage);
this.contentWrapper.appendChild(vantage);
}
removeVantage(vantage) {
this.#vantages.delete(vantage);
vantage.remove();
vantage.disconnect();
this.#currentTwisty3DPuzzleWrapper?.disconnect();
}
experimentalVantages() {
return this.#vantages.values();
}
scheduleRender() {
for (const vantage of this.#vantages) {
vantage.scheduleRender();
}
}
#currentTwisty3DPuzzleWrapper = null;
// #oldTwisty3DPuzzleWrappers: Twisty3DPuzzleWrapper[] = []; // TODO: Animate these out.
async setCurrentTwisty3DPuzzleWrapper(scene, twisty3DPuzzleWrapper) {
const old = this.#currentTwisty3DPuzzleWrapper;
try {
this.#currentTwisty3DPuzzleWrapper = twisty3DPuzzleWrapper;
old?.disconnect();
scene.add(await twisty3DPuzzleWrapper.twisty3DPuzzle());
} finally {
if (old) {
scene.remove(await old.twisty3DPuzzle());
}
}
this.#initialWrapperTracker.handleNewValue(twisty3DPuzzleWrapper);
}
#initialWrapperTracker = new InitialValueTracker();
/** @deprecated */
async experimentalTwisty3DPuzzleWrapper() {
return this.#currentTwisty3DPuzzleWrapper || this.#initialWrapperTracker.promise;
}
#twisty3DStaleDropper = new StaleDropper();
async onPuzzle(inputs) {
if (inputs[1] === "2D") {
return;
}
this.#currentTwisty3DPuzzleWrapper?.disconnect();
const [scene, twisty3DPuzzleWrapper] = await this.#twisty3DStaleDropper.queue(
Promise.all([
this.scene(),
new Twisty3DPuzzleWrapper(this.model, this, inputs[0], inputs[1])
// TODO
])
);
this.setCurrentTwisty3DPuzzleWrapper(scene, twisty3DPuzzleWrapper);
}
};
customElementsShim.define("twisty-3d-scene-wrapper", Twisty3DSceneWrapper);
// src/cubing/twisty/views/control-panel/TwistyButtons.css.ts
var buttonGridCSS = new cssStyleSheetShim();
buttonGridCSS.replaceSync(
`
:host {
width: 384px;
height: 24px;
display: grid;
}
.wrapper {
width: 100%;
height: 100%;
display: grid;
overflow: hidden;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.wrapper {
grid-auto-flow: column;
}
.viewer-link-none .twizzle-link-button {
display: none;
}
.wrapper twisty-button,
.wrapper twisty-control-button {
width: inherit;
height: inherit;
}
`
);
var buttonCSS = new cssStyleSheetShim();
buttonCSS.replaceSync(
`
:host:not([hidden]) {
display: grid;
}
:host {
width: 48px;
height: 24px;
}
.wrapper {
width: 100%;
height: 100%;
}
button {
width: 100%;
height: 100%;
border: none;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
background-color: rgba(196, 196, 196, 0.75);
}
button:enabled {
background-color: rgba(196, 196, 196, 0.75)
}
.dark-mode button:enabled {
background-color: #88888888;
}
button:disabled {
background-color: rgba(0, 0, 0, 0.4);
opacity: 0.25;
pointer-events: none;
}
.dark-mode button:disabled {
background-color: #ffffff44;
}
button:enabled:hover {
background-color: rgba(255, 255, 255, 0.75);
box-shadow: 0 0 1em rgba(0, 0, 0, 0.25);
cursor: pointer;
}
/* TODO: fullscreen icons have too much padding?? */
.svg-skip-to-start button,
button.svg-skip-to-start {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNTg0IiBoZWlnaHQ9IjM1ODQiIHZpZXdCb3g9IjAgMCAzNTg0IDM1ODQiPjxwYXRoIGQ9Ik0yNjQzIDEwMzdxMTktMTkgMzItMTN0MTMgMzJ2MTQ3MnEwIDI2LTEzIDMydC0zMi0xM2wtNzEwLTcxMHEtOS05LTEzLTE5djcxMHEwIDI2LTEzIDMydC0zMi0xM2wtNzEwLTcxMHEtOS05LTEzLTE5djY3OHEwIDI2LTE5IDQ1dC00NSAxOUg5NjBxLTI2IDAtNDUtMTl0LTE5LTQ1VjEwODhxMC0yNiAxOS00NXQ0NS0xOWgxMjhxMjYgMCA0NSAxOXQxOSA0NXY2NzhxNC0xMSAxMy0xOWw3MTAtNzEwcTE5LTE5IDMyLTEzdDEzIDMydjcxMHE0LTExIDEzLTE5eiIvPjwvc3ZnPg==");
}
.svg-skip-to-end button,
button.svg-skip-to-end {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNTg0IiBoZWlnaHQ9IjM1ODQiIHZpZXdCb3g9IjAgMCAzNTg0IDM1ODQiPjxwYXRoIGQ9Ik05NDEgMjU0N3EtMTkgMTktMzIgMTN0LTEzLTMyVjEwNTZxMC0yNiAxMy0zMnQzMiAxM2w3MTAgNzEwcTggOCAxMyAxOXYtNzEwcTAtMjYgMTMtMzJ0MzIgMTNsNzEwIDcxMHE4IDggMTMgMTl2LTY3OHEwLTI2IDE5LTQ1dDQ1LTE5aDEyOHEyNiAwIDQ1IDE5dDE5IDQ1djE0MDhxMCAyNi0xOSA0NXQtNDUgMTloLTEyOHEtMjYgMC00NS0xOXQtMTktNDV2LTY3OHEtNSAxMC0xMyAxOWwtNzEwIDcxMHEtMTkgMTktMzIgMTN0LTEzLTMydi03MTBxLTUgMTAtMTMgMTl6Ii8+PC9zdmc+");
}
.svg-step-forward button,
button.svg-step-forward {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNTg0IiBoZWlnaHQ9IjM1ODQiIHZpZXdCb3g9IjAgMCAzNTg0IDM1ODQiPjxwYXRoIGQ9Ik0yNjg4IDE1NjhxMCAyNi0xOSA0NWwtNTEyIDUxMnEtMTkgMTktNDUgMTl0LTQ1LTE5cS0xOS0xOS0xOS00NXYtMjU2aC0yMjRxLTk4IDAtMTc1LjUgNnQtMTU0IDIxLjVxLTc2LjUgMTUuNS0xMzMgNDIuNXQtMTA1LjUgNjkuNXEtNDkgNDIuNS04MCAxMDF0LTQ4LjUgMTM4LjVxLTE3LjUgODAtMTcuNSAxODEgMCA1NSA1IDEyMyAwIDYgMi41IDIzLjV0Mi41IDI2LjVxMCAxNS04LjUgMjV0LTIzLjUgMTBxLTE2IDAtMjgtMTctNy05LTEzLTIydC0xMy41LTMwcS03LjUtMTctMTAuNS0yNC0xMjctMjg1LTEyNy00NTEgMC0xOTkgNTMtMzMzIDE2Mi00MDMgODc1LTQwM2gyMjR2LTI1NnEwLTI2IDE5LTQ1dDQ1LTE5cTI2IDAgNDUgMTlsNTEyIDUxMnExOSAxOSAxOSA0NXoiLz48L3N2Zz4=");
}
.svg-step-backward button,
button.svg-step-backward {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNTg0IiBoZWlnaHQ9IjM1ODQiIHZpZXdCb3g9IjAgMCAzNTg0IDM1ODQiPjxwYXRoIGQ9Ik0yNjg4IDIwNDhxMCAxNjYtMTI3IDQ1MS0zIDctMTAuNSAyNHQtMTMuNSAzMHEtNiAxMy0xMyAyMi0xMiAxNy0yOCAxNy0xNSAwLTIzLjUtMTB0LTguNS0yNXEwLTkgMi41LTI2LjV0Mi41LTIzLjVxNS02OCA1LTEyMyAwLTEwMS0xNy41LTE4MXQtNDguNS0xMzguNXEtMzEtNTguNS04MC0xMDF0LTEwNS41LTY5LjVxLTU2LjUtMjctMTMzLTQyLjV0LTE1NC0yMS41cS03Ny41LTYtMTc1LjUtNmgtMjI0djI1NnEwIDI2LTE5IDQ1dC00NSAxOXEtMjYgMC00NS0xOWwtNTEyLTUxMnEtMTktMTktMTktNDV0MTktNDVsNTEyLTUxMnExOS0xOSA0NS0xOXQ0NSAxOXExOSAxOSAxOSA0NXYyNTZoMjI0cTcxMyAwIDg3NSA0MDMgNTMgMTM0IDUzIDMzM3oiLz48L3N2Zz4=");
}
.svg-pause button,
button.svg-pause {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNTg0IiBoZWlnaHQ9IjM1ODQiIHZpZXdCb3g9IjAgMCAzNTg0IDM1ODQiPjxwYXRoIGQ9Ik0yNTYwIDEwODh2MTQwOHEwIDI2LTE5IDQ1dC00NSAxOWgtNTEycS0yNiAwLTQ1LTE5dC0xOS00NVYxMDg4cTAtMjYgMTktNDV0NDUtMTloNTEycTI2IDAgNDUgMTl0MTkgNDV6bS04OTYgMHYxNDA4cTAgMjYtMTkgNDV0LTQ1IDE5aC01MTJxLTI2IDAtNDUtMTl0LTE5LTQ1VjEwODhxMC0yNiAxOS00NXQ0NS0xOWg1MTJxMjYgMCA0NSAxOXQxOSA0NXoiLz48L3N2Zz4=");
}
.svg-play button,
button.svg-play {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNTg0IiBoZWlnaHQ9IjM1ODQiIHZpZXdCb3g9IjAgMCAzNTg0IDM1ODQiPjxwYXRoIGQ9Ik0yNDcyLjUgMTgyM2wtMTMyOCA3MzhxLTIzIDEzLTM5LjUgM3QtMTYuNS0zNlYxMDU2cTAtMjYgMTYuNS0zNnQzOS41IDNsMTMyOCA3MzhxMjMgMTMgMjMgMzF0LTIzIDMxeiIvPjwvc3ZnPg==");
}
.svg-enter-fullscreen button,
button.svg-enter-fullscreen {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjgiIHZpZXdCb3g9IjAgMCAyOCAyOCIgd2lkdGg9IjI4Ij48cGF0aCBkPSJNMiAyaDI0djI0SDJ6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTkgMTZIN3Y1aDV2LTJIOXYtM3ptLTItNGgyVjloM1Y3SDd2NXptMTIgN2gtM3YyaDV2LTVoLTJ2M3pNMTYgN3YyaDN2M2gyVjdoLTV6Ii8+PC9zdmc+");
}
.svg-exit-fullscreen button,
button.svg-exit-fullscreen {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjgiIHZpZXdCb3g9IjAgMCAyOCAyOCIgd2lkdGg9IjI4Ij48cGF0aCBkPSJNMiAyaDI0djI0SDJ6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTcgMThoM3YzaDJ2LTVIN3Yyem0zLThIN3YyaDVWN2gtMnYzem02IDExaDJ2LTNoM3YtMmgtNXY1em0yLTExVjdoLTJ2NWg1di0yaC0zeiIvPjwvc3ZnPg==");
}
.svg-twizzle-tw button,
button.svg-twizzle-tw {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODY0IiBoZWlnaHQ9IjYwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMzk3LjU4MSAxNTEuMTh2NTcuMDg0aC04OS43MDN2MjQwLjM1MmgtNjYuOTU1VjIwOC4yNjRIMTUxLjIydi01Ny4wODNoMjQ2LjM2MXptNTQuMzEgNzEuNjc3bDcuNTEyIDMzLjY5MmMyLjcxOCAxMi4xNiA1LjU4IDI0LjY4IDguNTg0IDM3LjU1NWEyMTgwLjc3NSAyMTgwLjc3NSAwIDAwOS40NDIgMzguODQzIDEyNjYuMyAxMjY2LjMgMCAwMDEwLjA4NiAzNy41NTVjMy43Mi0xMi41OSA3LjM2OC0yNS40NjYgMTAuOTQ1LTM4LjYyOCAzLjU3Ni0xMy4xNjIgNy4wMS0yNi4xMSAxMC4zLTM4Ljg0M2w1Ljc2OS0yMi40NTZjMS4yNDgtNC44ODcgMi40NzItOS43MDUgMy42NzQtMTQuNDU1IDMuMDA0LTExLjg3NSA1LjY1MS0yMi45NjIgNy45NC0zMy4yNjNoNDYuMzU0bDIuMzg0IDEwLjU2M2EyMDAwLjc3IDIwMDAuNzcgMCAwMDMuOTM1IDE2LjgyOGw2LjcxMSAyNy43MWMxLjIxMyA0Ljk1NiAyLjQ1IDkuOTggMy43MDkgMTUuMDczYTMxMTkuNzc3IDMxMTkuNzc3IDAgMDA5Ljg3MSAzOC44NDMgMTI0OS4yMjcgMTI0OS4yMjcgMCAwMDEwLjczIDM4LjYyOCAxOTA3LjYwNSAxOTA3LjYwNSAwIDAwMTAuMzAxLTM3LjU1NSAxMzk3Ljk0IDEzOTcuOTQgMCAwMDkuNjU3LTM4Ljg0M2w0LjQtMTkuMDQ2Yy43MTUtMy4xMyAxLjQyMS02LjIzNiAyLjExOC05LjMyMWw5LjU3Ny00Mi44OGg2Ni41MjZhMjk4OC43MTggMjk4OC43MTggMCAwMS0xOS41MjkgNjYuMzExbC01LjcyOCAxOC40ODJhMzIzNy40NiAzMjM3LjQ2IDAgMDEtMTQuMDE1IDQzLjc1MmMtNi40MzggMTkuNi0xMi43MzMgMzcuNjk4LTE4Ljg4NSA1NC4yOTRsLTMuMzA2IDguODI1Yy00Ljg4NCAxMi44OTgtOS40MzMgMjQuMjYzLTEzLjY0NyAzNC4wOTVoLTQ5Ljc4N2E4NDE3LjI4OSA4NDE3LjI4OSAwIDAxLTIxLjAzMS02NC44MDkgMTI4OC42ODYgMTI4OC42ODYgMCAwMS0xOC44ODUtNjQuODEgMTk3Mi40NDQgMTk3Mi40NDQgMCAwMS0xOC4yNCA2NC44MSAyNTc5LjQxMiAyNTc5LjQxMiAwIDAxLTIwLjM4OCA2NC44MWgtNDkuNzg3Yy00LjY4Mi0xMC45MjYtOS43Mi0yMy43NDMtMTUuMTEtMzguNDUxbC0xLjYyOS00LjQ3Yy01LjI1OC0xNC41MjEtMTAuNjgtMzAuMTkyLTE2LjI2Ni00Ny4wMTRsLTIuNDA0LTcuMjhjLTYuNDM4LTE5LjYtMTMuMDItNDAuMzQ0LTE5Ljc0My02Mi4yMzRhMjk4OC43MDcgMjk4OC43MDcgMCAwMS0xOS41MjktNjYuMzExaDY3LjM4NXoiIGZpbGw9IiM0Mjg1RjQiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvc3ZnPg==");
}
`
);
// src/cubing/twisty/views/document.ts
var globalSafeDocument = typeof document === "undefined" ? null : document;
// src/cubing/twisty/views/control-panel/webkit-fullscreen.ts
var fullscreenEnabled = globalSafeDocument?.fullscreenEnabled || !!globalSafeDocument?.webkitFullscreenEnabled;
function documentExitFullscreen() {
if (document.exitFullscreen) {
return document.exitFullscreen();
} else {
return document.webkitExitFullscreen();
}
}
function documentFullscreenElement() {
if (document.fullscreenElement) {
return document.fullscreenElement;
} else {
return document.webkitFullscreenElement ?? null;
}
}
function requestFullscreen(element) {
if (element.requestFullscreen) {
return element.requestFullscreen();
} else {
return element.webkitRequestFullscreen();
}
}
// src/cubing/twisty/model/props/viewer/ButtonAppearanceProp.ts
var buttonIcons = [
"skip-to-start",
"skip-to-end",
"step-forward",
"step-backward",
"pause",
"play",
"enter-fullscreen",
"exit-fullscreen",
"twizzle-tw"
];
var ButtonAppearanceProp = class extends TwistyPropDerived {
// TODO: This still seems to fire twice for play/pause?
derive(inputs) {
const buttonAppearances = {
fullscreen: {
// TODO: Cache?// TODO: Cache?
enabled: fullscreenEnabled,
icon: (
// TODO: Check against the expected element?
// TODO: This will *not* update when we enter/leave fullscreen. We need to work more closely with the controller.
document.fullscreenElement === null ? "enter-fullscreen" : "exit-fullscreen"
),
title: "Enter fullscreen"
},
"jump-to-start": {
enabled: !inputs.coarseTimelineInfo.atStart,
icon: "skip-to-start",
title: "Restart"
},
"play-step-backwards": {
enabled: !inputs.coarseTimelineInfo.atStart,
icon: "step-backward",
title: "Step backward"
},
"play-pause": {
enabled: !(inputs.coarseTimelineInfo.atStart && inputs.coarseTimelineInfo.atEnd),
icon: inputs.coarseTimelineInfo.playing ? "pause" : "play",
title: inputs.coarseTimelineInfo.playing ? "Pause" : "Play"
},
"play-step": {
enabled: !inputs.coarseTimelineInfo.atEnd,
icon: "step-forward",
title: "Step forward"
},
"jump-to-end": {
enabled: !inputs.coarseTimelineInfo.atEnd,
icon: "skip-to-end",
title: "Skip to End"
},
"twizzle-link": {
enabled: true,
icon: "twizzle-tw",
title: "View at Twizzle",
hidden: inputs.viewerLink === "none"
}
};
return buttonAppearances;
}
};
// src/cubing/twisty/views/control-panel/TwistyButtons.ts
var buttonCommands = {
fullscreen: true,
"jump-to-start": true,
"play-step-backwards": true,
"play-pause": true,
"play-step": true,
"jump-to-end": true,
"twizzle-link": true
};
var TwistyButtons = class extends ManagedCustomElement {
// TODO: Privacy
constructor(model, controller, defaultFullscreenElement) {
super();
this.model = model;
this.controller = controller;
this.defaultFullscreenElement = defaultFullscreenElement;
}
buttons = null;
connectedCallback() {
this.addCSS(buttonGridCSS);
const buttons = {};
for (const command in buttonCommands) {
const button = new TwistyButton();
buttons[command] = button;
button.htmlButton.addEventListener(
"click",
() => this.#onCommand(command)
);
this.addElement(button);
}
this.buttons = buttons;
this.model?.buttonAppearance.addFreshListener(this.update.bind(this));
this.model?.twistySceneModel.colorScheme.addFreshListener(
this.updateColorScheme.bind(this)
);
}
#onCommand(command) {
switch (command) {
case "fullscreen": {
this.onFullscreenButton();
break;
}
case "jump-to-start": {
this.controller?.jumpToStart({ flash: true });
break;
}
case "play-step-backwards": {
this.controller?.animationController.play({
direction: -1 /* Backwards */,
untilBoundary: "move" /* Move */
});
break;
}
case "play-pause": {
this.controller?.togglePlay();
break;
}
case "play-step": {
this.controller?.animationController.play({
direction: 1 /* Forwards */,
untilBoundary: "move" /* Move */
});
break;
}
case "jump-to-end": {
this.controller?.jum