Compare commits
101 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e117d5877 | ||
|
|
517fd327fa | ||
|
|
f1690b62e1 | ||
|
|
cca5a781c4 | ||
|
|
abe231126d | ||
|
|
ee1c691787 | ||
|
|
19907838ce | ||
|
|
e3120f7109 | ||
|
|
18ebf3be2c | ||
|
|
edace8e4ea | ||
|
|
70bd39b9e5 | ||
|
|
25fa108e9b | ||
|
|
7977b11caf | ||
|
|
1c9fec36e5 | ||
|
|
721a8716d4 | ||
|
|
4f8f36053f | ||
|
|
28b1ecb8e9 | ||
|
|
b08dbd6f93 | ||
|
|
bd0982f821 | ||
|
|
2444649dd1 | ||
|
|
b3afd6f555 | ||
|
|
9b39fe56b8 | ||
|
|
f5486fb0dd | ||
|
|
4e3c86fb71 | ||
|
|
7ff1b9cb65 | ||
|
|
e6281cdcc6 | ||
|
|
fc85d15f83 | ||
|
|
7709c61f71 | ||
|
|
edee153e37 | ||
|
|
4a24a01928 | ||
|
|
050e2373a6 | ||
|
|
147e275823 | ||
|
|
d121385c18 | ||
|
|
78f8ef8215 | ||
|
|
96f8b6b5f3 | ||
|
|
96afad0c97 | ||
|
|
a60624884a | ||
|
|
93190e99da | ||
|
|
e2d3af2867 | ||
|
|
7cb01bab82 | ||
|
|
f47be08d98 | ||
|
|
cd18d594e0 | ||
|
|
49655a8d62 | ||
|
|
959e4cc8b5 | ||
|
|
49170671b4 | ||
|
|
0c2869d3f3 | ||
|
|
e6d1e0b865 | ||
|
|
d481181ef8 | ||
|
|
20b96a9764 | ||
|
|
634e97b659 | ||
|
|
336b940471 | ||
|
|
d3c9a08d22 | ||
|
|
aceac5e5c4 | ||
|
|
20d072d615 | ||
|
|
c67f37c934 | ||
|
|
2efc08d6c0 | ||
|
|
69ab888d5b | ||
|
|
0173b63e19 | ||
|
|
b289d2d4c3 | ||
|
|
163361184b | ||
|
|
ab830b194e | ||
|
|
3493a798d1 | ||
|
|
121934c4c3 | ||
|
|
a4236a34df | ||
|
|
f148552964 | ||
|
|
6db9f5be6c | ||
|
|
3a721a4cc8 | ||
|
|
8fde202911 | ||
|
|
4afc82034b | ||
|
|
e80adf831d | ||
|
|
3d7ee98dd6 | ||
|
|
c04e29f586 | ||
|
|
01c2af6615 | ||
|
|
a40a110788 | ||
|
|
f62f44b5a7 | ||
|
|
ec48592ef1 | ||
|
|
5e9c5db231 | ||
|
|
bf140efaf7 | ||
|
|
cbec31f5df | ||
|
|
b9370ceb41 | ||
|
|
85db7b9be0 | ||
|
|
c5fe725b1b | ||
|
|
5bf23fa789 | ||
|
|
206a2df480 | ||
|
|
c18cac642b | ||
|
|
8798683d25 | ||
|
|
b9587872d3 | ||
|
|
766d56027c | ||
|
|
25da6ca062 | ||
|
|
e3df765f16 | ||
|
|
f1029b3102 | ||
|
|
87763fc458 | ||
|
|
2ef0fdd3e2 | ||
|
|
d2cecf69db | ||
|
|
c78a041dc7 | ||
|
|
f274119da6 | ||
|
|
1fbeb23194 | ||
|
|
80b210e667 | ||
|
|
81f9b8e040 | ||
|
|
5885189b04 | ||
|
|
fd3cbae1b4 |
43 changed files with 1006 additions and 6919 deletions
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -1,2 +1,8 @@
|
||||||
ci-bin
|
node_modules
|
||||||
|
site
|
||||||
|
docbuild
|
||||||
|
__tests__
|
||||||
|
coverage
|
||||||
|
dyna3.zip
|
||||||
|
tmpproj
|
||||||
*~
|
*~
|
||||||
|
|
|
||||||
76
README.md
76
README.md
|
|
@ -12,78 +12,8 @@ Note that currently this is just the barest beginnings of the project, more of a
|
||||||
|
|
||||||
### Implementation goals
|
### Implementation goals
|
||||||
|
|
||||||
* Provide a comfortable, intuitive UI
|
* Comfortable, intuitive UI
|
||||||
|
|
||||||
* Allow execution in browser (so implemented in WASM-compatible language)
|
* Able to run in browser (so implemented in WASM-compatible language)
|
||||||
|
|
||||||
* Produce scalable graphics of 3D diagrams, and maybe STL files (or other fabricatable file format) as well
|
* Produce scalable graphics of 3D diagrams, and maybe STL files (or other fabricatable file format) as well.
|
||||||
|
|
||||||
## Prototype
|
|
||||||
|
|
||||||
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.
|
|
||||||
- In the future, `trunk` can be updated with the same command. (You may need the `--locked` flag if your ambient version of `rustc` does not match that required by `trunk`.)
|
|
||||||
6. Add the `.cargo/bin` folder in your home directory to your executable search path
|
|
||||||
- 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.
|
|
||||||
- Alternatively, if you don't want to adjust your `PATH`, you can install `trunk` in another directory `DIR` via `cargo install --root DIR trunk`.
|
|
||||||
|
|
||||||
### Play with the prototype
|
|
||||||
|
|
||||||
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 execute
|
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
|
||||||
1346
app-proto/Cargo.lock
generated
1346
app-proto/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,25 +1,19 @@
|
||||||
[package]
|
[package]
|
||||||
name = "dyna3"
|
name = "sketch-outline"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Aaron Fenyes", "Glen Whitney"]
|
authors = ["Aaron"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.86"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["console_error_panic_hook"]
|
default = ["console_error_panic_hook"]
|
||||||
dev = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
enum-iterator = "2.3.0"
|
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
js-sys = "0.3.70"
|
js-sys = "0.3.70"
|
||||||
lazy_static = "1.5.0"
|
|
||||||
nalgebra = "0.33.0"
|
nalgebra = "0.33.0"
|
||||||
readonly = "0.2.12"
|
rustc-hash = "2.0.0"
|
||||||
sycamore = "0.9.1"
|
slab = "0.4.9"
|
||||||
|
sycamore = "0.9.0-beta.3"
|
||||||
# We use Charming to help display engine diagnostics
|
|
||||||
charming = { version = "0.5.1", features = ["wasm"] }
|
|
||||||
|
|
||||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||||
# logging them with `console.error`. This is great for development, but requires
|
# logging them with `console.error`. This is great for development, but requires
|
||||||
|
|
@ -30,9 +24,7 @@ console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "0.3.69"
|
version = "0.3.69"
|
||||||
features = [
|
features = [
|
||||||
'DomRect',
|
|
||||||
'HtmlCanvasElement',
|
'HtmlCanvasElement',
|
||||||
'HtmlInputElement',
|
|
||||||
'Performance',
|
'Performance',
|
||||||
'WebGl2RenderingContext',
|
'WebGl2RenderingContext',
|
||||||
'WebGlBuffer',
|
'WebGlBuffer',
|
||||||
|
|
@ -42,41 +34,9 @@ features = [
|
||||||
'WebGlVertexArrayObject'
|
'WebGlVertexArrayObject'
|
||||||
]
|
]
|
||||||
|
|
||||||
# the self-dependency specifies features to use for tests and examples
|
|
||||||
#
|
|
||||||
# https://github.com/rust-lang/cargo/issues/2911#issuecomment-1483256987
|
|
||||||
#
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dyna3 = { path = ".", default-features = false, features = ["dev"] }
|
|
||||||
wasm-bindgen-test = "0.3.34"
|
wasm-bindgen-test = "0.3.34"
|
||||||
|
|
||||||
# turn off spurious warnings about the custom config that Sycamore uses
|
|
||||||
#
|
|
||||||
# https://sycamore.dev/book/troubleshooting#unexpected-cfg-condition-name--sycamore-force-ssr
|
|
||||||
#
|
|
||||||
[lints.rust]
|
|
||||||
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(sycamore_force_ssr)"] }
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "s" # optimize for small code size
|
opt-level = "s" # optimize for small code size
|
||||||
debug = true # include debug symbols
|
debug = true # include debug symbols
|
||||||
|
|
||||||
[[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
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
[build]
|
|
||||||
public_url = "./"
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use nalgebra::DMatrix;
|
|
||||||
|
|
||||||
use dyna3::engine::{Q, DescentHistory, Realization};
|
|
||||||
|
|
||||||
pub fn title(title: &str) {
|
|
||||||
println!("─── {title} ───");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn realization_diagnostics(realization: &Realization) {
|
|
||||||
let Realization { result, history } = realization;
|
|
||||||
println!();
|
|
||||||
if let Err(ref message) = result {
|
|
||||||
println!("❌️ {message}");
|
|
||||||
} else {
|
|
||||||
println!("✅️ Target accuracy achieved!");
|
|
||||||
}
|
|
||||||
println!("Steps: {}", history.scaled_loss.len() - 1);
|
|
||||||
println!("Loss: {}", history.scaled_loss.last().unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gram_matrix(config: &DMatrix<f64>) {
|
|
||||||
println!("\nCompleted Gram matrix:{}", (config.tr_mul(&*Q) * config).to_string().trim_end());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn config(config: &DMatrix<f64>) {
|
|
||||||
println!("\nConfiguration:{}", config.to_string().trim_end());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn loss_history(history: &DescentHistory) {
|
|
||||||
println!("\nStep │ Loss\n─────┼────────────────────────────────");
|
|
||||||
for (step, scaled_loss) in history.scaled_loss.iter().enumerate() {
|
|
||||||
println!("{:<4} │ {}", step, scaled_loss);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +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);
|
|
||||||
}
|
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -2,16 +2,8 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<title>dyna3</title>
|
<title>Sketch outline</title>
|
||||||
<link data-trunk rel="css" href="main.css"/>
|
<link data-trunk rel="css" href="main.css"/>
|
||||||
<link href="https://fonts.bunny.net/css?family=fira-sans:ital,wght@0,400;1,400&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.bunny.net/css?family=noto-emoji:wght@400&text=%f0%9f%94%97%e2%9a%a0&display=swap" rel="stylesheet">
|
|
||||||
|
|
||||||
<!--
|
|
||||||
the Charming visualization crate, which we use to show engine diagnostics,
|
|
||||||
depends the ECharts JavaScript package
|
|
||||||
-->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body></body>
|
<body></body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,7 @@
|
||||||
:root {
|
|
||||||
--text: #fcfcfc; /* almost white */
|
|
||||||
--text-bright: white;
|
|
||||||
--text-invalid: #f58fc2; /* bright pink */
|
|
||||||
--border: #555; /* light gray */
|
|
||||||
--border-focus-dark: #aaa; /* bright gray */
|
|
||||||
--border-focus-light: white;
|
|
||||||
--border-invalid: #70495c; /* dusky pink */
|
|
||||||
--selection-highlight: #444; /* medium gray */
|
|
||||||
--page-background: #222; /* dark gray */
|
|
||||||
--display-background: #020202; /* almost black */
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
color: var(--text);
|
color: #fcfcfc;
|
||||||
background-color: var(--page-background);
|
background-color: #222;
|
||||||
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 */
|
||||||
|
|
@ -35,13 +10,13 @@ body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
float: left;
|
float: left;
|
||||||
width: 500px;
|
width: 450px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
border-width: 0px 1px 0px 0px;
|
border-width: 0px 1px 0px 0px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: var(--border);
|
border-color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* add-remove */
|
/* add-remove */
|
||||||
|
|
@ -53,17 +28,8 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
#add-remove > button {
|
#add-remove > button {
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* KLUDGE */
|
|
||||||
/*
|
|
||||||
for convenience, we're using emoji as temporary icons for some buttons. these
|
|
||||||
buttons need to be displayed in an emoji font
|
|
||||||
*/
|
|
||||||
#add-remove > button.emoji {
|
|
||||||
width: 32px;
|
width: 32px;
|
||||||
font-family: 'Noto Emoji', sans-serif;
|
height: 32px;
|
||||||
font-size: large;
|
font-size: large;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,165 +51,74 @@ summary {
|
||||||
}
|
}
|
||||||
|
|
||||||
summary.selected {
|
summary.selected {
|
||||||
color: var(--text-bright);
|
color: #fff;
|
||||||
background-color: var(--selection-highlight);
|
background-color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
summary > div, .regulator {
|
summary > div, .cst {
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.element, .regulator {
|
.elt, .cst {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.element > input {
|
.elt-switch {
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.element-switch {
|
|
||||||
width: 18px;
|
width: 18px;
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
details:has(li) .element-switch::after {
|
details:has(li) .elt-switch::after {
|
||||||
content: '▸';
|
content: '▸';
|
||||||
}
|
}
|
||||||
|
|
||||||
details[open]:has(li) .element-switch::after {
|
details[open]:has(li) .elt-switch::after {
|
||||||
content: '▾';
|
content: '▾';
|
||||||
}
|
}
|
||||||
|
|
||||||
.element-label {
|
.elt-label {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.regulator-label {
|
.cst-label {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.element-representation {
|
.elt-rep {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.element-representation > div {
|
.elt-rep > div, .cst-rep {
|
||||||
padding: 2px 0px 0px 0px;
|
padding: 2px 0px 0px 0px;
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
font-variant-numeric: tabular-nums;
|
text-align: center;
|
||||||
text-align: right;
|
|
||||||
width: 56px;
|
width: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.regulator {
|
.cst {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.regulator-type {
|
.cst > input {
|
||||||
padding: 2px 8px 0px 8px;
|
margin: 0px 8px 0px 0px;
|
||||||
font-size: 10pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.regulator-input {
|
|
||||||
margin-right: 4px;
|
|
||||||
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);
|
|
||||||
border-color: var(--border-invalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
.regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after {
|
|
||||||
content: '⚠';
|
|
||||||
color: var(--text-invalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* diagnostics */
|
|
||||||
|
|
||||||
#diagnostics {
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#diagnostics-bar {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#realization-status {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#realization-status .status {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#realization-status :not(.status) {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#realization-status .status::after {
|
|
||||||
content: '✓';
|
|
||||||
}
|
|
||||||
|
|
||||||
#realization-status.invalid .status::after {
|
|
||||||
content: '⚠';
|
|
||||||
}
|
|
||||||
|
|
||||||
#step-input > label {
|
|
||||||
padding-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#step-input > input {
|
|
||||||
width: 45px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagnostics-panel {
|
|
||||||
margin-top: 10px;
|
|
||||||
min-height: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagnostics-chart {
|
|
||||||
background-color: var(--display-background);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* display */
|
/* display */
|
||||||
|
|
||||||
#display {
|
canvas {
|
||||||
float: left;
|
float: left;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
background-color: var(--display-background);
|
background-color: #020202;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid #555;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#display:focus {
|
canvas:focus {
|
||||||
border-color: var(--border-focus-dark);
|
border-color: #aaa;
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus {
|
|
||||||
border-color: var(--border-focus-light);
|
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
242
app-proto/src/add_remove.rs
Normal file
242
app-proto/src/add_remove.rs
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
use std::collections::BTreeSet; /* DEBUG */
|
||||||
|
use sycamore::prelude::*;
|
||||||
|
use web_sys::{console, wasm_bindgen::JsValue};
|
||||||
|
|
||||||
|
use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}};
|
||||||
|
|
||||||
|
/* DEBUG */
|
||||||
|
fn load_gen_assemb(assembly: &Assembly) {
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element {
|
||||||
|
id: String::from("gemini_a"),
|
||||||
|
label: String::from("Castor"),
|
||||||
|
color: [1.00_f32, 0.25_f32, 0.00_f32],
|
||||||
|
rep: engine::sphere(0.5, 0.5, 0.0, 1.0),
|
||||||
|
constraints: BTreeSet::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element {
|
||||||
|
id: String::from("gemini_b"),
|
||||||
|
label: String::from("Pollux"),
|
||||||
|
color: [0.00_f32, 0.25_f32, 1.00_f32],
|
||||||
|
rep: engine::sphere(-0.5, -0.5, 0.0, 1.0),
|
||||||
|
constraints: BTreeSet::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element {
|
||||||
|
id: String::from("ursa_major"),
|
||||||
|
label: String::from("Ursa major"),
|
||||||
|
color: [0.25_f32, 0.00_f32, 1.00_f32],
|
||||||
|
rep: engine::sphere(-0.5, 0.5, 0.0, 0.75),
|
||||||
|
constraints: BTreeSet::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element {
|
||||||
|
id: String::from("ursa_minor"),
|
||||||
|
label: String::from("Ursa minor"),
|
||||||
|
color: [0.25_f32, 1.00_f32, 0.00_f32],
|
||||||
|
rep: engine::sphere(0.5, -0.5, 0.0, 0.5),
|
||||||
|
constraints: BTreeSet::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element {
|
||||||
|
id: String::from("moon_deimos"),
|
||||||
|
label: String::from("Deimos"),
|
||||||
|
color: [0.75_f32, 0.75_f32, 0.00_f32],
|
||||||
|
rep: engine::sphere(0.0, 0.15, 1.0, 0.25),
|
||||||
|
constraints: BTreeSet::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element {
|
||||||
|
id: String::from("moon_phobos"),
|
||||||
|
label: String::from("Phobos"),
|
||||||
|
color: [0.00_f32, 0.75_f32, 0.50_f32],
|
||||||
|
rep: engine::sphere(0.0, -0.15, -1.0, 0.25),
|
||||||
|
constraints: BTreeSet::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assembly.insert_constraint(
|
||||||
|
Constraint {
|
||||||
|
args: (
|
||||||
|
assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_a"]),
|
||||||
|
assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_b"])
|
||||||
|
),
|
||||||
|
rep: 0.5,
|
||||||
|
active: create_signal(true)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DEBUG */
|
||||||
|
fn load_low_curv_assemb(assembly: &Assembly) {
|
||||||
|
let a = 0.75_f64.sqrt();
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element {
|
||||||
|
id: "central".to_string(),
|
||||||
|
label: "Central".to_string(),
|
||||||
|
color: [0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
|
rep: engine::sphere(0.0, 0.0, 0.0, 1.0),
|
||||||
|
constraints: BTreeSet::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element {
|
||||||
|
id: "assemb_plane".to_string(),
|
||||||
|
label: "Assembly plane".to_string(),
|
||||||
|
color: [0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
|
rep: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0),
|
||||||
|
constraints: BTreeSet::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element {
|
||||||
|
id: "side1".to_string(),
|
||||||
|
label: "Side 1".to_string(),
|
||||||
|
color: [1.00_f32, 0.00_f32, 0.25_f32],
|
||||||
|
rep: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0),
|
||||||
|
constraints: BTreeSet::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element {
|
||||||
|
id: "side2".to_string(),
|
||||||
|
label: "Side 2".to_string(),
|
||||||
|
color: [0.25_f32, 1.00_f32, 0.00_f32],
|
||||||
|
rep: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0),
|
||||||
|
constraints: BTreeSet::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element {
|
||||||
|
id: "side3".to_string(),
|
||||||
|
label: "Side 3".to_string(),
|
||||||
|
color: [0.00_f32, 0.25_f32, 1.00_f32],
|
||||||
|
rep: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0),
|
||||||
|
constraints: BTreeSet::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element {
|
||||||
|
id: "corner1".to_string(),
|
||||||
|
label: "Corner 1".to_string(),
|
||||||
|
color: [0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
|
rep: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0),
|
||||||
|
constraints: BTreeSet::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element {
|
||||||
|
id: "corner2".to_string(),
|
||||||
|
label: "Corner 2".to_string(),
|
||||||
|
color: [0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
|
rep: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0),
|
||||||
|
constraints: BTreeSet::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Element {
|
||||||
|
id: String::from("corner3"),
|
||||||
|
label: String::from("Corner 3"),
|
||||||
|
color: [0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
|
rep: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0),
|
||||||
|
constraints: BTreeSet::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn AddRemove() -> View {
|
||||||
|
/* DEBUG */
|
||||||
|
let assembly_name = create_signal("general".to_string());
|
||||||
|
create_effect(move || {
|
||||||
|
// get name of chosen assembly
|
||||||
|
let name = assembly_name.get_clone();
|
||||||
|
console::log_1(
|
||||||
|
&JsValue::from(format!("Showing assembly \"{}\"", name.clone()))
|
||||||
|
);
|
||||||
|
|
||||||
|
batch(|| {
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
let assembly = &state.assembly;
|
||||||
|
|
||||||
|
// clear state
|
||||||
|
assembly.elements.update(|elts| elts.clear());
|
||||||
|
assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear());
|
||||||
|
state.selection.update(|sel| sel.clear());
|
||||||
|
|
||||||
|
// load assembly
|
||||||
|
match name.as_str() {
|
||||||
|
"general" => load_gen_assemb(assembly),
|
||||||
|
"low-curv" => load_low_curv_assemb(assembly),
|
||||||
|
_ => ()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
div(id="add-remove") {
|
||||||
|
button(
|
||||||
|
on:click=|_| {
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
state.assembly.insert_new_element();
|
||||||
|
|
||||||
|
/* DEBUG */
|
||||||
|
// print updated list of elements by identifier
|
||||||
|
console::log_1(&JsValue::from("elements by identifier:"));
|
||||||
|
for (id, key) in state.assembly.elements_by_id.get_clone().iter() {
|
||||||
|
console::log_3(
|
||||||
|
&JsValue::from(" "),
|
||||||
|
&JsValue::from(id),
|
||||||
|
&JsValue::from(*key)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { "+" }
|
||||||
|
button(
|
||||||
|
disabled={
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
state.selection.with(|sel| sel.len() != 2)
|
||||||
|
},
|
||||||
|
on:click=|_| {
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
let args = state.selection.with(
|
||||||
|
|sel| {
|
||||||
|
let arg_vec: Vec<_> = sel.into_iter().collect();
|
||||||
|
(arg_vec[0].clone(), arg_vec[1].clone())
|
||||||
|
}
|
||||||
|
);
|
||||||
|
state.assembly.insert_constraint(Constraint {
|
||||||
|
args: args,
|
||||||
|
rep: 0.0,
|
||||||
|
active: create_signal(true)
|
||||||
|
});
|
||||||
|
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.args.0),
|
||||||
|
&JsValue::from(cst.args.1),
|
||||||
|
&JsValue::from(":"),
|
||||||
|
&JsValue::from(cst.rep)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
) { "🔗" }
|
||||||
|
select(bind:value=assembly_name) { /* DEBUG */
|
||||||
|
option(value="general") { "General" }
|
||||||
|
option(value="low-curv") { "Low-curvature" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +0,0 @@
|
||||||
pub mod add_remove;
|
|
||||||
pub mod diagnostics;
|
|
||||||
pub mod display;
|
|
||||||
pub mod outline;
|
|
||||||
pub mod test_assembly_chooser;
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
use std::rc::Rc;
|
|
||||||
use sycamore::prelude::*;
|
|
||||||
|
|
||||||
use super::test_assembly_chooser::TestAssemblyChooser;
|
|
||||||
use crate::{
|
|
||||||
AppState,
|
|
||||||
assembly::{InversiveDistanceRegulator, Point, Sphere},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn AddRemove() -> View {
|
|
||||||
view! {
|
|
||||||
div(id = "add-remove") {
|
|
||||||
button(
|
|
||||||
on:click = |_| {
|
|
||||||
let state = use_context::<AppState>();
|
|
||||||
batch(|| {
|
|
||||||
// this call is batched to avoid redundant realizations.
|
|
||||||
// it updates the element list and the regulator list,
|
|
||||||
// which are both tracked by the realization effect
|
|
||||||
/* TO DO */
|
|
||||||
// it would make more to do the batching inside
|
|
||||||
// `insert_element_default`, but that will have to wait
|
|
||||||
// until Sycamore handles nested batches correctly.
|
|
||||||
//
|
|
||||||
// https://github.com/sycamore-rs/sycamore/issues/802
|
|
||||||
//
|
|
||||||
// the nested batch issue is relevant here because the
|
|
||||||
// assembly loaders in the test assembly chooser use
|
|
||||||
// `insert_element_default` within larger batches
|
|
||||||
state.assembly.insert_element_default::<Sphere>();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
) { "Add sphere" }
|
|
||||||
button(
|
|
||||||
on:click = |_| {
|
|
||||||
let state = use_context::<AppState>();
|
|
||||||
state.assembly.insert_element_default::<Point>();
|
|
||||||
}
|
|
||||||
) { "Add point" }
|
|
||||||
button(
|
|
||||||
/* KLUDGE */ // for convenience, we're using an emoji as an
|
|
||||||
// icon for this button
|
|
||||||
class = "emoji",
|
|
||||||
disabled = {
|
|
||||||
let state = use_context::<AppState>();
|
|
||||||
state.selection.with(|sel| sel.len() != 2)
|
|
||||||
},
|
|
||||||
on:click = |_| {
|
|
||||||
let state = use_context::<AppState>();
|
|
||||||
let subjects: [_; 2] = state.selection.with(
|
|
||||||
// the button is only enabled when two elements are
|
|
||||||
// selected, so we know the cast to a two-element array
|
|
||||||
// will succeed
|
|
||||||
|sel| sel
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.try_into()
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
state.assembly.insert_regulator(
|
|
||||||
Rc::new(InversiveDistanceRegulator::new(subjects))
|
|
||||||
);
|
|
||||||
state.selection.update(|sel| sel.clear());
|
|
||||||
}
|
|
||||||
) { "🔗" }
|
|
||||||
TestAssemblyChooser {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,320 +0,0 @@
|
||||||
use charming::{
|
|
||||||
Chart,
|
|
||||||
WasmRenderer,
|
|
||||||
component::{Axis, DataZoom, Grid},
|
|
||||||
element::{AxisType, Symbol},
|
|
||||||
series::{Line, Scatter},
|
|
||||||
};
|
|
||||||
use sycamore::prelude::*;
|
|
||||||
|
|
||||||
use crate::{AppState, specified::SpecifiedValue};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct DiagnosticsState {
|
|
||||||
active_tab: Signal<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DiagnosticsState {
|
|
||||||
fn new(initial_tab: String) -> Self {
|
|
||||||
Self { active_tab: create_signal(initial_tab) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// a realization status indicator
|
|
||||||
#[component]
|
|
||||||
fn RealizationStatus() -> View {
|
|
||||||
let state = use_context::<AppState>();
|
|
||||||
let realization_status = state.assembly.realization_status;
|
|
||||||
view! {
|
|
||||||
div(
|
|
||||||
id = "realization-status",
|
|
||||||
class = realization_status.with(
|
|
||||||
|status| match status {
|
|
||||||
Ok(_) => "",
|
|
||||||
Err(_) => "invalid",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
div(class = "status")
|
|
||||||
div {
|
|
||||||
(realization_status.with(
|
|
||||||
|status| match status {
|
|
||||||
Ok(_) => "Target accuracy achieved".to_string(),
|
|
||||||
Err(message) => message.clone(),
|
|
||||||
}
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// history step input
|
|
||||||
#[component]
|
|
||||||
fn StepInput() -> View {
|
|
||||||
// get the assembly
|
|
||||||
let state = use_context::<AppState>();
|
|
||||||
let assembly = state.assembly;
|
|
||||||
|
|
||||||
// the `last_step` signal holds the index of the last step
|
|
||||||
let last_step = assembly.descent_history.map(
|
|
||||||
|history| match history.config.len() {
|
|
||||||
0 => None,
|
|
||||||
n => Some(n - 1),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let input_max = last_step.map(|last| last.unwrap_or(0));
|
|
||||||
|
|
||||||
// these signals hold the entered step number
|
|
||||||
let value = create_signal(String::new());
|
|
||||||
let value_as_number = create_signal(0.0);
|
|
||||||
|
|
||||||
create_effect(move || {
|
|
||||||
value.set(assembly.step.with(|n| n.spec.clone()));
|
|
||||||
});
|
|
||||||
|
|
||||||
view! {
|
|
||||||
div(id = "step-input") {
|
|
||||||
label { "Step" }
|
|
||||||
input(
|
|
||||||
r#type = "number",
|
|
||||||
min = "0",
|
|
||||||
max = input_max.with(|max| max.to_string()),
|
|
||||||
bind:value = value,
|
|
||||||
bind:valueAsNumber = value_as_number,
|
|
||||||
on:change = move |_| {
|
|
||||||
if last_step.with(|last| last.is_some()) {
|
|
||||||
// clamp the step within its allowed range. the lower
|
|
||||||
// bound is redundant on browsers that make it
|
|
||||||
// impossible to type negative values into a number
|
|
||||||
// input with a non-negative `min`, but there's no harm
|
|
||||||
// in being careful
|
|
||||||
let step_raw = value.with(
|
|
||||||
|val| SpecifiedValue::try_from(val.clone())
|
|
||||||
.unwrap_or(SpecifiedValue::from_empty_spec()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
let step = SpecifiedValue::from(
|
|
||||||
step_raw.value.map(
|
|
||||||
|val| val.clamp(0.0, input_max.get() as f64)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// set the input string and the assembly's active step
|
|
||||||
value.set(step.spec.clone());
|
|
||||||
assembly.step.set(step);
|
|
||||||
} else {
|
|
||||||
value.set(String::new());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_log10_time_point((step, value): (usize, f64)) -> Vec<Option<f64>> {
|
|
||||||
vec![
|
|
||||||
Some(step as f64),
|
|
||||||
if value == 0.0 { None } else { Some(value.abs().log10()) },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// the loss history from the last realization
|
|
||||||
#[component]
|
|
||||||
fn LossHistory() -> View {
|
|
||||||
const CONTAINER_ID: &str = "loss-history";
|
|
||||||
let state = use_context::<AppState>();
|
|
||||||
let renderer = WasmRenderer::new_opt(None, Some(178));
|
|
||||||
|
|
||||||
on_mount(move || {
|
|
||||||
create_effect(move || {
|
|
||||||
// get the loss history
|
|
||||||
let scaled_loss: Vec<_> = state.assembly.descent_history.with(
|
|
||||||
|history| history.scaled_loss
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(step, &loss)| (step, loss))
|
|
||||||
.map(into_log10_time_point)
|
|
||||||
.collect()
|
|
||||||
);
|
|
||||||
|
|
||||||
// initialize the chart axes
|
|
||||||
let step_axis = Axis::new()
|
|
||||||
.type_(AxisType::Category)
|
|
||||||
.boundary_gap(false);
|
|
||||||
let scaled_loss_axis = Axis::new();
|
|
||||||
|
|
||||||
// load the chart data. when there's no history, we load the data
|
|
||||||
// point (0, None) to clear the chart. it would feel more natural to
|
|
||||||
// load empty data vectors, but that turns out not to clear the
|
|
||||||
// chart: it instead leads to previous data being re-used
|
|
||||||
let scaled_loss_series = Line::new().data(
|
|
||||||
if scaled_loss.len() > 0 {
|
|
||||||
scaled_loss
|
|
||||||
} else {
|
|
||||||
vec![vec![Some(0.0), None::<f64>]]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let chart = Chart::new()
|
|
||||||
.animation(false)
|
|
||||||
.data_zoom(DataZoom::new().y_axis_index(0).right(40))
|
|
||||||
.x_axis(step_axis)
|
|
||||||
.y_axis(scaled_loss_axis)
|
|
||||||
.grid(Grid::new().top(20).right(80).bottom(30).left(60))
|
|
||||||
.series(scaled_loss_series);
|
|
||||||
renderer.render(CONTAINER_ID, &chart).unwrap();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
view! {
|
|
||||||
div(id = CONTAINER_ID, class = "diagnostics-chart")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// the spectrum of the Hessian during the last realization
|
|
||||||
#[component]
|
|
||||||
fn SpectrumHistory() -> View {
|
|
||||||
const CONTAINER_ID: &str = "spectrum-history";
|
|
||||||
let state = use_context::<AppState>();
|
|
||||||
let renderer = WasmRenderer::new(478, 178);
|
|
||||||
|
|
||||||
on_mount(move || {
|
|
||||||
create_effect(move || {
|
|
||||||
// get the spectrum of the Hessian at each step, split into its
|
|
||||||
// positive, negative, and strictly-zero parts
|
|
||||||
let (
|
|
||||||
hess_eigvals_zero,
|
|
||||||
hess_eigvals_nonzero,
|
|
||||||
): (Vec<_>, Vec<_>) = state.assembly.descent_history.with(
|
|
||||||
|history| history.hess_eigvals
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(
|
|
||||||
|(step, eigvals)| eigvals.iter().map(
|
|
||||||
move |&val| (step, val)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.flatten()
|
|
||||||
.partition(|&(_, val)| val == 0.0)
|
|
||||||
);
|
|
||||||
let zero_level = hess_eigvals_nonzero
|
|
||||||
.iter()
|
|
||||||
.map(|(_, val)| val.abs())
|
|
||||||
.reduce(f64::min)
|
|
||||||
.map(|val| 0.1 * val)
|
|
||||||
.unwrap_or(1.0);
|
|
||||||
let (
|
|
||||||
hess_eigvals_pos,
|
|
||||||
hess_eigvals_neg,
|
|
||||||
): (Vec<_>, Vec<_>) = hess_eigvals_nonzero
|
|
||||||
.into_iter()
|
|
||||||
.partition(|&(_, val)| val > 0.0);
|
|
||||||
|
|
||||||
// initialize the chart axes
|
|
||||||
let step_axis = Axis::new()
|
|
||||||
.type_(AxisType::Category)
|
|
||||||
.boundary_gap(false);
|
|
||||||
let eigval_axis = Axis::new();
|
|
||||||
|
|
||||||
// load the chart data. when there's no history, we load the data
|
|
||||||
// point (0, None) to clear the chart. it would feel more natural to
|
|
||||||
// load empty data vectors, but that turns out not to clear the
|
|
||||||
// chart: it instead leads to previous data being re-used
|
|
||||||
let eigval_series_pos = Scatter::new()
|
|
||||||
.symbol_size(4.5)
|
|
||||||
.data(
|
|
||||||
if hess_eigvals_pos.len() > 0 {
|
|
||||||
hess_eigvals_pos
|
|
||||||
.into_iter()
|
|
||||||
.map(into_log10_time_point)
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
vec![vec![Some(0.0), None::<f64>]]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let eigval_series_neg = Scatter::new()
|
|
||||||
.symbol(Symbol::Diamond)
|
|
||||||
.symbol_size(6.0)
|
|
||||||
.data(
|
|
||||||
if hess_eigvals_neg.len() > 0 {
|
|
||||||
hess_eigvals_neg
|
|
||||||
.into_iter()
|
|
||||||
.map(into_log10_time_point)
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
vec![vec![Some(0.0), None::<f64>]]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let eigval_series_zero = Scatter::new()
|
|
||||||
.symbol(Symbol::Triangle)
|
|
||||||
.symbol_size(5.0)
|
|
||||||
.data(
|
|
||||||
if hess_eigvals_zero.len() > 0 {
|
|
||||||
hess_eigvals_zero
|
|
||||||
.into_iter()
|
|
||||||
.map(|(step, _)| (step, zero_level))
|
|
||||||
.map(into_log10_time_point)
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
vec![vec![Some(0.0), None::<f64>]]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let chart = Chart::new()
|
|
||||||
.animation(false)
|
|
||||||
.data_zoom(DataZoom::new().y_axis_index(0).right(40))
|
|
||||||
.x_axis(step_axis)
|
|
||||||
.y_axis(eigval_axis)
|
|
||||||
.grid(Grid::new().top(20).right(80).bottom(30).left(60))
|
|
||||||
.series(eigval_series_pos)
|
|
||||||
.series(eigval_series_neg)
|
|
||||||
.series(eigval_series_zero);
|
|
||||||
renderer.render(CONTAINER_ID, &chart).unwrap();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
view! {
|
|
||||||
div(id = CONTAINER_ID, class = "diagnostics-chart")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component(inline_props)]
|
|
||||||
fn DiagnosticsPanel(name: &'static str, children: Children) -> View {
|
|
||||||
let diagnostics_state = use_context::<DiagnosticsState>();
|
|
||||||
view! {
|
|
||||||
div(
|
|
||||||
class = "diagnostics-panel",
|
|
||||||
"hidden" = diagnostics_state.active_tab.with(
|
|
||||||
|active_tab| {
|
|
||||||
if active_tab == name {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
(children)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Diagnostics() -> View {
|
|
||||||
let diagnostics_state = DiagnosticsState::new("loss".to_string());
|
|
||||||
let active_tab = diagnostics_state.active_tab.clone();
|
|
||||||
provide_context(diagnostics_state);
|
|
||||||
|
|
||||||
view! {
|
|
||||||
div(id = "diagnostics") {
|
|
||||||
div(id = "diagnostics-bar") {
|
|
||||||
RealizationStatus {}
|
|
||||||
select(bind:value = active_tab) {
|
|
||||||
option(value = "loss") { "Loss" }
|
|
||||||
option(value = "spectrum") { "Spectrum" }
|
|
||||||
}
|
|
||||||
StepInput {}
|
|
||||||
}
|
|
||||||
DiagnosticsPanel(name = "loss") { LossHistory {} }
|
|
||||||
DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,991 +0,0 @@
|
||||||
use core::array;
|
|
||||||
use nalgebra::{DMatrix, DVector, Rotation3, Vector3};
|
|
||||||
use std::rc::Rc;
|
|
||||||
use sycamore::{prelude::*, motion::create_raf};
|
|
||||||
use web_sys::{
|
|
||||||
console,
|
|
||||||
window,
|
|
||||||
KeyboardEvent,
|
|
||||||
MouseEvent,
|
|
||||||
WebGl2RenderingContext,
|
|
||||||
WebGlBuffer,
|
|
||||||
WebGlProgram,
|
|
||||||
WebGlShader,
|
|
||||||
WebGlUniformLocation,
|
|
||||||
wasm_bindgen::{JsCast, JsValue},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
AppState,
|
|
||||||
assembly::{Element, ElementColor, ElementMotion, Point, Sphere},
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- color ---
|
|
||||||
|
|
||||||
const COLOR_SIZE: usize = 3;
|
|
||||||
type ColorWithOpacity = [f32; COLOR_SIZE + 1];
|
|
||||||
|
|
||||||
fn combine_channels(color: ElementColor, opacity: f32) -> ColorWithOpacity {
|
|
||||||
let mut color_with_opacity = [0.0; COLOR_SIZE + 1];
|
|
||||||
color_with_opacity[..COLOR_SIZE].copy_from_slice(&color);
|
|
||||||
color_with_opacity[COLOR_SIZE] = opacity;
|
|
||||||
color_with_opacity
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- scene data ---
|
|
||||||
|
|
||||||
struct SceneSpheres {
|
|
||||||
representations: Vec<DVector<f64>>,
|
|
||||||
colors_with_opacity: Vec<ColorWithOpacity>,
|
|
||||||
highlights: Vec<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SceneSpheres {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
representations: Vec::new(),
|
|
||||||
colors_with_opacity: Vec::new(),
|
|
||||||
highlights: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn len_i32(&self) -> i32 {
|
|
||||||
self.representations.len().try_into()
|
|
||||||
.expect("Number of spheres must fit in a 32-bit integer")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push(
|
|
||||||
&mut self, representation: DVector<f64>,
|
|
||||||
color: ElementColor, opacity: f32, highlight: f32,
|
|
||||||
) {
|
|
||||||
self.representations.push(representation);
|
|
||||||
self.colors_with_opacity.push(combine_channels(color, opacity));
|
|
||||||
self.highlights.push(highlight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ScenePoints {
|
|
||||||
representations: Vec<DVector<f64>>,
|
|
||||||
colors_with_opacity: Vec<ColorWithOpacity>,
|
|
||||||
highlights: Vec<f32>,
|
|
||||||
selections: Vec<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScenePoints {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
representations: Vec::new(),
|
|
||||||
colors_with_opacity: Vec::new(),
|
|
||||||
highlights: Vec::new(),
|
|
||||||
selections: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push(
|
|
||||||
&mut self, representation: DVector<f64>,
|
|
||||||
color: ElementColor, opacity: f32, highlight: f32, selected: bool,
|
|
||||||
) {
|
|
||||||
self.representations.push(representation);
|
|
||||||
self.colors_with_opacity.push(combine_channels(color, opacity));
|
|
||||||
self.highlights.push(highlight);
|
|
||||||
self.selections.push(if selected { 1.0 } else { 0.0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Scene {
|
|
||||||
spheres: SceneSpheres,
|
|
||||||
points: ScenePoints,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Scene {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
spheres: SceneSpheres::new(),
|
|
||||||
points: ScenePoints::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait DisplayItem {
|
|
||||||
fn show(&self, scene: &mut Scene, selected: bool);
|
|
||||||
|
|
||||||
// the smallest positive depth, represented as a multiple of `dir`, where
|
|
||||||
// the line generated by `dir` hits the element. returns `None` if the line
|
|
||||||
// misses the element
|
|
||||||
fn cast(
|
|
||||||
&self,
|
|
||||||
dir: Vector3<f64>,
|
|
||||||
assembly_to_world: &DMatrix<f64>,
|
|
||||||
pixel_size: f64,
|
|
||||||
) -> Option<f64>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DisplayItem for Sphere {
|
|
||||||
fn show(&self, scene: &mut Scene, selected: bool) {
|
|
||||||
/* SCAFFOLDING */
|
|
||||||
const DEFAULT_OPACITY: f32 = 0.5;
|
|
||||||
const GHOST_OPACITY: f32 = 0.2;
|
|
||||||
const HIGHLIGHT: f32 = 0.2;
|
|
||||||
|
|
||||||
let representation = self.representation.get_clone_untracked();
|
|
||||||
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) }
|
|
||||||
else { self.color };
|
|
||||||
let opacity = if self.ghost.get() { GHOST_OPACITY }
|
|
||||||
else { DEFAULT_OPACITY };
|
|
||||||
let highlight = if selected { 1.0 } else { HIGHLIGHT };
|
|
||||||
scene.spheres.push(representation, color, opacity, highlight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// this method should be kept synchronized with `sphere_cast` in
|
|
||||||
// `spheres.frag`, which does essentially the same thing on the GPU side
|
|
||||||
fn cast(
|
|
||||||
&self,
|
|
||||||
dir: Vector3<f64>,
|
|
||||||
assembly_to_world: &DMatrix<f64>,
|
|
||||||
_pixel_size: f64,
|
|
||||||
) -> Option<f64> {
|
|
||||||
// if `a/b` is less than this threshold, we approximate
|
|
||||||
// `a*u^2 + b*u + c` by the linear function `b*u + c`
|
|
||||||
const DEG_THRESHOLD: f64 = 1e-9;
|
|
||||||
|
|
||||||
let rep = self.representation
|
|
||||||
.with_untracked(|rep| assembly_to_world * rep);
|
|
||||||
let a = -rep[3] * dir.norm_squared();
|
|
||||||
let b = rep.rows_range(..3).dot(&dir);
|
|
||||||
let c = -rep[4];
|
|
||||||
|
|
||||||
let adjust = 4.0*a*c/(b*b);
|
|
||||||
if adjust < 1.0 {
|
|
||||||
// as long as `b` is non-zero, the linear approximation of
|
|
||||||
//
|
|
||||||
// a*u^2 + b*u + c
|
|
||||||
//
|
|
||||||
// at `u = 0` will reach zero at a finite depth `u_lin`. the root of
|
|
||||||
// the quadratic adjacent to `u_lin` is stored in `lin_root`. if
|
|
||||||
// both roots have the same sign, `lin_root` will be the one closer
|
|
||||||
// to `u = 0`
|
|
||||||
let square_rect_ratio = 1.0 + (1.0 - adjust).sqrt();
|
|
||||||
let lin_root = -(2.0*c)/b / square_rect_ratio;
|
|
||||||
if a.abs() > DEG_THRESHOLD * b.abs() {
|
|
||||||
if lin_root > 0.0 {
|
|
||||||
Some(lin_root)
|
|
||||||
} else {
|
|
||||||
let other_root = -b/(2.*a) * square_rect_ratio;
|
|
||||||
(other_root > 0.0).then_some(other_root)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(lin_root > 0.0).then_some(lin_root)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// the line through `dir` misses the sphere completely
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DisplayItem for Point {
|
|
||||||
fn show(&self, scene: &mut Scene, selected: bool) {
|
|
||||||
/* SCAFFOLDING */
|
|
||||||
const GHOST_OPACITY: f32 = 0.4;
|
|
||||||
const HIGHLIGHT: f32 = 0.5;
|
|
||||||
|
|
||||||
let representation = self.representation.get_clone_untracked();
|
|
||||||
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) }
|
|
||||||
else { self.color };
|
|
||||||
let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 };
|
|
||||||
let highlight = if selected { 1.0 } else { HIGHLIGHT };
|
|
||||||
scene.points.push(representation, color, opacity, highlight, selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* SCAFFOLDING */
|
|
||||||
fn cast(
|
|
||||||
&self,
|
|
||||||
dir: Vector3<f64>,
|
|
||||||
assembly_to_world: &DMatrix<f64>,
|
|
||||||
pixel_size: f64,
|
|
||||||
) -> Option<f64> {
|
|
||||||
let rep = self.representation
|
|
||||||
.with_untracked(|rep| assembly_to_world * rep);
|
|
||||||
if rep[2] < 0.0 {
|
|
||||||
// this constant should be kept synchronized with `point.frag`
|
|
||||||
const POINT_RADIUS_PX: f64 = 4.0;
|
|
||||||
|
|
||||||
// find the radius of the point in screen projection units
|
|
||||||
let point_radius_proj = POINT_RADIUS_PX * pixel_size;
|
|
||||||
|
|
||||||
// find the squared distance between the screen projections of the
|
|
||||||
// ray and the point
|
|
||||||
let dir_proj = -dir.fixed_rows::<2>(0) / dir[2];
|
|
||||||
let rep_proj = -rep.fixed_rows::<2>(0) / rep[2];
|
|
||||||
let dist_sq = (dir_proj - rep_proj).norm_squared();
|
|
||||||
|
|
||||||
// if the ray hits the point, return its depth
|
|
||||||
if dist_sq < point_radius_proj * point_radius_proj {
|
|
||||||
Some(rep[2] / dir[2])
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- WebGL utilities ---
|
|
||||||
|
|
||||||
fn compile_shader(
|
|
||||||
context: &WebGl2RenderingContext,
|
|
||||||
shader_type: u32,
|
|
||||||
source: &str,
|
|
||||||
) -> WebGlShader {
|
|
||||||
let shader = context.create_shader(shader_type).unwrap();
|
|
||||||
context.shader_source(&shader, source);
|
|
||||||
context.compile_shader(&shader);
|
|
||||||
shader
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_up_program(
|
|
||||||
context: &WebGl2RenderingContext,
|
|
||||||
vertex_shader_source: &str,
|
|
||||||
fragment_shader_source: &str,
|
|
||||||
) -> WebGlProgram {
|
|
||||||
// compile the shaders
|
|
||||||
let vertex_shader = compile_shader(
|
|
||||||
&context,
|
|
||||||
WebGl2RenderingContext::VERTEX_SHADER,
|
|
||||||
vertex_shader_source,
|
|
||||||
);
|
|
||||||
let fragment_shader = compile_shader(
|
|
||||||
&context,
|
|
||||||
WebGl2RenderingContext::FRAGMENT_SHADER,
|
|
||||||
fragment_shader_source,
|
|
||||||
);
|
|
||||||
|
|
||||||
// create the program and attach the shaders
|
|
||||||
let program = context.create_program().unwrap();
|
|
||||||
context.attach_shader(&program, &vertex_shader);
|
|
||||||
context.attach_shader(&program, &fragment_shader);
|
|
||||||
context.link_program(&program);
|
|
||||||
|
|
||||||
/* DEBUG */
|
|
||||||
// report whether linking succeeded
|
|
||||||
let link_status = context
|
|
||||||
.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
|
|
||||||
.as_bool()
|
|
||||||
.unwrap();
|
|
||||||
let link_msg = if link_status {
|
|
||||||
"Linked successfully"
|
|
||||||
} else {
|
|
||||||
"Linking failed"
|
|
||||||
};
|
|
||||||
console::log_1(&JsValue::from(link_msg));
|
|
||||||
|
|
||||||
program
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_uniform_array_locations<const N: usize>(
|
|
||||||
context: &WebGl2RenderingContext,
|
|
||||||
program: &WebGlProgram,
|
|
||||||
var_name: &str,
|
|
||||||
member_name_opt: Option<&str>,
|
|
||||||
) -> [Option<WebGlUniformLocation>; N] {
|
|
||||||
array::from_fn(|n| {
|
|
||||||
let name = match member_name_opt {
|
|
||||||
Some(member_name) => format!("{var_name}[{n}].{member_name}"),
|
|
||||||
None => format!("{var_name}[{n}]"),
|
|
||||||
};
|
|
||||||
context.get_uniform_location(&program, name.as_str())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// bind the given vertex buffer object to the given vertex attribute
|
|
||||||
fn bind_to_attribute(
|
|
||||||
context: &WebGl2RenderingContext,
|
|
||||||
attr_index: u32,
|
|
||||||
attr_size: i32,
|
|
||||||
buffer: &Option<WebGlBuffer>,
|
|
||||||
) {
|
|
||||||
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
|
|
||||||
context.vertex_attrib_pointer_with_i32(
|
|
||||||
attr_index,
|
|
||||||
attr_size,
|
|
||||||
WebGl2RenderingContext::FLOAT,
|
|
||||||
false, // don't normalize
|
|
||||||
0, // zero stride
|
|
||||||
0, // zero offset
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// load the given data into a new vertex buffer object
|
|
||||||
fn load_new_buffer(
|
|
||||||
context: &WebGl2RenderingContext,
|
|
||||||
data: &[f32],
|
|
||||||
) -> Option<WebGlBuffer> {
|
|
||||||
// create a buffer and bind it to ARRAY_BUFFER
|
|
||||||
let buffer = context.create_buffer();
|
|
||||||
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
|
|
||||||
|
|
||||||
// load the given data into the buffer. this block is unsafe because
|
|
||||||
// `Float32Array::view` creates a raw view into our module's
|
|
||||||
// `WebAssembly.Memory` buffer. allocating more memory will change the
|
|
||||||
// buffer, invalidating the view, so we have to make sure we don't allocate
|
|
||||||
// any memory until the view is dropped. we're okay here because the view is
|
|
||||||
// used as soon as it's created
|
|
||||||
unsafe {
|
|
||||||
context.buffer_data_with_array_buffer_view(
|
|
||||||
WebGl2RenderingContext::ARRAY_BUFFER,
|
|
||||||
&js_sys::Float32Array::view(&data),
|
|
||||||
WebGl2RenderingContext::STATIC_DRAW,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bind_new_buffer_to_attribute(
|
|
||||||
context: &WebGl2RenderingContext,
|
|
||||||
attr_index: u32,
|
|
||||||
attr_size: i32,
|
|
||||||
data: &[f32],
|
|
||||||
) {
|
|
||||||
let buffer = load_new_buffer(context, data);
|
|
||||||
bind_to_attribute(context, attr_index, attr_size, &buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// the direction in camera space that a mouse event is pointing along
|
|
||||||
fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
|
|
||||||
let target: web_sys::Element = event.target().unwrap().unchecked_into();
|
|
||||||
let rect = target.get_bounding_client_rect();
|
|
||||||
let width = rect.width();
|
|
||||||
let height = rect.height();
|
|
||||||
let shortdim = width.min(height);
|
|
||||||
|
|
||||||
// this constant should be kept synchronized with `spheres.frag` and
|
|
||||||
// `point.vert`
|
|
||||||
const FOCAL_SLOPE: f64 = 0.3;
|
|
||||||
let x_relative = f64::from(event.client_x()) - rect.left();
|
|
||||||
let y_relative = rect.bottom() - f64::from(event.client_y());
|
|
||||||
(
|
|
||||||
Vector3::new(
|
|
||||||
FOCAL_SLOPE * (2.0*x_relative - width) / shortdim,
|
|
||||||
FOCAL_SLOPE * (2.0*y_relative - height) / shortdim,
|
|
||||||
-1.0,
|
|
||||||
),
|
|
||||||
FOCAL_SLOPE * 2.0 / shortdim,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- display component ---
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Display() -> View {
|
|
||||||
let state = use_context::<AppState>();
|
|
||||||
|
|
||||||
// canvas
|
|
||||||
let display = create_node_ref();
|
|
||||||
|
|
||||||
// viewpoint
|
|
||||||
let assembly_to_world = create_signal(DMatrix::<f64>::identity(5, 5));
|
|
||||||
|
|
||||||
// navigation
|
|
||||||
let pitch_up = create_signal(0.0);
|
|
||||||
let pitch_down = create_signal(0.0);
|
|
||||||
let yaw_right = create_signal(0.0);
|
|
||||||
let yaw_left = create_signal(0.0);
|
|
||||||
let roll_ccw = create_signal(0.0);
|
|
||||||
let roll_cw = create_signal(0.0);
|
|
||||||
let zoom_in = create_signal(0.0);
|
|
||||||
let zoom_out = create_signal(0.0);
|
|
||||||
let turntable = create_signal(false); /* BENCHMARKING */
|
|
||||||
|
|
||||||
// manipulation
|
|
||||||
let translate_neg_x = create_signal(0.0);
|
|
||||||
let translate_pos_x = create_signal(0.0);
|
|
||||||
let translate_neg_y = create_signal(0.0);
|
|
||||||
let translate_pos_y = create_signal(0.0);
|
|
||||||
let translate_neg_z = create_signal(0.0);
|
|
||||||
let translate_pos_z = create_signal(0.0);
|
|
||||||
let shrink_neg = create_signal(0.0);
|
|
||||||
let shrink_pos = create_signal(0.0);
|
|
||||||
|
|
||||||
// change listener
|
|
||||||
let scene_changed = create_signal(true);
|
|
||||||
create_effect(move || {
|
|
||||||
state.assembly.elements.with(|elts| {
|
|
||||||
for elt in elts {
|
|
||||||
elt.representation().track();
|
|
||||||
elt.ghost().track();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
state.selection.track();
|
|
||||||
scene_changed.set(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* INSTRUMENTS */
|
|
||||||
const SAMPLE_PERIOD: i32 = 60;
|
|
||||||
let mut last_sample_time = 0.0;
|
|
||||||
let mut frames_since_last_sample = 0;
|
|
||||||
let mean_frame_interval = create_signal(0.0);
|
|
||||||
|
|
||||||
let assembly_for_raf = state.assembly.clone();
|
|
||||||
on_mount(move || {
|
|
||||||
// timing
|
|
||||||
let mut last_time = 0.0;
|
|
||||||
|
|
||||||
// viewpoint
|
|
||||||
const ROT_SPEED: f64 = 0.4; // in radians per second
|
|
||||||
const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second
|
|
||||||
const TURNTABLE_SPEED: f64 = 0.1; /* BENCHMARKING */
|
|
||||||
let mut orientation = DMatrix::<f64>::identity(5, 5);
|
|
||||||
let mut rotation = DMatrix::<f64>::identity(5, 5);
|
|
||||||
let mut location_z: f64 = 5.0;
|
|
||||||
|
|
||||||
// manipulation
|
|
||||||
const TRANSLATION_SPEED: f64 = 0.15; // in length units per second
|
|
||||||
const SHRINKING_SPEED: f64 = 0.15; // in length units per second
|
|
||||||
|
|
||||||
// display parameters
|
|
||||||
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
|
|
||||||
const DEBUG_MODE: i32 = 0; /* DEBUG */
|
|
||||||
|
|
||||||
/* INSTRUMENTS */
|
|
||||||
let performance = window().unwrap().performance().unwrap();
|
|
||||||
|
|
||||||
// get the display canvas
|
|
||||||
let canvas
|
|
||||||
= display.get().unchecked_into::<web_sys::HtmlCanvasElement>();
|
|
||||||
let ctx = canvas
|
|
||||||
.get_context("webgl2")
|
|
||||||
.unwrap()
|
|
||||||
.unwrap()
|
|
||||||
.dyn_into::<WebGl2RenderingContext>()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// disable depth testing
|
|
||||||
ctx.disable(WebGl2RenderingContext::DEPTH_TEST);
|
|
||||||
|
|
||||||
// set blend mode
|
|
||||||
ctx.enable(WebGl2RenderingContext::BLEND);
|
|
||||||
ctx.blend_func(
|
|
||||||
WebGl2RenderingContext::SRC_ALPHA,
|
|
||||||
WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA,
|
|
||||||
);
|
|
||||||
|
|
||||||
// set up the sphere rendering program
|
|
||||||
let sphere_program = set_up_program(
|
|
||||||
&ctx,
|
|
||||||
include_str!("identity.vert"),
|
|
||||||
include_str!("spheres.frag"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// set up the point rendering program
|
|
||||||
let point_program = set_up_program(
|
|
||||||
&ctx,
|
|
||||||
include_str!("point.vert"),
|
|
||||||
include_str!("point.frag"),
|
|
||||||
);
|
|
||||||
|
|
||||||
/* DEBUG */
|
|
||||||
// print the maximum number of vectors that can be passed as
|
|
||||||
// uniforms to a fragment shader. the OpenGL ES 3.0 standard
|
|
||||||
// requires this maximum to be at least 224, as discussed in the
|
|
||||||
// documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter
|
|
||||||
// here:
|
|
||||||
//
|
|
||||||
// https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml
|
|
||||||
//
|
|
||||||
// there are also other size limits. for example, on Aaron's
|
|
||||||
// machine, the the length of a float or genType array seems to be
|
|
||||||
// capped at 1024 elements
|
|
||||||
console::log_2(
|
|
||||||
&ctx.get_parameter(
|
|
||||||
WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS
|
|
||||||
).unwrap(),
|
|
||||||
&JsValue::from("uniform vectors available"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// find the sphere program's vertex attribute
|
|
||||||
let viewport_position_attr = ctx
|
|
||||||
.get_attrib_location(&sphere_program, "position") as u32;
|
|
||||||
|
|
||||||
// find the sphere program's uniforms
|
|
||||||
const SPHERE_MAX: usize = 200;
|
|
||||||
let sphere_cnt_loc = ctx
|
|
||||||
.get_uniform_location(&sphere_program, "sphere_cnt");
|
|
||||||
let sphere_sp_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
|
||||||
&ctx, &sphere_program, "sphere_list", Some("sp")
|
|
||||||
);
|
|
||||||
let sphere_lt_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
|
||||||
&ctx, &sphere_program, "sphere_list", Some("lt")
|
|
||||||
);
|
|
||||||
let sphere_color_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
|
||||||
&ctx, &sphere_program, "color_list", None
|
|
||||||
);
|
|
||||||
let sphere_highlight_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
|
||||||
&ctx, &sphere_program, "highlight_list", None
|
|
||||||
);
|
|
||||||
let resolution_loc = ctx
|
|
||||||
.get_uniform_location(&sphere_program, "resolution");
|
|
||||||
let shortdim_loc = ctx
|
|
||||||
.get_uniform_location(&sphere_program, "shortdim");
|
|
||||||
let layer_threshold_loc = ctx
|
|
||||||
.get_uniform_location(&sphere_program, "layer_threshold");
|
|
||||||
let debug_mode_loc = ctx
|
|
||||||
.get_uniform_location(&sphere_program, "debug_mode");
|
|
||||||
|
|
||||||
// load the viewport vertex positions into a new vertex buffer object
|
|
||||||
const VERTEX_CNT: usize = 6;
|
|
||||||
let viewport_positions: [f32; 3*VERTEX_CNT] = [
|
|
||||||
// northwest triangle
|
|
||||||
-1.0, -1.0, 0.0,
|
|
||||||
-1.0, 1.0, 0.0,
|
|
||||||
1.0, 1.0, 0.0,
|
|
||||||
// southeast triangle
|
|
||||||
-1.0, -1.0, 0.0,
|
|
||||||
1.0, 1.0, 0.0,
|
|
||||||
1.0, -1.0, 0.0,
|
|
||||||
];
|
|
||||||
let viewport_position_buffer
|
|
||||||
= load_new_buffer(&ctx, &viewport_positions);
|
|
||||||
|
|
||||||
// find the point program's vertex attributes
|
|
||||||
let point_position_attr = ctx
|
|
||||||
.get_attrib_location(&point_program, "position") as u32;
|
|
||||||
let point_color_attr = ctx
|
|
||||||
.get_attrib_location(&point_program, "color") as u32;
|
|
||||||
let point_highlight_attr = ctx
|
|
||||||
.get_attrib_location(&point_program, "highlight") as u32;
|
|
||||||
let point_selection_attr = ctx
|
|
||||||
.get_attrib_location(&point_program, "selected") as u32;
|
|
||||||
|
|
||||||
// set up a repainting routine
|
|
||||||
let (_, start_animation_loop, _) = create_raf(move || {
|
|
||||||
// get the time step
|
|
||||||
let time = performance.now();
|
|
||||||
let time_step = 0.001*(time - last_time);
|
|
||||||
last_time = time;
|
|
||||||
|
|
||||||
// get the navigation state
|
|
||||||
let pitch_up_val = pitch_up.get();
|
|
||||||
let pitch_down_val = pitch_down.get();
|
|
||||||
let yaw_right_val = yaw_right.get();
|
|
||||||
let yaw_left_val = yaw_left.get();
|
|
||||||
let roll_ccw_val = roll_ccw.get();
|
|
||||||
let roll_cw_val = roll_cw.get();
|
|
||||||
let zoom_in_val = zoom_in.get();
|
|
||||||
let zoom_out_val = zoom_out.get();
|
|
||||||
let turntable_val = turntable.get(); /* BENCHMARKING */
|
|
||||||
|
|
||||||
// get the manipulation state
|
|
||||||
let translate_neg_x_val = translate_neg_x.get();
|
|
||||||
let translate_pos_x_val = translate_pos_x.get();
|
|
||||||
let translate_neg_y_val = translate_neg_y.get();
|
|
||||||
let translate_pos_y_val = translate_pos_y.get();
|
|
||||||
let translate_neg_z_val = translate_neg_z.get();
|
|
||||||
let translate_pos_z_val = translate_pos_z.get();
|
|
||||||
let shrink_neg_val = shrink_neg.get();
|
|
||||||
let shrink_pos_val = shrink_pos.get();
|
|
||||||
|
|
||||||
// update the assembly's orientation
|
|
||||||
let ang_vel = {
|
|
||||||
let pitch = pitch_up_val - pitch_down_val;
|
|
||||||
let yaw = yaw_right_val - yaw_left_val;
|
|
||||||
let roll = roll_ccw_val - roll_cw_val;
|
|
||||||
if pitch != 0.0 || yaw != 0.0 || roll != 0.0 {
|
|
||||||
ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize()
|
|
||||||
} else {
|
|
||||||
Vector3::zeros()
|
|
||||||
}
|
|
||||||
} /* BENCHMARKING */ + if turntable_val {
|
|
||||||
Vector3::new(0.0, TURNTABLE_SPEED, 0.0)
|
|
||||||
} else {
|
|
||||||
Vector3::zeros()
|
|
||||||
};
|
|
||||||
let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0);
|
|
||||||
rotation_sp.copy_from(
|
|
||||||
Rotation3::from_scaled_axis(time_step * ang_vel).matrix()
|
|
||||||
);
|
|
||||||
orientation = &rotation * &orientation;
|
|
||||||
|
|
||||||
// update the assembly's location
|
|
||||||
let zoom = zoom_out_val - zoom_in_val;
|
|
||||||
location_z *= (time_step * ZOOM_SPEED * zoom).exp();
|
|
||||||
|
|
||||||
// manipulate the assembly
|
|
||||||
/* KLUDGE */
|
|
||||||
// to avoid the complexity of making tangent space projection
|
|
||||||
// conditional and dealing with unnormalized representation vectors,
|
|
||||||
// we only allow manipulation when we're looking at the last step of
|
|
||||||
// a successful realization
|
|
||||||
let realization_successful = state.assembly.realization_status.with(
|
|
||||||
|status| status.is_ok()
|
|
||||||
);
|
|
||||||
let step_val = state.assembly
|
|
||||||
.step.with_untracked(|step| step.value);
|
|
||||||
let on_init_step = step_val.is_some_and(|n| n == 0.0);
|
|
||||||
let on_last_step = step_val.is_some_and(
|
|
||||||
|n| state.assembly.descent_history.with_untracked(
|
|
||||||
|history| n as usize + 1 == history.config.len().max(1)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
let on_manipulable_step =
|
|
||||||
!realization_successful && on_init_step
|
|
||||||
|| realization_successful && on_last_step;
|
|
||||||
if on_manipulable_step
|
|
||||||
&& state.selection.with(|sel| sel.len() == 1)
|
|
||||||
{
|
|
||||||
let sel = state.selection.with(
|
|
||||||
|sel| sel.into_iter().next().unwrap().clone()
|
|
||||||
);
|
|
||||||
let translate_x = translate_pos_x_val - translate_neg_x_val;
|
|
||||||
let translate_y = translate_pos_y_val - translate_neg_y_val;
|
|
||||||
let translate_z = translate_pos_z_val - translate_neg_z_val;
|
|
||||||
let shrink = shrink_pos_val - shrink_neg_val;
|
|
||||||
let translating =
|
|
||||||
translate_x != 0.0
|
|
||||||
|| translate_y != 0.0
|
|
||||||
|| translate_z != 0.0;
|
|
||||||
if translating || shrink != 0.0 {
|
|
||||||
let elt_motion = {
|
|
||||||
let u = if translating {
|
|
||||||
TRANSLATION_SPEED * Vector3::new(
|
|
||||||
translate_x, translate_y, translate_z
|
|
||||||
).normalize()
|
|
||||||
} else {
|
|
||||||
Vector3::zeros()
|
|
||||||
};
|
|
||||||
time_step * DVector::from_column_slice(
|
|
||||||
&[u[0], u[1], u[2], SHRINKING_SPEED * shrink]
|
|
||||||
)
|
|
||||||
};
|
|
||||||
assembly_for_raf.deform(
|
|
||||||
vec![
|
|
||||||
ElementMotion {
|
|
||||||
element: sel,
|
|
||||||
velocity: elt_motion.as_view(),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
scene_changed.set(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if scene_changed.get() {
|
|
||||||
const SPACE_DIM: usize = 3;
|
|
||||||
const COLOR_SIZE: usize = 3;
|
|
||||||
|
|
||||||
/* INSTRUMENTS */
|
|
||||||
// measure mean frame interval
|
|
||||||
frames_since_last_sample += 1;
|
|
||||||
if frames_since_last_sample >= SAMPLE_PERIOD {
|
|
||||||
mean_frame_interval
|
|
||||||
.set((time - last_sample_time) / SAMPLE_PERIOD as f64);
|
|
||||||
last_sample_time = time;
|
|
||||||
frames_since_last_sample = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- get the assembly ---
|
|
||||||
|
|
||||||
let mut scene = Scene::new();
|
|
||||||
|
|
||||||
// find the map from assembly space to world space
|
|
||||||
let location = {
|
|
||||||
let u = -location_z;
|
|
||||||
DMatrix::from_column_slice(5, 5, &[
|
|
||||||
1.0, 0.0, 0.0, 0.0, 0.0,
|
|
||||||
0.0, 1.0, 0.0, 0.0, 0.0,
|
|
||||||
0.0, 0.0, 1.0, 0.0, u,
|
|
||||||
0.0, 0.0, 2.0*u, 1.0, u*u,
|
|
||||||
0.0, 0.0, 0.0, 0.0, 1.0,
|
|
||||||
])
|
|
||||||
};
|
|
||||||
let asm_to_world = &location * &orientation;
|
|
||||||
|
|
||||||
// set up the scene
|
|
||||||
state.assembly.elements.with_untracked(
|
|
||||||
|elts| for elt in elts {
|
|
||||||
let selected = state.selection
|
|
||||||
.with(|sel| sel.contains(elt));
|
|
||||||
elt.show(&mut scene, selected);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let sphere_cnt = scene.spheres.len_i32();
|
|
||||||
|
|
||||||
// --- draw the spheres ---
|
|
||||||
|
|
||||||
// use the sphere rendering program
|
|
||||||
ctx.use_program(Some(&sphere_program));
|
|
||||||
|
|
||||||
// enable the sphere program's vertex attribute
|
|
||||||
ctx.enable_vertex_attrib_array(viewport_position_attr);
|
|
||||||
|
|
||||||
// write the spheres in world coordinates
|
|
||||||
let sphere_reps_world: Vec<_> = scene.spheres.representations
|
|
||||||
.into_iter()
|
|
||||||
.map(|rep| (&asm_to_world * rep).cast::<f32>())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// set the resolution
|
|
||||||
let width = canvas.width() as f32;
|
|
||||||
let height = canvas.height() as f32;
|
|
||||||
ctx.uniform2f(resolution_loc.as_ref(), width, height);
|
|
||||||
ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
|
|
||||||
|
|
||||||
// pass the scene data
|
|
||||||
ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt);
|
|
||||||
for n in 0..sphere_reps_world.len() {
|
|
||||||
let v = &sphere_reps_world[n];
|
|
||||||
ctx.uniform3fv_with_f32_array(
|
|
||||||
sphere_sp_locs[n].as_ref(),
|
|
||||||
v.rows(0, 3).as_slice(),
|
|
||||||
);
|
|
||||||
ctx.uniform2fv_with_f32_array(
|
|
||||||
sphere_lt_locs[n].as_ref(),
|
|
||||||
v.rows(3, 2).as_slice(),
|
|
||||||
);
|
|
||||||
ctx.uniform4fv_with_f32_array(
|
|
||||||
sphere_color_locs[n].as_ref(),
|
|
||||||
&scene.spheres.colors_with_opacity[n],
|
|
||||||
);
|
|
||||||
ctx.uniform1f(
|
|
||||||
sphere_highlight_locs[n].as_ref(),
|
|
||||||
scene.spheres.highlights[n],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// pass the display parameters
|
|
||||||
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
|
|
||||||
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
|
|
||||||
|
|
||||||
// bind the viewport vertex position buffer to the position
|
|
||||||
// attribute in the vertex shader
|
|
||||||
bind_to_attribute(
|
|
||||||
&ctx, viewport_position_attr,
|
|
||||||
SPACE_DIM as i32, &viewport_position_buffer,
|
|
||||||
);
|
|
||||||
|
|
||||||
// draw the scene
|
|
||||||
ctx.draw_arrays(
|
|
||||||
WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
|
|
||||||
|
|
||||||
// disable the sphere program's vertex attribute
|
|
||||||
ctx.disable_vertex_attrib_array(viewport_position_attr);
|
|
||||||
|
|
||||||
// --- draw the points ---
|
|
||||||
|
|
||||||
if !scene.points.representations.is_empty() {
|
|
||||||
// use the point rendering program
|
|
||||||
ctx.use_program(Some(&point_program));
|
|
||||||
|
|
||||||
// enable the point program's vertex attributes
|
|
||||||
ctx.enable_vertex_attrib_array(point_position_attr);
|
|
||||||
ctx.enable_vertex_attrib_array(point_color_attr);
|
|
||||||
ctx.enable_vertex_attrib_array(point_highlight_attr);
|
|
||||||
ctx.enable_vertex_attrib_array(point_selection_attr);
|
|
||||||
|
|
||||||
// write the points in world coordinates
|
|
||||||
let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM);
|
|
||||||
let point_positions = DMatrix::from_columns(
|
|
||||||
&scene.points.representations.into_iter().map(
|
|
||||||
|rep| &asm_to_world_sp * rep
|
|
||||||
).collect::<Vec<_>>().as_slice()
|
|
||||||
).cast::<f32>();
|
|
||||||
|
|
||||||
// load the point positions and colors into new buffers and
|
|
||||||
// bind them to the corresponding attributes in the vertex
|
|
||||||
// shader
|
|
||||||
bind_new_buffer_to_attribute(
|
|
||||||
&ctx, point_position_attr,
|
|
||||||
SPACE_DIM as i32, point_positions.as_slice(),
|
|
||||||
);
|
|
||||||
bind_new_buffer_to_attribute(
|
|
||||||
&ctx, point_color_attr,
|
|
||||||
(COLOR_SIZE + 1) as i32,
|
|
||||||
scene.points.colors_with_opacity.concat().as_slice());
|
|
||||||
bind_new_buffer_to_attribute(
|
|
||||||
&ctx, point_highlight_attr,
|
|
||||||
1i32, scene.points.highlights.as_slice(),
|
|
||||||
);
|
|
||||||
bind_new_buffer_to_attribute(
|
|
||||||
&ctx, point_selection_attr,
|
|
||||||
1i32, scene.points.selections.as_slice(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// draw the scene
|
|
||||||
ctx.draw_arrays(
|
|
||||||
WebGl2RenderingContext::POINTS,
|
|
||||||
0,
|
|
||||||
point_positions.ncols() as i32,
|
|
||||||
);
|
|
||||||
|
|
||||||
// disable the point program's vertex attributes
|
|
||||||
ctx.disable_vertex_attrib_array(point_position_attr);
|
|
||||||
ctx.disable_vertex_attrib_array(point_color_attr);
|
|
||||||
ctx.disable_vertex_attrib_array(point_highlight_attr);
|
|
||||||
ctx.disable_vertex_attrib_array(point_selection_attr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- update the display state ---
|
|
||||||
|
|
||||||
// update the viewpoint
|
|
||||||
assembly_to_world.set(asm_to_world);
|
|
||||||
|
|
||||||
// clear the scene change flag
|
|
||||||
scene_changed.set(
|
|
||||||
pitch_up_val != 0.0
|
|
||||||
|| pitch_down_val != 0.0
|
|
||||||
|| yaw_left_val != 0.0
|
|
||||||
|| yaw_right_val != 0.0
|
|
||||||
|| roll_cw_val != 0.0
|
|
||||||
|| roll_ccw_val != 0.0
|
|
||||||
|| zoom_in_val != 0.0
|
|
||||||
|| zoom_out_val != 0.0
|
|
||||||
|| turntable_val /* BENCHMARKING */
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
frames_since_last_sample = 0;
|
|
||||||
mean_frame_interval.set(-1.0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
start_animation_loop();
|
|
||||||
});
|
|
||||||
|
|
||||||
let set_nav_signal = move |event: &KeyboardEvent, value: f64| {
|
|
||||||
let mut navigating = true;
|
|
||||||
let shift = event.shift_key();
|
|
||||||
match event.key().as_str() {
|
|
||||||
"ArrowUp" if shift => zoom_in.set(value),
|
|
||||||
"ArrowDown" if shift => zoom_out.set(value),
|
|
||||||
"ArrowUp" => pitch_up.set(value),
|
|
||||||
"ArrowDown" => pitch_down.set(value),
|
|
||||||
"ArrowRight" if shift => roll_cw.set(value),
|
|
||||||
"ArrowLeft" if shift => roll_ccw.set(value),
|
|
||||||
"ArrowRight" => yaw_right.set(value),
|
|
||||||
"ArrowLeft" => yaw_left.set(value),
|
|
||||||
_ => navigating = false,
|
|
||||||
};
|
|
||||||
if navigating {
|
|
||||||
scene_changed.set(true);
|
|
||||||
event.prevent_default();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let set_manip_signal = move |event: &KeyboardEvent, value: f64| {
|
|
||||||
let mut manipulating = true;
|
|
||||||
let shift = event.shift_key();
|
|
||||||
match event.key().as_str() {
|
|
||||||
"d" | "D" => translate_pos_x.set(value),
|
|
||||||
"a" | "A" => translate_neg_x.set(value),
|
|
||||||
"w" | "W" if shift => translate_neg_z.set(value),
|
|
||||||
"s" | "S" if shift => translate_pos_z.set(value),
|
|
||||||
"w" | "W" => translate_pos_y.set(value),
|
|
||||||
"s" | "S" => translate_neg_y.set(value),
|
|
||||||
"]" | "}" => shrink_neg.set(value),
|
|
||||||
"[" | "{" => shrink_pos.set(value),
|
|
||||||
_ => manipulating = false,
|
|
||||||
};
|
|
||||||
if manipulating {
|
|
||||||
event.prevent_default();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
view! {
|
|
||||||
/* TO DO */
|
|
||||||
// switch back to integer-valued parameters when that becomes possible
|
|
||||||
// again
|
|
||||||
canvas(
|
|
||||||
ref = display,
|
|
||||||
id = "display",
|
|
||||||
width = "600",
|
|
||||||
height = "600",
|
|
||||||
tabindex = "0",
|
|
||||||
on:keydown = move |event: KeyboardEvent| {
|
|
||||||
if event.key() == "Shift" {
|
|
||||||
// swap navigation inputs
|
|
||||||
roll_cw.set(yaw_right.get());
|
|
||||||
roll_ccw.set(yaw_left.get());
|
|
||||||
zoom_in.set(pitch_up.get());
|
|
||||||
zoom_out.set(pitch_down.get());
|
|
||||||
yaw_right.set(0.0);
|
|
||||||
yaw_left.set(0.0);
|
|
||||||
pitch_up.set(0.0);
|
|
||||||
pitch_down.set(0.0);
|
|
||||||
|
|
||||||
// swap manipulation inputs
|
|
||||||
translate_pos_z.set(translate_neg_y.get());
|
|
||||||
translate_neg_z.set(translate_pos_y.get());
|
|
||||||
translate_pos_y.set(0.0);
|
|
||||||
translate_neg_y.set(0.0);
|
|
||||||
} else {
|
|
||||||
if event.key() == "Enter" { /* BENCHMARKING */
|
|
||||||
turntable.set_fn(|turn| !turn);
|
|
||||||
scene_changed.set(true);
|
|
||||||
}
|
|
||||||
set_nav_signal(&event, 1.0);
|
|
||||||
set_manip_signal(&event, 1.0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
on:keyup = move |event: KeyboardEvent| {
|
|
||||||
if event.key() == "Shift" {
|
|
||||||
// swap navigation inputs
|
|
||||||
yaw_right.set(roll_cw.get());
|
|
||||||
yaw_left.set(roll_ccw.get());
|
|
||||||
pitch_up.set(zoom_in.get());
|
|
||||||
pitch_down.set(zoom_out.get());
|
|
||||||
roll_cw.set(0.0);
|
|
||||||
roll_ccw.set(0.0);
|
|
||||||
zoom_in.set(0.0);
|
|
||||||
zoom_out.set(0.0);
|
|
||||||
|
|
||||||
// swap manipulation inputs
|
|
||||||
translate_pos_y.set(translate_neg_z.get());
|
|
||||||
translate_neg_y.set(translate_pos_z.get());
|
|
||||||
translate_pos_z.set(0.0);
|
|
||||||
translate_neg_z.set(0.0);
|
|
||||||
} else {
|
|
||||||
set_nav_signal(&event, 0.0);
|
|
||||||
set_manip_signal(&event, 0.0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
on:blur = move |_| {
|
|
||||||
pitch_up.set(0.0);
|
|
||||||
pitch_down.set(0.0);
|
|
||||||
yaw_right.set(0.0);
|
|
||||||
yaw_left.set(0.0);
|
|
||||||
roll_ccw.set(0.0);
|
|
||||||
roll_cw.set(0.0);
|
|
||||||
},
|
|
||||||
on:click = move |event: MouseEvent| {
|
|
||||||
// find the nearest element along the pointer direction
|
|
||||||
let (dir, pixel_size) = event_dir(&event);
|
|
||||||
console::log_1(&JsValue::from(dir.to_string()));
|
|
||||||
let mut clicked: Option<(Rc<dyn Element>, f64)> = None;
|
|
||||||
let tangible_elts = state.assembly.elements
|
|
||||||
.get_clone_untracked()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|elt| !elt.ghost().get());
|
|
||||||
for elt in tangible_elts {
|
|
||||||
let cast = assembly_to_world.with(
|
|
||||||
|asm_to_world| elt.cast(dir, asm_to_world, pixel_size));
|
|
||||||
match cast {
|
|
||||||
Some(depth) => match clicked {
|
|
||||||
Some((_, best_depth)) => {
|
|
||||||
if depth < best_depth {
|
|
||||||
clicked = Some((elt, depth))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => clicked = Some((elt, depth)),
|
|
||||||
},
|
|
||||||
None => (),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we clicked something, select it
|
|
||||||
match clicked {
|
|
||||||
Some((elt, _)) => state.select(&elt, event.shift_key()),
|
|
||||||
None => state.selection.update(|sel| sel.clear()),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,285 +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,
|
|
||||||
PointCoordinateRegulator,
|
|
||||||
Regulator,
|
|
||||||
},
|
|
||||||
specified::SpecifiedValue
|
|
||||||
};
|
|
||||||
|
|
||||||
// an editable view of a regulator
|
|
||||||
#[component(inline_props)]
|
|
||||||
fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
|
|
||||||
// get the regulator's measurement and set point signals
|
|
||||||
let measurement = regulator.measurement();
|
|
||||||
let set_point = regulator.set_point();
|
|
||||||
|
|
||||||
// the `valid` signal tracks whether the last entered value is a valid set
|
|
||||||
// point specification
|
|
||||||
let valid = create_signal(true);
|
|
||||||
|
|
||||||
// the `value` signal holds the current set point specification
|
|
||||||
let value = create_signal(
|
|
||||||
set_point.with_untracked(|set_pt| set_pt.spec.clone())
|
|
||||||
);
|
|
||||||
|
|
||||||
// this `reset_value` closure resets the input value to the regulator's set
|
|
||||||
// point specification
|
|
||||||
let reset_value = move || {
|
|
||||||
batch(|| {
|
|
||||||
valid.set(true);
|
|
||||||
value.set(set_point.with(|set_pt| set_pt.spec.clone()));
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
// reset the input value whenever the regulator's set point specification
|
|
||||||
// is updated
|
|
||||||
create_effect(reset_value);
|
|
||||||
|
|
||||||
view! {
|
|
||||||
input(
|
|
||||||
r#type = "text",
|
|
||||||
class = move || {
|
|
||||||
if valid.get() {
|
|
||||||
set_point.with(|set_pt| {
|
|
||||||
if set_pt.is_present() {
|
|
||||||
"regulator-input constraint"
|
|
||||||
} else {
|
|
||||||
"regulator-input"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
"regulator-input invalid"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
placeholder = measurement.with(|result| result.to_string()),
|
|
||||||
bind:value = value,
|
|
||||||
on:change = move |_| {
|
|
||||||
let sv = SpecifiedValue::try_from(value.get_clone_untracked());
|
|
||||||
valid.set(
|
|
||||||
match sv {
|
|
||||||
Ok(set_pt) => {
|
|
||||||
set_point.set(set_pt);
|
|
||||||
true
|
|
||||||
},
|
|
||||||
Err(_) => false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
on:keydown = {
|
|
||||||
move |event: KeyboardEvent| {
|
|
||||||
match event.key().as_str() {
|
|
||||||
"Escape" => reset_value(),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait OutlineItem {
|
|
||||||
fn outline_item(self: Rc<Self>, element: &Rc<dyn Element>) -> View;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OutlineItem for InversiveDistanceRegulator {
|
|
||||||
fn outline_item(self: Rc<Self>, element: &Rc<dyn Element>) -> View {
|
|
||||||
let other_subject_label = if self.subjects[0] == element.clone() {
|
|
||||||
self.subjects[1].label()
|
|
||||||
} else {
|
|
||||||
self.subjects[0].label()
|
|
||||||
}.clone();
|
|
||||||
view! {
|
|
||||||
li(class = "regulator") {
|
|
||||||
div(class = "regulator-label") { (other_subject_label) }
|
|
||||||
div(class = "regulator-type") { "Inversive distance" }
|
|
||||||
RegulatorInput(regulator = self)
|
|
||||||
div(class = "status")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OutlineItem for HalfCurvatureRegulator {
|
|
||||||
fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View {
|
|
||||||
view! {
|
|
||||||
li(class = "regulator") {
|
|
||||||
div(class = "regulator-label") // for spacing
|
|
||||||
div(class = "regulator-type") { "Half-curvature" }
|
|
||||||
RegulatorInput(regulator = self)
|
|
||||||
div(class = "status")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OutlineItem for PointCoordinateRegulator {
|
|
||||||
fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View {
|
|
||||||
let name = format!("{} coordinate", self.axis);
|
|
||||||
view! {
|
|
||||||
li(class = "regulator") {
|
|
||||||
div(class = "regulator-label") // for spacing
|
|
||||||
div(class = "regulator-type") { (name) }
|
|
||||||
RegulatorInput(regulator = self)
|
|
||||||
div(class = "status")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// a list item that shows an element in an outline view of an assembly
|
|
||||||
#[component(inline_props)]
|
|
||||||
fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
|
|
||||||
let state = use_context::<AppState>();
|
|
||||||
let class = {
|
|
||||||
let element_for_class = element.clone();
|
|
||||||
state.selection.map(
|
|
||||||
move |sel|
|
|
||||||
if sel.contains(&element_for_class) { "selected" }
|
|
||||||
else { "" }
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let label = element.label().clone();
|
|
||||||
let representation = element.representation().clone();
|
|
||||||
let rep_components = move || {
|
|
||||||
representation.with(
|
|
||||||
|rep| rep.iter().map(
|
|
||||||
|u| {
|
|
||||||
let u_str = format!("{:.3}", u).replace("-", "\u{2212}");
|
|
||||||
view! { div { (u_str) } }
|
|
||||||
}
|
|
||||||
).collect::<Vec<_>>()
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let regulated = element.regulators().map(|regs| regs.len() > 0);
|
|
||||||
let regulator_list = element.regulators().map(
|
|
||||||
|regs| regs
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.sorted_by_key(|reg| reg.subjects().len())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
);
|
|
||||||
let details_node = create_node_ref();
|
|
||||||
view! {
|
|
||||||
li {
|
|
||||||
details(ref = details_node) {
|
|
||||||
summary(
|
|
||||||
class = class.get(),
|
|
||||||
on:keydown = {
|
|
||||||
let element_for_handler = element.clone();
|
|
||||||
move |event: KeyboardEvent| {
|
|
||||||
match event.key().as_str() {
|
|
||||||
"Enter" => {
|
|
||||||
state.select(
|
|
||||||
&element_for_handler,
|
|
||||||
event.shift_key(),
|
|
||||||
);
|
|
||||||
event.prevent_default();
|
|
||||||
},
|
|
||||||
"ArrowRight" if regulated.get() => {
|
|
||||||
let _ = details_node
|
|
||||||
.get()
|
|
||||||
.unchecked_into::<web_sys::Element>()
|
|
||||||
.set_attribute("open", "");
|
|
||||||
},
|
|
||||||
"ArrowLeft" => {
|
|
||||||
let _ = details_node
|
|
||||||
.get()
|
|
||||||
.unchecked_into::<web_sys::Element>()
|
|
||||||
.remove_attribute("open");
|
|
||||||
},
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
div(
|
|
||||||
class = "element-switch",
|
|
||||||
on:click = |event: MouseEvent| event.stop_propagation()
|
|
||||||
)
|
|
||||||
div(
|
|
||||||
class = "element",
|
|
||||||
on:click = {
|
|
||||||
let state_for_handler = state.clone();
|
|
||||||
let element_for_handler = element.clone();
|
|
||||||
move |event: MouseEvent| {
|
|
||||||
state_for_handler.select(
|
|
||||||
&element_for_handler, event.shift_key());
|
|
||||||
event.stop_propagation();
|
|
||||||
event.prevent_default();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
div(class = "element-label") { (label) }
|
|
||||||
div(class = "element-representation") {
|
|
||||||
(rep_components)
|
|
||||||
}
|
|
||||||
input(
|
|
||||||
r#type = "checkbox",
|
|
||||||
bind:checked = element.ghost(),
|
|
||||||
on:click
|
|
||||||
= |event: MouseEvent| event.stop_propagation()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ul(class = "regulators") {
|
|
||||||
Keyed(
|
|
||||||
list = regulator_list,
|
|
||||||
view = move |reg| reg.outline_item(&element),
|
|
||||||
key = |reg| reg.serial()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// a component that lists the elements of the current assembly, showing each
|
|
||||||
// element's regulators in a collapsible sub-list. its implementation is based
|
|
||||||
// on Kate Morley's HTML + CSS tree views:
|
|
||||||
//
|
|
||||||
// https://iamkate.com/code/tree-views/
|
|
||||||
//
|
|
||||||
#[component]
|
|
||||||
pub fn Outline() -> View {
|
|
||||||
let state = use_context::<AppState>();
|
|
||||||
|
|
||||||
// list the elements alphabetically by ID
|
|
||||||
/* TO DO */
|
|
||||||
// this code is designed to generalize easily to other sort keys. if we only
|
|
||||||
// ever wanted to sort by ID, we could do that more simply using the
|
|
||||||
// `elements_by_id` index
|
|
||||||
let element_list = state.assembly.elements.map(
|
|
||||||
|elts| elts
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.sorted_by_key(|elt| elt.id().clone())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
);
|
|
||||||
|
|
||||||
view! {
|
|
||||||
ul(
|
|
||||||
id = "outline",
|
|
||||||
on:click = {
|
|
||||||
let state = use_context::<AppState>();
|
|
||||||
move |_| state.selection.update(|sel| sel.clear())
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Keyed(
|
|
||||||
list = element_list,
|
|
||||||
view = |elt| view! {
|
|
||||||
ElementOutlineItem(element = elt)
|
|
||||||
},
|
|
||||||
key = |elt| elt.serial()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
#version 300 es
|
|
||||||
|
|
||||||
precision highp float;
|
|
||||||
|
|
||||||
in vec4 point_color;
|
|
||||||
in float point_highlight;
|
|
||||||
in float total_radius;
|
|
||||||
|
|
||||||
out vec4 outColor;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
float r = total_radius * length(2.*gl_PointCoord - vec2(1.));
|
|
||||||
|
|
||||||
const float POINT_RADIUS = 4.;
|
|
||||||
float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r);
|
|
||||||
float disk = 1. - smoothstep(total_radius - 1., total_radius, r);
|
|
||||||
vec4 color = mix(point_color, vec4(1.), border * point_highlight);
|
|
||||||
outColor = vec4(vec3(1.), disk) * color;
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
#version 300 es
|
|
||||||
|
|
||||||
in vec4 position;
|
|
||||||
in vec4 color;
|
|
||||||
in float highlight;
|
|
||||||
in float selected;
|
|
||||||
|
|
||||||
out vec4 point_color;
|
|
||||||
out float point_highlight;
|
|
||||||
out float total_radius;
|
|
||||||
|
|
||||||
// camera
|
|
||||||
const float focal_slope = 0.3;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
total_radius = 5. + 0.5*selected;
|
|
||||||
|
|
||||||
float depth = -focal_slope * position.z;
|
|
||||||
gl_Position = vec4(position.xy / depth, 0., 1.);
|
|
||||||
gl_PointSize = 2.*total_radius;
|
|
||||||
|
|
||||||
point_color = color;
|
|
||||||
point_highlight = highlight;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
443
app-proto/src/display.rs
Normal file
443
app-proto/src/display.rs
Normal file
|
|
@ -0,0 +1,443 @@
|
||||||
|
use core::array;
|
||||||
|
use nalgebra::{DMatrix, Rotation3, Vector3};
|
||||||
|
use sycamore::{prelude::*, motion::create_raf};
|
||||||
|
use web_sys::{
|
||||||
|
console,
|
||||||
|
window,
|
||||||
|
KeyboardEvent,
|
||||||
|
WebGl2RenderingContext,
|
||||||
|
WebGlProgram,
|
||||||
|
WebGlShader,
|
||||||
|
WebGlUniformLocation,
|
||||||
|
wasm_bindgen::{JsCast, JsValue}
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
fn compile_shader(
|
||||||
|
context: &WebGl2RenderingContext,
|
||||||
|
shader_type: u32,
|
||||||
|
source: &str,
|
||||||
|
) -> WebGlShader {
|
||||||
|
let shader = context.create_shader(shader_type).unwrap();
|
||||||
|
context.shader_source(&shader, source);
|
||||||
|
context.compile_shader(&shader);
|
||||||
|
shader
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_uniform_array_locations<const N: usize>(
|
||||||
|
context: &WebGl2RenderingContext,
|
||||||
|
program: &WebGlProgram,
|
||||||
|
var_name: &str,
|
||||||
|
member_name_opt: Option<&str>
|
||||||
|
) -> [Option<WebGlUniformLocation>; N] {
|
||||||
|
array::from_fn(|n| {
|
||||||
|
let name = match member_name_opt {
|
||||||
|
Some(member_name) => format!("{var_name}[{n}].{member_name}"),
|
||||||
|
None => format!("{var_name}[{n}]")
|
||||||
|
};
|
||||||
|
context.get_uniform_location(&program, name.as_str())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the given data into the vertex input of the given name
|
||||||
|
fn bind_vertex_attrib(
|
||||||
|
context: &WebGl2RenderingContext,
|
||||||
|
index: u32,
|
||||||
|
size: i32,
|
||||||
|
data: &[f32]
|
||||||
|
) {
|
||||||
|
// create a data buffer and bind it to ARRAY_BUFFER
|
||||||
|
let buffer = context.create_buffer().unwrap();
|
||||||
|
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer));
|
||||||
|
|
||||||
|
// load the given data into the buffer. the function `Float32Array::view`
|
||||||
|
// creates a raw view into our module's `WebAssembly.Memory` buffer.
|
||||||
|
// allocating more memory will change the buffer, invalidating the view.
|
||||||
|
// that means we have to make sure we don't allocate any memory until the
|
||||||
|
// view is dropped
|
||||||
|
unsafe {
|
||||||
|
context.buffer_data_with_array_buffer_view(
|
||||||
|
WebGl2RenderingContext::ARRAY_BUFFER,
|
||||||
|
&js_sys::Float32Array::view(&data),
|
||||||
|
WebGl2RenderingContext::STATIC_DRAW,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow the target attribute to be used
|
||||||
|
context.enable_vertex_attrib_array(index);
|
||||||
|
|
||||||
|
// take whatever's bound to ARRAY_BUFFER---here, the data buffer created
|
||||||
|
// above---and bind it to the target attribute
|
||||||
|
//
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer
|
||||||
|
//
|
||||||
|
context.vertex_attrib_pointer_with_i32(
|
||||||
|
index,
|
||||||
|
size,
|
||||||
|
WebGl2RenderingContext::FLOAT,
|
||||||
|
false, // don't normalize
|
||||||
|
0, // zero stride
|
||||||
|
0, // zero offset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Display() -> View {
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
|
||||||
|
// canvas
|
||||||
|
let display = create_node_ref();
|
||||||
|
|
||||||
|
// navigation
|
||||||
|
let pitch_up = create_signal(0.0);
|
||||||
|
let pitch_down = create_signal(0.0);
|
||||||
|
let yaw_right = create_signal(0.0);
|
||||||
|
let yaw_left = create_signal(0.0);
|
||||||
|
let roll_ccw = create_signal(0.0);
|
||||||
|
let roll_cw = create_signal(0.0);
|
||||||
|
let zoom_in = create_signal(0.0);
|
||||||
|
let zoom_out = create_signal(0.0);
|
||||||
|
let turntable = create_signal(false); /* BENCHMARKING */
|
||||||
|
|
||||||
|
// change listener
|
||||||
|
let scene_changed = create_signal(true);
|
||||||
|
create_effect(move || {
|
||||||
|
state.assembly.elements.track();
|
||||||
|
state.selection.track();
|
||||||
|
scene_changed.set(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* INSTRUMENTS */
|
||||||
|
const SAMPLE_PERIOD: i32 = 60;
|
||||||
|
let mut last_sample_time = 0.0;
|
||||||
|
let mut frames_since_last_sample = 0;
|
||||||
|
let mean_frame_interval = create_signal(0.0);
|
||||||
|
|
||||||
|
on_mount(move || {
|
||||||
|
// timing
|
||||||
|
let mut last_time = 0.0;
|
||||||
|
|
||||||
|
// viewpoint
|
||||||
|
const ROT_SPEED: f64 = 0.4; // in radians per second
|
||||||
|
const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second
|
||||||
|
const TURNTABLE_SPEED: f64 = 0.1; /* BENCHMARKING */
|
||||||
|
let mut orientation = DMatrix::<f64>::identity(5, 5);
|
||||||
|
let mut rotation = DMatrix::<f64>::identity(5, 5);
|
||||||
|
let mut location_z: f64 = 5.0;
|
||||||
|
|
||||||
|
// display parameters
|
||||||
|
const OPACITY: f32 = 0.5; /* SCAFFOLDING */
|
||||||
|
const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */
|
||||||
|
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
|
||||||
|
const DEBUG_MODE: i32 = 0; /* DEBUG */
|
||||||
|
|
||||||
|
/* INSTRUMENTS */
|
||||||
|
let performance = window().unwrap().performance().unwrap();
|
||||||
|
|
||||||
|
// get the display canvas
|
||||||
|
let canvas = display.get().unchecked_into::<web_sys::HtmlCanvasElement>();
|
||||||
|
let ctx = canvas
|
||||||
|
.get_context("webgl2")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into::<WebGl2RenderingContext>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// compile and attach the vertex and fragment shaders
|
||||||
|
let vertex_shader = compile_shader(
|
||||||
|
&ctx,
|
||||||
|
WebGl2RenderingContext::VERTEX_SHADER,
|
||||||
|
include_str!("identity.vert"),
|
||||||
|
);
|
||||||
|
let fragment_shader = compile_shader(
|
||||||
|
&ctx,
|
||||||
|
WebGl2RenderingContext::FRAGMENT_SHADER,
|
||||||
|
include_str!("inversive.frag"),
|
||||||
|
);
|
||||||
|
let program = ctx.create_program().unwrap();
|
||||||
|
ctx.attach_shader(&program, &vertex_shader);
|
||||||
|
ctx.attach_shader(&program, &fragment_shader);
|
||||||
|
ctx.link_program(&program);
|
||||||
|
let link_status = ctx
|
||||||
|
.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
|
||||||
|
.as_bool()
|
||||||
|
.unwrap();
|
||||||
|
let link_msg = if link_status {
|
||||||
|
"Linked successfully"
|
||||||
|
} else {
|
||||||
|
"Linking failed"
|
||||||
|
};
|
||||||
|
console::log_1(&JsValue::from(link_msg));
|
||||||
|
ctx.use_program(Some(&program));
|
||||||
|
|
||||||
|
/* DEBUG */
|
||||||
|
// print the maximum number of vectors that can be passed as
|
||||||
|
// uniforms to a fragment shader. the OpenGL ES 3.0 standard
|
||||||
|
// requires this maximum to be at least 224, as discussed in the
|
||||||
|
// documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter
|
||||||
|
// here:
|
||||||
|
//
|
||||||
|
// https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml
|
||||||
|
//
|
||||||
|
// there are also other size limits. for example, on Aaron's
|
||||||
|
// machine, the the length of a float or genType array seems to be
|
||||||
|
// capped at 1024 elements
|
||||||
|
console::log_2(
|
||||||
|
&ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(),
|
||||||
|
&JsValue::from("uniform vectors available")
|
||||||
|
);
|
||||||
|
|
||||||
|
// find indices of vertex attributes and uniforms
|
||||||
|
const SPHERE_MAX: usize = 200;
|
||||||
|
let position_index = ctx.get_attrib_location(&program, "position") as u32;
|
||||||
|
let sphere_cnt_loc = ctx.get_uniform_location(&program, "sphere_cnt");
|
||||||
|
let sphere_sp_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||||
|
&ctx, &program, "sphere_list", Some("sp")
|
||||||
|
);
|
||||||
|
let sphere_lt_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||||
|
&ctx, &program, "sphere_list", Some("lt")
|
||||||
|
);
|
||||||
|
let color_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||||
|
&ctx, &program, "color_list", None
|
||||||
|
);
|
||||||
|
let highlight_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||||
|
&ctx, &program, "highlight_list", None
|
||||||
|
);
|
||||||
|
let resolution_loc = ctx.get_uniform_location(&program, "resolution");
|
||||||
|
let shortdim_loc = ctx.get_uniform_location(&program, "shortdim");
|
||||||
|
let opacity_loc = ctx.get_uniform_location(&program, "opacity");
|
||||||
|
let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold");
|
||||||
|
let debug_mode_loc = ctx.get_uniform_location(&program, "debug_mode");
|
||||||
|
|
||||||
|
// create a vertex array and bind it to the graphics context
|
||||||
|
let vertex_array = ctx.create_vertex_array().unwrap();
|
||||||
|
ctx.bind_vertex_array(Some(&vertex_array));
|
||||||
|
|
||||||
|
// set the vertex positions
|
||||||
|
const VERTEX_CNT: usize = 6;
|
||||||
|
let positions: [f32; 3*VERTEX_CNT] = [
|
||||||
|
// northwest triangle
|
||||||
|
-1.0, -1.0, 0.0,
|
||||||
|
-1.0, 1.0, 0.0,
|
||||||
|
1.0, 1.0, 0.0,
|
||||||
|
// southeast triangle
|
||||||
|
-1.0, -1.0, 0.0,
|
||||||
|
1.0, 1.0, 0.0,
|
||||||
|
1.0, -1.0, 0.0
|
||||||
|
];
|
||||||
|
bind_vertex_attrib(&ctx, position_index, 3, &positions);
|
||||||
|
|
||||||
|
// set up a repainting routine
|
||||||
|
let (_, start_animation_loop, _) = create_raf(move || {
|
||||||
|
// get the time step
|
||||||
|
let time = performance.now();
|
||||||
|
let time_step = 0.001*(time - last_time);
|
||||||
|
last_time = time;
|
||||||
|
|
||||||
|
// get the navigation state
|
||||||
|
let pitch_up_val = pitch_up.get();
|
||||||
|
let pitch_down_val = pitch_down.get();
|
||||||
|
let yaw_right_val = yaw_right.get();
|
||||||
|
let yaw_left_val = yaw_left.get();
|
||||||
|
let roll_ccw_val = roll_ccw.get();
|
||||||
|
let roll_cw_val = roll_cw.get();
|
||||||
|
let zoom_in_val = zoom_in.get();
|
||||||
|
let zoom_out_val = zoom_out.get();
|
||||||
|
let turntable_val = turntable.get(); /* BENCHMARKING */
|
||||||
|
|
||||||
|
// update the assembly's orientation
|
||||||
|
let ang_vel = {
|
||||||
|
let pitch = pitch_up_val - pitch_down_val;
|
||||||
|
let yaw = yaw_right_val - yaw_left_val;
|
||||||
|
let roll = roll_ccw_val - roll_cw_val;
|
||||||
|
if pitch != 0.0 || yaw != 0.0 || roll != 0.0 {
|
||||||
|
ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize()
|
||||||
|
} else {
|
||||||
|
Vector3::zeros()
|
||||||
|
}
|
||||||
|
} /* BENCHMARKING */ + if turntable_val {
|
||||||
|
Vector3::new(0.0, TURNTABLE_SPEED, 0.0)
|
||||||
|
} else {
|
||||||
|
Vector3::zeros()
|
||||||
|
};
|
||||||
|
let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0);
|
||||||
|
rotation_sp.copy_from(
|
||||||
|
Rotation3::from_scaled_axis(time_step * ang_vel).matrix()
|
||||||
|
);
|
||||||
|
orientation = &rotation * &orientation;
|
||||||
|
|
||||||
|
// update the assembly's location
|
||||||
|
let zoom = zoom_out_val - zoom_in_val;
|
||||||
|
location_z *= (time_step * ZOOM_SPEED * zoom).exp();
|
||||||
|
|
||||||
|
if scene_changed.get() {
|
||||||
|
/* INSTRUMENTS */
|
||||||
|
// measure mean frame interval
|
||||||
|
frames_since_last_sample += 1;
|
||||||
|
if frames_since_last_sample >= SAMPLE_PERIOD {
|
||||||
|
mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64));
|
||||||
|
last_sample_time = time;
|
||||||
|
frames_since_last_sample = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the map from assembly space to world space
|
||||||
|
let location = {
|
||||||
|
let u = -location_z;
|
||||||
|
DMatrix::from_column_slice(5, 5, &[
|
||||||
|
1.0, 0.0, 0.0, 0.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0, 0.0, 0.0,
|
||||||
|
0.0, 0.0, 1.0, 0.0, u,
|
||||||
|
0.0, 0.0, 2.0*u, 1.0, u*u,
|
||||||
|
0.0, 0.0, 0.0, 0.0, 1.0
|
||||||
|
])
|
||||||
|
};
|
||||||
|
let assembly_to_world = &location * &orientation;
|
||||||
|
|
||||||
|
// get the assembly
|
||||||
|
let elements = state.assembly.elements.get_clone();
|
||||||
|
let element_iter = (&elements).into_iter();
|
||||||
|
let reps_world: Vec<_> = element_iter.clone().map(|(_, elt)| &assembly_to_world * &elt.rep).collect();
|
||||||
|
let colors: Vec<_> = element_iter.clone().map(|(key, elt)|
|
||||||
|
if state.selection.with(|sel| sel.contains(&key)) {
|
||||||
|
elt.color.map(|ch| 0.2 + 0.8*ch)
|
||||||
|
} else {
|
||||||
|
elt.color
|
||||||
|
}
|
||||||
|
).collect();
|
||||||
|
let highlights: Vec<_> = element_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(), elements.len() as i32);
|
||||||
|
for n in 0..reps_world.len() {
|
||||||
|
let v = &reps_world[n];
|
||||||
|
ctx.uniform3f(
|
||||||
|
sphere_sp_locs[n].as_ref(),
|
||||||
|
v[0] as f32, v[1] as f32, v[2] as f32
|
||||||
|
);
|
||||||
|
ctx.uniform2f(
|
||||||
|
sphere_lt_locs[n].as_ref(),
|
||||||
|
v[3] as f32, v[4] as f32
|
||||||
|
);
|
||||||
|
ctx.uniform3fv_with_f32_array(
|
||||||
|
color_locs[n].as_ref(),
|
||||||
|
&colors[n]
|
||||||
|
);
|
||||||
|
ctx.uniform1f(
|
||||||
|
highlight_locs[n].as_ref(),
|
||||||
|
highlights[n]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass the display parameters
|
||||||
|
ctx.uniform1f(opacity_loc.as_ref(), OPACITY);
|
||||||
|
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
|
||||||
|
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
|
||||||
|
|
||||||
|
// draw the scene
|
||||||
|
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
|
||||||
|
|
||||||
|
// clear the scene change flag
|
||||||
|
scene_changed.set(
|
||||||
|
pitch_up_val != 0.0
|
||||||
|
|| pitch_down_val != 0.0
|
||||||
|
|| yaw_left_val != 0.0
|
||||||
|
|| yaw_right_val != 0.0
|
||||||
|
|| roll_cw_val != 0.0
|
||||||
|
|| roll_ccw_val != 0.0
|
||||||
|
|| zoom_in_val != 0.0
|
||||||
|
|| zoom_out_val != 0.0
|
||||||
|
|| turntable_val /* BENCHMARKING */
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
frames_since_last_sample = 0;
|
||||||
|
mean_frame_interval.set(-1.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
start_animation_loop();
|
||||||
|
});
|
||||||
|
|
||||||
|
let set_nav_signal = move |event: KeyboardEvent, value: f64| {
|
||||||
|
let mut navigating = true;
|
||||||
|
let shift = event.shift_key();
|
||||||
|
match event.key().as_str() {
|
||||||
|
"ArrowUp" if shift => zoom_in.set(value),
|
||||||
|
"ArrowDown" if shift => zoom_out.set(value),
|
||||||
|
"ArrowUp" => pitch_up.set(value),
|
||||||
|
"ArrowDown" => pitch_down.set(value),
|
||||||
|
"ArrowRight" if shift => roll_cw.set(value),
|
||||||
|
"ArrowLeft" if shift => roll_ccw.set(value),
|
||||||
|
"ArrowRight" => yaw_right.set(value),
|
||||||
|
"ArrowLeft" => yaw_left.set(value),
|
||||||
|
_ => navigating = false
|
||||||
|
};
|
||||||
|
if navigating {
|
||||||
|
scene_changed.set(true);
|
||||||
|
event.prevent_default();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
/* TO DO */
|
||||||
|
// switch back to integer-valued parameters when that becomes possible
|
||||||
|
// again
|
||||||
|
canvas(
|
||||||
|
ref=display,
|
||||||
|
width="600",
|
||||||
|
height="600",
|
||||||
|
tabindex="0",
|
||||||
|
on:keydown=move |event: KeyboardEvent| {
|
||||||
|
if event.key() == "Shift" {
|
||||||
|
roll_cw.set(yaw_right.get());
|
||||||
|
roll_ccw.set(yaw_left.get());
|
||||||
|
zoom_in.set(pitch_up.get());
|
||||||
|
zoom_out.set(pitch_down.get());
|
||||||
|
yaw_right.set(0.0);
|
||||||
|
yaw_left.set(0.0);
|
||||||
|
pitch_up.set(0.0);
|
||||||
|
pitch_down.set(0.0);
|
||||||
|
} else {
|
||||||
|
if event.key() == "Enter" { /* BENCHMARKING */
|
||||||
|
turntable.set_fn(|turn| !turn);
|
||||||
|
scene_changed.set(true);
|
||||||
|
}
|
||||||
|
set_nav_signal(event, 1.0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on:keyup=move |event: KeyboardEvent| {
|
||||||
|
if event.key() == "Shift" {
|
||||||
|
yaw_right.set(roll_cw.get());
|
||||||
|
yaw_left.set(roll_ccw.get());
|
||||||
|
pitch_up.set(zoom_in.get());
|
||||||
|
pitch_down.set(zoom_out.get());
|
||||||
|
roll_cw.set(0.0);
|
||||||
|
roll_ccw.set(0.0);
|
||||||
|
zoom_in.set(0.0);
|
||||||
|
zoom_out.set(0.0);
|
||||||
|
} else {
|
||||||
|
set_nav_signal(event, 0.0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on:blur=move |_| {
|
||||||
|
pitch_up.set(0.0);
|
||||||
|
pitch_down.set(0.0);
|
||||||
|
yaw_right.set(0.0);
|
||||||
|
yaw_left.set(0.0);
|
||||||
|
roll_ccw.set(0.0);
|
||||||
|
roll_cw.set(0.0);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -17,7 +17,7 @@ struct vecInv {
|
||||||
const int SPHERE_MAX = 200;
|
const int SPHERE_MAX = 200;
|
||||||
uniform int sphere_cnt;
|
uniform int sphere_cnt;
|
||||||
uniform vecInv sphere_list[SPHERE_MAX];
|
uniform vecInv sphere_list[SPHERE_MAX];
|
||||||
uniform vec4 color_list[SPHERE_MAX];
|
uniform vec3 color_list[SPHERE_MAX];
|
||||||
uniform float highlight_list[SPHERE_MAX];
|
uniform float highlight_list[SPHERE_MAX];
|
||||||
|
|
||||||
// view
|
// view
|
||||||
|
|
@ -25,6 +25,7 @@ uniform vec2 resolution;
|
||||||
uniform float shortdim;
|
uniform float shortdim;
|
||||||
|
|
||||||
// controls
|
// controls
|
||||||
|
uniform float opacity;
|
||||||
uniform int layer_threshold;
|
uniform int layer_threshold;
|
||||||
uniform bool debug_mode;
|
uniform bool debug_mode;
|
||||||
|
|
||||||
|
|
@ -68,7 +69,7 @@ struct Fragment {
|
||||||
vec4 color;
|
vec4 color;
|
||||||
};
|
};
|
||||||
|
|
||||||
Fragment sphere_shading(vecInv v, vec3 pt, vec4 base_color) {
|
Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) {
|
||||||
// the expression for normal needs to be checked. it's supposed to give the
|
// the expression for normal needs to be checked. it's supposed to give the
|
||||||
// negative gradient of the lorentz product between the impact point vector
|
// negative gradient of the lorentz product between the impact point vector
|
||||||
// and the sphere vector with respect to the coordinates of the impact
|
// and the sphere vector with respect to the coordinates of the impact
|
||||||
|
|
@ -78,7 +79,7 @@ Fragment sphere_shading(vecInv v, vec3 pt, vec4 base_color) {
|
||||||
|
|
||||||
float incidence = dot(normal, light_dir);
|
float incidence = dot(normal, light_dir);
|
||||||
float illum = mix(0.4, 1.0, max(incidence, 0.0));
|
float illum = mix(0.4, 1.0, max(incidence, 0.0));
|
||||||
return Fragment(pt, normal, vec4(illum * base_color.rgb, base_color.a));
|
return Fragment(pt, normal, vec4(illum * base_color, opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
float intersection_dist(Fragment a, Fragment b) {
|
float intersection_dist(Fragment a, Fragment b) {
|
||||||
|
|
@ -175,9 +176,8 @@ void main() {
|
||||||
if (debug_mode) {
|
if (debug_mode) {
|
||||||
// at the bottom of the screen, show the color scale instead of the
|
// at the bottom of the screen, show the color scale instead of the
|
||||||
// layer count
|
// layer count
|
||||||
if (gl_FragCoord.y < 10.) {
|
if (gl_FragCoord.y < 10.) layer_cnt = int(16. * gl_FragCoord.x / resolution.x);
|
||||||
layer_cnt = int(16. * gl_FragCoord.x / resolution.x);
|
|
||||||
}
|
|
||||||
// convert number to color
|
// convert number to color
|
||||||
ivec3 bits = layer_cnt / ivec3(1, 2, 4);
|
ivec3 bits = layer_cnt / ivec3(1, 2, 4);
|
||||||
vec3 color = mod(vec3(bits), 2.);
|
vec3 color = mod(vec3(bits), 2.);
|
||||||
|
|
@ -192,11 +192,10 @@ void main() {
|
||||||
vec3 color = vec3(0.);
|
vec3 color = vec3(0.);
|
||||||
int layer = layer_cnt - 1;
|
int layer = layer_cnt - 1;
|
||||||
TaggedDepth hit = top_hits[layer];
|
TaggedDepth hit = top_hits[layer];
|
||||||
vec4 sphere_color = color_list[hit.id];
|
|
||||||
Fragment frag_next = sphere_shading(
|
Fragment frag_next = sphere_shading(
|
||||||
sphere_list[hit.id],
|
sphere_list[hit.id],
|
||||||
hit.depth * dir,
|
hit.depth * dir,
|
||||||
vec4(hit.dimming * sphere_color.rgb, sphere_color.a)
|
hit.dimming * color_list[hit.id]
|
||||||
);
|
);
|
||||||
float highlight_next = highlight_list[hit.id];
|
float highlight_next = highlight_list[hit.id];
|
||||||
--layer;
|
--layer;
|
||||||
|
|
@ -207,28 +206,24 @@ void main() {
|
||||||
|
|
||||||
// shade the next fragment
|
// shade the next fragment
|
||||||
hit = top_hits[layer];
|
hit = top_hits[layer];
|
||||||
sphere_color = color_list[hit.id];
|
|
||||||
frag_next = sphere_shading(
|
frag_next = sphere_shading(
|
||||||
sphere_list[hit.id],
|
sphere_list[hit.id],
|
||||||
hit.depth * dir,
|
hit.depth * dir,
|
||||||
vec4(hit.dimming * sphere_color.rgb, sphere_color.a)
|
hit.dimming * color_list[hit.id]
|
||||||
);
|
);
|
||||||
highlight_next = highlight_list[hit.id];
|
highlight_next = highlight_list[hit.id];
|
||||||
|
|
||||||
// highlight intersections
|
// highlight intersections
|
||||||
float ixn_dist = intersection_dist(frag, frag_next);
|
float ixn_dist = intersection_dist(frag, frag_next);
|
||||||
float max_highlight = max(highlight, highlight_next);
|
float max_highlight = max(highlight, highlight_next);
|
||||||
float ixn_highlight = 0.5 * max_highlight * (1. - smoothstep(
|
float ixn_highlight = 0.5 * max_highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist));
|
||||||
2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist));
|
|
||||||
frag.color = mix(frag.color, vec4(1.), ixn_highlight);
|
frag.color = mix(frag.color, vec4(1.), ixn_highlight);
|
||||||
frag_next.color = mix(frag_next.color, vec4(1.), ixn_highlight);
|
frag_next.color = mix(frag_next.color, vec4(1.), ixn_highlight);
|
||||||
|
|
||||||
// highlight cusps
|
// highlight cusps
|
||||||
float cusp_cos = abs(dot(dir, frag.normal));
|
float cusp_cos = abs(dot(dir, frag.normal));
|
||||||
float cusp_threshold
|
float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[hit.id].lt.s);
|
||||||
= 2.*sqrt( ixn_threshold * sphere_list[hit.id].lt.s);
|
float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos));
|
||||||
float cusp_highlight = highlight * (1. - smoothstep(
|
|
||||||
2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos));
|
|
||||||
frag.color = mix(frag.color, vec4(1.), cusp_highlight);
|
frag.color = mix(frag.color, vec4(1.), cusp_highlight);
|
||||||
|
|
||||||
// composite the current fragment
|
// composite the current fragment
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
pub mod engine;
|
|
||||||
|
|
@ -1,59 +1,33 @@
|
||||||
|
mod add_remove;
|
||||||
mod assembly;
|
mod assembly;
|
||||||
mod components;
|
mod display;
|
||||||
mod engine;
|
mod engine;
|
||||||
mod specified;
|
mod outline;
|
||||||
|
|
||||||
#[cfg(test)]
|
use rustc_hash::FxHashSet;
|
||||||
mod tests;
|
|
||||||
|
|
||||||
use std::{collections::BTreeSet, rc::Rc};
|
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
|
|
||||||
use assembly::{Assembly, Element};
|
use add_remove::AddRemove;
|
||||||
use components::{
|
use assembly::Assembly;
|
||||||
add_remove::AddRemove,
|
use display::Display;
|
||||||
diagnostics::Diagnostics,
|
use outline::Outline;
|
||||||
display::Display,
|
|
||||||
outline::Outline,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
assembly: Assembly,
|
assembly: Assembly,
|
||||||
selection: Signal<BTreeSet<Rc<dyn Element>>>,
|
selection: Signal<FxHashSet<usize>>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
fn new() -> Self {
|
fn new() -> AppState {
|
||||||
Self {
|
AppState {
|
||||||
assembly: Assembly::new(),
|
assembly: Assembly::new(),
|
||||||
selection: create_signal(BTreeSet::default()),
|
selection: create_signal(FxHashSet::default())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// in single-selection mode, select the given element. in multiple-selection
|
|
||||||
// mode, toggle whether the given element is selected
|
|
||||||
fn select(&self, element: &Rc<dyn Element>, multi: bool) {
|
|
||||||
if multi {
|
|
||||||
self.selection.update(|sel| {
|
|
||||||
if !sel.remove(element) {
|
|
||||||
sel.insert(element.clone());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
self.selection.update(|sel| {
|
|
||||||
sel.clear();
|
|
||||||
sel.insert(element.clone());
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// set the console error panic hook
|
|
||||||
#[cfg(feature = "console_error_panic_hook")]
|
|
||||||
console_error_panic_hook::set_once();
|
|
||||||
|
|
||||||
sycamore::render(|| {
|
sycamore::render(|| {
|
||||||
provide_context(AppState::new());
|
provide_context(AppState::new());
|
||||||
|
|
||||||
|
|
@ -61,7 +35,6 @@ fn main() {
|
||||||
div(id="sidebar") {
|
div(id="sidebar") {
|
||||||
AddRemove {}
|
AddRemove {}
|
||||||
Outline {}
|
Outline {}
|
||||||
Diagnostics {}
|
|
||||||
}
|
}
|
||||||
Display {}
|
Display {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
161
app-proto/src/outline.rs
Normal file
161
app-proto/src/outline.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
use itertools::Itertools;
|
||||||
|
use sycamore::{prelude::*, web::tags::div};
|
||||||
|
use web_sys::{Element, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast};
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
// this component lists the elements of the 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 {
|
||||||
|
// sort the elements alphabetically by ID
|
||||||
|
let elements_sorted = create_memo(|| {
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
state.assembly.elements
|
||||||
|
.get_clone()
|
||||||
|
.into_iter()
|
||||||
|
.sorted_by_key(|(_, elt)| elt.id.clone())
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
ul(
|
||||||
|
id="outline",
|
||||||
|
on:click={
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
move |_| state.selection.update(|sel| sel.clear())
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Keyed(
|
||||||
|
list=elements_sorted,
|
||||||
|
view=|(key, elt)| {
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
let class = create_memo({
|
||||||
|
move || {
|
||||||
|
if state.selection.with(|sel| sel.contains(&key)) {
|
||||||
|
"selected"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let label = elt.label.clone();
|
||||||
|
let rep_components = elt.rep.iter().map(|u| {
|
||||||
|
let u_coord = u.to_string().replace("-", "\u{2212}");
|
||||||
|
View::from(div().children(u_coord))
|
||||||
|
}).collect::<Vec<_>>();
|
||||||
|
let constrained = elt.constraints.len() > 0;
|
||||||
|
let details_node = create_node_ref();
|
||||||
|
view! {
|
||||||
|
/* [TO DO] switch to integer-valued parameters whenever
|
||||||
|
that becomes possible again */
|
||||||
|
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 => {
|
||||||
|
let _ = details_node
|
||||||
|
.get()
|
||||||
|
.unchecked_into::<Element>()
|
||||||
|
.set_attribute("open", "");
|
||||||
|
},
|
||||||
|
"ArrowLeft" => {
|
||||||
|
let _ = details_node
|
||||||
|
.get()
|
||||||
|
.unchecked_into::<Element>()
|
||||||
|
.remove_attribute("open");
|
||||||
|
},
|
||||||
|
_ => ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
div(
|
||||||
|
class="elt-switch",
|
||||||
|
on:click=|event: MouseEvent| event.stop_propagation()
|
||||||
|
)
|
||||||
|
div(
|
||||||
|
class="elt",
|
||||||
|
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="elt-label") { (label) }
|
||||||
|
div(class="elt-rep") { (rep_components) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ul(class="constraints") {
|
||||||
|
Keyed(
|
||||||
|
list=elt.constraints.into_iter().collect::<Vec<_>>(),
|
||||||
|
view=move |c_key: usize| {
|
||||||
|
let c_state = use_context::<AppState>();
|
||||||
|
let assembly = &c_state.assembly;
|
||||||
|
let cst = assembly.constraints.with(|csts| csts[c_key].clone());
|
||||||
|
let other_arg = if cst.args.0 == key {
|
||||||
|
cst.args.1
|
||||||
|
} else {
|
||||||
|
cst.args.0
|
||||||
|
};
|
||||||
|
let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone());
|
||||||
|
view! {
|
||||||
|
li(class="cst") {
|
||||||
|
input(r#type="checkbox", bind:checked=cst.active)
|
||||||
|
div(class="cst-label") { (other_arg_label) }
|
||||||
|
div(class="cst-rep") { (cst.rep) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key=|c_key| c_key.clone()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key=|(key, elt)| (
|
||||||
|
key.clone(),
|
||||||
|
elt.id.clone(),
|
||||||
|
elt.label.clone(),
|
||||||
|
elt.constraints.clone()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,55 +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<f64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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 floating-point option, which is
|
|
||||||
// given a canonical specification
|
|
||||||
impl From<Option<f64>> for SpecifiedValue {
|
|
||||||
fn from(value: Option<f64>) -> Self {
|
|
||||||
match value {
|
|
||||||
Some(x) => SpecifiedValue{ spec: x.to_string(), value },
|
|
||||||
None => SpecifiedValue::from_empty_spec(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// a `SpecifiedValue` can be constructed from a specification string, formatted
|
|
||||||
// as described in the comment on the structure definition. the result is `Ok`
|
|
||||||
// if the specification is properly formatted, and `Error` if not
|
|
||||||
impl TryFrom<String> for SpecifiedValue {
|
|
||||||
type Error = ParseFloatError;
|
|
||||||
|
|
||||||
fn try_from(spec: String) -> Result<Self, Self::Error> {
|
|
||||||
if spec.is_empty() {
|
|
||||||
Ok(Self::from_empty_spec())
|
|
||||||
} else {
|
|
||||||
spec.parse::<f64>().map(
|
|
||||||
|value| Self { spec, value: Some(value) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
5
deploy/.gitignore
vendored
5
deploy/.gitignore
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
/dyna3.zip
|
|
||||||
/dyna3/index.html
|
|
||||||
/dyna3/dyna3-*.js
|
|
||||||
/dyna3/dyna3-*.wasm
|
|
||||||
/dyna3/main-*.css
|
|
||||||
|
|
@ -8,8 +8,7 @@ using Optim
|
||||||
|
|
||||||
export
|
export
|
||||||
rand_on_shell, Q, DescentHistory,
|
rand_on_shell, Q, DescentHistory,
|
||||||
realize_gram_gradient, realize_gram_newton, realize_gram_optim,
|
realize_gram_gradient, realize_gram_newton, realize_gram_optim, realize_gram
|
||||||
realize_gram_alt_proj, realize_gram
|
|
||||||
|
|
||||||
# === guessing ===
|
# === guessing ===
|
||||||
|
|
||||||
|
|
@ -144,7 +143,7 @@ function realize_gram_gradient(
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
# find the negative gradient of the loss function
|
# find negative gradient of loss function
|
||||||
neg_grad = 4*Q*L*Δ_proj
|
neg_grad = 4*Q*L*Δ_proj
|
||||||
slope = norm(neg_grad)
|
slope = norm(neg_grad)
|
||||||
dir = neg_grad / slope
|
dir = neg_grad / slope
|
||||||
|
|
@ -233,7 +232,7 @@ function realize_gram_newton(
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
# find the negative gradient of the loss function
|
# find the negative gradient of loss function
|
||||||
neg_grad = 4*Q*L*Δ_proj
|
neg_grad = 4*Q*L*Δ_proj
|
||||||
|
|
||||||
# find the negative Hessian of the loss function
|
# find the negative Hessian of the loss function
|
||||||
|
|
@ -314,129 +313,6 @@ function realize_gram_optim(
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# seek a matrix `L` for which `L'QL` matches the sparse matrix `gram` at every
|
|
||||||
# explicit entry of `gram`. use gradient descent starting from `guess`, with an
|
|
||||||
# alternate technique for finding the projected base step from the unprojected
|
|
||||||
# Hessian
|
|
||||||
function realize_gram_alt_proj(
|
|
||||||
gram::SparseMatrixCSC{T, <:Any},
|
|
||||||
guess::Matrix{T},
|
|
||||||
frozen = CartesianIndex[];
|
|
||||||
scaled_tol = 1e-30,
|
|
||||||
min_efficiency = 0.5,
|
|
||||||
backoff = 0.9,
|
|
||||||
reg_scale = 1.1,
|
|
||||||
max_descent_steps = 200,
|
|
||||||
max_backoff_steps = 110
|
|
||||||
) where T <: Number
|
|
||||||
# start history
|
|
||||||
history = DescentHistory{T}()
|
|
||||||
|
|
||||||
# find the dimension of the search space
|
|
||||||
dims = size(guess)
|
|
||||||
element_dim, construction_dim = dims
|
|
||||||
total_dim = element_dim * construction_dim
|
|
||||||
|
|
||||||
# list the constrained entries of the gram matrix
|
|
||||||
J, K, _ = findnz(gram)
|
|
||||||
constrained = zip(J, K)
|
|
||||||
|
|
||||||
# scale the tolerance
|
|
||||||
scale_adjustment = sqrt(T(length(constrained)))
|
|
||||||
tol = scale_adjustment * scaled_tol
|
|
||||||
|
|
||||||
# convert the frozen indices to stacked format
|
|
||||||
frozen_stacked = [(index[2]-1)*element_dim + index[1] for index in frozen]
|
|
||||||
|
|
||||||
# initialize search state
|
|
||||||
L = copy(guess)
|
|
||||||
Δ_proj = proj_diff(gram, L'*Q*L)
|
|
||||||
loss = dot(Δ_proj, Δ_proj)
|
|
||||||
|
|
||||||
# use Newton's method with backtracking and gradient descent backup
|
|
||||||
for step in 1:max_descent_steps
|
|
||||||
# stop if the loss is tolerably low
|
|
||||||
if loss < tol
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
# find the negative gradient of the loss function
|
|
||||||
neg_grad = 4*Q*L*Δ_proj
|
|
||||||
|
|
||||||
# find the negative Hessian of the loss function
|
|
||||||
hess = Matrix{T}(undef, total_dim, total_dim)
|
|
||||||
indices = [(j, k) for k in 1:construction_dim for j in 1:element_dim]
|
|
||||||
for (j, k) in indices
|
|
||||||
basis_mat = basis_matrix(T, j, k, dims)
|
|
||||||
neg_dΔ = basis_mat'*Q*L + L'*Q*basis_mat
|
|
||||||
neg_dΔ_proj = proj_to_entries(neg_dΔ, constrained)
|
|
||||||
deriv_grad = 4*Q*(-basis_mat*Δ_proj + L*neg_dΔ_proj)
|
|
||||||
hess[:, (k-1)*element_dim + j] = reshape(deriv_grad, total_dim)
|
|
||||||
end
|
|
||||||
hess_sym = Hermitian(hess)
|
|
||||||
push!(history.hess, hess_sym)
|
|
||||||
|
|
||||||
# regularize the Hessian
|
|
||||||
min_eigval = minimum(eigvals(hess_sym))
|
|
||||||
push!(history.positive, min_eigval > 0)
|
|
||||||
if min_eigval <= 0
|
|
||||||
hess -= reg_scale * min_eigval * I
|
|
||||||
end
|
|
||||||
|
|
||||||
# compute the Newton step
|
|
||||||
neg_grad_stacked = reshape(neg_grad, total_dim)
|
|
||||||
for k in frozen_stacked
|
|
||||||
neg_grad_stacked[k] = 0
|
|
||||||
hess[k, :] .= 0
|
|
||||||
hess[:, k] .= 0
|
|
||||||
hess[k, k] = 1
|
|
||||||
end
|
|
||||||
base_step_stacked = Hermitian(hess) \ neg_grad_stacked
|
|
||||||
base_step = reshape(base_step_stacked, dims)
|
|
||||||
push!(history.base_step, base_step)
|
|
||||||
|
|
||||||
# store the current position, loss, and slope
|
|
||||||
L_last = L
|
|
||||||
loss_last = loss
|
|
||||||
push!(history.scaled_loss, loss / scale_adjustment)
|
|
||||||
push!(history.neg_grad, neg_grad)
|
|
||||||
push!(history.slope, norm(neg_grad))
|
|
||||||
|
|
||||||
# find a good step size using backtracking line search
|
|
||||||
push!(history.stepsize, 0)
|
|
||||||
push!(history.backoff_steps, max_backoff_steps)
|
|
||||||
empty!(history.last_line_L)
|
|
||||||
empty!(history.last_line_loss)
|
|
||||||
rate = one(T)
|
|
||||||
step_success = false
|
|
||||||
base_target_improvement = dot(neg_grad, base_step)
|
|
||||||
for backoff_steps in 0:max_backoff_steps
|
|
||||||
history.stepsize[end] = rate
|
|
||||||
L = L_last + rate * base_step
|
|
||||||
Δ_proj = proj_diff(gram, L'*Q*L)
|
|
||||||
loss = dot(Δ_proj, Δ_proj)
|
|
||||||
improvement = loss_last - loss
|
|
||||||
push!(history.last_line_L, L)
|
|
||||||
push!(history.last_line_loss, loss / scale_adjustment)
|
|
||||||
if improvement >= min_efficiency * rate * base_target_improvement
|
|
||||||
history.backoff_steps[end] = backoff_steps
|
|
||||||
step_success = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
rate *= backoff
|
|
||||||
end
|
|
||||||
|
|
||||||
# if we've hit a wall, quit
|
|
||||||
if !step_success
|
|
||||||
return L_last, false, history
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# return the factorization and its history
|
|
||||||
push!(history.scaled_loss, loss / scale_adjustment)
|
|
||||||
L, loss < tol, history
|
|
||||||
end
|
|
||||||
|
|
||||||
# seek a matrix `L` for which `L'QL` matches the sparse matrix `gram` at every
|
# seek a matrix `L` for which `L'QL` matches the sparse matrix `gram` at every
|
||||||
# explicit entry of `gram`. use gradient descent starting from `guess`
|
# explicit entry of `gram`. use gradient descent starting from `guess`
|
||||||
function realize_gram(
|
function realize_gram(
|
||||||
|
|
@ -445,6 +321,7 @@ function realize_gram(
|
||||||
frozen = nothing;
|
frozen = nothing;
|
||||||
scaled_tol = 1e-30,
|
scaled_tol = 1e-30,
|
||||||
min_efficiency = 0.5,
|
min_efficiency = 0.5,
|
||||||
|
init_rate = 1.0,
|
||||||
backoff = 0.9,
|
backoff = 0.9,
|
||||||
reg_scale = 1.1,
|
reg_scale = 1.1,
|
||||||
max_descent_steps = 200,
|
max_descent_steps = 200,
|
||||||
|
|
@ -475,19 +352,20 @@ function realize_gram(
|
||||||
unfrozen_stacked = reshape(is_unfrozen, total_dim)
|
unfrozen_stacked = reshape(is_unfrozen, total_dim)
|
||||||
end
|
end
|
||||||
|
|
||||||
# initialize search state
|
# initialize variables
|
||||||
|
grad_rate = init_rate
|
||||||
L = copy(guess)
|
L = copy(guess)
|
||||||
Δ_proj = proj_diff(gram, L'*Q*L)
|
|
||||||
loss = dot(Δ_proj, Δ_proj)
|
|
||||||
|
|
||||||
# use Newton's method with backtracking and gradient descent backup
|
# use Newton's method with backtracking and gradient descent backup
|
||||||
|
Δ_proj = proj_diff(gram, L'*Q*L)
|
||||||
|
loss = dot(Δ_proj, Δ_proj)
|
||||||
for step in 1:max_descent_steps
|
for step in 1:max_descent_steps
|
||||||
# stop if the loss is tolerably low
|
# stop if the loss is tolerably low
|
||||||
if loss < tol
|
if loss < tol
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
# find the negative gradient of the loss function
|
# find the negative gradient of loss function
|
||||||
neg_grad = 4*Q*L*Δ_proj
|
neg_grad = 4*Q*L*Δ_proj
|
||||||
|
|
||||||
# find the negative Hessian of the loss function
|
# find the negative Hessian of the loss function
|
||||||
|
|
@ -542,7 +420,6 @@ function realize_gram(
|
||||||
empty!(history.last_line_loss)
|
empty!(history.last_line_loss)
|
||||||
rate = one(T)
|
rate = one(T)
|
||||||
step_success = false
|
step_success = false
|
||||||
base_target_improvement = dot(neg_grad, base_step)
|
|
||||||
for backoff_steps in 0:max_backoff_steps
|
for backoff_steps in 0:max_backoff_steps
|
||||||
history.stepsize[end] = rate
|
history.stepsize[end] = rate
|
||||||
L = L_last + rate * base_step
|
L = L_last + rate * base_step
|
||||||
|
|
@ -551,7 +428,7 @@ function realize_gram(
|
||||||
improvement = loss_last - loss
|
improvement = loss_last - loss
|
||||||
push!(history.last_line_L, L)
|
push!(history.last_line_L, L)
|
||||||
push!(history.last_line_loss, loss / scale_adjustment)
|
push!(history.last_line_loss, loss / scale_adjustment)
|
||||||
if improvement >= min_efficiency * rate * base_target_improvement
|
if improvement >= min_efficiency * rate * dot(neg_grad, base_step)
|
||||||
history.backoff_steps[end] = backoff_steps
|
history.backoff_steps[end] = backoff_steps
|
||||||
step_success = true
|
step_success = true
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -75,12 +75,3 @@ if success
|
||||||
println(" ", 1 / L[4,k], " sun")
|
println(" ", 1 / L[4,k], " sun")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# test an alternate technique for finding the projected base step from the
|
|
||||||
# unprojected Hessian
|
|
||||||
L_alt, success_alt, history_alt = Engine.realize_gram_alt_proj(gram, guess, frozen)
|
|
||||||
completed_gram_alt = L_alt'*Engine.Q*L_alt
|
|
||||||
println("\nDifference in result using alternate projection:\n")
|
|
||||||
display(completed_gram_alt - completed_gram)
|
|
||||||
println("\nDifference in steps: ", size(history_alt.scaled_loss, 1) - size(history.scaled_loss, 1))
|
|
||||||
println("Difference in loss: ", history_alt.scaled_loss[end] - history.scaled_loss[end], "\n")
|
|
||||||
|
|
@ -65,12 +65,3 @@ else
|
||||||
end
|
end
|
||||||
println("Steps: ", size(history.scaled_loss, 1))
|
println("Steps: ", size(history.scaled_loss, 1))
|
||||||
println("Loss: ", history.scaled_loss[end], "\n")
|
println("Loss: ", history.scaled_loss[end], "\n")
|
||||||
|
|
||||||
# test an alternate technique for finding the projected base step from the
|
|
||||||
# unprojected Hessian
|
|
||||||
L_alt, success_alt, history_alt = Engine.realize_gram_alt_proj(gram, guess, frozen)
|
|
||||||
completed_gram_alt = L_alt'*Engine.Q*L_alt
|
|
||||||
println("\nDifference in result using alternate projection:\n")
|
|
||||||
display(completed_gram_alt - completed_gram)
|
|
||||||
println("\nDifference in steps: ", size(history_alt.scaled_loss, 1) - size(history.scaled_loss, 1))
|
|
||||||
println("Difference in loss: ", history_alt.scaled_loss[end] - history.scaled_loss[end], "\n")
|
|
||||||
|
|
@ -94,12 +94,3 @@ if success
|
||||||
radius_ratio = dot(infty, Engine.Q * L[:,5]) / dot(infty, Engine.Q * L[:,6])
|
radius_ratio = dot(infty, Engine.Q * L[:,5]) / dot(infty, Engine.Q * L[:,6])
|
||||||
println("\nCircumradius / inradius: ", radius_ratio)
|
println("\nCircumradius / inradius: ", radius_ratio)
|
||||||
end
|
end
|
||||||
|
|
||||||
# test an alternate technique for finding the projected base step from the
|
|
||||||
# unprojected Hessian
|
|
||||||
L_alt, success_alt, history_alt = Engine.realize_gram_alt_proj(gram, guess, frozen)
|
|
||||||
completed_gram_alt = L_alt'*Engine.Q*L_alt
|
|
||||||
println("\nDifference in result using alternate projection:\n")
|
|
||||||
display(completed_gram_alt - completed_gram)
|
|
||||||
println("\nDifference in steps: ", size(history_alt.scaled_loss, 1) - size(history.scaled_loss, 1))
|
|
||||||
println("Difference in loss: ", history_alt.scaled_loss[end] - history.scaled_loss[end], "\n")
|
|
||||||
|
|
@ -41,25 +41,3 @@ I will have to work out formulas for the Euclidean distance between two entities
|
||||||
In this vein, it seems as though if J1 and J2 are the reps of two points, then Q(J1,J2) = d^2/2. So then the sphere centered at J1 through J2 is (J1-(2Q(J1,J2),0,0,0,0))/sqrt(2Q(J1,J2)). Ugh has a sqrt in it. Similarly for sphere centered at J3 through J2, (J3-(2Q(J3,J2),0000))/sqrt(2Q(J3,J2)). J1,J2,J3 are collinear if these spheres are tangent, i.e. if those vectors have Q-inner-product 1, which is to say Q(J1,J3) - Q(J1,J2) - Q(J3,J2) = 2sqrt(Q(J1,J2)Q(J2,J3)). But maybe that's not the simplest way of putting it. After all, we can just say that the cross-product of the two differences is 0; that has no square roots in it.
|
In this vein, it seems as though if J1 and J2 are the reps of two points, then Q(J1,J2) = d^2/2. So then the sphere centered at J1 through J2 is (J1-(2Q(J1,J2),0,0,0,0))/sqrt(2Q(J1,J2)). Ugh has a sqrt in it. Similarly for sphere centered at J3 through J2, (J3-(2Q(J3,J2),0000))/sqrt(2Q(J3,J2)). J1,J2,J3 are collinear if these spheres are tangent, i.e. if those vectors have Q-inner-product 1, which is to say Q(J1,J3) - Q(J1,J2) - Q(J3,J2) = 2sqrt(Q(J1,J2)Q(J2,J3)). But maybe that's not the simplest way of putting it. After all, we can just say that the cross-product of the two differences is 0; that has no square roots in it.
|
||||||
|
|
||||||
One conceivable way to canonicalize lines is to use the *perpendicular* plane that goes through the origin, that's uniquely defined, and anyway just amounts to I = (0,0,d) where d is the ordinary direction vector of the line; and a point J in that plane that the line goes through, which just amounts to J=(r^2,1,E) with Q(I,J) = 0, i.e. E\dot d = 0. It's also the point on the line closest to the origin. The reason that we don't usually use that point as the companion to the direction vector is that the resulting set of six coordinates is not homogeneous. But here that's not an issue, since we have our standard point coordinates and plane coordinates; and for a plane through the origin, only two of the direction coordinates are really free, and then we have the one dot-product relation, so only two of the point coordinates are really free, giving us the correct dimensionality of 4 for the set of lines. So in some sense this says that we could take naively as coordinates for a line the projection of the unit direction vector to the xy plane and the projection of the line's closest point to the origin to the xy plane. That doesn't seem to have any weird gimbal locks or discontinuities or anything. And with these coordinates, you can test if the point E=x,y,z is on the line (dx,dy,cx,cy) by extending (dx,dy) to d via dz = sqrt(1-dx^2 - dy^2), extending (cx,cy) to c by determining cz via d\dot c = 0, and then checking if d\cross(E-c) = 0. And you can see if two lines are parallel just by checking if they have the same direction vector, and if not, you can see if they are coplanar by projecting both of their closest points perpendicularly onto the line in the direction of the cross product of their directions, and if the projections match they are coplanar.
|
One conceivable way to canonicalize lines is to use the *perpendicular* plane that goes through the origin, that's uniquely defined, and anyway just amounts to I = (0,0,d) where d is the ordinary direction vector of the line; and a point J in that plane that the line goes through, which just amounts to J=(r^2,1,E) with Q(I,J) = 0, i.e. E\dot d = 0. It's also the point on the line closest to the origin. The reason that we don't usually use that point as the companion to the direction vector is that the resulting set of six coordinates is not homogeneous. But here that's not an issue, since we have our standard point coordinates and plane coordinates; and for a plane through the origin, only two of the direction coordinates are really free, and then we have the one dot-product relation, so only two of the point coordinates are really free, giving us the correct dimensionality of 4 for the set of lines. So in some sense this says that we could take naively as coordinates for a line the projection of the unit direction vector to the xy plane and the projection of the line's closest point to the origin to the xy plane. That doesn't seem to have any weird gimbal locks or discontinuities or anything. And with these coordinates, you can test if the point E=x,y,z is on the line (dx,dy,cx,cy) by extending (dx,dy) to d via dz = sqrt(1-dx^2 - dy^2), extending (cx,cy) to c by determining cz via d\dot c = 0, and then checking if d\cross(E-c) = 0. And you can see if two lines are parallel just by checking if they have the same direction vector, and if not, you can see if they are coplanar by projecting both of their closest points perpendicularly onto the line in the direction of the cross product of their directions, and if the projections match they are coplanar.
|
||||||
|
|
||||||
#### Engine Conventions
|
|
||||||
|
|
||||||
The coordinate conventions used in the engine are different from the ones used in these notes. Marking the engine vectors and coordinates with $'$, we have
|
|
||||||
$$I' = (x', y', z', b', c'),$$
|
|
||||||
where
|
|
||||||
$$
|
|
||||||
\begin{align*}
|
|
||||||
x' & = x & b' & = b/2 \\
|
|
||||||
y' & = y & c' & = c/2. \\
|
|
||||||
z' & = z
|
|
||||||
\end{align*}
|
|
||||||
$$
|
|
||||||
The engine uses the quadratic form $Q' = -Q$, which is expressed in engine coordinates as
|
|
||||||
$$Q'(I'_1, I'_2) = x'_1 x'_2 + y'_1 y'_2 + z'_1 z'_2 - 2(b'_1c'_2 + c'_1 b'_2).$$
|
|
||||||
In the `engine` module, the matrix of $Q'$ is encoded in the lazy static variable `Q`.
|
|
||||||
|
|
||||||
In the engine's coordinate conventions, a sphere with radius $r > 0$ centered on $P = (P_x, P_y, P_z)$ is represented by the vector
|
|
||||||
$$I'_s = \left(\frac{P_x}{r}, \frac{P_y}{r}, \frac{P_z}{r}, \frac1{2r}, \frac{\|P\|^2 - r^2}{2r}\right),$$
|
|
||||||
which has the normalization $Q'(I'_s, I'_s) = 1$. The point $P$ is represented by the vector
|
|
||||||
$$I'_P = \left(P_x, P_y, P_z, \frac{1}{2}, \frac{\|P\|^2}{2}\right).$$
|
|
||||||
In the `engine` module, these formulas are encoded in the `sphere` and `point` functions.
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ His final mathematical advice was reasonably encouraging, however:
|
||||||
"But still I would consider it all more or less doable. One should very precisely think about a doable scope.
|
"But still I would consider it all more or less doable. One should very precisely think about a doable scope.
|
||||||
I think three things are essential for the math no matter what you exactly plan.
|
I think three things are essential for the math no matter what you exactly plan.
|
||||||
|
|
||||||
1. Think projectively.
|
1. Think projectively,
|
||||||
Use Projective Geometry, Homogeneous Coordinates (or to a certain extent Quaternions, and Clifford Algebras, which are more or less an elegant way to merge Complex numbers with projective concepts.)
|
Use Projective Geometry, Homogeneous Coordinates (or to a certain extent Quaternions, and Clifford Algebras, which are more or less an elegant way to merge Complex numbers with projective concepts.)
|
||||||
2. Consider ambient complex spaces.
|
2. Consider ambient complex spaces.
|
||||||
The true nature of the objects can only be understood if embedded into a complex ambient space.
|
The true nature of the objects can only be understood if embedded into a complex ambient space.
|
||||||
|
|
@ -42,3 +42,5 @@ CindyJS uses very concrete basic objects: 2D points are represented via projecti
|
||||||
Lines are given by explicit coordinates as well (not sure of the internal details/exact coordinatization, or of how a "LineThrough" is represented).
|
Lines are given by explicit coordinates as well (not sure of the internal details/exact coordinatization, or of how a "LineThrough" is represented).
|
||||||
|
|
||||||
Was unclear to me how the complex parametrization for preserving continuity was handled in the code, even though Jürgen harps on complex ambient spaces; where are the complex numbers? Perhaps that part of Cinderella was never re-implemented?
|
Was unclear to me how the complex parametrization for preserving continuity was handled in the code, even though Jürgen harps on complex ambient spaces; where are the complex numbers? Perhaps that part of Cinderella was never re-implemented?
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,5 @@
|
||||||
<body><script type="module" src="dyna3.js"></script>
|
<body><script type="module" src="dyna3.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
# set paths. this technique for getting the script location comes from
|
|
||||||
# `mklement0` on Stack Overflow
|
|
||||||
#
|
|
||||||
# https://stackoverflow.com/a/24114056
|
|
||||||
#
|
|
||||||
TOOLS=$(dirname -- $0)
|
|
||||||
SRC="$TOOLS/../app-proto/dist"
|
|
||||||
DEST="$TOOLS/../deploy/dyna3"
|
|
||||||
|
|
||||||
# remove the old hash-named files
|
|
||||||
[ -e "$DEST"/dyna3-*.js ] && rm "$DEST"/dyna3-*.js
|
|
||||||
[ -e "$DEST"/dyna3-*.wasm ] && rm "$DEST"/dyna3-*.wasm
|
|
||||||
[ -e "$DEST"/main-*.css ] && rm "$DEST"/main-*.css
|
|
||||||
|
|
||||||
# copy the distribution
|
|
||||||
cp -r "$SRC/." "$DEST"
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
# run all Cargo examples, as described here:
|
|
||||||
#
|
|
||||||
# Karol Kuczmarski. "Add examples to your Rust libraries"
|
|
||||||
# http://xion.io/post/code/rust-examples.html
|
|
||||||
#
|
|
||||||
# you should invoke this script by calling `sh` or another interpreter, rather
|
|
||||||
# than calling `souce`, to ensure that the script can find the manifest file for
|
|
||||||
# the application prototype
|
|
||||||
|
|
||||||
# find the manifest file for the application prototype
|
|
||||||
MANIFEST="$(dirname -- $0)/../app-proto/Cargo.toml"
|
|
||||||
|
|
||||||
# set up the command that runs each example
|
|
||||||
RUN_EXAMPLE="cargo run --manifest-path $MANIFEST --example"
|
|
||||||
|
|
||||||
# run the examples
|
|
||||||
$RUN_EXAMPLE irisawa-hexlet; echo
|
|
||||||
$RUN_EXAMPLE three-spheres; echo
|
|
||||||
$RUN_EXAMPLE point-on-sphere; echo
|
|
||||||
$RUN_EXAMPLE kaleidocycle
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue