pymations 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +24 -2
  3. package/THIRD-PARTY-NOTICES +217 -0
  4. package/engine/anim.js +8 -0
  5. package/engine/fonts/KaTeX_AMS-Regular.ttf +0 -0
  6. package/engine/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  7. package/engine/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  8. package/engine/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  9. package/engine/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  10. package/engine/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  11. package/engine/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  12. package/engine/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  13. package/engine/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  14. package/engine/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  15. package/engine/fonts/KaTeX_Main-Bold.ttf +0 -0
  16. package/engine/fonts/KaTeX_Main-Bold.woff2 +0 -0
  17. package/engine/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  18. package/engine/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  19. package/engine/fonts/KaTeX_Main-Italic.ttf +0 -0
  20. package/engine/fonts/KaTeX_Main-Italic.woff2 +0 -0
  21. package/engine/fonts/KaTeX_Main-Regular.ttf +0 -0
  22. package/engine/fonts/KaTeX_Main-Regular.woff2 +0 -0
  23. package/engine/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  24. package/engine/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  25. package/engine/fonts/KaTeX_Math-Italic.ttf +0 -0
  26. package/engine/fonts/KaTeX_Math-Italic.woff2 +0 -0
  27. package/engine/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  28. package/engine/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  29. package/engine/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  30. package/engine/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  31. package/engine/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  32. package/engine/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  33. package/engine/fonts/KaTeX_Script-Regular.ttf +0 -0
  34. package/engine/fonts/KaTeX_Script-Regular.woff2 +0 -0
  35. package/engine/fonts/KaTeX_Size1-Regular.ttf +0 -0
  36. package/engine/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  37. package/engine/fonts/KaTeX_Size2-Regular.ttf +0 -0
  38. package/engine/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  39. package/engine/fonts/KaTeX_Size3-Regular.ttf +0 -0
  40. package/engine/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  41. package/engine/fonts/KaTeX_Size4-Regular.ttf +0 -0
  42. package/engine/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  43. package/engine/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  44. package/engine/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  45. package/engine/fonts/LICENSE_KATEX_FONTS.txt +106 -0
  46. package/engine/fonts/LICENSE_LIBERATION.txt +102 -0
  47. package/engine/fonts/LiberationSans-Bold.ttf +0 -0
  48. package/engine/fonts/LiberationSans-BoldItalic.ttf +0 -0
  49. package/engine/fonts/LiberationSans-Italic.ttf +0 -0
  50. package/engine/fonts/LiberationSans-Regular.ttf +0 -0
  51. package/engine/katex.min.css +1 -0
  52. package/engine/katex.min.js +1 -0
  53. package/engine/opentype.min.js +2 -0
  54. package/package.json +34 -4
  55. package/pyctures.py +895 -0
  56. package/pymations.js +1805 -0
  57. package/pymations.py +4346 -0
