Skip to content

Commit b64d142

Browse files
committed
feat: a wasm/Web-Worker transport — the host supplies the pipe
Third transport beside with_notebook (Jupyter Comm) and no_notebook (http.server + autobahn in threads). In a Web Worker none of that exists — no Jupyter, no servers, no threads — but the host JS environment has postMessage. So the contract is two functions on the JS global scope: * the host defines __trinket_vpython_send(jsonString) BEFORE importing vpython; every outbound update package (and the 'trigger' handshake) goes through it * this module sets __trinket_vpython_dispatch(jsonArrayOfEvents); the host calls it as browser events arrive, and every call — including a bare trigger — is answered with a flush, the same request/reply rhythm the websocket transport uses, paced by the browser's ~33 ms canvas_update timer Selection is sys.platform == 'emscripten' in baseObj.__init__, tried after _isnotebook so a notebook running under Pyodide (JupyterLite) still gets its Comm. Importing the module IS the setup, exactly like the other two: GlowWidget() first (it nulls the module-global sender outside a notebook), then install ours, then an initial trigger() so the scene canvas buffered during `import vpython` flushes and baseObj.sent unblocks appendcmd. Verified end-to-end in trinket's real #108 worker (Pyodide 3.13.2, this branch's pure wheel, deps=False), with send captured on the Python side: MSG 0: {"cmds": [{"cmd": "canvas", ...}, {"cmd": "distant_light", ...} x2]} MSG 1: {"cmds": [{"cmd": "sphere", "idx": 4, "color": [1,0,0], ...}], "attrs": ["a4a1,2,3"]} MSG 1's attrs entry is ball.pos = vector(1,2,3) in the protocol's compact per-attribute wire coding — constructors, attribute updates and the browser-driven flush all cross the seam. What remains is the OTHER half of the pipe: a browser-side consumer of this stream (glowcomm.js reads the identical format from a Comm), which lives in the embedding host, not here.
1 parent 3c06704 commit b64d142

2 files changed

Lines changed: 83 additions & 0 deletions

File tree

‎vpython/trinket_worker.py‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Transport for Pyodide/wasm hosts — e.g. trinket's #108 Web Worker.
2+
3+
The notebook transport talks over a Jupyter Comm; the standalone transport
4+
stands up an ``http.server`` plus autobahn websockets in threads. Neither
5+
exists inside a Web Worker: there is no Jupyter, no server, and no threads.
6+
What a worker does have is a host JS environment with ``postMessage`` — so
7+
here the host supplies the pipe and this module supplies the seam.
8+
9+
The contract with the host, kept deliberately tiny:
10+
11+
* Before ``import vpython``, the host defines ``__trinket_vpython_send`` on
12+
the JS global scope: a function of one argument, a JSON string. Every
13+
outbound update package (and the bare ``"trigger"`` handshake) goes through
14+
it. What the host does with the string — ``postMessage`` it to a page, hand
15+
it to a renderer — is its business.
16+
17+
* This module sets ``__trinket_vpython_dispatch`` on the JS global scope: a
18+
function of one argument, a JSON **array** of browser events in glowcomm's
19+
wire format. The host calls it whenever events arrive. Each call processes
20+
the events and then flushes buffered updates back out through
21+
``__trinket_vpython_send`` — the same request/reply rhythm the websocket
22+
transport uses, with the host (ultimately the browser's ~33 ms
23+
``canvas_update`` timer) setting the pace.
24+
25+
Like the other transports, importing this module IS the setup; it ends by
26+
binding the module-global ``sender`` and starting the update ping-pong.
27+
"""
28+
29+
import json
30+
31+
import js
32+
from pyodide.ffi import create_proxy
33+
34+
from .vpython import GlowWidget, baseObj
35+
from . import vpython as _protocol
36+
37+
38+
def _send(msg):
39+
"""Ship one update package (or the 'trigger' handshake) to the host.
40+
41+
``trigger()`` hands us either the bare string ``'trigger'`` or the
42+
``{cmds, methods, attrs}`` dict from ``baseObj.package``. Encode
43+
uniformly; the receiving side re-parses.
44+
"""
45+
js.__trinket_vpython_send(json.dumps(msg))
46+
47+
48+
def _dispatch(events_json):
49+
"""Process a JSON array of browser events, then flush updates back.
50+
51+
Mirrors the websocket transports: events go one at a time to
52+
``handle_msg`` (bound-event handlers rely on that), and every inbound
53+
message — including a bare trigger with no events — is answered with a
54+
flush. ``_isnotebook`` is False here, so ``handle_msg`` does not trigger
55+
by itself; the flush below is that reply.
56+
"""
57+
events = json.loads(events_json) if events_json else []
58+
for evt in events:
59+
if isinstance(evt, dict) and 'trigger' in evt:
60+
continue # pacing only; the flush below answers it
61+
baseObj.glow.handle_msg({'content': {'data': [evt]}})
62+
baseObj.trigger()
63+
64+
65+
# GlowWidget() records itself as baseObj.glow. Outside a notebook it sets the
66+
# module-global sender to None, so ours is installed after it.
67+
GlowWidget()
68+
_protocol.sender = _send
69+
70+
js.__trinket_vpython_dispatch = create_proxy(_dispatch)
71+
72+
# Start the ping-pong exactly as with_notebook does: the first trigger()
73+
# flushes anything already buffered (the scene canvas constructed during
74+
# `import vpython` is sitting in the buffer at this point) and sets
75+
# baseObj.sent, which appendcmd()/addmethod() spin on.
76+
baseObj.trigger()
77+
78+
# Dummy name to import, matching the other transports.
79+
_ = None

‎vpython/vpython.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ def __init__(self, **kwargs):
263263
baseObj._canvas_constructing):
264264
if _isnotebook:
265265
from .with_notebook import _
266+
elif sys.platform == 'emscripten':
267+
# Pyodide/wasm (e.g. a Web Worker): no Jupyter, no servers, no
268+
# threads. The host JS environment supplies the pipe instead.
269+
from .trinket_worker import _
266270
else:
267271
from .no_notebook import _
268272
baseObj._view_constructed = True

0 commit comments

Comments
 (0)