hyperbook 0.96.0 → 0.96.2
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/dist/assets/directive-pyide/client.js +747 -1011
- package/dist/index.js +2 -3
- package/package.json +2 -2
|
@@ -1,153 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const PYODIDE_CDN = "https://cdn.jsdelivr.net/pyodide/v0.29.4/full/pyodide.js";
|
|
12
|
-
|
|
13
|
-
const loadPyodideScript = () => {
|
|
14
|
-
if (window.loadPyodide) {
|
|
15
|
-
return Promise.resolve();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return new Promise((resolve, reject) => {
|
|
19
|
-
const script = document.createElement("script");
|
|
20
|
-
script.src = PYODIDE_CDN;
|
|
21
|
-
script.onload = () => resolve();
|
|
22
|
-
script.onerror = () => reject(new Error("Failed to load Pyodide"));
|
|
23
|
-
document.head.appendChild(script);
|
|
24
|
-
});
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const pyodideReadyPromise = (async () => {
|
|
28
|
-
await loadPyodideScript();
|
|
29
|
-
return window.loadPyodide;
|
|
30
|
-
})();
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @type {Map<string, any>}
|
|
34
|
-
*/
|
|
35
|
-
const runtimes = new Map();
|
|
36
|
-
/**
|
|
37
|
-
* @type {Map<string, Set<string>>}
|
|
38
|
-
*/
|
|
39
|
-
const installedMicropipPackages = new Map();
|
|
40
|
-
/**
|
|
41
|
-
* @type {Map<string, any>}
|
|
42
|
-
*/
|
|
43
|
-
const turtleModules = new Map();
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* @type {Map<string, { running: boolean, stopping: boolean, stopRequested: boolean, type: "run" | "test" | null }>}
|
|
47
|
-
*/
|
|
48
|
-
const executionStates = new Map();
|
|
49
|
-
/**
|
|
50
|
-
* @type {Map<string, Int32Array>}
|
|
51
|
-
*/
|
|
52
|
-
const interruptBuffers = new Map();
|
|
53
|
-
|
|
54
|
-
const getExecutionState = (id) => {
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// assets/directive-pyide/src/state.js
|
|
4
|
+
var runtimes = /* @__PURE__ */ new Map();
|
|
5
|
+
var installedMicropipPackages = /* @__PURE__ */ new Map();
|
|
6
|
+
var turtleModules = /* @__PURE__ */ new Map();
|
|
7
|
+
var executionStates = /* @__PURE__ */ new Map();
|
|
8
|
+
var interruptBuffers = /* @__PURE__ */ new Map();
|
|
9
|
+
var pytamaroStdoutCarry = /* @__PURE__ */ new Map();
|
|
10
|
+
var getExecutionState = (id) => {
|
|
55
11
|
if (!executionStates.has(id)) {
|
|
56
12
|
executionStates.set(id, {
|
|
57
13
|
running: false,
|
|
58
14
|
stopping: false,
|
|
59
15
|
stopRequested: false,
|
|
60
|
-
type: null
|
|
16
|
+
type: null
|
|
61
17
|
});
|
|
62
18
|
}
|
|
63
19
|
return executionStates.get(id);
|
|
64
20
|
};
|
|
65
21
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const loadPyodide = await pyodideReadyPromise;
|
|
71
|
-
const pyodide = await loadPyodide();
|
|
72
|
-
if (typeof pyodide.registerJsModule === "function") {
|
|
73
|
-
const turtleModule = createTurtleJsFFI(id);
|
|
74
|
-
turtleModule.__setPyodide(pyodide);
|
|
75
|
-
pyodide.registerJsModule("turtle", turtleModule);
|
|
76
|
-
pyodide.registerJsModule("jturtle", turtleModule);
|
|
77
|
-
pyodide.registerJsModule("pytamaro_js_ffi", createPytamaroJsFFI());
|
|
78
|
-
turtleModules.set(id, turtleModule);
|
|
79
|
-
}
|
|
80
|
-
if (
|
|
81
|
-
typeof SharedArrayBuffer !== "undefined" &&
|
|
82
|
-
window.crossOriginIsolated &&
|
|
83
|
-
typeof pyodide.setInterruptBuffer === "function"
|
|
84
|
-
) {
|
|
85
|
-
const interruptBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
86
|
-
pyodide.setInterruptBuffer(interruptBuffer);
|
|
87
|
-
interruptBuffers.set(id, interruptBuffer);
|
|
88
|
-
}
|
|
89
|
-
runtimes.set(id, pyodide);
|
|
90
|
-
return pyodide;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const PYTAMARO_URI_BEGIN = "@@@PYTAMARO_DATA_URI_BEGIN@@@";
|
|
94
|
-
const PYTAMARO_URI_END = "@@@PYTAMARO_DATA_URI_END@@@";
|
|
95
|
-
/**
|
|
96
|
-
* @type {Map<string, string>}
|
|
97
|
-
*/
|
|
98
|
-
const pytamaroStdoutCarry = new Map();
|
|
99
|
-
const pytamaroCanvasTargets = new Set();
|
|
22
|
+
// assets/directive-pyide/src/constants.js
|
|
23
|
+
var PYODIDE_CDN = "https://cdn.jsdelivr.net/pyodide/v0.29.4/full/pyodide.js";
|
|
24
|
+
var PYTAMARO_URI_BEGIN = "@@@PYTAMARO_DATA_URI_BEGIN@@@";
|
|
25
|
+
var PYTAMARO_URI_END = "@@@PYTAMARO_DATA_URI_END@@@";
|
|
100
26
|
|
|
101
|
-
|
|
27
|
+
// assets/directive-pyide/src/output.js
|
|
28
|
+
var getOutput = (id) => {
|
|
102
29
|
return document.getElementById(id)?.getElementsByClassName("output")[0];
|
|
103
30
|
};
|
|
104
|
-
|
|
105
|
-
const getCanvas = (id) => {
|
|
106
|
-
return document.getElementById(id)?.getElementsByClassName("canvas")[0];
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const setPytamaroCanvasTarget = (id, enabled) => {
|
|
110
|
-
if (enabled) {
|
|
111
|
-
pytamaroCanvasTargets.add(id);
|
|
112
|
-
} else {
|
|
113
|
-
pytamaroCanvasTargets.delete(id);
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const renderPytamaroDataUri = (id, container, dataUri) => {
|
|
118
|
-
if (id && pytamaroCanvasTargets.has(id)) {
|
|
119
|
-
const canvas = getCanvas(id);
|
|
120
|
-
const context = canvas?.getContext?.("2d");
|
|
121
|
-
if (canvas && context) {
|
|
122
|
-
const img = new Image();
|
|
123
|
-
img.onload = () => {
|
|
124
|
-
const width = Math.max(1, img.naturalWidth || img.width);
|
|
125
|
-
const height = Math.max(1, img.naturalHeight || img.height);
|
|
126
|
-
canvas.width = width;
|
|
127
|
-
canvas.height = height;
|
|
128
|
-
context.clearRect(0, 0, width, height);
|
|
129
|
-
context.drawImage(img, 0, 0, width, height);
|
|
130
|
-
};
|
|
131
|
-
img.onerror = () => {
|
|
132
|
-
appendOutputErrorLine(id, "Failed to render pytamaro graphic.");
|
|
133
|
-
};
|
|
134
|
-
img.src = dataUri;
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
31
|
+
var renderPytamaroDataUri = (id, container, dataUri) => {
|
|
139
32
|
const img = document.createElement("img");
|
|
140
33
|
img.src = dataUri;
|
|
141
34
|
img.style.maxWidth = "100%";
|
|
142
35
|
img.style.display = "block";
|
|
143
36
|
container.appendChild(img);
|
|
144
37
|
};
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Renders a message that may contain pytamaro data URI image markers into
|
|
148
|
-
* the given container, creating <img> elements for each embedded image.
|
|
149
|
-
*/
|
|
150
|
-
const renderOutputSegments = (container, message, id = null) => {
|
|
38
|
+
var renderOutputSegments = (container, message, id = null) => {
|
|
151
39
|
let remaining = String(message);
|
|
152
40
|
while (remaining.length > 0) {
|
|
153
41
|
const beginIdx = remaining.indexOf(PYTAMARO_URI_BEGIN);
|
|
@@ -156,7 +44,9 @@ hyperbook.python = (function () {
|
|
|
156
44
|
break;
|
|
157
45
|
}
|
|
158
46
|
if (beginIdx > 0) {
|
|
159
|
-
container.appendChild(
|
|
47
|
+
container.appendChild(
|
|
48
|
+
document.createTextNode(remaining.slice(0, beginIdx))
|
|
49
|
+
);
|
|
160
50
|
}
|
|
161
51
|
const afterBegin = remaining.slice(beginIdx + PYTAMARO_URI_BEGIN.length);
|
|
162
52
|
const endIdx = afterBegin.indexOf(PYTAMARO_URI_END);
|
|
@@ -169,12 +59,7 @@ hyperbook.python = (function () {
|
|
|
169
59
|
remaining = afterBegin.slice(endIdx + PYTAMARO_URI_END.length);
|
|
170
60
|
}
|
|
171
61
|
};
|
|
172
|
-
|
|
173
|
-
const scriptLooksLikeTurtle = (script) => {
|
|
174
|
-
return /\bfrom\s+turtle\s+import\b|\bimport\s+turtle\b/.test(String(script || ""));
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
const getTrailingPrefixLength = (text, marker) => {
|
|
62
|
+
var getTrailingPrefixLength = (text, marker) => {
|
|
178
63
|
const max = Math.min(text.length, marker.length - 1);
|
|
179
64
|
for (let len = max; len > 0; len -= 1) {
|
|
180
65
|
if (text.endsWith(marker.slice(0, len))) {
|
|
@@ -183,61 +68,64 @@ hyperbook.python = (function () {
|
|
|
183
68
|
}
|
|
184
69
|
return 0;
|
|
185
70
|
};
|
|
186
|
-
|
|
187
|
-
const appendText = (output, text) => {
|
|
71
|
+
var appendText = (output, text) => {
|
|
188
72
|
if (!text) return;
|
|
189
73
|
output.appendChild(document.createTextNode(text));
|
|
190
74
|
};
|
|
191
|
-
|
|
192
|
-
const appendOutputLine = (id, message) => {
|
|
75
|
+
var appendOutputLine = (id, message) => {
|
|
193
76
|
const output = getOutput(id);
|
|
194
77
|
if (!output) return;
|
|
195
78
|
const msg = String(message ?? "");
|
|
196
79
|
const carry = pytamaroStdoutCarry.get(id) || "";
|
|
197
80
|
let combined = carry + msg;
|
|
198
81
|
pytamaroStdoutCarry.delete(id);
|
|
199
|
-
|
|
200
|
-
// Fast path for regular stdout chunks.
|
|
201
82
|
if (!combined.includes(PYTAMARO_URI_BEGIN) && carry.length === 0) {
|
|
202
|
-
const partialBeginLength = getTrailingPrefixLength(
|
|
83
|
+
const partialBeginLength = getTrailingPrefixLength(
|
|
84
|
+
combined,
|
|
85
|
+
PYTAMARO_URI_BEGIN
|
|
86
|
+
);
|
|
203
87
|
if (partialBeginLength > 0) {
|
|
204
88
|
const visible = combined.slice(0, combined.length - partialBeginLength);
|
|
205
89
|
appendText(output, visible);
|
|
206
|
-
pytamaroStdoutCarry.set(
|
|
90
|
+
pytamaroStdoutCarry.set(
|
|
91
|
+
id,
|
|
92
|
+
combined.slice(combined.length - partialBeginLength)
|
|
93
|
+
);
|
|
207
94
|
} else {
|
|
208
95
|
appendText(output, combined);
|
|
209
96
|
}
|
|
210
97
|
return;
|
|
211
98
|
}
|
|
212
|
-
|
|
213
99
|
while (combined.length > 0) {
|
|
214
100
|
const beginIdx = combined.indexOf(PYTAMARO_URI_BEGIN);
|
|
215
101
|
if (beginIdx === -1) {
|
|
216
|
-
const partialBeginLength = getTrailingPrefixLength(
|
|
102
|
+
const partialBeginLength = getTrailingPrefixLength(
|
|
103
|
+
combined,
|
|
104
|
+
PYTAMARO_URI_BEGIN
|
|
105
|
+
);
|
|
217
106
|
const visible = combined.slice(0, combined.length - partialBeginLength);
|
|
218
107
|
appendText(output, visible);
|
|
219
108
|
if (partialBeginLength > 0) {
|
|
220
|
-
pytamaroStdoutCarry.set(
|
|
109
|
+
pytamaroStdoutCarry.set(
|
|
110
|
+
id,
|
|
111
|
+
combined.slice(combined.length - partialBeginLength)
|
|
112
|
+
);
|
|
221
113
|
}
|
|
222
114
|
break;
|
|
223
115
|
}
|
|
224
|
-
|
|
225
116
|
appendText(output, combined.slice(0, beginIdx));
|
|
226
117
|
const afterBegin = combined.slice(beginIdx + PYTAMARO_URI_BEGIN.length);
|
|
227
118
|
const endIdx = afterBegin.indexOf(PYTAMARO_URI_END);
|
|
228
119
|
if (endIdx === -1) {
|
|
229
|
-
// Keep incomplete marker and continue when the next stdout chunk arrives.
|
|
230
120
|
pytamaroStdoutCarry.set(id, combined.slice(beginIdx));
|
|
231
121
|
break;
|
|
232
122
|
}
|
|
233
|
-
|
|
234
123
|
const dataUri = afterBegin.slice(0, endIdx);
|
|
235
124
|
renderPytamaroDataUri(id, output, dataUri);
|
|
236
125
|
combined = afterBegin.slice(endIdx + PYTAMARO_URI_END.length);
|
|
237
126
|
}
|
|
238
127
|
};
|
|
239
|
-
|
|
240
|
-
const appendOutputErrorLine = (id, message) => {
|
|
128
|
+
var appendOutputErrorLine = (id, message) => {
|
|
241
129
|
const output = getOutput(id);
|
|
242
130
|
if (!output) return;
|
|
243
131
|
const line = document.createElement("span");
|
|
@@ -245,17 +133,13 @@ hyperbook.python = (function () {
|
|
|
245
133
|
line.textContent = String(message);
|
|
246
134
|
output.appendChild(line);
|
|
247
135
|
};
|
|
248
|
-
|
|
249
|
-
// ─── Python Friendly Error Messages ────────────────────────────────────────
|
|
250
|
-
// Loaded from python-friendly-error-messages.js (built from npm package)
|
|
251
|
-
|
|
252
136
|
if (window.PythonFriendlyErrorMessages) {
|
|
253
137
|
const { loadCopydeckFor, registerAdapter, pyodideAdapter } = window.PythonFriendlyErrorMessages;
|
|
254
|
-
|
|
138
|
+
const lang = document.documentElement.lang || "en";
|
|
139
|
+
loadCopydeckFor(lang);
|
|
255
140
|
registerAdapter("pyodide", pyodideAdapter);
|
|
256
141
|
}
|
|
257
|
-
|
|
258
|
-
const appendFriendlyError = (output, errorString, code) => {
|
|
142
|
+
var appendFriendlyError = (output, errorString, code) => {
|
|
259
143
|
if (!output) return;
|
|
260
144
|
let result = null;
|
|
261
145
|
if (window.PythonFriendlyErrorMessages) {
|
|
@@ -263,9 +147,10 @@ hyperbook.python = (function () {
|
|
|
263
147
|
result = window.PythonFriendlyErrorMessages.friendlyExplain({
|
|
264
148
|
error: String(errorString),
|
|
265
149
|
code,
|
|
266
|
-
runtime: "pyodide"
|
|
150
|
+
runtime: "pyodide"
|
|
267
151
|
});
|
|
268
|
-
} catch {
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
269
154
|
}
|
|
270
155
|
if (result?.html) {
|
|
271
156
|
const div = document.createElement("div");
|
|
@@ -279,11 +164,8 @@ hyperbook.python = (function () {
|
|
|
279
164
|
output.appendChild(span);
|
|
280
165
|
}
|
|
281
166
|
};
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const appendOutput = (output, message, isError = false, id = null) => {
|
|
286
|
-
if (!output || message === undefined || message === null) return;
|
|
167
|
+
var appendOutput = (output, message, isError = false, id = null) => {
|
|
168
|
+
if (!output || message === void 0 || message === null) return;
|
|
287
169
|
if (isError) {
|
|
288
170
|
const line = document.createElement("span");
|
|
289
171
|
line.classList.add("error-line");
|
|
@@ -298,75 +180,67 @@ hyperbook.python = (function () {
|
|
|
298
180
|
}
|
|
299
181
|
output.appendChild(document.createTextNode(msg));
|
|
300
182
|
};
|
|
301
|
-
|
|
302
|
-
const clearPytamaroStdoutCarry = (id) => {
|
|
183
|
+
var clearPytamaroStdoutCarry = (id) => {
|
|
303
184
|
pytamaroStdoutCarry.delete(id);
|
|
304
|
-
pytamaroCanvasTargets.delete(id);
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
const updateFullscreenButtonState = (elem, button) => {
|
|
308
|
-
if (!elem || !button) return;
|
|
309
|
-
const isFullscreen = document.fullscreenElement === elem;
|
|
310
|
-
const label = hyperbook.i18n.get("ide-fullscreen-enter");
|
|
311
|
-
button.textContent = "⛶";
|
|
312
|
-
button.title = label;
|
|
313
|
-
button.setAttribute("aria-label", label);
|
|
314
|
-
button.classList.toggle("active", isFullscreen);
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const toggleFullscreen = async (elem) => {
|
|
318
|
-
if (!elem) return;
|
|
319
|
-
if (document.fullscreenElement === elem) {
|
|
320
|
-
await document.exitFullscreen();
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
await elem.requestFullscreen();
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
const syncFullscreenButtons = () => {
|
|
327
|
-
const elems = document.getElementsByClassName("directive-pyide");
|
|
328
|
-
for (const elem of elems) {
|
|
329
|
-
const fullscreen = elem.getElementsByClassName("fullscreen")[0];
|
|
330
|
-
updateFullscreenButtonState(elem, fullscreen);
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
const releaseKeyboardCapture = (id) => {
|
|
335
|
-
const elem = document.getElementById(id);
|
|
336
|
-
if (!elem) return;
|
|
337
|
-
const canvas = elem.getElementsByClassName("canvas")[0];
|
|
338
|
-
canvas?.blur?.();
|
|
339
185
|
};
|
|
340
186
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const context = canvas.getContext("2d");
|
|
344
|
-
context?.clearRect(0, 0, canvas.width, canvas.height);
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
const createTurtleJsFFI = (id) => {
|
|
187
|
+
// assets/directive-pyide/src/turtle-ffi.js
|
|
188
|
+
var createTurtleJsFFI = (id) => {
|
|
348
189
|
const DEFAULT_LINE_WIDTH = 1;
|
|
349
190
|
const DEFAULT_FONT_SIZE = 8;
|
|
350
191
|
const DEFAULT_SHAPE = "classic";
|
|
351
|
-
|
|
352
|
-
// Shape polygons in canvas-local coords (+x = forward, +y = down).
|
|
353
|
-
// Derived from CPython/RPi turtle shapes by applying a 90° CCW screen rotation:
|
|
354
|
-
// (sx, sy) → (sy, -sx).
|
|
355
192
|
const TURTLE_SHAPES = {
|
|
356
|
-
classic:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
[
|
|
364
|
-
[
|
|
365
|
-
[
|
|
193
|
+
classic: [
|
|
194
|
+
[0, 0],
|
|
195
|
+
[-9, 5],
|
|
196
|
+
[-7, 0],
|
|
197
|
+
[-9, -5]
|
|
198
|
+
],
|
|
199
|
+
arrow: [
|
|
200
|
+
[0, 10],
|
|
201
|
+
[0, -10],
|
|
202
|
+
[10, 0]
|
|
366
203
|
],
|
|
204
|
+
triangle: [
|
|
205
|
+
[-5.77, -10],
|
|
206
|
+
[11.55, 0],
|
|
207
|
+
[-5.77, 10]
|
|
208
|
+
],
|
|
209
|
+
square: [
|
|
210
|
+
[-10, -10],
|
|
211
|
+
[10, -10],
|
|
212
|
+
[10, 10],
|
|
213
|
+
[-10, 10]
|
|
214
|
+
],
|
|
215
|
+
circle: null,
|
|
216
|
+
// rendered as arc, not polygon
|
|
217
|
+
turtle: [
|
|
218
|
+
[16, 0],
|
|
219
|
+
[14, 2],
|
|
220
|
+
[10, 1],
|
|
221
|
+
[7, 4],
|
|
222
|
+
[9, 7],
|
|
223
|
+
[8, 9],
|
|
224
|
+
[5, 6],
|
|
225
|
+
[1, 7],
|
|
226
|
+
[-3, 5],
|
|
227
|
+
[-6, 8],
|
|
228
|
+
[-8, 6],
|
|
229
|
+
[-5, 4],
|
|
230
|
+
[-7, 0],
|
|
231
|
+
[-5, -4],
|
|
232
|
+
[-8, -6],
|
|
233
|
+
[-6, -8],
|
|
234
|
+
[-3, -5],
|
|
235
|
+
[1, -7],
|
|
236
|
+
[5, -6],
|
|
237
|
+
[8, -9],
|
|
238
|
+
[9, -7],
|
|
239
|
+
[7, -4],
|
|
240
|
+
[10, -1],
|
|
241
|
+
[14, -2]
|
|
242
|
+
]
|
|
367
243
|
};
|
|
368
|
-
|
|
369
|
-
// ---- Shared canvas/screen state ----
|
|
370
244
|
let pyodide = null;
|
|
371
245
|
let canvas = null;
|
|
372
246
|
let context = null;
|
|
@@ -376,7 +250,7 @@ hyperbook.python = (function () {
|
|
|
376
250
|
let active = false;
|
|
377
251
|
let backgroundColor = "#ffffff";
|
|
378
252
|
let backgroundImage = null;
|
|
379
|
-
let colorMode = 1
|
|
253
|
+
let colorMode = 1;
|
|
380
254
|
let delayMs = 80;
|
|
381
255
|
let turtleSpeed = 3;
|
|
382
256
|
let screenWidth = 640;
|
|
@@ -386,22 +260,18 @@ hyperbook.python = (function () {
|
|
|
386
260
|
let queueRunning = false;
|
|
387
261
|
const textMeasureCanvas = document.createElement("canvas");
|
|
388
262
|
const textMeasureContext = textMeasureCanvas.getContext("2d");
|
|
389
|
-
|
|
390
|
-
// Registry of all active turtle pens (rendering state objects)
|
|
391
263
|
const allPens = [];
|
|
392
|
-
|
|
393
|
-
// ---- Shared helper functions ----
|
|
394
264
|
const normalizeAngle = (angle) => {
|
|
395
265
|
const value = Number(angle) || 0;
|
|
396
|
-
return (
|
|
266
|
+
return (value % 360 + 360) % 360;
|
|
397
267
|
};
|
|
398
|
-
|
|
399
|
-
const toRadians = (angle) => (Number(angle) * Math.PI) / 180;
|
|
268
|
+
const toRadians = (angle) => Number(angle) * Math.PI / 180;
|
|
400
269
|
const toCanvasX = (value) => cssWidth / 2 + value;
|
|
401
270
|
const toCanvasY = (value) => cssHeight / 2 - value;
|
|
402
271
|
const toPlainNumber = (value, fallback = Number.NaN) => {
|
|
403
|
-
if (value === null || value ===
|
|
404
|
-
if (typeof value === "number")
|
|
272
|
+
if (value === null || value === void 0) return fallback;
|
|
273
|
+
if (typeof value === "number")
|
|
274
|
+
return Number.isFinite(value) ? value : fallback;
|
|
405
275
|
if (typeof value === "bigint") return Number(value);
|
|
406
276
|
if (typeof value.toJs === "function") {
|
|
407
277
|
return toPlainNumber(value.toJs({ pyproxies: [] }), fallback);
|
|
@@ -410,7 +280,7 @@ hyperbook.python = (function () {
|
|
|
410
280
|
return Number.isFinite(numeric) ? numeric : fallback;
|
|
411
281
|
};
|
|
412
282
|
const toPlainBoolean = (value, fallback = false) => {
|
|
413
|
-
if (value === null || value ===
|
|
283
|
+
if (value === null || value === void 0) return fallback;
|
|
414
284
|
if (typeof value === "boolean") return value;
|
|
415
285
|
if (typeof value === "number") return value !== 0;
|
|
416
286
|
if (typeof value === "bigint") return value !== 0n;
|
|
@@ -429,15 +299,15 @@ hyperbook.python = (function () {
|
|
|
429
299
|
const numeric = toPlainNumber(value, Number.NaN);
|
|
430
300
|
if (!Number.isFinite(numeric) || numeric === 0) {
|
|
431
301
|
const fallback = DEFAULT_FONT_SIZE;
|
|
432
|
-
return { size: `${fallback}pt`, pxApprox:
|
|
302
|
+
return { size: `${fallback}pt`, pxApprox: fallback * 96 / 72 };
|
|
433
303
|
}
|
|
434
304
|
if (numeric < 0) {
|
|
435
305
|
return { size: `${Math.abs(numeric)}px`, pxApprox: Math.abs(numeric) };
|
|
436
306
|
}
|
|
437
|
-
return { size: `${numeric}pt`, pxApprox:
|
|
307
|
+
return { size: `${numeric}pt`, pxApprox: numeric * 96 / 72 };
|
|
438
308
|
};
|
|
439
309
|
const toPlainString = (value, fallback = "") => {
|
|
440
|
-
if (value === null || value ===
|
|
310
|
+
if (value === null || value === void 0) return fallback;
|
|
441
311
|
if (typeof value === "string") return value;
|
|
442
312
|
if (typeof value.toJs === "function") {
|
|
443
313
|
return toPlainString(value.toJs({ pyproxies: [] }), fallback);
|
|
@@ -445,7 +315,7 @@ hyperbook.python = (function () {
|
|
|
445
315
|
return String(value);
|
|
446
316
|
};
|
|
447
317
|
const toSequence = (value) => {
|
|
448
|
-
if (value === null || value ===
|
|
318
|
+
if (value === null || value === void 0) return null;
|
|
449
319
|
if (Array.isArray(value)) return value;
|
|
450
320
|
if (typeof value === "string") return [value];
|
|
451
321
|
if (typeof value.toJs === "function") {
|
|
@@ -454,20 +324,24 @@ hyperbook.python = (function () {
|
|
|
454
324
|
if (typeof value[Symbol.iterator] === "function") {
|
|
455
325
|
try {
|
|
456
326
|
return Array.from(value);
|
|
457
|
-
} catch {
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
458
329
|
}
|
|
459
330
|
if (typeof value === "object" && "length" in value) {
|
|
460
331
|
try {
|
|
461
332
|
return Array.from(value);
|
|
462
|
-
} catch {
|
|
333
|
+
} catch {
|
|
334
|
+
}
|
|
463
335
|
}
|
|
464
336
|
return null;
|
|
465
337
|
};
|
|
466
338
|
const toPlainObject = (value) => {
|
|
467
|
-
if (value === null || value ===
|
|
339
|
+
if (value === null || value === void 0) return null;
|
|
468
340
|
if (typeof value.toJs === "function") {
|
|
469
341
|
try {
|
|
470
|
-
return toPlainObject(
|
|
342
|
+
return toPlainObject(
|
|
343
|
+
value.toJs({ pyproxies: [], dict_converter: Object.fromEntries })
|
|
344
|
+
);
|
|
471
345
|
} catch {
|
|
472
346
|
return toPlainObject(value.toJs({ pyproxies: [] }));
|
|
473
347
|
}
|
|
@@ -479,13 +353,7 @@ hyperbook.python = (function () {
|
|
|
479
353
|
const isWriteKwargsObject = (value) => {
|
|
480
354
|
const obj = toPlainObject(value);
|
|
481
355
|
if (!obj) return false;
|
|
482
|
-
return (
|
|
483
|
-
hasOwn(obj, "arg") ||
|
|
484
|
-
hasOwn(obj, "text") ||
|
|
485
|
-
hasOwn(obj, "move") ||
|
|
486
|
-
hasOwn(obj, "align") ||
|
|
487
|
-
hasOwn(obj, "font")
|
|
488
|
-
);
|
|
356
|
+
return hasOwn(obj, "arg") || hasOwn(obj, "text") || hasOwn(obj, "move") || hasOwn(obj, "align") || hasOwn(obj, "font");
|
|
489
357
|
};
|
|
490
358
|
const normalizeFontStyle = (value) => {
|
|
491
359
|
const style = toPlainString(value, "normal").trim().toLowerCase();
|
|
@@ -516,7 +384,7 @@ hyperbook.python = (function () {
|
|
|
516
384
|
return {
|
|
517
385
|
font,
|
|
518
386
|
width: String(text || "").length * pxApprox * 0.6,
|
|
519
|
-
descent: pxApprox * 0.2
|
|
387
|
+
descent: pxApprox * 0.2
|
|
520
388
|
};
|
|
521
389
|
}
|
|
522
390
|
ctx.font = font;
|
|
@@ -527,14 +395,8 @@ hyperbook.python = (function () {
|
|
|
527
395
|
width: metrics.width,
|
|
528
396
|
left: metrics.actualBoundingBoxLeft || 0,
|
|
529
397
|
right: metrics.actualBoundingBoxRight || metrics.width,
|
|
530
|
-
ascent:
|
|
531
|
-
|
|
532
|
-
metrics.actualBoundingBoxAscent ||
|
|
533
|
-
pxApprox * 0.8,
|
|
534
|
-
descent:
|
|
535
|
-
metrics.fontBoundingBoxDescent ||
|
|
536
|
-
metrics.actualBoundingBoxDescent ||
|
|
537
|
-
pxApprox * 0.2,
|
|
398
|
+
ascent: metrics.fontBoundingBoxAscent || metrics.actualBoundingBoxAscent || pxApprox * 0.8,
|
|
399
|
+
descent: metrics.fontBoundingBoxDescent || metrics.actualBoundingBoxDescent || pxApprox * 0.2
|
|
538
400
|
};
|
|
539
401
|
};
|
|
540
402
|
const toColorString = (value) => {
|
|
@@ -544,12 +406,12 @@ hyperbook.python = (function () {
|
|
|
544
406
|
if (value && typeof value.toJs === "function") {
|
|
545
407
|
return toColorString(value.toJs({ pyproxies: [] }));
|
|
546
408
|
}
|
|
547
|
-
if (Array.isArray(value) ||
|
|
409
|
+
if (Array.isArray(value) || value && typeof value === "object" && "length" in value) {
|
|
548
410
|
const parts = Array.from(value).slice(0, 3).map((part) => Number(part));
|
|
549
411
|
if (parts.length !== 3 || parts.some((part) => !Number.isFinite(part))) {
|
|
550
412
|
throw new Error(`bad color sequence: ${String(value)}`);
|
|
551
413
|
}
|
|
552
|
-
if (colorMode === 1
|
|
414
|
+
if (colorMode === 1) {
|
|
553
415
|
if (parts.some((part) => part < 0 || part > 1)) {
|
|
554
416
|
throw new Error(`bad color sequence: ${String(value)}`);
|
|
555
417
|
}
|
|
@@ -567,7 +429,6 @@ hyperbook.python = (function () {
|
|
|
567
429
|
}
|
|
568
430
|
return "#000000";
|
|
569
431
|
};
|
|
570
|
-
|
|
571
432
|
const sleep = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
572
433
|
const clearQueue = () => {
|
|
573
434
|
queueGeneration += 1;
|
|
@@ -600,7 +461,6 @@ hyperbook.python = (function () {
|
|
|
600
461
|
}
|
|
601
462
|
})();
|
|
602
463
|
};
|
|
603
|
-
|
|
604
464
|
const ensureContext = () => {
|
|
605
465
|
if (!canvas) {
|
|
606
466
|
canvas = getCanvas(id);
|
|
@@ -611,22 +471,19 @@ hyperbook.python = (function () {
|
|
|
611
471
|
}
|
|
612
472
|
return !!context;
|
|
613
473
|
};
|
|
614
|
-
|
|
615
474
|
const setupCanvasResolution = () => {
|
|
616
475
|
if (!ensureContext()) return false;
|
|
617
476
|
dpr = window.devicePixelRatio || 1;
|
|
618
477
|
const wrapperRect = canvas.parentElement?.getBoundingClientRect?.();
|
|
619
|
-
const panelWidth =
|
|
620
|
-
|
|
621
|
-
const panelHeight =
|
|
622
|
-
wrapperRect?.height || canvas.parentElement?.clientHeight || canvas.clientHeight || canvas.height || 400;
|
|
478
|
+
const panelWidth = wrapperRect?.width || canvas.parentElement?.clientWidth || canvas.clientWidth || canvas.width || 800;
|
|
479
|
+
const panelHeight = wrapperRect?.height || canvas.parentElement?.clientHeight || canvas.clientHeight || canvas.height || 400;
|
|
623
480
|
const width = Math.max(
|
|
624
481
|
1,
|
|
625
|
-
Math.floor(screenWidth !== null ? screenWidth : panelWidth)
|
|
482
|
+
Math.floor(screenWidth !== null ? screenWidth : panelWidth)
|
|
626
483
|
);
|
|
627
484
|
const height = Math.max(
|
|
628
485
|
1,
|
|
629
|
-
Math.floor(screenHeight !== null ? screenHeight : panelHeight)
|
|
486
|
+
Math.floor(screenHeight !== null ? screenHeight : panelHeight)
|
|
630
487
|
);
|
|
631
488
|
cssWidth = width;
|
|
632
489
|
cssHeight = height;
|
|
@@ -637,7 +494,6 @@ hyperbook.python = (function () {
|
|
|
637
494
|
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
638
495
|
return true;
|
|
639
496
|
};
|
|
640
|
-
|
|
641
497
|
const drawBackground = () => {
|
|
642
498
|
context.save();
|
|
643
499
|
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
@@ -648,10 +504,8 @@ hyperbook.python = (function () {
|
|
|
648
504
|
}
|
|
649
505
|
context.restore();
|
|
650
506
|
};
|
|
651
|
-
|
|
652
507
|
const drawPathSegment = (path) => {
|
|
653
508
|
if (!path?.points?.length) return;
|
|
654
|
-
|
|
655
509
|
if (path.fill) {
|
|
656
510
|
context.beginPath();
|
|
657
511
|
path.points.forEach((point, index) => {
|
|
@@ -668,13 +522,12 @@ hyperbook.python = (function () {
|
|
|
668
522
|
context.fill();
|
|
669
523
|
return;
|
|
670
524
|
}
|
|
671
|
-
|
|
672
525
|
let startedLine = false;
|
|
673
526
|
context.beginPath();
|
|
674
527
|
for (const point of path.points) {
|
|
675
528
|
const px = toCanvasX(point.x);
|
|
676
529
|
const py = toCanvasY(point.y);
|
|
677
|
-
if (point.text !==
|
|
530
|
+
if (point.text !== void 0) {
|
|
678
531
|
const family = point.fontfamily || path.fontfamily || "Arial";
|
|
679
532
|
const style = point.fontstyle || path.fontstyle || "normal";
|
|
680
533
|
const size = point.fontsize ?? path.fontsize ?? DEFAULT_FONT_SIZE;
|
|
@@ -704,14 +557,12 @@ hyperbook.python = (function () {
|
|
|
704
557
|
}
|
|
705
558
|
context.lineTo(px, py);
|
|
706
559
|
}
|
|
707
|
-
|
|
708
560
|
if (path.down) {
|
|
709
561
|
context.strokeStyle = path.stroke;
|
|
710
562
|
context.lineWidth = path.lineWidth || DEFAULT_LINE_WIDTH;
|
|
711
563
|
context.stroke();
|
|
712
564
|
}
|
|
713
565
|
};
|
|
714
|
-
|
|
715
566
|
const drawTurtleShape = (pen) => {
|
|
716
567
|
if (!pen.renderedTurtleVisible) return;
|
|
717
568
|
context.save();
|
|
@@ -739,7 +590,6 @@ hyperbook.python = (function () {
|
|
|
739
590
|
}
|
|
740
591
|
context.restore();
|
|
741
592
|
};
|
|
742
|
-
|
|
743
593
|
const draw = () => {
|
|
744
594
|
if (!active) return;
|
|
745
595
|
if (!setupCanvasResolution()) return;
|
|
@@ -751,8 +601,6 @@ hyperbook.python = (function () {
|
|
|
751
601
|
drawTurtleShape(pen);
|
|
752
602
|
}
|
|
753
603
|
};
|
|
754
|
-
|
|
755
|
-
// ---- Per-turtle pen factory ----
|
|
756
604
|
const createTurtlePen = () => {
|
|
757
605
|
let x = 0;
|
|
758
606
|
let y = 0;
|
|
@@ -767,8 +615,6 @@ hyperbook.python = (function () {
|
|
|
767
615
|
let filling = false;
|
|
768
616
|
let fillPath = null;
|
|
769
617
|
let currentPath = null;
|
|
770
|
-
|
|
771
|
-
// Mutable rendering state exposed to the shared draw() function
|
|
772
618
|
const pen = {
|
|
773
619
|
paths: [],
|
|
774
620
|
renderedX: 0,
|
|
@@ -776,9 +622,8 @@ hyperbook.python = (function () {
|
|
|
776
622
|
renderedHeading: 0,
|
|
777
623
|
renderedTurtleVisible: true,
|
|
778
624
|
shapeColor: "#000000",
|
|
779
|
-
shapeName: DEFAULT_SHAPE
|
|
625
|
+
shapeName: DEFAULT_SHAPE
|
|
780
626
|
};
|
|
781
|
-
|
|
782
627
|
const makePath = (overrides = {}) => ({
|
|
783
628
|
down: penDown,
|
|
784
629
|
stroke: strokeColor,
|
|
@@ -789,42 +634,36 @@ hyperbook.python = (function () {
|
|
|
789
634
|
fill: false,
|
|
790
635
|
fillstyle: fillColor,
|
|
791
636
|
points: [{ x, y }],
|
|
792
|
-
...overrides
|
|
637
|
+
...overrides
|
|
793
638
|
});
|
|
794
|
-
|
|
795
639
|
const beginCurrentPath = () => {
|
|
796
640
|
currentPath = makePath();
|
|
797
641
|
pen.paths.push(currentPath);
|
|
798
642
|
return currentPath;
|
|
799
643
|
};
|
|
800
|
-
|
|
801
644
|
const commitStyleToNewPath = () => {
|
|
802
645
|
currentPath = makePath({
|
|
803
646
|
down: penDown,
|
|
804
|
-
lineWidth: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH
|
|
647
|
+
lineWidth: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH
|
|
805
648
|
});
|
|
806
649
|
pen.paths.push(currentPath);
|
|
807
650
|
return currentPath;
|
|
808
651
|
};
|
|
809
|
-
|
|
810
652
|
const ensurePath = () => {
|
|
811
653
|
if (!currentPath) {
|
|
812
654
|
beginCurrentPath();
|
|
813
655
|
}
|
|
814
656
|
return currentPath;
|
|
815
657
|
};
|
|
816
|
-
|
|
817
658
|
const getPenState = () => ({
|
|
818
659
|
pendown: penDown,
|
|
819
660
|
pencolor: strokeColor,
|
|
820
661
|
fillcolor: fillColor,
|
|
821
662
|
pensize: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH,
|
|
822
663
|
speed: turtleSpeed,
|
|
823
|
-
shown: turtleVisible
|
|
664
|
+
shown: turtleVisible
|
|
824
665
|
});
|
|
825
|
-
|
|
826
666
|
beginCurrentPath();
|
|
827
|
-
|
|
828
667
|
const forward = (distance) => {
|
|
829
668
|
if (!ensureContext()) return;
|
|
830
669
|
const path = ensurePath();
|
|
@@ -845,7 +684,6 @@ hyperbook.python = (function () {
|
|
|
845
684
|
draw();
|
|
846
685
|
});
|
|
847
686
|
};
|
|
848
|
-
|
|
849
687
|
const backward = (distance) => forward(-Number(distance || 0));
|
|
850
688
|
const right = (angle) => {
|
|
851
689
|
heading = normalizeAngle(heading - Number(angle || 0));
|
|
@@ -877,9 +715,8 @@ hyperbook.python = (function () {
|
|
|
877
715
|
if (!ensureContext()) return;
|
|
878
716
|
let nextX = Number(a);
|
|
879
717
|
let nextY = Number(b);
|
|
880
|
-
if (b ===
|
|
881
|
-
const source =
|
|
882
|
-
a && typeof a.toJs === "function" ? a.toJs({ pyproxies: [] }) : a;
|
|
718
|
+
if (b === void 0 && (Array.isArray(a) || a && typeof a === "object")) {
|
|
719
|
+
const source = a && typeof a.toJs === "function" ? a.toJs({ pyproxies: [] }) : a;
|
|
883
720
|
nextX = Number(source?.[0] ?? source?.x ?? 0);
|
|
884
721
|
nextY = Number(source?.[1] ?? source?.y ?? 0);
|
|
885
722
|
}
|
|
@@ -920,7 +757,7 @@ hyperbook.python = (function () {
|
|
|
920
757
|
const dx = Number(tx) - x;
|
|
921
758
|
const dy = Number(ty) - y;
|
|
922
759
|
if (!Number.isFinite(dx) || !Number.isFinite(dy)) return 0;
|
|
923
|
-
return normalizeAngle(
|
|
760
|
+
return normalizeAngle(Math.atan2(dy, dx) * 180 / Math.PI);
|
|
924
761
|
};
|
|
925
762
|
const speed = (value = 0) => {
|
|
926
763
|
const numeric = Number(value);
|
|
@@ -968,7 +805,7 @@ hyperbook.python = (function () {
|
|
|
968
805
|
fill: true,
|
|
969
806
|
stroke: "transparent",
|
|
970
807
|
lineWidth: 1,
|
|
971
|
-
fillstyle: fillColor
|
|
808
|
+
fillstyle: fillColor
|
|
972
809
|
});
|
|
973
810
|
pen.paths.push(fillPath);
|
|
974
811
|
draw();
|
|
@@ -990,9 +827,9 @@ hyperbook.python = (function () {
|
|
|
990
827
|
x,
|
|
991
828
|
y,
|
|
992
829
|
dotRadius: Math.max(0.5, Number(size) || 5) / 2,
|
|
993
|
-
color: colorValue ? toColorString(colorValue) : strokeColor
|
|
994
|
-
}
|
|
995
|
-
]
|
|
830
|
+
color: colorValue ? toColorString(colorValue) : strokeColor
|
|
831
|
+
}
|
|
832
|
+
]
|
|
996
833
|
});
|
|
997
834
|
enqueueOperation(() => {
|
|
998
835
|
pen.paths.push(segment);
|
|
@@ -1004,7 +841,7 @@ hyperbook.python = (function () {
|
|
|
1004
841
|
const stepCount = Math.max(8, Number(steps) | 0);
|
|
1005
842
|
const circumference = 2 * Math.PI * Math.abs(numericRadius);
|
|
1006
843
|
const stepLength = circumference / stepCount;
|
|
1007
|
-
const stepTurn =
|
|
844
|
+
const stepTurn = 360 / stepCount * (numericRadius >= 0 ? 1 : -1);
|
|
1008
845
|
for (let index = 0; index < stepCount; index += 1) {
|
|
1009
846
|
forward(stepLength);
|
|
1010
847
|
left(stepTurn);
|
|
@@ -1032,7 +869,6 @@ hyperbook.python = (function () {
|
|
|
1032
869
|
if (isWriteKwargsObject(writeFont)) writeFont = null;
|
|
1033
870
|
if (isWriteKwargsObject(writeMove)) writeMove = false;
|
|
1034
871
|
if (isWriteKwargsObject(writeAlign)) writeAlign = "left";
|
|
1035
|
-
|
|
1036
872
|
let family = currentFontFamily;
|
|
1037
873
|
let size = fontSize;
|
|
1038
874
|
let style = currentFontStyle;
|
|
@@ -1040,12 +876,13 @@ hyperbook.python = (function () {
|
|
|
1040
876
|
const source = toSequence(writeFont);
|
|
1041
877
|
if (source?.length) {
|
|
1042
878
|
if (source[0]) family = toPlainString(source[0], family);
|
|
1043
|
-
if (source[1] !==
|
|
879
|
+
if (source[1] !== void 0 && source[1] !== null) {
|
|
1044
880
|
size = toPlainNumber(source[1], size);
|
|
1045
881
|
}
|
|
1046
882
|
if (source[2]) style = toPlainString(source[2], style);
|
|
1047
883
|
}
|
|
1048
|
-
} catch {
|
|
884
|
+
} catch {
|
|
885
|
+
}
|
|
1049
886
|
const normalizedAlign = normalizeTextAlign(writeAlign);
|
|
1050
887
|
const segment = makePath({
|
|
1051
888
|
down: false,
|
|
@@ -1060,19 +897,23 @@ hyperbook.python = (function () {
|
|
|
1060
897
|
fontsize: size,
|
|
1061
898
|
fontfamily: family,
|
|
1062
899
|
fontstyle: style,
|
|
1063
|
-
color: strokeColor
|
|
1064
|
-
}
|
|
1065
|
-
]
|
|
900
|
+
color: strokeColor
|
|
901
|
+
}
|
|
902
|
+
]
|
|
1066
903
|
});
|
|
1067
904
|
if (toPlainBoolean(writeMove, false)) {
|
|
1068
|
-
const metrics = measureTurtleText(
|
|
905
|
+
const metrics = measureTurtleText(
|
|
906
|
+
String(writeText),
|
|
907
|
+
family,
|
|
908
|
+
size,
|
|
909
|
+
style
|
|
910
|
+
);
|
|
1069
911
|
const textWidth = metrics.width || 0;
|
|
1070
912
|
if (normalizedAlign === "left") {
|
|
1071
913
|
x += textWidth;
|
|
1072
914
|
} else if (normalizedAlign === "center") {
|
|
1073
915
|
x += textWidth / 2;
|
|
1074
916
|
}
|
|
1075
|
-
// right alignment: turtle stays at its position (text ends there)
|
|
1076
917
|
commitStyleToNewPath();
|
|
1077
918
|
}
|
|
1078
919
|
enqueueOperation(() => {
|
|
@@ -1082,7 +923,10 @@ hyperbook.python = (function () {
|
|
|
1082
923
|
};
|
|
1083
924
|
const setfontsize = (value) => {
|
|
1084
925
|
const nextSize = toPlainNumber(value, DEFAULT_FONT_SIZE);
|
|
1085
|
-
fontSize = Math.max(
|
|
926
|
+
fontSize = Math.max(
|
|
927
|
+
1,
|
|
928
|
+
Number.isFinite(nextSize) ? nextSize : DEFAULT_FONT_SIZE
|
|
929
|
+
);
|
|
1086
930
|
commitStyleToNewPath();
|
|
1087
931
|
draw();
|
|
1088
932
|
};
|
|
@@ -1101,8 +945,8 @@ hyperbook.python = (function () {
|
|
|
1101
945
|
});
|
|
1102
946
|
};
|
|
1103
947
|
const isvisible = () => turtleVisible;
|
|
1104
|
-
const shape = (name =
|
|
1105
|
-
if (name ===
|
|
948
|
+
const shape = (name = void 0) => {
|
|
949
|
+
if (name === void 0 || name === null) {
|
|
1106
950
|
return pen.shapeName;
|
|
1107
951
|
}
|
|
1108
952
|
const nameStr = toPlainString(name, DEFAULT_SHAPE).toLowerCase().trim();
|
|
@@ -1112,14 +956,11 @@ hyperbook.python = (function () {
|
|
|
1112
956
|
draw();
|
|
1113
957
|
return pen.shapeName;
|
|
1114
958
|
};
|
|
1115
|
-
const penFn = (options =
|
|
1116
|
-
if (options ===
|
|
959
|
+
const penFn = (options = void 0) => {
|
|
960
|
+
if (options === void 0) {
|
|
1117
961
|
return getPenState();
|
|
1118
962
|
}
|
|
1119
|
-
const source =
|
|
1120
|
-
options && typeof options.toJs === "function"
|
|
1121
|
-
? options.toJs({ pyproxies: [], dict_converter: Object.fromEntries })
|
|
1122
|
-
: options;
|
|
963
|
+
const source = options && typeof options.toJs === "function" ? options.toJs({ pyproxies: [], dict_converter: Object.fromEntries }) : options;
|
|
1123
964
|
if (!source || typeof source !== "object") return getPenState();
|
|
1124
965
|
if ("pendown" in source) {
|
|
1125
966
|
penDown = !!source.pendown;
|
|
@@ -1131,7 +972,10 @@ hyperbook.python = (function () {
|
|
|
1131
972
|
fillColor = toColorString(source.fillcolor);
|
|
1132
973
|
}
|
|
1133
974
|
if ("pensize" in source) {
|
|
1134
|
-
ensurePath().lineWidth = Math.max(
|
|
975
|
+
ensurePath().lineWidth = Math.max(
|
|
976
|
+
1,
|
|
977
|
+
Number(source.pensize) || DEFAULT_LINE_WIDTH
|
|
978
|
+
);
|
|
1135
979
|
}
|
|
1136
980
|
if ("shown" in source) {
|
|
1137
981
|
turtleVisible = !!source.shown;
|
|
@@ -1171,8 +1015,7 @@ hyperbook.python = (function () {
|
|
|
1171
1015
|
filling = false;
|
|
1172
1016
|
beginCurrentPath();
|
|
1173
1017
|
};
|
|
1174
|
-
|
|
1175
|
-
const api = {
|
|
1018
|
+
const api2 = {
|
|
1176
1019
|
forward,
|
|
1177
1020
|
fd: forward,
|
|
1178
1021
|
backward,
|
|
@@ -1222,15 +1065,12 @@ hyperbook.python = (function () {
|
|
|
1222
1065
|
isvisible,
|
|
1223
1066
|
shape,
|
|
1224
1067
|
clear,
|
|
1225
|
-
reset: resetPen
|
|
1068
|
+
reset: resetPen
|
|
1226
1069
|
};
|
|
1227
|
-
|
|
1228
|
-
return { pen, api, resetPen };
|
|
1070
|
+
return { pen, api: api2, resetPen };
|
|
1229
1071
|
};
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
const colormode = (mode = undefined) => {
|
|
1233
|
-
if (mode === undefined) return colorMode;
|
|
1072
|
+
const colormode = (mode = void 0) => {
|
|
1073
|
+
if (mode === void 0) return colorMode;
|
|
1234
1074
|
const numeric = Number(mode);
|
|
1235
1075
|
if (numeric !== 1 && numeric !== 255) {
|
|
1236
1076
|
throw new Error("colormode must be 1.0 or 255");
|
|
@@ -1238,23 +1078,23 @@ hyperbook.python = (function () {
|
|
|
1238
1078
|
colorMode = numeric;
|
|
1239
1079
|
return colorMode;
|
|
1240
1080
|
};
|
|
1241
|
-
const screensize = (canvwidth =
|
|
1242
|
-
if (canvwidth ===
|
|
1081
|
+
const screensize = (canvwidth = void 0, canvheight = void 0, bg = void 0) => {
|
|
1082
|
+
if (canvwidth === void 0 && canvheight === void 0 && bg === void 0) {
|
|
1243
1083
|
return [screenWidth || cssWidth || 0, screenHeight || cssHeight || 0];
|
|
1244
1084
|
}
|
|
1245
|
-
if (canvwidth !==
|
|
1085
|
+
if (canvwidth !== void 0 && canvwidth !== null) {
|
|
1246
1086
|
const width = Math.floor(Number(canvwidth));
|
|
1247
1087
|
if (Number.isFinite(width) && width > 0) {
|
|
1248
1088
|
screenWidth = width;
|
|
1249
1089
|
}
|
|
1250
1090
|
}
|
|
1251
|
-
if (canvheight !==
|
|
1091
|
+
if (canvheight !== void 0 && canvheight !== null) {
|
|
1252
1092
|
const height = Math.floor(Number(canvheight));
|
|
1253
1093
|
if (Number.isFinite(height) && height > 0) {
|
|
1254
1094
|
screenHeight = height;
|
|
1255
1095
|
}
|
|
1256
1096
|
}
|
|
1257
|
-
if (bg !==
|
|
1097
|
+
if (bg !== void 0 && bg !== null) {
|
|
1258
1098
|
backgroundColor = toColorString(bg);
|
|
1259
1099
|
}
|
|
1260
1100
|
draw();
|
|
@@ -1282,7 +1122,7 @@ hyperbook.python = (function () {
|
|
|
1282
1122
|
name,
|
|
1283
1123
|
`/home/pyodide/${name}`,
|
|
1284
1124
|
`/home/pyodide/files/${name}`,
|
|
1285
|
-
`/home/pyodide/uploads/${name}
|
|
1125
|
+
`/home/pyodide/uploads/${name}`
|
|
1286
1126
|
];
|
|
1287
1127
|
for (const candidate of candidates) {
|
|
1288
1128
|
try {
|
|
@@ -1298,14 +1138,12 @@ hyperbook.python = (function () {
|
|
|
1298
1138
|
image.onerror = () => URL.revokeObjectURL(url);
|
|
1299
1139
|
image.src = url;
|
|
1300
1140
|
return;
|
|
1301
|
-
} catch {
|
|
1141
|
+
} catch {
|
|
1142
|
+
}
|
|
1302
1143
|
}
|
|
1303
1144
|
};
|
|
1304
|
-
|
|
1305
|
-
// Create the default turtle pen and register it
|
|
1306
1145
|
const defaultPenObj = createTurtlePen();
|
|
1307
1146
|
allPens.push(defaultPenObj.pen);
|
|
1308
|
-
|
|
1309
1147
|
const bindCanvas = (nextCanvas) => {
|
|
1310
1148
|
canvas = nextCanvas || getCanvas(id);
|
|
1311
1149
|
context = canvas?.getContext?.("2d") || null;
|
|
@@ -1314,7 +1152,6 @@ hyperbook.python = (function () {
|
|
|
1314
1152
|
}
|
|
1315
1153
|
draw();
|
|
1316
1154
|
};
|
|
1317
|
-
|
|
1318
1155
|
const deactivate = () => {
|
|
1319
1156
|
active = false;
|
|
1320
1157
|
clearQueue();
|
|
@@ -1323,27 +1160,20 @@ hyperbook.python = (function () {
|
|
|
1323
1160
|
pen.renderedTurtleVisible = false;
|
|
1324
1161
|
}
|
|
1325
1162
|
};
|
|
1326
|
-
|
|
1327
1163
|
const resetState = () => {
|
|
1328
1164
|
clearQueue();
|
|
1329
|
-
// Remove all extra turtles, keeping only the default pen
|
|
1330
1165
|
allPens.length = 0;
|
|
1331
1166
|
allPens.push(defaultPenObj.pen);
|
|
1332
1167
|
defaultPenObj.resetPen();
|
|
1333
1168
|
backgroundColor = "#ffffff";
|
|
1334
1169
|
backgroundImage = null;
|
|
1335
|
-
colorMode = 1
|
|
1170
|
+
colorMode = 1;
|
|
1336
1171
|
screenWidth = 640;
|
|
1337
1172
|
screenHeight = 480;
|
|
1338
1173
|
active = true;
|
|
1339
1174
|
draw();
|
|
1340
1175
|
};
|
|
1341
|
-
|
|
1342
|
-
// Turtle constructor — creates an additional independent turtle on the same canvas.
|
|
1343
|
-
// The pen is registered into allPens via enqueueOperation so it enters the render
|
|
1344
|
-
// loop in queue order, preventing the turtle from appearing at center before prior
|
|
1345
|
-
// drawing operations have completed.
|
|
1346
|
-
const Turtle = function () {
|
|
1176
|
+
const Turtle = function() {
|
|
1347
1177
|
const penObj = createTurtlePen();
|
|
1348
1178
|
enqueueOperation(() => {
|
|
1349
1179
|
allPens.push(penObj.pen);
|
|
@@ -1351,7 +1181,6 @@ hyperbook.python = (function () {
|
|
|
1351
1181
|
});
|
|
1352
1182
|
return penObj.api;
|
|
1353
1183
|
};
|
|
1354
|
-
|
|
1355
1184
|
const api = {
|
|
1356
1185
|
__bindCanvas: bindCanvas,
|
|
1357
1186
|
__deactivate: deactivate,
|
|
@@ -1365,16 +1194,15 @@ hyperbook.python = (function () {
|
|
|
1365
1194
|
colormode,
|
|
1366
1195
|
screensize,
|
|
1367
1196
|
bgcolor,
|
|
1368
|
-
bgpic
|
|
1197
|
+
bgpic
|
|
1369
1198
|
};
|
|
1370
|
-
|
|
1371
1199
|
return api;
|
|
1372
1200
|
};
|
|
1373
1201
|
|
|
1374
|
-
|
|
1202
|
+
// assets/directive-pyide/src/pytamaro-ffi.js
|
|
1203
|
+
var createPytamaroJsFFI = () => {
|
|
1375
1204
|
const floatBuffer = new ArrayBuffer(4);
|
|
1376
1205
|
const floatView = new DataView(floatBuffer);
|
|
1377
|
-
|
|
1378
1206
|
const unProxy = (obj) => {
|
|
1379
1207
|
if (typeof obj === "object" && obj !== null && typeof obj.toJs === "function") {
|
|
1380
1208
|
try {
|
|
@@ -1386,31 +1214,24 @@ hyperbook.python = (function () {
|
|
|
1386
1214
|
}
|
|
1387
1215
|
return obj;
|
|
1388
1216
|
};
|
|
1389
|
-
|
|
1390
1217
|
const uint32ToFloat = (u32) => {
|
|
1391
1218
|
floatView.setUint32(0, u32 >>> 0, false);
|
|
1392
1219
|
return floatView.getFloat32(0, false);
|
|
1393
1220
|
};
|
|
1394
|
-
|
|
1395
1221
|
const decodePoint = (value, width, height) => {
|
|
1396
1222
|
const packed = typeof value === "bigint" ? value : BigInt(value || 0);
|
|
1397
|
-
const x = uint32ToFloat(Number(
|
|
1223
|
+
const x = uint32ToFloat(Number(packed >> 32n & 0xffffffffn));
|
|
1398
1224
|
const y = uint32ToFloat(Number(packed & 0xffffffffn));
|
|
1399
1225
|
return { x: x * width * 0.5, y: -y * height * 0.5 };
|
|
1400
1226
|
};
|
|
1401
|
-
|
|
1402
1227
|
const colorToCss = (value) => {
|
|
1403
|
-
const argb =
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
const
|
|
1408
|
-
const r = (argb >> 16) & 0xff;
|
|
1409
|
-
const g = (argb >> 8) & 0xff;
|
|
1410
|
-
const b = argb & 0xff;
|
|
1228
|
+
const argb = typeof value === "bigint" ? Number(value & 0xffffffffn) : Number(value >>> 0);
|
|
1229
|
+
const a = (argb >> 24 & 255) / 255;
|
|
1230
|
+
const r = argb >> 16 & 255;
|
|
1231
|
+
const g = argb >> 8 & 255;
|
|
1232
|
+
const b = argb & 255;
|
|
1411
1233
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
1412
1234
|
};
|
|
1413
|
-
|
|
1414
1235
|
const rotatePoint = (point, pivot, angleRad) => {
|
|
1415
1236
|
const dx = point.x - pivot.x;
|
|
1416
1237
|
const dy = point.y - pivot.y;
|
|
@@ -1418,31 +1239,22 @@ hyperbook.python = (function () {
|
|
|
1418
1239
|
const sin = Math.sin(angleRad);
|
|
1419
1240
|
return {
|
|
1420
1241
|
x: pivot.x + dx * cos - dy * sin,
|
|
1421
|
-
y: pivot.y + dx * sin + dy * cos
|
|
1242
|
+
y: pivot.y + dx * sin + dy * cos
|
|
1422
1243
|
};
|
|
1423
1244
|
};
|
|
1424
|
-
|
|
1425
1245
|
const buildGraphic = (specs) => {
|
|
1426
1246
|
const stack = [];
|
|
1427
1247
|
const measureCanvas = document.createElement("canvas");
|
|
1428
1248
|
const measureCtx = measureCanvas.getContext("2d");
|
|
1429
|
-
|
|
1430
1249
|
for (const spec of specs || []) {
|
|
1431
1250
|
if (!spec || typeof spec !== "object") continue;
|
|
1432
1251
|
const type = spec.t;
|
|
1433
|
-
if (
|
|
1434
|
-
type === "Empty" ||
|
|
1435
|
-
type === "Rectangle" ||
|
|
1436
|
-
type === "Ellipse" ||
|
|
1437
|
-
type === "CircularSector" ||
|
|
1438
|
-
type === "Triangle" ||
|
|
1439
|
-
type === "Text"
|
|
1440
|
-
) {
|
|
1252
|
+
if (type === "Empty" || type === "Rectangle" || type === "Ellipse" || type === "CircularSector" || type === "Triangle" || type === "Text") {
|
|
1441
1253
|
let width = 0;
|
|
1442
1254
|
let height = 0;
|
|
1443
1255
|
let pin = { x: 0, y: 0 };
|
|
1444
|
-
let draw = () => {
|
|
1445
|
-
|
|
1256
|
+
let draw = () => {
|
|
1257
|
+
};
|
|
1446
1258
|
if (type === "Rectangle") {
|
|
1447
1259
|
width = Math.max(0, Number(spec.width) || 0);
|
|
1448
1260
|
height = Math.max(0, Number(spec.height) || 0);
|
|
@@ -1470,7 +1282,7 @@ hyperbook.python = (function () {
|
|
|
1470
1282
|
draw = (ctx) => {
|
|
1471
1283
|
ctx.beginPath();
|
|
1472
1284
|
ctx.moveTo(0, 0);
|
|
1473
|
-
ctx.arc(0, 0, radius, 0,
|
|
1285
|
+
ctx.arc(0, 0, radius, 0, -angle * Math.PI / 180, true);
|
|
1474
1286
|
ctx.closePath();
|
|
1475
1287
|
ctx.fillStyle = fill;
|
|
1476
1288
|
ctx.fill();
|
|
@@ -1481,14 +1293,17 @@ hyperbook.python = (function () {
|
|
|
1481
1293
|
const angle = (Number(spec.angle) || 0) * (Math.PI / 180);
|
|
1482
1294
|
const p1 = { x: 0, y: 0 };
|
|
1483
1295
|
const p2 = { x: side1, y: 0 };
|
|
1484
|
-
const p3 = {
|
|
1296
|
+
const p3 = {
|
|
1297
|
+
x: side2 * Math.cos(angle),
|
|
1298
|
+
y: -side2 * Math.sin(angle)
|
|
1299
|
+
};
|
|
1485
1300
|
const centroid = {
|
|
1486
1301
|
x: (p1.x + p2.x + p3.x) / 3,
|
|
1487
|
-
y: (p1.y + p2.y + p3.y) / 3
|
|
1302
|
+
y: (p1.y + p2.y + p3.y) / 3
|
|
1488
1303
|
};
|
|
1489
1304
|
const points = [p1, p2, p3].map((p) => ({
|
|
1490
1305
|
x: p.x - centroid.x,
|
|
1491
|
-
y: p.y - centroid.y
|
|
1306
|
+
y: p.y - centroid.y
|
|
1492
1307
|
}));
|
|
1493
1308
|
const xs = points.map((p) => p.x);
|
|
1494
1309
|
const ys = points.map((p) => p.y);
|
|
@@ -1525,11 +1340,9 @@ hyperbook.python = (function () {
|
|
|
1525
1340
|
ctx.fillText(text, -width / 2, -centerY);
|
|
1526
1341
|
};
|
|
1527
1342
|
}
|
|
1528
|
-
|
|
1529
1343
|
stack.push({ width, height, pin, draw });
|
|
1530
1344
|
continue;
|
|
1531
1345
|
}
|
|
1532
|
-
|
|
1533
1346
|
if (type === "Pin") {
|
|
1534
1347
|
const child = stack.pop();
|
|
1535
1348
|
if (!child) continue;
|
|
@@ -1537,17 +1350,16 @@ hyperbook.python = (function () {
|
|
|
1537
1350
|
stack.push({ ...child, pin });
|
|
1538
1351
|
continue;
|
|
1539
1352
|
}
|
|
1540
|
-
|
|
1541
1353
|
if (type === "Rotate") {
|
|
1542
1354
|
const child = stack.pop();
|
|
1543
1355
|
if (!child) continue;
|
|
1544
1356
|
const angleDeg = Number(spec.angle) || 0;
|
|
1545
|
-
const angleRad =
|
|
1357
|
+
const angleRad = -angleDeg * Math.PI / 180;
|
|
1546
1358
|
const corners = [
|
|
1547
1359
|
{ x: -child.width / 2, y: -child.height / 2 },
|
|
1548
1360
|
{ x: child.width / 2, y: -child.height / 2 },
|
|
1549
1361
|
{ x: child.width / 2, y: child.height / 2 },
|
|
1550
|
-
{ x: -child.width / 2, y: child.height / 2 }
|
|
1362
|
+
{ x: -child.width / 2, y: child.height / 2 }
|
|
1551
1363
|
].map((p) => rotatePoint(p, child.pin, angleRad));
|
|
1552
1364
|
const minX = Math.min(...corners.map((p) => p.x));
|
|
1553
1365
|
const maxX = Math.max(...corners.map((p) => p.x));
|
|
@@ -1556,7 +1368,6 @@ hyperbook.python = (function () {
|
|
|
1556
1368
|
const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
|
|
1557
1369
|
const offset = { x: -center.x, y: -center.y };
|
|
1558
1370
|
const pin = { x: child.pin.x + offset.x, y: child.pin.y + offset.y };
|
|
1559
|
-
|
|
1560
1371
|
stack.push({
|
|
1561
1372
|
width: maxX - minX,
|
|
1562
1373
|
height: maxY - minY,
|
|
@@ -1569,54 +1380,44 @@ hyperbook.python = (function () {
|
|
|
1569
1380
|
ctx.translate(-child.pin.x, -child.pin.y);
|
|
1570
1381
|
child.draw(ctx);
|
|
1571
1382
|
ctx.restore();
|
|
1572
|
-
}
|
|
1383
|
+
}
|
|
1573
1384
|
});
|
|
1574
1385
|
continue;
|
|
1575
1386
|
}
|
|
1576
|
-
|
|
1577
1387
|
if (type === "Compose") {
|
|
1578
1388
|
const bg = stack.pop();
|
|
1579
1389
|
const fg = stack.pop();
|
|
1580
1390
|
if (!fg || !bg) continue;
|
|
1581
|
-
const fgPin = spec.fg_pin
|
|
1582
|
-
|
|
1583
|
-
: fg.pin;
|
|
1584
|
-
const bgPin = spec.bg_pin
|
|
1585
|
-
? decodePoint(spec.bg_pin, bg.width, bg.height)
|
|
1586
|
-
: bg.pin;
|
|
1587
|
-
|
|
1391
|
+
const fgPin = spec.fg_pin ? decodePoint(spec.fg_pin, fg.width, fg.height) : fg.pin;
|
|
1392
|
+
const bgPin = spec.bg_pin ? decodePoint(spec.bg_pin, bg.width, bg.height) : bg.pin;
|
|
1588
1393
|
const bgCenter = { x: 0, y: 0 };
|
|
1589
1394
|
const fgCenter = { x: bgPin.x - fgPin.x, y: bgPin.y - fgPin.y };
|
|
1590
1395
|
const minX = Math.min(
|
|
1591
1396
|
fgCenter.x - fg.width / 2,
|
|
1592
|
-
bgCenter.x - bg.width / 2
|
|
1397
|
+
bgCenter.x - bg.width / 2
|
|
1593
1398
|
);
|
|
1594
1399
|
const maxX = Math.max(
|
|
1595
1400
|
fgCenter.x + fg.width / 2,
|
|
1596
|
-
bgCenter.x + bg.width / 2
|
|
1401
|
+
bgCenter.x + bg.width / 2
|
|
1597
1402
|
);
|
|
1598
1403
|
const minY = Math.min(
|
|
1599
1404
|
fgCenter.y - fg.height / 2,
|
|
1600
|
-
bgCenter.y - bg.height / 2
|
|
1405
|
+
bgCenter.y - bg.height / 2
|
|
1601
1406
|
);
|
|
1602
1407
|
const maxY = Math.max(
|
|
1603
1408
|
fgCenter.y + fg.height / 2,
|
|
1604
|
-
bgCenter.y + bg.height / 2
|
|
1409
|
+
bgCenter.y + bg.height / 2
|
|
1605
1410
|
);
|
|
1606
|
-
|
|
1607
1411
|
const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
|
|
1608
1412
|
const fgOffset = {
|
|
1609
1413
|
x: fgCenter.x - center.x,
|
|
1610
|
-
y: fgCenter.y - center.y
|
|
1414
|
+
y: fgCenter.y - center.y
|
|
1611
1415
|
};
|
|
1612
1416
|
const bgOffset = {
|
|
1613
1417
|
x: bgCenter.x - center.x,
|
|
1614
|
-
y: bgCenter.y - center.y
|
|
1418
|
+
y: bgCenter.y - center.y
|
|
1615
1419
|
};
|
|
1616
|
-
const pin = spec.pin
|
|
1617
|
-
? decodePoint(spec.pin, maxX - minX, maxY - minY)
|
|
1618
|
-
: { x: bgPin.x - center.x, y: bgPin.y - center.y };
|
|
1619
|
-
|
|
1420
|
+
const pin = spec.pin ? decodePoint(spec.pin, maxX - minX, maxY - minY) : { x: bgPin.x - center.x, y: bgPin.y - center.y };
|
|
1620
1421
|
stack.push({
|
|
1621
1422
|
width: maxX - minX,
|
|
1622
1423
|
height: maxY - minY,
|
|
@@ -1630,44 +1431,28 @@ hyperbook.python = (function () {
|
|
|
1630
1431
|
ctx.translate(fgOffset.x, fgOffset.y);
|
|
1631
1432
|
fg.draw(ctx);
|
|
1632
1433
|
ctx.restore();
|
|
1633
|
-
}
|
|
1434
|
+
}
|
|
1634
1435
|
});
|
|
1635
1436
|
}
|
|
1636
1437
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
pin: { x: 0, y: 0 },
|
|
1643
|
-
draw: () => {},
|
|
1438
|
+
return stack[stack.length - 1] || {
|
|
1439
|
+
width: 0,
|
|
1440
|
+
height: 0,
|
|
1441
|
+
pin: { x: 0, y: 0 },
|
|
1442
|
+
draw: () => {
|
|
1644
1443
|
}
|
|
1645
|
-
|
|
1444
|
+
};
|
|
1646
1445
|
};
|
|
1647
|
-
|
|
1648
|
-
// SVG-based renderer — mirrors buildGraphic but produces SVG markup.
|
|
1649
|
-
// Each stack node has { width, height, pin, svg } where `svg` is a string
|
|
1650
|
-
// drawn with the coordinate origin at the graphic's centre (y-down, same as canvas).
|
|
1651
1446
|
const buildSvgElements = (specs) => {
|
|
1652
1447
|
const stack = [];
|
|
1653
1448
|
const measureCanvas = document.createElement("canvas");
|
|
1654
1449
|
const measureCtx = measureCanvas.getContext("2d");
|
|
1655
|
-
|
|
1656
1450
|
for (const spec of specs || []) {
|
|
1657
1451
|
if (!spec || typeof spec !== "object") continue;
|
|
1658
1452
|
const type = spec.t;
|
|
1659
|
-
|
|
1660
|
-
if (
|
|
1661
|
-
type === "Empty" ||
|
|
1662
|
-
type === "Rectangle" ||
|
|
1663
|
-
type === "Ellipse" ||
|
|
1664
|
-
type === "CircularSector" ||
|
|
1665
|
-
type === "Triangle" ||
|
|
1666
|
-
type === "Text"
|
|
1667
|
-
) {
|
|
1453
|
+
if (type === "Empty" || type === "Rectangle" || type === "Ellipse" || type === "CircularSector" || type === "Triangle" || type === "Text") {
|
|
1668
1454
|
let width = 0, height = 0, pin = { x: 0, y: 0 };
|
|
1669
1455
|
let svg = "";
|
|
1670
|
-
|
|
1671
1456
|
if (type === "Rectangle") {
|
|
1672
1457
|
width = Math.max(0, Number(spec.width) || 0);
|
|
1673
1458
|
height = Math.max(0, Number(spec.height) || 0);
|
|
@@ -1684,8 +1469,6 @@ hyperbook.python = (function () {
|
|
|
1684
1469
|
width = radius * 2;
|
|
1685
1470
|
height = radius * 2;
|
|
1686
1471
|
const fill = colorToCss(spec.color);
|
|
1687
|
-
// canvas: ctx.arc(0, 0, r, 0, -angle*PI/180, counterclockwise=true)
|
|
1688
|
-
// SVG arc from (r,0) counterclockwise to the same end-point (sweep=0)
|
|
1689
1472
|
const endAngleRad = -angle * Math.PI / 180;
|
|
1690
1473
|
const endX = radius * Math.cos(endAngleRad);
|
|
1691
1474
|
const endY = radius * Math.sin(endAngleRad);
|
|
@@ -1697,9 +1480,18 @@ hyperbook.python = (function () {
|
|
|
1697
1480
|
const angle = (Number(spec.angle) || 0) * (Math.PI / 180);
|
|
1698
1481
|
const p1 = { x: 0, y: 0 };
|
|
1699
1482
|
const p2 = { x: side1, y: 0 };
|
|
1700
|
-
const p3 = {
|
|
1701
|
-
|
|
1702
|
-
|
|
1483
|
+
const p3 = {
|
|
1484
|
+
x: side2 * Math.cos(angle),
|
|
1485
|
+
y: -side2 * Math.sin(angle)
|
|
1486
|
+
};
|
|
1487
|
+
const centroid = {
|
|
1488
|
+
x: (p1.x + p2.x + p3.x) / 3,
|
|
1489
|
+
y: (p1.y + p2.y + p3.y) / 3
|
|
1490
|
+
};
|
|
1491
|
+
const points = [p1, p2, p3].map((p) => ({
|
|
1492
|
+
x: p.x - centroid.x,
|
|
1493
|
+
y: p.y - centroid.y
|
|
1494
|
+
}));
|
|
1703
1495
|
const xs = points.map((p) => p.x);
|
|
1704
1496
|
const ys = points.map((p) => p.y);
|
|
1705
1497
|
width = Math.max(...xs) - Math.min(...xs);
|
|
@@ -1718,14 +1510,11 @@ hyperbook.python = (function () {
|
|
|
1718
1510
|
const descent = metrics.actualBoundingBoxDescent || textSize * 0.2;
|
|
1719
1511
|
height = ascent + descent;
|
|
1720
1512
|
pin = { x: -width / 2, y: (ascent - descent) / 2 };
|
|
1721
|
-
// y attribute is the text baseline; mirrors canvas fillText(-w/2, (ascent-descent)/2)
|
|
1722
1513
|
svg = `<text x="${-width / 2}" y="${(ascent - descent) / 2}" font-family="${fontName}" font-size="${textSize}" fill="${fill}" text-anchor="start">${text}</text>`;
|
|
1723
1514
|
}
|
|
1724
|
-
|
|
1725
1515
|
stack.push({ width, height, pin, svg });
|
|
1726
1516
|
continue;
|
|
1727
1517
|
}
|
|
1728
|
-
|
|
1729
1518
|
if (type === "Pin") {
|
|
1730
1519
|
const child = stack.pop();
|
|
1731
1520
|
if (!child) continue;
|
|
@@ -1733,17 +1522,16 @@ hyperbook.python = (function () {
|
|
|
1733
1522
|
stack.push({ ...child, pin });
|
|
1734
1523
|
continue;
|
|
1735
1524
|
}
|
|
1736
|
-
|
|
1737
1525
|
if (type === "Rotate") {
|
|
1738
1526
|
const child = stack.pop();
|
|
1739
1527
|
if (!child) continue;
|
|
1740
1528
|
const angleDeg = Number(spec.angle) || 0;
|
|
1741
|
-
const angleRad =
|
|
1529
|
+
const angleRad = -angleDeg * Math.PI / 180;
|
|
1742
1530
|
const corners = [
|
|
1743
1531
|
{ x: -child.width / 2, y: -child.height / 2 },
|
|
1744
1532
|
{ x: child.width / 2, y: -child.height / 2 },
|
|
1745
1533
|
{ x: child.width / 2, y: child.height / 2 },
|
|
1746
|
-
{ x: -child.width / 2, y: child.height / 2 }
|
|
1534
|
+
{ x: -child.width / 2, y: child.height / 2 }
|
|
1747
1535
|
].map((p) => rotatePoint(p, child.pin, angleRad));
|
|
1748
1536
|
const minX = Math.min(...corners.map((p) => p.x));
|
|
1749
1537
|
const maxX = Math.max(...corners.map((p) => p.x));
|
|
@@ -1752,18 +1540,15 @@ hyperbook.python = (function () {
|
|
|
1752
1540
|
const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
|
|
1753
1541
|
const offset = { x: -center.x, y: -center.y };
|
|
1754
1542
|
const pin = { x: child.pin.x + offset.x, y: child.pin.y + offset.y };
|
|
1755
|
-
// SVG transform mirrors the canvas sequence: translate(offset) rotate(-angleDeg, pin)
|
|
1756
|
-
// SVG and canvas share the same y-down convention so signs are identical.
|
|
1757
1543
|
const transform = `translate(${offset.x},${offset.y}) rotate(${-angleDeg},${child.pin.x},${child.pin.y})`;
|
|
1758
1544
|
stack.push({
|
|
1759
1545
|
width: maxX - minX,
|
|
1760
1546
|
height: maxY - minY,
|
|
1761
1547
|
pin,
|
|
1762
|
-
svg: `<g transform="${transform}">${child.svg}</g
|
|
1548
|
+
svg: `<g transform="${transform}">${child.svg}</g>`
|
|
1763
1549
|
});
|
|
1764
1550
|
continue;
|
|
1765
1551
|
}
|
|
1766
|
-
|
|
1767
1552
|
if (type === "Compose") {
|
|
1768
1553
|
const bg = stack.pop();
|
|
1769
1554
|
const fg = stack.pop();
|
|
@@ -1772,30 +1557,42 @@ hyperbook.python = (function () {
|
|
|
1772
1557
|
const bgPin = spec.bg_pin ? decodePoint(spec.bg_pin, bg.width, bg.height) : bg.pin;
|
|
1773
1558
|
const bgCenter = { x: 0, y: 0 };
|
|
1774
1559
|
const fgCenter = { x: bgPin.x - fgPin.x, y: bgPin.y - fgPin.y };
|
|
1775
|
-
const minX = Math.min(
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1560
|
+
const minX = Math.min(
|
|
1561
|
+
fgCenter.x - fg.width / 2,
|
|
1562
|
+
bgCenter.x - bg.width / 2
|
|
1563
|
+
);
|
|
1564
|
+
const maxX = Math.max(
|
|
1565
|
+
fgCenter.x + fg.width / 2,
|
|
1566
|
+
bgCenter.x + bg.width / 2
|
|
1567
|
+
);
|
|
1568
|
+
const minY = Math.min(
|
|
1569
|
+
fgCenter.y - fg.height / 2,
|
|
1570
|
+
bgCenter.y - bg.height / 2
|
|
1571
|
+
);
|
|
1572
|
+
const maxY = Math.max(
|
|
1573
|
+
fgCenter.y + fg.height / 2,
|
|
1574
|
+
bgCenter.y + bg.height / 2
|
|
1575
|
+
);
|
|
1779
1576
|
const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
|
|
1780
1577
|
const fgOffset = { x: fgCenter.x - center.x, y: fgCenter.y - center.y };
|
|
1781
1578
|
const bgOffset = { x: bgCenter.x - center.x, y: bgCenter.y - center.y };
|
|
1782
|
-
const pin = spec.pin
|
|
1783
|
-
? decodePoint(spec.pin, maxX - minX, maxY - minY)
|
|
1784
|
-
: { x: bgPin.x - center.x, y: bgPin.y - center.y };
|
|
1785
|
-
// bg drawn first (behind), fg on top — same draw order as canvas
|
|
1579
|
+
const pin = spec.pin ? decodePoint(spec.pin, maxX - minX, maxY - minY) : { x: bgPin.x - center.x, y: bgPin.y - center.y };
|
|
1786
1580
|
stack.push({
|
|
1787
1581
|
width: maxX - minX,
|
|
1788
1582
|
height: maxY - minY,
|
|
1789
1583
|
pin,
|
|
1790
|
-
svg: `<g transform="translate(${bgOffset.x},${bgOffset.y})">${bg.svg}</g><g transform="translate(${fgOffset.x},${fgOffset.y})">${fg.svg}</g
|
|
1584
|
+
svg: `<g transform="translate(${bgOffset.x},${bgOffset.y})">${bg.svg}</g><g transform="translate(${fgOffset.x},${fgOffset.y})">${fg.svg}</g>`
|
|
1791
1585
|
});
|
|
1792
1586
|
}
|
|
1793
1587
|
}
|
|
1794
|
-
|
|
1795
|
-
|
|
1588
|
+
return stack[stack.length - 1] || {
|
|
1589
|
+
width: 0,
|
|
1590
|
+
height: 0,
|
|
1591
|
+
pin: { x: 0, y: 0 },
|
|
1592
|
+
svg: ""
|
|
1593
|
+
};
|
|
1796
1594
|
};
|
|
1797
|
-
|
|
1798
|
-
return {
|
|
1595
|
+
return {
|
|
1799
1596
|
js_graphic_size: (specs) => {
|
|
1800
1597
|
try {
|
|
1801
1598
|
const unproxiedSpecs = unProxy(specs);
|
|
@@ -1820,7 +1617,6 @@ hyperbook.python = (function () {
|
|
|
1820
1617
|
ctx.scale(scale, scale);
|
|
1821
1618
|
ctx.translate(width / 2, height / 2);
|
|
1822
1619
|
graphic.draw(ctx);
|
|
1823
|
-
|
|
1824
1620
|
if (debug) {
|
|
1825
1621
|
ctx.strokeStyle = "red";
|
|
1826
1622
|
ctx.lineWidth = 1 / scale;
|
|
@@ -1833,7 +1629,6 @@ hyperbook.python = (function () {
|
|
|
1833
1629
|
ctx.lineTo(graphic.pin.x, graphic.pin.y + 8);
|
|
1834
1630
|
ctx.stroke();
|
|
1835
1631
|
}
|
|
1836
|
-
|
|
1837
1632
|
return canvas.toDataURL("image/png");
|
|
1838
1633
|
} catch (e) {
|
|
1839
1634
|
console.error("js_render_graphic error:", e);
|
|
@@ -1863,50 +1658,88 @@ hyperbook.python = (function () {
|
|
|
1863
1658
|
link.href = String(content || "");
|
|
1864
1659
|
link.download = String(filename || "graphic.png");
|
|
1865
1660
|
link.click();
|
|
1866
|
-
}
|
|
1661
|
+
}
|
|
1867
1662
|
};
|
|
1868
1663
|
};
|
|
1869
1664
|
|
|
1870
|
-
//
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
/(?:^|\W)(?:import\s+pygame\b|from\s+pygame\b\s+import\b)/m.test(text);
|
|
1883
|
-
const hasSasPygame =
|
|
1884
|
-
/(?:^|\W)(?:import\s+sas_pygame\b|from\s+sas_pygame\b\s+import\b)/m.test(text);
|
|
1885
|
-
const hasAnyWhile = /^\s*while\s+.+:\s*$/m.test(text);
|
|
1886
|
-
if (!hasAnyWhile) return false;
|
|
1887
|
-
return (hasPygame || hasSasPygame) && hasAnyWhile;
|
|
1888
|
-
};
|
|
1889
|
-
|
|
1890
|
-
const indentBlock = (source, spaces) => {
|
|
1891
|
-
const pad = " ".repeat(spaces);
|
|
1892
|
-
return String(source || "")
|
|
1893
|
-
.split(/\r\n|\r|\n/)
|
|
1894
|
-
.map((line) => (line.length ? pad + line : line))
|
|
1895
|
-
.join("\n");
|
|
1665
|
+
// assets/directive-pyide/src/pyodide.js
|
|
1666
|
+
var loadPyodideScript = () => {
|
|
1667
|
+
if (window.loadPyodide) {
|
|
1668
|
+
return Promise.resolve();
|
|
1669
|
+
}
|
|
1670
|
+
return new Promise((resolve, reject) => {
|
|
1671
|
+
const script = document.createElement("script");
|
|
1672
|
+
script.src = PYODIDE_CDN;
|
|
1673
|
+
script.onload = () => resolve();
|
|
1674
|
+
script.onerror = () => reject(new Error("Failed to load Pyodide"));
|
|
1675
|
+
document.head.appendChild(script);
|
|
1676
|
+
});
|
|
1896
1677
|
};
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
const
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1678
|
+
var pyodideReadyPromise = (async () => {
|
|
1679
|
+
await loadPyodideScript();
|
|
1680
|
+
return window.loadPyodide;
|
|
1681
|
+
})();
|
|
1682
|
+
var getRuntime = async (id) => {
|
|
1683
|
+
if (runtimes.has(id)) {
|
|
1684
|
+
return runtimes.get(id);
|
|
1685
|
+
}
|
|
1686
|
+
const loadPyodide = await pyodideReadyPromise;
|
|
1687
|
+
const pyodide = await loadPyodide();
|
|
1688
|
+
if (typeof pyodide.registerJsModule === "function") {
|
|
1689
|
+
const turtleModule = createTurtleJsFFI(id);
|
|
1690
|
+
turtleModule.__setPyodide(pyodide);
|
|
1691
|
+
pyodide.registerJsModule("turtle", turtleModule);
|
|
1692
|
+
pyodide.registerJsModule("jturtle", turtleModule);
|
|
1693
|
+
pyodide.registerJsModule("pytamaro_js_ffi", createPytamaroJsFFI());
|
|
1694
|
+
turtleModules.set(id, turtleModule);
|
|
1695
|
+
}
|
|
1696
|
+
if (typeof SharedArrayBuffer !== "undefined" && window.crossOriginIsolated && typeof pyodide.setInterruptBuffer === "function") {
|
|
1697
|
+
const interruptBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
1698
|
+
pyodide.setInterruptBuffer(interruptBuffer);
|
|
1699
|
+
interruptBuffers.set(id, interruptBuffer);
|
|
1700
|
+
}
|
|
1701
|
+
runtimes.set(id, pyodide);
|
|
1702
|
+
return pyodide;
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1705
|
+
// assets/directive-pyide/src/execution.js
|
|
1706
|
+
var scriptLooksLikeTurtle = (script) => {
|
|
1707
|
+
return /\bfrom\s+turtle\s+import\b|\bimport\s+turtle\b/.test(
|
|
1708
|
+
String(script || "")
|
|
1709
|
+
);
|
|
1710
|
+
};
|
|
1711
|
+
var resetCanvas = (canvas) => {
|
|
1712
|
+
if (!canvas) return;
|
|
1713
|
+
const context = canvas.getContext("2d");
|
|
1714
|
+
context?.clearRect(0, 0, canvas.width, canvas.height);
|
|
1715
|
+
};
|
|
1716
|
+
var hasExplicitMain = (code) => {
|
|
1717
|
+
const text = String(code || "");
|
|
1718
|
+
return /^(\s*)(?:async\s+def|def)\s+main\s*\(/m.test(text) || /__name__\s*==\s*["']__main__["']/.test(text);
|
|
1719
|
+
};
|
|
1720
|
+
var looksLikeTopLevelGameLoop = (code) => {
|
|
1721
|
+
const text = String(code || "");
|
|
1722
|
+
const hasPygame = /(?:^|\W)(?:import\s+pygame\b|from\s+pygame\b\s+import\b)/m.test(text);
|
|
1723
|
+
const hasSasPygame = /(?:^|\W)(?:import\s+sas_pygame\b|from\s+sas_pygame\b\s+import\b)/m.test(
|
|
1724
|
+
text
|
|
1725
|
+
);
|
|
1726
|
+
const hasAnyWhile = /^\s*while\s+.+:\s*$/m.test(text);
|
|
1727
|
+
if (!hasAnyWhile) return false;
|
|
1728
|
+
return (hasPygame || hasSasPygame) && hasAnyWhile;
|
|
1729
|
+
};
|
|
1730
|
+
var indentBlock = (source, spaces) => {
|
|
1731
|
+
const pad = " ".repeat(spaces);
|
|
1732
|
+
return String(source || "").split(/\r\n|\r|\n/).map((line) => line.length ? pad + line : line).join("\n");
|
|
1733
|
+
};
|
|
1734
|
+
var wrapTopLevelIntoAsyncMain = (userCode) => {
|
|
1735
|
+
const code = String(userCode || "").replace(/\r\n/g, "\n");
|
|
1736
|
+
if (hasExplicitMain(code)) return code;
|
|
1737
|
+
const lines = code.split("\n");
|
|
1738
|
+
const keep = [];
|
|
1739
|
+
const body = [];
|
|
1740
|
+
let index = 0;
|
|
1741
|
+
while (index < lines.length) {
|
|
1742
|
+
const line = lines[index];
|
|
1910
1743
|
if (/^\s*$/.test(line)) {
|
|
1911
1744
|
keep.push(line);
|
|
1912
1745
|
index += 1;
|
|
@@ -1922,7 +1755,6 @@ hyperbook.python = (function () {
|
|
|
1922
1755
|
index += 1;
|
|
1923
1756
|
continue;
|
|
1924
1757
|
}
|
|
1925
|
-
|
|
1926
1758
|
if (/^\s*(?:def|async\s+def|class)\s+/.test(line)) {
|
|
1927
1759
|
keep.push(line);
|
|
1928
1760
|
index += 1;
|
|
@@ -1933,12 +1765,7 @@ hyperbook.python = (function () {
|
|
|
1933
1765
|
index += 1;
|
|
1934
1766
|
continue;
|
|
1935
1767
|
}
|
|
1936
|
-
if (
|
|
1937
|
-
/^\S/.test(nextLine) &&
|
|
1938
|
-
!/^\s*#/.test(nextLine) &&
|
|
1939
|
-
!/^\s*(?:from\s+\S+\s+import\b|import\s+\S+)/.test(nextLine) &&
|
|
1940
|
-
!/^\s*(?:def|async\s+def|class)\s+/.test(nextLine)
|
|
1941
|
-
) {
|
|
1768
|
+
if (/^\S/.test(nextLine) && !/^\s*#/.test(nextLine) && !/^\s*(?:from\s+\S+\s+import\b|import\s+\S+)/.test(nextLine) && !/^\s*(?:def|async\s+def|class)\s+/.test(nextLine)) {
|
|
1942
1769
|
break;
|
|
1943
1770
|
}
|
|
1944
1771
|
keep.push(nextLine);
|
|
@@ -1948,52 +1775,44 @@ hyperbook.python = (function () {
|
|
|
1948
1775
|
}
|
|
1949
1776
|
break;
|
|
1950
1777
|
}
|
|
1951
|
-
|
|
1952
1778
|
for (; index < lines.length; index += 1) {
|
|
1953
1779
|
body.push(lines[index]);
|
|
1954
1780
|
}
|
|
1955
|
-
|
|
1956
1781
|
let bodyText = body.join("\n");
|
|
1957
1782
|
bodyText = bodyText.replace(/^([ \t]*\n)+/, "");
|
|
1958
1783
|
bodyText = bodyText.replace(/(\n[ \t]*)+$/, "");
|
|
1959
1784
|
bodyText = bodyText.replace(/\t/g, " ");
|
|
1960
1785
|
if (!bodyText.replace(/[\s\n]+/g, "").length) return code;
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
.
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
)
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
"$1$2$3\n$1await asyncio.sleep(0)",
|
|
1974
|
-
);
|
|
1975
|
-
|
|
1786
|
+
const injected = bodyText.replace(
|
|
1787
|
+
/^(\s*)(\w+\s*\.\s*tick\s*\([^\)]*\)\s*)$/gm,
|
|
1788
|
+
"$1$2\n$1await asyncio.sleep(0)"
|
|
1789
|
+
).replace(
|
|
1790
|
+
/^(\s*)(pygame\s*\.\s*display\s*\.\s*flip\s*\(\s*\)\s*)$/gm,
|
|
1791
|
+
"$1$2\n$1await asyncio.sleep(0)"
|
|
1792
|
+
).replace(
|
|
1793
|
+
/^([\t ]*)([A-Za-z_][\w]*(?:\s*\.\s*[A-Za-z_][\w]*)*\s*\.\s*step\s*\([^\)]*\)\s*)(#.*)?$/gm,
|
|
1794
|
+
"$1$2$3\n$1await asyncio.sleep(0)"
|
|
1795
|
+
);
|
|
1796
|
+
const hasAsyncio = /(?:^|\W)import\s+asyncio\b/m.test(code);
|
|
1797
|
+
const hasPygameImport = /(?:^|\W)(?:import\s+pygame\b|from\s+pygame\b\s+import\b)/m.test(code);
|
|
1976
1798
|
const prelude = [
|
|
1977
1799
|
"# --- auto-wrapped by IDE for browser pygame compatibility ---",
|
|
1978
|
-
"import asyncio",
|
|
1979
|
-
"import pygame",
|
|
1800
|
+
...hasAsyncio ? [] : ["import asyncio"],
|
|
1801
|
+
...hasPygameImport ? [] : ["import pygame"],
|
|
1980
1802
|
"",
|
|
1981
1803
|
"async def main():",
|
|
1982
1804
|
indentBlock(injected, 4),
|
|
1983
1805
|
"",
|
|
1984
1806
|
"await main()",
|
|
1985
1807
|
"# --- end auto-wrapped ---",
|
|
1986
|
-
""
|
|
1808
|
+
""
|
|
1987
1809
|
].join("\n");
|
|
1988
|
-
|
|
1989
1810
|
let keepText = keep.join("\n");
|
|
1990
1811
|
if (keepText && !keepText.endsWith("\n")) keepText += "\n";
|
|
1991
1812
|
if (keepText && !/\n\s*\n$/.test(keepText)) keepText += "\n";
|
|
1992
|
-
|
|
1993
1813
|
return keepText + prelude;
|
|
1994
1814
|
};
|
|
1995
|
-
|
|
1996
|
-
const maybeAutoWrapPygame = (code) => {
|
|
1815
|
+
var maybeAutoWrapPygame = (code) => {
|
|
1997
1816
|
try {
|
|
1998
1817
|
const text = String(code || "");
|
|
1999
1818
|
if (!looksLikeTopLevelGameLoop(text)) return text;
|
|
@@ -2002,16 +1821,14 @@ hyperbook.python = (function () {
|
|
|
2002
1821
|
return String(code || "");
|
|
2003
1822
|
}
|
|
2004
1823
|
};
|
|
2005
|
-
|
|
2006
|
-
const ensureMicropipPackages = async (id, pyodide, packages = []) => {
|
|
1824
|
+
var ensureMicropipPackages = async (id, pyodide, packages = []) => {
|
|
2007
1825
|
if (packages.length === 0) return;
|
|
2008
1826
|
if (!installedMicropipPackages.has(id)) {
|
|
2009
|
-
installedMicropipPackages.set(id, new Set());
|
|
1827
|
+
installedMicropipPackages.set(id, /* @__PURE__ */ new Set());
|
|
2010
1828
|
}
|
|
2011
1829
|
const installed = installedMicropipPackages.get(id);
|
|
2012
1830
|
const toInstall = packages.filter((pkg) => !installed.has(pkg));
|
|
2013
1831
|
if (toInstall.length === 0) return;
|
|
2014
|
-
|
|
2015
1832
|
await pyodide.loadPackage("micropip");
|
|
2016
1833
|
const micropip = pyodide.pyimport("micropip");
|
|
2017
1834
|
try {
|
|
@@ -2023,15 +1840,13 @@ hyperbook.python = (function () {
|
|
|
2023
1840
|
micropip?.destroy?.();
|
|
2024
1841
|
}
|
|
2025
1842
|
};
|
|
2026
|
-
|
|
2027
|
-
const executeScript = async (id, script, context = {}, packages = []) => {
|
|
1843
|
+
var executeScript = async (id, script, context = {}, packages = []) => {
|
|
2028
1844
|
const filename = "<exec>";
|
|
2029
1845
|
try {
|
|
2030
1846
|
const pyodide = await getRuntime(id);
|
|
2031
1847
|
const { canvas, ...globalsContext } = context;
|
|
2032
1848
|
const decoder = new TextDecoder("utf-8");
|
|
2033
1849
|
const executableScript = maybeAutoWrapPygame(script);
|
|
2034
|
-
|
|
2035
1850
|
if (canvas) {
|
|
2036
1851
|
try {
|
|
2037
1852
|
resetCanvas(canvas);
|
|
@@ -2049,7 +1864,6 @@ hyperbook.python = (function () {
|
|
|
2049
1864
|
appendOutputErrorLine(id, `Canvas setup failed: ${error.message}`);
|
|
2050
1865
|
}
|
|
2051
1866
|
}
|
|
2052
|
-
|
|
2053
1867
|
let lastStdinPrompt = "";
|
|
2054
1868
|
pyodide.setStdout({
|
|
2055
1869
|
write: (msg) => {
|
|
@@ -2061,28 +1875,26 @@ hyperbook.python = (function () {
|
|
|
2061
1875
|
}
|
|
2062
1876
|
appendOutputLine(id, text);
|
|
2063
1877
|
return msg?.length ?? text.length;
|
|
2064
|
-
}
|
|
1878
|
+
}
|
|
2065
1879
|
});
|
|
2066
1880
|
pyodide.setStdin({
|
|
2067
1881
|
stdin: () => {
|
|
2068
|
-
const promptText =
|
|
2069
|
-
lastStdinPrompt || hyperbook.i18n.get("pyide-input-prompt");
|
|
1882
|
+
const promptText = lastStdinPrompt || hyperbook.i18n.get("pyide-input-prompt");
|
|
2070
1883
|
lastStdinPrompt = "";
|
|
2071
1884
|
const value = window.prompt(promptText);
|
|
2072
1885
|
if (value === null) {
|
|
2073
1886
|
return "";
|
|
2074
1887
|
}
|
|
2075
1888
|
return value;
|
|
2076
|
-
}
|
|
1889
|
+
}
|
|
2077
1890
|
});
|
|
2078
1891
|
pyodide.setStderr({
|
|
2079
1892
|
write: (msg) => {
|
|
2080
1893
|
const text = typeof msg === "string" ? msg : decoder.decode(msg);
|
|
2081
1894
|
appendOutputErrorLine(id, text);
|
|
2082
1895
|
return msg?.length ?? text.length;
|
|
2083
|
-
}
|
|
1896
|
+
}
|
|
2084
1897
|
});
|
|
2085
|
-
|
|
2086
1898
|
await ensureMicropipPackages(id, pyodide, packages);
|
|
2087
1899
|
await pyodide.loadPackagesFromImports(executableScript);
|
|
2088
1900
|
const dict = pyodide.globals.get("dict");
|
|
@@ -2094,7 +1906,7 @@ hyperbook.python = (function () {
|
|
|
2094
1906
|
const results = await pyodide.runPythonAsync(executableScript, {
|
|
2095
1907
|
globals,
|
|
2096
1908
|
locals: globals,
|
|
2097
|
-
filename
|
|
1909
|
+
filename
|
|
2098
1910
|
});
|
|
2099
1911
|
return { results };
|
|
2100
1912
|
} finally {
|
|
@@ -2110,7 +1922,7 @@ if _pg:
|
|
|
2110
1922
|
_pg.quit()
|
|
2111
1923
|
except Exception:
|
|
2112
1924
|
pass`,
|
|
2113
|
-
{ filename: "<cleanup>" }
|
|
1925
|
+
{ filename: "<cleanup>" }
|
|
2114
1926
|
);
|
|
2115
1927
|
} catch (e) {
|
|
2116
1928
|
console.warn("pygame cleanup failed:", e);
|
|
@@ -2122,41 +1934,44 @@ if _pg:
|
|
|
2122
1934
|
if (message.startsWith("Traceback")) {
|
|
2123
1935
|
const lines = message?.split("\n") || [];
|
|
2124
1936
|
const i = lines.findIndex((line) => line.includes(filename));
|
|
2125
|
-
message = lines[0] + "\n" + lines.slice(i).join("\n");
|
|
1937
|
+
message = i >= 0 ? lines[0] + "\n" + lines.slice(i).join("\n") : message;
|
|
2126
1938
|
}
|
|
2127
1939
|
return { error: message };
|
|
2128
1940
|
}
|
|
2129
1941
|
};
|
|
2130
1942
|
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
1943
|
+
// assets/directive-pyide/src/ui.js
|
|
1944
|
+
var updateFullscreenButtonState = (elem, button) => {
|
|
1945
|
+
if (!elem || !button) return;
|
|
1946
|
+
const isFullscreen = document.fullscreenElement === elem;
|
|
1947
|
+
const label = hyperbook.i18n.get("ide-fullscreen-enter");
|
|
1948
|
+
button.textContent = "\u26F6";
|
|
1949
|
+
button.title = label;
|
|
1950
|
+
button.setAttribute("aria-label", label);
|
|
1951
|
+
button.classList.toggle("active", isFullscreen);
|
|
1952
|
+
};
|
|
1953
|
+
var toggleFullscreen = async (elem) => {
|
|
1954
|
+
if (!elem) return;
|
|
1955
|
+
if (document.fullscreenElement === elem) {
|
|
1956
|
+
await document.exitFullscreen();
|
|
1957
|
+
return;
|
|
2143
1958
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
1959
|
+
await elem.requestFullscreen();
|
|
1960
|
+
};
|
|
1961
|
+
var syncFullscreenButtons = () => {
|
|
1962
|
+
const elems = document.getElementsByClassName("directive-pyide");
|
|
1963
|
+
for (const elem of elems) {
|
|
1964
|
+
const fullscreen = elem.getElementsByClassName("fullscreen")[0];
|
|
1965
|
+
updateFullscreenButtonState(elem, fullscreen);
|
|
2150
1966
|
}
|
|
2151
1967
|
};
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
1968
|
+
var releaseKeyboardCapture = (id) => {
|
|
1969
|
+
const elem = document.getElementById(id);
|
|
1970
|
+
if (!elem) return;
|
|
1971
|
+
const canvas = elem.getElementsByClassName("canvas")[0];
|
|
1972
|
+
canvas?.blur?.();
|
|
2157
1973
|
};
|
|
2158
|
-
|
|
2159
|
-
const getRunningInstanceId = () => {
|
|
1974
|
+
var getRunningInstanceId = () => {
|
|
2160
1975
|
const elems = document.getElementsByClassName("directive-pyide");
|
|
2161
1976
|
for (const elem of elems) {
|
|
2162
1977
|
if (getExecutionState(elem.id).running) {
|
|
@@ -2165,8 +1980,7 @@ if _pg:
|
|
|
2165
1980
|
}
|
|
2166
1981
|
return null;
|
|
2167
1982
|
};
|
|
2168
|
-
|
|
2169
|
-
const updateRunning = () => {
|
|
1983
|
+
var updateRunning = () => {
|
|
2170
1984
|
const runningInstanceId = getRunningInstanceId();
|
|
2171
1985
|
const elems = document.getElementsByClassName("directive-pyide");
|
|
2172
1986
|
for (let elem of elems) {
|
|
@@ -2178,11 +1992,7 @@ if _pg:
|
|
|
2178
1992
|
const state = getExecutionState(elem.id);
|
|
2179
1993
|
const hasRuntime = runtimes.has(elem.id);
|
|
2180
1994
|
const hasInterrupt = interruptBuffers.has(elem.id);
|
|
2181
|
-
const lockedByOther =
|
|
2182
|
-
runningInstanceId !== null &&
|
|
2183
|
-
runningInstanceId !== elem.id &&
|
|
2184
|
-
!state.running;
|
|
2185
|
-
|
|
1995
|
+
const lockedByOther = runningInstanceId !== null && runningInstanceId !== elem.id && !state.running;
|
|
2186
1996
|
stop?.removeEventListener("click", handleStopClick);
|
|
2187
1997
|
run.classList.remove("stopping");
|
|
2188
1998
|
run.classList.remove("locked");
|
|
@@ -2190,7 +2000,6 @@ if _pg:
|
|
|
2190
2000
|
test?.classList.remove("locked");
|
|
2191
2001
|
stop?.classList.remove("stopping");
|
|
2192
2002
|
elem.classList.toggle("locked-by-other", lockedByOther);
|
|
2193
|
-
|
|
2194
2003
|
if (state.running || lockedByOther) {
|
|
2195
2004
|
editor?.classList.add("running");
|
|
2196
2005
|
editorCm?.setReadOnly(true);
|
|
@@ -2209,29 +2018,24 @@ if _pg:
|
|
|
2209
2018
|
run.classList.add("running");
|
|
2210
2019
|
run.disabled = true;
|
|
2211
2020
|
} else {
|
|
2212
|
-
const lockLabel = lockedByOther
|
|
2213
|
-
? "pyide-locked-other-instance-running"
|
|
2214
|
-
: "pyide-run";
|
|
2021
|
+
const lockLabel = lockedByOther ? "pyide-locked-other-instance-running" : "pyide-run";
|
|
2215
2022
|
run.textContent = hyperbook.i18n.get(lockLabel);
|
|
2216
2023
|
run.classList.add("running");
|
|
2217
2024
|
run.classList.toggle("locked", lockedByOther);
|
|
2218
2025
|
run.disabled = true;
|
|
2219
2026
|
if (test) {
|
|
2220
2027
|
test.textContent = hyperbook.i18n.get(
|
|
2221
|
-
lockedByOther ? "pyide-locked-other-instance-running" : "pyide-test"
|
|
2028
|
+
lockedByOther ? "pyide-locked-other-instance-running" : "pyide-test"
|
|
2222
2029
|
);
|
|
2223
2030
|
test.classList.toggle("locked", lockedByOther);
|
|
2224
2031
|
test.classList.add("running");
|
|
2225
2032
|
test.disabled = true;
|
|
2226
2033
|
}
|
|
2227
2034
|
}
|
|
2228
|
-
|
|
2229
2035
|
if (stop) {
|
|
2230
2036
|
const stopLabel = hasInterrupt ? "pyide-stop" : "pyide-stop-refresh";
|
|
2231
2037
|
if (state.running) {
|
|
2232
|
-
stop.textContent = state.stopping
|
|
2233
|
-
? hyperbook.i18n.get("pyide-stopping")
|
|
2234
|
-
: hyperbook.i18n.get(stopLabel);
|
|
2038
|
+
stop.textContent = state.stopping ? hyperbook.i18n.get("pyide-stopping") : hyperbook.i18n.get(stopLabel);
|
|
2235
2039
|
stop.disabled = false;
|
|
2236
2040
|
stop.addEventListener("click", handleStopClick);
|
|
2237
2041
|
} else {
|
|
@@ -2257,40 +2061,51 @@ if _pg:
|
|
|
2257
2061
|
stop.classList.remove("stopping");
|
|
2258
2062
|
stop.classList.remove("running");
|
|
2259
2063
|
stop.textContent = hyperbook.i18n.get(
|
|
2260
|
-
hasInterrupt ? "pyide-stop" : "pyide-stop-refresh"
|
|
2064
|
+
hasInterrupt ? "pyide-stop" : "pyide-stop-refresh"
|
|
2261
2065
|
);
|
|
2262
2066
|
stop.disabled = true;
|
|
2263
2067
|
}
|
|
2264
2068
|
}
|
|
2265
2069
|
}
|
|
2266
2070
|
};
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2071
|
+
var requestStop = (id) => {
|
|
2072
|
+
const state = getExecutionState(id);
|
|
2073
|
+
const hasRuntime = runtimes.has(id);
|
|
2074
|
+
if (!state.running && !hasRuntime || state.stopRequested) return;
|
|
2075
|
+
state.stopRequested = true;
|
|
2076
|
+
state.stopping = true;
|
|
2077
|
+
const interruptBuffer = interruptBuffers.get(id);
|
|
2078
|
+
if (interruptBuffer) {
|
|
2079
|
+
interruptBuffer[0] = 2;
|
|
2080
|
+
appendOutputLine(id, "Stop requested. Interrupting execution...");
|
|
2081
|
+
} else {
|
|
2082
|
+
appendOutputLine(id, hyperbook.i18n.get("pyide-stop-reloading"));
|
|
2083
|
+
}
|
|
2084
|
+
releaseKeyboardCapture(id);
|
|
2085
|
+
updateRunning();
|
|
2086
|
+
if (!interruptBuffer) {
|
|
2087
|
+
window.setTimeout(() => {
|
|
2088
|
+
window.location.reload();
|
|
2089
|
+
}, 50);
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
var handleStopClick = (event) => {
|
|
2093
|
+
const elem = event.currentTarget.closest(".directive-pyide");
|
|
2094
|
+
if (!elem?.id) return;
|
|
2095
|
+
requestStop(elem.id);
|
|
2096
|
+
};
|
|
2097
|
+
var setupSplitter = (elem, container, editorContainer, splitter, onSplitChanged) => {
|
|
2275
2098
|
if (!container || !editorContainer || !splitter) return;
|
|
2276
|
-
|
|
2277
2099
|
const minPanelSize = 120;
|
|
2278
|
-
|
|
2279
|
-
const getIsHorizontal = () =>
|
|
2280
|
-
getComputedStyle(elem).flexDirection.startsWith("row");
|
|
2281
|
-
|
|
2100
|
+
const getIsHorizontal = () => getComputedStyle(elem).flexDirection.startsWith("row");
|
|
2282
2101
|
const applySplitSize = (rawSize, isHorizontal) => {
|
|
2283
2102
|
const total = isHorizontal ? elem.clientWidth : elem.clientHeight;
|
|
2284
2103
|
const splitterSize = isHorizontal ? splitter.offsetWidth : splitter.offsetHeight;
|
|
2285
|
-
const maxSize = Math.max(
|
|
2286
|
-
minPanelSize,
|
|
2287
|
-
total - splitterSize - minPanelSize
|
|
2288
|
-
);
|
|
2104
|
+
const maxSize = Math.max(minPanelSize, total - splitterSize - minPanelSize);
|
|
2289
2105
|
const clamped = Math.max(minPanelSize, Math.min(rawSize, maxSize));
|
|
2290
2106
|
container.style.flex = `0 0 ${clamped}px`;
|
|
2291
2107
|
return clamped;
|
|
2292
2108
|
};
|
|
2293
|
-
|
|
2294
2109
|
const applyStoredSplitSize = () => {
|
|
2295
2110
|
const isHorizontal = getIsHorizontal();
|
|
2296
2111
|
elem.classList.toggle("split-horizontal", isHorizontal);
|
|
@@ -2303,29 +2118,21 @@ if _pg:
|
|
|
2303
2118
|
}
|
|
2304
2119
|
applySplitSize(rawStored, isHorizontal);
|
|
2305
2120
|
};
|
|
2306
|
-
|
|
2307
2121
|
applyStoredSplitSize();
|
|
2308
|
-
|
|
2309
2122
|
splitter.addEventListener("pointerdown", (event) => {
|
|
2310
2123
|
event.preventDefault();
|
|
2311
2124
|
splitter.setPointerCapture(event.pointerId);
|
|
2312
|
-
|
|
2313
2125
|
const isHorizontal = getIsHorizontal();
|
|
2314
2126
|
const key = isHorizontal ? "splitHorizontal" : "splitVertical";
|
|
2315
2127
|
const startPointer = isHorizontal ? event.clientX : event.clientY;
|
|
2316
|
-
const startSize = isHorizontal
|
|
2317
|
-
? container.getBoundingClientRect().width
|
|
2318
|
-
: container.getBoundingClientRect().height;
|
|
2319
|
-
|
|
2128
|
+
const startSize = isHorizontal ? container.getBoundingClientRect().width : container.getBoundingClientRect().height;
|
|
2320
2129
|
elem.classList.add("resizing");
|
|
2321
|
-
|
|
2322
2130
|
const onPointerMove = (moveEvent) => {
|
|
2323
2131
|
const pointer = isHorizontal ? moveEvent.clientX : moveEvent.clientY;
|
|
2324
2132
|
const delta = pointer - startPointer;
|
|
2325
2133
|
const size = applySplitSize(startSize + delta, isHorizontal);
|
|
2326
2134
|
elem.dataset[key] = String(Math.round(size));
|
|
2327
2135
|
};
|
|
2328
|
-
|
|
2329
2136
|
const onPointerUp = () => {
|
|
2330
2137
|
elem.classList.remove("resizing");
|
|
2331
2138
|
splitter.removeEventListener("pointermove", onPointerMove);
|
|
@@ -2334,42 +2141,25 @@ if _pg:
|
|
|
2334
2141
|
const splitHorizontal = Number(elem.dataset.splitHorizontal);
|
|
2335
2142
|
const splitVertical = Number(elem.dataset.splitVertical);
|
|
2336
2143
|
onSplitChanged?.({
|
|
2337
|
-
...
|
|
2338
|
-
|
|
2339
|
-
: {}),
|
|
2340
|
-
...(Number.isFinite(splitVertical) && splitVertical > 0
|
|
2341
|
-
? { splitVertical: Math.round(splitVertical) }
|
|
2342
|
-
: {}),
|
|
2144
|
+
...Number.isFinite(splitHorizontal) && splitHorizontal > 0 ? { splitHorizontal: Math.round(splitHorizontal) } : {},
|
|
2145
|
+
...Number.isFinite(splitVertical) && splitVertical > 0 ? { splitVertical: Math.round(splitVertical) } : {}
|
|
2343
2146
|
});
|
|
2344
2147
|
};
|
|
2345
|
-
|
|
2346
2148
|
splitter.addEventListener("pointermove", onPointerMove);
|
|
2347
2149
|
splitter.addEventListener("pointerup", onPointerUp);
|
|
2348
2150
|
splitter.addEventListener("pointercancel", onPointerUp);
|
|
2349
2151
|
});
|
|
2350
|
-
|
|
2351
2152
|
window.addEventListener("resize", applyStoredSplitSize);
|
|
2352
2153
|
return applyStoredSplitSize;
|
|
2353
2154
|
};
|
|
2354
|
-
|
|
2355
|
-
const setupCanvasOutputSplitter = (
|
|
2356
|
-
elem,
|
|
2357
|
-
container,
|
|
2358
|
-
canvasWrapper,
|
|
2359
|
-
output,
|
|
2360
|
-
splitter,
|
|
2361
|
-
onSplitChanged,
|
|
2362
|
-
) => {
|
|
2155
|
+
var setupCanvasOutputSplitter = (elem, container, canvasWrapper, output, splitter, onSplitChanged) => {
|
|
2363
2156
|
if (!elem || !container || !canvasWrapper || !output || !splitter) return;
|
|
2364
|
-
|
|
2365
2157
|
const minPanelSize = 80;
|
|
2366
|
-
|
|
2367
2158
|
const getAvailableHeight = () => {
|
|
2368
2159
|
const tabs = container.querySelector(".buttons");
|
|
2369
2160
|
const tabsHeight = tabs && tabs.offsetParent !== null ? tabs.offsetHeight : 0;
|
|
2370
2161
|
return container.clientHeight - tabsHeight - splitter.offsetHeight;
|
|
2371
2162
|
};
|
|
2372
|
-
|
|
2373
2163
|
const applySplitSize = (rawSize) => {
|
|
2374
2164
|
const total = getAvailableHeight();
|
|
2375
2165
|
const maxSize = Math.max(minPanelSize, total - minPanelSize);
|
|
@@ -2378,7 +2168,6 @@ if _pg:
|
|
|
2378
2168
|
output.style.flex = "1 1 0";
|
|
2379
2169
|
return clamped;
|
|
2380
2170
|
};
|
|
2381
|
-
|
|
2382
2171
|
const applyStoredSplitSize = () => {
|
|
2383
2172
|
const rawStored = Number(elem.dataset.splitCanvasOutput);
|
|
2384
2173
|
if (!Number.isFinite(rawStored) || rawStored <= 0) {
|
|
@@ -2388,22 +2177,17 @@ if _pg:
|
|
|
2388
2177
|
}
|
|
2389
2178
|
applySplitSize(rawStored);
|
|
2390
2179
|
};
|
|
2391
|
-
|
|
2392
2180
|
splitter.addEventListener("pointerdown", (event) => {
|
|
2393
2181
|
event.preventDefault();
|
|
2394
2182
|
splitter.setPointerCapture(event.pointerId);
|
|
2395
|
-
|
|
2396
2183
|
const startPointer = event.clientY;
|
|
2397
2184
|
const startSize = canvasWrapper.getBoundingClientRect().height;
|
|
2398
|
-
|
|
2399
2185
|
elem.classList.add("resizing");
|
|
2400
|
-
|
|
2401
2186
|
const onPointerMove = (moveEvent) => {
|
|
2402
2187
|
const delta = moveEvent.clientY - startPointer;
|
|
2403
2188
|
const size = applySplitSize(startSize + delta);
|
|
2404
2189
|
elem.dataset.splitCanvasOutput = String(Math.round(size));
|
|
2405
2190
|
};
|
|
2406
|
-
|
|
2407
2191
|
const onPointerUp = () => {
|
|
2408
2192
|
elem.classList.remove("resizing");
|
|
2409
2193
|
splitter.removeEventListener("pointermove", onPointerMove);
|
|
@@ -2414,393 +2198,345 @@ if _pg:
|
|
|
2414
2198
|
onSplitChanged?.({ splitCanvasOutput: Math.round(splitCanvasOutput) });
|
|
2415
2199
|
}
|
|
2416
2200
|
};
|
|
2417
|
-
|
|
2418
2201
|
splitter.addEventListener("pointermove", onPointerMove);
|
|
2419
2202
|
splitter.addEventListener("pointerup", onPointerUp);
|
|
2420
2203
|
splitter.addEventListener("pointercancel", onPointerUp);
|
|
2421
2204
|
});
|
|
2422
|
-
|
|
2423
2205
|
window.addEventListener("resize", applyStoredSplitSize);
|
|
2424
2206
|
return applyStoredSplitSize;
|
|
2425
2207
|
};
|
|
2426
2208
|
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
)
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2209
|
+
// assets/directive-pyide/src/index.js
|
|
2210
|
+
hyperbook.python = function() {
|
|
2211
|
+
const init = (root) => {
|
|
2212
|
+
const elems = root.getElementsByClassName("directive-pyide");
|
|
2213
|
+
for (let elem of elems) {
|
|
2214
|
+
let showOutput2 = function() {
|
|
2215
|
+
if (isWideCanvasMode()) {
|
|
2216
|
+
applyCanvasOutputLayout();
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
showOutputTab();
|
|
2220
|
+
}, showCanvas2 = function() {
|
|
2221
|
+
if (isWideCanvasMode()) {
|
|
2222
|
+
applyCanvasOutputLayout();
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
showCanvasTab();
|
|
2226
|
+
};
|
|
2227
|
+
var showOutput = showOutput2, showCanvas = showCanvas2;
|
|
2228
|
+
if (elem.getAttribute("data-pyide-initialized") === "true") continue;
|
|
2229
|
+
elem.setAttribute("data-pyide-initialized", "true");
|
|
2230
|
+
const editorDiv = elem.getElementsByClassName("editor")[0];
|
|
2231
|
+
const container = elem.getElementsByClassName("container")[0];
|
|
2232
|
+
const editorContainer = elem.getElementsByClassName("editor-container")[0];
|
|
2233
|
+
const splitter = elem.getElementsByClassName("splitter")[0];
|
|
2234
|
+
const run = elem.getElementsByClassName("run")[0];
|
|
2235
|
+
const test = elem.getElementsByClassName("test")[0];
|
|
2236
|
+
const stop = elem.getElementsByClassName("stop")[0];
|
|
2237
|
+
const output = elem.getElementsByClassName("output")[0];
|
|
2238
|
+
const canvas = elem.getElementsByClassName("canvas")[0];
|
|
2239
|
+
const canvasWrapper = elem.getElementsByClassName("canvas-wrapper")[0] || canvas;
|
|
2240
|
+
const canvasOutputSplitter = elem.getElementsByClassName(
|
|
2241
|
+
"canvas-output-splitter"
|
|
2242
|
+
)[0];
|
|
2243
|
+
const canvasHeader = elem.getElementsByClassName("canvas-header")[0];
|
|
2244
|
+
const outputHeader = elem.getElementsByClassName("output-header")[0];
|
|
2245
|
+
const outputBtn = elem.getElementsByClassName("output-btn")[0];
|
|
2246
|
+
const canvasBtn = elem.getElementsByClassName("canvas-btn")[0];
|
|
2247
|
+
const canvasTabs = outputBtn?.closest(".buttons");
|
|
2248
|
+
const copyEl = elem.getElementsByClassName("copy")[0];
|
|
2249
|
+
const resetEl = elem.getElementsByClassName("reset")[0];
|
|
2250
|
+
const downloadEl = elem.getElementsByClassName("download")[0];
|
|
2251
|
+
const fullscreenEl = elem.getElementsByClassName("fullscreen")[0];
|
|
2252
|
+
const id = elem.id;
|
|
2253
|
+
const hasCanvas = elem.getAttribute("data-canvas") === "true";
|
|
2254
|
+
const additionalPackages = Array.from(
|
|
2255
|
+
new Set(
|
|
2256
|
+
(elem.getAttribute("data-packages") || "").split(",").map((pkg) => pkg.trim()).filter((pkg) => pkg.length > 0)
|
|
2257
|
+
)
|
|
2258
|
+
);
|
|
2259
|
+
let pyideState = { id };
|
|
2260
|
+
const initialSource = editorDiv ? editorDiv.textContent : "";
|
|
2261
|
+
if (editorDiv) editorDiv.textContent = "";
|
|
2262
|
+
const cm = editorDiv ? HyperbookCM.create(editorDiv, {
|
|
2263
|
+
lang: editorDiv.dataset.lang || "python",
|
|
2264
|
+
value: initialSource,
|
|
2265
|
+
onChange: (code) => {
|
|
2266
|
+
void persistPyideState({ script: code });
|
|
2267
|
+
}
|
|
2268
|
+
}) : null;
|
|
2269
|
+
if (editorDiv && cm) editorDiv._cm = cm;
|
|
2270
|
+
const getEditorValue = () => cm?.getValue() ?? "";
|
|
2271
|
+
pyideState = { ...pyideState, script: getEditorValue() };
|
|
2272
|
+
const persistPyideState = (updates = {}) => {
|
|
2273
|
+
pyideState = { ...pyideState, ...updates, id };
|
|
2274
|
+
return hyperbook.store.db.pyide.put(pyideState);
|
|
2275
|
+
};
|
|
2276
|
+
copyEl?.addEventListener("click", async () => {
|
|
2277
|
+
try {
|
|
2278
|
+
await navigator.clipboard.writeText(getEditorValue());
|
|
2279
|
+
} catch (error) {
|
|
2280
|
+
console.error(error.message);
|
|
2281
|
+
}
|
|
2282
|
+
});
|
|
2283
|
+
resetEl?.addEventListener("click", () => {
|
|
2284
|
+
hyperbook.store.db.pyide.delete(id);
|
|
2285
|
+
window.location.reload();
|
|
2286
|
+
});
|
|
2287
|
+
downloadEl?.addEventListener("click", () => {
|
|
2288
|
+
const a = document.createElement("a");
|
|
2289
|
+
const blob = new Blob([getEditorValue()], { type: "text/plain" });
|
|
2290
|
+
a.href = URL.createObjectURL(blob);
|
|
2291
|
+
a.download = `script-${id}.py`;
|
|
2292
|
+
a.click();
|
|
2293
|
+
});
|
|
2294
|
+
fullscreenEl?.addEventListener("click", async () => {
|
|
2295
|
+
try {
|
|
2296
|
+
await toggleFullscreen(elem);
|
|
2297
|
+
} catch (error) {
|
|
2298
|
+
console.error(error.message);
|
|
2299
|
+
}
|
|
2300
|
+
});
|
|
2301
|
+
updateFullscreenButtonState(elem, fullscreenEl);
|
|
2302
|
+
let tests = [];
|
|
2518
2303
|
try {
|
|
2519
|
-
|
|
2520
|
-
} catch (
|
|
2521
|
-
console.error(error.message);
|
|
2304
|
+
tests = JSON.parse(atob(elem.getAttribute("data-tests")));
|
|
2305
|
+
} catch (e) {
|
|
2522
2306
|
}
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
tests = JSON.parse(atob(elem.getAttribute("data-tests")));
|
|
2528
|
-
} catch (e) {}
|
|
2529
|
-
|
|
2530
|
-
const isWideCanvasMode = () =>
|
|
2531
|
-
hasCanvas && window.matchMedia("(min-width: 1024px)").matches;
|
|
2532
|
-
|
|
2533
|
-
let activeCanvasView = "output";
|
|
2534
|
-
|
|
2535
|
-
const showOutputTab = () => {
|
|
2536
|
-
activeCanvasView = "output";
|
|
2537
|
-
outputBtn.classList.add("active");
|
|
2538
|
-
if (canvasBtn) canvasBtn.classList.remove("active");
|
|
2539
|
-
canvasHeader?.classList.add("hidden");
|
|
2540
|
-
outputHeader?.classList.add("hidden");
|
|
2541
|
-
output.classList.remove("hidden");
|
|
2542
|
-
if (canvasWrapper) canvasWrapper.classList.add("hidden");
|
|
2543
|
-
canvasOutputSplitter?.classList.add("hidden");
|
|
2544
|
-
};
|
|
2545
|
-
const showCanvasTab = () => {
|
|
2546
|
-
activeCanvasView = "canvas";
|
|
2547
|
-
outputBtn.classList.remove("active");
|
|
2548
|
-
if (canvasBtn) canvasBtn.classList.add("active");
|
|
2549
|
-
canvasHeader?.classList.add("hidden");
|
|
2550
|
-
outputHeader?.classList.add("hidden");
|
|
2551
|
-
output.classList.add("hidden");
|
|
2552
|
-
if (canvasWrapper) canvasWrapper.classList.remove("hidden");
|
|
2553
|
-
canvasOutputSplitter?.classList.add("hidden");
|
|
2554
|
-
turtleModules.get(id)?.__redraw?.();
|
|
2555
|
-
};
|
|
2556
|
-
|
|
2557
|
-
const applyStoredCanvasOutputSplit = setupCanvasOutputSplitter(
|
|
2558
|
-
elem,
|
|
2559
|
-
container,
|
|
2560
|
-
canvasWrapper,
|
|
2561
|
-
output,
|
|
2562
|
-
canvasOutputSplitter,
|
|
2563
|
-
(splitState) => {
|
|
2564
|
-
void persistPyideState(splitState);
|
|
2565
|
-
},
|
|
2566
|
-
);
|
|
2567
|
-
|
|
2568
|
-
const applyCanvasOutputLayout = () => {
|
|
2569
|
-
if (!hasCanvas || !canvasWrapper || !canvasOutputSplitter) return;
|
|
2570
|
-
if (isWideCanvasMode()) {
|
|
2571
|
-
elem.classList.add("canvas-split-mode");
|
|
2572
|
-
canvasTabs?.classList.add("hidden");
|
|
2573
|
-
output.classList.remove("hidden");
|
|
2574
|
-
canvasWrapper.classList.remove("hidden");
|
|
2575
|
-
canvasOutputSplitter.classList.remove("hidden");
|
|
2576
|
-
canvasHeader?.classList.remove("hidden");
|
|
2577
|
-
outputHeader?.classList.remove("hidden");
|
|
2307
|
+
const isWideCanvasMode = () => hasCanvas && window.matchMedia("(min-width: 1024px)").matches;
|
|
2308
|
+
let activeCanvasView = "output";
|
|
2309
|
+
const showOutputTab = () => {
|
|
2310
|
+
activeCanvasView = "output";
|
|
2578
2311
|
outputBtn.classList.add("active");
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2312
|
+
if (canvasBtn) canvasBtn.classList.remove("active");
|
|
2313
|
+
canvasHeader?.classList.add("hidden");
|
|
2314
|
+
outputHeader?.classList.add("hidden");
|
|
2315
|
+
output.classList.remove("hidden");
|
|
2316
|
+
if (canvasWrapper) canvasWrapper.classList.add("hidden");
|
|
2317
|
+
canvasOutputSplitter?.classList.add("hidden");
|
|
2318
|
+
};
|
|
2319
|
+
const showCanvasTab = () => {
|
|
2320
|
+
activeCanvasView = "canvas";
|
|
2321
|
+
outputBtn.classList.remove("active");
|
|
2322
|
+
if (canvasBtn) canvasBtn.classList.add("active");
|
|
2323
|
+
canvasHeader?.classList.add("hidden");
|
|
2324
|
+
outputHeader?.classList.add("hidden");
|
|
2325
|
+
output.classList.add("hidden");
|
|
2326
|
+
if (canvasWrapper) canvasWrapper.classList.remove("hidden");
|
|
2327
|
+
canvasOutputSplitter?.classList.add("hidden");
|
|
2585
2328
|
turtleModules.get(id)?.__redraw?.();
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
canvasBtn.disabled = false;
|
|
2596
|
-
}
|
|
2597
|
-
if (activeCanvasView === "canvas") {
|
|
2598
|
-
showCanvasTab();
|
|
2599
|
-
} else {
|
|
2600
|
-
showOutputTab();
|
|
2601
|
-
}
|
|
2602
|
-
};
|
|
2603
|
-
|
|
2604
|
-
function showOutput() {
|
|
2605
|
-
if (isWideCanvasMode()) {
|
|
2606
|
-
applyCanvasOutputLayout();
|
|
2607
|
-
return;
|
|
2608
|
-
}
|
|
2609
|
-
showOutputTab();
|
|
2610
|
-
}
|
|
2611
|
-
function showCanvas() {
|
|
2612
|
-
if (isWideCanvasMode()) {
|
|
2613
|
-
applyCanvasOutputLayout();
|
|
2614
|
-
return;
|
|
2615
|
-
}
|
|
2616
|
-
showCanvasTab();
|
|
2617
|
-
}
|
|
2618
|
-
|
|
2619
|
-
outputBtn?.addEventListener("click", showOutput);
|
|
2620
|
-
canvasBtn?.addEventListener("click", showCanvas);
|
|
2621
|
-
const applyStoredSplitSize = setupSplitter(
|
|
2622
|
-
elem,
|
|
2623
|
-
container,
|
|
2624
|
-
editorContainer,
|
|
2625
|
-
splitter,
|
|
2626
|
-
(splitState) => {
|
|
2627
|
-
void persistPyideState(splitState);
|
|
2628
|
-
},
|
|
2629
|
-
);
|
|
2630
|
-
|
|
2631
|
-
let editorStateRestored = false;
|
|
2632
|
-
const restoreEditorState = async () => {
|
|
2633
|
-
if (editorStateRestored) return;
|
|
2634
|
-
editorStateRestored = true;
|
|
2635
|
-
|
|
2636
|
-
const result = await hyperbook.store.db.pyide.get(id);
|
|
2637
|
-
if (result) {
|
|
2638
|
-
pyideState = { ...pyideState, ...result };
|
|
2639
|
-
if (typeof result.script === "string") {
|
|
2640
|
-
cm?.setValue(result.script);
|
|
2329
|
+
};
|
|
2330
|
+
const applyStoredCanvasOutputSplit = setupCanvasOutputSplitter(
|
|
2331
|
+
elem,
|
|
2332
|
+
container,
|
|
2333
|
+
canvasWrapper,
|
|
2334
|
+
output,
|
|
2335
|
+
canvasOutputSplitter,
|
|
2336
|
+
(splitState) => {
|
|
2337
|
+
void persistPyideState(splitState);
|
|
2641
2338
|
}
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
) {
|
|
2646
|
-
elem.
|
|
2339
|
+
);
|
|
2340
|
+
const applyCanvasOutputLayout = () => {
|
|
2341
|
+
if (!hasCanvas || !canvasWrapper || !canvasOutputSplitter) return;
|
|
2342
|
+
if (isWideCanvasMode()) {
|
|
2343
|
+
elem.classList.add("canvas-split-mode");
|
|
2344
|
+
canvasTabs?.classList.add("hidden");
|
|
2345
|
+
output.classList.remove("hidden");
|
|
2346
|
+
canvasWrapper.classList.remove("hidden");
|
|
2347
|
+
canvasOutputSplitter.classList.remove("hidden");
|
|
2348
|
+
canvasHeader?.classList.remove("hidden");
|
|
2349
|
+
outputHeader?.classList.remove("hidden");
|
|
2350
|
+
outputBtn.classList.add("active");
|
|
2351
|
+
outputBtn.disabled = true;
|
|
2352
|
+
if (canvasBtn) {
|
|
2353
|
+
canvasBtn.classList.add("active");
|
|
2354
|
+
canvasBtn.disabled = true;
|
|
2355
|
+
}
|
|
2356
|
+
applyStoredCanvasOutputSplit?.();
|
|
2357
|
+
turtleModules.get(id)?.__redraw?.();
|
|
2358
|
+
return;
|
|
2647
2359
|
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2360
|
+
elem.classList.remove("canvas-split-mode");
|
|
2361
|
+
canvasTabs?.classList.remove("hidden");
|
|
2362
|
+
output.style.flex = "";
|
|
2363
|
+
canvasWrapper.style.flex = "";
|
|
2364
|
+
outputBtn.disabled = false;
|
|
2365
|
+
if (canvasBtn) {
|
|
2366
|
+
canvasBtn.disabled = false;
|
|
2653
2367
|
}
|
|
2654
|
-
if (
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
elem.dataset.splitCanvasOutput = String(
|
|
2659
|
-
Math.round(result.splitCanvasOutput),
|
|
2660
|
-
);
|
|
2368
|
+
if (activeCanvasView === "canvas") {
|
|
2369
|
+
showCanvasTab();
|
|
2370
|
+
} else {
|
|
2371
|
+
showOutputTab();
|
|
2661
2372
|
}
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
const interruptBuffer = interruptBuffers.get(id);
|
|
2684
|
-
if (interruptBuffer) interruptBuffer[0] = 0;
|
|
2685
|
-
updateRunning();
|
|
2686
|
-
|
|
2687
|
-
output.innerHTML = "";
|
|
2688
|
-
clearPytamaroStdoutCarry(id);
|
|
2689
|
-
|
|
2690
|
-
const script = getEditorValue();
|
|
2691
|
-
try {
|
|
2692
|
-
for (let test of tests) {
|
|
2693
|
-
if (state.stopRequested) {
|
|
2694
|
-
appendOutputLine(id, "Stopped pending test execution.");
|
|
2695
|
-
break;
|
|
2373
|
+
};
|
|
2374
|
+
outputBtn?.addEventListener("click", showOutput2);
|
|
2375
|
+
canvasBtn?.addEventListener("click", showCanvas2);
|
|
2376
|
+
const applyStoredSplitSize = setupSplitter(
|
|
2377
|
+
elem,
|
|
2378
|
+
container,
|
|
2379
|
+
editorContainer,
|
|
2380
|
+
splitter,
|
|
2381
|
+
(splitState) => {
|
|
2382
|
+
void persistPyideState(splitState);
|
|
2383
|
+
}
|
|
2384
|
+
);
|
|
2385
|
+
let editorStateRestored = false;
|
|
2386
|
+
const restoreEditorState = async () => {
|
|
2387
|
+
if (editorStateRestored) return;
|
|
2388
|
+
editorStateRestored = true;
|
|
2389
|
+
const result = await hyperbook.store.db.pyide.get(id);
|
|
2390
|
+
if (result) {
|
|
2391
|
+
pyideState = { ...pyideState, ...result };
|
|
2392
|
+
if (typeof result.script === "string") {
|
|
2393
|
+
cm?.setValue(result.script);
|
|
2696
2394
|
}
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
if (results) {
|
|
2712
|
-
appendOutput(output, results);
|
|
2713
|
-
} else if (error) {
|
|
2714
|
-
appendFriendlyError(output, error, testCode);
|
|
2395
|
+
if (Number.isFinite(result.splitHorizontal) && result.splitHorizontal > 0) {
|
|
2396
|
+
elem.dataset.splitHorizontal = String(
|
|
2397
|
+
Math.round(result.splitHorizontal)
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
if (Number.isFinite(result.splitVertical) && result.splitVertical > 0) {
|
|
2401
|
+
elem.dataset.splitVertical = String(
|
|
2402
|
+
Math.round(result.splitVertical)
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
if (Number.isFinite(result.splitCanvasOutput) && result.splitCanvasOutput > 0) {
|
|
2406
|
+
elem.dataset.splitCanvasOutput = String(
|
|
2407
|
+
Math.round(result.splitCanvasOutput)
|
|
2408
|
+
);
|
|
2715
2409
|
}
|
|
2410
|
+
applyStoredSplitSize?.();
|
|
2411
|
+
applyCanvasOutputLayout();
|
|
2716
2412
|
}
|
|
2717
|
-
}
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2413
|
+
};
|
|
2414
|
+
void restoreEditorState();
|
|
2415
|
+
window.addEventListener("resize", () => {
|
|
2416
|
+
applyCanvasOutputLayout();
|
|
2417
|
+
turtleModules.get(id)?.__redraw?.();
|
|
2418
|
+
});
|
|
2419
|
+
applyCanvasOutputLayout();
|
|
2420
|
+
test?.addEventListener("click", async () => {
|
|
2421
|
+
showOutput2();
|
|
2422
|
+
const state = getExecutionState(id);
|
|
2423
|
+
if (state.running || getRunningInstanceId() !== null) return;
|
|
2424
|
+
state.running = true;
|
|
2425
|
+
state.type = "test";
|
|
2426
|
+
state.stopRequested = false;
|
|
2724
2427
|
state.stopping = false;
|
|
2725
|
-
|
|
2726
|
-
|
|
2428
|
+
const interruptBuffer = interruptBuffers.get(id);
|
|
2429
|
+
if (interruptBuffer) interruptBuffer[0] = 0;
|
|
2727
2430
|
updateRunning();
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
setPytamaroCanvasTarget(id, renderPytamaroToCanvas);
|
|
2754
|
-
const { results, error } = await executeScript(id, script, {
|
|
2755
|
-
...(hasCanvas && canvas ? { canvas } : {}),
|
|
2756
|
-
}, additionalPackages);
|
|
2757
|
-
if (!state.stopRequested) {
|
|
2758
|
-
if (results) {
|
|
2759
|
-
appendOutput(output, results, false, id);
|
|
2760
|
-
} else if (error) {
|
|
2761
|
-
showOutput();
|
|
2762
|
-
appendFriendlyError(output, error, script);
|
|
2431
|
+
output.innerHTML = "";
|
|
2432
|
+
clearPytamaroStdoutCarry(id);
|
|
2433
|
+
const script = getEditorValue();
|
|
2434
|
+
try {
|
|
2435
|
+
for (let test2 of tests) {
|
|
2436
|
+
if (state.stopRequested) {
|
|
2437
|
+
appendOutputLine(id, "Stopped pending test execution.");
|
|
2438
|
+
break;
|
|
2439
|
+
}
|
|
2440
|
+
const testCode = test2.code.replace("#SCRIPT#", script);
|
|
2441
|
+
const heading = document.createElement("div");
|
|
2442
|
+
heading.innerHTML = `== Test ${test2.name} ==`;
|
|
2443
|
+
heading.classList.add("test-heading");
|
|
2444
|
+
output.appendChild(heading);
|
|
2445
|
+
const { results, error } = await executeScript(
|
|
2446
|
+
id,
|
|
2447
|
+
testCode,
|
|
2448
|
+
{},
|
|
2449
|
+
additionalPackages
|
|
2450
|
+
);
|
|
2451
|
+
if (results) {
|
|
2452
|
+
appendOutput(output, results);
|
|
2453
|
+
} else if (error) {
|
|
2454
|
+
appendFriendlyError(output, error, testCode);
|
|
2455
|
+
}
|
|
2763
2456
|
}
|
|
2457
|
+
} catch (e) {
|
|
2458
|
+
output.innerHTML = "";
|
|
2459
|
+
appendOutput(output, `Error: ${e}`, true);
|
|
2460
|
+
console.log(e);
|
|
2461
|
+
} finally {
|
|
2462
|
+
clearPytamaroStdoutCarry(id);
|
|
2463
|
+
state.running = false;
|
|
2464
|
+
state.stopping = false;
|
|
2465
|
+
state.type = null;
|
|
2466
|
+
releaseKeyboardCapture(id);
|
|
2467
|
+
updateRunning();
|
|
2468
|
+
}
|
|
2469
|
+
});
|
|
2470
|
+
run?.addEventListener("click", async () => {
|
|
2471
|
+
const script = getEditorValue();
|
|
2472
|
+
if (hasCanvas) {
|
|
2473
|
+
showCanvas2();
|
|
2764
2474
|
} else {
|
|
2765
|
-
|
|
2475
|
+
showOutput2();
|
|
2766
2476
|
}
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
} finally {
|
|
2773
|
-
clearPytamaroStdoutCarry(id);
|
|
2774
|
-
state.running = false;
|
|
2477
|
+
const state = getExecutionState(id);
|
|
2478
|
+
if (state.running || getRunningInstanceId() !== null) return;
|
|
2479
|
+
state.running = true;
|
|
2480
|
+
state.type = "run";
|
|
2481
|
+
state.stopRequested = false;
|
|
2775
2482
|
state.stopping = false;
|
|
2776
|
-
|
|
2777
|
-
|
|
2483
|
+
const interruptBuffer = interruptBuffers.get(id);
|
|
2484
|
+
if (interruptBuffer) interruptBuffer[0] = 0;
|
|
2778
2485
|
updateRunning();
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2486
|
+
output.innerHTML = "";
|
|
2487
|
+
clearPytamaroStdoutCarry(id);
|
|
2488
|
+
try {
|
|
2489
|
+
const { results, error } = await executeScript(
|
|
2490
|
+
id,
|
|
2491
|
+
script,
|
|
2492
|
+
{
|
|
2493
|
+
...hasCanvas && canvas ? { canvas } : {}
|
|
2494
|
+
},
|
|
2495
|
+
additionalPackages
|
|
2496
|
+
);
|
|
2497
|
+
if (!state.stopRequested) {
|
|
2498
|
+
if (results) {
|
|
2499
|
+
appendOutput(output, results, false, id);
|
|
2500
|
+
} else if (error) {
|
|
2501
|
+
showOutput2();
|
|
2502
|
+
appendFriendlyError(output, error, script);
|
|
2503
|
+
}
|
|
2504
|
+
} else {
|
|
2505
|
+
appendOutputLine(id, "Execution stopped.");
|
|
2506
|
+
}
|
|
2507
|
+
} catch (e) {
|
|
2508
|
+
showOutput2();
|
|
2509
|
+
output.innerHTML = "";
|
|
2510
|
+
appendOutput(output, `Error: ${e}`, true, id);
|
|
2511
|
+
console.log(e);
|
|
2512
|
+
} finally {
|
|
2513
|
+
clearPytamaroStdoutCarry(id);
|
|
2514
|
+
state.running = false;
|
|
2515
|
+
state.stopping = false;
|
|
2516
|
+
state.type = null;
|
|
2517
|
+
releaseKeyboardCapture(id);
|
|
2518
|
+
updateRunning();
|
|
2792
2519
|
}
|
|
2793
2520
|
});
|
|
2521
|
+
stop?.addEventListener("click", handleStopClick);
|
|
2794
2522
|
}
|
|
2523
|
+
};
|
|
2524
|
+
const observer = new MutationObserver((mutations) => {
|
|
2525
|
+
mutations.forEach((mutation) => {
|
|
2526
|
+
if (mutation.addedNodes.length) {
|
|
2527
|
+
mutation.addedNodes.forEach((node) => {
|
|
2528
|
+
if (node.nodeType === 1 && node.classList?.contains("directive-pyide")) {
|
|
2529
|
+
init(node);
|
|
2530
|
+
}
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
});
|
|
2795
2534
|
});
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
init
|
|
2802
|
-
});
|
|
2803
|
-
document.addEventListener("fullscreenchange", syncFullscreenButtons);
|
|
2804
|
-
|
|
2805
|
-
return { init };
|
|
2535
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
2536
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
2537
|
+
init(document);
|
|
2538
|
+
});
|
|
2539
|
+
document.addEventListener("fullscreenchange", syncFullscreenButtons);
|
|
2540
|
+
return { init };
|
|
2541
|
+
}();
|
|
2806
2542
|
})();
|