Source code for simvx.core.input.web_events

"""Decode the browser input wire format into :class:`InputRouter` calls.

Two paths deliver input from a browser: the exported static page
(``simvx.web``) and the streaming dev client (``simvx.graphics.streaming``).
Both page scripts serialise the same JSON events, so both decode here.

Event shapes, all keys optional with the defaults shown:

===========  =========================================================
``key``      ``code`` (engine key code), ``pressed``, ``repeat``
``char``     ``codepoint``
``mouse``    ``button`` (DOM ordering), ``pressed``
``mousemove````x``/``y``, or ``dx``/``dy`` with ``relative`` under pointer lock
``scroll``   ``dx``, ``dy`` (positive ``dy`` scrolls up)
``touch``    ``id``, ``action`` (0=down, 1=up, 2=move), ``x``, ``y``, ``pressure``
===========  =========================================================

Any event may carry ``ctrl``/``shift``/``alt``/``meta`` modifier flags.
"""

import logging
from collections.abc import Iterable
from typing import Any

from .enums import MouseButton
from .router import InputRouter

log = logging.getLogger(__name__)

# DOM MouseEvent.button numbers the auxiliary (middle) button 1 and the
# secondary (right) button 2; the engine follows GLFW, which has those two the
# other way round. Remapping here rather than in the page scripts means already
# exported bundles are corrected too, without a re-export.
_DOM_BUTTONS = {0: MouseButton.LEFT, 1: MouseButton.MIDDLE, 2: MouseButton.RIGHT}

_MOD_KEYS = ("ctrl", "shift", "alt", "meta")


def _mods(evt: dict[str, Any]) -> dict[str, bool]:
    """Extract modifier flags from an event, defaulting to not-held."""
    return {name: bool(evt.get(name, False)) for name in _MOD_KEYS}


def _drop_touch_synthesized_mouse(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Remove the mouse events a page emits alongside its touch events.

    Page scripts have historically sent a synthetic left-button ``mouse`` event
    and a ``mousemove`` next to every touch, so that pages predating touch
    support still received clicks. The router emulates the pointer from the
    touch itself, so those duplicates would dispatch every tap twice. Touch and
    real mouse input do not meaningfully interleave inside one frame's batch,
    so dropping them whenever the batch carries touch is safe, and it keeps
    already-exported pages working correctly.
    """
    if not any(evt.get("type") == "touch" for evt in events):
        return events
    return [
        evt
        for evt in events
        if not (evt.get("type") == "mousemove" or (evt.get("type") == "mouse" and evt.get("button", 0) == 0))
    ]


[docs] def route_browser_events(router: InputRouter, events: Iterable[dict[str, Any]]) -> None: """Route a batch of browser input events through *router*. Args: router: The :class:`InputRouter` bound to the running scene tree. events: Decoded JSON event objects, in the order the page sent them. """ for evt in _drop_touch_synthesized_mouse(list(events)): etype = evt.get("type") if etype == "key": code = int(evt.get("code", 0)) pressed = bool(evt.get("pressed", False)) # A held key repeats: the browser re-fires keydown, and pages that # predate the `repeat` flag do not say so. Treating a press of an # already-held key as a repeat keeps game actions firing once per # physical press on those pages too. echo = bool(evt.get("repeat", False)) or (pressed and router.is_key_down(code)) router.key(code, pressed, echo=echo, **_mods(evt)) elif etype == "char": router.char(chr(int(evt.get("codepoint", 0)))) elif etype == "mouse": button = _DOM_BUTTONS.get(int(evt.get("button", 0))) if button is not None: router.mouse_button(button, bool(evt.get("pressed", False)), **_mods(evt)) elif etype == "mousemove": if evt.get("relative"): router.mouse_motion_relative(float(evt.get("dx", 0.0)), float(evt.get("dy", 0.0))) else: router.mouse_motion(float(evt.get("x", 0.0)), float(evt.get("y", 0.0))) elif etype == "scroll": router.scroll(float(evt.get("dx", 0.0)), float(evt.get("dy", 0.0))) elif etype == "touch": router.touch( int(evt.get("id", 0)), int(evt.get("action", 0)), float(evt.get("x", 0.0)), float(evt.get("y", 0.0)), float(evt.get("pressure", 1.0)), )
__all__ = ["route_browser_events"]