<body style="margin: 0; padding: 0; background-color: transparent; overflow: hidden">

<script src="../backend.js"></script>
<script src="ui.js"></script>
<script src="dist/QR.js"></script>
<script src="qr.js"></script>

<style>

    .background {
        z-index: -1;
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        overflow: hidden;
        background-color: transparent;
        display: flex;
        justify-content: center;
        align-items: center;
    }

    .poster {
        z-index: 1;
        position: fixed;
        top: 50%;
        left: 50%;
        width: 50%;
        height: 50%;
        transform: translate(-50%, -50%);
        background: transparent;
    }
    .poster-img {
        width: 100%;
        height: 100%;
        object-fit: contain;
    }

    /*  */

    .my-button {
        display: flex;
        flex-direction: row;
        justify-content: center;
        align-items: center;
        text-align: center;
        padding: calc(1vh + 0.5vw) calc(2vw + 1vh); /* Scaled padding */
        background: rgba(0, 0, 0, 0.3); /* Semi-transparent black */
        /* backdrop-filter: blur(6px); */ /* Blur effect */
        border: 1px solid rgba(255, 255, 255, 0.3); /* Light border for definition */
        border-radius: 12px; /* Nicely curved border */
        color: white; /* White text */
        font-size: calc(1vw + 0.5vh); /* Scaled font size */
        font-weight: 400;
        font-family: Montserrat;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow */
        cursor: pointer;
    }

    /*  */

    .qr-container {
        z-index: 1;
        position: absolute;
        top: 50%;
        left: 50%;
        width: 256px;
        height: 256px;
        transform: translate(-50%, -50%);
        padding: 8px;
        border-radius: 8px;
        background: rgba(255, 255, 255, 0.8);
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
        display: none;
    }
    .qr-img {
        width: 100%;
        height: 100%;
    }

    /*  */

    .hover-effect {
        background-color: rgba(0, 0, 0, 0.3);
        color: white;
        transform: scale(1);
        transition: background-color 0.2s, color 0.3s, transform 0.2s;
    }

    .hover-effect:hover {
        background-color: rgba(255, 255, 255, 0.8);
        color: black;
        transform: scale(1.05); /* Slight zoom effect */
    }

    /*  */

    .dot{
        display: none;
    }

    .dim{
        background: #fff;
        border-radius: 4px;
        border: none;
        box-sizing: border-box;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
        color: rgba(0, 0, 0, 0.8);
        display: block;
        font-family: Montserrat, Futura, Helvetica Neue, sans-serif;
        font-size: 0.75em;
        font-weight: 400;
        max-width: 128px;
        overflow-wrap: break-word;
        padding: 0.5em 1em;
        position: absolute;
        width: max-content;
        height: max-content;
        transform: translate3d(-50%, -50%, 0);
        pointer-events: none;
        --min-hotspot-opacity: 0;
    }
    
    .dimensionLineContainer{
        pointer-events: none;
        display: block;
    }
    .dimensionLine{
        stroke: #16a5e6;
        stroke-width: 2;
        stroke-dasharray: 2;
    }

    .hide{
        display: none;
    }
    :not(:defined) > * { /* This keeps child nodes hidden while the element loads */
        display: none;
    }

    /*  */

    .annotation-div {
        z-index: 999;
        background: rgba(255, 255, 255, 0.3); /* Semi-transparent black */
        /* backdrop-filter: blur(6px); */ /* Blur effect for glass-like appearance */
        padding: calc(1vh + 0.5vw) calc(2vw + 1vh); /* Scaled padding */
        border-radius: 8px;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow */
        color: white;
        font-size: calc(1vw + 0.5vh); /* Scaled font size */
        font-family: Montserrat, sans-serif; /* Consistent font */
        max-width: 33%;
        max-height: 33%;
        overflow-y: auto;
        scrollbar-width: thin;
        scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
    } /* Custom scrollbar styles for Webkit (Chrome, Edge, Safari) */
    .annotation-div::-webkit-scrollbar {
        width: 8px;
    }
    .annotation-div::-webkit-scrollbar-thumb {
        background: rgba(255, 255, 255, 0.3);
        border-radius: 4px;
    }
    .annotation-div::-webkit-scrollbar-thumb:hover {
        background: rgba(255, 255, 255, 0.5);
    }
    .annotation-div::-webkit-scrollbar-track {
        background: transparent;
    }

    .annotation-list {
        z-index: 999;
        background: rgba(0, 0, 0, 0.3); /* Semi-transparent black */
        /* backdrop-filter: blur(6px); */ /* Blur effect for glass-like appearance */
        padding: calc(0.5vh + 0.25vw) calc(1vh + 0.5vw); /* Scaled padding */
        border-radius: 8px;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow */
        text-align: right; /* Aligns all child items to the right */
        max-width: 33%;
        max-height: 45%;
        overflow-y: auto;
        scrollbar-width: thin;
        scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
    }

    .annotation-item {
        background: transparent;
        border: 1px solid rgba(255, 255, 255, 0.42); /* Consistent border style */
        border-radius: 8px; /* Rounded for a modern look */
        padding: calc(1vh + 0.5vw) calc(2vw + 1vh); /* Scaled padding */
        margin: calc(0.5vh + 0.25vw) 0; /* Spacing between items */
        width: 100%;
        color: white;
        text-align: center;
        transition: transform 0.2s ease, background-color 0.3s ease;
        cursor: pointer;
    }

</style>

<!--  -->

<script>
    self.ModelViewerElement = self.ModelViewerElement || {}
    self.ModelViewerElement.meshoptDecoderLocation = 'https://cdn.jsdelivr.net/npm/meshoptimizer/meshopt_decoder.js'
</script>

<model-viewer
    id="model-viewer"
    alt="4D Viewer by RunFox"
    poster=""
    src=""
    ios-src=""
    loading="eager"
    reveal="manual"
    environment-image="neutral"
    exposure="1"
    shadow-softness="0.6"
    shadow-intensity="1"
    tone-mapping="Neutral"
    interaction-prompt="none"
    camera-controls
    camera-target="auto auto auto"
    camera-orbit="auto auto auto"
    min-camera-orbit="-Infinity 0deg auto"
    max-camera-orbit="Infinity 90deg auto"
    field-of-view="auto"
    min-field-of-view="0deg"
    max-field-of-view="90deg"
    ar
    ar-scale="fixed"
    ar-modes="scene-viewer webxr quick-look"
    ar-placement="floor"
    visibility="hidden"
>

