From 12abef4076492fb3bcc2fdf968afa57f4b10ef6d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 28 Jul 2024 21:10:04 -0700 Subject: [PATCH] Trial a Rust engine powering a Civet interface Write a basic web app with a Rust engine, compiled to WebAssembly, powering a Civet interface. Do linear algebra in the engine using the `nalgebra` crate. --- lang-trials/rust/Cargo.toml | 29 +++++++++ lang-trials/rust/Makefile | 6 ++ lang-trials/rust/app.civet | 113 +++++++++++++++++++++++++++++++++ lang-trials/rust/app.css | 38 +++++++++++ lang-trials/rust/index.html | 21 ++++++ lang-trials/rust/src/lib.rs | 55 ++++++++++++++++ lang-trials/rust/src/utils.rs | 10 +++ lang-trials/rust/tsconfig.json | 15 +++++ 8 files changed, 287 insertions(+) create mode 100644 lang-trials/rust/Cargo.toml create mode 100644 lang-trials/rust/Makefile create mode 100644 lang-trials/rust/app.civet create mode 100644 lang-trials/rust/app.css create mode 100644 lang-trials/rust/index.html create mode 100644 lang-trials/rust/src/lib.rs create mode 100644 lang-trials/rust/src/utils.rs create mode 100644 lang-trials/rust/tsconfig.json diff --git a/lang-trials/rust/Cargo.toml b/lang-trials/rust/Cargo.toml new file mode 100644 index 0000000..01c0b86 --- /dev/null +++ b/lang-trials/rust/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "engine" +version = "0.1.0" +authors = ["Aaron"] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +nalgebra = "0.33.0" +js-sys = "0.3.69" +wasm-bindgen = "0.2.84" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.7", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.3.34" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" diff --git a/lang-trials/rust/Makefile b/lang-trials/rust/Makefile new file mode 100644 index 0000000..b94935b --- /dev/null +++ b/lang-trials/rust/Makefile @@ -0,0 +1,6 @@ +app.js: app.civet pkg/engine_bg.wasm + civet --typecheck app.civet + civet --js -c app.civet -o .js + +pkg/engine_bg.wasm: src/lib.rs + wasm-pack build --target web diff --git a/lang-trials/rust/app.civet b/lang-trials/rust/app.civet new file mode 100644 index 0000000..ab35044 --- /dev/null +++ b/lang-trials/rust/app.civet @@ -0,0 +1,113 @@ +// engine functions +{default: init, Circle, circThru} from "./pkg/engine.js" + +// === elements and state === + +// input +dataInputs .= new Array(6) +data .= new Float64Array(6) + +// output and display +let circ: Circle | null +let display: HTMLCanvasElement +let ctx: CanvasRenderingContext2D + +// === display === + +// display style +const rView = 5 +const highlightStyle = "#fcfcfc" +const gridStyle = "#404040" +const dataFillStyles = ["#ba5d09", "#0e8a06", "#8951fb"] +const dataStrokeStyles = ["#f89142", "#58c145", "#c396fc"] + +function render: void + // update resolution + res .= display.width / (2*rView) + + // set transformation + ctx.setTransform 1, 0, 0, -1, 0.5*display.width, 0.5*display.height + + // clear previous frame + ctx.clearRect -0.5*display.width, -0.5*display.height, display.width, display.height + + // draw grid + gridRange .= [Math.ceil(-rView + 0.01) .. Math.floor(rView - 0.01)] + edgeScr .= res*rView + ctx.strokeStyle = gridStyle + for t of gridRange + tScr .= res*t + + // draw horizontal grid line + ctx.beginPath() + ctx.moveTo -edgeScr, tScr + ctx.lineTo edgeScr, tScr + ctx.stroke() + + // draw vertical grid line + ctx.beginPath() + ctx.moveTo tScr, -edgeScr + ctx.lineTo tScr, edgeScr + ctx.stroke() + + // draw circle + if circ + ctx.beginPath() + ctx.strokeStyle = highlightStyle + ctx.arc res*circ.center_x, res*circ.center_y, res*circ.radius, 0, 2*Math.PI + ctx.stroke() + + // draw data points + for n of [0..2] + const ind_x = 2*n + const ind_y = ind_x + 1 + if dataInputs[ind_x].validity.valid and dataInputs[ind_y].validity.valid + ctx.beginPath() + ctx.fillStyle = dataFillStyles[n] + ctx.strokeStyle = dataStrokeStyles[n] + ctx.arc res*data[ind_x], res*data[ind_y], 3, 0, 2*Math.PI + ctx.fill() + ctx.stroke() + +// === interaction === + +// --- inputs --- + +function inputData: void + allDataValid .= true + for n of [0 .. dataInputs.length-1] + if dataInputs[n].validity.valid + data[n] = dataInputs[n].valueAsNumber + else + allDataValid = false + + if allDataValid + try + circ = circThru data + catch ex + circ = null + console.error ex + else + circ = null + + // update the display + requestAnimationFrame(render) + +// --- binding --- + +function bindDoc: Promise + // wait for the engine to load + await init() + + // set up the data inputs + for n of [0..5] + dataInputs[n] = document.querySelector(`#data-input-${n}`) as HTMLInputElement + dataInputs[n].addEventListener "input", inputData + dataInputs[n].style.borderColor = dataFillStyles[Math.floor(0.5*n)] + + // initialize the display + display = document.querySelector("#display") as HTMLCanvasElement + ctx = display.getContext("2d") as CanvasRenderingContext2D + inputData() + +document.addEventListener "DOMContentLoaded", bindDoc diff --git a/lang-trials/rust/app.css b/lang-trials/rust/app.css new file mode 100644 index 0000000..5486be3 --- /dev/null +++ b/lang-trials/rust/app.css @@ -0,0 +1,38 @@ +body { + margin-left: 20px; + margin-top: 20px; + color: #fcfcfc; + background-color: #202020; +} + +input { + color: inherit; + background-color: #020202; + border: 1px solid #606060; + min-width: 40px; + border-radius: 4px; +} + +#data-panel { + float: left; + margin-left: 20px; + display: grid; + grid-template-columns: auto auto; + gap: 10px 10px; + width: 120px; +} + +#data-panel > div { + text-align: center; +} + +#result-display { + margin-top: 10px; + font-weight: bold; +} + +canvas { + float: left; + background-color: #020202; + border-radius: 10px; +} \ No newline at end of file diff --git a/lang-trials/rust/index.html b/lang-trials/rust/index.html new file mode 100644 index 0000000..34e6d97 --- /dev/null +++ b/lang-trials/rust/index.html @@ -0,0 +1,21 @@ + + + + Lattice circle + + + + + +
+
x
+
y
+ + + + + + +
+ + diff --git a/lang-trials/rust/src/lib.rs b/lang-trials/rust/src/lib.rs new file mode 100644 index 0000000..22c20bc --- /dev/null +++ b/lang-trials/rust/src/lib.rs @@ -0,0 +1,55 @@ +mod utils; + +extern crate js_sys; + +use nalgebra::*; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct Circle { + pub center_x: f64, + pub center_y: f64, + pub radius: f64, +} + +// construct the circle through +// +// (x1, y1), (x2, y2), (x3, y3) +// +// from the array +// +// [x1, y1, x2, y2, x3, y3] +// +#[wasm_bindgen] +pub fn circThru(data_raw: js_sys::Float64Array) -> Result { + // represent the given points as the columns of a matrix + let data = Matrix2x3::from_vec(data_raw.to_vec()); + + // build the matrix that maps the circle's coefficient vector to the + // negative of the linear part of the circle's equation, evaluated at the + // given points + let neg_lin_part = stack![2.0*data.transpose(), Vector3::repeat(1.0)]; + + // find the quadrdatic part of the circle's equation, evaluated at the given + // points + let quad_part = Vector3::from_iterator( + data.column_iter().map(|v| v.dot(&v)) + ); + + // find the circle's coefficient vector, and from there its center and + // radius + match neg_lin_part.lu().solve(&quad_part) { + None => Err(JsValue::from("Couldn't solve system")), + Some(coeffs) => { + let center_x = coeffs[0]; + let center_y = coeffs[1]; + Ok(Circle { + center_x: center_x, + center_y: center_y, + radius: ( + coeffs[2] + center_x*center_x + center_y*center_y + ).sqrt(), + }) + } + } +} diff --git a/lang-trials/rust/src/utils.rs b/lang-trials/rust/src/utils.rs new file mode 100644 index 0000000..b1d7929 --- /dev/null +++ b/lang-trials/rust/src/utils.rs @@ -0,0 +1,10 @@ +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} diff --git a/lang-trials/rust/tsconfig.json b/lang-trials/rust/tsconfig.json new file mode 100644 index 0000000..a6c05ac --- /dev/null +++ b/lang-trials/rust/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "strict": true, + "jsx": "preserve", + "lib": ["es2021", "dom"], + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true + }, + "ts-node": { + "transpileOnly": true, + "compilerOptions": { + "module": "ES2020" + } + } +}