UNPKG

cubing

Version:

A collection of JavaScript cubing libraries.

1,696 lines (1,664 loc) 189 kB
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