diff --git a/.forgejo/setup-trunk/action.yaml b/.forgejo/setup-trunk/action.yaml deleted file mode 100644 index 6007527..0000000 --- a/.forgejo/setup-trunk/action.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# set up the Trunk web build system -# -# https://trunkrs.dev -# -# the `curl` call is based on David Tolnay's `rust-toolchain` action -# -# https://github.com/dtolnay/rust-toolchain -# -runs: - using: "composite" - steps: - - run: rustup target add wasm32-unknown-unknown - - # install the Trunk binary to `ci-bin` within the workspace directory, which - # is determined by the `github.workspace` label and reflected in the - # `GITHUB_WORKSPACE` environment variable. then, make the `trunk` command - # available by placing the fully qualified path to `ci-bin` on the - # workflow's search path - - run: mkdir -p ci-bin - - run: curl --output - --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail 'https://github.com/trunk-rs/trunk/releases/download/v0.21.12/trunk-x86_64-unknown-linux-gnu.tar.gz' | tar --gunzip --extract --file - - working-directory: ci-bin - - run: echo "${{ github.workspace }}/ci-bin" >> $GITHUB_PATH diff --git a/.forgejo/workflows/continuous-integration.yaml b/.forgejo/workflows/continuous-integration.yaml deleted file mode 100644 index f3b0130..0000000 --- a/.forgejo/workflows/continuous-integration.yaml +++ /dev/null @@ -1,29 +0,0 @@ -on: - pull_request: - push: - branches: [main] -jobs: - # run the automated tests, reporting success if the tests pass and were built - # without warnings. the examples are run as tests, because we've configured - # each example target with `test = true` and `harness = false` in Cargo.toml. - # Trunk build failures caused by problems outside the Rust source code, like - # missing assets, should be caught by `trunk_build_test` - test: - runs-on: docker - container: - image: cimg/rust:1.86-node - defaults: - run: - # set the default working directory for each `run` step, relative to the - # workspace directory. this default only affects `run` steps (and if we - # tried to set the `working-directory` label for any other kind of step, - # it wouldn't be recognized anyway) - working-directory: app-proto - steps: - # Check out the repository so that its top-level directory is the - # workspace directory (action variable `github.workspace`, environment - # variable `$GITHUB_WORKSPACE`): - - uses: https://code.forgejo.org/actions/checkout@v4 - - - uses: ./.forgejo/setup-trunk - - run: RUSTFLAGS='-D warnings' cargo test \ No newline at end of file diff --git a/.gitignore b/.gitignore index ba2944f..3e95fba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,8 @@ -ci-bin +node_modules +site +docbuild +__tests__ +coverage +dyna3.zip +tmpproj *~ diff --git a/README.md b/README.md index cf3e589..9ea9cbf 100644 --- a/README.md +++ b/README.md @@ -17,71 +17,3 @@ Note that currently this is just the barest beginnings of the project, more of a * Able to run in browser (so implemented in WASM-compatible language) * Produce scalable graphics of 3D diagrams, and maybe STL files (or other fabricatable file format) as well. - -## Prototype - -The latest prototype is in the folder `app-proto`. It includes both a user interface and a numerical constraint-solving engine. - -### Install the prerequisites - -1. Install [`rustup`](https://rust-lang.github.io/rustup/): the officially recommended Rust toolchain manager - - It's available on Ubuntu as a [Snap](https://snapcraft.io/rustup) -2. Call `rustup default stable` to "download the latest stable release of Rust and set it as your default toolchain" - - If you forget, the `rustup` [help system](https://github.com/rust-lang/rustup/blob/d9b3601c3feb2e88cf3f8ca4f7ab4fdad71441fd/src/errors.rs#L109-L112) will remind you -3. Call `rustup target add wasm32-unknown-unknown` to add the [most generic 32-bit WebAssembly target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-unknown-unknown.html) -4. Call `cargo install wasm-pack` to install the [WebAssembly toolchain](https://rustwasm.github.io/docs/wasm-pack/) -5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool -6. Add the `.cargo/bin` folder in your home directory to your executable search path - - This lets you call Trunk, and other tools installed by Cargo, without specifying their paths - - On POSIX systems, the search path is stored in the `PATH` environment variable - -### Play with the prototype - -1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype - - The crates the prototype depends on will be downloaded and served automatically - - For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag - - If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]` from there instead. -3. In a web browser, visit one of the URLs listed under the message `INFO πŸ“‘ server listening at:` - - Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype -4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype - -### Run the engine on some example problems - -1. Use `sh` to run the script `tools/run-examples.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/run-examples.sh - ``` - - For each example problem, the engine will print the value of the loss function at each optimization step - - The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then - - ```julia - include("irisawa-hexlet.jl") - for (step, scaled_loss) in enumerate(history_alt.scaled_loss) - println(rpad(step-1, 4), " | ", scaled_loss) - end - ``` - - you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show - -### Run the automated tests - -1. Go into the `app-proto` folder -2. Call `cargo test` - -### 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 \ No newline at end of file diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock deleted file mode 100644 index 4f75c45..0000000 --- a/app-proto/Cargo.lock +++ /dev/null @@ -1,1325 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "allocator-api2" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -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]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - -[[package]] -name = "autocfg" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -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]] -name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - -[[package]] -name = "bytemuck" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "cc" -version = "1.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -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]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "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]] -name = "dyna3" -version = "0.1.0" -dependencies = [ - "charming", - "console_error_panic_hook", - "dyna3", - "itertools", - "js-sys", - "lazy_static", - "nalgebra", - "readonly", - "sycamore", - "wasm-bindgen-test", - "web-sys", -] - -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -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]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "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]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "html-escape" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" -dependencies = [ - "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]] -name = "indexmap" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" -dependencies = [ - "equivalent", - "hashbrown 0.14.5", - "serde", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "js-sys" -version = "0.3.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.158" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "matrixmultiply" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" -dependencies = [ - "autocfg", - "rawpointer", -] - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "minicov" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169" -dependencies = [ - "cc", - "walkdir", -] - -[[package]] -name = "nalgebra" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c4b5f057b303842cf3262c27e465f4c303572e7f6b0648f60e16248ac3397f4" -dependencies = [ - "approx", - "matrixmultiply", - "nalgebra-macros", - "num-complex", - "num-rational", - "num-traits", - "simba", - "typenum", -] - -[[package]] -name = "nalgebra-macros" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "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]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -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]] -name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rawpointer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" - -[[package]] -name = "readonly" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2a62d85ed81ca5305dc544bd42c8804c5060b78ffa5ad3c64b0fb6a8c13d062" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "safe_arch" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -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]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "simba" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa" -dependencies = [ - "approx", - "num-complex", - "num-traits", - "paste", - "wide", -] - -[[package]] -name = "slotmap" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "sycamore" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc" -dependencies = [ - "hashbrown 0.14.5", - "indexmap 2.5.0", - "paste", - "sycamore-core", - "sycamore-macro", - "sycamore-reactive", - "sycamore-web", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "sycamore-core" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187" -dependencies = [ - "hashbrown 0.14.5", - "paste", - "sycamore-reactive", -] - -[[package]] -name = "sycamore-macro" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c1d2eddc94db6d03e67eb832df5512b967e81053a573cd01bf3e1c3db00137" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "rand", - "sycamore-view-parser", - "syn", -] - -[[package]] -name = "sycamore-reactive" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2bacf810535efc2701187a716a5652197ad241d620d5b00fb12caa6dfa23add" -dependencies = [ - "paste", - "slotmap", - "smallvec", - "wasm-bindgen", -] - -[[package]] -name = "sycamore-view-parser" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c22875843db83cd4d49c0123a195e433bdc74e13ed0fff4ace0e77bb0a67033" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "sycamore-web" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b17aa5875f59f541cdf6fb58751ec702a6ed9801f30dd2b4d5f2279025b98bd" -dependencies = [ - "html-escape", - "js-sys", - "once_cell", - "paste", - "smallvec", - "sycamore-core", - "sycamore-macro", - "sycamore-reactive", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "syn" -version = "2.0.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" -dependencies = [ - "proc-macro2", - "quote", - "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]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "unicode-ident" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" - -[[package]] -name = "utf8-width" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" -dependencies = [ - "cfg-if", - "once_cell", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" - -[[package]] -name = "wasm-bindgen-test" -version = "0.3.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9" -dependencies = [ - "console_error_panic_hook", - "js-sys", - "minicov", - "scoped-tls", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test-macro", -] - -[[package]] -name = "wasm-bindgen-test-macro" -version = "0.3.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "web-sys" -version = "0.3.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wide" -version = "0.7.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b828f995bf1e9622031f8009f8481a85406ce1f4d4588ff746d872043e855690" -dependencies = [ - "bytemuck", - "safe_arch", -] - -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "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]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 1230b47..e623b26 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -3,22 +3,18 @@ name = "dyna3" version = "0.1.0" authors = ["Aaron Fenyes", "Glen Whitney"] edition = "2021" -rust-version = "1.86" [features] default = ["console_error_panic_hook"] -dev = [] [dependencies] itertools = "0.13.0" js-sys = "0.3.70" lazy_static = "1.5.0" nalgebra = "0.33.0" -readonly = "0.2.12" -sycamore = "0.9.1" - -# We use Charming to help display engine diagnostics -charming = { version = "0.5.1", features = ["wasm"] } +rustc-hash = "2.0.0" +slab = "0.4.9" +sycamore = "0.9.0-beta.3" # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires @@ -29,7 +25,6 @@ console_error_panic_hook = { version = "0.1.7", optional = true } [dependencies.web-sys] version = "0.3.69" features = [ - 'DomRect', 'HtmlCanvasElement', 'HtmlInputElement', 'Performance', @@ -41,41 +36,9 @@ features = [ 'WebGlVertexArrayObject' ] -# the self-dependency specifies features to use for tests and examples -# -# https://github.com/rust-lang/cargo/issues/2911#issuecomment-1483256987 -# [dev-dependencies] -dyna3 = { path = ".", default-features = false, features = ["dev"] } wasm-bindgen-test = "0.3.34" -# turn off spurious warnings about the custom config that Sycamore uses -# -# https://sycamore.dev/book/troubleshooting#unexpected-cfg-condition-name--sycamore-force-ssr -# -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ["cfg(sycamore_force_ssr)"] } - [profile.release] opt-level = "s" # optimize for small code size debug = true # include debug symbols - -[[example]] -name = "irisawa-hexlet" -test = true -harness = false - -[[example]] -name = "kaleidocycle" -test = true -harness = false - -[[example]] -name = "point-on-sphere" -test = true -harness = false - -[[example]] -name = "three-spheres" -test = true -harness = false diff --git a/app-proto/Trunk.toml b/app-proto/Trunk.toml deleted file mode 100644 index 017deba..0000000 --- a/app-proto/Trunk.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -public_url = "./" \ No newline at end of file diff --git a/app-proto/examples/common/print.rs b/app-proto/examples/common/print.rs deleted file mode 100644 index 2aa6a39..0000000 --- a/app-proto/examples/common/print.rs +++ /dev/null @@ -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) { - println!("\nCompleted Gram matrix:{}", (config.tr_mul(&*Q) * config).to_string().trim_end()); -} - -pub fn config(config: &DMatrix) { - 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); - } -} \ No newline at end of file diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs deleted file mode 100644 index 0d710ff..0000000 --- a/app-proto/examples/irisawa-hexlet.rs +++ /dev/null @@ -1,23 +0,0 @@ -#[path = "common/print.rs"] -mod print; - -use dyna3::engine::{ConfigNeighborhood, examples::realize_irisawa_hexlet}; - -fn main() { - const SCALED_TOL: f64 = 1.0e-12; - let realization = realize_irisawa_hexlet(SCALED_TOL); - print::title("Irisawa hexlet"); - print::realization_diagnostics(&realization); - if let Ok(ConfigNeighborhood { config, .. }) = realization.result { - // print the diameters of the chain spheres - println!("\nChain diameters:"); - println!(" {} sun (given)", 1.0 / config[(3, 3)]); - for k in 4..9 { - println!(" {} sun", 1.0 / config[(3, k)]); - } - - // print the completed Gram matrix - print::gram_matrix(&config); - } - print::loss_history(&realization.history); -} \ No newline at end of file diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs deleted file mode 100644 index ae4eb07..0000000 --- a/app-proto/examples/kaleidocycle.rs +++ /dev/null @@ -1,32 +0,0 @@ -#[path = "common/print.rs"] -mod print; - -use nalgebra::{DMatrix, DVector}; - -use dyna3::engine::{ConfigNeighborhood, examples::realize_kaleidocycle}; - -fn main() { - const SCALED_TOL: f64 = 1.0e-12; - let realization = realize_kaleidocycle(SCALED_TOL); - print::title("Kaleidocycle"); - print::realization_diagnostics(&realization); - if let Ok(ConfigNeighborhood { config, nbhd: tangent }) = realization.result { - // print the completed Gram matrix and the realized configuration - print::gram_matrix(&config); - print::config(&config); - - // find the kaleidocycle's twist motion by projecting onto the tangent - // space - const N_POINTS: usize = 12; - let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); - let down = -&up; - let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( - |n| [ - tangent.proj(&up.as_view(), n), - tangent.proj(&down.as_view(), n+1), - ] - ).sum(); - let normalization = 5.0 / twist_motion[(2, 0)]; - println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end()); - } -} \ No newline at end of file diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs deleted file mode 100644 index a73490e..0000000 --- a/app-proto/examples/point-on-sphere.rs +++ /dev/null @@ -1,33 +0,0 @@ -#[path = "common/print.rs"] -mod print; - -use dyna3::engine::{ - point, - realize_gram, - sphere, - ConfigNeighborhood, - ConstraintProblem, -}; - -fn main() { - let mut problem = ConstraintProblem::from_guess(&[ - point(0.0, 0.0, 2.0), - sphere(0.0, 0.0, 0.0, 1.0) - ]); - for j in 0..2 { - for k in j..2 { - problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); - } - } - problem.frozen.push(3, 0, problem.guess[(3, 0)]); - let realization = realize_gram( - &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 - ); - print::title("Point on a sphere"); - print::realization_diagnostics(&realization); - if let Ok(ConfigNeighborhood { config, .. }) = realization.result { - print::gram_matrix(&config); - print::config(&config); - } - print::loss_history(&realization.history); -} \ No newline at end of file diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs deleted file mode 100644 index 7901e31..0000000 --- a/app-proto/examples/three-spheres.rs +++ /dev/null @@ -1,34 +0,0 @@ -#[path = "common/print.rs"] -mod print; - -use dyna3::engine::{ - realize_gram, - sphere, - ConfigNeighborhood, - ConstraintProblem, -}; - -fn main() { - let mut problem = ConstraintProblem::from_guess({ - let a: f64 = 0.75_f64.sqrt(); - &[ - sphere(1.0, 0.0, 0.0, 1.0), - sphere(-0.5, a, 0.0, 1.0), - sphere(-0.5, -a, 0.0, 1.0), - ] - }); - for j in 0..3 { - for k in j..3 { - problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); - } - } - let realization = realize_gram( - &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 - ); - print::title("Three spheres"); - print::realization_diagnostics(&realization); - if let Ok(ConfigNeighborhood { config, .. }) = realization.result { - print::gram_matrix(&config); - } - print::loss_history(&realization.history); -} \ No newline at end of file diff --git a/app-proto/index.html b/app-proto/index.html index 4fbe52f..92238f4 100644 --- a/app-proto/index.html +++ b/app-proto/index.html @@ -6,12 +6,6 @@ - - - diff --git a/app-proto/main.css b/app-proto/main.css index 7981285..b9fc0a1 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -3,8 +3,7 @@ --text-bright: white; --text-invalid: #f58fc2; /* bright pink */ --border: #555; /* light gray */ - --border-focus-dark: #aaa; /* bright gray */ - --border-focus-light: white; + --border-focus: #aaa; /* bright gray */ --border-invalid: #70495c; /* dusky pink */ --selection-highlight: #444; /* medium gray */ --page-background: #222; /* dark gray */ @@ -18,24 +17,13 @@ body { font-family: 'Fira Sans', sans-serif; } -.invalid { - color: var(--text-invalid); -} - -.status { - width: 20px; - text-align: center; - font-family: 'Noto Emoji'; - font-style: normal; -} - /* sidebar */ #sidebar { display: flex; flex-direction: column; float: left; - width: 500px; + width: 450px; height: 100vh; margin: 0px; padding: 0px; @@ -53,7 +41,9 @@ body { } #add-remove > button { + width: 32px; height: 32px; + font-size: large; } /* KLUDGE */ @@ -62,9 +52,7 @@ body { buttons need to be displayed in an emoji font */ #add-remove > button.emoji { - width: 32px; font-family: 'Noto Emoji', sans-serif; - font-size: large; } /* outline */ @@ -89,22 +77,18 @@ summary.selected { background-color: var(--selection-highlight); } -summary > div, .regulator { +summary > div, .constraint { padding-top: 4px; padding-bottom: 4px; } -.element, .regulator { +.element, .constraint { display: flex; flex-grow: 1; padding-left: 8px; padding-right: 8px; } -.element > input { - margin-left: 8px; -} - .element-switch { width: 18px; padding-left: 2px; @@ -123,7 +107,7 @@ details[open]:has(li) .element-switch::after { flex-grow: 1; } -.regulator-label { +.constraint-label { flex-grow: 1; } @@ -139,88 +123,45 @@ details[open]:has(li) .element-switch::after { width: 56px; } -.regulator { +.constraint { font-style: italic; } -.regulator-type { - padding: 2px 8px 0px 8px; - font-size: 10pt; +.constraint.invalid { + color: var(--text-invalid); } -.regulator-input { - margin-right: 4px; +.constraint > input[type=checkbox] { + margin: 0px 8px 0px 0px; +} + +.constraint > input[type=text] { color: inherit; background-color: inherit; border: 1px solid var(--border); border-radius: 2px; } -.regulator-input::placeholder { - color: inherit; - opacity: 54%; - font-style: italic; -} - -.regulator-input.constraint { - background-color: var(--display-background); -} - -.regulator-input.invalid { - color: var(--text-invalid); +.constraint.invalid > input[type=text] { border-color: var(--border-invalid); } -.regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after { +.status { + width: 20px; + padding-left: 4px; + text-align: center; + font-family: 'Noto Emoji'; + font-style: normal; +} + +.invalid > .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); } -/* diagnostics */ - -#diagnostics { - margin: 10px; -} - -#diagnostics-bar { - display: flex; -} - -#realization-status { - display: flex; - flex-grow: 1; -} - -#realization-status .status { - margin-right: 4px; -} - -#realization-status :not(.status) { - flex-grow: 1; -} - -#realization-status .status::after { - content: 'βœ“'; -} - -#realization-status.invalid .status::after { - content: '⚠'; -} - -.diagnostics-panel { - margin-top: 10px; - min-height: 180px; -} - -.diagnostics-chart { - background-color: var(--display-background); - border: 1px solid var(--border); - border-radius: 8px; -} - /* display */ -#display { +canvas { float: left; margin-left: 20px; margin-top: 20px; @@ -229,12 +170,6 @@ details[open]:has(li) .element-switch::after { border-radius: 16px; } -#display:focus { - border-color: var(--border-focus-dark); - outline: none; -} - -input:focus { - border-color: var(--border-focus-light); - outline: none; +canvas:focus { + border-color: var(--border-focus); } \ No newline at end of file diff --git a/app-proto/run-examples b/app-proto/run-examples new file mode 100755 index 0000000..6a5e3ae --- /dev/null +++ b/app-proto/run-examples @@ -0,0 +1,8 @@ +# based on "Enabling print statements in Cargo tests", by Jon Almeida +# +# https://jonalmeida.com/posts/2015/01/23/print-cargo/ +# + +cargo test -- --nocapture engine::tests::irisawa_hexlet_test +cargo test -- --nocapture engine::tests::three_spheres_example +cargo test -- --nocapture engine::tests::point_on_sphere_example diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs new file mode 100644 index 0000000..f21c2ab --- /dev/null +++ b/app-proto/src/add_remove.rs @@ -0,0 +1,243 @@ +use sycamore::prelude::*; +use web_sys::{console, wasm_bindgen::JsValue}; + +use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}}; + +/* DEBUG */ +// load an example assembly for testing. this code will be removed once we've +// built a more formal test assembly system +fn load_gen_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Element::new( + String::from("gemini_a"), + String::from("Castor"), + [1.00_f32, 0.25_f32, 0.00_f32], + engine::sphere(0.5, 0.5, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Element::new( + String::from("gemini_b"), + String::from("Pollux"), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere(-0.5, -0.5, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Element::new( + String::from("ursa_major"), + String::from("Ursa major"), + [0.25_f32, 0.00_f32, 1.00_f32], + engine::sphere(-0.5, 0.5, 0.0, 0.75) + ) + ); + let _ = assembly.try_insert_element( + Element::new( + String::from("ursa_minor"), + String::from("Ursa minor"), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere(0.5, -0.5, 0.0, 0.5) + ) + ); + let _ = assembly.try_insert_element( + Element::new( + String::from("moon_deimos"), + String::from("Deimos"), + [0.75_f32, 0.75_f32, 0.00_f32], + engine::sphere(0.0, 0.15, 1.0, 0.25) + ) + ); + let _ = assembly.try_insert_element( + Element::new( + String::from("moon_phobos"), + String::from("Phobos"), + [0.00_f32, 0.75_f32, 0.50_f32], + engine::sphere(0.0, -0.15, -1.0, 0.25) + ) + ); +} + +/* DEBUG */ +// load an example assembly for testing. this code will be removed once we've +// built a more formal test assembly system +fn load_low_curv_assemb(assembly: &Assembly) { + let a = 0.75_f64.sqrt(); + let _ = assembly.try_insert_element( + Element::new( + "central".to_string(), + "Central".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Element::new( + "assemb_plane".to_string(), + "Assembly plane".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Element::new( + "side1".to_string(), + "Side 1".to_string(), + [1.00_f32, 0.00_f32, 0.25_f32], + engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Element::new( + "side2".to_string(), + "Side 2".to_string(), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Element::new( + "side3".to_string(), + "Side 3".to_string(), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Element::new( + "corner1".to_string(), + "Corner 1".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) + ) + ); + let _ = assembly.try_insert_element( + Element::new( + "corner2".to_string(), + "Corner 2".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) + ) + ); + let _ = assembly.try_insert_element( + Element::new( + String::from("corner3"), + String::from("Corner 3"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0) + ) + ); +} + +#[component] +pub fn AddRemove() -> View { + /* DEBUG */ + let assembly_name = create_signal("general".to_string()); + create_effect(move || { + // get name of chosen assembly + let name = assembly_name.get_clone(); + console::log_1( + &JsValue::from(format!("Showing assembly \"{}\"", name.clone())) + ); + + batch(|| { + let state = use_context::(); + let assembly = &state.assembly; + + // clear state + assembly.elements.update(|elts| elts.clear()); + assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); + state.selection.update(|sel| sel.clear()); + + // increment assembly serial number + state.assembly_serial.set_fn_silent(|serial| serial.wrapping_add(1)); + + // 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::(); + state.assembly.insert_new_element(); + + /* DEBUG */ + // print updated list of elements by identifier + console::log_1(&JsValue::from("elements by identifier:")); + for (id, key) in state.assembly.elements_by_id.get_clone().iter() { + console::log_3( + &JsValue::from(" "), + &JsValue::from(id), + &JsValue::from(*key) + ); + } + } + ) { "+" } + button( + class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button + disabled={ + let state = use_context::(); + state.selection.with(|sel| sel.len() != 2) + }, + on:click=|_| { + let state = use_context::(); + let subjects = state.selection.with( + |sel| { + let subject_vec: Vec<_> = sel.into_iter().collect(); + (subject_vec[0].clone(), subject_vec[1].clone()) + } + ); + let lorentz_prod = create_signal(0.0); + let lorentz_prod_valid = create_signal(false); + let active = create_signal(true); + state.assembly.insert_constraint(Constraint { + subjects: subjects, + lorentz_prod: lorentz_prod, + lorentz_prod_text: create_signal(String::new()), + lorentz_prod_valid: lorentz_prod_valid, + active: active, + }); + state.selection.update(|sel| sel.clear()); + + /* DEBUG */ + // print updated constraint list + console::log_1(&JsValue::from("Constraints:")); + state.assembly.constraints.with(|csts| { + for (_, cst) in csts.into_iter() { + console::log_5( + &JsValue::from(" "), + &JsValue::from(cst.subjects.0), + &JsValue::from(cst.subjects.1), + &JsValue::from(":"), + &JsValue::from(cst.lorentz_prod.get_untracked()) + ); + } + }); + + // update the realization when the constraint becomes active + // and valid, or is edited while active and valid + create_effect(move || { + console::log_1(&JsValue::from( + format!("Constraint ({}, {}) updated", subjects.0, subjects.1) + )); + lorentz_prod.track(); + if active.get() && lorentz_prod_valid.get() { + state.assembly.realize(); + } + }); + } + ) { "πŸ”—" } + select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser + option(value="general") { "General" } + option(value="low-curv") { "Low-curvature" } + option(value="empty") { "Empty" } + } + } + } +} \ No newline at end of file diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 94e7b3c..35b4417 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,592 +1,93 @@ -use nalgebra::{DMatrix, DVector, DVectorView}; -use std::{ - cell::Cell, - cmp::Ordering, - collections::{BTreeMap, BTreeSet}, - fmt, - fmt::{Debug, Formatter}, - hash::{Hash, Hasher}, - rc::Rc, - sync::{atomic, atomic::AtomicU64}, -}; +use nalgebra::{DMatrix, DVector}; +use rustc_hash::FxHashMap; +use slab::Slab; +use std::collections::BTreeSet; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ -use crate::{ - components::{display::DisplayItem, outline::OutlineItem}, - engine::{ - Q, - local_unif_to_std, - point, - project_point_to_normalized, - project_sphere_to_normalized, - realize_gram, - sphere, - ConfigNeighborhood, - ConfigSubspace, - ConstraintProblem, - DescentHistory, - Realization, - }, - specified::SpecifiedValue, -}; +use crate::engine::{realize_gram, PartialMatrix}; + +// the types of the keys we use to access an assembly's elements and constraints +pub type ElementKey = usize; +pub type ConstraintKey = usize; pub type ElementColor = [f32; 3]; -/* KLUDGE */ -// we should reconsider this design when we build a system for switching between -// assemblies. at that point, we might want to switch to hierarchical keys, -// where each each item has a key that identifies it within its assembly and -// each assembly has a key that identifies it within the sesssion -static NEXT_SERIAL: AtomicU64 = AtomicU64::new(0); - -pub trait Serial { - // a serial number that uniquely identifies this element - fn serial(&self) -> u64; - - // take the next serial number, panicking if that was the last one left - fn next_serial() -> u64 where Self: Sized { - // the technique we use to panic on overflow is taken from _Rust Atomics - // and Locks_, by Mara Bos - // - // https://marabos.nl/atomics/atomics.html#example-handle-overflow - // - NEXT_SERIAL.fetch_update( - atomic::Ordering::SeqCst, atomic::Ordering::SeqCst, - |serial| serial.checked_add(1) - ).expect("Out of serial numbers for elements") - } -} - -impl Hash for dyn Serial { - fn hash(&self, state: &mut H) { - self.serial().hash(state) - } -} - -impl PartialEq for dyn Serial { - fn eq(&self, other: &Self) -> bool { - self.serial() == other.serial() - } -} - -impl Eq for dyn Serial {} - -impl PartialOrd for dyn Serial { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for dyn Serial { - fn cmp(&self, other: &Self) -> Ordering { - self.serial().cmp(&other.serial()) - } -} - -pub trait ProblemPoser { - fn pose(&self, problem: &mut ConstraintProblem); -} - -pub trait Element: Serial + ProblemPoser + DisplayItem { - // the default identifier for an element of this type - fn default_id() -> String where Self: Sized; - - // the default example of an element of this type - fn default(id: String, id_num: u64) -> Self where Self: Sized; - - // the default regulators that come with this element - fn default_regulators(self: Rc) -> Vec> { - Vec::new() - } - - fn id(&self) -> &String; - fn label(&self) -> &String; - fn representation(&self) -> Signal>; - fn ghost(&self) -> Signal; - - // the regulators the element is subject to. the assembly that owns the - // element is responsible for keeping this set up to date - fn regulators(&self) -> Signal>>; - - // project a representation vector for this kind of element onto its - // normalization variety - fn project_to_normalized(&self, rep: &mut DVector); - - // the configuration matrix column index that was assigned to the element - // last time the assembly was realized, or `None` if the element has never - // been through a realization - fn column_index(&self) -> Option; - - // assign the element a configuration matrix column index. this method must - // be used carefully to preserve invariant (1), described in the comment on - // the `tangent` field of the `Assembly` structure - fn set_column_index(&self, index: usize); -} - -impl Debug for dyn Element { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { - self.id().fmt(f) - } -} - -impl Hash for dyn Element { - fn hash(&self, state: &mut H) { - ::hash(self, state) - } -} - -impl PartialEq for dyn Element { - fn eq(&self, other: &Self) -> bool { - ::eq(self, other) - } -} - -impl Eq for dyn Element {} - -impl PartialOrd for dyn Element { - fn partial_cmp(&self, other: &Self) -> Option { - ::partial_cmp(self, other) - } -} - -impl Ord for dyn Element { - fn cmp(&self, other: &Self) -> Ordering { - ::cmp(self, other) - } -} - -pub struct Sphere { +#[derive(Clone, PartialEq)] +pub struct Element { pub id: String, pub label: String, pub color: ElementColor, pub representation: Signal>, - pub ghost: Signal, - pub regulators: Signal>>, - serial: u64, - column_index: Cell>, + pub constraints: Signal>, + + // the configuration matrix column index that was assigned to this element + // last time the assembly was realized + column_index: usize } -impl Sphere { - const CURVATURE_COMPONENT: usize = 3; - +impl Element { pub fn new( id: String, label: String, color: ElementColor, - representation: DVector, - ) -> Self { - Self { - id, - label, - color, + representation: DVector + ) -> Element { + Element { + id: id, + label: label, + color: color, representation: create_signal(representation), - ghost: create_signal(false), - regulators: create_signal(BTreeSet::new()), - serial: Self::next_serial(), - column_index: None.into(), + constraints: create_signal(BTreeSet::default()), + column_index: 0 } } } + -impl Element for Sphere { - fn default_id() -> String { - "sphere".to_string() - } - - fn default(id: String, id_num: u64) -> Self { - Self::new( - id, - format!("Sphere {id_num}"), - [0.75_f32, 0.75_f32, 0.75_f32], - sphere(0.0, 0.0, 0.0, 1.0), - ) - } - - fn default_regulators(self: Rc) -> Vec> { - vec![Rc::new(HalfCurvatureRegulator::new(self))] - } - - fn id(&self) -> &String { - &self.id - } - - fn label(&self) -> &String { - &self.label - } - - fn representation(&self) -> Signal> { - self.representation - } - - fn ghost(&self) -> Signal { - self.ghost - } - - fn regulators(&self) -> Signal>> { - self.regulators - } - - fn project_to_normalized(&self, rep: &mut DVector) { - project_sphere_to_normalized(rep); - } - - fn column_index(&self) -> Option { - self.column_index.get() - } - - fn set_column_index(&self, index: usize) { - self.column_index.set(Some(index)); - } +#[derive(Clone)] +pub struct Constraint { + pub subjects: (ElementKey, ElementKey), + pub lorentz_prod: Signal, + pub lorentz_prod_text: Signal, + pub lorentz_prod_valid: Signal, + pub active: Signal } -impl Serial for Sphere { - fn serial(&self) -> u64 { - self.serial - } -} - -impl ProblemPoser for Sphere { - fn pose(&self, problem: &mut ConstraintProblem) { - let index = self.column_index().expect( - format!("Sphere \"{}\" should be indexed before writing problem data", self.id).as_str() - ); - problem.gram.push_sym(index, index, 1.0); - problem.guess.set_column(index, &self.representation.get_clone_untracked()); - } -} - -pub struct Point { - pub id: String, - pub label: String, - pub color: ElementColor, - pub representation: Signal>, - pub ghost: Signal, - pub regulators: Signal>>, - serial: u64, - column_index: Cell>, -} - -impl Point { - const WEIGHT_COMPONENT: usize = 3; - - pub fn new( - id: String, - label: String, - color: ElementColor, - representation: DVector, - ) -> Self { - Self { - id, - label, - color, - representation: create_signal(representation), - ghost: create_signal(false), - regulators: create_signal(BTreeSet::new()), - serial: Self::next_serial(), - column_index: None.into(), - } - } -} - -impl Element for Point { - fn default_id() -> String { - "point".to_string() - } - - fn default(id: String, id_num: u64) -> Self { - Self::new( - id, - format!("Point {id_num}"), - [0.75_f32, 0.75_f32, 0.75_f32], - point(0.0, 0.0, 0.0), - ) - } - - fn id(&self) -> &String { - &self.id - } - - fn label(&self) -> &String { - &self.label - } - - fn representation(&self) -> Signal> { - self.representation - } - - fn ghost(&self) -> Signal { - self.ghost - } - - fn regulators(&self) -> Signal>> { - self.regulators - } - - fn project_to_normalized(&self, rep: &mut DVector) { - project_point_to_normalized(rep); - } - - fn column_index(&self) -> Option { - self.column_index.get() - } - - fn set_column_index(&self, index: usize) { - self.column_index.set(Some(index)); - } -} - -impl Serial for Point { - fn serial(&self) -> u64 { - self.serial - } -} - -impl ProblemPoser for Point { - fn pose(&self, problem: &mut ConstraintProblem) { - let index = self.column_index().expect( - format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str() - ); - problem.gram.push_sym(index, index, 0.0); - problem.frozen.push(Self::WEIGHT_COMPONENT, index, 0.5); - problem.guess.set_column(index, &self.representation.get_clone_untracked()); - } -} - -pub trait Regulator: Serial + ProblemPoser + OutlineItem { - fn subjects(&self) -> Vec>; - fn measurement(&self) -> ReadSignal; - fn set_point(&self) -> Signal; -} - -impl Hash for dyn Regulator { - fn hash(&self, state: &mut H) { - ::hash(self, state) - } -} - -impl PartialEq for dyn Regulator { - fn eq(&self, other: &Self) -> bool { - ::eq(self, other) - } -} - -impl Eq for dyn Regulator {} - -impl PartialOrd for dyn Regulator { - fn partial_cmp(&self, other: &Self) -> Option { - ::partial_cmp(self, other) - } -} - -impl Ord for dyn Regulator { - fn cmp(&self, other: &Self) -> Ordering { - ::cmp(self, other) - } -} - -pub struct InversiveDistanceRegulator { - pub subjects: [Rc; 2], - pub measurement: ReadSignal, - pub set_point: Signal, - serial: u64, -} - -impl InversiveDistanceRegulator { - pub fn new(subjects: [Rc; 2]) -> Self { - let representations = subjects.each_ref().map(|subj| subj.representation()); - let measurement = create_memo(move || { - representations[0].with(|rep_0| - representations[1].with(|rep_1| - rep_0.dot(&(&*Q * rep_1)) - ) - ) - }); - - let set_point = create_signal(SpecifiedValue::from_empty_spec()); - let serial = Self::next_serial(); - - Self { subjects, measurement, set_point, serial } - } -} - -impl Regulator for InversiveDistanceRegulator { - fn subjects(&self) -> Vec> { - self.subjects.clone().into() - } - - fn measurement(&self) -> ReadSignal { - self.measurement - } - - fn set_point(&self) -> Signal { - self.set_point - } -} - -impl Serial for InversiveDistanceRegulator { - fn serial(&self) -> u64 { - self.serial - } -} - -impl ProblemPoser for InversiveDistanceRegulator { - fn pose(&self, problem: &mut ConstraintProblem) { - self.set_point.with_untracked(|set_pt| { - if let Some(val) = set_pt.value { - let [row, col] = self.subjects.each_ref().map( - |subj| subj.column_index().expect( - "Subjects should be indexed before inversive distance regulator writes problem data" - ) - ); - problem.gram.push_sym(row, col, val); - } - }); - } -} - -pub struct HalfCurvatureRegulator { - pub subject: Rc, - pub measurement: ReadSignal, - pub set_point: Signal, - serial: u64, -} - -impl HalfCurvatureRegulator { - pub fn new(subject: Rc) -> Self { - let measurement = subject.representation().map( - |rep| rep[Sphere::CURVATURE_COMPONENT] - ); - - let set_point = create_signal(SpecifiedValue::from_empty_spec()); - let serial = Self::next_serial(); - - Self { subject, measurement, set_point, serial } - } -} - -impl Regulator for HalfCurvatureRegulator { - fn subjects(&self) -> Vec> { - vec![self.subject.clone()] - } - - fn measurement(&self) -> ReadSignal { - self.measurement - } - - fn set_point(&self) -> Signal { - self.set_point - } -} - -impl Serial for HalfCurvatureRegulator { - fn serial(&self) -> u64 { - self.serial - } -} - -impl ProblemPoser for HalfCurvatureRegulator { - fn pose(&self, problem: &mut ConstraintProblem) { - self.set_point.with_untracked(|set_pt| { - if let Some(val) = set_pt.value { - let col = self.subject.column_index().expect( - "Subject should be indexed before half-curvature regulator writes problem data" - ); - problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val); - } - }); - } -} - -// the velocity is expressed in uniform coordinates -pub struct ElementMotion<'a> { - pub element: Rc, - pub velocity: DVectorView<'a, f64>, -} - -type AssemblyMotion<'a> = Vec>; - // a complete, view-independent description of an assembly #[derive(Clone)] pub struct Assembly { - // elements and regulators - pub elements: Signal>>, - pub regulators: Signal>>, - - // solution variety tangent space. the basis vectors are stored in - // configuration matrix format, ordered according to the elements' column - // indices. when you realize the assembly, every element that's present - // during realization gets a column index and is reflected in the tangent - // space. since the methods in this module never assign column indices - // without later realizing the assembly, we get the following invariant: - // - // (1) if an element has a column index, its tangent motions can be found - // in that column of the tangent space basis matrices - // - pub tangent: Signal, + // elements and constraints + pub elements: Signal>, + pub constraints: Signal>, // indexing - pub elements_by_id: Signal>>, - - // realization control - pub realization_trigger: Signal<()>, - - // realization diagnostics - pub realization_status: Signal>, - pub descent_history: Signal, + pub elements_by_id: Signal> } impl Assembly { pub fn new() -> Assembly { - // create an assembly - let assembly = Assembly { - elements: create_signal(BTreeSet::new()), - regulators: create_signal(BTreeSet::new()), - tangent: create_signal(ConfigSubspace::zero(0)), - elements_by_id: create_signal(BTreeMap::default()), - realization_trigger: create_signal(()), - realization_status: create_signal(Ok(())), - descent_history: create_signal(DescentHistory::new()), - }; - - // realize the assembly whenever the element list, the regulator list, - // a regulator's set point, or the realization trigger is updated - let assembly_for_effect = assembly.clone(); - create_effect(move || { - assembly_for_effect.elements.track(); - assembly_for_effect.regulators.with( - |regs| for reg in regs { - reg.set_point().track(); - } - ); - assembly_for_effect.realization_trigger.track(); - assembly_for_effect.realize(); - }); - - assembly + Assembly { + elements: create_signal(Slab::new()), + constraints: create_signal(Slab::new()), + elements_by_id: create_signal(FxHashMap::default()) + } } - // --- inserting elements and regulators --- + // --- inserting elements and constraints --- // insert an element into the assembly without checking whether we already // have an element with the same identifier. any element that does have the // same identifier will get kicked out of the `elements_by_id` index - fn insert_element_unchecked(&self, elt: impl Element + 'static) { - // insert the element - let id = elt.id().clone(); - let elt_rc = Rc::new(elt); - self.elements.update(|elts| elts.insert(elt_rc.clone())); - self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, elt_rc.clone())); - - // create and insert the element's default regulators - for reg in elt_rc.default_regulators() { - self.insert_regulator(reg); - } + fn insert_element_unchecked(&self, elt: Element) { + let id = elt.id.clone(); + let key = self.elements.update(|elts| elts.insert(elt)); + self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); } - pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool { + pub fn try_insert_element(&self, elt: Element) -> bool { let can_insert = self.elements_by_id.with_untracked( - |elts_by_id| !elts_by_id.contains_key(elt.id()) + |elts_by_id| !elts_by_id.contains_key(&elt.id) ); if can_insert { self.insert_element_unchecked(elt); @@ -594,57 +95,36 @@ impl Assembly { can_insert } - pub fn insert_element_default(&self) { + pub fn insert_new_element(&self) { // find the next unused identifier in the default sequence - let default_id = T::default_id(); let mut id_num = 1; - let mut id = format!("{default_id}{id_num}"); + let mut id = format!("sphere{}", id_num); while self.elements_by_id.with_untracked( |elts_by_id| elts_by_id.contains_key(&id) ) { id_num += 1; - id = format!("{default_id}{id_num}"); + id = format!("sphere{}", id_num); } - // create and insert the default example of `T` - let _ = self.insert_element_unchecked(T::default(id, id_num)); + // create and insert a new element + self.insert_element_unchecked( + Element::new( + id, + format!("Sphere {}", id_num), + [0.75_f32, 0.75_f32, 0.75_f32], + DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]) + ) + ); } - pub fn insert_regulator(&self, regulator: Rc) { - // add the regulator to the assembly's regulator list - self.regulators.update( - |regs| regs.insert(regulator.clone()) + pub fn insert_constraint(&self, constraint: Constraint) { + let subjects = constraint.subjects; + let key = self.constraints.update(|csts| csts.insert(constraint)); + let subject_constraints = self.elements.with( + |elts| (elts[subjects.0].constraints, elts[subjects.1].constraints) ); - - // add the regulator to each subject's regulator list - let subject_regulators: Vec<_> = regulator.subjects().into_iter().map( - |subj| subj.regulators() - ).collect(); - for regulators in subject_regulators { - regulators.update(|regs| regs.insert(regulator.clone())); - } - - /* DEBUG */ - // print an updated list of regulators - console_log!("Regulators:"); - self.regulators.with_untracked(|regs| { - for reg in regs.into_iter() { - console_log!( - " {:?}: {}", - reg.subjects(), - reg.set_point().with_untracked( - |set_pt| { - let spec = &set_pt.spec; - if spec.is_empty() { - "__".to_string() - } else { - spec.clone() - } - } - ) - ); - } - }); + subject_constraints.0.update(|csts| csts.insert(key)); + subject_constraints.1.update(|csts| csts.insert(key)); } // --- realization --- @@ -652,248 +132,79 @@ impl Assembly { pub fn realize(&self) { // index the elements self.elements.update_silent(|elts| { - for (index, elt) in elts.iter().enumerate() { - elt.set_column_index(index); + for (index, (_, elt)) in elts.into_iter().enumerate() { + elt.column_index = index; } }); - // set up the constraint problem - let problem = self.elements.with_untracked(|elts| { - let mut problem = ConstraintProblem::new(elts.len()); - for elt in elts { - elt.pose(&mut problem); - } - self.regulators.with_untracked(|regs| { - for reg in regs { - reg.pose(&mut problem); + // set up the Gram matrix and the initial configuration matrix + let (gram, guess) = self.elements.with_untracked(|elts| { + // set up the off-diagonal part of the Gram matrix + let mut gram_to_be = PartialMatrix::new(); + self.constraints.with_untracked(|csts| { + for (_, cst) in csts { + if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() { + let subjects = cst.subjects; + let row = elts[subjects.0].column_index; + let col = elts[subjects.1].column_index; + gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked()); + } } }); - problem + + // set up the initial configuration matrix and the diagonal of the + // Gram matrix + let mut guess_to_be = DMatrix::::zeros(5, elts.len()); + for (_, elt) in elts { + let index = elt.column_index; + gram_to_be.push_sym(index, index, 1.0); + guess_to_be.set_column(index, &elt.representation.get_clone_untracked()); + } + + (gram_to_be, guess_to_be) }); /* DEBUG */ // log the Gram matrix - console_log!("Gram matrix:\n{}", problem.gram); + console::log_1(&JsValue::from("Gram matrix:")); + gram.log_to_console(); /* DEBUG */ // log the initial configuration matrix - console_log!("Old configuration:{:>8.3}", problem.guess); + console::log_1(&JsValue::from("Old configuration:")); + for j in 0..guess.nrows() { + let mut row_str = String::new(); + for k in 0..guess.ncols() { + row_str.push_str(format!(" {:>8.3}", guess[(j, k)]).as_str()); + } + console::log_1(&JsValue::from(row_str)); + } // look for a configuration with the given Gram matrix - let Realization { result, history } = realize_gram( - &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + let (config, success, history) = realize_gram( + &gram, guess, &[], + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); /* DEBUG */ - // report the outcome of the search in the browser console - if let Err(ref message) = result { - console_log!("❌️ {message}"); - } else { - console_log!("βœ…οΈ Target accuracy achieved!"); - } - if history.scaled_loss.len() > 0 { - console_log!("Steps: {}", history.scaled_loss.len() - 1); - console_log!("Loss: {}", history.scaled_loss.last().unwrap()); - } - - // report the loss history - self.descent_history.set(history); - - match result { - Ok(ConfigNeighborhood { config, nbhd: tangent }) => { - /* DEBUG */ - // report the tangent dimension - console_log!("Tangent dimension: {}", tangent.dim()); - - // report the realization status - self.realization_status.set(Ok(())); - - // read out the solution - for elt in self.elements.get_clone_untracked() { - elt.representation().update( - |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) - ); - } - - // save the tangent space - self.tangent.set_silent(tangent); - }, - Err(message) => { - // report the realization status. the `Err(message)` we're - // setting the status to has a different type than the - // `Err(message)` we received from the match: we're changing the - // `Ok` type from `Realization` to `()` - self.realization_status.set(Err(message)) - }, - } - } - - // --- deformation --- - - // project the given motion to the tangent space of the solution variety and - // move the assembly along it. the implementation is based on invariant (1) - // from above and the following additional invariant: - // - // (2) if an element is affected by a constraint, it has a column index - // - // we have this invariant because the assembly gets realized each time you - // add a constraint - pub fn deform(&self, motion: AssemblyMotion) { - /* KLUDGE */ - // when the tangent space is zero, deformation won't do anything, but - // the attempt to deform should be registered in the UI. this console - // message will do for now - if self.tangent.with(|tan| tan.dim() <= 0 && tan.assembly_dim() > 0) { - console::log_1(&JsValue::from("The assembly is rigid")); - } - - // give a column index to each moving element that doesn't have one yet. - // this temporarily breaks invariant (1), but the invariant will be - // restored when we realize the assembly at the end of the deformation. - // in the process, we find out how many matrix columns we'll need to - // hold the deformation - let realized_dim = self.tangent.with(|tan| tan.assembly_dim()); - let motion_dim = { - let mut next_column_index = realized_dim; - for elt_motion in motion.iter() { - let moving_elt = &elt_motion.element; - if moving_elt.column_index().is_none() { - moving_elt.set_column_index(next_column_index); - next_column_index += 1; - } - } - next_column_index - }; - - // project the element motions onto the tangent space of the solution - // variety and sum them to get a deformation of the whole assembly. the - // matrix `motion_proj` that holds the deformation has extra columns for - // any moving elements that aren't reflected in the saved tangent space - const ELEMENT_DIM: usize = 5; - let mut motion_proj = DMatrix::zeros(ELEMENT_DIM, motion_dim); - for elt_motion in motion { - // we can unwrap the column index because we know that every moving - // element has one at this point - let column_index = elt_motion.element.column_index().unwrap(); - - if column_index < realized_dim { - // this element had a column index when we started, so by - // invariant (1), it's reflected in the tangent space - let mut target_columns = motion_proj.columns_mut(0, realized_dim); - target_columns += self.tangent.with( - |tan| tan.proj(&elt_motion.velocity, column_index) - ); + // report the outcome of the search + console::log_1(&JsValue::from( + if success { + "Target accuracy achieved!" } else { - // this element didn't have a column index when we started, so - // by invariant (2), it's unconstrained - let mut target_column = motion_proj.column_mut(column_index); - let unif_to_std = elt_motion.element.representation().with_untracked( - |rep| local_unif_to_std(rep.as_view()) - ); - target_column += unif_to_std * elt_motion.velocity; + "Failed to reach target accuracy" } - } + )); + console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1)); + console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap())); - // step the assembly along the deformation. this changes the elements' - // normalizations, so we restore those afterward - for elt in self.elements.get_clone_untracked() { - elt.representation().update_silent(|rep| { - match elt.column_index() { - Some(column_index) => { - // step the element along the deformation and then - // restore its normalization - *rep += motion_proj.column(column_index); - elt.project_to_normalized(rep); - }, - None => { - console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) - }, - }; - }); - } - - // trigger a realization to bring the configuration back onto the - // solution variety. this also gets the elements' column indices and the - // saved tangent space back in sync - self.realization_trigger.set(()); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::engine; - - #[test] - #[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")] - fn unindexed_element_test() { - let _ = create_root(|| { - let elt = Sphere::default("sphere".to_string(), 0); - elt.pose(&mut ConstraintProblem::new(1)); - }); - } - - #[test] - #[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")] - fn unindexed_subject_test_inversive_distance() { - let _ = create_root(|| { - let subjects = [0, 1].map( - |k| Rc::new(Sphere::default(format!("sphere{k}"), k)) as Rc - ); - subjects[0].set_column_index(0); - InversiveDistanceRegulator { - subjects: subjects, - measurement: create_memo(|| 0.0), - set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()), - serial: InversiveDistanceRegulator::next_serial() - }.pose(&mut ConstraintProblem::new(2)); - }); - } - - #[test] - fn curvature_drift_test() { - const INITIAL_RADIUS: f64 = 0.25; - let _ = create_root(|| { - // set up an assembly containing a single sphere centered at the - // origin - let assembly = Assembly::new(); - let sphere_id = "sphere0"; - let _ = assembly.try_insert_element( - // we create the sphere by hand for two reasons: to choose the - // curvature (which can affect drift rate) and to make the test - // independent of `Sphere::default` - Sphere::new( - String::from(sphere_id), - String::from("Sphere 0"), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS), - ) - ); - - // nudge the sphere repeatedly along the `z` axis - const STEP_SIZE: f64 = 0.0025; - const STEP_CNT: usize = 400; - let sphere = assembly.elements_by_id.with(|elts_by_id| elts_by_id[sphere_id].clone()); - let velocity = DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]); - for _ in 0..STEP_CNT { - assembly.deform( - vec![ - ElementMotion { - element: sphere.clone(), - velocity: velocity.as_view(), - } - ] + if success { + // read out the solution + for (_, elt) in self.elements.get_clone_untracked() { + elt.representation.update( + |rep| rep.set_column(0, &config.column(elt.column_index)) ); } - - // check how much the sphere's curvature has drifted - const INITIAL_HALF_CURV: f64 = 0.5 / INITIAL_RADIUS; - const DRIFT_TOL: f64 = 0.015; - let final_half_curv = sphere.representation().with_untracked( - |rep| rep[Sphere::CURVATURE_COMPONENT] - ); - assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL); - }); + } } } \ No newline at end of file diff --git a/app-proto/src/components.rs b/app-proto/src/components.rs deleted file mode 100644 index 7387d58..0000000 --- a/app-proto/src/components.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod add_remove; -pub mod diagnostics; -pub mod display; -pub mod outline; -pub mod test_assembly_chooser; \ No newline at end of file diff --git a/app-proto/src/components/add_remove.rs b/app-proto/src/components/add_remove.rs deleted file mode 100644 index 4196640..0000000 --- a/app-proto/src/components/add_remove.rs +++ /dev/null @@ -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::(); - 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::(); - }); - } - ) { "Add sphere" } - button( - on:click = |_| { - let state = use_context::(); - state.assembly.insert_element_default::(); - } - ) { "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::(); - state.selection.with(|sel| sel.len() != 2) - }, - on:click = |_| { - let state = use_context::(); - 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::>() - .try_into() - .unwrap() - ); - state.assembly.insert_regulator( - Rc::new(InversiveDistanceRegulator::new(subjects)) - ); - state.selection.update(|sel| sel.clear()); - } - ) { "πŸ”—" } - TestAssemblyChooser {} - } - } -} \ No newline at end of file diff --git a/app-proto/src/components/diagnostics.rs b/app-proto/src/components/diagnostics.rs deleted file mode 100644 index e265982..0000000 --- a/app-proto/src/components/diagnostics.rs +++ /dev/null @@ -1,256 +0,0 @@ -use charming::{ - Chart, - WasmRenderer, - component::{Axis, DataZoom, Grid}, - element::{AxisType, Symbol}, - series::{Line, Scatter}, -}; -use sycamore::prelude::*; - -use crate::AppState; - -#[derive(Clone)] -struct DiagnosticsState { - active_tab: Signal, -} - -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::(); - let realization_status = state.assembly.realization_status; - view! { - div( - id = "realization-status", - class = realization_status.with( - |status| match status { - Ok(_) => "", - Err(_) => "invalid", - } - ) - ) { - div(class = "status") - div { - (realization_status.with( - |status| match status { - Ok(_) => "Target accuracy achieved".to_string(), - Err(message) => message.clone(), - } - )) - } - } - } -} - -fn into_log10_time_point((step, value): (usize, f64)) -> Vec> { - 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::(); - 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::]] - } - ); - 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::(); - 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::]] - } - ); - 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::]] - } - ); - 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::]] - } - ); - 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::(); - view! { - div( - class = "diagnostics-panel", - "hidden" = diagnostics_state.active_tab.with( - |active_tab| { - if active_tab == name { - None - } else { - Some("") - } - } - ) - ) { - (children) - } - } -} - -#[component] -pub fn Diagnostics() -> View { - let diagnostics_state = DiagnosticsState::new("loss".to_string()); - let active_tab = diagnostics_state.active_tab.clone(); - provide_context(diagnostics_state); - - view! { - div(id = "diagnostics") { - div(id = "diagnostics-bar") { - RealizationStatus {} - select(bind:value = active_tab) { - option(value = "loss") { "Loss" } - option(value = "spectrum") { "Spectrum" } - } - } - DiagnosticsPanel(name = "loss") { LossHistory {} } - DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} } - } - } -} \ No newline at end of file diff --git a/app-proto/src/components/display.rs b/app-proto/src/components/display.rs deleted file mode 100644 index da921dd..0000000 --- a/app-proto/src/components/display.rs +++ /dev/null @@ -1,921 +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>, - colors_with_opacity: Vec, - highlights: Vec, -} - -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, - 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>, - colors_with_opacity: Vec, - highlights: Vec, - selections: Vec, -} - -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, - 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, - assembly_to_world: &DMatrix, - pixel_size: f64, - ) -> Option; -} - -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, - assembly_to_world: &DMatrix, - _pixel_size: f64, - ) -> Option { - // 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, - assembly_to_world: &DMatrix, - pixel_size: f64, - ) -> Option { - 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( - context: &WebGl2RenderingContext, - program: &WebGlProgram, - var_name: &str, - member_name_opt: Option<&str>, -) -> [Option; 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, -) { - 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 { - // 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) { - 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::(); - - // canvas - let display = create_node_ref(); - - // viewpoint - let assembly_to_world = create_signal(DMatrix::::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::::identity(5, 5); - let mut rotation = DMatrix::::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::(); - let ctx = canvas - .get_context("webgl2") - .unwrap() - .unwrap() - .dyn_into::() - .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::( - &ctx, &sphere_program, "sphere_list", Some("sp") - ); - let sphere_lt_locs = get_uniform_array_locations::( - &ctx, &sphere_program, "sphere_list", Some("lt") - ); - let sphere_color_locs = get_uniform_array_locations::( - &ctx, &sphere_program, "color_list", None - ); - let sphere_highlight_locs = get_uniform_array_locations::( - &ctx, &sphere_program, "highlight_list", None - ); - let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution"); - let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim"); - let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold"); - let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode"); - - // load the viewport vertex positions into a new vertex buffer object - const VERTEX_CNT: usize = 6; - let viewport_positions: [f32; 3*VERTEX_CNT] = [ - // northwest triangle - -1.0, -1.0, 0.0, - -1.0, 1.0, 0.0, - 1.0, 1.0, 0.0, - // southeast triangle - -1.0, -1.0, 0.0, - 1.0, 1.0, 0.0, - 1.0, -1.0, 0.0, - ]; - let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions); - - // find the point program's vertex attributes - let point_position_attr = ctx.get_attrib_location(&point_program, "position") as u32; - let point_color_attr = ctx.get_attrib_location(&point_program, "color") as u32; - let point_highlight_attr = ctx.get_attrib_location(&point_program, "highlight") as u32; - let point_selection_attr = ctx.get_attrib_location(&point_program, "selected") as u32; - - // set up a repainting routine - let (_, start_animation_loop, _) = create_raf(move || { - // get the time step - let time = performance.now(); - let time_step = 0.001*(time - last_time); - last_time = time; - - // get the navigation state - let pitch_up_val = pitch_up.get(); - let pitch_down_val = pitch_down.get(); - let yaw_right_val = yaw_right.get(); - let yaw_left_val = yaw_left.get(); - let roll_ccw_val = roll_ccw.get(); - let roll_cw_val = roll_cw.get(); - let zoom_in_val = zoom_in.get(); - let zoom_out_val = zoom_out.get(); - let turntable_val = turntable.get(); /* BENCHMARKING */ - - // get the manipulation state - let translate_neg_x_val = translate_neg_x.get(); - let translate_pos_x_val = translate_pos_x.get(); - let translate_neg_y_val = translate_neg_y.get(); - let translate_pos_y_val = translate_pos_y.get(); - let translate_neg_z_val = translate_neg_z.get(); - let translate_pos_z_val = translate_pos_z.get(); - let shrink_neg_val = shrink_neg.get(); - let shrink_pos_val = shrink_pos.get(); - - // update the assembly's orientation - let ang_vel = { - let pitch = pitch_up_val - pitch_down_val; - let yaw = yaw_right_val - yaw_left_val; - let roll = roll_ccw_val - roll_cw_val; - if pitch != 0.0 || yaw != 0.0 || roll != 0.0 { - ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize() - } else { - Vector3::zeros() - } - } /* BENCHMARKING */ + if turntable_val { - Vector3::new(0.0, TURNTABLE_SPEED, 0.0) - } else { - Vector3::zeros() - }; - let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0); - rotation_sp.copy_from( - Rotation3::from_scaled_axis(time_step * ang_vel).matrix() - ); - orientation = &rotation * &orientation; - - // update the assembly's location - let zoom = zoom_out_val - zoom_in_val; - location_z *= (time_step * ZOOM_SPEED * zoom).exp(); - - // manipulate the assembly - if state.selection.with(|sel| sel.len() == 1) { - let sel = state.selection.with( - |sel| sel.into_iter().next().unwrap().clone() - ); - let translate_x = translate_pos_x_val - translate_neg_x_val; - let translate_y = translate_pos_y_val - translate_neg_y_val; - let translate_z = translate_pos_z_val - translate_neg_z_val; - let shrink = shrink_pos_val - shrink_neg_val; - let translating = - translate_x != 0.0 - || translate_y != 0.0 - || translate_z != 0.0; - if translating || shrink != 0.0 { - let elt_motion = { - let u = if translating { - TRANSLATION_SPEED * Vector3::new( - translate_x, translate_y, translate_z - ).normalize() - } else { - Vector3::zeros() - }; - time_step * DVector::from_column_slice( - &[u[0], u[1], u[2], SHRINKING_SPEED * shrink] - ) - }; - assembly_for_raf.deform( - vec![ - ElementMotion { - element: sel, - velocity: elt_motion.as_view(), - } - ] - ); - scene_changed.set(true); - } - } - - if scene_changed.get() { - const SPACE_DIM: usize = 3; - const COLOR_SIZE: usize = 3; - - /* INSTRUMENTS */ - // measure mean frame interval - frames_since_last_sample += 1; - if frames_since_last_sample >= SAMPLE_PERIOD { - mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64)); - last_sample_time = time; - frames_since_last_sample = 0; - } - - // --- get the assembly --- - - let mut scene = Scene::new(); - - // find the map from assembly space to world space - let location = { - let u = -location_z; - DMatrix::from_column_slice(5, 5, &[ - 1.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, u, - 0.0, 0.0, 2.0*u, 1.0, u*u, - 0.0, 0.0, 0.0, 0.0, 1.0, - ]) - }; - let asm_to_world = &location * &orientation; - - // set up the scene - state.assembly.elements.with_untracked( - |elts| for elt in elts { - let selected = state.selection.with(|sel| sel.contains(elt)); - elt.show(&mut scene, selected); - } - ); - let sphere_cnt = scene.spheres.len_i32(); - - // --- draw the spheres --- - - // use the sphere rendering program - ctx.use_program(Some(&sphere_program)); - - // enable the sphere program's vertex attribute - ctx.enable_vertex_attrib_array(viewport_position_attr); - - // write the spheres in world coordinates - let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map( - |rep| (&asm_to_world * rep).cast::() - ).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::>().as_slice() - ).cast::(); - - // 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, 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()), - }; - }, - ) - } -} \ No newline at end of file diff --git a/app-proto/src/components/outline.rs b/app-proto/src/components/outline.rs deleted file mode 100644 index 5355042..0000000 --- a/app-proto/src/components/outline.rs +++ /dev/null @@ -1,260 +0,0 @@ -use itertools::Itertools; -use std::rc::Rc; -use sycamore::prelude::*; -use web_sys::{KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; - -use crate::{ - AppState, - assembly::{ - Element, - HalfCurvatureRegulator, - InversiveDistanceRegulator, - Regulator, - }, - specified::SpecifiedValue -}; - -// an editable view of a regulator -#[component(inline_props)] -fn RegulatorInput(regulator: Rc) -> View { - // get the regulator's measurement and set point signals - let measurement = regulator.measurement(); - let set_point = regulator.set_point(); - - // the `valid` signal tracks whether the last entered value is a valid set - // point specification - let valid = create_signal(true); - - // the `value` signal holds the current set point specification - let value = create_signal( - set_point.with_untracked(|set_pt| set_pt.spec.clone()) - ); - - // this `reset_value` closure resets the input value to the regulator's set - // point specification - let reset_value = move || { - batch(|| { - valid.set(true); - value.set(set_point.with(|set_pt| set_pt.spec.clone())); - }) - }; - - // reset the input value whenever the regulator's set point specification - // is updated - create_effect(reset_value); - - view! { - input( - r#type = "text", - class = move || { - if valid.get() { - set_point.with(|set_pt| { - if set_pt.is_present() { - "regulator-input constraint" - } else { - "regulator-input" - } - }) - } else { - "regulator-input invalid" - } - }, - placeholder = measurement.with(|result| result.to_string()), - bind:value = value, - on:change = move |_| { - valid.set( - match SpecifiedValue::try_from(value.get_clone_untracked()) { - Ok(set_pt) => { - set_point.set(set_pt); - true - }, - Err(_) => false, - } - ) - }, - on:keydown = { - move |event: KeyboardEvent| { - match event.key().as_str() { - "Escape" => reset_value(), - _ => (), - } - } - }, - ) - } -} - -pub trait OutlineItem { - fn outline_item(self: Rc, element: &Rc) -> View; -} - -impl OutlineItem for InversiveDistanceRegulator { - fn outline_item(self: Rc, element: &Rc) -> View { - let other_subject_label = if self.subjects[0] == element.clone() { - self.subjects[1].label() - } else { - self.subjects[0].label() - }.clone(); - view! { - li(class = "regulator") { - div(class = "regulator-label") { (other_subject_label) } - div(class = "regulator-type") { "Inversive distance" } - RegulatorInput(regulator = self) - div(class = "status") - } - } - } -} - -impl OutlineItem for HalfCurvatureRegulator { - fn outline_item(self: Rc, _element: &Rc) -> View { - view! { - li(class = "regulator") { - div(class = "regulator-label") // for spacing - div(class = "regulator-type") { "Half-curvature" } - RegulatorInput(regulator = self) - div(class = "status") - } - } - } -} - -// a list item that shows an element in an outline view of an assembly -#[component(inline_props)] -fn ElementOutlineItem(element: Rc) -> View { - let state = use_context::(); - let class = { - let element_for_class = element.clone(); - state.selection.map( - move |sel| if sel.contains(&element_for_class) { "selected" } else { "" } - ) - }; - let label = element.label().clone(); - let representation = element.representation().clone(); - let rep_components = move || { - representation.with( - |rep| rep.iter().map( - |u| { - let u_str = format!("{:.3}", u).replace("-", "\u{2212}"); - view! { div { (u_str) } } - } - ).collect::>() - ) - }; - let regulated = element.regulators().map(|regs| regs.len() > 0); - let regulator_list = element.regulators().map( - |regs| regs - .clone() - .into_iter() - .sorted_by_key(|reg| reg.subjects().len()) - .collect::>() - ); - let details_node = create_node_ref(); - view! { - li { - details(ref = details_node) { - summary( - class = class.get(), - on:keydown = { - let element_for_handler = element.clone(); - move |event: KeyboardEvent| { - match event.key().as_str() { - "Enter" => { - state.select(&element_for_handler, event.shift_key()); - event.prevent_default(); - }, - "ArrowRight" if regulated.get() => { - let _ = details_node - .get() - .unchecked_into::() - .set_attribute("open", ""); - }, - "ArrowLeft" => { - let _ = details_node - .get() - .unchecked_into::() - .remove_attribute("open"); - }, - _ => (), - } - } - } - ) { - div( - class = "element-switch", - on:click = |event: MouseEvent| event.stop_propagation() - ) - div( - class = "element", - on:click = { - let state_for_handler = state.clone(); - let element_for_handler = element.clone(); - move |event: MouseEvent| { - state_for_handler.select(&element_for_handler, event.shift_key()); - event.stop_propagation(); - event.prevent_default(); - } - } - ) { - div(class = "element-label") { (label) } - div(class = "element-representation") { (rep_components) } - input( - r#type = "checkbox", - bind:checked = element.ghost(), - on:click = |event: MouseEvent| event.stop_propagation() - ) - } - } - ul(class = "regulators") { - Keyed( - list = regulator_list, - view = move |reg| reg.outline_item(&element), - key = |reg| reg.serial() - ) - } - } - } - } -} - -// a component that lists the elements of the current assembly, showing each -// element's regulators in a collapsible sub-list. its implementation is based -// on Kate Morley's HTML + CSS tree views: -// -// https://iamkate.com/code/tree-views/ -// -#[component] -pub fn Outline() -> View { - let state = use_context::(); - - // list the elements alphabetically by ID - /* TO DO */ - // this code is designed to generalize easily to other sort keys. if we only - // ever wanted to sort by ID, we could do that more simply using the - // `elements_by_id` index - let element_list = state.assembly.elements.map( - |elts| elts - .clone() - .into_iter() - .sorted_by_key(|elt| elt.id().clone()) - .collect::>() - ); - - view! { - ul( - id = "outline", - on:click = { - let state = use_context::(); - move |_| state.selection.update(|sel| sel.clear()) - } - ) { - Keyed( - list = element_list, - view = |elt| view! { - ElementOutlineItem(element = elt) - }, - key = |elt| elt.serial() - ) - } - } -} \ No newline at end of file diff --git a/app-proto/src/components/point.frag b/app-proto/src/components/point.frag deleted file mode 100644 index 194a072..0000000 --- a/app-proto/src/components/point.frag +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/app-proto/src/components/point.vert b/app-proto/src/components/point.vert deleted file mode 100644 index 0b76bc1..0000000 --- a/app-proto/src/components/point.vert +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/app-proto/src/components/test_assembly_chooser.rs b/app-proto/src/components/test_assembly_chooser.rs deleted file mode 100644 index 0d387d3..0000000 --- a/app-proto/src/components/test_assembly_chooser.rs +++ /dev/null @@ -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::>::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::(); - 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" } - } - } -} \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs new file mode 100644 index 0000000..ee0af47 --- /dev/null +++ b/app-proto/src/display.rs @@ -0,0 +1,464 @@ +use core::array; +use nalgebra::{DMatrix, Rotation3, Vector3}; +use sycamore::{prelude::*, motion::create_raf}; +use web_sys::{ + console, + window, + KeyboardEvent, + WebGl2RenderingContext, + WebGlProgram, + WebGlShader, + WebGlUniformLocation, + wasm_bindgen::{JsCast, JsValue} +}; + +use crate::AppState; + +fn compile_shader( + context: &WebGl2RenderingContext, + shader_type: u32, + source: &str, +) -> WebGlShader { + let shader = context.create_shader(shader_type).unwrap(); + context.shader_source(&shader, source); + context.compile_shader(&shader); + shader +} + +fn get_uniform_array_locations( + context: &WebGl2RenderingContext, + program: &WebGlProgram, + var_name: &str, + member_name_opt: Option<&str> +) -> [Option; N] { + array::from_fn(|n| { + let name = match member_name_opt { + Some(member_name) => format!("{var_name}[{n}].{member_name}"), + None => format!("{var_name}[{n}]") + }; + context.get_uniform_location(&program, name.as_str()) + }) +} + +// load the given data into the vertex input of the given name +fn bind_vertex_attrib( + context: &WebGl2RenderingContext, + index: u32, + size: i32, + data: &[f32] +) { + // create a data buffer and bind it to ARRAY_BUFFER + let buffer = context.create_buffer().unwrap(); + context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer)); + + // load the given data into the buffer. the function `Float32Array::view` + // creates a raw view into our module's `WebAssembly.Memory` buffer. + // allocating more memory will change the buffer, invalidating the view. + // that means we have to make sure we don't allocate any memory until the + // view is dropped + unsafe { + context.buffer_data_with_array_buffer_view( + WebGl2RenderingContext::ARRAY_BUFFER, + &js_sys::Float32Array::view(&data), + WebGl2RenderingContext::STATIC_DRAW, + ); + } + + // allow the target attribute to be used + context.enable_vertex_attrib_array(index); + + // take whatever's bound to ARRAY_BUFFER---here, the data buffer created + // above---and bind it to the target attribute + // + // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer + // + context.vertex_attrib_pointer_with_i32( + index, + size, + WebGl2RenderingContext::FLOAT, + false, // don't normalize + 0, // zero stride + 0, // zero offset + ); +} + +#[component] +pub fn Display() -> View { + let state = use_context::(); + + // canvas + let display = create_node_ref(); + + // navigation + let pitch_up = create_signal(0.0); + let pitch_down = create_signal(0.0); + let yaw_right = create_signal(0.0); + let yaw_left = create_signal(0.0); + let roll_ccw = create_signal(0.0); + let roll_cw = create_signal(0.0); + let zoom_in = create_signal(0.0); + let zoom_out = create_signal(0.0); + let turntable = create_signal(false); /* BENCHMARKING */ + + // change listener + let scene_changed = create_signal(true); + create_effect(move || { + state.assembly.elements.with(|elts| { + for (_, elt) in elts { + elt.representation.track(); + } + }); + state.selection.track(); + scene_changed.set(true); + }); + + /* INSTRUMENTS */ + const SAMPLE_PERIOD: i32 = 60; + let mut last_sample_time = 0.0; + let mut frames_since_last_sample = 0; + let mean_frame_interval = create_signal(0.0); + + on_mount(move || { + // timing + let mut last_time = 0.0; + + // viewpoint + const ROT_SPEED: f64 = 0.4; // in radians per second + const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second + const TURNTABLE_SPEED: f64 = 0.1; /* BENCHMARKING */ + let mut orientation = DMatrix::::identity(5, 5); + let mut rotation = DMatrix::::identity(5, 5); + let mut location_z: f64 = 5.0; + + // display parameters + const OPACITY: f32 = 0.5; /* SCAFFOLDING */ + const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ + const LAYER_THRESHOLD: i32 = 0; /* DEBUG */ + const DEBUG_MODE: i32 = 0; /* DEBUG */ + + /* INSTRUMENTS */ + let performance = window().unwrap().performance().unwrap(); + + // get the display canvas + let canvas = display.get().unchecked_into::(); + let ctx = canvas + .get_context("webgl2") + .unwrap() + .unwrap() + .dyn_into::() + .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::( + &ctx, &program, "sphere_list", Some("sp") + ); + let sphere_lt_locs = get_uniform_array_locations::( + &ctx, &program, "sphere_list", Some("lt") + ); + let color_locs = get_uniform_array_locations::( + &ctx, &program, "color_list", None + ); + let highlight_locs = get_uniform_array_locations::( + &ctx, &program, "highlight_list", None + ); + let resolution_loc = ctx.get_uniform_location(&program, "resolution"); + let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); + let opacity_loc = ctx.get_uniform_location(&program, "opacity"); + let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); + let debug_mode_loc = ctx.get_uniform_location(&program, "debug_mode"); + + // create a vertex array and bind it to the graphics context + let vertex_array = ctx.create_vertex_array().unwrap(); + ctx.bind_vertex_array(Some(&vertex_array)); + + // set the vertex positions + const VERTEX_CNT: usize = 6; + let positions: [f32; 3*VERTEX_CNT] = [ + // northwest triangle + -1.0, -1.0, 0.0, + -1.0, 1.0, 0.0, + 1.0, 1.0, 0.0, + // southeast triangle + -1.0, -1.0, 0.0, + 1.0, 1.0, 0.0, + 1.0, -1.0, 0.0 + ]; + bind_vertex_attrib(&ctx, position_index, 3, &positions); + + // set up a repainting routine + let (_, start_animation_loop, _) = create_raf(move || { + // get the time step + let time = performance.now(); + let time_step = 0.001*(time - last_time); + last_time = time; + + // get the navigation state + let pitch_up_val = pitch_up.get(); + let pitch_down_val = pitch_down.get(); + let yaw_right_val = yaw_right.get(); + let yaw_left_val = yaw_left.get(); + let roll_ccw_val = roll_ccw.get(); + let roll_cw_val = roll_cw.get(); + let zoom_in_val = zoom_in.get(); + let zoom_out_val = zoom_out.get(); + let turntable_val = turntable.get(); /* BENCHMARKING */ + + // update the assembly's orientation + let ang_vel = { + let pitch = pitch_up_val - pitch_down_val; + let yaw = yaw_right_val - yaw_left_val; + let roll = roll_ccw_val - roll_cw_val; + if pitch != 0.0 || yaw != 0.0 || roll != 0.0 { + ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize() + } else { + Vector3::zeros() + } + } /* BENCHMARKING */ + if turntable_val { + Vector3::new(0.0, TURNTABLE_SPEED, 0.0) + } else { + Vector3::zeros() + }; + let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0); + rotation_sp.copy_from( + Rotation3::from_scaled_axis(time_step * ang_vel).matrix() + ); + orientation = &rotation * &orientation; + + // update the assembly's location + let zoom = zoom_out_val - zoom_in_val; + location_z *= (time_step * ZOOM_SPEED * zoom).exp(); + + if scene_changed.get() { + /* INSTRUMENTS */ + // measure mean frame interval + frames_since_last_sample += 1; + if frames_since_last_sample >= SAMPLE_PERIOD { + mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64)); + last_sample_time = time; + frames_since_last_sample = 0; + } + + // find the map from assembly space to world space + let location = { + let u = -location_z; + DMatrix::from_column_slice(5, 5, &[ + 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, u, + 0.0, 0.0, 2.0*u, 1.0, u*u, + 0.0, 0.0, 0.0, 0.0, 1.0 + ]) + }; + let assembly_to_world = &location * &orientation; + + // get the assembly + let ( + elt_cnt, + reps_world, + colors, + highlights + ) = state.assembly.elements.with(|elts| { + ( + // number of elements + elts.len() as i32, + + // representation vectors in world coordinates + elts.iter().map( + |(_, elt)| elt.representation.with(|rep| &assembly_to_world * rep) + ).collect::>(), + + // 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::>(), + + // highlight levels + elts.iter().map(|(key, _)| { + if state.selection.with(|sel| sel.contains(&key)) { + 1.0_f32 + } else { + HIGHLIGHT + } + }).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 assembly + ctx.uniform1i(sphere_cnt_loc.as_ref(), elt_cnt); + for n in 0..reps_world.len() { + let v = &reps_world[n]; + ctx.uniform3f( + sphere_sp_locs[n].as_ref(), + v[0] as f32, v[1] as f32, v[2] as f32 + ); + ctx.uniform2f( + sphere_lt_locs[n].as_ref(), + v[3] as f32, v[4] as f32 + ); + ctx.uniform3fv_with_f32_array( + color_locs[n].as_ref(), + &colors[n] + ); + ctx.uniform1f( + highlight_locs[n].as_ref(), + highlights[n] + ); + } + + // pass the display parameters + ctx.uniform1f(opacity_loc.as_ref(), OPACITY); + ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); + ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); + + // draw the scene + ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); + + // clear the scene change flag + scene_changed.set( + pitch_up_val != 0.0 + || pitch_down_val != 0.0 + || yaw_left_val != 0.0 + || yaw_right_val != 0.0 + || roll_cw_val != 0.0 + || roll_ccw_val != 0.0 + || zoom_in_val != 0.0 + || zoom_out_val != 0.0 + || turntable_val /* BENCHMARKING */ + ); + } else { + frames_since_last_sample = 0; + mean_frame_interval.set(-1.0); + } + }); + start_animation_loop(); + }); + + let set_nav_signal = move |event: KeyboardEvent, value: f64| { + let mut navigating = true; + let shift = event.shift_key(); + match event.key().as_str() { + "ArrowUp" if shift => zoom_in.set(value), + "ArrowDown" if shift => zoom_out.set(value), + "ArrowUp" => pitch_up.set(value), + "ArrowDown" => pitch_down.set(value), + "ArrowRight" if shift => roll_cw.set(value), + "ArrowLeft" if shift => roll_ccw.set(value), + "ArrowRight" => yaw_right.set(value), + "ArrowLeft" => yaw_left.set(value), + _ => navigating = false + }; + if navigating { + scene_changed.set(true); + event.prevent_default(); + } + }; + + view! { + /* TO DO */ + // switch back to integer-valued parameters when that becomes possible + // again + canvas( + ref=display, + width="600", + height="600", + tabindex="0", + on:keydown=move |event: KeyboardEvent| { + if event.key() == "Shift" { + roll_cw.set(yaw_right.get()); + roll_ccw.set(yaw_left.get()); + zoom_in.set(pitch_up.get()); + zoom_out.set(pitch_down.get()); + yaw_right.set(0.0); + yaw_left.set(0.0); + pitch_up.set(0.0); + pitch_down.set(0.0); + } else { + if event.key() == "Enter" { /* BENCHMARKING */ + turntable.set_fn(|turn| !turn); + scene_changed.set(true); + } + set_nav_signal(event, 1.0); + } + }, + on:keyup=move |event: KeyboardEvent| { + if event.key() == "Shift" { + yaw_right.set(roll_cw.get()); + yaw_left.set(roll_ccw.get()); + pitch_up.set(zoom_in.get()); + pitch_down.set(zoom_out.get()); + roll_cw.set(0.0); + roll_ccw.set(0.0); + zoom_in.set(0.0); + zoom_out.set(0.0); + } else { + set_nav_signal(event, 0.0); + } + }, + on:blur=move |_| { + pitch_up.set(0.0); + pitch_down.set(0.0); + yaw_right.set(0.0); + yaw_left.set(0.0); + roll_ccw.set(0.0); + roll_cw.set(0.0); + } + ) + } +} \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index d033c01..343b96e 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -1,9 +1,10 @@ use lazy_static::lazy_static; -use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen}; -use std::fmt::{Display, Error, Formatter}; +use nalgebra::{Const, DMatrix, DVector, Dyn}; +use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ // --- elements --- +#[cfg(test)] pub fn point(x: f64, y: f64, z: f64) -> DVector { 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_z / radius, 0.5 / radius, - 0.5 * (center_norm_sq / radius - radius), + 0.5 * (center_norm_sq / radius - radius) ]) } @@ -30,264 +31,112 @@ 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_z, 0.5 * curv, - off * (1.0 + 0.5 * off * curv), + off * (1.0 + 0.5 * off * curv) ]) } -// project a sphere's representation vector to the normalization variety by -// contracting toward the last coordinate axis -pub fn project_sphere_to_normalized(rep: &mut DVector) { - let q_sp = rep.fixed_rows::<3>(0).norm_squared(); - let half_q_lt = -2.0 * rep[3] * rep[4]; - let half_q_lt_sq = half_q_lt * half_q_lt; - let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); - rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); -} - -// normalize a point's representation vector by scaling -pub fn project_point_to_normalized(rep: &mut DVector) { - rep.scale_mut(0.5 / rep[3]); -} - // --- partial matrices --- -pub struct MatrixEntry { +struct MatrixEntry { index: (usize, usize), - value: f64, + value: f64 } pub struct PartialMatrix(Vec); impl PartialMatrix { - pub fn new() -> Self { - Self(Vec::::new()) - } - - pub fn push(&mut self, row: usize, col: usize, value: f64) { - let Self(entries) = self; - entries.push(MatrixEntry { index: (row, col), value }); + pub fn new() -> PartialMatrix { + PartialMatrix(Vec::::new()) } pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { - self.push(row, col, value); + let PartialMatrix(entries) = self; + entries.push(MatrixEntry { index: (row, col), value: value }); if row != col { - self.push(col, row, value); + entries.push(MatrixEntry { index: (col, row), value: value }); } } - fn freeze(&self, a: &DMatrix) -> DMatrix { - let mut result = a.clone(); - for &MatrixEntry { index, value } in self { - result[index] = value; + /* DEBUG */ + pub fn log_to_console(&self) { + let PartialMatrix(entries) = self; + for ent in entries { + let ent_str = format!(" {} {} {}", ent.index.0, ent.index.1, ent.value); + console::log_1(&JsValue::from(ent_str.as_str())); } - result } fn proj(&self, a: &DMatrix) -> DMatrix { let mut result = DMatrix::::zeros(a.nrows(), a.ncols()); - for &MatrixEntry { index, .. } in self { - result[index] = a[index]; + let PartialMatrix(entries) = self; + for ent in entries { + result[ent.index] = a[ent.index]; } result } fn sub_proj(&self, rhs: &DMatrix) -> DMatrix { let mut result = DMatrix::::zeros(rhs.nrows(), rhs.ncols()); - for &MatrixEntry { index, value } in self { - result[index] = value - rhs[index]; + let PartialMatrix(entries) = self; + for ent in entries { + result[ent.index] = ent.value - rhs[ent.index]; } result } } -impl Display for PartialMatrix { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { - for &MatrixEntry { index: (row, col), value } in self { - writeln!(f, " {row} {col} {value}")?; - } - Ok(()) - } -} - -impl IntoIterator for PartialMatrix { - type Item = MatrixEntry; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - let Self(entries) = self; - entries.into_iter() - } -} - -impl<'a> IntoIterator for &'a PartialMatrix { - type Item = &'a MatrixEntry; - type IntoIter = std::slice::Iter<'a, MatrixEntry>; - - fn into_iter(self) -> Self::IntoIter { - let PartialMatrix(entries) = self; - entries.into_iter() - } -} - -// --- configuration subspaces --- - -#[derive(Clone)] -pub struct ConfigSubspace { - assembly_dim: usize, - basis_std: Vec>, - basis_proj: Vec>, -} - -impl ConfigSubspace { - pub fn zero(assembly_dim: usize) -> Self { - Self { - assembly_dim, - basis_proj: Vec::new(), - basis_std: Vec::new(), - } - } - - // approximate the kernel of a symmetric endomorphism of the configuration - // space for `assembly_dim` elements. we consider an eigenvector to be part - // of the kernel if its eigenvalue is smaller than the constant `THRESHOLD` - fn symmetric_kernel( - a: DMatrix, - proj_to_std: DMatrix, - assembly_dim: usize, - ) -> Self { - // find a basis for the kernel. the basis is expressed in the projection - // coordinates, and it's orthonormal with respect to the projection - // inner product - const THRESHOLD: f64 = 0.1; - let eig = SymmetricEigen::new(proj_to_std.tr_mul(&a) * &proj_to_std); - let eig_vecs = eig.eigenvectors.column_iter(); - let eig_pairs = eig.eigenvalues.iter().zip(eig_vecs); - let basis_proj = DMatrix::from_columns( - eig_pairs.filter_map( - |(Ξ», v)| (Ξ».abs() < THRESHOLD).then_some(v) - ).collect::>().as_slice() - ); - - // express the basis in the standard coordinates - let basis_std = proj_to_std * &basis_proj; - - const ELEMENT_DIM: usize = 5; - const UNIFORM_DIM: usize = 4; - Self { - assembly_dim, - basis_std: basis_std.column_iter().map( - |v| Into::>::into( - v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim)) - ) - ).collect(), - basis_proj: basis_proj.column_iter().map( - |v| Into::>::into( - v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim)) - ) - ).collect(), - } - } - - pub fn dim(&self) -> usize { - self.basis_std.len() - } - - pub fn assembly_dim(&self) -> usize { - self.assembly_dim - } - - // 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 - // projection coordinates, and the projection is done with respect to the - // projection inner product - pub fn proj(&self, v: &DVectorView, column_index: usize) -> DMatrix { - if self.dim() == 0 { - const ELEMENT_DIM: usize = 5; - DMatrix::zeros(ELEMENT_DIM, self.assembly_dim) - } else { - self.basis_proj.iter().zip(self.basis_std.iter()).map( - |(b_proj, b_std)| b_proj.column(column_index).dot(&v) * b_std - ).sum() - } - } -} - // --- descent history --- pub struct DescentHistory { pub config: Vec>, pub scaled_loss: Vec, pub neg_grad: Vec>, - pub hess_eigvals: Vec>, + pub min_eigval: Vec, pub base_step: Vec>, - pub backoff_steps: Vec, + pub backoff_steps: Vec } impl DescentHistory { - pub fn new() -> Self { - Self { + fn new() -> DescentHistory { + DescentHistory { config: Vec::>::new(), scaled_loss: Vec::::new(), neg_grad: Vec::>::new(), - hess_eigvals: Vec::>::new(), + min_eigval: Vec::::new(), base_step: Vec::>::new(), backoff_steps: Vec::::new(), } } } -// --- constraint problems --- - -pub struct ConstraintProblem { - pub gram: PartialMatrix, - pub frozen: PartialMatrix, - pub guess: DMatrix, -} - -impl ConstraintProblem { - pub fn new(element_count: usize) -> Self { - const ELEMENT_DIM: usize = 5; - Self { - gram: PartialMatrix::new(), - frozen: PartialMatrix::new(), - guess: DMatrix::::zeros(ELEMENT_DIM, element_count), - } - } - - #[cfg(feature = "dev")] - pub fn from_guess(guess_columns: &[DVector]) -> Self { - Self { - gram: PartialMatrix::new(), - frozen: PartialMatrix::new(), - guess: DMatrix::from_columns(guess_columns), - } - } -} - // --- gram matrix realization --- // the Lorentz form lazy_static! { - pub static ref Q: DMatrix = DMatrix::from_row_slice(5, 5, &[ + static ref Q: DMatrix = DMatrix::from_row_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, 0.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 { config: DMatrix, err_proj: DMatrix, - loss: f64, + loss: f64 } impl SearchState { - fn from_config(gram: &PartialMatrix, config: DMatrix) -> Self { + fn from_config(gram: &PartialMatrix, config: DMatrix) -> SearchState { let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config)); let loss = err_proj.norm_squared(); - Self { config, err_proj, loss } + SearchState { + config: config, + err_proj: err_proj, + loss: loss + } } } @@ -297,37 +146,6 @@ fn basis_matrix(index: (usize, usize), nrows: usize, ncols: usize) -> DMatrix) -> DMatrix { - const ELEMENT_DIM: usize = 5; - const UNIFORM_DIM: usize = 4; - let curv = 2.0*v[3]; - if v.dot(&(&*Q * v)) < 0.5 { - // `v` represents a point. the normalization condition says that the - // curvature component of `v` is 1/2 - DMatrix::from_column_slice(ELEMENT_DIM, UNIFORM_DIM, &[ - curv, 0.0, 0.0, 0.0, v[0], - 0.0, curv, 0.0, 0.0, v[1], - 0.0, 0.0, curv, 0.0, v[2], - 0.0, 0.0, 0.0, 0.0, 1.0, - ]) - } else { - // `v` represents a sphere. the normalization condition says that the - // Lorentz product of `v` with itself is 1 - DMatrix::from_column_slice(ELEMENT_DIM, UNIFORM_DIM, &[ - curv, 0.0, 0.0, 0.0, v[0], - 0.0, curv, 0.0, 0.0, v[1], - 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, - ]) - } -} - // use backtracking line search to find a better configuration fn seek_better_config( gram: &PartialMatrix, @@ -336,7 +154,7 @@ fn seek_better_config( base_target_improvement: f64, min_efficiency: f64, backoff: f64, - max_backoff_steps: i32, + max_backoff_steps: i32 ) -> Option<(SearchState, i32)> { let mut rate = 1.0; for backoff_steps in 0..max_backoff_steps { @@ -351,52 +169,25 @@ fn seek_better_config( None } -// a first-order neighborhood of a configuration -pub struct ConfigNeighborhood { - pub config: DMatrix, - pub nbhd: ConfigSubspace, -} - -pub struct Realization { - pub result: Result, - pub history: DescentHistory, -} - -// seek a matrix `config` that matches the partial matrix `problem.frozen` and -// has `config' * Q * config` matching the partial matrix `problem.gram`. start -// at `problem.guess`, set the frozen entries to their desired values, and then -// use a regularized Newton's method to seek the desired Gram matrix +// seek a matrix `config` for which `config' * Q * config` matches the partial +// matrix `gram`. use gradient descent starting from `guess` pub fn realize_gram( - problem: &ConstraintProblem, + gram: &PartialMatrix, + guess: DMatrix, + frozen: &[(usize, usize)], scaled_tol: f64, min_efficiency: f64, backoff: f64, reg_scale: f64, max_descent_steps: i32, - max_backoff_steps: i32, -) -> Realization { - // destructure the problem data - let ConstraintProblem { gram, guess, frozen } = problem; - + max_backoff_steps: i32 +) -> (DMatrix, bool, DescentHistory) { // start the descent history let mut history = DescentHistory::new(); - // handle the case where the assembly is empty. our general realization - // routine can't handle this case because it builds the Hessian using - // `DMatrix::from_columns`, which panics when the list of columns is empty - let assembly_dim = guess.ncols(); - if assembly_dim == 0 { - let result = Ok( - ConfigNeighborhood { - config: guess.clone(), - nbhd: ConfigSubspace::zero(0), - } - ); - return Realization { result, history }; - } - // find the dimension of the search space let element_dim = guess.nrows(); + let assembly_dim = guess.ncols(); let total_dim = element_dim * assembly_dim; // scale the tolerance @@ -405,13 +196,17 @@ pub fn realize_gram( // convert the frozen indices to stacked format let frozen_stacked: Vec = frozen.into_iter().map( - |MatrixEntry { index: (row, col), .. }| col*element_dim + row + |index| index.1*element_dim + index.0 ).collect(); - // use a regularized Newton's method with backtracking - let mut state = SearchState::from_config(gram, frozen.freeze(guess)); - let mut hess = DMatrix::zeros(element_dim, assembly_dim); + // use Newton's method with backtracking and gradient descent backup + let mut state = SearchState::from_config(gram, guess); for _ in 0..max_descent_steps { + // stop if the loss is tolerably low + history.config.push(state.config.clone()); + history.scaled_loss.push(state.loss / scale_adjustment); + if state.loss < tol { break; } + // find the negative gradient of the loss function 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>); @@ -434,15 +229,14 @@ pub fn realize_gram( hess_cols.push(deriv_grad.reshape_generic(Dyn(total_dim), Const::<1>)); } } - hess = DMatrix::from_columns(hess_cols.as_slice()); + let mut hess = DMatrix::from_columns(hess_cols.as_slice()); // regularize the Hessian - let hess_eigvals = hess.symmetric_eigenvalues(); - let min_eigval = hess_eigvals.min(); + let min_eigval = hess.symmetric_eigenvalues().min(); if min_eigval <= 0.0 { 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 // orthogonal complement of the frozen subspace @@ -455,75 +249,85 @@ pub fn realize_gram( hess[(k, k)] = 1.0; } - // stop if the loss is tolerably low - history.config.push(state.config.clone()); - history.scaled_loss.push(state.loss / scale_adjustment); - if state.loss < tol { break; } - // compute the Newton step - /* TO DO */ /* - we should change our regularization to ensure that the Hessian is - is positive-definite, rather than just positive-semidefinite. ideally, - that would guarantee the success of the Cholesky decomposition--- - although we'd still need the error-handling routine in case of - numerical hiccups + we need to either handle or eliminate the case where the minimum + eigenvalue of the Hessian is zero, so the regularized Hessian is + singular. right now, this causes the Cholesky decomposition to return + `None`, leading to a panic when we unrap */ - let hess_cholesky = match hess.clone().cholesky() { - Some(cholesky) => cholesky, - None => return Realization { - result: Err("Cholesky decomposition failed".to_string()), - history, - }, - }; - let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked); + let base_step_stacked = hess.cholesky().unwrap().solve(&neg_grad_stacked); let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); history.base_step.push(base_step.clone()); // 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), - min_efficiency, backoff, max_backoff_steps, + min_efficiency, backoff, max_backoff_steps ) { - state = better_state; - history.backoff_steps.push(backoff_steps); - } else { - return Realization { - result: Err("Line search failed".to_string()), - history, - }; - } + Some((better_state, backoff_steps)) => { + state = better_state; + history.backoff_steps.push(backoff_steps); + }, + None => return (state.config, false, history) + }; } - let result = if state.loss < tol { - // express the uniform basis in the standard basis - const UNIFORM_DIM: usize = 4; - let total_dim_unif = UNIFORM_DIM * assembly_dim; - let mut unif_to_std = DMatrix::::zeros(total_dim, total_dim_unif); - for n in 0..assembly_dim { - let block_start = (element_dim * n, UNIFORM_DIM * n); - unif_to_std - .view_mut(block_start, (element_dim, UNIFORM_DIM)) - .copy_from(&local_unif_to_std(state.config.column(n))); - } - - // find the kernel of the Hessian. give it the uniform inner product - let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim); - - Ok(ConfigNeighborhood { config: state.config, nbhd: tangent }) - } else { - Err("Failed to reach target accuracy".to_string()) - }; - Realization { result, history } + (state.config, state.loss < tol, history) } // --- tests --- -#[cfg(feature = "dev")] -pub mod examples { - use std::f64::consts::PI; +#[cfg(test)] +mod tests { + use std::{array, f64::consts::PI}; use super::*; + #[test] + fn sub_proj_test() { + let target = PartialMatrix(vec![ + MatrixEntry { index: (0, 0), value: 19.0 }, + MatrixEntry { index: (0, 2), value: 39.0 }, + MatrixEntry { index: (1, 1), value: 59.0 }, + MatrixEntry { index: (1, 2), value: 69.0 } + ]); + let attempt = DMatrix::::from_row_slice(2, 3, &[ + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0 + ]); + let expected_result = DMatrix::::from_row_slice(2, 3, &[ + 18.0, 0.0, 36.0, + 0.0, 54.0, 63.0 + ]); + assert_eq!(target.sub_proj(&attempt), expected_result); + } + + #[test] + fn zero_loss_test() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for j in 0..3 { + for k in 0..3 { + entries.push(MatrixEntry { + index: (j, k), + value: if j == k { 1.0 } else { -1.0 } + }); + } + } + entries + }); + let config = { + let a: f64 = 0.75_f64.sqrt(); + DMatrix::from_columns(&[ + sphere(1.0, 0.0, 0.0, a), + sphere(-0.5, a, 0.0, a), + sphere(-0.5, -a, 0.0, a) + ]) + }; + let state = SearchState::from_config(&gram, config); + assert!(state.loss.abs() < f64::EPSILON); + } + // this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article // below includes a nice translation of the problem statement, which was // recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and @@ -532,12 +336,43 @@ pub mod examples { // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki // https://www.nippon.com/en/japan-topics/c12801/ // - pub fn realize_irisawa_hexlet(scaled_tol: f64) -> Realization { - let mut problem = ConstraintProblem::from_guess( + #[test] + fn irisawa_hexlet_test() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for s in 0..9 { + // each sphere is represented by a spacelike vector + entries.push(MatrixEntry { index: (s, s), value: 1.0 }); + + // the circumscribing sphere is tangent to all of the other + // spheres, with matching orientation + if s > 0 { + entries.push(MatrixEntry { index: (0, s), value: 1.0 }); + entries.push(MatrixEntry { index: (s, 0), value: 1.0 }); + } + + if s > 2 { + // each chain sphere is tangent to the "sun" and "moon" + // spheres, with opposing orientation + for n in 1..3 { + entries.push(MatrixEntry { index: (s, n), value: -1.0 }); + entries.push(MatrixEntry { index: (n, s), value: -1.0 }); + } + + // each chain sphere is tangent to the next chain sphere, + // with opposing orientation + let s_next = 3 + (s-2) % 6; + entries.push(MatrixEntry { index: (s, s_next), value: -1.0 }); + entries.push(MatrixEntry { index: (s_next, s), value: -1.0 }); + } + } + entries + }); + let guess = DMatrix::from_columns( [ sphere(0.0, 0.0, 0.0, 15.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( (1..=6).map( |k| { @@ -547,429 +382,159 @@ pub mod examples { ) ).collect::>().as_slice() ); - - for s in 0..9 { - // each sphere is represented by a spacelike vector - problem.gram.push_sym(s, s, 1.0); - - // the circumscribing sphere is tangent to all of the other - // spheres, with matching orientation - if s > 0 { - problem.gram.push_sym(0, s, 1.0); - } - - if s > 2 { - // each chain sphere is tangent to the "sun" and "moon" - // spheres, with opposing orientation - for n in 1..3 { - problem.gram.push_sym(s, n, -1.0); - } - - // each chain sphere is tangent to the next chain sphere, - // with opposing orientation - let s_next = 3 + (s-2) % 6; - problem.gram.push_sym(s, s_next, -1.0); - } - } - - // the frozen entries fix the radii of the circumscribing sphere, the - // "sun" and "moon" spheres, and one of the chain spheres - for k in 0..4 { - problem.frozen.push(3, k, problem.guess[(3, k)]); - } - - 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, - // and find its tangent space - pub fn realize_kaleidocycle(scaled_tol: f64) -> Realization { - const N_HINGES: usize = 6; - let mut problem = ConstraintProblem::from_guess( - (0..N_HINGES).step_by(2).flat_map( - |n| { - let ang_hor = (n as f64) * PI/3.0; - let ang_vert = ((n + 1) as f64) * PI/3.0; - let x_vert = ang_vert.cos(); - let y_vert = ang_vert.sin(); - [ - point(0.0, 0.0, 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), - ] - } - ).collect::>().as_slice() - ); - - const N_POINTS: usize = 2 * N_HINGES; - for block in (0..N_POINTS).step_by(2) { - let block_next = (block + 2) % N_POINTS; - for j in 0..2 { - // diagonal and hinge edges - for k in j..2 { - problem.gram.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); - } - - // non-hinge edges - for k in 0..2 { - problem.gram.push_sym(block + j, block_next + k, -0.625); - } - } - } - - for k in 0..N_POINTS { - problem.frozen.push(3, k, problem.guess[(3, k)]) - } - - realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110) - } -} - -#[cfg(test)] -mod tests { - use nalgebra::Vector3; - use std::{f64::consts::{FRAC_1_SQRT_2, PI}, iter}; - - use super::{*, examples::*}; - - #[test] - fn freeze_test() { - let frozen = PartialMatrix(vec![ - MatrixEntry { index: (0, 0), value: 14.0 }, - MatrixEntry { index: (0, 2), value: 28.0 }, - MatrixEntry { index: (1, 1), value: 42.0 }, - MatrixEntry { index: (1, 2), value: 49.0 }, - ]); - let config = DMatrix::::from_row_slice(2, 3, &[ - 1.0, 2.0, 3.0, - 4.0, 5.0, 6.0, - ]); - let expected_result = DMatrix::::from_row_slice(2, 3, &[ - 14.0, 2.0, 28.0, - 4.0, 42.0, 49.0, - ]); - assert_eq!(frozen.freeze(&config), expected_result); - } - - #[test] - fn sub_proj_test() { - let target = PartialMatrix(vec![ - MatrixEntry { index: (0, 0), value: 19.0 }, - MatrixEntry { index: (0, 2), value: 39.0 }, - MatrixEntry { index: (1, 1), value: 59.0 }, - MatrixEntry { index: (1, 2), value: 69.0 }, - ]); - let attempt = DMatrix::::from_row_slice(2, 3, &[ - 1.0, 2.0, 3.0, - 4.0, 5.0, 6.0, - ]); - let expected_result = DMatrix::::from_row_slice(2, 3, &[ - 18.0, 0.0, 36.0, - 0.0, 54.0, 63.0, - ]); - assert_eq!(target.sub_proj(&attempt), expected_result); - } - - #[test] - fn zero_loss_test() { - let mut gram = PartialMatrix::new(); - for j in 0..3 { - for k in 0..3 { - gram.push(j, k, if j == k { 1.0 } else { -1.0 }); - } - } - let config = { - let a = 0.75_f64.sqrt(); - DMatrix::from_columns(&[ - sphere(1.0, 0.0, 0.0, a), - sphere(-0.5, a, 0.0, a), - sphere(-0.5, -a, 0.0, a), - ]) - }; - let state = SearchState::from_config(&gram, config); - assert!(state.loss.abs() < f64::EPSILON); - } - - /* TO DO */ - // at the frozen indices, the optimization steps should have exact zeros, - // and the realized configuration should have the desired values - #[test] - fn frozen_entry_test() { - let mut problem = ConstraintProblem::from_guess(&[ - point(0.0, 0.0, 2.0), - sphere(0.0, 0.0, 0.0, 0.95), - ]); - for j in 0..2 { - for k in j..2 { - problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); - } - } - problem.frozen.push(3, 0, problem.guess[(3, 0)]); - problem.frozen.push(3, 1, 0.5); - let Realization { result, history } = realize_gram( - &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 - ); - let config = result.unwrap().config; - for base_step in history.base_step.into_iter() { - for &MatrixEntry { index, .. } in &problem.frozen { - assert_eq!(base_step[index], 0.0); - } - } - for MatrixEntry { index, value } in problem.frozen { - assert_eq!(config[index], value); - } - } - - #[test] - fn irisawa_hexlet_test() { - // solve Irisawa's problem + let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k)); const SCALED_TOL: f64 = 1.0e-12; - let config = realize_irisawa_hexlet(SCALED_TOL).result.unwrap().config; - - // check against Irisawa's solution + let (config, success, history) = realize_gram( + &gram, guess, &frozen, + SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + ); 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]; for (k, diam) in solution_diams.into_iter().enumerate() { assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol); } - } - - #[test] - fn tangent_test_three_spheres() { - const SCALED_TOL: f64 = 1.0e-12; - const ELEMENT_DIM: usize = 5; - let mut problem = ConstraintProblem::from_guess(&[ - 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), - ]); - for j in 0..3 { - for k in j..3 { - problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + if success { + println!("Target accuracy achieved!"); + } 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!(" {} sun (given)", 1.0 / config[(3, 3)]); + for k in 4..9 { + println!(" {} sun", 1.0 / config[(3, k)]); } } - for n in 0..ELEMENT_DIM { - problem.frozen.push(n, 0, problem.guess[(n, 0)]); - } - let Realization { result, history } = realize_gram( - &problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 - ); - let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); - assert_eq!(config, problem.guess); - assert_eq!(history.scaled_loss.len(), 1); - - // list some motions that should form a basis for the tangent space of - // the solution variety - const UNIFORM_DIM: usize = 4; - let element_dim = problem.guess.nrows(); - let assembly_dim = problem.guess.ncols(); - let tangent_motions_unif = vec![ - basis_matrix((0, 1), UNIFORM_DIM, assembly_dim), - basis_matrix((1, 1), UNIFORM_DIM, assembly_dim), - basis_matrix((0, 2), UNIFORM_DIM, assembly_dim), - basis_matrix((1, 2), UNIFORM_DIM, assembly_dim), - DMatrix::::from_column_slice(UNIFORM_DIM, assembly_dim, &[ - 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, -0.5, -0.5, - 0.0, 0.0, -0.5, 0.5, - ]), - ]; - let tangent_motions_std = vec![ - basis_matrix((0, 1), element_dim, assembly_dim), - basis_matrix((1, 1), element_dim, assembly_dim), - basis_matrix((0, 2), element_dim, assembly_dim), - basis_matrix((1, 2), element_dim, assembly_dim), - DMatrix::::from_column_slice(element_dim, assembly_dim, &[ - 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, - ]), - ]; - - // confirm that the dimension of the tangent space is no greater than - // expected - assert_eq!(tangent.basis_std.len(), tangent_motions_std.len()); - - // confirm that the tangent space contains all the motions we expect it - // 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 - let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL; - for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) { - let motion_proj: DMatrix<_> = motion_unif.column_iter().enumerate().map( - |(k, v)| tangent.proj(&v, k) - ).sum(); - assert!((motion_std - motion_proj).norm_squared() < tol_sq); + println!("\nStep β”‚ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} β”‚ {}", step, scaled_loss); } } - fn translation_motion_unif(vel: &Vector3, assembly_dim: usize) -> Vec> { - let mut elt_motion = DVector::zeros(4); - elt_motion.fixed_rows_mut::<3>(0).copy_from(vel); - iter::repeat(elt_motion).take(assembly_dim).collect() - } + // --- process inspection examples --- - fn rotation_motion_unif(ang_vel: &Vector3, points: Vec>) -> Vec> { - points.into_iter().map( - |pt| { - let vel = ang_vel.cross(&pt.fixed_rows::<3>(0)); - let mut elt_motion = DVector::zeros(4); - elt_motion.fixed_rows_mut::<3>(0).copy_from(&vel); - elt_motion - } - ).collect() - } + // these tests are meant for human inspection, not automated use. run them + // one at a time in `--nocapture` mode and read through the results and + // optimization histories that they print out. the `run-examples` script + // will run all of them #[test] - fn tangent_test_kaleidocycle() { - // set up a kaleidocycle and find its tangent space - const SCALED_TOL: f64 = 1.0e-12; - let Realization { result, history } = realize_kaleidocycle(SCALED_TOL); - let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); - assert_eq!(history.scaled_loss.len(), 1); - - // list some motions that should form a basis for the tangent space of - // the solution variety - const N_HINGES: usize = 6; - let element_dim = config.nrows(); - let assembly_dim = config.ncols(); - let tangent_motions_unif = vec![ - // the translations along the coordinate axes - 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, 0.0, 1.0), assembly_dim), - - // 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(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()), - - // the twist motion. more precisely: a motion that keeps the center - // of mass stationary and preserves the distances between the - // vertices to first order. this has to be the twist as long as: - // - twisting is the kaleidocycle's only internal degree of - // freedom - // - every first-order motion of the kaleidocycle comes from an - // actual motion - (0..N_HINGES).step_by(2).flat_map( - |n| { - let ang_vert = ((n + 1) as f64) * PI/3.0; - let vel_vert_x = 4.0 * ang_vert.cos(); - let vel_vert_y = 4.0 * ang_vert.sin(); - [ - 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(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]), - DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0]), - ] + fn three_spheres_example() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for j in 0..3 { + for k in 0..3 { + entries.push(MatrixEntry { + index: (j, k), + value: if j == k { 1.0 } else { -1.0 } + }); } - ).collect::>(), - ]; - let tangent_motions_std = tangent_motions_unif.iter().map( - |motion| DMatrix::from_columns( - &config.column_iter().zip(motion).map( - |(v, elt_motion)| local_unif_to_std(v) * elt_motion - ).collect::>() - ) - ).collect::>(); - - // confirm that the dimension of the tangent space is no greater than - // expected - assert_eq!(tangent.basis_std.len(), tangent_motions_unif.len()); - - // confirm that the tangent space contains all the motions we expect it - // 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 - let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL; - for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) { - let motion_proj: DMatrix<_> = motion_unif.into_iter().enumerate().map( - |(k, v)| tangent.proj(&v.as_view(), k) - ).sum(); - assert!((motion_std - motion_proj).norm_squared() < tol_sq); - } - } - - fn translation(dis: Vector3) -> DMatrix { - const ELEMENT_DIM: usize = 5; - DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[ - 1.0, 0.0, 0.0, 0.0, dis[0], - 0.0, 1.0, 0.0, 0.0, dis[1], - 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(), - 0.0, 0.0, 0.0, 0.0, 1.0, - ]) - } - - // confirm that projection onto a configuration subspace is equivariant with - // respect to Euclidean motions - #[test] - fn proj_equivar_test() { - // find a pair of spheres that meet at 120Β° - const SCALED_TOL: f64 = 1.0e-12; - 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), - ]); - problem_orig.gram.push_sym(0, 0, 1.0); - problem_orig.gram.push_sym(1, 1, 1.0); - problem_orig.gram.push_sym(0, 1, 0.5); - let Realization { result: result_orig, history: history_orig } = realize_gram( - &problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 - ); - let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap(); - assert_eq!(config_orig, problem_orig.guess); - assert_eq!(history_orig.scaled_loss.len(), 1); - - // find another pair of spheres that meet at 120Β°. we'll think of this - // solution as a transformed version of the original one - let guess_tfm = { - let a = 0.5 * FRAC_1_SQRT_2; + } + entries + }); + let guess = { + let a: f64 = 0.75_f64.sqrt(); DMatrix::from_columns(&[ - sphere(a, 0.0, 7.0 + a, 1.0), - sphere(-a, 0.0, 7.0 - a, 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) ]) }; - let problem_tfm = ConstraintProblem { - gram: problem_orig.gram, - frozen: problem_orig.frozen, - guess: guess_tfm, - }; - let Realization { result: result_tfm, history: history_tfm } = realize_gram( - &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + println!(); + let (config, success, history) = realize_gram( + &gram, guess, &[], + 1.0e-12, 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!(history_tfm.scaled_loss.len(), 1); - - // project a nudge to the tangent space of the solution variety at the - // original solution - 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); - - // project the equivalent nudge to the tangent space of the 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_proj = tangent_tfm.proj(&motion_tfm.as_view(), 0); - - // take the transformation that sends the original solution to the - // transformed solution and apply it to the motion that the original - // solution makes in response to the nudge - const ELEMENT_DIM: usize = 5; - let rot = DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[ - FRAC_1_SQRT_2, 0.0, -FRAC_1_SQRT_2, 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, - 0.0, 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 1.0, + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + if success { + println!("Target accuracy achieved!"); + } 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); + } + } + + #[test] + fn point_on_sphere_example() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for j in 0..2 { + for k in 0..2 { + entries.push(MatrixEntry { + index: (j, k), + value: if (j, k) == (1, 1) { 1.0 } else { 0.0 } + }); + } + } + entries + }); + let guess = DMatrix::from_columns(&[ + point(0.0, 0.0, 2.0), + sphere(0.0, 0.0, 0.0, 1.0) ]); - let transl = translation(Vector3::new(0.0, 0.0, 7.0)); - let motion_proj_tfm = transl * rot * motion_orig_proj; - - // confirm that the projection of the nudge is equivariant. we loosen - // the comparison tolerance because the transformation seems to - // introduce some numerical error - const SCALED_TOL_TFM: f64 = 1.0e-9; - 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); + let frozen = [(3, 0)]; + println!(); + let (config, success, history) = realize_gram( + &gram, guess, &frozen, + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + print!("Configuration:{}", config); + if success { + println!("Target accuracy achieved!"); + } 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); + } + } + + /* TO DO */ + // --- new test placed here to avoid merge conflict --- + + // at the frozen indices, the optimization steps should have exact zeros, + // and the realized configuration should match the initial guess + #[test] + fn frozen_entry_test() { + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for j in 0..2 { + for k in j..2 { + gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + gram_to_be + }; + let guess = DMatrix::from_columns(&[ + point(0.0, 0.0, 2.0), + sphere(0.0, 0.0, 0.0, 1.0) + ]); + let frozen = [(3, 0), (3, 1)]; + println!(); + let (config, success, history) = realize_gram( + &gram, guess.clone(), &frozen, + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + assert_eq!(success, true); + for base_step in history.base_step.into_iter() { + for index in frozen { + assert_eq!(base_step[index], 0.0); + } + } + for index in frozen { + assert_eq!(config[index], guess[index]); + } } } \ No newline at end of file diff --git a/app-proto/src/components/identity.vert b/app-proto/src/identity.vert similarity index 100% rename from app-proto/src/components/identity.vert rename to app-proto/src/identity.vert diff --git a/app-proto/src/components/spheres.frag b/app-proto/src/inversive.frag similarity index 95% rename from app-proto/src/components/spheres.frag rename to app-proto/src/inversive.frag index fa317a8..d50cb1e 100644 --- a/app-proto/src/components/spheres.frag +++ b/app-proto/src/inversive.frag @@ -17,7 +17,7 @@ struct vecInv { const int SPHERE_MAX = 200; uniform int sphere_cnt; uniform vecInv sphere_list[SPHERE_MAX]; -uniform vec4 color_list[SPHERE_MAX]; +uniform vec3 color_list[SPHERE_MAX]; uniform float highlight_list[SPHERE_MAX]; // view @@ -25,6 +25,7 @@ uniform vec2 resolution; uniform float shortdim; // controls +uniform float opacity; uniform int layer_threshold; uniform bool debug_mode; @@ -68,7 +69,7 @@ struct Fragment { 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 // negative gradient of the lorentz product between the impact point vector // and the sphere vector with respect to the coordinates of the impact @@ -78,7 +79,7 @@ Fragment sphere_shading(vecInv v, vec3 pt, vec4 base_color) { float incidence = dot(normal, light_dir); float illum = mix(0.4, 1.0, max(incidence, 0.0)); - return Fragment(pt, normal, vec4(illum * base_color.rgb, base_color.a)); + return Fragment(pt, normal, vec4(illum * base_color, opacity)); } float intersection_dist(Fragment a, Fragment b) { @@ -191,11 +192,10 @@ void main() { vec3 color = vec3(0.); int layer = layer_cnt - 1; TaggedDepth hit = top_hits[layer]; - vec4 sphere_color = color_list[hit.id]; Fragment frag_next = sphere_shading( sphere_list[hit.id], hit.depth * dir, - vec4(hit.dimming * sphere_color.rgb, sphere_color.a) + hit.dimming * color_list[hit.id] ); float highlight_next = highlight_list[hit.id]; --layer; @@ -206,11 +206,10 @@ void main() { // shade the next fragment hit = top_hits[layer]; - sphere_color = color_list[hit.id]; frag_next = sphere_shading( sphere_list[hit.id], hit.depth * dir, - vec4(hit.dimming * sphere_color.rgb, sphere_color.a) + hit.dimming * color_list[hit.id] ); highlight_next = highlight_list[hit.id]; diff --git a/app-proto/src/lib.rs b/app-proto/src/lib.rs deleted file mode 100644 index 0d9bc4a..0000000 --- a/app-proto/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod engine; \ No newline at end of file diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index a03b026..d916a81 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -1,67 +1,42 @@ +mod add_remove; mod assembly; -mod components; +mod display; mod engine; -mod specified; +mod outline; -#[cfg(test)] -mod tests; - -use std::{collections::BTreeSet, rc::Rc}; +use rustc_hash::FxHashSet; use sycamore::prelude::*; -use assembly::{Assembly, Element}; -use components::{ - add_remove::AddRemove, - diagnostics::Diagnostics, - display::Display, - outline::Outline, -}; +use add_remove::AddRemove; +use assembly::{Assembly, ElementKey}; +use display::Display; +use outline::Outline; #[derive(Clone)] struct AppState { assembly: Assembly, - selection: Signal>>, + assembly_serial: Signal, + selection: Signal> } impl AppState { - fn new() -> Self { - Self { + fn new() -> AppState { + AppState { assembly: Assembly::new(), - selection: create_signal(BTreeSet::default()), - } - } - - // in single-selection mode, select the given element. in multiple-selection - // mode, toggle whether the given element is selected - fn select(&self, element: &Rc, multi: bool) { - if multi { - self.selection.update(|sel| { - if !sel.remove(element) { - sel.insert(element.clone()); - } - }); - } else { - self.selection.update(|sel| { - sel.clear(); - sel.insert(element.clone()); - }); + assembly_serial: create_signal(0), + selection: create_signal(FxHashSet::default()) } } } fn main() { - // set the console error panic hook - #[cfg(feature = "console_error_panic_hook")] - console_error_panic_hook::set_once(); - sycamore::render(|| { provide_context(AppState::new()); view! { - div(id = "sidebar") { + div(id="sidebar") { AddRemove {} Outline {} - Diagnostics {} } Display {} } diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs new file mode 100644 index 0000000..39247fa --- /dev/null +++ b/app-proto/src/outline.rs @@ -0,0 +1,211 @@ +use itertools::Itertools; +use sycamore::prelude::*; +use web_sys::{ + Event, + HtmlInputElement, + KeyboardEvent, + MouseEvent, + wasm_bindgen::JsCast +}; + +use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}}; + +// an editable view of the Lorentz product representing a constraint +#[component(inline_props)] +fn LorentzProductInput(constraint: Constraint) -> View { + view! { + input( + r#type="text", + bind:value=constraint.lorentz_prod_text, + on:change=move |event: Event| { + let target: HtmlInputElement = event.target().unwrap().unchecked_into(); + match target.value().parse::() { + Ok(lorentz_prod) => batch(|| { + constraint.lorentz_prod.set(lorentz_prod); + constraint.lorentz_prod_valid.set(true); + }), + Err(_) => constraint.lorentz_prod_valid.set(false) + }; + } + ) + } +} + +// a list item that shows a constraint in an outline view of an element +#[component(inline_props)] +fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) -> View { + let state = use_context::(); + let assembly = &state.assembly; + let constraint = assembly.constraints.with(|csts| csts[constraint_key].clone()); + let other_subject = if constraint.subjects.0 == element_key { + constraint.subjects.1 + } else { + constraint.subjects.0 + }; + let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); + let class = constraint.lorentz_prod_valid.map( + |&lorentz_prod_valid| if lorentz_prod_valid { "constraint" } else { "constraint invalid" } + ); + view! { + li(class=class.get()) { + input(r#type="checkbox", bind:checked=constraint.active) + div(class="constraint-label") { (other_subject_label) } + LorentzProductInput(constraint=constraint) + div(class="status") + } + } +} + +// a list item that shows an element in an outline view of an assembly +#[component(inline_props)] +fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { + let state = use_context::(); + let class = state.selection.map( + move |sel| if sel.contains(&key) { "selected" } else { "" } + ); + let label = element.label.clone(); + let rep_components = element.representation.map( + |rep| rep.iter().map( + |u| format!("{:.3}", u).replace("-", "\u{2212}") + ).collect() + ); + let constrained = element.constraints.map(|csts| csts.len() > 0); + let constraint_list = element.constraints.map( + |csts| csts.clone().into_iter().collect() + ); + let details_node = create_node_ref(); + view! { + li { + details(ref=details_node) { + summary( + class=class.get(), + on:keydown={ + move |event: KeyboardEvent| { + match event.key().as_str() { + "Enter" => { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + event.prevent_default(); + }, + "ArrowRight" if constrained.get() => { + let _ = details_node + .get() + .unchecked_into::() + .set_attribute("open", ""); + }, + "ArrowLeft" => { + let _ = details_node + .get() + .unchecked_into::() + .remove_attribute("open"); + }, + _ => () + } + } + } + ) { + div( + class="element-switch", + on:click=|event: MouseEvent| event.stop_propagation() + ) + div( + class="element", + on:click={ + move |event: MouseEvent| { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + event.stop_propagation(); + event.prevent_default(); + } + } + ) { + div(class="element-label") { (label) } + div(class="element-representation") { + Indexed( + list=rep_components, + view=|coord_str| view! { + div { (coord_str) } + } + ) + } + div(class="status") + } + } + ul(class="constraints") { + Keyed( + list=constraint_list, + view=move |cst_key| view! { + ConstraintOutlineItem( + constraint_key=cst_key, + element_key=key + ) + }, + key=|cst_key| cst_key.clone() + ) + } + } + } + } +} + +// a component that lists the elements of the current assembly, showing the +// constraints on each element as a collapsible sub-list. its implementation +// is based on Kate Morley's HTML + CSS tree views: +// +// https://iamkate.com/code/tree-views/ +// +#[component] +pub fn Outline() -> View { + let state = use_context::(); + + // list the elements alphabetically by ID + let element_list = state.assembly.elements.map( + move |elts| { + let asm_serial = state.assembly_serial.get_untracked(); + elts + .clone() + .into_iter() + .sorted_by_key(|(_, elt)| elt.id.clone()) + .map(|(key, elt)| (asm_serial, key, elt)) + .collect() + } + ); + + view! { + ul( + id="outline", + on:click={ + let state = use_context::(); + move |_| state.selection.update(|sel| sel.clear()) + } + ) { + Keyed( + list=element_list, + view=|(_, key, elt)| view! { + ElementOutlineItem(key=key, element=elt) + }, + key=|(asm_serial, key, _)| (asm_serial.clone(), key.clone()) + ) + } + } +} \ No newline at end of file diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs deleted file mode 100644 index 788460b..0000000 --- a/app-proto/src/specified.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::num::ParseFloatError; - -// a real number described by a specification string. since the structure is -// read-only, we can guarantee that `spec` always specifies `value` in the -// following format -// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -// β”‚ `spec` β”‚ `value` β”‚ -// ┝━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━β”₯ -// β”‚ a string that parses to the floating-point value `x` β”‚ `Some(x)` β”‚ -// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -// β”‚ the empty string β”‚ `None` β”‚ -// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -#[readonly::make] -pub struct SpecifiedValue { - pub spec: String, - pub value: Option, -} - -impl SpecifiedValue { - pub fn from_empty_spec() -> Self { - Self { spec: String::new(), value: None } - } - - pub fn is_present(&self) -> bool { - matches!(self.value, Some(_)) - } -} - -// a `SpecifiedValue` can be constructed from a specification string, formatted -// as described in the comment on the structure definition. the result is `Ok` -// if the specification is properly formatted, and `Error` if not -impl TryFrom for SpecifiedValue { - type Error = ParseFloatError; - - fn try_from(spec: String) -> Result { - if spec.is_empty() { - Ok(Self::from_empty_spec()) - } else { - spec.parse::().map( - |value| Self { spec, value: Some(value) } - ) - } - } -} \ No newline at end of file diff --git a/app-proto/src/tests.rs b/app-proto/src/tests.rs deleted file mode 100644 index 2c5436b..0000000 --- a/app-proto/src/tests.rs +++ /dev/null @@ -1,14 +0,0 @@ -use std::process::Command; - -// build and bundle the application, reporting success if there are no errors or -// warnings. to see this test fail while others succeed, try moving `index.html` -// or one of the assets that it links to -#[test] -fn trunk_build_test() { - let build_status = Command::new("trunk") - .arg("build") - .env("RUSTFLAGS", "-D warnings") - .status() - .expect("Call to Trunk failed"); - assert!(build_status.success()); -} \ No newline at end of file diff --git a/deploy/.gitignore b/deploy/.gitignore deleted file mode 100644 index 192f529..0000000 --- a/deploy/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -/dyna3.zip -/dyna3/index.html -/dyna3/dyna3-*.js -/dyna3/dyna3-*.wasm -/dyna3/main-*.css \ No newline at end of file diff --git a/tools/package-for-deployment.sh b/tools/package-for-deployment.sh deleted file mode 100644 index fdda434..0000000 --- a/tools/package-for-deployment.sh +++ /dev/null @@ -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" diff --git a/tools/run-examples.sh b/tools/run-examples.sh deleted file mode 100644 index 0946d92..0000000 --- a/tools/run-examples.sh +++ /dev/null @@ -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 \ No newline at end of file