diff --git a/editor-solidjs/src/Editor.jsx b/editor-solidjs/src/Editor.jsx index bde9cbf..4c3213f 100644 --- a/editor-solidjs/src/Editor.jsx +++ b/editor-solidjs/src/Editor.jsx @@ -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 ( -
+
- +
); } diff --git a/editor-solidjs/src/MiddlePane.jsx b/editor-solidjs/src/MiddlePane.jsx index bd49037..5e40d0e 100644 --- a/editor-solidjs/src/MiddlePane.jsx +++ b/editor-solidjs/src/MiddlePane.jsx @@ -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 (
{ 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. diff --git a/editor-solidjs/src/MiddlePane.module.css b/editor-solidjs/src/MiddlePane.module.css index 1082d5c..ccc7081 100644 --- a/editor-solidjs/src/MiddlePane.module.css +++ b/editor-solidjs/src/MiddlePane.module.css @@ -1,5 +1,6 @@ .MiddlePane { flex-grow: 1; + background: #202027; } .canvas { diff --git a/editor-solidjs/src/RightPane.jsx b/editor-solidjs/src/RightPane.jsx index e28e6ae..47146ef 100644 --- a/editor-solidjs/src/RightPane.jsx +++ b/editor-solidjs/src/RightPane.jsx @@ -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 (
+ 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 (
+ Nothing selected}> +

Coordinates:

+ ({settings.selected[0]}, {settings.selected[1]}) +

State:

+ {selected().state} +

Signal:

+ + +
); } diff --git a/editor-solidjs/src/RightPane.module.css b/editor-solidjs/src/RightPane.module.css new file mode 100644 index 0000000..4ffddb1 --- /dev/null +++ b/editor-solidjs/src/RightPane.module.css @@ -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; +} diff --git a/editor-solidjs/src/Signal.jsx b/editor-solidjs/src/Signal.jsx new file mode 100644 index 0000000..aa8d5df --- /dev/null +++ b/editor-solidjs/src/Signal.jsx @@ -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 (No signal}> +
+

Direction:

+ + signal()?.direction} setValue={(dir) => { + setSignal((signal) => { + signal.direction = dir; + return signal; + }); + }} /> + +

Stack:

+
    + (Empty)}> + {(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
  1. item} setValue={setValue} />
  2. ; + + // if (setSignal) { + // if ("Number" in item) { + // // Return number input for the `index()`-th element + // return
  3. + // { + // console.log("input"); + // setSignal((signal) => { + // signal.stack[index()] = {"Number": +evt.currentTarget.value}; + // return signal; + // }); + // }} + // /> + //
  4. ; + // } else if ("String" in item) { + // // Return string input for the `index()`-th element + // return
  5. " + // { + // setSignal((signal) => { + // signal.stack[index()] = {"String": evt.currentTarget.value}; + // return signal; + // }); + // }} + // /> + // "
  6. ; + // } + // } else { + // if (item?.["Number"]) { + // return
  7. {item["Number"]}
  8. ; + // } else if (item?.["String"]) { + // return
  9. "{item["String"]}"
  10. ; + // } + // } + }} +
    + +
    + + +
    +
    +
+
+
); +} diff --git a/editor-solidjs/src/Tile.jsx b/editor-solidjs/src/Tile.jsx new file mode 100644 index 0000000..58e5f44 --- /dev/null +++ b/editor-solidjs/src/Tile.jsx @@ -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) => ); +COMPONENTS.set("Direction", Direction); +COMPONENTS.set("Orientation", Orientation); +COMPONENTS.set("Uint", (props) => ); +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 (<> +

Tile:

+ No tile}> +
+
    + No properties}> + {([label, type, value]) => { + if (COMPONENTS.has(type)) { + let setValue = bindSetValue(label, value); + return
  1. {label}: {COMPONENTS.get(type)({value: () => value, setValue})}
  2. + } else { + return
  3. {label}: {value?.toString()}
  4. + } + }} +
    +
+
+
+ ); +} + +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; + } + } +} diff --git a/editor-solidjs/src/input/Direction.jsx b/editor-solidjs/src/input/Direction.jsx new file mode 100644 index 0000000..7feaa09 --- /dev/null +++ b/editor-solidjs/src/input/Direction.jsx @@ -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 (); +} diff --git a/editor-solidjs/src/input/Number.jsx b/editor-solidjs/src/input/Number.jsx new file mode 100644 index 0000000..372ea1a --- /dev/null +++ b/editor-solidjs/src/input/Number.jsx @@ -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 ( setValue(+evt.currentTarget.value)} + />); +} diff --git a/editor-solidjs/src/input/Orientation.jsx b/editor-solidjs/src/input/Orientation.jsx new file mode 100644 index 0000000..bebbf6b --- /dev/null +++ b/editor-solidjs/src/input/Orientation.jsx @@ -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 (); +} diff --git a/editor-solidjs/src/input/String.jsx b/editor-solidjs/src/input/String.jsx new file mode 100644 index 0000000..b6276e3 --- /dev/null +++ b/editor-solidjs/src/input/String.jsx @@ -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 (<>" setValue(evt.currentTarget.value)} + />"); +} diff --git a/editor-solidjs/src/input/Value.jsx b/editor-solidjs/src/input/Value.jsx new file mode 100644 index 0000000..6aba2df --- /dev/null +++ b/editor-solidjs/src/input/Value.jsx @@ -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 (<> + + + + + { + setValue({"Number": +evt.currentTarget.value}); + }} + /> + + + " { + setValue({"String": evt.currentTarget.value}); + }} + /> + " + + + + + + + {value()["Number"]} + + + "{value()["String"]}" + + + + ); +} diff --git a/editor-solidjs/src/input/input.module.css b/editor-solidjs/src/input/input.module.css new file mode 100644 index 0000000..6de3646 --- /dev/null +++ b/editor-solidjs/src/input/input.module.css @@ -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); +} diff --git a/stackline-wasm/src/lib.rs b/stackline-wasm/src/lib.rs index 94c31f6..17c31ab 100644 --- a/stackline-wasm/src/lib.rs +++ b/stackline-wasm/src/lib.rs @@ -202,19 +202,34 @@ impl FullTile { AnyTile::new(name).map(|tile| FullTile(SLFullTile::new(Some(tile)))) } + pub fn clone(&self) -> FullTile { + ::clone(self) + } + #[allow(non_snake_case)] pub fn toString(&self) -> String { format!("{:#?}", self.0) } #[wasm_bindgen(getter)] - pub fn signal(&self) -> Option { - 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) { - 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(>::from) .collect::>(), ) .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) {