mirror of
https://github.com/remko/waforth
synced 2025-01-14 08:01:34 +01:00
thurtle: Refactor
This commit is contained in:
parent
f2e37ba093
commit
eded367819
2 changed files with 188 additions and 199 deletions
168
src/web/thurtle/draw.tsx
Normal file
168
src/web/thurtle/draw.tsx
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
import * as jsx from "./jsx";
|
||||||
|
import WAForth from "waforth";
|
||||||
|
import thurtleFS from "./thurtle.fs";
|
||||||
|
import turtle from "./turtle.svg";
|
||||||
|
|
||||||
|
const padding = 0.05;
|
||||||
|
|
||||||
|
enum PenState {
|
||||||
|
Up = 0,
|
||||||
|
Down = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Path = {
|
||||||
|
strokeWidth?: number;
|
||||||
|
d: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function draw({
|
||||||
|
program,
|
||||||
|
drawEl,
|
||||||
|
outputEl,
|
||||||
|
showTurtle = true,
|
||||||
|
}: {
|
||||||
|
program?: string;
|
||||||
|
drawEl: SVGSVGElement;
|
||||||
|
outputEl?: HTMLElement;
|
||||||
|
showTurtle?: boolean;
|
||||||
|
}) {
|
||||||
|
// Initialize state
|
||||||
|
let rotation = 270;
|
||||||
|
let position = { x: 0, y: 0 };
|
||||||
|
let boundingBox = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
||||||
|
let pen = PenState.Down;
|
||||||
|
let visible = true;
|
||||||
|
let paths: Array<Path> = [{ d: [`M${position.x} ${position.y}`] }];
|
||||||
|
|
||||||
|
function updatePosition(x: number, y: number) {
|
||||||
|
position.x = x;
|
||||||
|
position.y = y;
|
||||||
|
if (x < boundingBox.minX) {
|
||||||
|
boundingBox.minX = x;
|
||||||
|
}
|
||||||
|
if (x > boundingBox.maxX) {
|
||||||
|
boundingBox.maxX = x;
|
||||||
|
}
|
||||||
|
if (y < boundingBox.minY) {
|
||||||
|
boundingBox.minY = y;
|
||||||
|
}
|
||||||
|
if (y > boundingBox.maxY) {
|
||||||
|
boundingBox.maxY = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run program
|
||||||
|
if (program != null) {
|
||||||
|
const forth = new WAForth();
|
||||||
|
await forth.load();
|
||||||
|
|
||||||
|
forth.bind("forward", (stack) => {
|
||||||
|
const d = stack.pop();
|
||||||
|
const dx = d * Math.cos((rotation * Math.PI) / 180.0);
|
||||||
|
const dy = d * Math.sin((rotation * Math.PI) / 180.0);
|
||||||
|
paths[paths.length - 1].d.push(
|
||||||
|
[pen === PenState.Down ? "l" : "m", dx, dy].join(" ")
|
||||||
|
);
|
||||||
|
updatePosition(position.x + dx, position.y + dy);
|
||||||
|
});
|
||||||
|
|
||||||
|
forth.bind("rotate", (stack) => {
|
||||||
|
rotation = rotation - stack.pop();
|
||||||
|
});
|
||||||
|
|
||||||
|
forth.bind("pen", (stack) => {
|
||||||
|
pen = stack.pop();
|
||||||
|
});
|
||||||
|
|
||||||
|
forth.bind("turtle", (stack) => {
|
||||||
|
visible = stack.pop() !== 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
forth.bind("setpensize", (stack) => {
|
||||||
|
const s = stack.pop();
|
||||||
|
paths.push({ d: [`M ${position.x} ${position.y}`], strokeWidth: s });
|
||||||
|
});
|
||||||
|
|
||||||
|
forth.bind("setxy", (stack) => {
|
||||||
|
const y = stack.pop();
|
||||||
|
const x = stack.pop();
|
||||||
|
paths[paths.length - 1].d.push(
|
||||||
|
[pen === PenState.Down ? "l" : "M", x, y].join(" ")
|
||||||
|
);
|
||||||
|
updatePosition(x, y);
|
||||||
|
});
|
||||||
|
|
||||||
|
forth.bind("setheading", (stack) => {
|
||||||
|
rotation = -90 - stack.pop();
|
||||||
|
});
|
||||||
|
|
||||||
|
forth.interpret(thurtleFS);
|
||||||
|
if (outputEl != null) {
|
||||||
|
forth.onEmit = (c) => {
|
||||||
|
outputEl.appendChild(document.createTextNode(c));
|
||||||
|
if (c === "\n") {
|
||||||
|
outputEl.scrollTop = outputEl.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
forth.interpret(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw
|
||||||
|
drawEl.innerHTML = "";
|
||||||
|
const pathsEl = (
|
||||||
|
<g fill-opacity="0" stroke="#000" xmlns="http://www.w3.org/2000/svg"></g>
|
||||||
|
);
|
||||||
|
const turtleEl = (
|
||||||
|
<image
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
data-hook="turtle"
|
||||||
|
width="50"
|
||||||
|
height="50"
|
||||||
|
href={turtle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
pathsEl.innerHTML = "";
|
||||||
|
for (const path of paths) {
|
||||||
|
const pathEl = (
|
||||||
|
<path
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
d={path.d.join(" ")}
|
||||||
|
stroke-width={(path.strokeWidth ?? 5) + ""}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
pathsEl.appendChild(pathEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
turtleEl.style.display = visible ? "block" : "none";
|
||||||
|
turtleEl.setAttribute(
|
||||||
|
"transform",
|
||||||
|
`rotate(${rotation} ${position.x} ${position.y}) translate(${
|
||||||
|
position.x - 25
|
||||||
|
} ${position.y - 25})`
|
||||||
|
);
|
||||||
|
|
||||||
|
const width = boundingBox.maxX - boundingBox.minX;
|
||||||
|
const height = boundingBox.maxY - boundingBox.minY;
|
||||||
|
if (width == 0 || height == 0) {
|
||||||
|
drawEl.setAttribute("viewBox", "-500 -500 1000 1000");
|
||||||
|
} else {
|
||||||
|
const paddingX = width * padding;
|
||||||
|
const paddingY = height * padding;
|
||||||
|
drawEl.setAttribute(
|
||||||
|
"viewBox",
|
||||||
|
[
|
||||||
|
Math.floor(boundingBox.minX - paddingX),
|
||||||
|
Math.floor(boundingBox.minY - paddingY),
|
||||||
|
Math.ceil(width + 2 * paddingX),
|
||||||
|
Math.ceil(height + 2 * paddingY),
|
||||||
|
].join(" ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawEl.appendChild(pathsEl);
|
||||||
|
if (showTurtle) {
|
||||||
|
drawEl.appendChild(turtleEl);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,11 +7,8 @@
|
||||||
/////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
import * as jsx from "./jsx";
|
import * as jsx from "./jsx";
|
||||||
import WAForth from "waforth";
|
|
||||||
import "./thurtle.css";
|
import "./thurtle.css";
|
||||||
import turtle from "./turtle.svg";
|
|
||||||
import logo from "../../../doc/logo.svg";
|
import logo from "../../../doc/logo.svg";
|
||||||
import thurtleFS from "./thurtle.fs";
|
|
||||||
import {
|
import {
|
||||||
deleteProgram,
|
deleteProgram,
|
||||||
getProgram,
|
getProgram,
|
||||||
|
@ -20,6 +17,7 @@ import {
|
||||||
} from "./programs";
|
} from "./programs";
|
||||||
import Editor from "./Editor";
|
import Editor from "./Editor";
|
||||||
import { saveAs } from "file-saver";
|
import { saveAs } from "file-saver";
|
||||||
|
import draw from "./draw";
|
||||||
|
|
||||||
declare let bootstrap: any;
|
declare let bootstrap: any;
|
||||||
|
|
||||||
|
@ -214,25 +212,9 @@ const rootEl = (
|
||||||
<div class="d-flex flex-column ms-2 right-pane">
|
<div class="d-flex flex-column ms-2 right-pane">
|
||||||
<svg
|
<svg
|
||||||
class="world"
|
class="world"
|
||||||
viewBox="-500 -500 1000 1000"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
data-hook="world"
|
data-hook="world"
|
||||||
>
|
/>
|
||||||
<g
|
|
||||||
fill-opacity="0"
|
|
||||||
stroke="#000"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="paths"
|
|
||||||
></g>
|
|
||||||
<image
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="turtle"
|
|
||||||
data-hook="turtle"
|
|
||||||
width="50"
|
|
||||||
height="50"
|
|
||||||
href={turtle}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<form>
|
<form>
|
||||||
<div class="form-group mt-2">
|
<div class="form-group mt-2">
|
||||||
<label>Output</label>
|
<label>Output</label>
|
||||||
|
@ -383,9 +365,6 @@ const rootEl = (
|
||||||
);
|
);
|
||||||
document.body.appendChild(rootEl);
|
document.body.appendChild(rootEl);
|
||||||
|
|
||||||
const turtleEl = document.getElementById("turtle")!;
|
|
||||||
let pathEl: SVGPathElement;
|
|
||||||
const patshEl = document.getElementById("paths")!;
|
|
||||||
const runButtonEl = rootEl.querySelector(
|
const runButtonEl = rootEl.querySelector(
|
||||||
"button[data-hook=run]"
|
"button[data-hook=run]"
|
||||||
)! as HTMLButtonElement;
|
)! as HTMLButtonElement;
|
||||||
|
@ -411,130 +390,6 @@ const shareModalURLEl = rootEl.querySelector(
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const worldEl = rootEl.querySelector("[data-hook=world]") as SVGSVGElement;
|
const worldEl = rootEl.querySelector("[data-hook=world]") as SVGSVGElement;
|
||||||
|
|
||||||
enum PenState {
|
|
||||||
Up = 0,
|
|
||||||
Down = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Path = {
|
|
||||||
strokeWidth?: number;
|
|
||||||
d: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
let rotation = 0;
|
|
||||||
let position = { x: 0, y: 0 };
|
|
||||||
let boundingBox = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
|
||||||
let pen = PenState.Down;
|
|
||||||
let visible = true;
|
|
||||||
let paths: Array<Path> = [{ d: [`M${position.x} ${position.y}`] }];
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
position.x = position.y = 0;
|
|
||||||
boundingBox = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
|
||||||
rotation = 270;
|
|
||||||
pen = PenState.Down;
|
|
||||||
paths = [{ d: [`M ${position.x} ${position.y}`] }];
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotate(deg: number) {
|
|
||||||
rotation = rotation + deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRotation(deg: number) {
|
|
||||||
rotation = deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
function forward(d: number) {
|
|
||||||
const dx = d * Math.cos((rotation * Math.PI) / 180.0);
|
|
||||||
const dy = d * Math.sin((rotation * Math.PI) / 180.0);
|
|
||||||
paths[paths.length - 1].d.push(
|
|
||||||
[pen === PenState.Down ? "l" : "m", dx, dy].join(" ")
|
|
||||||
);
|
|
||||||
updatePosition(position.x + dx, position.y + dy);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setXY(x: number, y: number) {
|
|
||||||
paths[paths.length - 1].d.push(
|
|
||||||
[pen === PenState.Down ? "l" : "M", x, y].join(" ")
|
|
||||||
);
|
|
||||||
updatePosition(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPen(s: PenState) {
|
|
||||||
pen = s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPenSize(s: number) {
|
|
||||||
paths.push({ d: [`M ${position.x} ${position.y}`], strokeWidth: s });
|
|
||||||
}
|
|
||||||
|
|
||||||
function setVisible(b: boolean) {
|
|
||||||
visible = b;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePosition(x: number, y: number) {
|
|
||||||
position.x = x;
|
|
||||||
position.y = y;
|
|
||||||
if (x < boundingBox.minX) {
|
|
||||||
boundingBox.minX = x;
|
|
||||||
}
|
|
||||||
if (x > boundingBox.maxX) {
|
|
||||||
boundingBox.maxX = x;
|
|
||||||
}
|
|
||||||
if (y < boundingBox.minY) {
|
|
||||||
boundingBox.minY = y;
|
|
||||||
}
|
|
||||||
if (y > boundingBox.maxY) {
|
|
||||||
boundingBox.maxY = y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Drawing
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
const padding = 0.05;
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
patshEl.innerHTML = "";
|
|
||||||
for (const path of paths) {
|
|
||||||
const pathEl = (
|
|
||||||
<path
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
d={path.d.join(" ")}
|
|
||||||
stroke-width={(path.strokeWidth ?? 5) + ""}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
patshEl.appendChild(pathEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
turtleEl.style.display = visible ? "block" : "none";
|
|
||||||
turtleEl.setAttribute(
|
|
||||||
"transform",
|
|
||||||
`rotate(${rotation} ${position.x} ${position.y}) translate(${
|
|
||||||
position.x - 25
|
|
||||||
} ${position.y - 25})`
|
|
||||||
);
|
|
||||||
|
|
||||||
const width = boundingBox.maxX - boundingBox.minX;
|
|
||||||
const height = boundingBox.maxY - boundingBox.minY;
|
|
||||||
if (width == 0 || height == 0) {
|
|
||||||
worldEl.setAttribute("viewBox", "-500 -500 1000 1000");
|
|
||||||
} else {
|
|
||||||
const paddingX = width * padding;
|
|
||||||
const paddingY = height * padding;
|
|
||||||
worldEl.setAttribute(
|
|
||||||
"viewBox",
|
|
||||||
[
|
|
||||||
Math.floor(boundingBox.minX - paddingX),
|
|
||||||
Math.floor(boundingBox.minY - paddingY),
|
|
||||||
Math.ceil(width + 2 * paddingX),
|
|
||||||
Math.ceil(height + 2 * paddingY),
|
|
||||||
].join(" ")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Programs
|
// Programs
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -602,16 +457,15 @@ async function getSVG(): Promise<{
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
}> {
|
}> {
|
||||||
await run();
|
const svgEl = <svg xmlns="http://www.w3.org/2000/svg" />;
|
||||||
const n = worldEl.cloneNode(true) as SVGSVGElement;
|
await draw({ program: editor.getValue(), drawEl: svgEl, showTurtle: false });
|
||||||
n.querySelector("[data-hook=turtle]")?.remove();
|
const viewBox = svgEl.getAttribute("viewBox")!.split(" ");
|
||||||
const viewBox = n.getAttribute("viewBox")!.split(" ");
|
svgEl.setAttribute("width", parseInt(viewBox[2]) + "");
|
||||||
n.setAttribute("width", parseInt(viewBox[2]) + "");
|
svgEl.setAttribute("height", parseInt(viewBox[3]) + "");
|
||||||
n.setAttribute("height", parseInt(viewBox[3]) + "");
|
|
||||||
return {
|
return {
|
||||||
width: parseInt(n.getAttribute("width")!),
|
width: parseInt(svgEl.getAttribute("width")!),
|
||||||
height: parseInt(n.getAttribute("height")!),
|
height: parseInt(svgEl.getAttribute("height")!),
|
||||||
data: n.outerHTML,
|
data: svgEl.outerHTML,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -678,49 +532,19 @@ programsEl.addEventListener("change", (ev) => {
|
||||||
loadProgram((ev.target! as HTMLSelectElement).value);
|
loadProgram((ev.target! as HTMLSelectElement).value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (ev) => {
|
||||||
|
if (ev.key == "Enter" && (ev.metaKey || ev.ctrlKey)) {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
runButtonEl.disabled = true;
|
runButtonEl.disabled = true;
|
||||||
reset();
|
await draw({ program: editor.getValue(), drawEl: worldEl, outputEl });
|
||||||
|
|
||||||
const forth = new WAForth();
|
|
||||||
await forth.load();
|
|
||||||
forth.bind("forward", (stack) => {
|
|
||||||
forward(stack.pop());
|
|
||||||
});
|
|
||||||
forth.bind("rotate", (stack) => {
|
|
||||||
rotate(-stack.pop());
|
|
||||||
});
|
|
||||||
forth.bind("pen", (stack) => {
|
|
||||||
setPen(stack.pop());
|
|
||||||
});
|
|
||||||
forth.bind("turtle", (stack) => {
|
|
||||||
setVisible(stack.pop() != 0);
|
|
||||||
});
|
|
||||||
forth.bind("setpensize", (stack) => {
|
|
||||||
setPenSize(stack.pop());
|
|
||||||
});
|
|
||||||
forth.bind("setxy", (stack) => {
|
|
||||||
const y = stack.pop();
|
|
||||||
const x = stack.pop();
|
|
||||||
setXY(x, -y);
|
|
||||||
});
|
|
||||||
forth.bind("setheading", (stack) => {
|
|
||||||
setRotation(-90 - stack.pop());
|
|
||||||
});
|
|
||||||
forth.interpret(thurtleFS);
|
|
||||||
forth.onEmit = (c) => {
|
|
||||||
outputEl.appendChild(document.createTextNode(c));
|
|
||||||
if (c === "\n") {
|
|
||||||
outputEl.scrollTop = outputEl.scrollHeight;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
forth.interpret(editor.getValue());
|
|
||||||
editor.focus();
|
editor.focus();
|
||||||
|
|
||||||
draw();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -728,11 +552,9 @@ async function run() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("keydown", (ev) => {
|
async function reset() {
|
||||||
if (ev.key == "Enter" && (ev.metaKey || ev.ctrlKey)) {
|
await draw({ drawEl: worldEl, outputEl });
|
||||||
run();
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@ -749,5 +571,4 @@ if (qs.ar) {
|
||||||
run();
|
run();
|
||||||
} else {
|
} else {
|
||||||
reset();
|
reset();
|
||||||
draw();
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue