hyperbook 0.95.0 → 0.96.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.
|
@@ -246,6 +246,42 @@ hyperbook.python = (function () {
|
|
|
246
246
|
output.appendChild(line);
|
|
247
247
|
};
|
|
248
248
|
|
|
249
|
+
// ─── Python Friendly Error Messages ────────────────────────────────────────
|
|
250
|
+
// Loaded from python-friendly-error-messages.js (built from npm package)
|
|
251
|
+
|
|
252
|
+
if (window.PythonFriendlyErrorMessages) {
|
|
253
|
+
const { loadCopydeckFor, registerAdapter, pyodideAdapter } = window.PythonFriendlyErrorMessages;
|
|
254
|
+
loadCopydeckFor("en");
|
|
255
|
+
registerAdapter("pyodide", pyodideAdapter);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const appendFriendlyError = (output, errorString, code) => {
|
|
259
|
+
if (!output) return;
|
|
260
|
+
let result = null;
|
|
261
|
+
if (window.PythonFriendlyErrorMessages) {
|
|
262
|
+
try {
|
|
263
|
+
result = window.PythonFriendlyErrorMessages.friendlyExplain({
|
|
264
|
+
error: String(errorString),
|
|
265
|
+
code,
|
|
266
|
+
runtime: "pyodide",
|
|
267
|
+
});
|
|
268
|
+
} catch {}
|
|
269
|
+
}
|
|
270
|
+
if (result?.html) {
|
|
271
|
+
const div = document.createElement("div");
|
|
272
|
+
div.classList.add("pfem");
|
|
273
|
+
div.innerHTML = result.html;
|
|
274
|
+
output.appendChild(div);
|
|
275
|
+
} else {
|
|
276
|
+
const span = document.createElement("span");
|
|
277
|
+
span.classList.add("error-line");
|
|
278
|
+
span.textContent = String(errorString);
|
|
279
|
+
output.appendChild(span);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
249
285
|
const appendOutput = (output, message, isError = false, id = null) => {
|
|
250
286
|
if (!output || message === undefined || message === null) return;
|
|
251
287
|
if (isError) {
|
|
@@ -311,10 +347,26 @@ hyperbook.python = (function () {
|
|
|
311
347
|
const createTurtleJsFFI = (id) => {
|
|
312
348
|
const DEFAULT_LINE_WIDTH = 1;
|
|
313
349
|
const DEFAULT_FONT_SIZE = 8;
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
350
|
+
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
|
+
const TURTLE_SHAPES = {
|
|
356
|
+
classic: [[0, 0], [-9, 5], [-7, 0], [-9, -5]],
|
|
357
|
+
arrow: [[0, 10], [0, -10], [10, 0]],
|
|
358
|
+
triangle: [[-5.77, -10], [11.55, 0], [-5.77, 10]],
|
|
359
|
+
square: [[-10, -10], [10, -10], [10, 10], [-10, 10]],
|
|
360
|
+
circle: null, // rendered as arc, not polygon
|
|
361
|
+
turtle: [
|
|
362
|
+
[16, 0], [14, 2], [10, 1], [7, 4], [9, 7], [8, 9],
|
|
363
|
+
[5, 6], [1, 7], [-3, 5], [-6, 8], [-8, 6], [-5, 4],
|
|
364
|
+
[-7, 0], [-5, -4], [-8, -6], [-6, -8], [-3, -5],
|
|
365
|
+
[1, -7], [5, -6], [8, -9], [9, -7], [7, -4], [10, -1], [14, -2],
|
|
366
|
+
],
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// ---- Shared canvas/screen state ----
|
|
318
370
|
let pyodide = null;
|
|
319
371
|
let canvas = null;
|
|
320
372
|
let context = null;
|
|
@@ -322,39 +374,23 @@ hyperbook.python = (function () {
|
|
|
322
374
|
let cssHeight = 0;
|
|
323
375
|
let dpr = 1;
|
|
324
376
|
let active = false;
|
|
325
|
-
|
|
326
|
-
let x = 0;
|
|
327
|
-
let y = 0;
|
|
328
|
-
let heading = 0;
|
|
329
|
-
let renderedX = 0;
|
|
330
|
-
let renderedY = 0;
|
|
331
|
-
let renderedHeading = 0;
|
|
332
|
-
let penDown = true;
|
|
333
|
-
let turtleVisible = true;
|
|
334
|
-
let renderedTurtleVisible = true;
|
|
335
|
-
let strokeColor = "#000000";
|
|
336
|
-
let fillColor = "#000000";
|
|
337
377
|
let backgroundColor = "#ffffff";
|
|
338
378
|
let backgroundImage = null;
|
|
339
|
-
let fontSize = DEFAULT_FONT_SIZE;
|
|
340
|
-
let currentFontFamily = "Arial";
|
|
341
|
-
let currentFontStyle = "normal";
|
|
342
379
|
let colorMode = 1.0;
|
|
343
380
|
let delayMs = 80;
|
|
344
381
|
let turtleSpeed = 3;
|
|
345
|
-
let screenWidth =
|
|
346
|
-
let screenHeight =
|
|
347
|
-
let filling = false;
|
|
348
|
-
let fillPath = null;
|
|
349
|
-
let currentPath = null;
|
|
350
|
-
/** @type {Array<any>} */
|
|
351
|
-
let paths = [];
|
|
382
|
+
let screenWidth = 640;
|
|
383
|
+
let screenHeight = 480;
|
|
352
384
|
let operationQueue = [];
|
|
353
385
|
let queueGeneration = 0;
|
|
354
386
|
let queueRunning = false;
|
|
355
387
|
const textMeasureCanvas = document.createElement("canvas");
|
|
356
388
|
const textMeasureContext = textMeasureCanvas.getContext("2d");
|
|
357
389
|
|
|
390
|
+
// Registry of all active turtle pens (rendering state objects)
|
|
391
|
+
const allPens = [];
|
|
392
|
+
|
|
393
|
+
// ---- Shared helper functions ----
|
|
358
394
|
const normalizeAngle = (angle) => {
|
|
359
395
|
const value = Number(angle) || 0;
|
|
360
396
|
return ((value % 360) + 360) % 360;
|
|
@@ -501,6 +537,37 @@ hyperbook.python = (function () {
|
|
|
501
537
|
pxApprox * 0.2,
|
|
502
538
|
};
|
|
503
539
|
};
|
|
540
|
+
const toColorString = (value) => {
|
|
541
|
+
if (typeof value === "string") {
|
|
542
|
+
return value;
|
|
543
|
+
}
|
|
544
|
+
if (value && typeof value.toJs === "function") {
|
|
545
|
+
return toColorString(value.toJs({ pyproxies: [] }));
|
|
546
|
+
}
|
|
547
|
+
if (Array.isArray(value) || (value && typeof value === "object" && "length" in value)) {
|
|
548
|
+
const parts = Array.from(value).slice(0, 3).map((part) => Number(part));
|
|
549
|
+
if (parts.length !== 3 || parts.some((part) => !Number.isFinite(part))) {
|
|
550
|
+
throw new Error(`bad color sequence: ${String(value)}`);
|
|
551
|
+
}
|
|
552
|
+
if (colorMode === 1.0) {
|
|
553
|
+
if (parts.some((part) => part < 0 || part > 1)) {
|
|
554
|
+
throw new Error(`bad color sequence: ${String(value)}`);
|
|
555
|
+
}
|
|
556
|
+
const [r, g, b] = parts.map((part) => Math.round(part * 255));
|
|
557
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
558
|
+
}
|
|
559
|
+
if (colorMode === 255) {
|
|
560
|
+
if (parts.some((part) => part < 0 || part > 255)) {
|
|
561
|
+
throw new Error(`bad color sequence: ${String(value)}`);
|
|
562
|
+
}
|
|
563
|
+
const [r, g, b] = parts.map((part) => Math.round(part));
|
|
564
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
565
|
+
}
|
|
566
|
+
throw new Error(`bad color sequence: ${String(value)}`);
|
|
567
|
+
}
|
|
568
|
+
return "#000000";
|
|
569
|
+
};
|
|
570
|
+
|
|
504
571
|
const sleep = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
505
572
|
const clearQueue = () => {
|
|
506
573
|
queueGeneration += 1;
|
|
@@ -571,41 +638,6 @@ hyperbook.python = (function () {
|
|
|
571
638
|
return true;
|
|
572
639
|
};
|
|
573
640
|
|
|
574
|
-
const makePath = (overrides = {}) => ({
|
|
575
|
-
down: penDown,
|
|
576
|
-
stroke: strokeColor,
|
|
577
|
-
lineWidth: DEFAULT_LINE_WIDTH,
|
|
578
|
-
fontsize: fontSize,
|
|
579
|
-
fontfamily: currentFontFamily,
|
|
580
|
-
fontstyle: currentFontStyle,
|
|
581
|
-
fill: false,
|
|
582
|
-
fillstyle: fillColor,
|
|
583
|
-
points: [{ x, y }],
|
|
584
|
-
...overrides,
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
const beginCurrentPath = () => {
|
|
588
|
-
currentPath = makePath();
|
|
589
|
-
paths.push(currentPath);
|
|
590
|
-
return currentPath;
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
const commitStyleToNewPath = () => {
|
|
594
|
-
currentPath = makePath({
|
|
595
|
-
down: penDown,
|
|
596
|
-
lineWidth: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH,
|
|
597
|
-
});
|
|
598
|
-
paths.push(currentPath);
|
|
599
|
-
return currentPath;
|
|
600
|
-
};
|
|
601
|
-
|
|
602
|
-
const ensurePath = () => {
|
|
603
|
-
if (!currentPath) {
|
|
604
|
-
beginCurrentPath();
|
|
605
|
-
}
|
|
606
|
-
return currentPath;
|
|
607
|
-
};
|
|
608
|
-
|
|
609
641
|
const drawBackground = () => {
|
|
610
642
|
context.save();
|
|
611
643
|
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
@@ -649,22 +681,11 @@ hyperbook.python = (function () {
|
|
|
649
681
|
const text = String(point.text);
|
|
650
682
|
const metrics = measureTurtleText(text, family, size, style);
|
|
651
683
|
const align = normalizeTextAlign(point.align);
|
|
652
|
-
const left = metrics.left || 0;
|
|
653
|
-
const right = metrics.right || metrics.width || 0;
|
|
654
|
-
const descent = metrics.descent || 0;
|
|
655
|
-
const anchorX = px + TK_TEXT_X_OFFSET;
|
|
656
|
-
let drawX = anchorX + left;
|
|
657
|
-
if (align === "center") {
|
|
658
|
-
drawX = anchorX + (left - right) / 2;
|
|
659
|
-
} else if (align === "right") {
|
|
660
|
-
drawX = anchorX - right;
|
|
661
|
-
}
|
|
662
|
-
const drawY = py - descent;
|
|
663
684
|
context.font = metrics.font;
|
|
664
685
|
context.fillStyle = point.color || path.stroke;
|
|
665
|
-
context.textAlign =
|
|
686
|
+
context.textAlign = align;
|
|
666
687
|
context.textBaseline = "alphabetic";
|
|
667
|
-
context.fillText(text,
|
|
688
|
+
context.fillText(text, px, py);
|
|
668
689
|
continue;
|
|
669
690
|
}
|
|
670
691
|
if (point.dotRadius) {
|
|
@@ -691,22 +712,31 @@ hyperbook.python = (function () {
|
|
|
691
712
|
}
|
|
692
713
|
};
|
|
693
714
|
|
|
694
|
-
const
|
|
695
|
-
if (!renderedTurtleVisible) return;
|
|
715
|
+
const drawTurtleShape = (pen) => {
|
|
716
|
+
if (!pen.renderedTurtleVisible) return;
|
|
696
717
|
context.save();
|
|
697
|
-
context.translate(toCanvasX(renderedX), toCanvasY(renderedY));
|
|
698
|
-
context.rotate(-toRadians(renderedHeading));
|
|
699
|
-
context.
|
|
700
|
-
context.
|
|
701
|
-
context.lineTo(-TURTLE_SIZE / 2, TURTLE_SIZE / 3);
|
|
702
|
-
context.lineTo(-TURTLE_SIZE / 4, 0);
|
|
703
|
-
context.lineTo(-TURTLE_SIZE / 2, -TURTLE_SIZE / 3);
|
|
704
|
-
context.closePath();
|
|
705
|
-
context.fillStyle = "#2f9e44";
|
|
706
|
-
context.strokeStyle = "#1b5e20";
|
|
718
|
+
context.translate(toCanvasX(pen.renderedX), toCanvasY(pen.renderedY));
|
|
719
|
+
context.rotate(-toRadians(pen.renderedHeading));
|
|
720
|
+
context.fillStyle = pen.shapeColor;
|
|
721
|
+
context.strokeStyle = pen.shapeColor;
|
|
707
722
|
context.lineWidth = 1;
|
|
708
|
-
|
|
709
|
-
|
|
723
|
+
const shapeName = pen.shapeName || DEFAULT_SHAPE;
|
|
724
|
+
if (shapeName === "circle") {
|
|
725
|
+
context.beginPath();
|
|
726
|
+
context.arc(0, 0, 10, 0, 2 * Math.PI);
|
|
727
|
+
context.fill();
|
|
728
|
+
context.stroke();
|
|
729
|
+
} else {
|
|
730
|
+
const points = TURTLE_SHAPES[shapeName] || TURTLE_SHAPES[DEFAULT_SHAPE];
|
|
731
|
+
context.beginPath();
|
|
732
|
+
context.moveTo(points[0][0], points[0][1]);
|
|
733
|
+
for (let i = 1; i < points.length; i++) {
|
|
734
|
+
context.lineTo(points[i][0], points[i][1]);
|
|
735
|
+
}
|
|
736
|
+
context.closePath();
|
|
737
|
+
context.fill();
|
|
738
|
+
context.stroke();
|
|
739
|
+
}
|
|
710
740
|
context.restore();
|
|
711
741
|
};
|
|
712
742
|
|
|
@@ -714,205 +744,491 @@ hyperbook.python = (function () {
|
|
|
714
744
|
if (!active) return;
|
|
715
745
|
if (!setupCanvasResolution()) return;
|
|
716
746
|
drawBackground();
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
747
|
+
for (const pen of allPens) {
|
|
748
|
+
pen.paths.forEach(drawPathSegment);
|
|
749
|
+
}
|
|
750
|
+
for (const pen of allPens) {
|
|
751
|
+
drawTurtleShape(pen);
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// ---- Per-turtle pen factory ----
|
|
756
|
+
const createTurtlePen = () => {
|
|
757
|
+
let x = 0;
|
|
758
|
+
let y = 0;
|
|
759
|
+
let heading = 0;
|
|
760
|
+
let penDown = true;
|
|
761
|
+
let turtleVisible = true;
|
|
762
|
+
let strokeColor = "#000000";
|
|
763
|
+
let fillColor = "#000000";
|
|
764
|
+
let fontSize = DEFAULT_FONT_SIZE;
|
|
765
|
+
let currentFontFamily = "Arial";
|
|
766
|
+
let currentFontStyle = "normal";
|
|
767
|
+
let filling = false;
|
|
768
|
+
let fillPath = null;
|
|
769
|
+
let currentPath = null;
|
|
770
|
+
|
|
771
|
+
// Mutable rendering state exposed to the shared draw() function
|
|
772
|
+
const pen = {
|
|
773
|
+
paths: [],
|
|
774
|
+
renderedX: 0,
|
|
775
|
+
renderedY: 0,
|
|
776
|
+
renderedHeading: 0,
|
|
777
|
+
renderedTurtleVisible: true,
|
|
778
|
+
shapeColor: "#000000",
|
|
779
|
+
shapeName: DEFAULT_SHAPE,
|
|
780
|
+
};
|
|
720
781
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
renderedTurtleVisible = true;
|
|
734
|
-
strokeColor = "#000000";
|
|
735
|
-
fillColor = "#000000";
|
|
736
|
-
backgroundColor = "#ffffff";
|
|
737
|
-
backgroundImage = null;
|
|
738
|
-
fontSize = DEFAULT_FONT_SIZE;
|
|
739
|
-
currentFontFamily = "Arial";
|
|
740
|
-
currentFontStyle = "normal";
|
|
741
|
-
colorMode = 1.0;
|
|
742
|
-
screenWidth = null;
|
|
743
|
-
screenHeight = null;
|
|
744
|
-
filling = false;
|
|
745
|
-
active = true;
|
|
746
|
-
clearQueue();
|
|
747
|
-
beginCurrentPath();
|
|
748
|
-
draw();
|
|
749
|
-
};
|
|
782
|
+
const makePath = (overrides = {}) => ({
|
|
783
|
+
down: penDown,
|
|
784
|
+
stroke: strokeColor,
|
|
785
|
+
lineWidth: DEFAULT_LINE_WIDTH,
|
|
786
|
+
fontsize: fontSize,
|
|
787
|
+
fontfamily: currentFontFamily,
|
|
788
|
+
fontstyle: currentFontStyle,
|
|
789
|
+
fill: false,
|
|
790
|
+
fillstyle: fillColor,
|
|
791
|
+
points: [{ x, y }],
|
|
792
|
+
...overrides,
|
|
793
|
+
});
|
|
750
794
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
fillPath = null;
|
|
757
|
-
turtleVisible = false;
|
|
758
|
-
renderedTurtleVisible = false;
|
|
759
|
-
};
|
|
795
|
+
const beginCurrentPath = () => {
|
|
796
|
+
currentPath = makePath();
|
|
797
|
+
pen.paths.push(currentPath);
|
|
798
|
+
return currentPath;
|
|
799
|
+
};
|
|
760
800
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
770
|
-
draw();
|
|
771
|
-
};
|
|
801
|
+
const commitStyleToNewPath = () => {
|
|
802
|
+
currentPath = makePath({
|
|
803
|
+
down: penDown,
|
|
804
|
+
lineWidth: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH,
|
|
805
|
+
});
|
|
806
|
+
pen.paths.push(currentPath);
|
|
807
|
+
return currentPath;
|
|
808
|
+
};
|
|
772
809
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
}
|
|
777
|
-
if (value && typeof value.toJs === "function") {
|
|
778
|
-
return toColorString(value.toJs({ pyproxies: [] }));
|
|
779
|
-
}
|
|
780
|
-
if (Array.isArray(value) || (value && typeof value === "object" && "length" in value)) {
|
|
781
|
-
const parts = Array.from(value).slice(0, 3).map((part) => Number(part));
|
|
782
|
-
if (parts.length !== 3 || parts.some((part) => !Number.isFinite(part))) {
|
|
783
|
-
throw new Error(`bad color sequence: ${String(value)}`);
|
|
810
|
+
const ensurePath = () => {
|
|
811
|
+
if (!currentPath) {
|
|
812
|
+
beginCurrentPath();
|
|
784
813
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
814
|
+
return currentPath;
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
const getPenState = () => ({
|
|
818
|
+
pendown: penDown,
|
|
819
|
+
pencolor: strokeColor,
|
|
820
|
+
fillcolor: fillColor,
|
|
821
|
+
pensize: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH,
|
|
822
|
+
speed: turtleSpeed,
|
|
823
|
+
shown: turtleVisible,
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
beginCurrentPath();
|
|
827
|
+
|
|
828
|
+
const forward = (distance) => {
|
|
829
|
+
if (!ensureContext()) return;
|
|
830
|
+
const path = ensurePath();
|
|
831
|
+
const fill = filling ? fillPath : null;
|
|
832
|
+
const length = Number(distance) || 0;
|
|
833
|
+
const nextX = x + length * Math.cos(toRadians(heading));
|
|
834
|
+
const nextY = y + length * Math.sin(toRadians(heading));
|
|
835
|
+
x = nextX;
|
|
836
|
+
y = nextY;
|
|
837
|
+
const point = { x, y, move: !penDown };
|
|
838
|
+
enqueueOperation(() => {
|
|
839
|
+
pen.renderedX = point.x;
|
|
840
|
+
pen.renderedY = point.y;
|
|
841
|
+
path.points.push(point);
|
|
842
|
+
if (fill) {
|
|
843
|
+
fill.points.push({ x, y });
|
|
788
844
|
}
|
|
789
|
-
|
|
790
|
-
|
|
845
|
+
draw();
|
|
846
|
+
});
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
const backward = (distance) => forward(-Number(distance || 0));
|
|
850
|
+
const right = (angle) => {
|
|
851
|
+
heading = normalizeAngle(heading - Number(angle || 0));
|
|
852
|
+
const nextHeading = heading;
|
|
853
|
+
enqueueOperation(() => {
|
|
854
|
+
pen.renderedHeading = nextHeading;
|
|
855
|
+
draw();
|
|
856
|
+
});
|
|
857
|
+
};
|
|
858
|
+
const left = (angle) => {
|
|
859
|
+
heading = normalizeAngle(heading + Number(angle || 0));
|
|
860
|
+
const nextHeading = heading;
|
|
861
|
+
enqueueOperation(() => {
|
|
862
|
+
pen.renderedHeading = nextHeading;
|
|
863
|
+
draw();
|
|
864
|
+
});
|
|
865
|
+
};
|
|
866
|
+
const penup = () => {
|
|
867
|
+
penDown = false;
|
|
868
|
+
commitStyleToNewPath();
|
|
869
|
+
draw();
|
|
870
|
+
};
|
|
871
|
+
const pendownFn = () => {
|
|
872
|
+
penDown = true;
|
|
873
|
+
commitStyleToNewPath();
|
|
874
|
+
draw();
|
|
875
|
+
};
|
|
876
|
+
const goto_ = (a, b) => {
|
|
877
|
+
if (!ensureContext()) return;
|
|
878
|
+
let nextX = Number(a);
|
|
879
|
+
let nextY = Number(b);
|
|
880
|
+
if (b === undefined && (Array.isArray(a) || (a && typeof a === "object"))) {
|
|
881
|
+
const source =
|
|
882
|
+
a && typeof a.toJs === "function" ? a.toJs({ pyproxies: [] }) : a;
|
|
883
|
+
nextX = Number(source?.[0] ?? source?.x ?? 0);
|
|
884
|
+
nextY = Number(source?.[1] ?? source?.y ?? 0);
|
|
791
885
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
886
|
+
x = Number.isFinite(nextX) ? nextX : 0;
|
|
887
|
+
y = Number.isFinite(nextY) ? nextY : 0;
|
|
888
|
+
const path = ensurePath();
|
|
889
|
+
const fill = filling ? fillPath : null;
|
|
890
|
+
const point = { x, y, move: !penDown };
|
|
891
|
+
enqueueOperation(() => {
|
|
892
|
+
pen.renderedX = point.x;
|
|
893
|
+
pen.renderedY = point.y;
|
|
894
|
+
path.points.push(point);
|
|
895
|
+
if (fill) {
|
|
896
|
+
fill.points.push({ x, y });
|
|
795
897
|
}
|
|
796
|
-
|
|
797
|
-
|
|
898
|
+
draw();
|
|
899
|
+
});
|
|
900
|
+
};
|
|
901
|
+
const setx = (value) => goto_(Number(value), y);
|
|
902
|
+
const sety = (value) => goto_(x, Number(value));
|
|
903
|
+
const position = () => [x, y];
|
|
904
|
+
const xcor = () => x;
|
|
905
|
+
const ycor = () => y;
|
|
906
|
+
const heading_ = () => heading;
|
|
907
|
+
const setheading = (angle) => {
|
|
908
|
+
heading = normalizeAngle(angle);
|
|
909
|
+
const nextHeading = heading;
|
|
910
|
+
enqueueOperation(() => {
|
|
911
|
+
pen.renderedHeading = nextHeading;
|
|
912
|
+
draw();
|
|
913
|
+
});
|
|
914
|
+
};
|
|
915
|
+
const home = () => {
|
|
916
|
+
goto_(0, 0);
|
|
917
|
+
setheading(0);
|
|
918
|
+
};
|
|
919
|
+
const towards = (tx, ty) => {
|
|
920
|
+
const dx = Number(tx) - x;
|
|
921
|
+
const dy = Number(ty) - y;
|
|
922
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy)) return 0;
|
|
923
|
+
return normalizeAngle((Math.atan2(dy, dx) * 180) / Math.PI);
|
|
924
|
+
};
|
|
925
|
+
const speed = (value = 0) => {
|
|
926
|
+
const numeric = Number(value);
|
|
927
|
+
turtleSpeed = Number.isFinite(numeric) ? numeric : 0;
|
|
928
|
+
delayMs = turtleSpeed <= 0 ? 0 : Math.max(0, Math.round(300 / turtleSpeed));
|
|
929
|
+
return delayMs;
|
|
930
|
+
};
|
|
931
|
+
const color = (...args) => {
|
|
932
|
+
const stroke = toColorString(args[0]);
|
|
933
|
+
const fill = args.length > 1 ? toColorString(args[1]) : stroke;
|
|
934
|
+
strokeColor = stroke;
|
|
935
|
+
fillColor = fill;
|
|
936
|
+
pen.shapeColor = stroke;
|
|
937
|
+
commitStyleToNewPath();
|
|
938
|
+
if (filling && fillPath) {
|
|
939
|
+
fillPath.fillstyle = fillColor;
|
|
798
940
|
}
|
|
799
|
-
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
fillcolor
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
const forward = (distance) => {
|
|
814
|
-
if (!ensureContext()) return;
|
|
815
|
-
const path = ensurePath();
|
|
816
|
-
const fill = filling ? fillPath : null;
|
|
817
|
-
const length = Number(distance) || 0;
|
|
818
|
-
const nextX = x + length * Math.cos(toRadians(heading));
|
|
819
|
-
const nextY = y + length * Math.sin(toRadians(heading));
|
|
820
|
-
x = nextX;
|
|
821
|
-
y = nextY;
|
|
822
|
-
const point = { x, y, move: !penDown };
|
|
823
|
-
enqueueOperation(() => {
|
|
824
|
-
renderedX = point.x;
|
|
825
|
-
renderedY = point.y;
|
|
826
|
-
path.points.push(point);
|
|
827
|
-
if (fill) {
|
|
828
|
-
fill.points.push({ x, y });
|
|
941
|
+
draw();
|
|
942
|
+
};
|
|
943
|
+
const pencolor = (value) => {
|
|
944
|
+
strokeColor = toColorString(value);
|
|
945
|
+
pen.shapeColor = strokeColor;
|
|
946
|
+
commitStyleToNewPath();
|
|
947
|
+
draw();
|
|
948
|
+
};
|
|
949
|
+
const fillcolor = (value) => {
|
|
950
|
+
fillColor = toColorString(value);
|
|
951
|
+
if (filling && fillPath) {
|
|
952
|
+
fillPath.fillstyle = fillColor;
|
|
829
953
|
}
|
|
954
|
+
commitStyleToNewPath();
|
|
830
955
|
draw();
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const right = (angle) => {
|
|
836
|
-
heading = normalizeAngle(heading - Number(angle || 0));
|
|
837
|
-
const nextHeading = heading;
|
|
838
|
-
enqueueOperation(() => {
|
|
839
|
-
renderedHeading = nextHeading;
|
|
956
|
+
};
|
|
957
|
+
const pensize = (value) => {
|
|
958
|
+
ensurePath().lineWidth = Math.max(1, Number(value) || DEFAULT_LINE_WIDTH);
|
|
959
|
+
commitStyleToNewPath();
|
|
840
960
|
draw();
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
961
|
+
};
|
|
962
|
+
const width = (value) => pensize(value);
|
|
963
|
+
const begin_fill = () => {
|
|
964
|
+
if (filling) return;
|
|
965
|
+
filling = true;
|
|
966
|
+
fillPath = makePath({
|
|
967
|
+
down: false,
|
|
968
|
+
fill: true,
|
|
969
|
+
stroke: "transparent",
|
|
970
|
+
lineWidth: 1,
|
|
971
|
+
fillstyle: fillColor,
|
|
972
|
+
});
|
|
973
|
+
pen.paths.push(fillPath);
|
|
848
974
|
draw();
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
975
|
+
};
|
|
976
|
+
const end_fill = () => {
|
|
977
|
+
filling = false;
|
|
978
|
+
fillPath = null;
|
|
979
|
+
commitStyleToNewPath();
|
|
980
|
+
draw();
|
|
981
|
+
};
|
|
982
|
+
const dot = (size = 5, colorValue = null) => {
|
|
983
|
+
const segment = makePath({
|
|
984
|
+
down: false,
|
|
985
|
+
fill: false,
|
|
986
|
+
stroke: colorValue ? toColorString(colorValue) : strokeColor,
|
|
987
|
+
fillstyle: colorValue ? toColorString(colorValue) : strokeColor,
|
|
988
|
+
points: [
|
|
989
|
+
{
|
|
990
|
+
x,
|
|
991
|
+
y,
|
|
992
|
+
dotRadius: Math.max(0.5, Number(size) || 5) / 2,
|
|
993
|
+
color: colorValue ? toColorString(colorValue) : strokeColor,
|
|
994
|
+
},
|
|
995
|
+
],
|
|
996
|
+
});
|
|
997
|
+
enqueueOperation(() => {
|
|
998
|
+
pen.paths.push(segment);
|
|
999
|
+
draw();
|
|
1000
|
+
});
|
|
1001
|
+
};
|
|
1002
|
+
const circle = (radius, steps = 120) => {
|
|
1003
|
+
const numericRadius = Number(radius) || 0;
|
|
1004
|
+
const stepCount = Math.max(8, Number(steps) | 0);
|
|
1005
|
+
const circumference = 2 * Math.PI * Math.abs(numericRadius);
|
|
1006
|
+
const stepLength = circumference / stepCount;
|
|
1007
|
+
const stepTurn = (360 / stepCount) * (numericRadius >= 0 ? 1 : -1);
|
|
1008
|
+
for (let index = 0; index < stepCount; index += 1) {
|
|
1009
|
+
forward(stepLength);
|
|
1010
|
+
left(stepTurn);
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
const write = (text, move = false, align = "left", font = null) => {
|
|
1014
|
+
let writeText = text;
|
|
1015
|
+
let writeMove = move;
|
|
1016
|
+
let writeAlign = align;
|
|
1017
|
+
let writeFont = font;
|
|
1018
|
+
const applyWriteKwargs = (candidate) => {
|
|
1019
|
+
if (!isWriteKwargsObject(candidate)) return;
|
|
1020
|
+
const kwargs = toPlainObject(candidate);
|
|
1021
|
+
if (!kwargs) return;
|
|
1022
|
+
if (hasOwn(kwargs, "arg")) writeText = kwargs.arg;
|
|
1023
|
+
else if (hasOwn(kwargs, "text")) writeText = kwargs.text;
|
|
1024
|
+
if (hasOwn(kwargs, "move")) writeMove = kwargs.move;
|
|
1025
|
+
if (hasOwn(kwargs, "align")) writeAlign = kwargs.align;
|
|
1026
|
+
if (hasOwn(kwargs, "font")) writeFont = kwargs.font;
|
|
1027
|
+
};
|
|
1028
|
+
applyWriteKwargs(writeText);
|
|
1029
|
+
applyWriteKwargs(writeMove);
|
|
1030
|
+
applyWriteKwargs(writeAlign);
|
|
1031
|
+
applyWriteKwargs(writeFont);
|
|
1032
|
+
if (isWriteKwargsObject(writeFont)) writeFont = null;
|
|
1033
|
+
if (isWriteKwargsObject(writeMove)) writeMove = false;
|
|
1034
|
+
if (isWriteKwargsObject(writeAlign)) writeAlign = "left";
|
|
1035
|
+
|
|
1036
|
+
let family = currentFontFamily;
|
|
1037
|
+
let size = fontSize;
|
|
1038
|
+
let style = currentFontStyle;
|
|
1039
|
+
try {
|
|
1040
|
+
const source = toSequence(writeFont);
|
|
1041
|
+
if (source?.length) {
|
|
1042
|
+
if (source[0]) family = toPlainString(source[0], family);
|
|
1043
|
+
if (source[1] !== undefined && source[1] !== null) {
|
|
1044
|
+
size = toPlainNumber(source[1], size);
|
|
1045
|
+
}
|
|
1046
|
+
if (source[2]) style = toPlainString(source[2], style);
|
|
1047
|
+
}
|
|
1048
|
+
} catch {}
|
|
1049
|
+
const normalizedAlign = normalizeTextAlign(writeAlign);
|
|
1050
|
+
const segment = makePath({
|
|
1051
|
+
down: false,
|
|
1052
|
+
fill: false,
|
|
1053
|
+
stroke: strokeColor,
|
|
1054
|
+
points: [
|
|
1055
|
+
{
|
|
1056
|
+
x,
|
|
1057
|
+
y,
|
|
1058
|
+
text: String(writeText),
|
|
1059
|
+
align: normalizedAlign,
|
|
1060
|
+
fontsize: size,
|
|
1061
|
+
fontfamily: family,
|
|
1062
|
+
fontstyle: style,
|
|
1063
|
+
color: strokeColor,
|
|
1064
|
+
},
|
|
1065
|
+
],
|
|
1066
|
+
});
|
|
1067
|
+
if (toPlainBoolean(writeMove, false)) {
|
|
1068
|
+
const metrics = measureTurtleText(String(writeText), family, size, style);
|
|
1069
|
+
const textWidth = metrics.width || 0;
|
|
1070
|
+
if (normalizedAlign === "left") {
|
|
1071
|
+
x += textWidth;
|
|
1072
|
+
} else if (normalizedAlign === "center") {
|
|
1073
|
+
x += textWidth / 2;
|
|
1074
|
+
}
|
|
1075
|
+
// right alignment: turtle stays at its position (text ends there)
|
|
1076
|
+
commitStyleToNewPath();
|
|
1077
|
+
}
|
|
1078
|
+
enqueueOperation(() => {
|
|
1079
|
+
pen.paths.push(segment);
|
|
1080
|
+
draw();
|
|
1081
|
+
});
|
|
1082
|
+
};
|
|
1083
|
+
const setfontsize = (value) => {
|
|
1084
|
+
const nextSize = toPlainNumber(value, DEFAULT_FONT_SIZE);
|
|
1085
|
+
fontSize = Math.max(1, Number.isFinite(nextSize) ? nextSize : DEFAULT_FONT_SIZE);
|
|
1086
|
+
commitStyleToNewPath();
|
|
1087
|
+
draw();
|
|
1088
|
+
};
|
|
1089
|
+
const showturtle = () => {
|
|
1090
|
+
turtleVisible = true;
|
|
1091
|
+
enqueueOperation(() => {
|
|
1092
|
+
pen.renderedTurtleVisible = true;
|
|
1093
|
+
draw();
|
|
1094
|
+
});
|
|
1095
|
+
};
|
|
1096
|
+
const hideturtle = () => {
|
|
1097
|
+
turtleVisible = false;
|
|
1098
|
+
enqueueOperation(() => {
|
|
1099
|
+
pen.renderedTurtleVisible = false;
|
|
1100
|
+
draw();
|
|
1101
|
+
});
|
|
1102
|
+
};
|
|
1103
|
+
const isvisible = () => turtleVisible;
|
|
1104
|
+
const shape = (name = undefined) => {
|
|
1105
|
+
if (name === undefined || name === null) {
|
|
1106
|
+
return pen.shapeName;
|
|
1107
|
+
}
|
|
1108
|
+
const nameStr = toPlainString(name, DEFAULT_SHAPE).toLowerCase().trim();
|
|
1109
|
+
if (nameStr in TURTLE_SHAPES) {
|
|
1110
|
+
pen.shapeName = nameStr;
|
|
1111
|
+
}
|
|
1112
|
+
draw();
|
|
1113
|
+
return pen.shapeName;
|
|
1114
|
+
};
|
|
1115
|
+
const penFn = (options = undefined) => {
|
|
1116
|
+
if (options === undefined) {
|
|
1117
|
+
return getPenState();
|
|
1118
|
+
}
|
|
866
1119
|
const source =
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
if (
|
|
881
|
-
|
|
1120
|
+
options && typeof options.toJs === "function"
|
|
1121
|
+
? options.toJs({ pyproxies: [], dict_converter: Object.fromEntries })
|
|
1122
|
+
: options;
|
|
1123
|
+
if (!source || typeof source !== "object") return getPenState();
|
|
1124
|
+
if ("pendown" in source) {
|
|
1125
|
+
penDown = !!source.pendown;
|
|
1126
|
+
}
|
|
1127
|
+
if ("pencolor" in source) {
|
|
1128
|
+
strokeColor = toColorString(source.pencolor);
|
|
1129
|
+
}
|
|
1130
|
+
if ("fillcolor" in source) {
|
|
1131
|
+
fillColor = toColorString(source.fillcolor);
|
|
1132
|
+
}
|
|
1133
|
+
if ("pensize" in source) {
|
|
1134
|
+
ensurePath().lineWidth = Math.max(1, Number(source.pensize) || DEFAULT_LINE_WIDTH);
|
|
882
1135
|
}
|
|
1136
|
+
if ("shown" in source) {
|
|
1137
|
+
turtleVisible = !!source.shown;
|
|
1138
|
+
pen.renderedTurtleVisible = turtleVisible;
|
|
1139
|
+
}
|
|
1140
|
+
commitStyleToNewPath();
|
|
883
1141
|
draw();
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
const heading_ = () => heading;
|
|
892
|
-
const setheading = (angle) => {
|
|
893
|
-
heading = normalizeAngle(angle);
|
|
894
|
-
const nextHeading = heading;
|
|
895
|
-
enqueueOperation(() => {
|
|
896
|
-
renderedHeading = nextHeading;
|
|
1142
|
+
return getPenState();
|
|
1143
|
+
};
|
|
1144
|
+
const clear = () => {
|
|
1145
|
+
pen.paths = [];
|
|
1146
|
+
currentPath = null;
|
|
1147
|
+
fillPath = null;
|
|
1148
|
+
beginCurrentPath();
|
|
897
1149
|
draw();
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1150
|
+
};
|
|
1151
|
+
const resetPen = () => {
|
|
1152
|
+
pen.paths = [];
|
|
1153
|
+
currentPath = null;
|
|
1154
|
+
fillPath = null;
|
|
1155
|
+
x = 0;
|
|
1156
|
+
y = 0;
|
|
1157
|
+
heading = 0;
|
|
1158
|
+
pen.renderedX = 0;
|
|
1159
|
+
pen.renderedY = 0;
|
|
1160
|
+
pen.renderedHeading = 0;
|
|
1161
|
+
penDown = true;
|
|
1162
|
+
turtleVisible = true;
|
|
1163
|
+
pen.renderedTurtleVisible = true;
|
|
1164
|
+
strokeColor = "#000000";
|
|
1165
|
+
fillColor = "#000000";
|
|
1166
|
+
pen.shapeColor = "#000000";
|
|
1167
|
+
pen.shapeName = DEFAULT_SHAPE;
|
|
1168
|
+
fontSize = DEFAULT_FONT_SIZE;
|
|
1169
|
+
currentFontFamily = "Arial";
|
|
1170
|
+
currentFontStyle = "normal";
|
|
1171
|
+
filling = false;
|
|
1172
|
+
beginCurrentPath();
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
const api = {
|
|
1176
|
+
forward,
|
|
1177
|
+
fd: forward,
|
|
1178
|
+
backward,
|
|
1179
|
+
bk: backward,
|
|
1180
|
+
back: backward,
|
|
1181
|
+
right,
|
|
1182
|
+
rt: right,
|
|
1183
|
+
left,
|
|
1184
|
+
lt: left,
|
|
1185
|
+
goto: (...args) => goto_(...args),
|
|
1186
|
+
setpos: (...args) => goto_(...args),
|
|
1187
|
+
setposition: (...args) => goto_(...args),
|
|
1188
|
+
setx,
|
|
1189
|
+
sety,
|
|
1190
|
+
setheading,
|
|
1191
|
+
seth: setheading,
|
|
1192
|
+
home,
|
|
1193
|
+
circle,
|
|
1194
|
+
dot,
|
|
1195
|
+
position,
|
|
1196
|
+
pos: position,
|
|
1197
|
+
xcor,
|
|
1198
|
+
ycor,
|
|
1199
|
+
heading: heading_,
|
|
1200
|
+
towards,
|
|
1201
|
+
pendown: pendownFn,
|
|
1202
|
+
pd: pendownFn,
|
|
1203
|
+
down: pendownFn,
|
|
1204
|
+
penup,
|
|
1205
|
+
pu: penup,
|
|
1206
|
+
up: penup,
|
|
1207
|
+
pensize,
|
|
1208
|
+
width,
|
|
1209
|
+
pen: penFn,
|
|
1210
|
+
write,
|
|
1211
|
+
setfontsize,
|
|
1212
|
+
color,
|
|
1213
|
+
pencolor,
|
|
1214
|
+
fillcolor,
|
|
1215
|
+
begin_fill,
|
|
1216
|
+
end_fill,
|
|
1217
|
+
speed,
|
|
1218
|
+
showturtle,
|
|
1219
|
+
st: showturtle,
|
|
1220
|
+
hideturtle,
|
|
1221
|
+
ht: hideturtle,
|
|
1222
|
+
isvisible,
|
|
1223
|
+
shape,
|
|
1224
|
+
clear,
|
|
1225
|
+
reset: resetPen,
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
return { pen, api, resetPen };
|
|
915
1229
|
};
|
|
1230
|
+
|
|
1231
|
+
// Screen-level functions
|
|
916
1232
|
const colormode = (mode = undefined) => {
|
|
917
1233
|
if (mode === undefined) return colorMode;
|
|
918
1234
|
const numeric = Number(mode);
|
|
@@ -949,164 +1265,6 @@ hyperbook.python = (function () {
|
|
|
949
1265
|
}
|
|
950
1266
|
return [screenWidth || cssWidth || 0, screenHeight || cssHeight || 0];
|
|
951
1267
|
};
|
|
952
|
-
const color = (...args) => {
|
|
953
|
-
const stroke = toColorString(args[0]);
|
|
954
|
-
const fill = args.length > 1 ? toColorString(args[1]) : stroke;
|
|
955
|
-
strokeColor = stroke;
|
|
956
|
-
fillColor = fill;
|
|
957
|
-
commitStyleToNewPath();
|
|
958
|
-
if (filling && fillPath) {
|
|
959
|
-
fillPath.fillstyle = fillColor;
|
|
960
|
-
}
|
|
961
|
-
draw();
|
|
962
|
-
};
|
|
963
|
-
const pencolor = (value) => {
|
|
964
|
-
strokeColor = toColorString(value);
|
|
965
|
-
commitStyleToNewPath();
|
|
966
|
-
draw();
|
|
967
|
-
};
|
|
968
|
-
const fillcolor = (value) => {
|
|
969
|
-
fillColor = toColorString(value);
|
|
970
|
-
if (filling && fillPath) {
|
|
971
|
-
fillPath.fillstyle = fillColor;
|
|
972
|
-
}
|
|
973
|
-
commitStyleToNewPath();
|
|
974
|
-
draw();
|
|
975
|
-
};
|
|
976
|
-
const pensize = (value) => {
|
|
977
|
-
ensurePath().lineWidth = Math.max(1, Number(value) || DEFAULT_LINE_WIDTH);
|
|
978
|
-
commitStyleToNewPath();
|
|
979
|
-
draw();
|
|
980
|
-
};
|
|
981
|
-
const width = (value) => pensize(value);
|
|
982
|
-
const begin_fill = () => {
|
|
983
|
-
if (filling) return;
|
|
984
|
-
filling = true;
|
|
985
|
-
fillPath = makePath({
|
|
986
|
-
down: false,
|
|
987
|
-
fill: true,
|
|
988
|
-
stroke: "transparent",
|
|
989
|
-
lineWidth: 1,
|
|
990
|
-
fillstyle: fillColor,
|
|
991
|
-
});
|
|
992
|
-
paths.push(fillPath);
|
|
993
|
-
draw();
|
|
994
|
-
};
|
|
995
|
-
const end_fill = () => {
|
|
996
|
-
filling = false;
|
|
997
|
-
fillPath = null;
|
|
998
|
-
commitStyleToNewPath();
|
|
999
|
-
draw();
|
|
1000
|
-
};
|
|
1001
|
-
const dot = (size = 5, colorValue = null) => {
|
|
1002
|
-
const segment = makePath({
|
|
1003
|
-
down: false,
|
|
1004
|
-
fill: false,
|
|
1005
|
-
stroke: colorValue ? toColorString(colorValue) : strokeColor,
|
|
1006
|
-
fillstyle: colorValue ? toColorString(colorValue) : strokeColor,
|
|
1007
|
-
points: [
|
|
1008
|
-
{
|
|
1009
|
-
x,
|
|
1010
|
-
y,
|
|
1011
|
-
dotRadius: Math.max(0.5, Number(size) || 5) / 2,
|
|
1012
|
-
color: colorValue ? toColorString(colorValue) : strokeColor,
|
|
1013
|
-
},
|
|
1014
|
-
],
|
|
1015
|
-
});
|
|
1016
|
-
enqueueOperation(() => {
|
|
1017
|
-
paths.push(segment);
|
|
1018
|
-
draw();
|
|
1019
|
-
});
|
|
1020
|
-
};
|
|
1021
|
-
const circle = (radius, steps = 120) => {
|
|
1022
|
-
const numericRadius = Number(radius) || 0;
|
|
1023
|
-
const stepCount = Math.max(8, Number(steps) | 0);
|
|
1024
|
-
const circumference = 2 * Math.PI * Math.abs(numericRadius);
|
|
1025
|
-
const stepLength = circumference / stepCount;
|
|
1026
|
-
const stepTurn = (360 / stepCount) * (numericRadius >= 0 ? 1 : -1);
|
|
1027
|
-
for (let index = 0; index < stepCount; index += 1) {
|
|
1028
|
-
forward(stepLength);
|
|
1029
|
-
left(stepTurn);
|
|
1030
|
-
}
|
|
1031
|
-
};
|
|
1032
|
-
const write = (text, move = false, align = "left", font = null) => {
|
|
1033
|
-
let writeText = text;
|
|
1034
|
-
let writeMove = move;
|
|
1035
|
-
let writeAlign = align;
|
|
1036
|
-
let writeFont = font;
|
|
1037
|
-
const applyWriteKwargs = (candidate) => {
|
|
1038
|
-
if (!isWriteKwargsObject(candidate)) return;
|
|
1039
|
-
const kwargs = toPlainObject(candidate);
|
|
1040
|
-
if (!kwargs) return;
|
|
1041
|
-
if (hasOwn(kwargs, "arg")) writeText = kwargs.arg;
|
|
1042
|
-
else if (hasOwn(kwargs, "text")) writeText = kwargs.text;
|
|
1043
|
-
if (hasOwn(kwargs, "move")) writeMove = kwargs.move;
|
|
1044
|
-
if (hasOwn(kwargs, "align")) writeAlign = kwargs.align;
|
|
1045
|
-
if (hasOwn(kwargs, "font")) writeFont = kwargs.font;
|
|
1046
|
-
};
|
|
1047
|
-
applyWriteKwargs(writeText);
|
|
1048
|
-
applyWriteKwargs(writeMove);
|
|
1049
|
-
applyWriteKwargs(writeAlign);
|
|
1050
|
-
applyWriteKwargs(writeFont);
|
|
1051
|
-
if (isWriteKwargsObject(writeFont)) writeFont = null;
|
|
1052
|
-
|
|
1053
|
-
let family = currentFontFamily;
|
|
1054
|
-
let size = fontSize;
|
|
1055
|
-
let style = currentFontStyle;
|
|
1056
|
-
try {
|
|
1057
|
-
const source = toSequence(writeFont);
|
|
1058
|
-
if (source?.length) {
|
|
1059
|
-
if (source[0]) family = toPlainString(source[0], family);
|
|
1060
|
-
if (source[1] !== undefined && source[1] !== null) {
|
|
1061
|
-
size = toPlainNumber(source[1], size);
|
|
1062
|
-
}
|
|
1063
|
-
if (source[2]) style = toPlainString(source[2], style);
|
|
1064
|
-
}
|
|
1065
|
-
} catch {}
|
|
1066
|
-
const normalizedAlign = normalizeTextAlign(writeAlign);
|
|
1067
|
-
const segment = makePath({
|
|
1068
|
-
down: false,
|
|
1069
|
-
fill: false,
|
|
1070
|
-
stroke: strokeColor,
|
|
1071
|
-
points: [
|
|
1072
|
-
{
|
|
1073
|
-
x,
|
|
1074
|
-
y,
|
|
1075
|
-
text: String(writeText),
|
|
1076
|
-
align: normalizedAlign,
|
|
1077
|
-
fontsize: size,
|
|
1078
|
-
fontfamily: family,
|
|
1079
|
-
fontstyle: style,
|
|
1080
|
-
color: strokeColor,
|
|
1081
|
-
},
|
|
1082
|
-
],
|
|
1083
|
-
});
|
|
1084
|
-
if (toPlainBoolean(writeMove, false)) {
|
|
1085
|
-
const metrics = measureTurtleText(String(writeText), family, size, style);
|
|
1086
|
-
const left = metrics.left || 0;
|
|
1087
|
-
const right = metrics.right || metrics.width || 0;
|
|
1088
|
-
const align = normalizeTextAlign(normalizedAlign);
|
|
1089
|
-
const textWidth = left + right;
|
|
1090
|
-
if (align === "left") {
|
|
1091
|
-
x += TK_TEXT_X_OFFSET + textWidth;
|
|
1092
|
-
} else if (align === "center") {
|
|
1093
|
-
x += TK_TEXT_X_OFFSET + right;
|
|
1094
|
-
} else {
|
|
1095
|
-
x += TK_TEXT_X_OFFSET;
|
|
1096
|
-
}
|
|
1097
|
-
commitStyleToNewPath();
|
|
1098
|
-
}
|
|
1099
|
-
enqueueOperation(() => {
|
|
1100
|
-
paths.push(segment);
|
|
1101
|
-
draw();
|
|
1102
|
-
});
|
|
1103
|
-
};
|
|
1104
|
-
const setfontsize = (value) => {
|
|
1105
|
-
const nextSize = toPlainNumber(value, DEFAULT_FONT_SIZE);
|
|
1106
|
-
fontSize = Math.max(1, Number.isFinite(nextSize) ? nextSize : DEFAULT_FONT_SIZE);
|
|
1107
|
-
commitStyleToNewPath();
|
|
1108
|
-
draw();
|
|
1109
|
-
};
|
|
1110
1268
|
const bgcolor = (value) => {
|
|
1111
1269
|
backgroundColor = toColorString(value);
|
|
1112
1270
|
draw();
|
|
@@ -1143,60 +1301,55 @@ hyperbook.python = (function () {
|
|
|
1143
1301
|
} catch {}
|
|
1144
1302
|
}
|
|
1145
1303
|
};
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1304
|
+
|
|
1305
|
+
// Create the default turtle pen and register it
|
|
1306
|
+
const defaultPenObj = createTurtlePen();
|
|
1307
|
+
allPens.push(defaultPenObj.pen);
|
|
1308
|
+
|
|
1309
|
+
const bindCanvas = (nextCanvas) => {
|
|
1310
|
+
canvas = nextCanvas || getCanvas(id);
|
|
1311
|
+
context = canvas?.getContext?.("2d") || null;
|
|
1312
|
+
if (canvas) {
|
|
1313
|
+
canvas.tabIndex = 0;
|
|
1314
|
+
}
|
|
1152
1315
|
draw();
|
|
1153
1316
|
};
|
|
1154
|
-
|
|
1155
|
-
|
|
1317
|
+
|
|
1318
|
+
const deactivate = () => {
|
|
1319
|
+
active = false;
|
|
1320
|
+
clearQueue();
|
|
1321
|
+
for (const pen of allPens) {
|
|
1322
|
+
pen.paths = [];
|
|
1323
|
+
pen.renderedTurtleVisible = false;
|
|
1324
|
+
}
|
|
1156
1325
|
};
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1326
|
+
|
|
1327
|
+
const resetState = () => {
|
|
1328
|
+
clearQueue();
|
|
1329
|
+
// Remove all extra turtles, keeping only the default pen
|
|
1330
|
+
allPens.length = 0;
|
|
1331
|
+
allPens.push(defaultPenObj.pen);
|
|
1332
|
+
defaultPenObj.resetPen();
|
|
1333
|
+
backgroundColor = "#ffffff";
|
|
1334
|
+
backgroundImage = null;
|
|
1335
|
+
colorMode = 1.0;
|
|
1336
|
+
screenWidth = 640;
|
|
1337
|
+
screenHeight = 480;
|
|
1338
|
+
active = true;
|
|
1339
|
+
draw();
|
|
1163
1340
|
};
|
|
1164
|
-
|
|
1165
|
-
|
|
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 () {
|
|
1347
|
+
const penObj = createTurtlePen();
|
|
1166
1348
|
enqueueOperation(() => {
|
|
1167
|
-
|
|
1349
|
+
allPens.push(penObj.pen);
|
|
1168
1350
|
draw();
|
|
1169
1351
|
});
|
|
1170
|
-
|
|
1171
|
-
const isvisible = () => turtleVisible;
|
|
1172
|
-
const pen = (options = undefined) => {
|
|
1173
|
-
if (options === undefined) {
|
|
1174
|
-
return getPenState();
|
|
1175
|
-
}
|
|
1176
|
-
const source =
|
|
1177
|
-
options && typeof options.toJs === "function"
|
|
1178
|
-
? options.toJs({ pyproxies: [], dict_converter: Object.fromEntries })
|
|
1179
|
-
: options;
|
|
1180
|
-
if (!source || typeof source !== "object") return getPenState();
|
|
1181
|
-
if ("pendown" in source) {
|
|
1182
|
-
penDown = !!source.pendown;
|
|
1183
|
-
}
|
|
1184
|
-
if ("pencolor" in source) {
|
|
1185
|
-
strokeColor = toColorString(source.pencolor);
|
|
1186
|
-
}
|
|
1187
|
-
if ("fillcolor" in source) {
|
|
1188
|
-
fillColor = toColorString(source.fillcolor);
|
|
1189
|
-
}
|
|
1190
|
-
if ("pensize" in source) {
|
|
1191
|
-
ensurePath().lineWidth = Math.max(1, Number(source.pensize) || DEFAULT_LINE_WIDTH);
|
|
1192
|
-
}
|
|
1193
|
-
if ("shown" in source) {
|
|
1194
|
-
turtleVisible = !!source.shown;
|
|
1195
|
-
renderedTurtleVisible = turtleVisible;
|
|
1196
|
-
}
|
|
1197
|
-
commitStyleToNewPath();
|
|
1198
|
-
draw();
|
|
1199
|
-
return getPenState();
|
|
1352
|
+
return penObj.api;
|
|
1200
1353
|
};
|
|
1201
1354
|
|
|
1202
1355
|
const api = {
|
|
@@ -1207,59 +1360,12 @@ hyperbook.python = (function () {
|
|
|
1207
1360
|
__setPyodide: (runtime) => {
|
|
1208
1361
|
pyodide = runtime;
|
|
1209
1362
|
},
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
backward,
|
|
1213
|
-
bk: backward,
|
|
1214
|
-
back: backward,
|
|
1215
|
-
right,
|
|
1216
|
-
rt: right,
|
|
1217
|
-
left,
|
|
1218
|
-
lt: left,
|
|
1219
|
-
goto: (...args) => goto_(...args),
|
|
1220
|
-
setpos: (...args) => goto_(...args),
|
|
1221
|
-
setposition: (...args) => goto_(...args),
|
|
1222
|
-
setx,
|
|
1223
|
-
sety,
|
|
1224
|
-
setheading,
|
|
1225
|
-
seth: setheading,
|
|
1226
|
-
home,
|
|
1227
|
-
circle,
|
|
1228
|
-
dot,
|
|
1229
|
-
position,
|
|
1230
|
-
pos: position,
|
|
1231
|
-
xcor,
|
|
1232
|
-
ycor,
|
|
1233
|
-
heading: heading_,
|
|
1234
|
-
towards,
|
|
1235
|
-
pendown: pendownFn,
|
|
1236
|
-
pd: pendownFn,
|
|
1237
|
-
down: pendownFn,
|
|
1238
|
-
penup,
|
|
1239
|
-
pu: penup,
|
|
1240
|
-
up: penup,
|
|
1241
|
-
pensize,
|
|
1242
|
-
width,
|
|
1243
|
-
pen,
|
|
1244
|
-
write,
|
|
1245
|
-
setfontsize,
|
|
1246
|
-
color,
|
|
1247
|
-
pencolor,
|
|
1248
|
-
fillcolor,
|
|
1249
|
-
begin_fill,
|
|
1250
|
-
end_fill,
|
|
1251
|
-
bgcolor,
|
|
1252
|
-
bgpic,
|
|
1253
|
-
reset,
|
|
1254
|
-
clear,
|
|
1255
|
-
speed,
|
|
1363
|
+
...defaultPenObj.api,
|
|
1364
|
+
Turtle,
|
|
1256
1365
|
colormode,
|
|
1257
1366
|
screensize,
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
hideturtle,
|
|
1261
|
-
ht: hideturtle,
|
|
1262
|
-
isvisible,
|
|
1367
|
+
bgcolor,
|
|
1368
|
+
bgpic,
|
|
1263
1369
|
};
|
|
1264
1370
|
|
|
1265
1371
|
return api;
|
|
@@ -1539,6 +1645,156 @@ hyperbook.python = (function () {
|
|
|
1539
1645
|
);
|
|
1540
1646
|
};
|
|
1541
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
|
+
const buildSvgElements = (specs) => {
|
|
1652
|
+
const stack = [];
|
|
1653
|
+
const measureCanvas = document.createElement("canvas");
|
|
1654
|
+
const measureCtx = measureCanvas.getContext("2d");
|
|
1655
|
+
|
|
1656
|
+
for (const spec of specs || []) {
|
|
1657
|
+
if (!spec || typeof spec !== "object") continue;
|
|
1658
|
+
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
|
+
) {
|
|
1668
|
+
let width = 0, height = 0, pin = { x: 0, y: 0 };
|
|
1669
|
+
let svg = "";
|
|
1670
|
+
|
|
1671
|
+
if (type === "Rectangle") {
|
|
1672
|
+
width = Math.max(0, Number(spec.width) || 0);
|
|
1673
|
+
height = Math.max(0, Number(spec.height) || 0);
|
|
1674
|
+
const fill = colorToCss(spec.color);
|
|
1675
|
+
svg = `<rect x="${-width / 2}" y="${-height / 2}" width="${width}" height="${height}" fill="${fill}"/>`;
|
|
1676
|
+
} else if (type === "Ellipse") {
|
|
1677
|
+
width = Math.max(0, Number(spec.width) || 0);
|
|
1678
|
+
height = Math.max(0, Number(spec.height) || 0);
|
|
1679
|
+
const fill = colorToCss(spec.color);
|
|
1680
|
+
svg = `<ellipse cx="0" cy="0" rx="${width / 2}" ry="${height / 2}" fill="${fill}"/>`;
|
|
1681
|
+
} else if (type === "CircularSector") {
|
|
1682
|
+
const radius = Math.max(0, Number(spec.radius) || 0);
|
|
1683
|
+
const angle = Number(spec.angle) || 0;
|
|
1684
|
+
width = radius * 2;
|
|
1685
|
+
height = radius * 2;
|
|
1686
|
+
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
|
+
const endAngleRad = -angle * Math.PI / 180;
|
|
1690
|
+
const endX = radius * Math.cos(endAngleRad);
|
|
1691
|
+
const endY = radius * Math.sin(endAngleRad);
|
|
1692
|
+
const largeArcFlag = angle > 180 ? 1 : 0;
|
|
1693
|
+
svg = `<path d="M 0 0 L ${radius} 0 A ${radius} ${radius} 0 ${largeArcFlag} 0 ${endX} ${endY} Z" fill="${fill}"/>`;
|
|
1694
|
+
} else if (type === "Triangle") {
|
|
1695
|
+
const side1 = Math.max(0, Number(spec.side1) || 0);
|
|
1696
|
+
const side2 = Math.max(0, Number(spec.side2) || 0);
|
|
1697
|
+
const angle = (Number(spec.angle) || 0) * (Math.PI / 180);
|
|
1698
|
+
const p1 = { x: 0, y: 0 };
|
|
1699
|
+
const p2 = { x: side1, y: 0 };
|
|
1700
|
+
const p3 = { x: side2 * Math.cos(angle), y: -side2 * Math.sin(angle) };
|
|
1701
|
+
const centroid = { x: (p1.x + p2.x + p3.x) / 3, y: (p1.y + p2.y + p3.y) / 3 };
|
|
1702
|
+
const points = [p1, p2, p3].map((p) => ({ x: p.x - centroid.x, y: p.y - centroid.y }));
|
|
1703
|
+
const xs = points.map((p) => p.x);
|
|
1704
|
+
const ys = points.map((p) => p.y);
|
|
1705
|
+
width = Math.max(...xs) - Math.min(...xs);
|
|
1706
|
+
height = Math.max(...ys) - Math.min(...ys);
|
|
1707
|
+
const fill = colorToCss(spec.color);
|
|
1708
|
+
svg = `<polygon points="${points.map((p) => `${p.x},${p.y}`).join(" ")}" fill="${fill}"/>`;
|
|
1709
|
+
} else if (type === "Text") {
|
|
1710
|
+
const text = String(spec.text || "");
|
|
1711
|
+
const fontName = String(spec.font_name || "sans-serif");
|
|
1712
|
+
const textSize = Math.max(1, Number(spec.text_size) || 1);
|
|
1713
|
+
const fill = colorToCss(spec.color);
|
|
1714
|
+
measureCtx.font = `${textSize}px ${fontName}`;
|
|
1715
|
+
const metrics = measureCtx.measureText(text);
|
|
1716
|
+
width = Math.max(0, metrics.width || 0);
|
|
1717
|
+
const ascent = metrics.actualBoundingBoxAscent || textSize * 0.8;
|
|
1718
|
+
const descent = metrics.actualBoundingBoxDescent || textSize * 0.2;
|
|
1719
|
+
height = ascent + descent;
|
|
1720
|
+
pin = { x: -width / 2, y: (ascent - descent) / 2 };
|
|
1721
|
+
// y attribute is the text baseline; mirrors canvas fillText(-w/2, (ascent-descent)/2)
|
|
1722
|
+
svg = `<text x="${-width / 2}" y="${(ascent - descent) / 2}" font-family="${fontName}" font-size="${textSize}" fill="${fill}" text-anchor="start">${text}</text>`;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
stack.push({ width, height, pin, svg });
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
if (type === "Pin") {
|
|
1730
|
+
const child = stack.pop();
|
|
1731
|
+
if (!child) continue;
|
|
1732
|
+
const pin = decodePoint(spec.pin, child.width, child.height);
|
|
1733
|
+
stack.push({ ...child, pin });
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
if (type === "Rotate") {
|
|
1738
|
+
const child = stack.pop();
|
|
1739
|
+
if (!child) continue;
|
|
1740
|
+
const angleDeg = Number(spec.angle) || 0;
|
|
1741
|
+
const angleRad = (-angleDeg * Math.PI) / 180;
|
|
1742
|
+
const corners = [
|
|
1743
|
+
{ x: -child.width / 2, y: -child.height / 2 },
|
|
1744
|
+
{ x: child.width / 2, y: -child.height / 2 },
|
|
1745
|
+
{ x: child.width / 2, y: child.height / 2 },
|
|
1746
|
+
{ x: -child.width / 2, y: child.height / 2 },
|
|
1747
|
+
].map((p) => rotatePoint(p, child.pin, angleRad));
|
|
1748
|
+
const minX = Math.min(...corners.map((p) => p.x));
|
|
1749
|
+
const maxX = Math.max(...corners.map((p) => p.x));
|
|
1750
|
+
const minY = Math.min(...corners.map((p) => p.y));
|
|
1751
|
+
const maxY = Math.max(...corners.map((p) => p.y));
|
|
1752
|
+
const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
|
|
1753
|
+
const offset = { x: -center.x, y: -center.y };
|
|
1754
|
+
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
|
+
const transform = `translate(${offset.x},${offset.y}) rotate(${-angleDeg},${child.pin.x},${child.pin.y})`;
|
|
1758
|
+
stack.push({
|
|
1759
|
+
width: maxX - minX,
|
|
1760
|
+
height: maxY - minY,
|
|
1761
|
+
pin,
|
|
1762
|
+
svg: `<g transform="${transform}">${child.svg}</g>`,
|
|
1763
|
+
});
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (type === "Compose") {
|
|
1768
|
+
const bg = stack.pop();
|
|
1769
|
+
const fg = stack.pop();
|
|
1770
|
+
if (!fg || !bg) continue;
|
|
1771
|
+
const fgPin = spec.fg_pin ? decodePoint(spec.fg_pin, fg.width, fg.height) : fg.pin;
|
|
1772
|
+
const bgPin = spec.bg_pin ? decodePoint(spec.bg_pin, bg.width, bg.height) : bg.pin;
|
|
1773
|
+
const bgCenter = { x: 0, y: 0 };
|
|
1774
|
+
const fgCenter = { x: bgPin.x - fgPin.x, y: bgPin.y - fgPin.y };
|
|
1775
|
+
const minX = Math.min(fgCenter.x - fg.width / 2, bgCenter.x - bg.width / 2);
|
|
1776
|
+
const maxX = Math.max(fgCenter.x + fg.width / 2, bgCenter.x + bg.width / 2);
|
|
1777
|
+
const minY = Math.min(fgCenter.y - fg.height / 2, bgCenter.y - bg.height / 2);
|
|
1778
|
+
const maxY = Math.max(fgCenter.y + fg.height / 2, bgCenter.y + bg.height / 2);
|
|
1779
|
+
const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
|
|
1780
|
+
const fgOffset = { x: fgCenter.x - center.x, y: fgCenter.y - center.y };
|
|
1781
|
+
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
|
|
1786
|
+
stack.push({
|
|
1787
|
+
width: maxX - minX,
|
|
1788
|
+
height: maxY - minY,
|
|
1789
|
+
pin,
|
|
1790
|
+
svg: `<g transform="translate(${bgOffset.x},${bgOffset.y})">${bg.svg}</g><g transform="translate(${fgOffset.x},${fgOffset.y})">${fg.svg}</g>`,
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
return stack[stack.length - 1] || { width: 0, height: 0, pin: { x: 0, y: 0 }, svg: "" };
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1542
1798
|
return {
|
|
1543
1799
|
js_graphic_size: (specs) => {
|
|
1544
1800
|
try {
|
|
@@ -1584,6 +1840,24 @@ hyperbook.python = (function () {
|
|
|
1584
1840
|
throw e;
|
|
1585
1841
|
}
|
|
1586
1842
|
},
|
|
1843
|
+
js_svg_graphic: (specs, debug) => {
|
|
1844
|
+
try {
|
|
1845
|
+
const unproxiedSpecs = unProxy(specs);
|
|
1846
|
+
const result = buildSvgElements(unproxiedSpecs);
|
|
1847
|
+
const width = Math.max(1, Math.ceil(result.width));
|
|
1848
|
+
const height = Math.max(1, Math.ceil(result.height));
|
|
1849
|
+
let inner = result.svg;
|
|
1850
|
+
if (debug) {
|
|
1851
|
+
inner += `<rect x="${-width / 2}" y="${-height / 2}" width="${width}" height="${height}" fill="none" stroke="red" stroke-width="1"/>`;
|
|
1852
|
+
inner += `<line x1="${result.pin.x - 8}" y1="${result.pin.y}" x2="${result.pin.x + 8}" y2="${result.pin.y}" stroke="rgba(255,255,0,0.8)" stroke-width="1"/>`;
|
|
1853
|
+
inner += `<line x1="${result.pin.x}" y1="${result.pin.y - 8}" x2="${result.pin.x}" y2="${result.pin.y + 8}" stroke="rgba(255,255,0,0.8)" stroke-width="1"/>`;
|
|
1854
|
+
}
|
|
1855
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><g transform="translate(${width / 2},${height / 2})">${inner}</g></svg>`;
|
|
1856
|
+
} catch (e) {
|
|
1857
|
+
console.error("js_svg_graphic error:", e);
|
|
1858
|
+
throw e;
|
|
1859
|
+
}
|
|
1860
|
+
},
|
|
1587
1861
|
js_save: (filename, content) => {
|
|
1588
1862
|
const link = document.createElement("a");
|
|
1589
1863
|
link.href = String(content || "");
|
|
@@ -1776,22 +2050,31 @@ hyperbook.python = (function () {
|
|
|
1776
2050
|
}
|
|
1777
2051
|
}
|
|
1778
2052
|
|
|
2053
|
+
let lastStdinPrompt = "";
|
|
2054
|
+
pyodide.setStdout({
|
|
2055
|
+
write: (msg) => {
|
|
2056
|
+
const text = typeof msg === "string" ? msg : decoder.decode(msg);
|
|
2057
|
+
if (text.endsWith("\n")) {
|
|
2058
|
+
lastStdinPrompt = "";
|
|
2059
|
+
} else {
|
|
2060
|
+
lastStdinPrompt += text;
|
|
2061
|
+
}
|
|
2062
|
+
appendOutputLine(id, text);
|
|
2063
|
+
return msg?.length ?? text.length;
|
|
2064
|
+
},
|
|
2065
|
+
});
|
|
1779
2066
|
pyodide.setStdin({
|
|
1780
2067
|
stdin: () => {
|
|
1781
|
-
const
|
|
2068
|
+
const promptText =
|
|
2069
|
+
lastStdinPrompt || hyperbook.i18n.get("pyide-input-prompt");
|
|
2070
|
+
lastStdinPrompt = "";
|
|
2071
|
+
const value = window.prompt(promptText);
|
|
1782
2072
|
if (value === null) {
|
|
1783
2073
|
return "";
|
|
1784
2074
|
}
|
|
1785
2075
|
return value;
|
|
1786
2076
|
},
|
|
1787
2077
|
});
|
|
1788
|
-
pyodide.setStdout({
|
|
1789
|
-
write: (msg) => {
|
|
1790
|
-
const text = typeof msg === "string" ? msg : decoder.decode(msg);
|
|
1791
|
-
appendOutputLine(id, text);
|
|
1792
|
-
return msg?.length ?? text.length;
|
|
1793
|
-
},
|
|
1794
|
-
});
|
|
1795
2078
|
pyodide.setStderr({
|
|
1796
2079
|
write: (msg) => {
|
|
1797
2080
|
const text = typeof msg === "string" ? msg : decoder.decode(msg);
|
|
@@ -2428,7 +2711,7 @@ if _pg:
|
|
|
2428
2711
|
if (results) {
|
|
2429
2712
|
appendOutput(output, results);
|
|
2430
2713
|
} else if (error) {
|
|
2431
|
-
|
|
2714
|
+
appendFriendlyError(output, error, testCode);
|
|
2432
2715
|
}
|
|
2433
2716
|
}
|
|
2434
2717
|
} catch (e) {
|
|
@@ -2476,7 +2759,7 @@ if _pg:
|
|
|
2476
2759
|
appendOutput(output, results, false, id);
|
|
2477
2760
|
} else if (error) {
|
|
2478
2761
|
showOutput();
|
|
2479
|
-
|
|
2762
|
+
appendFriendlyError(output, error, script);
|
|
2480
2763
|
}
|
|
2481
2764
|
} else {
|
|
2482
2765
|
appendOutputLine(id, "Execution stopped.");
|