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.
- package/LICENSE +21 -0
- package/README.md +24 -2
- package/THIRD-PARTY-NOTICES +240 -0
- package/engine/LICENSE-polygon-clipping.txt +11 -0
- package/engine/anim.js +9 -0
- package/engine/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/engine/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/engine/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/engine/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/engine/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/engine/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/engine/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/engine/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/engine/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/engine/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/engine/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/engine/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/engine/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/engine/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/engine/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/engine/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/engine/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/engine/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/engine/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/engine/fonts/LICENSE_KATEX_FONTS.txt +106 -0
- package/engine/fonts/LICENSE_LIBERATION.txt +102 -0
- package/engine/fonts/LiberationSans-Bold.ttf +0 -0
- package/engine/fonts/LiberationSans-BoldItalic.ttf +0 -0
- package/engine/fonts/LiberationSans-Italic.ttf +0 -0
- package/engine/fonts/LiberationSans-Regular.ttf +0 -0
- package/engine/katex.min.css +1 -0
- package/engine/katex.min.js +1 -0
- package/engine/manifest.json +1 -0
- package/engine/opentype.min.js +2 -0
- package/package.json +34 -4
- package/pyctures.py +897 -0
- package/pymations.js +1818 -0
- 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")
|