Compare commits

..

23 commits

Author SHA1 Message Date
Aaron Fenyes
7d6a394156 Name element column index more descriptively 2024-11-14 01:18:20 -08:00
Aaron Fenyes
a48fef3641 Comment example assembly chooser
Also, add a way to load the empty assembly.
2024-11-14 00:55:33 -08:00
Aaron Fenyes
2b083be998 Explain emoji icon kludge 2024-11-14 00:16:32 -08:00
Aaron Fenyes
0c8022d78e Give CSS colors meaningful names 2024-11-14 00:07:02 -08:00
Aaron Fenyes
3f3c1739cb Write out "element" in HTML element classes
Write out "representation" too.
2024-11-13 16:40:39 -08:00
Aaron Fenyes
882286c0e3 Write out "constraint" in HTML element classes 2024-11-13 16:36:03 -08:00
Aaron Fenyes
b3470b597d Make Element::index private 2024-11-12 23:51:37 -08:00
Aaron Fenyes
102f400553 Switch font to Fira Sans
It has tabular numbers, and it's nice and big too.
2024-11-12 23:33:25 -08:00
Aaron Fenyes
d223df869c Stop Assembly::realize from reacting to itself
Previously, `realize` both tracked and updated the element vectors, so
calling it in a reactive context could start a feedback loop.
2024-11-12 23:33:25 -08:00
Aaron Fenyes
b94280c456 Test representation validity in realization effect 2024-11-12 23:33:25 -08:00
Aaron Fenyes
92b91df435 Use tabular numbers for element vectors 2024-11-12 23:33:25 -08:00
Aaron Fenyes
33dd5dbe82 Make element vectors reactive 2024-11-12 23:33:25 -08:00
Aaron Fenyes
037a0c376f Add an element constructor 2024-11-12 23:33:25 -08:00
Aaron Fenyes
ec1911b889 Simplify memos 2024-11-12 23:33:25 -08:00
Aaron Fenyes
ce9b114dd6 Render constraint lists dynamically 2024-11-12 23:33:25 -08:00
Aaron Fenyes
cc126fc527 Correct typo in comment 2024-11-12 23:33:25 -08:00
Aaron Fenyes
70978c640b Include vector representation in element diff key 2024-11-12 23:33:25 -08:00
Aaron Fenyes
db0a8c2da8 Round element vectors to three decimal places 2024-11-12 23:33:25 -08:00
Aaron Fenyes
4ecb39e73a Specify fonts
This should help the interface look more consistent across platforms.
The font choices are just placeholders: consistency is the main goal.
2024-11-12 23:33:25 -08:00
Aaron Fenyes
7361f1a721 Flag constraints with invalid input 2024-11-12 23:33:25 -08:00
Aaron Fenyes
f2f73b31cf Update title and authors 2024-11-12 23:33:25 -08:00
Aaron Fenyes
dcf5764993 Factor out element outline item 2024-11-12 23:33:25 -08:00
Aaron Fenyes
d526ac9f1f Factor out constraint outline item 2024-11-12 23:33:25 -08:00
37 changed files with 1384 additions and 5918 deletions

View file

@ -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

View file

@ -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
View file

@ -1,2 +1,8 @@
ci-bin node_modules
site
docbuild
__tests__
coverage
dyna3.zip
tmpproj
*~ *~

View file

