forked from StudioInfinity/dyna3
Compare commits
13 commits
curvature-
...
main
Author | SHA1 | Date | |
---|---|---|---|
af18a8e7d1 | |||
a4565281d5 | |||
ef1a579ac0 | |||
2eba80fb69 | |||
0801200210 | |||
5864017e6f | |||
4cb3262555 | |||
e447e7ea96 | |||
a671a8273a | |||
2adf4669f4 | |||
a2478febc1 | |||
360ce12d8b | |||
23ba5acad7 |
33 changed files with 4428 additions and 1771 deletions
|
@ -11,7 +11,7 @@ jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: cimg/rust:1.85-node
|
image: cimg/rust:1.86-node
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
# set the default working directory for each `run` step, relative to the
|
# set the default working directory for each `run` step, relative to the
|
||||||
|
|
48
README.md
48
README.md
|
@ -25,32 +25,37 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter
|
||||||
### Install the prerequisites
|
### Install the prerequisites
|
||||||
|
|
||||||
1. Install [`rustup`](https://rust-lang.github.io/rustup/): the officially recommended Rust toolchain manager
|
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)
|
- 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"
|
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
|
- 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)
|
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/)
|
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
|
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
|
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
|
- 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
|
- On POSIX systems, the search path is stored in the `PATH` environment variable
|
||||||
|
|
||||||
### Play with the prototype
|
### Play with the prototype
|
||||||
|
|
||||||
1. Go into the `app-proto` folder
|
1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype
|
||||||
2. Call `trunk serve --release` to build and serve the prototype
|
- The crates the prototype depends on will be downloaded and served automatically
|
||||||
* *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
|
||||||
* *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:`
|
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*
|
- 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
|
4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype
|
||||||
|
|
||||||
### Run the engine on some example problems
|
### Run the engine on some example problems
|
||||||
|
|
||||||
1. Go into the `app-proto` folder
|
1. Use `sh` to run the script `tools/run-examples.sh`
|
||||||
2. Call `./run-examples`
|
- The script is location-independent, so you can do this from anywhere in the dyna3 repository
|
||||||
* *For each example problem, the engine will print the value of the loss function at each optimization step*
|
- The call from the top level of the repository is:
|
||||||
* *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*
|
|
||||||
|
```bash
|
||||||
|
sh tools/run-examples.sh
|
||||||
|
```
|
||||||
|
- 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
|
```julia
|
||||||
include("irisawa-hexlet.jl")
|
include("irisawa-hexlet.jl")
|
||||||
|
@ -59,9 +64,24 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter
|
||||||
end
|
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*
|
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
|
### Run the automated tests
|
||||||
|
|
||||||
1. Go into the `app-proto` folder
|
1. Go into the `app-proto` folder
|
||||||
2. Call `cargo test`
|
2. Call `cargo test`
|
||||||
|
|
||||||
|
### Deploy the prototype
|
||||||
|
|
||||||
|
1. From the `app-proto` folder, call `trunk build --release`
|
||||||
|
- Building in [release mode](https://doc.rust-lang.org/cargo/reference/profiles.html#release) produces an executable which is smaller and often much faster, but harder to debug and more time-consuming to build
|
||||||
|
- If you want to stay in the top-level folder, you can call `trunk build --config app-proto --release` from there instead
|
||||||
|
2. Use `sh` to run the packaging script `tools/package-for-deployment.sh`.
|
||||||
|
- The script is location-independent, so you can do this from anywhere in the dyna3 repository
|
||||||
|
- The call from the top level of the repository is:
|
||||||
|
```bash
|
||||||
|
sh tools/package-for-deployment.sh
|
||||||
|
```
|
||||||
|
- This will overwrite or replace the files in `deploy/dyna3`
|
||||||
|
3. Put the contents of `deploy/dyna3` in the folder on your server that the prototype will be served from.
|
||||||
|
- To simplify uploading, you might want to combine these files into an archive called `deploy/dyna3.zip`. Git has been set to ignore this path
|
605
app-proto/Cargo.lock
generated
605
app-proto/Cargo.lock
generated
|
@ -1,6 +1,6 @@
|
||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
|
@ -20,6 +20,21 @@ version = "0.2.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android-tzdata"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "approx"
|
name = "approx"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
@ -35,6 +50,21 @@ version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.16.0"
|
version = "3.16.0"
|
||||||
|
@ -68,6 +98,35 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charming"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "73ffae2e616ae7d66b2e9ea369f1c7650042bdcdc1dc08b04b027107007b4f09"
|
||||||
|
dependencies = [
|
||||||
|
"handlebars",
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||||
|
dependencies = [
|
||||||
|
"android-tzdata",
|
||||||
|
"iana-time-zone",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console_error_panic_hook"
|
name = "console_error_panic_hook"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
|
@ -78,10 +137,122 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"darling_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_core"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_macro"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_core",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dyna3"
|
name = "dyna3"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"charming",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"dyna3",
|
"dyna3",
|
||||||
"itertools",
|
"itertools",
|
||||||
|
@ -89,8 +260,6 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"nalgebra",
|
"nalgebra",
|
||||||
"readonly",
|
"readonly",
|
||||||
"rustc-hash",
|
|
||||||
"slab",
|
|
||||||
"sycamore",
|
"sycamore",
|
||||||
"wasm-bindgen-test",
|
"wasm-bindgen-test",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
@ -108,6 +277,22 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fnv"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
@ -119,6 +304,28 @@ dependencies = [
|
||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "handlebars"
|
||||||
|
version = "6.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder",
|
||||||
|
"log",
|
||||||
|
"num-order",
|
||||||
|
"pest",
|
||||||
|
"pest_derive",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
|
@ -129,6 +336,12 @@ dependencies = [
|
||||||
"allocator-api2",
|
"allocator-api2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "html-escape"
|
name = "html-escape"
|
||||||
version = "0.2.13"
|
version = "0.2.13"
|
||||||
|
@ -138,6 +351,47 @@ dependencies = [
|
||||||
"utf8-width",
|
"utf8-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.63"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "1.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"hashbrown 0.12.3",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
@ -145,7 +399,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.14.5",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -157,6 +412,12 @@ dependencies = [
|
||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.70"
|
version = "0.3.70"
|
||||||
|
@ -194,6 +455,12 @@ dependencies = [
|
||||||
"rawpointer",
|
"rawpointer",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minicov"
|
name = "minicov"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
|
@ -250,6 +517,12 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.46"
|
version = "0.1.46"
|
||||||
|
@ -259,6 +532,21 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-modular"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-order"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6"
|
||||||
|
dependencies = [
|
||||||
|
"num-modular",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-rational"
|
name = "num-rational"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
|
@ -291,6 +579,57 @@ version = "1.0.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"thiserror",
|
||||||
|
"ucd-trie",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_derive"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_generator",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_generator"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_meta",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_meta"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"pest",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.20"
|
version = "0.2.20"
|
||||||
|
@ -366,10 +705,10 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "ryu"
|
||||||
version = "2.0.0"
|
version = "1.0.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "safe_arch"
|
name = "safe_arch"
|
||||||
|
@ -395,6 +734,90 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde-wasm-bindgen"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.140"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with"
|
||||||
|
version = "3.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"chrono",
|
||||||
|
"hex",
|
||||||
|
"indexmap 1.9.3",
|
||||||
|
"indexmap 2.5.0",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with_macros",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with_macros"
|
||||||
|
version = "3.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
@ -414,15 +837,6 @@ dependencies = [
|
||||||
"wide",
|
"wide",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "slab"
|
|
||||||
version = "0.4.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slotmap"
|
name = "slotmap"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -439,13 +853,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore"
|
name = "strsim"
|
||||||
version = "0.9.0-beta.3"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dedaf7237c05913604a5b0b2536b613f6c8510c6b213d2583b1294869755cabd"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sycamore"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown",
|
"hashbrown 0.14.5",
|
||||||
"indexmap",
|
"indexmap 2.5.0",
|
||||||
"paste",
|
"paste",
|
||||||
"sycamore-core",
|
"sycamore-core",
|
||||||
"sycamore-macro",
|
"sycamore-macro",
|
||||||
|
@ -457,20 +877,20 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore-core"
|
name = "sycamore-core"
|
||||||
version = "0.9.0-beta.3"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5ddddc3d1bcb38c04ad55d2d1ab4f6a358e4daaeae0a0436892f1fade9fb31a"
|
checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown",
|
"hashbrown 0.14.5",
|
||||||
"paste",
|
"paste",
|
||||||
"sycamore-reactive",
|
"sycamore-reactive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore-macro"
|
name = "sycamore-macro"
|
||||||
version = "0.9.0-beta.3"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77181c27cb753e86065308901871ccc7456fb19527b6a4ffacad3b63175ed014"
|
checksum = "a0c1d2eddc94db6d03e67eb832df5512b967e81053a573cd01bf3e1c3db00137"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
@ -482,20 +902,21 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore-reactive"
|
name = "sycamore-reactive"
|
||||||
version = "0.9.0-beta.3"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7aa6870203507c07e850687c0ccf528eb0f04240e3596bac9137007ffb6c50b1"
|
checksum = "f2bacf810535efc2701187a716a5652197ad241d620d5b00fb12caa6dfa23add"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"paste",
|
"paste",
|
||||||
"slotmap",
|
"slotmap",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore-view-parser"
|
name = "sycamore-view-parser"
|
||||||
version = "0.9.0-beta.3"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6144640af2eafffc68a92f3aacbbfaa21f7fd31906e2336fe304fd100fe226b"
|
checksum = "6c22875843db83cd4d49c0123a195e433bdc74e13ed0fff4ace0e77bb0a67033"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -504,9 +925,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore-web"
|
name = "sycamore-web"
|
||||||
version = "0.9.0-beta.3"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bca93dcf1b1830bf1aac93508ed51babcda92c1d32d96067ab416d94e4b7c475"
|
checksum = "4b17aa5875f59f541cdf6fb58751ec702a6ed9801f30dd2b4d5f2279025b98bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"html-escape",
|
"html-escape",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
@ -522,21 +943,78 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.77"
|
version = "2.0.87"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
|
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"itoa",
|
||||||
|
"num-conv",
|
||||||
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ucd-trie"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.13"
|
version = "1.0.13"
|
||||||
|
@ -693,6 +1171,65 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.59.0"
|
version = "0.59.0"
|
||||||
|
|
|
@ -3,6 +3,7 @@ name = "dyna3"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Aaron Fenyes", "Glen Whitney"]
|
authors = ["Aaron Fenyes", "Glen Whitney"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
rust-version = "1.86"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["console_error_panic_hook"]
|
default = ["console_error_panic_hook"]
|
||||||
|
@ -14,9 +15,10 @@ js-sys = "0.3.70"
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
nalgebra = "0.33.0"
|
nalgebra = "0.33.0"
|
||||||
readonly = "0.2.12"
|
readonly = "0.2.12"
|
||||||
rustc-hash = "2.0.0"
|
sycamore = "0.9.1"
|
||||||
slab = "0.4.9"
|
|
||||||
sycamore = "0.9.0-beta.3"
|
# 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
|
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||||
# logging them with `console.error`. This is great for development, but requires
|
# logging them with `console.error`. This is great for development, but requires
|
||||||
|
@ -47,6 +49,13 @@ features = [
|
||||||
dyna3 = { path = ".", default-features = false, features = ["dev"] }
|
dyna3 = { path = ".", default-features = false, features = ["dev"] }
|
||||||
wasm-bindgen-test = "0.3.34"
|
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]
|
[profile.release]
|
||||||
opt-level = "s" # optimize for small code size
|
opt-level = "s" # optimize for small code size
|
||||||
debug = true # include debug symbols
|
debug = true # include debug symbols
|
||||||
|
|
2
app-proto/Trunk.toml
Normal file
2
app-proto/Trunk.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[build]
|
||||||
|
public_url = "./"
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,25 +1,23 @@
|
||||||
use dyna3::engine::{Q, examples::realize_irisawa_hexlet};
|
#[path = "common/print.rs"]
|
||||||
|
mod print;
|
||||||
|
|
||||||
|
use dyna3::engine::{ConfigNeighborhood, examples::realize_irisawa_hexlet};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
const SCALED_TOL: f64 = 1.0e-12;
|
const SCALED_TOL: f64 = 1.0e-12;
|
||||||
let (config, _, success, history) = realize_irisawa_hexlet(SCALED_TOL);
|
let realization = realize_irisawa_hexlet(SCALED_TOL);
|
||||||
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
|
print::title("Irisawa hexlet");
|
||||||
if success {
|
print::realization_diagnostics(&realization);
|
||||||
println!("Target accuracy achieved!");
|
if let Ok(ConfigNeighborhood { config, .. }) = realization.result {
|
||||||
} else {
|
// print the diameters of the chain spheres
|
||||||
println!("Failed to reach target accuracy");
|
|
||||||
}
|
|
||||||
println!("Steps: {}", history.scaled_loss.len() - 1);
|
|
||||||
println!("Loss: {}", history.scaled_loss.last().unwrap());
|
|
||||||
if success {
|
|
||||||
println!("\nChain diameters:");
|
println!("\nChain diameters:");
|
||||||
println!(" {} sun (given)", 1.0 / config[(3, 3)]);
|
println!(" {} sun (given)", 1.0 / config[(3, 3)]);
|
||||||
for k in 4..9 {
|
for k in 4..9 {
|
||||||
println!(" {} sun", 1.0 / config[(3, k)]);
|
println!(" {} sun", 1.0 / config[(3, k)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// print the completed Gram matrix
|
||||||
|
print::gram_matrix(&config);
|
||||||
}
|
}
|
||||||
println!("\nStep │ Loss\n─────┼────────────────────────────────");
|
print::loss_history(&realization.history);
|
||||||
for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() {
|
|
||||||
println!("{:<4} │ {}", step, scaled_loss);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,30 +1,32 @@
|
||||||
|
#[path = "common/print.rs"]
|
||||||
|
mod print;
|
||||||
|
|
||||||
use nalgebra::{DMatrix, DVector};
|
use nalgebra::{DMatrix, DVector};
|
||||||
|
|
||||||
use dyna3::engine::{Q, examples::realize_kaleidocycle};
|
use dyna3::engine::{ConfigNeighborhood, examples::realize_kaleidocycle};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
const SCALED_TOL: f64 = 1.0e-12;
|
const SCALED_TOL: f64 = 1.0e-12;
|
||||||
let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL);
|
let realization = realize_kaleidocycle(SCALED_TOL);
|
||||||
print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config);
|
print::title("Kaleidocycle");
|
||||||
print!("Configuration:{}", config);
|
print::realization_diagnostics(&realization);
|
||||||
if success {
|
if let Ok(ConfigNeighborhood { config, nbhd: tangent }) = realization.result {
|
||||||
println!("Target accuracy achieved!");
|
// print the completed Gram matrix and the realized configuration
|
||||||
} else {
|
print::gram_matrix(&config);
|
||||||
println!("Failed to reach target accuracy");
|
print::config(&config);
|
||||||
}
|
|
||||||
println!("Steps: {}", history.scaled_loss.len() - 1);
|
|
||||||
println!("Loss: {}\n", history.scaled_loss.last().unwrap());
|
|
||||||
|
|
||||||
// find the kaleidocycle's twist motion by projecting onto the tangent space
|
// find the kaleidocycle's twist motion by projecting onto the tangent
|
||||||
|
// space
|
||||||
const N_POINTS: usize = 12;
|
const N_POINTS: usize = 12;
|
||||||
let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]);
|
let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]);
|
||||||
let down = -&up;
|
let down = -&up;
|
||||||
let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map(
|
let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map(
|
||||||
|n| [
|
|n| [
|
||||||
tangent.proj(&up.as_view(), n),
|
tangent.proj(&up.as_view(), n),
|
||||||
tangent.proj(&down.as_view(), n+1)
|
tangent.proj(&down.as_view(), n+1),
|
||||||
]
|
]
|
||||||
).sum();
|
).sum();
|
||||||
let normalization = 5.0 / twist_motion[(2, 0)];
|
let normalization = 5.0 / twist_motion[(2, 0)];
|
||||||
print!("Twist motion:{}", normalization * twist_motion);
|
println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end());
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,38 +1,33 @@
|
||||||
use nalgebra::DMatrix;
|
#[path = "common/print.rs"]
|
||||||
|
mod print;
|
||||||
|
|
||||||
use dyna3::engine::{Q, point, realize_gram, sphere, PartialMatrix};
|
use dyna3::engine::{
|
||||||
|
point,
|
||||||
|
realize_gram,
|
||||||
|
sphere,
|
||||||
|
ConfigNeighborhood,
|
||||||
|
ConstraintProblem,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let gram = {
|
let mut problem = ConstraintProblem::from_guess(&[
|
||||||
let mut gram_to_be = PartialMatrix::new();
|
|
||||||
for j in 0..2 {
|
|
||||||
for k in j..2 {
|
|
||||||
gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gram_to_be
|
|
||||||
};
|
|
||||||
let guess = DMatrix::from_columns(&[
|
|
||||||
point(0.0, 0.0, 2.0),
|
point(0.0, 0.0, 2.0),
|
||||||
sphere(0.0, 0.0, 0.0, 1.0)
|
sphere(0.0, 0.0, 0.0, 1.0)
|
||||||
]);
|
]);
|
||||||
let frozen = [(3, 0)];
|
for j in 0..2 {
|
||||||
println!();
|
for k in j..2 {
|
||||||
let (config, _, success, history) = realize_gram(
|
problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 });
|
||||||
&gram, guess, &frozen,
|
}
|
||||||
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
}
|
||||||
|
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!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
|
print::title("Point on a sphere");
|
||||||
print!("Configuration:{}", config);
|
print::realization_diagnostics(&realization);
|
||||||
if success {
|
if let Ok(ConfigNeighborhood { config, .. }) = realization.result {
|
||||||
println!("Target accuracy achieved!");
|
print::gram_matrix(&config);
|
||||||
} else {
|
print::config(&config);
|
||||||
println!("Failed to reach target accuracy");
|
|
||||||
}
|
|
||||||
println!("Steps: {}", history.scaled_loss.len() - 1);
|
|
||||||
println!("Loss: {}", history.scaled_loss.last().unwrap());
|
|
||||||
println!("\nStep │ Loss\n─────┼────────────────────────────────");
|
|
||||||
for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() {
|
|
||||||
println!("{:<4} │ {}", step, scaled_loss);
|
|
||||||
}
|
}
|
||||||
|
print::loss_history(&realization.history);
|
||||||
}
|
}
|
|
@ -1,40 +1,34 @@
|
||||||
use nalgebra::DMatrix;
|
#[path = "common/print.rs"]
|
||||||
|
mod print;
|
||||||
|
|
||||||
use dyna3::engine::{Q, realize_gram, sphere, PartialMatrix};
|
use dyna3::engine::{
|
||||||
|
realize_gram,
|
||||||
|
sphere,
|
||||||
|
ConfigNeighborhood,
|
||||||
|
ConstraintProblem,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let gram = {
|
let mut problem = ConstraintProblem::from_guess({
|
||||||
let mut gram_to_be = PartialMatrix::new();
|
|
||||||
for j in 0..3 {
|
|
||||||
for k in j..3 {
|
|
||||||
gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gram_to_be
|
|
||||||
};
|
|
||||||
let guess = {
|
|
||||||
let a: f64 = 0.75_f64.sqrt();
|
let a: f64 = 0.75_f64.sqrt();
|
||||||
DMatrix::from_columns(&[
|
&[
|
||||||
sphere(1.0, 0.0, 0.0, 1.0),
|
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),
|
||||||
sphere(-0.5, -a, 0.0, 1.0)
|
sphere(-0.5, -a, 0.0, 1.0),
|
||||||
])
|
]
|
||||||
};
|
});
|
||||||
println!();
|
for j in 0..3 {
|
||||||
let (config, _, success, history) = realize_gram(
|
for k in j..3 {
|
||||||
&gram, guess, &[],
|
problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
|
||||||
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
}
|
||||||
|
}
|
||||||
|
let realization = realize_gram(
|
||||||
|
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||||
);
|
);
|
||||||
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
|
print::title("Three spheres");
|
||||||
if success {
|
print::realization_diagnostics(&realization);
|
||||||
println!("Target accuracy achieved!");
|
if let Ok(ConfigNeighborhood { config, .. }) = realization.result {
|
||||||
} else {
|
print::gram_matrix(&config);
|
||||||
println!("Failed to reach target accuracy");
|
|
||||||
}
|
|
||||||
println!("Steps: {}", history.scaled_loss.len() - 1);
|
|
||||||
println!("Loss: {}", history.scaled_loss.last().unwrap());
|
|
||||||
println!("\nStep │ Loss\n─────┼────────────────────────────────");
|
|
||||||
for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() {
|
|
||||||
println!("{:<4} │ {}", step, scaled_loss);
|
|
||||||
}
|
}
|
||||||
|
print::loss_history(&realization.history);
|
||||||
}
|
}
|
|
@ -6,6 +6,12 @@
|
||||||
<link data-trunk rel="css" href="main.css"/>
|
<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=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">
|
<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>
|
</head>
|
||||||
<body></body>
|
<body></body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -18,6 +18,17 @@ body {
|
||||||
font-family: 'Fira Sans', sans-serif;
|
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 */
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
|
@ -42,9 +53,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
#add-remove > button {
|
#add-remove > button {
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
height: 32px;
|
||||||
font-size: large;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* KLUDGE */
|
/* KLUDGE */
|
||||||
|
@ -53,7 +62,9 @@ body {
|
||||||
buttons need to be displayed in an emoji font
|
buttons need to be displayed in an emoji font
|
||||||
*/
|
*/
|
||||||
#add-remove > button.emoji {
|
#add-remove > button.emoji {
|
||||||
|
width: 32px;
|
||||||
font-family: 'Noto Emoji', sans-serif;
|
font-family: 'Noto Emoji', sans-serif;
|
||||||
|
font-size: large;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* outline */
|
/* outline */
|
||||||
|
@ -90,6 +101,10 @@ summary > div, .regulator {
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.element > input {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.element-switch {
|
.element-switch {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
|
@ -134,6 +149,7 @@ details[open]:has(li) .element-switch::after {
|
||||||
}
|
}
|
||||||
|
|
||||||
.regulator-input {
|
.regulator-input {
|
||||||
|
margin-right: 4px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
@ -155,22 +171,56 @@ details[open]:has(li) .element-switch::after {
|
||||||
border-color: var(--border-invalid);
|
border-color: var(--border-invalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
|
||||||
width: 20px;
|
|
||||||
padding-left: 4px;
|
|
||||||
text-align: center;
|
|
||||||
font-family: 'Noto Emoji';
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after {
|
.regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after {
|
||||||
content: '⚠';
|
content: '⚠';
|
||||||
color: var(--text-invalid);
|
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 */
|
/* display */
|
||||||
|
|
||||||
canvas {
|
#display {
|
||||||
float: left;
|
float: left;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
@ -179,7 +229,7 @@ canvas {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas:focus {
|
#display:focus {
|
||||||
border-color: var(--border-focus-dark);
|
border-color: var(--border-focus-dark);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# run all Cargo examples, as described here:
|
|
||||||
#
|
|
||||||
# Karol Kuczmarski. "Add examples to your Rust libraries"
|
|
||||||
# http://xion.io/post/code/rust-examples.html
|
|
||||||
#
|
|
||||||
|
|
||||||
cargo run --example irisawa-hexlet
|
|
||||||
cargo run --example three-spheres
|
|
||||||
cargo run --example point-on-sphere
|
|
||||||
cargo run --example kaleidocycle
|
|
|
@ -1,208 +0,0 @@
|
||||||
use sycamore::prelude::*;
|
|
||||||
use web_sys::{console, wasm_bindgen::JsValue};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
engine,
|
|
||||||
AppState,
|
|
||||||
assembly::{Assembly, 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())
|
|
||||||
}
|
|
||||||
);
|
|
||||||
state.assembly.insert_new_regulator(subjects);
|
|
||||||
state.selection.update(|sel| sel.clear());
|
|
||||||
}
|
|
||||||
) { "🔗" }
|
|
||||||
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;
|
69
app-proto/src/components/add_remove.rs
Normal file
69
app-proto/src/components/add_remove.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
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>();
|
||||||
|
batch(|| {
|
||||||
|
// this call is batched to avoid redundant realizations.
|
||||||
|
// it updates the element list and the regulator list,
|
||||||
|
// which are both tracked by the realization effect
|
||||||
|
/* TO DO */
|
||||||
|
// it would make more to do the batching inside
|
||||||
|
// `insert_element_default`, but that will have to wait
|
||||||
|
// until Sycamore handles nested batches correctly.
|
||||||
|
//
|
||||||
|
// https://github.com/sycamore-rs/sycamore/issues/802
|
||||||
|
//
|
||||||
|
// the nested batch issue is relevant here because the
|
||||||
|
// assembly loaders in the test assembly chooser use
|
||||||
|
// `insert_element_default` within larger batches
|
||||||
|
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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
256
app-proto/src/components/diagnostics.rs
Normal file
256
app-proto/src/components/diagnostics.rs
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
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) -> Self {
|
||||||
|
Self { 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 {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
921
app-proto/src/components/display.rs
Normal file
921
app-proto/src/components/display.rs
Normal file
|
@ -0,0 +1,921 @@
|
||||||
|
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() -> Self {
|
||||||
|
Self {
|
||||||
|
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() -> Self {
|
||||||
|
Self {
|
||||||
|
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() -> Self {
|
||||||
|
Self {
|
||||||
|
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()),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
260
app-proto/src/components/outline.rs
Normal file
260
app-proto/src/components/outline.rs
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
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;
|
const int SPHERE_MAX = 200;
|
||||||
uniform int sphere_cnt;
|
uniform int sphere_cnt;
|
||||||
uniform vecInv sphere_list[SPHERE_MAX];
|
uniform vecInv sphere_list[SPHERE_MAX];
|
||||||
uniform vec3 color_list[SPHERE_MAX];
|
uniform vec4 color_list[SPHERE_MAX];
|
||||||
uniform float highlight_list[SPHERE_MAX];
|
uniform float highlight_list[SPHERE_MAX];
|
||||||
|
|
||||||
// view
|
// view
|
||||||
|
@ -25,7 +25,6 @@ uniform vec2 resolution;
|
||||||
uniform float shortdim;
|
uniform float shortdim;
|
||||||
|
|
||||||
// controls
|
// controls
|
||||||
uniform float opacity;
|
|
||||||
uniform int layer_threshold;
|
uniform int layer_threshold;
|
||||||
uniform bool debug_mode;
|
uniform bool debug_mode;
|
||||||
|
|
||||||
|
@ -69,7 +68,7 @@ struct Fragment {
|
||||||
vec4 color;
|
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
|
// 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
|
// negative gradient of the lorentz product between the impact point vector
|
||||||
// and the sphere vector with respect to the coordinates of the impact
|
// 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 incidence = dot(normal, light_dir);
|
||||||
float illum = mix(0.4, 1.0, max(incidence, 0.0));
|
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) {
|
float intersection_dist(Fragment a, Fragment b) {
|
||||||
|
@ -192,10 +191,11 @@ void main() {
|
||||||
vec3 color = vec3(0.);
|
vec3 color = vec3(0.);
|
||||||
int layer = layer_cnt - 1;
|
int layer = layer_cnt - 1;
|
||||||
TaggedDepth hit = top_hits[layer];
|
TaggedDepth hit = top_hits[layer];
|
||||||
|
vec4 sphere_color = color_list[hit.id];
|
||||||
Fragment frag_next = sphere_shading(
|
Fragment frag_next = sphere_shading(
|
||||||
sphere_list[hit.id],
|
sphere_list[hit.id],
|
||||||
hit.depth * dir,
|
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];
|
float highlight_next = highlight_list[hit.id];
|
||||||
--layer;
|
--layer;
|
||||||
|
@ -206,10 +206,11 @@ void main() {
|
||||||
|
|
||||||
// shade the next fragment
|
// shade the next fragment
|
||||||
hit = top_hits[layer];
|
hit = top_hits[layer];
|
||||||
|
sphere_color = color_list[hit.id];
|
||||||
frag_next = sphere_shading(
|
frag_next = sphere_shading(
|
||||||
sphere_list[hit.id],
|
sphere_list[hit.id],
|
||||||
hit.depth * dir,
|
hit.depth * dir,
|
||||||
hit.dimming * color_list[hit.id]
|
vec4(hit.dimming * sphere_color.rgb, sphere_color.a)
|
||||||
);
|
);
|
||||||
highlight_next = highlight_list[hit.id];
|
highlight_next = highlight_list[hit.id];
|
||||||
|
|
941
app-proto/src/components/test_assembly_chooser.rs
Normal file
941
app-proto/src/components/test_assembly_chooser.rs
Normal file
|
@ -0,0 +1,941 @@
|
||||||
|
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,
|
||||||
|
assembly::{
|
||||||
|
Assembly,
|
||||||
|
Element,
|
||||||
|
ElementColor,
|
||||||
|
InversiveDistanceRegulator,
|
||||||
|
Point,
|
||||||
|
Sphere,
|
||||||
|
},
|
||||||
|
engine,
|
||||||
|
engine::DescentHistory,
|
||||||
|
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_general(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_curvature(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(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_tridiminished_icosahedron(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_dodecahedral_packing(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(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(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(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(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;
|
||||||
|
|
||||||
|
// 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_general(assembly),
|
||||||
|
"low-curvature" => load_low_curvature(assembly),
|
||||||
|
"pointed" => load_pointed(assembly),
|
||||||
|
"tridiminished-icosahedron" => load_tridiminished_icosahedron(assembly),
|
||||||
|
"dodecahedral-packing" => load_dodecahedral_packing(assembly),
|
||||||
|
"balanced" => load_balanced(assembly),
|
||||||
|
"off-center" => load_off_center(assembly),
|
||||||
|
"radius-ratio" => load_radius_ratio(assembly),
|
||||||
|
"irisawa-hexlet" => load_irisawa_hexlet(assembly),
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// build the chooser
|
||||||
|
view! {
|
||||||
|
select(bind:value = assembly_name) {
|
||||||
|
option(value = "general") { "General" }
|
||||||
|
option(value = "low-curvature") { "Low-curvature" }
|
||||||
|
option(value = "pointed") { "Pointed" }
|
||||||
|
option(value = "tridiminished-icosahedron") { "Tridiminished icosahedron" }
|
||||||
|
option(value = "dodecahedral-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,613 +0,0 @@
|
||||||
use core::array;
|
|
||||||
use nalgebra::{DMatrix, DVector, Rotation3, Vector3};
|
|
||||||
use sycamore::{prelude::*, motion::create_raf};
|
|
||||||
use web_sys::{
|
|
||||||
console,
|
|
||||||
window,
|
|
||||||
Element,
|
|
||||||
KeyboardEvent,
|
|
||||||
MouseEvent,
|
|
||||||
WebGl2RenderingContext,
|
|
||||||
WebGlProgram,
|
|
||||||
WebGlShader,
|
|
||||||
WebGlUniformLocation,
|
|
||||||
wasm_bindgen::{JsCast, JsValue}
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{AppState, assembly::{ElementKey, ElementMotion}};
|
|
||||||
|
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// the direction in camera space that a mouse event is pointing along
|
|
||||||
fn event_dir(event: &MouseEvent) -> Vector3<f64> {
|
|
||||||
let target: 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 `inversive.frag`
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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 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 */
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
);
|
|
||||||
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 {
|
|
||||||
key: sel,
|
|
||||||
velocity: elt_motion.as_view()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
scene_changed.set(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 asm_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| &asm_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);
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
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 = event_dir(&event);
|
|
||||||
console::log_1(&JsValue::from(dir.to_string()));
|
|
||||||
let mut clicked: Option<(ElementKey, f64)> = None;
|
|
||||||
for (key, elt) in state.assembly.elements.get_clone_untracked() {
|
|
||||||
match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world)) {
|
|
||||||
Some(depth) => match clicked {
|
|
||||||
Some((_, best_depth)) => {
|
|
||||||
if depth < best_depth {
|
|
||||||
clicked = Some((key, depth))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => clicked = Some((key, depth))
|
|
||||||
}
|
|
||||||
None => ()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we clicked something, select it
|
|
||||||
match clicked {
|
|
||||||
Some((key, _)) => state.select(key, event.shift_key()),
|
|
||||||
None => state.selection.update(|sel| sel.clear())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,9 @@
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen};
|
use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen};
|
||||||
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
use std::fmt::{Display, Error, Formatter};
|
||||||
|
|
||||||
// --- elements ---
|
// --- elements ---
|
||||||
|
|
||||||
#[cfg(feature = "dev")]
|
|
||||||
pub fn point(x: f64, y: f64, z: f64) -> DVector<f64> {
|
pub fn point(x: f64, y: f64, z: f64) -> DVector<f64> {
|
||||||
DVector::from_column_slice(&[x, y, z, 0.5, 0.5*(x*x + y*y + z*z)])
|
DVector::from_column_slice(&[x, y, z, 0.5, 0.5*(x*x + y*y + z*z)])
|
||||||
}
|
}
|
||||||
|
@ -17,7 +16,7 @@ pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVect
|
||||||
center_y / radius,
|
center_y / radius,
|
||||||
center_z / radius,
|
center_z / radius,
|
||||||
0.5 / radius,
|
0.5 / radius,
|
||||||
0.5 * (center_norm_sq / radius - radius)
|
0.5 * (center_norm_sq / radius - radius),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,82 +30,131 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6
|
||||||
norm_sp * dir_y,
|
norm_sp * dir_y,
|
||||||
norm_sp * dir_z,
|
norm_sp * dir_z,
|
||||||
0.5 * curv,
|
0.5 * curv,
|
||||||
off * (1.0 + 0.5 * off * curv)
|
off * (1.0 + 0.5 * off * curv),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// project a sphere's representation vector to the normalization variety by
|
||||||
|
// contracting toward the last coordinate axis
|
||||||
|
pub fn project_sphere_to_normalized(rep: &mut DVector<f64>) {
|
||||||
|
let q_sp = rep.fixed_rows::<3>(0).norm_squared();
|
||||||
|
let half_q_lt = -2.0 * rep[3] * rep[4];
|
||||||
|
let half_q_lt_sq = half_q_lt * half_q_lt;
|
||||||
|
let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt();
|
||||||
|
rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling);
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalize a point's representation vector by scaling
|
||||||
|
pub fn project_point_to_normalized(rep: &mut DVector<f64>) {
|
||||||
|
rep.scale_mut(0.5 / rep[3]);
|
||||||
|
}
|
||||||
|
|
||||||
// --- partial matrices ---
|
// --- partial matrices ---
|
||||||
|
|
||||||
struct MatrixEntry {
|
pub struct MatrixEntry {
|
||||||
index: (usize, usize),
|
index: (usize, usize),
|
||||||
value: f64
|
value: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PartialMatrix(Vec<MatrixEntry>);
|
pub struct PartialMatrix(Vec<MatrixEntry>);
|
||||||
|
|
||||||
impl PartialMatrix {
|
impl PartialMatrix {
|
||||||
pub fn new() -> PartialMatrix {
|
pub fn new() -> Self {
|
||||||
PartialMatrix(Vec::<MatrixEntry>::new())
|
Self(Vec::<MatrixEntry>::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, row: usize, col: usize, value: f64) {
|
||||||
|
let Self(entries) = self;
|
||||||
|
entries.push(MatrixEntry { index: (row, col), value });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_sym(&mut self, row: usize, col: usize, value: f64) {
|
pub fn push_sym(&mut self, row: usize, col: usize, value: f64) {
|
||||||
let PartialMatrix(entries) = self;
|
self.push(row, col, value);
|
||||||
entries.push(MatrixEntry { index: (row, col), value: value });
|
|
||||||
if row != col {
|
if row != col {
|
||||||
entries.push(MatrixEntry { index: (col, row), value: value });
|
self.push(col, row, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* DEBUG */
|
fn freeze(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
|
||||||
pub fn log_to_console(&self) {
|
let mut result = a.clone();
|
||||||
let PartialMatrix(entries) = self;
|
for &MatrixEntry { index, value } in self {
|
||||||
for ent in entries {
|
result[index] = value;
|
||||||
let ent_str = format!(" {} {} {}", ent.index.0, ent.index.1, ent.value);
|
|
||||||
console::log_1(&JsValue::from(ent_str.as_str()));
|
|
||||||
}
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn proj(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
|
fn proj(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
|
||||||
let mut result = DMatrix::<f64>::zeros(a.nrows(), a.ncols());
|
let mut result = DMatrix::<f64>::zeros(a.nrows(), a.ncols());
|
||||||
let PartialMatrix(entries) = self;
|
for &MatrixEntry { index, .. } in self {
|
||||||
for ent in entries {
|
result[index] = a[index];
|
||||||
result[ent.index] = a[ent.index];
|
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sub_proj(&self, rhs: &DMatrix<f64>) -> DMatrix<f64> {
|
fn sub_proj(&self, rhs: &DMatrix<f64>) -> DMatrix<f64> {
|
||||||
let mut result = DMatrix::<f64>::zeros(rhs.nrows(), rhs.ncols());
|
let mut result = DMatrix::<f64>::zeros(rhs.nrows(), rhs.ncols());
|
||||||
let PartialMatrix(entries) = self;
|
for &MatrixEntry { index, value } in self {
|
||||||
for ent in entries {
|
result[index] = value - rhs[index];
|
||||||
result[ent.index] = ent.value - rhs[ent.index];
|
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for PartialMatrix {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
|
||||||
|
for &MatrixEntry { index: (row, col), value } in self {
|
||||||
|
writeln!(f, " {row} {col} {value}")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for PartialMatrix {
|
||||||
|
type Item = MatrixEntry;
|
||||||
|
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
let Self(entries) = self;
|
||||||
|
entries.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoIterator for &'a PartialMatrix {
|
||||||
|
type Item = &'a MatrixEntry;
|
||||||
|
type IntoIter = std::slice::Iter<'a, MatrixEntry>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
let PartialMatrix(entries) = self;
|
||||||
|
entries.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- configuration subspaces ---
|
// --- configuration subspaces ---
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ConfigSubspace {
|
pub struct ConfigSubspace {
|
||||||
assembly_dim: usize,
|
assembly_dim: usize,
|
||||||
basis_std: Vec<DMatrix<f64>>,
|
basis_std: Vec<DMatrix<f64>>,
|
||||||
basis_proj: Vec<DMatrix<f64>>
|
basis_proj: Vec<DMatrix<f64>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigSubspace {
|
impl ConfigSubspace {
|
||||||
pub fn zero(assembly_dim: usize) -> ConfigSubspace {
|
pub fn zero(assembly_dim: usize) -> Self {
|
||||||
ConfigSubspace {
|
Self {
|
||||||
assembly_dim: assembly_dim,
|
assembly_dim,
|
||||||
basis_proj: Vec::new(),
|
basis_proj: Vec::new(),
|
||||||
basis_std: Vec::new()
|
basis_std: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// approximate the kernel of a symmetric endomorphism of the configuration
|
// approximate the kernel of a symmetric endomorphism of the configuration
|
||||||
// space for `assembly_dim` elements. we consider an eigenvector to be part
|
// space for `assembly_dim` elements. we consider an eigenvector to be part
|
||||||
// of the kernel if its eigenvalue is smaller than the constant `THRESHOLD`
|
// of the kernel if its eigenvalue is smaller than the constant `THRESHOLD`
|
||||||
fn symmetric_kernel(a: DMatrix<f64>, proj_to_std: DMatrix<f64>, assembly_dim: usize) -> ConfigSubspace {
|
fn symmetric_kernel(
|
||||||
|
a: DMatrix<f64>,
|
||||||
|
proj_to_std: DMatrix<f64>,
|
||||||
|
assembly_dim: usize,
|
||||||
|
) -> Self {
|
||||||
// find a basis for the kernel. the basis is expressed in the projection
|
// find a basis for the kernel. the basis is expressed in the projection
|
||||||
// coordinates, and it's orthonormal with respect to the projection
|
// coordinates, and it's orthonormal with respect to the projection
|
||||||
// inner product
|
// inner product
|
||||||
|
@ -120,20 +168,13 @@ impl ConfigSubspace {
|
||||||
).collect::<Vec<_>>().as_slice()
|
).collect::<Vec<_>>().as_slice()
|
||||||
);
|
);
|
||||||
|
|
||||||
/* DEBUG */
|
|
||||||
// print the eigenvalues
|
|
||||||
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
|
|
||||||
console::log_1(&JsValue::from(
|
|
||||||
format!("Eigenvalues used to find kernel:{}", eig.eigenvalues)
|
|
||||||
));
|
|
||||||
|
|
||||||
// express the basis in the standard coordinates
|
// express the basis in the standard coordinates
|
||||||
let basis_std = proj_to_std * &basis_proj;
|
let basis_std = proj_to_std * &basis_proj;
|
||||||
|
|
||||||
const ELEMENT_DIM: usize = 5;
|
const ELEMENT_DIM: usize = 5;
|
||||||
const UNIFORM_DIM: usize = 4;
|
const UNIFORM_DIM: usize = 4;
|
||||||
ConfigSubspace {
|
Self {
|
||||||
assembly_dim: assembly_dim,
|
assembly_dim,
|
||||||
basis_std: basis_std.column_iter().map(
|
basis_std: basis_std.column_iter().map(
|
||||||
|v| Into::<DMatrix<f64>>::into(
|
|v| Into::<DMatrix<f64>>::into(
|
||||||
v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim))
|
v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim))
|
||||||
|
@ -143,7 +184,7 @@ impl ConfigSubspace {
|
||||||
|v| Into::<DMatrix<f64>>::into(
|
|v| Into::<DMatrix<f64>>::into(
|
||||||
v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim))
|
v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim))
|
||||||
)
|
)
|
||||||
).collect()
|
).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,24 +218,52 @@ pub struct DescentHistory {
|
||||||
pub config: Vec<DMatrix<f64>>,
|
pub config: Vec<DMatrix<f64>>,
|
||||||
pub scaled_loss: Vec<f64>,
|
pub scaled_loss: Vec<f64>,
|
||||||
pub neg_grad: Vec<DMatrix<f64>>,
|
pub neg_grad: Vec<DMatrix<f64>>,
|
||||||
pub min_eigval: Vec<f64>,
|
pub hess_eigvals: Vec<DVector<f64>>,
|
||||||
pub base_step: Vec<DMatrix<f64>>,
|
pub base_step: Vec<DMatrix<f64>>,
|
||||||
pub backoff_steps: Vec<i32>
|
pub backoff_steps: Vec<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DescentHistory {
|
impl DescentHistory {
|
||||||
fn new() -> DescentHistory {
|
pub fn new() -> Self {
|
||||||
DescentHistory {
|
Self {
|
||||||
config: Vec::<DMatrix<f64>>::new(),
|
config: Vec::<DMatrix<f64>>::new(),
|
||||||
scaled_loss: Vec::<f64>::new(),
|
scaled_loss: Vec::<f64>::new(),
|
||||||
neg_grad: Vec::<DMatrix<f64>>::new(),
|
neg_grad: Vec::<DMatrix<f64>>::new(),
|
||||||
min_eigval: Vec::<f64>::new(),
|
hess_eigvals: Vec::<DVector<f64>>::new(),
|
||||||
base_step: Vec::<DMatrix<f64>>::new(),
|
base_step: Vec::<DMatrix<f64>>::new(),
|
||||||
backoff_steps: Vec::<i32>::new(),
|
backoff_steps: Vec::<i32>::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- constraint problems ---
|
||||||
|
|
||||||
|
pub struct ConstraintProblem {
|
||||||
|
pub gram: PartialMatrix,
|
||||||
|
pub frozen: PartialMatrix,
|
||||||
|
pub guess: DMatrix<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConstraintProblem {
|
||||||
|
pub fn new(element_count: usize) -> Self {
|
||||||
|
const ELEMENT_DIM: usize = 5;
|
||||||
|
Self {
|
||||||
|
gram: PartialMatrix::new(),
|
||||||
|
frozen: PartialMatrix::new(),
|
||||||
|
guess: DMatrix::<f64>::zeros(ELEMENT_DIM, element_count),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "dev")]
|
||||||
|
pub fn from_guess(guess_columns: &[DVector<f64>]) -> Self {
|
||||||
|
Self {
|
||||||
|
gram: PartialMatrix::new(),
|
||||||
|
frozen: PartialMatrix::new(),
|
||||||
|
guess: DMatrix::from_columns(guess_columns),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- gram matrix realization ---
|
// --- gram matrix realization ---
|
||||||
|
|
||||||
// the Lorentz form
|
// the Lorentz form
|
||||||
|
@ -204,25 +273,21 @@ lazy_static! {
|
||||||
0.0, 1.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, 0.0,
|
0.0, 0.0, 1.0, 0.0, 0.0,
|
||||||
0.0, 0.0, 0.0, 0.0, -2.0,
|
0.0, 0.0, 0.0, 0.0, -2.0,
|
||||||
0.0, 0.0, 0.0, -2.0, 0.0
|
0.0, 0.0, 0.0, -2.0, 0.0,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SearchState {
|
struct SearchState {
|
||||||
config: DMatrix<f64>,
|
config: DMatrix<f64>,
|
||||||
err_proj: DMatrix<f64>,
|
err_proj: DMatrix<f64>,
|
||||||
loss: f64
|
loss: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SearchState {
|
impl SearchState {
|
||||||
fn from_config(gram: &PartialMatrix, config: DMatrix<f64>) -> SearchState {
|
fn from_config(gram: &PartialMatrix, config: DMatrix<f64>) -> Self {
|
||||||
let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config));
|
let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config));
|
||||||
let loss = err_proj.norm_squared();
|
let loss = err_proj.norm_squared();
|
||||||
SearchState {
|
Self { config, err_proj, loss }
|
||||||
config: config,
|
|
||||||
err_proj: err_proj,
|
|
||||||
loss: loss
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,7 +314,7 @@ pub fn local_unif_to_std(v: DVectorView<f64>) -> DMatrix<f64> {
|
||||||
curv, 0.0, 0.0, 0.0, v[0],
|
curv, 0.0, 0.0, 0.0, v[0],
|
||||||
0.0, curv, 0.0, 0.0, v[1],
|
0.0, curv, 0.0, 0.0, v[1],
|
||||||
0.0, 0.0, curv, 0.0, v[2],
|
0.0, 0.0, curv, 0.0, v[2],
|
||||||
0.0, 0.0, 0.0, 0.0, 1.0
|
0.0, 0.0, 0.0, 0.0, 1.0,
|
||||||
])
|
])
|
||||||
} else {
|
} else {
|
||||||
// `v` represents a sphere. the normalization condition says that the
|
// `v` represents a sphere. the normalization condition says that the
|
||||||
|
@ -258,7 +323,7 @@ pub fn local_unif_to_std(v: DVectorView<f64>) -> DMatrix<f64> {
|
||||||
curv, 0.0, 0.0, 0.0, v[0],
|
curv, 0.0, 0.0, 0.0, v[0],
|
||||||
0.0, curv, 0.0, 0.0, v[1],
|
0.0, curv, 0.0, 0.0, v[1],
|
||||||
0.0, 0.0, curv, 0.0, v[2],
|
0.0, 0.0, curv, 0.0, v[2],
|
||||||
curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0
|
curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -271,7 +336,7 @@ fn seek_better_config(
|
||||||
base_target_improvement: f64,
|
base_target_improvement: f64,
|
||||||
min_efficiency: f64,
|
min_efficiency: f64,
|
||||||
backoff: f64,
|
backoff: f64,
|
||||||
max_backoff_steps: i32
|
max_backoff_steps: i32,
|
||||||
) -> Option<(SearchState, i32)> {
|
) -> Option<(SearchState, i32)> {
|
||||||
let mut rate = 1.0;
|
let mut rate = 1.0;
|
||||||
for backoff_steps in 0..max_backoff_steps {
|
for backoff_steps in 0..max_backoff_steps {
|
||||||
|
@ -286,25 +351,52 @@ fn seek_better_config(
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
// seek a matrix `config` for which `config' * Q * config` matches the partial
|
// a first-order neighborhood of a configuration
|
||||||
// matrix `gram`. use gradient descent starting from `guess`
|
pub struct ConfigNeighborhood {
|
||||||
|
pub config: DMatrix<f64>,
|
||||||
|
pub nbhd: ConfigSubspace,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Realization {
|
||||||
|
pub result: Result<ConfigNeighborhood, String>,
|
||||||
|
pub history: DescentHistory,
|
||||||
|
}
|
||||||
|
|
||||||
|
// seek a matrix `config` that matches the partial matrix `problem.frozen` and
|
||||||
|
// has `config' * Q * config` matching the partial matrix `problem.gram`. start
|
||||||
|
// at `problem.guess`, set the frozen entries to their desired values, and then
|
||||||
|
// use a regularized Newton's method to seek the desired Gram matrix
|
||||||
pub fn realize_gram(
|
pub fn realize_gram(
|
||||||
gram: &PartialMatrix,
|
problem: &ConstraintProblem,
|
||||||
guess: DMatrix<f64>,
|
|
||||||
frozen: &[(usize, usize)],
|
|
||||||
scaled_tol: f64,
|
scaled_tol: f64,
|
||||||
min_efficiency: f64,
|
min_efficiency: f64,
|
||||||
backoff: f64,
|
backoff: f64,
|
||||||
reg_scale: f64,
|
reg_scale: f64,
|
||||||
max_descent_steps: i32,
|
max_descent_steps: i32,
|
||||||
max_backoff_steps: i32
|
max_backoff_steps: i32,
|
||||||
) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
|
) -> Realization {
|
||||||
|
// destructure the problem data
|
||||||
|
let ConstraintProblem { gram, guess, frozen } = problem;
|
||||||
|
|
||||||
// start the descent history
|
// start the descent history
|
||||||
let mut history = DescentHistory::new();
|
let mut history = DescentHistory::new();
|
||||||
|
|
||||||
|
// handle the case where the assembly is empty. our general realization
|
||||||
|
// routine can't handle this case because it builds the Hessian using
|
||||||
|
// `DMatrix::from_columns`, which panics when the list of columns is empty
|
||||||
|
let assembly_dim = guess.ncols();
|
||||||
|
if assembly_dim == 0 {
|
||||||
|
let result = Ok(
|
||||||
|
ConfigNeighborhood {
|
||||||
|
config: guess.clone(),
|
||||||
|
nbhd: ConfigSubspace::zero(0),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return Realization { result, history };
|
||||||
|
}
|
||||||
|
|
||||||
// find the dimension of the search space
|
// find the dimension of the search space
|
||||||
let element_dim = guess.nrows();
|
let element_dim = guess.nrows();
|
||||||
let assembly_dim = guess.ncols();
|
|
||||||
let total_dim = element_dim * assembly_dim;
|
let total_dim = element_dim * assembly_dim;
|
||||||
|
|
||||||
// scale the tolerance
|
// scale the tolerance
|
||||||
|
@ -313,11 +405,11 @@ pub fn realize_gram(
|
||||||
|
|
||||||
// convert the frozen indices to stacked format
|
// convert the frozen indices to stacked format
|
||||||
let frozen_stacked: Vec<usize> = frozen.into_iter().map(
|
let frozen_stacked: Vec<usize> = frozen.into_iter().map(
|
||||||
|index| index.1*element_dim + index.0
|
|MatrixEntry { index: (row, col), .. }| col*element_dim + row
|
||||||
).collect();
|
).collect();
|
||||||
|
|
||||||
// use Newton's method with backtracking and gradient descent backup
|
// use a regularized Newton's method with backtracking
|
||||||
let mut state = SearchState::from_config(gram, guess);
|
let mut state = SearchState::from_config(gram, frozen.freeze(guess));
|
||||||
let mut hess = DMatrix::zeros(element_dim, assembly_dim);
|
let mut hess = DMatrix::zeros(element_dim, assembly_dim);
|
||||||
for _ in 0..max_descent_steps {
|
for _ in 0..max_descent_steps {
|
||||||
// find the negative gradient of the loss function
|
// find the negative gradient of the loss function
|
||||||
|
@ -345,11 +437,12 @@ pub fn realize_gram(
|
||||||
hess = DMatrix::from_columns(hess_cols.as_slice());
|
hess = DMatrix::from_columns(hess_cols.as_slice());
|
||||||
|
|
||||||
// regularize the Hessian
|
// regularize the Hessian
|
||||||
let min_eigval = hess.symmetric_eigenvalues().min();
|
let hess_eigvals = hess.symmetric_eigenvalues();
|
||||||
|
let min_eigval = hess_eigvals.min();
|
||||||
if min_eigval <= 0.0 {
|
if min_eigval <= 0.0 {
|
||||||
hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim);
|
hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim);
|
||||||
}
|
}
|
||||||
history.min_eigval.push(min_eigval);
|
history.hess_eigvals.push(hess_eigvals);
|
||||||
|
|
||||||
// project the negative gradient and negative Hessian onto the
|
// project the negative gradient and negative Hessian onto the
|
||||||
// orthogonal complement of the frozen subspace
|
// orthogonal complement of the frozen subspace
|
||||||
|
@ -368,30 +461,40 @@ pub fn realize_gram(
|
||||||
if state.loss < tol { break; }
|
if state.loss < tol { break; }
|
||||||
|
|
||||||
// compute the Newton step
|
// compute the Newton step
|
||||||
|
/* TO DO */
|
||||||
/*
|
/*
|
||||||
we need to either handle or eliminate the case where the minimum
|
we should change our regularization to ensure that the Hessian is
|
||||||
eigenvalue of the Hessian is zero, so the regularized Hessian is
|
is positive-definite, rather than just positive-semidefinite. ideally,
|
||||||
singular. right now, this causes the Cholesky decomposition to return
|
that would guarantee the success of the Cholesky decomposition---
|
||||||
`None`, leading to a panic when we unrap
|
although we'd still need the error-handling routine in case of
|
||||||
|
numerical hiccups
|
||||||
*/
|
*/
|
||||||
let base_step_stacked = hess.clone().cholesky().unwrap().solve(&neg_grad_stacked);
|
let hess_cholesky = match hess.clone().cholesky() {
|
||||||
|
Some(cholesky) => cholesky,
|
||||||
|
None => return Realization {
|
||||||
|
result: Err("Cholesky decomposition failed".to_string()),
|
||||||
|
history,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked);
|
||||||
let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim));
|
let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim));
|
||||||
history.base_step.push(base_step.clone());
|
history.base_step.push(base_step.clone());
|
||||||
|
|
||||||
// use backtracking line search to find a better configuration
|
// use backtracking line search to find a better configuration
|
||||||
match seek_better_config(
|
if let Some((better_state, backoff_steps)) = seek_better_config(
|
||||||
gram, &state, &base_step, neg_grad.dot(&base_step),
|
gram, &state, &base_step, neg_grad.dot(&base_step),
|
||||||
min_efficiency, backoff, max_backoff_steps
|
min_efficiency, backoff, max_backoff_steps,
|
||||||
) {
|
) {
|
||||||
Some((better_state, backoff_steps)) => {
|
|
||||||
state = better_state;
|
state = better_state;
|
||||||
history.backoff_steps.push(backoff_steps);
|
history.backoff_steps.push(backoff_steps);
|
||||||
},
|
} else {
|
||||||
None => return (state.config, ConfigSubspace::zero(assembly_dim), false, history)
|
return Realization {
|
||||||
|
result: Err("Line search failed".to_string()),
|
||||||
|
history,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let success = state.loss < tol;
|
}
|
||||||
let tangent = if success {
|
let result = if state.loss < tol {
|
||||||
// express the uniform basis in the standard basis
|
// express the uniform basis in the standard basis
|
||||||
const UNIFORM_DIM: usize = 4;
|
const UNIFORM_DIM: usize = 4;
|
||||||
let total_dim_unif = UNIFORM_DIM * assembly_dim;
|
let total_dim_unif = UNIFORM_DIM * assembly_dim;
|
||||||
|
@ -404,18 +507,20 @@ pub fn realize_gram(
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the kernel of the Hessian. give it the uniform inner product
|
// find the kernel of the Hessian. give it the uniform inner product
|
||||||
ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim)
|
let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim);
|
||||||
|
|
||||||
|
Ok(ConfigNeighborhood { config: state.config, nbhd: tangent })
|
||||||
} else {
|
} else {
|
||||||
ConfigSubspace::zero(assembly_dim)
|
Err("Failed to reach target accuracy".to_string())
|
||||||
};
|
};
|
||||||
(state.config, tangent, success, history)
|
Realization { result, history }
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- tests ---
|
// --- tests ---
|
||||||
|
|
||||||
#[cfg(feature = "dev")]
|
#[cfg(feature = "dev")]
|
||||||
pub mod examples {
|
pub mod examples {
|
||||||
use std::{array, f64::consts::PI};
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -427,40 +532,12 @@ pub mod examples {
|
||||||
// "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki
|
// "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki
|
||||||
// https://www.nippon.com/en/japan-topics/c12801/
|
// https://www.nippon.com/en/japan-topics/c12801/
|
||||||
//
|
//
|
||||||
pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
|
pub fn realize_irisawa_hexlet(scaled_tol: f64) -> Realization {
|
||||||
let gram = {
|
let mut problem = ConstraintProblem::from_guess(
|
||||||
let mut gram_to_be = PartialMatrix::new();
|
|
||||||
for s in 0..9 {
|
|
||||||
// each sphere is represented by a spacelike vector
|
|
||||||
gram_to_be.push_sym(s, s, 1.0);
|
|
||||||
|
|
||||||
// the circumscribing sphere is tangent to all of the other
|
|
||||||
// spheres, with matching orientation
|
|
||||||
if s > 0 {
|
|
||||||
gram_to_be.push_sym(0, s, 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if s > 2 {
|
|
||||||
// each chain sphere is tangent to the "sun" and "moon"
|
|
||||||
// spheres, with opposing orientation
|
|
||||||
for n in 1..3 {
|
|
||||||
gram_to_be.push_sym(s, n, -1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// each chain sphere is tangent to the next chain sphere,
|
|
||||||
// with opposing orientation
|
|
||||||
let s_next = 3 + (s-2) % 6;
|
|
||||||
gram_to_be.push_sym(s, s_next, -1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gram_to_be
|
|
||||||
};
|
|
||||||
|
|
||||||
let guess = DMatrix::from_columns(
|
|
||||||
[
|
[
|
||||||
sphere(0.0, 0.0, 0.0, 15.0),
|
sphere(0.0, 0.0, 0.0, 15.0),
|
||||||
sphere(0.0, 0.0, -9.0, 5.0),
|
sphere(0.0, 0.0, -9.0, 5.0),
|
||||||
sphere(0.0, 0.0, 11.0, 3.0)
|
sphere(0.0, 0.0, 11.0, 3.0),
|
||||||
].into_iter().chain(
|
].into_iter().chain(
|
||||||
(1..=6).map(
|
(1..=6).map(
|
||||||
|k| {
|
|k| {
|
||||||
|
@ -471,42 +548,45 @@ pub mod examples {
|
||||||
).collect::<Vec<_>>().as_slice()
|
).collect::<Vec<_>>().as_slice()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for s in 0..9 {
|
||||||
|
// each sphere is represented by a spacelike vector
|
||||||
|
problem.gram.push_sym(s, s, 1.0);
|
||||||
|
|
||||||
|
// the circumscribing sphere is tangent to all of the other
|
||||||
|
// spheres, with matching orientation
|
||||||
|
if s > 0 {
|
||||||
|
problem.gram.push_sym(0, s, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if s > 2 {
|
||||||
|
// each chain sphere is tangent to the "sun" and "moon"
|
||||||
|
// spheres, with opposing orientation
|
||||||
|
for n in 1..3 {
|
||||||
|
problem.gram.push_sym(s, n, -1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// each chain sphere is tangent to the next chain sphere,
|
||||||
|
// with opposing orientation
|
||||||
|
let s_next = 3 + (s-2) % 6;
|
||||||
|
problem.gram.push_sym(s, s_next, -1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// the frozen entries fix the radii of the circumscribing sphere, the
|
// the frozen entries fix the radii of the circumscribing sphere, the
|
||||||
// "sun" and "moon" spheres, and one of the chain spheres
|
// "sun" and "moon" spheres, and one of the chain spheres
|
||||||
let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k));
|
for k in 0..4 {
|
||||||
|
problem.frozen.push(3, k, problem.guess[(3, k)]);
|
||||||
|
}
|
||||||
|
|
||||||
realize_gram(
|
realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110)
|
||||||
&gram, guess, &frozen,
|
|
||||||
scaled_tol, 0.5, 0.9, 1.1, 200, 110
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set up a kaleidocycle, made of points with fixed distances between them,
|
// set up a kaleidocycle, made of points with fixed distances between them,
|
||||||
// and find its tangent space
|
// and find its tangent space
|
||||||
pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
|
pub fn realize_kaleidocycle(scaled_tol: f64) -> Realization {
|
||||||
const N_POINTS: usize = 12;
|
|
||||||
let gram = {
|
|
||||||
let mut gram_to_be = PartialMatrix::new();
|
|
||||||
for block in (0..N_POINTS).step_by(2) {
|
|
||||||
let block_next = (block + 2) % N_POINTS;
|
|
||||||
for j in 0..2 {
|
|
||||||
// diagonal and hinge edges
|
|
||||||
for k in j..2 {
|
|
||||||
gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// non-hinge edges
|
|
||||||
for k in 0..2 {
|
|
||||||
gram_to_be.push_sym(block + j, block_next + k, -0.625);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gram_to_be
|
|
||||||
};
|
|
||||||
|
|
||||||
let guess = {
|
|
||||||
const N_HINGES: usize = 6;
|
const N_HINGES: usize = 6;
|
||||||
let guess_elts = (0..N_HINGES).step_by(2).flat_map(
|
let mut problem = ConstraintProblem::from_guess(
|
||||||
|
(0..N_HINGES).step_by(2).flat_map(
|
||||||
|n| {
|
|n| {
|
||||||
let ang_hor = (n as f64) * PI/3.0;
|
let ang_hor = (n as f64) * PI/3.0;
|
||||||
let ang_vert = ((n + 1) as f64) * PI/3.0;
|
let ang_vert = ((n + 1) as f64) * PI/3.0;
|
||||||
|
@ -516,19 +596,33 @@ pub mod examples {
|
||||||
point(0.0, 0.0, 0.0),
|
point(0.0, 0.0, 0.0),
|
||||||
point(ang_hor.cos(), ang_hor.sin(), 0.0),
|
point(ang_hor.cos(), ang_hor.sin(), 0.0),
|
||||||
point(x_vert, y_vert, -0.5),
|
point(x_vert, y_vert, -0.5),
|
||||||
point(x_vert, y_vert, 0.5)
|
point(x_vert, y_vert, 0.5),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
).collect::<Vec<_>>();
|
).collect::<Vec<_>>().as_slice()
|
||||||
DMatrix::from_columns(&guess_elts)
|
);
|
||||||
};
|
|
||||||
|
|
||||||
let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k));
|
const N_POINTS: usize = 2 * N_HINGES;
|
||||||
|
for block in (0..N_POINTS).step_by(2) {
|
||||||
|
let block_next = (block + 2) % N_POINTS;
|
||||||
|
for j in 0..2 {
|
||||||
|
// diagonal and hinge edges
|
||||||
|
for k in j..2 {
|
||||||
|
problem.gram.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 });
|
||||||
|
}
|
||||||
|
|
||||||
realize_gram(
|
// non-hinge edges
|
||||||
&gram, guess, &frozen,
|
for k in 0..2 {
|
||||||
scaled_tol, 0.5, 0.9, 1.1, 200, 110
|
problem.gram.push_sym(block + j, block_next + k, -0.625);
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k in 0..N_POINTS {
|
||||||
|
problem.frozen.push(3, k, problem.guess[(3, k)])
|
||||||
|
}
|
||||||
|
|
||||||
|
realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -539,82 +633,91 @@ mod tests {
|
||||||
|
|
||||||
use super::{*, examples::*};
|
use super::{*, examples::*};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freeze_test() {
|
||||||
|
let frozen = PartialMatrix(vec![
|
||||||
|
MatrixEntry { index: (0, 0), value: 14.0 },
|
||||||
|
MatrixEntry { index: (0, 2), value: 28.0 },
|
||||||
|
MatrixEntry { index: (1, 1), value: 42.0 },
|
||||||
|
MatrixEntry { index: (1, 2), value: 49.0 },
|
||||||
|
]);
|
||||||
|
let config = DMatrix::<f64>::from_row_slice(2, 3, &[
|
||||||
|
1.0, 2.0, 3.0,
|
||||||
|
4.0, 5.0, 6.0,
|
||||||
|
]);
|
||||||
|
let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[
|
||||||
|
14.0, 2.0, 28.0,
|
||||||
|
4.0, 42.0, 49.0,
|
||||||
|
]);
|
||||||
|
assert_eq!(frozen.freeze(&config), expected_result);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sub_proj_test() {
|
fn sub_proj_test() {
|
||||||
let target = PartialMatrix(vec![
|
let target = PartialMatrix(vec![
|
||||||
MatrixEntry { index: (0, 0), value: 19.0 },
|
MatrixEntry { index: (0, 0), value: 19.0 },
|
||||||
MatrixEntry { index: (0, 2), value: 39.0 },
|
MatrixEntry { index: (0, 2), value: 39.0 },
|
||||||
MatrixEntry { index: (1, 1), value: 59.0 },
|
MatrixEntry { index: (1, 1), value: 59.0 },
|
||||||
MatrixEntry { index: (1, 2), value: 69.0 }
|
MatrixEntry { index: (1, 2), value: 69.0 },
|
||||||
]);
|
]);
|
||||||
let attempt = DMatrix::<f64>::from_row_slice(2, 3, &[
|
let attempt = DMatrix::<f64>::from_row_slice(2, 3, &[
|
||||||
1.0, 2.0, 3.0,
|
1.0, 2.0, 3.0,
|
||||||
4.0, 5.0, 6.0
|
4.0, 5.0, 6.0,
|
||||||
]);
|
]);
|
||||||
let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[
|
let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[
|
||||||
18.0, 0.0, 36.0,
|
18.0, 0.0, 36.0,
|
||||||
0.0, 54.0, 63.0
|
0.0, 54.0, 63.0,
|
||||||
]);
|
]);
|
||||||
assert_eq!(target.sub_proj(&attempt), expected_result);
|
assert_eq!(target.sub_proj(&attempt), expected_result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn zero_loss_test() {
|
fn zero_loss_test() {
|
||||||
let gram = PartialMatrix({
|
let mut gram = PartialMatrix::new();
|
||||||
let mut entries = Vec::<MatrixEntry>::new();
|
|
||||||
for j in 0..3 {
|
for j in 0..3 {
|
||||||
for k in 0..3 {
|
for k in 0..3 {
|
||||||
entries.push(MatrixEntry {
|
gram.push(j, k, if j == k { 1.0 } else { -1.0 });
|
||||||
index: (j, k),
|
|
||||||
value: if j == k { 1.0 } else { -1.0 }
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entries
|
|
||||||
});
|
|
||||||
let config = {
|
let config = {
|
||||||
let a = 0.75_f64.sqrt();
|
let a = 0.75_f64.sqrt();
|
||||||
DMatrix::from_columns(&[
|
DMatrix::from_columns(&[
|
||||||
sphere(1.0, 0.0, 0.0, a),
|
sphere(1.0, 0.0, 0.0, a),
|
||||||
sphere(-0.5, a, 0.0, a),
|
sphere(-0.5, a, 0.0, a),
|
||||||
sphere(-0.5, -a, 0.0, a)
|
sphere(-0.5, -a, 0.0, a),
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
let state = SearchState::from_config(&gram, config);
|
let state = SearchState::from_config(&gram, config);
|
||||||
assert!(state.loss.abs() < f64::EPSILON);
|
assert!(state.loss.abs() < f64::EPSILON);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TO DO */
|
||||||
// at the frozen indices, the optimization steps should have exact zeros,
|
// at the frozen indices, the optimization steps should have exact zeros,
|
||||||
// and the realized configuration should match the initial guess
|
// and the realized configuration should have the desired values
|
||||||
#[test]
|
#[test]
|
||||||
fn frozen_entry_test() {
|
fn frozen_entry_test() {
|
||||||
let gram = {
|
let mut problem = ConstraintProblem::from_guess(&[
|
||||||
let mut gram_to_be = PartialMatrix::new();
|
point(0.0, 0.0, 2.0),
|
||||||
|
sphere(0.0, 0.0, 0.0, 0.95),
|
||||||
|
]);
|
||||||
for j in 0..2 {
|
for j in 0..2 {
|
||||||
for k in j..2 {
|
for k in j..2 {
|
||||||
gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 });
|
problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gram_to_be
|
problem.frozen.push(3, 0, problem.guess[(3, 0)]);
|
||||||
};
|
problem.frozen.push(3, 1, 0.5);
|
||||||
let guess = DMatrix::from_columns(&[
|
let Realization { result, history } = realize_gram(
|
||||||
point(0.0, 0.0, 2.0),
|
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||||
sphere(0.0, 0.0, 0.0, 1.0)
|
|
||||||
]);
|
|
||||||
let frozen = [(3, 0), (3, 1)];
|
|
||||||
println!();
|
|
||||||
let (config, _, success, history) = realize_gram(
|
|
||||||
&gram, guess.clone(), &frozen,
|
|
||||||
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
|
||||||
);
|
);
|
||||||
assert_eq!(success, true);
|
let config = result.unwrap().config;
|
||||||
for base_step in history.base_step.into_iter() {
|
for base_step in history.base_step.into_iter() {
|
||||||
for index in frozen {
|
for &MatrixEntry { index, .. } in &problem.frozen {
|
||||||
assert_eq!(base_step[index], 0.0);
|
assert_eq!(base_step[index], 0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for index in frozen {
|
for MatrixEntry { index, value } in problem.frozen {
|
||||||
assert_eq!(config[index], guess[index]);
|
assert_eq!(config[index], value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -622,7 +725,7 @@ mod tests {
|
||||||
fn irisawa_hexlet_test() {
|
fn irisawa_hexlet_test() {
|
||||||
// solve Irisawa's problem
|
// solve Irisawa's problem
|
||||||
const SCALED_TOL: f64 = 1.0e-12;
|
const SCALED_TOL: f64 = 1.0e-12;
|
||||||
let (config, _, _, _) = realize_irisawa_hexlet(SCALED_TOL);
|
let config = realize_irisawa_hexlet(SCALED_TOL).result.unwrap().config;
|
||||||
|
|
||||||
// check against Irisawa's solution
|
// check against Irisawa's solution
|
||||||
let entry_tol = SCALED_TOL.sqrt();
|
let entry_tol = SCALED_TOL.sqrt();
|
||||||
|
@ -635,34 +738,32 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn tangent_test_three_spheres() {
|
fn tangent_test_three_spheres() {
|
||||||
const SCALED_TOL: f64 = 1.0e-12;
|
const SCALED_TOL: f64 = 1.0e-12;
|
||||||
let gram = {
|
const ELEMENT_DIM: usize = 5;
|
||||||
let mut gram_to_be = PartialMatrix::new();
|
let mut problem = ConstraintProblem::from_guess(&[
|
||||||
for j in 0..3 {
|
|
||||||
for k in j..3 {
|
|
||||||
gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gram_to_be
|
|
||||||
};
|
|
||||||
let guess = DMatrix::from_columns(&[
|
|
||||||
sphere(0.0, 0.0, 0.0, -2.0),
|
sphere(0.0, 0.0, 0.0, -2.0),
|
||||||
sphere(0.0, 0.0, 1.0, 1.0),
|
sphere(0.0, 0.0, 1.0, 1.0),
|
||||||
sphere(0.0, 0.0, -1.0, 1.0)
|
sphere(0.0, 0.0, -1.0, 1.0),
|
||||||
]);
|
]);
|
||||||
let frozen: [_; 5] = std::array::from_fn(|k| (k, 0));
|
for j in 0..3 {
|
||||||
let (config, tangent, success, history) = realize_gram(
|
for k in j..3 {
|
||||||
&gram, guess.clone(), &frozen,
|
problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
|
||||||
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
}
|
||||||
|
}
|
||||||
|
for n in 0..ELEMENT_DIM {
|
||||||
|
problem.frozen.push(n, 0, problem.guess[(n, 0)]);
|
||||||
|
}
|
||||||
|
let Realization { result, history } = realize_gram(
|
||||||
|
&problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
||||||
);
|
);
|
||||||
assert_eq!(config, guess);
|
let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap();
|
||||||
assert_eq!(success, true);
|
assert_eq!(config, problem.guess);
|
||||||
assert_eq!(history.scaled_loss.len(), 1);
|
assert_eq!(history.scaled_loss.len(), 1);
|
||||||
|
|
||||||
// list some motions that should form a basis for the tangent space of
|
// list some motions that should form a basis for the tangent space of
|
||||||
// the solution variety
|
// the solution variety
|
||||||
const UNIFORM_DIM: usize = 4;
|
const UNIFORM_DIM: usize = 4;
|
||||||
let element_dim = guess.nrows();
|
let element_dim = problem.guess.nrows();
|
||||||
let assembly_dim = guess.ncols();
|
let assembly_dim = problem.guess.ncols();
|
||||||
let tangent_motions_unif = vec![
|
let tangent_motions_unif = vec![
|
||||||
basis_matrix((0, 1), UNIFORM_DIM, assembly_dim),
|
basis_matrix((0, 1), UNIFORM_DIM, assembly_dim),
|
||||||
basis_matrix((1, 1), UNIFORM_DIM, assembly_dim),
|
basis_matrix((1, 1), UNIFORM_DIM, assembly_dim),
|
||||||
|
@ -671,8 +772,8 @@ mod tests {
|
||||||
DMatrix::<f64>::from_column_slice(UNIFORM_DIM, assembly_dim, &[
|
DMatrix::<f64>::from_column_slice(UNIFORM_DIM, assembly_dim, &[
|
||||||
0.0, 0.0, 0.0, 0.0,
|
0.0, 0.0, 0.0, 0.0,
|
||||||
0.0, 0.0, -0.5, -0.5,
|
0.0, 0.0, -0.5, -0.5,
|
||||||
0.0, 0.0, -0.5, 0.5
|
0.0, 0.0, -0.5, 0.5,
|
||||||
])
|
]),
|
||||||
];
|
];
|
||||||
let tangent_motions_std = vec![
|
let tangent_motions_std = vec![
|
||||||
basis_matrix((0, 1), element_dim, assembly_dim),
|
basis_matrix((0, 1), element_dim, assembly_dim),
|
||||||
|
@ -682,8 +783,8 @@ mod tests {
|
||||||
DMatrix::<f64>::from_column_slice(element_dim, assembly_dim, &[
|
DMatrix::<f64>::from_column_slice(element_dim, assembly_dim, &[
|
||||||
0.0, 0.0, 0.0, 0.00, 0.0,
|
0.0, 0.0, 0.0, 0.00, 0.0,
|
||||||
0.0, 0.0, -1.0, -0.25, -1.0,
|
0.0, 0.0, -1.0, -0.25, -1.0,
|
||||||
0.0, 0.0, -1.0, 0.25, 1.0
|
0.0, 0.0, -1.0, 0.25, 1.0,
|
||||||
])
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
// confirm that the dimension of the tangent space is no greater than
|
// confirm that the dimension of the tangent space is no greater than
|
||||||
|
@ -723,8 +824,8 @@ mod tests {
|
||||||
fn tangent_test_kaleidocycle() {
|
fn tangent_test_kaleidocycle() {
|
||||||
// set up a kaleidocycle and find its tangent space
|
// set up a kaleidocycle and find its tangent space
|
||||||
const SCALED_TOL: f64 = 1.0e-12;
|
const SCALED_TOL: f64 = 1.0e-12;
|
||||||
let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL);
|
let Realization { result, history } = realize_kaleidocycle(SCALED_TOL);
|
||||||
assert_eq!(success, true);
|
let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap();
|
||||||
assert_eq!(history.scaled_loss.len(), 1);
|
assert_eq!(history.scaled_loss.len(), 1);
|
||||||
|
|
||||||
// list some motions that should form a basis for the tangent space of
|
// list some motions that should form a basis for the tangent space of
|
||||||
|
@ -759,10 +860,10 @@ mod tests {
|
||||||
DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]),
|
DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]),
|
||||||
DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]),
|
DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]),
|
||||||
DVector::from_column_slice(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]),
|
DVector::from_column_slice(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]),
|
||||||
DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0])
|
DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0]),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
).collect::<Vec<_>>()
|
).collect::<Vec<_>>(),
|
||||||
];
|
];
|
||||||
let tangent_motions_std = tangent_motions_unif.iter().map(
|
let tangent_motions_std = tangent_motions_unif.iter().map(
|
||||||
|motion| DMatrix::from_columns(
|
|motion| DMatrix::from_columns(
|
||||||
|
@ -795,7 +896,7 @@ mod tests {
|
||||||
0.0, 1.0, 0.0, 0.0, dis[1],
|
0.0, 1.0, 0.0, 0.0, dis[1],
|
||||||
0.0, 0.0, 1.0, 0.0, dis[2],
|
0.0, 0.0, 1.0, 0.0, dis[2],
|
||||||
2.0*dis[0], 2.0*dis[1], 2.0*dis[2], 1.0, dis.norm_squared(),
|
2.0*dis[0], 2.0*dis[1], 2.0*dis[2], 1.0, dis.norm_squared(),
|
||||||
0.0, 0.0, 0.0, 0.0, 1.0
|
0.0, 0.0, 0.0, 0.0, 1.0,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -805,23 +906,18 @@ mod tests {
|
||||||
fn proj_equivar_test() {
|
fn proj_equivar_test() {
|
||||||
// find a pair of spheres that meet at 120°
|
// find a pair of spheres that meet at 120°
|
||||||
const SCALED_TOL: f64 = 1.0e-12;
|
const SCALED_TOL: f64 = 1.0e-12;
|
||||||
let gram = {
|
let mut problem_orig = ConstraintProblem::from_guess(&[
|
||||||
let mut gram_to_be = PartialMatrix::new();
|
|
||||||
gram_to_be.push_sym(0, 0, 1.0);
|
|
||||||
gram_to_be.push_sym(1, 1, 1.0);
|
|
||||||
gram_to_be.push_sym(0, 1, 0.5);
|
|
||||||
gram_to_be
|
|
||||||
};
|
|
||||||
let guess_orig = DMatrix::from_columns(&[
|
|
||||||
sphere(0.0, 0.0, 0.5, 1.0),
|
sphere(0.0, 0.0, 0.5, 1.0),
|
||||||
sphere(0.0, 0.0, -0.5, 1.0)
|
sphere(0.0, 0.0, -0.5, 1.0),
|
||||||
]);
|
]);
|
||||||
let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram(
|
problem_orig.gram.push_sym(0, 0, 1.0);
|
||||||
&gram, guess_orig.clone(), &[],
|
problem_orig.gram.push_sym(1, 1, 1.0);
|
||||||
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
problem_orig.gram.push_sym(0, 1, 0.5);
|
||||||
|
let Realization { result: result_orig, history: history_orig } = realize_gram(
|
||||||
|
&problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
||||||
);
|
);
|
||||||
assert_eq!(config_orig, guess_orig);
|
let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap();
|
||||||
assert_eq!(success_orig, true);
|
assert_eq!(config_orig, problem_orig.guess);
|
||||||
assert_eq!(history_orig.scaled_loss.len(), 1);
|
assert_eq!(history_orig.scaled_loss.len(), 1);
|
||||||
|
|
||||||
// find another pair of spheres that meet at 120°. we'll think of this
|
// find another pair of spheres that meet at 120°. we'll think of this
|
||||||
|
@ -830,15 +926,19 @@ mod tests {
|
||||||
let a = 0.5 * FRAC_1_SQRT_2;
|
let a = 0.5 * FRAC_1_SQRT_2;
|
||||||
DMatrix::from_columns(&[
|
DMatrix::from_columns(&[
|
||||||
sphere(a, 0.0, 7.0 + a, 1.0),
|
sphere(a, 0.0, 7.0 + a, 1.0),
|
||||||
sphere(-a, 0.0, 7.0 - a, 1.0)
|
sphere(-a, 0.0, 7.0 - a, 1.0),
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram(
|
let problem_tfm = ConstraintProblem {
|
||||||
&gram, guess_tfm.clone(), &[],
|
gram: problem_orig.gram,
|
||||||
SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
frozen: problem_orig.frozen,
|
||||||
|
guess: guess_tfm,
|
||||||
|
};
|
||||||
|
let Realization { result: result_tfm, history: history_tfm } = realize_gram(
|
||||||
|
&problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
||||||
);
|
);
|
||||||
assert_eq!(config_tfm, guess_tfm);
|
let ConfigNeighborhood { config: config_tfm, nbhd: tangent_tfm } = result_tfm.unwrap();
|
||||||
assert_eq!(success_tfm, true);
|
assert_eq!(config_tfm, problem_tfm.guess);
|
||||||
assert_eq!(history_tfm.scaled_loss.len(), 1);
|
assert_eq!(history_tfm.scaled_loss.len(), 1);
|
||||||
|
|
||||||
// project a nudge to the tangent space of the solution variety at the
|
// project a nudge to the tangent space of the solution variety at the
|
||||||
|
@ -860,7 +960,7 @@ mod tests {
|
||||||
0.0, 1.0, 0.0, 0.0, 0.0,
|
0.0, 1.0, 0.0, 0.0, 0.0,
|
||||||
FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0, 0.0,
|
FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0, 0.0,
|
||||||
0.0, 0.0, 0.0, 1.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, 0.0, 0.0, 0.0, 1.0,
|
||||||
]);
|
]);
|
||||||
let transl = translation(Vector3::new(0.0, 0.0, 7.0));
|
let transl = translation(Vector3::new(0.0, 0.0, 7.0));
|
||||||
let motion_proj_tfm = transl * rot * motion_orig_proj;
|
let motion_proj_tfm = transl * rot * motion_orig_proj;
|
||||||
|
@ -869,7 +969,7 @@ mod tests {
|
||||||
// the comparison tolerance because the transformation seems to
|
// the comparison tolerance because the transformation seems to
|
||||||
// introduce some numerical error
|
// introduce some numerical error
|
||||||
const SCALED_TOL_TFM: f64 = 1.0e-9;
|
const SCALED_TOL_TFM: f64 = 1.0e-9;
|
||||||
let tol_sq = ((guess_orig.nrows() * guess_orig.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM;
|
let tol_sq = ((problem_orig.guess.nrows() * problem_orig.guess.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM;
|
||||||
assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq);
|
assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,49 +1,49 @@
|
||||||
mod add_remove;
|
|
||||||
mod assembly;
|
mod assembly;
|
||||||
mod display;
|
mod components;
|
||||||
mod engine;
|
mod engine;
|
||||||
mod outline;
|
|
||||||
mod specified;
|
mod specified;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use rustc_hash::FxHashSet;
|
use std::{collections::BTreeSet, rc::Rc};
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
|
|
||||||
use add_remove::AddRemove;
|
use assembly::{Assembly, Element};
|
||||||
use assembly::{Assembly, ElementKey};
|
use components::{
|
||||||
use display::Display;
|
add_remove::AddRemove,
|
||||||
use outline::Outline;
|
diagnostics::Diagnostics,
|
||||||
|
display::Display,
|
||||||
|
outline::Outline,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
assembly: Assembly,
|
assembly: Assembly,
|
||||||
selection: Signal<FxHashSet<ElementKey>>
|
selection: Signal<BTreeSet<Rc<dyn Element>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
fn new() -> AppState {
|
fn new() -> Self {
|
||||||
AppState {
|
Self {
|
||||||
assembly: Assembly::new(),
|
assembly: Assembly::new(),
|
||||||
selection: create_signal(FxHashSet::default())
|
selection: create_signal(BTreeSet::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// in single-selection mode, select the element with the given key. in
|
// in single-selection mode, select the given element. in multiple-selection
|
||||||
// multiple-selection mode, toggle whether the element with the given key
|
// mode, toggle whether the given element is selected
|
||||||
// is selected
|
fn select(&self, element: &Rc<dyn Element>, multi: bool) {
|
||||||
fn select(&self, key: ElementKey, multi: bool) {
|
|
||||||
if multi {
|
if multi {
|
||||||
self.selection.update(|sel| {
|
self.selection.update(|sel| {
|
||||||
if !sel.remove(&key) {
|
if !sel.remove(element) {
|
||||||
sel.insert(key);
|
sel.insert(element.clone());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
self.selection.update(|sel| {
|
self.selection.update(|sel| {
|
||||||
sel.clear();
|
sel.clear();
|
||||||
sel.insert(key);
|
sel.insert(element.clone());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,7 @@ fn main() {
|
||||||
div(id = "sidebar") {
|
div(id = "sidebar") {
|
||||||
AddRemove {}
|
AddRemove {}
|
||||||
Outline {}
|
Outline {}
|
||||||
|
Diagnostics {}
|
||||||
}
|
}
|
||||||
Display {}
|
Display {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,235 +0,0 @@
|
||||||
use itertools::Itertools;
|
|
||||||
use sycamore::prelude::*;
|
|
||||||
use web_sys::{
|
|
||||||
KeyboardEvent,
|
|
||||||
MouseEvent,
|
|
||||||
wasm_bindgen::JsCast
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
AppState,
|
|
||||||
assembly,
|
|
||||||
assembly::{ElementKey, Regulator, RegulatorKey},
|
|
||||||
specified::SpecifiedValue
|
|
||||||
};
|
|
||||||
|
|
||||||
// an editable view of a regulator
|
|
||||||
#[component(inline_props)]
|
|
||||||
fn RegulatorInput(regulator: Regulator) -> View {
|
|
||||||
let valid = create_signal(true);
|
|
||||||
let value = create_signal(
|
|
||||||
regulator.set_point.with_untracked(|set_pt| set_pt.spec.clone())
|
|
||||||
);
|
|
||||||
|
|
||||||
// this closure resets the input value to the regulator's set point
|
|
||||||
// specification
|
|
||||||
let reset_value = move || {
|
|
||||||
batch(|| {
|
|
||||||
valid.set(true);
|
|
||||||
value.set(regulator.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() {
|
|
||||||
regulator.set_point.with(|set_pt| {
|
|
||||||
if set_pt.is_present() {
|
|
||||||
"regulator-input constraint"
|
|
||||||
} else {
|
|
||||||
"regulator-input"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
"regulator-input invalid"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
placeholder=regulator.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) => {
|
|
||||||
regulator.set_point.set(set_pt);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Err(_) => false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
on:keydown={
|
|
||||||
move |event: KeyboardEvent| {
|
|
||||||
match event.key().as_str() {
|
|
||||||
"Escape" => reset_value(),
|
|
||||||
_ => ()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// a list item that shows a regulator in an outline view of an element
|
|
||||||
#[component(inline_props)]
|
|
||||||
fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View {
|
|
||||||
let state = use_context::<AppState>();
|
|
||||||
let assembly = &state.assembly;
|
|
||||||
let regulator = assembly.regulators.with(|regs| regs[regulator_key]);
|
|
||||||
let other_subject = if regulator.subjects.0 == element_key {
|
|
||||||
regulator.subjects.1
|
|
||||||
} else {
|
|
||||||
regulator.subjects.0
|
|
||||||
};
|
|
||||||
let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone());
|
|
||||||
view! {
|
|
||||||
li(class="regulator") {
|
|
||||||
div(class="regulator-label") { (other_subject_label) }
|
|
||||||
div(class="regulator-type") { "Inversive distance" }
|
|
||||||
RegulatorInput(regulator=regulator)
|
|
||||||
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 = move || {
|
|
||||||
element.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().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" => {
|
|
||||||
state.select(key, 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={
|
|
||||||
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") { (rep_components) }
|
|
||||||
div(class="status")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ul(class="regulators") {
|
|
||||||
Keyed(
|
|
||||||
list=regulator_list,
|
|
||||||
view=move |reg_key| view! {
|
|
||||||
RegulatorOutlineItem(
|
|
||||||
regulator_key=reg_key,
|
|
||||||
element_key=key
|
|
||||||
)
|
|
||||||
},
|
|
||||||
key=|reg_key| reg_key.clone()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
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=|(_, elt)| elt.serial
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,12 +13,12 @@ use std::num::ParseFloatError;
|
||||||
#[readonly::make]
|
#[readonly::make]
|
||||||
pub struct SpecifiedValue {
|
pub struct SpecifiedValue {
|
||||||
pub spec: String,
|
pub spec: String,
|
||||||
pub value: Option<f64>
|
pub value: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpecifiedValue {
|
impl SpecifiedValue {
|
||||||
pub fn from_empty_spec() -> SpecifiedValue {
|
pub fn from_empty_spec() -> Self {
|
||||||
SpecifiedValue { spec: String::new(), value: None }
|
Self { spec: String::new(), value: None }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_present(&self) -> bool {
|
pub fn is_present(&self) -> bool {
|
||||||
|
@ -34,10 +34,10 @@ impl TryFrom<String> for SpecifiedValue {
|
||||||
|
|
||||||
fn try_from(spec: String) -> Result<Self, Self::Error> {
|
fn try_from(spec: String) -> Result<Self, Self::Error> {
|
||||||
if spec.is_empty() {
|
if spec.is_empty() {
|
||||||
Ok(SpecifiedValue::from_empty_spec())
|
Ok(Self::from_empty_spec())
|
||||||
} else {
|
} else {
|
||||||
spec.parse::<f64>().map(
|
spec.parse::<f64>().map(
|
||||||
|value| SpecifiedValue { spec: spec, value: Some(value) }
|
|value| Self { spec, value: Some(value) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
5
deploy/.gitignore
vendored
Normal file
5
deploy/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/dyna3.zip
|
||||||
|
/dyna3/index.html
|
||||||
|
/dyna3/dyna3-*.js
|
||||||
|
/dyna3/dyna3-*.wasm
|
||||||
|
/dyna3/main-*.css
|
16
tools/package-for-deployment.sh
Normal file
16
tools/package-for-deployment.sh
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# set paths. this technique for getting the script location comes from
|
||||||
|
# `mklement0` on Stack Overflow
|
||||||
|
#
|
||||||
|
# https://stackoverflow.com/a/24114056
|
||||||
|
#
|
||||||
|
TOOLS=$(dirname -- $0)
|
||||||
|
SRC="$TOOLS/../app-proto/dist"
|
||||||
|
DEST="$TOOLS/../deploy/dyna3"
|
||||||
|
|
||||||
|
# remove the old hash-named files
|
||||||
|
[ -e "$DEST"/dyna3-*.js ] && rm "$DEST"/dyna3-*.js
|
||||||
|
[ -e "$DEST"/dyna3-*.wasm ] && rm "$DEST"/dyna3-*.wasm
|
||||||
|
[ -e "$DEST"/main-*.css ] && rm "$DEST"/main-*.css
|
||||||
|
|
||||||
|
# copy the distribution
|
||||||
|
cp -r "$SRC/." "$DEST"
|
20
tools/run-examples.sh
Normal file
20
tools/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)/../app-proto/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
|
Loading…
Add table
Add a link
Reference in a new issue