From 461abcac43916cc12c8021b6dd19aaf604cd3956 Mon Sep 17 00:00:00 2001 From: Adrien Burgun Date: Wed, 17 Aug 2022 23:22:59 +0200 Subject: [PATCH] :sparkles: :fire: Prototype property editor --- editor/canvas.js | 3 + editor/index.html | 56 ++++++++++- editor/inspect.js | 190 ++++++++++++++++++++++++++++++++++++ editor/style.css | 52 +++++++++- stackline-wasm/src/lib.rs | 2 +- stackline/src/tile/mod.rs | 16 ++- stackline/tiles/stack.rs | 20 ++++ stackline/tiles/storage.rs | 15 +++ stackline/tiles/transmit.rs | 11 +++ stackline/tiles/wire.rs | 8 +- 10 files changed, 360 insertions(+), 13 deletions(-) diff --git a/editor/canvas.js b/editor/canvas.js index c089fec..b68d393 100644 --- a/editor/canvas.js +++ b/editor/canvas.js @@ -22,7 +22,10 @@ let mouse_x = 0, mouse_y = 0; let mouse_down = false; let hovered = false; +let start = performance.now(); + export function draw() { + world.set_blink_duration((performance.now() - start) / 1000.0); let zoom_factor = Math.ceil(Math.pow(2, zoom)); let tile_size = 10 * zoom_factor; ctx.fillStyle = "#202027"; diff --git a/editor/index.html b/editor/index.html index 26d8824..a99ad65 100644 --- a/editor/index.html +++ b/editor/index.html @@ -25,11 +25,63 @@

Signal:

Stack:

-
    +

      Tile:

      - Type: + Type: + + + + + + + +
      diff --git a/editor/inspect.js b/editor/inspect.js index bb61410..4b8a1db 100644 --- a/editor/inspect.js +++ b/editor/inspect.js @@ -13,6 +13,9 @@ const signal_elem = document.getElementById("signal"); const state_elem = document.getElementById("state"); const tile_elem = document.getElementById("tile"); const tile_name_elem = document.getElementById("tile-name"); +const properties_elem = document.getElementById("tile-properties"); + +let templates = new Map(); export function update_selected(x, y) { coordinates.innerText = `(${x}, ${y})`; @@ -54,6 +57,38 @@ export function update_selected(x, y) { let name = Object.keys(tile)[0]; tile_name_elem.innerText = name; + + // Set properties + // TODO: only change the properties when necessary + + while (properties_elem.children.length > 0) { + properties_elem.removeChild(properties_elem.firstChild); + } + + let schema = full_tile.schema(); + + for (let [label, type, value] of schema_iter(schema, tile[name])) { + let input = create_input(type, value, on_change); + + if (!input) continue; + + let elem = document.createElement("li"); + let label_elem = document.createElement("b"); + label_elem.innerText = `${label}: `; + elem.appendChild(label_elem); + + elem.appendChild(input); + properties_elem.appendChild(elem); + + function on_change(new_value) { + // Traverse `tile` and change the corresponding schema element + tile[name] = schema_recurse(schema, tile[name], label, new_value); + + let full_tile = world.get(x, y); + full_tile.tile = tile; + world.set(x, y, full_tile); + } + } } else { tile_elem.classList.remove("has-tile"); } @@ -72,4 +107,159 @@ export function init() { // option.value = name; // tile_name_elem.appendChild(option); // } + + document.querySelectorAll(".property-template").forEach((template) => { + templates.set(template.attributes?.name?.value ?? template.id, template); + }); +} + +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; + } +} + +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; + } + } +} + +function create_input(type, value, change_cb) { + let options = type.split("|"); + + for (let option of options) { + if (option === "Value") { + let res = templates.get("Value").content.cloneNode(true).querySelector("*"); + + let select_elem = res.querySelector("select"); + let type = Object.keys(value)[0]; + set_type(type); + + function set_type(new_type) { + let map = new Map([["String", "string"], ["Number", "number"]]); + for (let [ty, cn] of map) { + if (ty === new_type) { + res.classList.add(cn); + } else { + res.classList.remove(cn); + } + } + type = new_type; + } + + select_elem.addEventListener("change", () => { + set_type(select_elem.value); + + if (type === "Number") { + value = {"Number": 0.0}; + number_input.value = 0; + } else if (type === "String") { + value = {"String": ""}; + string_input.value = ""; + } + + change_cb(value); + }); + + let number_input = res.querySelector(".value-number input"); + + if (type === "Number") { + number_input.value = value["Number"]; + } + + number_input.addEventListener("change", (evt) => { + if (type === "Number") { + value["Number"] = +number_input.value; + change_cb(value); + } + }); + + let string_input = res.querySelector(".value-string input"); + + if (type === "String") { + string_input.value = value["String"]; + } + + string_input.addEventListener("change", (evt) => { + if (type === "String") { + value["String"] = string_input.value; + change_cb(value); + } + }); + + return res; + } else if (option === "Uint" || option === "Int") { + let res = templates.get(option).content.cloneNode(true).querySelector("*"); + + if (value !== undefined) { + res.value = value; + } + + res.addEventListener("change", () => { + change_cb(+res.value); + }); + + return res; + } else if (option === "Signal") { + let res = templates.get(option).content.cloneNode(true).querySelector("*"); + + let stack_elem = res.querySelector(".stack"); + + for (let elem of value.stack) { + let li = document.createElement("li"); + + if (elem["Number"]) { + li.innerText = elem["Number"]; + } else if (elem["String"]) { + li.innerText = `"${elem["String"]}"`; + } + + stack_elem.appendChild(li); + } + + let direction_elem = res.querySelector(".direction"); + direction_elem.value = value.direction; + + return res; + } + + if (templates.has(option)) { + let res = templates.get(option).content.cloneNode(true).querySelector("*"); + + if (value !== undefined) { + res.value = value; + } + + res.addEventListener("change", () => { + change_cb(res.value); + }); + + return res; + } + } } diff --git a/editor/style.css b/editor/style.css index 6c8b023..8d4a455 100644 --- a/editor/style.css +++ b/editor/style.css @@ -84,23 +84,69 @@ h3 { color: #808080; } -#stack { +.stack { list-style: none; padding-left: 0em; margin: 0 0; } -#stack > li { +.stack > li { margin-top: 2px; margin-bottom: 2px; } -#stack > li::before { +.stack > li::before { content: "\25b6"; margin-right: 0.5em; color: #808080; } +#tile-properties { + list-style-type: none; + padding-left: 0; + margin-top: 0; +} + +#tile-properties > li { + margin-top: 0.25em; + margin-bottom: 0.25em; +} + +select, input { + background: rgba(0, 0, 0, 0.1); + color: inherit; + font-family: inherit; + font-size: inherit; + border: 1px solid #808080; + border-radius: 2px; +} + +select:hover, input:hover, input:active { + color: white; + background: rgba(0, 0, 0, 0.4); + border: 1px solid #a0a0a0; +} + +#tile-properties input[type="string"], #tile-properties input[type="number"] { + width: 6em; +} + +.value-input { + display: inline; +} + +.value-input .value-number, .value-input .value-string { + display: none; +} + +.value-input.number .value-number, .value-input.string .value-string { + display: inline; +} + +.value-input .value-number input, .value-input .value-string input { + display: inline; +} + .indent { margin-left: 0.5em; padding-left: calc(0.5em - 1px); diff --git a/stackline-wasm/src/lib.rs b/stackline-wasm/src/lib.rs index 6b15b85..94c31f6 100644 --- a/stackline-wasm/src/lib.rs +++ b/stackline-wasm/src/lib.rs @@ -270,7 +270,7 @@ impl FullTile { .map(|(key, value)| (key, construct_value(value))) .collect(), ), - TileSchema::Value(string) => Value::String(string), + TileSchema::Value(label, ty) => Value::String(format!("{}:{}", label, ty)), } } diff --git a/stackline/src/tile/mod.rs b/stackline/src/tile/mod.rs index a0fd479..c65d18d 100644 --- a/stackline/src/tile/mod.rs +++ b/stackline/src/tile/mod.rs @@ -197,7 +197,8 @@ pub trait Tile: std::clone::Clone + std::fmt::Debug + Serialize + for<'d> Deseri pub enum TileSchema { Tuple(Vec), Map(HashMap), - Value(String), + /// (label, type) + Value(String, String), } impl TileSchema { @@ -209,8 +210,8 @@ impl TileSchema { Self::Map(HashMap::new()) } - pub fn value(name: S) -> Self { - Self::Value(name.to_string()) + pub fn value(label: S1, ty: S2) -> Self { + Self::Value(label.to_string(), ty.to_string()) } pub fn add(mut self, name: S, value: TileSchema) -> Self { @@ -221,6 +222,15 @@ impl TileSchema { self } + + pub fn push(mut self, value: TileSchema) -> Self { + match self { + Self::Tuple(ref mut vec) => vec.push(value), + _ => panic!("Invalid TileSchema builder: cannot push() to a non-Tuple"), + }; + + self + } } pub mod prelude { diff --git a/stackline/tiles/stack.rs b/stackline/tiles/stack.rs index 1be8a35..34e806a 100644 --- a/stackline/tiles/stack.rs +++ b/stackline/tiles/stack.rs @@ -41,6 +41,11 @@ impl Tile for Push { fn draw_simple(&self, ctx: DrawContext) -> TextChar { TextChar::from_state('P', ctx.state) } + + fn schema(&self) -> TileSchema { + TileSchema::map() + .add("value", TileSchema::value("Value", "Value")) + } } /// Pops `n` values from the top of the stack, then lets the signal through @@ -87,6 +92,11 @@ impl Tile for Pop { TextChar::from_state(ctx.blink.scroll(&format!("\u{2c63}\u{2c63}\u{2c63}{}", self.amount)), ctx.state) } } + + fn schema(&self) -> TileSchema { + TileSchema::map() + .add("amount", TileSchema::value("Amount", "Uint")) + } } /// Reverses the order of the top `n` values from the stack, then lets the signal through @@ -138,6 +148,11 @@ impl Tile for Swap { TextChar::from_state(ctx.blink.scroll(&format!("sss{}", self.amount)), ctx.state) } } + + fn schema(&self) -> TileSchema { + TileSchema::map() + .add("amount", TileSchema::value("Amount", "Uint")) + } } /// Duplicates the nth element from the stack, pushing it back on top @@ -179,6 +194,11 @@ impl Tile for Duplicate { TextChar::from_state(ctx.blink.scroll(&format!("ddd{}", self.offset)), ctx.state) } } + + fn schema(&self) -> TileSchema { + TileSchema::map() + .add("offset", TileSchema::value("Offset", "Uint")) + } } // TODO: SwapN, RotateLeft, RotateRight, RotateLeftN, RotateRightN diff --git a/stackline/tiles/storage.rs b/stackline/tiles/storage.rs index 62df5dd..cd7891a 100644 --- a/stackline/tiles/storage.rs +++ b/stackline/tiles/storage.rs @@ -42,6 +42,11 @@ impl Tile for Store { TextChar::from_state('\u{25cb}', ctx.state) // WHITE CIRCLE } } + + fn schema(&self) -> TileSchema { + TileSchema::map() + .add("signal", TileSchema::value("Stored signal", "Signal|null")) + } } /// When a signal is received, reads a signal from a [`Store`] (at the reader's tail), @@ -97,6 +102,10 @@ impl Tile for Reader { Direction::Up => TextChar::from_state('\u{25b3}', ctx.state), // WHITE UP-POINTING TRIANGLE } } + + fn schema(&self) -> TileSchema { + TileSchema::value("Direction", "Direction") + } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -193,6 +202,12 @@ impl Tile for Stacker { _ => TextChar::from_state('+', ctx.state), } } + + fn schema(&self) -> TileSchema { + TileSchema::map() + .add("signal", TileSchema::value("Signal to stack", "Signal|null")) + .add("orientation", TileSchema::value("Orientation", "Orientation")) + } } /// Tries to convert a [`FullTile`] to a [`Store`] diff --git a/stackline/tiles/transmit.rs b/stackline/tiles/transmit.rs index 4dc0e05..8e90e25 100644 --- a/stackline/tiles/transmit.rs +++ b/stackline/tiles/transmit.rs @@ -31,6 +31,17 @@ impl Tile for Teleporter { fn draw_simple(&self, ctx: DrawContext) -> TextChar { TextChar::from_state('T', ctx.state) } + + fn schema(&self) -> TileSchema { + TileSchema::map() + .add( + "coordinates", + TileSchema::tuple() + .push(TileSchema::value("Pane", "String")) + .push(TileSchema::value("X", "Uint")) + .push(TileSchema::value("Y", "Uint")) + ) + } } /// Sends a signal through a virtual wire towards `coordinates`. diff --git a/stackline/tiles/wire.rs b/stackline/tiles/wire.rs index e7e461d..d2e4bc5 100644 --- a/stackline/tiles/wire.rs +++ b/stackline/tiles/wire.rs @@ -45,7 +45,7 @@ impl Tile for Wire { } fn schema(&self) -> TileSchema { - TileSchema::value("Orientation") + TileSchema::value("Orientation", "Orientation") } } @@ -93,7 +93,7 @@ impl Tile for Diode { } fn schema(&self) -> TileSchema { - TileSchema::value("Direction") + TileSchema::value("Direction", "Direction") } } @@ -146,8 +146,8 @@ impl Tile for Resistor { fn schema(&self) -> TileSchema { TileSchema::map() - .add("direction", TileSchema::value("Direction")) - .add("signal", TileSchema::value("Signal|null")) + .add("direction", TileSchema::value("Direction", "Direction")) + .add("signal", TileSchema::value("Signal", "Signal|null")) } }