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.
This commit is contained in:
parent
d7dbee4c05
commit
12abef4076
29
lang-trials/rust/Cargo.toml
Normal file
29
lang-trials/rust/Cargo.toml
Normal file
@ -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"
|
6
lang-trials/rust/Makefile
Normal file
6
lang-trials/rust/Makefile
Normal file
@ -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
|
113
lang-trials/rust/app.civet
Normal file
113
lang-trials/rust/app.civet
Normal file
@ -0,0 +1,113 @@
|
||||
// engine functions
|
||||
{default: init, Circle, circThru} from "./pkg/engine.js"
|
||||
|
||||
// === elements and state ===
|
||||
|
||||
// input
|
||||
dataInputs .= new Array<HTMLInputElement>(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<void>
|
||||
// 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
|
38
lang-trials/rust/app.css
Normal file
38
lang-trials/rust/app.css
Normal file
@ -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;
|
||||
}
|
21
lang-trials/rust/index.html
Normal file
21
lang-trials/rust/index.html
Normal file
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Lattice circle</title>
|
||||
<script type="module" src="app.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="app.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="display" width=600, height=600></canvas>
|
||||
<div id="data-panel">
|
||||
<div>x</div>
|
||||
<div>y</div>
|
||||
<input type="number" id="data-input-0" value="-1"/>
|
||||
<input type="number" id="data-input-1" value="0"/>
|
||||
<input type="number" id="data-input-2" value="0"/>
|
||||
<input type="number" id="data-input-3" value="-1"/>
|
||||
<input type="number" id="data-input-4" value="1"/>
|
||||
<input type="number" id="data-input-5" value="0"/>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
55
lang-trials/rust/src/lib.rs
Normal file
55
lang-trials/rust/src/lib.rs
Normal file
@ -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<Circle, JsValue> {
|
||||
// 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
10
lang-trials/rust/src/utils.rs
Normal file
10
lang-trials/rust/src/utils.rs
Normal file
@ -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();
|
||||
}
|
15
lang-trials/rust/tsconfig.json
Normal file
15
lang-trials/rust/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"lib": ["es2021", "dom"],
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"ts-node": {
|
||||
"transpileOnly": true,
|
||||
"compilerOptions": {
|
||||
"module": "ES2020"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user