<!-- 
    autoplay
    auto-rotate
    auto-rotate-delay="10000"
 -->

    <div class="progress-bar hide" slot="progress-bar">
        <div class="update-bar"></div>
    </div>

    <button slot="ar-button" style="display: none;">
    </button>

    <button slot="hotspot-dot+X-Y+Z" class="dot dimdot hide" data-position="1 -1 1" data-normal="1 0 0"></button>
    <button slot="hotspot-dim+X-Y" class="dim dimdot hide" data-position="1 -1 0" data-normal="1 0 0"></button>
    <button slot="hotspot-dot+X-Y-Z" class="dot dimdot hide" data-position="1 -1 -1" data-normal="1 0 0"></button>
    <button slot="hotspot-dim+X-Z" class="dim dimdot hide" data-position="1 0 -1" data-normal="1 0 0"></button>
    <button slot="hotspot-dot+X+Y-Z" class="dot dimdot hide" data-position="1 1 -1" data-normal="0 1 0"></button>
    <button slot="hotspot-dim+Y-Z" class="dim dimdot hide" data-position="0 -1 -1" data-normal="0 1 0"></button>
    <button slot="hotspot-dot-X+Y-Z" class="dot dimdot hide" data-position="-1 1 -1" data-normal="0 1 0"></button>
    <button slot="hotspot-dim-X-Z" class="dim dimdot hide" data-position="-1 0 -1" data-normal="-1 0 0"></button>
    <button slot="hotspot-dot-X-Y-Z" class="dot dimdot hide" data-position="-1 -1 -1" data-normal="-1 0 0"></button>
    <button slot="hotspot-dim-X-Y" class="dim dimdot hide" data-position="-1 -1 0" data-normal="-1 0 0"></button>
    <button slot="hotspot-dot-X-Y+Z" class="dot dimdot hide" data-position="-1 -1 1" data-normal="-1 0 0"></button>

    <svg id="dimLines" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" class="dimensionLineContainer hide">
        <defs>
            <filter id="glow">
                <feGaussianBlur stdDeviation="3" result="coloredBlur" />
                <feMerge>
                    <feMergeNode in="coloredBlur" />
                    <feMergeNode in="coloredBlur" />
                    <feMergeNode in="SourceGraphic" />
                </feMerge>
            </filter>
            <filter id="shadow">
                <feDropShadow dx="2" dy="2" stdDeviation="2" flood-color="black" flood-opacity="0.5" />
            </filter>
        </defs>
        <line class="dimensionLine" filter="url(#glow)"></line>
        <line class="dimensionLine" filter="url(#glow)"></line>
        <line class="dimensionLine" filter="url(#glow)"></line>
        <line class="dimensionLine" filter="url(#glow)"></line>
        <line class="dimensionLine" filter="url(#glow)"></line>
    </svg>
    
    <!--  -->
    
    <div id="storyboard-display" class="annotation-list hide" style="position: absolute; top: calc(2vh + 1vw); right: calc(2vh + 1vw); border-radius: 12px;">
        <ul id="storyboard-list" style="list-style: none; padding: 0; margin: 0;">
        </ul>
    </div>
    
    <div id="annotation-display" class="annotation-div hide" style="position: absolute; bottom: calc(2vh + 1vw); left: 50%;"> <!-- transform: translate(-50%, 0%); -->
        <div id="annotation-div">
        </div>
    </div>

    <button id="next-button" class="my-button hover-effect hide" style="z-index: 1; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 64px; height: 64px; border-radius: 50%;">
        <span class="material-icons">arrow_forward_ios</span>
    </button>

</model-viewer>

<button id="dim-button" class="my-button hide hover-effect" style="position: absolute; bottom: calc(2vh + 1vw); left: calc(2vh + 1vw);">
    <span class="material-icons" style="margin-right: calc(1vh + 0.5vw);">design_services</span>Show Sizes
</button>

<button id="ar-button" class="my-button hide hover-effect" style="position: absolute; bottom: calc(2vh + 1vw); right: calc(2vh + 1vw);">
    <span class="material-icons" style="margin-right: calc(1vh + 0.5vw);">view_in_ar</span>View in AR
</button>

<div style="position:absolute; top: calc(2vh + 1vw); left: calc(2vh + 1vw); display: flex; flex-direction: column; justify-items: start; align-items: start; gap: calc(1vh + 0.5vw);">
    <button id="newchunk-button" class="my-button hide hover-effect">
        <span class="material-icons" style="margin-right: calc(1vh + 0.5vw);">auto_stories</span>New Page
    </button>
    <button id="addtochunk-button" class="my-button hide hover-effect">
        <span class="material-icons" style="margin-right: calc(1vh + 0.5vw);">add_a_photo</span>New Step
    </button>
    <button id="raycast-button" class="my-button hide hover-effect">
        <span class="material-icons" style="margin-right: calc(1vh + 0.5vw);"><span class="material-symbols-outlined">stylus_laser_pointer</span></span>Raycast
    </button>
    <button id="focus-button" class="my-button hide hover-effect">
        <span class="material-icons" style="margin-right: calc(1vh + 0.5vw);">camera</span>Focus
    </button>
    <button id="download-button" class="my-button hide hover-effect">
        <span class="material-icons" style="margin-right: calc(1vh + 0.5vw);">download</span>Save
    </button>
</div>

<div id="qr-container" class="qr-container">
    <img id="qr-img" class="qr-img"></img>
</div>

<div id="background" class="background"></div>

<div id="poster" class="poster">
    <img id="poster-img" class="poster-img" alt="Loading..." />
</div>

