Compare commits

...
Sign in to create a new pull request.

5 commits

Author SHA1 Message Date
af18a8e7d1 Write a deployment packaging script (#113)
Adds a packaging script to help automate deployment. Documents the deployment process in `README.md`.

Also, moves `run-examples.sh` into the tools folder that we created for the packaging script.

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: StudioInfinity/dyna3#113
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-08-11 03:33:19 +00:00
a4565281d5 Refactor: rename loaders and adopt 'Self' type convention (#111)
Resolves #109.
Resolves #110.

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: StudioInfinity/dyna3#111
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-08-07 23:24:07 +00:00
ef1a579ac0 refactor: Code formatting (#108)
Primarily, switch to using trailing commas. Also uniformizes commas with respect to switch branches, makes function call layout more consistent, line breaking more consistent, alphabetizes imports, uses the field init shorthand when possible, etc.

Resolves #99.

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: StudioInfinity/dyna3#108
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-08-04 23:34:33 +00:00
2eba80fb69 Simplify the realization triggering system (#105)
Simplifies the system that reactively triggers realizations, at the cost of removing the preconditioning step described in issue #101 and doing unnecessary realizations after certain kinds of updates.

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: StudioInfinity/dyna3#105
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-07-31 22:21:32 +00:00
0801200210 Add more test assemblies (#103)
This PR helps probe the capabilities of the engine.

Also adjusts the realization triggering system to reduce redundant realizations as we set an assembly's regulators during loading. Specificially, consolidates all calls to `realize()` into a single effect, which is triggered by the `needs_realization` signal.
Also introduces a `keep_realized` signal and use it to pause realization while loading assemblies, but this signal is planned for removal as ultimately we do not want a separate "mode" of interpreting commands during loading, for maximal reproducibility of results (and simplicity of system).

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: StudioInfinity/dyna3#103
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-07-22 22:01:37 +00:00
23 changed files with 1389 additions and 621 deletions

View file

@ -25,32 +25,37 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter
### Install the prerequisites ### Install the prerequisites
1. Install [`rustup`](https://rust-lang.github.io/rustup/): the officially recommended Rust toolchain manager 1. Install [`rustup`](https://rust-lang.github.io/rustup/): the officially recommended Rust toolchain manager
* It's available on Ubuntu as a [Snap](https://snapcraft.io/rustup) - It's available on Ubuntu as a [Snap](https://snapcraft.io/rustup)
2. Call `rustup default stable` to "download the latest stable release of Rust and set it as your default toolchain" 2. Call `rustup default stable` to "download the latest stable release of Rust and set it as your default toolchain"
* If you forget, the `rustup` [help system](https://github.com/rust-lang/rustup/blob/d9b3601c3feb2e88cf3f8ca4f7ab4fdad71441fd/src/errors.rs#L109-L112) will remind you - If you forget, the `rustup` [help system](https://github.com/rust-lang/rustup/blob/d9b3601c3feb2e88cf3f8ca4f7ab4fdad71441fd/src/errors.rs#L109-L112) will remind you
3. Call `rustup target add wasm32-unknown-unknown` to add the [most generic 32-bit WebAssembly target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-unknown-unknown.html) 3. Call `rustup target add wasm32-unknown-unknown` to add the [most generic 32-bit WebAssembly target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-unknown-unknown.html)
4. Call `cargo install wasm-pack` to install the [WebAssembly toolchain](https://rustwasm.github.io/docs/wasm-pack/) 4. Call `cargo install wasm-pack` to install the [WebAssembly toolchain](https://rustwasm.github.io/docs/wasm-pack/)
5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool 5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool
6. Add the `.cargo/bin` folder in your home directory to your executable search path 6. Add the `.cargo/bin` folder in your home directory to your executable search path
* This lets you call Trunk, and other tools installed by Cargo, without specifying their paths - This lets you call Trunk, and other tools installed by Cargo, without specifying their paths
* On POSIX systems, the search path is stored in the `PATH` environment variable - On POSIX systems, the search path is stored in the `PATH` environment variable
### Play with the prototype ### Play with the prototype
1. From the `app-proto` folder, call `trunk serve --release` to build and serve 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* - The crates the prototype depends on will be downloaded and served automatically
* *For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag* - For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag
* *If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]`* from there instead. - If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]` from there instead.
3. In a web browser, visit one of the URLs listed under the message `INFO 📡 server listening at:` 3. In a web browser, visit one of the URLs listed under the message `INFO 📡 server listening at:`
* *Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype* - Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype
4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype 4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype
### Run the engine on some example problems ### Run the engine on some example problems
1. Go into the `app-proto` folder 1. Use `sh` to run the script `tools/run-examples.sh`
2. Call `./run-examples` - The script is location-independent, so you can do this from anywhere in the dyna3 repository
* *For each example problem, the engine will print the value of the loss function at each optimization step* - The call from the top level of the repository is:
* *The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then*
```bash
sh tools/run-examples.sh
```
- For each example problem, the engine will print the value of the loss function at each optimization step
- The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then
```julia ```julia
include("irisawa-hexlet.jl") include("irisawa-hexlet.jl")
@ -59,9 +64,24 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter
end end
``` ```
*you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show* you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show
### Run the automated tests ### Run the automated tests
1. Go into the `app-proto` folder 1. Go into the `app-proto` folder
2. Call `cargo test` 2. Call `cargo test`
### Deploy the prototype
1. From the `app-proto` folder, call `trunk build --release`
- Building in [release mode](https://doc.rust-lang.org/cargo/reference/profiles.html#release) produces an executable which is smaller and often much faster, but harder to debug and more time-consuming to build
- If you want to stay in the top-level folder, you can call `trunk build --config app-proto --release` from there instead
2. Use `sh` to run the packaging script `tools/package-for-deployment.sh`.
- The script is location-independent, so you can do this from anywhere in the dyna3 repository
- The call from the top level of the repository is:
```bash
sh tools/package-for-deployment.sh
```
- This will overwrite or replace the files in `deploy/dyna3`
3. Put the contents of `deploy/dyna3` in the folder on your server that the prototype will be served from.
- To simplify uploading, you might want to combine these files into an archive called `deploy/dyna3.zip`. Git has been set to ignore this path

2
app-proto/Trunk.toml Normal file
View file

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

View file

@ -23,7 +23,7 @@ fn main() {
let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map(
|n| [ |n| [
tangent.proj(&up.as_view(), n), tangent.proj(&up.as_view(), n),
tangent.proj(&down.as_view(), n+1) tangent.proj(&down.as_view(), n+1),
] ]
).sum(); ).sum();
let normalization = 5.0 / twist_motion[(2, 0)]; let normalization = 5.0 / twist_motion[(2, 0)];

View file

@ -6,7 +6,7 @@ use dyna3::engine::{
realize_gram, realize_gram,
sphere, sphere,
ConfigNeighborhood, ConfigNeighborhood,
ConstraintProblem ConstraintProblem,
}; };
fn main() { fn main() {
@ -25,7 +25,7 @@ fn main() {
); );
print::title("Point on a sphere"); print::title("Point on a sphere");
print::realization_diagnostics(&realization); print::realization_diagnostics(&realization);
if let Ok(ConfigNeighborhood{ config, .. }) = realization.result { if let Ok(ConfigNeighborhood { config, .. }) = realization.result {
print::gram_matrix(&config); print::gram_matrix(&config);
print::config(&config); print::config(&config);
} }

View file

@ -5,7 +5,7 @@ use dyna3::engine::{
realize_gram, realize_gram,
sphere, sphere,
ConfigNeighborhood, ConfigNeighborhood,
ConstraintProblem ConstraintProblem,
}; };
fn main() { fn main() {
@ -14,7 +14,7 @@ fn main() {
&[ &[
sphere(1.0, 0.0, 0.0, 1.0), sphere(1.0, 0.0, 0.0, 1.0),
sphere(-0.5, a, 0.0, 1.0), sphere(-0.5, a, 0.0, 1.0),
sphere(-0.5, -a, 0.0, 1.0) sphere(-0.5, -a, 0.0, 1.0),
] ]
}); });
for j in 0..3 { for j in 0..3 {
@ -27,7 +27,7 @@ fn main() {
); );
print::title("Three spheres"); print::title("Three spheres");
print::realization_diagnostics(&realization); print::realization_diagnostics(&realization);
if let Ok(ConfigNeighborhood{ config, .. }) = realization.result { if let Ok(ConfigNeighborhood { config, .. }) = realization.result {
print::gram_matrix(&config); print::gram_matrix(&config);
} }
print::loss_history(&realization.history); print::loss_history(&realization.history);

View file

@ -1,259 +0,0 @@
use std::{f64::consts::FRAC_1_SQRT_2, rc::Rc};
use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue};
use crate::{
AppState,
engine,
engine::DescentHistory,
assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere}
};
/* 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(
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)
)
);
}
/* 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(
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)
)
);
}
fn load_pointed_assemb(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)
)
);
}
}
}
#[component]
pub fn AddRemove() -> View {
/* DEBUG */
let assembly_name = create_signal("general".to_string());
create_effect(move || {
// get name of chosen assembly
let name = assembly_name.get_clone();
console::log_1(
&JsValue::from(format!("Showing assembly \"{}\"", name.clone()))
);
batch(|| {
let state = use_context::<AppState>();
let assembly = &state.assembly;
// clear state
assembly.regulators.update(|regs| regs.clear());
assembly.elements.update(|elts| elts.clear());
assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear());
assembly.descent_history.set(DescentHistory::new());
state.selection.update(|sel| sel.clear());
// load assembly
match name.as_str() {
"general" => load_gen_assemb(assembly),
"low-curv" => load_low_curv_assemb(assembly),
"pointed" => load_pointed_assemb(assembly),
_ => ()
};
});
});
view! {
div(id="add-remove") {
button(
on:click=|_| {
let state = use_context::<AppState>();
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());
}
) { "🔗" }
select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser
option(value="general") { "General" }
option(value="low-curv") { "Low-curvature" }
option(value="pointed") { "Pointed" }
option(value="empty") { "Empty" }
}
}
}
}

View file

@ -1,22 +1,21 @@
use nalgebra::{DMatrix, DVector, DVectorView}; use nalgebra::{DMatrix, DVector, DVectorView};
use std::{ use std::{
cell::Cell, cell::Cell,
collections::{BTreeMap, BTreeSet},
cmp::Ordering, cmp::Ordering,
collections::{BTreeMap, BTreeSet},
fmt, fmt,
fmt::{Debug, Formatter}, fmt::{Debug, Formatter},
hash::{Hash, Hasher}, hash::{Hash, Hasher},
rc::Rc, rc::Rc,
sync::{atomic, atomic::AtomicU64} sync::{atomic, atomic::AtomicU64},
}; };
use sycamore::prelude::*; use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
use crate::{ use crate::{
display::DisplayItem, components::{display::DisplayItem, outline::OutlineItem},
engine::{ engine::{
Q, Q,
change_half_curvature,
local_unif_to_std, local_unif_to_std,
point, point,
project_point_to_normalized, project_point_to_normalized,
@ -27,10 +26,9 @@ use crate::{
ConfigSubspace, ConfigSubspace,
ConstraintProblem, ConstraintProblem,
DescentHistory, DescentHistory,
Realization Realization,
}, },
outline::OutlineItem, specified::SpecifiedValue,
specified::SpecifiedValue
}; };
pub type ElementColor = [f32; 3]; pub type ElementColor = [f32; 3];
@ -166,7 +164,7 @@ pub struct Sphere {
pub ghost: Signal<bool>, pub ghost: Signal<bool>,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>, pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
serial: u64, serial: u64,
column_index: Cell<Option<usize>> column_index: Cell<Option<usize>>,
} }
impl Sphere { impl Sphere {
@ -176,17 +174,17 @@ impl Sphere {
id: String, id: String,
label: String, label: String,
color: ElementColor, color: ElementColor,
representation: DVector<f64> representation: DVector<f64>,
) -> Sphere { ) -> Self {
Sphere { Self {
id: id, id,
label: label, label,
color: color, color,
representation: create_signal(representation), representation: create_signal(representation),
ghost: create_signal(false), ghost: create_signal(false),
regulators: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(), serial: Self::next_serial(),
column_index: None.into() column_index: None.into(),
} }
} }
} }
@ -196,12 +194,12 @@ impl Element for Sphere {
"sphere".to_string() "sphere".to_string()
} }
fn default(id: String, id_num: u64) -> Sphere { fn default(id: String, id_num: u64) -> Self {
Sphere::new( Self::new(
id, id,
format!("Sphere {id_num}"), format!("Sphere {id_num}"),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
sphere(0.0, 0.0, 0.0, 1.0) sphere(0.0, 0.0, 0.0, 1.0),
) )
} }
@ -266,7 +264,7 @@ pub struct Point {
pub ghost: Signal<bool>, pub ghost: Signal<bool>,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>, pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
serial: u64, serial: u64,
column_index: Cell<Option<usize>> column_index: Cell<Option<usize>>,
} }
impl Point { impl Point {
@ -276,9 +274,9 @@ impl Point {
id: String, id: String,
label: String, label: String,
color: ElementColor, color: ElementColor,
representation: DVector<f64> representation: DVector<f64>,
) -> Point { ) -> Self {
Point { Self {
id, id,
label, label,
color, color,
@ -286,7 +284,7 @@ impl Point {
ghost: create_signal(false), ghost: create_signal(false),
regulators: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(), serial: Self::next_serial(),
column_index: None.into() column_index: None.into(),
} }
} }
} }
@ -296,12 +294,12 @@ impl Element for Point {
"point".to_string() "point".to_string()
} }
fn default(id: String, id_num: u64) -> Point { fn default(id: String, id_num: u64) -> Self {
Point::new( Self::new(
id, id,
format!("Point {id_num}"), format!("Point {id_num}"),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
point(0.0, 0.0, 0.0) point(0.0, 0.0, 0.0),
) )
} }
@ -350,7 +348,7 @@ impl ProblemPoser for Point {
format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str() format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str()
); );
problem.gram.push_sym(index, index, 0.0); problem.gram.push_sym(index, index, 0.0);
problem.frozen.push(Point::WEIGHT_COMPONENT, index, 0.5); problem.frozen.push(Self::WEIGHT_COMPONENT, index, 0.5);
problem.guess.set_column(index, &self.representation.get_clone_untracked()); problem.guess.set_column(index, &self.representation.get_clone_untracked());
} }
} }
@ -359,16 +357,6 @@ pub trait Regulator: Serial + ProblemPoser + OutlineItem {
fn subjects(&self) -> Vec<Rc<dyn Element>>; fn subjects(&self) -> Vec<Rc<dyn Element>>;
fn measurement(&self) -> ReadSignal<f64>; fn measurement(&self) -> ReadSignal<f64>;
fn set_point(&self) -> Signal<SpecifiedValue>; fn set_point(&self) -> Signal<SpecifiedValue>;
// this method is used to responsively precondition the assembly for
// realization when the regulator becomes a constraint, or is edited while
// acting as a constraint. it should track the set point, do any desired
// preconditioning when the set point is present, and use its return value
// to report whether the set is present. the default implementation does no
// preconditioning
fn try_activate(&self) -> bool {
self.set_point().with(|set_pt| set_pt.is_present())
}
} }
impl Hash for dyn Regulator { impl Hash for dyn Regulator {
@ -401,11 +389,11 @@ pub struct InversiveDistanceRegulator {
pub subjects: [Rc<dyn Element>; 2], pub subjects: [Rc<dyn Element>; 2],
pub measurement: ReadSignal<f64>, pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>, pub set_point: Signal<SpecifiedValue>,
serial: u64 serial: u64,
} }
impl InversiveDistanceRegulator { impl InversiveDistanceRegulator {
pub fn new(subjects: [Rc<dyn Element>; 2]) -> InversiveDistanceRegulator { pub fn new(subjects: [Rc<dyn Element>; 2]) -> Self {
let representations = subjects.each_ref().map(|subj| subj.representation()); let representations = subjects.each_ref().map(|subj| subj.representation());
let measurement = create_memo(move || { let measurement = create_memo(move || {
representations[0].with(|rep_0| representations[0].with(|rep_0|
@ -418,7 +406,7 @@ impl InversiveDistanceRegulator {
let set_point = create_signal(SpecifiedValue::from_empty_spec()); let set_point = create_signal(SpecifiedValue::from_empty_spec());
let serial = Self::next_serial(); let serial = Self::next_serial();
InversiveDistanceRegulator { subjects, measurement, set_point, serial } Self { subjects, measurement, set_point, serial }
} }
} }
@ -461,11 +449,11 @@ pub struct HalfCurvatureRegulator {
pub subject: Rc<dyn Element>, pub subject: Rc<dyn Element>,
pub measurement: ReadSignal<f64>, pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>, pub set_point: Signal<SpecifiedValue>,
serial: u64 serial: u64,
} }
impl HalfCurvatureRegulator { impl HalfCurvatureRegulator {
pub fn new(subject: Rc<dyn Element>) -> HalfCurvatureRegulator { pub fn new(subject: Rc<dyn Element>) -> Self {
let measurement = subject.representation().map( let measurement = subject.representation().map(
|rep| rep[Sphere::CURVATURE_COMPONENT] |rep| rep[Sphere::CURVATURE_COMPONENT]
); );
@ -473,7 +461,7 @@ impl HalfCurvatureRegulator {
let set_point = create_signal(SpecifiedValue::from_empty_spec()); let set_point = create_signal(SpecifiedValue::from_empty_spec());
let serial = Self::next_serial(); let serial = Self::next_serial();
HalfCurvatureRegulator { subject, measurement, set_point, serial } Self { subject, measurement, set_point, serial }
} }
} }
@ -489,18 +477,6 @@ impl Regulator for HalfCurvatureRegulator {
fn set_point(&self) -> Signal<SpecifiedValue> { fn set_point(&self) -> Signal<SpecifiedValue> {
self.set_point self.set_point
} }
fn try_activate(&self) -> bool {
match self.set_point.with(|set_pt| set_pt.value) {
Some(half_curv) => {
self.subject.representation().update(
|rep| change_half_curvature(rep, half_curv)
);
true
}
None => false
}
}
} }
impl Serial for HalfCurvatureRegulator { impl Serial for HalfCurvatureRegulator {
@ -525,7 +501,7 @@ impl ProblemPoser for HalfCurvatureRegulator {
// the velocity is expressed in uniform coordinates // the velocity is expressed in uniform coordinates
pub struct ElementMotion<'a> { pub struct ElementMotion<'a> {
pub element: Rc<dyn Element>, pub element: Rc<dyn Element>,
pub velocity: DVectorView<'a, f64> pub velocity: DVectorView<'a, f64>,
} }
type AssemblyMotion<'a> = Vec<ElementMotion<'a>>; type AssemblyMotion<'a> = Vec<ElementMotion<'a>>;
@ -552,21 +528,42 @@ pub struct Assembly {
// indexing // indexing
pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>, pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>,
// realization control
pub realization_trigger: Signal<()>,
// realization diagnostics // realization diagnostics
pub realization_status: Signal<Result<(), String>>, pub realization_status: Signal<Result<(), String>>,
pub descent_history: Signal<DescentHistory> pub descent_history: Signal<DescentHistory>,
} }
impl Assembly { impl Assembly {
pub fn new() -> Assembly { pub fn new() -> Assembly {
Assembly { // create an assembly
let assembly = Assembly {
elements: create_signal(BTreeSet::new()), elements: create_signal(BTreeSet::new()),
regulators: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()),
tangent: create_signal(ConfigSubspace::zero(0)), tangent: create_signal(ConfigSubspace::zero(0)),
elements_by_id: create_signal(BTreeMap::default()), elements_by_id: create_signal(BTreeMap::default()),
realization_trigger: create_signal(()),
realization_status: create_signal(Ok(())), realization_status: create_signal(Ok(())),
descent_history: create_signal(DescentHistory::new()) descent_history: create_signal(DescentHistory::new()),
} };
// realize the assembly whenever the element list, the regulator list,
// a regulator's set point, or the realization trigger is updated
let assembly_for_effect = assembly.clone();
create_effect(move || {
assembly_for_effect.elements.track();
assembly_for_effect.regulators.with(
|regs| for reg in regs {
reg.set_point().track();
}
);
assembly_for_effect.realization_trigger.track();
assembly_for_effect.realize();
});
assembly
} }
// --- inserting elements and regulators --- // --- inserting elements and regulators ---
@ -627,19 +624,6 @@ impl Assembly {
regulators.update(|regs| regs.insert(regulator.clone())); regulators.update(|regs| regs.insert(regulator.clone()));
} }
// update the realization when the regulator becomes a constraint, or is
// edited while acting as a constraint
let self_for_effect = self.clone();
create_effect(move || {
/* DEBUG */
// log the regulator update
console_log!("Updated regulator with subjects {:?}", regulator.subjects());
if regulator.try_activate() {
self_for_effect.realize();
}
});
/* DEBUG */ /* DEBUG */
// print an updated list of regulators // print an updated list of regulators
console_log!("Regulators:"); console_log!("Regulators:");
@ -707,8 +691,10 @@ impl Assembly {
} else { } else {
console_log!("✅️ Target accuracy achieved!"); console_log!("✅️ Target accuracy achieved!");
} }
console_log!("Steps: {}", history.scaled_loss.len() - 1); if history.scaled_loss.len() > 0 {
console_log!("Loss: {}", history.scaled_loss.last().unwrap()); console_log!("Steps: {}", history.scaled_loss.len() - 1);
console_log!("Loss: {}", history.scaled_loss.last().unwrap());
}
// report the loss history // report the loss history
self.descent_history.set(history); self.descent_history.set(history);
@ -738,7 +724,7 @@ impl Assembly {
// `Err(message)` we received from the match: we're changing the // `Err(message)` we received from the match: we're changing the
// `Ok` type from `Realization` to `()` // `Ok` type from `Realization` to `()`
self.realization_status.set(Err(message)) self.realization_status.set(Err(message))
} },
} }
} }
@ -821,15 +807,15 @@ impl Assembly {
}, },
None => { None => {
console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) console_log!("No velocity to unpack for fresh element \"{}\"", elt.id())
} },
}; };
}); });
} }
// bring the configuration back onto the solution variety. this also // trigger a realization to bring the configuration back onto the
// gets the elements' column indices and the saved tangent space back in // solution variety. this also gets the elements' column indices and the
// sync // saved tangent space back in sync
self.realize(); self.realization_trigger.set(());
} }
} }
@ -881,7 +867,7 @@ mod tests {
String::from(sphere_id), String::from(sphere_id),
String::from("Sphere 0"), String::from("Sphere 0"),
[0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS) engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS),
) )
); );
@ -895,7 +881,7 @@ mod tests {
vec![ vec![
ElementMotion { ElementMotion {
element: sphere.clone(), element: sphere.clone(),
velocity: velocity.as_view() velocity: velocity.as_view(),
} }
] ]
); );

