thurtle: Add Load/Save

This commit is contained in:
Remko Tronçon 2022-05-18 21:10:26 +02:00
parent 84a06ac1ba
commit d6f2418248
2 changed files with 199 additions and 13 deletions

View file

@ -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();
}

View file

@ -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);