mirror of
https://github.com/remko/waforth
synced 2025-01-14 08:01:34 +01:00
thurtle: Add Load/Save
This commit is contained in:
parent
84a06ac1ba
commit
d6f2418248
2 changed files with 199 additions and 13 deletions
|
@ -1,11 +1,13 @@
|
||||||
export type Example = {
|
export type Program = {
|
||||||
name: string;
|
name: string;
|
||||||
program: string;
|
program: string;
|
||||||
|
isExample: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default [
|
const examples: Program[] = [
|
||||||
{
|
{
|
||||||
name: "Square",
|
name: "Square",
|
||||||
|
isExample: true,
|
||||||
program: `
|
program: `
|
||||||
200 FORWARD
|
200 FORWARD
|
||||||
90 RIGHT
|
90 RIGHT
|
||||||
|
@ -19,6 +21,7 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Square (w/ LOOP)",
|
name: "Square (w/ LOOP)",
|
||||||
|
isExample: true,
|
||||||
program: `
|
program: `
|
||||||
: SQUARE ( n -- )
|
: SQUARE ( n -- )
|
||||||
4 0 DO
|
4 0 DO
|
||||||
|
@ -32,6 +35,7 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Pentagram",
|
name: "Pentagram",
|
||||||
|
isExample: true,
|
||||||
program: `
|
program: `
|
||||||
: PENTAGRAM ( n -- )
|
: PENTAGRAM ( n -- )
|
||||||
18 RIGHT
|
18 RIGHT
|
||||||
|
@ -46,6 +50,7 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Seeker",
|
name: "Seeker",
|
||||||
|
isExample: true,
|
||||||
program: `
|
program: `
|
||||||
: SEEKER ( n -- )
|
: SEEKER ( n -- )
|
||||||
4 0 DO
|
4 0 DO
|
||||||
|
@ -63,6 +68,7 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Flower",
|
name: "Flower",
|
||||||
|
isExample: true,
|
||||||
program: `
|
program: `
|
||||||
: SQUARE ( n -- )
|
: SQUARE ( n -- )
|
||||||
4 0 DO
|
4 0 DO
|
||||||
|
@ -83,6 +89,7 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Spiral (Recursive)",
|
name: "Spiral (Recursive)",
|
||||||
|
isExample: true,
|
||||||
program: `
|
program: `
|
||||||
: SPIRAL ( n -- )
|
: SPIRAL ( n -- )
|
||||||
DUP 1 < IF DROP EXIT THEN
|
DUP 1 < IF DROP EXIT THEN
|
||||||
|
@ -97,6 +104,7 @@ PENUP -500 -180 SETXY PENDOWN
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Outward Square Spiral",
|
name: "Outward Square Spiral",
|
||||||
|
isExample: true,
|
||||||
program: `
|
program: `
|
||||||
: SPIRAL ( n1 n2 -- )
|
: SPIRAL ( n1 n2 -- )
|
||||||
OVER 800 > IF 2DROP EXIT THEN
|
OVER 800 > IF 2DROP EXIT THEN
|
||||||
|
@ -111,6 +119,7 @@ PENUP -500 -180 SETXY PENDOWN
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Crooked Outward Square Spiral",
|
name: "Crooked Outward Square Spiral",
|
||||||
|
isExample: true,
|
||||||
program: `
|
program: `
|
||||||
91 CONSTANT ANGLE
|
91 CONSTANT ANGLE
|
||||||
|
|
||||||
|
@ -125,3 +134,56 @@ PENUP -500 -180 SETXY PENDOWN
|
||||||
1 SPIRAL`,
|
1 SPIRAL`,
|
||||||
},
|
},
|
||||||
].map((e) => ({ ...e, program: e.program.trimStart() }));
|
].map((e) => ({ ...e, program: e.program.trimStart() }));
|
||||||
|
|
||||||
|
// Load programs
|
||||||
|
let programs = examples.slice();
|
||||||
|
try {
|
||||||
|
const prgs = window.localStorage.getItem("thurtle:programs");
|
||||||
|
if (prgs != null) {
|
||||||
|
programs.push(...JSON.parse(prgs));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listPrograms(): Program[] {
|
||||||
|
return programs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProgram(name: string): Program | undefined {
|
||||||
|
return listPrograms().find((e) => e.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePrograms() {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
"thurtle:programs",
|
||||||
|
JSON.stringify(programs.filter((p) => !p.isExample))
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
window.alert("Unable to save");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveProgram(name: string, program: string): boolean {
|
||||||
|
const prg = getProgram(name);
|
||||||
|
let isNew = false;
|
||||||
|
if (prg != null) {
|
||||||
|
prg.program = program;
|
||||||
|
} else {
|
||||||
|
isNew = true;
|
||||||
|
programs.push({
|
||||||
|
name,
|
||||||
|
isExample: false,
|
||||||
|
program,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
savePrograms();
|
||||||
|
return isNew;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteProgram(name: string) {
|
||||||
|
programs = programs.filter((p) => p.name != name);
|
||||||
|
savePrograms();
|
||||||
|
}
|
|
@ -4,7 +4,12 @@ import "./thurtle.css";
|
||||||
import turtle from "./turtle.svg";
|
import turtle from "./turtle.svg";
|
||||||
import logo from "../../../doc/logo.svg";
|
import logo from "../../../doc/logo.svg";
|
||||||
import thurtleFS from "./thurtle.fs";
|
import thurtleFS from "./thurtle.fs";
|
||||||
import examples from "./examples";
|
import {
|
||||||
|
deleteProgram,
|
||||||
|
getProgram,
|
||||||
|
listPrograms,
|
||||||
|
saveProgram,
|
||||||
|
} from "./programs";
|
||||||
import Editor from "./Editor";
|
import Editor from "./Editor";
|
||||||
|
|
||||||
function About() {
|
function About() {
|
||||||
|
@ -77,7 +82,66 @@ const rootEl = (
|
||||||
<div class="main d-flex flex-column p-2">
|
<div class="main d-flex flex-column p-2">
|
||||||
<div class="d-flex flex-row flex-grow-1">
|
<div class="d-flex flex-row flex-grow-1">
|
||||||
<div class="left-pane d-flex flex-column">
|
<div class="left-pane d-flex flex-column">
|
||||||
<select class="form-select mb-2" data-hook="examples"></select>
|
<div class="d-flex flex-row">
|
||||||
|
<select class="form-select mb-2" data-hook="examples"></select>
|
||||||
|
<div>
|
||||||
|
<div class="btn-group ms-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-light"
|
||||||
|
data-hook="save-btn"
|
||||||
|
onclick={save}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="bi bi-hdd"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
d="M4.5 11a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1zM3 10.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
d="M16 11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V9.51c0-.418.105-.83.305-1.197l2.472-4.531A1.5 1.5 0 0 1 4.094 3h7.812a1.5 1.5 0 0 1 1.317.782l2.472 4.53c.2.368.305.78.305 1.198V11zM3.655 4.26 1.592 8.043C1.724 8.014 1.86 8 2 8h12c.14 0 .276.014.408.042L12.345 4.26a.5.5 0 0 0-.439-.26H4.094a.5.5 0 0 0-.44.26zM1 10v1a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-1a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-light dropdown-toggle dropdown-toggle-split"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="visually-hidden">Toggle Dropdown</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
href="#"
|
||||||
|
onclick={(ev) => save(ev, true)}
|
||||||
|
>
|
||||||
|
Save as
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
href="#"
|
||||||
|
data-hook="delete-action"
|
||||||
|
onclick={del}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{editor.el}
|
{editor.el}
|
||||||
<button data-hook="run" class="btn btn-primary mt-2">
|
<button data-hook="run" class="btn btn-primary mt-2">
|
||||||
Run
|
Run
|
||||||
|
@ -205,12 +269,15 @@ 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;
|
||||||
const examplesEl = rootEl.querySelector(
|
const programsEl = rootEl.querySelector(
|
||||||
"[data-hook=examples]"
|
"[data-hook=examples]"
|
||||||
)! as HTMLSelectElement;
|
)! as HTMLSelectElement;
|
||||||
const outputEl = rootEl.querySelector(
|
const outputEl = rootEl.querySelector(
|
||||||
"pre[data-hook=output]"
|
"pre[data-hook=output]"
|
||||||
) as HTMLPreElement;
|
) as HTMLPreElement;
|
||||||
|
const deleteActionEl = rootEl.querySelector(
|
||||||
|
"[data-hook=delete-action]"
|
||||||
|
) as HTMLAnchorElement;
|
||||||
|
|
||||||
enum PenState {
|
enum PenState {
|
||||||
Up = 0,
|
Up = 0,
|
||||||
|
@ -301,17 +368,74 @@ function setVisible(b: boolean) {
|
||||||
updateTurtle();
|
updateTurtle();
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadExample(name: string) {
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
editor.setValue(examples.find((e) => e.name === name)!.program);
|
// Programs
|
||||||
examplesEl.value = name;
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
const DEFAULT_PROGRAM = "Flower";
|
||||||
|
|
||||||
|
function loadProgram(name: string) {
|
||||||
|
const program = getProgram(name)!;
|
||||||
|
editor.setValue(program.program);
|
||||||
|
if (program.isExample) {
|
||||||
|
deleteActionEl.classList.add("disabled");
|
||||||
|
} else {
|
||||||
|
deleteActionEl.classList.remove("disabled");
|
||||||
|
}
|
||||||
|
programsEl.value = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const ex of examples) {
|
function loadPrograms() {
|
||||||
examplesEl.appendChild(<option value={ex.name}>{ex.name}</option>);
|
programsEl.innerText = "";
|
||||||
|
for (const ex of listPrograms().filter((p) => !p.isExample)) {
|
||||||
|
programsEl.appendChild(<option value={ex.name}>{ex.name}</option>);
|
||||||
|
}
|
||||||
|
programsEl.appendChild(<option disabled={true}>Examples</option>);
|
||||||
|
for (const ex of listPrograms().filter((p) => p.isExample)) {
|
||||||
|
programsEl.appendChild(<option value={ex.name}>{ex.name}</option>);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
examplesEl.addEventListener("change", (ev) => {
|
|
||||||
loadExample((ev.target! as HTMLSelectElement).value);
|
function save(ev: MouseEvent, forceSaveAs?: boolean) {
|
||||||
|
ev.preventDefault();
|
||||||
|
let name = programsEl.value;
|
||||||
|
const program = getProgram(name);
|
||||||
|
if (program?.isExample || forceSaveAs) {
|
||||||
|
let title = program?.isExample ? name + " (Copy)" : name;
|
||||||
|
const newName = window.prompt("Program name", title);
|
||||||
|
if (newName == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (getProgram(newName)?.isExample) {
|
||||||
|
window.alert(`Cannot save as example '${name}'`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
name = newName;
|
||||||
|
}
|
||||||
|
if (saveProgram(name, editor.getValue())) {
|
||||||
|
loadPrograms();
|
||||||
|
loadProgram(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function del(ev: MouseEvent) {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (
|
||||||
|
!window.confirm(`Are you sure you want to delete '${programsEl.value}'?`)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deleteProgram(programsEl.value);
|
||||||
|
loadPrograms();
|
||||||
|
loadProgram(DEFAULT_PROGRAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
programsEl.addEventListener("change", (ev) => {
|
||||||
|
loadProgram((ev.target! as HTMLSelectElement).value);
|
||||||
});
|
});
|
||||||
|
loadPrograms();
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
|
@ -363,4 +487,4 @@ document.addEventListener("keydown", (ev) => {
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
|
|
||||||
loadExample(examples[4].name);
|
loadProgram(DEFAULT_PROGRAM);
|
||||||
|
|
Loading…
Reference in a new issue