Feature parity for the solid.js refactor of the editor

main
Shad Amethyst 2 years ago
parent 167a7747db
commit 17422fc1e6
Signed by: amethyst
GPG Key ID: D970C8DD1D6DEE36

@ -9,29 +9,34 @@ import LeftPane from "./LeftPane.jsx";
import MiddlePane from "./MiddlePane.jsx";
import RightPane from "./RightPane.jsx";
import {World} from "../stackline-wasm/stackline_wasm.js";
import {World, Signal} from "../stackline-wasm/stackline_wasm.js";
let json = await (await fetch("/stackline-wasm/prime.json")).json();
export default function Editor() {
let [world, setWorld] = createSignal(World.deserialize(json), {equals: false});
world.init = () => setWorld((world) => {
world.init();
return world;
});
world.step = () => setWorld((world) => {
world.step();
return world;
});
let [settings, setSettings] = createStore({
selected: null,
time: performance.now(),
cx: 0,
cy: 0,
zoom: 0,
grid: true,
get zoom_factor() {
return Math.ceil(Math.pow(2, this.zoom));
},
get tile_size() {
return 10 * this.zoom_factor;
}
});
world().init();
setWorld((world) => {
let tile = world.get(4, 0);
tile.signal = {direction: "Up", stack: []};
tile.state = "Active";
world.set(4, 0, tile);
world.init();
console.log(world.toString());
@ -39,11 +44,69 @@ export default function Editor() {
return world;
});
setInterval(() => {
setSettings("time", performance.now());
}, 100);
let [running, setRunning] = createSignal(null);
function step() {
world.init();
world.step();
}
function pause() {
if (running()) {
clearInterval(running());
setRunning(null);
}
}
function play() {
world.init();
if (running()) {
clearInterval(running());
}
setRunning(setInterval(() => {
world.step();
}, 100));
}
function keydown(event) {
if (event.code === "Space") {
if (running()) {
pause();
} else if (event.shiftKey) {
play();
} else {
step();
}
} else if (event.code === "KeyG") {
// TODO: immediately trigger a redraw?
setSettings("grid", !settings.grid);
}
}
function mount() {
window.addEventListener("keydown", keydown);
}
function cleanup() {
window.removeEventListener("keydown", keydown);
if (running()) {
clearInterval(running());
setRunning(null);
}
}
return (
<div class={styles.Editor}>
<div class={styles.Editor} onMount={mount()} onCleanup={cleanup()}>
<LeftPane />
<MiddlePane settings={settings} world={world} setSettings={setSettings} />
<RightPane />
<RightPane settings={settings} world={world} setWorld={setWorld} setSettings={setSettings} />
</div>
);
}

@ -19,6 +19,16 @@ export default function MiddlePane(props) {
});
let [view, setView] = createStore({
cx: 0,
cy: 0,
zoom: 2,
get zoom_factor() {
return Math.ceil(Math.pow(2, this.zoom));
},
get tile_size() {
return 10 * this.zoom_factor;
},
from_x: 0,
to_x: 0,
from_y: 0,
@ -40,10 +50,10 @@ export default function MiddlePane(props) {
});
createEffect(() => {
let from_y = Math.floor((-settings.cy - canvas.height / 2) / settings.tile_size);
let to_y = Math.ceil((-settings.cy + canvas.height / 2) / settings.tile_size);
let from_x = Math.floor((-settings.cx - canvas.width / 2) / settings.tile_size);
let to_x = Math.ceil((-settings.cx + canvas.width / 2) / settings.tile_size);
let from_y = Math.floor((-view.cy - canvas.height / 2) / view.tile_size);
let to_y = Math.ceil((-view.cy + canvas.height / 2) / view.tile_size);
let from_x = Math.floor((-view.cx - canvas.width / 2) / view.tile_size);
let to_x = Math.ceil((-view.cx + canvas.width / 2) / view.tile_size);
// effect automatically batches
setView("from_x", from_x);
@ -83,9 +93,8 @@ export default function MiddlePane(props) {
if (!canvas.ctx) return;
let surface = getSurface();
let {tile_size, zoom_factor, cx, cy} = settings;
let {width, height, ctx} = canvas;
let {from_x, to_x, from_y, to_y} = view;
let {tile_size, zoom_factor, cx, cy, from_x, to_x, from_y, to_y} = view;
let view_width = view.width;
let view_height = view.height;
let panes = getPanes();
@ -179,7 +188,7 @@ export default function MiddlePane(props) {
}
if (mouse.hovering) {
let [x, y] = get_hovered();
let [x, y] = getHovered();
ctx.strokeStyle = "rgba(230, 255, 230, 0.2)";
ctx.fillStyle = "rgba(230, 255, 230, 0.07)";
@ -187,9 +196,42 @@ export default function MiddlePane(props) {
fill_rect(x, y, 1, 1);
}
if (mounted) {
window.requestAnimationFrame(() => {
if (settings.selected) {
let [x, y] = settings.selected;
ctx.strokeStyle = "rgba(230, 230, 255, 0.1)";
stroke_rect(x, y, 1, 1);
}
}
let running = false;
function loop() {
let should_redraw = true;
function cb() {
if (should_redraw) {
draw();
should_redraw = false;
}
if (mounted) {
window.requestAnimationFrame(cb);
}
}
if (!running) {
running = true;
untrack(cb);
createEffect(() => {
let _ = [
view.cx, view.cy,
canvas.width, canvas.height,
mouse.x, mouse.y,
getPanes(), getHovered(),
settings.selected, settings.time,
];
should_redraw = true;
});
}
}
@ -207,15 +249,15 @@ export default function MiddlePane(props) {
createEffect(() => {
resize_listener();
untrack(draw);
loop();
});
function get_hovered() {
let x = Math.floor((mouse.x - canvas.width / 2 - settings.cx) / settings.tile_size);
let y = Math.floor((mouse.y - canvas.height / 2 - settings.cy) / settings.tile_size);
let getHovered = createMemo(() => {
let x = Math.floor((mouse.x - canvas.width / 2 - view.cx) / view.tile_size);
let y = Math.floor((mouse.y - canvas.height / 2 - view.cy) / view.tile_size);
return [x, y];
}
});
let _canvas;
return (<div
@ -242,7 +284,7 @@ export default function MiddlePane(props) {
x: mouse_x,
y: mouse_y,
click_x: mouse_x,
click_y: mouse_x,
click_y: mouse_y,
hovering: true,
down: true,
}
@ -253,8 +295,8 @@ export default function MiddlePane(props) {
if (mouse.down) {
// TODO: numerically stable solution
setSettings("cx", settings.cx + (evt.clientX - canvas.element.offsetLeft) - mouse.x);
setSettings("cy", settings.cy + (evt.clientY - canvas.element.offsetTop) - mouse.y);
setView("cx", view.cx + (evt.clientX - canvas.element.offsetLeft) - mouse.x);
setView("cy", view.cy + (evt.clientY - canvas.element.offsetTop) - mouse.y);
}
setMouse("x", evt.clientX - canvas.element.offsetLeft);
@ -262,11 +304,12 @@ export default function MiddlePane(props) {
}}
onMouseUp={(evt) => {
setMouse("down", false);
setMouse("x", evt.clientX - canvas.element.offsetLeft);
setMouse("y", evt.clientY - canvas.element.offsetTop);
let dist = Math.sqrt((mouse.x - mouse.click_x) ** 2 + (mouse.y - mouse.click_y) ** 2);
if (dist < 10) {
// TODO
// select(...get_hovered());
setSettings("selected", getHovered());
}
}}
onMouseEnter={() => {
@ -277,14 +320,14 @@ export default function MiddlePane(props) {
setMouse("hovering", false);
}}
onWheel={(evt) => {
let old_zoom = settings.zoom;
let zoom = settings.zoom + event.deltaY * ZOOM_STRENGTH;
let old_zoom = view.zoom;
let zoom = view.zoom + event.deltaY * ZOOM_STRENGTH;
zoom = Math.min(Math.max(zoom, 0.0), 4.0);
setSettings("zoom", zoom);
setView("zoom", zoom);
let delta = Math.ceil(Math.pow(2, zoom)) / Math.ceil(Math.pow(2, old_zoom));
setSettings("cx", settings.cx * delta);
setSettings("cy", settings.cy * delta);
setView("cx", view.cx * delta);
setView("cy", view.cy * delta);
}}
>
Sowwy, your browser does not support canvases.

@ -1,5 +1,6 @@
.MiddlePane {
flex-grow: 1;
background: #202027;
}
.canvas {

@ -1,7 +1,86 @@
import {createEffect, createSignal, createMemo, untrack} from "solid-js";
import Tile from "./Tile.jsx";
import Signal from "./Signal.jsx";
import styles from "./RightPane.module.css";
export default function RightPane(props) {
return (<div id="right-pane">
let {settings, setSettings, world, setWorld} = props;
let selected = createMemo((old) => {
if (old?.ptr) {
old.free();
}
if (settings.selected) {
return world().get(...settings.selected) ?? null;
} else {
return null;
}
}, {equals: false});
let signal = createMemo((old) => {
if (old?.ptr) {
old.free();
}
if (selected()) {
return selected().signal ?? null;
} else {
return null;
}
}, {equals: false});
function setSignal(new_signal) {
setWorld((world) => {
let [x, y] = settings.selected;
let full_tile = world.get(x, y);
if (typeof new_signal === "function") {
full_tile.signal = new_signal(full_tile.signal);
} else {
full_tile.signal = new_signal;
}
world.set(x, y, full_tile);
return world;
});
}
function set_full_tile(tile) {
if (settings.selected) {
setWorld((world) => {
let [x, y ] = settings.selected;
if (typeof tile === "function") {
// world.get(...) is passed by ownership, and an owned value is expected back
world.set(x, y, tile(world.get(x, y)));
} else {
world.set(x, y, tile);
}
return world;
});
}
};
function cleanup() {
let sel = selected();
if (sel.ptr) {
sel.free();
}
let sig = signal();
if (sig.ptr) {
sig.free();
}
}
return (<div class={styles.RightPane} onCleanup={cleanup}>
<Show when={selected() !== null} fallback={<i class={styles.gray}>Nothing selected</i>}>
<h2 class={styles.h2}>Coordinates:</h2>
({settings.selected[0]}, {settings.selected[1]})
<h2 class={styles.h2}>State:</h2>
{selected().state}
<h2 class={styles.h2}>Signal:</h2>
<Signal signal={signal} setSignal={setSignal} />
<Tile full_tile={selected} set_full_tile={set_full_tile} />
</Show>
</div>);
}

@ -0,0 +1,66 @@
.RightPane {
width: 24em;
max-width: 25vw;
background: #18181b;
color: #d0d0d0;
padding: 1.5em 1em;
font-family: monospace;
font-size: 15px;
}
.gray, .empty {
color: #a0a0a0;
}
.h2 {
font-size: inherit;
margin: 0;
color: white;
}
.h3 {
font-size: inherit;
margin: 0;
color: white;
font-weight: normal;
}
.indent {
margin-left: 0.5em;
padding-left: calc(0.5em - 1px);
border-left: 1px solid #808080;
}
.stack {
list-style: none;
padding-left: 0em;
margin: 0 0;
}
.stack > li {
margin-top: .2em;
margin-bottom: .2em;
}
.stack > li::before {
content: "\25b6";
margin-right: 0.5em;
color: #808080;
}
.empty {
display: block;
margin-top: 0.25em;
margin-bottom: 0.25em;
}
.properties {
list-style-type: none;
padding-left: 0;
margin-top: 0;
}
.properties > li {
margin-top: 0.25em;
margin-bottom: 0.25em;
}

@ -0,0 +1,119 @@
import {createEffect} from "solid-js";
import Direction from "./input/Direction.jsx";
import Value from "./input/Value.jsx";
import styles from "./RightPane.module.css";
import input_styles from "./input/input.module.css";
export const DIRECTIONS = [
"Up",
"Right",
"Down",
"Left"
];
export default function Signal(props) {
let {signal, setSignal} = props;
return (<Show when={signal()} fallback={<i class={styles.gray}>No signal</i>}>
<div class={styles.indent}>
<h3 class={styles.h3}>Direction:</h3>
<Show when={setSignal} fallback={signal()?.direction}>
<Direction value={() => signal()?.direction} setValue={(dir) => {
setSignal((signal) => {
signal.direction = dir;
return signal;
});
}} />
</Show>
<h3 class={styles.h3}>Stack:</h3>
<ol class={styles.stack}>
<For each={signal()?.stack} fallback={<i class={styles.empty}>(Empty)</i>}>
{(item, index) => {
let setValue = setSignal ? (new_value) => {
if (typeof new_value === "function") {
new_value = new_value(item);
}
setSignal((signal) => {
signal.stack[index()] = new_value;
return signal;
});
} : null;
return <li><Value value={() => item} setValue={setValue} /></li>;
// if (setSignal) {
// if ("Number" in item) {
// // Return number input for the `index()`-th element
// return <li>
// <input
// class={input_styles.input}
// type="number"
// value={item["Number"]}
// onChange={(evt) => {
// console.log("input");
// setSignal((signal) => {
// signal.stack[index()] = {"Number": +evt.currentTarget.value};
// return signal;
// });
// }}
// />
// </li>;
// } else if ("String" in item) {
// // Return string input for the `index()`-th element
// return <li>"
// <input
// class={input_styles.input}
// type="string"
// value={item["String"]}
// onChange={(evt) => {
// setSignal((signal) => {
// signal.stack[index()] = {"String": evt.currentTarget.value};
// return signal;
// });
// }}
// />
// "</li>;
// }
// } else {
// if (item?.["Number"]) {
// return <li>{item["Number"]}</li>;
// } else if (item?.["String"]) {
// return <li>"{item["String"]}"</li>;
// }
// }
}}
</For>
<Show when={setSignal}>
<div>
<button
class={input_styles.button}
aria-label="Remove a value from the stack"
title="Pop value"
disabled={!signal() || signal().stack.length == 0}
onClick={(evt) => {
setSignal((signal) => {
signal.stack.pop();
return signal;
});
}}
>-</button>
<button
class={input_styles.button}
aria-label="Add a value to the stack"
title="Push value"
onClick={(evt) => {
setSignal((signal) => {
signal.stack.push({"Number": 0.0});
return signal;
});
}}
>+</button>
</div>
</Show>
</ol>
</div>
</Show>);
}

@ -0,0 +1,112 @@
import {createEffect, createSignal, createMemo, untrack} from "solid-js";
import styles from "./RightPane.module.css";
import Signal from "./Signal.jsx";
import Direction from "./input/Direction.jsx";
import Orientation from "./input/Orientation.jsx";
import Number from "./input/Number.jsx";
import Value from "./input/Value.jsx";
export const COMPONENTS = new Map();
COMPONENTS.set("Signal", (props) => <Signal signal={props.value} setSignal={props.setValue} />);
COMPONENTS.set("Direction", Direction);
COMPONENTS.set("Orientation", Orientation);
COMPONENTS.set("Uint", (props) => <Number value={props.value} setValue={props.setValue} min={0} />);
COMPONENTS.set("Int", Number);
COMPONENTS.set("Value", Value);
export default function Tile(props) {
let {full_tile, set_full_tile} = props;
let tile = createMemo(() => {
return full_tile()?.tile;
});
let schema = createMemo(() => {
return full_tile()?.schema();
});
let tile_name = createMemo(() => {
if (tile()) {
return Object.keys(tile())[0];
} else {
return null;
}
});
// This abstraction is needed to propagate the modifications of the Tile up to the FullTile
function bindSetValue(label, value) {
return function setValue(new_value) {
set_full_tile((full_tile) => {
let tile = full_tile.tile;
let name = Object.keys(tile)[0];
if (typeof new_value === "function") {
value = new_value(value);
} else {
value = new_value;
}
tile[name] = schema_recurse(schema(), tile[name], label, value);
full_tile.tile = tile;
return full_tile;
});
}
}
return (<>
<h2 class={styles.h2}>Tile:</h2>
<Show when={tile()} fallback={<i class={styles.gray}>No tile</i>}>
<div class={styles.indent}>
<ol class={styles.properties}>
<For each={[...schema_iter(schema(), tile()[tile_name()])]} fallback={<li><i class={styles.gray}>No properties</i></li>}>
{([label, type, value]) => {
if (COMPONENTS.has(type)) {
let setValue = bindSetValue(label, value);
return <li><b>{label}: </b>{COMPONENTS.get(type)({value: () => value, setValue})}</li>
} else {
return <li><b>{label}: </b>{value?.toString()}</li>
}
}}
</For>
</ol>
</div>
</Show>
</>);
}
export function* schema_iter(schema, tile) {
if (Array.isArray(schema)) {
for (let n = 0; n < schema.length; n++) {
yield* schema_iter(schema[n], tile[n]);
}
} else if (typeof schema === "object") {
for (let name in schema) {
yield* schema_iter(schema[name], tile[name]);
}
} else if (typeof schema === "string") {
let res = schema.split(":");
res.push(tile);
yield res;
}
}
export function schema_recurse(schema, tile, label, value) {
if (Array.isArray(schema)) {
for (let n = 0; n < schema.length; n++) {
tile[n] = schema_recurse(schema[n], tile[n], label, value);
}
return tile;
} else if (typeof schema === "object") {
for (let name in schema) {
tile[name] = schema_recurse(schema[name], tile[name], label, value);
}
return tile;
} else if (typeof schema === "string") {
if (schema.split(":")[0] === label) {
return value;
} else {
return tile;
}
}
}

@ -0,0 +1,19 @@
import {createEffect} from "solid-js";
import styles from "./input.module.css";
export default function Direction(props) {
let {value, setValue} = props;
let select;
createEffect(() => {
select.value = value();
});
return (<select class={styles.select} title="Direction" ref={select} onChange={() => setValue(select.value)}>
<option value="Up" default>Up</option>
<option value="Right">Right</option>
<option value="Down">Down</option>
<option value="Left">Left</option>
</select>);
}

@ -0,0 +1,17 @@
import {createEffect} from "solid-js";
import styles from "./input.module.css";
export default function Number(props) {
let {value, setValue} = props;
let select;
return (<input
type="number"
class={styles.input}
value={value()}
min={props.min}
max={props.max}
onChange={(evt) => setValue(+evt.currentTarget.value)}
/>);
}

@ -0,0 +1,18 @@
import {createEffect} from "solid-js";
import styles from "./input.module.css";
export default function Orientation(props) {
let {value, setValue} = props;
let select;
createEffect(() => {
select.value = value();
});
return (<select class={styles.select} title="Orientation" ref={select} onChange={() => setValue(select.value)}>
<Show when={!props.no_any}><option value="Any" default>Any</option></Show>
<option value="Vertical">Vertical</option>
<option value="Horizontal">Horizontal</option>
</select>);
}

@ -0,0 +1,14 @@
import {createEffect} from "solid-js";
import styles from "./input.module.css";
export default function String(props) {
let {value, setValue} = props;
return (<>"<input
type="string"
class={styles.input}
value={value()}
onChange={(evt) => setValue(evt.currentTarget.value)}
/>"</>);
}

@ -0,0 +1,64 @@
import {createEffect} from "solid-js";
import styles from "./input.module.css";
export default function Value(props) {
let {value, setValue} = props;
let select;
createEffect(() => {
if (select) {
select.value = Object.keys(value())[0];
}
});
return (<>
<Show when={setValue}>
<select title="Type" ref={select} class={styles.select_type} onChange={(evt) => {
if (!(select.value in value())) {
if (select.value === "Number") {
setValue({"Number": 0.0});
} else if (select.value === "String") {
setValue({"String": ""});
}
}
}}>
<option value="Number" title="Number" default>N</option>
<option value="String" title="String">S</option>
</select>
<Switch>
<Match when={"Number" in value()}>
<input
class={styles.input}
type="number"
value={value()["Number"]}
onChange={(evt) => {
setValue({"Number": +evt.currentTarget.value});
}}
/>
</Match>
<Match when={"String" in value()}>
"<input
class={styles.input}
type="string"
value={value()["String"]}
onChange={(evt) => {
setValue({"String": evt.currentTarget.value});
}}
/>
"
</Match>
</Switch>
</Show>
<Show when={!setValue}>
<Switch>
<Match when={"Number" in value()}>
{value()["Number"]}
</Match>
<Match when={"String" in value()}>
"{value()["String"]}"
</Match>
</Switch>
</Show>
</>);
}

@ -0,0 +1,65 @@
.select, .input, .select_type {
background: rgba(0, 0, 0, 0.1);
color: inherit;
font-family: inherit;
font-size: inherit;
border: 1px solid #808080;
border-radius: 2px;
}
.select {
min-width: 6em;
max-width: 12em;
}
.select_type {
width: 2.5em;
margin-right: 0.25em;
}
.input[type="number"] {
width: 6em;
}
.input[type="string"] {
min-width: 6em;
max-width: 12em;
}
.select:hover, .input:hover, .select_type:hover {
color: white;
background: rgba(0, 0, 0, 0.4);
border: 1px solid #a0a0a0;
}
.input:active {
color: white;
background: rgba(0, 0, 0, 0.4);
border: 1px solid white;
}
.button {
min-width: 1.5em;
background: rgb(64, 68, 96, 0.5);
color: white;
border: 1px solid #d0d0d0;
border-radius: 2px;
margin-left: 2px;
margin-right: 2px;
}
.button[disabled] {
background: transparent;
border: 1px solid #a0a0a0;
color: #d0d0d0;
}
.button:not([disabled]):hover {
background: rgb(64, 68, 96, 0.7);
cursor: pointer;
border: 1px solid white;
}
.button:not([disabled]):active {
background: rgb(64, 68, 96, 1.0);
}

@ -202,19 +202,34 @@ impl FullTile {
AnyTile::new(name).map(|tile| FullTile(SLFullTile::new(Some(tile))))
}
pub fn clone(&self) -> FullTile {
<Self as Clone>::clone(self)
}
#[allow(non_snake_case)]
pub fn toString(&self) -> String {
format!("{:#?}", self.0)
}
#[wasm_bindgen(getter)]
pub fn signal(&self) -> Option<Signal> {
self.0.signal().map(|signal| Signal(signal.clone()))
pub fn signal(&self) -> JsValue {
self.0.signal().and_then(|signal| JsValue::from_serde(signal).ok()).unwrap_or(JsValue::NULL)
}
#[wasm_bindgen(setter)]
pub fn set_signal(&mut self, signal: Option<Signal>) {
self.0.set_signal(signal.map(|s| s.0));
pub fn set_signal(&mut self, signal: JsValue) {
if signal.is_null() {
self.0.set_signal(None);
} else {
match signal.into_serde() {
Ok(signal) => {
self.0.set_signal(Some(signal));
}
Err(err) => {
err!("Couldn't serialize Signal: {:?}", err);
}
}
}
}
#[wasm_bindgen(getter)]
@ -343,33 +358,26 @@ impl Signal {
.0
.stack()
.iter()
.map(<UntaggedValue as From<&'_ Value>>::from)
.collect::<Vec<_>>(),
)
.unwrap()
}
#[wasm_bindgen(getter)]
pub fn direction(&self) -> u8 {
match self.0.direction() {
Direction::Up => 0,
Direction::Right => 1,
Direction::Down => 2,
Direction::Left => 3,
}
pub fn direction(&self) -> JsValue {
JsValue::from_serde(&self.0.direction()).expect("Couldn't serialize Direction")
}
#[wasm_bindgen(setter)]
pub fn set_direction(&mut self, direction: u8) {
let direction = match direction {
0 => Direction::Up,
1 => Direction::Right,
2 => Direction::Down,
3 => Direction::Left,
_ => return,
};
self.0.set_direction(direction);
pub fn set_direction(&mut self, direction: JsValue) {
match direction.into_serde() {
Ok(dir) => {
self.0.set_direction(dir);
}
Err(err) => {
err!("Couldn't serialize Direction: {:?}", err);
}
}
}
pub fn push(&mut self, value: JsValue) {

Loading…
Cancel
Save