hyperbook 0.99.1 → 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/codemirror/codemirror.bundle.js +10 -10
- package/dist/assets/directive-abc-music/style.css +23 -0
- package/dist/assets/directive-archive/style.css +9 -3
- package/dist/assets/directive-download/style.css +9 -3
- package/dist/assets/directive-openscad/client.js +17 -14
- package/dist/assets/directive-openscad/style.css +36 -3
- package/dist/assets/directive-p5/client.js +8 -7
- package/dist/assets/directive-p5/style.css +25 -0
- package/dist/assets/directive-protect/style.css +11 -5
- package/dist/assets/directive-pyide/client.js +1207 -170
- package/dist/assets/directive-pyide/style.css +158 -2
- package/dist/assets/directive-typst/style.css +44 -2
- package/dist/assets/directive-webide/client.js +40 -18
- package/dist/assets/directive-webide/style.css +31 -1
- package/dist/assets/shell.css +16 -16
- package/dist/index.js +558 -318
- package/dist/locales/de.json +1 -0
- package/dist/locales/en.json +1 -0
- package/package.json +7 -5
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
var PYODIDE_CDN = "https://cdn.jsdelivr.net/pyodide/v0.29.4/full/pyodide.js";
|
|
24
24
|
var PYTAMARO_URI_BEGIN = "@@@PYTAMARO_DATA_URI_BEGIN@@@";
|
|
25
25
|
var PYTAMARO_URI_END = "@@@PYTAMARO_DATA_URI_END@@@";
|
|
26
|
+
var scriptLooksLikeTurtle = (script) => /\bfrom\s+turtle\s+import\b|\bimport\s+turtle\b/.test(String(script || ""));
|
|
26
27
|
|
|
27
28
|
// assets/directive-pyide/src/output.js
|
|
28
29
|
var getOutput = (id) => {
|
|
@@ -184,11 +185,77 @@
|
|
|
184
185
|
pytamaroStdoutCarry.delete(id);
|
|
185
186
|
};
|
|
186
187
|
|
|
188
|
+
// assets/directive-pyide/src/stdin.js
|
|
189
|
+
var pending = /* @__PURE__ */ new Map();
|
|
190
|
+
var parts = (id) => {
|
|
191
|
+
const elem = document.getElementById(id);
|
|
192
|
+
if (!elem) return null;
|
|
193
|
+
const form = elem.getElementsByClassName("stdin")[0];
|
|
194
|
+
const field = elem.getElementsByClassName("stdin-field")[0];
|
|
195
|
+
if (!form || !field) return null;
|
|
196
|
+
const label = elem.getElementsByClassName("stdin-prompt")[0] || null;
|
|
197
|
+
return { form, field, label };
|
|
198
|
+
};
|
|
199
|
+
var hideStdinField = (id) => {
|
|
200
|
+
const found = parts(id);
|
|
201
|
+
if (!found) return;
|
|
202
|
+
found.form.classList.add("hidden");
|
|
203
|
+
found.field.value = "";
|
|
204
|
+
if (found.label) found.label.textContent = "";
|
|
205
|
+
};
|
|
206
|
+
var cancelStdin = (id, value = "") => {
|
|
207
|
+
const entry = pending.get(id);
|
|
208
|
+
if (!entry) return;
|
|
209
|
+
pending.delete(id);
|
|
210
|
+
entry.cleanup();
|
|
211
|
+
hideStdinField(id);
|
|
212
|
+
entry.resolve(value);
|
|
213
|
+
};
|
|
214
|
+
var askStdinAsync = (id, promptText = "") => {
|
|
215
|
+
const found = parts(id);
|
|
216
|
+
if (!found) return Promise.resolve(askStdinSync(id, promptText));
|
|
217
|
+
cancelStdin(id);
|
|
218
|
+
const { form, field, label } = found;
|
|
219
|
+
if (label) label.textContent = String(promptText ?? "").trimEnd();
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
const onSubmit = (event) => {
|
|
222
|
+
event.preventDefault();
|
|
223
|
+
const value = field.value;
|
|
224
|
+
pending.delete(id);
|
|
225
|
+
cleanup();
|
|
226
|
+
hideStdinField(id);
|
|
227
|
+
resolve(value);
|
|
228
|
+
};
|
|
229
|
+
const cleanup = () => form.removeEventListener("submit", onSubmit);
|
|
230
|
+
pending.set(id, { resolve, cleanup });
|
|
231
|
+
form.addEventListener("submit", onSubmit);
|
|
232
|
+
form.classList.remove("hidden");
|
|
233
|
+
field.value = "";
|
|
234
|
+
field.focus();
|
|
235
|
+
form.scrollIntoView?.({ block: "nearest" });
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
var askStdinSync = (id, promptText = "") => {
|
|
239
|
+
const text = promptText || hyperbook.i18n.get("pyide-input-prompt");
|
|
240
|
+
const value = window.prompt(text);
|
|
241
|
+
return value === null ? "" : value;
|
|
242
|
+
};
|
|
243
|
+
|
|
187
244
|
// assets/directive-pyide/src/turtle-ffi.js
|
|
188
245
|
var createTurtleJsFFI = (id) => {
|
|
189
246
|
const DEFAULT_LINE_WIDTH = 1;
|
|
190
247
|
const DEFAULT_FONT_SIZE = 8;
|
|
191
248
|
const DEFAULT_SHAPE = "classic";
|
|
249
|
+
const DEFAULT_SPEED = 3;
|
|
250
|
+
const DEFAULT_DELAY_MS = 80;
|
|
251
|
+
const DEFAULT_UNDO_BUFFER = 1e3;
|
|
252
|
+
const SPEED_NAMES = {
|
|
253
|
+
fastest: 0,
|
|
254
|
+
fast: 10,
|
|
255
|
+
normal: 6,
|
|
256
|
+
slow: 3,
|
|
257
|
+
slowest: 1
|
|
258
|
+
};
|
|
192
259
|
const TURTLE_SHAPES = {
|
|
193
260
|
classic: [
|
|
194
261
|
[0, 0],
|
|
@@ -212,8 +279,9 @@
|
|
|
212
279
|
[10, 10],
|
|
213
280
|
[-10, 10]
|
|
214
281
|
],
|
|
215
|
-
circle:
|
|
282
|
+
circle: "arc",
|
|
216
283
|
// rendered as arc, not polygon
|
|
284
|
+
blank: [],
|
|
217
285
|
turtle: [
|
|
218
286
|
[16, 0],
|
|
219
287
|
[14, 2],
|
|
@@ -251,16 +319,25 @@
|
|
|
251
319
|
let backgroundColor = "#ffffff";
|
|
252
320
|
let backgroundImage = null;
|
|
253
321
|
let colorMode = 1;
|
|
254
|
-
let delayMs =
|
|
255
|
-
let turtleSpeed =
|
|
322
|
+
let delayMs = DEFAULT_DELAY_MS;
|
|
323
|
+
let turtleSpeed = DEFAULT_SPEED;
|
|
256
324
|
let screenWidth = 640;
|
|
257
325
|
let screenHeight = 480;
|
|
326
|
+
let screenMode = "standard";
|
|
327
|
+
let screenTitle = "";
|
|
328
|
+
let tracerN = 1;
|
|
329
|
+
let tracerCounter = 0;
|
|
258
330
|
let operationQueue = [];
|
|
259
331
|
let queueGeneration = 0;
|
|
260
332
|
let queueRunning = false;
|
|
333
|
+
let stampCounter = 0;
|
|
334
|
+
let undoBufferSize = DEFAULT_UNDO_BUFFER;
|
|
335
|
+
let eventCleanups = [];
|
|
336
|
+
let pendingTimers = [];
|
|
261
337
|
const textMeasureCanvas = document.createElement("canvas");
|
|
262
338
|
const textMeasureContext = textMeasureCanvas.getContext("2d");
|
|
263
339
|
const allPens = [];
|
|
340
|
+
const getCanvas = (elementId) => document.getElementById(elementId)?.getElementsByClassName("canvas")[0] || null;
|
|
264
341
|
const normalizeAngle = (angle) => {
|
|
265
342
|
const value = Number(angle) || 0;
|
|
266
343
|
return (value % 360 + 360) % 360;
|
|
@@ -358,9 +435,9 @@
|
|
|
358
435
|
const normalizeFontStyle = (value) => {
|
|
359
436
|
const style = toPlainString(value, "normal").trim().toLowerCase();
|
|
360
437
|
if (!style || style === "normal") return "";
|
|
361
|
-
const
|
|
362
|
-
const hasItalic =
|
|
363
|
-
const hasBold =
|
|
438
|
+
const parts2 = style.split(/\s+/).filter(Boolean);
|
|
439
|
+
const hasItalic = parts2.includes("italic");
|
|
440
|
+
const hasBold = parts2.includes("bold");
|
|
364
441
|
if (hasItalic && hasBold) return "italic bold";
|
|
365
442
|
if (hasBold) return "bold";
|
|
366
443
|
if (hasItalic) return "italic";
|
|
@@ -407,28 +484,33 @@
|
|
|
407
484
|
return toColorString(value.toJs({ pyproxies: [] }));
|
|
408
485
|
}
|
|
409
486
|
if (Array.isArray(value) || value && typeof value === "object" && "length" in value) {
|
|
410
|
-
const
|
|
411
|
-
if (
|
|
487
|
+
const parts2 = Array.from(value).slice(0, 3).map((part) => Number(part));
|
|
488
|
+
if (parts2.length !== 3 || parts2.some((part) => !Number.isFinite(part))) {
|
|
412
489
|
throw new Error(`bad color sequence: ${String(value)}`);
|
|
413
490
|
}
|
|
414
491
|
if (colorMode === 1) {
|
|
415
|
-
if (
|
|
492
|
+
if (parts2.some((part) => part < 0 || part > 1)) {
|
|
416
493
|
throw new Error(`bad color sequence: ${String(value)}`);
|
|
417
494
|
}
|
|
418
|
-
const [r, g, b] =
|
|
495
|
+
const [r, g, b] = parts2.map((part) => Math.round(part * 255));
|
|
419
496
|
return `rgb(${r}, ${g}, ${b})`;
|
|
420
497
|
}
|
|
421
498
|
if (colorMode === 255) {
|
|
422
|
-
if (
|
|
499
|
+
if (parts2.some((part) => part < 0 || part > 255)) {
|
|
423
500
|
throw new Error(`bad color sequence: ${String(value)}`);
|
|
424
501
|
}
|
|
425
|
-
const [r, g, b] =
|
|
502
|
+
const [r, g, b] = parts2.map((part) => Math.round(part));
|
|
426
503
|
return `rgb(${r}, ${g}, ${b})`;
|
|
427
504
|
}
|
|
428
505
|
throw new Error(`bad color sequence: ${String(value)}`);
|
|
429
506
|
}
|
|
430
507
|
return "#000000";
|
|
431
508
|
};
|
|
509
|
+
const colorFromArgs = (args) => {
|
|
510
|
+
if (args.length === 0) return null;
|
|
511
|
+
if (args.length === 1) return toColorString(args[0]);
|
|
512
|
+
return toColorString(args.map((part) => toPlainNumber(part, Number.NaN)));
|
|
513
|
+
};
|
|
432
514
|
const sleep = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
433
515
|
const clearQueue = () => {
|
|
434
516
|
queueGeneration += 1;
|
|
@@ -436,7 +518,7 @@
|
|
|
436
518
|
queueRunning = false;
|
|
437
519
|
};
|
|
438
520
|
const enqueueOperation = (fn) => {
|
|
439
|
-
if (delayMs <= 0) {
|
|
521
|
+
if (delayMs <= 0 || tracerN === 0) {
|
|
440
522
|
fn();
|
|
441
523
|
return;
|
|
442
524
|
}
|
|
@@ -504,7 +586,36 @@
|
|
|
504
586
|
}
|
|
505
587
|
context.restore();
|
|
506
588
|
};
|
|
589
|
+
const drawCursor = (cursor) => {
|
|
590
|
+
const points = TURTLE_SHAPES[cursor.shapeName] ?? TURTLE_SHAPES[DEFAULT_SHAPE];
|
|
591
|
+
if (Array.isArray(points) && points.length === 0) return;
|
|
592
|
+
context.save();
|
|
593
|
+
context.translate(toCanvasX(cursor.x), toCanvasY(cursor.y));
|
|
594
|
+
context.rotate(-toRadians(cursor.heading + (cursor.tilt || 0)));
|
|
595
|
+
context.scale(cursor.stretchLen || 1, cursor.stretchWid || 1);
|
|
596
|
+
context.fillStyle = cursor.fillColor;
|
|
597
|
+
context.strokeStyle = cursor.penColor;
|
|
598
|
+
context.lineWidth = cursor.outline || 1;
|
|
599
|
+
if (cursor.shapeName === "circle" || !Array.isArray(points)) {
|
|
600
|
+
context.beginPath();
|
|
601
|
+
context.arc(0, 0, 10, 0, 2 * Math.PI);
|
|
602
|
+
} else {
|
|
603
|
+
context.beginPath();
|
|
604
|
+
context.moveTo(points[0][0], points[0][1]);
|
|
605
|
+
for (let i = 1; i < points.length; i++) {
|
|
606
|
+
context.lineTo(points[i][0], points[i][1]);
|
|
607
|
+
}
|
|
608
|
+
context.closePath();
|
|
609
|
+
}
|
|
610
|
+
context.fill();
|
|
611
|
+
context.stroke();
|
|
612
|
+
context.restore();
|
|
613
|
+
};
|
|
507
614
|
const drawPathSegment = (path) => {
|
|
615
|
+
if (path?.stamp) {
|
|
616
|
+
drawCursor(path.stamp);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
508
619
|
if (!path?.points?.length) return;
|
|
509
620
|
if (path.fill) {
|
|
510
621
|
context.beginPath();
|
|
@@ -565,32 +676,21 @@
|
|
|
565
676
|
};
|
|
566
677
|
const drawTurtleShape = (pen) => {
|
|
567
678
|
if (!pen.renderedTurtleVisible) return;
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
const points = TURTLE_SHAPES[shapeName] || TURTLE_SHAPES[DEFAULT_SHAPE];
|
|
582
|
-
context.beginPath();
|
|
583
|
-
context.moveTo(points[0][0], points[0][1]);
|
|
584
|
-
for (let i = 1; i < points.length; i++) {
|
|
585
|
-
context.lineTo(points[i][0], points[i][1]);
|
|
586
|
-
}
|
|
587
|
-
context.closePath();
|
|
588
|
-
context.fill();
|
|
589
|
-
context.stroke();
|
|
590
|
-
}
|
|
591
|
-
context.restore();
|
|
679
|
+
drawCursor({
|
|
680
|
+
x: pen.renderedX,
|
|
681
|
+
y: pen.renderedY,
|
|
682
|
+
heading: pen.renderedHeading,
|
|
683
|
+
shapeName: pen.shapeName || DEFAULT_SHAPE,
|
|
684
|
+
// CPython fills the cursor with fillcolor and outlines it with pencolor.
|
|
685
|
+
fillColor: pen.shapeFillColor,
|
|
686
|
+
penColor: pen.shapeColor,
|
|
687
|
+
stretchLen: pen.stretchLen,
|
|
688
|
+
stretchWid: pen.stretchWid,
|
|
689
|
+
outline: pen.outlineWidth,
|
|
690
|
+
tilt: pen.tilt
|
|
691
|
+
});
|
|
592
692
|
};
|
|
593
|
-
const
|
|
693
|
+
const render = () => {
|
|
594
694
|
if (!active) return;
|
|
595
695
|
if (!setupCanvasResolution()) return;
|
|
596
696
|
drawBackground();
|
|
@@ -601,6 +701,12 @@
|
|
|
601
701
|
drawTurtleShape(pen);
|
|
602
702
|
}
|
|
603
703
|
};
|
|
704
|
+
const draw = () => {
|
|
705
|
+
if (tracerN === 0) return;
|
|
706
|
+
tracerCounter += 1;
|
|
707
|
+
if (tracerN > 1 && tracerCounter % tracerN !== 0) return;
|
|
708
|
+
render();
|
|
709
|
+
};
|
|
604
710
|
const createTurtlePen = () => {
|
|
605
711
|
let x = 0;
|
|
606
712
|
let y = 0;
|
|
@@ -609,12 +715,15 @@
|
|
|
609
715
|
let turtleVisible = true;
|
|
610
716
|
let strokeColor = "#000000";
|
|
611
717
|
let fillColor = "#000000";
|
|
718
|
+
let penWidth = DEFAULT_LINE_WIDTH;
|
|
612
719
|
let fontSize = DEFAULT_FONT_SIZE;
|
|
613
720
|
let currentFontFamily = "Arial";
|
|
614
721
|
let currentFontStyle = "normal";
|
|
615
722
|
let filling = false;
|
|
616
723
|
let fillPath = null;
|
|
617
724
|
let currentPath = null;
|
|
725
|
+
let degreesPerUnit = 1;
|
|
726
|
+
const undoStack = [];
|
|
618
727
|
const pen = {
|
|
619
728
|
paths: [],
|
|
620
729
|
renderedX: 0,
|
|
@@ -622,12 +731,29 @@
|
|
|
622
731
|
renderedHeading: 0,
|
|
623
732
|
renderedTurtleVisible: true,
|
|
624
733
|
shapeColor: "#000000",
|
|
625
|
-
|
|
734
|
+
shapeFillColor: "#000000",
|
|
735
|
+
shapeName: DEFAULT_SHAPE,
|
|
736
|
+
stretchLen: 1,
|
|
737
|
+
stretchWid: 1,
|
|
738
|
+
outlineWidth: 1,
|
|
739
|
+
tilt: 0
|
|
740
|
+
};
|
|
741
|
+
const toUserAngle = (internal) => normalizeAngle(screenMode === "logo" ? 90 - internal : internal) / degreesPerUnit;
|
|
742
|
+
const toInternalAngle = (user) => {
|
|
743
|
+
const degrees = Number(user || 0) * degreesPerUnit;
|
|
744
|
+
return normalizeAngle(screenMode === "logo" ? 90 - degrees : degrees);
|
|
745
|
+
};
|
|
746
|
+
const pushUndo = (entry) => {
|
|
747
|
+
if (undoBufferSize === 0) return;
|
|
748
|
+
undoStack.push(entry);
|
|
749
|
+
if (undoBufferSize > 0 && undoStack.length > undoBufferSize) {
|
|
750
|
+
undoStack.shift();
|
|
751
|
+
}
|
|
626
752
|
};
|
|
627
753
|
const makePath = (overrides = {}) => ({
|
|
628
754
|
down: penDown,
|
|
629
755
|
stroke: strokeColor,
|
|
630
|
-
lineWidth:
|
|
756
|
+
lineWidth: penWidth,
|
|
631
757
|
fontsize: fontSize,
|
|
632
758
|
fontfamily: currentFontFamily,
|
|
633
759
|
fontstyle: currentFontStyle,
|
|
@@ -642,10 +768,7 @@
|
|
|
642
768
|
return currentPath;
|
|
643
769
|
};
|
|
644
770
|
const commitStyleToNewPath = () => {
|
|
645
|
-
currentPath = makePath(
|
|
646
|
-
down: penDown,
|
|
647
|
-
lineWidth: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH
|
|
648
|
-
});
|
|
771
|
+
currentPath = makePath();
|
|
649
772
|
pen.paths.push(currentPath);
|
|
650
773
|
return currentPath;
|
|
651
774
|
};
|
|
@@ -659,144 +782,206 @@
|
|
|
659
782
|
pendown: penDown,
|
|
660
783
|
pencolor: strokeColor,
|
|
661
784
|
fillcolor: fillColor,
|
|
662
|
-
pensize:
|
|
785
|
+
pensize: penWidth,
|
|
663
786
|
speed: turtleSpeed,
|
|
664
|
-
shown: turtleVisible
|
|
787
|
+
shown: turtleVisible,
|
|
788
|
+
stretchfactor: [pen.stretchWid, pen.stretchLen],
|
|
789
|
+
outline: pen.outlineWidth,
|
|
790
|
+
tilt: pen.tilt
|
|
665
791
|
});
|
|
666
792
|
beginCurrentPath();
|
|
667
|
-
const
|
|
668
|
-
|
|
793
|
+
const moveTo = (nextX, nextY) => {
|
|
794
|
+
const prevX = x;
|
|
795
|
+
const prevY = y;
|
|
669
796
|
const path = ensurePath();
|
|
670
797
|
const fill = filling ? fillPath : null;
|
|
671
|
-
const length = Number(distance) || 0;
|
|
672
|
-
const nextX = x + length * Math.cos(toRadians(heading));
|
|
673
|
-
const nextY = y + length * Math.sin(toRadians(heading));
|
|
674
798
|
x = nextX;
|
|
675
799
|
y = nextY;
|
|
676
800
|
const point = { x, y, move: !penDown };
|
|
801
|
+
pushUndo(() => {
|
|
802
|
+
x = prevX;
|
|
803
|
+
y = prevY;
|
|
804
|
+
if (path.points[path.points.length - 1] === point) path.points.pop();
|
|
805
|
+
if (fill) fill.points.pop();
|
|
806
|
+
pen.renderedX = prevX;
|
|
807
|
+
pen.renderedY = prevY;
|
|
808
|
+
});
|
|
677
809
|
enqueueOperation(() => {
|
|
678
810
|
pen.renderedX = point.x;
|
|
679
811
|
pen.renderedY = point.y;
|
|
680
812
|
path.points.push(point);
|
|
681
813
|
if (fill) {
|
|
682
|
-
fill.points.push({ x, y });
|
|
814
|
+
fill.points.push({ x: point.x, y: point.y });
|
|
683
815
|
}
|
|
684
816
|
draw();
|
|
685
817
|
});
|
|
686
818
|
};
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
heading = normalizeAngle(
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
819
|
+
const turnTo = (nextHeading) => {
|
|
820
|
+
const prevHeading = heading;
|
|
821
|
+
heading = normalizeAngle(nextHeading);
|
|
822
|
+
const target = heading;
|
|
823
|
+
pushUndo(() => {
|
|
824
|
+
heading = prevHeading;
|
|
825
|
+
pen.renderedHeading = prevHeading;
|
|
694
826
|
});
|
|
695
|
-
};
|
|
696
|
-
const left = (angle) => {
|
|
697
|
-
heading = normalizeAngle(heading + Number(angle || 0));
|
|
698
|
-
const nextHeading = heading;
|
|
699
827
|
enqueueOperation(() => {
|
|
700
|
-
pen.renderedHeading =
|
|
828
|
+
pen.renderedHeading = target;
|
|
701
829
|
draw();
|
|
702
830
|
});
|
|
703
831
|
};
|
|
832
|
+
const forward = (distance2) => {
|
|
833
|
+
if (!ensureContext()) return;
|
|
834
|
+
const length = toPlainNumber(distance2, 0) || 0;
|
|
835
|
+
moveTo(
|
|
836
|
+
x + length * Math.cos(toRadians(heading)),
|
|
837
|
+
y + length * Math.sin(toRadians(heading))
|
|
838
|
+
);
|
|
839
|
+
};
|
|
840
|
+
const backward = (distance2) => forward(-toPlainNumber(distance2, 0));
|
|
841
|
+
const right = (angle) => turnTo(heading - toPlainNumber(angle, 0) * degreesPerUnit);
|
|
842
|
+
const left = (angle) => turnTo(heading + toPlainNumber(angle, 0) * degreesPerUnit);
|
|
704
843
|
const penup = () => {
|
|
844
|
+
const previous = penDown;
|
|
705
845
|
penDown = false;
|
|
706
846
|
commitStyleToNewPath();
|
|
847
|
+
pushUndo(() => {
|
|
848
|
+
penDown = previous;
|
|
849
|
+
commitStyleToNewPath();
|
|
850
|
+
});
|
|
707
851
|
draw();
|
|
708
852
|
};
|
|
709
853
|
const pendownFn = () => {
|
|
854
|
+
const previous = penDown;
|
|
710
855
|
penDown = true;
|
|
711
856
|
commitStyleToNewPath();
|
|
857
|
+
pushUndo(() => {
|
|
858
|
+
penDown = previous;
|
|
859
|
+
commitStyleToNewPath();
|
|
860
|
+
});
|
|
712
861
|
draw();
|
|
713
862
|
};
|
|
863
|
+
const toPoint = (a, b) => {
|
|
864
|
+
if (b !== void 0 && b !== null) {
|
|
865
|
+
return [toPlainNumber(a, 0), toPlainNumber(b, 0)];
|
|
866
|
+
}
|
|
867
|
+
const source = toSequence(a);
|
|
868
|
+
if (source && source.length >= 2) {
|
|
869
|
+
return [toPlainNumber(source[0], 0), toPlainNumber(source[1], 0)];
|
|
870
|
+
}
|
|
871
|
+
const obj = a && typeof a === "object" ? a : null;
|
|
872
|
+
if (obj && ("x" in obj || "y" in obj)) {
|
|
873
|
+
return [toPlainNumber(obj.x, 0), toPlainNumber(obj.y, 0)];
|
|
874
|
+
}
|
|
875
|
+
return [toPlainNumber(a, 0), 0];
|
|
876
|
+
};
|
|
714
877
|
const goto_ = (a, b) => {
|
|
715
878
|
if (!ensureContext()) return;
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
if (b === void 0 && (Array.isArray(a) || a && typeof a === "object")) {
|
|
719
|
-
const source = a && typeof a.toJs === "function" ? a.toJs({ pyproxies: [] }) : a;
|
|
720
|
-
nextX = Number(source?.[0] ?? source?.x ?? 0);
|
|
721
|
-
nextY = Number(source?.[1] ?? source?.y ?? 0);
|
|
722
|
-
}
|
|
723
|
-
x = Number.isFinite(nextX) ? nextX : 0;
|
|
724
|
-
y = Number.isFinite(nextY) ? nextY : 0;
|
|
725
|
-
const path = ensurePath();
|
|
726
|
-
const fill = filling ? fillPath : null;
|
|
727
|
-
const point = { x, y, move: !penDown };
|
|
728
|
-
enqueueOperation(() => {
|
|
729
|
-
pen.renderedX = point.x;
|
|
730
|
-
pen.renderedY = point.y;
|
|
731
|
-
path.points.push(point);
|
|
732
|
-
if (fill) {
|
|
733
|
-
fill.points.push({ x, y });
|
|
734
|
-
}
|
|
735
|
-
draw();
|
|
736
|
-
});
|
|
879
|
+
const [nextX, nextY] = toPoint(a, b);
|
|
880
|
+
moveTo(nextX, nextY);
|
|
737
881
|
};
|
|
738
|
-
const setx = (value) => goto_(
|
|
739
|
-
const sety = (value) => goto_(x,
|
|
882
|
+
const setx = (value) => goto_(toPlainNumber(value, 0), y);
|
|
883
|
+
const sety = (value) => goto_(x, toPlainNumber(value, 0));
|
|
740
884
|
const position = () => [x, y];
|
|
741
885
|
const xcor = () => x;
|
|
742
886
|
const ycor = () => y;
|
|
743
|
-
const heading_ = () => heading;
|
|
744
|
-
const setheading = (angle) =>
|
|
745
|
-
heading = normalizeAngle(angle);
|
|
746
|
-
const nextHeading = heading;
|
|
747
|
-
enqueueOperation(() => {
|
|
748
|
-
pen.renderedHeading = nextHeading;
|
|
749
|
-
draw();
|
|
750
|
-
});
|
|
751
|
-
};
|
|
887
|
+
const heading_ = () => toUserAngle(heading);
|
|
888
|
+
const setheading = (angle) => turnTo(toInternalAngle(angle));
|
|
752
889
|
const home = () => {
|
|
753
890
|
goto_(0, 0);
|
|
754
891
|
setheading(0);
|
|
755
892
|
};
|
|
756
893
|
const towards = (tx, ty) => {
|
|
757
|
-
const
|
|
758
|
-
const
|
|
894
|
+
const [px, py] = toPoint(tx, ty);
|
|
895
|
+
const dx = px - x;
|
|
896
|
+
const dy = py - y;
|
|
759
897
|
if (!Number.isFinite(dx) || !Number.isFinite(dy)) return 0;
|
|
760
|
-
return
|
|
898
|
+
return toUserAngle(Math.atan2(dy, dx) * 180 / Math.PI);
|
|
899
|
+
};
|
|
900
|
+
const distance = (tx, ty) => {
|
|
901
|
+
const [px, py] = toPoint(tx, ty);
|
|
902
|
+
return Math.hypot(px - x, py - y);
|
|
903
|
+
};
|
|
904
|
+
const degreesFn = (fullcircle = 360) => {
|
|
905
|
+
const circle2 = toPlainNumber(fullcircle, 360) || 360;
|
|
906
|
+
degreesPerUnit = 360 / circle2;
|
|
761
907
|
};
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
908
|
+
const radians = () => degreesFn(2 * Math.PI);
|
|
909
|
+
const speed = (value = void 0) => {
|
|
910
|
+
if (value === void 0 || value === null) return turtleSpeed;
|
|
911
|
+
const name = typeof value === "string" ? value.trim().toLowerCase() : null;
|
|
912
|
+
let numeric = name !== null && name in SPEED_NAMES ? SPEED_NAMES[name] : toPlainNumber(value, Number.NaN);
|
|
913
|
+
if (!Number.isFinite(numeric)) numeric = DEFAULT_SPEED;
|
|
914
|
+
if (numeric < 0.5 || numeric > 10.5) numeric = 0;
|
|
915
|
+
turtleSpeed = Math.round(numeric);
|
|
765
916
|
delayMs = turtleSpeed <= 0 ? 0 : Math.max(0, Math.round(300 / turtleSpeed));
|
|
766
|
-
return
|
|
917
|
+
return turtleSpeed;
|
|
767
918
|
};
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
919
|
+
const setStroke = (value) => {
|
|
920
|
+
strokeColor = value;
|
|
921
|
+
pen.shapeColor = value;
|
|
922
|
+
};
|
|
923
|
+
const setFill = (value) => {
|
|
924
|
+
fillColor = value;
|
|
925
|
+
pen.shapeFillColor = value;
|
|
775
926
|
if (filling && fillPath) {
|
|
776
|
-
fillPath.fillstyle =
|
|
927
|
+
fillPath.fillstyle = value;
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
const color = (...args) => {
|
|
931
|
+
if (args.length === 0) return [strokeColor, fillColor];
|
|
932
|
+
const previous = [strokeColor, fillColor];
|
|
933
|
+
if (args.length === 2) {
|
|
934
|
+
setStroke(toColorString(args[0]));
|
|
935
|
+
setFill(toColorString(args[1]));
|
|
936
|
+
} else {
|
|
937
|
+
const value = colorFromArgs(args);
|
|
938
|
+
setStroke(value);
|
|
939
|
+
setFill(value);
|
|
777
940
|
}
|
|
941
|
+
commitStyleToNewPath();
|
|
942
|
+
pushUndo(() => {
|
|
943
|
+
setStroke(previous[0]);
|
|
944
|
+
setFill(previous[1]);
|
|
945
|
+
commitStyleToNewPath();
|
|
946
|
+
});
|
|
778
947
|
draw();
|
|
779
948
|
};
|
|
780
|
-
const pencolor = (
|
|
781
|
-
|
|
782
|
-
|
|
949
|
+
const pencolor = (...args) => {
|
|
950
|
+
if (args.length === 0) return strokeColor;
|
|
951
|
+
const previous = strokeColor;
|
|
952
|
+
setStroke(colorFromArgs(args));
|
|
783
953
|
commitStyleToNewPath();
|
|
954
|
+
pushUndo(() => {
|
|
955
|
+
setStroke(previous);
|
|
956
|
+
commitStyleToNewPath();
|
|
957
|
+
});
|
|
784
958
|
draw();
|
|
785
959
|
};
|
|
786
|
-
const fillcolor = (
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
}
|
|
960
|
+
const fillcolor = (...args) => {
|
|
961
|
+
if (args.length === 0) return fillColor;
|
|
962
|
+
const previous = fillColor;
|
|
963
|
+
setFill(colorFromArgs(args));
|
|
791
964
|
commitStyleToNewPath();
|
|
965
|
+
pushUndo(() => {
|
|
966
|
+
setFill(previous);
|
|
967
|
+
commitStyleToNewPath();
|
|
968
|
+
});
|
|
792
969
|
draw();
|
|
793
970
|
};
|
|
794
|
-
const pensize = (value) => {
|
|
795
|
-
|
|
971
|
+
const pensize = (value = void 0) => {
|
|
972
|
+
if (value === void 0 || value === null) return penWidth;
|
|
973
|
+
const previous = penWidth;
|
|
974
|
+
penWidth = Math.max(1, toPlainNumber(value, DEFAULT_LINE_WIDTH));
|
|
796
975
|
commitStyleToNewPath();
|
|
976
|
+
pushUndo(() => {
|
|
977
|
+
penWidth = previous;
|
|
978
|
+
commitStyleToNewPath();
|
|
979
|
+
});
|
|
797
980
|
draw();
|
|
981
|
+
return penWidth;
|
|
798
982
|
};
|
|
799
|
-
const width = (value) => pensize(value);
|
|
983
|
+
const width = (value = void 0) => pensize(value);
|
|
984
|
+
const isfilling = () => filling;
|
|
800
985
|
const begin_fill = () => {
|
|
801
986
|
if (filling) return;
|
|
802
987
|
filling = true;
|
|
@@ -804,48 +989,75 @@
|
|
|
804
989
|
down: false,
|
|
805
990
|
fill: true,
|
|
806
991
|
stroke: "transparent",
|
|
807
|
-
lineWidth: 1,
|
|
808
992
|
fillstyle: fillColor
|
|
809
993
|
});
|
|
810
994
|
pen.paths.push(fillPath);
|
|
995
|
+
commitStyleToNewPath();
|
|
996
|
+
const started = fillPath;
|
|
997
|
+
pushUndo(() => {
|
|
998
|
+
filling = false;
|
|
999
|
+
fillPath = null;
|
|
1000
|
+
const index = pen.paths.indexOf(started);
|
|
1001
|
+
if (index >= 0) pen.paths.splice(index, 1);
|
|
1002
|
+
});
|
|
811
1003
|
draw();
|
|
812
1004
|
};
|
|
813
1005
|
const end_fill = () => {
|
|
1006
|
+
if (!filling) return;
|
|
1007
|
+
const finished = fillPath;
|
|
814
1008
|
filling = false;
|
|
815
1009
|
fillPath = null;
|
|
816
1010
|
commitStyleToNewPath();
|
|
1011
|
+
pushUndo(() => {
|
|
1012
|
+
filling = true;
|
|
1013
|
+
fillPath = finished;
|
|
1014
|
+
});
|
|
817
1015
|
draw();
|
|
818
1016
|
};
|
|
819
|
-
const dot = (size =
|
|
1017
|
+
const dot = (size = void 0, ...colorArgs) => {
|
|
1018
|
+
const requested = toPlainNumber(size, Number.NaN);
|
|
1019
|
+
const diameter = Number.isFinite(requested) ? Math.max(1, requested) : Math.max(penWidth + 4, penWidth * 2);
|
|
1020
|
+
const dotColor = colorArgs.length > 0 ? colorFromArgs(colorArgs) : strokeColor;
|
|
820
1021
|
const segment = makePath({
|
|
821
1022
|
down: false,
|
|
822
1023
|
fill: false,
|
|
823
|
-
stroke:
|
|
824
|
-
fillstyle:
|
|
825
|
-
points: [
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
color: colorValue ? toColorString(colorValue) : strokeColor
|
|
831
|
-
}
|
|
832
|
-
]
|
|
1024
|
+
stroke: dotColor,
|
|
1025
|
+
fillstyle: dotColor,
|
|
1026
|
+
points: [{ x, y, dotRadius: diameter / 2, color: dotColor }]
|
|
1027
|
+
});
|
|
1028
|
+
pushUndo(() => {
|
|
1029
|
+
const index = pen.paths.indexOf(segment);
|
|
1030
|
+
if (index >= 0) pen.paths.splice(index, 1);
|
|
833
1031
|
});
|
|
834
1032
|
enqueueOperation(() => {
|
|
835
1033
|
pen.paths.push(segment);
|
|
836
1034
|
draw();
|
|
837
1035
|
});
|
|
838
1036
|
};
|
|
839
|
-
const circle = (radius, steps =
|
|
840
|
-
const numericRadius =
|
|
841
|
-
const
|
|
842
|
-
const
|
|
843
|
-
const
|
|
844
|
-
const
|
|
1037
|
+
const circle = (radius, extent = void 0, steps = void 0) => {
|
|
1038
|
+
const numericRadius = toPlainNumber(radius, 0) || 0;
|
|
1039
|
+
const fullCircle = 360 / degreesPerUnit;
|
|
1040
|
+
const extentUnits = toPlainNumber(extent, fullCircle);
|
|
1041
|
+
const extentDegrees = extentUnits * degreesPerUnit;
|
|
1042
|
+
const requestedSteps = toPlainNumber(steps, Number.NaN);
|
|
1043
|
+
const stepCount = Number.isFinite(requestedSteps) ? Math.max(1, Math.trunc(requestedSteps)) : (
|
|
1044
|
+
// CPython scales the step count with the arc's size.
|
|
1045
|
+
1 + Math.trunc(
|
|
1046
|
+
Math.min(11 + Math.abs(numericRadius) / 6, 59) * (Math.abs(extentUnits) / fullCircle)
|
|
1047
|
+
)
|
|
1048
|
+
);
|
|
1049
|
+
let stepTurn = extentDegrees / stepCount;
|
|
1050
|
+
let stepLength = 2 * numericRadius * Math.sin(toRadians(stepTurn / 2));
|
|
1051
|
+
if (numericRadius < 0) {
|
|
1052
|
+
stepLength = -stepLength;
|
|
1053
|
+
stepTurn = -stepTurn;
|
|
1054
|
+
}
|
|
1055
|
+
turnTo(heading + stepTurn / 2);
|
|
845
1056
|
for (let index = 0; index < stepCount; index += 1) {
|
|
846
1057
|
forward(stepLength);
|
|
847
|
-
|
|
1058
|
+
turnTo(heading + stepTurn);
|
|
848
1059
|
}
|
|
1060
|
+
turnTo(heading - stepTurn / 2);
|
|
849
1061
|
};
|
|
850
1062
|
const write = (text, move = false, align = "left", font = null) => {
|
|
851
1063
|
let writeText = text;
|
|
@@ -916,6 +1128,10 @@
|
|
|
916
1128
|
}
|
|
917
1129
|
commitStyleToNewPath();
|
|
918
1130
|
}
|
|
1131
|
+
pushUndo(() => {
|
|
1132
|
+
const index = pen.paths.indexOf(segment);
|
|
1133
|
+
if (index >= 0) pen.paths.splice(index, 1);
|
|
1134
|
+
});
|
|
919
1135
|
enqueueOperation(() => {
|
|
920
1136
|
pen.paths.push(segment);
|
|
921
1137
|
draw();
|
|
@@ -949,13 +1165,97 @@
|
|
|
949
1165
|
if (name === void 0 || name === null) {
|
|
950
1166
|
return pen.shapeName;
|
|
951
1167
|
}
|
|
952
|
-
const nameStr = toPlainString(name,
|
|
953
|
-
if (nameStr in TURTLE_SHAPES) {
|
|
954
|
-
|
|
1168
|
+
const nameStr = toPlainString(name, "").toLowerCase().trim();
|
|
1169
|
+
if (!(nameStr in TURTLE_SHAPES)) {
|
|
1170
|
+
throw new Error(
|
|
1171
|
+
`There is no shape named ${nameStr} (available: ${Object.keys(
|
|
1172
|
+
TURTLE_SHAPES
|
|
1173
|
+
).join(", ")})`
|
|
1174
|
+
);
|
|
955
1175
|
}
|
|
1176
|
+
pen.shapeName = nameStr;
|
|
956
1177
|
draw();
|
|
957
1178
|
return pen.shapeName;
|
|
958
1179
|
};
|
|
1180
|
+
const shapesize = (stretchWid = void 0, stretchLen = void 0, outline = void 0) => {
|
|
1181
|
+
if (stretchWid === void 0 && stretchLen === void 0 && outline === void 0) {
|
|
1182
|
+
return [pen.stretchWid, pen.stretchLen, pen.outlineWidth];
|
|
1183
|
+
}
|
|
1184
|
+
const wid = toPlainNumber(stretchWid, Number.NaN);
|
|
1185
|
+
const len = toPlainNumber(stretchLen, Number.NaN);
|
|
1186
|
+
if (Number.isFinite(wid)) {
|
|
1187
|
+
pen.stretchWid = wid;
|
|
1188
|
+
pen.stretchLen = Number.isFinite(len) ? len : wid;
|
|
1189
|
+
} else if (Number.isFinite(len)) {
|
|
1190
|
+
pen.stretchLen = len;
|
|
1191
|
+
}
|
|
1192
|
+
const outlineWidth = toPlainNumber(outline, Number.NaN);
|
|
1193
|
+
if (Number.isFinite(outlineWidth)) pen.outlineWidth = outlineWidth;
|
|
1194
|
+
draw();
|
|
1195
|
+
return [pen.stretchWid, pen.stretchLen, pen.outlineWidth];
|
|
1196
|
+
};
|
|
1197
|
+
const settiltangle = (angle) => {
|
|
1198
|
+
pen.tilt = toPlainNumber(angle, 0) * degreesPerUnit;
|
|
1199
|
+
draw();
|
|
1200
|
+
};
|
|
1201
|
+
const tilt = (angle) => settiltangle(pen.tilt / degreesPerUnit + toPlainNumber(angle, 0));
|
|
1202
|
+
const tiltangle = (angle = void 0) => {
|
|
1203
|
+
if (angle === void 0 || angle === null) {
|
|
1204
|
+
return normalizeAngle(pen.tilt) / degreesPerUnit;
|
|
1205
|
+
}
|
|
1206
|
+
settiltangle(angle);
|
|
1207
|
+
return normalizeAngle(pen.tilt) / degreesPerUnit;
|
|
1208
|
+
};
|
|
1209
|
+
const cursorSnapshot = () => ({
|
|
1210
|
+
x,
|
|
1211
|
+
y,
|
|
1212
|
+
heading,
|
|
1213
|
+
shapeName: pen.shapeName,
|
|
1214
|
+
fillColor,
|
|
1215
|
+
penColor: strokeColor,
|
|
1216
|
+
stretchLen: pen.stretchLen,
|
|
1217
|
+
stretchWid: pen.stretchWid,
|
|
1218
|
+
outline: pen.outlineWidth,
|
|
1219
|
+
tilt: pen.tilt
|
|
1220
|
+
});
|
|
1221
|
+
const stamp = () => {
|
|
1222
|
+
stampCounter += 1;
|
|
1223
|
+
const segment = { stampId: stampCounter, stamp: cursorSnapshot() };
|
|
1224
|
+
pushUndo(() => {
|
|
1225
|
+
const index = pen.paths.indexOf(segment);
|
|
1226
|
+
if (index >= 0) pen.paths.splice(index, 1);
|
|
1227
|
+
});
|
|
1228
|
+
enqueueOperation(() => {
|
|
1229
|
+
pen.paths.push(segment);
|
|
1230
|
+
draw();
|
|
1231
|
+
});
|
|
1232
|
+
return stampCounter;
|
|
1233
|
+
};
|
|
1234
|
+
const clearstamp = (stampId) => {
|
|
1235
|
+
const wanted = toPlainNumber(stampId, Number.NaN);
|
|
1236
|
+
const index = pen.paths.findIndex((path) => path.stampId === wanted);
|
|
1237
|
+
if (index >= 0) pen.paths.splice(index, 1);
|
|
1238
|
+
draw();
|
|
1239
|
+
};
|
|
1240
|
+
const clearstamps = (n = void 0) => {
|
|
1241
|
+
const count = toPlainNumber(n, Number.NaN);
|
|
1242
|
+
let stamps = pen.paths.filter((path) => path.stampId !== void 0);
|
|
1243
|
+
if (Number.isFinite(count) && count !== 0) {
|
|
1244
|
+
stamps = count > 0 ? stamps.slice(0, count) : stamps.slice(count);
|
|
1245
|
+
}
|
|
1246
|
+
for (const entry of stamps) {
|
|
1247
|
+
const index = pen.paths.indexOf(entry);
|
|
1248
|
+
if (index >= 0) pen.paths.splice(index, 1);
|
|
1249
|
+
}
|
|
1250
|
+
draw();
|
|
1251
|
+
};
|
|
1252
|
+
const undo = () => {
|
|
1253
|
+
const entry = undoStack.pop();
|
|
1254
|
+
if (!entry) return;
|
|
1255
|
+
entry();
|
|
1256
|
+
draw();
|
|
1257
|
+
};
|
|
1258
|
+
const undobufferentries = () => undoStack.length;
|
|
959
1259
|
const penFn = (options = void 0) => {
|
|
960
1260
|
if (options === void 0) {
|
|
961
1261
|
return getPenState();
|
|
@@ -966,16 +1266,16 @@
|
|
|
966
1266
|
penDown = !!source.pendown;
|
|
967
1267
|
}
|
|
968
1268
|
if ("pencolor" in source) {
|
|
969
|
-
|
|
1269
|
+
setStroke(toColorString(source.pencolor));
|
|
970
1270
|
}
|
|
971
1271
|
if ("fillcolor" in source) {
|
|
972
|
-
|
|
1272
|
+
setFill(toColorString(source.fillcolor));
|
|
973
1273
|
}
|
|
974
1274
|
if ("pensize" in source) {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
);
|
|
1275
|
+
penWidth = Math.max(1, toPlainNumber(source.pensize, penWidth));
|
|
1276
|
+
}
|
|
1277
|
+
if ("speed" in source) {
|
|
1278
|
+
speed(source.speed);
|
|
979
1279
|
}
|
|
980
1280
|
if ("shown" in source) {
|
|
981
1281
|
turtleVisible = !!source.shown;
|
|
@@ -989,6 +1289,8 @@
|
|
|
989
1289
|
pen.paths = [];
|
|
990
1290
|
currentPath = null;
|
|
991
1291
|
fillPath = null;
|
|
1292
|
+
filling = false;
|
|
1293
|
+
undoStack.length = 0;
|
|
992
1294
|
beginCurrentPath();
|
|
993
1295
|
draw();
|
|
994
1296
|
};
|
|
@@ -996,22 +1298,30 @@
|
|
|
996
1298
|
pen.paths = [];
|
|
997
1299
|
currentPath = null;
|
|
998
1300
|
fillPath = null;
|
|
1301
|
+
undoStack.length = 0;
|
|
999
1302
|
x = 0;
|
|
1000
1303
|
y = 0;
|
|
1001
|
-
heading = 0;
|
|
1304
|
+
heading = screenMode === "logo" ? 90 : 0;
|
|
1002
1305
|
pen.renderedX = 0;
|
|
1003
1306
|
pen.renderedY = 0;
|
|
1004
|
-
pen.renderedHeading =
|
|
1307
|
+
pen.renderedHeading = heading;
|
|
1005
1308
|
penDown = true;
|
|
1006
1309
|
turtleVisible = true;
|
|
1007
1310
|
pen.renderedTurtleVisible = true;
|
|
1008
1311
|
strokeColor = "#000000";
|
|
1009
1312
|
fillColor = "#000000";
|
|
1313
|
+
penWidth = DEFAULT_LINE_WIDTH;
|
|
1010
1314
|
pen.shapeColor = "#000000";
|
|
1315
|
+
pen.shapeFillColor = "#000000";
|
|
1011
1316
|
pen.shapeName = DEFAULT_SHAPE;
|
|
1317
|
+
pen.stretchLen = 1;
|
|
1318
|
+
pen.stretchWid = 1;
|
|
1319
|
+
pen.outlineWidth = 1;
|
|
1320
|
+
pen.tilt = 0;
|
|
1012
1321
|
fontSize = DEFAULT_FONT_SIZE;
|
|
1013
1322
|
currentFontFamily = "Arial";
|
|
1014
1323
|
currentFontStyle = "normal";
|
|
1324
|
+
degreesPerUnit = 1;
|
|
1015
1325
|
filling = false;
|
|
1016
1326
|
beginCurrentPath();
|
|
1017
1327
|
};
|
|
@@ -1041,6 +1351,9 @@
|
|
|
1041
1351
|
ycor,
|
|
1042
1352
|
heading: heading_,
|
|
1043
1353
|
towards,
|
|
1354
|
+
distance,
|
|
1355
|
+
degrees: degreesFn,
|
|
1356
|
+
radians,
|
|
1044
1357
|
pendown: pendownFn,
|
|
1045
1358
|
pd: pendownFn,
|
|
1046
1359
|
down: pendownFn,
|
|
@@ -1057,6 +1370,7 @@
|
|
|
1057
1370
|
fillcolor,
|
|
1058
1371
|
begin_fill,
|
|
1059
1372
|
end_fill,
|
|
1373
|
+
filling: isfilling,
|
|
1060
1374
|
speed,
|
|
1061
1375
|
showturtle,
|
|
1062
1376
|
st: showturtle,
|
|
@@ -1064,14 +1378,24 @@
|
|
|
1064
1378
|
ht: hideturtle,
|
|
1065
1379
|
isvisible,
|
|
1066
1380
|
shape,
|
|
1381
|
+
shapesize,
|
|
1382
|
+
turtlesize: shapesize,
|
|
1383
|
+
tilt,
|
|
1384
|
+
tiltangle,
|
|
1385
|
+
settiltangle,
|
|
1386
|
+
stamp,
|
|
1387
|
+
clearstamp,
|
|
1388
|
+
clearstamps,
|
|
1389
|
+
undo,
|
|
1390
|
+
undobufferentries,
|
|
1067
1391
|
clear,
|
|
1068
1392
|
reset: resetPen
|
|
1069
1393
|
};
|
|
1070
1394
|
return { pen, api: api2, resetPen };
|
|
1071
1395
|
};
|
|
1072
|
-
const colormode = (
|
|
1073
|
-
if (
|
|
1074
|
-
const numeric = Number(
|
|
1396
|
+
const colormode = (mode2 = void 0) => {
|
|
1397
|
+
if (mode2 === void 0) return colorMode;
|
|
1398
|
+
const numeric = Number(mode2);
|
|
1075
1399
|
if (numeric !== 1 && numeric !== 255) {
|
|
1076
1400
|
throw new Error("colormode must be 1.0 or 255");
|
|
1077
1401
|
}
|
|
@@ -1142,7 +1466,138 @@
|
|
|
1142
1466
|
}
|
|
1143
1467
|
}
|
|
1144
1468
|
};
|
|
1469
|
+
const mode = (value = void 0) => {
|
|
1470
|
+
if (value === void 0 || value === null) return screenMode;
|
|
1471
|
+
const next = toPlainString(value, "").toLowerCase().trim();
|
|
1472
|
+
if (next !== "standard" && next !== "logo") {
|
|
1473
|
+
throw new Error(`No mode named ${next} (available: standard, logo)`);
|
|
1474
|
+
}
|
|
1475
|
+
screenMode = next;
|
|
1476
|
+
for (const penObj of allPenObjects) penObj.resetPen();
|
|
1477
|
+
draw();
|
|
1478
|
+
return screenMode;
|
|
1479
|
+
};
|
|
1480
|
+
const tracer = (n = void 0, delay2 = void 0) => {
|
|
1481
|
+
if (n === void 0 || n === null) return tracerN;
|
|
1482
|
+
tracerN = Math.max(0, Math.trunc(toPlainNumber(n, 1)));
|
|
1483
|
+
tracerCounter = 0;
|
|
1484
|
+
const nextDelay = toPlainNumber(delay2, Number.NaN);
|
|
1485
|
+
if (Number.isFinite(nextDelay)) delayMs = Math.max(0, nextDelay);
|
|
1486
|
+
if (tracerN !== 0) render();
|
|
1487
|
+
return tracerN;
|
|
1488
|
+
};
|
|
1489
|
+
const update = () => {
|
|
1490
|
+
tracerCounter = 0;
|
|
1491
|
+
render();
|
|
1492
|
+
};
|
|
1493
|
+
const delay = (ms = void 0) => {
|
|
1494
|
+
if (ms === void 0 || ms === null) return delayMs;
|
|
1495
|
+
delayMs = Math.max(0, toPlainNumber(ms, delayMs));
|
|
1496
|
+
return delayMs;
|
|
1497
|
+
};
|
|
1498
|
+
const setundobuffer = (size) => {
|
|
1499
|
+
const next = toPlainNumber(size, DEFAULT_UNDO_BUFFER);
|
|
1500
|
+
undoBufferSize = Number.isFinite(next) ? Math.max(0, next) : 0;
|
|
1501
|
+
};
|
|
1502
|
+
const window_width = () => cssWidth || screenWidth;
|
|
1503
|
+
const window_height = () => cssHeight || screenHeight;
|
|
1504
|
+
const title = (value = void 0) => {
|
|
1505
|
+
if (value === void 0) return screenTitle;
|
|
1506
|
+
screenTitle = toPlainString(value, "");
|
|
1507
|
+
if (canvas) canvas.setAttribute("aria-label", screenTitle);
|
|
1508
|
+
return screenTitle;
|
|
1509
|
+
};
|
|
1510
|
+
const register_shape = (name, shapePoints = void 0) => {
|
|
1511
|
+
const shapeName = toPlainString(name, "").toLowerCase().trim();
|
|
1512
|
+
if (!shapeName) throw new Error("register_shape needs a name");
|
|
1513
|
+
const source = toSequence(shapePoints);
|
|
1514
|
+
if (!source) {
|
|
1515
|
+
throw new Error(`register_shape: no polygon given for ${shapeName}`);
|
|
1516
|
+
}
|
|
1517
|
+
TURTLE_SHAPES[shapeName] = source.map((point) => {
|
|
1518
|
+
const pair = toSequence(point) || [0, 0];
|
|
1519
|
+
const sx = toPlainNumber(pair[0], 0);
|
|
1520
|
+
const sy = toPlainNumber(pair[1], 0);
|
|
1521
|
+
return [sy, -sx];
|
|
1522
|
+
});
|
|
1523
|
+
return shapeName;
|
|
1524
|
+
};
|
|
1525
|
+
const inputLabel = (promptTitle, promptText) => [toPlainString(promptTitle, ""), toPlainString(promptText, "")].filter(Boolean).join(" ").trim();
|
|
1526
|
+
const numinput = (promptTitle = "", promptText = "") => {
|
|
1527
|
+
const answer = askStdinSync(id, inputLabel(promptTitle, promptText));
|
|
1528
|
+
if (!answer) return null;
|
|
1529
|
+
const numeric = Number(answer);
|
|
1530
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
1531
|
+
};
|
|
1532
|
+
const textinput = (promptTitle = "", promptText = "") => askStdinSync(id, inputLabel(promptTitle, promptText));
|
|
1533
|
+
const addListener = (target, type, handler) => {
|
|
1534
|
+
if (!target) return;
|
|
1535
|
+
target.addEventListener(type, handler);
|
|
1536
|
+
eventCleanups.push(() => target.removeEventListener(type, handler));
|
|
1537
|
+
};
|
|
1538
|
+
const clearListeners = () => {
|
|
1539
|
+
for (const cleanup of eventCleanups) {
|
|
1540
|
+
try {
|
|
1541
|
+
cleanup();
|
|
1542
|
+
} catch {
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
eventCleanups = [];
|
|
1546
|
+
for (const timerId of pendingTimers) window.clearTimeout(timerId);
|
|
1547
|
+
pendingTimers = [];
|
|
1548
|
+
};
|
|
1549
|
+
const canvasPoint = (event) => {
|
|
1550
|
+
const rect = canvas?.getBoundingClientRect?.();
|
|
1551
|
+
if (!rect) return [0, 0];
|
|
1552
|
+
return [
|
|
1553
|
+
event.clientX - rect.left - cssWidth / 2,
|
|
1554
|
+
cssHeight / 2 - (event.clientY - rect.top)
|
|
1555
|
+
];
|
|
1556
|
+
};
|
|
1557
|
+
const onscreenclick = (fn, btn = 1) => {
|
|
1558
|
+
if (!ensureContext()) return;
|
|
1559
|
+
if (!fn) return;
|
|
1560
|
+
const wanted = toPlainNumber(btn, 1) - 1;
|
|
1561
|
+
addListener(canvas, "mousedown", (event) => {
|
|
1562
|
+
if (event.button !== wanted) return;
|
|
1563
|
+
const [px, py] = canvasPoint(event);
|
|
1564
|
+
fn(px, py);
|
|
1565
|
+
});
|
|
1566
|
+
};
|
|
1567
|
+
const onkey = (fn, key = void 0) => {
|
|
1568
|
+
if (!ensureContext()) return;
|
|
1569
|
+
const wanted = toPlainString(key, "");
|
|
1570
|
+
addListener(canvas, "keyup", (event) => {
|
|
1571
|
+
if (wanted && event.key !== wanted && event.code !== wanted) return;
|
|
1572
|
+
if (fn) fn();
|
|
1573
|
+
});
|
|
1574
|
+
};
|
|
1575
|
+
const onkeypress = (fn, key = void 0) => {
|
|
1576
|
+
if (!ensureContext()) return;
|
|
1577
|
+
const wanted = toPlainString(key, "");
|
|
1578
|
+
addListener(canvas, "keydown", (event) => {
|
|
1579
|
+
if (wanted && event.key !== wanted && event.code !== wanted) return;
|
|
1580
|
+
if (fn) fn();
|
|
1581
|
+
});
|
|
1582
|
+
};
|
|
1583
|
+
const listen = () => {
|
|
1584
|
+
if (!ensureContext()) return;
|
|
1585
|
+
canvas.focus?.();
|
|
1586
|
+
};
|
|
1587
|
+
const ontimer = (fn, ms = 0) => {
|
|
1588
|
+
if (!fn) return;
|
|
1589
|
+
const timerId = window.setTimeout(
|
|
1590
|
+
() => {
|
|
1591
|
+
pendingTimers = pendingTimers.filter((entry) => entry !== timerId);
|
|
1592
|
+
fn();
|
|
1593
|
+
},
|
|
1594
|
+
Math.max(0, toPlainNumber(ms, 0))
|
|
1595
|
+
);
|
|
1596
|
+
pendingTimers.push(timerId);
|
|
1597
|
+
};
|
|
1598
|
+
const allPenObjects = [];
|
|
1145
1599
|
const defaultPenObj = createTurtlePen();
|
|
1600
|
+
allPenObjects.push(defaultPenObj);
|
|
1146
1601
|
allPens.push(defaultPenObj.pen);
|
|
1147
1602
|
const bindCanvas = (nextCanvas) => {
|
|
1148
1603
|
canvas = nextCanvas || getCanvas(id);
|
|
@@ -1155,6 +1610,7 @@
|
|
|
1155
1610
|
const deactivate = () => {
|
|
1156
1611
|
active = false;
|
|
1157
1612
|
clearQueue();
|
|
1613
|
+
clearListeners();
|
|
1158
1614
|
for (const pen of allPens) {
|
|
1159
1615
|
pen.paths = [];
|
|
1160
1616
|
pen.renderedTurtleVisible = false;
|
|
@@ -1162,39 +1618,114 @@
|
|
|
1162
1618
|
};
|
|
1163
1619
|
const resetState = () => {
|
|
1164
1620
|
clearQueue();
|
|
1621
|
+
clearListeners();
|
|
1165
1622
|
allPens.length = 0;
|
|
1166
1623
|
allPens.push(defaultPenObj.pen);
|
|
1624
|
+
allPenObjects.length = 0;
|
|
1625
|
+
allPenObjects.push(defaultPenObj);
|
|
1167
1626
|
defaultPenObj.resetPen();
|
|
1168
1627
|
backgroundColor = "#ffffff";
|
|
1169
1628
|
backgroundImage = null;
|
|
1170
1629
|
colorMode = 1;
|
|
1171
1630
|
screenWidth = 640;
|
|
1172
1631
|
screenHeight = 480;
|
|
1632
|
+
screenMode = "standard";
|
|
1633
|
+
screenTitle = "";
|
|
1634
|
+
turtleSpeed = DEFAULT_SPEED;
|
|
1635
|
+
delayMs = DEFAULT_DELAY_MS;
|
|
1636
|
+
tracerN = 1;
|
|
1637
|
+
tracerCounter = 0;
|
|
1638
|
+
stampCounter = 0;
|
|
1639
|
+
undoBufferSize = DEFAULT_UNDO_BUFFER;
|
|
1173
1640
|
active = true;
|
|
1641
|
+
render();
|
|
1642
|
+
};
|
|
1643
|
+
const clearscreen = () => {
|
|
1644
|
+
for (const penObj of allPenObjects) penObj.resetPen();
|
|
1645
|
+
backgroundColor = "#ffffff";
|
|
1646
|
+
backgroundImage = null;
|
|
1174
1647
|
draw();
|
|
1175
1648
|
};
|
|
1649
|
+
const resetscreen = () => {
|
|
1650
|
+
for (const penObj of allPenObjects) penObj.resetPen();
|
|
1651
|
+
draw();
|
|
1652
|
+
};
|
|
1653
|
+
const setup = (width = void 0, height = void 0, startx = void 0, starty = void 0) => {
|
|
1654
|
+
const w = toPlainNumber(width, Number.NaN);
|
|
1655
|
+
const h = toPlainNumber(height, Number.NaN);
|
|
1656
|
+
return screensize(
|
|
1657
|
+
Number.isFinite(w) && w > 1 ? w : void 0,
|
|
1658
|
+
Number.isFinite(h) && h > 1 ? h : void 0
|
|
1659
|
+
);
|
|
1660
|
+
};
|
|
1176
1661
|
const Turtle = function() {
|
|
1177
1662
|
const penObj = createTurtlePen();
|
|
1663
|
+
allPenObjects.push(penObj);
|
|
1178
1664
|
enqueueOperation(() => {
|
|
1179
1665
|
allPens.push(penObj.pen);
|
|
1180
1666
|
draw();
|
|
1181
1667
|
});
|
|
1182
1668
|
return penObj.api;
|
|
1183
1669
|
};
|
|
1670
|
+
const screenApi = {
|
|
1671
|
+
colormode,
|
|
1672
|
+
screensize,
|
|
1673
|
+
setup,
|
|
1674
|
+
bgcolor,
|
|
1675
|
+
bgpic,
|
|
1676
|
+
mode,
|
|
1677
|
+
tracer,
|
|
1678
|
+
update,
|
|
1679
|
+
delay,
|
|
1680
|
+
title,
|
|
1681
|
+
register_shape,
|
|
1682
|
+
addshape: register_shape,
|
|
1683
|
+
window_width,
|
|
1684
|
+
window_height,
|
|
1685
|
+
numinput,
|
|
1686
|
+
textinput,
|
|
1687
|
+
listen,
|
|
1688
|
+
onkey,
|
|
1689
|
+
onkeyrelease: onkey,
|
|
1690
|
+
onkeypress,
|
|
1691
|
+
onscreenclick,
|
|
1692
|
+
onclick: onscreenclick,
|
|
1693
|
+
ontimer,
|
|
1694
|
+
clearscreen,
|
|
1695
|
+
clear: clearscreen,
|
|
1696
|
+
resetscreen,
|
|
1697
|
+
reset: resetscreen,
|
|
1698
|
+
// The canvas is always on screen, so there is no event loop to enter or leave.
|
|
1699
|
+
mainloop: () => {
|
|
1700
|
+
},
|
|
1701
|
+
done: () => {
|
|
1702
|
+
},
|
|
1703
|
+
exitonclick: () => {
|
|
1704
|
+
},
|
|
1705
|
+
bye: () => {
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1184
1708
|
const api = {
|
|
1185
1709
|
__bindCanvas: bindCanvas,
|
|
1186
1710
|
__deactivate: deactivate,
|
|
1187
|
-
__redraw:
|
|
1711
|
+
__redraw: render,
|
|
1188
1712
|
__resetState: resetState,
|
|
1189
1713
|
__setPyodide: (runtime) => {
|
|
1190
1714
|
pyodide = runtime;
|
|
1191
1715
|
},
|
|
1192
1716
|
...defaultPenObj.api,
|
|
1193
1717
|
Turtle,
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1718
|
+
Screen: () => screenApi,
|
|
1719
|
+
getscreen: () => screenApi,
|
|
1720
|
+
setundobuffer,
|
|
1721
|
+
// Screen helpers are also exposed at module level, matching `from turtle import *`.
|
|
1722
|
+
...screenApi,
|
|
1723
|
+
// The turtle's own clear()/reset() win over the screen's at module level,
|
|
1724
|
+
// exactly as CPython's functional interface binds them.
|
|
1725
|
+
clear: defaultPenObj.api.clear,
|
|
1726
|
+
reset: defaultPenObj.api.reset,
|
|
1727
|
+
clearscreen,
|
|
1728
|
+
resetscreen
|
|
1198
1729
|
};
|
|
1199
1730
|
return api;
|
|
1200
1731
|
};
|
|
@@ -1737,10 +2268,125 @@
|
|
|
1737
2268
|
};
|
|
1738
2269
|
|
|
1739
2270
|
// assets/directive-pyide/src/execution.js
|
|
1740
|
-
var
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2271
|
+
var runSyncSupport = /* @__PURE__ */ new Map();
|
|
2272
|
+
var PROBE_RUN_SYNC = `
|
|
2273
|
+
def _hyperbook_probe():
|
|
2274
|
+
try:
|
|
2275
|
+
from pyodide.ffi import run_sync
|
|
2276
|
+
import js
|
|
2277
|
+
except Exception:
|
|
2278
|
+
return False
|
|
2279
|
+
try:
|
|
2280
|
+
run_sync(js.Promise.resolve(True))
|
|
2281
|
+
return True
|
|
2282
|
+
except Exception:
|
|
2283
|
+
return False
|
|
2284
|
+
|
|
2285
|
+
_hyperbook_probe()
|
|
2286
|
+
`;
|
|
2287
|
+
var INPUT_SHIM = `
|
|
2288
|
+
def input(prompt=""):
|
|
2289
|
+
import sys
|
|
2290
|
+
from pyodide.ffi import run_sync
|
|
2291
|
+
|
|
2292
|
+
text = "" if prompt is None else str(prompt)
|
|
2293
|
+
if text:
|
|
2294
|
+
sys.stdout.write(text)
|
|
2295
|
+
sys.stdout.flush()
|
|
2296
|
+
answer = run_sync(__hyperbook_ask_stdin(text))
|
|
2297
|
+
answer = "" if answer is None else str(answer)
|
|
2298
|
+
# A terminal echoes what was typed and the Enter that ended it; nothing
|
|
2299
|
+
# does that here, so the transcript has to include it explicitly.
|
|
2300
|
+
sys.stdout.write(answer + "\\n")
|
|
2301
|
+
sys.stdout.flush()
|
|
2302
|
+
return answer
|
|
2303
|
+
`;
|
|
2304
|
+
var TURTLE_INPUT_SHIM = `
|
|
2305
|
+
def _hyperbook_patch_turtle():
|
|
2306
|
+
import turtle
|
|
2307
|
+
from pyodide.ffi import run_sync
|
|
2308
|
+
|
|
2309
|
+
def _label(title, prompt):
|
|
2310
|
+
return " ".join(str(part) for part in (title, prompt) if part).strip()
|
|
2311
|
+
|
|
2312
|
+
def textinput(title="", prompt=""):
|
|
2313
|
+
return run_sync(__hyperbook_ask_stdin(_label(title, prompt)))
|
|
2314
|
+
|
|
2315
|
+
def numinput(title="", prompt="", default=None, minval=None, maxval=None):
|
|
2316
|
+
hint = ""
|
|
2317
|
+
if minval is not None and maxval is not None:
|
|
2318
|
+
hint = " [{0}, {1}]".format(minval, maxval)
|
|
2319
|
+
elif minval is not None:
|
|
2320
|
+
hint = " [>= {0}]".format(minval)
|
|
2321
|
+
elif maxval is not None:
|
|
2322
|
+
hint = " [<= {0}]".format(maxval)
|
|
2323
|
+
while True:
|
|
2324
|
+
answer = run_sync(__hyperbook_ask_stdin(_label(title, prompt) + hint))
|
|
2325
|
+
# An empty answer stands in for tkinter's Cancel button.
|
|
2326
|
+
if answer is None or str(answer).strip() == "":
|
|
2327
|
+
return default
|
|
2328
|
+
try:
|
|
2329
|
+
value = float(str(answer).strip().replace(",", "."))
|
|
2330
|
+
except ValueError:
|
|
2331
|
+
continue
|
|
2332
|
+
if minval is not None and value < minval:
|
|
2333
|
+
continue
|
|
2334
|
+
if maxval is not None and value > maxval:
|
|
2335
|
+
continue
|
|
2336
|
+
return value
|
|
2337
|
+
|
|
2338
|
+
# Screen() hands back one shared object, so patching it once is enough.
|
|
2339
|
+
try:
|
|
2340
|
+
screen = turtle.Screen()
|
|
2341
|
+
screen.numinput = numinput
|
|
2342
|
+
screen.textinput = textinput
|
|
2343
|
+
except Exception:
|
|
2344
|
+
pass
|
|
2345
|
+
|
|
2346
|
+
try:
|
|
2347
|
+
turtle.numinput = numinput
|
|
2348
|
+
turtle.textinput = textinput
|
|
2349
|
+
except Exception:
|
|
2350
|
+
pass
|
|
2351
|
+
if getattr(turtle, "numinput", None) is not numinput:
|
|
2352
|
+
# The JS module rejected the assignment; shadow it with a real Python
|
|
2353
|
+
# module so \`from turtle import *\` still picks up the patched names.
|
|
2354
|
+
import sys
|
|
2355
|
+
import types
|
|
2356
|
+
|
|
2357
|
+
wrapper = types.ModuleType("turtle")
|
|
2358
|
+
for name in dir(turtle):
|
|
2359
|
+
try:
|
|
2360
|
+
setattr(wrapper, name, getattr(turtle, name))
|
|
2361
|
+
except Exception:
|
|
2362
|
+
pass
|
|
2363
|
+
wrapper.numinput = numinput
|
|
2364
|
+
wrapper.textinput = textinput
|
|
2365
|
+
sys.modules["turtle"] = wrapper
|
|
2366
|
+
sys.modules["jturtle"] = wrapper
|
|
2367
|
+
|
|
2368
|
+
_hyperbook_patch_turtle()
|
|
2369
|
+
del _hyperbook_patch_turtle
|
|
2370
|
+
`;
|
|
2371
|
+
var supportsRunSync = async (id, pyodide) => {
|
|
2372
|
+
if (runSyncSupport.has(id)) return runSyncSupport.get(id);
|
|
2373
|
+
let supported = false;
|
|
2374
|
+
const dict = pyodide.globals.get("dict");
|
|
2375
|
+
const probeGlobals = dict();
|
|
2376
|
+
try {
|
|
2377
|
+
supported = !!await pyodide.runPythonAsync(PROBE_RUN_SYNC, {
|
|
2378
|
+
globals: probeGlobals,
|
|
2379
|
+
locals: probeGlobals,
|
|
2380
|
+
filename: "<hyperbook-probe>"
|
|
2381
|
+
});
|
|
2382
|
+
} catch {
|
|
2383
|
+
supported = false;
|
|
2384
|
+
} finally {
|
|
2385
|
+
probeGlobals.destroy();
|
|
2386
|
+
dict.destroy();
|
|
2387
|
+
}
|
|
2388
|
+
runSyncSupport.set(id, supported);
|
|
2389
|
+
return supported;
|
|
1744
2390
|
};
|
|
1745
2391
|
var resetCanvas = (canvas) => {
|
|
1746
2392
|
if (!canvas) return;
|
|
@@ -1912,13 +2558,15 @@
|
|
|
1912
2558
|
}
|
|
1913
2559
|
});
|
|
1914
2560
|
pyodide.setStdin({
|
|
2561
|
+
// Reached by sys.stdin reads, and by input() when Python cannot be
|
|
2562
|
+
// suspended. Both block the main thread; there is no way around that
|
|
2563
|
+
// without stack switching.
|
|
1915
2564
|
stdin: () => {
|
|
1916
|
-
const promptText = lastStdinPrompt
|
|
2565
|
+
const promptText = lastStdinPrompt;
|
|
1917
2566
|
lastStdinPrompt = "";
|
|
1918
|
-
const value =
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
}
|
|
2567
|
+
const value = askStdinSync(id, promptText);
|
|
2568
|
+
appendOutputLine(id, `${value}
|
|
2569
|
+
`);
|
|
1922
2570
|
return value;
|
|
1923
2571
|
}
|
|
1924
2572
|
});
|
|
@@ -1931,12 +2579,31 @@
|
|
|
1931
2579
|
});
|
|
1932
2580
|
await ensureMicropipPackages(id, pyodide, packages);
|
|
1933
2581
|
await pyodide.loadPackagesFromImports(executableScript);
|
|
2582
|
+
const canSuspend = await supportsRunSync(id, pyodide);
|
|
1934
2583
|
const dict = pyodide.globals.get("dict");
|
|
1935
2584
|
const globals = dict();
|
|
1936
2585
|
try {
|
|
1937
2586
|
for (const [key, value] of Object.entries(globalsContext)) {
|
|
1938
2587
|
globals.set(key, value);
|
|
1939
2588
|
}
|
|
2589
|
+
if (canSuspend) {
|
|
2590
|
+
globals.set(
|
|
2591
|
+
"__hyperbook_ask_stdin",
|
|
2592
|
+
(promptText) => askStdinAsync(id, promptText)
|
|
2593
|
+
);
|
|
2594
|
+
await pyodide.runPythonAsync(INPUT_SHIM, {
|
|
2595
|
+
globals,
|
|
2596
|
+
locals: globals,
|
|
2597
|
+
filename: "<hyperbook-stdin>"
|
|
2598
|
+
});
|
|
2599
|
+
if (scriptLooksLikeTurtle(executableScript)) {
|
|
2600
|
+
await pyodide.runPythonAsync(TURTLE_INPUT_SHIM, {
|
|
2601
|
+
globals,
|
|
2602
|
+
locals: globals,
|
|
2603
|
+
filename: "<hyperbook-stdin>"
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
1940
2607
|
const results = await pyodide.runPythonAsync(executableScript, {
|
|
1941
2608
|
globals,
|
|
1942
2609
|
locals: globals,
|
|
@@ -1944,6 +2611,7 @@
|
|
|
1944
2611
|
});
|
|
1945
2612
|
return { results };
|
|
1946
2613
|
} finally {
|
|
2614
|
+
hideStdinField(id);
|
|
1947
2615
|
globals.destroy();
|
|
1948
2616
|
dict.destroy();
|
|
1949
2617
|
if (canvas) {
|
|
@@ -1974,12 +2642,379 @@ if _pg:
|
|
|
1974
2642
|
}
|
|
1975
2643
|
};
|
|
1976
2644
|
|
|
2645
|
+
// assets/directive-pyide/src/turtle-completions.js
|
|
2646
|
+
var TURTLE_MOVE = [
|
|
2647
|
+
[
|
|
2648
|
+
"forward",
|
|
2649
|
+
"forward(distance)",
|
|
2650
|
+
"Move the turtle forward by distance, in the direction it is headed."
|
|
2651
|
+
],
|
|
2652
|
+
["fd", "fd(distance)", "Short form of forward()."],
|
|
2653
|
+
[
|
|
2654
|
+
"backward",
|
|
2655
|
+
"backward(distance)",
|
|
2656
|
+
"Move the turtle backward by distance, without changing its heading."
|
|
2657
|
+
],
|
|
2658
|
+
["bk", "bk(distance)", "Short form of backward()."],
|
|
2659
|
+
["back", "back(distance)", "Short form of backward()."],
|
|
2660
|
+
[
|
|
2661
|
+
"right",
|
|
2662
|
+
"right(angle)",
|
|
2663
|
+
"Turn the turtle right (clockwise) by angle units."
|
|
2664
|
+
],
|
|
2665
|
+
["rt", "rt(angle)", "Short form of right()."],
|
|
2666
|
+
[
|
|
2667
|
+
"left",
|
|
2668
|
+
"left(angle)",
|
|
2669
|
+
"Turn the turtle left (counterclockwise) by angle units."
|
|
2670
|
+
],
|
|
2671
|
+
["lt", "lt(angle)", "Short form of left()."],
|
|
2672
|
+
[
|
|
2673
|
+
"goto",
|
|
2674
|
+
"goto(x, y=None)",
|
|
2675
|
+
"Move the turtle to an absolute position. Draws a line if the pen is down."
|
|
2676
|
+
],
|
|
2677
|
+
["setpos", "setpos(x, y=None)", "Same as goto()."],
|
|
2678
|
+
["setposition", "setposition(x, y=None)", "Same as goto()."],
|
|
2679
|
+
[
|
|
2680
|
+
"setx",
|
|
2681
|
+
"setx(x)",
|
|
2682
|
+
"Set the turtle's first coordinate, leaving the second unchanged."
|
|
2683
|
+
],
|
|
2684
|
+
[
|
|
2685
|
+
"sety",
|
|
2686
|
+
"sety(y)",
|
|
2687
|
+
"Set the turtle's second coordinate, leaving the first unchanged."
|
|
2688
|
+
],
|
|
2689
|
+
[
|
|
2690
|
+
"setheading",
|
|
2691
|
+
"setheading(angle)",
|
|
2692
|
+
"Set the turtle's orientation to angle. 0 is east in standard mode, north in logo mode."
|
|
2693
|
+
],
|
|
2694
|
+
["seth", "seth(angle)", "Short form of setheading()."],
|
|
2695
|
+
[
|
|
2696
|
+
"home",
|
|
2697
|
+
"home()",
|
|
2698
|
+
"Move the turtle to the origin and set its heading back to 0."
|
|
2699
|
+
],
|
|
2700
|
+
[
|
|
2701
|
+
"circle",
|
|
2702
|
+
"circle(radius, extent=None, steps=None)",
|
|
2703
|
+
"Draw a circle or, with extent, an arc. A positive radius curves to the left."
|
|
2704
|
+
],
|
|
2705
|
+
[
|
|
2706
|
+
"dot",
|
|
2707
|
+
"dot(size=None, *color)",
|
|
2708
|
+
"Draw a filled dot at the current position."
|
|
2709
|
+
],
|
|
2710
|
+
["position", "position()", "Return the turtle's current position as (x, y)."],
|
|
2711
|
+
["pos", "pos()", "Short form of position()."],
|
|
2712
|
+
["xcor", "xcor()", "Return the turtle's x coordinate."],
|
|
2713
|
+
["ycor", "ycor()", "Return the turtle's y coordinate."],
|
|
2714
|
+
["heading", "heading()", "Return the turtle's current heading."],
|
|
2715
|
+
[
|
|
2716
|
+
"towards",
|
|
2717
|
+
"towards(x, y=None)",
|
|
2718
|
+
"Return the angle from the turtle to the given position."
|
|
2719
|
+
],
|
|
2720
|
+
[
|
|
2721
|
+
"distance",
|
|
2722
|
+
"distance(x, y=None)",
|
|
2723
|
+
"Return the distance from the turtle to the given position."
|
|
2724
|
+
],
|
|
2725
|
+
[
|
|
2726
|
+
"degrees",
|
|
2727
|
+
"degrees(fullcircle=360)",
|
|
2728
|
+
"Measure angles in degrees, or in fullcircle units per revolution."
|
|
2729
|
+
],
|
|
2730
|
+
["radians", "radians()", "Measure angles in radians."],
|
|
2731
|
+
[
|
|
2732
|
+
"speed",
|
|
2733
|
+
"speed(speed=None)",
|
|
2734
|
+
"Set the animation speed from 1 (slowest) to 10 (fastest), or 0 for no animation. Also accepts 'fastest', 'fast', 'normal', 'slow' and 'slowest'. Without an argument it returns the current speed."
|
|
2735
|
+
],
|
|
2736
|
+
["undo", "undo()", "Undo the most recent turtle action."],
|
|
2737
|
+
[
|
|
2738
|
+
"undobufferentries",
|
|
2739
|
+
"undobufferentries()",
|
|
2740
|
+
"Return the number of actions that can still be undone."
|
|
2741
|
+
]
|
|
2742
|
+
];
|
|
2743
|
+
var TURTLE_PEN = [
|
|
2744
|
+
[
|
|
2745
|
+
"pendown",
|
|
2746
|
+
"pendown()",
|
|
2747
|
+
"Put the pen down \u2014 the turtle draws when it moves."
|
|
2748
|
+
],
|
|
2749
|
+
["pd", "pd()", "Short form of pendown()."],
|
|
2750
|
+
["down", "down()", "Short form of pendown()."],
|
|
2751
|
+
["penup", "penup()", "Pull the pen up \u2014 the turtle moves without drawing."],
|
|
2752
|
+
["pu", "pu()", "Short form of penup()."],
|
|
2753
|
+
["up", "up()", "Short form of penup()."],
|
|
2754
|
+
[
|
|
2755
|
+
"pensize",
|
|
2756
|
+
"pensize(width=None)",
|
|
2757
|
+
"Set the line thickness. Without an argument it returns the current width."
|
|
2758
|
+
],
|
|
2759
|
+
["width", "width(width=None)", "Same as pensize()."],
|
|
2760
|
+
[
|
|
2761
|
+
"pen",
|
|
2762
|
+
"pen(pen=None, **pendict)",
|
|
2763
|
+
"Read or set the pen's state as a dictionary (pendown, pencolor, fillcolor, pensize, speed, shown)."
|
|
2764
|
+
],
|
|
2765
|
+
[
|
|
2766
|
+
"color",
|
|
2767
|
+
"color(*args)",
|
|
2768
|
+
"Set pen and fill colour at once: color(c), color(pen, fill) or color(r, g, b). Without arguments it returns both."
|
|
2769
|
+
],
|
|
2770
|
+
[
|
|
2771
|
+
"pencolor",
|
|
2772
|
+
"pencolor(*args)",
|
|
2773
|
+
"Set the line colour, as a name, an (r, g, b) tuple or three numbers. Without arguments it returns the current colour."
|
|
2774
|
+
],
|
|
2775
|
+
[
|
|
2776
|
+
"fillcolor",
|
|
2777
|
+
"fillcolor(*args)",
|
|
2778
|
+
"Set the fill colour, as a name, an (r, g, b) tuple or three numbers. Without arguments it returns the current colour."
|
|
2779
|
+
],
|
|
2780
|
+
[
|
|
2781
|
+
"begin_fill",
|
|
2782
|
+
"begin_fill()",
|
|
2783
|
+
"Start recording a shape to fill. Call end_fill() to fill it."
|
|
2784
|
+
],
|
|
2785
|
+
[
|
|
2786
|
+
"end_fill",
|
|
2787
|
+
"end_fill()",
|
|
2788
|
+
"Fill the shape drawn since the last begin_fill()."
|
|
2789
|
+
],
|
|
2790
|
+
["filling", "filling()", "Return True while a fill is being recorded."],
|
|
2791
|
+
[
|
|
2792
|
+
"write",
|
|
2793
|
+
"write(arg, move=False, align='left', font=('Arial', 8, 'normal'))",
|
|
2794
|
+
"Write text at the turtle's position."
|
|
2795
|
+
],
|
|
2796
|
+
[
|
|
2797
|
+
"setfontsize",
|
|
2798
|
+
"setfontsize(size)",
|
|
2799
|
+
"Set the default font size used by write()."
|
|
2800
|
+
],
|
|
2801
|
+
[
|
|
2802
|
+
"clear",
|
|
2803
|
+
"clear()",
|
|
2804
|
+
"Erase this turtle's drawings, leaving its position and pen state alone."
|
|
2805
|
+
],
|
|
2806
|
+
[
|
|
2807
|
+
"reset",
|
|
2808
|
+
"reset()",
|
|
2809
|
+
"Erase this turtle's drawings and restore its starting state."
|
|
2810
|
+
]
|
|
2811
|
+
];
|
|
2812
|
+
var TURTLE_CURSOR = [
|
|
2813
|
+
["showturtle", "showturtle()", "Make the turtle visible."],
|
|
2814
|
+
["st", "st()", "Short form of showturtle()."],
|
|
2815
|
+
[
|
|
2816
|
+
"hideturtle",
|
|
2817
|
+
"hideturtle()",
|
|
2818
|
+
"Make the turtle invisible. Drawing is noticeably faster while hidden."
|
|
2819
|
+
],
|
|
2820
|
+
["ht", "ht()", "Short form of hideturtle()."],
|
|
2821
|
+
["isvisible", "isvisible()", "Return True if the turtle is visible."],
|
|
2822
|
+
[
|
|
2823
|
+
"shape",
|
|
2824
|
+
"shape(name=None)",
|
|
2825
|
+
"Set the turtle's shape: 'classic', 'arrow', 'turtle', 'circle', 'square', 'triangle' or 'blank'."
|
|
2826
|
+
],
|
|
2827
|
+
[
|
|
2828
|
+
"shapesize",
|
|
2829
|
+
"shapesize(stretch_wid=None, stretch_len=None, outline=None)",
|
|
2830
|
+
"Scale the turtle cursor and set its outline width."
|
|
2831
|
+
],
|
|
2832
|
+
[
|
|
2833
|
+
"turtlesize",
|
|
2834
|
+
"turtlesize(stretch_wid=None, stretch_len=None, outline=None)",
|
|
2835
|
+
"Same as shapesize()."
|
|
2836
|
+
],
|
|
2837
|
+
[
|
|
2838
|
+
"tilt",
|
|
2839
|
+
"tilt(angle)",
|
|
2840
|
+
"Rotate the turtle's shape by angle, without changing its heading."
|
|
2841
|
+
],
|
|
2842
|
+
[
|
|
2843
|
+
"tiltangle",
|
|
2844
|
+
"tiltangle(angle=None)",
|
|
2845
|
+
"Read or set the shape's rotation relative to the heading."
|
|
2846
|
+
],
|
|
2847
|
+
[
|
|
2848
|
+
"settiltangle",
|
|
2849
|
+
"settiltangle(angle)",
|
|
2850
|
+
"Set the shape's rotation relative to the heading."
|
|
2851
|
+
],
|
|
2852
|
+
[
|
|
2853
|
+
"stamp",
|
|
2854
|
+
"stamp()",
|
|
2855
|
+
"Stamp a copy of the turtle's shape onto the canvas and return its id."
|
|
2856
|
+
],
|
|
2857
|
+
["clearstamp", "clearstamp(stampid)", "Remove the stamp with the given id."],
|
|
2858
|
+
[
|
|
2859
|
+
"clearstamps",
|
|
2860
|
+
"clearstamps(n=None)",
|
|
2861
|
+
"Remove all stamps, the first n, or the last n if n is negative."
|
|
2862
|
+
]
|
|
2863
|
+
];
|
|
2864
|
+
var TURTLE_SCREEN = [
|
|
2865
|
+
[
|
|
2866
|
+
"Screen",
|
|
2867
|
+
"Screen()",
|
|
2868
|
+
"Return the screen object, which carries the window-level functions."
|
|
2869
|
+
],
|
|
2870
|
+
["getscreen", "getscreen()", "Return the screen the turtle draws on."],
|
|
2871
|
+
[
|
|
2872
|
+
"Turtle",
|
|
2873
|
+
"Turtle()",
|
|
2874
|
+
"Create an additional turtle drawing on the same canvas."
|
|
2875
|
+
],
|
|
2876
|
+
["bgcolor", "bgcolor(color)", "Set the background colour of the canvas."],
|
|
2877
|
+
[
|
|
2878
|
+
"bgpic",
|
|
2879
|
+
"bgpic(picname=None)",
|
|
2880
|
+
"Set a background image from a file in the IDE's file system."
|
|
2881
|
+
],
|
|
2882
|
+
[
|
|
2883
|
+
"screensize",
|
|
2884
|
+
"screensize(canvwidth=None, canvheight=None, bg=None)",
|
|
2885
|
+
"Set the size of the drawing canvas in pixels."
|
|
2886
|
+
],
|
|
2887
|
+
[
|
|
2888
|
+
"setup",
|
|
2889
|
+
"setup(width, height, startx=None, starty=None)",
|
|
2890
|
+
"Set the canvas size in pixels."
|
|
2891
|
+
],
|
|
2892
|
+
[
|
|
2893
|
+
"colormode",
|
|
2894
|
+
"colormode(cmode=None)",
|
|
2895
|
+
"Use colour values from 0 to 1 (cmode=1.0) or 0 to 255 (cmode=255)."
|
|
2896
|
+
],
|
|
2897
|
+
[
|
|
2898
|
+
"mode",
|
|
2899
|
+
"mode(mode=None)",
|
|
2900
|
+
"Switch between 'standard' (0 = east) and 'logo' (0 = north). Changing the mode resets every turtle."
|
|
2901
|
+
],
|
|
2902
|
+
[
|
|
2903
|
+
"tracer",
|
|
2904
|
+
"tracer(n=None, delay=None)",
|
|
2905
|
+
"Draw only every n-th update. tracer(0) turns animation off until update() is called \u2014 the usual way to draw complex figures quickly."
|
|
2906
|
+
],
|
|
2907
|
+
["update", "update()", "Redraw the canvas. Needed after tracer(0)."],
|
|
2908
|
+
[
|
|
2909
|
+
"delay",
|
|
2910
|
+
"delay(delay=None)",
|
|
2911
|
+
"Read or set the delay between drawing steps, in milliseconds."
|
|
2912
|
+
],
|
|
2913
|
+
["title", "title(titlestring)", "Set the canvas title."],
|
|
2914
|
+
["window_width", "window_width()", "Return the canvas width in pixels."],
|
|
2915
|
+
["window_height", "window_height()", "Return the canvas height in pixels."],
|
|
2916
|
+
[
|
|
2917
|
+
"register_shape",
|
|
2918
|
+
"register_shape(name, shape)",
|
|
2919
|
+
"Register a polygon as a new turtle shape."
|
|
2920
|
+
],
|
|
2921
|
+
["addshape", "addshape(name, shape)", "Same as register_shape()."],
|
|
2922
|
+
["clearscreen", "clearscreen()", "Erase everything and reset every turtle."],
|
|
2923
|
+
["resetscreen", "resetscreen()", "Reset every turtle to its starting state."],
|
|
2924
|
+
[
|
|
2925
|
+
"setundobuffer",
|
|
2926
|
+
"setundobuffer(size)",
|
|
2927
|
+
"Set how many actions undo() can step back through."
|
|
2928
|
+
],
|
|
2929
|
+
[
|
|
2930
|
+
"numinput",
|
|
2931
|
+
"numinput(title, prompt)",
|
|
2932
|
+
"Ask the user for a number and return it, or None if cancelled."
|
|
2933
|
+
],
|
|
2934
|
+
[
|
|
2935
|
+
"textinput",
|
|
2936
|
+
"textinput(title, prompt)",
|
|
2937
|
+
"Ask the user for a string and return it, or None if cancelled."
|
|
2938
|
+
],
|
|
2939
|
+
[
|
|
2940
|
+
"listen",
|
|
2941
|
+
"listen()",
|
|
2942
|
+
"Give the canvas keyboard focus, so key handlers fire."
|
|
2943
|
+
],
|
|
2944
|
+
[
|
|
2945
|
+
"onkey",
|
|
2946
|
+
"onkey(fun, key)",
|
|
2947
|
+
"Call fun when key is released. Requires listen()."
|
|
2948
|
+
],
|
|
2949
|
+
[
|
|
2950
|
+
"onkeypress",
|
|
2951
|
+
"onkeypress(fun, key=None)",
|
|
2952
|
+
"Call fun when key is pressed. Requires listen()."
|
|
2953
|
+
],
|
|
2954
|
+
["onkeyrelease", "onkeyrelease(fun, key)", "Same as onkey()."],
|
|
2955
|
+
[
|
|
2956
|
+
"onclick",
|
|
2957
|
+
"onclick(fun, btn=1)",
|
|
2958
|
+
"Call fun(x, y) when the canvas is clicked."
|
|
2959
|
+
],
|
|
2960
|
+
["onscreenclick", "onscreenclick(fun, btn=1)", "Same as onclick()."],
|
|
2961
|
+
["ontimer", "ontimer(fun, t=0)", "Call fun after t milliseconds."],
|
|
2962
|
+
["mainloop", "mainloop()", "No-op here \u2014 the canvas is always on screen."],
|
|
2963
|
+
["done", "done()", "No-op here \u2014 the canvas is always on screen."],
|
|
2964
|
+
[
|
|
2965
|
+
"exitonclick",
|
|
2966
|
+
"exitonclick()",
|
|
2967
|
+
"No-op here \u2014 the canvas is always on screen."
|
|
2968
|
+
],
|
|
2969
|
+
["bye", "bye()", "No-op here \u2014 the canvas is always on screen."]
|
|
2970
|
+
];
|
|
2971
|
+
var toOptions = (entries, boost = 0) => entries.map(([label, detail, info]) => ({
|
|
2972
|
+
label,
|
|
2973
|
+
type: "function",
|
|
2974
|
+
detail,
|
|
2975
|
+
info,
|
|
2976
|
+
boost
|
|
2977
|
+
}));
|
|
2978
|
+
var isSecondary = ([, detail, info]) => /^(Short form|Same as|No-op)/.test(info);
|
|
2979
|
+
var buildOptions = (groups) => {
|
|
2980
|
+
const entries = groups.flat();
|
|
2981
|
+
return [
|
|
2982
|
+
...toOptions(
|
|
2983
|
+
entries.filter((entry) => !isSecondary(entry)),
|
|
2984
|
+
1
|
|
2985
|
+
),
|
|
2986
|
+
...toOptions(entries.filter(isSecondary), -1)
|
|
2987
|
+
];
|
|
2988
|
+
};
|
|
2989
|
+
var MODULE_OPTIONS = buildOptions([
|
|
2990
|
+
TURTLE_MOVE,
|
|
2991
|
+
TURTLE_PEN,
|
|
2992
|
+
TURTLE_CURSOR,
|
|
2993
|
+
TURTLE_SCREEN
|
|
2994
|
+
]);
|
|
2995
|
+
var MEMBER_OPTIONS = MODULE_OPTIONS.filter(
|
|
2996
|
+
(option) => option.label !== "Turtle" && option.label !== "Screen"
|
|
2997
|
+
);
|
|
2998
|
+
var MEMBER_BASE_PATTERN = /(?:^|\n)\s*([A-Za-z_]\w*)\s*=\s*(?:turtle\s*\.\s*)?(?:Turtle|Screen|getscreen)\s*\(/g;
|
|
2999
|
+
var memberBases = (doc) => {
|
|
3000
|
+
const bases = ["turtle"];
|
|
3001
|
+
for (const match of doc.matchAll(MEMBER_BASE_PATTERN)) {
|
|
3002
|
+
if (!bases.includes(match[1])) bases.push(match[1]);
|
|
3003
|
+
}
|
|
3004
|
+
return bases;
|
|
3005
|
+
};
|
|
3006
|
+
var turtleCompletions = {
|
|
3007
|
+
enabled: scriptLooksLikeTurtle,
|
|
3008
|
+
globals: MODULE_OPTIONS,
|
|
3009
|
+
members: MEMBER_OPTIONS,
|
|
3010
|
+
memberBases
|
|
3011
|
+
};
|
|
3012
|
+
|
|
1977
3013
|
// assets/directive-pyide/src/ui.js
|
|
1978
3014
|
var updateFullscreenButtonState = (elem, button) => {
|
|
1979
3015
|
if (!elem || !button) return;
|
|
1980
3016
|
const isFullscreen = document.fullscreenElement === elem;
|
|
1981
3017
|
const label = hyperbook.i18n.get("ide-fullscreen-enter");
|
|
1982
|
-
button.textContent = "\u26F6";
|
|
1983
3018
|
button.title = label;
|
|
1984
3019
|
button.setAttribute("aria-label", label);
|
|
1985
3020
|
button.classList.toggle("active", isFullscreen);
|
|
@@ -2108,6 +3143,7 @@ if _pg:
|
|
|
2108
3143
|
if (!state.running && !hasRuntime || state.stopRequested) return;
|
|
2109
3144
|
state.stopRequested = true;
|
|
2110
3145
|
state.stopping = true;
|
|
3146
|
+
cancelStdin(id);
|
|
2111
3147
|
const interruptBuffer = interruptBuffers.get(id);
|
|
2112
3148
|
if (interruptBuffer) {
|
|
2113
3149
|
interruptBuffer[0] = 2;
|
|
@@ -2296,6 +3332,7 @@ if _pg:
|
|
|
2296
3332
|
const cm = editorDiv ? HyperbookCM.create(editorDiv, {
|
|
2297
3333
|
lang: editorDiv.dataset.lang || "python",
|
|
2298
3334
|
value: initialSource,
|
|
3335
|
+
completions: [turtleCompletions],
|
|
2299
3336
|
onChange: (code) => {
|
|
2300
3337
|
void persistPyideState({ script: code });
|
|
2301
3338
|
}
|