UNPKG

@vscubing/cubing

Version:

A collection of JavaScript cubing libraries.

1,520 lines (1,467 loc) 207 kB
import { DEGREES_PER_RADIAN, FreshListenerManager, HTMLElementShim, HintFaceletProp, ManagedCustomElement, NO_VALUE, RenderScheduler, SimpleTwistyPropSource, StaleDropper, THREEJS, Twisty3DVantage, TwistyPropDerived, TwistyPropSource, cssStyleSheetShim, customElementsShim, proxy3D, rawRenderPooled, setCameraFromOrbitCoordinates, setTwistyDebug } from "../chunks/chunk-7PX3O4TS.js"; import { countAnimatedLeaves, countLeavesInExpansionForSimultaneousMoveIndexer, countMetricMoves } from "../chunks/chunk-6Y3EQTJY.js"; import { cube3x3x3, puzzles } from "../chunks/chunk-SYALPVNJ.js"; import { PuzzleStickering, StickeringManager, customPGPuzzleLoader, getPartialAppendOptionsForPuzzleSpecificSimplifyOptions, getPieceStickeringMask } from "../chunks/chunk-5EP7MK62.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 THREEJS).Scene())(); } 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 = proxy3D(); 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, three] = await Promise.all([ e.detail.cameraPromise, THREEJS ]); const raycaster = new three.Raycaster(); const mouse = new (await THREEJS).Vector2( 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 THREEJS).Scene())(); } #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?.jumpToEnd({ flash: true }); break; } case "twizzle-link": { this.controller?.visitTwizzleLink(); break; } default: throw new Error("Missing command"); } } // TODO: Should we h