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