@ -17,71 +17,3 @@ Note that currently this is just the barest beginnings of the project, more of a
* Able to run in browser (so implemented in WASM-compatible language) * 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
6. Add the `.cargo/bin` folder in your home directory to your executable search path
- This lets you call Trunk, and other tools installed by Cargo, without specifying their paths
- On POSIX systems, the search path is stored in the `PATH` environment variable
### Play with the prototype
1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype
- The crates the prototype depends on will be downloaded and served automatically
- For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag
- If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]` from there instead.
3. In a web browser, visit one of the URLs listed under the message `INFO 📡 server listening at:`
- Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype
4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype
### Run the engine on some example problems
1. Use `sh` to run the script `tools/run-examples.sh`
- The script is location-independent, so you can do this from anywhere in the dyna3 repository
- The call from the top level of the repository is:
```bash
sh tools/run-examples.sh
```
- For each example problem, the engine will print the value of the loss function at each optimization step
- The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then
```julia
include("irisawa-hexlet.jl")
for (step, scaled_loss) in enumerate(history_alt.scaled_loss)
println(rpad(step-1, 4), " | ", scaled_loss)
end
```
you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show
### Run the automated tests
1. Go into the `app-proto` folder
2. Call `cargo test`
### Deploy the prototype
1. From the `app-proto` folder, call `trunk build --release`
- Building in [release mode](https://doc.rust-lang.org/cargo/reference/profiles.html#release) produces an executable which is smaller and often much faster, but harder to debug and more time-consuming to build
- If you want to stay in the top-level folder, you can call `trunk build --config app-proto --release` from there instead
2. Use `sh` to run the packaging script `tools/package-for-deployment.sh`.
- The script is location-independent, so you can do this from anywhere in the dyna3 repository
- The call from the top level of the repository is:
```bash
sh tools/package-for-deployment.sh
```
- This will overwrite or replace the files in `deploy/dyna3`
3. Put the contents of `deploy/dyna3` in the folder on your server that the prototype will be served from.
- To simplify uploading, you might want to combine these files into an archive called `deploy/dyna3.zip`. Git has been set to ignore this path

1325
app-proto/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,22 +3,18 @@ name = "dyna3"
version = "0.1.0" version = "0.1.0"
authors = ["Aaron Fenyes", "Glen Whitney"] authors = ["Aaron Fenyes", "Glen Whitney"]
edition = "2021" edition = "2021"
rust-version = "1.86"
[features] [features]
default = ["console_error_panic_hook"] default = ["console_error_panic_hook"]
dev = []
[dependencies] [dependencies]
itertools = "0.13.0" itertools = "0.13.0"
js-sys = "0.3.70" js-sys = "0.3.70"
lazy_static = "1.5.0" lazy_static = "1.5.0"
nalgebra = "0.33.0" nalgebra = "0.33.0"
readonly = "0.2.12" 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
@ -29,7 +25,6 @@ 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', 'HtmlInputElement',
'Performance', 'Performance',
@ -41,41 +36,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

View file

@ -1,2 +0,0 @@
[build]
public_url = "./"

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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());
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -6,12 +6,6 @@
<link data-trunk rel="css" href="main.css"/> <link data-trunk rel="css" href="main.css"/>
<link href="https://fonts.bunny.net/css?family=fira-sans:ital,wght@0,400;1,400&display=swap" rel="stylesheet"> <link href="https://fonts.bunny.net/css?family=fira-sans:ital,wght@0,400;1,400&display=swap" rel="stylesheet">
<link href="https://fonts.bunny.net/css?family=noto-emoji:wght@400&text=%f0%9f%94%97%e2%9a%a0&display=swap" rel="stylesheet"> <link href="https://fonts.bunny.net/css?family=noto-emoji:wght@400&text=%f0%9f%94%97%e2%9a%a0&display=swap" rel="stylesheet">
<!--
the Charming visualization crate, which we use to show engine diagnostics,
depends the ECharts JavaScript package
-->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
</head> </head>
<body></body> <body></body>
</html> </html>

View file

@ -3,8 +3,7 @@
--text-bright: white; --text-bright: white;
--text-invalid: #f58fc2; /* bright pink */ --text-invalid: #f58fc2; /* bright pink */
--border: #555; /* light gray */ --border: #555; /* light gray */
--border-focus-dark: #aaa; /* bright gray */ --border-focus: #aaa; /* bright gray */
--border-focus-light: white;
--border-invalid: #70495c; /* dusky pink */ --border-invalid: #70495c; /* dusky pink */
--selection-highlight: #444; /* medium gray */ --selection-highlight: #444; /* medium gray */
--page-background: #222; /* dark gray */ --page-background: #222; /* dark gray */
@ -18,24 +17,13 @@ body {
font-family: 'Fira Sans', sans-serif; font-family: 'Fira Sans', sans-serif;
} }
.invalid {
color: var(--text-invalid);
}
.status {
width: 20px;
text-align: center;
font-family: 'Noto Emoji';
font-style: normal;
}
/* sidebar */ /* sidebar */
#sidebar { #sidebar {
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;
@ -53,7 +41,9 @@ body {
} }
#add-remove > button { #add-remove > button {
width: 32px;
height: 32px; height: 32px;
font-size: large;
} }
/* KLUDGE */ /* KLUDGE */
@ -62,9 +52,7 @@ body {
buttons need to be displayed in an emoji font buttons need to be displayed in an emoji font
*/ */
#add-remove > button.emoji { #add-remove > button.emoji {
width: 32px;
font-family: 'Noto Emoji', sans-serif; font-family: 'Noto Emoji', sans-serif;
font-size: large;
} }
/* outline */ /* outline */
@ -89,22 +77,18 @@ summary.selected {
background-color: var(--selection-highlight); background-color: var(--selection-highlight);
} }
summary > div, .regulator { summary > div, .constraint {
padding-top: 4px; padding-top: 4px;
padding-bottom: 4px; padding-bottom: 4px;
} }
.element, .regulator { .element, .constraint {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
padding-left: 8px; padding-left: 8px;
padding-right: 8px; padding-right: 8px;
} }
.element > input {
margin-left: 8px;
}
.element-switch { .element-switch {
width: 18px; width: 18px;
padding-left: 2px; padding-left: 2px;
@ -123,7 +107,7 @@ details[open]:has(li) .element-switch::after {
flex-grow: 1; flex-grow: 1;
} }
.regulator-label { .constraint-label {
flex-grow: 1; flex-grow: 1;
} }
@ -139,88 +123,45 @@ details[open]:has(li) .element-switch::after {
width: 56px; width: 56px;
} }
.regulator { .constraint {
font-style: italic; font-style: italic;
} }
.regulator-type { .constraint.invalid {
padding: 2px 8px 0px 8px; color: var(--text-invalid);
font-size: 10pt;
} }
.regulator-input { .constraint > input[type=checkbox] {
margin-right: 4px; margin: 0px 8px 0px 0px;
}
.constraint > input[type=text] {
color: inherit; color: inherit;
background-color: inherit; background-color: inherit;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 2px; border-radius: 2px;
} }
.regulator-input::placeholder { .constraint.invalid > input[type=text] {
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); border-color: var(--border-invalid);
} }
.regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after { .status {
width: 20px;
padding-left: 4px;
text-align: center;
font-family: 'Noto Emoji';
font-style: normal;
}
.invalid > .status::after, details:has(.invalid):not([open]) .status::after {
content: '⚠'; content: '⚠';
color: var(--text-invalid); color: var(--text-invalid);
} }
/* diagnostics */
#diagnostics {
margin: 10px;
}
#diagnostics-bar {
display: flex;
}
#realization-status {
display: flex;
flex-grow: 1;
}
#realization-status .status {
margin-right: 4px;
}
#realization-status :not(.status) {
flex-grow: 1;
}
#realization-status .status::after {
content: '✓';
}
#realization-status.invalid .status::after {
content: '⚠';
}
.diagnostics-panel {
margin-top: 10px;
min-height: 180px;
}
.diagnostics-chart {
background-color: var(--display-background);
border: 1px solid var(--border);
border-radius: 8px;
}
/* display */ /* display */
#display { canvas {
float: left; float: left;
margin-left: 20px; margin-left: 20px;
margin-top: 20px; margin-top: 20px;
@ -229,12 +170,6 @@ details[open]:has(li) .element-switch::after {
border-radius: 16px; border-radius: 16px;
} }
#display:focus { canvas:focus {
border-color: var(--border-focus-dark); border-color: var(--border-focus);
outline: none;
}
input:focus {
border-color: var(--border-focus-light);
outline: none;
} }

8
app-proto/run-examples Executable file
View file

@ -0,0 +1,8 @@
# based on "Enabling print statements in Cargo tests", by Jon Almeida
#
# https://jonalmeida.com/posts/2015/01/23/print-cargo/
#
cargo test -- --nocapture engine::tests::irisawa_hexlet_test
cargo test -- --nocapture engine::tests::three_spheres_example
cargo test -- --nocapture engine::tests::point_on_sphere_example

240
app-proto/src/add_remove.rs Normal file
View file

@ -0,0 +1,240 @@
use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue};
use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}};
/* DEBUG */
// load an example assembly for testing. this code will be removed once we've
// built a more formal test assembly system
fn load_gen_assemb(assembly: &Assembly) {
let _ = assembly.try_insert_element(
Element::new(
String::from("gemini_a"),
String::from("Castor"),
[1.00_f32, 0.25_f32, 0.00_f32],
engine::sphere(0.5, 0.5, 0.0, 1.0)
)
);
let _ = assembly.try_insert_element(
Element::new(
String::from("gemini_b"),
String::from("Pollux"),
[0.00_f32, 0.25_f32, 1.00_f32],
engine::sphere(-0.5, -0.5, 0.0, 1.0)
)
);
let _ = assembly.try_insert_element(
Element::new(
String::from("ursa_major"),
String::from("Ursa major"),
[0.25_f32, 0.00_f32, 1.00_f32],
engine::sphere(-0.5, 0.5, 0.0, 0.75)
)
);
let _ = assembly.try_insert_element(
Element::new(
String::from("ursa_minor"),
String::from("Ursa minor"),
[0.25_f32, 1.00_f32, 0.00_f32],
engine::sphere(0.5, -0.5, 0.0, 0.5)
)
);
let _ = assembly.try_insert_element(
Element::new(
String::from("moon_deimos"),
String::from("Deimos"),
[0.75_f32, 0.75_f32, 0.00_f32],
engine::sphere(0.0, 0.15, 1.0, 0.25)
)
);
let _ = assembly.try_insert_element(
Element::new(
String::from("moon_phobos"),
String::from("Phobos"),
[0.00_f32, 0.75_f32, 0.50_f32],
engine::sphere(0.0, -0.15, -1.0, 0.25)
)
);
}
/* DEBUG */
// load an example assembly for testing. this code will be removed once we've
// built a more formal test assembly system
fn load_low_curv_assemb(assembly: &Assembly) {
let a = 0.75_f64.sqrt();
let _ = assembly.try_insert_element(
Element::new(
"central".to_string(),
"Central".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, 0.0, 0.0, 1.0)
)
);
let _ = assembly.try_insert_element(
Element::new(
"assemb_plane".to_string(),
"Assembly plane".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0)
)
);
let _ = assembly.try_insert_element(
Element::new(
"side1".to_string(),
"Side 1".to_string(),
[1.00_f32, 0.00_f32, 0.25_f32],
engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0)
)
);
let _ = assembly.try_insert_element(
Element::new(
"side2".to_string(),
"Side 2".to_string(),
[0.25_f32, 1.00_f32, 0.00_f32],
engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0)
)
);
let _ = assembly.try_insert_element(
Element::new(
"side3".to_string(),
"Side 3".to_string(),
[0.00_f32, 0.25_f32, 1.00_f32],
engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0)
)
);
let _ = assembly.try_insert_element(
Element::new(
"corner1".to_string(),
"Corner 1".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0)
)
);
let _ = assembly.try_insert_element(
Element::new(
"corner2".to_string(),
"Corner 2".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0)
)
);
let _ = assembly.try_insert_element(
Element::new(
String::from("corner3"),
String::from("Corner 3"),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0)
)
);
}
#[component]
pub fn AddRemove() -> View {
/* DEBUG */
let assembly_name = create_signal("general".to_string());
create_effect(move || {
// get name of chosen assembly
let name = assembly_name.get_clone();
console::log_1(
&JsValue::from(format!("Showing assembly \"{}\"", name.clone()))
);
batch(|| {
let state = use_context::<AppState>();
let assembly = &state.assembly;
// clear state
assembly.elements.update(|elts| elts.clear());
assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear());
state.selection.update(|sel| sel.clear());
// load assembly
match name.as_str() {
"general" => load_gen_assemb(assembly),
"low-curv" => load_low_curv_assemb(assembly),
_ => ()
};
});
});
view! {
div(id="add-remove") {
button(
on:click=|_| {
let state = use_context::<AppState>();
state.assembly.insert_new_element();
/* DEBUG */
// print updated list of elements by identifier
console::log_1(&JsValue::from("elements by identifier:"));
for (id, key) in state.assembly.elements_by_id.get_clone().iter() {
console::log_3(
&JsValue::from(" "),
&JsValue::from(id),
&JsValue::from(*key)
);
}
}
) { "+" }
button(
class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button
disabled={
let state = use_context::<AppState>();
state.selection.with(|sel| sel.len() != 2)
},
on:click=|_| {
let state = use_context::<AppState>();
let subjects = state.selection.with(
|sel| {
let subject_vec: Vec<_> = sel.into_iter().collect();
(subject_vec[0].clone(), subject_vec[1].clone())
}
);
let lorentz_prod = create_signal(0.0);
let lorentz_prod_valid = create_signal(false);
let active = create_signal(true);
state.assembly.insert_constraint(Constraint {
subjects: subjects,
lorentz_prod: lorentz_prod,
lorentz_prod_text: create_signal(String::new()),
lorentz_prod_valid: lorentz_prod_valid,
active: active,
});
state.selection.update(|sel| sel.clear());
/* DEBUG */
// print updated constraint list
console::log_1(&JsValue::from("Constraints:"));
state.assembly.constraints.with(|csts| {
for (_, cst) in csts.into_iter() {
console::log_5(
&JsValue::from(" "),
&JsValue::from(cst.subjects.0),
&JsValue::from(cst.subjects.1),
&JsValue::from(":"),
&JsValue::from(cst.lorentz_prod.get_untracked())
);
}
});
// update the realization when the constraint becomes active
// and valid, or is edited while active and valid
create_effect(move || {
console::log_1(&JsValue::from(
format!("Constraint ({}, {}) updated", subjects.0, subjects.1)
));
lorentz_prod.track();
if active.get() && lorentz_prod_valid.get() {
state.assembly.realize();
}
});
}
) { "🔗" }
select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser
option(value="general") { "General" }
option(value="low-curv") { "Low-curvature" }
option(value="empty") { "Empty" }
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,5 +0,0 @@
pub mod add_remove;
pub mod diagnostics;
pub mod display;
pub mod outline;
pub mod test_assembly_chooser;

View file

@ -1,69 +0,0 @@
use std::rc::Rc;
use sycamore::prelude::*;
use super::test_assembly_chooser::TestAssemblyChooser;
use crate::{
AppState,
assembly::{InversiveDistanceRegulator, Point, Sphere},
};
#[component]
pub fn AddRemove() -> View {
view! {
div(id = "add-remove") {
button(
on:click = |_| {
let state = use_context::<AppState>();
batch(|| {
// this call is batched to avoid redundant realizations.
// it updates the element list and the regulator list,
// which are both tracked by the realization effect
/* TO DO */
// it would make more to do the batching inside
// `insert_element_default`, but that will have to wait
// until Sycamore handles nested batches correctly.
//
// https://github.com/sycamore-rs/sycamore/issues/802
//
// the nested batch issue is relevant here because the
// assembly loaders in the test assembly chooser use
// `insert_element_default` within larger batches
state.assembly.insert_element_default::<Sphere>();
});
}
) { "Add sphere" }
button(
on:click = |_| {
let state = use_context::<AppState>();
state.assembly.insert_element_default::<Point>();
}
) { "Add point" }
button(
class = "emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button
disabled = {
let state = use_context::<AppState>();
state.selection.with(|sel| sel.len() != 2)
},
on:click = |_| {
let state = use_context::<AppState>();
let subjects: [_; 2] = state.selection.with(
// the button is only enabled when two elements are
// selected, so we know the cast to a two-element array
// will succeed
|sel| sel
.clone()
.into_iter()
.collect::<Vec<_>>()
.try_into()
.unwrap()
);
state.assembly.insert_regulator(
Rc::new(InversiveDistanceRegulator::new(subjects))
);
state.selection.update(|sel| sel.clear());
}
) { "🔗" }
TestAssemblyChooser {}
}
}
}

View file

@ -1,256 +0,0 @@
use charming::{
Chart,
WasmRenderer,
component::{Axis, DataZoom, Grid},
element::{AxisType, Symbol},
series::{Line, Scatter},
};
use sycamore::prelude::*;
use crate::AppState;
#[derive(Clone)]
struct DiagnosticsState {
active_tab: Signal<String>,
}
impl DiagnosticsState {
fn new(initial_tab: String) -> Self {
Self { active_tab: create_signal(initial_tab) }
}
}
// a realization status indicator
#[component]
fn RealizationStatus() -> View {
let state = use_context::<AppState>();
let realization_status = state.assembly.realization_status;
view! {
div(
id = "realization-status",
class = realization_status.with(
|status| match status {
Ok(_) => "",
Err(_) => "invalid",
}
)
) {
div(class = "status")
div {
(realization_status.with(
|status| match status {
Ok(_) => "Target accuracy achieved".to_string(),
Err(message) => message.clone(),
}
))
}
}
}
}
fn into_log10_time_point((step, value): (usize, f64)) -> Vec<Option<f64>> {
vec![
Some(step as f64),
if value == 0.0 { None } else { Some(value.abs().log10()) },
]
}
// the loss history from the last realization
#[component]
fn LossHistory() -> View {
const CONTAINER_ID: &str = "loss-history";
let state = use_context::<AppState>();
let renderer = WasmRenderer::new_opt(None, Some(178));
on_mount(move || {
create_effect(move || {
// get the loss history
let scaled_loss: Vec<_> = state.assembly.descent_history.with(
|history| history.scaled_loss
.iter()
.enumerate()
.map(|(step, &loss)| (step, loss))
.map(into_log10_time_point)
.collect()
);
// initialize the chart axes
let step_axis = Axis::new()
.type_(AxisType::Category)
.boundary_gap(false);
let scaled_loss_axis = Axis::new();
// load the chart data. when there's no history, we load the data
// point (0, None) to clear the chart. it would feel more natural to
// load empty data vectors, but that turns out not to clear the
// chart: it instead leads to previous data being re-used
let scaled_loss_series = Line::new().data(
if scaled_loss.len() > 0 {
scaled_loss
} else {
vec![vec![Some(0.0), None::<f64>]]
}
);
let chart = Chart::new()
.animation(false)
.data_zoom(DataZoom::new().y_axis_index(0).right(40))
.x_axis(step_axis)
.y_axis(scaled_loss_axis)
.grid(Grid::new().top(20).right(80).bottom(30).left(60))
.series(scaled_loss_series);
renderer.render(CONTAINER_ID, &chart).unwrap();
});
});
view! {
div(id = CONTAINER_ID, class = "diagnostics-chart")
}
}
// the spectrum of the Hessian during the last realization
#[component]
fn SpectrumHistory() -> View {
const CONTAINER_ID: &str = "spectrum-history";
let state = use_context::<AppState>();
let renderer = WasmRenderer::new(478, 178);
on_mount(move || {
create_effect(move || {
// get the spectrum of the Hessian at each step, split into its
// positive, negative, and strictly-zero parts
let (
hess_eigvals_zero,
hess_eigvals_nonzero,
): (Vec<_>, Vec<_>) = state.assembly.descent_history.with(
|history| history.hess_eigvals
.iter()
.enumerate()
.map(
|(step, eigvals)| eigvals.iter().map(
move |&val| (step, val)
)
)
.flatten()
.partition(|&(_, val)| val == 0.0)
);
let zero_level = hess_eigvals_nonzero
.iter()
.map(|(_, val)| val.abs())
.reduce(f64::min)
.map(|val| 0.1 * val)
.unwrap_or(1.0);
let (
hess_eigvals_pos,
hess_eigvals_neg,
): (Vec<_>, Vec<_>) = hess_eigvals_nonzero
.into_iter()
.partition(|&(_, val)| val > 0.0);
// initialize the chart axes
let step_axis = Axis::new()
.type_(AxisType::Category)
.boundary_gap(false);
let eigval_axis = Axis::new();
// load the chart data. when there's no history, we load the data
// point (0, None) to clear the chart. it would feel more natural to
// load empty data vectors, but that turns out not to clear the
// chart: it instead leads to previous data being re-used
let eigval_series_pos = Scatter::new()
.symbol_size(4.5)
.data(
if hess_eigvals_pos.len() > 0 {
hess_eigvals_pos
.into_iter()
.map(into_log10_time_point)
.collect()
} else {
vec![vec![Some(0.0), None::<f64>]]
}
);
let eigval_series_neg = Scatter::new()
.symbol(Symbol::Diamond)
.symbol_size(6.0)
.data(
if hess_eigvals_neg.len() > 0 {
hess_eigvals_neg
.into_iter()
.map(into_log10_time_point)
.collect()
} else {
vec![vec![Some(0.0), None::<f64>]]
}
);
let eigval_series_zero = Scatter::new()
.symbol(Symbol::Triangle)
.symbol_size(5.0)
.data(
if hess_eigvals_zero.len() > 0 {
hess_eigvals_zero
.into_iter()
.map(|(step, _)| (step, zero_level))
.map(into_log10_time_point)
.collect()
} else {
vec![vec![Some(0.0), None::<f64>]]
}
);
let chart = Chart::new()
.animation(false)
.data_zoom(DataZoom::new().y_axis_index(0).right(40))
.x_axis(step_axis)
.y_axis(eigval_axis)
.grid(Grid::new().top(20).right(80).bottom(30).left(60))
.series(eigval_series_pos)
.series(eigval_series_neg)
.series(eigval_series_zero);
renderer.render(CONTAINER_ID, &chart).unwrap();
});
});
view! {
div(id = CONTAINER_ID, class = "diagnostics-chart")
}
}
#[component(inline_props)]
fn DiagnosticsPanel(name: &'static str, children: Children) -> View {
let diagnostics_state = use_context::<DiagnosticsState>();
view! {
div(
class = "diagnostics-panel",
"hidden" = diagnostics_state.active_tab.with(
|active_tab| {
if active_tab == name {
None
} else {
Some("")
}
}
)
) {
(children)
}
}
}
#[component]
pub fn Diagnostics() -> View {
let diagnostics_state = DiagnosticsState::new("loss".to_string());
let active_tab = diagnostics_state.active_tab.clone();
provide_context(diagnostics_state);
view! {
div(id = "diagnostics") {
div(id = "diagnostics-bar") {
RealizationStatus {}
select(bind:value = active_tab) {
option(value = "loss") { "Loss" }
option(value = "spectrum") { "Spectrum" }
}
}
DiagnosticsPanel(name = "loss") { LossHistory {} }
DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} }
}
}
}

View file

@ -1,921 +0,0 @@
use core::array;
use nalgebra::{DMatrix, DVector, Rotation3, Vector3};
use std::rc::Rc;
use sycamore::{prelude::*, motion::create_raf};
use web_sys::{
console,
window,
KeyboardEvent,
MouseEvent,
WebGl2RenderingContext,
WebGlBuffer,
WebGlProgram,
WebGlShader,
WebGlUniformLocation,
wasm_bindgen::{JsCast, JsValue},
};
use crate::{
AppState,
assembly::{Element, ElementColor, ElementMotion, Point, Sphere},
};
// --- color ---
const COLOR_SIZE: usize = 3;
type ColorWithOpacity = [f32; COLOR_SIZE + 1];
fn combine_channels(color: ElementColor, opacity: f32) -> ColorWithOpacity {
let mut color_with_opacity = [0.0; COLOR_SIZE + 1];
color_with_opacity[..COLOR_SIZE].copy_from_slice(&color);
color_with_opacity[COLOR_SIZE] = opacity;
color_with_opacity
}
// --- scene data ---
struct SceneSpheres {
representations: Vec<DVector<f64>>,
colors_with_opacity: Vec<ColorWithOpacity>,
highlights: Vec<f32>,
}
impl SceneSpheres {
fn new() -> Self {
Self {
representations: Vec::new(),
colors_with_opacity: Vec::new(),
highlights: Vec::new(),
}
}
fn len_i32(&self) -> i32 {
self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer")
}
fn push(
&mut self, representation: DVector<f64>,
color: ElementColor, opacity: f32, highlight: f32,
) {
self.representations.push(representation);
self.colors_with_opacity.push(combine_channels(color, opacity));
self.highlights.push(highlight);
}
}
struct ScenePoints {
representations: Vec<DVector<f64>>,
colors_with_opacity: Vec<ColorWithOpacity>,
highlights: Vec<f32>,
selections: Vec<f32>,
}
impl ScenePoints {
fn new() -> Self {
Self {
representations: Vec::new(),
colors_with_opacity: Vec::new(),
highlights: Vec::new(),
selections: Vec::new(),
}
}
fn push(
&mut self, representation: DVector<f64>,
color: ElementColor, opacity: f32, highlight: f32, selected: bool,
) {
self.representations.push(representation);
self.colors_with_opacity.push(combine_channels(color, opacity));
self.highlights.push(highlight);
self.selections.push(if selected { 1.0 } else { 0.0 });
}
}
pub struct Scene {
spheres: SceneSpheres,
points: ScenePoints,
}
impl Scene {
fn new() -> Self {
Self {
spheres: SceneSpheres::new(),
points: ScenePoints::new(),
}
}
}
pub trait DisplayItem {
fn show(&self, scene: &mut Scene, selected: bool);
// the smallest positive depth, represented as a multiple of `dir`, where
// the line generated by `dir` hits the element. returns `None` if the line
// misses the element
fn cast(
&self,
dir: Vector3<f64>,
assembly_to_world: &DMatrix<f64>,
pixel_size: f64,
) -> Option<f64>;
}
impl DisplayItem for Sphere {
fn show(&self, scene: &mut Scene, selected: bool) {
/* SCAFFOLDING */
const DEFAULT_OPACITY: f32 = 0.5;
const GHOST_OPACITY: f32 = 0.2;
const HIGHLIGHT: f32 = 0.2;
let representation = self.representation.get_clone_untracked();
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
let opacity = if self.ghost.get() { GHOST_OPACITY } else { DEFAULT_OPACITY };
let highlight = if selected { 1.0 } else { HIGHLIGHT };
scene.spheres.push(representation, color, opacity, highlight);
}
// this method should be kept synchronized with `sphere_cast` in
// `spheres.frag`, which does essentially the same thing on the GPU side
fn cast(
&self,
dir: Vector3<f64>,
assembly_to_world: &DMatrix<f64>,
_pixel_size: f64,
) -> Option<f64> {
// if `a/b` is less than this threshold, we approximate
// `a*u^2 + b*u + c` by the linear function `b*u + c`
const DEG_THRESHOLD: f64 = 1e-9;
let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
let a = -rep[3] * dir.norm_squared();
let b = rep.rows_range(..3).dot(&dir);
let c = -rep[4];
let adjust = 4.0*a*c/(b*b);
if adjust < 1.0 {
// as long as `b` is non-zero, the linear approximation of
//
// a*u^2 + b*u + c
//
// at `u = 0` will reach zero at a finite depth `u_lin`. the root of
// the quadratic adjacent to `u_lin` is stored in `lin_root`. if
// both roots have the same sign, `lin_root` will be the one closer
// to `u = 0`
let square_rect_ratio = 1.0 + (1.0 - adjust).sqrt();
let lin_root = -(2.0*c)/b / square_rect_ratio;
if a.abs() > DEG_THRESHOLD * b.abs() {
if lin_root > 0.0 {
Some(lin_root)
} else {
let other_root = -b/(2.*a) * square_rect_ratio;
(other_root > 0.0).then_some(other_root)
}
} else {
(lin_root > 0.0).then_some(lin_root)
}
} else {
// the line through `dir` misses the sphere completely
None
}
}
}
impl DisplayItem for Point {
fn show(&self, scene: &mut Scene, selected: bool) {
/* SCAFFOLDING */
const GHOST_OPACITY: f32 = 0.4;
const HIGHLIGHT: f32 = 0.5;
let representation = self.representation.get_clone_untracked();
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 };
let highlight = if selected { 1.0 } else { HIGHLIGHT };
scene.points.push(representation, color, opacity, highlight, selected);
}
/* SCAFFOLDING */
fn cast(
&self,
dir: Vector3<f64>,
assembly_to_world: &DMatrix<f64>,
pixel_size: f64,
) -> Option<f64> {
let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
if rep[2] < 0.0 {
// this constant should be kept synchronized with `point.frag`
const POINT_RADIUS_PX: f64 = 4.0;
// find the radius of the point in screen projection units
let point_radius_proj = POINT_RADIUS_PX * pixel_size;
// find the squared distance between the screen projections of the
// ray and the point
let dir_proj = -dir.fixed_rows::<2>(0) / dir[2];
let rep_proj = -rep.fixed_rows::<2>(0) / rep[2];
let dist_sq = (dir_proj - rep_proj).norm_squared();
// if the ray hits the point, return its depth
if dist_sq < point_radius_proj * point_radius_proj {
Some(rep[2] / dir[2])
} else {
None
}
} else {
None
}
}
}
// --- WebGL utilities ---
fn compile_shader(
context: &WebGl2RenderingContext,
shader_type: u32,
source: &str,
) -> WebGlShader {
let shader = context.create_shader(shader_type).unwrap();
context.shader_source(&shader, source);
context.compile_shader(&shader);
shader
}
fn set_up_program(
context: &WebGl2RenderingContext,
vertex_shader_source: &str,
fragment_shader_source: &str,
) -> WebGlProgram {
// compile the shaders
let vertex_shader = compile_shader(
&context,
WebGl2RenderingContext::VERTEX_SHADER,
vertex_shader_source,
);
let fragment_shader = compile_shader(
&context,
WebGl2RenderingContext::FRAGMENT_SHADER,
fragment_shader_source,
);
// create the program and attach the shaders
let program = context.create_program().unwrap();
context.attach_shader(&program, &vertex_shader);
context.attach_shader(&program, &fragment_shader);
context.link_program(&program);
/* DEBUG */
// report whether linking succeeded
let link_status = context
.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
.as_bool()
.unwrap();
let link_msg = if link_status {
"Linked successfully"
} else {
"Linking failed"
};
console::log_1(&JsValue::from(link_msg));
program
}
fn get_uniform_array_locations<const N: usize>(
context: &WebGl2RenderingContext,
program: &WebGlProgram,
var_name: &str,
member_name_opt: Option<&str>,
) -> [Option<WebGlUniformLocation>; N] {
array::from_fn(|n| {
let name = match member_name_opt {
Some(member_name) => format!("{var_name}[{n}].{member_name}"),
None => format!("{var_name}[{n}]"),
};
context.get_uniform_location(&program, name.as_str())
})
}
// bind the given vertex buffer object to the given vertex attribute
fn bind_to_attribute(
context: &WebGl2RenderingContext,
attr_index: u32,
attr_size: i32,
buffer: &Option<WebGlBuffer>,
) {
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
context.vertex_attrib_pointer_with_i32(
attr_index,
attr_size,
WebGl2RenderingContext::FLOAT,
false, // don't normalize
0, // zero stride
0, // zero offset
);
}
// load the given data into a new vertex buffer object
fn load_new_buffer(
context: &WebGl2RenderingContext,
data: &[f32],
) -> Option<WebGlBuffer> {
// create a buffer and bind it to ARRAY_BUFFER
let buffer = context.create_buffer();
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
// load the given data into the buffer. this block is unsafe because
// `Float32Array::view` creates a raw view into our module's
// `WebAssembly.Memory` buffer. allocating more memory will change the
// buffer, invalidating the view, so we have to make sure we don't allocate
// any memory until the view is dropped. we're okay here because the view is
// used as soon as it's created
unsafe {
context.buffer_data_with_array_buffer_view(
WebGl2RenderingContext::ARRAY_BUFFER,
&js_sys::Float32Array::view(&data),
WebGl2RenderingContext::STATIC_DRAW,
);
}
buffer
}
fn bind_new_buffer_to_attribute(
context: &WebGl2RenderingContext,
attr_index: u32,
attr_size: i32,
data: &[f32],
) {
let buffer = load_new_buffer(context, data);
bind_to_attribute(context, attr_index, attr_size, &buffer);
}
// the direction in camera space that a mouse event is pointing along
fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
let target: web_sys::Element = event.target().unwrap().unchecked_into();
let rect = target.get_bounding_client_rect();
let width = rect.width();
let height = rect.height();
let shortdim = width.min(height);
// this constant should be kept synchronized with `spheres.frag` and
// `point.vert`
const FOCAL_SLOPE: f64 = 0.3;
(
Vector3::new(
FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim,
FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim,
-1.0,
),
FOCAL_SLOPE * 2.0 / shortdim,
)
}
// --- display component ---
#[component]
pub fn Display() -> View {
let state = use_context::<AppState>();
// canvas
let display = create_node_ref();
// viewpoint
let assembly_to_world = create_signal(DMatrix::<f64>::identity(5, 5));
// navigation
let pitch_up = create_signal(0.0);
let pitch_down = create_signal(0.0);
let yaw_right = create_signal(0.0);
let yaw_left = create_signal(0.0);
let roll_ccw = create_signal(0.0);
let roll_cw = create_signal(0.0);
let zoom_in = create_signal(0.0);
let zoom_out = create_signal(0.0);
let turntable = create_signal(false); /* BENCHMARKING */
// manipulation
let translate_neg_x = create_signal(0.0);
let translate_pos_x = create_signal(0.0);
let translate_neg_y = create_signal(0.0);
let translate_pos_y = create_signal(0.0);
let translate_neg_z = create_signal(0.0);
let translate_pos_z = create_signal(0.0);
let shrink_neg = create_signal(0.0);
let shrink_pos = create_signal(0.0);
// change listener
let scene_changed = create_signal(true);
create_effect(move || {
state.assembly.elements.with(|elts| {
for elt in elts {
elt.representation().track();
elt.ghost().track();
}
});
state.selection.track();
scene_changed.set(true);
});
/* INSTRUMENTS */
const SAMPLE_PERIOD: i32 = 60;
let mut last_sample_time = 0.0;
let mut frames_since_last_sample = 0;
let mean_frame_interval = create_signal(0.0);
let assembly_for_raf = state.assembly.clone();
on_mount(move || {
// timing
let mut last_time = 0.0;
// viewpoint
const ROT_SPEED: f64 = 0.4; // in radians per second
const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second
const TURNTABLE_SPEED: f64 = 0.1; /* BENCHMARKING */
let mut orientation = DMatrix::<f64>::identity(5, 5);
let mut rotation = DMatrix::<f64>::identity(5, 5);
let mut location_z: f64 = 5.0;
// manipulation
const TRANSLATION_SPEED: f64 = 0.15; // in length units per second
const SHRINKING_SPEED: f64 = 0.15; // in length units per second
// display parameters
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
const DEBUG_MODE: i32 = 0; /* DEBUG */
/* INSTRUMENTS */
let performance = window().unwrap().performance().unwrap();
// get the display canvas
let canvas = display.get().unchecked_into::<web_sys::HtmlCanvasElement>();
let ctx = canvas
.get_context("webgl2")
.unwrap()
.unwrap()
.dyn_into::<WebGl2RenderingContext>()
.unwrap();
// disable depth testing
ctx.disable(WebGl2RenderingContext::DEPTH_TEST);
// set blend mode
ctx.enable(WebGl2RenderingContext::BLEND);
ctx.blend_func(WebGl2RenderingContext::SRC_ALPHA, WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA);
// set up the sphere rendering program
let sphere_program = set_up_program(
&ctx,
include_str!("identity.vert"),
include_str!("spheres.frag"),
);
// set up the point rendering program
let point_program = set_up_program(
&ctx,
include_str!("point.vert"),
include_str!("point.frag"),
);
/* DEBUG */
// print the maximum number of vectors that can be passed as
// uniforms to a fragment shader. the OpenGL ES 3.0 standard
// requires this maximum to be at least 224, as discussed in the
// documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter
// here:
//
// https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml
//
// there are also other size limits. for example, on Aaron's
// machine, the the length of a float or genType array seems to be
// capped at 1024 elements
console::log_2(
&ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(),
&JsValue::from("uniform vectors available"),
);
// find the sphere program's vertex attribute
let viewport_position_attr = ctx.get_attrib_location(&sphere_program, "position") as u32;
// find the sphere program's uniforms
const SPHERE_MAX: usize = 200;
let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt");
let sphere_sp_locs = get_uniform_array_locations::<SPHERE_MAX>(
&ctx, &sphere_program, "sphere_list", Some("sp")
);
let sphere_lt_locs = get_uniform_array_locations::<SPHERE_MAX>(
&ctx, &sphere_program, "sphere_list", Some("lt")
);
let sphere_color_locs = get_uniform_array_locations::<SPHERE_MAX>(
&ctx, &sphere_program, "color_list", None
);
let sphere_highlight_locs = get_uniform_array_locations::<SPHERE_MAX>(
&ctx, &sphere_program, "highlight_list", None
);
let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution");
let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim");
let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold");
let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode");
// load the viewport vertex positions into a new vertex buffer object
const VERTEX_CNT: usize = 6;
let viewport_positions: [f32; 3*VERTEX_CNT] = [
// northwest triangle
-1.0, -1.0, 0.0,
-1.0, 1.0, 0.0,
1.0, 1.0, 0.0,
// southeast triangle
-1.0, -1.0, 0.0,
1.0, 1.0, 0.0,
1.0, -1.0, 0.0,
];
let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions);
// find the point program's vertex attributes
let point_position_attr = ctx.get_attrib_location(&point_program, "position") as u32;
let point_color_attr = ctx.get_attrib_location(&point_program, "color") as u32;
let point_highlight_attr = ctx.get_attrib_location(&point_program, "highlight") as u32;
let point_selection_attr = ctx.get_attrib_location(&point_program, "selected") as u32;
// set up a repainting routine
let (_, start_animation_loop, _) = create_raf(move || {
// get the time step
let time = performance.now();
let time_step = 0.001*(time - last_time);
last_time = time;
// get the navigation state
let pitch_up_val = pitch_up.get();
let pitch_down_val = pitch_down.get();
let yaw_right_val = yaw_right.get();
let yaw_left_val = yaw_left.get();
let roll_ccw_val = roll_ccw.get();
let roll_cw_val = roll_cw.get();
let zoom_in_val = zoom_in.get();
let zoom_out_val = zoom_out.get();
let turntable_val = turntable.get(); /* BENCHMARKING */
// get the manipulation state
let translate_neg_x_val = translate_neg_x.get();
let translate_pos_x_val = translate_pos_x.get();
let translate_neg_y_val = translate_neg_y.get();
let translate_pos_y_val = translate_pos_y.get();
let translate_neg_z_val = translate_neg_z.get();
let translate_pos_z_val = translate_pos_z.get();
let shrink_neg_val = shrink_neg.get();
let shrink_pos_val = shrink_pos.get();
// update the assembly's orientation
let ang_vel = {
let pitch = pitch_up_val - pitch_down_val;
let yaw = yaw_right_val - yaw_left_val;
let roll = roll_ccw_val - roll_cw_val;
if pitch != 0.0 || yaw != 0.0 || roll != 0.0 {
ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize()
} else {
Vector3::zeros()
}
} /* BENCHMARKING */ + if turntable_val {
Vector3::new(0.0, TURNTABLE_SPEED, 0.0)
} else {
Vector3::zeros()
};
let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0);
rotation_sp.copy_from(
Rotation3::from_scaled_axis(time_step * ang_vel).matrix()
);
orientation = &rotation * &orientation;
// update the assembly's location
let zoom = zoom_out_val - zoom_in_val;
location_z *= (time_step * ZOOM_SPEED * zoom).exp();
// manipulate the assembly
if state.selection.with(|sel| sel.len() == 1) {
let sel = state.selection.with(
|sel| sel.into_iter().next().unwrap().clone()
);
let translate_x = translate_pos_x_val - translate_neg_x_val;
let translate_y = translate_pos_y_val - translate_neg_y_val;
let translate_z = translate_pos_z_val - translate_neg_z_val;
let shrink = shrink_pos_val - shrink_neg_val;
let translating =
translate_x != 0.0
|| translate_y != 0.0
|| translate_z != 0.0;
if translating || shrink != 0.0 {
let elt_motion = {
let u = if translating {
TRANSLATION_SPEED * Vector3::new(
translate_x, translate_y, translate_z
).normalize()
} else {
Vector3::zeros()
};
time_step * DVector::from_column_slice(
&[u[0], u[1], u[2], SHRINKING_SPEED * shrink]
)
};
assembly_for_raf.deform(
vec![
ElementMotion {
element: sel,
velocity: elt_motion.as_view(),
}
]
);
scene_changed.set(true);
}
}
if scene_changed.get() {
const SPACE_DIM: usize = 3;
const COLOR_SIZE: usize = 3;
/* INSTRUMENTS */
// measure mean frame interval
frames_since_last_sample += 1;
if frames_since_last_sample >= SAMPLE_PERIOD {
mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64));
last_sample_time = time;
frames_since_last_sample = 0;
}
// --- get the assembly ---
let mut scene = Scene::new();
// find the map from assembly space to world space
let location = {
let u = -location_z;
DMatrix::from_column_slice(5, 5, &[
1.0, 0.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0, u,
0.0, 0.0, 2.0*u, 1.0, u*u,
0.0, 0.0, 0.0, 0.0, 1.0,
])
};
let asm_to_world = &location * &orientation;
// set up the scene
state.assembly.elements.with_untracked(
|elts| for elt in elts {
let selected = state.selection.with(|sel| sel.contains(elt));
elt.show(&mut scene, selected);
}
);
let sphere_cnt = scene.spheres.len_i32();
// --- draw the spheres ---
// use the sphere rendering program
ctx.use_program(Some(&sphere_program));
// enable the sphere program's vertex attribute
ctx.enable_vertex_attrib_array(viewport_position_attr);
// write the spheres in world coordinates
let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map(
|rep| (&asm_to_world * rep).cast::<f32>()
).collect();
// set the resolution
let width = canvas.width() as f32;
let height = canvas.height() as f32;
ctx.uniform2f(resolution_loc.as_ref(), width, height);
ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
// pass the scene data
ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt);
for n in 0..sphere_reps_world.len() {
let v = &sphere_reps_world[n];
ctx.uniform3fv_with_f32_array(
sphere_sp_locs[n].as_ref(),
v.rows(0, 3).as_slice(),
);
ctx.uniform2fv_with_f32_array(
sphere_lt_locs[n].as_ref(),
v.rows(3, 2).as_slice(),
);
ctx.uniform4fv_with_f32_array(
sphere_color_locs[n].as_ref(),
&scene.spheres.colors_with_opacity[n],
);
ctx.uniform1f(
sphere_highlight_locs[n].as_ref(),
scene.spheres.highlights[n],
);
}
// pass the display parameters
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
// bind the viewport vertex position buffer to the position
// attribute in the vertex shader
bind_to_attribute(&ctx, viewport_position_attr, SPACE_DIM as i32, &viewport_position_buffer);
// draw the scene
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
// disable the sphere program's vertex attribute
ctx.disable_vertex_attrib_array(viewport_position_attr);
// --- draw the points ---
if !scene.points.representations.is_empty() {
// use the point rendering program
ctx.use_program(Some(&point_program));
// enable the point program's vertex attributes
ctx.enable_vertex_attrib_array(point_position_attr);
ctx.enable_vertex_attrib_array(point_color_attr);
ctx.enable_vertex_attrib_array(point_highlight_attr);
ctx.enable_vertex_attrib_array(point_selection_attr);
// write the points in world coordinates
let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM);
let point_positions = DMatrix::from_columns(
&scene.points.representations.into_iter().map(
|rep| &asm_to_world_sp * rep
).collect::<Vec<_>>().as_slice()
).cast::<f32>();
// load the point positions and colors into new buffers and
// bind them to the corresponding attributes in the vertex
// shader
bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice());
bind_new_buffer_to_attribute(&ctx, point_color_attr, (COLOR_SIZE + 1) as i32, scene.points.colors_with_opacity.concat().as_slice());
bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice());
bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice());
// draw the scene
ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32);
// disable the point program's vertex attributes
ctx.disable_vertex_attrib_array(point_position_attr);
ctx.disable_vertex_attrib_array(point_color_attr);
ctx.disable_vertex_attrib_array(point_highlight_attr);
ctx.disable_vertex_attrib_array(point_selection_attr);
}
// --- update the display state ---
// update the viewpoint
assembly_to_world.set(asm_to_world);
// clear the scene change flag
scene_changed.set(
pitch_up_val != 0.0
|| pitch_down_val != 0.0
|| yaw_left_val != 0.0
|| yaw_right_val != 0.0
|| roll_cw_val != 0.0
|| roll_ccw_val != 0.0
|| zoom_in_val != 0.0
|| zoom_out_val != 0.0
|| turntable_val /* BENCHMARKING */
);
} else {
frames_since_last_sample = 0;
mean_frame_interval.set(-1.0);
}
});
start_animation_loop();
});
let set_nav_signal = move |event: &KeyboardEvent, value: f64| {
let mut navigating = true;
let shift = event.shift_key();
match event.key().as_str() {
"ArrowUp" if shift => zoom_in.set(value),
"ArrowDown" if shift => zoom_out.set(value),
"ArrowUp" => pitch_up.set(value),
"ArrowDown" => pitch_down.set(value),
"ArrowRight" if shift => roll_cw.set(value),
"ArrowLeft" if shift => roll_ccw.set(value),
"ArrowRight" => yaw_right.set(value),
"ArrowLeft" => yaw_left.set(value),
_ => navigating = false,
};
if navigating {
scene_changed.set(true);
event.prevent_default();
}
};
let set_manip_signal = move |event: &KeyboardEvent, value: f64| {
let mut manipulating = true;
let shift = event.shift_key();
match event.key().as_str() {
"d" | "D" => translate_pos_x.set(value),
"a" | "A" => translate_neg_x.set(value),
"w" | "W" if shift => translate_neg_z.set(value),
"s" | "S" if shift => translate_pos_z.set(value),
"w" | "W" => translate_pos_y.set(value),
"s" | "S" => translate_neg_y.set(value),
"]" | "}" => shrink_neg.set(value),
"[" | "{" => shrink_pos.set(value),
_ => manipulating = false,
};
if manipulating {
event.prevent_default();
}
};
view! {
/* TO DO */
// switch back to integer-valued parameters when that becomes possible
// again
canvas(
ref = display,
id = "display",
width = "600",
height = "600",
tabindex = "0",
on:keydown = move |event: KeyboardEvent| {
if event.key() == "Shift" {
// swap navigation inputs
roll_cw.set(yaw_right.get());
roll_ccw.set(yaw_left.get());
zoom_in.set(pitch_up.get());
zoom_out.set(pitch_down.get());
yaw_right.set(0.0);
yaw_left.set(0.0);
pitch_up.set(0.0);
pitch_down.set(0.0);
// swap manipulation inputs
translate_pos_z.set(translate_neg_y.get());
translate_neg_z.set(translate_pos_y.get());
translate_pos_y.set(0.0);
translate_neg_y.set(0.0);
} else {
if event.key() == "Enter" { /* BENCHMARKING */
turntable.set_fn(|turn| !turn);
scene_changed.set(true);
}
set_nav_signal(&event, 1.0);
set_manip_signal(&event, 1.0);
}
},
on:keyup = move |event: KeyboardEvent| {
if event.key() == "Shift" {
// swap navigation inputs
yaw_right.set(roll_cw.get());
yaw_left.set(roll_ccw.get());
pitch_up.set(zoom_in.get());
pitch_down.set(zoom_out.get());
roll_cw.set(0.0);
roll_ccw.set(0.0);
zoom_in.set(0.0);
zoom_out.set(0.0);
// swap manipulation inputs
translate_pos_y.set(translate_neg_z.get());
translate_neg_y.set(translate_pos_z.get());
translate_pos_z.set(0.0);
translate_neg_z.set(0.0);
} else {
set_nav_signal(&event, 0.0);
set_manip_signal(&event, 0.0);
}
},
on:blur = move |_| {
pitch_up.set(0.0);
pitch_down.set(0.0);
yaw_right.set(0.0);
yaw_left.set(0.0);
roll_ccw.set(0.0);
roll_cw.set(0.0);
},
on:click = move |event: MouseEvent| {
// find the nearest element along the pointer direction
let (dir, pixel_size) = event_dir(&event);
console::log_1(&JsValue::from(dir.to_string()));
let mut clicked: Option<(Rc<dyn Element>, f64)> = None;
let tangible_elts = state.assembly.elements
.get_clone_untracked()
.into_iter()
.filter(|elt| !elt.ghost().get());
for elt in tangible_elts {
match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) {
Some(depth) => match clicked {
Some((_, best_depth)) => {
if depth < best_depth {
clicked = Some((elt, depth))
}
},
None => clicked = Some((elt, depth)),
},
None => (),
};
}
// if we clicked something, select it
match clicked {
Some((elt, _)) => state.select(&elt, event.shift_key()),
None => state.selection.update(|sel| sel.clear()),
};
},
)
}
}

View file

@ -1,260 +0,0 @@
use itertools::Itertools;
use std::rc::Rc;
use sycamore::prelude::*;
use web_sys::{KeyboardEvent, MouseEvent, wasm_bindgen::JsCast};
use crate::{
AppState,
assembly::{
Element,
HalfCurvatureRegulator,
InversiveDistanceRegulator,
Regulator,
},
specified::SpecifiedValue
};
// an editable view of a regulator
#[component(inline_props)]
fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
// get the regulator's measurement and set point signals
let measurement = regulator.measurement();
let set_point = regulator.set_point();
// the `valid` signal tracks whether the last entered value is a valid set
// point specification
let valid = create_signal(true);
// the `value` signal holds the current set point specification
let value = create_signal(
set_point.with_untracked(|set_pt| set_pt.spec.clone())
);
// this `reset_value` closure resets the input value to the regulator's set
// point specification
let reset_value = move || {
batch(|| {
valid.set(true);
value.set(set_point.with(|set_pt| set_pt.spec.clone()));
})
};
// reset the input value whenever the regulator's set point specification
// is updated
create_effect(reset_value);
view! {
input(
r#type = "text",
class = move || {
if valid.get() {
set_point.with(|set_pt| {
if set_pt.is_present() {
"regulator-input constraint"
} else {
"regulator-input"
}
})
} else {
"regulator-input invalid"
}
},
placeholder = measurement.with(|result| result.to_string()),
bind:value = value,
on:change = move |_| {
valid.set(
match SpecifiedValue::try_from(value.get_clone_untracked()) {
Ok(set_pt) => {
set_point.set(set_pt);
true
},
Err(_) => false,
}
)
},
on:keydown = {
move |event: KeyboardEvent| {
match event.key().as_str() {
"Escape" => reset_value(),
_ => (),
}
}
},
)
}
}
pub trait OutlineItem {
fn outline_item(self: Rc<Self>, element: &Rc<dyn Element>) -> View;
}
impl OutlineItem for InversiveDistanceRegulator {
fn outline_item(self: Rc<Self>, element: &Rc<dyn Element>) -> View {
let other_subject_label = if self.subjects[0] == element.clone() {
self.subjects[1].label()
} else {
self.subjects[0].label()
}.clone();
view! {
li(class = "regulator") {
div(class = "regulator-label") { (other_subject_label) }
div(class = "regulator-type") { "Inversive distance" }
RegulatorInput(regulator = self)
div(class = "status")
}
}
}
}
impl OutlineItem for HalfCurvatureRegulator {
fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View {
view! {
li(class = "regulator") {
div(class = "regulator-label") // for spacing
div(class = "regulator-type") { "Half-curvature" }
RegulatorInput(regulator = self)
div(class = "status")
}
}
}
}
// a list item that shows an element in an outline view of an assembly
#[component(inline_props)]
fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
let state = use_context::<AppState>();
let class = {
let element_for_class = element.clone();
state.selection.map(
move |sel| if sel.contains(&element_for_class) { "selected" } else { "" }
)
};
let label = element.label().clone();
let representation = element.representation().clone();
let rep_components = move || {
representation.with(
|rep| rep.iter().map(
|u| {
let u_str = format!("{:.3}", u).replace("-", "\u{2212}");
view! { div { (u_str) } }
}
).collect::<Vec<_>>()
)
};
let regulated = element.regulators().map(|regs| regs.len() > 0);
let regulator_list = element.regulators().map(
|regs| regs
.clone()
.into_iter()
.sorted_by_key(|reg| reg.subjects().len())
.collect::<Vec<_>>()
);
let details_node = create_node_ref();
view! {
li {
details(ref = details_node) {
summary(
class = class.get(),
on:keydown = {
let element_for_handler = element.clone();
move |event: KeyboardEvent| {
match event.key().as_str() {
"Enter" => {
state.select(&element_for_handler, event.shift_key());
event.prevent_default();
},
"ArrowRight" if regulated.get() => {
let _ = details_node
.get()
.unchecked_into::<web_sys::Element>()
.set_attribute("open", "");
},
"ArrowLeft" => {
let _ = details_node
.get()
.unchecked_into::<web_sys::Element>()
.remove_attribute("open");
},
_ => (),
}
}
}
) {
div(
class = "element-switch",
on:click = |event: MouseEvent| event.stop_propagation()
)
div(
class = "element",
on:click = {
let state_for_handler = state.clone();
let element_for_handler = element.clone();
move |event: MouseEvent| {
state_for_handler.select(&element_for_handler, event.shift_key());
event.stop_propagation();
event.prevent_default();
}
}
) {
div(class = "element-label") { (label) }
div(class = "element-representation") { (rep_components) }
input(
r#type = "checkbox",
bind:checked = element.ghost(),
on:click = |event: MouseEvent| event.stop_propagation()
)
}
}
ul(class = "regulators") {
Keyed(
list = regulator_list,
view = move |reg| reg.outline_item(&element),
key = |reg| reg.serial()
)
}
}
}
}
}
// a component that lists the elements of the current assembly, showing each
// element's regulators in a collapsible sub-list. its implementation is based
// on Kate Morley's HTML + CSS tree views:
//
// https://iamkate.com/code/tree-views/
//
#[component]
pub fn Outline() -> View {
let state = use_context::<AppState>();
// list the elements alphabetically by ID
/* TO DO */
// this code is designed to generalize easily to other sort keys. if we only
// ever wanted to sort by ID, we could do that more simply using the
// `elements_by_id` index
let element_list = state.assembly.elements.map(
|elts| elts
.clone()
.into_iter()
.sorted_by_key(|elt| elt.id().clone())
.collect::<Vec<_>>()
);
view! {
ul(
id = "outline",
on:click = {
let state = use_context::<AppState>();
move |_| state.selection.update(|sel| sel.clear())
}
) {
Keyed(
list = element_list,
view = |elt| view! {
ElementOutlineItem(element = elt)
},
key = |elt| elt.serial()
)
}
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -1,941 +0,0 @@
use itertools::izip;
use std::{f64::consts::{FRAC_1_SQRT_2, PI}, rc::Rc};
use nalgebra::Vector3;
use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue};
use crate::{
AppState,
assembly::{
Assembly,
Element,
ElementColor,
InversiveDistanceRegulator,
Point,
Sphere,
},
engine,
engine::DescentHistory,
specified::SpecifiedValue,
};
// --- loaders ---
/* DEBUG */
// each of these functions loads an example assembly for testing. once we've
// done more work on saving and loading assemblies, we should come back to this
// code to see if it can be simplified
fn load_general(assembly: &Assembly) {
let _ = assembly.try_insert_element(
Sphere::new(
String::from("gemini_a"),
String::from("Castor"),
[1.00_f32, 0.25_f32, 0.00_f32],
engine::sphere(0.5, 0.5, 0.0, 1.0),
)
);
let _ = assembly.try_insert_element(
Sphere::new(
String::from("gemini_b"),
String::from("Pollux"),
[0.00_f32, 0.25_f32, 1.00_f32],
engine::sphere(-0.5, -0.5, 0.0, 1.0),
)
);
let _ = assembly.try_insert_element(
Sphere::new(
String::from("ursa_major"),
String::from("Ursa major"),
[0.25_f32, 0.00_f32, 1.00_f32],
engine::sphere(-0.5, 0.5, 0.0, 0.75),
)
);
let _ = assembly.try_insert_element(
Sphere::new(
String::from("ursa_minor"),
String::from("Ursa minor"),
[0.25_f32, 1.00_f32, 0.00_f32],
engine::sphere(0.5, -0.5, 0.0, 0.5),
)
);
let _ = assembly.try_insert_element(
Sphere::new(
String::from("moon_deimos"),
String::from("Deimos"),
[0.75_f32, 0.75_f32, 0.00_f32],
engine::sphere(0.0, 0.15, 1.0, 0.25),
)
);
let _ = assembly.try_insert_element(
Sphere::new(
String::from("moon_phobos"),
String::from("Phobos"),
[0.00_f32, 0.75_f32, 0.50_f32],
engine::sphere(0.0, -0.15, -1.0, 0.25),
)
);
}
fn load_low_curvature(assembly: &Assembly) {
// create the spheres
let a = 0.75_f64.sqrt();
let _ = assembly.try_insert_element(
Sphere::new(
"central".to_string(),
"Central".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, 0.0, 0.0, 1.0),
)
);
let _ = assembly.try_insert_element(
Sphere::new(
"assemb_plane".to_string(),
"Assembly plane".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0),
)
);
let _ = assembly.try_insert_element(
Sphere::new(
"side1".to_string(),
"Side 1".to_string(),
[1.00_f32, 0.00_f32, 0.25_f32],
engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0),
)
);
let _ = assembly.try_insert_element(
Sphere::new(
"side2".to_string(),
"Side 2".to_string(),
[0.25_f32, 1.00_f32, 0.00_f32],
engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0),
)
);
let _ = assembly.try_insert_element(
Sphere::new(
"side3".to_string(),
"Side 3".to_string(),
[0.00_f32, 0.25_f32, 1.00_f32],
engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0),
)
);
let _ = assembly.try_insert_element(
Sphere::new(
"corner1".to_string(),
"Corner 1".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0),
)
);
let _ = assembly.try_insert_element(
Sphere::new(
"corner2".to_string(),
"Corner 2".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0),
)
);
let _ = assembly.try_insert_element(
Sphere::new(
String::from("corner3"),
String::from("Corner 3"),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0),
)
);
// impose the desired tangencies and make the sides planar
let index_range = 1..=3;
let [central, assemb_plane] = ["central", "assemb_plane"].map(
|id| assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[id].clone()
)
);
let sides = index_range.clone().map(
|k| assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&format!("side{k}")].clone()
)
);
let corners = index_range.map(
|k| assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&format!("corner{k}")].clone()
)
);
for plane in [assemb_plane.clone()].into_iter().chain(sides.clone()) {
// fix the curvature of each plane
let curvature = plane.regulators().with_untracked(
|regs| regs.first().unwrap().clone()
);
curvature.set_point().set(SpecifiedValue::try_from("0".to_string()).unwrap());
}
let all_perpendicular = [central.clone()].into_iter()
.chain(sides.clone())
.chain(corners.clone());
for sphere in all_perpendicular {
// make each side and packed sphere perpendicular to the assembly plane
let right_angle = InversiveDistanceRegulator::new([sphere, assemb_plane.clone()]);
right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
assembly.insert_regulator(Rc::new(right_angle));
}
for sphere in sides.clone().chain(corners.clone()) {
// make each side and corner sphere tangent to the central sphere
let tangency = InversiveDistanceRegulator::new([sphere.clone(), central.clone()]);
tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap());
assembly.insert_regulator(Rc::new(tangency));
}
for (side_index, side) in sides.enumerate() {
// make each side tangent to the two adjacent corner spheres
for (corner_index, corner) in corners.clone().enumerate() {
if side_index != corner_index {
let tangency = InversiveDistanceRegulator::new([side.clone(), corner]);
tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap());
assembly.insert_regulator(Rc::new(tangency));
}
}
}
}
fn load_pointed(assembly: &Assembly) {
let _ = assembly.try_insert_element(
Point::new(
format!("point_front"),
format!("Front point"),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::point(0.0, 0.0, FRAC_1_SQRT_2),
)
);
let _ = assembly.try_insert_element(
Point::new(
format!("point_back"),
format!("Back point"),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::point(0.0, 0.0, -FRAC_1_SQRT_2),
)
);
for index_x in 0..=1 {
for index_y in 0..=1 {
let x = index_x as f64 - 0.5;
let y = index_y as f64 - 0.5;
let _ = assembly.try_insert_element(
Sphere::new(
format!("sphere{index_x}{index_y}"),
format!("Sphere {index_x}{index_y}"),
[0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32],
engine::sphere(x, y, 0.0, 1.0),
)
);
let _ = assembly.try_insert_element(
Point::new(
format!("point{index_x}{index_y}"),
format!("Point {index_x}{index_y}"),
[0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32],
engine::point(x, y, 0.0),
)
);
}
}
}
// to finish describing the tridiminished icosahedron, set the inversive
// distance regulators as follows:
// A-A -0.25
// A-B "
// B-C "
// C-C "
// A-C -0.25 * φ^2 = -0.6545084971874737
fn load_tridiminished_icosahedron(assembly: &Assembly) {
// create the vertices
const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.25_f32];
const COLOR_B: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32];
const COLOR_C: ElementColor = [0.25_f32, 0.50_f32, 1.00_f32];
let vertices = [
Point::new(
"a1".to_string(),
"A₁".to_string(),
COLOR_A,
engine::point(0.25, 0.75, 0.75),
),
Point::new(
"a2".to_string(),
"A₂".to_string(),
COLOR_A,
engine::point(0.75, 0.25, 0.75),
),
Point::new(
"a3".to_string(),
"A₃".to_string(),
COLOR_A,
engine::point(0.75, 0.75, 0.25),
),
Point::new(
"b1".to_string(),
"B₁".to_string(),
COLOR_B,
engine::point(0.75, -0.25, -0.25),
),
Point::new(
"b2".to_string(),
"B₂".to_string(),
COLOR_B,
engine::point(-0.25, 0.75, -0.25),
),
Point::new(
"b3".to_string(),
"B₃".to_string(),
COLOR_B,
engine::point(-0.25, -0.25, 0.75),
),
Point::new(
"c1".to_string(),
"C₁".to_string(),
COLOR_C,
engine::point(0.0, -1.0, -1.0),
),
Point::new(
"c2".to_string(),
"C₂".to_string(),
COLOR_C,
engine::point(-1.0, 0.0, -1.0),
),
Point::new(
"c3".to_string(),
"C₃".to_string(),
COLOR_C,
engine::point(-1.0, -1.0, 0.0),
),
];
for vertex in vertices {
let _ = assembly.try_insert_element(vertex);
}
// create the faces
const COLOR_FACE: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32];
let frac_1_sqrt_6 = 1.0 / 6.0_f64.sqrt();
let frac_2_sqrt_6 = 2.0 * frac_1_sqrt_6;
let faces = [
Sphere::new(
"face1".to_string(),
"Face 1".to_string(),
COLOR_FACE,
engine::sphere_with_offset(frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0),
),
Sphere::new(
"face2".to_string(),
"Face 2".to_string(),
COLOR_FACE,
engine::sphere_with_offset(-frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0),
),
Sphere::new(
"face3".to_string(),
"Face 3".to_string(),
COLOR_FACE,
engine::sphere_with_offset(-frac_1_sqrt_6, -frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, 0.0),
),
];
for face in faces {
face.ghost().set(true);
let _ = assembly.try_insert_element(face);
}
let index_range = 1..=3;
for j in index_range.clone() {
// make each face planar
let face = assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&format!("face{j}")].clone()
);
let curvature_regulator = face.regulators().with_untracked(
|regs| regs.first().unwrap().clone()
);
curvature_regulator.set_point().set(
SpecifiedValue::try_from("0".to_string()).unwrap()
);
// put each A vertex on the face it belongs to
let vertex_a = assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&format!("a{j}")].clone()
);
let incidence_a = InversiveDistanceRegulator::new([face.clone(), vertex_a.clone()]);
incidence_a.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
assembly.insert_regulator(Rc::new(incidence_a));
// regulate the B-C vertex distances
let vertices_bc = ["b", "c"].map(
|series| assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&format!("{series}{j}")].clone()
)
);
assembly.insert_regulator(
Rc::new(InversiveDistanceRegulator::new(vertices_bc))
);
// get the pair of indices adjacent to `j`
let adjacent_indices = [j % 3 + 1, (j + 1) % 3 + 1];
for k in adjacent_indices.clone() {
for series in ["b", "c"] {
// put each B and C vertex on the faces it belongs to
let vertex = assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&format!("{series}{k}")].clone()
);
let incidence = InversiveDistanceRegulator::new([face.clone(), vertex.clone()]);
incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
assembly.insert_regulator(Rc::new(incidence));
// regulate the A-B and A-C vertex distances
assembly.insert_regulator(
Rc::new(InversiveDistanceRegulator::new([vertex_a.clone(), vertex]))
);
}
}
// regulate the A-A and C-C vertex distances
let adjacent_pairs = ["a", "c"].map(
|series| adjacent_indices.map(
|index| assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&format!("{series}{index}")].clone()
)
)
);
for pair in adjacent_pairs {
assembly.insert_regulator(
Rc::new(InversiveDistanceRegulator::new(pair))
);
}
}
}
// to finish describing the dodecahedral circle packing, set the inversive
// distance regulators to -1. some of the regulators have already been set
fn load_dodecahedral_packing(assembly: &Assembly) {
// add the substrate
let _ = assembly.try_insert_element(
Sphere::new(
"substrate".to_string(),
"Substrate".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, 0.0, 0.0, 1.0),
)
);
let substrate = assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id["substrate"].clone()
);
// fix the substrate's curvature
substrate.regulators().with_untracked(
|regs| regs.first().unwrap().clone()
).set_point().set(
SpecifiedValue::try_from("0.5".to_string()).unwrap()
);
// add the circles to be packed
const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.00_f32];
const COLOR_B: ElementColor = [1.00_f32, 0.00_f32, 0.25_f32];
const COLOR_C: ElementColor = [0.25_f32, 0.00_f32, 1.00_f32];
let phi = 0.5 + 1.25_f64.sqrt(); /* TO DO */ // replace with std::f64::consts::PHI when that gets stabilized
let phi_inv = 1.0 / phi;
let coord_scale = (phi + 2.0).sqrt();
let face_scales = [phi_inv, (13.0 / 12.0) / coord_scale];
let face_radii = [phi_inv, 5.0 / 12.0];
let mut faces = Vec::<Rc<dyn Element>>::new();
let subscripts = ["", ""];
for j in 0..2 {
for k in 0..2 {
let small_coord = face_scales[k] * (2.0*(j as f64) - 1.0);
let big_coord = face_scales[k] * (2.0*(k as f64) - 1.0) * phi;
let id_num = format!("{j}{k}");
let label_sub = format!("{}{}", subscripts[j], subscripts[k]);
// add the A face
let id_a = format!("a{id_num}");
let _ = assembly.try_insert_element(
Sphere::new(
id_a.clone(),
format!("A{label_sub}"),
COLOR_A,
engine::sphere(0.0, small_coord, big_coord, face_radii[k]),
)
);
faces.push(
assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&id_a].clone()
)
);
// add the B face
let id_b = format!("b{id_num}");
let _ = assembly.try_insert_element(
Sphere::new(
id_b.clone(),
format!("B{label_sub}"),
COLOR_B,
engine::sphere(small_coord, big_coord, 0.0, face_radii[k]),
)
);
faces.push(
assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&id_b].clone()
)
);
// add the C face
let id_c = format!("c{id_num}");
let _ = assembly.try_insert_element(
Sphere::new(
id_c.clone(),
format!("C{label_sub}"),
COLOR_C,
engine::sphere(big_coord, 0.0, small_coord, face_radii[k]),
)
);
faces.push(
assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&id_c].clone()
)
);
}
}
// make each face sphere perpendicular to the substrate
for face in faces {
let right_angle = InversiveDistanceRegulator::new([face, substrate.clone()]);
right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
assembly.insert_regulator(Rc::new(right_angle));
}
// set up the tangencies that define the packing
for [long_edge_plane, short_edge_plane] in [["a", "b"], ["b", "c"], ["c", "a"]] {
for k in 0..2 {
let long_edge_ids = [
format!("{long_edge_plane}{k}0"),
format!("{long_edge_plane}{k}1")
];
let short_edge_ids = [
format!("{short_edge_plane}0{k}"),
format!("{short_edge_plane}1{k}")
];
let [long_edge, short_edge] = [long_edge_ids, short_edge_ids].map(
|edge_ids| edge_ids.map(
|id| assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&id].clone()
)
)
);
// set up the short-edge tangency
let short_tangency = InversiveDistanceRegulator::new(short_edge.clone());
if k == 0 {
short_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap());
}
assembly.insert_regulator(Rc::new(short_tangency));
// set up the side tangencies
for i in 0..2 {
for j in 0..2 {
let side_tangency = InversiveDistanceRegulator::new(
[long_edge[i].clone(), short_edge[j].clone()]
);
if i == 0 && k == 0 {
side_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap());
}
assembly.insert_regulator(Rc::new(side_tangency));
}
}
}
}
}
// the initial configuration of this test assembly deliberately violates the
// constraints, so loading the assembly will trigger a non-trivial realization
fn load_balanced(assembly: &Assembly) {
// create the spheres
const R_OUTER: f64 = 10.0;
const R_INNER: f64 = 4.0;
let spheres = [
Sphere::new(
"outer".to_string(),
"Outer".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, 0.0, 0.0, R_OUTER),
),
Sphere::new(
"a".to_string(),
"A".to_string(),
[1.00_f32, 0.00_f32, 0.25_f32],
engine::sphere(0.0, 4.0, 0.0, R_INNER),
),
Sphere::new(
"b".to_string(),
"B".to_string(),
[0.00_f32, 0.25_f32, 1.00_f32],
engine::sphere(0.0, -4.0, 0.0, R_INNER),
),
];
for sphere in spheres {
let _ = assembly.try_insert_element(sphere);
}
// get references to the spheres
let [outer, a, b] = ["outer", "a", "b"].map(
|id| assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[id].clone()
)
);
// fix the diameters of the outer, sun, and moon spheres
for (sphere, radius) in [
(outer.clone(), R_OUTER),
(a.clone(), R_INNER),
(b.clone(), R_INNER),
] {
let curvature_regulator = sphere.regulators().with_untracked(
|regs| regs.first().unwrap().clone()
);
let curvature = 0.5 / radius;
curvature_regulator.set_point().set(
SpecifiedValue::try_from(curvature.to_string()).unwrap()
);
}
// set the inversive distances between the spheres. as described above, the
// initial configuration deliberately violates these constraints
for inner in [a, b] {
let tangency = InversiveDistanceRegulator::new([outer.clone(), inner]);
tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap());
assembly.insert_regulator(Rc::new(tangency));
}
}
// the initial configuration of this test assembly deliberately violates the
// constraints, so loading the assembly will trigger a non-trivial realization
fn load_off_center(assembly: &Assembly) {
// create a point almost at the origin and a sphere centered on the origin
let _ = assembly.try_insert_element(
Point::new(
"point".to_string(),
"Point".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::point(1e-9, 0.0, 0.0),
),
);
let _ = assembly.try_insert_element(
Sphere::new(
"sphere".to_string(),
"Sphere".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, 0.0, 0.0, 1.0),
),
);
// get references to the elements
let point_and_sphere = ["point", "sphere"].map(
|id| assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[id].clone()
)
);
// put the point on the sphere
let incidence = InversiveDistanceRegulator::new(point_and_sphere);
incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
assembly.insert_regulator(Rc::new(incidence));
}
// setting the inversive distances between the vertices to -2 gives a regular
// tetrahedron with side length 1, whose insphere and circumsphere have radii
// sqrt(1/6) and sqrt(3/2), respectively. to measure those radii, set an
// inversive distance of -1 between the insphere and each face, and then set an
// inversive distance of 0 between the circumsphere and each vertex
fn load_radius_ratio(assembly: &Assembly) {
let index_range = 1..=4;
// create the spheres
const GRAY: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32];
let spheres = [
Sphere::new(
"sphere_faces".to_string(),
"Insphere".to_string(),
GRAY,
engine::sphere(0.0, 0.0, 0.0, 0.5),
),
Sphere::new(
"sphere_vertices".to_string(),
"Circumsphere".to_string(),
GRAY,
engine::sphere(0.0, 0.0, 0.0, 0.25),
),
];
for sphere in spheres {
let _ = assembly.try_insert_element(sphere);
}
// create the vertices
let vertices = izip!(
index_range.clone(),
[
[1.00_f32, 0.50_f32, 0.75_f32],
[1.00_f32, 0.75_f32, 0.50_f32],
[1.00_f32, 1.00_f32, 0.50_f32],
[0.75_f32, 0.50_f32, 1.00_f32],
].into_iter(),
[
engine::point(-0.6, -0.8, -0.6),
engine::point(-0.6, 0.8, 0.6),
engine::point(0.6, -0.8, 0.6),
engine::point(0.6, 0.8, -0.6),
].into_iter()
).map(
|(k, color, representation)| {
Point::new(
format!("v{k}"),
format!("Vertex {k}"),
color,
representation,
)
}
);
for vertex in vertices {
let _ = assembly.try_insert_element(vertex);
}
// create the faces
let base_dir = Vector3::new(1.0, 0.75, 1.0).normalize();
let offset = base_dir.dot(&Vector3::new(-0.6, 0.8, 0.6));
let faces = izip!(
index_range.clone(),
[
[1.00_f32, 0.00_f32, 0.25_f32],
[1.00_f32, 0.25_f32, 0.00_f32],
[0.75_f32, 0.75_f32, 0.00_f32],
[0.25_f32, 0.00_f32, 1.00_f32],
].into_iter(),
[
engine::sphere_with_offset(base_dir[0], base_dir[1], base_dir[2], offset, 0.0),
engine::sphere_with_offset(base_dir[0], -base_dir[1], -base_dir[2], offset, 0.0),
engine::sphere_with_offset(-base_dir[0], base_dir[1], -base_dir[2], offset, 0.0),
engine::sphere_with_offset(-base_dir[0], -base_dir[1], base_dir[2], offset, 0.0),
].into_iter()
).map(
|(k, color, representation)| {
Sphere::new(
format!("f{k}"),
format!("Face {k}"),
color,
representation,
)
}
);
for face in faces {
face.ghost().set(true);
let _ = assembly.try_insert_element(face);
}
// impose the constraints
for j in index_range.clone() {
let [face_j, vertex_j] = [
format!("f{j}"),
format!("v{j}"),
].map(
|id| assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&id].clone()
)
);
// make the faces planar
let curvature_regulator = face_j.regulators().with_untracked(
|regs| regs.first().unwrap().clone()
);
curvature_regulator.set_point().set(
SpecifiedValue::try_from("0".to_string()).unwrap()
);
for k in index_range.clone().filter(|&index| index != j) {
let vertex_k = assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&format!("v{k}")].clone()
);
// fix the distances between the vertices
if j < k {
let distance_regulator = InversiveDistanceRegulator::new(
[vertex_j.clone(), vertex_k.clone()]
);
assembly.insert_regulator(Rc::new(distance_regulator));
}
// put the vertices on the faces
let incidence_regulator = InversiveDistanceRegulator::new([face_j.clone(), vertex_k.clone()]);
incidence_regulator.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
assembly.insert_regulator(Rc::new(incidence_regulator));
}
}
}
// to finish setting up the problem, fix the following curvatures:
// sun 1
// moon 5/3 = 1.666666666666666...
// chain1 2
// a tiny `x` or `z` nudge of the outer sphere reliably prevents realization
// failures before they happen, or resolves them after they happen. the result
// depends sensitively on the translation direction, suggesting that realization
// is failing because the engine is having trouble breaking a symmetry
// /* TO DO */
// the engine's performance on this problem is scale-dependent! with the current
// initial conditions, realization fails for any order of imposing the remaining
// curvature constraints. scaling everything up by a factor of ten, as done in
// the original problem, makes realization succeed reliably. one potentially
// relevant difference is that a lot of the numbers in the current initial
// conditions are exactly representable as floats, unlike the analogous numbers
// in the scaled-up problem. the inexact representations might break the
// symmetry that's getting the engine stuck
fn load_irisawa_hexlet(assembly: &Assembly) {
let index_range = 1..=6;
let colors = [
[1.00_f32, 0.00_f32, 0.25_f32],
[1.00_f32, 0.25_f32, 0.00_f32],
[0.75_f32, 0.75_f32, 0.00_f32],
[0.25_f32, 1.00_f32, 0.00_f32],
[0.00_f32, 0.25_f32, 1.00_f32],
[0.25_f32, 0.00_f32, 1.00_f32],
].into_iter();
// create the spheres
let spheres = [
Sphere::new(
"outer".to_string(),
"Outer".to_string(),
[0.5_f32, 0.5_f32, 0.5_f32],
engine::sphere(0.0, 0.0, 0.0, 1.5),
),
Sphere::new(
"sun".to_string(),
"Sun".to_string(),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, -0.75, 0.0, 0.75),
),
Sphere::new(
"moon".to_string(),
"Moon".to_string(),
[0.25_f32, 0.25_f32, 0.25_f32],
engine::sphere(0.0, 0.75, 0.0, 0.75),
),
].into_iter().chain(
index_range.clone().zip(colors).map(
|(k, color)| {
let ang = (k as f64) * PI/3.0;
Sphere::new(
format!("chain{k}"),
format!("Chain {k}"),
color,
engine::sphere(1.0 * ang.sin(), 0.0, 1.0 * ang.cos(), 0.5),
)
}
)
);
for sphere in spheres {
let _ = assembly.try_insert_element(sphere);
}
// put the outer sphere in ghost mode and fix its curvature
let outer = assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id["outer"].clone()
);
outer.ghost().set(true);
let outer_curvature_regulator = outer.regulators().with_untracked(
|regs| regs.first().unwrap().clone()
);
outer_curvature_regulator.set_point().set(
SpecifiedValue::try_from((1.0 / 3.0).to_string()).unwrap()
);
// impose the desired tangencies
let [outer, sun, moon] = ["outer", "sun", "moon"].map(
|id| assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[id].clone()
)
);
let chain = index_range.map(
|k| assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&format!("chain{k}")].clone()
)
);
for (chain_sphere, chain_sphere_next) in chain.clone().zip(chain.cycle().skip(1)) {
for (other_sphere, inversive_distance) in [
(outer.clone(), "1"),
(sun.clone(), "-1"),
(moon.clone(), "-1"),
(chain_sphere_next.clone(), "-1"),
] {
let tangency = InversiveDistanceRegulator::new([chain_sphere.clone(), other_sphere]);
tangency.set_point.set(SpecifiedValue::try_from(inversive_distance.to_string()).unwrap());
assembly.insert_regulator(Rc::new(tangency));
}
}
let outer_sun_tangency = InversiveDistanceRegulator::new([outer.clone(), sun]);
outer_sun_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap());
assembly.insert_regulator(Rc::new(outer_sun_tangency));
let outer_moon_tangency = InversiveDistanceRegulator::new([outer.clone(), moon]);
outer_moon_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap());
assembly.insert_regulator(Rc::new(outer_moon_tangency));
}
// --- chooser ---
/* DEBUG */
#[component]
pub fn TestAssemblyChooser() -> View {
// create an effect that loads the selected test assembly
let assembly_name = create_signal("general".to_string());
create_effect(move || {
// get name of chosen assembly
let name = assembly_name.get_clone();
console::log_1(
&JsValue::from(format!("Showing assembly \"{}\"", name.clone()))
);
batch(|| {
let state = use_context::<AppState>();
let assembly = &state.assembly;
// clear state
assembly.regulators.update(|regs| regs.clear());
assembly.elements.update(|elts| elts.clear());
assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear());
assembly.descent_history.set(DescentHistory::new());
state.selection.update(|sel| sel.clear());
// load assembly
match name.as_str() {
"general" => load_general(assembly),
"low-curvature" => load_low_curvature(assembly),
"pointed" => load_pointed(assembly),
"tridiminished-icosahedron" => load_tridiminished_icosahedron(assembly),
"dodecahedral-packing" => load_dodecahedral_packing(assembly),
"balanced" => load_balanced(assembly),
"off-center" => load_off_center(assembly),
"radius-ratio" => load_radius_ratio(assembly),
"irisawa-hexlet" => load_irisawa_hexlet(assembly),
_ => (),
};
});
});
// build the chooser
view! {
select(bind:value = assembly_name) {
option(value = "general") { "General" }
option(value = "low-curvature") { "Low-curvature" }
option(value = "pointed") { "Pointed" }
option(value = "tridiminished-icosahedron") { "Tridiminished icosahedron" }
option(value = "dodecahedral-packing") { "Dodecahedral packing" }
option(value = "balanced") { "Balanced" }
option(value = "off-center") { "Off-center" }
option(value = "radius-ratio") { "Radius ratio" }
option(value = "irisawa-hexlet") { "Irisawa hexlet" }
option(value = "empty") { "Empty" }
}
}
}

464
app-proto/src/display.rs Normal file
View file

@ -0,0 +1,464 @@
use core::array;
use nalgebra::{DMatrix, Rotation3, Vector3};
use sycamore::{prelude::*, motion::create_raf};
use web_sys::{
console,
window,
KeyboardEvent,
WebGl2RenderingContext,
WebGlProgram,
WebGlShader,
WebGlUniformLocation,
wasm_bindgen::{JsCast, JsValue}
};
use crate::AppState;
fn compile_shader(
context: &WebGl2RenderingContext,
shader_type: u32,
source: &str,
) -> WebGlShader {
let shader = context.create_shader(shader_type).unwrap();
context.shader_source(&shader, source);
context.compile_shader(&shader);
shader
}
fn get_uniform_array_locations<const N: usize>(
context: &WebGl2RenderingContext,
program: &WebGlProgram,
var_name: &str,
member_name_opt: Option<&str>
) -> [Option<WebGlUniformLocation>; N] {
array::from_fn(|n| {
let name = match member_name_opt {
Some(member_name) => format!("{var_name}[{n}].{member_name}"),
None => format!("{var_name}[{n}]")
};
context.get_uniform_location(&program, name.as_str())
})
}
// load the given data into the vertex input of the given name
fn bind_vertex_attrib(
context: &WebGl2RenderingContext,
index: u32,
size: i32,
data: &[f32]
) {
// create a data buffer and bind it to ARRAY_BUFFER
let buffer = context.create_buffer().unwrap();
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer));
// load the given data into the buffer. the function `Float32Array::view`
// creates a raw view into our module's `WebAssembly.Memory` buffer.
// allocating more memory will change the buffer, invalidating the view.
// that means we have to make sure we don't allocate any memory until the
// view is dropped
unsafe {
context.buffer_data_with_array_buffer_view(
WebGl2RenderingContext::ARRAY_BUFFER,
&js_sys::Float32Array::view(&data),
WebGl2RenderingContext::STATIC_DRAW,
);
}
// allow the target attribute to be used
context.enable_vertex_attrib_array(index);
// take whatever's bound to ARRAY_BUFFER---here, the data buffer created
// above---and bind it to the target attribute
//
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer
//
context.vertex_attrib_pointer_with_i32(
index,
size,
WebGl2RenderingContext::FLOAT,
false, // don't normalize
0, // zero stride
0, // zero offset
);
}
#[component]
pub fn Display() -> View {
let state = use_context::<AppState>();
// canvas
let display = create_node_ref();
// navigation
let pitch_up = create_signal(0.0);
let pitch_down = create_signal(0.0);
let yaw_right = create_signal(0.0);
let yaw_left = create_signal(0.0);
let roll_ccw = create_signal(0.0);
let roll_cw = create_signal(0.0);
let zoom_in = create_signal(0.0);
let zoom_out = create_signal(0.0);
let turntable = create_signal(false); /* BENCHMARKING */
// change listener
let scene_changed = create_signal(true);
create_effect(move || {
state.assembly.elements.with(|elts| {
for (_, elt) in elts {
elt.representation.track();
}
});
state.selection.track();
scene_changed.set(true);
});
/* INSTRUMENTS */
const SAMPLE_PERIOD: i32 = 60;
let mut last_sample_time = 0.0;
let mut frames_since_last_sample = 0;
let mean_frame_interval = create_signal(0.0);
on_mount(move || {
// timing
let mut last_time = 0.0;
// viewpoint
const ROT_SPEED: f64 = 0.4; // in radians per second
const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second
const TURNTABLE_SPEED: f64 = 0.1; /* BENCHMARKING */
let mut orientation = DMatrix::<f64>::identity(5, 5);
let mut rotation = DMatrix::<f64>::identity(5, 5);
let mut location_z: f64 = 5.0;
// display parameters
const OPACITY: f32 = 0.5; /* SCAFFOLDING */
const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
const DEBUG_MODE: i32 = 0; /* DEBUG */
/* INSTRUMENTS */
let performance = window().unwrap().performance().unwrap();
// get the display canvas
let canvas = display.get().unchecked_into::<web_sys::HtmlCanvasElement>();
let ctx = canvas
.get_context("webgl2")
.unwrap()
.unwrap()
.dyn_into::<WebGl2RenderingContext>()
.unwrap();
// compile and attach the vertex and fragment shaders
let vertex_shader = compile_shader(
&ctx,
WebGl2RenderingContext::VERTEX_SHADER,
include_str!("identity.vert"),
);
let fragment_shader = compile_shader(
&ctx,
WebGl2RenderingContext::FRAGMENT_SHADER,
include_str!("inversive.frag"),
);
let program = ctx.create_program().unwrap();
ctx.attach_shader(&program, &vertex_shader);
ctx.attach_shader(&program, &fragment_shader);
ctx.link_program(&program);
let link_status = ctx
.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
.as_bool()
.unwrap();
let link_msg = if link_status {
"Linked successfully"
} else {
"Linking failed"
};
console::log_1(&JsValue::from(link_msg));
ctx.use_program(Some(&program));
/* DEBUG */
// print the maximum number of vectors that can be passed as
// uniforms to a fragment shader. the OpenGL ES 3.0 standard
// requires this maximum to be at least 224, as discussed in the
// documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter
// here:
//
// https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml
//
// there are also other size limits. for example, on Aaron's
// machine, the the length of a float or genType array seems to be
// capped at 1024 elements
console::log_2(
&ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(),
&JsValue::from("uniform vectors available")
);
// find indices of vertex attributes and uniforms
const SPHERE_MAX: usize = 200;
let position_index = ctx.get_attrib_location(&program, "position") as u32;
let sphere_cnt_loc = ctx.get_uniform_location(&program, "sphere_cnt");
let sphere_sp_locs = get_uniform_array_locations::<SPHERE_MAX>(
&ctx, &program, "sphere_list", Some("sp")
);
let sphere_lt_locs = get_uniform_array_locations::<SPHERE_MAX>(
&ctx, &program, "sphere_list", Some("lt")
);
let color_locs = get_uniform_array_locations::<SPHERE_MAX>(
&ctx, &program, "color_list", None
);
let highlight_locs = get_uniform_array_locations::<SPHERE_MAX>(
&ctx, &program, "highlight_list", None
);
let resolution_loc = ctx.get_uniform_location(&program, "resolution");
let shortdim_loc = ctx.get_uniform_location(&program, "shortdim");
let opacity_loc = ctx.get_uniform_location(&program, "opacity");
let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold");
let debug_mode_loc = ctx.get_uniform_location(&program, "debug_mode");
// create a vertex array and bind it to the graphics context
let vertex_array = ctx.create_vertex_array().unwrap();
ctx.bind_vertex_array(Some(&vertex_array));
// set the vertex positions
const VERTEX_CNT: usize = 6;
let positions: [f32; 3*VERTEX_CNT] = [
// northwest triangle
-1.0, -1.0, 0.0,
-1.0, 1.0, 0.0,
1.0, 1.0, 0.0,
// southeast triangle
-1.0, -1.0, 0.0,
1.0, 1.0, 0.0,
1.0, -1.0, 0.0
];
bind_vertex_attrib(&ctx, position_index, 3, &positions);
// set up a repainting routine
let (_, start_animation_loop, _) = create_raf(move || {
// get the time step
let time = performance.now();
let time_step = 0.001*(time - last_time);
last_time = time;
// get the navigation state
let pitch_up_val = pitch_up.get();
let pitch_down_val = pitch_down.get();
let yaw_right_val = yaw_right.get();
let yaw_left_val = yaw_left.get();
let roll_ccw_val = roll_ccw.get();
let roll_cw_val = roll_cw.get();
let zoom_in_val = zoom_in.get();
let zoom_out_val = zoom_out.get();
let turntable_val = turntable.get(); /* BENCHMARKING */
// update the assembly's orientation
let ang_vel = {
let pitch = pitch_up_val - pitch_down_val;
let yaw = yaw_right_val - yaw_left_val;
let roll = roll_ccw_val - roll_cw_val;
if pitch != 0.0 || yaw != 0.0 || roll != 0.0 {
ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize()
} else {
Vector3::zeros()
}
} /* BENCHMARKING */ + if turntable_val {
Vector3::new(0.0, TURNTABLE_SPEED, 0.0)
} else {
Vector3::zeros()
};
let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0);
rotation_sp.copy_from(
Rotation3::from_scaled_axis(time_step * ang_vel).matrix()
);
orientation = &rotation * &orientation;
// update the assembly's location
let zoom = zoom_out_val - zoom_in_val;
location_z *= (time_step * ZOOM_SPEED * zoom).exp();
if scene_changed.get() {
/* INSTRUMENTS */
// measure mean frame interval
frames_since_last_sample += 1;
if frames_since_last_sample >= SAMPLE_PERIOD {
mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64));
last_sample_time = time;
frames_since_last_sample = 0;
}
// find the map from assembly space to world space
let location = {
let u = -location_z;
DMatrix::from_column_slice(5, 5, &[
1.0, 0.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0, u,
0.0, 0.0, 2.0*u, 1.0, u*u,
0.0, 0.0, 0.0, 0.0, 1.0
])
};
let assembly_to_world = &location * &orientation;
// get the assembly
let (
elt_cnt,
reps_world,
colors,
highlights
) = state.assembly.elements.with(|elts| {
(
// number of elements
elts.len() as i32,
// representation vectors in world coordinates
elts.iter().map(
|(_, elt)| elt.representation.with(|rep| &assembly_to_world * rep)
).collect::<Vec<_>>(),
// colors
elts.iter().map(|(key, elt)| {
if state.selection.with(|sel| sel.contains(&key)) {
elt.color.map(|ch| 0.2 + 0.8*ch)
} else {
elt.color
}
}).collect::<Vec<_>>(),
// highlight levels
elts.iter().map(|(key, _)| {
if state.selection.with(|sel| sel.contains(&key)) {
1.0_f32
} else {
HIGHLIGHT
}
}).collect::<Vec<_>>()
)
});
// set the resolution
let width = canvas.width() as f32;
let height = canvas.height() as f32;
ctx.uniform2f(resolution_loc.as_ref(), width, height);
ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
// pass the assembly
ctx.uniform1i(sphere_cnt_loc.as_ref(), elt_cnt);
for n in 0..reps_world.len() {
let v = &reps_world[n];
ctx.uniform3f(
sphere_sp_locs[n].as_ref(),
v[0] as f32, v[1] as f32, v[2] as f32
);
ctx.uniform2f(
sphere_lt_locs[n].as_ref(),
v[3] as f32, v[4] as f32
);
ctx.uniform3fv_with_f32_array(
color_locs[n].as_ref(),
&colors[n]
);
ctx.uniform1f(
highlight_locs[n].as_ref(),
highlights[n]
);
}
// pass the display parameters
ctx.uniform1f(opacity_loc.as_ref(), OPACITY);
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
// draw the scene
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
// clear the scene change flag
scene_changed.set(
pitch_up_val != 0.0
|| pitch_down_val != 0.0
|| yaw_left_val != 0.0
|| yaw_right_val != 0.0
|| roll_cw_val != 0.0
|| roll_ccw_val != 0.0
|| zoom_in_val != 0.0
|| zoom_out_val != 0.0
|| turntable_val /* BENCHMARKING */
);
} else {
frames_since_last_sample = 0;
mean_frame_interval.set(-1.0);
}
});
start_animation_loop();
});
let set_nav_signal = move |event: KeyboardEvent, value: f64| {
let mut navigating = true;
let shift = event.shift_key();
match event.key().as_str() {
"ArrowUp" if shift => zoom_in.set(value),
"ArrowDown" if shift => zoom_out.set(value),
"ArrowUp" => pitch_up.set(value),
"ArrowDown" => pitch_down.set(value),
"ArrowRight" if shift => roll_cw.set(value),
"ArrowLeft" if shift => roll_ccw.set(value),
"ArrowRight" => yaw_right.set(value),
"ArrowLeft" => yaw_left.set(value),
_ => navigating = false
};
if navigating {
scene_changed.set(true);
event.prevent_default();
}
};
view! {
/* TO DO */
// switch back to integer-valued parameters when that becomes possible
// again
canvas(
ref=display,
width="600",
height="600",
tabindex="0",
on:keydown=move |event: KeyboardEvent| {
if event.key() == "Shift" {
roll_cw.set(yaw_right.get());
roll_ccw.set(yaw_left.get());
zoom_in.set(pitch_up.get());
zoom_out.set(pitch_down.get());
yaw_right.set(0.0);
yaw_left.set(0.0);
pitch_up.set(0.0);
pitch_down.set(0.0);
} else {
if event.key() == "Enter" { /* BENCHMARKING */
turntable.set_fn(|turn| !turn);
scene_changed.set(true);
}
set_nav_signal(event, 1.0);
}
},
on:keyup=move |event: KeyboardEvent| {
if event.key() == "Shift" {
yaw_right.set(roll_cw.get());
yaw_left.set(roll_ccw.get());
pitch_up.set(zoom_in.get());
pitch_down.set(zoom_out.get());
roll_cw.set(0.0);
roll_ccw.set(0.0);
zoom_in.set(0.0);
zoom_out.set(0.0);
} else {
set_nav_signal(event, 0.0);
}
},
on:blur=move |_| {
pitch_up.set(0.0);
pitch_down.set(0.0);
yaw_right.set(0.0);
yaw_left.set(0.0);
roll_ccw.set(0.0);
roll_cw.set(0.0);
}
)
}
}

File diff suppressed because it is too large Load diff

View file

@ -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) {
@ -191,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;
@ -206,11 +206,10 @@ 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];

View file

@ -1 +0,0 @@
pub mod engine;

View file

@ -1,67 +1,40 @@
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, ElementKey};
add_remove::AddRemove, use display::Display;
diagnostics::Diagnostics, use outline::Outline;
display::Display,
outline::Outline,
};
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
assembly: Assembly, assembly: Assembly,
selection: Signal<BTreeSet<Rc<dyn Element>>>, selection: Signal<FxHashSet<ElementKey>>
} }
impl AppState { impl AppState {
fn new() -> Self { fn new() -> AppState {
Self { AppState {
assembly: Assembly::new(), assembly: Assembly::new(),
selection: create_signal(BTreeSet::default()), selection: create_signal(FxHashSet::default())
}
}
// in single-selection mode, select the given element. in multiple-selection
// 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());
view! { view! {
div(id = "sidebar") { div(id="sidebar") {
AddRemove {} AddRemove {}
Outline {} Outline {}
Diagnostics {}
} }
Display {} Display {}
} }

207
app-proto/src/outline.rs Normal file
View file

@ -0,0 +1,207 @@
use itertools::Itertools;
use sycamore::prelude::*;
use web_sys::{
Event,
HtmlInputElement,
KeyboardEvent,
MouseEvent,
wasm_bindgen::JsCast
};
use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}};
// an editable view of the Lorentz product representing a constraint
#[component(inline_props)]
fn LorentzProductInput(constraint: Constraint) -> View {
view! {
input(
r#type="text",
bind:value=constraint.lorentz_prod_text,
on:change=move |event: Event| {
let target: HtmlInputElement = event.target().unwrap().unchecked_into();
match target.value().parse::<f64>() {
Ok(lorentz_prod) => batch(|| {
constraint.lorentz_prod.set(lorentz_prod);
constraint.lorentz_prod_valid.set(true);
}),
Err(_) => constraint.lorentz_prod_valid.set(false)
};
}
)
}
}
// a list item that shows a constraint in an outline view of an element
#[component(inline_props)]
fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) -> View {
let state = use_context::<AppState>();
let assembly = &state.assembly;
let constraint = assembly.constraints.with(|csts| csts[constraint_key].clone());
let other_subject = if constraint.subjects.0 == element_key {
constraint.subjects.1
} else {
constraint.subjects.0
};
let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone());
let class = constraint.lorentz_prod_valid.map(
|&lorentz_prod_valid| if lorentz_prod_valid { "constraint" } else { "constraint invalid" }
);
view! {
li(class=class.get()) {
input(r#type="checkbox", bind:checked=constraint.active)
div(class="constraint-label") { (other_subject_label) }
LorentzProductInput(constraint=constraint)
div(class="status")
}
}
}
// a list item that shows an element in an outline view of an assembly
#[component(inline_props)]
fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
let state = use_context::<AppState>();
let class = state.selection.map(
move |sel| if sel.contains(&key) { "selected" } else { "" }
);
let label = element.label.clone();
let rep_components = element.representation.map(
|rep| rep.iter().map(
|u| format!("{:.3}", u).replace("-", "\u{2212}")
).collect()
);
let constrained = element.constraints.map(|csts| csts.len() > 0);
let constraint_list = element.constraints.map(
|csts| csts.clone().into_iter().collect()
);
let details_node = create_node_ref();
view! {
li {
details(ref=details_node) {
summary(
class=class.get(),
on:keydown={
move |event: KeyboardEvent| {
match event.key().as_str() {
"Enter" => {
if event.shift_key() {
state.selection.update(|sel| {
if !sel.remove(&key) {
sel.insert(key);
}
});
} else {
state.selection.update(|sel| {
sel.clear();
sel.insert(key);
});
}
event.prevent_default();
},
"ArrowRight" if constrained.get() => {
let _ = details_node
.get()
.unchecked_into::<web_sys::Element>()
.set_attribute("open", "");
},
"ArrowLeft" => {
let _ = details_node
.get()
.unchecked_into::<web_sys::Element>()
.remove_attribute("open");
},
_ => ()
}
}
}
) {
div(
class="element-switch",
on:click=|event: MouseEvent| event.stop_propagation()
)
div(
class="element",
on:click={
move |event: MouseEvent| {
if event.shift_key() {
state.selection.update(|sel| {
if !sel.remove(&key) {
sel.insert(key);
}
});
} else {
state.selection.update(|sel| {
sel.clear();
sel.insert(key);
});
}
event.stop_propagation();
event.prevent_default();
}
}
) {
div(class="element-label") { (label) }
div(class="element-representation") {
Indexed(
list=rep_components,
view=|coord_str| view! {
div { (coord_str) }
}
)
}
div(class="status")
}
}
ul(class="constraints") {
Keyed(
list=constraint_list,
view=move |cst_key| view! {
ConstraintOutlineItem(
constraint_key=cst_key,
element_key=key
)
},
key=|cst_key| cst_key.clone()
)
}
}
}
}
}
// a component that lists the elements of the current assembly, showing the
// constraints on each element as a collapsible sub-list. its implementation
// is based on Kate Morley's HTML + CSS tree views:
//
// https://iamkate.com/code/tree-views/
//
#[component]
pub fn Outline() -> View {
let state = use_context::<AppState>();
// list the elements alphabetically by ID
let element_list = state.assembly.elements.map(
|elts| elts
.clone()
.into_iter()
.sorted_by_key(|(_, elt)| elt.id.clone())
.collect()
);
view! {
ul(
id="outline",
on:click={
let state = use_context::<AppState>();
move |_| state.selection.update(|sel| sel.clear())
}
) {
Keyed(
list=element_list,
view=|(key, elt)| view! {
ElementOutlineItem(key=key, element=elt)
},
key=|(key, _)| key.clone()
)
}
}
}

View file

@ -1,44 +0,0 @@
use std::num::ParseFloatError;
// a real number described by a specification string. since the structure is
// read-only, we can guarantee that `spec` always specifies `value` in the
// following format
// ┌──────────────────────────────────────────────────────┬───────────┐
// │ `spec` │ `value` │
// ┝━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━┥
// │ a string that parses to the floating-point value `x` │ `Some(x)` │
// ├──────────────────────────────────────────────────────┼───────────┤
// │ the empty string │ `None` │
// └──────────────────────────────────────────────────────┴───────────┘
#[readonly::make]
pub struct SpecifiedValue {
pub spec: String,
pub value: Option<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 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) }
)
}
}
}

View file

@ -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
View file

@ -1,5 +0,0 @@
/dyna3.zip
/dyna3/index.html
/dyna3/dyna3-*.js
/dyna3/dyna3-*.wasm
/dyna3/main-*.css

View file

@ -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"

View file

@ -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