package/pyctures.py ADDED
@@ -0,0 +1,895 @@
1
+ # GENERATED by circuitry/scripts/sync-pymations-lib.mjs from src/lib/python-canvas/canvas-runtime-py.ts (version 12).
2
+ # Do not edit here: change it in Circuitry, then run the script.
3
+ # Pymations is MIT licensed; see LICENSE.
4
+ import sys, io, json, time, math, traceback, threading, types
5
+
6
+ VERSION = "12"
7
+ USER_FILE = "main.py"
8
+
9
+ # Fixed handles the frame defines before anything runs.
10
+ H_WINDOW, H_CANVAS, H_THREE, H_CTX, H_ADDONS = 1, 2, 3, 4, 5
11
+
12
+
13
+ class _State:
14
+ def __init__(self):
15
+ self.cmds = []
16
+ self.next_read = 1
17
+ self.reads = {} # read id -> result once answered
18
+ self.callbacks = {} # callback id -> Python function
19
+ self.next_cb = 1
20
+ self.tasks = [] # live coroutines: [coro, waiting]
21
+ self.ns = None # the user's namespace
22
+ self.out = io.StringIO()
23
+ self.err = io.StringIO()
24
+ self.started = 0.0
25
+ self.frame_no = 0
26
+ self.thread = None
27
+ self.busy = False
28
+ self.stopped = False
29
+ self.pictures = {} # (text, size) -> a canvas with the text drawn once
30
+ self.measuring = False # measure mode: time each part of a frame
31
+ self.sections = {} # name -> ms this frame (measure mode)
32
+ self.vt = None # virtual seconds while recording, else None
33
+ self.live = False # frame code has run (draw(), await frame(), an event)
34
+
35
+
36
+ # WHICH RUN A THREAD IS WORKING FOR. On the device every frame is its own
37
+ # request on its own thread, and a frame from a run the user has just stopped
38
+ # can still be finishing while the next run starts. If both shared one state,
39
+ # the old frame's commands landed in the new run (objects the new preview never
40
+ # made). So each start()/tick() pins its run's state to its thread, and \`S\`
41
+ # reads through that pin; with no pin, it is the latest run.
42
+ _tls = threading.local()
43
+ _latest = [_State()]
44
+
45
+
46
+ class _Current:
47
+ __slots__ = ()
48
+
49
+ def __getattr__(self, name):
50
+ return getattr(getattr(_tls, "state", None) or _latest[0], name)
51
+
52
+ def __setattr__(self, name, value):
53
+ setattr(getattr(_tls, "state", None) or _latest[0], name, value)
54
+
55
+
56
+ S = _Current()
57
+
58
+ # Never reset between runs: a reference from the LAST run that is collected
59
+ # during this one sends a 'free' for its number, and that number must not
60
+ # belong to anything new.
61
+ _next_handle = [1000]
62
+
63
+
64
+ def _user_line():
65
+ f = sys._getframe(1)
66
+ while f is not None:
67
+ if f.f_code.co_filename == USER_FILE:
68
+ return f.f_lineno
69
+ f = f.f_back
70
+ return 0
71
+
72
+
73
+ def _emit(*cmd):
74
+ S.cmds.append([cmd[0], _user_line(), *cmd[1:]])
75
+
76
+
77
+ def _new_handle():
78
+ _next_handle[0] += 1
79
+ return _next_handle[0]
80
+
81
+
82
+ class PreviewValueError(TypeError):
83
+ pass
84
+
85
+
86
+ _READ_HINT = ("this value lives in the preview, not in Python — "
87
+ "read it with value = await <expression>")
88
+
89
+
90
+ class _Read:
91
+ """An awaitable for one value the frame has to send back.
92
+
93
+ A PROMISE is awaited in the preview: what it resolves to comes back as
94
+ data, or, when it is an object (a GPU adapter, a device, a loaded font),
95
+ as a reference that can be used like any other. Its handle number is
96
+ reserved here, with the read, so the two sides never disagree on it.
97
+ """
98
+ __slots__ = ("rid",)
99
+
100
+ def __init__(self, encoded):
101
+ self.rid = S.next_read
102
+ S.next_read += 1
103
+ _emit("read", self.rid, encoded, _new_handle())
104
+
105
+ def __await__(self):
106
+ result = yield self
107
+ if isinstance(result, dict) and len(result) == 1 and "$h" in result:
108
+ return JsRef(result["$h"], owned=True)
109
+ if isinstance(result, dict) and len(result) == 1 and "$error" in result:
110
+ raise RuntimeError(result["$error"])
111
+ return result
112
+
113
+
114
+ def _clock():
115
+ """The run's clock: virtual seconds while recording, else the wall clock."""
116
+ vt = S.vt
117
+ return time.monotonic() if vt is None else vt
118
+
119
+
120
+ def now():
121
+ """Seconds since the run started (the video's time while recording)."""
122
+ vt = S.vt
123
+ return time.monotonic() - S.started if vt is None else vt
124
+
125
+
126
+ class _Frame:
127
+ """'await frame()' — park until the next frame (also sends this one)."""
128
+ __slots__ = ("until",)
129
+
130
+ def __init__(self, seconds=0.0):
131
+ self.until = _clock() + seconds
132
+
133
+ def __await__(self):
134
+ S.live = True
135
+ yield self
136
+
137
+
138
+ def frame():
139
+ return _Frame()
140
+
141
+
142
+ def flush():
143
+ """Send what has been drawn so far, now, and carry on next frame."""
144
+ return _Frame()
145
+
146
+
147
+ def sleep(seconds):
148
+ return _Frame(seconds)
149
+
150
+
151
+ # ---------------------------------------------------------------- expressions
152
+
153
+ class _Expr:
154
+ __slots__ = ("tree",)
155
+
156
+ def __init__(self, tree):
157
+ object.__setattr__(self, "tree", tree)
158
+
159
+ def _enc(self):
160
+ return {"$e": self.tree}
161
+
162
+ def __await__(self):
163
+ return _Read(self._enc()).__await__()
164
+
165
+ def __bool__(self):
166
+ raise PreviewValueError(_READ_HINT)
167
+
168
+ def __repr__(self):
169
+ return "<preview expression>"
170
+
171
+
172
+ def _expr_of(v):
173
+ if isinstance(v, _Expr):
174
+ return v.tree
175
+ if isinstance(v, JsRef):
176
+ return ["ref", v._enc()]
177
+ return ["v", _encode(v)]
178
+
179
+
180
+ def _binop(op):
181
+ def fwd(self, other):
182
+ return _Expr([op, _expr_of(self), _expr_of(other)])
183
+
184
+ def rev(self, other):
185
+ return _Expr([op, _expr_of(other), _expr_of(self)])
186
+ return fwd, rev
187
+
188
+
189
+ for _name, _op in (("add", "+"), ("sub", "-"), ("mul", "*"), ("truediv", "/"),
190
+ ("mod", "%"), ("pow", "**"), ("floordiv", "//")):
191
+ _f, _r = _binop(_op)
192
+ setattr(_Expr, "__%s__" % _name, _f)
193
+ setattr(_Expr, "__r%s__" % _name, _r)
194
+ _Expr.__neg__ = lambda self: _Expr(["neg", _expr_of(self)])
195
+
196
+
197
+ # ---------------------------------------------------------------- references
198
+
199
+ class JsRef:
200
+ """A JavaScript object in the preview frame, seen from Python."""
201
+ __slots__ = ("_h", "_parent", "_name", "_shadow", "_kids", "_owned", "_cmd", "__weakref__")
202
+
203
+ def __init__(self, handle=None, parent=None, name=None, owned=False, cmd=None):
204
+ object.__setattr__(self, "_h", handle)
205
+ object.__setattr__(self, "_parent", parent)
206
+ object.__setattr__(self, "_name", name)
207
+ object.__setattr__(self, "_shadow", {})
208
+ object.__setattr__(self, "_kids", {})
209
+ object.__setattr__(self, "_owned", owned)
210
+ # The unsent command that made this value. While it is still unsent and
211
+ # the value is unused, a discarded result needs no handle at all.
212
+ object.__setattr__(self, "_cmd", cmd)
213
+
214
+ def _use(self):
215
+ object.__setattr__(self, "_cmd", None)
216
+
217
+ # A lazy member becomes a real handle the first time it is used as one.
218
+ def _handle(self):
219
+ self._use()
220
+ if self._h is None:
221
+ h = _new_handle()
222
+ _emit("get", h, self._parent._handle(), self._name)
223
+ object.__setattr__(self, "_h", h)
224
+ object.__setattr__(self, "_owned", True)
225
+ return self._h
226
+
227
+ def _enc(self):
228
+ return {"$h": self._handle()}
229
+
230
+ def __getattr__(self, name):
231
+ if name.startswith("__") and name.endswith("__"):
232
+ raise AttributeError(name)
233
+ self._use()
234
+ shadow = self._shadow
235
+ if name in shadow:
236
+ return shadow[name]
237
+ kid = self._kids.get(name)
238
+ if kid is None:
239
+ kid = JsRef(parent=self, name=name)
240
+ self._kids[name] = kid
241
+ return kid
242
+
243
+ def __setattr__(self, name, value):
244
+ self._use()
245
+ _emit("set", self._handle(), name, _encode(value))
246
+ if isinstance(value, (int, float, str, bool)) or value is None:
247
+ self._shadow[name] = value
248
+ else:
249
+ self._shadow.pop(name, None)
250
+ self._kids.pop(name, None)
251
+
252
+ def __getitem__(self, key):
253
+ return self.__getattr__(str(key))
254
+
255
+ def __setitem__(self, key, value):
256
+ self.__setattr__(str(key), value)
257
+
258
+ def __call__(self, *args, **kwargs):
259
+ enc = [_encode(a) for a in args]
260
+ if kwargs:
261
+ enc.append({"$d": {k: _encode(v) for k, v in kwargs.items()}})
262
+ self._use()
263
+ h = _new_handle()
264
+ if self._parent is not None and self._h is None:
265
+ kind = "new" if self._name[:1].isupper() else "call"
266
+ _emit(kind, h, self._parent._handle(), self._name, enc)
267
+ else:
268
+ _emit("fcall", h, self._handle(), enc)
269
+ return JsRef(h, owned=True, cmd=S.cmds[-1])
270
+
271
+ def new(self, *args, **kwargs):
272
+ enc = [_encode(a) for a in args]
273
+ if kwargs:
274
+ enc.append({"$d": {k: _encode(v) for k, v in kwargs.items()}})
275
+ h = _new_handle()
276
+ if self._parent is not None and self._h is None:
277
+ _emit("new", h, self._parent._handle(), self._name, enc)
278
+ else:
279
+ _emit("fnew", h, self._handle(), enc)
280
+ return JsRef(h, owned=True)
281
+
282
+ def __await__(self):
283
+ if self._h is None and self._parent is not None:
284
+ enc = {"$e": ["get", _expr_of(self._parent), self._name]}
285
+ else:
286
+ enc = self._enc()
287
+ return _Read(enc).__await__()
288
+
289
+ def __bool__(self):
290
+ return True
291
+
292
+ def __del__(self):
293
+ try:
294
+ cmd = self._cmd
295
+ if cmd is not None and S.cmds and any(c is cmd for c in S.cmds[-64:]):
296
+ cmd[2] = 0 # never used and not sent yet: do not keep it
297
+ elif self._owned and self._h is not None and not S.stopped:
298
+ S.cmds.append(["free", 0, self._h])
299
+ except Exception:
300
+ pass
301
+
302
+ def __repr__(self):
303
+ if self._parent is not None and self._h is None:
304
+ return "<preview %s>" % self._name
305
+ return "<preview object #%s>" % self._h
306
+
307
+
308
+ # Arithmetic on a reference to a value in the frame is deferred.
309
+ def _ref_expr(op, rev=False):
310
+ def f(self, other):
311
+ lhs = ["get", _expr_of(self._parent), self._name] if (self._h is None and self._parent is not None) else _expr_of(self)
312
+ rhs = _expr_of(other)
313
+ return _Expr([op, rhs, lhs] if rev else [op, lhs, rhs])
314
+ return f
315
+
316
+
317
+ for _name, _op in (("add", "+"), ("sub", "-"), ("mul", "*"), ("truediv", "/"),
318
+ ("mod", "%"), ("pow", "**"), ("floordiv", "//")):
319
+ setattr(JsRef, "__%s__" % _name, _ref_expr(_op))
320
+ setattr(JsRef, "__r%s__" % _name, _ref_expr(_op, rev=True))
321
+
322
+
323
+ def _cmp_error(self, other):
324
+ raise PreviewValueError(_READ_HINT)
325
+
326
+
327
+ for _name in ("lt", "le", "gt", "ge"):
328
+ setattr(JsRef, "__%s__" % _name, _cmp_error)
329
+ setattr(_Expr, "__%s__" % _name, _cmp_error)
330
+
331
+
332
+ # ---------------------------------------------------------------- values
333
+
334
+ def _encode(v):
335
+ if v is None or isinstance(v, (bool, int, float, str)):
336
+ if isinstance(v, float) and (v != v or v in (float("inf"), float("-inf"))):
337
+ return {"$e": ["num", "NaN" if v != v else ("Infinity" if v > 0 else "-Infinity")]}
338
+ return v
339
+ if isinstance(v, JsRef):
340
+ return v._enc()
341
+ if isinstance(v, _Expr):
342
+ return v._enc()
343
+ if isinstance(v, (list, tuple)):
344
+ return [_encode(x) for x in v]
345
+ if isinstance(v, dict):
346
+ return {"$d": {str(k): _encode(x) for k, x in v.items()}}
347
+ if callable(v):
348
+ cid = S.next_cb
349
+ S.next_cb += 1
350
+ S.callbacks[cid] = v
351
+ return {"$cb": cid}
352
+ tolist = getattr(v, "tolist", None) # numpy arrays and scalars
353
+ if tolist is not None:
354
+ return _encode(tolist())
355
+ return str(v)
356
+
357
+
358
+ class Event(dict):
359
+ """An event from the preview: event.key, event.x … (also event["key"])."""
360
+ def __getattr__(self, name):
361
+ try:
362
+ return self[name]
363
+ except KeyError:
364
+ raise AttributeError(name)
365
+
366
+
367
+ # ---------------------------------------------------------------- input state
368
+
369
+ keys = set()
370
+
371
+
372
+ class _Mouse:
373
+ def __init__(self):
374
+ self.x = 0.0
375
+ self.y = 0.0
376
+ self.down = False
377
+
378
+ def __repr__(self):
379
+ return "mouse(x=%g, y=%g, down=%s)" % (self.x, self.y, self.down)
380
+
381
+
382
+ mouse = _Mouse()
383
+
384
+
385
+ # ---------------------------------------------------------------- sprites
386
+
387
+ class Sprite:
388
+ """A picture (an image, or any emoji/text) that moves. Draws itself with ctx."""
389
+
390
+ def __init__(self, picture, x=0, y=0, size=48):
391
+ self.picture = picture
392
+ self.x, self.y = x, y
393
+ self.vx = self.vy = 0.0
394
+ self.angle = 0.0
395
+ self.size = size
396
+ self.visible = True
397
+
398
+ def update(self, dt):
399
+ self.x += self.vx * dt
400
+ self.y += self.vy * dt
401
+
402
+ def draw(self):
403
+ if not self.visible:
404
+ return
405
+ c = ctx
406
+ c.save()
407
+ c.translate(self.x, self.y)
408
+ if self.angle:
409
+ c.rotate(self.angle)
410
+ s = self.size
411
+ if isinstance(self.picture, str):
412
+ # Emoji/text is drawn ONCE into a small canvas and stamped from then
413
+ # on: redrawing text every frame is the slow path on phones.
414
+ box = s * 1.3
415
+ c.drawImage(_picture(self.picture, s), -box / 2, -box / 2, box, box)
416
+ else:
417
+ c.drawImage(self.picture, -s / 2, -s / 2, s, s)
418
+ c.restore()
419
+
420
+ def touches(self, other):
421
+ r = (self.size + other.size) * 0.4
422
+ return (self.x - other.x) ** 2 + (self.y - other.y) ** 2 < r * r
423
+
424
+
425
+ def _picture(text, size):
426
+ key = (text, size)
427
+ pic = S.pictures.get(key)
428
+ if pic is None:
429
+ ratio = (S.ns or {}).get("pixel_ratio", 1) or 1
430
+ px = max(1, int(size * 1.3 * ratio + 0.5))
431
+ pic = js.document.createElement("canvas")
432
+ pic.width = px
433
+ pic.height = px
434
+ g = pic.getContext("2d")
435
+ g.font = "%dpx system-ui, sans-serif" % int(size * ratio)
436
+ g.textAlign = "center"
437
+ g.textBaseline = "middle"
438
+ g.fillText(text, px / 2, px / 2 + size * ratio * 0.06)
439
+ S.pictures[key] = pic
440
+ return pic
441
+
442
+
443
+ def load_image(src):
444
+ """An image for drawImage or a Sprite. Draws once it has loaded."""
445
+ img = js.Image()
446
+ img.src = src
447
+ return img
448
+
449
+
450
+ # ---------------------------------------------------------------- the namespace
451
+
452
+ js = JsRef(H_WINDOW)
453
+ canvas = JsRef(H_CANVAS)
454
+ THREE = JsRef(H_THREE)
455
+ ctx = JsRef(H_CTX)
456
+ addons = JsRef(H_ADDONS)
457
+
458
+ class measure:
459
+ """Time a section of your frame: with measure("physics"): ... Shown in measure mode."""
460
+ __slots__ = ("name", "t0")
461
+
462
+ def __init__(self, name):
463
+ self.name = str(name)
464
+
465
+ def __enter__(self):
466
+ self.t0 = time.perf_counter() if S.measuring else 0.0
467
+ return self
468
+
469
+ def __exit__(self, *exc):
470
+ if self.t0:
471
+ sec = S.sections
472
+ sec[self.name] = sec.get(self.name, 0.0) + (time.perf_counter() - self.t0) * 1000
473
+ return False
474
+
475
+
476
+ EXPORTS = ("js", "canvas", "ctx", "THREE", "addons", "frame", "flush", "sleep",
477
+ "keys", "mouse", "Sprite", "load_image", "Event", "measure", "now")
478
+
479
+ # HOOKS for modules built on this one (Pymations). Each _on_start hook gets
480
+ # the run's info before the file runs; each _after_top_level hook gets the
481
+ # user's namespace after the top level and setup(), and may return a coroutine,
482
+ # which is driven like the file's own. Each _before_frame hook gets a tick's
483
+ # payload before any frame code runs; each _on_frame hook runs at the end of
484
+ # every frame (start and tick) with the info or tick payload; a _live_hooks
485
+ # entry returning True keeps the program running. _hidden_vars: name -> object
486
+ # a module star-imports, kept out of the debugger's variables while it is
487
+ # still that object.
488
+ _on_start = []
489
+ _after_top_level = []
490
+ _before_frame = []
491
+ _on_frame = []
492
+ _live_hooks = []
493
+ _hidden_vars = {}
494
+
495
+
496
+ # ---------------------------------------------------------------- breakpoints
497
+ #
498
+ # A pause HOLDS the thread running the frame, so only an interpreter that
499
+ # answers other requests meanwhile can pause (the device's CPython: one thread
500
+ # per request). The host asks debug_state() and answers with debug_resume()
501
+ # from those other requests. Only the user's file is traced.
502
+
503
+ class _Debug:
504
+ def __init__(self):
505
+ self.on = False
506
+ self.bps = set()
507
+ self.mode = None # 'over' | 'into' | 'out' after a step
508
+ self.depth = 0
509
+ self.paused = False
510
+ self.line = 0
511
+ self.vars = {}
512
+ self.action = None
513
+ self.seq = 0 # one number per pause, so the host answers each once
514
+
515
+
516
+ DBG = _Debug()
517
+ _STEP = {"stepOver": "over", "stepInto": "into", "stepOut": "out"}
518
+
519
+
520
+ def _user_depth(frame):
521
+ d = 0
522
+ while frame is not None:
523
+ if frame.f_code.co_filename == USER_FILE:
524
+ d += 1
525
+ frame = frame.f_back
526
+ return d
527
+
528
+
529
+ def _vars(frame):
530
+ out = {}
531
+ names = dict(frame.f_globals) if frame.f_globals is not None else {}
532
+ names.update(frame.f_locals)
533
+ for name, value in names.items():
534
+ if name.startswith("_") or name in EXPORTS or _hidden_vars.get(name, _vars) is value:
535
+ continue
536
+ if isinstance(value, types.ModuleType) or (callable(value) and not isinstance(value, type)):
537
+ continue
538
+ try:
539
+ if isinstance(value, (int, float, str, bool, type(None))):
540
+ out[name] = {"value": value, "type": type(value).__name__}
541
+ elif isinstance(value, (list, tuple, dict, set)):
542
+ out[name] = {"value": repr(value)[:200], "type": type(value).__name__, "length": len(value)}
543
+ else:
544
+ out[name] = {"value": repr(value)[:200], "type": type(value).__name__}
545
+ except Exception:
546
+ out[name] = {"value": "<unreadable>", "type": type(value).__name__}
547
+ return out
548
+
549
+
550
+ def _local_trace(frame, event, arg):
551
+ if event == "line" and DBG.on and not S.stopped:
552
+ line = frame.f_lineno
553
+ mode = DBG.mode
554
+ d = _user_depth(frame) if mode else 0
555
+ if (line in DBG.bps or mode == "into"
556
+ or (mode == "over" and d <= DBG.depth)
557
+ or (mode == "out" and d < DBG.depth)):
558
+ _pause(frame, line)
559
+ return _local_trace
560
+
561
+
562
+ def _global_trace(frame, event, arg):
563
+ if frame.f_code.co_filename != USER_FILE:
564
+ return None
565
+ return _local_trace
566
+
567
+
568
+ def _pause(frame, line):
569
+ # This run's state, not whatever run is latest: a new run (or a reloaded
570
+ # page) must still release a pause left behind.
571
+ state = getattr(_tls, "state", None) or _latest[0]
572
+ DBG.mode = None
573
+ DBG.line = line
574
+ DBG.vars = _vars(frame)
575
+ DBG.action = None
576
+ DBG.seq += 1
577
+ DBG.paused = True
578
+ try:
579
+ while DBG.action is None and not state.stopped:
580
+ time.sleep(0.02)
581
+ finally:
582
+ DBG.paused = False
583
+ action = DBG.action
584
+ if action == "stop" or state.stopped:
585
+ raise KeyboardInterrupt("Stopped")
586
+ DBG.mode = _STEP.get(action)
587
+ DBG.depth = _user_depth(frame)
588
+
589
+
590
+ def debug_set(lines_json, on):
591
+ DBG.on = bool(on)
592
+ DBG.bps = set(json.loads(lines_json))
593
+ DBG.mode = None
594
+ return "ok"
595
+
596
+
597
+ def debug_state():
598
+ return json.dumps({"paused": DBG.paused, "seq": DBG.seq, "line": DBG.line,
599
+ "vars": DBG.vars if DBG.paused else {}}, default=str)
600
+
601
+
602
+ def debug_resume(action):
603
+ DBG.action = action
604
+ return "ok"
605
+
606
+
607
+ class _Traced:
608
+ """Trace the user's file for the length of one start() or tick()."""
609
+ def __enter__(self):
610
+ if DBG.on:
611
+ sys.settrace(_global_trace)
612
+
613
+ def __exit__(self, *exc):
614
+ if DBG.on:
615
+ sys.settrace(None)
616
+ return False
617
+
618
+
619
+ # ---------------------------------------------------------------- running
620
+
621
+ def _drive(task, value=None):
622
+ """Step one coroutine until it waits; returns False when it has finished."""
623
+ coro = task[0]
624
+ try:
625
+ waiting = coro.send(value)
626
+ except StopIteration:
627
+ return False
628
+ task[1] = waiting
629
+ return True
630
+
631
+
632
+ def _format_error(e):
633
+ tb = traceback.extract_tb(e.__traceback__)
634
+ user = [fr for fr in tb if fr.filename == USER_FILE]
635
+ lines = ["Traceback (most recent call last):"]
636
+ for fr in user:
637
+ lines.append(' line %d, in %s' % (fr.lineno, fr.name))
638
+ if fr.line:
639
+ lines.append(" " + fr.line.strip())
640
+ lines.append("%s: %s" % (type(e).__name__, e))
641
+ line = user[-1].lineno if user else 0
642
+ return "\n".join(lines), line
643
+
644
+
645
+ def _reply(error=None, error_line=0):
646
+ cmds, S.cmds = S.cmds, []
647
+ out = S.out.getvalue()
648
+ err = S.err.getvalue()
649
+ S.out = io.StringIO()
650
+ S.err = io.StringIO()
651
+ running = S.ns is not None and not error and (
652
+ bool(S.tasks) or bool(S.callbacks)
653
+ or any(callable(S.ns.get(n)) for n in _LIVE_NAMES)
654
+ or any(h() for h in _live_hooks))
655
+ reply = {
656
+ "cmds": cmds, "out": out, "err": err, "running": running,
657
+ "error": error, "errorLine": error_line,
658
+ }
659
+ if S.measuring:
660
+ reply["t"] = S.sections
661
+ S.sections = {}
662
+ return json.dumps(reply, default=str)
663
+
664
+
665
+ _LIVE_NAMES = ("draw", "update", "on_key_down", "on_key_up",
666
+ "on_mouse_down", "on_mouse_move", "on_mouse_up")
667
+
668
+
669
+ def _apply_info(p):
670
+ ns = S.ns
671
+ if ns is None:
672
+ return
673
+ for k in ("width", "height", "pixel_ratio"):
674
+ if k in p:
675
+ ns[k] = p[k]
676
+
677
+
678
+ def _apply_events(p):
679
+ ns = S.ns
680
+ if p.get("events"):
681
+ S.live = True
682
+ for ev in p.get("events", ()):
683
+ e = Event(ev.get("e") or {})
684
+ kind = ev.get("k")
685
+ if kind == "cb":
686
+ fn = S.callbacks.get(ev.get("id"))
687
+ if fn is not None:
688
+ r = fn(e)
689
+ if hasattr(r, "send"):
690
+ # Kept only while it is still waiting: a callback that
691
+ # finishes without awaiting must not be driven again.
692
+ task = [r, None]
693
+ if _drive(task):
694
+ S.tasks.append(task)
695
+ continue
696
+ t = e.get("type")
697
+ if t == "keydown":
698
+ keys.add(e.get("key"))
699
+ fn = ns.get("on_key_down")
700
+ if callable(fn):
701
+ fn(e.get("key"))
702
+ elif t == "keyup":
703
+ keys.discard(e.get("key"))
704
+ fn = ns.get("on_key_up")
705
+ if callable(fn):
706
+ fn(e.get("key"))
707
+ elif t in ("pointerdown", "pointermove", "pointerup"):
708
+ mouse.x, mouse.y = e.get("x", mouse.x), e.get("y", mouse.y)
709
+ if t == "pointerdown":
710
+ mouse.down = True
711
+ elif t == "pointerup":
712
+ mouse.down = False
713
+ fn = ns.get({"pointerdown": "on_mouse_down", "pointermove": "on_mouse_move",
714
+ "pointerup": "on_mouse_up"}[t])
715
+ if callable(fn):
716
+ fn(mouse.x, mouse.y)
717
+
718
+
719
+ def _step_tasks(answers):
720
+ now = _clock()
721
+ alive = []
722
+ for task in S.tasks:
723
+ waiting = task[1]
724
+ if isinstance(waiting, _Read):
725
+ if waiting.rid not in answers:
726
+ alive.append(task)
727
+ continue
728
+ keep = _drive(task, answers[waiting.rid])
729
+ elif isinstance(waiting, _Frame):
730
+ if waiting.until > now:
731
+ alive.append(task)
732
+ continue
733
+ keep = _drive(task)
734
+ else:
735
+ keep = _drive(task)
736
+ if keep:
737
+ alive.append(task)
738
+ S.tasks = alive
739
+
740
+
741
+ def _call_frame_functions(dt):
742
+ ns = S.ns
743
+ for name, args in (("update", (dt,)), ("draw", ())):
744
+ fn = ns.get(name)
745
+ if not callable(fn):
746
+ continue
747
+ S.live = True
748
+ if any(t[2:] == [name] for t in S.tasks):
749
+ continue # last frame's async draw is still waiting on a read
750
+ takes = getattr(getattr(fn, "__code__", None), "co_argcount", 1)
751
+ t0 = time.perf_counter() if S.measuring else 0.0
752
+ r = fn(*args) if (name != "update" or takes) else fn()
753
+ if t0:
754
+ S.sections[name] = S.sections.get(name, 0.0) + (time.perf_counter() - t0) * 1000
755
+ if hasattr(r, "send"):
756
+ task = [r, None, name]
757
+ if _drive(task):
758
+ S.tasks.append(task)
759
+
760
+
761
+ def start(source, info_json):
762
+ """Run the file's top level. Returns the first frame's reply (JSON)."""
763
+ _latest[0].stopped = True
764
+ _latest[0] = _State()
765
+ _tls.state = _latest[0]
766
+ # FRESH ROOT NAMES EVERY RUN. A name like \`js\` remembers what was looked
767
+ # up through it (js.document, THREE.DoubleSide) as handles in the preview
768
+ # page; a new run has a new page, so reused names pointed at objects that
769
+ # no longer exist ("that object no longer exists in the preview").
770
+ global js, canvas, THREE, ctx, addons
771
+ js, canvas, THREE = JsRef(H_WINDOW), JsRef(H_CANVAS), JsRef(H_THREE)
772
+ ctx, addons = JsRef(H_CTX), JsRef(H_ADDONS)
773
+ keys.clear()
774
+ mouse.x = mouse.y = 0.0
775
+ mouse.down = False
776
+ S.thread = threading.get_ident()
777
+ S.busy = True
778
+ S.started = time.monotonic()
779
+ info = json.loads(info_json)
780
+ S.measuring = bool(info.get("measure"))
781
+ # Recording: virtual time from frame 0, and the same random numbers each take.
782
+ S.vt = info.get("vt", 0.0 if info.get("record") else None)
783
+ if info.get("record"):
784
+ import random
785
+ random.seed(0)
786
+ ns = {"__name__": "__main__", "__builtins__": __builtins__}
787
+ for name in EXPORTS:
788
+ ns[name] = globals()[name]
789
+ S.ns = ns
790
+ _apply_info(info)
791
+ for hook in _on_start:
792
+ hook(info)
793
+ old_out, old_err = sys.stdout, sys.stderr
794
+ sys.stdout, sys.stderr = S.out, S.err
795
+ tracer = _Traced()
796
+ try:
797
+ tracer.__enter__()
798
+ import ast
799
+ flags = getattr(ast, "PyCF_ALLOW_TOP_LEVEL_AWAIT", 0)
800
+ code = compile(source, USER_FILE, "exec", flags=flags)
801
+ result = eval(code, ns)
802
+ if hasattr(result, "send"):
803
+ task = [result, None]
804
+ if _drive(task):
805
+ S.tasks.append(task)
806
+ if callable(ns.get("setup")):
807
+ r = ns["setup"]()
808
+ if hasattr(r, "send"):
809
+ task = [r, None]
810
+ if _drive(task):
811
+ S.tasks.append(task)
812
+ for hook in _after_top_level:
813
+ r = hook(ns)
814
+ if hasattr(r, "send"):
815
+ task = [r, None]
816
+ if _drive(task):
817
+ S.tasks.append(task)
818
+ _call_frame_functions(0.0)
819
+ for hook in _on_frame:
820
+ hook(info)
821
+ except BaseException as e: # noqa: BLE001 — every failure is the user's to read
822
+ msg, line = _format_error(e)
823
+ S.ns = None
824
+ return _reply(msg, line)
825
+ finally:
826
+ tracer.__exit__()
827
+ sys.stdout, sys.stderr = old_out, old_err
828
+ S.busy = False
829
+ return _reply()
830
+
831
+
832
+ def tick(payload_json):
833
+ """One frame: events and read answers in, this frame's commands out (JSON)."""
834
+ _tls.state = _latest[0]
835
+ if S.ns is None:
836
+ return _reply("Nothing is running", 0)
837
+ S.thread = threading.get_ident()
838
+ S.busy = True
839
+ p = json.loads(payload_json)
840
+ S.frame_no += 1
841
+ S.measuring = bool(p.get("measure"))
842
+ t_tick = time.perf_counter() if S.measuring else 0.0
843
+ old_out, old_err = sys.stdout, sys.stderr
844
+ sys.stdout, sys.stderr = S.out, S.err
845
+ tracer = _Traced()
846
+ try:
847
+ tracer.__enter__()
848
+ if "bps" in p:
849
+ DBG.bps = set(p["bps"])
850
+ if "vt" in p:
851
+ S.vt = float(p["vt"])
852
+ for hook in _before_frame:
853
+ hook(p)
854
+ _apply_info(p)
855
+ answers = {int(k): v for k, v in (p.get("reads") or {}).items()}
856
+ _apply_events(p)
857
+ _step_tasks(answers)
858
+ _call_frame_functions(float(p.get("dt", 0.0)))
859
+ for hook in _on_frame:
860
+ hook(p)
861
+ except BaseException as e: # noqa: BLE001
862
+ msg, line = _format_error(e)
863
+ S.ns = None
864
+ return _reply(msg, line)
865
+ finally:
866
+ tracer.__exit__()
867
+ sys.stdout, sys.stderr = old_out, old_err
868
+ S.busy = False
869
+ if t_tick:
870
+ S.sections["python"] = (time.perf_counter() - t_tick) * 1000
871
+ return _reply()
872
+
873
+
874
+ def stop():
875
+ """Stop: drop the program, and interrupt it if it is stuck in a loop."""
876
+ _tls.state = _latest[0]
877
+ S.stopped = True
878
+ S.ns = None
879
+ S.tasks = []
880
+ DBG.action = "stop"
881
+ tid = S.thread
882
+ # Only while a frame is actually running: a finished frame's thread id may
883
+ # already belong to another request.
884
+ if S.busy and tid is not None and tid != threading.get_ident():
885
+ try:
886
+ import ctypes
887
+ ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_ulong(tid), ctypes.py_object(KeyboardInterrupt))
888
+ except Exception:
889
+ pass
890
+ return "stopped"
891
+
892
+
893
+ def emit_marked(reply):
894
+ """Circuit's session hands back printed text only: mark the reply in it."""
895
+ print("\x02CIRCANVAS" + reply + "\x03")