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;
|
||||
program: string;
|
||||
isExample: boolean;
|
||||
};
|
||||
|
||||
export default [
|
||||
const examples: Program[] = [
|
||||
{
|
||||
name: "Square",
|
||||
isExample: true,
|
||||
program: `
|
||||
200 FORWARD
|
||||
90 RIGHT
|
||||
|
@ -19,6 +21,7 @@ export default [
|
|||
},
|
||||
{
|
||||
name: "Square (w/ LOOP)",
|
||||
isExample: true,
|
||||
program: `
|
||||
: SQUARE ( n -- )
|
||||
4 0 DO
|
||||
|
@ -32,6 +35,7 @@ export default [
|
|||
},
|
||||
{
|
||||
name: "Pentagram",
|
||||
isExample: true,
|
||||
program: `
|
||||
: PENTAGRAM ( n -- )
|
||||
18 RIGHT
|
||||
|
@ -46,6 +50,7 @@ export default [
|
|||
},
|
||||
{
|
||||
name: "Seeker",
|
||||
isExample: true,
|
||||
program: `
|
||||
: SEEKER ( n -- )
|
||||
4 0 DO
|
||||
|
@ -63,6 +68,7 @@ export default [
|
|||
},
|
||||
{
|
||||
name: "Flower",
|
||||
isExample: true,
|
||||
program: `
|
||||
: SQUARE ( n -- )
|
||||
4 0 DO
|
||||
|
@ -83,6 +89,7 @@ export default [
|
|||
},
|
||||
{
|
||||
name: "Spiral (Recursive)",
|
||||
isExample: true,
|
||||
program: `
|
||||
: SPIRAL ( n -- )
|
||||
DUP 1 < IF DROP EXIT THEN
|
||||
|
@ -97,6 +104,7 @@ PENUP -500 -180 SETXY PENDOWN
|
|||
},
|
||||
{
|
||||
name: "Outward Square Spiral",
|
||||
isExample: true,
|
||||
program: `
|
||||
: SPIRAL ( n1 n2 -- )
|
||||
OVER 800 > IF 2DROP EXIT THEN
|
||||
|
@ -111,6 +119,7 @@ PENUP -500 -180 SETXY PENDOWN
|
|||
},
|
||||
{
|
||||
name: "Crooked Outward Square Spiral",
|
||||
isExample: true,
|
||||
program: `
|
||||
91 CONSTANT ANGLE
|
||||
|
||||
|
@ -125,3 +134,56 @@ PENUP -500 -180 SETXY PENDOWN
|
|||
1 SPIRAL`,
|
||||
},
|
||||
].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 logo from "../../../doc/logo.svg";
|
||||
import thurtleFS from "./thurtle.fs";
|
||||
import examples from "./examples";
|
||||
import {
|
||||
deleteProgram,
|
||||
getProgram,
|
||||
listPrograms,
|
||||
saveProgram,
|
||||
} from "./programs";
|
||||
import Editor from "./Editor";
|
||||
|
||||
function About() {
|
||||
|
@ -77,7 +82,66 @@ const rootEl = (
|
|||
<div class="main d-flex flex-column p-2">
|
||||
<div class="d-flex flex-row flex-grow-1">
|
||||
<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}
|
||||
<button data-hook="run" class="btn btn-primary mt-2">
|
||||
Run
|
||||
|
@ -205,12 +269,15 @@ const patshEl = document.getElementById("paths")!;
|
|||
const runButtonEl = rootEl.querySelector(
|
||||
"button[data-hook=run]"
|
||||
)! as HTMLButtonElement;
|
||||
const examplesEl = rootEl.querySelector(
|
||||
const programsEl = rootEl.querySelector(
|
||||
"[data-hook=examples]"
|
||||
)! as HTMLSelectElement;
|
||||
const outputEl = rootEl.querySelector(
|
||||
"pre[data-hook=output]"
|
||||
) as HTMLPreElement;
|
||||
const deleteActionEl = rootEl.querySelector(
|
||||
"[data-hook=delete-action]"
|
||||
) as HTMLAnchorElement;
|
||||
|
||||
enum PenState {
|
||||
Up = 0,
|
||||
|
@ -301,17 +368,74 @@ function setVisible(b: boolean) {
|
|||
updateTurtle();
|
||||
}
|
||||
|
||||
function loadExample(name: string) {
|
||||
editor.setValue(examples.find((e) => e.name === name)!.program);
|
||||
examplesEl.value = name;
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Programs
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
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) {
|
||||
examplesEl.appendChild(<option value={ex.name}>{ex.name}</option>);
|
||||
function loadPrograms() {
|
||||
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() {
|
||||
try {
|
||||
|
@ -363,4 +487,4 @@ document.addEventListener("keydown", (ev) => {
|
|||
|
||||
reset();
|
||||
|
||||
loadExample(examples[4].name);
|
||||
loadProgram(DEFAULT_PROGRAM);
|
||||
|
|
Loading…
Reference in a new issue