forked from StudioInfinity/dyna3
Compare commits
20 commits
outline-cl
...
main
Author | SHA1 | Date | |
---|---|---|---|
0801200210 | |||
5864017e6f | |||
4cb3262555 | |||
e447e7ea96 | |||
a671a8273a | |||
2adf4669f4 | |||
a2478febc1 | |||
360ce12d8b | |||
23ba5acad7 | |||
b86f176151 | |||
2c4fd39c1f | |||
da28bc99d2 | |||
46324fecc6 | |||
25017176fd | |||
817a446fad | |||
22870342f3 | |||
b490c8707f | |||
a8e13b8110 | |||
e917272c60 | |||
65cee1ecc2 |
34 changed files with 5876 additions and 1342 deletions
22
.forgejo/setup-trunk/action.yaml
Normal file
22
.forgejo/setup-trunk/action.yaml
Normal file
|
@ -0,0 +1,22 @@
|
|||
# set up the Trunk web build system
|
||||
#
|
||||
# https://trunkrs.dev
|
||||
#
|
||||
# the `curl` call is based on David Tolnay's `rust-toolchain` action
|
||||
#
|
||||
# https://github.com/dtolnay/rust-toolchain
|
||||
#
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
# install the Trunk binary to `ci-bin` within the workspace directory, which
|
||||
# is determined by the `github.workspace` label and reflected in the
|
||||
# `GITHUB_WORKSPACE` environment variable. then, make the `trunk` command
|
||||
# available by placing the fully qualified path to `ci-bin` on the
|
||||
# workflow's search path
|
||||
- run: mkdir -p ci-bin
|
||||
- run: curl --output - --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail 'https://github.com/trunk-rs/trunk/releases/download/v0.21.12/trunk-x86_64-unknown-linux-gnu.tar.gz' | tar --gunzip --extract --file -
|
||||
working-directory: ci-bin
|
||||
- run: echo "${{ github.workspace }}/ci-bin" >> $GITHUB_PATH
|
29
.forgejo/workflows/continuous-integration.yaml
Normal file
29
.forgejo/workflows/continuous-integration.yaml
Normal file
|
@ -0,0 +1,29 @@
|
|||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
jobs:
|
||||
# run the automated tests, reporting success if the tests pass and were built
|
||||
# without warnings. the examples are run as tests, because we've configured
|
||||
# each example target with `test = true` and `harness = false` in Cargo.toml.
|
||||
# Trunk build failures caused by problems outside the Rust source code, like
|
||||
# missing assets, should be caught by `trunk_build_test`
|
||||
test:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: cimg/rust:1.86-node
|
||||
defaults:
|
||||
run:
|
||||
# set the default working directory for each `run` step, relative to the
|
||||
# workspace directory. this default only affects `run` steps (and if we
|
||||
# tried to set the `working-directory` label for any other kind of step,
|
||||
# it wouldn't be recognized anyway)
|
||||
working-directory: app-proto
|
||||
steps:
|
||||
# Check out the repository so that its top-level directory is the
|
||||
# workspace directory (action variable `github.workspace`, environment
|
||||
# variable `$GITHUB_WORKSPACE`):
|
||||
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||
|
||||
- uses: ./.forgejo/setup-trunk
|
||||
- run: RUSTFLAGS='-D warnings' cargo test
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -1,8 +1,2 @@
|
|||
node_modules
|
||||
site
|
||||
docbuild
|
||||
__tests__
|
||||
coverage
|
||||
dyna3.zip
|
||||
tmpproj
|
||||
ci-bin
|
||||
*~
|
||||
|
|
48
README.md
48
README.md
|
@ -17,3 +17,51 @@ Note that currently this is just the barest beginnings of the project, more of a
|
|||
* Able to run in browser (so implemented in WASM-compatible language)
|
||||
|
||||
* Produce scalable graphics of 3D diagrams, and maybe STL files (or other fabricatable file format) as well.
|
||||
|
||||
## Prototype
|
||||
|
||||
The latest prototype is in the folder `app-proto`. It includes both a user interface and a numerical constraint-solving engine.
|
||||
|
||||
### Install the prerequisites
|
||||
|
||||
1. Install [`rustup`](https://rust-lang.github.io/rustup/): the officially recommended Rust toolchain manager
|
||||
* It's available on Ubuntu as a [Snap](https://snapcraft.io/rustup)
|
||||
2. Call `rustup default stable` to "download the latest stable release of Rust and set it as your default toolchain"
|
||||
* If you forget, the `rustup` [help system](https://github.com/rust-lang/rustup/blob/d9b3601c3feb2e88cf3f8ca4f7ab4fdad71441fd/src/errors.rs#L109-L112) will remind you
|
||||
3. Call `rustup target add wasm32-unknown-unknown` to add the [most generic 32-bit WebAssembly target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-unknown-unknown.html)
|
||||
4. Call `cargo install wasm-pack` to install the [WebAssembly toolchain](https://rustwasm.github.io/docs/wasm-pack/)
|
||||
5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool
|
||||
6. Add the `.cargo/bin` folder in your home directory to your executable search path
|
||||
* This lets you call Trunk, and other tools installed by Cargo, without specifying their paths
|
||||
* On POSIX systems, the search path is stored in the `PATH` environment variable
|
||||
|
||||
### Play with the prototype
|
||||
|
||||
1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype
|
||||
* *The crates the prototype depends on will be downloaded and served automatically*
|
||||
* *For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag*
|
||||
* *If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]`* from there instead.
|
||||
3. In a web browser, visit one of the URLs listed under the message `INFO 📡 server listening at:`
|
||||
* *Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype*
|
||||
4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype
|
||||
|
||||
### Run the engine on some example problems
|
||||
|
||||
1. Go into the `app-proto` folder
|
||||
2. Call `./run-examples`
|
||||
* *For each example problem, the engine will print the value of the loss function at each optimization step*
|
||||
* *The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then*
|
||||
|
||||
```julia
|
||||
include("irisawa-hexlet.jl")
|
||||
for (step, scaled_loss) in enumerate(history_alt.scaled_loss)
|
||||
println(rpad(step-1, 4), " | ", scaled_loss)
|
||||
end
|
||||
```
|
||||
|
||||
*you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show*
|
||||
|
||||
### Run the automated tests
|
||||
|
||||
1. Go into the `app-proto` folder
|
||||
2. Call `cargo test`
|
||||
|
|
1325
app-proto/Cargo.lock
generated
Normal file
1325
app-proto/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -3,18 +3,22 @@ name = "dyna3"
|
|||
version = "0.1.0"
|
||||
authors = ["Aaron Fenyes", "Glen Whitney"]
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
|
||||
[features]
|
||||
default = ["console_error_panic_hook"]
|
||||
dev = []
|
||||
|
||||
[dependencies]
|
||||
itertools = "0.13.0"
|
||||
js-sys = "0.3.70"
|
||||
lazy_static = "1.5.0"
|
||||
nalgebra = "0.33.0"
|
||||
rustc-hash = "2.0.0"
|
||||
slab = "0.4.9"
|
||||
sycamore = "0.9.0-beta.3"
|
||||
readonly = "0.2.12"
|
||||
sycamore = "0.9.1"
|
||||
|
||||
# We use Charming to help display engine diagnostics
|
||||
charming = { version = "0.5.1", features = ["wasm"] }
|
||||
|
||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||
# logging them with `console.error`. This is great for development, but requires
|
||||
|
@ -25,6 +29,7 @@ console_error_panic_hook = { version = "0.1.7", optional = true }
|
|||
[dependencies.web-sys]
|
||||
version = "0.3.69"
|
||||
features = [
|
||||
'DomRect',
|
||||
'HtmlCanvasElement',
|
||||
'HtmlInputElement',
|
||||
'Performance',
|
||||
|
@ -36,9 +41,41 @@ features = [
|
|||
'WebGlVertexArrayObject'
|
||||
]
|
||||
|
||||
# the self-dependency specifies features to use for tests and examples
|
||||
#
|
||||
# https://github.com/rust-lang/cargo/issues/2911#issuecomment-1483256987
|
||||
#
|
||||
[dev-dependencies]
|
||||
dyna3 = { path = ".", default-features = false, features = ["dev"] }
|
||||
wasm-bindgen-test = "0.3.34"
|
||||
|
||||
# turn off spurious warnings about the custom config that Sycamore uses
|
||||
#
|
||||
# https://sycamore.dev/book/troubleshooting#unexpected-cfg-condition-name--sycamore-force-ssr
|
||||
#
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(sycamore_force_ssr)"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s" # optimize for small code size
|
||||
debug = true # include debug symbols
|
||||
|
||||
[[example]]
|
||||
name = "irisawa-hexlet"
|
||||
test = true
|
||||
harness = false
|
||||
|
||||
[[example]]
|
||||
name = "kaleidocycle"
|
||||
test = true
|
||||
harness = false
|
||||
|
||||
[[example]]
|
||||
name = "point-on-sphere"
|
||||
test = true
|
||||
harness = false
|
||||
|
||||
[[example]]
|
||||
name = "three-spheres"
|
||||
test = true
|
||||
harness = false
|
||||
|
|
36
app-proto/examples/common/print.rs
Normal file
36
app-proto/examples/common/print.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use nalgebra::DMatrix;
|
||||
|
||||
use dyna3::engine::{Q, DescentHistory, Realization};
|
||||
|
||||
pub fn title(title: &str) {
|
||||
println!("─── {title} ───");
|
||||
}
|
||||
|
||||
pub fn realization_diagnostics(realization: &Realization) {
|
||||
let Realization { result, history } = realization;
|
||||
println!();
|
||||
if let Err(ref message) = result {
|
||||
println!("❌️ {message}");
|
||||
} else {
|
||||
println!("✅️ Target accuracy achieved!");
|
||||
}
|
||||
println!("Steps: {}", history.scaled_loss.len() - 1);
|
||||
println!("Loss: {}", history.scaled_loss.last().unwrap());
|
||||
}
|
||||
|
||||
pub fn gram_matrix(config: &DMatrix<f64>) {
|
||||
println!("\nCompleted Gram matrix:{}", (config.tr_mul(&*Q) * config).to_string().trim_end());
|
||||
}
|
||||
|
||||
pub fn config(config: &DMatrix<f64>) {
|
||||
println!("\nConfiguration:{}", config.to_string().trim_end());
|
||||
}
|
||||
|
||||
pub fn loss_history(history: &DescentHistory) {
|
||||
println!("\nStep │ Loss\n─────┼────────────────────────────────");
|
||||
for (step, scaled_loss) in history.scaled_loss.iter().enumerate() {
|
||||
println!("{:<4} │ {}", step, scaled_loss);
|
||||
}
|
||||
}
|
23
app-proto/examples/irisawa-hexlet.rs
Normal file
23
app-proto/examples/irisawa-hexlet.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
#[path = "common/print.rs"]
|
||||
mod print;
|
||||
|
||||
use dyna3::engine::{ConfigNeighborhood, examples::realize_irisawa_hexlet};
|
||||
|
||||
fn main() {
|
||||
const SCALED_TOL: f64 = 1.0e-12;
|
||||
let realization = realize_irisawa_hexlet(SCALED_TOL);
|
||||
print::title("Irisawa hexlet");
|
||||
print::realization_diagnostics(&realization);
|
||||
if let Ok(ConfigNeighborhood { config, .. }) = realization.result {
|
||||
// print the diameters of the chain spheres
|
||||
println!("\nChain diameters:");
|
||||
println!(" {} sun (given)", 1.0 / config[(3, 3)]);
|
||||
for k in 4..9 {
|
||||
println!(" {} sun", 1.0 / config[(3, k)]);
|
||||
}
|
||||
|
||||
// print the completed Gram matrix
|
||||
print::gram_matrix(&config);
|
||||
}
|
||||
print::loss_history(&realization.history);
|
||||
}
|
32
app-proto/examples/kaleidocycle.rs
Normal file
32
app-proto/examples/kaleidocycle.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
#[path = "common/print.rs"]
|
||||
mod print;
|
||||
|
||||
use nalgebra::{DMatrix, DVector};
|
||||
|
||||
use dyna3::engine::{ConfigNeighborhood, examples::realize_kaleidocycle};
|
||||
|
||||
fn main() {
|
||||
const SCALED_TOL: f64 = 1.0e-12;
|
||||
let realization = realize_kaleidocycle(SCALED_TOL);
|
||||
print::title("Kaleidocycle");
|
||||
print::realization_diagnostics(&realization);
|
||||
if let Ok(ConfigNeighborhood { config, nbhd: tangent }) = realization.result {
|
||||
// print the completed Gram matrix and the realized configuration
|
||||
print::gram_matrix(&config);
|
||||
print::config(&config);
|
||||
|
||||
// find the kaleidocycle's twist motion by projecting onto the tangent
|
||||
// space
|
||||
const N_POINTS: usize = 12;
|
||||
let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]);
|
||||
let down = -&up;
|
||||
let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map(
|
||||
|n| [
|
||||
tangent.proj(&up.as_view(), n),
|
||||
tangent.proj(&down.as_view(), n+1)
|
||||
]
|
||||
).sum();
|
||||
let normalization = 5.0 / twist_motion[(2, 0)];
|
||||
println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end());
|
||||
}
|
||||
}
|
33
app-proto/examples/point-on-sphere.rs
Normal file
33
app-proto/examples/point-on-sphere.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
#[path = "common/print.rs"]
|
||||
mod print;
|
||||
|
||||
use dyna3::engine::{
|
||||
point,
|
||||
realize_gram,
|
||||
sphere,
|
||||
ConfigNeighborhood,
|
||||
ConstraintProblem
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let mut problem = ConstraintProblem::from_guess(&[
|
||||
point(0.0, 0.0, 2.0),
|
||||
sphere(0.0, 0.0, 0.0, 1.0)
|
||||
]);
|
||||
for j in 0..2 {
|
||||
for k in j..2 {
|
||||
problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 });
|
||||
}
|
||||
}
|
||||
problem.frozen.push(3, 0, problem.guess[(3, 0)]);
|
||||
let realization = realize_gram(
|
||||
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||
);
|
||||
print::title("Point on a sphere");
|
||||
print::realization_diagnostics(&realization);
|
||||
if let Ok(ConfigNeighborhood{ config, .. }) = realization.result {
|
||||
print::gram_matrix(&config);
|
||||
print::config(&config);
|
||||
}
|
||||
print::loss_history(&realization.history);
|
||||
}
|
34
app-proto/examples/three-spheres.rs
Normal file
34
app-proto/examples/three-spheres.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
#[path = "common/print.rs"]
|
||||
mod print;
|
||||
|
||||
use dyna3::engine::{
|
||||
realize_gram,
|
||||
sphere,
|
||||
ConfigNeighborhood,
|
||||
ConstraintProblem
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let mut problem = ConstraintProblem::from_guess({
|
||||
let a: f64 = 0.75_f64.sqrt();
|
||||
&[
|
||||
sphere(1.0, 0.0, 0.0, 1.0),
|
||||
sphere(-0.5, a, 0.0, 1.0),
|
||||
sphere(-0.5, -a, 0.0, 1.0)
|
||||
]
|
||||
});
|
||||
for j in 0..3 {
|
||||
for k in j..3 {
|
||||
problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
|
||||
}
|
||||
}
|
||||
let realization = realize_gram(
|
||||
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||
);
|
||||
print::title("Three spheres");
|
||||
print::realization_diagnostics(&realization);
|
||||
if let Ok(ConfigNeighborhood{ config, .. }) = realization.result {
|
||||
print::gram_matrix(&config);
|
||||
}
|
||||
print::loss_history(&realization.history);
|
||||
}
|
|
@ -6,6 +6,12 @@
|
|||
<link data-trunk rel="css" href="main.css"/>
|
||||
<link href="https://fonts.bunny.net/css?family=fira-sans:ital,wght@0,400;1,400&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.bunny.net/css?family=noto-emoji:wght@400&text=%f0%9f%94%97%e2%9a%a0&display=swap" rel="stylesheet">
|
||||
|
||||
<!--
|
||||
the Charming visualization crate, which we use to show engine diagnostics,
|
||||
depends the ECharts JavaScript package
|
||||
-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
--text-bright: white;
|
||||
--text-invalid: #f58fc2; /* bright pink */
|
||||
--border: #555; /* light gray */
|
||||
--border-focus: #aaa; /* bright gray */
|
||||
--border-focus-dark: #aaa; /* bright gray */
|
||||
--border-focus-light: white;
|
||||
--border-invalid: #70495c; /* dusky pink */
|
||||
--selection-highlight: #444; /* medium gray */
|
||||
--page-background: #222; /* dark gray */
|
||||
|
@ -17,13 +18,24 @@ body {
|
|||
font-family: 'Fira Sans', sans-serif;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
color: var(--text-invalid);
|
||||
}
|
||||
|
||||
.status {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
font-family: 'Noto Emoji';
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* sidebar */
|
||||
|
||||
#sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
float: left;
|
||||
width: 450px;
|
||||
width: 500px;
|
||||
height: 100vh;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
|
@ -41,9 +53,7 @@ body {
|
|||
}
|
||||
|
||||
#add-remove > button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
/* KLUDGE */
|
||||
|
@ -52,7 +62,9 @@ body {
|
|||
buttons need to be displayed in an emoji font
|
||||
*/
|
||||
#add-remove > button.emoji {
|
||||
width: 32px;
|
||||
font-family: 'Noto Emoji', sans-serif;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
/* outline */
|
||||
|
@ -77,18 +89,22 @@ summary.selected {
|
|||
background-color: var(--selection-highlight);
|
||||
}
|
||||
|
||||
summary > div, .constraint {
|
||||
summary > div, .regulator {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.element, .constraint {
|
||||
.element, .regulator {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.element > input {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.element-switch {
|
||||
width: 18px;
|
||||
padding-left: 2px;
|
||||
|
@ -107,7 +123,7 @@ details[open]:has(li) .element-switch::after {
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.constraint-label {
|
||||
.regulator-label {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
@ -123,45 +139,88 @@ details[open]:has(li) .element-switch::after {
|
|||
width: 56px;
|
||||
}
|
||||
|
||||
.constraint {
|
||||
.regulator {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.constraint.invalid {
|
||||
color: var(--text-invalid);
|
||||
.regulator-type {
|
||||
padding: 2px 8px 0px 8px;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.constraint > input[type=checkbox] {
|
||||
margin: 0px 8px 0px 0px;
|
||||
}
|
||||
|
||||
.constraint > input[type=text] {
|
||||
.regulator-input {
|
||||
margin-right: 4px;
|
||||
color: inherit;
|
||||
background-color: inherit;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.constraint.invalid > input[type=text] {
|
||||
.regulator-input::placeholder {
|
||||
color: inherit;
|
||||
opacity: 54%;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.regulator-input.constraint {
|
||||
background-color: var(--display-background);
|
||||
}
|
||||
|
||||
.regulator-input.invalid {
|
||||
color: var(--text-invalid);
|
||||
border-color: var(--border-invalid);
|
||||
}
|
||||
|
||||
.status {
|
||||
width: 20px;
|
||||
padding-left: 4px;
|
||||
text-align: center;
|
||||
font-family: 'Noto Emoji';
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.invalid > .status::after, details:has(.invalid):not([open]) .status::after {
|
||||
.regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after {
|
||||
content: '⚠';
|
||||
color: var(--text-invalid);
|
||||
}
|
||||
|
||||
/* diagnostics */
|
||||
|
||||
#diagnostics {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#diagnostics-bar {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#realization-status {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#realization-status .status {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#realization-status :not(.status) {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#realization-status .status::after {
|
||||
content: '✓';
|
||||
}
|
||||
|
||||
#realization-status.invalid .status::after {
|
||||
content: '⚠';
|
||||
}
|
||||
|
||||
.diagnostics-panel {
|
||||
margin-top: 10px;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.diagnostics-chart {
|
||||
background-color: var(--display-background);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* display */
|
||||
|
||||
canvas {
|
||||
#display {
|
||||
float: left;
|
||||
margin-left: 20px;
|
||||
margin-top: 20px;
|
||||
|
@ -170,6 +229,12 @@ canvas {
|
|||
border-radius: 16px;
|
||||
}
|
||||
|
||||
canvas:focus {
|
||||
border-color: var(--border-focus);
|
||||
#display:focus {
|
||||
border-color: var(--border-focus-dark);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: var(--border-focus-light);
|
||||
outline: none;
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
# based on "Enabling print statements in Cargo tests", by Jon Almeida
|
||||
#
|
||||
# https://jonalmeida.com/posts/2015/01/23/print-cargo/
|
||||
#
|
||||
|
||||
cargo test -- --nocapture engine::tests::irisawa_hexlet_test
|
||||
cargo test -- --nocapture engine::tests::three_spheres_example
|
||||
cargo test -- --nocapture engine::tests::point_on_sphere_example
|
20
app-proto/run-examples.sh
Normal file
20
app-proto/run-examples.sh
Normal file
|
@ -0,0 +1,20 @@
|
|||
# run all Cargo examples, as described here:
|
||||
#
|
||||
# Karol Kuczmarski. "Add examples to your Rust libraries"
|
||||
# http://xion.io/post/code/rust-examples.html
|
||||
#
|
||||
# you should invoke this script by calling `sh` or another interpreter, rather
|
||||
# than calling `souce`, to ensure that the script can find the manifest file for
|
||||
# the application prototype
|
||||
|
||||
# find the manifest file for the application prototype
|
||||
MANIFEST="$(dirname -- $0)/Cargo.toml"
|
||||
|
||||
# set up the command that runs each example
|
||||
RUN_EXAMPLE="cargo run --manifest-path $MANIFEST --example"
|
||||
|
||||
# run the examples
|
||||
$RUN_EXAMPLE irisawa-hexlet; echo
|
||||
$RUN_EXAMPLE three-spheres; echo
|
||||
$RUN_EXAMPLE point-on-sphere; echo
|
||||
$RUN_EXAMPLE kaleidocycle
|
|
@ -1,240 +0,0 @@
|
|||
use sycamore::prelude::*;
|
||||
use web_sys::{console, wasm_bindgen::JsValue};
|
||||
|
||||
use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}};
|
||||
|
||||
/* DEBUG */
|
||||
// load an example assembly for testing. this code will be removed once we've
|
||||
// built a more formal test assembly system
|
||||
fn load_gen_assemb(assembly: &Assembly) {
|
||||
let _ = assembly.try_insert_element(
|
||||
Element::new(
|
||||
String::from("gemini_a"),
|
||||
String::from("Castor"),
|
||||
[1.00_f32, 0.25_f32, 0.00_f32],
|
||||
engine::sphere(0.5, 0.5, 0.0, 1.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Element::new(
|
||||
String::from("gemini_b"),
|
||||
String::from("Pollux"),
|
||||
[0.00_f32, 0.25_f32, 1.00_f32],
|
||||
engine::sphere(-0.5, -0.5, 0.0, 1.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Element::new(
|
||||
String::from("ursa_major"),
|
||||
String::from("Ursa major"),
|
||||
[0.25_f32, 0.00_f32, 1.00_f32],
|
||||
engine::sphere(-0.5, 0.5, 0.0, 0.75)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Element::new(
|
||||
String::from("ursa_minor"),
|
||||
String::from("Ursa minor"),
|
||||
[0.25_f32, 1.00_f32, 0.00_f32],
|
||||
engine::sphere(0.5, -0.5, 0.0, 0.5)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Element::new(
|
||||
String::from("moon_deimos"),
|
||||
String::from("Deimos"),
|
||||
[0.75_f32, 0.75_f32, 0.00_f32],
|
||||
engine::sphere(0.0, 0.15, 1.0, 0.25)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Element::new(
|
||||
String::from("moon_phobos"),
|
||||
String::from("Phobos"),
|
||||
[0.00_f32, 0.75_f32, 0.50_f32],
|
||||
engine::sphere(0.0, -0.15, -1.0, 0.25)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/* DEBUG */
|
||||
// load an example assembly for testing. this code will be removed once we've
|
||||
// built a more formal test assembly system
|
||||
fn load_low_curv_assemb(assembly: &Assembly) {
|
||||
let a = 0.75_f64.sqrt();
|
||||
let _ = assembly.try_insert_element(
|
||||
Element::new(
|
||||
"central".to_string(),
|
||||
"Central".to_string(),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::sphere(0.0, 0.0, 0.0, 1.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Element::new(
|
||||
"assemb_plane".to_string(),
|
||||
"Assembly plane".to_string(),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Element::new(
|
||||
"side1".to_string(),
|
||||
"Side 1".to_string(),
|
||||
[1.00_f32, 0.00_f32, 0.25_f32],
|
||||
engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Element::new(
|
||||
"side2".to_string(),
|
||||
"Side 2".to_string(),
|
||||
[0.25_f32, 1.00_f32, 0.00_f32],
|
||||
engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Element::new(
|
||||
"side3".to_string(),
|
||||
"Side 3".to_string(),
|
||||
[0.00_f32, 0.25_f32, 1.00_f32],
|
||||
engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Element::new(
|
||||
"corner1".to_string(),
|
||||
"Corner 1".to_string(),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Element::new(
|
||||
"corner2".to_string(),
|
||||
"Corner 2".to_string(),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Element::new(
|
||||
String::from("corner3"),
|
||||
String::from("Corner 3"),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn AddRemove() -> View {
|
||||
/* DEBUG */
|
||||
let assembly_name = create_signal("general".to_string());
|
||||
create_effect(move || {
|
||||
// get name of chosen assembly
|
||||
let name = assembly_name.get_clone();
|
||||
console::log_1(
|
||||
&JsValue::from(format!("Showing assembly \"{}\"", name.clone()))
|
||||
);
|
||||
|
||||
batch(|| {
|
||||
let state = use_context::<AppState>();
|
||||
let assembly = &state.assembly;
|
||||
|
||||
// clear state
|
||||
assembly.elements.update(|elts| elts.clear());
|
||||
assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear());
|
||||
state.selection.update(|sel| sel.clear());
|
||||
|
||||
// load assembly
|
||||
match name.as_str() {
|
||||
"general" => load_gen_assemb(assembly),
|
||||
"low-curv" => load_low_curv_assemb(assembly),
|
||||
_ => ()
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
view! {
|
||||
div(id="add-remove") {
|
||||
button(
|
||||
on:click=|_| {
|
||||
let state = use_context::<AppState>();
|
||||
state.assembly.insert_new_element();
|
||||
|
||||
/* DEBUG */
|
||||
// print updated list of elements by identifier
|
||||
console::log_1(&JsValue::from("elements by identifier:"));
|
||||
for (id, key) in state.assembly.elements_by_id.get_clone().iter() {
|
||||
console::log_3(
|
||||
&JsValue::from(" "),
|
||||
&JsValue::from(id),
|
||||
&JsValue::from(*key)
|
||||
);
|
||||
}
|
||||
}
|
||||
) { "+" }
|
||||
button(
|
||||
class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button
|
||||
disabled={
|
||||
let state = use_context::<AppState>();
|
||||
state.selection.with(|sel| sel.len() != 2)
|
||||
},
|
||||
on:click=|_| {
|
||||
let state = use_context::<AppState>();
|
||||
let subjects = state.selection.with(
|
||||
|sel| {
|
||||
let subject_vec: Vec<_> = sel.into_iter().collect();
|
||||
(subject_vec[0].clone(), subject_vec[1].clone())
|
||||
}
|
||||
);
|
||||
let lorentz_prod = create_signal(0.0);
|
||||
let lorentz_prod_valid = create_signal(false);
|
||||
let active = create_signal(true);
|
||||
state.assembly.insert_constraint(Constraint {
|
||||
subjects: subjects,
|
||||
lorentz_prod: lorentz_prod,
|
||||
lorentz_prod_text: create_signal(String::new()),
|
||||
lorentz_prod_valid: lorentz_prod_valid,
|
||||
active: active,
|
||||
});
|
||||
state.selection.update(|sel| sel.clear());
|
||||
|
||||
/* DEBUG */
|
||||
// print updated constraint list
|
||||
console::log_1(&JsValue::from("Constraints:"));
|
||||
state.assembly.constraints.with(|csts| {
|
||||
for (_, cst) in csts.into_iter() {
|
||||
console::log_5(
|
||||
&JsValue::from(" "),
|
||||
&JsValue::from(cst.subjects.0),
|
||||
&JsValue::from(cst.subjects.1),
|
||||
&JsValue::from(":"),
|
||||
&JsValue::from(cst.lorentz_prod.get_untracked())
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// update the realization when the constraint becomes active
|
||||
// and valid, or is edited while active and valid
|
||||
create_effect(move || {
|
||||
console::log_1(&JsValue::from(
|
||||
format!("Constraint ({}, {}) updated", subjects.0, subjects.1)
|
||||
));
|
||||
lorentz_prod.track();
|
||||
if active.get() && lorentz_prod_valid.get() {
|
||||
state.assembly.realize();
|
||||
}
|
||||
});
|
||||
}
|
||||
) { "🔗" }
|
||||
select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser
|
||||
option(value="general") { "General" }
|
||||
option(value="low-curv") { "Low-curvature" }
|
||||
option(value="empty") { "Empty" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
5
app-proto/src/components.rs
Normal file
5
app-proto/src/components.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod add_remove;
|
||||
pub mod diagnostics;
|
||||
pub mod display;
|
||||
pub mod outline;
|
||||
pub mod test_assembly_chooser;
|
54
app-proto/src/components/add_remove.rs
Normal file
54
app-proto/src/components/add_remove.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use std::rc::Rc;
|
||||
use sycamore::prelude::*;
|
||||
|
||||
use super::test_assembly_chooser::TestAssemblyChooser;
|
||||
use crate::{
|
||||
AppState,
|
||||
assembly::{InversiveDistanceRegulator, Point, Sphere}
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn AddRemove() -> View {
|
||||
view! {
|
||||
div(id="add-remove") {
|
||||
button(
|
||||
on:click=|_| {
|
||||
let state = use_context::<AppState>();
|
||||
state.assembly.insert_element_default::<Sphere>();
|
||||
}
|
||||
) { "Add sphere" }
|
||||
button(
|
||||
on:click=|_| {
|
||||
let state = use_context::<AppState>();
|
||||
state.assembly.insert_element_default::<Point>();
|
||||
}
|
||||
) { "Add point" }
|
||||
button(
|
||||
class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button
|
||||
disabled={
|
||||
let state = use_context::<AppState>();
|
||||
state.selection.with(|sel| sel.len() != 2)
|
||||
},
|
||||
on:click=|_| {
|
||||
let state = use_context::<AppState>();
|
||||
let subjects: [_; 2] = state.selection.with(
|
||||
// the button is only enabled when two elements are
|
||||
// selected, so we know the cast to a two-element array
|
||||
// will succeed
|
||||
|sel| sel
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.try_into()
|
||||
.unwrap()
|
||||
);
|
||||
state.assembly.insert_regulator(
|
||||
Rc::new(InversiveDistanceRegulator::new(subjects))
|
||||
);
|
||||
state.selection.update(|sel| sel.clear());
|
||||
}
|
||||
) { "🔗" }
|
||||
TestAssemblyChooser {}
|
||||
}
|
||||
}
|
||||
}
|
258
app-proto/src/components/diagnostics.rs
Normal file
258
app-proto/src/components/diagnostics.rs
Normal file
|
@ -0,0 +1,258 @@
|
|||
use charming::{
|
||||
Chart,
|
||||
WasmRenderer,
|
||||
component::{Axis, DataZoom, Grid},
|
||||
element::{AxisType, Symbol},
|
||||
series::{Line, Scatter},
|
||||
};
|
||||
use sycamore::prelude::*;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DiagnosticsState {
|
||||
active_tab: Signal<String>
|
||||
}
|
||||
|
||||
impl DiagnosticsState {
|
||||
fn new(initial_tab: String) -> DiagnosticsState {
|
||||
DiagnosticsState {
|
||||
active_tab: create_signal(initial_tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a realization status indicator
|
||||
#[component]
|
||||
fn RealizationStatus() -> View {
|
||||
let state = use_context::<AppState>();
|
||||
let realization_status = state.assembly.realization_status;
|
||||
view! {
|
||||
div(
|
||||
id="realization-status",
|
||||
class=realization_status.with(
|
||||
|status| match status {
|
||||
Ok(_) => "",
|
||||
Err(_) => "invalid"
|
||||
}
|
||||
)
|
||||
) {
|
||||
div(class="status")
|
||||
div {
|
||||
(realization_status.with(
|
||||
|status| match status {
|
||||
Ok(_) => "Target accuracy achieved".to_string(),
|
||||
Err(message) => message.clone()
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn into_log10_time_point((step, value): (usize, f64)) -> Vec<Option<f64>> {
|
||||
vec![
|
||||
Some(step as f64),
|
||||
if value == 0.0 { None } else { Some(value.abs().log10()) }
|
||||
]
|
||||
}
|
||||
|
||||
// the loss history from the last realization
|
||||
#[component]
|
||||
fn LossHistory() -> View {
|
||||
const CONTAINER_ID: &str = "loss-history";
|
||||
let state = use_context::<AppState>();
|
||||
let renderer = WasmRenderer::new_opt(None, Some(178));
|
||||
|
||||
on_mount(move || {
|
||||
create_effect(move || {
|
||||
// get the loss history
|
||||
let scaled_loss: Vec<_> = state.assembly.descent_history.with(
|
||||
|history| history.scaled_loss
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(step, &loss)| (step, loss))
|
||||
.map(into_log10_time_point)
|
||||
.collect()
|
||||
);
|
||||
|
||||
// initialize the chart axes
|
||||
let step_axis = Axis::new()
|
||||
.type_(AxisType::Category)
|
||||
.boundary_gap(false);
|
||||
let scaled_loss_axis = Axis::new();
|
||||
|
||||
// load the chart data. when there's no history, we load the data
|
||||
// point (0, None) to clear the chart. it would feel more natural to
|
||||
// load empty data vectors, but that turns out not to clear the
|
||||
// chart: it instead leads to previous data being re-used
|
||||
let scaled_loss_series = Line::new().data(
|
||||
if scaled_loss.len() > 0 {
|
||||
scaled_loss
|
||||
} else {
|
||||
vec![vec![Some(0.0), None::<f64>]]
|
||||
}
|
||||
);
|
||||
let chart = Chart::new()
|
||||
.animation(false)
|
||||
.data_zoom(DataZoom::new().y_axis_index(0).right(40))
|
||||
.x_axis(step_axis)
|
||||
.y_axis(scaled_loss_axis)
|
||||
.grid(Grid::new().top(20).right(80).bottom(30).left(60))
|
||||
.series(scaled_loss_series);
|
||||
renderer.render(CONTAINER_ID, &chart).unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
view! {
|
||||
div(id=CONTAINER_ID, class="diagnostics-chart")
|
||||
}
|
||||
}
|
||||
|
||||
// the spectrum of the Hessian during the last realization
|
||||
#[component]
|
||||
fn SpectrumHistory() -> View {
|
||||
const CONTAINER_ID: &str = "spectrum-history";
|
||||
let state = use_context::<AppState>();
|
||||
let renderer = WasmRenderer::new(478, 178);
|
||||
|
||||
on_mount(move || {
|
||||
create_effect(move || {
|
||||
// get the spectrum of the Hessian at each step, split into its
|
||||
// positive, negative, and strictly-zero parts
|
||||
let (
|
||||
hess_eigvals_zero,
|
||||
hess_eigvals_nonzero
|
||||
): (Vec<_>, Vec<_>) = state.assembly.descent_history.with(
|
||||
|history| history.hess_eigvals
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(
|
||||
|(step, eigvals)| eigvals.iter().map(
|
||||
move |&val| (step, val)
|
||||
)
|
||||
)
|
||||
.flatten()
|
||||
.partition(|&(_, val)| val == 0.0)
|
||||
);
|
||||
let zero_level = hess_eigvals_nonzero
|
||||
.iter()
|
||||
.map(|(_, val)| val.abs())
|
||||
.reduce(f64::min)
|
||||
.map(|val| 0.1 * val)
|
||||
.unwrap_or(1.0);
|
||||
let (
|
||||
hess_eigvals_pos,
|
||||
hess_eigvals_neg
|
||||
): (Vec<_>, Vec<_>) = hess_eigvals_nonzero
|
||||
.into_iter()
|
||||
.partition(|&(_, val)| val > 0.0);
|
||||
|
||||
// initialize the chart axes
|
||||
let step_axis = Axis::new()
|
||||
.type_(AxisType::Category)
|
||||
.boundary_gap(false);
|
||||
let eigval_axis = Axis::new();
|
||||
|
||||
// load the chart data. when there's no history, we load the data
|
||||
// point (0, None) to clear the chart. it would feel more natural to
|
||||
// load empty data vectors, but that turns out not to clear the
|
||||
// chart: it instead leads to previous data being re-used
|
||||
let eigval_series_pos = Scatter::new()
|
||||
.symbol_size(4.5)
|
||||
.data(
|
||||
if hess_eigvals_pos.len() > 0 {
|
||||
hess_eigvals_pos
|
||||
.into_iter()
|
||||
.map(into_log10_time_point)
|
||||
.collect()
|
||||
} else {
|
||||
vec![vec![Some(0.0), None::<f64>]]
|
||||
}
|
||||
);
|
||||
let eigval_series_neg = Scatter::new()
|
||||
.symbol(Symbol::Diamond)
|
||||
.symbol_size(6.0)
|
||||
.data(
|
||||
if hess_eigvals_neg.len() > 0 {
|
||||
hess_eigvals_neg
|
||||
.into_iter()
|
||||
.map(into_log10_time_point)
|
||||
.collect()
|
||||
} else {
|
||||
vec![vec![Some(0.0), None::<f64>]]
|
||||
}
|
||||
);
|
||||
let eigval_series_zero = Scatter::new()
|
||||
.symbol(Symbol::Triangle)
|
||||
.symbol_size(5.0)
|
||||
.data(
|
||||
if hess_eigvals_zero.len() > 0 {
|
||||
hess_eigvals_zero
|
||||
.into_iter()
|
||||
.map(|(step, _)| (step, zero_level))
|
||||
.map(into_log10_time_point)
|
||||
.collect()
|
||||
} else {
|
||||
vec![vec![Some(0.0), None::<f64>]]
|
||||
}
|
||||
);
|
||||
let chart = Chart::new()
|
||||
.animation(false)
|
||||
.data_zoom(DataZoom::new().y_axis_index(0).right(40))
|
||||
.x_axis(step_axis)
|
||||
.y_axis(eigval_axis)
|
||||
.grid(Grid::new().top(20).right(80).bottom(30).left(60))
|
||||
.series(eigval_series_pos)
|
||||
.series(eigval_series_neg)
|
||||
.series(eigval_series_zero);
|
||||
renderer.render(CONTAINER_ID, &chart).unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
view! {
|
||||
div(id=CONTAINER_ID, class="diagnostics-chart")
|
||||
}
|
||||
}
|
||||
|
||||
#[component(inline_props)]
|
||||
fn DiagnosticsPanel(name: &'static str, children: Children) -> View {
|
||||
let diagnostics_state = use_context::<DiagnosticsState>();
|
||||
view! {
|
||||
div(
|
||||
class="diagnostics-panel",
|
||||
"hidden"=diagnostics_state.active_tab.with(
|
||||
|active_tab| {
|
||||
if active_tab == name {
|
||||
None
|
||||
} else {
|
||||
Some("")
|
||||
}
|
||||
}
|
||||
)
|
||||
) {
|
||||
(children)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Diagnostics() -> View {
|
||||
let diagnostics_state = DiagnosticsState::new("loss".to_string());
|
||||
let active_tab = diagnostics_state.active_tab.clone();
|
||||
provide_context(diagnostics_state);
|
||||
|
||||
view! {
|
||||
div(id="diagnostics") {
|
||||
div(id="diagnostics-bar") {
|
||||
RealizationStatus {}
|
||||
select(bind:value=active_tab) {
|
||||
option(value="loss") { "Loss" }
|
||||
option(value="spectrum") { "Spectrum" }
|
||||
}
|
||||
}
|
||||
DiagnosticsPanel(name="loss") { LossHistory {} }
|
||||
DiagnosticsPanel(name="spectrum") { SpectrumHistory {} }
|
||||
}
|
||||
}
|
||||
}
|
900
app-proto/src/components/display.rs
Normal file
900
app-proto/src/components/display.rs
Normal file
|
@ -0,0 +1,900 @@
|
|||
use core::array;
|
||||
use nalgebra::{DMatrix, DVector, Rotation3, Vector3};
|
||||
use std::rc::Rc;
|
||||
use sycamore::{prelude::*, motion::create_raf};
|
||||
use web_sys::{
|
||||
console,
|
||||
window,
|
||||
KeyboardEvent,
|
||||
MouseEvent,
|
||||
WebGl2RenderingContext,
|
||||
WebGlBuffer,
|
||||
WebGlProgram,
|
||||
WebGlShader,
|
||||
WebGlUniformLocation,
|
||||
wasm_bindgen::{JsCast, JsValue}
|
||||
};
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
assembly::{Element, ElementColor, ElementMotion, Point, Sphere}
|
||||
};
|
||||
|
||||
// --- color ---
|
||||
|
||||
const COLOR_SIZE: usize = 3;
|
||||
type ColorWithOpacity = [f32; COLOR_SIZE + 1];
|
||||
|
||||
fn combine_channels(color: ElementColor, opacity: f32) -> ColorWithOpacity {
|
||||
let mut color_with_opacity = [0.0; COLOR_SIZE + 1];
|
||||
color_with_opacity[..COLOR_SIZE].copy_from_slice(&color);
|
||||
color_with_opacity[COLOR_SIZE] = opacity;
|
||||
color_with_opacity
|
||||
}
|
||||
|
||||
// --- scene data ---
|
||||
|
||||
struct SceneSpheres {
|
||||
representations: Vec<DVector<f64>>,
|
||||
colors_with_opacity: Vec<ColorWithOpacity>,
|
||||
highlights: Vec<f32>
|
||||
}
|
||||
|
||||
impl SceneSpheres {
|
||||
fn new() -> SceneSpheres{
|
||||
SceneSpheres {
|
||||
representations: Vec::new(),
|
||||
colors_with_opacity: Vec::new(),
|
||||
highlights: Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn len_i32(&self) -> i32 {
|
||||
self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer")
|
||||
}
|
||||
|
||||
fn push(&mut self, representation: DVector<f64>, color: ElementColor, opacity: f32, highlight: f32) {
|
||||
self.representations.push(representation);
|
||||
self.colors_with_opacity.push(combine_channels(color, opacity));
|
||||
self.highlights.push(highlight);
|
||||
}
|
||||
}
|
||||
|
||||
struct ScenePoints {
|
||||
representations: Vec<DVector<f64>>,
|
||||
colors_with_opacity: Vec<ColorWithOpacity>,
|
||||
highlights: Vec<f32>,
|
||||
selections: Vec<f32>
|
||||
}
|
||||
|
||||
impl ScenePoints {
|
||||
fn new() -> ScenePoints {
|
||||
ScenePoints {
|
||||
representations: Vec::new(),
|
||||
colors_with_opacity: Vec::new(),
|
||||
highlights: Vec::new(),
|
||||
selections: Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, representation: DVector<f64>, color: ElementColor, opacity: f32, highlight: f32, selected: bool) {
|
||||
self.representations.push(representation);
|
||||
self.colors_with_opacity.push(combine_channels(color, opacity));
|
||||
self.highlights.push(highlight);
|
||||
self.selections.push(if selected { 1.0 } else { 0.0 });
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Scene {
|
||||
spheres: SceneSpheres,
|
||||
points: ScenePoints
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
fn new() -> Scene {
|
||||
Scene {
|
||||
spheres: SceneSpheres::new(),
|
||||
points: ScenePoints::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DisplayItem {
|
||||
fn show(&self, scene: &mut Scene, selected: bool);
|
||||
|
||||
// the smallest positive depth, represented as a multiple of `dir`, where
|
||||
// the line generated by `dir` hits the element. returns `None` if the line
|
||||
// misses the element
|
||||
fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, pixel_size: f64) -> Option<f64>;
|
||||
}
|
||||
|
||||
impl DisplayItem for Sphere {
|
||||
fn show(&self, scene: &mut Scene, selected: bool) {
|
||||
/* SCAFFOLDING */
|
||||
const DEFAULT_OPACITY: f32 = 0.5;
|
||||
const GHOST_OPACITY: f32 = 0.2;
|
||||
const HIGHLIGHT: f32 = 0.2;
|
||||
|
||||
let representation = self.representation.get_clone_untracked();
|
||||
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
|
||||
let opacity = if self.ghost.get() { GHOST_OPACITY } else { DEFAULT_OPACITY };
|
||||
let highlight = if selected { 1.0 } else { HIGHLIGHT };
|
||||
scene.spheres.push(representation, color, opacity, highlight);
|
||||
}
|
||||
|
||||
// this method should be kept synchronized with `sphere_cast` in
|
||||
// `spheres.frag`, which does essentially the same thing on the GPU side
|
||||
fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, _pixel_size: f64) -> Option<f64> {
|
||||
// if `a/b` is less than this threshold, we approximate
|
||||
// `a*u^2 + b*u + c` by the linear function `b*u + c`
|
||||
const DEG_THRESHOLD: f64 = 1e-9;
|
||||
|
||||
let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
|
||||
let a = -rep[3] * dir.norm_squared();
|
||||
let b = rep.rows_range(..3).dot(&dir);
|
||||
let c = -rep[4];
|
||||
|
||||
let adjust = 4.0*a*c/(b*b);
|
||||
if adjust < 1.0 {
|
||||
// as long as `b` is non-zero, the linear approximation of
|
||||
//
|
||||
// a*u^2 + b*u + c
|
||||
//
|
||||
// at `u = 0` will reach zero at a finite depth `u_lin`. the root of
|
||||
// the quadratic adjacent to `u_lin` is stored in `lin_root`. if
|
||||
// both roots have the same sign, `lin_root` will be the one closer
|
||||
// to `u = 0`
|
||||
let square_rect_ratio = 1.0 + (1.0 - adjust).sqrt();
|
||||
let lin_root = -(2.0*c)/b / square_rect_ratio;
|
||||
if a.abs() > DEG_THRESHOLD * b.abs() {
|
||||
if lin_root > 0.0 {
|
||||
Some(lin_root)
|
||||
} else {
|
||||
let other_root = -b/(2.*a) * square_rect_ratio;
|
||||
(other_root > 0.0).then_some(other_root)
|
||||
}
|
||||
} else {
|
||||
(lin_root > 0.0).then_some(lin_root)
|
||||
}
|
||||
} else {
|
||||
// the line through `dir` misses the sphere completely
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayItem for Point {
|
||||
fn show(&self, scene: &mut Scene, selected: bool) {
|
||||
/* SCAFFOLDING */
|
||||
const GHOST_OPACITY: f32 = 0.4;
|
||||
const HIGHLIGHT: f32 = 0.5;
|
||||
|
||||
let representation = self.representation.get_clone_untracked();
|
||||
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
|
||||
let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 };
|
||||
let highlight = if selected { 1.0 } else { HIGHLIGHT };
|
||||
scene.points.push(representation, color, opacity, highlight, selected);
|
||||
}
|
||||
|
||||
/* SCAFFOLDING */
|
||||
fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, pixel_size: f64) -> Option<f64> {
|
||||
let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
|
||||
if rep[2] < 0.0 {
|
||||
// this constant should be kept synchronized with `point.frag`
|
||||
const POINT_RADIUS_PX: f64 = 4.0;
|
||||
|
||||
// find the radius of the point in screen projection units
|
||||
let point_radius_proj = POINT_RADIUS_PX * pixel_size;
|
||||
|
||||
// find the squared distance between the screen projections of the
|
||||
// ray and the point
|
||||
let dir_proj = -dir.fixed_rows::<2>(0) / dir[2];
|
||||
let rep_proj = -rep.fixed_rows::<2>(0) / rep[2];
|
||||
let dist_sq = (dir_proj - rep_proj).norm_squared();
|
||||
|
||||
// if the ray hits the point, return its depth
|
||||
if dist_sq < point_radius_proj * point_radius_proj {
|
||||
Some(rep[2] / dir[2])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- WebGL utilities ---
|
||||
|
||||
fn compile_shader(
|
||||
context: &WebGl2RenderingContext,
|
||||
shader_type: u32,
|
||||
source: &str,
|
||||
) -> WebGlShader {
|
||||
let shader = context.create_shader(shader_type).unwrap();
|
||||
context.shader_source(&shader, source);
|
||||
context.compile_shader(&shader);
|
||||
shader
|
||||
}
|
||||
|
||||
fn set_up_program(
|
||||
context: &WebGl2RenderingContext,
|
||||
vertex_shader_source: &str,
|
||||
fragment_shader_source: &str
|
||||
) -> WebGlProgram {
|
||||
// compile the shaders
|
||||
let vertex_shader = compile_shader(
|
||||
&context,
|
||||
WebGl2RenderingContext::VERTEX_SHADER,
|
||||
vertex_shader_source,
|
||||
);
|
||||
let fragment_shader = compile_shader(
|
||||
&context,
|
||||
WebGl2RenderingContext::FRAGMENT_SHADER,
|
||||
fragment_shader_source,
|
||||
);
|
||||
|
||||
// create the program and attach the shaders
|
||||
let program = context.create_program().unwrap();
|
||||
context.attach_shader(&program, &vertex_shader);
|
||||
context.attach_shader(&program, &fragment_shader);
|
||||
context.link_program(&program);
|
||||
|
||||
/* DEBUG */
|
||||
// report whether linking succeeded
|
||||
let link_status = context
|
||||
.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
|
||||
.as_bool()
|
||||
.unwrap();
|
||||
let link_msg = if link_status {
|
||||
"Linked successfully"
|
||||
} else {
|
||||
"Linking failed"
|
||||
};
|
||||
console::log_1(&JsValue::from(link_msg));
|
||||
|
||||
program
|
||||
}
|
||||
|
||||
fn get_uniform_array_locations<const N: usize>(
|
||||
context: &WebGl2RenderingContext,
|
||||
program: &WebGlProgram,
|
||||
var_name: &str,
|
||||
member_name_opt: Option<&str>
|
||||
) -> [Option<WebGlUniformLocation>; N] {
|
||||
array::from_fn(|n| {
|
||||
let name = match member_name_opt {
|
||||
Some(member_name) => format!("{var_name}[{n}].{member_name}"),
|
||||
None => format!("{var_name}[{n}]")
|
||||
};
|
||||
context.get_uniform_location(&program, name.as_str())
|
||||
})
|
||||
}
|
||||
|
||||
// bind the given vertex buffer object to the given vertex attribute
|
||||
fn bind_to_attribute(
|
||||
context: &WebGl2RenderingContext,
|
||||
attr_index: u32,
|
||||
attr_size: i32,
|
||||
buffer: &Option<WebGlBuffer>
|
||||
) {
|
||||
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
|
||||
context.vertex_attrib_pointer_with_i32(
|
||||
attr_index,
|
||||
attr_size,
|
||||
WebGl2RenderingContext::FLOAT,
|
||||
false, // don't normalize
|
||||
0, // zero stride
|
||||
0, // zero offset
|
||||
);
|
||||
}
|
||||
|
||||
// load the given data into a new vertex buffer object
|
||||
fn load_new_buffer(
|
||||
context: &WebGl2RenderingContext,
|
||||
data: &[f32]
|
||||
) -> Option<WebGlBuffer> {
|
||||
// create a buffer and bind it to ARRAY_BUFFER
|
||||
let buffer = context.create_buffer();
|
||||
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
|
||||
|
||||
// load the given data into the buffer. this block is unsafe because
|
||||
// `Float32Array::view` creates a raw view into our module's
|
||||
// `WebAssembly.Memory` buffer. allocating more memory will change the
|
||||
// buffer, invalidating the view, so we have to make sure we don't allocate
|
||||
// any memory until the view is dropped. we're okay here because the view is
|
||||
// used as soon as it's created
|
||||
unsafe {
|
||||
context.buffer_data_with_array_buffer_view(
|
||||
WebGl2RenderingContext::ARRAY_BUFFER,
|
||||
&js_sys::Float32Array::view(&data),
|
||||
WebGl2RenderingContext::STATIC_DRAW,
|
||||
);
|
||||
}
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
fn bind_new_buffer_to_attribute(
|
||||
context: &WebGl2RenderingContext,
|
||||
attr_index: u32,
|
||||
attr_size: i32,
|
||||
data: &[f32]
|
||||
) {
|
||||
let buffer = load_new_buffer(context, data);
|
||||
bind_to_attribute(context, attr_index, attr_size, &buffer);
|
||||
}
|
||||
|
||||
// the direction in camera space that a mouse event is pointing along
|
||||
fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
|
||||
let target: web_sys::Element = event.target().unwrap().unchecked_into();
|
||||
let rect = target.get_bounding_client_rect();
|
||||
let width = rect.width();
|
||||
let height = rect.height();
|
||||
let shortdim = width.min(height);
|
||||
|
||||
// this constant should be kept synchronized with `spheres.frag` and
|
||||
// `point.vert`
|
||||
const FOCAL_SLOPE: f64 = 0.3;
|
||||
|
||||
(
|
||||
Vector3::new(
|
||||
FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim,
|
||||
FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim,
|
||||
-1.0
|
||||
),
|
||||
FOCAL_SLOPE * 2.0 / shortdim
|
||||
)
|
||||
}
|
||||
|
||||
// --- display component ---
|
||||
|
||||
#[component]
|
||||
pub fn Display() -> View {
|
||||
let state = use_context::<AppState>();
|
||||
|
||||
// canvas
|
||||
let display = create_node_ref();
|
||||
|
||||
// viewpoint
|
||||
let assembly_to_world = create_signal(DMatrix::<f64>::identity(5, 5));
|
||||
|
||||
// navigation
|
||||
let pitch_up = create_signal(0.0);
|
||||
let pitch_down = create_signal(0.0);
|
||||
let yaw_right = create_signal(0.0);
|
||||
let yaw_left = create_signal(0.0);
|
||||
let roll_ccw = create_signal(0.0);
|
||||
let roll_cw = create_signal(0.0);
|
||||
let zoom_in = create_signal(0.0);
|
||||
let zoom_out = create_signal(0.0);
|
||||
let turntable = create_signal(false); /* BENCHMARKING */
|
||||
|
||||
// manipulation
|
||||
let translate_neg_x = create_signal(0.0);
|
||||
let translate_pos_x = create_signal(0.0);
|
||||
let translate_neg_y = create_signal(0.0);
|
||||
let translate_pos_y = create_signal(0.0);
|
||||
let translate_neg_z = create_signal(0.0);
|
||||
let translate_pos_z = create_signal(0.0);
|
||||
let shrink_neg = create_signal(0.0);
|
||||
let shrink_pos = create_signal(0.0);
|
||||
|
||||
// change listener
|
||||
let scene_changed = create_signal(true);
|
||||
create_effect(move || {
|
||||
state.assembly.elements.with(|elts| {
|
||||
for elt in elts {
|
||||
elt.representation().track();
|
||||
elt.ghost().track();
|
||||
}
|
||||
});
|
||||
state.selection.track();
|
||||
scene_changed.set(true);
|
||||
});
|
||||
|
||||
/* INSTRUMENTS */
|
||||
const SAMPLE_PERIOD: i32 = 60;
|
||||
let mut last_sample_time = 0.0;
|
||||
let mut frames_since_last_sample = 0;
|
||||
let mean_frame_interval = create_signal(0.0);
|
||||
|
||||
let assembly_for_raf = state.assembly.clone();
|
||||
on_mount(move || {
|
||||
// timing
|
||||
let mut last_time = 0.0;
|
||||
|
||||
// viewpoint
|
||||
const ROT_SPEED: f64 = 0.4; // in radians per second
|
||||
const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second
|
||||
const TURNTABLE_SPEED: f64 = 0.1; /* BENCHMARKING */
|
||||
let mut orientation = DMatrix::<f64>::identity(5, 5);
|
||||
let mut rotation = DMatrix::<f64>::identity(5, 5);
|
||||
let mut location_z: f64 = 5.0;
|
||||
|
||||
// manipulation
|
||||
const TRANSLATION_SPEED: f64 = 0.15; // in length units per second
|
||||
const SHRINKING_SPEED: f64 = 0.15; // in length units per second
|
||||
|
||||
// display parameters
|
||||
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
|
||||
const DEBUG_MODE: i32 = 0; /* DEBUG */
|
||||
|
||||
/* INSTRUMENTS */
|
||||
let performance = window().unwrap().performance().unwrap();
|
||||
|
||||
// get the display canvas
|
||||
let canvas = display.get().unchecked_into::<web_sys::HtmlCanvasElement>();
|
||||
let ctx = canvas
|
||||
.get_context("webgl2")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<WebGl2RenderingContext>()
|
||||
.unwrap();
|
||||
|
||||
// disable depth testing
|
||||
ctx.disable(WebGl2RenderingContext::DEPTH_TEST);
|
||||
|
||||
// set blend mode
|
||||
ctx.enable(WebGl2RenderingContext::BLEND);
|
||||
ctx.blend_func(WebGl2RenderingContext::SRC_ALPHA, WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// set up the sphere rendering program
|
||||
let sphere_program = set_up_program(
|
||||
&ctx,
|
||||
include_str!("identity.vert"),
|
||||
include_str!("spheres.frag")
|
||||
);
|
||||
|
||||
// set up the point rendering program
|
||||
let point_program = set_up_program(
|
||||
&ctx,
|
||||
include_str!("point.vert"),
|
||||
include_str!("point.frag")
|
||||
);
|
||||
|
||||
/* DEBUG */
|
||||
// print the maximum number of vectors that can be passed as
|
||||
// uniforms to a fragment shader. the OpenGL ES 3.0 standard
|
||||
// requires this maximum to be at least 224, as discussed in the
|
||||
// documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter
|
||||
// here:
|
||||
//
|
||||
// https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml
|
||||
//
|
||||
// there are also other size limits. for example, on Aaron's
|
||||
// machine, the the length of a float or genType array seems to be
|
||||
// capped at 1024 elements
|
||||
console::log_2(
|
||||
&ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(),
|
||||
&JsValue::from("uniform vectors available")
|
||||
);
|
||||
|
||||
// find the sphere program's vertex attribute
|
||||
let viewport_position_attr = ctx.get_attrib_location(&sphere_program, "position") as u32;
|
||||
|
||||
// find the sphere program's uniforms
|
||||
const SPHERE_MAX: usize = 200;
|
||||
let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt");
|
||||
let sphere_sp_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||
&ctx, &sphere_program, "sphere_list", Some("sp")
|
||||
);
|
||||
let sphere_lt_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||
&ctx, &sphere_program, "sphere_list", Some("lt")
|
||||
);
|
||||
let sphere_color_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||
&ctx, &sphere_program, "color_list", None
|
||||
);
|
||||
let sphere_highlight_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||
&ctx, &sphere_program, "highlight_list", None
|
||||
);
|
||||
let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution");
|
||||
let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim");
|
||||
let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold");
|
||||
let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode");
|
||||
|
||||
// load the viewport vertex positions into a new vertex buffer object
|
||||
const VERTEX_CNT: usize = 6;
|
||||
let viewport_positions: [f32; 3*VERTEX_CNT] = [
|
||||
// northwest triangle
|
||||
-1.0, -1.0, 0.0,
|
||||
-1.0, 1.0, 0.0,
|
||||
1.0, 1.0, 0.0,
|
||||
// southeast triangle
|
||||
-1.0, -1.0, 0.0,
|
||||
1.0, 1.0, 0.0,
|
||||
1.0, -1.0, 0.0
|
||||
];
|
||||
let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions);
|
||||
|
||||
// find the point program's vertex attributes
|
||||
let point_position_attr = ctx.get_attrib_location(&point_program, "position") as u32;
|
||||
let point_color_attr = ctx.get_attrib_location(&point_program, "color") as u32;
|
||||
let point_highlight_attr = ctx.get_attrib_location(&point_program, "highlight") as u32;
|
||||
let point_selection_attr = ctx.get_attrib_location(&point_program, "selected") as u32;
|
||||
|
||||
// set up a repainting routine
|
||||
let (_, start_animation_loop, _) = create_raf(move || {
|
||||
// get the time step
|
||||
let time = performance.now();
|
||||
let time_step = 0.001*(time - last_time);
|
||||
last_time = time;
|
||||
|
||||
// get the navigation state
|
||||
let pitch_up_val = pitch_up.get();
|
||||
let pitch_down_val = pitch_down.get();
|
||||
let yaw_right_val = yaw_right.get();
|
||||
let yaw_left_val = yaw_left.get();
|
||||
let roll_ccw_val = roll_ccw.get();
|
||||
let roll_cw_val = roll_cw.get();
|
||||
let zoom_in_val = zoom_in.get();
|
||||
let zoom_out_val = zoom_out.get();
|
||||
let turntable_val = turntable.get(); /* BENCHMARKING */
|
||||
|
||||
// get the manipulation state
|
||||
let translate_neg_x_val = translate_neg_x.get();
|
||||
let translate_pos_x_val = translate_pos_x.get();
|
||||
let translate_neg_y_val = translate_neg_y.get();
|
||||
let translate_pos_y_val = translate_pos_y.get();
|
||||
let translate_neg_z_val = translate_neg_z.get();
|
||||
let translate_pos_z_val = translate_pos_z.get();
|
||||
let shrink_neg_val = shrink_neg.get();
|
||||
let shrink_pos_val = shrink_pos.get();
|
||||
|
||||
// update the assembly's orientation
|
||||
let ang_vel = {
|
||||
let pitch = pitch_up_val - pitch_down_val;
|
||||
let yaw = yaw_right_val - yaw_left_val;
|
||||
let roll = roll_ccw_val - roll_cw_val;
|
||||
if pitch != 0.0 || yaw != 0.0 || roll != 0.0 {
|
||||
ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize()
|
||||
} else {
|
||||
Vector3::zeros()
|
||||
}
|
||||
} /* BENCHMARKING */ + if turntable_val {
|
||||
Vector3::new(0.0, TURNTABLE_SPEED, 0.0)
|
||||
} else {
|
||||
Vector3::zeros()
|
||||
};
|
||||
let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0);
|
||||
rotation_sp.copy_from(
|
||||
Rotation3::from_scaled_axis(time_step * ang_vel).matrix()
|
||||
);
|
||||
orientation = &rotation * &orientation;
|
||||
|
||||
// update the assembly's location
|
||||
let zoom = zoom_out_val - zoom_in_val;
|
||||
location_z *= (time_step * ZOOM_SPEED * zoom).exp();
|
||||
|
||||
// manipulate the assembly
|
||||
if state.selection.with(|sel| sel.len() == 1) {
|
||||
let sel = state.selection.with(
|
||||
|sel| sel.into_iter().next().unwrap().clone()
|
||||
);
|
||||
let translate_x = translate_pos_x_val - translate_neg_x_val;
|
||||
let translate_y = translate_pos_y_val - translate_neg_y_val;
|
||||
let translate_z = translate_pos_z_val - translate_neg_z_val;
|
||||
let shrink = shrink_pos_val - shrink_neg_val;
|
||||
let translating =
|
||||
translate_x != 0.0
|
||||
|| translate_y != 0.0
|
||||
|| translate_z != 0.0;
|
||||
if translating || shrink != 0.0 {
|
||||
let elt_motion = {
|
||||
let u = if translating {
|
||||
TRANSLATION_SPEED * Vector3::new(
|
||||
translate_x, translate_y, translate_z
|
||||
).normalize()
|
||||
} else {
|
||||
Vector3::zeros()
|
||||
};
|
||||
time_step * DVector::from_column_slice(
|
||||
&[u[0], u[1], u[2], SHRINKING_SPEED * shrink]
|
||||
)
|
||||
};
|
||||
assembly_for_raf.deform(
|
||||
vec![
|
||||
ElementMotion {
|
||||
element: sel,
|
||||
velocity: elt_motion.as_view()
|
||||
}
|
||||
]
|
||||
);
|
||||
scene_changed.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
if scene_changed.get() {
|
||||
const SPACE_DIM: usize = 3;
|
||||
const COLOR_SIZE: usize = 3;
|
||||
|
||||
/* INSTRUMENTS */
|
||||
// measure mean frame interval
|
||||
frames_since_last_sample += 1;
|
||||
if frames_since_last_sample >= SAMPLE_PERIOD {
|
||||
mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64));
|
||||
last_sample_time = time;
|
||||
frames_since_last_sample = 0;
|
||||
}
|
||||
|
||||
// --- get the assembly ---
|
||||
|
||||
let mut scene = Scene::new();
|
||||
|
||||
// find the map from assembly space to world space
|
||||
let location = {
|
||||
let u = -location_z;
|
||||
DMatrix::from_column_slice(5, 5, &[
|
||||
1.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0, u,
|
||||
0.0, 0.0, 2.0*u, 1.0, u*u,
|
||||
0.0, 0.0, 0.0, 0.0, 1.0
|
||||
])
|
||||
};
|
||||
let asm_to_world = &location * &orientation;
|
||||
|
||||
// set up the scene
|
||||
state.assembly.elements.with_untracked(
|
||||
|elts| for elt in elts {
|
||||
let selected = state.selection.with(|sel| sel.contains(elt));
|
||||
elt.show(&mut scene, selected);
|
||||
}
|
||||
);
|
||||
let sphere_cnt = scene.spheres.len_i32();
|
||||
|
||||
// --- draw the spheres ---
|
||||
|
||||
// use the sphere rendering program
|
||||
ctx.use_program(Some(&sphere_program));
|
||||
|
||||
// enable the sphere program's vertex attribute
|
||||
ctx.enable_vertex_attrib_array(viewport_position_attr);
|
||||
|
||||
// write the spheres in world coordinates
|
||||
let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map(
|
||||
|rep| (&asm_to_world * rep).cast::<f32>()
|
||||
).collect();
|
||||
|
||||
// set the resolution
|
||||
let width = canvas.width() as f32;
|
||||
let height = canvas.height() as f32;
|
||||
ctx.uniform2f(resolution_loc.as_ref(), width, height);
|
||||
ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
|
||||
|
||||
// pass the scene data
|
||||
ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt);
|
||||
for n in 0..sphere_reps_world.len() {
|
||||
let v = &sphere_reps_world[n];
|
||||
ctx.uniform3fv_with_f32_array(
|
||||
sphere_sp_locs[n].as_ref(),
|
||||
v.rows(0, 3).as_slice()
|
||||
);
|
||||
ctx.uniform2fv_with_f32_array(
|
||||
sphere_lt_locs[n].as_ref(),
|
||||
v.rows(3, 2).as_slice()
|
||||
);
|
||||
ctx.uniform4fv_with_f32_array(
|
||||
sphere_color_locs[n].as_ref(),
|
||||
&scene.spheres.colors_with_opacity[n]
|
||||
);
|
||||
ctx.uniform1f(
|
||||
sphere_highlight_locs[n].as_ref(),
|
||||
scene.spheres.highlights[n]
|
||||
);
|
||||
}
|
||||
|
||||
// pass the display parameters
|
||||
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
|
||||
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
|
||||
|
||||
// bind the viewport vertex position buffer to the position
|
||||
// attribute in the vertex shader
|
||||
bind_to_attribute(&ctx, viewport_position_attr, SPACE_DIM as i32, &viewport_position_buffer);
|
||||
|
||||
// draw the scene
|
||||
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
|
||||
|
||||
// disable the sphere program's vertex attribute
|
||||
ctx.disable_vertex_attrib_array(viewport_position_attr);
|
||||
|
||||
// --- draw the points ---
|
||||
|
||||
if !scene.points.representations.is_empty() {
|
||||
// use the point rendering program
|
||||
ctx.use_program(Some(&point_program));
|
||||
|
||||
// enable the point program's vertex attributes
|
||||
ctx.enable_vertex_attrib_array(point_position_attr);
|
||||
ctx.enable_vertex_attrib_array(point_color_attr);
|
||||
ctx.enable_vertex_attrib_array(point_highlight_attr);
|
||||
ctx.enable_vertex_attrib_array(point_selection_attr);
|
||||
|
||||
// write the points in world coordinates
|
||||
let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM);
|
||||
let point_positions = DMatrix::from_columns(
|
||||
&scene.points.representations.into_iter().map(
|
||||
|rep| &asm_to_world_sp * rep
|
||||
).collect::<Vec<_>>().as_slice()
|
||||
).cast::<f32>();
|
||||
|
||||
// load the point positions and colors into new buffers and
|
||||
// bind them to the corresponding attributes in the vertex
|
||||
// shader
|
||||
bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice());
|
||||
bind_new_buffer_to_attribute(&ctx, point_color_attr, (COLOR_SIZE + 1) as i32, scene.points.colors_with_opacity.concat().as_slice());
|
||||
bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice());
|
||||
bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice());
|
||||
|
||||
// draw the scene
|
||||
ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32);
|
||||
|
||||
// disable the point program's vertex attributes
|
||||
ctx.disable_vertex_attrib_array(point_position_attr);
|
||||
ctx.disable_vertex_attrib_array(point_color_attr);
|
||||
ctx.disable_vertex_attrib_array(point_highlight_attr);
|
||||
ctx.disable_vertex_attrib_array(point_selection_attr);
|
||||
}
|
||||
|
||||
// --- update the display state ---
|
||||
|
||||
// update the viewpoint
|
||||
assembly_to_world.set(asm_to_world);
|
||||
|
||||
// clear the scene change flag
|
||||
scene_changed.set(
|
||||
pitch_up_val != 0.0
|
||||
|| pitch_down_val != 0.0
|
||||
|| yaw_left_val != 0.0
|
||||
|| yaw_right_val != 0.0
|
||||
|| roll_cw_val != 0.0
|
||||
|| roll_ccw_val != 0.0
|
||||
|| zoom_in_val != 0.0
|
||||
|| zoom_out_val != 0.0
|
||||
|| turntable_val /* BENCHMARKING */
|
||||
);
|
||||
} else {
|
||||
frames_since_last_sample = 0;
|
||||
mean_frame_interval.set(-1.0);
|
||||
}
|
||||
});
|
||||
start_animation_loop();
|
||||
});
|
||||
|
||||
let set_nav_signal = move |event: &KeyboardEvent, value: f64| {
|
||||
let mut navigating = true;
|
||||
let shift = event.shift_key();
|
||||
match event.key().as_str() {
|
||||
"ArrowUp" if shift => zoom_in.set(value),
|
||||
"ArrowDown" if shift => zoom_out.set(value),
|
||||
"ArrowUp" => pitch_up.set(value),
|
||||
"ArrowDown" => pitch_down.set(value),
|
||||
"ArrowRight" if shift => roll_cw.set(value),
|
||||
"ArrowLeft" if shift => roll_ccw.set(value),
|
||||
"ArrowRight" => yaw_right.set(value),
|
||||
"ArrowLeft" => yaw_left.set(value),
|
||||
_ => navigating = false
|
||||
};
|
||||
if navigating {
|
||||
scene_changed.set(true);
|
||||
event.prevent_default();
|
||||
}
|
||||
};
|
||||
|
||||
let set_manip_signal = move |event: &KeyboardEvent, value: f64| {
|
||||
let mut manipulating = true;
|
||||
let shift = event.shift_key();
|
||||
match event.key().as_str() {
|
||||
"d" | "D" => translate_pos_x.set(value),
|
||||
"a" | "A" => translate_neg_x.set(value),
|
||||
"w" | "W" if shift => translate_neg_z.set(value),
|
||||
"s" | "S" if shift => translate_pos_z.set(value),
|
||||
"w" | "W" => translate_pos_y.set(value),
|
||||
"s" | "S" => translate_neg_y.set(value),
|
||||
"]" | "}" => shrink_neg.set(value),
|
||||
"[" | "{" => shrink_pos.set(value),
|
||||
_ => manipulating = false
|
||||
};
|
||||
if manipulating {
|
||||
event.prevent_default();
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
/* TO DO */
|
||||
// switch back to integer-valued parameters when that becomes possible
|
||||
// again
|
||||
canvas(
|
||||
ref=display,
|
||||
id="display",
|
||||
width="600",
|
||||
height="600",
|
||||
tabindex="0",
|
||||
on:keydown=move |event: KeyboardEvent| {
|
||||
if event.key() == "Shift" {
|
||||
// swap navigation inputs
|
||||
roll_cw.set(yaw_right.get());
|
||||
roll_ccw.set(yaw_left.get());
|
||||
zoom_in.set(pitch_up.get());
|
||||
zoom_out.set(pitch_down.get());
|
||||
yaw_right.set(0.0);
|
||||
yaw_left.set(0.0);
|
||||
pitch_up.set(0.0);
|
||||
pitch_down.set(0.0);
|
||||
|
||||
// swap manipulation inputs
|
||||
translate_pos_z.set(translate_neg_y.get());
|
||||
translate_neg_z.set(translate_pos_y.get());
|
||||
translate_pos_y.set(0.0);
|
||||
translate_neg_y.set(0.0);
|
||||
} else {
|
||||
if event.key() == "Enter" { /* BENCHMARKING */
|
||||
turntable.set_fn(|turn| !turn);
|
||||
scene_changed.set(true);
|
||||
}
|
||||
set_nav_signal(&event, 1.0);
|
||||
set_manip_signal(&event, 1.0);
|
||||
}
|
||||
},
|
||||
on:keyup=move |event: KeyboardEvent| {
|
||||
if event.key() == "Shift" {
|
||||
// swap navigation inputs
|
||||
yaw_right.set(roll_cw.get());
|
||||
yaw_left.set(roll_ccw.get());
|
||||
pitch_up.set(zoom_in.get());
|
||||
pitch_down.set(zoom_out.get());
|
||||
roll_cw.set(0.0);
|
||||
roll_ccw.set(0.0);
|
||||
zoom_in.set(0.0);
|
||||
zoom_out.set(0.0);
|
||||
|
||||
// swap manipulation inputs
|
||||
translate_pos_y.set(translate_neg_z.get());
|
||||
translate_neg_y.set(translate_pos_z.get());
|
||||
translate_pos_z.set(0.0);
|
||||
translate_neg_z.set(0.0);
|
||||
} else {
|
||||
set_nav_signal(&event, 0.0);
|
||||
set_manip_signal(&event, 0.0);
|
||||
}
|
||||
},
|
||||
on:blur=move |_| {
|
||||
pitch_up.set(0.0);
|
||||
pitch_down.set(0.0);
|
||||
yaw_right.set(0.0);
|
||||
yaw_left.set(0.0);
|
||||
roll_ccw.set(0.0);
|
||||
roll_cw.set(0.0);
|
||||
},
|
||||
on:click=move |event: MouseEvent| {
|
||||
// find the nearest element along the pointer direction
|
||||
let (dir, pixel_size) = event_dir(&event);
|
||||
console::log_1(&JsValue::from(dir.to_string()));
|
||||
let mut clicked: Option<(Rc<dyn Element>, f64)> = None;
|
||||
let tangible_elts = state.assembly.elements
|
||||
.get_clone_untracked()
|
||||
.into_iter()
|
||||
.filter(|elt| !elt.ghost().get());
|
||||
for elt in tangible_elts {
|
||||
match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) {
|
||||
Some(depth) => match clicked {
|
||||
Some((_, best_depth)) => {
|
||||
if depth < best_depth {
|
||||
clicked = Some((elt, depth))
|
||||
}
|
||||
},
|
||||
None => clicked = Some((elt, depth))
|
||||
}
|
||||
None => ()
|
||||
};
|
||||
}
|
||||
|
||||
// if we clicked something, select it
|
||||
match clicked {
|
||||
Some((elt, _)) => state.select(&elt, event.shift_key()),
|
||||
None => state.selection.update(|sel| sel.clear())
|
||||
};
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
264
app-proto/src/components/outline.rs
Normal file
264
app-proto/src/components/outline.rs
Normal file
|
@ -0,0 +1,264 @@
|
|||
use itertools::Itertools;
|
||||
use std::rc::Rc;
|
||||
use sycamore::prelude::*;
|
||||
use web_sys::{
|
||||
KeyboardEvent,
|
||||
MouseEvent,
|
||||
wasm_bindgen::JsCast
|
||||
};
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
assembly::{
|
||||
Element,
|
||||
HalfCurvatureRegulator,
|
||||
InversiveDistanceRegulator,
|
||||
Regulator
|
||||
},
|
||||
specified::SpecifiedValue
|
||||
};
|
||||
|
||||
// an editable view of a regulator
|
||||
#[component(inline_props)]
|
||||
fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
|
||||
// get the regulator's measurement and set point signals
|
||||
let measurement = regulator.measurement();
|
||||
let set_point = regulator.set_point();
|
||||
|
||||
// the `valid` signal tracks whether the last entered value is a valid set
|
||||
// point specification
|
||||
let valid = create_signal(true);
|
||||
|
||||
// the `value` signal holds the current set point specification
|
||||
let value = create_signal(
|
||||
set_point.with_untracked(|set_pt| set_pt.spec.clone())
|
||||
);
|
||||
|
||||
// this `reset_value` closure resets the input value to the regulator's set
|
||||
// point specification
|
||||
let reset_value = move || {
|
||||
batch(|| {
|
||||
valid.set(true);
|
||||
value.set(set_point.with(|set_pt| set_pt.spec.clone()));
|
||||
})
|
||||
};
|
||||
|
||||
// reset the input value whenever the regulator's set point specification
|
||||
// is updated
|
||||
create_effect(reset_value);
|
||||
|
||||
view! {
|
||||
input(
|
||||
r#type="text",
|
||||
class=move || {
|
||||
if valid.get() {
|
||||
set_point.with(|set_pt| {
|
||||
if set_pt.is_present() {
|
||||
"regulator-input constraint"
|
||||
} else {
|
||||
"regulator-input"
|
||||
}
|
||||
})
|
||||
} else {
|
||||
"regulator-input invalid"
|
||||
}
|
||||
},
|
||||
placeholder=measurement.with(|result| result.to_string()),
|
||||
bind:value=value,
|
||||
on:change=move |_| {
|
||||
valid.set(
|
||||
match SpecifiedValue::try_from(value.get_clone_untracked()) {
|
||||
Ok(set_pt) => {
|
||||
set_point.set(set_pt);
|
||||
true
|
||||
}
|
||||
Err(_) => false
|
||||
}
|
||||
)
|
||||
},
|
||||
on:keydown={
|
||||
move |event: KeyboardEvent| {
|
||||
match event.key().as_str() {
|
||||
"Escape" => reset_value(),
|
||||
_ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OutlineItem {
|
||||
fn outline_item(self: Rc<Self>, element: &Rc<dyn Element>) -> View;
|
||||
}
|
||||
|
||||
impl OutlineItem for InversiveDistanceRegulator {
|
||||
fn outline_item(self: Rc<Self>, element: &Rc<dyn Element>) -> View {
|
||||
let other_subject_label = if self.subjects[0] == element.clone() {
|
||||
self.subjects[1].label()
|
||||
} else {
|
||||
self.subjects[0].label()
|
||||
}.clone();
|
||||
view! {
|
||||
li(class="regulator") {
|
||||
div(class="regulator-label") { (other_subject_label) }
|
||||
div(class="regulator-type") { "Inversive distance" }
|
||||
RegulatorInput(regulator=self)
|
||||
div(class="status")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OutlineItem for HalfCurvatureRegulator {
|
||||
fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View {
|
||||
view! {
|
||||
li(class="regulator") {
|
||||
div(class="regulator-label") // for spacing
|
||||
div(class="regulator-type") { "Half-curvature" }
|
||||
RegulatorInput(regulator=self)
|
||||
div(class="status")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a list item that shows an element in an outline view of an assembly
|
||||
#[component(inline_props)]
|
||||
fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
|
||||
let state = use_context::<AppState>();
|
||||
let class = {
|
||||
let element_for_class = element.clone();
|
||||
state.selection.map(
|
||||
move |sel| if sel.contains(&element_for_class) { "selected" } else { "" }
|
||||
)
|
||||
};
|
||||
let label = element.label().clone();
|
||||
let representation = element.representation().clone();
|
||||
let rep_components = move || {
|
||||
representation.with(
|
||||
|rep| rep.iter().map(
|
||||
|u| {
|
||||
let u_str = format!("{:.3}", u).replace("-", "\u{2212}");
|
||||
view! { div { (u_str) } }
|
||||
}
|
||||
).collect::<Vec<_>>()
|
||||
)
|
||||
};
|
||||
let regulated = element.regulators().map(|regs| regs.len() > 0);
|
||||
let regulator_list = element.regulators().map(
|
||||
|regs| regs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.sorted_by_key(|reg| reg.subjects().len())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
let details_node = create_node_ref();
|
||||
view! {
|
||||
li {
|
||||
details(ref=details_node) {
|
||||
summary(
|
||||
class=class.get(),
|
||||
on:keydown={
|
||||
let element_for_handler = element.clone();
|
||||
move |event: KeyboardEvent| {
|
||||
match event.key().as_str() {
|
||||
"Enter" => {
|
||||
state.select(&element_for_handler, event.shift_key());
|
||||
event.prevent_default();
|
||||
},
|
||||
"ArrowRight" if regulated.get() => {
|
||||
let _ = details_node
|
||||
.get()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.set_attribute("open", "");
|
||||
},
|
||||
"ArrowLeft" => {
|
||||
let _ = details_node
|
||||
.get()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.remove_attribute("open");
|
||||
},
|
||||
_ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
div(
|
||||
class="element-switch",
|
||||
on:click=|event: MouseEvent| event.stop_propagation()
|
||||
)
|
||||
div(
|
||||
class="element",
|
||||
on:click={
|
||||
let state_for_handler = state.clone();
|
||||
let element_for_handler = element.clone();
|
||||
move |event: MouseEvent| {
|
||||
state_for_handler.select(&element_for_handler, event.shift_key());
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
) {
|
||||
div(class="element-label") { (label) }
|
||||
div(class="element-representation") { (rep_components) }
|
||||
input(
|
||||
r#type="checkbox",
|
||||
bind:checked=element.ghost(),
|
||||
on:click=|event: MouseEvent| event.stop_propagation()
|
||||
)
|
||||
}
|
||||
}
|
||||
ul(class="regulators") {
|
||||
Keyed(
|
||||
list=regulator_list,
|
||||
view=move |reg| reg.outline_item(&element),
|
||||
key=|reg| reg.serial()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a component that lists the elements of the current assembly, showing each
|
||||
// element's regulators in a collapsible sub-list. its implementation is based
|
||||
// on Kate Morley's HTML + CSS tree views:
|
||||
//
|
||||
// https://iamkate.com/code/tree-views/
|
||||
//
|
||||
#[component]
|
||||
pub fn Outline() -> View {
|
||||
let state = use_context::<AppState>();
|
||||
|
||||
// list the elements alphabetically by ID
|
||||
/* TO DO */
|
||||
// this code is designed to generalize easily to other sort keys. if we only
|
||||
// ever wanted to sort by ID, we could do that more simply using the
|
||||
// `elements_by_id` index
|
||||
let element_list = state.assembly.elements.map(
|
||||
|elts| elts
|
||||
.clone()
|
||||
.into_iter()
|
||||
.sorted_by_key(|elt| elt.id().clone())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
view! {
|
||||
ul(
|
||||
id="outline",
|
||||
on:click={
|
||||
let state = use_context::<AppState>();
|
||||
move |_| state.selection.update(|sel| sel.clear())
|
||||
}
|
||||
) {
|
||||
Keyed(
|
||||
list=element_list,
|
||||
view=|elt| view! {
|
||||
ElementOutlineItem(element=elt)
|
||||
},
|
||||
key=|elt| elt.serial()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
19
app-proto/src/components/point.frag
Normal file
19
app-proto/src/components/point.frag
Normal file
|
@ -0,0 +1,19 @@
|
|||
#version 300 es
|
||||
|
||||
precision highp float;
|
||||
|
||||
in vec4 point_color;
|
||||
in float point_highlight;
|
||||
in float total_radius;
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
float r = total_radius * length(2.*gl_PointCoord - vec2(1.));
|
||||
|
||||
const float POINT_RADIUS = 4.;
|
||||
float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r);
|
||||
float disk = 1. - smoothstep(total_radius - 1., total_radius, r);
|
||||
vec4 color = mix(point_color, vec4(1.), border * point_highlight);
|
||||
outColor = vec4(vec3(1.), disk) * color;
|
||||
}
|
24
app-proto/src/components/point.vert
Normal file
24
app-proto/src/components/point.vert
Normal file
|
@ -0,0 +1,24 @@
|
|||
#version 300 es
|
||||
|
||||
in vec4 position;
|
||||
in vec4 color;
|
||||
in float highlight;
|
||||
in float selected;
|
||||
|
||||
out vec4 point_color;
|
||||
out float point_highlight;
|
||||
out float total_radius;
|
||||
|
||||
// camera
|
||||
const float focal_slope = 0.3;
|
||||
|
||||
void main() {
|
||||
total_radius = 5. + 0.5*selected;
|
||||
|
||||
float depth = -focal_slope * position.z;
|
||||
gl_Position = vec4(position.xy / depth, 0., 1.);
|
||||
gl_PointSize = 2.*total_radius;
|
||||
|
||||
point_color = color;
|
||||
point_highlight = highlight;
|
||||
}
|
|
@ -17,7 +17,7 @@ struct vecInv {
|
|||
const int SPHERE_MAX = 200;
|
||||
uniform int sphere_cnt;
|
||||
uniform vecInv sphere_list[SPHERE_MAX];
|
||||
uniform vec3 color_list[SPHERE_MAX];
|
||||
uniform vec4 color_list[SPHERE_MAX];
|
||||
uniform float highlight_list[SPHERE_MAX];
|
||||
|
||||
// view
|
||||
|
@ -25,7 +25,6 @@ uniform vec2 resolution;
|
|||
uniform float shortdim;
|
||||
|
||||
// controls
|
||||
uniform float opacity;
|
||||
uniform int layer_threshold;
|
||||
uniform bool debug_mode;
|
||||
|
||||
|
@ -69,7 +68,7 @@ struct Fragment {
|
|||
vec4 color;
|
||||
};
|
||||
|
||||
Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) {
|
||||
Fragment sphere_shading(vecInv v, vec3 pt, vec4 base_color) {
|
||||
// the expression for normal needs to be checked. it's supposed to give the
|
||||
// negative gradient of the lorentz product between the impact point vector
|
||||
// and the sphere vector with respect to the coordinates of the impact
|
||||
|
@ -79,7 +78,7 @@ Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) {
|
|||
|
||||
float incidence = dot(normal, light_dir);
|
||||
float illum = mix(0.4, 1.0, max(incidence, 0.0));
|
||||
return Fragment(pt, normal, vec4(illum * base_color, opacity));
|
||||
return Fragment(pt, normal, vec4(illum * base_color.rgb, base_color.a));
|
||||
}
|
||||
|
||||
float intersection_dist(Fragment a, Fragment b) {
|
||||
|
@ -192,10 +191,11 @@ void main() {
|
|||
vec3 color = vec3(0.);
|
||||
int layer = layer_cnt - 1;
|
||||
TaggedDepth hit = top_hits[layer];
|
||||
vec4 sphere_color = color_list[hit.id];
|
||||
Fragment frag_next = sphere_shading(
|
||||
sphere_list[hit.id],
|
||||
hit.depth * dir,
|
||||
hit.dimming * color_list[hit.id]
|
||||
vec4(hit.dimming * sphere_color.rgb, sphere_color.a)
|
||||
);
|
||||
float highlight_next = highlight_list[hit.id];
|
||||
--layer;
|
||||
|
@ -206,10 +206,11 @@ void main() {
|
|||
|
||||
// shade the next fragment
|
||||
hit = top_hits[layer];
|
||||
sphere_color = color_list[hit.id];
|
||||
frag_next = sphere_shading(
|
||||
sphere_list[hit.id],
|
||||
hit.depth * dir,
|
||||
hit.dimming * color_list[hit.id]
|
||||
vec4(hit.dimming * sphere_color.rgb, sphere_color.a)
|
||||
);
|
||||
highlight_next = highlight_list[hit.id];
|
||||
|
947
app-proto/src/components/test_assembly_chooser.rs
Normal file
947
app-proto/src/components/test_assembly_chooser.rs
Normal file
|
@ -0,0 +1,947 @@
|
|||
use itertools::izip;
|
||||
use std::{f64::consts::{FRAC_1_SQRT_2, PI}, rc::Rc};
|
||||
use nalgebra::Vector3;
|
||||
use sycamore::prelude::*;
|
||||
use web_sys::{console, wasm_bindgen::JsValue};
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
engine,
|
||||
engine::DescentHistory,
|
||||
assembly::{
|
||||
Assembly,
|
||||
Element,
|
||||
ElementColor,
|
||||
InversiveDistanceRegulator,
|
||||
Point,
|
||||
Sphere
|
||||
},
|
||||
specified::SpecifiedValue
|
||||
};
|
||||
|
||||
// --- loaders ---
|
||||
|
||||
/* DEBUG */
|
||||
// each of these functions loads an example assembly for testing. once we've
|
||||
// done more work on saving and loading assemblies, we should come back to this
|
||||
// code to see if it can be simplified
|
||||
|
||||
fn load_gen_assemb(assembly: &Assembly) {
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
String::from("gemini_a"),
|
||||
String::from("Castor"),
|
||||
[1.00_f32, 0.25_f32, 0.00_f32],
|
||||
engine::sphere(0.5, 0.5, 0.0, 1.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
String::from("gemini_b"),
|
||||
String::from("Pollux"),
|
||||
[0.00_f32, 0.25_f32, 1.00_f32],
|
||||
engine::sphere(-0.5, -0.5, 0.0, 1.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
String::from("ursa_major"),
|
||||
String::from("Ursa major"),
|
||||
[0.25_f32, 0.00_f32, 1.00_f32],
|
||||
engine::sphere(-0.5, 0.5, 0.0, 0.75)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
String::from("ursa_minor"),
|
||||
String::from("Ursa minor"),
|
||||
[0.25_f32, 1.00_f32, 0.00_f32],
|
||||
engine::sphere(0.5, -0.5, 0.0, 0.5)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
String::from("moon_deimos"),
|
||||
String::from("Deimos"),
|
||||
[0.75_f32, 0.75_f32, 0.00_f32],
|
||||
engine::sphere(0.0, 0.15, 1.0, 0.25)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
String::from("moon_phobos"),
|
||||
String::from("Phobos"),
|
||||
[0.00_f32, 0.75_f32, 0.50_f32],
|
||||
engine::sphere(0.0, -0.15, -1.0, 0.25)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
fn load_low_curv_assemb(assembly: &Assembly) {
|
||||
// create the spheres
|
||||
let a = 0.75_f64.sqrt();
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
"central".to_string(),
|
||||
"Central".to_string(),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::sphere(0.0, 0.0, 0.0, 1.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
"assemb_plane".to_string(),
|
||||
"Assembly plane".to_string(),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
"side1".to_string(),
|
||||
"Side 1".to_string(),
|
||||
[1.00_f32, 0.00_f32, 0.25_f32],
|
||||
engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
"side2".to_string(),
|
||||
"Side 2".to_string(),
|
||||
[0.25_f32, 1.00_f32, 0.00_f32],
|
||||
engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
"side3".to_string(),
|
||||
"Side 3".to_string(),
|
||||
[0.00_f32, 0.25_f32, 1.00_f32],
|
||||
engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
"corner1".to_string(),
|
||||
"Corner 1".to_string(),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
"corner2".to_string(),
|
||||
"Corner 2".to_string(),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
String::from("corner3"),
|
||||
String::from("Corner 3"),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0)
|
||||
)
|
||||
);
|
||||
|
||||
// impose the desired tangencies and make the sides planar
|
||||
let index_range = 1..=3;
|
||||
let [central, assemb_plane] = ["central", "assemb_plane"].map(
|
||||
|id| assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[id].clone()
|
||||
)
|
||||
);
|
||||
let sides = index_range.clone().map(
|
||||
|k| assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&format!("side{k}")].clone()
|
||||
)
|
||||
);
|
||||
let corners = index_range.map(
|
||||
|k| assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&format!("corner{k}")].clone()
|
||||
)
|
||||
);
|
||||
for plane in [assemb_plane.clone()].into_iter().chain(sides.clone()) {
|
||||
// fix the curvature of each plane
|
||||
let curvature = plane.regulators().with_untracked(
|
||||
|regs| regs.first().unwrap().clone()
|
||||
);
|
||||
curvature.set_point().set(SpecifiedValue::try_from("0".to_string()).unwrap());
|
||||
}
|
||||
let all_perpendicular = [central.clone()].into_iter()
|
||||
.chain(sides.clone())
|
||||
.chain(corners.clone());
|
||||
for sphere in all_perpendicular {
|
||||
// make each side and packed sphere perpendicular to the assembly plane
|
||||
let right_angle = InversiveDistanceRegulator::new([sphere, assemb_plane.clone()]);
|
||||
right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(right_angle));
|
||||
}
|
||||
for sphere in sides.clone().chain(corners.clone()) {
|
||||
// make each side and corner sphere tangent to the central sphere
|
||||
let tangency = InversiveDistanceRegulator::new([sphere.clone(), central.clone()]);
|
||||
tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(tangency));
|
||||
}
|
||||
for (side_index, side) in sides.enumerate() {
|
||||
// make each side tangent to the two adjacent corner spheres
|
||||
for (corner_index, corner) in corners.clone().enumerate() {
|
||||
if side_index != corner_index {
|
||||
let tangency = InversiveDistanceRegulator::new([side.clone(), corner]);
|
||||
tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(tangency));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_pointed_assemb(assembly: &Assembly) {
|
||||
let _ = assembly.try_insert_element(
|
||||
Point::new(
|
||||
format!("point_front"),
|
||||
format!("Front point"),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::point(0.0, 0.0, FRAC_1_SQRT_2)
|
||||
)
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Point::new(
|
||||
format!("point_back"),
|
||||
format!("Back point"),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::point(0.0, 0.0, -FRAC_1_SQRT_2)
|
||||
)
|
||||
);
|
||||
for index_x in 0..=1 {
|
||||
for index_y in 0..=1 {
|
||||
let x = index_x as f64 - 0.5;
|
||||
let y = index_y as f64 - 0.5;
|
||||
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
format!("sphere{index_x}{index_y}"),
|
||||
format!("Sphere {index_x}{index_y}"),
|
||||
[0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32],
|
||||
engine::sphere(x, y, 0.0, 1.0)
|
||||
)
|
||||
);
|
||||
|
||||
let _ = assembly.try_insert_element(
|
||||
Point::new(
|
||||
format!("point{index_x}{index_y}"),
|
||||
format!("Point {index_x}{index_y}"),
|
||||
[0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32],
|
||||
engine::point(x, y, 0.0)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// to finish describing the tridiminished icosahedron, set the inversive
|
||||
// distance regulators as follows:
|
||||
// A-A -0.25
|
||||
// A-B "
|
||||
// B-C "
|
||||
// C-C "
|
||||
// A-C -0.25 * φ^2 = -0.6545084971874737
|
||||
fn load_tridim_icosahedron_assemb(assembly: &Assembly) {
|
||||
// create the vertices
|
||||
const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.25_f32];
|
||||
const COLOR_B: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32];
|
||||
const COLOR_C: ElementColor = [0.25_f32, 0.50_f32, 1.00_f32];
|
||||
let vertices = [
|
||||
Point::new(
|
||||
"a1".to_string(),
|
||||
"A₁".to_string(),
|
||||
COLOR_A,
|
||||
engine::point(0.25, 0.75, 0.75)
|
||||
),
|
||||
Point::new(
|
||||
"a2".to_string(),
|
||||
"A₂".to_string(),
|
||||
COLOR_A,
|
||||
engine::point(0.75, 0.25, 0.75)
|
||||
),
|
||||
Point::new(
|
||||
"a3".to_string(),
|
||||
"A₃".to_string(),
|
||||
COLOR_A,
|
||||
engine::point(0.75, 0.75, 0.25)
|
||||
),
|
||||
Point::new(
|
||||
"b1".to_string(),
|
||||
"B₁".to_string(),
|
||||
COLOR_B,
|
||||
engine::point(0.75, -0.25, -0.25)
|
||||
),
|
||||
Point::new(
|
||||
"b2".to_string(),
|
||||
"B₂".to_string(),
|
||||
COLOR_B,
|
||||
engine::point(-0.25, 0.75, -0.25)
|
||||
),
|
||||
Point::new(
|
||||
"b3".to_string(),
|
||||
"B₃".to_string(),
|
||||
COLOR_B,
|
||||
engine::point(-0.25, -0.25, 0.75)
|
||||
),
|
||||
Point::new(
|
||||
"c1".to_string(),
|
||||
"C₁".to_string(),
|
||||
COLOR_C,
|
||||
engine::point(0.0, -1.0, -1.0)
|
||||
),
|
||||
Point::new(
|
||||
"c2".to_string(),
|
||||
"C₂".to_string(),
|
||||
COLOR_C,
|
||||
engine::point(-1.0, 0.0, -1.0)
|
||||
),
|
||||
Point::new(
|
||||
"c3".to_string(),
|
||||
"C₃".to_string(),
|
||||
COLOR_C,
|
||||
engine::point(-1.0, -1.0, 0.0)
|
||||
)
|
||||
];
|
||||
for vertex in vertices {
|
||||
let _ = assembly.try_insert_element(vertex);
|
||||
}
|
||||
|
||||
// create the faces
|
||||
const COLOR_FACE: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32];
|
||||
let frac_1_sqrt_6 = 1.0 / 6.0_f64.sqrt();
|
||||
let frac_2_sqrt_6 = 2.0 * frac_1_sqrt_6;
|
||||
let faces = [
|
||||
Sphere::new(
|
||||
"face1".to_string(),
|
||||
"Face 1".to_string(),
|
||||
COLOR_FACE,
|
||||
engine::sphere_with_offset(frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0)
|
||||
),
|
||||
Sphere::new(
|
||||
"face2".to_string(),
|
||||
"Face 2".to_string(),
|
||||
COLOR_FACE,
|
||||
engine::sphere_with_offset(-frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0)
|
||||
),
|
||||
Sphere::new(
|
||||
"face3".to_string(),
|
||||
"Face 3".to_string(),
|
||||
COLOR_FACE,
|
||||
engine::sphere_with_offset(-frac_1_sqrt_6, -frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, 0.0)
|
||||
)
|
||||
];
|
||||
for face in faces {
|
||||
face.ghost().set(true);
|
||||
let _ = assembly.try_insert_element(face);
|
||||
}
|
||||
|
||||
let index_range = 1..=3;
|
||||
for j in index_range.clone() {
|
||||
// make each face planar
|
||||
let face = assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&format!("face{j}")].clone()
|
||||
);
|
||||
let curvature_regulator = face.regulators().with_untracked(
|
||||
|regs| regs.first().unwrap().clone()
|
||||
);
|
||||
curvature_regulator.set_point().set(
|
||||
SpecifiedValue::try_from("0".to_string()).unwrap()
|
||||
);
|
||||
|
||||
// put each A vertex on the face it belongs to
|
||||
let vertex_a = assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&format!("a{j}")].clone()
|
||||
);
|
||||
let incidence_a = InversiveDistanceRegulator::new([face.clone(), vertex_a.clone()]);
|
||||
incidence_a.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(incidence_a));
|
||||
|
||||
// regulate the B-C vertex distances
|
||||
let vertices_bc = ["b", "c"].map(
|
||||
|series| assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&format!("{series}{j}")].clone()
|
||||
)
|
||||
);
|
||||
assembly.insert_regulator(
|
||||
Rc::new(InversiveDistanceRegulator::new(vertices_bc))
|
||||
);
|
||||
|
||||
// get the pair of indices adjacent to `j`
|
||||
let adjacent_indices = [j % 3 + 1, (j + 1) % 3 + 1];
|
||||
|
||||
for k in adjacent_indices.clone() {
|
||||
for series in ["b", "c"] {
|
||||
// put each B and C vertex on the faces it belongs to
|
||||
let vertex = assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&format!("{series}{k}")].clone()
|
||||
);
|
||||
let incidence = InversiveDistanceRegulator::new([face.clone(), vertex.clone()]);
|
||||
incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(incidence));
|
||||
|
||||
// regulate the A-B and A-C vertex distances
|
||||
assembly.insert_regulator(
|
||||
Rc::new(InversiveDistanceRegulator::new([vertex_a.clone(), vertex]))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// regulate the A-A and C-C vertex distances
|
||||
let adjacent_pairs = ["a", "c"].map(
|
||||
|series| adjacent_indices.map(
|
||||
|index| assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&format!("{series}{index}")].clone()
|
||||
)
|
||||
)
|
||||
);
|
||||
for pair in adjacent_pairs {
|
||||
assembly.insert_regulator(
|
||||
Rc::new(InversiveDistanceRegulator::new(pair))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// to finish describing the dodecahedral circle packing, set the inversive
|
||||
// distance regulators to -1. some of the regulators have already been set
|
||||
fn load_dodeca_packing_assemb(assembly: &Assembly) {
|
||||
// add the substrate
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
"substrate".to_string(),
|
||||
"Substrate".to_string(),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::sphere(0.0, 0.0, 0.0, 1.0)
|
||||
)
|
||||
);
|
||||
let substrate = assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id["substrate"].clone()
|
||||
);
|
||||
|
||||
// fix the substrate's curvature
|
||||
substrate.regulators().with_untracked(
|
||||
|regs| regs.first().unwrap().clone()
|
||||
).set_point().set(
|
||||
SpecifiedValue::try_from("0.5".to_string()).unwrap()
|
||||
);
|
||||
|
||||
// add the circles to be packed
|
||||
const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.00_f32];
|
||||
const COLOR_B: ElementColor = [1.00_f32, 0.00_f32, 0.25_f32];
|
||||
const COLOR_C: ElementColor = [0.25_f32, 0.00_f32, 1.00_f32];
|
||||
let phi = 0.5 + 1.25_f64.sqrt(); /* TO DO */ // replace with std::f64::consts::PHI when that gets stabilized
|
||||
let phi_inv = 1.0 / phi;
|
||||
let coord_scale = (phi + 2.0).sqrt();
|
||||
let face_scales = [phi_inv, (13.0 / 12.0) / coord_scale];
|
||||
let face_radii = [phi_inv, 5.0 / 12.0];
|
||||
let mut faces = Vec::<Rc<dyn Element>>::new();
|
||||
let subscripts = ["₀", "₁"];
|
||||
for j in 0..2 {
|
||||
for k in 0..2 {
|
||||
let small_coord = face_scales[k] * (2.0*(j as f64) - 1.0);
|
||||
let big_coord = face_scales[k] * (2.0*(k as f64) - 1.0) * phi;
|
||||
|
||||
let id_num = format!("{j}{k}");
|
||||
let label_sub = format!("{}{}", subscripts[j], subscripts[k]);
|
||||
|
||||
// add the A face
|
||||
let id_a = format!("a{id_num}");
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
id_a.clone(),
|
||||
format!("A{label_sub}"),
|
||||
COLOR_A,
|
||||
engine::sphere(0.0, small_coord, big_coord, face_radii[k])
|
||||
)
|
||||
);
|
||||
faces.push(
|
||||
assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&id_a].clone()
|
||||
)
|
||||
);
|
||||
|
||||
// add the B face
|
||||
let id_b = format!("b{id_num}");
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
id_b.clone(),
|
||||
format!("B{label_sub}"),
|
||||
COLOR_B,
|
||||
engine::sphere(small_coord, big_coord, 0.0, face_radii[k])
|
||||
)
|
||||
);
|
||||
faces.push(
|
||||
assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&id_b].clone()
|
||||
)
|
||||
);
|
||||
|
||||
// add the C face
|
||||
let id_c = format!("c{id_num}");
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
id_c.clone(),
|
||||
format!("C{label_sub}"),
|
||||
COLOR_C,
|
||||
engine::sphere(big_coord, 0.0, small_coord, face_radii[k])
|
||||
)
|
||||
);
|
||||
faces.push(
|
||||
assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&id_c].clone()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// make each face sphere perpendicular to the substrate
|
||||
for face in faces {
|
||||
let right_angle = InversiveDistanceRegulator::new([face, substrate.clone()]);
|
||||
right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(right_angle));
|
||||
}
|
||||
|
||||
// set up the tangencies that define the packing
|
||||
for [long_edge_plane, short_edge_plane] in [["a", "b"], ["b", "c"], ["c", "a"]] {
|
||||
for k in 0..2 {
|
||||
let long_edge_ids = [
|
||||
format!("{long_edge_plane}{k}0"),
|
||||
format!("{long_edge_plane}{k}1")
|
||||
];
|
||||
let short_edge_ids = [
|
||||
format!("{short_edge_plane}0{k}"),
|
||||
format!("{short_edge_plane}1{k}")
|
||||
];
|
||||
let [long_edge, short_edge] = [long_edge_ids, short_edge_ids].map(
|
||||
|edge_ids| edge_ids.map(
|
||||
|id| assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&id].clone()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// set up the short-edge tangency
|
||||
let short_tangency = InversiveDistanceRegulator::new(short_edge.clone());
|
||||
if k == 0 {
|
||||
short_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap());
|
||||
}
|
||||
assembly.insert_regulator(Rc::new(short_tangency));
|
||||
|
||||
// set up the side tangencies
|
||||
for i in 0..2 {
|
||||
for j in 0..2 {
|
||||
let side_tangency = InversiveDistanceRegulator::new(
|
||||
[long_edge[i].clone(), short_edge[j].clone()]
|
||||
);
|
||||
if i == 0 && k == 0 {
|
||||
side_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap());
|
||||
}
|
||||
assembly.insert_regulator(Rc::new(side_tangency));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the initial configuration of this test assembly deliberately violates the
|
||||
// constraints, so loading the assembly will trigger a non-trivial realization
|
||||
fn load_balanced_assemb(assembly: &Assembly) {
|
||||
// create the spheres
|
||||
const R_OUTER: f64 = 10.0;
|
||||
const R_INNER: f64 = 4.0;
|
||||
let spheres = [
|
||||
Sphere::new(
|
||||
"outer".to_string(),
|
||||
"Outer".to_string(),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::sphere(0.0, 0.0, 0.0, R_OUTER)
|
||||
),
|
||||
Sphere::new(
|
||||
"a".to_string(),
|
||||
"A".to_string(),
|
||||
[1.00_f32, 0.00_f32, 0.25_f32],
|
||||
engine::sphere(0.0, 4.0, 0.0, R_INNER)
|
||||
),
|
||||
Sphere::new(
|
||||
"b".to_string(),
|
||||
"B".to_string(),
|
||||
[0.00_f32, 0.25_f32, 1.00_f32],
|
||||
engine::sphere(0.0, -4.0, 0.0, R_INNER)
|
||||
),
|
||||
];
|
||||
for sphere in spheres {
|
||||
let _ = assembly.try_insert_element(sphere);
|
||||
}
|
||||
|
||||
// get references to the spheres
|
||||
let [outer, a, b] = ["outer", "a", "b"].map(
|
||||
|id| assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[id].clone()
|
||||
)
|
||||
);
|
||||
|
||||
// fix the diameters of the outer, sun, and moon spheres
|
||||
for (sphere, radius) in [
|
||||
(outer.clone(), R_OUTER),
|
||||
(a.clone(), R_INNER),
|
||||
(b.clone(), R_INNER)
|
||||
] {
|
||||
let curvature_regulator = sphere.regulators().with_untracked(
|
||||
|regs| regs.first().unwrap().clone()
|
||||
);
|
||||
let curvature = 0.5 / radius;
|
||||
curvature_regulator.set_point().set(
|
||||
SpecifiedValue::try_from(curvature.to_string()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
// set the inversive distances between the spheres. as described above, the
|
||||
// initial configuration deliberately violates these constraints
|
||||
for inner in [a, b] {
|
||||
let tangency = InversiveDistanceRegulator::new([outer.clone(), inner]);
|
||||
tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(tangency));
|
||||
}
|
||||
}
|
||||
|
||||
// the initial configuration of this test assembly deliberately violates the
|
||||
// constraints, so loading the assembly will trigger a non-trivial realization
|
||||
fn load_off_center_assemb(assembly: &Assembly) {
|
||||
// create a point almost at the origin and a sphere centered on the origin
|
||||
let _ = assembly.try_insert_element(
|
||||
Point::new(
|
||||
"point".to_string(),
|
||||
"Point".to_string(),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::point(1e-9, 0.0, 0.0)
|
||||
),
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
"sphere".to_string(),
|
||||
"Sphere".to_string(),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::sphere(0.0, 0.0, 0.0, 1.0)
|
||||
),
|
||||
);
|
||||
|
||||
// get references to the elements
|
||||
let point_and_sphere = ["point", "sphere"].map(
|
||||
|id| assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[id].clone()
|
||||
)
|
||||
);
|
||||
|
||||
// put the point on the sphere
|
||||
let incidence = InversiveDistanceRegulator::new(point_and_sphere);
|
||||
incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(incidence));
|
||||
}
|
||||
|
||||
// setting the inversive distances between the vertices to -2 gives a regular
|
||||
// tetrahedron with side length 1, whose insphere and circumsphere have radii
|
||||
// sqrt(1/6) and sqrt(3/2), respectively. to measure those radii, set an
|
||||
// inversive distance of -1 between the insphere and each face, and then set an
|
||||
// inversive distance of 0 between the circumsphere and each vertex
|
||||
fn load_radius_ratio_assemb(assembly: &Assembly) {
|
||||
let index_range = 1..=4;
|
||||
|
||||
// create the spheres
|
||||
const GRAY: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32];
|
||||
let spheres = [
|
||||
Sphere::new(
|
||||
"sphere_faces".to_string(),
|
||||
"Insphere".to_string(),
|
||||
GRAY,
|
||||
engine::sphere(0.0, 0.0, 0.0, 0.5)
|
||||
),
|
||||
Sphere::new(
|
||||
"sphere_vertices".to_string(),
|
||||
"Circumsphere".to_string(),
|
||||
GRAY,
|
||||
engine::sphere(0.0, 0.0, 0.0, 0.25)
|
||||
)
|
||||
];
|
||||
for sphere in spheres {
|
||||
let _ = assembly.try_insert_element(sphere);
|
||||
}
|
||||
|
||||
// create the vertices
|
||||
let vertices = izip!(
|
||||
index_range.clone(),
|
||||
[
|
||||
[1.00_f32, 0.50_f32, 0.75_f32],
|
||||
[1.00_f32, 0.75_f32, 0.50_f32],
|
||||
[1.00_f32, 1.00_f32, 0.50_f32],
|
||||
[0.75_f32, 0.50_f32, 1.00_f32]
|
||||
].into_iter(),
|
||||
[
|
||||
engine::point(-0.6, -0.8, -0.6),
|
||||
engine::point(-0.6, 0.8, 0.6),
|
||||
engine::point(0.6, -0.8, 0.6),
|
||||
engine::point(0.6, 0.8, -0.6)
|
||||
].into_iter()
|
||||
).map(
|
||||
|(k, color, representation)| {
|
||||
Point::new(
|
||||
format!("v{k}"),
|
||||
format!("Vertex {k}"),
|
||||
color,
|
||||
representation
|
||||
)
|
||||
}
|
||||
);
|
||||
for vertex in vertices {
|
||||
let _ = assembly.try_insert_element(vertex);
|
||||
}
|
||||
|
||||
// create the faces
|
||||
let base_dir = Vector3::new(1.0, 0.75, 1.0).normalize();
|
||||
let offset = base_dir.dot(&Vector3::new(-0.6, 0.8, 0.6));
|
||||
let faces = izip!(
|
||||
index_range.clone(),
|
||||
[
|
||||
[1.00_f32, 0.00_f32, 0.25_f32],
|
||||
[1.00_f32, 0.25_f32, 0.00_f32],
|
||||
[0.75_f32, 0.75_f32, 0.00_f32],
|
||||
[0.25_f32, 0.00_f32, 1.00_f32]
|
||||
].into_iter(),
|
||||
[
|
||||
engine::sphere_with_offset(base_dir[0], base_dir[1], base_dir[2], offset, 0.0),
|
||||
engine::sphere_with_offset(base_dir[0], -base_dir[1], -base_dir[2], offset, 0.0),
|
||||
engine::sphere_with_offset(-base_dir[0], base_dir[1], -base_dir[2], offset, 0.0),
|
||||
engine::sphere_with_offset(-base_dir[0], -base_dir[1], base_dir[2], offset, 0.0)
|
||||
].into_iter()
|
||||
).map(
|
||||
|(k, color, representation)| {
|
||||
Sphere::new(
|
||||
format!("f{k}"),
|
||||
format!("Face {k}"),
|
||||
color,
|
||||
representation
|
||||
)
|
||||
}
|
||||
);
|
||||
for face in faces {
|
||||
face.ghost().set(true);
|
||||
let _ = assembly.try_insert_element(face);
|
||||
}
|
||||
|
||||
// impose the constraints
|
||||
for j in index_range.clone() {
|
||||
let [face_j, vertex_j] = [
|
||||
format!("f{j}"),
|
||||
format!("v{j}")
|
||||
].map(
|
||||
|id| assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&id].clone()
|
||||
)
|
||||
);
|
||||
|
||||
// make the faces planar
|
||||
let curvature_regulator = face_j.regulators().with_untracked(
|
||||
|regs| regs.first().unwrap().clone()
|
||||
);
|
||||
curvature_regulator.set_point().set(
|
||||
SpecifiedValue::try_from("0".to_string()).unwrap()
|
||||
);
|
||||
|
||||
for k in index_range.clone().filter(|&index| index != j) {
|
||||
let vertex_k = assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&format!("v{k}")].clone()
|
||||
);
|
||||
|
||||
// fix the distances between the vertices
|
||||
if j < k {
|
||||
let distance_regulator = InversiveDistanceRegulator::new(
|
||||
[vertex_j.clone(), vertex_k.clone()]
|
||||
);
|
||||
assembly.insert_regulator(Rc::new(distance_regulator));
|
||||
}
|
||||
|
||||
// put the vertices on the faces
|
||||
let incidence_regulator = InversiveDistanceRegulator::new([face_j.clone(), vertex_k.clone()]);
|
||||
incidence_regulator.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(incidence_regulator));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// to finish setting up the problem, fix the following curvatures:
|
||||
// sun 1
|
||||
// moon 5/3 = 1.666666666666666...
|
||||
// chain1 2
|
||||
// a tiny `x` or `z` nudge of the outer sphere reliably prevents realization
|
||||
// failures before they happen, or resolves them after they happen. the result
|
||||
// depends sensitively on the translation direction, suggesting that realization
|
||||
// is failing because the engine is having trouble breaking a symmetry
|
||||
// /* TO DO */
|
||||
// the engine's performance on this problem is scale-dependent! with the current
|
||||
// initial conditions, realization fails for any order of imposing the remaining
|
||||
// curvature constraints. scaling everything up by a factor of ten, as done in
|
||||
// the original problem, makes realization succeed reliably. one potentially
|
||||
// relevant difference is that a lot of the numbers in the current initial
|
||||
// conditions are exactly representable as floats, unlike the analogous numbers
|
||||
// in the scaled-up problem. the inexact representations might break the
|
||||
// symmetry that's getting the engine stuck
|
||||
fn load_irisawa_hexlet_assemb(assembly: &Assembly) {
|
||||
let index_range = 1..=6;
|
||||
let colors = [
|
||||
[1.00_f32, 0.00_f32, 0.25_f32],
|
||||
[1.00_f32, 0.25_f32, 0.00_f32],
|
||||
[0.75_f32, 0.75_f32, 0.00_f32],
|
||||
[0.25_f32, 1.00_f32, 0.00_f32],
|
||||
[0.00_f32, 0.25_f32, 1.00_f32],
|
||||
[0.25_f32, 0.00_f32, 1.00_f32]
|
||||
].into_iter();
|
||||
|
||||
// create the spheres
|
||||
let spheres = [
|
||||
Sphere::new(
|
||||
"outer".to_string(),
|
||||
"Outer".to_string(),
|
||||
[0.5_f32, 0.5_f32, 0.5_f32],
|
||||
engine::sphere(0.0, 0.0, 0.0, 1.5)
|
||||
),
|
||||
Sphere::new(
|
||||
"sun".to_string(),
|
||||
"Sun".to_string(),
|
||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||
engine::sphere(0.0, -0.75, 0.0, 0.75)
|
||||
),
|
||||
Sphere::new(
|
||||
"moon".to_string(),
|
||||
"Moon".to_string(),
|
||||
[0.25_f32, 0.25_f32, 0.25_f32],
|
||||
engine::sphere(0.0, 0.75, 0.0, 0.75)
|
||||
),
|
||||
].into_iter().chain(
|
||||
index_range.clone().zip(colors).map(
|
||||
|(k, color)| {
|
||||
let ang = (k as f64) * PI/3.0;
|
||||
Sphere::new(
|
||||
format!("chain{k}"),
|
||||
format!("Chain {k}"),
|
||||
color,
|
||||
engine::sphere(1.0 * ang.sin(), 0.0, 1.0 * ang.cos(), 0.5)
|
||||
)
|
||||
}
|
||||
)
|
||||
);
|
||||
for sphere in spheres {
|
||||
let _ = assembly.try_insert_element(sphere);
|
||||
}
|
||||
|
||||
// put the outer sphere in ghost mode and fix its curvature
|
||||
let outer = assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id["outer"].clone()
|
||||
);
|
||||
outer.ghost().set(true);
|
||||
let outer_curvature_regulator = outer.regulators().with_untracked(
|
||||
|regs| regs.first().unwrap().clone()
|
||||
);
|
||||
outer_curvature_regulator.set_point().set(
|
||||
SpecifiedValue::try_from((1.0 / 3.0).to_string()).unwrap()
|
||||
);
|
||||
|
||||
// impose the desired tangencies
|
||||
let [outer, sun, moon] = ["outer", "sun", "moon"].map(
|
||||
|id| assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[id].clone()
|
||||
)
|
||||
);
|
||||
let chain = index_range.map(
|
||||
|k| assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&format!("chain{k}")].clone()
|
||||
)
|
||||
);
|
||||
for (chain_sphere, chain_sphere_next) in chain.clone().zip(chain.cycle().skip(1)) {
|
||||
for (other_sphere, inversive_distance) in [
|
||||
(outer.clone(), "1"),
|
||||
(sun.clone(), "-1"),
|
||||
(moon.clone(), "-1"),
|
||||
(chain_sphere_next.clone(), "-1")
|
||||
] {
|
||||
let tangency = InversiveDistanceRegulator::new([chain_sphere.clone(), other_sphere]);
|
||||
tangency.set_point.set(SpecifiedValue::try_from(inversive_distance.to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(tangency));
|
||||
}
|
||||
}
|
||||
|
||||
let outer_sun_tangency = InversiveDistanceRegulator::new([outer.clone(), sun]);
|
||||
outer_sun_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(outer_sun_tangency));
|
||||
|
||||
let outer_moon_tangency = InversiveDistanceRegulator::new([outer.clone(), moon]);
|
||||
outer_moon_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(outer_moon_tangency));
|
||||
}
|
||||
|
||||
// --- chooser ---
|
||||
|
||||
/* DEBUG */
|
||||
#[component]
|
||||
pub fn TestAssemblyChooser() -> View {
|
||||
// create an effect that loads the selected test assembly
|
||||
let assembly_name = create_signal("general".to_string());
|
||||
create_effect(move || {
|
||||
// get name of chosen assembly
|
||||
let name = assembly_name.get_clone();
|
||||
console::log_1(
|
||||
&JsValue::from(format!("Showing assembly \"{}\"", name.clone()))
|
||||
);
|
||||
|
||||
batch(|| {
|
||||
let state = use_context::<AppState>();
|
||||
let assembly = &state.assembly;
|
||||
|
||||
// pause realization
|
||||
assembly.keep_realized.set(false);
|
||||
|
||||
// clear state
|
||||
assembly.regulators.update(|regs| regs.clear());
|
||||
assembly.elements.update(|elts| elts.clear());
|
||||
assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear());
|
||||
assembly.descent_history.set(DescentHistory::new());
|
||||
state.selection.update(|sel| sel.clear());
|
||||
|
||||
// load assembly
|
||||
match name.as_str() {
|
||||
"general" => load_gen_assemb(assembly),
|
||||
"low-curv" => load_low_curv_assemb(assembly),
|
||||
"pointed" => load_pointed_assemb(assembly),
|
||||
"tridim-icosahedron" => load_tridim_icosahedron_assemb(assembly),
|
||||
"dodeca-packing" => load_dodeca_packing_assemb(assembly),
|
||||
"balanced" => load_balanced_assemb(assembly),
|
||||
"off-center" => load_off_center_assemb(assembly),
|
||||
"radius-ratio" => load_radius_ratio_assemb(assembly),
|
||||
"irisawa-hexlet" => load_irisawa_hexlet_assemb(assembly),
|
||||
_ => ()
|
||||
};
|
||||
|
||||
// resume realization
|
||||
assembly.keep_realized.set(true);
|
||||
});
|
||||
});
|
||||
|
||||
// build the chooser
|
||||
view! {
|
||||
select(bind:value=assembly_name) {
|
||||
option(value="general") { "General" }
|
||||
option(value="low-curv") { "Low-curvature" }
|
||||
option(value="pointed") { "Pointed" }
|
||||
option(value="tridim-icosahedron") { "Tridiminished icosahedron" }
|
||||
option(value="dodeca-packing") { "Dodecahedral packing" }
|
||||
option(value="balanced") { "Balanced" }
|
||||
option(value="off-center") { "Off-center" }
|
||||
option(value="radius-ratio") { "Radius ratio" }
|
||||
option(value="irisawa-hexlet") { "Irisawa hexlet" }
|
||||
option(value="empty") { "Empty" }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,464 +0,0 @@
|
|||
use core::array;
|
||||
use nalgebra::{DMatrix, Rotation3, Vector3};
|
||||
use sycamore::{prelude::*, motion::create_raf};
|
||||
use web_sys::{
|
||||
console,
|
||||
window,
|
||||
KeyboardEvent,
|
||||
WebGl2RenderingContext,
|
||||
WebGlProgram,
|
||||
WebGlShader,
|
||||
WebGlUniformLocation,
|
||||
wasm_bindgen::{JsCast, JsValue}
|
||||
};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
fn compile_shader(
|
||||
context: &WebGl2RenderingContext,
|
||||
shader_type: u32,
|
||||
source: &str,
|
||||
) -> WebGlShader {
|
||||
let shader = context.create_shader(shader_type).unwrap();
|
||||
context.shader_source(&shader, source);
|
||||
context.compile_shader(&shader);
|
||||
shader
|
||||
}
|
||||
|
||||
fn get_uniform_array_locations<const N: usize>(
|
||||
context: &WebGl2RenderingContext,
|
||||
program: &WebGlProgram,
|
||||
var_name: &str,
|
||||
member_name_opt: Option<&str>
|
||||
) -> [Option<WebGlUniformLocation>; N] {
|
||||
array::from_fn(|n| {
|
||||
let name = match member_name_opt {
|
||||
Some(member_name) => format!("{var_name}[{n}].{member_name}"),
|
||||
None => format!("{var_name}[{n}]")
|
||||
};
|
||||
context.get_uniform_location(&program, name.as_str())
|
||||
})
|
||||
}
|
||||
|
||||
// load the given data into the vertex input of the given name
|
||||
fn bind_vertex_attrib(
|
||||
context: &WebGl2RenderingContext,
|
||||
index: u32,
|
||||
size: i32,
|
||||
data: &[f32]
|
||||
) {
|
||||
// create a data buffer and bind it to ARRAY_BUFFER
|
||||
let buffer = context.create_buffer().unwrap();
|
||||
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer));
|
||||
|
||||
// load the given data into the buffer. the function `Float32Array::view`
|
||||
// creates a raw view into our module's `WebAssembly.Memory` buffer.
|
||||
// allocating more memory will change the buffer, invalidating the view.
|
||||
// that means we have to make sure we don't allocate any memory until the
|
||||
// view is dropped
|
||||
unsafe {
|
||||
context.buffer_data_with_array_buffer_view(
|
||||
WebGl2RenderingContext::ARRAY_BUFFER,
|
||||
&js_sys::Float32Array::view(&data),
|
||||
WebGl2RenderingContext::STATIC_DRAW,
|
||||
);
|
||||
}
|
||||
|
||||
// allow the target attribute to be used
|
||||
context.enable_vertex_attrib_array(index);
|
||||
|
||||
// take whatever's bound to ARRAY_BUFFER---here, the data buffer created
|
||||
// above---and bind it to the target attribute
|
||||
//
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer
|
||||
//
|
||||
context.vertex_attrib_pointer_with_i32(
|
||||
index,
|
||||
size,
|
||||
WebGl2RenderingContext::FLOAT,
|
||||
false, // don't normalize
|
||||
0, // zero stride
|
||||
0, // zero offset
|
||||
);
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Display() -> View {
|
||||
let state = use_context::<AppState>();
|
||||
|
||||
// canvas
|
||||
let display = create_node_ref();
|
||||
|
||||
// navigation
|
||||
let pitch_up = create_signal(0.0);
|
||||
let pitch_down = create_signal(0.0);
|
||||
let yaw_right = create_signal(0.0);
|
||||
let yaw_left = create_signal(0.0);
|
||||
let roll_ccw = create_signal(0.0);
|
||||
let roll_cw = create_signal(0.0);
|
||||
let zoom_in = create_signal(0.0);
|
||||
let zoom_out = create_signal(0.0);
|
||||
let turntable = create_signal(false); /* BENCHMARKING */
|
||||
|
||||
// change listener
|
||||
let scene_changed = create_signal(true);
|
||||
create_effect(move || {
|
||||
state.assembly.elements.with(|elts| {
|
||||
for (_, elt) in elts {
|
||||
elt.representation.track();
|
||||
}
|
||||
});
|
||||
state.selection.track();
|
||||
scene_changed.set(true);
|
||||
});
|
||||
|
||||
/* INSTRUMENTS */
|
||||
const SAMPLE_PERIOD: i32 = 60;
|
||||
let mut last_sample_time = 0.0;
|
||||
let mut frames_since_last_sample = 0;
|
||||
let mean_frame_interval = create_signal(0.0);
|
||||
|
||||
on_mount(move || {
|
||||
// timing
|
||||
let mut last_time = 0.0;
|
||||
|
||||
// viewpoint
|
||||
const ROT_SPEED: f64 = 0.4; // in radians per second
|
||||
const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second
|
||||
const TURNTABLE_SPEED: f64 = 0.1; /* BENCHMARKING */
|
||||
let mut orientation = DMatrix::<f64>::identity(5, 5);
|
||||
let mut rotation = DMatrix::<f64>::identity(5, 5);
|
||||
let mut location_z: f64 = 5.0;
|
||||
|
||||
// display parameters
|
||||
const OPACITY: f32 = 0.5; /* SCAFFOLDING */
|
||||
const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */
|
||||
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
|
||||
const DEBUG_MODE: i32 = 0; /* DEBUG */
|
||||
|
||||
/* INSTRUMENTS */
|
||||
let performance = window().unwrap().performance().unwrap();
|
||||
|
||||
// get the display canvas
|
||||
let canvas = display.get().unchecked_into::<web_sys::HtmlCanvasElement>();
|
||||
let ctx = canvas
|
||||
.get_context("webgl2")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<WebGl2RenderingContext>()
|
||||
.unwrap();
|
||||
|
||||
// compile and attach the vertex and fragment shaders
|
||||
let vertex_shader = compile_shader(
|
||||
&ctx,
|
||||
WebGl2RenderingContext::VERTEX_SHADER,
|
||||
include_str!("identity.vert"),
|
||||
);
|
||||
let fragment_shader = compile_shader(
|
||||
&ctx,
|
||||
WebGl2RenderingContext::FRAGMENT_SHADER,
|
||||
include_str!("inversive.frag"),
|
||||
);
|
||||
let program = ctx.create_program().unwrap();
|
||||
ctx.attach_shader(&program, &vertex_shader);
|
||||
ctx.attach_shader(&program, &fragment_shader);
|
||||
ctx.link_program(&program);
|
||||
let link_status = ctx
|
||||
.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
|
||||
.as_bool()
|
||||
.unwrap();
|
||||
let link_msg = if link_status {
|
||||
"Linked successfully"
|
||||
} else {
|
||||
"Linking failed"
|
||||
};
|
||||
console::log_1(&JsValue::from(link_msg));
|
||||
ctx.use_program(Some(&program));
|
||||
|
||||
/* DEBUG */
|
||||
// print the maximum number of vectors that can be passed as
|
||||
// uniforms to a fragment shader. the OpenGL ES 3.0 standard
|
||||
// requires this maximum to be at least 224, as discussed in the
|
||||
// documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter
|
||||
// here:
|
||||
//
|
||||
// https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml
|
||||
//
|
||||
// there are also other size limits. for example, on Aaron's
|
||||
// machine, the the length of a float or genType array seems to be
|
||||
// capped at 1024 elements
|
||||
console::log_2(
|
||||
&ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(),
|
||||
&JsValue::from("uniform vectors available")
|
||||
);
|
||||
|
||||
// find indices of vertex attributes and uniforms
|
||||
const SPHERE_MAX: usize = 200;
|
||||
let position_index = ctx.get_attrib_location(&program, "position") as u32;
|
||||
let sphere_cnt_loc = ctx.get_uniform_location(&program, "sphere_cnt");
|
||||
let sphere_sp_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||
&ctx, &program, "sphere_list", Some("sp")
|
||||
);
|
||||
let sphere_lt_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||
&ctx, &program, "sphere_list", Some("lt")
|
||||
);
|
||||
let color_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||
&ctx, &program, "color_list", None
|
||||
);
|
||||
let highlight_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||
&ctx, &program, "highlight_list", None
|
||||
);
|
||||
let resolution_loc = ctx.get_uniform_location(&program, "resolution");
|
||||
let shortdim_loc = ctx.get_uniform_location(&program, "shortdim");
|
||||
let opacity_loc = ctx.get_uniform_location(&program, "opacity");
|
||||
let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold");
|
||||
let debug_mode_loc = ctx.get_uniform_location(&program, "debug_mode");
|
||||
|
||||
// create a vertex array and bind it to the graphics context
|
||||
let vertex_array = ctx.create_vertex_array().unwrap();
|
||||
ctx.bind_vertex_array(Some(&vertex_array));
|
||||
|
||||
// set the vertex positions
|
||||
const VERTEX_CNT: usize = 6;
|
||||
let positions: [f32; 3*VERTEX_CNT] = [
|
||||
// northwest triangle
|
||||
-1.0, -1.0, 0.0,
|
||||
-1.0, 1.0, 0.0,
|
||||
1.0, 1.0, 0.0,
|
||||
// southeast triangle
|
||||
-1.0, -1.0, 0.0,
|
||||
1.0, 1.0, 0.0,
|
||||
1.0, -1.0, 0.0
|
||||
];
|
||||
bind_vertex_attrib(&ctx, position_index, 3, &positions);
|
||||
|
||||
// set up a repainting routine
|
||||
let (_, start_animation_loop, _) = create_raf(move || {
|
||||
// get the time step
|
||||
let time = performance.now();
|
||||
let time_step = 0.001*(time - last_time);
|
||||
last_time = time;
|
||||
|
||||
// get the navigation state
|
||||
let pitch_up_val = pitch_up.get();
|
||||
let pitch_down_val = pitch_down.get();
|
||||
let yaw_right_val = yaw_right.get();
|
||||
let yaw_left_val = yaw_left.get();
|
||||
let roll_ccw_val = roll_ccw.get();
|
||||
let roll_cw_val = roll_cw.get();
|
||||
let zoom_in_val = zoom_in.get();
|
||||
let zoom_out_val = zoom_out.get();
|
||||
let turntable_val = turntable.get(); /* BENCHMARKING */
|
||||
|
||||
// update the assembly's orientation
|
||||
let ang_vel = {
|
||||
let pitch = pitch_up_val - pitch_down_val;
|
||||
let yaw = yaw_right_val - yaw_left_val;
|
||||
let roll = roll_ccw_val - roll_cw_val;
|
||||
if pitch != 0.0 || yaw != 0.0 || roll != 0.0 {
|
||||
ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize()
|
||||
} else {
|
||||
Vector3::zeros()
|
||||
}
|
||||
} /* BENCHMARKING */ + if turntable_val {
|
||||
Vector3::new(0.0, TURNTABLE_SPEED, 0.0)
|
||||
} else {
|
||||
Vector3::zeros()
|
||||
};
|
||||
let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0);
|
||||
rotation_sp.copy_from(
|
||||
Rotation3::from_scaled_axis(time_step * ang_vel).matrix()
|
||||
);
|
||||
orientation = &rotation * &orientation;
|
||||
|
||||
// update the assembly's location
|
||||
let zoom = zoom_out_val - zoom_in_val;
|
||||
location_z *= (time_step * ZOOM_SPEED * zoom).exp();
|
||||
|
||||
if scene_changed.get() {
|
||||
/* INSTRUMENTS */
|
||||
// measure mean frame interval
|
||||
frames_since_last_sample += 1;
|
||||
if frames_since_last_sample >= SAMPLE_PERIOD {
|
||||
mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64));
|
||||
last_sample_time = time;
|
||||
frames_since_last_sample = 0;
|
||||
}
|
||||
|
||||
// find the map from assembly space to world space
|
||||
let location = {
|
||||
let u = -location_z;
|
||||
DMatrix::from_column_slice(5, 5, &[
|
||||
1.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0, u,
|
||||
0.0, 0.0, 2.0*u, 1.0, u*u,
|
||||
0.0, 0.0, 0.0, 0.0, 1.0
|
||||
])
|
||||
};
|
||||
let assembly_to_world = &location * &orientation;
|
||||
|
||||
// get the assembly
|
||||
let (
|
||||
elt_cnt,
|
||||
reps_world,
|
||||
colors,
|
||||
highlights
|
||||
) = state.assembly.elements.with(|elts| {
|
||||
(
|
||||
// number of elements
|
||||
elts.len() as i32,
|
||||
|
||||
// representation vectors in world coordinates
|
||||
elts.iter().map(
|
||||
|(_, elt)| elt.representation.with(|rep| &assembly_to_world * rep)
|
||||
).collect::<Vec<_>>(),
|
||||
|
||||
// colors
|
||||
elts.iter().map(|(key, elt)| {
|
||||
if state.selection.with(|sel| sel.contains(&key)) {
|
||||
elt.color.map(|ch| 0.2 + 0.8*ch)
|
||||
} else {
|
||||
elt.color
|
||||
}
|
||||
}).collect::<Vec<_>>(),
|
||||
|
||||
// highlight levels
|
||||
elts.iter().map(|(key, _)| {
|
||||
if state.selection.with(|sel| sel.contains(&key)) {
|
||||
1.0_f32
|
||||
} else {
|
||||
HIGHLIGHT
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
)
|
||||
});
|
||||
|
||||
// set the resolution
|
||||
let width = canvas.width() as f32;
|
||||
let height = canvas.height() as f32;
|
||||
ctx.uniform2f(resolution_loc.as_ref(), width, height);
|
||||
ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
|
||||
|
||||
// pass the assembly
|
||||
ctx.uniform1i(sphere_cnt_loc.as_ref(), elt_cnt);
|
||||
for n in 0..reps_world.len() {
|
||||
let v = &reps_world[n];
|
||||
ctx.uniform3f(
|
||||
sphere_sp_locs[n].as_ref(),
|
||||
v[0] as f32, v[1] as f32, v[2] as f32
|
||||
);
|
||||
ctx.uniform2f(
|
||||
sphere_lt_locs[n].as_ref(),
|
||||
v[3] as f32, v[4] as f32
|
||||
);
|
||||
ctx.uniform3fv_with_f32_array(
|
||||
color_locs[n].as_ref(),
|
||||
&colors[n]
|
||||
);
|
||||
ctx.uniform1f(
|
||||
highlight_locs[n].as_ref(),
|
||||
highlights[n]
|
||||
);
|
||||
}
|
||||
|
||||
// pass the display parameters
|
||||
ctx.uniform1f(opacity_loc.as_ref(), OPACITY);
|
||||
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
|
||||
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
|
||||
|
||||
// draw the scene
|
||||
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
|
||||
|
||||
// clear the scene change flag
|
||||
scene_changed.set(
|
||||
pitch_up_val != 0.0
|
||||
|| pitch_down_val != 0.0
|
||||
|| yaw_left_val != 0.0
|
||||
|| yaw_right_val != 0.0
|
||||
|| roll_cw_val != 0.0
|
||||
|| roll_ccw_val != 0.0
|
||||
|| zoom_in_val != 0.0
|
||||
|| zoom_out_val != 0.0
|
||||
|| turntable_val /* BENCHMARKING */
|
||||
);
|
||||
} else {
|
||||
frames_since_last_sample = 0;
|
||||
mean_frame_interval.set(-1.0);
|
||||
}
|
||||
});
|
||||
start_animation_loop();
|
||||
});
|
||||
|
||||
let set_nav_signal = move |event: KeyboardEvent, value: f64| {
|
||||
let mut navigating = true;
|
||||
let shift = event.shift_key();
|
||||
match event.key().as_str() {
|
||||
"ArrowUp" if shift => zoom_in.set(value),
|
||||
"ArrowDown" if shift => zoom_out.set(value),
|
||||
"ArrowUp" => pitch_up.set(value),
|
||||
"ArrowDown" => pitch_down.set(value),
|
||||
"ArrowRight" if shift => roll_cw.set(value),
|
||||
"ArrowLeft" if shift => roll_ccw.set(value),
|
||||
"ArrowRight" => yaw_right.set(value),
|
||||
"ArrowLeft" => yaw_left.set(value),
|
||||
_ => navigating = false
|
||||
};
|
||||
if navigating {
|
||||
scene_changed.set(true);
|
||||
event.prevent_default();
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
/* TO DO */
|
||||
// switch back to integer-valued parameters when that becomes possible
|
||||
// again
|
||||
canvas(
|
||||
ref=display,
|
||||
width="600",
|
||||
height="600",
|
||||
tabindex="0",
|
||||
on:keydown=move |event: KeyboardEvent| {
|
||||
if event.key() == "Shift" {
|
||||
roll_cw.set(yaw_right.get());
|
||||
roll_ccw.set(yaw_left.get());
|
||||
zoom_in.set(pitch_up.get());
|
||||
zoom_out.set(pitch_down.get());
|
||||
yaw_right.set(0.0);
|
||||
yaw_left.set(0.0);
|
||||
pitch_up.set(0.0);
|
||||
pitch_down.set(0.0);
|
||||
} else {
|
||||
if event.key() == "Enter" { /* BENCHMARKING */
|
||||
turntable.set_fn(|turn| !turn);
|
||||
scene_changed.set(true);
|
||||
}
|
||||
set_nav_signal(event, 1.0);
|
||||
}
|
||||
},
|
||||
on:keyup=move |event: KeyboardEvent| {
|
||||
if event.key() == "Shift" {
|
||||
yaw_right.set(roll_cw.get());
|
||||
yaw_left.set(roll_ccw.get());
|
||||
pitch_up.set(zoom_in.get());
|
||||
pitch_down.set(zoom_out.get());
|
||||
roll_cw.set(0.0);
|
||||
roll_ccw.set(0.0);
|
||||
zoom_in.set(0.0);
|
||||
zoom_out.set(0.0);
|
||||
} else {
|
||||
set_nav_signal(event, 0.0);
|
||||
}
|
||||
},
|
||||
on:blur=move |_| {
|
||||
pitch_up.set(0.0);
|
||||
pitch_down.set(0.0);
|
||||
yaw_right.set(0.0);
|
||||
yaw_left.set(0.0);
|
||||
roll_ccw.set(0.0);
|
||||
roll_cw.set(0.0);
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
1
app-proto/src/lib.rs
Normal file
1
app-proto/src/lib.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod engine;
|
|
@ -1,33 +1,59 @@
|
|||
mod add_remove;
|
||||
mod assembly;
|
||||
mod display;
|
||||
mod components;
|
||||
mod engine;
|
||||
mod outline;
|
||||
mod specified;
|
||||
|
||||
use rustc_hash::FxHashSet;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::{collections::BTreeSet, rc::Rc};
|
||||
use sycamore::prelude::*;
|
||||
|
||||
use add_remove::AddRemove;
|
||||
use assembly::{Assembly, ElementKey};
|
||||
use display::Display;
|
||||
use outline::Outline;
|
||||
use assembly::{Assembly, Element};
|
||||
use components::{
|
||||
add_remove::AddRemove,
|
||||
diagnostics::Diagnostics,
|
||||
display::Display,
|
||||
outline::Outline
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
assembly: Assembly,
|
||||
selection: Signal<FxHashSet<ElementKey>>
|
||||
selection: Signal<BTreeSet<Rc<dyn Element>>>
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
fn new() -> AppState {
|
||||
AppState {
|
||||
assembly: Assembly::new(),
|
||||
selection: create_signal(FxHashSet::default())
|
||||
selection: create_signal(BTreeSet::default())
|
||||
}
|
||||
}
|
||||
|
||||
// in single-selection mode, select the given element. in multiple-selection
|
||||
// mode, toggle whether the given element is selected
|
||||
fn select(&self, element: &Rc<dyn Element>, multi: bool) {
|
||||
if multi {
|
||||
self.selection.update(|sel| {
|
||||
if !sel.remove(element) {
|
||||
sel.insert(element.clone());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
self.selection.update(|sel| {
|
||||
sel.clear();
|
||||
sel.insert(element.clone());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// set the console error panic hook
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
sycamore::render(|| {
|
||||
provide_context(AppState::new());
|
||||
|
||||
|
@ -35,6 +61,7 @@ fn main() {
|
|||
div(id="sidebar") {
|
||||
AddRemove {}
|
||||
Outline {}
|
||||
Diagnostics {}
|
||||
}
|
||||
Display {}
|
||||
}
|
||||
|
|
|
@ -1,207 +0,0 @@
|
|||
use itertools::Itertools;
|
||||
use sycamore::prelude::*;
|
||||
use web_sys::{
|
||||
Event,
|
||||
HtmlInputElement,
|
||||
KeyboardEvent,
|
||||
MouseEvent,
|
||||
wasm_bindgen::JsCast
|
||||
};
|
||||
|
||||
use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}};
|
||||
|
||||
// an editable view of the Lorentz product representing a constraint
|
||||
#[component(inline_props)]
|
||||
fn LorentzProductInput(constraint: Constraint) -> View {
|
||||
view! {
|
||||
input(
|
||||
r#type="text",
|
||||
bind:value=constraint.lorentz_prod_text,
|
||||
on:change=move |event: Event| {
|
||||
let target: HtmlInputElement = event.target().unwrap().unchecked_into();
|
||||
match target.value().parse::<f64>() {
|
||||
Ok(lorentz_prod) => batch(|| {
|
||||
constraint.lorentz_prod.set(lorentz_prod);
|
||||
constraint.lorentz_prod_valid.set(true);
|
||||
}),
|
||||
Err(_) => constraint.lorentz_prod_valid.set(false)
|
||||
};
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// a list item that shows a constraint in an outline view of an element
|
||||
#[component(inline_props)]
|
||||
fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) -> View {
|
||||
let state = use_context::<AppState>();
|
||||
let assembly = &state.assembly;
|
||||
let constraint = assembly.constraints.with(|csts| csts[constraint_key].clone());
|
||||
let other_subject = if constraint.subjects.0 == element_key {
|
||||
constraint.subjects.1
|
||||
} else {
|
||||
constraint.subjects.0
|
||||
};
|
||||
let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone());
|
||||
let class = constraint.lorentz_prod_valid.map(
|
||||
|&lorentz_prod_valid| if lorentz_prod_valid { "constraint" } else { "constraint invalid" }
|
||||
);
|
||||
view! {
|
||||
li(class=class.get()) {
|
||||
input(r#type="checkbox", bind:checked=constraint.active)
|
||||
div(class="constraint-label") { (other_subject_label) }
|
||||
LorentzProductInput(constraint=constraint)
|
||||
div(class="status")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a list item that shows an element in an outline view of an assembly
|
||||
#[component(inline_props)]
|
||||
fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
|
||||
let state = use_context::<AppState>();
|
||||
let class = state.selection.map(
|
||||
move |sel| if sel.contains(&key) { "selected" } else { "" }
|
||||
);
|
||||
let label = element.label.clone();
|
||||
let rep_components = element.representation.map(
|
||||
|rep| rep.iter().map(
|
||||
|u| format!("{:.3}", u).replace("-", "\u{2212}")
|
||||
).collect()
|
||||
);
|
||||
let constrained = element.constraints.map(|csts| csts.len() > 0);
|
||||
let constraint_list = element.constraints.map(
|
||||
|csts| csts.clone().into_iter().collect()
|
||||
);
|
||||
let details_node = create_node_ref();
|
||||
view! {
|
||||
li {
|
||||
details(ref=details_node) {
|
||||
summary(
|
||||
class=class.get(),
|
||||
on:keydown={
|
||||
move |event: KeyboardEvent| {
|
||||
match event.key().as_str() {
|
||||
"Enter" => {
|
||||
if event.shift_key() {
|
||||
state.selection.update(|sel| {
|
||||
if !sel.remove(&key) {
|
||||
sel.insert(key);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
state.selection.update(|sel| {
|
||||
sel.clear();
|
||||
sel.insert(key);
|
||||
});
|
||||
}
|
||||
event.prevent_default();
|
||||
},
|
||||
"ArrowRight" if constrained.get() => {
|
||||
let _ = details_node
|
||||
.get()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.set_attribute("open", "");
|
||||
},
|
||||
"ArrowLeft" => {
|
||||
let _ = details_node
|
||||
.get()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.remove_attribute("open");
|
||||
},
|
||||
_ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
div(
|
||||
class="element-switch",
|
||||
on:click=|event: MouseEvent| event.stop_propagation()
|
||||
)
|
||||
div(
|
||||
class="element",
|
||||
on:click={
|
||||
move |event: MouseEvent| {
|
||||
if event.shift_key() {
|
||||
state.selection.update(|sel| {
|
||||
if !sel.remove(&key) {
|
||||
sel.insert(key);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
state.selection.update(|sel| {
|
||||
sel.clear();
|
||||
sel.insert(key);
|
||||
});
|
||||
}
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
) {
|
||||
div(class="element-label") { (label) }
|
||||
div(class="element-representation") {
|
||||
Indexed(
|
||||
list=rep_components,
|
||||
view=|coord_str| view! {
|
||||
div { (coord_str) }
|
||||
}
|
||||
)
|
||||
}
|
||||
div(class="status")
|
||||
}
|
||||
}
|
||||
ul(class="constraints") {
|
||||
Keyed(
|
||||
list=constraint_list,
|
||||
view=move |cst_key| view! {
|
||||
ConstraintOutlineItem(
|
||||
constraint_key=cst_key,
|
||||
element_key=key
|
||||
)
|
||||
},
|
||||
key=|cst_key| cst_key.clone()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a component that lists the elements of the current assembly, showing the
|
||||
// constraints on each element as a collapsible sub-list. its implementation
|
||||
// is based on Kate Morley's HTML + CSS tree views:
|
||||
//
|
||||
// https://iamkate.com/code/tree-views/
|
||||
//
|
||||
#[component]
|
||||
pub fn Outline() -> View {
|
||||
let state = use_context::<AppState>();
|
||||
|
||||
// list the elements alphabetically by ID
|
||||
let element_list = state.assembly.elements.map(
|
||||
|elts| elts
|
||||
.clone()
|
||||
.into_iter()
|
||||
.sorted_by_key(|(_, elt)| elt.id.clone())
|
||||
.collect()
|
||||
);
|
||||
|
||||
view! {
|
||||
ul(
|
||||
id="outline",
|
||||
on:click={
|
||||
let state = use_context::<AppState>();
|
||||
move |_| state.selection.update(|sel| sel.clear())
|
||||
}
|
||||
) {
|
||||
Keyed(
|
||||
list=element_list,
|
||||
view=|(key, elt)| view! {
|
||||
ElementOutlineItem(key=key, element=elt)
|
||||
},
|
||||
key=|(key, _)| key.clone()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
44
app-proto/src/specified.rs
Normal file
44
app-proto/src/specified.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use std::num::ParseFloatError;
|
||||
|
||||
// a real number described by a specification string. since the structure is
|
||||
// read-only, we can guarantee that `spec` always specifies `value` in the
|
||||
// following format
|
||||
// ┌──────────────────────────────────────────────────────┬───────────┐
|
||||
// │ `spec` │ `value` │
|
||||
// ┝━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━┥
|
||||
// │ a string that parses to the floating-point value `x` │ `Some(x)` │
|
||||
// ├──────────────────────────────────────────────────────┼───────────┤
|
||||
// │ the empty string │ `None` │
|
||||
// └──────────────────────────────────────────────────────┴───────────┘
|
||||
#[readonly::make]
|
||||
pub struct SpecifiedValue {
|
||||
pub spec: String,
|
||||
pub value: Option<f64>
|
||||
}
|
||||
|
||||
impl SpecifiedValue {
|
||||
pub fn from_empty_spec() -> SpecifiedValue {
|
||||
SpecifiedValue { spec: String::new(), value: None }
|
||||
}
|
||||
|
||||
pub fn is_present(&self) -> bool {
|
||||
matches!(self.value, Some(_))
|
||||
}
|
||||
}
|
||||
|
||||
// a `SpecifiedValue` can be constructed from a specification string, formatted
|
||||
// as described in the comment on the structure definition. the result is `Ok`
|
||||
// if the specification is properly formatted, and `Error` if not
|
||||
impl TryFrom<String> for SpecifiedValue {
|
||||
type Error = ParseFloatError;
|
||||
|
||||
fn try_from(spec: String) -> Result<Self, Self::Error> {
|
||||
if spec.is_empty() {
|
||||
Ok(SpecifiedValue::from_empty_spec())
|
||||
} else {
|
||||
spec.parse::<f64>().map(
|
||||
|value| SpecifiedValue { spec: spec, value: Some(value) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
14
app-proto/src/tests.rs
Normal file
14
app-proto/src/tests.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use std::process::Command;
|
||||
|
||||
// build and bundle the application, reporting success if there are no errors or
|
||||
// warnings. to see this test fail while others succeed, try moving `index.html`
|
||||
// or one of the assets that it links to
|
||||
#[test]
|
||||
fn trunk_build_test() {
|
||||
let build_status = Command::new("trunk")
|
||||
.arg("build")
|
||||
.env("RUSTFLAGS", "-D warnings")
|
||||
.status()
|
||||
.expect("Call to Trunk failed");
|
||||
assert!(build_status.success());
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue