cubing / cubing.js

🛠 A library for displaying and working with twisty puzzles. Also currently home to the code for Twizzle.
https://js.cubing.net/cubing/
GNU General Public License v3.0
232 stars 42 forks source link

Confused about initializing TwistyPlayer properly #238

Closed anicolao closed 1 year ago

anicolao commented 1 year ago

Steps to reproduce the issue

I recently refactored my cubing site with the intention of making it possible for users to share solves without everyone being authenticated.

The svelte component I wrote doesn't work in this new context; it only works if I first load the site, and later load the component. I really don't have much of a clue as to what the race condition is and am hoping for some pointers to debug it.

Observed behaviour

The error displayed to the user instead of the web page is a 500 return code with the text:

this.attachShadow is not a function
TypeError: this.attachShadow is not a function
    at new ManagedCustomElement (file:///Users/anicolao/projects/rubik/blueroux/node_modules/cubing/dist/esm/chunk-JQLCWEVU.js:350:24)
    at new TwistyPlayerSettable (file:///Users/anicolao/projects/rubik/blueroux/node_modules/cubing/dist/esm/twisty/index.js:3759:5)
    at new TwistyPlayer (file:///Users/anicolao/projects/rubik/blueroux/node_modules/cubing/dist/esm/twisty/index.js:3997:5)
    at Cube.svelte:8:19
    at Object.$$render (/node_modules/svelte/internal/index.mjs:1770:22)
    at eval (/src/lib/components/Solve.svelte:404:85)
    at Object.$$render (/node_modules/svelte/internal/index.mjs:1770:22)
    at eval (/src/routes/embed/+page.svelte:69:93)
    at Object.$$render (/node_modules/svelte/internal/index.mjs:1770:22)
    at Object.default (root.svelte:47:39)

The code that triggers it is in a file called Cube.svelte that looks like this:

<script lang="ts">
    import { CubieCube, Mask } from '$lib/third_party/onionhoney/CubeLib';
    import type { MaskT } from '$lib/third_party/onionhoney/CubeLib';
    import { TwistyPlayer } from 'cubing/twisty';
    import { onMount } from 'svelte';
    import { store } from '$lib/store';

    const twistyPlayer: TwistyPlayer = new TwistyPlayer();
    export let controlPanel = 'none';
    export let scramble = '';
    export let solve = '';
    export let playHead = 0;
    export let stickering = '';
    export let stickeringOrientation = '';
    export let visualization = 'PG3D';

    async function setStickersString(mask: MaskT, priorMask?: MaskT) {
        const cubies = new CubieCube().apply(stickeringOrientation);
        const regular = '-';
        const dim = 'D';
        const muted = 'I';

        function stringify(
            perm: number[],
            cubies: number[],
            mask: number[],
            prior: number[] | undefined
        ) {
            const ret: string[] = [];
            for (let j = 0; j < perm.length; ++j) {
                const i = cubies[perm[j]];
                if (mask[i] === 1) {
                    if (prior && prior[i] === 1) ret.push(dim);
                    else ret.push(regular);
                } else {
                    ret.push(muted);
                }
            }
            return ret.join('');
        }
        //[UF, UL, UB, UR, DF, DL, DB, DR, FL, BL, BR, FR];
        //[UF, UR, UB, UL, DF, DR, DB, DL, FR, FL, BR, BL];
        const edgePerm = [0, 3, 2, 1, 4, 7, 6, 5, 11, 8, 10, 9];
        const edges =
            'EDGES:' + stringify(edgePerm, cubies.ep, mask.ep, priorMask ? priorMask.ep : undefined);

        //  0 .  1 .  2 .  3 .  4 .  5 .  6 .  7
        //[ULF, UBL, URB, UFR, DFL, DLB, DBR, DRF];
        //[UFR, URB, UBL, ULF, DRF, DFL, DLB, DBR
        const cornerPerm: number[] = [3, 2, 1, 0, 7, 4, 5, 6];
        const corners =
            'CORNERS:' + stringify(cornerPerm, cubies.cp, mask.cp, priorMask ? priorMask.cp : undefined);

        //[0, 1, 2, 3, 4, 5];
        //[U, D, F, B, L, R];
        //[U, L, F, R, B, D
        const centerPerm = [0, 4, 2, 5, 3, 1];
        const centers =
            'CENTERS:' +
            stringify(
                centerPerm,
                cubies.tp,
                mask.tp || [1, 1, 1, 1, 1, 1],
                priorMask && priorMask.tp ? priorMask.tp : undefined
            );

        twistyPlayer.experimentalStickeringMaskOrbits = `${edges},${corners},${centers}`;
    }

    async function setStickers(mask: MaskT, priorMask?: MaskT) {
        setStickersString(mask, priorMask);
    }

    $: if (stickering) {
        console.log('RESET STICKERING to: ', stickering);
        const stageToMask: { [key: string]: MaskT } = {};
        stageToMask['fb'] = Mask.fb_mask;
        stageToMask['ss'] = Mask.sb_mask;
        stageToMask['sp'] = Mask.sb_mask;
        stageToMask['cmll'] = Mask.lse_mask;
        stageToMask['lse'] = Mask.solved_mask;
        stageToMask['pre_ss'] = stageToMask['fb'];
        stageToMask['pre_sp'] = stageToMask['fb'];
        stageToMask['pre_cmll'] = stageToMask['sp'];
        stageToMask['pre_lse'] = stageToMask['cmll'];
        if (stageToMask[stickering]) {
            setStickers(stageToMask[stickering], stageToMask['pre_' + stickering]);
        } else if (stickering.indexOf('|') !== -1) {
            const keys = stickering.split('|');
            console.log({ keys });
            const preMask = $store.stages.stageIdToStageMap[keys[0]]?.mask;
            const mask = $store.stages.stageIdToStageMap[keys[1]].mask;
            setStickers(mask, preMask);
        } else {
            setStickers(Mask.solved_mask);
        }
    } else {
        console.log('CLEAR STICKERS');
        setStickers(Mask.solved_mask);
    }

    $: if (scramble) {
        twistyPlayer.experimentalSetupAlg = scramble;
    }
    $: if (solve) {
        twistyPlayer.alg = solve;
    }

    let playerPosition = -1;
    $: if (playHead !== playerPosition) {
        const p = async () => {
            console.log({ playHead });
            twistyPlayer.pause();
            const timestampPromise = (async (): Promise<any> => {
                const indexer = await twistyPlayer.experimentalModel.indexer.get();
                const offset = 250;
                return (indexer.indexToMoveStartTimestamp(playHead) ?? -offset) + offset;
            })();
            twistyPlayer.experimentalModel.timestampRequest.set(
                await timestampPromise // TODO
            );
        };
        p();
    }
    onMount(async () => {
        let contentElem = document.querySelector('#twisty-content');
        if (contentElem) {
            twistyPlayer.background = 'none';
            twistyPlayer.visualization = visualization;
            if (controlPanel === 'none') {
                twistyPlayer.controlPanel = 'none';
            }
            const model = twistyPlayer.experimentalModel;
            model.currentMoveInfo.addFreshListener((currentMoveInfo: any) => {
                playHead = currentMoveInfo.stateIndex;
                playerPosition = playHead;
            });
            twistyPlayer.experimentalSetupAlg = scramble;
            twistyPlayer.alg = solve;
            twistyPlayer.tempoScale = 4;
            twistyPlayer.backView = 'top-right';
            twistyPlayer.hintFacelets = 'none';
            contentElem.appendChild(twistyPlayer);
        }
    });
</script>

<div id="twisty-content" />

<style>
    div {
        text-align: -webkit-center;
    }
</style>

Perhaps it is not safe to call new TwistyPlayer() before there is a DOM?

Expected behaviour

I would have expected the TwistyPlayer class to be instantiable outside of a browser's javascript execution environment, but this might be wrong.

Environment

MacOS Chrome

🖼 Screenshots

No response

Additional info

No response

anicolao commented 1 year ago

Deferring new TwistyPlayer() until onMount made the issue go away, so this can be closed if that is WAI.

lgarron commented 1 year ago

Perhaps it is not safe to call new TwistyPlayer() before there is a DOM?

That depends, but it's certainly possible that SSR code is making assumptions that are incompatible with our use of the DOM. We could avoid .attachShadow() until .connectedCallback(), but I don't think that's a great workaround.

Do you have a self-contained repro I could test against?

anicolao commented 1 year ago

Not really, though the pasted file could be thinned out to not contain the stickering pretty quickly. SSR is disbabled in my application, but the JS inside a svelte component isn't in a normal context until onMount is called. I am not at all familiar with the initialization order as it basically never bites me.

lgarron commented 1 year ago

Unfortunately, I'm not able to reproduce this at all. The following works as expected:

<script lang="ts">
  import { TwistyPlayer } from "cubing/twisty";
  import { onMount } from 'svelte';

  const twistyPlayer = new TwistyPlayer();
  onMount(async () => {
    document.body.appendChild(twistyPlayer);
  });
</script>

I would need a more specific repro to debug this.

anicolao commented 1 year ago

Hmm. I may have made some terrible error and enabled SSR in some way I don't understand. Svelte has been great but has also proven to be a bit of a moving target, I'll see if I can narrow it down ... at the moment, nothing is serving.

anicolao commented 1 year ago

I wound up in some sort of npm versioning purgatory and after much penance, repeated npm update commands and npm obsolete commands, eventually exorcized whatever version mismatch was making svelte unhappy. When all that was done, this problem no longer reproduced...

lgarron commented 1 year ago

I wound up in some sort of npm versioning purgatory and after much penance, repeated npm update commands and npm obsolete commands, eventually exorcized whatever version mismatch was making svelte unhappy. When all that was done, this problem no longer reproduced...

Glad to see it you managed to resolve it! 😄