View file

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

View file

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

View file

@ -11,14 +11,12 @@ use crate::AppState;
#[derive(Clone)] #[derive(Clone)]
struct DiagnosticsState { struct DiagnosticsState {
active_tab: Signal<String> active_tab: Signal<String>,
} }
impl DiagnosticsState { impl DiagnosticsState {
fn new(initial_tab: String) -> DiagnosticsState { fn new(initial_tab: String) -> Self {
DiagnosticsState { Self { active_tab: create_signal(initial_tab) }
active_tab: create_signal(initial_tab)
}
} }
} }
@ -29,20 +27,20 @@ fn RealizationStatus() -> View {
let realization_status = state.assembly.realization_status; let realization_status = state.assembly.realization_status;
view! { view! {
div( div(
id="realization-status", id = "realization-status",
class=realization_status.with( class = realization_status.with(
|status| match status { |status| match status {
Ok(_) => "", Ok(_) => "",
Err(_) => "invalid" Err(_) => "invalid",
} }
) )
) { ) {
div(class="status") div(class = "status")
div { div {
(realization_status.with( (realization_status.with(
|status| match status { |status| match status {
Ok(_) => "Target accuracy achieved".to_string(), Ok(_) => "Target accuracy achieved".to_string(),
Err(message) => message.clone() Err(message) => message.clone(),
} }
)) ))
} }
@ -53,7 +51,7 @@ fn RealizationStatus() -> View {
fn into_log10_time_point((step, value): (usize, f64)) -> Vec<Option<f64>> { fn into_log10_time_point((step, value): (usize, f64)) -> Vec<Option<f64>> {
vec![ vec![
Some(step as f64), Some(step as f64),
if value == 0.0 { None } else { Some(value.abs().log10()) } if value == 0.0 { None } else { Some(value.abs().log10()) },
] ]
} }
@ -105,7 +103,7 @@ fn LossHistory() -> View {
}); });
view! { view! {
div(id=CONTAINER_ID, class="diagnostics-chart") div(id = CONTAINER_ID, class = "diagnostics-chart")
} }
} }
@ -122,7 +120,7 @@ fn SpectrumHistory() -> View {
// positive, negative, and strictly-zero parts // positive, negative, and strictly-zero parts
let ( let (
hess_eigvals_zero, hess_eigvals_zero,
hess_eigvals_nonzero hess_eigvals_nonzero,
): (Vec<_>, Vec<_>) = state.assembly.descent_history.with( ): (Vec<_>, Vec<_>) = state.assembly.descent_history.with(
|history| history.hess_eigvals |history| history.hess_eigvals
.iter() .iter()
@ -143,7 +141,7 @@ fn SpectrumHistory() -> View {
.unwrap_or(1.0); .unwrap_or(1.0);
let ( let (
hess_eigvals_pos, hess_eigvals_pos,
hess_eigvals_neg hess_eigvals_neg,
): (Vec<_>, Vec<_>) = hess_eigvals_nonzero ): (Vec<_>, Vec<_>) = hess_eigvals_nonzero
.into_iter() .into_iter()
.partition(|&(_, val)| val > 0.0); .partition(|&(_, val)| val > 0.0);
@ -211,7 +209,7 @@ fn SpectrumHistory() -> View {
}); });
view! { view! {
div(id=CONTAINER_ID, class="diagnostics-chart") div(id = CONTAINER_ID, class = "diagnostics-chart")
} }
} }
@ -220,8 +218,8 @@ fn DiagnosticsPanel(name: &'static str, children: Children) -> View {
let diagnostics_state = use_context::<DiagnosticsState>(); let diagnostics_state = use_context::<DiagnosticsState>();
view! { view! {
div( div(
class="diagnostics-panel", class = "diagnostics-panel",
"hidden"=diagnostics_state.active_tab.with( "hidden" = diagnostics_state.active_tab.with(
|active_tab| { |active_tab| {
if active_tab == name { if active_tab == name {
None None
@ -243,16 +241,16 @@ pub fn Diagnostics() -> View {
provide_context(diagnostics_state); provide_context(diagnostics_state);
view! { view! {
div(id="diagnostics") { div(id = "diagnostics") {
div(id="diagnostics-bar") { div(id = "diagnostics-bar") {
RealizationStatus {} RealizationStatus {}
select(bind:value=active_tab) { select(bind:value = active_tab) {
option(value="loss") { "Loss" } option(value = "loss") { "Loss" }
option(value="spectrum") { "Spectrum" } option(value = "spectrum") { "Spectrum" }
} }
} }
DiagnosticsPanel(name="loss") { LossHistory {} } DiagnosticsPanel(name = "loss") { LossHistory {} }
DiagnosticsPanel(name="spectrum") { SpectrumHistory {} } DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} }
} }
} }
} }

View file

@ -12,12 +12,12 @@ use web_sys::{
WebGlProgram, WebGlProgram,
WebGlShader, WebGlShader,
WebGlUniformLocation, WebGlUniformLocation,
wasm_bindgen::{JsCast, JsValue} wasm_bindgen::{JsCast, JsValue},
}; };
use crate::{ use crate::{
AppState, AppState,
assembly::{Element, ElementColor, ElementMotion, Point, Sphere} assembly::{Element, ElementColor, ElementMotion, Point, Sphere},
}; };
// --- color --- // --- color ---
@ -37,15 +37,15 @@ fn combine_channels(color: ElementColor, opacity: f32) -> ColorWithOpacity {
struct SceneSpheres { struct SceneSpheres {
representations: Vec<DVector<f64>>, representations: Vec<DVector<f64>>,
colors_with_opacity: Vec<ColorWithOpacity>, colors_with_opacity: Vec<ColorWithOpacity>,
highlights: Vec<f32> highlights: Vec<f32>,
} }
impl SceneSpheres { impl SceneSpheres {
fn new() -> SceneSpheres{ fn new() -> Self {
SceneSpheres { Self {
representations: Vec::new(), representations: Vec::new(),
colors_with_opacity: Vec::new(), colors_with_opacity: Vec::new(),
highlights: Vec::new() highlights: Vec::new(),
} }
} }
@ -53,7 +53,10 @@ impl SceneSpheres {
self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer") 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) { fn push(
&mut self, representation: DVector<f64>,
color: ElementColor, opacity: f32, highlight: f32,
) {
self.representations.push(representation); self.representations.push(representation);
self.colors_with_opacity.push(combine_channels(color, opacity)); self.colors_with_opacity.push(combine_channels(color, opacity));
self.highlights.push(highlight); self.highlights.push(highlight);
@ -64,20 +67,23 @@ struct ScenePoints {
representations: Vec<DVector<f64>>, representations: Vec<DVector<f64>>,
colors_with_opacity: Vec<ColorWithOpacity>, colors_with_opacity: Vec<ColorWithOpacity>,
highlights: Vec<f32>, highlights: Vec<f32>,
selections: Vec<f32> selections: Vec<f32>,
} }
impl ScenePoints { impl ScenePoints {
fn new() -> ScenePoints { fn new() -> Self {
ScenePoints { Self {
representations: Vec::new(), representations: Vec::new(),
colors_with_opacity: Vec::new(), colors_with_opacity: Vec::new(),
highlights: Vec::new(), highlights: Vec::new(),
selections: Vec::new() selections: Vec::new(),
} }
} }
fn push(&mut self, representation: DVector<f64>, color: ElementColor, opacity: f32, highlight: f32, selected: bool) { fn push(
&mut self, representation: DVector<f64>,
color: ElementColor, opacity: f32, highlight: f32, selected: bool,
) {
self.representations.push(representation); self.representations.push(representation);
self.colors_with_opacity.push(combine_channels(color, opacity)); self.colors_with_opacity.push(combine_channels(color, opacity));
self.highlights.push(highlight); self.highlights.push(highlight);
@ -87,14 +93,14 @@ impl ScenePoints {
pub struct Scene { pub struct Scene {
spheres: SceneSpheres, spheres: SceneSpheres,
points: ScenePoints points: ScenePoints,
} }
impl Scene { impl Scene {
fn new() -> Scene { fn new() -> Self {
Scene { Self {
spheres: SceneSpheres::new(), spheres: SceneSpheres::new(),
points: ScenePoints::new() points: ScenePoints::new(),
} }
} }
} }
@ -105,7 +111,12 @@ pub trait DisplayItem {
// the smallest positive depth, represented as a multiple of `dir`, where // the smallest positive depth, represented as a multiple of `dir`, where
// the line generated by `dir` hits the element. returns `None` if the line // the line generated by `dir` hits the element. returns `None` if the line
// misses the element // misses the element
fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, pixel_size: f64) -> Option<f64>; fn cast(
&self,
dir: Vector3<f64>,
assembly_to_world: &DMatrix<f64>,
pixel_size: f64,
) -> Option<f64>;
} }
impl DisplayItem for Sphere { impl DisplayItem for Sphere {
@ -124,7 +135,12 @@ impl DisplayItem for Sphere {
// this method should be kept synchronized with `sphere_cast` in // this method should be kept synchronized with `sphere_cast` in
// `spheres.frag`, which does essentially the same thing on the GPU side // `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> { 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 // if `a/b` is less than this threshold, we approximate
// `a*u^2 + b*u + c` by the linear function `b*u + c` // `a*u^2 + b*u + c` by the linear function `b*u + c`
const DEG_THRESHOLD: f64 = 1e-9; const DEG_THRESHOLD: f64 = 1e-9;
@ -177,7 +193,12 @@ impl DisplayItem for Point {
} }
/* SCAFFOLDING */ /* SCAFFOLDING */
fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, pixel_size: f64) -> Option<f64> { 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); let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
if rep[2] < 0.0 { if rep[2] < 0.0 {
// this constant should be kept synchronized with `point.frag` // this constant should be kept synchronized with `point.frag`
@ -220,7 +241,7 @@ fn compile_shader(
fn set_up_program( fn set_up_program(
context: &WebGl2RenderingContext, context: &WebGl2RenderingContext,
vertex_shader_source: &str, vertex_shader_source: &str,
fragment_shader_source: &str fragment_shader_source: &str,
) -> WebGlProgram { ) -> WebGlProgram {
// compile the shaders // compile the shaders
let vertex_shader = compile_shader( let vertex_shader = compile_shader(
@ -260,12 +281,12 @@ fn get_uniform_array_locations<const N: usize>(
context: &WebGl2RenderingContext, context: &WebGl2RenderingContext,
program: &WebGlProgram, program: &WebGlProgram,
var_name: &str, var_name: &str,
member_name_opt: Option<&str> member_name_opt: Option<&str>,
) -> [Option<WebGlUniformLocation>; N] { ) -> [Option<WebGlUniformLocation>; N] {
array::from_fn(|n| { array::from_fn(|n| {
let name = match member_name_opt { let name = match member_name_opt {
Some(member_name) => format!("{var_name}[{n}].{member_name}"), Some(member_name) => format!("{var_name}[{n}].{member_name}"),
None => format!("{var_name}[{n}]") None => format!("{var_name}[{n}]"),
}; };
context.get_uniform_location(&program, name.as_str()) context.get_uniform_location(&program, name.as_str())
}) })
@ -276,7 +297,7 @@ fn bind_to_attribute(
context: &WebGl2RenderingContext, context: &WebGl2RenderingContext,
attr_index: u32, attr_index: u32,
attr_size: i32, attr_size: i32,
buffer: &Option<WebGlBuffer> buffer: &Option<WebGlBuffer>,
) { ) {
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref()); context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
context.vertex_attrib_pointer_with_i32( context.vertex_attrib_pointer_with_i32(
@ -292,7 +313,7 @@ fn bind_to_attribute(
// load the given data into a new vertex buffer object // load the given data into a new vertex buffer object
fn load_new_buffer( fn load_new_buffer(
context: &WebGl2RenderingContext, context: &WebGl2RenderingContext,
data: &[f32] data: &[f32],
) -> Option<WebGlBuffer> { ) -> Option<WebGlBuffer> {
// create a buffer and bind it to ARRAY_BUFFER // create a buffer and bind it to ARRAY_BUFFER
let buffer = context.create_buffer(); let buffer = context.create_buffer();
@ -319,7 +340,7 @@ fn bind_new_buffer_to_attribute(
context: &WebGl2RenderingContext, context: &WebGl2RenderingContext,
attr_index: u32, attr_index: u32,
attr_size: i32, attr_size: i32,
data: &[f32] data: &[f32],
) { ) {
let buffer = load_new_buffer(context, data); let buffer = load_new_buffer(context, data);
bind_to_attribute(context, attr_index, attr_size, &buffer); bind_to_attribute(context, attr_index, attr_size, &buffer);
@ -341,9 +362,9 @@ fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
Vector3::new( Vector3::new(
FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim, 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, FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim,
-1.0 -1.0,
), ),
FOCAL_SLOPE * 2.0 / shortdim FOCAL_SLOPE * 2.0 / shortdim,
) )
} }
@ -443,14 +464,14 @@ pub fn Display() -> View {
let sphere_program = set_up_program( let sphere_program = set_up_program(
&ctx, &ctx,
include_str!("identity.vert"), include_str!("identity.vert"),
include_str!("spheres.frag") include_str!("spheres.frag"),
); );
// set up the point rendering program // set up the point rendering program
let point_program = set_up_program( let point_program = set_up_program(
&ctx, &ctx,
include_str!("point.vert"), include_str!("point.vert"),
include_str!("point.frag") include_str!("point.frag"),
); );
/* DEBUG */ /* DEBUG */
@ -467,7 +488,7 @@ pub fn Display() -> View {
// capped at 1024 elements // capped at 1024 elements
console::log_2( console::log_2(
&ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(), &ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(),
&JsValue::from("uniform vectors available") &JsValue::from("uniform vectors available"),
); );
// find the sphere program's vertex attribute // find the sphere program's vertex attribute
@ -503,7 +524,7 @@ pub fn Display() -> View {
// southeast triangle // southeast triangle
-1.0, -1.0, 0.0, -1.0, -1.0, 0.0,
1.0, 1.0, 0.0, 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); let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions);
@ -596,7 +617,7 @@ pub fn Display() -> View {
vec![ vec![
ElementMotion { ElementMotion {
element: sel, element: sel,
velocity: elt_motion.as_view() velocity: elt_motion.as_view(),
} }
] ]
); );
@ -629,7 +650,7 @@ pub fn Display() -> View {
0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0, u, 0.0, 0.0, 1.0, 0.0, u,
0.0, 0.0, 2.0*u, 1.0, u*u, 0.0, 0.0, 2.0*u, 1.0, u*u,
0.0, 0.0, 0.0, 0.0, 1.0 0.0, 0.0, 0.0, 0.0, 1.0,
]) ])
}; };
let asm_to_world = &location * &orientation; let asm_to_world = &location * &orientation;
@ -668,19 +689,19 @@ pub fn Display() -> View {
let v = &sphere_reps_world[n]; let v = &sphere_reps_world[n];
ctx.uniform3fv_with_f32_array( ctx.uniform3fv_with_f32_array(
sphere_sp_locs[n].as_ref(), sphere_sp_locs[n].as_ref(),
v.rows(0, 3).as_slice() v.rows(0, 3).as_slice(),
); );
ctx.uniform2fv_with_f32_array( ctx.uniform2fv_with_f32_array(
sphere_lt_locs[n].as_ref(), sphere_lt_locs[n].as_ref(),
v.rows(3, 2).as_slice() v.rows(3, 2).as_slice(),
); );
ctx.uniform4fv_with_f32_array( ctx.uniform4fv_with_f32_array(
sphere_color_locs[n].as_ref(), sphere_color_locs[n].as_ref(),
&scene.spheres.colors_with_opacity[n] &scene.spheres.colors_with_opacity[n],
); );
ctx.uniform1f( ctx.uniform1f(
sphere_highlight_locs[n].as_ref(), sphere_highlight_locs[n].as_ref(),
scene.spheres.highlights[n] scene.spheres.highlights[n],
); );
} }
@ -773,7 +794,7 @@ pub fn Display() -> View {
"ArrowLeft" if shift => roll_ccw.set(value), "ArrowLeft" if shift => roll_ccw.set(value),
"ArrowRight" => yaw_right.set(value), "ArrowRight" => yaw_right.set(value),
"ArrowLeft" => yaw_left.set(value), "ArrowLeft" => yaw_left.set(value),
_ => navigating = false _ => navigating = false,
}; };
if navigating { if navigating {
scene_changed.set(true); scene_changed.set(true);
@ -793,7 +814,7 @@ pub fn Display() -> View {
"s" | "S" => translate_neg_y.set(value), "s" | "S" => translate_neg_y.set(value),
"]" | "}" => shrink_neg.set(value), "]" | "}" => shrink_neg.set(value),
"[" | "{" => shrink_pos.set(value), "[" | "{" => shrink_pos.set(value),
_ => manipulating = false _ => manipulating = false,
}; };
if manipulating { if manipulating {
event.prevent_default(); event.prevent_default();
@ -805,12 +826,12 @@ pub fn Display() -> View {
// switch back to integer-valued parameters when that becomes possible // switch back to integer-valued parameters when that becomes possible
// again // again
canvas( canvas(
ref=display, ref = display,
id="display", id = "display",
width="600", width = "600",
height="600", height = "600",
tabindex="0", tabindex = "0",
on:keydown=move |event: KeyboardEvent| { on:keydown = move |event: KeyboardEvent| {
if event.key() == "Shift" { if event.key() == "Shift" {
// swap navigation inputs // swap navigation inputs
roll_cw.set(yaw_right.get()); roll_cw.set(yaw_right.get());
@ -836,7 +857,7 @@ pub fn Display() -> View {
set_manip_signal(&event, 1.0); set_manip_signal(&event, 1.0);
} }
}, },
on:keyup=move |event: KeyboardEvent| { on:keyup = move |event: KeyboardEvent| {
if event.key() == "Shift" { if event.key() == "Shift" {
// swap navigation inputs // swap navigation inputs
yaw_right.set(roll_cw.get()); yaw_right.set(roll_cw.get());
@ -858,7 +879,7 @@ pub fn Display() -> View {
set_manip_signal(&event, 0.0); set_manip_signal(&event, 0.0);
} }
}, },
on:blur=move |_| { on:blur = move |_| {
pitch_up.set(0.0); pitch_up.set(0.0);
pitch_down.set(0.0); pitch_down.set(0.0);
yaw_right.set(0.0); yaw_right.set(0.0);
@ -866,7 +887,7 @@ pub fn Display() -> View {
roll_ccw.set(0.0); roll_ccw.set(0.0);
roll_cw.set(0.0); roll_cw.set(0.0);
}, },
on:click=move |event: MouseEvent| { on:click = move |event: MouseEvent| {
// find the nearest element along the pointer direction // find the nearest element along the pointer direction
let (dir, pixel_size) = event_dir(&event); let (dir, pixel_size) = event_dir(&event);
console::log_1(&JsValue::from(dir.to_string())); console::log_1(&JsValue::from(dir.to_string()));
@ -883,18 +904,18 @@ pub fn Display() -> View {
clicked = Some((elt, depth)) clicked = Some((elt, depth))
} }
}, },
None => clicked = Some((elt, depth)) None => clicked = Some((elt, depth)),
} },
None => () None => (),
}; };
} }
// if we clicked something, select it // if we clicked something, select it
match clicked { match clicked {
Some((elt, _)) => state.select(&elt, event.shift_key()), Some((elt, _)) => state.select(&elt, event.shift_key()),
None => state.selection.update(|sel| sel.clear()) None => state.selection.update(|sel| sel.clear()),
}; };
} },
) )
} }
} }

View file

@ -1,11 +1,7 @@
use itertools::Itertools; use itertools::Itertools;
use std::rc::Rc; use std::rc::Rc;
use sycamore::prelude::*; use sycamore::prelude::*;
use web_sys::{ use web_sys::{KeyboardEvent, MouseEvent, wasm_bindgen::JsCast};
KeyboardEvent,
MouseEvent,
wasm_bindgen::JsCast
};
use crate::{ use crate::{
AppState, AppState,
@ -13,7 +9,7 @@ use crate::{
Element, Element,
HalfCurvatureRegulator, HalfCurvatureRegulator,
InversiveDistanceRegulator, InversiveDistanceRegulator,
Regulator Regulator,
}, },
specified::SpecifiedValue specified::SpecifiedValue
}; };
@ -49,8 +45,8 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
view! { view! {
input( input(
r#type="text", r#type = "text",
class=move || { class = move || {
if valid.get() { if valid.get() {
set_point.with(|set_pt| { set_point.with(|set_pt| {
if set_pt.is_present() { if set_pt.is_present() {
@ -63,27 +59,27 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
"regulator-input invalid" "regulator-input invalid"
} }
}, },
placeholder=measurement.with(|result| result.to_string()), placeholder = measurement.with(|result| result.to_string()),
bind:value=value, bind:value = value,
on:change=move |_| { on:change = move |_| {
valid.set( valid.set(
match SpecifiedValue::try_from(value.get_clone_untracked()) { match SpecifiedValue::try_from(value.get_clone_untracked()) {
Ok(set_pt) => { Ok(set_pt) => {
set_point.set(set_pt); set_point.set(set_pt);
true true
} },
Err(_) => false Err(_) => false,
} }
) )
}, },
on:keydown={ on:keydown = {
move |event: KeyboardEvent| { move |event: KeyboardEvent| {
match event.key().as_str() { match event.key().as_str() {
"Escape" => reset_value(), "Escape" => reset_value(),
_ => () _ => (),
} }
} }
} },
) )
} }
} }
@ -100,11 +96,11 @@ impl OutlineItem for InversiveDistanceRegulator {
self.subjects[0].label() self.subjects[0].label()
}.clone(); }.clone();
view! { view! {
li(class="regulator") { li(class = "regulator") {
div(class="regulator-label") { (other_subject_label) } div(class = "regulator-label") { (other_subject_label) }
div(class="regulator-type") { "Inversive distance" } div(class = "regulator-type") { "Inversive distance" }
RegulatorInput(regulator=self) RegulatorInput(regulator = self)
div(class="status") div(class = "status")
} }
} }
} }
@ -113,11 +109,11 @@ impl OutlineItem for InversiveDistanceRegulator {
impl OutlineItem for HalfCurvatureRegulator { impl OutlineItem for HalfCurvatureRegulator {
fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View { fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View {
view! { view! {
li(class="regulator") { li(class = "regulator") {
div(class="regulator-label") // for spacing div(class = "regulator-label") // for spacing
div(class="regulator-type") { "Half-curvature" } div(class = "regulator-type") { "Half-curvature" }
RegulatorInput(regulator=self) RegulatorInput(regulator = self)
div(class="status") div(class = "status")
} }
} }
} }
@ -156,10 +152,10 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
let details_node = create_node_ref(); let details_node = create_node_ref();
view! { view! {
li { li {
details(ref=details_node) { details(ref = details_node) {
summary( summary(
class=class.get(), class = class.get(),
on:keydown={ on:keydown = {
let element_for_handler = element.clone(); let element_for_handler = element.clone();
move |event: KeyboardEvent| { move |event: KeyboardEvent| {
match event.key().as_str() { match event.key().as_str() {
@ -179,18 +175,18 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
.unchecked_into::<web_sys::Element>() .unchecked_into::<web_sys::Element>()
.remove_attribute("open"); .remove_attribute("open");
}, },
_ => () _ => (),
} }
} }
} }
) { ) {
div( div(
class="element-switch", class = "element-switch",
on:click=|event: MouseEvent| event.stop_propagation() on:click = |event: MouseEvent| event.stop_propagation()
) )
div( div(
class="element", class = "element",
on:click={ on:click = {
let state_for_handler = state.clone(); let state_for_handler = state.clone();
let element_for_handler = element.clone(); let element_for_handler = element.clone();
move |event: MouseEvent| { move |event: MouseEvent| {
@ -200,20 +196,20 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
} }
} }
) { ) {
div(class="element-label") { (label) } div(class = "element-label") { (label) }
div(class="element-representation") { (rep_components) } div(class = "element-representation") { (rep_components) }
input( input(
r#type="checkbox", r#type = "checkbox",
bind:checked=element.ghost(), bind:checked = element.ghost(),
on:click=|event: MouseEvent| event.stop_propagation() on:click = |event: MouseEvent| event.stop_propagation()
) )
} }
} }
ul(class="regulators") { ul(class = "regulators") {
Keyed( Keyed(
list=regulator_list, list = regulator_list,
view=move |reg| reg.outline_item(&element), view = move |reg| reg.outline_item(&element),
key=|reg| reg.serial() key = |reg| reg.serial()
) )
} }
} }
@ -246,18 +242,18 @@ pub fn Outline() -> View {
view! { view! {
ul( ul(
id="outline", id = "outline",
on:click={ on:click = {
let state = use_context::<AppState>(); let state = use_context::<AppState>();
move |_| state.selection.update(|sel| sel.clear()) move |_| state.selection.update(|sel| sel.clear())
} }
) { ) {
Keyed( Keyed(
list=element_list, list = element_list,
view=|elt| view! { view = |elt| view! {
ElementOutlineItem(element=elt) ElementOutlineItem(element = elt)
}, },
key=|elt| elt.serial() key = |elt| elt.serial()
) )
} }
} }

View file

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

View file

@ -1,7 +1,6 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen}; use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen};
use std::fmt::{Display, Error, Formatter}; use std::fmt::{Display, Error, Formatter};
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
// --- elements --- // --- elements ---
@ -17,7 +16,7 @@ pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVect
center_y / radius, center_y / radius,
center_z / radius, center_z / radius,
0.5 / radius, 0.5 / radius,
0.5 * (center_norm_sq / radius - radius) 0.5 * (center_norm_sq / radius - radius),
]) ])
} }
@ -31,7 +30,7 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6
norm_sp * dir_y, norm_sp * dir_y,
norm_sp * dir_z, norm_sp * dir_z,
0.5 * curv, 0.5 * curv,
off * (1.0 + 0.5 * off * curv) off * (1.0 + 0.5 * off * curv),
]) ])
} }
@ -50,57 +49,23 @@ pub fn project_point_to_normalized(rep: &mut DVector<f64>) {
rep.scale_mut(0.5 / rep[3]); rep.scale_mut(0.5 / rep[3]);
} }
// given a sphere's representation vector, change the sphere's half-curvature to
// `half-curv` and then restore normalization by contracting the representation
// vector toward the curvature axis
pub fn change_half_curvature(rep: &mut DVector<f64>, half_curv: f64) {
// set the sphere's half-curvature to the desired value
rep[3] = half_curv;
// restore normalization by contracting toward the curvature axis
const SIZE_THRESHOLD: f64 = 1e-9;
let half_q_lt = -2.0 * half_curv * rep[4];
let half_q_lt_sq = half_q_lt * half_q_lt;
let mut spatial = rep.fixed_rows_mut::<3>(0);
let q_sp = spatial.norm_squared();
if q_sp < SIZE_THRESHOLD && half_q_lt_sq < SIZE_THRESHOLD {
spatial.copy_from_slice(
&[0.0, 0.0, (1.0 - 2.0 * half_q_lt).sqrt()]
);
} else {
let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt();
spatial.scale_mut(1.0 / scaling);
rep[4] /= scaling;
}
/* DEBUG */
// verify normalization
let rep_for_debug = rep.clone();
console::log_1(&JsValue::from(
format!(
"Sphere self-product after curvature change: {}",
rep_for_debug.dot(&(&*Q * &rep_for_debug))
)
));
}
// --- partial matrices --- // --- partial matrices ---
pub struct MatrixEntry { pub struct MatrixEntry {
index: (usize, usize), index: (usize, usize),
value: f64 value: f64,
} }
pub struct PartialMatrix(Vec<MatrixEntry>); pub struct PartialMatrix(Vec<MatrixEntry>);
impl PartialMatrix { impl PartialMatrix {
pub fn new() -> PartialMatrix { pub fn new() -> Self {
PartialMatrix(Vec::<MatrixEntry>::new()) Self(Vec::<MatrixEntry>::new())
} }
pub fn push(&mut self, row: usize, col: usize, value: f64) { pub fn push(&mut self, row: usize, col: usize, value: f64) {
let PartialMatrix(entries) = self; let Self(entries) = self;
entries.push(MatrixEntry { index: (row, col), value: value }); entries.push(MatrixEntry { index: (row, col), value });
} }
pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { pub fn push_sym(&mut self, row: usize, col: usize, value: f64) {
@ -149,7 +114,7 @@ impl IntoIterator for PartialMatrix {
type IntoIter = std::vec::IntoIter<Self::Item>; type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
let PartialMatrix(entries) = self; let Self(entries) = self;
entries.into_iter() entries.into_iter()
} }
} }
@ -170,22 +135,26 @@ impl<'a> IntoIterator for &'a PartialMatrix {
pub struct ConfigSubspace { pub struct ConfigSubspace {
assembly_dim: usize, assembly_dim: usize,
basis_std: Vec<DMatrix<f64>>, basis_std: Vec<DMatrix<f64>>,
basis_proj: Vec<DMatrix<f64>> basis_proj: Vec<DMatrix<f64>>,
} }
impl ConfigSubspace { impl ConfigSubspace {
pub fn zero(assembly_dim: usize) -> ConfigSubspace { pub fn zero(assembly_dim: usize) -> Self {
ConfigSubspace { Self {
assembly_dim: assembly_dim, assembly_dim,
basis_proj: Vec::new(), basis_proj: Vec::new(),
basis_std: Vec::new() basis_std: Vec::new(),
} }
} }
// approximate the kernel of a symmetric endomorphism of the configuration // approximate the kernel of a symmetric endomorphism of the configuration
// space for `assembly_dim` elements. we consider an eigenvector to be part // space for `assembly_dim` elements. we consider an eigenvector to be part
// of the kernel if its eigenvalue is smaller than the constant `THRESHOLD` // of the kernel if its eigenvalue is smaller than the constant `THRESHOLD`
fn symmetric_kernel(a: DMatrix<f64>, proj_to_std: DMatrix<f64>, assembly_dim: usize) -> ConfigSubspace { fn symmetric_kernel(
a: DMatrix<f64>,
proj_to_std: DMatrix<f64>,
assembly_dim: usize,
) -> Self {
// find a basis for the kernel. the basis is expressed in the projection // find a basis for the kernel. the basis is expressed in the projection
// coordinates, and it's orthonormal with respect to the projection // coordinates, and it's orthonormal with respect to the projection
// inner product // inner product
@ -199,20 +168,13 @@ impl ConfigSubspace {
).collect::<Vec<_>>().as_slice() ).collect::<Vec<_>>().as_slice()
); );
/* DEBUG */
// print the eigenvalues
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
console::log_1(&JsValue::from(
format!("Eigenvalues used to find kernel:{}", eig.eigenvalues)
));
// express the basis in the standard coordinates // express the basis in the standard coordinates
let basis_std = proj_to_std * &basis_proj; let basis_std = proj_to_std * &basis_proj;
const ELEMENT_DIM: usize = 5; const ELEMENT_DIM: usize = 5;
const UNIFORM_DIM: usize = 4; const UNIFORM_DIM: usize = 4;
ConfigSubspace { Self {
assembly_dim: assembly_dim, assembly_dim,
basis_std: basis_std.column_iter().map( basis_std: basis_std.column_iter().map(
|v| Into::<DMatrix<f64>>::into( |v| Into::<DMatrix<f64>>::into(
v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim)) v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim))
@ -222,7 +184,7 @@ impl ConfigSubspace {
|v| Into::<DMatrix<f64>>::into( |v| Into::<DMatrix<f64>>::into(
v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim)) v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim))
) )
).collect() ).collect(),
} }
} }
@ -256,14 +218,14 @@ pub struct DescentHistory {
pub config: Vec<DMatrix<f64>>, pub config: Vec<DMatrix<f64>>,
pub scaled_loss: Vec<f64>, pub scaled_loss: Vec<f64>,
pub neg_grad: Vec<DMatrix<f64>>, pub neg_grad: Vec<DMatrix<f64>>,
pub hess_eigvals: Vec::<DVector<f64>>, pub hess_eigvals: Vec<DVector<f64>>,
pub base_step: Vec<DMatrix<f64>>, pub base_step: Vec<DMatrix<f64>>,
pub backoff_steps: Vec<i32> pub backoff_steps: Vec<i32>,
} }
impl DescentHistory { impl DescentHistory {
pub fn new() -> DescentHistory { pub fn new() -> Self {
DescentHistory { Self {
config: Vec::<DMatrix<f64>>::new(), config: Vec::<DMatrix<f64>>::new(),
scaled_loss: Vec::<f64>::new(), scaled_loss: Vec::<f64>::new(),
neg_grad: Vec::<DMatrix<f64>>::new(), neg_grad: Vec::<DMatrix<f64>>::new(),
@ -283,21 +245,21 @@ pub struct ConstraintProblem {
} }
impl ConstraintProblem { impl ConstraintProblem {
pub fn new(element_count: usize) -> ConstraintProblem { pub fn new(element_count: usize) -> Self {
const ELEMENT_DIM: usize = 5; const ELEMENT_DIM: usize = 5;
ConstraintProblem { Self {
gram: PartialMatrix::new(), gram: PartialMatrix::new(),
frozen: PartialMatrix::new(), frozen: PartialMatrix::new(),
guess: DMatrix::<f64>::zeros(ELEMENT_DIM, element_count) guess: DMatrix::<f64>::zeros(ELEMENT_DIM, element_count),
} }
} }
#[cfg(feature = "dev")] #[cfg(feature = "dev")]
pub fn from_guess(guess_columns: &[DVector<f64>]) -> ConstraintProblem { pub fn from_guess(guess_columns: &[DVector<f64>]) -> Self {
ConstraintProblem { Self {
gram: PartialMatrix::new(), gram: PartialMatrix::new(),
frozen: PartialMatrix::new(), frozen: PartialMatrix::new(),
guess: DMatrix::from_columns(guess_columns) guess: DMatrix::from_columns(guess_columns),
} }
} }
} }
@ -311,25 +273,21 @@ lazy_static! {
0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, -2.0, 0.0, 0.0, 0.0, 0.0, -2.0,
0.0, 0.0, 0.0, -2.0, 0.0 0.0, 0.0, 0.0, -2.0, 0.0,
]); ]);
} }
struct SearchState { struct SearchState {
config: DMatrix<f64>, config: DMatrix<f64>,
err_proj: DMatrix<f64>, err_proj: DMatrix<f64>,
loss: f64 loss: f64,
} }
impl SearchState { impl SearchState {
fn from_config(gram: &PartialMatrix, config: DMatrix<f64>) -> SearchState { fn from_config(gram: &PartialMatrix, config: DMatrix<f64>) -> Self {
let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config)); let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config));
let loss = err_proj.norm_squared(); let loss = err_proj.norm_squared();
SearchState { Self { config, err_proj, loss }
config: config,
err_proj: err_proj,
loss: loss
}
} }
} }
@ -356,7 +314,7 @@ pub fn local_unif_to_std(v: DVectorView<f64>) -> DMatrix<f64> {
curv, 0.0, 0.0, 0.0, v[0], curv, 0.0, 0.0, 0.0, v[0],
0.0, curv, 0.0, 0.0, v[1], 0.0, curv, 0.0, 0.0, v[1],
0.0, 0.0, curv, 0.0, v[2], 0.0, 0.0, curv, 0.0, v[2],
0.0, 0.0, 0.0, 0.0, 1.0 0.0, 0.0, 0.0, 0.0, 1.0,
]) ])
} else { } else {
// `v` represents a sphere. the normalization condition says that the // `v` represents a sphere. the normalization condition says that the
@ -365,7 +323,7 @@ pub fn local_unif_to_std(v: DVectorView<f64>) -> DMatrix<f64> {
curv, 0.0, 0.0, 0.0, v[0], curv, 0.0, 0.0, 0.0, v[0],
0.0, curv, 0.0, 0.0, v[1], 0.0, curv, 0.0, 0.0, v[1],
0.0, 0.0, curv, 0.0, v[2], 0.0, 0.0, curv, 0.0, v[2],
curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0 curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0,
]) ])
} }
} }
@ -378,7 +336,7 @@ fn seek_better_config(
base_target_improvement: f64, base_target_improvement: f64,
min_efficiency: f64, min_efficiency: f64,
backoff: f64, backoff: f64,
max_backoff_steps: i32 max_backoff_steps: i32,
) -> Option<(SearchState, i32)> { ) -> Option<(SearchState, i32)> {
let mut rate = 1.0; let mut rate = 1.0;
for backoff_steps in 0..max_backoff_steps { for backoff_steps in 0..max_backoff_steps {
@ -396,12 +354,12 @@ fn seek_better_config(
// a first-order neighborhood of a configuration // a first-order neighborhood of a configuration
pub struct ConfigNeighborhood { pub struct ConfigNeighborhood {
pub config: DMatrix<f64>, pub config: DMatrix<f64>,
pub nbhd: ConfigSubspace pub nbhd: ConfigSubspace,
} }
pub struct Realization { pub struct Realization {
pub result: Result<ConfigNeighborhood, String>, pub result: Result<ConfigNeighborhood, String>,
pub history: DescentHistory pub history: DescentHistory,
} }
// seek a matrix `config` that matches the partial matrix `problem.frozen` and // seek a matrix `config` that matches the partial matrix `problem.frozen` and
@ -415,19 +373,30 @@ pub fn realize_gram(
backoff: f64, backoff: f64,
reg_scale: f64, reg_scale: f64,
max_descent_steps: i32, max_descent_steps: i32,
max_backoff_steps: i32 max_backoff_steps: i32,
) -> Realization { ) -> Realization {
// destructure the problem data // destructure the problem data
let ConstraintProblem { let ConstraintProblem { gram, guess, frozen } = problem;
gram, guess, frozen
} = problem;
// start the descent history // start the descent history
let mut history = DescentHistory::new(); let mut history = DescentHistory::new();
// handle the case where the assembly is empty. our general realization
// routine can't handle this case because it builds the Hessian using
// `DMatrix::from_columns`, which panics when the list of columns is empty
let assembly_dim = guess.ncols();
if assembly_dim == 0 {
let result = Ok(
ConfigNeighborhood {
config: guess.clone(),
nbhd: ConfigSubspace::zero(0),
}
);
return Realization { result, history };
}
// find the dimension of the search space // find the dimension of the search space
let element_dim = guess.nrows(); let element_dim = guess.nrows();
let assembly_dim = guess.ncols();
let total_dim = element_dim * assembly_dim; let total_dim = element_dim * assembly_dim;
// scale the tolerance // scale the tolerance
@ -504,8 +473,8 @@ pub fn realize_gram(
Some(cholesky) => cholesky, Some(cholesky) => cholesky,
None => return Realization { None => return Realization {
result: Err("Cholesky decomposition failed".to_string()), result: Err("Cholesky decomposition failed".to_string()),
history history,
} },
}; };
let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked); let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked);
let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim));
@ -514,16 +483,16 @@ pub fn realize_gram(
// use backtracking line search to find a better configuration // use backtracking line search to find a better configuration
if let Some((better_state, backoff_steps)) = seek_better_config( if let Some((better_state, backoff_steps)) = seek_better_config(
gram, &state, &base_step, neg_grad.dot(&base_step), gram, &state, &base_step, neg_grad.dot(&base_step),
min_efficiency, backoff, max_backoff_steps min_efficiency, backoff, max_backoff_steps,
) { ) {
state = better_state; state = better_state;
history.backoff_steps.push(backoff_steps); history.backoff_steps.push(backoff_steps);
} else { } else {
return Realization { return Realization {
result: Err("Line search failed".to_string()), result: Err("Line search failed".to_string()),
history history,
} };
}; }
} }
let result = if state.loss < tol { let result = if state.loss < tol {
// express the uniform basis in the standard basis // express the uniform basis in the standard basis
@ -568,7 +537,7 @@ pub mod examples {
[ [
sphere(0.0, 0.0, 0.0, 15.0), sphere(0.0, 0.0, 0.0, 15.0),
sphere(0.0, 0.0, -9.0, 5.0), sphere(0.0, 0.0, -9.0, 5.0),
sphere(0.0, 0.0, 11.0, 3.0) sphere(0.0, 0.0, 11.0, 3.0),
].into_iter().chain( ].into_iter().chain(
(1..=6).map( (1..=6).map(
|k| { |k| {
@ -627,7 +596,7 @@ pub mod examples {
point(0.0, 0.0, 0.0), point(0.0, 0.0, 0.0),
point(ang_hor.cos(), ang_hor.sin(), 0.0), point(ang_hor.cos(), ang_hor.sin(), 0.0),
point(x_vert, y_vert, -0.5), point(x_vert, y_vert, -0.5),
point(x_vert, y_vert, 0.5) point(x_vert, y_vert, 0.5),
] ]
} }
).collect::<Vec<_>>().as_slice() ).collect::<Vec<_>>().as_slice()
@ -670,15 +639,15 @@ mod tests {
MatrixEntry { index: (0, 0), value: 14.0 }, MatrixEntry { index: (0, 0), value: 14.0 },
MatrixEntry { index: (0, 2), value: 28.0 }, MatrixEntry { index: (0, 2), value: 28.0 },
MatrixEntry { index: (1, 1), value: 42.0 }, MatrixEntry { index: (1, 1), value: 42.0 },
MatrixEntry { index: (1, 2), value: 49.0 } MatrixEntry { index: (1, 2), value: 49.0 },
]); ]);
let config = DMatrix::<f64>::from_row_slice(2, 3, &[ let config = DMatrix::<f64>::from_row_slice(2, 3, &[
1.0, 2.0, 3.0, 1.0, 2.0, 3.0,
4.0, 5.0, 6.0 4.0, 5.0, 6.0,
]); ]);
let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[ let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[
14.0, 2.0, 28.0, 14.0, 2.0, 28.0,
4.0, 42.0, 49.0 4.0, 42.0, 49.0,
]); ]);
assert_eq!(frozen.freeze(&config), expected_result); assert_eq!(frozen.freeze(&config), expected_result);
} }
@ -689,15 +658,15 @@ mod tests {
MatrixEntry { index: (0, 0), value: 19.0 }, MatrixEntry { index: (0, 0), value: 19.0 },
MatrixEntry { index: (0, 2), value: 39.0 }, MatrixEntry { index: (0, 2), value: 39.0 },
MatrixEntry { index: (1, 1), value: 59.0 }, MatrixEntry { index: (1, 1), value: 59.0 },
MatrixEntry { index: (1, 2), value: 69.0 } MatrixEntry { index: (1, 2), value: 69.0 },
]); ]);
let attempt = DMatrix::<f64>::from_row_slice(2, 3, &[ let attempt = DMatrix::<f64>::from_row_slice(2, 3, &[
1.0, 2.0, 3.0, 1.0, 2.0, 3.0,
4.0, 5.0, 6.0 4.0, 5.0, 6.0,
]); ]);
let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[ let expected_result = DMatrix::<f64>::from_row_slice(2, 3, &[
18.0, 0.0, 36.0, 18.0, 0.0, 36.0,
0.0, 54.0, 63.0 0.0, 54.0, 63.0,
]); ]);
assert_eq!(target.sub_proj(&attempt), expected_result); assert_eq!(target.sub_proj(&attempt), expected_result);
} }
@ -715,7 +684,7 @@ mod tests {
DMatrix::from_columns(&[ DMatrix::from_columns(&[
sphere(1.0, 0.0, 0.0, a), sphere(1.0, 0.0, 0.0, a),
sphere(-0.5, a, 0.0, a), sphere(-0.5, a, 0.0, a),
sphere(-0.5, -a, 0.0, a) sphere(-0.5, -a, 0.0, a),
]) ])
}; };
let state = SearchState::from_config(&gram, config); let state = SearchState::from_config(&gram, config);
@ -729,7 +698,7 @@ mod tests {
fn frozen_entry_test() { fn frozen_entry_test() {
let mut problem = ConstraintProblem::from_guess(&[ let mut problem = ConstraintProblem::from_guess(&[
point(0.0, 0.0, 2.0), point(0.0, 0.0, 2.0),
sphere(0.0, 0.0, 0.0, 0.95) sphere(0.0, 0.0, 0.0, 0.95),
]); ]);
for j in 0..2 { for j in 0..2 {
for k in j..2 { for k in j..2 {
@ -773,7 +742,7 @@ mod tests {
let mut problem = ConstraintProblem::from_guess(&[ let mut problem = ConstraintProblem::from_guess(&[
sphere(0.0, 0.0, 0.0, -2.0), sphere(0.0, 0.0, 0.0, -2.0),
sphere(0.0, 0.0, 1.0, 1.0), sphere(0.0, 0.0, 1.0, 1.0),
sphere(0.0, 0.0, -1.0, 1.0) sphere(0.0, 0.0, -1.0, 1.0),
]); ]);
for j in 0..3 { for j in 0..3 {
for k in j..3 { for k in j..3 {
@ -803,8 +772,8 @@ mod tests {
DMatrix::<f64>::from_column_slice(UNIFORM_DIM, assembly_dim, &[ DMatrix::<f64>::from_column_slice(UNIFORM_DIM, assembly_dim, &[
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, -0.5, -0.5, 0.0, 0.0, -0.5, -0.5,
0.0, 0.0, -0.5, 0.5 0.0, 0.0, -0.5, 0.5,
]) ]),
]; ];
let tangent_motions_std = vec![ let tangent_motions_std = vec![
basis_matrix((0, 1), element_dim, assembly_dim), basis_matrix((0, 1), element_dim, assembly_dim),
@ -814,8 +783,8 @@ mod tests {
DMatrix::<f64>::from_column_slice(element_dim, assembly_dim, &[ DMatrix::<f64>::from_column_slice(element_dim, assembly_dim, &[
0.0, 0.0, 0.0, 0.00, 0.0, 0.0, 0.0, 0.0, 0.00, 0.0,
0.0, 0.0, -1.0, -0.25, -1.0, 0.0, 0.0, -1.0, -0.25, -1.0,
0.0, 0.0, -1.0, 0.25, 1.0 0.0, 0.0, -1.0, 0.25, 1.0,
]) ]),
]; ];
// confirm that the dimension of the tangent space is no greater than // confirm that the dimension of the tangent space is no greater than
@ -891,10 +860,10 @@ mod tests {
DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]), DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]),
DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]), DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]),
DVector::from_column_slice(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]), DVector::from_column_slice(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]),
DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0]) DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0]),
] ]
} }
).collect::<Vec<_>>() ).collect::<Vec<_>>(),
]; ];
let tangent_motions_std = tangent_motions_unif.iter().map( let tangent_motions_std = tangent_motions_unif.iter().map(
|motion| DMatrix::from_columns( |motion| DMatrix::from_columns(
@ -927,7 +896,7 @@ mod tests {
0.0, 1.0, 0.0, 0.0, dis[1], 0.0, 1.0, 0.0, 0.0, dis[1],
0.0, 0.0, 1.0, 0.0, dis[2], 0.0, 0.0, 1.0, 0.0, dis[2],
2.0*dis[0], 2.0*dis[1], 2.0*dis[2], 1.0, dis.norm_squared(), 2.0*dis[0], 2.0*dis[1], 2.0*dis[2], 1.0, dis.norm_squared(),
0.0, 0.0, 0.0, 0.0, 1.0 0.0, 0.0, 0.0, 0.0, 1.0,
]) ])
} }
@ -939,7 +908,7 @@ mod tests {
const SCALED_TOL: f64 = 1.0e-12; const SCALED_TOL: f64 = 1.0e-12;
let mut problem_orig = ConstraintProblem::from_guess(&[ let mut problem_orig = ConstraintProblem::from_guess(&[
sphere(0.0, 0.0, 0.5, 1.0), sphere(0.0, 0.0, 0.5, 1.0),
sphere(0.0, 0.0, -0.5, 1.0) sphere(0.0, 0.0, -0.5, 1.0),
]); ]);
problem_orig.gram.push_sym(0, 0, 1.0); problem_orig.gram.push_sym(0, 0, 1.0);
problem_orig.gram.push_sym(1, 1, 1.0); problem_orig.gram.push_sym(1, 1, 1.0);
@ -957,13 +926,13 @@ mod tests {
let a = 0.5 * FRAC_1_SQRT_2; let a = 0.5 * FRAC_1_SQRT_2;
DMatrix::from_columns(&[ DMatrix::from_columns(&[
sphere(a, 0.0, 7.0 + a, 1.0), sphere(a, 0.0, 7.0 + a, 1.0),
sphere(-a, 0.0, 7.0 - a, 1.0) sphere(-a, 0.0, 7.0 - a, 1.0),
]) ])
}; };
let problem_tfm = ConstraintProblem { let problem_tfm = ConstraintProblem {
gram: problem_orig.gram, gram: problem_orig.gram,
frozen: problem_orig.frozen,
guess: guess_tfm, guess: guess_tfm,
frozen: problem_orig.frozen
}; };
let Realization { result: result_tfm, history: history_tfm } = realize_gram( let Realization { result: result_tfm, history: history_tfm } = realize_gram(
&problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
@ -991,7 +960,7 @@ mod tests {
0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0, 0.0, FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0, 0.0,
0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 0.0, 1.0 0.0, 0.0, 0.0, 0.0, 1.0,
]); ]);
let transl = translation(Vector3::new(0.0, 0.0, 7.0)); let transl = translation(Vector3::new(0.0, 0.0, 7.0));
let motion_proj_tfm = transl * rot * motion_orig_proj; let motion_proj_tfm = transl * rot * motion_orig_proj;

View file

@ -1,9 +1,6 @@
mod add_remove;
mod assembly; mod assembly;
mod diagnostics; mod components;
mod display;
mod engine; mod engine;
mod outline;
mod specified; mod specified;
#[cfg(test)] #[cfg(test)]
@ -12,23 +9,25 @@ mod tests;
use std::{collections::BTreeSet, rc::Rc}; use std::{collections::BTreeSet, rc::Rc};
use sycamore::prelude::*; use sycamore::prelude::*;
use add_remove::AddRemove;
use assembly::{Assembly, Element}; use assembly::{Assembly, Element};
use diagnostics::Diagnostics; use components::{
use display::Display; add_remove::AddRemove,
use outline::Outline; diagnostics::Diagnostics,
display::Display,
outline::Outline,
};
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
assembly: Assembly, assembly: Assembly,
selection: Signal<BTreeSet<Rc<dyn Element>>> selection: Signal<BTreeSet<Rc<dyn Element>>>,
} }
impl AppState { impl AppState {
fn new() -> AppState { fn new() -> Self {
AppState { Self {
assembly: Assembly::new(), assembly: Assembly::new(),
selection: create_signal(BTreeSet::default()) selection: create_signal(BTreeSet::default()),
} }
} }
@ -59,7 +58,7 @@ fn main() {
provide_context(AppState::new()); provide_context(AppState::new());
view! { view! {
div(id="sidebar") { div(id = "sidebar") {
AddRemove {} AddRemove {}
Outline {} Outline {}
Diagnostics {} Diagnostics {}

View file

@ -13,12 +13,12 @@ use std::num::ParseFloatError;
#[readonly::make] #[readonly::make]
pub struct SpecifiedValue { pub struct SpecifiedValue {
pub spec: String, pub spec: String,
pub value: Option<f64> pub value: Option<f64>,
} }
impl SpecifiedValue { impl SpecifiedValue {
pub fn from_empty_spec() -> SpecifiedValue { pub fn from_empty_spec() -> Self {
SpecifiedValue { spec: String::new(), value: None } Self { spec: String::new(), value: None }
} }
pub fn is_present(&self) -> bool { pub fn is_present(&self) -> bool {
@ -34,10 +34,10 @@ impl TryFrom<String> for SpecifiedValue {
fn try_from(spec: String) -> Result<Self, Self::Error> { fn try_from(spec: String) -> Result<Self, Self::Error> {
if spec.is_empty() { if spec.is_empty() {
Ok(SpecifiedValue::from_empty_spec()) Ok(Self::from_empty_spec())
} else { } else {
spec.parse::<f64>().map( spec.parse::<f64>().map(
|value| SpecifiedValue { spec: spec, value: Some(value) } |value| Self { spec, value: Some(value) }
) )
} }
} }

5
deploy/.gitignore vendored Normal file
View file

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

View file

@ -0,0 +1,16 @@
# set paths. this technique for getting the script location comes from
# `mklement0` on Stack Overflow
#
# https://stackoverflow.com/a/24114056
#
TOOLS=$(dirname -- $0)
SRC="$TOOLS/../app-proto/dist"
DEST="$TOOLS/../deploy/dyna3"
# remove the old hash-named files
[ -e "$DEST"/dyna3-*.js ] && rm "$DEST"/dyna3-*.js
[ -e "$DEST"/dyna3-*.wasm ] && rm "$DEST"/dyna3-*.wasm
[ -e "$DEST"/main-*.css ] && rm "$DEST"/main-*.css
# copy the distribution
cp -r "$SRC/." "$DEST"

View file

@ -8,7 +8,7 @@
# the application prototype # the application prototype
# find the manifest file for the application prototype # find the manifest file for the application prototype
MANIFEST="$(dirname -- $0)/Cargo.toml" MANIFEST="$(dirname -- $0)/../app-proto/Cargo.toml"
# set up the command that runs each example # set up the command that runs each example
RUN_EXAMPLE="cargo run --manifest-path $MANIFEST --example" RUN_EXAMPLE="cargo run --manifest-path $MANIFEST --example"