<script>

    // Globals //

    const poster = document.getElementById('poster')
    const posterImg = document.getElementById('poster-img')
    const background = document.getElementById('background')
    const modelviewer = document.getElementById('model-viewer')
    const arButton = document.getElementById('ar-button')
    const qrImg = document.getElementById('qr-img')
    const qrContainer = document.getElementById('qr-container')
    const dimButton = document.getElementById('dim-button')
    const dimLines = modelviewer.querySelectorAll('line')
    const dimElements = [...modelviewer.querySelectorAll('button[type="button"].dimdot'), modelviewer.querySelector('#dimLines')]
    const focusButton = document.getElementById("focus-button")
    const raycastButton = document.getElementById("raycast-button")
    const downloadButton = document.getElementById("download-button")
    const newSequenceButton = document.getElementById("newchunk-button")
    const newStepButton = document.getElementById("addtochunk-button")
    const storyboard_display = document.getElementById("storyboard-display")
    const storyboard_list = document.getElementById("storyboard-list")
    const annotation_display = document.getElementById("annotation-display")
    const annotation_div = document.getElementById("annotation-div")
    const nextButton = document.getElementById("next-button")

    // Window Functions //

    window.mw = modelviewer

    var current_background = []
    window.set_background = (bg='transparent', type='', height=null) => {
        current_background = [bg, type, height]
        if (bg.endsWith('.hdr') || bg.endsWith('.hdri') || bg.endsWith('.exr') || type.startsWith('sky') || type.startsWith('env')) {
            if (type.startsWith('sky')) { modelviewer.setAttribute('skybox-image', bg); if (height) modelviewer.setAttribute('skybox-height', `${height}m`) }
            if (type.startsWith('env')) { modelviewer.setAttribute('environment-image', bg) }
        } else if (bg.endsWith('.jpg') || bg.endsWith('.jpeg') || bg.endsWith('.png') || bg.endsWith('.gif')) {
            background.style.backgroundColor = 'transparent'
            background.style.backgroundSize = 'cover'
            background.style.backgroundPosition = 'center'
            background.style.backgroundImage = `url("${bg}")`
        } else {
            background.style.backgroundImage = ''
            background.style.backgroundColor = bg
        }
    }

    // Initializing //

    var ar_mode = true
    var dim_mode = true

    modelviewer.style.top = '0'
    modelviewer.style.left = '0'
    modelviewer.style.width = '100%'
    modelviewer.style.height = '100%'
    modelviewer.style.position = 'absolute'
    modelviewer.style.backgroundColor = 'transparent'

        // QR //

    arButton.addEventListener('click', async (event) => {
        if (modelviewer.canActivateAR) modelviewer.activateAR()
        else {
            qrVisible = !qrVisible
            qrContainer.style.display = qrVisible ? 'block' : 'none'
        }
        event.preventDefault()
        event.stopPropagation()
    }, true)
    window.addEventListener('click', (event) => {
        if (qrVisible) {
            qrVisible = false
            qrContainer.style.display = 'none'
            event.preventDefault()
            event.stopPropagation()
        }
    }, true)

    let qrVisible
    (async () => {
        let qr_imgdata = await create_qr(window.location.href, wh=[512,512])
        let qr_imgdatablob = URL.createObjectURL(await qr_imgdata.getRawData())
        //qrImg.onload = () => { qrImg.src = qr_revert_whiteblack(qrImg); qrImg.onload = null }
        qrImg.src = qr_imgdatablob
    })();

        // Dimensions //

    let dimVisible
    dimButton.addEventListener('click', () => {
        dimVisible = !dimVisible
        dimElements.forEach((element) => {
            if (dimVisible) element.classList.remove('hide')
            else element.classList.add('hide')
        })
        dimButton.innerHTML = `<span class="material-icons" style="margin-right: 8px;">design_services</span>${dimVisible?'Hide':'Show'} Sizes`
    })
    function drawLine(svgLine, dotHotspot1, dotHotspot2, dimensionHotspot) {
        if (dotHotspot1 && dotHotspot2) {
            svgLine.setAttribute('x1', dotHotspot1.canvasPosition.x)
            svgLine.setAttribute('y1', dotHotspot1.canvasPosition.y)
            svgLine.setAttribute('x2', dotHotspot2.canvasPosition.x)
            svgLine.setAttribute('y2', dotHotspot2.canvasPosition.y)
            if (dimensionHotspot && !dimensionHotspot.facingCamera) svgLine.classList.add('hide')
            else svgLine.classList.remove('hide')
        }
    }
    function renderSVG() {
        drawLine(dimLines[0], modelviewer.queryHotspot('hotspot-dot+X-Y+Z'), modelviewer.queryHotspot('hotspot-dot+X-Y-Z'), modelviewer.queryHotspot('hotspot-dim+X-Y'))
        drawLine(dimLines[1], modelviewer.queryHotspot('hotspot-dot+X-Y-Z'), modelviewer.queryHotspot('hotspot-dot+X+Y-Z'), modelviewer.queryHotspot('hotspot-dim+X-Z'))
        drawLine(dimLines[2], modelviewer.queryHotspot('hotspot-dot+X+Y-Z'), modelviewer.queryHotspot('hotspot-dot-X+Y-Z'), modelviewer.queryHotspot('hotspot-dim+Y-Z'))
        drawLine(dimLines[3], modelviewer.queryHotspot('hotspot-dot-X+Y-Z'), modelviewer.queryHotspot('hotspot-dot-X-Y-Z'), modelviewer.queryHotspot('hotspot-dim-X-Z'))
        drawLine(dimLines[4], modelviewer.queryHotspot('hotspot-dot-X-Y-Z'), modelviewer.queryHotspot('hotspot-dot-X-Y+Z'), modelviewer.queryHotspot('hotspot-dim-X-Y'))
    }

    modelviewer.addEventListener('load', () => {
        const center = modelviewer.getBoundingBoxCenter()
        const size = modelviewer.getDimensions()
        const x2 = size.x / 2
        const y2 = size.y / 2
        const z2 = size.z / 2
        modelviewer.updateHotspot({ name: 'hotspot-dot+X-Y+Z', position: `${center.x + x2} ${center.y - y2} ${center.z + z2}` })
        modelviewer.updateHotspot({ name: 'hotspot-dim+X-Y', position: `${center.x + x2 * 1.2} ${center.y - y2 * 1.1} ${center.z}` })
        modelviewer.querySelector('button[slot="hotspot-dim+X-Y"]').textContent = `${(size.z * 100).toFixed(0)} cm`
        modelviewer.updateHotspot({ name: 'hotspot-dot+X-Y-Z', position: `${center.x + x2} ${center.y - y2} ${center.z - z2}` })
        modelviewer.updateHotspot({ name: 'hotspot-dim+X-Z', position: `${center.x + x2 * 1.2} ${center.y} ${center.z - z2 * 1.2}` })
        modelviewer.querySelector('button[slot="hotspot-dim+X-Z"]').textContent = `${(size.y * 100).toFixed(0)} cm`
        modelviewer.updateHotspot({ name: 'hotspot-dot+X+Y-Z', position: `${center.x + x2} ${center.y + y2} ${center.z - z2}` })
        modelviewer.updateHotspot({ name: 'hotspot-dim+Y-Z', position: `${center.x} ${center.y + y2 * 1.1} ${center.z - z2 * 1.1}` })
        modelviewer.querySelector('button[slot="hotspot-dim+Y-Z"]').textContent = `${(size.x * 100).toFixed(0)} cm`
        modelviewer.updateHotspot({ name: 'hotspot-dot-X+Y-Z', position: `${center.x - x2} ${center.y + y2} ${center.z - z2}` })
        modelviewer.updateHotspot({ name: 'hotspot-dim-X-Z', position: `${center.x - x2 * 1.2} ${center.y} ${center.z - z2 * 1.2}` })
        modelviewer.querySelector('button[slot="hotspot-dim-X-Z"]').textContent = `${(size.y * 100).toFixed(0)} cm`
        modelviewer.updateHotspot({ name: 'hotspot-dot-X-Y-Z', position: `${center.x - x2} ${center.y - y2} ${center.z - z2}` })
        modelviewer.updateHotspot({ name: 'hotspot-dim-X-Y', position: `${center.x - x2 * 1.2} ${center.y - y2 * 1.1} ${center.z}` })
        modelviewer.querySelector('button[slot="hotspot-dim-X-Y"]').textContent = `${(size.z * 100).toFixed(0)} cm`
        modelviewer.updateHotspot({ name: 'hotspot-dot-X-Y+Z', position: `${center.x - x2} ${center.y - y2} ${center.z + z2}` })
        renderSVG() //modelviewer.addEventListener('camera-change', renderSVG)
    })

    function animate() {
        if (dimVisible) renderSVG()
        requestAnimationFrame(animate)
    }; animate()

        // Story helpers: Raycast, Focus //
            // these hit positions + the current rotation / target of the camera can be used to determine where you're looking
            // if there is a close hotspot nearby your middle screen gaze (1/4w 1/4h center rectange, sample from center to outside, to try hit something)

    let casting_ray
    raycastButton.addEventListener("click", () => {
        casting_ray = true
    })

    let mouseDownPos
    let last_hitResult
    const MOUSE_MOVE_THRESHOLD = 5
    modelviewer.addEventListener("mousedown", (event) => {
        mouseDownPos = { clientX: event.clientX, clientY: event.clientY }
    }, true)
    modelviewer.addEventListener("mouseup", async (event) => {
        if (!mouseDownPos) return
        if (Math.sqrt(Math.pow(event.clientX-mouseDownPos.clientX,2)+Math.pow(event.clientY-mouseDownPos.clientY,2)) <= MOUSE_MOVE_THRESHOLD) {
            let hitResult = modelviewer.positionAndNormalFromPoint(event.clientX, event.clientY)
            if (hitResult) {
                last_hitResult = hitResult
                let { position, normal } = hitResult
                if (casting_ray) { console.log('RAY position :::', position); console.log('RAY normal :::', normal); console.log('RAY position (str) :::', position.toString()); console.log('RAY normal (str) :::', normal.toString()); alert(`Position: ${position.toString()}\nNormal: ${normal.toString()}`) }
            } else { // SOFT RESET ON OUTSIDE TOUCH
                setTimeout(()=>{
                    if (starting_fov) modelviewer.fieldOfView = starting_fov
                    if (starting_orbit) modelviewer.cameraOrbit = starting_orbit
                    if (starting_target) modelviewer.cameraTarget = starting_target
                },200)
            }
        }
        mouseDownPos = null
        casting_ray = false
    }, false)

    focusButton.addEventListener("click", async (event) => {
        let focus = get_focus()
        alert(`Orbit: ${focus.orbit}\nTarget: ${focus.target}\nFov: ${focus.fov}\nCamPos: ${focus.campos}\nCamRot: ${focus.camrot}`)
    })

    function get_focus() {
        // data-orbit
        let { theta, phi, radius } = modelviewer.getCameraOrbit()
        let longitude = (theta * (180 / Math.PI)).toFixed(3)
        let latitude = (phi * (180 / Math.PI)).toFixed(3)
        radius = radius.toFixed(3)
        console.log('FOCUS orbit :::', longitude+'deg', latitude+'deg', radius+'m')
        console.log('FOCUS orbit (str) :::', modelviewer.getCameraOrbit().toString())
        // data-target
        let target = modelviewer.getCameraTarget()
        target.x = target.x.toFixed(3)
        target.y = target.y.toFixed(3)
        target.z = target.z.toFixed(3)
        console.log('FOCUS target :::', target.x+'m', target.y+'m', target.z+'m')
        console.log('FOCUS target (str) :::', modelviewer.getCameraTarget().toString())
        // data-fov
        let fov = modelviewer.getFieldOfView()
        fov = fov.toFixed(3)
        console.log('FOCUS fov :::', fov+'deg')
        console.log('FOCUS fov (str) :::', modelviewer.getFieldOfView().toString())
        // 3js-scene
        let scene = modelviewer[Object.getOwnPropertySymbols(modelviewer).find(e=>e.description=='scene')]
        window.scene = scene
        console.log('FOCUS scene ::: ', scene)
        console.log('FOCUS camera pos :::', scene.camera.position)
        console.log('FOCUS camera rot :::', scene.camera.rotation)
        //
        return {
            'orbit' : `${longitude}deg ${latitude}deg ${radius}m`,
            'target': `${target.x}m ${target.y}m ${target.z}m`,
            'fov'   : `${fov}deg`,
            'campos': `x${scene.camera.position.x.toFixed(3)} y${scene.camera.position.y.toFixed(3)} z${scene.camera.position.z.toFixed(3)}`,
            'camrot': `x${scene.camera.rotation.x.toFixed(3)} y${scene.camera.rotation.y.toFixed(3)} z${scene.camera.rotation.z.toFixed(3)}`,
        }
    }


        // Storyboard //

    var editor_mode = false
    let storyboard = {} // title -> chunk
    let storyboard_type = 'menu' // "menu" / "next"
    let current_chunk = {}

    newSequenceButton.addEventListener("click", async (event) => {
        let details = (await prompt('New Sequence', {
            "Details": {
                'Title': { "input_type":"text" },
                'Duration': { "input_type":"text" },
                'Transition': { "input_type":"text" },
                'Transition Nexts': { "input_type":"text" },
            }
        }))['Details']
        current_chunk = {
            'position': last_hitResult ? last_hitResult.position:null,
            'normal': last_hitResult ? last_hitResult.normal:null,
            'duration': details['Duration'], // seconds
            'transition': details['Transition'], // delay/click
            'transition_nexts': details['Transition Nexts'], // title1 / title1,etc,etc
        }
        current_chunk['sequence'] = []
        storyboard[details['Title']] = current_chunk
        refresh_storyboard_titles()
    })

    newStepButton.addEventListener("click", async (event) => {
        let focus = get_focus()
        let step = await prompt('New Step', {
            "Exposure": {
                "@ Time"  : { "input_type":"text" },
                "Duration": { "input_type":"text" },
                "From"    : { "input_type":"text" },
                "To"      : { "input_type":"text", 'input_default':'1' },
            },
            "Orbit": {
                "@ Time"  : { "input_type":"text" },
                "Duration": { "input_type":"text" },
                "From"    : { "input_type":"text" },
                "To"      : { "input_type":"text", 'input_default':focus.orbit },
            },
            "Target": {
                "@ Time"  : { "input_type":"text" },
                "Duration": { "input_type":"text" },
                "From"    : { "input_type":"text" },
                "To"      : { "input_type":"text", 'input_default':focus.target },
            },
            "Fov": {
                "@ Time"  : { "input_type":"text" },
                "Duration": { "input_type":"text" },
                "From"    : { "input_type":"text" },
                "To"      : { "input_type":"text", 'input_default':focus.fov },
            },
            "Turntable": {
                "@ Time"  : { "input_type":"text" },
                "Duration": { "input_type":"text" },
                "From"    : { "input_type":"text" },
                "To"      : { "input_type":"text", 'input_default':'6.28' },
            },
            "Animation": {
                "@ Time"    : { "input_type":"text" },
                "To"        : { "input_type":"choice", 'input_default':'', 'input_choices':[...[''],...modelviewer.availableAnimations] },
                "Anim Speed": { "input_type":"text" }, // animation's internal speed, 0.5 1 2 etc
                "Anim Begin": { "input_type":"text" }, // beginning time sec
                "Anim End"  : { "input_type":"text" }, // ending time sec
                "Loop"      : { "input_type":"check", 'input_default':'false' },
            },
            "Audio": {
                "@ Time"  : { "input_type":"text" },
                "Duration": { "input_type":"text" }, // works like "Until" , not interpolate.
                "To"      : { "input_type":"text" },
                "Type"    : { "input_type":"choice", 'input_default':'Narration', 'input_choices':['Narration','Effect','Ambience'] },
            },
            "InnerHTML": {
                "@ Time"    : { "input_type":"text" },
                "Duration"  : { "input_type":"text" }, // works like "Until" , not interpolate.
                "To"        : { "input_type":"text", "input_rows":"5", 'input_default':'<h1>This is a title</h1>\n<h2>This is another title</h2>\n<h3>This is yet another title</h3>\n<p>This is a simple paragraph that goes on for a while, lets see what happens next.</p>\n<a>https://runfox.org Visit the url to see it</a>\n<img>https://cdn.pixabay.com/photo/2023/10/16/10/51/fox-8318961_1280.png 0.5</img>\n<vid>https://www.pexels.com/download/video/15795015/?fps=29.969999313354492&h=1920&w=1080 0.5</vid>\n<frame>https://runfox.org/about 0.75</frame>'},
                "At X"      : { "input_type":"text", "input_default":"l50%" },
                "At Y"      : { "input_type":"text", "input_default":"b2%" },
                "Max Width" : { "input_type":"text", "input_default":"33%" },
                "Max Height": { "input_type":"text", "input_default":"33%" },
            },
            "Misc": {
                "@ Time"      : { "input_type":"text" },
                "update_src"  : { "input_type":"text" },
                "update_frame": { "input_type":"check", 'input_default':'false' }
            },
        })
        for (let [k,v] of Object.entries(step)) if (v['@ Time']) current_chunk['sequence'].push({ ...{'type':k}, ...v }) // tdo: cleanup for entries that are: '' or null  --- the last {v}
        current_chunk['sequence'] = current_chunk['sequence'].sort((a,b)=>parseFloat(a['@ Time'])-parseFloat(b['@ Time']))
        console.log('current chunk after push:', JSON.stringify(current_chunk,null,2)) // todo: rm me debug
    })

    function refresh_storyboard_titles() {
        storyboard_display.classList.remove('hide')
        storyboard_list.innerHTML = ""
        Object.entries(storyboard).forEach(([title, annotation]) => {
            let listItem = document.createElement("li") //listItem.innerHTML = `<button class="annotation-item hover-effect" onclick="storyboard_title_clicked(event, '${title}')">${title}</button>`
            let button = document.createElement('button')
            button.className = 'annotation-item hover-effect'
            button.textContent = title
            button.addEventListener('click', (event) => { storyboard_title_clicked(title); event.stopPropagation(); event.preventDefault() }, true)
            button.addEventListener('mousedown', (event) => { event.stopPropagation(); event.preventDefault() }, true)
            button.addEventListener('mouseup', (event) => { event.stopPropagation(); event.preventDefault() }, true)
            listItem.appendChild(button)
            storyboard_list.appendChild(listItem)
        })
        storyboard_list.scrollTop = storyboard_list.scrollHeight
    }

    async function storyboard_title_clicked(title) {
        if (editor_mode) {
            current_chunk = storyboard[title]
            let result = (await prompt(title, {
                "Editor": {
                    "Play on Exit": { "input_type":"check", "input_default":"false" },
                    "Content"     : { "input_type":"text", "input_default":JSON.stringify(current_chunk,null,2), "input_rows":"10" },
                },
            }))['Editor']
            current_chunk = JSON.parse(result['Content'])
            current_chunk['sequence'] = current_chunk['sequence'].sort((a,b)=>parseFloat(a['@ Time'])-parseFloat(b['@ Time']))
            storyboard[title] = current_chunk
            console.log('current chunk after edit:', current_chunk) // todo: rm me debug
            if (result['Play on Exit']) run_sequence(title)
        }
        else run_sequence(title)
    }


    var playing_title
    var playing_animation
    var playing_audios = []

    async function run_sequence(title) {

        if (playing_title) return 

        playing_title = title
        current_chunk = storyboard[title]

        //

        dimVisible = false

        hide_annotation()

        //

        let sequence = structuredClone(current_chunk['sequence'])
        let sequence_ = []
        const INTERP_INTERVAL = 0.02 // 50 fps
        for (let step of sequence) // "expand" the ones with "Duration" enabled, innerHTML is different.
            if (['Exposure','Orbit','Target','Fov','Turntable'].includes(step['type']) && step['Duration'] && step['Duration']!='0') {
                let duration = +step['Duration']
                let from = step['From']
                let to = step['To']
                let from_values = from.split(' ')
                let to_values = to.split(' ')
                let units = from_values.map(e => e.includes('deg') ? 'deg' : e.includes('m') ? 'm' : '')            
                from_values = from_values.map(e=>e.replace('deg','').replace('m',''))
                to_values = to_values.map(e=>e.replace('deg','').replace('m',''))
                let total_steps = Math.floor(duration/INTERP_INTERVAL)
                for (let s=0; s<=total_steps; s++) {
                    let intermediate_value = []
                    for (let i=0; i<units.length; i++) {
                        let start = +from_values[i]
                        let finish = +to_values[i]
                        let value = start+(finish-start)*(s/total_steps)
                        intermediate_value.push(`${value}${units[i]}`)
                    }
                    sequence_.push({ '@ Time':''+((+step['@ Time'])+(s/total_steps)*duration.toFixed(3)), 'type':step['type'], 'To':intermediate_value.join(' ') , 'interpolated':true })
                }
            } else sequence_.push(step)
    
        sequence_ = sequence_.sort((a,b)=>parseFloat(a['@ Time'])-parseFloat(b['@ Time']))
        sequence_ = sequence_.reduce((acc, value, index) => { acc[index] = value; return acc; }, {}) // i -> { "type":'' "@ Time":'' etc }
        console.log('RUNNING SEQUENCE:',sequence_) // todo: rm me debug

        let t = 0
        let T = +current_chunk['duration']

        while (Object.entries(sequence_).length) {

            for (let [i,e] of Object.entries(sequence_)) {

                let t_next = +Object.values(sequence_)[0]['@ Time']
                await new Promise(resolve => setTimeout(resolve,(t_next-t)*1_000)) //sleep(s)
                t = t_next

                console.log('event hit at t=',e['@ Time'],e)  // Todo: rm me debug

                switch (e['type']) {

                    case 'Exposure':
                        modelviewer.exposure = +e['To']
                        break

                    case 'Orbit':
                        modelviewer.cameraOrbit = e['To']
                        break

                    case 'Target':
                        modelviewer.cameraTarget = e['To']
                        break

                    case 'Fov':
                        modelviewer.fieldOfView = e['To']
                        break

                    case 'Turntable':
                        modelviewer.resetTurntableRotation(e['To'])
                        break

                    case 'Animation':

                        //if (equals(playing_animation, [e['To'],e['Anim Begin'],e['Anim End']])) break
                        //else playing_animation = [e['To'],e['Anim Begin'],e['Anim End']]
                        playing_animation = [e['To'],e['Anim Begin'],e['Anim End']]

                        if (e['To']) {

                            if (modelviewer.paused) modelviewer.play() // modelviewer.play({repetitions:e['Loop']?Infinity:1}) // WTF MW: this fucks up when repetitions is changed.

                            let firstever_animation = !modelviewer.animationName
                            let change_animation = modelviewer.animationName != e['To']

                            if (change_animation) modelviewer.animationName = e['To']
                            
                            if (e['Anim Begin']) { 
                                if (!change_animation || firstever_animation) modelviewer.currentTime = +e['Anim Begin']
                                else setTimeout(() => { modelviewer.currentTime = +e['Anim Begin'] }, .33*1000) // WTF MW: we need to wait a bit after setting animation name :(
                            }
                            
                            if (e['Anim End'] && !e['Loop'])
                                (()=>{
                                    let playing_title_ = title
                                    let playing_animation_ = structuredClone(playing_animation)
                                    setTimeout(() => {
                                        if (playing_title_==playing_title && equals(playing_animation_,playing_animation)) { modelviewer.pause(); console.log('ACB1 done.') }
                                        else console.log('ACB1 cancel.')
                                    }, (+e['Anim End']-+e['Anim Begin'])/modelviewer.timeScale*1000)
                                })();

                            if (e['Loop'])
                                (()=>{
                                    let playing_title_ = title
                                    let playing_animation_ = structuredClone(playing_animation)
                                    if (!e['Anim End']) {
                                        let callback = () => {
                                            if (playing_title_==playing_title && equals(playing_animation_,playing_animation)) { modelviewer.currentTime = +(playing_animation_[1]??0); console.log('ACB2 done.') }
                                            else { modelviewer.removeEventListener('finished', callback); console.log('ACB2 cancel.') }
                                        }
                                        modelviewer.addEventListener('finished', callback)
                                    } else {
                                        let id = setInterval(() => {
                                            if (playing_title_==playing_title && equals(playing_animation_,playing_animation)) { modelviewer.currentTime = +(playing_animation_[1]??0); console.log('ACB3 done.') }
                                            else { clearInterval(id); console.log('ACB3 cancel.') }
                                        }, (+e['Anim End']-+e['Anim Begin'])/modelviewer.timeScale*1000)
                                    }
                                })();

                        } else modelviewer.pause()

                        break

                    case 'InnerHTML':
                        show_annotation(e['To'], e['At X'], e['At Y'], e['Max Width'], e['Max Height'])
                        if (e['Duration']) setTimeout(() => { hide_annotation() }, +e['Duration']*1000)
                        break

                    case 'Audio':
                        if (!e['To']) for (let audio of playing_audios) { audio.pause(); audio.src = ''; audio.load(); audio = null }
                        else {
                            let audio = new Audio(e['To'])
                            audio.loop = e['Type']=='Ambience' // Narration/Effect doesn't loop.
                            audio.play()
                            playing_audios.push(audio)
                            let free_audio = () => { audio.pause(); audio.src = ''; audio.load(); audio = null }
                            if (e['Duration']) setTimeout(() => { free_audio() }, +e['Duration']*1000)
                            if (!audio.loop) audio.addEventListener('ended', () => { free_audio() })
                        }
                        break

                    default: // Misc
                        if (e['update_src']) modelviewer.src = e['update_src']
                        if (e['update_frame']) modelviewer.updateFraming()

                }

                delete sequence_[i]

            }

        }

        await new Promise(resolve => setTimeout(resolve,(T-t)*1_000)) //sleep(s)

        console.log('COMPLETED SEQUENCE.')

        //

        let next_titles = () => {
            let nexts = current_chunk['transition_nexts'].split(',')
            if (nexts.length) return nexts
            let found
            for (let k of Object.keys(storyboard)) {
                if (!found) { if (k==playing_title) found = true }
                else return [k]
            }
        }

        // TODO: if you have nexts[] which is 1 element or multiple element
            // 1 element case: just next button comes up
            // multi element case: the element names come up

        if (!current_chunk['transition']) { // No Transition
            playing_title = null
        }
        else if (current_chunk['transition']=='inf') { // Click Transition
            let next_clicked
            nextButton.style.display = 'flex'
            nextButton.onclick = () => { next_clicked = true; nextButton.style.display = 'none' }
            while (!next_clicked) await new Promise(resolve => setTimeout(resolve,.2*1_000)) //sleep(s)
            hide_annotation()
            playing_title = null; let nexts = next_titles(); if (nexts.length) run_sequence(nexts[0])
        } else { // Auto Transition
            await new Promise(resolve => setTimeout(resolve,+current_chunk['transition']*1_000)) //sleep(s)
            hide_annotation()
            playing_title = null; let nexts = next_titles(); if (nexts.length) run_sequence(nexts[0])
        }

    }

    function show_annotation(innerhtml, x='r1%', y='b1%', max_width='35%', max_height='30%') {

        let aspect_ratio_convert = (ratio) => { let denominator = 10; let numerator = Math.round(ratio * denominator);  while (numerator % 10 === 0 && denominator % 10 === 0) { numerator /= 10; denominator /= 10; }; return `${numerator} / ${denominator}` }

        let innerhtml_ = [];
        for (let line of innerhtml.trim().split("\n")) {
            line = line.trim()
            if (line.startsWith("<a>")) {
                let content = line.slice(3,-4) // Remove <a> and </a>
                let [url, ...text] = content.split(" ")
                innerhtml_.push(`<a href="${url}">${text.join(" ")}</a>`)
            } else if (line.startsWith("<img>")) {
                let content = line.slice(5, -6) // Remove <img> and </img>
                let [src, width] = content.split(" ")
                let style = width ? `style="width: ${parseFloat(width) * 100}%; height: auto;"` : ""
                innerhtml_.push(`<img src="${src}" alt="" ${style}/>`)
            } else if (line.startsWith("<vid>")) {
                let content = line.slice(5, -6) // Remove <vid> and </vid>
                let [src, width] = content.split(" ")
                let style = width ? `style="width: ${parseFloat(width) * 100}%; height: auto;"` : ""
                innerhtml_.push(`<video src="${src}" autoplay muted loop ${style}></video>`)
            } else if (line.startsWith("<frame>")) {
                let content = line.slice(7,-8) // Remove <frame> and </frame>
                let [url, aspectRatio] = content.split(" ")
                innerhtml_.push(`<div style="position: relative; width: 100%; padding-top: ${100*aspectRatio}%; overflow: hidden;"><iframe src="${url}" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"></iframe></div>`)
            } else innerhtml_.push(line)
        }
        innerhtml = innerhtml_.join("\n") 
        innerhtml = `<div class="prose">${innerhtml_.join("\n")}</div>`

        hide_annotation()

        // if (position && normal) {
        //     modelviewer.insertAdjacentHTML('beforeend', `
        //         <div id="hotspot" class="annotation-box" slot="hotspot" data-position="${position.replace('m','')}" data-normal="${normal.replace('m','')}">
        //             <div>${html_entry}</div>
        //         </div>
        //     `)
        // } else {
        annotation_display.classList.remove('hide')
        annotation_div.innerHTML = innerhtml

        annotation_display.style.maxWidth = max_width
        annotation_display.style.maxHeight = max_height
        
        let [tx,ty] = annotation_display.style.transform.replace('translate(','').replace('%, ',' ').replace('%)','').split(' ')
        if (x) {

            if (!x.startsWith('r') && !x.startsWith('l')) x = 'l'+x
            
            let xValue = +x.slice(1).replace('%','')
            if (xValue > 50) x = x.startsWith('l') ? 'r' + (100 - xValue) + '%' : 'l' + (100 - xValue) + '%'
            
            if (x.startsWith('l')) {
                annotation_display.style.right = 'auto'; // Clear any right positioning
                annotation_display.style.left = x.slice(1); // Apply the left offset
                tx = +x.slice(1).replace('%', '') < 35 ? 0 : -50; // Adjust translateX
            }
            if (x.startsWith('r')) {
                annotation_display.style.left = 'auto'; // Clear any left positioning
                annotation_display.style.right = x.slice(1); // Apply the right offset
                tx = +x.slice(1).replace('%', '') < 35 ? 0 : 50; // Adjust translateX for right alignment
            }
        }
        if (y) {

            if (!y.startsWith('t') && !y.startsWith('b')) y = 't'+y

            let yValue = +y.slice(1).replace('%', '')
            if (yValue > 50) y = y.startsWith('t') ? 'b' + (100 - yValue) + '%' : 't' + (100 - yValue) + '%'

            if (y.startsWith('t')) {
                annotation_display.style.bottom = 'auto'; // Clear any bottom positioning
                annotation_display.style.top = y.slice(1); // Apply the top offset
                ty = +y.slice(1).replace('%', '') < 35 ? 0 : -50; // Adjust translateY
            }
            if (y.startsWith('b')) {
                annotation_display.style.top = 'auto'; // Clear any top positioning
                annotation_display.style.bottom = y.slice(1); // Apply the bottom offset
                ty = +y.slice(1).replace('%', '') < 35 ? 0 : 50; // Adjust translateY for bottom alignment
            }
        }

        annotation_display.style.transform = `translate(${tx}%, ${ty}%)`

        // }

        annotation_display.scrollTop = 0

    }
    function hide_annotation() {
        let hotspot_ = modelviewer.querySelector('#hotspot'); if (hotspot_) hotspot_.remove()
        if (!annotation_display.classList.contains('hide')) annotation_display.classList.add('hide')
    }



    // Download .Viewer //

    downloadButton.addEventListener("click", async (event) => {

        let result_ = await prompt('Viewer Settings', {
            "Main": {
                "name" : { "input_type":"text", "input_default":'my' },
                "asset": { "input_type":"text", "input_default":modelviewer.src },
            },
            "Cosmetic": {
                "ar"               : { "input_type":"check", "input_default":ar_mode??true },
                "dims"             : { "input_type":"check", "input_default":dim_mode??true },
                "poster"           : { "input_type":"text", "input_default":viewer?.poster??posterImg.src },
                "poster_minsec"    : { "input_type":"text", "input_default":''+poster_minsec },
                "background"       : { "input_type":"text", "input_default":current_background[0] },
                "background_type"  : { "input_type":"text", "input_default":current_background[1]??'' },
                "background_height": { "input_type":"text", "input_default":current_background[2]??'' },
                "starting_fov"     : { "input_type":"text", "input_default":viewer?.starting_fov||''},
                "starting_orbit"   : { "input_type":"text", "input_default":viewer?.starting_orbit||'' },
                "starting_target"  : { "input_type":"text", "input_default":viewer?.starting_target||'' },
                "min_orbit"        : { "input_type":"text", "input_default":min_orbit||'-Infinity 0deg auto' },
                "max_orbit"        : { "input_type":"text", "input_default":max_orbit||'Infinity 90deg auto' },
                "init_chunk"       : { "input_type":"choice", "input_default":init_chunk||'', 'input_choices':['',...Object.keys(storyboard)]}
            },
            "Story": {
                "storyboard_type": { "input_type":"choice", "input_default":storyboard_type, 'input_choices':['menu','next'] },
                "storyboard"     : { "input_type":"text", "input_default":JSON.stringify(storyboard,null,2), "input_rows":16 },
            },
        })

        let viewer_ = {}
        for (let [k,v] of Object.entries(result_)) for (let [kk,vv] of Object.entries(v)) viewer_[kk] = vv
        let viewer_name = viewer_['name']
        delete viewer_['name']
        viewer_['storyboard'] = JSON.parse(viewer_['storyboard'])

        viewer = JSON.stringify(viewer_,null,4)

        let blob = new Blob([viewer], { type: "application/json" })
        let link = document.createElement("a")
        link.download = `${viewer_name}.viewer`
        link.href = URL.createObjectURL(blob)
        link.click()
        URL.revokeObjectURL(link.href)

    })



    // Loading //


    let poster_minsec = 2
    let poster_starttime = Date.now()

    let init_chunk

    let starting_fov
    let starting_orbit
    let starting_target
    let min_orbit
    let max_orbit


    posterImg.src = 'gifs/loading.gif'
    window.set_background('white')

    async function load_asset(url, url_ios=null) {
        modelviewer.setAttribute('src', url)
        if (!url_ios && await fetch_head(url.replace('.glb','.usdz'))) url_ios = url.replace('.glb','.usdz')
        if (url_ios) modelviewer.setAttribute('ios-src', url_ios)
    }

    modelviewer.addEventListener('load', async () => {

        await new Promise(resolve => setTimeout(resolve, Math.max(0, poster_minsec*1000-(Date.now()-poster_starttime))))
       
        modelviewer.style.visibility = 'visible'
        poster.style.display = 'none'
        modelviewer.dismissPoster()

        //

        console.log('model dimensions:', modelviewer.getDimensions())

        //

        if (!starting_orbit) {
            let { theta, phi, radius } = modelviewer.getCameraOrbit()
            let longitude = (theta * (180 / Math.PI)).toFixed(3) + "deg"
            let latitude = (phi * (180 / Math.PI)).toFixed(3) + "deg"
            let radiusInMeters = radius.toFixed(3) + "m"
            console.log('initial camera orbit:', 'Longitude:', longitude, 'deg', 'Latitude:', latitude, 'deg', 'Radius:', radius, 'm')
            modelviewer.cameraOrbit = `-33deg 70deg ${radius}m`
        } else modelviewer.cameraOrbit = starting_orbit

        //

        if (!starting_fov) {
            let fov = modelviewer.getFieldOfView()
            console.log('initial fov:', fov, 'deg')
            modelviewer.fieldOfView = `${fov*7/6}deg`
        } else modelviewer.fieldOfView = starting_fov

        //

        if (!starting_target) {
            let target = modelviewer.getCameraTarget()
            console.log('initial target:', target)
            modelviewer.cameraTarget = target
        } else modelviewer.cameraTarget = starting_target
        
        //

        if (min_orbit) modelviewer.setAttribute('min-camera-orbit', min_orbit)
        if (max_orbit) modelviewer.setAttribute('max-camera-orbit', max_orbit)

        //

        modelviewer.resetInteractionPrompt()
        modelviewer.resetTurntableRotation(0)
        // modelviewer.jumpCameraToGoal()
        
        //

        // modelviewer.orientation = `${roll.value}deg ${pitch.value}deg ${yaw.value}deg`
        // modelviewer.scale = `${x.value} ${y.value} ${z.value}`
        // modelviewer.updateFraming()

        //

        //

        // modelviewer.setAttribute('autoplay', '')
        // modelviewer.setAttribute('auto-rotate', '')
        // modelviewer.setAttribute('auto-rotate-delay', '10000')

        //

        // modelviewer.setAttribute('skybox-height', modelviewer.getDimensions().y+'1.5m')

        //

        let scene = modelviewer[Object.getOwnPropertySymbols(modelviewer).find(e=>e.description=='scene')]
        scene.camera.far = 1000
        scene.camera.updateProjectionMatrix()

        //

        // modelviewer.animationName = starting_animation
        // modelviewer.play()

        if (init_chunk) {
            console.log('has init chunk.')
            await run_sequence(init_chunk)
            delete viewer['storyboard'][init_chunk]
        }

        if (ar_mode) arButton.classList.remove('hide')

        if (dim_mode) dimButton.classList.remove('hide')

        if (editor_mode) {
            editor_grid()
            newSequenceButton.classList.remove('hide')
            newStepButton.classList.remove('hide')
            raycastButton.classList.remove('hide')
            focusButton.classList.remove('hide')
            downloadButton.classList.remove('hide')
        }

        if ((editor_mode || storyboard_type=='menu') && Object.keys(storyboard).length) refresh_storyboard_titles()
        
        //

    })



    // main //

    var viewer

    async function main() {
        let url = new URL(window.location.href)
        let params = new URLSearchParams(url.search)
        console.log('url params:',params.toString())
        if (params.toString()) {

            if (params.toString().endsWith('=')) {
                let asset = params.toString().slice(0,-1)
                if (!asset.includes('.')) asset += '.glb'
                load_asset(asset)
            }
            
            else {

                let asset = params.get('asset')
                let editor = params.get('editor')
                    viewer = params.get('viewer')
                
                if (editor) editor_mode = editor!='false'
                if (asset && !asset.includes('.')) asset+='.glb'
                if (asset && !viewer && await fetch_head(asset.replace('.glb','.viewer'))) viewer = asset.replace('.glb','.viewer')

                if (viewer) {
                    if (!viewer.includes('.')) viewer+='.viewer'
                    viewer = await fetch_(viewer)

                    asset ??=viewer['asset']

                    ar_mode = viewer['ar']
                    dim_mode = viewer['dims']
  
                    if (viewer['poster']) posterImg.src = viewer['poster']
                    if (viewer['poster_minsec']) poster_minsec = +viewer['poster_minsec']
                    if (viewer['background']) window.set_background(viewer['background'], viewer['background_type'], viewer['background_height'])
                    if (viewer['storyboard']) { storyboard = viewer['storyboard']; storyboard_type = viewer['storyboard_type'] }
                    if (viewer['init_chunk']) { init_chunk = viewer['init_chunk'] }

                    min_orbit = viewer['min_orbit']
                    max_orbit = viewer['max_orbit']
                    starting_fov = viewer['starting_fov']
                    starting_orbit = viewer['starting_orbit']
                    starting_target = viewer['starting_target']

                }

                if (asset) load_asset(asset)

            }
        }
    }; main()

    window.addEventListener('dragover', (e) => {
        e.preventDefault()
        e.stopPropagation()
    })
    window.addEventListener('drop', (e) => {
        e.preventDefault()
        e.stopPropagation()
        let file = e.dataTransfer.files[0]
        console.log('loading file:',file.name)
        load_asset(URL.createObjectURL(file), file.name)
    })
    
    //


    // Helpers //

    function editor_grid() {
        let canvas = document.createElement('canvas')
        let ctx = canvas.getContext('2d')
        document.body.appendChild(canvas)
        canvas.style.pointerEvents = 'none'
        canvas.style.position = 'absolute'
        canvas.style.top = '0'
        canvas.style.left = '0'
        canvas.style.zIndex = '-1'
        //
        function resizeCanvas() {
            let minDimension = Math.min(window.innerWidth, window.innerHeight)
            canvas.width = minDimension
            canvas.height = minDimension
            canvas.style.width = `${minDimension}px`
            canvas.style.height = `${minDimension}px`
            if (window.innerWidth > window.innerHeight) {
                canvas.style.left = '50%'
                canvas.style.top = '0'
                canvas.style.transform = 'translateX(-50%)'
            } else {
                canvas.style.left = '0'
                canvas.style.top = '50%'
                canvas.style.transform = 'translateY(-50%)'
            }
            drawGrid(minDimension)
        }
        //
        function drawGrid(size) {
            ctx.clearRect(0, 0, size, size)
            const cellSize = size / 3
            // Vertical lines
            for (let i = 1; i <= 2; i++) {
                const x = i * cellSize
                ctx.beginPath()
                ctx.moveTo(x, 0)
                ctx.lineTo(x, size)
                ctx.stroke()
            }
            // Horizontal lines
            for (let i = 1; i <= 2; i++) {
                const y = i * cellSize
                ctx.beginPath()
                ctx.moveTo(0, y)
                ctx.lineTo(size, y)
                ctx.stroke()
            }
        }
        //
        window.addEventListener('resize', resizeCanvas)
        resizeCanvas()
    }

</script>
</body>