|
17 | 17 | from pyodide_worker_runner import install_imports |
18 | 18 | from pyodide.ffi import JsException, create_proxy |
19 | 19 | from .util import to_py |
| 20 | +from .turtle_hook import TurtleImportHook |
20 | 21 | from pyodide.http import pyfetch |
21 | 22 | from types import ModuleType |
22 | 23 |
|
23 | 24 | SYS_RECURSION_LIMIT = 500 |
24 | 25 | MODULE_NAME = "sandbox" |
25 | 26 |
|
26 | 27 |
|
27 | | -class _TurtleImportHook: |
28 | | - """Import hook that lazily sets up SVG-based turtle graphics. |
29 | | -
|
30 | | - Installed in sys.meta_path. When user code does `import turtle`, |
31 | | - this hook intercepts it, imports svg-turtle, creates a shared |
32 | | - canvas/screen, and patches the turtle module to render SVG. |
33 | | -
|
34 | | - If user code never imports turtle, no setup occurs. |
35 | | - """ |
36 | | - |
37 | | - def __init__(self): |
38 | | - self.papyros = None |
39 | | - self.render = None |
40 | | - self._loading = False |
41 | | - self._turtle_module = None |
42 | | - |
43 | | - def find_spec(self, name, path, target=None): |
44 | | - if name == 'turtle' and not self._loading: |
45 | | - import importlib.util |
46 | | - return importlib.util.spec_from_loader(name, self) |
47 | | - return None |
48 | | - |
49 | | - def create_module(self, spec): |
50 | | - # Return the patched turtle module directly |
51 | | - return self._setup_turtle() |
52 | | - |
53 | | - def exec_module(self, module): |
54 | | - # Module was already set up in create_module |
55 | | - pass |
56 | | - |
57 | | - def _setup_turtle(self): |
58 | | - self._loading = True |
59 | | - try: |
60 | | - from svg_turtle import SvgTurtle |
61 | | - from svg_turtle.canvas import Canvas |
62 | | - |
63 | | - if self._turtle_module is None: |
64 | | - # First import: svg_turtle stubs tkinter, then imports turtle |
65 | | - self._turtle_module = sys.modules.get('turtle') |
66 | | - if self._turtle_module is None: |
67 | | - import turtle |
68 | | - self._turtle_module = sys.modules['turtle'] |
69 | | - |
70 | | - turtle_mod = self._turtle_module |
71 | | - sys.modules['turtle'] = turtle_mod |
72 | | - |
73 | | - # Fresh canvas and screen for this execution |
74 | | - canvas = Canvas(400, 400) |
75 | | - screen = SvgTurtle._Screen(canvas) |
76 | | - screen.cv.config(bg="") |
77 | | - |
78 | | - class PapyrosTurtle(SvgTurtle): |
79 | | - def __init__(self): |
80 | | - super().__init__(screen=screen) |
81 | | - |
82 | | - SvgTurtle._screen = screen |
83 | | - SvgTurtle._pen = PapyrosTurtle() |
84 | | - |
85 | | - rendered = [False] |
86 | | - papyros = self.papyros |
87 | | - |
88 | | - def render(): |
89 | | - if rendered[0]: |
90 | | - return |
91 | | - rendered[0] = True |
92 | | - svg_string = SvgTurtle._pen.to_svg() |
93 | | - if svg_string: |
94 | | - img = base64.b64encode(svg_string.encode("utf-8")).decode("utf-8") |
95 | | - papyros.output("img", img, contentType="img/svg+xml;base64") |
96 | | - |
97 | | - turtle_mod.Turtle = PapyrosTurtle |
98 | | - turtle_mod.done = render |
99 | | - turtle_mod.mainloop = render |
100 | | - turtle_mod.exitonclick = render |
101 | | - turtle_mod.bye = render |
102 | | - |
103 | | - self.render = render |
104 | | - return turtle_mod |
105 | | - except Exception: |
106 | | - self.render = None |
107 | | - return None |
108 | | - finally: |
109 | | - self._loading = False |
110 | | - |
111 | | - |
112 | 28 | class Papyros(python_runner.PyodideRunner): |
113 | 29 | def __init__( |
114 | 30 | self, |
@@ -184,7 +100,7 @@ def _render_turtle(self): |
184 | 100 |
|
185 | 101 | def override_turtle(self): |
186 | 102 | if not hasattr(self, '_turtle_hook'): |
187 | | - self._turtle_hook = _TurtleImportHook() |
| 103 | + self._turtle_hook = TurtleImportHook() |
188 | 104 |
|
189 | 105 | hook = self._turtle_hook |
190 | 106 | hook.papyros = self |
@@ -345,9 +261,8 @@ async def run_async(self, source_code, mode="exec", top_level_await=True): |
345 | 261 | def frame_callback(frame): |
346 | 262 | self._flush_open_files() |
347 | 263 | self._emit_created_files(emit_empty=True) |
348 | | - # Progressive turtle rendering: only emit SVG when drawing actually changed. |
349 | | - # Non-drawing commands (pencolor, left, etc.) don't alter the SVG output, |
350 | | - # so the string comparison naturally skips them without needing a hard cap. |
| 264 | + # The hook fires lazily when user code runs `import turtle`, so the |
| 265 | + # pen only exists partway through the trace — check per frame. |
351 | 266 | hook = getattr(self, '_turtle_hook', None) |
352 | 267 | if hook and hook.render: |
353 | 268 | try: |
|
0 commit comments