Compare commits
21 commits
main
...
curvature-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eeb0935ca | ||
|
|
99a9c3ec55 | ||
|
|
5506ec1f43 | ||
|
|
8dab223f6a | ||
|
|
620a6be918 | ||
|
|
7f21e7e999 | ||
|
|
52d99755f9 | ||
|
|
8f8e806d12 | ||
|
|
ee8a01b9cb | ||
|
|
955220c0bc | ||
|
|
4654bf06bf | ||
|
|
e1952d7d52 | ||
|
|
81e423fcbe | ||
|
|
63e3d733ba | ||
|
|
bba0ac3cd6 | ||
|
|
d57ff59730 | ||
|
|
96e4a34fa1 | ||
|
|
126d4c0cce | ||
|
|
00f60b0e90 | ||
|
|
7c40d60103 | ||
|
|
bb226c5f45 |
36 changed files with 1729 additions and 4336 deletions
|
|
@ -10,7 +10,7 @@ runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- run: rustup target add wasm32-unknown-unknown
|
- run: rustup target add wasm32-unknown-unknown
|
||||||
|
|
||||||
# install the Trunk binary to `ci-bin` within the workspace directory, which
|
# install the Trunk binary to `ci-bin` within the workspace directory, which
|
||||||
# is determined by the `github.workspace` label and reflected in the
|
# is determined by the `github.workspace` label and reflected in the
|
||||||
# `GITHUB_WORKSPACE` environment variable. then, make the `trunk` command
|
# `GITHUB_WORKSPACE` environment variable. then, make the `trunk` command
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: cimg/rust:1.86-node
|
image: cimg/rust:1.85-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
|
||||||
|
|
@ -24,6 +24,6 @@ jobs:
|
||||||
# workspace directory (action variable `github.workspace`, environment
|
# workspace directory (action variable `github.workspace`, environment
|
||||||
# variable `$GITHUB_WORKSPACE`):
|
# variable `$GITHUB_WORKSPACE`):
|
||||||
- uses: https://code.forgejo.org/actions/checkout@v4
|
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
- uses: ./.forgejo/setup-trunk
|
- uses: ./.forgejo/setup-trunk
|
||||||
- run: RUSTFLAGS='-D warnings' cargo test
|
- run: RUSTFLAGS='-D warnings' cargo test
|
||||||
78
README.md
78
README.md
|
|
@ -12,11 +12,11 @@ Note that currently this is just the barest beginnings of the project, more of a
|
||||||
|
|
||||||
### Implementation goals
|
### Implementation goals
|
||||||
|
|
||||||
* Provide a comfortable, intuitive UI
|
* Comfortable, intuitive UI
|
||||||
|
|
||||||
* Allow execution in browser (so implemented in WASM-compatible language)
|
* Able to run in browser (so implemented in WASM-compatible language)
|
||||||
|
|
||||||
* Produce scalable graphics of 3D diagrams, and maybe STL files (or other fabricatable file format) as well
|
* Produce scalable graphics of 3D diagrams, and maybe STL files (or other fabricatable file format) as well.
|
||||||
|
|
||||||
## Prototype
|
## Prototype
|
||||||
|
|
||||||
|
|
@ -24,66 +24,44 @@ 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
|
||||||
- In the future, `trunk` can be updated with the same command. (You may need the `--locked` flag if your ambient version of `rustc` does not match that required by `trunk`.)
|
|
||||||
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
|
||||||
- Alternatively, if you don't want to adjust your `PATH`, you can install `trunk` in another directory `DIR` via `cargo install --root DIR trunk`.
|
|
||||||
|
|
||||||
### Play with the prototype
|
### Play with the prototype
|
||||||
|
|
||||||
1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype.
|
1. Go into the `app-proto` folder
|
||||||
- The crates the prototype depends on will be downloaded and served automatically.
|
2. Call `trunk serve --release` to build and serve the prototype
|
||||||
- For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag.
|
* *The crates the prototype depends on will be downloaded and served automatically*
|
||||||
- If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]` from there instead.
|
* *For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag*
|
||||||
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. Use `sh` to run the script `tools/run-examples.sh`.
|
1. Go into the `app-proto` folder
|
||||||
- The script is location-independent, so you can do this from anywhere in the dyna3 repository.
|
2. Call `./run-examples`
|
||||||
- The call from the top level of the repository is:
|
* *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*
|
||||||
```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 execute
|
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
include("irisawa-hexlet.jl")
|
include("irisawa-hexlet.jl")
|
||||||
for (step, scaled_loss) in enumerate(history_alt.scaled_loss)
|
for (step, scaled_loss) in enumerate(history_alt.scaled_loss)
|
||||||
println(rpad(step-1, 4), " | ", scaled_loss)
|
println(rpad(step-1, 4), " | ", scaled_loss)
|
||||||
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.
|
|
||||||
|
|
|
||||||
624
app-proto/Cargo.lock
generated
624
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 = 4
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
|
|
@ -20,21 +20,6 @@ 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"
|
||||||
|
|
@ -50,21 +35,6 @@ 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"
|
||||||
|
|
@ -98,35 +68,6 @@ 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"
|
||||||
|
|
@ -137,130 +78,19 @@ 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",
|
||||||
"enum-iterator",
|
|
||||||
"itertools",
|
"itertools",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"nalgebra",
|
"nalgebra",
|
||||||
"readonly",
|
"readonly",
|
||||||
|
"rustc-hash",
|
||||||
|
"slab",
|
||||||
"sycamore",
|
"sycamore",
|
||||||
"wasm-bindgen-test",
|
"wasm-bindgen-test",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
|
@ -272,48 +102,12 @@ version = "1.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "enum-iterator"
|
|
||||||
version = "2.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016"
|
|
||||||
dependencies = [
|
|
||||||
"enum-iterator-derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "enum-iterator-derive"
|
|
||||||
version = "1.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
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"
|
||||||
|
|
@ -325,28 +119,6 @@ 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"
|
||||||
|
|
@ -357,12 +129,6 @@ 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"
|
||||||
|
|
@ -372,47 +138,6 @@ 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"
|
||||||
|
|
@ -420,8 +145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.14.5",
|
"hashbrown",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -433,12 +157,6 @@ 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"
|
||||||
|
|
@ -476,12 +194,6 @@ 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"
|
||||||
|
|
@ -538,12 +250,6 @@ 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"
|
||||||
|
|
@ -553,21 +259,6 @@ 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"
|
||||||
|
|
@ -600,57 +291,6 @@ 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"
|
||||||
|
|
@ -726,10 +366,10 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "rustc-hash"
|
||||||
version = "1.0.20"
|
version = "2.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "safe_arch"
|
name = "safe_arch"
|
||||||
|
|
@ -755,90 +395,6 @@ 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"
|
||||||
|
|
@ -858,6 +414,15 @@ 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"
|
||||||
|
|
@ -873,20 +438,14 @@ version = "1.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strsim"
|
|
||||||
version = "0.11.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore"
|
name = "sycamore"
|
||||||
version = "0.9.1"
|
version = "0.9.0-beta.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc"
|
checksum = "dedaf7237c05913604a5b0b2536b613f6c8510c6b213d2583b1294869755cabd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.14.5",
|
"hashbrown",
|
||||||
"indexmap 2.5.0",
|
"indexmap",
|
||||||
"paste",
|
"paste",
|
||||||
"sycamore-core",
|
"sycamore-core",
|
||||||
"sycamore-macro",
|
"sycamore-macro",
|
||||||
|
|
@ -898,20 +457,20 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore-core"
|
name = "sycamore-core"
|
||||||
version = "0.9.1"
|
version = "0.9.0-beta.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187"
|
checksum = "e5ddddc3d1bcb38c04ad55d2d1ab4f6a358e4daaeae0a0436892f1fade9fb31a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.14.5",
|
"hashbrown",
|
||||||
"paste",
|
"paste",
|
||||||
"sycamore-reactive",
|
"sycamore-reactive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore-macro"
|
name = "sycamore-macro"
|
||||||
version = "0.9.1"
|
version = "0.9.0-beta.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a0c1d2eddc94db6d03e67eb832df5512b967e81053a573cd01bf3e1c3db00137"
|
checksum = "77181c27cb753e86065308901871ccc7456fb19527b6a4ffacad3b63175ed014"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|
@ -923,21 +482,20 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore-reactive"
|
name = "sycamore-reactive"
|
||||||
version = "0.9.1"
|
version = "0.9.0-beta.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2bacf810535efc2701187a716a5652197ad241d620d5b00fb12caa6dfa23add"
|
checksum = "7aa6870203507c07e850687c0ccf528eb0f04240e3596bac9137007ffb6c50b1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"paste",
|
"paste",
|
||||||
"slotmap",
|
"slotmap",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore-view-parser"
|
name = "sycamore-view-parser"
|
||||||
version = "0.9.1"
|
version = "0.9.0-beta.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c22875843db83cd4d49c0123a195e433bdc74e13ed0fff4ace0e77bb0a67033"
|
checksum = "a6144640af2eafffc68a92f3aacbbfaa21f7fd31906e2336fe304fd100fe226b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -946,9 +504,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore-web"
|
name = "sycamore-web"
|
||||||
version = "0.9.1"
|
version = "0.9.0-beta.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b17aa5875f59f541cdf6fb58751ec702a6ed9801f30dd2b4d5f2279025b98bd"
|
checksum = "bca93dcf1b1830bf1aac93508ed51babcda92c1d32d96067ab416d94e4b7c475"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"html-escape",
|
"html-escape",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
|
@ -964,78 +522,21 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.87"
|
version = "2.0.77"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
|
||||||
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"
|
||||||
|
|
@ -1192,65 +693,6 @@ 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,23 +3,20 @@ 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"]
|
||||||
dev = []
|
dev = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
enum-iterator = "2.3.0"
|
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
js-sys = "0.3.70"
|
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"
|
||||||
sycamore = "0.9.1"
|
rustc-hash = "2.0.0"
|
||||||
|
slab = "0.4.9"
|
||||||
# We use Charming to help display engine diagnostics
|
sycamore = "0.9.0-beta.3"
|
||||||
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
|
||||||
|
|
@ -50,13 +47,6 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
[build]
|
|
||||||
public_url = "./"
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
#![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,23 +1,25 @@
|
||||||
#[path = "common/print.rs"]
|
use dyna3::engine::{Q, examples::realize_irisawa_hexlet};
|
||||||
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 realization = realize_irisawa_hexlet(SCALED_TOL);
|
let (config, _, success, history) = realize_irisawa_hexlet(SCALED_TOL);
|
||||||
print::title("Irisawa hexlet");
|
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
|
||||||
print::realization_diagnostics(&realization);
|
if success {
|
||||||
if let Ok(ConfigNeighborhood { config, .. }) = realization.result {
|
println!("Target accuracy achieved!");
|
||||||
// print the diameters of the chain spheres
|
} else {
|
||||||
|
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);
|
|
||||||
}
|
}
|
||||||
print::loss_history(&realization.history);
|
println!("\nStep │ Loss\n─────┼────────────────────────────────");
|
||||||
}
|
for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() {
|
||||||
|
println!("{:<4} │ {}", step, scaled_loss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,32 +1,30 @@
|
||||||
#[path = "common/print.rs"]
|
|
||||||
mod print;
|
|
||||||
|
|
||||||
use nalgebra::{DMatrix, DVector};
|
use nalgebra::{DMatrix, DVector};
|
||||||
|
|
||||||
use dyna3::engine::{ConfigNeighborhood, examples::realize_kaleidocycle};
|
use dyna3::engine::{Q, examples::realize_kaleidocycle};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
const SCALED_TOL: f64 = 1.0e-12;
|
const SCALED_TOL: f64 = 1.0e-12;
|
||||||
let realization = realize_kaleidocycle(SCALED_TOL);
|
let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL);
|
||||||
print::title("Kaleidocycle");
|
print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config);
|
||||||
print::realization_diagnostics(&realization);
|
print!("Configuration:{}", config);
|
||||||
if let Ok(ConfigNeighborhood { config, nbhd: tangent }) = realization.result {
|
if success {
|
||||||
// print the completed Gram matrix and the realized configuration
|
println!("Target accuracy achieved!");
|
||||||
print::gram_matrix(&config);
|
} else {
|
||||||
print::config(&config);
|
println!("Failed to reach target accuracy");
|
||||||
|
|
||||||
// find the kaleidocycle's twist motion by projecting onto the tangent
|
|
||||||
// space
|
|
||||||
const N_POINTS: usize = 12;
|
|
||||||
let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]);
|
|
||||||
let down = -&up;
|
|
||||||
let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map(
|
|
||||||
|n| [
|
|
||||||
tangent.proj(&up.as_view(), n),
|
|
||||||
tangent.proj(&down.as_view(), n+1),
|
|
||||||
]
|
|
||||||
).sum();
|
|
||||||
let normalization = 5.0 / twist_motion[(2, 0)];
|
|
||||||
println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end());
|
|
||||||
}
|
}
|
||||||
}
|
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
|
||||||
|
const N_POINTS: usize = 12;
|
||||||
|
let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]);
|
||||||
|
let down = -&up;
|
||||||
|
let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map(
|
||||||
|
|n| [
|
||||||
|
tangent.proj(&up.as_view(), n),
|
||||||
|
tangent.proj(&down.as_view(), n+1)
|
||||||
|
]
|
||||||
|
).sum();
|
||||||
|
let normalization = 5.0 / twist_motion[(2, 0)];
|
||||||
|
print!("Twist motion:{}", normalization * twist_motion);
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,4 @@
|
||||||
#[path = "common/print.rs"]
|
use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem};
|
||||||
mod print;
|
|
||||||
|
|
||||||
use dyna3::engine::{
|
|
||||||
point,
|
|
||||||
realize_gram,
|
|
||||||
sphere,
|
|
||||||
ConfigNeighborhood,
|
|
||||||
ConstraintProblem,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut problem = ConstraintProblem::from_guess(&[
|
let mut problem = ConstraintProblem::from_guess(&[
|
||||||
|
|
@ -20,14 +11,21 @@ fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
problem.frozen.push(3, 0, problem.guess[(3, 0)]);
|
problem.frozen.push(3, 0, problem.guess[(3, 0)]);
|
||||||
let realization = realize_gram(
|
println!();
|
||||||
|
let (config, _, success, history) = realize_gram(
|
||||||
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||||
);
|
);
|
||||||
print::title("Point on a sphere");
|
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
|
||||||
print::realization_diagnostics(&realization);
|
print!("Configuration:{}", config);
|
||||||
if let Ok(ConfigNeighborhood { config, .. }) = realization.result {
|
if success {
|
||||||
print::gram_matrix(&config);
|
println!("Target accuracy achieved!");
|
||||||
print::config(&config);
|
} else {
|
||||||
|
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,12 +1,4 @@
|
||||||
#[path = "common/print.rs"]
|
use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem};
|
||||||
mod print;
|
|
||||||
|
|
||||||
use dyna3::engine::{
|
|
||||||
realize_gram,
|
|
||||||
sphere,
|
|
||||||
ConfigNeighborhood,
|
|
||||||
ConstraintProblem,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut problem = ConstraintProblem::from_guess({
|
let mut problem = ConstraintProblem::from_guess({
|
||||||
|
|
@ -14,7 +6,7 @@ fn main() {
|
||||||
&[
|
&[
|
||||||
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)
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
for j in 0..3 {
|
for j in 0..3 {
|
||||||
|
|
@ -22,13 +14,20 @@ fn main() {
|
||||||
problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
|
problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let realization = realize_gram(
|
println!();
|
||||||
|
let (config, _, success, history) = realize_gram(
|
||||||
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||||
);
|
);
|
||||||
print::title("Three spheres");
|
print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config);
|
||||||
print::realization_diagnostics(&realization);
|
if success {
|
||||||
if let Ok(ConfigNeighborhood { config, .. }) = realization.result {
|
println!("Target accuracy achieved!");
|
||||||
print::gram_matrix(&config);
|
} else {
|
||||||
|
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,12 +6,6 @@
|
||||||
<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,17 +18,6 @@ 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 {
|
||||||
|
|
@ -53,7 +42,9 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
#add-remove > button {
|
#add-remove > button {
|
||||||
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
font-size: large;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* KLUDGE */
|
/* KLUDGE */
|
||||||
|
|
@ -62,9 +53,7 @@ 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 */
|
||||||
|
|
@ -101,10 +90,6 @@ 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;
|
||||||
|
|
@ -149,7 +134,6 @@ 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);
|
||||||
|
|
@ -171,65 +155,22 @@ 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;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#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: '⚠';
|
|
||||||
}
|
|
||||||
|
|
||||||
#step-input > label {
|
|
||||||
padding-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#step-input > input {
|
|
||||||
width: 45px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 */
|
||||||
|
|
||||||
#display {
|
canvas {
|
||||||
float: left;
|
float: left;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
|
@ -238,7 +179,7 @@ details[open]:has(li) .element-switch::after {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#display:focus {
|
canvas:focus {
|
||||||
border-color: var(--border-focus-dark);
|
border-color: var(--border-focus-dark);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
app-proto/run-examples
Executable file
12
app-proto/run-examples
Executable file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/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
|
||||||
205
app-proto/src/add_remove.rs
Normal file
205
app-proto/src/add_remove.rs
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
use sycamore::prelude::*;
|
||||||
|
use web_sys::{console, wasm_bindgen::JsValue};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
engine,
|
||||||
|
AppState,
|
||||||
|
assembly::{Assembly, Element, InversiveDistanceRegulator}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 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_sphere(
|
||||||
|
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_sphere(
|
||||||
|
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_sphere(
|
||||||
|
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_sphere(
|
||||||
|
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_sphere(
|
||||||
|
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_sphere(
|
||||||
|
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_sphere(
|
||||||
|
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_sphere(
|
||||||
|
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_sphere(
|
||||||
|
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_sphere(
|
||||||
|
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_sphere(
|
||||||
|
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_sphere(
|
||||||
|
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_sphere(
|
||||||
|
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_sphere(
|
||||||
|
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.regulators.update(|regs| regs.clear());
|
||||||
|
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_sphere();
|
||||||
|
}
|
||||||
|
) { "+" }
|
||||||
|
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(
|
||||||
|
InversiveDistanceRegulator::new(subjects, &state.assembly)
|
||||||
|
);
|
||||||
|
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
|
|
@ -1,5 +0,0 @@
|
||||||
pub mod add_remove;
|
|
||||||
pub mod diagnostics;
|
|
||||||
pub mod display;
|
|
||||||
pub mod outline;
|
|
||||||
pub mod test_assembly_chooser;
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
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 {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,320 +0,0 @@
|
||||||
use charming::{
|
|
||||||
Chart,
|
|
||||||
WasmRenderer,
|
|
||||||
component::{Axis, DataZoom, Grid},
|
|
||||||
element::{AxisType, Symbol},
|
|
||||||
series::{Line, Scatter},
|
|
||||||
};
|
|
||||||
use sycamore::prelude::*;
|
|
||||||
|
|
||||||
use crate::{AppState, specified::SpecifiedValue};
|
|
||||||
|
|
||||||
#[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(),
|
|
||||||
}
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// history step input
|
|
||||||
#[component]
|
|
||||||
fn StepInput() -> View {
|
|
||||||
// get the assembly
|
|
||||||
let state = use_context::<AppState>();
|
|
||||||
let assembly = state.assembly;
|
|
||||||
|
|
||||||
// the `last_step` signal holds the index of the last step
|
|
||||||
let last_step = assembly.descent_history.map(
|
|
||||||
|history| match history.config.len() {
|
|
||||||
0 => None,
|
|
||||||
n => Some(n - 1),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let input_max = last_step.map(|last| last.unwrap_or(0));
|
|
||||||
|
|
||||||
// these signals hold the entered step number
|
|
||||||
let value = create_signal(String::new());
|
|
||||||
let value_as_number = create_signal(0.0);
|
|
||||||
|
|
||||||
create_effect(move || {
|
|
||||||
value.set(assembly.step.with(|n| n.spec.clone()));
|
|
||||||
});
|
|
||||||
|
|
||||||
view! {
|
|
||||||
div(id = "step-input") {
|
|
||||||
label { "Step" }
|
|
||||||
input(
|
|
||||||
r#type = "number",
|
|
||||||
min = "0",
|
|
||||||
max = input_max.with(|max| max.to_string()),
|
|
||||||
bind:value = value,
|
|
||||||
bind:valueAsNumber = value_as_number,
|
|
||||||
on:change = move |_| {
|
|
||||||
if last_step.with(|last| last.is_some()) {
|
|
||||||
// clamp the step within its allowed range. the lower
|
|
||||||
// bound is redundant on browsers that make it
|
|
||||||
// impossible to type negative values into a number
|
|
||||||
// input with a non-negative `min`, but there's no harm
|
|
||||||
// in being careful
|
|
||||||
let step_raw = value.with(
|
|
||||||
|val| SpecifiedValue::try_from(val.clone())
|
|
||||||
.unwrap_or(SpecifiedValue::from_empty_spec()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
let step = SpecifiedValue::from(
|
|
||||||
step_raw.value.map(
|
|
||||||
|val| val.clamp(0.0, input_max.get() as f64)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// set the input string and the assembly's active step
|
|
||||||
value.set(step.spec.clone());
|
|
||||||
assembly.step.set(step);
|
|
||||||
} else {
|
|
||||||
value.set(String::new());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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" }
|
|
||||||
}
|
|
||||||
StepInput {}
|
|
||||||
}
|
|
||||||
DiagnosticsPanel(name = "loss") { LossHistory {} }
|
|
||||||
DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,939 +0,0 @@
|
||||||
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
|
|
||||||
/* KLUDGE */
|
|
||||||
// to avoid the complexity of making tangent space projection
|
|
||||||
// conditional and dealing with unnormalized representation vectors,
|
|
||||||
// we only allow manipulation when we're looking at the last step of
|
|
||||||
// a successful realization
|
|
||||||
let realization_successful = state.assembly.realization_status.with(
|
|
||||||
|status| status.is_ok()
|
|
||||||
);
|
|
||||||
let step_val = state.assembly.step.with_untracked(|step| step.value);
|
|
||||||
let on_init_step = step_val.is_some_and(|n| n == 0.0);
|
|
||||||
let on_last_step = step_val.is_some_and(
|
|
||||||
|n| state.assembly.descent_history.with_untracked(
|
|
||||||
|history| n as usize + 1 == history.config.len().max(1)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
let on_manipulable_step =
|
|
||||||
!realization_successful && on_init_step
|
|
||||||
|| realization_successful && on_last_step;
|
|
||||||
if on_manipulable_step && 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()),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
#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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
#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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,941 +0,0 @@
|
||||||
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" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
613
app-proto/src/display.rs
Normal file
613
app-proto/src/display.rs
Normal file
|
|
@ -0,0 +1,613 @@
|
||||||
|
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,9 +1,10 @@
|
||||||
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 std::fmt::{Display, Error, Formatter};
|
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
||||||
|
|
||||||
// --- 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)])
|
||||||
}
|
}
|
||||||
|
|
@ -16,7 +17,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)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,51 +31,79 @@ 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
|
// given a sphere's representation vector, change the sphere's half-curvature to
|
||||||
// contracting toward the last coordinate axis
|
// `half-curv` and then restore normalization by contracting the representation
|
||||||
pub fn project_sphere_to_normalized(rep: &mut DVector<f64>) {
|
// vector toward the curvature axis
|
||||||
let q_sp = rep.fixed_rows::<3>(0).norm_squared();
|
pub fn change_half_curvature(rep: &mut DVector<f64>, half_curv: f64) {
|
||||||
let half_q_lt = -2.0 * rep[3] * rep[4];
|
// set the sphere's half-curvature to the desired value
|
||||||
|
rep[3] = half_curv;
|
||||||
|
|
||||||
|
// restore normalization by contracting toward the curvature axis
|
||||||
|
const SIZE_THRESHOLD: f64 = 1e-9;
|
||||||
|
let half_q_lt = -2.0 * half_curv * rep[4];
|
||||||
let half_q_lt_sq = half_q_lt * half_q_lt;
|
let half_q_lt_sq = half_q_lt * half_q_lt;
|
||||||
let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt();
|
let mut spatial = rep.fixed_rows_mut::<3>(0);
|
||||||
rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling);
|
let q_sp = spatial.norm_squared();
|
||||||
}
|
if q_sp < SIZE_THRESHOLD && half_q_lt_sq < SIZE_THRESHOLD {
|
||||||
|
spatial.copy_from_slice(
|
||||||
// normalize a point's representation vector by scaling
|
&[0.0, 0.0, (1.0 - 2.0 * half_q_lt).sqrt()]
|
||||||
pub fn project_point_to_normalized(rep: &mut DVector<f64>) {
|
);
|
||||||
rep.scale_mut(0.5 / rep[3]);
|
} else {
|
||||||
|
let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt();
|
||||||
|
spatial.scale_mut(1.0 / scaling);
|
||||||
|
rep[4] /= scaling;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DEBUG */
|
||||||
|
// verify normalization
|
||||||
|
let rep_for_debug = rep.clone();
|
||||||
|
console::log_1(&JsValue::from(
|
||||||
|
format!(
|
||||||
|
"Sphere self-product after curvature change: {}",
|
||||||
|
rep_for_debug.dot(&(&*Q * &rep_for_debug))
|
||||||
|
)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- partial matrices ---
|
// --- partial matrices ---
|
||||||
|
|
||||||
pub struct MatrixEntry {
|
pub struct MatrixEntry {
|
||||||
pub index: (usize, usize),
|
index: (usize, usize),
|
||||||
pub value: f64,
|
value: f64
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PartialMatrix(Vec<MatrixEntry>);
|
pub struct PartialMatrix(Vec<MatrixEntry>);
|
||||||
|
|
||||||
impl PartialMatrix {
|
impl PartialMatrix {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> PartialMatrix {
|
||||||
Self(Vec::<MatrixEntry>::new())
|
PartialMatrix(Vec::<MatrixEntry>::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push(&mut self, row: usize, col: usize, value: f64) {
|
pub fn push(&mut self, row: usize, col: usize, value: f64) {
|
||||||
let Self(entries) = self;
|
let PartialMatrix(entries) = self;
|
||||||
entries.push(MatrixEntry { index: (row, col), value });
|
entries.push(MatrixEntry { index: (row, col), value: 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) {
|
||||||
self.push(row, col, value);
|
self.push(row, col, value);
|
||||||
if row != col {
|
if row != col {
|
||||||
self.push(col, row, value);
|
self.push(col, row, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* DEBUG */
|
||||||
|
pub fn log_to_console(&self) {
|
||||||
|
for &MatrixEntry { index: (row, col), value } in self {
|
||||||
|
console::log_1(&JsValue::from(
|
||||||
|
format!(" {} {} {}", row, col, value)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn freeze(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
|
fn freeze(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
|
||||||
let mut result = a.clone();
|
let mut result = a.clone();
|
||||||
for &MatrixEntry { index, value } in self {
|
for &MatrixEntry { index, value } in self {
|
||||||
|
|
@ -82,7 +111,7 @@ impl PartialMatrix {
|
||||||
}
|
}
|
||||||
result
|
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());
|
||||||
for &MatrixEntry { index, .. } in self {
|
for &MatrixEntry { index, .. } in self {
|
||||||
|
|
@ -90,7 +119,7 @@ impl PartialMatrix {
|
||||||
}
|
}
|
||||||
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());
|
||||||
for &MatrixEntry { index, value } in self {
|
for &MatrixEntry { index, value } in self {
|
||||||
|
|
@ -100,21 +129,12 @@ impl PartialMatrix {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
impl IntoIterator for PartialMatrix {
|
||||||
type Item = MatrixEntry;
|
type Item = MatrixEntry;
|
||||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
let Self(entries) = self;
|
let PartialMatrix(entries) = self;
|
||||||
entries.into_iter()
|
entries.into_iter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +142,7 @@ impl IntoIterator for PartialMatrix {
|
||||||
impl<'a> IntoIterator for &'a PartialMatrix {
|
impl<'a> IntoIterator for &'a PartialMatrix {
|
||||||
type Item = &'a MatrixEntry;
|
type Item = &'a MatrixEntry;
|
||||||
type IntoIter = std::slice::Iter<'a, MatrixEntry>;
|
type IntoIter = std::slice::Iter<'a, MatrixEntry>;
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
let PartialMatrix(entries) = self;
|
let PartialMatrix(entries) = self;
|
||||||
entries.into_iter()
|
entries.into_iter()
|
||||||
|
|
@ -135,26 +155,22 @@ impl<'a> IntoIterator for &'a PartialMatrix {
|
||||||
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) -> Self {
|
pub fn zero(assembly_dim: usize) -> ConfigSubspace {
|
||||||
Self {
|
ConfigSubspace {
|
||||||
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(
|
fn symmetric_kernel(a: DMatrix<f64>, proj_to_std: DMatrix<f64>, assembly_dim: usize) -> ConfigSubspace {
|
||||||
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
|
||||||
|
|
@ -167,14 +183,21 @@ impl ConfigSubspace {
|
||||||
|(λ, v)| (λ.abs() < THRESHOLD).then_some(v)
|
|(λ, v)| (λ.abs() < THRESHOLD).then_some(v)
|
||||||
).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;
|
||||||
Self {
|
ConfigSubspace {
|
||||||
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))
|
||||||
|
|
@ -184,18 +207,18 @@ 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dim(&self) -> usize {
|
pub fn dim(&self) -> usize {
|
||||||
self.basis_std.len()
|
self.basis_std.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assembly_dim(&self) -> usize {
|
pub fn assembly_dim(&self) -> usize {
|
||||||
self.assembly_dim
|
self.assembly_dim
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the projection onto this subspace of the motion where the element
|
// find the projection onto this subspace of the motion where the element
|
||||||
// with the given column index has velocity `v`. the velocity is given in
|
// with the given column index has velocity `v`. the velocity is given in
|
||||||
// projection coordinates, and the projection is done with respect to the
|
// projection coordinates, and the projection is done with respect to the
|
||||||
|
|
@ -218,18 +241,18 @@ 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 hess_eigvals: Vec<DVector<f64>>,
|
pub min_eigval: Vec<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 {
|
||||||
pub fn new() -> Self {
|
fn new() -> DescentHistory {
|
||||||
Self {
|
DescentHistory {
|
||||||
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(),
|
||||||
hess_eigvals: Vec::<DVector<f64>>::new(),
|
min_eigval: Vec::<f64>::new(),
|
||||||
base_step: Vec::<DMatrix<f64>>::new(),
|
base_step: Vec::<DMatrix<f64>>::new(),
|
||||||
backoff_steps: Vec::<i32>::new(),
|
backoff_steps: Vec::<i32>::new(),
|
||||||
}
|
}
|
||||||
|
|
@ -245,21 +268,21 @@ pub struct ConstraintProblem {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConstraintProblem {
|
impl ConstraintProblem {
|
||||||
pub fn new(element_count: usize) -> Self {
|
pub fn new(element_count: usize) -> ConstraintProblem {
|
||||||
const ELEMENT_DIM: usize = 5;
|
const ELEMENT_DIM: usize = 5;
|
||||||
Self {
|
ConstraintProblem {
|
||||||
gram: PartialMatrix::new(),
|
gram: PartialMatrix::new(),
|
||||||
frozen: PartialMatrix::new(),
|
frozen: PartialMatrix::new(),
|
||||||
guess: DMatrix::<f64>::zeros(ELEMENT_DIM, element_count),
|
guess: DMatrix::<f64>::zeros(ELEMENT_DIM, element_count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "dev")]
|
#[cfg(feature = "dev")]
|
||||||
pub fn from_guess(guess_columns: &[DVector<f64>]) -> Self {
|
pub fn from_guess(guess_columns: &[DVector<f64>]) -> ConstraintProblem {
|
||||||
Self {
|
ConstraintProblem {
|
||||||
gram: PartialMatrix::new(),
|
gram: PartialMatrix::new(),
|
||||||
frozen: PartialMatrix::new(),
|
frozen: PartialMatrix::new(),
|
||||||
guess: DMatrix::from_columns(guess_columns),
|
guess: DMatrix::from_columns(guess_columns)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -273,21 +296,25 @@ 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>) -> Self {
|
fn from_config(gram: &PartialMatrix, config: DMatrix<f64>) -> SearchState {
|
||||||
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();
|
||||||
Self { config, err_proj, loss }
|
SearchState {
|
||||||
|
config: config,
|
||||||
|
err_proj: err_proj,
|
||||||
|
loss: loss
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,7 +341,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
|
||||||
|
|
@ -323,7 +350,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
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -336,7 +363,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 {
|
||||||
|
|
@ -351,17 +378,6 @@ fn seek_better_config(
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
// a first-order neighborhood of a configuration
|
|
||||||
pub struct ConfigNeighborhood {
|
|
||||||
#[cfg(feature = "dev")] 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
|
// seek a matrix `config` that matches the partial matrix `problem.frozen` and
|
||||||
// has `config' * Q * config` matching the partial matrix `problem.gram`. start
|
// has `config' * Q * config` matching the partial matrix `problem.gram`. start
|
||||||
// at `problem.guess`, set the frozen entries to their desired values, and then
|
// at `problem.guess`, set the frozen entries to their desired values, and then
|
||||||
|
|
@ -373,41 +389,30 @@ pub fn realize_gram(
|
||||||
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
|
||||||
) -> Realization {
|
) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
|
||||||
// destructure the problem data
|
// destructure the problem data
|
||||||
let ConstraintProblem { gram, guess, frozen } = problem;
|
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 {
|
|
||||||
#[cfg(feature = "dev")] 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
|
||||||
let scale_adjustment = (gram.0.len() as f64).sqrt();
|
let scale_adjustment = (gram.0.len() as f64).sqrt();
|
||||||
let tol = scale_adjustment * scaled_tol;
|
let tol = scale_adjustment * scaled_tol;
|
||||||
|
|
||||||
// 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(
|
||||||
|MatrixEntry { index: (row, col), .. }| col*element_dim + row
|
|MatrixEntry { index: (row, col), .. }| col*element_dim + row
|
||||||
).collect();
|
).collect();
|
||||||
|
|
||||||
// use a regularized Newton's method with backtracking
|
// use a regularized Newton's method with backtracking
|
||||||
let mut state = SearchState::from_config(gram, frozen.freeze(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);
|
||||||
|
|
@ -416,7 +421,7 @@ pub fn realize_gram(
|
||||||
let neg_grad = 4.0 * &*Q * &state.config * &state.err_proj;
|
let neg_grad = 4.0 * &*Q * &state.config * &state.err_proj;
|
||||||
let mut neg_grad_stacked = neg_grad.clone().reshape_generic(Dyn(total_dim), Const::<1>);
|
let mut neg_grad_stacked = neg_grad.clone().reshape_generic(Dyn(total_dim), Const::<1>);
|
||||||
history.neg_grad.push(neg_grad.clone());
|
history.neg_grad.push(neg_grad.clone());
|
||||||
|
|
||||||
// find the negative Hessian of the loss function
|
// find the negative Hessian of the loss function
|
||||||
let mut hess_cols = Vec::<DVector<f64>>::with_capacity(total_dim);
|
let mut hess_cols = Vec::<DVector<f64>>::with_capacity(total_dim);
|
||||||
for col in 0..assembly_dim {
|
for col in 0..assembly_dim {
|
||||||
|
|
@ -435,15 +440,14 @@ 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 hess_eigvals = hess.symmetric_eigenvalues();
|
let min_eigval = hess.symmetric_eigenvalues().min();
|
||||||
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.hess_eigvals.push(hess_eigvals);
|
history.min_eigval.push(min_eigval);
|
||||||
|
|
||||||
// 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
|
||||||
let zero_col = DVector::zeros(total_dim);
|
let zero_col = DVector::zeros(total_dim);
|
||||||
|
|
@ -454,47 +458,37 @@ pub fn realize_gram(
|
||||||
hess.set_column(k, &zero_col);
|
hess.set_column(k, &zero_col);
|
||||||
hess[(k, k)] = 1.0;
|
hess[(k, k)] = 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop if the loss is tolerably low
|
// stop if the loss is tolerably low
|
||||||
history.config.push(state.config.clone());
|
history.config.push(state.config.clone());
|
||||||
history.scaled_loss.push(state.loss / scale_adjustment);
|
history.scaled_loss.push(state.loss / scale_adjustment);
|
||||||
if state.loss < tol { break; }
|
if state.loss < tol { break; }
|
||||||
|
|
||||||
// compute the Newton step
|
// compute the Newton step
|
||||||
/* TO DO */
|
|
||||||
/*
|
/*
|
||||||
we should change our regularization to ensure that the Hessian is
|
we need to either handle or eliminate the case where the minimum
|
||||||
is positive-definite, rather than just positive-semidefinite. ideally,
|
eigenvalue of the Hessian is zero, so the regularized Hessian is
|
||||||
that would guarantee the success of the Cholesky decomposition---
|
singular. right now, this causes the Cholesky decomposition to return
|
||||||
although we'd still need the error-handling routine in case of
|
`None`, leading to a panic when we unrap
|
||||||
numerical hiccups
|
|
||||||
*/
|
*/
|
||||||
let hess_cholesky = match hess.clone().cholesky() {
|
let base_step_stacked = hess.clone().cholesky().unwrap().solve(&neg_grad_stacked);
|
||||||
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
|
||||||
if let Some((better_state, backoff_steps)) = seek_better_config(
|
match 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
|
||||||
) {
|
) {
|
||||||
state = better_state;
|
Some((better_state, backoff_steps)) => {
|
||||||
history.backoff_steps.push(backoff_steps);
|
state = better_state;
|
||||||
} else {
|
history.backoff_steps.push(backoff_steps);
|
||||||
return Realization {
|
},
|
||||||
result: Err("Line search failed".to_string()),
|
None => return (state.config, ConfigSubspace::zero(assembly_dim), false, history)
|
||||||
history,
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let result = if state.loss < tol {
|
let success = state.loss < tol;
|
||||||
|
let tangent = if success {
|
||||||
// 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;
|
||||||
|
|
@ -505,15 +499,13 @@ pub fn realize_gram(
|
||||||
.view_mut(block_start, (element_dim, UNIFORM_DIM))
|
.view_mut(block_start, (element_dim, UNIFORM_DIM))
|
||||||
.copy_from(&local_unif_to_std(state.config.column(n)));
|
.copy_from(&local_unif_to_std(state.config.column(n)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the kernel of the Hessian. give it the uniform inner product
|
// find the kernel of the Hessian. give it the uniform inner product
|
||||||
let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim);
|
ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim)
|
||||||
|
|
||||||
Ok(ConfigNeighborhood { #[cfg(feature = "dev")] config: state.config, nbhd: tangent })
|
|
||||||
} else {
|
} else {
|
||||||
Err("Failed to reach target accuracy".to_string())
|
ConfigSubspace::zero(assembly_dim)
|
||||||
};
|
};
|
||||||
Realization { result, history }
|
(state.config, tangent, success, history)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- tests ---
|
// --- tests ---
|
||||||
|
|
@ -521,9 +513,9 @@ pub fn realize_gram(
|
||||||
#[cfg(feature = "dev")]
|
#[cfg(feature = "dev")]
|
||||||
pub mod examples {
|
pub mod examples {
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
// this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article
|
// this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article
|
||||||
// below includes a nice translation of the problem statement, which was
|
// below includes a nice translation of the problem statement, which was
|
||||||
// recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and
|
// recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and
|
||||||
|
|
@ -532,12 +524,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) -> Realization {
|
pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
|
||||||
let mut problem = ConstraintProblem::from_guess(
|
let mut problem = ConstraintProblem::from_guess(
|
||||||
[
|
[
|
||||||
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| {
|
||||||
|
|
@ -547,43 +539,43 @@ pub mod examples {
|
||||||
)
|
)
|
||||||
).collect::<Vec<_>>().as_slice()
|
).collect::<Vec<_>>().as_slice()
|
||||||
);
|
);
|
||||||
|
|
||||||
for s in 0..9 {
|
for s in 0..9 {
|
||||||
// each sphere is represented by a spacelike vector
|
// each sphere is represented by a spacelike vector
|
||||||
problem.gram.push_sym(s, s, 1.0);
|
problem.gram.push_sym(s, s, 1.0);
|
||||||
|
|
||||||
// the circumscribing sphere is tangent to all of the other
|
// the circumscribing sphere is tangent to all of the other
|
||||||
// spheres, with matching orientation
|
// spheres, with matching orientation
|
||||||
if s > 0 {
|
if s > 0 {
|
||||||
problem.gram.push_sym(0, s, 1.0);
|
problem.gram.push_sym(0, s, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if s > 2 {
|
if s > 2 {
|
||||||
// each chain sphere is tangent to the "sun" and "moon"
|
// each chain sphere is tangent to the "sun" and "moon"
|
||||||
// spheres, with opposing orientation
|
// spheres, with opposing orientation
|
||||||
for n in 1..3 {
|
for n in 1..3 {
|
||||||
problem.gram.push_sym(s, n, -1.0);
|
problem.gram.push_sym(s, n, -1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// each chain sphere is tangent to the next chain sphere,
|
// each chain sphere is tangent to the next chain sphere,
|
||||||
// with opposing orientation
|
// with opposing orientation
|
||||||
let s_next = 3 + (s-2) % 6;
|
let s_next = 3 + (s-2) % 6;
|
||||||
problem.gram.push_sym(s, s_next, -1.0);
|
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
|
||||||
for k in 0..4 {
|
for k in 0..4 {
|
||||||
problem.frozen.push(3, k, problem.guess[(3, k)]);
|
problem.frozen.push(3, k, problem.guess[(3, k)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110)
|
realize_gram(&problem, 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) -> Realization {
|
pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix<f64>, ConfigSubspace, bool, DescentHistory) {
|
||||||
const N_HINGES: usize = 6;
|
const N_HINGES: usize = 6;
|
||||||
let mut problem = ConstraintProblem::from_guess(
|
let mut problem = ConstraintProblem::from_guess(
|
||||||
(0..N_HINGES).step_by(2).flat_map(
|
(0..N_HINGES).step_by(2).flat_map(
|
||||||
|
|
@ -596,12 +588,12 @@ 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<_>>().as_slice()
|
).collect::<Vec<_>>().as_slice()
|
||||||
);
|
);
|
||||||
|
|
||||||
const N_POINTS: usize = 2 * N_HINGES;
|
const N_POINTS: usize = 2 * N_HINGES;
|
||||||
for block in (0..N_POINTS).step_by(2) {
|
for block in (0..N_POINTS).step_by(2) {
|
||||||
let block_next = (block + 2) % N_POINTS;
|
let block_next = (block + 2) % N_POINTS;
|
||||||
|
|
@ -610,18 +602,18 @@ pub mod examples {
|
||||||
for k in j..2 {
|
for k in j..2 {
|
||||||
problem.gram.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 });
|
problem.gram.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// non-hinge edges
|
// non-hinge edges
|
||||||
for k in 0..2 {
|
for k in 0..2 {
|
||||||
problem.gram.push_sym(block + j, block_next + k, -0.625);
|
problem.gram.push_sym(block + j, block_next + k, -0.625);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for k in 0..N_POINTS {
|
for k in 0..N_POINTS {
|
||||||
problem.frozen.push(3, k, problem.guess[(3, k)])
|
problem.frozen.push(3, k, problem.guess[(3, k)])
|
||||||
}
|
}
|
||||||
|
|
||||||
realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110)
|
realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -630,47 +622,47 @@ pub mod examples {
|
||||||
mod tests {
|
mod tests {
|
||||||
use nalgebra::Vector3;
|
use nalgebra::Vector3;
|
||||||
use std::{f64::consts::{FRAC_1_SQRT_2, PI}, iter};
|
use std::{f64::consts::{FRAC_1_SQRT_2, PI}, iter};
|
||||||
|
|
||||||
use super::{*, examples::*};
|
use super::{*, examples::*};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn freeze_test() {
|
fn freeze_test() {
|
||||||
let frozen = PartialMatrix(vec![
|
let frozen = PartialMatrix(vec![
|
||||||
MatrixEntry { index: (0, 0), value: 14.0 },
|
MatrixEntry { index: (0, 0), value: 14.0 },
|
||||||
MatrixEntry { index: (0, 2), value: 28.0 },
|
MatrixEntry { index: (0, 2), value: 28.0 },
|
||||||
MatrixEntry { index: (1, 1), value: 42.0 },
|
MatrixEntry { index: (1, 1), value: 42.0 },
|
||||||
MatrixEntry { index: (1, 2), value: 49.0 },
|
MatrixEntry { index: (1, 2), value: 49.0 }
|
||||||
]);
|
]);
|
||||||
let config = DMatrix::<f64>::from_row_slice(2, 3, &[
|
let config = 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, &[
|
||||||
14.0, 2.0, 28.0,
|
14.0, 2.0, 28.0,
|
||||||
4.0, 42.0, 49.0,
|
4.0, 42.0, 49.0
|
||||||
]);
|
]);
|
||||||
assert_eq!(frozen.freeze(&config), expected_result);
|
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 mut gram = PartialMatrix::new();
|
let mut gram = PartialMatrix::new();
|
||||||
|
|
@ -684,13 +676,13 @@ mod tests {
|
||||||
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 */
|
/* 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 have the desired values
|
// and the realized configuration should have the desired values
|
||||||
|
|
@ -698,7 +690,7 @@ mod tests {
|
||||||
fn frozen_entry_test() {
|
fn frozen_entry_test() {
|
||||||
let mut problem = ConstraintProblem::from_guess(&[
|
let mut problem = ConstraintProblem::from_guess(&[
|
||||||
point(0.0, 0.0, 2.0),
|
point(0.0, 0.0, 2.0),
|
||||||
sphere(0.0, 0.0, 0.0, 0.95),
|
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 {
|
||||||
|
|
@ -707,10 +699,10 @@ mod tests {
|
||||||
}
|
}
|
||||||
problem.frozen.push(3, 0, problem.guess[(3, 0)]);
|
problem.frozen.push(3, 0, problem.guess[(3, 0)]);
|
||||||
problem.frozen.push(3, 1, 0.5);
|
problem.frozen.push(3, 1, 0.5);
|
||||||
let Realization { result, history } = realize_gram(
|
let (config, _, success, history) = realize_gram(
|
||||||
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||||
);
|
);
|
||||||
let config = result.unwrap().config;
|
assert_eq!(success, true);
|
||||||
for base_step in history.base_step.into_iter() {
|
for base_step in history.base_step.into_iter() {
|
||||||
for &MatrixEntry { index, .. } in &problem.frozen {
|
for &MatrixEntry { index, .. } in &problem.frozen {
|
||||||
assert_eq!(base_step[index], 0.0);
|
assert_eq!(base_step[index], 0.0);
|
||||||
|
|
@ -720,13 +712,13 @@ mod tests {
|
||||||
assert_eq!(config[index], value);
|
assert_eq!(config[index], value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
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).result.unwrap().config;
|
let (config, _, _, _) = realize_irisawa_hexlet(SCALED_TOL);
|
||||||
|
|
||||||
// check against Irisawa's solution
|
// check against Irisawa's solution
|
||||||
let entry_tol = SCALED_TOL.sqrt();
|
let entry_tol = SCALED_TOL.sqrt();
|
||||||
let solution_diams = [30.0, 10.0, 6.0, 5.0, 15.0, 10.0, 3.75, 2.5, 2.0 + 8.0/11.0];
|
let solution_diams = [30.0, 10.0, 6.0, 5.0, 15.0, 10.0, 3.75, 2.5, 2.0 + 8.0/11.0];
|
||||||
|
|
@ -734,7 +726,7 @@ mod tests {
|
||||||
assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol);
|
assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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;
|
||||||
|
|
@ -742,7 +734,7 @@ mod tests {
|
||||||
let mut problem = ConstraintProblem::from_guess(&[
|
let mut problem = ConstraintProblem::from_guess(&[
|
||||||
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)
|
||||||
]);
|
]);
|
||||||
for j in 0..3 {
|
for j in 0..3 {
|
||||||
for k in j..3 {
|
for k in j..3 {
|
||||||
|
|
@ -752,13 +744,13 @@ mod tests {
|
||||||
for n in 0..ELEMENT_DIM {
|
for n in 0..ELEMENT_DIM {
|
||||||
problem.frozen.push(n, 0, problem.guess[(n, 0)]);
|
problem.frozen.push(n, 0, problem.guess[(n, 0)]);
|
||||||
}
|
}
|
||||||
let Realization { result, history } = realize_gram(
|
let (config, tangent, success, history) = realize_gram(
|
||||||
&problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
&problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
||||||
);
|
);
|
||||||
let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap();
|
|
||||||
assert_eq!(config, problem.guess);
|
assert_eq!(config, problem.guess);
|
||||||
|
assert_eq!(success, true);
|
||||||
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;
|
||||||
|
|
@ -772,8 +764,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),
|
||||||
|
|
@ -783,14 +775,14 @@ 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
|
||||||
// expected
|
// expected
|
||||||
assert_eq!(tangent.basis_std.len(), tangent_motions_std.len());
|
assert_eq!(tangent.basis_std.len(), tangent_motions_std.len());
|
||||||
|
|
||||||
// confirm that the tangent space contains all the motions we expect it
|
// confirm that the tangent space contains all the motions we expect it
|
||||||
// to. since we've already bounded the dimension of the tangent space,
|
// to. since we've already bounded the dimension of the tangent space,
|
||||||
// this confirms that the tangent space is what we expect it to be
|
// this confirms that the tangent space is what we expect it to be
|
||||||
|
|
@ -802,13 +794,13 @@ mod tests {
|
||||||
assert!((motion_std - motion_proj).norm_squared() < tol_sq);
|
assert!((motion_std - motion_proj).norm_squared() < tol_sq);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translation_motion_unif(vel: &Vector3<f64>, assembly_dim: usize) -> Vec<DVector<f64>> {
|
fn translation_motion_unif(vel: &Vector3<f64>, assembly_dim: usize) -> Vec<DVector<f64>> {
|
||||||
let mut elt_motion = DVector::zeros(4);
|
let mut elt_motion = DVector::zeros(4);
|
||||||
elt_motion.fixed_rows_mut::<3>(0).copy_from(vel);
|
elt_motion.fixed_rows_mut::<3>(0).copy_from(vel);
|
||||||
iter::repeat(elt_motion).take(assembly_dim).collect()
|
iter::repeat(elt_motion).take(assembly_dim).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rotation_motion_unif(ang_vel: &Vector3<f64>, points: Vec<DVectorView<f64>>) -> Vec<DVector<f64>> {
|
fn rotation_motion_unif(ang_vel: &Vector3<f64>, points: Vec<DVectorView<f64>>) -> Vec<DVector<f64>> {
|
||||||
points.into_iter().map(
|
points.into_iter().map(
|
||||||
|pt| {
|
|pt| {
|
||||||
|
|
@ -819,15 +811,15 @@ mod tests {
|
||||||
}
|
}
|
||||||
).collect()
|
).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
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 Realization { result, history } = realize_kaleidocycle(SCALED_TOL);
|
let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL);
|
||||||
let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap();
|
assert_eq!(success, true);
|
||||||
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 N_HINGES: usize = 6;
|
const N_HINGES: usize = 6;
|
||||||
|
|
@ -838,12 +830,12 @@ mod tests {
|
||||||
translation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), assembly_dim),
|
translation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), assembly_dim),
|
||||||
translation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), assembly_dim),
|
translation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), assembly_dim),
|
||||||
translation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), assembly_dim),
|
translation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), assembly_dim),
|
||||||
|
|
||||||
// the rotations about the coordinate axes
|
// the rotations about the coordinate axes
|
||||||
rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), config.column_iter().collect()),
|
rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), config.column_iter().collect()),
|
||||||
rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), config.column_iter().collect()),
|
rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), config.column_iter().collect()),
|
||||||
rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), config.column_iter().collect()),
|
rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), config.column_iter().collect()),
|
||||||
|
|
||||||
// the twist motion. more precisely: a motion that keeps the center
|
// the twist motion. more precisely: a motion that keeps the center
|
||||||
// of mass stationary and preserves the distances between the
|
// of mass stationary and preserves the distances between the
|
||||||
// vertices to first order. this has to be the twist as long as:
|
// vertices to first order. this has to be the twist as long as:
|
||||||
|
|
@ -860,10 +852,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(
|
||||||
|
|
@ -872,11 +864,11 @@ mod tests {
|
||||||
).collect::<Vec<_>>()
|
).collect::<Vec<_>>()
|
||||||
)
|
)
|
||||||
).collect::<Vec<_>>();
|
).collect::<Vec<_>>();
|
||||||
|
|
||||||
// confirm that the dimension of the tangent space is no greater than
|
// confirm that the dimension of the tangent space is no greater than
|
||||||
// expected
|
// expected
|
||||||
assert_eq!(tangent.basis_std.len(), tangent_motions_unif.len());
|
assert_eq!(tangent.basis_std.len(), tangent_motions_unif.len());
|
||||||
|
|
||||||
// confirm that the tangent space contains all the motions we expect it
|
// confirm that the tangent space contains all the motions we expect it
|
||||||
// to. since we've already bounded the dimension of the tangent space,
|
// to. since we've already bounded the dimension of the tangent space,
|
||||||
// this confirms that the tangent space is what we expect it to be
|
// this confirms that the tangent space is what we expect it to be
|
||||||
|
|
@ -888,7 +880,7 @@ mod tests {
|
||||||
assert!((motion_std - motion_proj).norm_squared() < tol_sq);
|
assert!((motion_std - motion_proj).norm_squared() < tol_sq);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translation(dis: Vector3<f64>) -> DMatrix<f64> {
|
fn translation(dis: Vector3<f64>) -> DMatrix<f64> {
|
||||||
const ELEMENT_DIM: usize = 5;
|
const ELEMENT_DIM: usize = 5;
|
||||||
DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[
|
DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[
|
||||||
|
|
@ -896,10 +888,10 @@ 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
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
// confirm that projection onto a configuration subspace is equivariant with
|
// confirm that projection onto a configuration subspace is equivariant with
|
||||||
// respect to Euclidean motions
|
// respect to Euclidean motions
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -908,49 +900,49 @@ mod tests {
|
||||||
const SCALED_TOL: f64 = 1.0e-12;
|
const SCALED_TOL: f64 = 1.0e-12;
|
||||||
let mut problem_orig = ConstraintProblem::from_guess(&[
|
let mut problem_orig = ConstraintProblem::from_guess(&[
|
||||||
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)
|
||||||
]);
|
]);
|
||||||
problem_orig.gram.push_sym(0, 0, 1.0);
|
problem_orig.gram.push_sym(0, 0, 1.0);
|
||||||
problem_orig.gram.push_sym(1, 1, 1.0);
|
problem_orig.gram.push_sym(1, 1, 1.0);
|
||||||
problem_orig.gram.push_sym(0, 1, 0.5);
|
problem_orig.gram.push_sym(0, 1, 0.5);
|
||||||
let Realization { result: result_orig, history: history_orig } = realize_gram(
|
let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram(
|
||||||
&problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
&problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
||||||
);
|
);
|
||||||
let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap();
|
|
||||||
assert_eq!(config_orig, problem_orig.guess);
|
assert_eq!(config_orig, problem_orig.guess);
|
||||||
|
assert_eq!(success_orig, true);
|
||||||
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
|
||||||
// solution as a transformed version of the original one
|
// solution as a transformed version of the original one
|
||||||
let guess_tfm = {
|
let guess_tfm = {
|
||||||
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 problem_tfm = ConstraintProblem {
|
let problem_tfm = ConstraintProblem {
|
||||||
gram: problem_orig.gram,
|
gram: problem_orig.gram,
|
||||||
frozen: problem_orig.frozen,
|
|
||||||
guess: guess_tfm,
|
guess: guess_tfm,
|
||||||
|
frozen: problem_orig.frozen
|
||||||
};
|
};
|
||||||
let Realization { result: result_tfm, history: history_tfm } = realize_gram(
|
let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram(
|
||||||
&problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
&problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
|
||||||
);
|
);
|
||||||
let ConfigNeighborhood { config: config_tfm, nbhd: tangent_tfm } = result_tfm.unwrap();
|
|
||||||
assert_eq!(config_tfm, problem_tfm.guess);
|
assert_eq!(config_tfm, problem_tfm.guess);
|
||||||
|
assert_eq!(success_tfm, true);
|
||||||
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
|
||||||
// original solution
|
// original solution
|
||||||
let motion_orig = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]);
|
let motion_orig = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]);
|
||||||
let motion_orig_proj = tangent_orig.proj(&motion_orig.as_view(), 0);
|
let motion_orig_proj = tangent_orig.proj(&motion_orig.as_view(), 0);
|
||||||
|
|
||||||
// project the equivalent nudge to the tangent space of the solution
|
// project the equivalent nudge to the tangent space of the solution
|
||||||
// variety at the transformed solution
|
// variety at the transformed solution
|
||||||
let motion_tfm = DVector::from_column_slice(&[FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0]);
|
let motion_tfm = DVector::from_column_slice(&[FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0]);
|
||||||
let motion_tfm_proj = tangent_tfm.proj(&motion_tfm.as_view(), 0);
|
let motion_tfm_proj = tangent_tfm.proj(&motion_tfm.as_view(), 0);
|
||||||
|
|
||||||
// take the transformation that sends the original solution to the
|
// take the transformation that sends the original solution to the
|
||||||
// transformed solution and apply it to the motion that the original
|
// transformed solution and apply it to the motion that the original
|
||||||
// solution makes in response to the nudge
|
// solution makes in response to the nudge
|
||||||
|
|
@ -960,11 +952,11 @@ 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;
|
||||||
|
|
||||||
// confirm that the projection of the nudge is equivariant. we loosen
|
// confirm that the projection of the nudge is equivariant. we loosen
|
||||||
// the comparison tolerance because the transformation seems to
|
// the comparison tolerance because the transformation seems to
|
||||||
// introduce some numerical error
|
// introduce some numerical error
|
||||||
|
|
@ -972,4 +964,4 @@ mod tests {
|
||||||
let tol_sq = ((problem_orig.guess.nrows() * problem_orig.guess.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 vec4 color_list[SPHERE_MAX];
|
uniform vec3 color_list[SPHERE_MAX];
|
||||||
uniform float highlight_list[SPHERE_MAX];
|
uniform float highlight_list[SPHERE_MAX];
|
||||||
|
|
||||||
// view
|
// view
|
||||||
|
|
@ -25,6 +25,7 @@ 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;
|
||||||
|
|
||||||
|
|
@ -68,17 +69,17 @@ struct Fragment {
|
||||||
vec4 color;
|
vec4 color;
|
||||||
};
|
};
|
||||||
|
|
||||||
Fragment sphere_shading(vecInv v, vec3 pt, vec4 base_color) {
|
Fragment sphere_shading(vecInv v, vec3 pt, vec3 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
|
||||||
// point. i calculated it in my head and decided that the result looked good
|
// point. i calculated it in my head and decided that the result looked good
|
||||||
// enough for now
|
// enough for now
|
||||||
vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt);
|
vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt);
|
||||||
|
|
||||||
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.rgb, base_color.a));
|
return Fragment(pt, normal, vec4(illum * base_color, opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
float intersection_dist(Fragment a, Fragment b) {
|
float intersection_dist(Fragment a, Fragment b) {
|
||||||
|
|
@ -110,7 +111,7 @@ vec2 sphere_cast(vecInv v, vec3 dir) {
|
||||||
float a = -v.lt.s * dot(dir, dir);
|
float a = -v.lt.s * dot(dir, dir);
|
||||||
float b = dot(v.sp, dir);
|
float b = dot(v.sp, dir);
|
||||||
float c = -v.lt.t;
|
float c = -v.lt.t;
|
||||||
|
|
||||||
float adjust = 4.*a*c/(b*b);
|
float adjust = 4.*a*c/(b*b);
|
||||||
if (adjust < 1.) {
|
if (adjust < 1.) {
|
||||||
// as long as `b` is non-zero, the linear approximation of
|
// as long as `b` is non-zero, the linear approximation of
|
||||||
|
|
@ -136,7 +137,7 @@ vec2 sphere_cast(vecInv v, vec3 dir) {
|
||||||
void main() {
|
void main() {
|
||||||
vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim;
|
vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim;
|
||||||
vec3 dir = vec3(focal_slope * scr, -1.);
|
vec3 dir = vec3(focal_slope * scr, -1.);
|
||||||
|
|
||||||
// cast rays through the spheres
|
// cast rays through the spheres
|
||||||
const int LAYER_MAX = 12;
|
const int LAYER_MAX = 12;
|
||||||
TaggedDepth top_hits [LAYER_MAX];
|
TaggedDepth top_hits [LAYER_MAX];
|
||||||
|
|
@ -144,7 +145,7 @@ void main() {
|
||||||
for (int id = 0; id < sphere_cnt; ++id) {
|
for (int id = 0; id < sphere_cnt; ++id) {
|
||||||
// find out where the ray hits the sphere
|
// find out where the ray hits the sphere
|
||||||
vec2 hit_depths = sphere_cast(sphere_list[id], dir);
|
vec2 hit_depths = sphere_cast(sphere_list[id], dir);
|
||||||
|
|
||||||
// insertion-sort the points we hit into the hit list
|
// insertion-sort the points we hit into the hit list
|
||||||
float dimming = 1.;
|
float dimming = 1.;
|
||||||
for (int side = 0; side < 2; ++side) {
|
for (int side = 0; side < 2; ++side) {
|
||||||
|
|
@ -169,14 +170,14 @@ void main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* DEBUG */
|
/* DEBUG */
|
||||||
// in debug mode, show the layer count instead of the shaded image
|
// in debug mode, show the layer count instead of the shaded image
|
||||||
if (debug_mode) {
|
if (debug_mode) {
|
||||||
// at the bottom of the screen, show the color scale instead of the
|
// at the bottom of the screen, show the color scale instead of the
|
||||||
// layer count
|
// layer count
|
||||||
if (gl_FragCoord.y < 10.) layer_cnt = int(16. * gl_FragCoord.x / resolution.x);
|
if (gl_FragCoord.y < 10.) layer_cnt = int(16. * gl_FragCoord.x / resolution.x);
|
||||||
|
|
||||||
// convert number to color
|
// convert number to color
|
||||||
ivec3 bits = layer_cnt / ivec3(1, 2, 4);
|
ivec3 bits = layer_cnt / ivec3(1, 2, 4);
|
||||||
vec3 color = mod(vec3(bits), 2.);
|
vec3 color = mod(vec3(bits), 2.);
|
||||||
|
|
@ -186,16 +187,15 @@ void main() {
|
||||||
outColor = vec4(color, 1.);
|
outColor = vec4(color, 1.);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// composite the sphere fragments
|
// composite the sphere fragments
|
||||||
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,
|
||||||
vec4(hit.dimming * sphere_color.rgb, sphere_color.a)
|
hit.dimming * color_list[hit.id]
|
||||||
);
|
);
|
||||||
float highlight_next = highlight_list[hit.id];
|
float highlight_next = highlight_list[hit.id];
|
||||||
--layer;
|
--layer;
|
||||||
|
|
@ -203,33 +203,32 @@ void main() {
|
||||||
// load the current fragment
|
// load the current fragment
|
||||||
Fragment frag = frag_next;
|
Fragment frag = frag_next;
|
||||||
float highlight = highlight_next;
|
float highlight = highlight_next;
|
||||||
|
|
||||||
// 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,
|
||||||
vec4(hit.dimming * sphere_color.rgb, sphere_color.a)
|
hit.dimming * color_list[hit.id]
|
||||||
);
|
);
|
||||||
highlight_next = highlight_list[hit.id];
|
highlight_next = highlight_list[hit.id];
|
||||||
|
|
||||||
// highlight intersections
|
// highlight intersections
|
||||||
float ixn_dist = intersection_dist(frag, frag_next);
|
float ixn_dist = intersection_dist(frag, frag_next);
|
||||||
float max_highlight = max(highlight, highlight_next);
|
float max_highlight = max(highlight, highlight_next);
|
||||||
float ixn_highlight = 0.5 * max_highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist));
|
float ixn_highlight = 0.5 * max_highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist));
|
||||||
frag.color = mix(frag.color, vec4(1.), ixn_highlight);
|
frag.color = mix(frag.color, vec4(1.), ixn_highlight);
|
||||||
frag_next.color = mix(frag_next.color, vec4(1.), ixn_highlight);
|
frag_next.color = mix(frag_next.color, vec4(1.), ixn_highlight);
|
||||||
|
|
||||||
// highlight cusps
|
// highlight cusps
|
||||||
float cusp_cos = abs(dot(dir, frag.normal));
|
float cusp_cos = abs(dot(dir, frag.normal));
|
||||||
float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[hit.id].lt.s);
|
float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[hit.id].lt.s);
|
||||||
float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos));
|
float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos));
|
||||||
frag.color = mix(frag.color, vec4(1.), cusp_highlight);
|
frag.color = mix(frag.color, vec4(1.), cusp_highlight);
|
||||||
|
|
||||||
// composite the current fragment
|
// composite the current fragment
|
||||||
color = mix(color, frag.color.rgb, frag.color.a);
|
color = mix(color, frag.color.rgb, frag.color.a);
|
||||||
}
|
}
|
||||||
color = mix(color, frag_next.color.rgb, frag_next.color.a);
|
color = mix(color, frag_next.color.rgb, frag_next.color.a);
|
||||||
outColor = vec4(sRGB(color), 1.);
|
outColor = vec4(sRGB(color), 1.);
|
||||||
}
|
}
|
||||||
|
|
@ -1,49 +1,49 @@
|
||||||
|
mod add_remove;
|
||||||
mod assembly;
|
mod assembly;
|
||||||
mod components;
|
mod display;
|
||||||
mod engine;
|
mod engine;
|
||||||
|
mod outline;
|
||||||
mod specified;
|
mod specified;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use std::{collections::BTreeSet, rc::Rc};
|
use rustc_hash::FxHashSet;
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
|
|
||||||
use assembly::{Assembly, Element};
|
use add_remove::AddRemove;
|
||||||
use components::{
|
use assembly::{Assembly, ElementKey};
|
||||||
add_remove::AddRemove,
|
use display::Display;
|
||||||
diagnostics::Diagnostics,
|
use outline::Outline;
|
||||||
display::Display,
|
|
||||||
outline::Outline,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
assembly: Assembly,
|
assembly: Assembly,
|
||||||
selection: Signal<BTreeSet<Rc<dyn Element>>>,
|
selection: Signal<FxHashSet<ElementKey>>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
fn new() -> Self {
|
fn new() -> AppState {
|
||||||
Self {
|
AppState {
|
||||||
assembly: Assembly::new(),
|
assembly: Assembly::new(),
|
||||||
selection: create_signal(BTreeSet::default()),
|
selection: create_signal(FxHashSet::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// in single-selection mode, select the given element. in multiple-selection
|
// in single-selection mode, select the element with the given key. in
|
||||||
// mode, toggle whether the given element is selected
|
// multiple-selection mode, toggle whether the element with the given key
|
||||||
fn select(&self, element: &Rc<dyn Element>, multi: bool) {
|
// is selected
|
||||||
|
fn select(&self, key: ElementKey, multi: bool) {
|
||||||
if multi {
|
if multi {
|
||||||
self.selection.update(|sel| {
|
self.selection.update(|sel| {
|
||||||
if !sel.remove(element) {
|
if !sel.remove(&key) {
|
||||||
sel.insert(element.clone());
|
sel.insert(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
self.selection.update(|sel| {
|
self.selection.update(|sel| {
|
||||||
sel.clear();
|
sel.clear();
|
||||||
sel.insert(element.clone());
|
sel.insert(key);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -53,17 +53,16 @@ fn main() {
|
||||||
// set the console error panic hook
|
// set the console error panic hook
|
||||||
#[cfg(feature = "console_error_panic_hook")]
|
#[cfg(feature = "console_error_panic_hook")]
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
sycamore::render(|| {
|
sycamore::render(|| {
|
||||||
provide_context(AppState::new());
|
provide_context(AppState::new());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
div(id = "sidebar") {
|
div(id="sidebar") {
|
||||||
AddRemove {}
|
AddRemove {}
|
||||||
Outline {}
|
Outline {}
|
||||||
Diagnostics {}
|
|
||||||
}
|
}
|
||||||
Display {}
|
Display {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use web_sys::{KeyboardEvent, MouseEvent, wasm_bindgen::JsCast};
|
use web_sys::{
|
||||||
|
KeyboardEvent,
|
||||||
|
MouseEvent,
|
||||||
|
wasm_bindgen::JsCast
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState,
|
AppState,
|
||||||
|
assembly,
|
||||||
assembly::{
|
assembly::{
|
||||||
Element,
|
ElementKey,
|
||||||
HalfCurvatureRegulator,
|
HalfCurvatureRegulator,
|
||||||
InversiveDistanceRegulator,
|
InversiveDistanceRegulator,
|
||||||
PointCoordinateRegulator,
|
|
||||||
Regulator,
|
Regulator,
|
||||||
|
RegulatorKey
|
||||||
},
|
},
|
||||||
specified::SpecifiedValue
|
specified::SpecifiedValue
|
||||||
};
|
};
|
||||||
|
|
@ -21,16 +26,16 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
|
||||||
// get the regulator's measurement and set point signals
|
// get the regulator's measurement and set point signals
|
||||||
let measurement = regulator.measurement();
|
let measurement = regulator.measurement();
|
||||||
let set_point = regulator.set_point();
|
let set_point = regulator.set_point();
|
||||||
|
|
||||||
// the `valid` signal tracks whether the last entered value is a valid set
|
// the `valid` signal tracks whether the last entered value is a valid set
|
||||||
// point specification
|
// point specification
|
||||||
let valid = create_signal(true);
|
let valid = create_signal(true);
|
||||||
|
|
||||||
// the `value` signal holds the current set point specification
|
// the `value` signal holds the current set point specification
|
||||||
let value = create_signal(
|
let value = create_signal(
|
||||||
set_point.with_untracked(|set_pt| set_pt.spec.clone())
|
set_point.with_untracked(|set_pt| set_pt.spec.clone())
|
||||||
);
|
);
|
||||||
|
|
||||||
// this `reset_value` closure resets the input value to the regulator's set
|
// this `reset_value` closure resets the input value to the regulator's set
|
||||||
// point specification
|
// point specification
|
||||||
let reset_value = move || {
|
let reset_value = move || {
|
||||||
|
|
@ -39,15 +44,15 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
|
||||||
value.set(set_point.with(|set_pt| set_pt.spec.clone()));
|
value.set(set_point.with(|set_pt| set_pt.spec.clone()));
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// reset the input value whenever the regulator's set point specification
|
// reset the input value whenever the regulator's set point specification
|
||||||
// is updated
|
// is updated
|
||||||
create_effect(reset_value);
|
create_effect(reset_value);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
input(
|
input(
|
||||||
r#type = "text",
|
r#type="text",
|
||||||
class = move || {
|
class=move || {
|
||||||
if valid.get() {
|
if valid.get() {
|
||||||
set_point.with(|set_pt| {
|
set_point.with(|set_pt| {
|
||||||
if set_pt.is_present() {
|
if set_pt.is_present() {
|
||||||
|
|
@ -60,94 +65,90 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
|
||||||
"regulator-input invalid"
|
"regulator-input invalid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
placeholder = measurement.with(|result| result.to_string()),
|
placeholder=measurement.with(|result| result.to_string()),
|
||||||
bind:value = value,
|
bind:value=value,
|
||||||
on:change = move |_| {
|
on:change=move |_| {
|
||||||
valid.set(
|
valid.set(
|
||||||
match SpecifiedValue::try_from(value.get_clone_untracked()) {
|
match SpecifiedValue::try_from(value.get_clone_untracked()) {
|
||||||
Ok(set_pt) => {
|
Ok(set_pt) => {
|
||||||
set_point.set(set_pt);
|
set_point.set(set_pt);
|
||||||
true
|
true
|
||||||
},
|
}
|
||||||
Err(_) => false,
|
Err(_) => false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
on:keydown = {
|
on:keydown={
|
||||||
move |event: KeyboardEvent| {
|
move |event: KeyboardEvent| {
|
||||||
match event.key().as_str() {
|
match event.key().as_str() {
|
||||||
"Escape" => reset_value(),
|
"Escape" => reset_value(),
|
||||||
_ => (),
|
_ => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait OutlineItem {
|
pub trait OutlineItem {
|
||||||
fn outline_item(self: Rc<Self>, element: &Rc<dyn Element>) -> View;
|
fn outline_item(self: Rc<Self>, element_key: ElementKey) -> View;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutlineItem for InversiveDistanceRegulator {
|
impl OutlineItem for InversiveDistanceRegulator {
|
||||||
fn outline_item(self: Rc<Self>, element: &Rc<dyn Element>) -> View {
|
fn outline_item(self: Rc<Self>, element_key: ElementKey) -> View {
|
||||||
let other_subject_label = if self.subjects[0] == element.clone() {
|
let state = use_context::<AppState>();
|
||||||
self.subjects[1].label()
|
let other_subject = if self.subjects[0] == element_key {
|
||||||
|
self.subjects[1]
|
||||||
} else {
|
} else {
|
||||||
self.subjects[0].label()
|
self.subjects[0]
|
||||||
}.clone();
|
};
|
||||||
|
let other_subject_label = state.assembly.elements.with(
|
||||||
|
|elts| elts[other_subject].label.clone()
|
||||||
|
);
|
||||||
view! {
|
view! {
|
||||||
li(class = "regulator") {
|
li(class="regulator") {
|
||||||
div(class = "regulator-label") { (other_subject_label) }
|
div(class="regulator-label") { (other_subject_label) }
|
||||||
div(class = "regulator-type") { "Inversive distance" }
|
div(class="regulator-type") { "Inversive distance" }
|
||||||
RegulatorInput(regulator = self)
|
RegulatorInput(regulator=self)
|
||||||
div(class = "status")
|
div(class="status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutlineItem for HalfCurvatureRegulator {
|
impl OutlineItem for HalfCurvatureRegulator {
|
||||||
fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View {
|
fn outline_item(self: Rc<Self>, _element_key: ElementKey) -> View {
|
||||||
view! {
|
view! {
|
||||||
li(class = "regulator") {
|
li(class="regulator") {
|
||||||
div(class = "regulator-label") // for spacing
|
div(class="regulator-label") // for spacing
|
||||||
div(class = "regulator-type") { "Half-curvature" }
|
div(class="regulator-type") { "Half-curvature" }
|
||||||
RegulatorInput(regulator = self)
|
RegulatorInput(regulator=self)
|
||||||
div(class = "status")
|
div(class="status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutlineItem for PointCoordinateRegulator {
|
// a list item that shows a regulator in an outline view of an element
|
||||||
fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View {
|
#[component(inline_props)]
|
||||||
let name = format!("{} coordinate", self.axis);
|
fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View {
|
||||||
view! {
|
let state = use_context::<AppState>();
|
||||||
li(class = "regulator") {
|
let regulator = state.assembly.regulators.with(
|
||||||
div(class = "regulator-label") // for spacing
|
|regs| regs[regulator_key].clone()
|
||||||
div(class = "regulator-type") { (name) }
|
);
|
||||||
RegulatorInput(regulator = self)
|
regulator.outline_item(element_key)
|
||||||
div(class = "status")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// a list item that shows an element in an outline view of an assembly
|
// a list item that shows an element in an outline view of an assembly
|
||||||
#[component(inline_props)]
|
#[component(inline_props)]
|
||||||
fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
|
fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
|
||||||
let state = use_context::<AppState>();
|
let state = use_context::<AppState>();
|
||||||
let class = {
|
let class = state.selection.map(
|
||||||
let element_for_class = element.clone();
|
move |sel| if sel.contains(&key) { "selected" } else { "" }
|
||||||
state.selection.map(
|
);
|
||||||
move |sel| if sel.contains(&element_for_class) { "selected" } else { "" }
|
let label = element.label.clone();
|
||||||
)
|
|
||||||
};
|
|
||||||
let label = element.label().clone();
|
|
||||||
let representation = element.representation().clone();
|
|
||||||
let rep_components = move || {
|
let rep_components = move || {
|
||||||
representation.with(
|
element.representation.with(
|
||||||
|rep| rep.iter().map(
|
|rep| rep.iter().map(
|
||||||
|u| {
|
|u| {
|
||||||
let u_str = format!("{:.3}", u).replace("-", "\u{2212}");
|
let u_str = format!("{:.3}", u).replace("-", "\u{2212}");
|
||||||
|
|
@ -156,26 +157,29 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
|
||||||
).collect::<Vec<_>>()
|
).collect::<Vec<_>>()
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let regulated = element.regulators().map(|regs| regs.len() > 0);
|
let regulated = element.regulators.map(|regs| regs.len() > 0);
|
||||||
let regulator_list = element.regulators().map(
|
let regulator_list = element.regulators.map(
|
||||||
|regs| regs
|
move |elt_reg_keys| elt_reg_keys
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.sorted_by_key(|reg| reg.subjects().len())
|
.sorted_by_key(
|
||||||
.collect::<Vec<_>>()
|
|®_key| state.assembly.regulators.with(
|
||||||
|
|regs| regs[reg_key].subjects().len()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.collect()
|
||||||
);
|
);
|
||||||
let details_node = create_node_ref();
|
let details_node = create_node_ref();
|
||||||
view! {
|
view! {
|
||||||
li {
|
li {
|
||||||
details(ref = details_node) {
|
details(ref=details_node) {
|
||||||
summary(
|
summary(
|
||||||
class = class.get(),
|
class=class.get(),
|
||||||
on:keydown = {
|
on:keydown={
|
||||||
let element_for_handler = element.clone();
|
|
||||||
move |event: KeyboardEvent| {
|
move |event: KeyboardEvent| {
|
||||||
match event.key().as_str() {
|
match event.key().as_str() {
|
||||||
"Enter" => {
|
"Enter" => {
|
||||||
state.select(&element_for_handler, event.shift_key());
|
state.select(key, event.shift_key());
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
},
|
},
|
||||||
"ArrowRight" if regulated.get() => {
|
"ArrowRight" if regulated.get() => {
|
||||||
|
|
@ -190,41 +194,51 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
|
||||||
.unchecked_into::<web_sys::Element>()
|
.unchecked_into::<web_sys::Element>()
|
||||||
.remove_attribute("open");
|
.remove_attribute("open");
|
||||||
},
|
},
|
||||||
_ => (),
|
_ => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
div(
|
div(
|
||||||
class = "element-switch",
|
class="element-switch",
|
||||||
on:click = |event: MouseEvent| event.stop_propagation()
|
on:click=|event: MouseEvent| event.stop_propagation()
|
||||||
)
|
)
|
||||||
div(
|
div(
|
||||||
class = "element",
|
class="element",
|
||||||
on:click = {
|
on:click={
|
||||||
let state_for_handler = state.clone();
|
|
||||||
let element_for_handler = element.clone();
|
|
||||||
move |event: MouseEvent| {
|
move |event: MouseEvent| {
|
||||||
state_for_handler.select(&element_for_handler, event.shift_key());
|
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.stop_propagation();
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
div(class = "element-label") { (label) }
|
div(class="element-label") { (label) }
|
||||||
div(class = "element-representation") { (rep_components) }
|
div(class="element-representation") { (rep_components) }
|
||||||
input(
|
div(class="status")
|
||||||
r#type = "checkbox",
|
|
||||||
bind:checked = element.ghost(),
|
|
||||||
on:click = |event: MouseEvent| event.stop_propagation()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ul(class = "regulators") {
|
ul(class="regulators") {
|
||||||
Keyed(
|
Keyed(
|
||||||
list = regulator_list,
|
list=regulator_list,
|
||||||
view = move |reg| reg.outline_item(&element),
|
view=move |reg_key| view! {
|
||||||
key = |reg| reg.serial()
|
RegulatorOutlineItem(
|
||||||
|
regulator_key=reg_key,
|
||||||
|
element_key=key
|
||||||
|
)
|
||||||
|
},
|
||||||
|
key=|reg_key| reg_key.clone()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -241,35 +255,31 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Outline() -> View {
|
pub fn Outline() -> View {
|
||||||
let state = use_context::<AppState>();
|
let state = use_context::<AppState>();
|
||||||
|
|
||||||
// list the elements alphabetically by ID
|
// 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(
|
let element_list = state.assembly.elements.map(
|
||||||
|elts| elts
|
|elts| elts
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.sorted_by_key(|elt| elt.id().clone())
|
.sorted_by_key(|(_, elt)| elt.id.clone())
|
||||||
.collect::<Vec<_>>()
|
.collect()
|
||||||
);
|
);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
ul(
|
ul(
|
||||||
id = "outline",
|
id="outline",
|
||||||
on:click = {
|
on:click={
|
||||||
let state = use_context::<AppState>();
|
let state = use_context::<AppState>();
|
||||||
move |_| state.selection.update(|sel| sel.clear())
|
move |_| state.selection.update(|sel| sel.clear())
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Keyed(
|
Keyed(
|
||||||
list = element_list,
|
list=element_list,
|
||||||
view = |elt| view! {
|
view=|(key, elt)| view! {
|
||||||
ElementOutlineItem(element = elt)
|
ElementOutlineItem(key=key, element=elt)
|
||||||
},
|
},
|
||||||
key = |elt| elt.serial()
|
key=|(_, elt)| elt.serial
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -13,43 +13,32 @@ 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() -> Self {
|
pub fn from_empty_spec() -> SpecifiedValue {
|
||||||
Self { spec: String::new(), value: None }
|
SpecifiedValue { spec: String::new(), value: None }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_present(&self) -> bool {
|
pub fn is_present(&self) -> bool {
|
||||||
matches!(self.value, Some(_))
|
matches!(self.value, Some(_))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// a `SpecifiedValue` can be constructed from a floating-point option, which is
|
|
||||||
// given a canonical specification
|
|
||||||
impl From<Option<f64>> for SpecifiedValue {
|
|
||||||
fn from(value: Option<f64>) -> Self {
|
|
||||||
match value {
|
|
||||||
Some(x) => SpecifiedValue{ spec: x.to_string(), value },
|
|
||||||
None => SpecifiedValue::from_empty_spec(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// a `SpecifiedValue` can be constructed from a specification string, formatted
|
// a `SpecifiedValue` can be constructed from a specification string, formatted
|
||||||
// as described in the comment on the structure definition. the result is `Ok`
|
// as described in the comment on the structure definition. the result is `Ok`
|
||||||
// if the specification is properly formatted, and `Error` if not
|
// if the specification is properly formatted, and `Error` if not
|
||||||
impl TryFrom<String> for SpecifiedValue {
|
impl TryFrom<String> for SpecifiedValue {
|
||||||
type Error = ParseFloatError;
|
type Error = ParseFloatError;
|
||||||
|
|
||||||
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(Self::from_empty_spec())
|
Ok(SpecifiedValue::from_empty_spec())
|
||||||
} else {
|
} else {
|
||||||
spec.parse::<f64>().map(
|
spec.parse::<f64>().map(
|
||||||
|value| Self { spec, value: Some(value) }
|
|value| SpecifiedValue { spec: spec, value: Some(value) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
5
deploy/.gitignore
vendored
5
deploy/.gitignore
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
/dyna3.zip
|
|
||||||
/dyna3/index.html
|
|
||||||
/dyna3/dyna3-*.js
|
|
||||||
/dyna3/dyna3-*.wasm
|
|
||||||
/dyna3/main-*.css
|
|
||||||
|
|
@ -33,7 +33,7 @@ The unification of spheres/planes is indeed attractive for a project like Dyna3.
|
||||||
Discussed coordinates with Alex Kontorovich. He was suggesting "inversive coordinates" -- for a sphere, that's 1/coradius, 1/radius, center/radius (where coradius is radius of sphere inverted in the unit sphere.) The advantage is tangent to and perpendicular to are linear in these coordinates (in the sense that if one is known, the condition of being tangent to or perpendicular to that one are linear). Planes have 1/radius = 0, and in fact, you can take the coordinates to be (2s, 0, x, y, z) where s is the distance to the origin and x,y,z are the normal direction. (Note the normal direction is only determined up to a scalar multiple. So could always scale so that the first non-zero coordinate is 1, or if you like only allow x, y to vary and let z be determined as sqrt(1-x^2^-y^2^). ) Points can be given by (r^2,1,x,y,z) where x,y,z are the coordinates and r is the distance to the origin. Quadratic form that tells you if something is a sphere/plane, or in the boundary, or up in the hyperbolic plane above. There are some details, but not quite explicit for modeling R^3, at http://sites.math.rutgers.edu/~alexk/files/LetterToDuke.pdf -- all this emphasize need to be agnostic with respect to geometric model so that we can experiment. Not really sure exactly how this relates or not to conformal geometric algebra, and whether it can be combined with geometric algebra. As formulated, there are clear-ish reps for planes/spheres and for points, but not as clear for lines. Have to see how to compute distance and/or specify a given distance. To combine inversive coordinates and geometric algebra, maybe think dually; there should be a lift from a normal vector and distance from origin to the five-vector; bivectors would rep circles/lines; trivectors would rep point pairs/points. What is the signature of this algebra, i.e. how many coordinates square to +1, -1, or 0? But it doesn't seem worth it for three dimensions, because there is a natural representation of points, as follows:
|
Discussed coordinates with Alex Kontorovich. He was suggesting "inversive coordinates" -- for a sphere, that's 1/coradius, 1/radius, center/radius (where coradius is radius of sphere inverted in the unit sphere.) The advantage is tangent to and perpendicular to are linear in these coordinates (in the sense that if one is known, the condition of being tangent to or perpendicular to that one are linear). Planes have 1/radius = 0, and in fact, you can take the coordinates to be (2s, 0, x, y, z) where s is the distance to the origin and x,y,z are the normal direction. (Note the normal direction is only determined up to a scalar multiple. So could always scale so that the first non-zero coordinate is 1, or if you like only allow x, y to vary and let z be determined as sqrt(1-x^2^-y^2^). ) Points can be given by (r^2,1,x,y,z) where x,y,z are the coordinates and r is the distance to the origin. Quadratic form that tells you if something is a sphere/plane, or in the boundary, or up in the hyperbolic plane above. There are some details, but not quite explicit for modeling R^3, at http://sites.math.rutgers.edu/~alexk/files/LetterToDuke.pdf -- all this emphasize need to be agnostic with respect to geometric model so that we can experiment. Not really sure exactly how this relates or not to conformal geometric algebra, and whether it can be combined with geometric algebra. As formulated, there are clear-ish reps for planes/spheres and for points, but not as clear for lines. Have to see how to compute distance and/or specify a given distance. To combine inversive coordinates and geometric algebra, maybe think dually; there should be a lift from a normal vector and distance from origin to the five-vector; bivectors would rep circles/lines; trivectors would rep point pairs/points. What is the signature of this algebra, i.e. how many coordinates square to +1, -1, or 0? But it doesn't seem worth it for three dimensions, because there is a natural representation of points, as follows:
|
||||||
|
|
||||||
The signature of Q will be (1,4), and in fact Q(I1,I2) = 1/2(ab+ba) - E1\dot E2, where a is the "first" or "coradius" coordinate, "b" is the "second" or "radius" coordinate, and E is the Euclidean part (x,y,z). Then the inversive coordinates of a sphere with center (x,y,z) and radius r will be I = (1/\hat{r},1/r,x/r,y/r,z/r) where \hat{r} = r/(|E|^2 -r^2). These coordinates satisfy Q(I,I) = -1. For this to make sense, of course r > 0, but we get planes by letting the radius of a tangent sphere to the plane go to infinity, and we get I = (2s, 0, x0, y0, z0) where (x0,y0,z0) is the unit normal to the plane and s is the perpendicular distance from the plane to the origin. Still Q(I,I) = -1.
|
The signature of Q will be (1,4), and in fact Q(I1,I2) = 1/2(ab+ba) - E1\dot E2, where a is the "first" or "coradius" coordinate, "b" is the "second" or "radius" coordinate, and E is the Euclidean part (x,y,z). Then the inversive coordinates of a sphere with center (x,y,z) and radius r will be I = (1/\hat{r},1/r,x/r,y/r,z/r) where \hat{r} = r/(|E|^2 -r^2). These coordinates satisfy Q(I,I) = -1. For this to make sense, of course r > 0, but we get planes by letting the radius of a tangent sphere to the plane go to infinity, and we get I = (2s, 0, x0, y0, z0) where (x0,y0,z0) is the unit normal to the plane and s is the perpendicular distance from the plane to the origin. Still Q(I,I) = -1.
|
||||||
Since r>0, we can't represent individual points this way. Instead we will use some coordinates J for which Q(J,J) = 0. In particular, if you take for the Euclidean point E = (u,v,w) the coordinates J = (`|E|`^2,1,u,v,w) then Q(J,J) = 0 and moreover it comes out that Q(I,J) = 0
|
Since r>0, we can't represent individual points this way. Instead we will use some coordinates J for which Q(J,J) = 0. In particular, if you take for the Euclidean point E = (u,v,w) the coordinates J = (`|E|`^2,1,u,v,w) then Q(J,J) = 0 and moreover it comes out that Q(I,J) = 0
|
||||||
whenever E lies on the sphere or plane described by some I with Q(I,I) = -1.
|
whenever E lies on the sphere or plane described by some I with Q(I,I) = -1.
|
||||||
The condition that two spheres I1 and I2 are tangent seems to be that Q(I1,I2) = 1. So given a fixed sphere, the condition that another sphere be tangent to it is linear in the coordinates of that other sphere.
|
The condition that two spheres I1 and I2 are tangent seems to be that Q(I1,I2) = 1. So given a fixed sphere, the condition that another sphere be tangent to it is linear in the coordinates of that other sphere.
|
||||||
This system does seem promising for encoding points, spheres, and planes, and doing basic computations with them. I guess I would just encode a circle as the intersection of the concentric sphere and the containing plane, and a line as either a pair of points or a pair of planes (modulo some equivalence relation, since I can't see any canonical choice of either two planes or two points). Or actually as described below, there is a more canonical choice.
|
This system does seem promising for encoding points, spheres, and planes, and doing basic computations with them. I guess I would just encode a circle as the intersection of the concentric sphere and the containing plane, and a line as either a pair of points or a pair of planes (modulo some equivalence relation, since I can't see any canonical choice of either two planes or two points). Or actually as described below, there is a more canonical choice.
|
||||||
|
|
@ -62,4 +62,4 @@ In the engine's coordinate conventions, a sphere with radius $r > 0$ centered on
|
||||||
$$I'_s = \left(\frac{P_x}{r}, \frac{P_y}{r}, \frac{P_z}{r}, \frac1{2r}, \frac{\|P\|^2 - r^2}{2r}\right),$$
|
$$I'_s = \left(\frac{P_x}{r}, \frac{P_y}{r}, \frac{P_z}{r}, \frac1{2r}, \frac{\|P\|^2 - r^2}{2r}\right),$$
|
||||||
which has the normalization $Q'(I'_s, I'_s) = 1$. The point $P$ is represented by the vector
|
which has the normalization $Q'(I'_s, I'_s) = 1$. The point $P$ is represented by the vector
|
||||||
$$I'_P = \left(P_x, P_y, P_z, \frac{1}{2}, \frac{\|P\|^2}{2}\right).$$
|
$$I'_P = \left(P_x, P_y, P_z, \frac{1}{2}, \frac{\|P\|^2}{2}\right).$$
|
||||||
In the `engine` module, these formulas are encoded in the `sphere` and `point` functions.
|
In the `engine` module, these formulas are encoded in the `sphere` and `point` functions.
|
||||||
|
|
@ -24,7 +24,7 @@ His final mathematical advice was reasonably encouraging, however:
|
||||||
"But still I would consider it all more or less doable. One should very precisely think about a doable scope.
|
"But still I would consider it all more or less doable. One should very precisely think about a doable scope.
|
||||||
I think three things are essential for the math no matter what you exactly plan.
|
I think three things are essential for the math no matter what you exactly plan.
|
||||||
|
|
||||||
1. Think projectively.
|
1. Think projectively,
|
||||||
Use Projective Geometry, Homogeneous Coordinates (or to a certain extent Quaternions, and Clifford Algebras, which are more or less an elegant way to merge Complex numbers with projective concepts.)
|
Use Projective Geometry, Homogeneous Coordinates (or to a certain extent Quaternions, and Clifford Algebras, which are more or less an elegant way to merge Complex numbers with projective concepts.)
|
||||||
2. Consider ambient complex spaces.
|
2. Consider ambient complex spaces.
|
||||||
The true nature of the objects can only be understood if embedded into a complex ambient space.
|
The true nature of the objects can only be understood if embedded into a complex ambient space.
|
||||||
|
|
@ -37,8 +37,10 @@ I think three things are essential for the math no matter what you exactly plan.
|
||||||
|
|
||||||
It would be nice to see how Jürgen handled some of these issues in a 2D system that he designed. Unfortunately, Cinderella was and remains closed-source; it was distributed for profit for some stretch of time. However, (a part of?) it was reimplemented in JavaScript as CindyJS, which is open source. I took a relatively quick look at that source code at one point, and these were my observations:
|
It would be nice to see how Jürgen handled some of these issues in a 2D system that he designed. Unfortunately, Cinderella was and remains closed-source; it was distributed for profit for some stretch of time. However, (a part of?) it was reimplemented in JavaScript as CindyJS, which is open source. I took a relatively quick look at that source code at one point, and these were my observations:
|
||||||
|
|
||||||
CindyJS uses very concrete basic objects: 2D points are represented via projective geometry as a list of three floating-point numbers, and everything is done numerically. There are no symbolic representations or exact algebraic numbers. (Not sure how a point on a circle or line is handled, that would take further investigation.)
|
CindyJS uses very concrete basic objects: 2D points are represented via projective geometry as a list of three floating-point numbers, and everything is done numerically. There are no symbolic representations or exact algebraic numbers. (Not sure how a point on a circle or line is handled, that would take further investigation.)
|
||||||
|
|
||||||
Lines are given by explicit coordinates as well (not sure of the internal details/exact coordinatization, or of how a "LineThrough" is represented).
|
Lines are given by explicit coordinates as well (not sure of the internal details/exact coordinatization, or of how a "LineThrough" is represented).
|
||||||
|
|
||||||
Was unclear to me how the complex parametrization for preserving continuity was handled in the code, even though Jürgen harps on complex ambient spaces; where are the complex numbers? Perhaps that part of Cinderella was never re-implemented?
|
Was unclear to me how the complex parametrization for preserving continuity was handled in the code, even though Jürgen harps on complex ambient spaces; where are the complex numbers? Perhaps that part of Cinderella was never re-implemented?
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,5 @@
|
||||||
<body><script type="module" src="dyna3.js"></script>
|
<body><script type="module" src="dyna3.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
# 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"
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
# 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