Compare commits

..

9 commits

Author SHA1 Message Date
Aaron Fenyes
5233d8eb93 Move the components into their own module
All checks were successful
/ test (pull_request) Successful in 3m36s
This makes the module tree more reflective of module use patterns. In
particular, it restores the condition that every top-level module is
used in top-level code, which was broken by the addition of the
`test_assembly_chooser` module in commit 91e4e1f.
2025-07-22 13:28:48 -07:00
Aaron Fenyes
91e4e1f414 Make the test assembly chooser its own component
All checks were successful
/ test (pull_request) Successful in 3m32s
2025-07-21 12:16:16 -07:00
Aaron Fenyes
4e5cd6e4a4 Add a dodecahedral circle packing test assembly
All checks were successful
/ test (pull_request) Successful in 3m33s
2025-07-20 23:27:10 -07:00
Aaron Fenyes
97d61e154a Add a tridiminished icosahedron test assembly 2025-07-20 23:27:10 -07:00
Aaron Fenyes
9c22eebb46 Add an off-center test assembly
This test assembly should provide a simple example where the loss
function is saddle-shaped near its manifold of minima.
2025-07-20 23:27:10 -07:00
Aaron Fenyes
27a8cbfd69 Add a balanced test assembly
This test assembly reveals one way that the engine can stall, indicated
by a long plateau in the loss history. You can make the plateau even
longer by shrinking the inner spheres.
2025-07-20 23:27:10 -07:00
Aaron Fenyes
fae3f4531e Add an Irisawa hexlet test assembly
This partial setup of the Irisawa hexlet problem is from the summer 2025
tech demo.
2025-07-20 23:27:10 -07:00
Aaron Fenyes
543bc4020f Add a tetrahedron radius ratio test assembly
This partial setup of the tetrahedron radius ratio problem is from the
summer 2025 tech demo.
2025-07-20 23:27:10 -07:00
Aaron Fenyes
40d665d8ac Pause realization while loading assemblies
This avoids redundant realizations as we set an assembly's regulators
during loading. Adding some regulators to the low-curvature assembly
confirms that the feature is working as intended.
2025-07-20 23:27:10 -07:00
20 changed files with 511 additions and 775 deletions

View file

@ -12,11 +12,11 @@ Note that currently this is just the barest beginnings of the project, more of a
### Implementation goals
* Provide a comfortable, intuitive UI
* Comfortable, intuitive UI
* Allow execution in browser (so implemented in WASM-compatible language)
* Able to run in browser (so implemented in WASM-compatible language)
* Produce scalable graphics of 3D diagrams, and maybe STL files (or other fabricatable file format) as well
* Produce scalable graphics of 3D diagrams, and maybe STL files (or other fabricatable file format) as well.
## Prototype
@ -24,40 +24,33 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter
### Install the prerequisites
1. Install [`rustup`](https://rust-lang.github.io/rustup/): the officially recommended Rust toolchain manager.
- It's available on Ubuntu as a [Snap](https://snapcraft.io/rustup).
2. Call `rustup default stable` to "download the latest stable release of Rust and set it as your default toolchain".
- If you forget, the `rustup` [help system](https://github.com/rust-lang/rustup/blob/d9b3601c3feb2e88cf3f8ca4f7ab4fdad71441fd/src/errors.rs#L109-L112) will remind you.
3. Call `rustup target add wasm32-unknown-unknown` to add the [most generic 32-bit WebAssembly target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-unknown-unknown.html).
4. Call `cargo install wasm-pack` to install the [WebAssembly toolchain](https://rustwasm.github.io/docs/wasm-pack/).
5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool.
- In the future, `trunk` can be updated with the same command. (You may need the `--locked` flag if your ambient version of `rustc` does not match that required by `trunk`.)
1. Install [`rustup`](https://rust-lang.github.io/rustup/): the officially recommended Rust toolchain manager
* It's available on Ubuntu as a [Snap](https://snapcraft.io/rustup)
2. Call `rustup default stable` to "download the latest stable release of Rust and set it as your default toolchain"
* If you forget, the `rustup` [help system](https://github.com/rust-lang/rustup/blob/d9b3601c3feb2e88cf3f8ca4f7ab4fdad71441fd/src/errors.rs#L109-L112) will remind you
3. Call `rustup target add wasm32-unknown-unknown` to add the [most generic 32-bit WebAssembly target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-unknown-unknown.html)
4. Call `cargo install wasm-pack` to install the [WebAssembly toolchain](https://rustwasm.github.io/docs/wasm-pack/)
5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool
6. Add the `.cargo/bin` folder in your home directory to your executable search path
- This lets you call Trunk, and other tools installed by Cargo, without specifying their paths.
- On POSIX systems, the search path is stored in the `PATH` environment variable.
- Alternatively, if you don't want to adjust your `PATH`, you can install `trunk` in another directory `DIR` via `cargo install --root DIR trunk`.
* This lets you call Trunk, and other tools installed by Cargo, without specifying their paths
* On POSIX systems, the search path is stored in the `PATH` environment variable
### Play with the prototype
1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype.
- The crates the prototype depends on will be downloaded and served automatically.
- For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag.
- If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]` from there instead.
3. In a web browser, visit one of the URLs listed under the message `INFO 📡 server listening at:`.
- Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype.
4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype.
1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype
* *The crates the prototype depends on will be downloaded and served automatically*
* *For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag*
* *If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]`* from there instead.
3. In a web browser, visit one of the URLs listed under the message `INFO 📡 server listening at:`
* *Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype*
4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype
### Run the engine on some example problems
1. Use `sh` to run the script `tools/run-examples.sh`.
- The script is location-independent, so you can do this from anywhere in the dyna3 repository.
- The call from the top level of the repository is:
```bash
sh tools/run-examples.sh
```
- For each example problem, the engine will print the value of the loss function at each optimization step.
- The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then execute
1. Go into the `app-proto` folder
2. Call `./run-examples`
* *For each example problem, the engine will print the value of the loss function at each optimization step*
* *The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then*
```julia
include("irisawa-hexlet.jl")
@ -66,24 +59,9 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter
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
1. Go into the `app-proto` folder.
2. Call `cargo test`.
### Deploy the prototype
1. From the `app-proto` folder, call `trunk build --release`.
- Building in [release mode](https://doc.rust-lang.org/cargo/reference/profiles.html#release) produces an executable which is smaller and often much faster, but harder to debug and more time-consuming to build.
- If you want to stay in the top-level folder, you can call `trunk build --config app-proto --release` from there instead.
2. Use `sh` to run the packaging script `tools/package-for-deployment.sh`.
- The script is location-independent, so you can do this from anywhere in the dyna3 repository.
- The call from the top level of the repository is:
```bash
sh tools/package-for-deployment.sh
```
- This will overwrite or replace the files in `deploy/dyna3`.
3. Put the contents of `deploy/dyna3` in the folder on your server that the prototype will be served from.
- To simplify uploading, you might want to combine these files into an archive called `deploy/dyna3.zip`. Git has been set to ignore this path.
1. Go into the `app-proto` folder
2. Call `cargo test`

21
app-proto/Cargo.lock generated
View file

@ -255,7 +255,6 @@ dependencies = [
"charming",
"console_error_panic_hook",
"dyna3",
"enum-iterator",
"itertools",
"js-sys",
"lazy_static",
@ -272,26 +271,6 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "enum-iterator"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016"
dependencies = [
"enum-iterator-derive",
]
[[package]]
name = "enum-iterator-derive"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "equivalent"
version = "1.0.1"

View file

@ -10,7 +10,6 @@ default = ["console_error_panic_hook"]
dev = []
[dependencies]
enum-iterator = "2.3.0"
itertools = "0.13.0"
js-sys = "0.3.70"
lazy_static = "1.5.0"

View file

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

View file

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

View file

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

View file

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

View file

@ -184,7 +184,6 @@ details[open]:has(li) .element-switch::after {
#diagnostics-bar {
display: flex;
gap: 8px;
}
#realization-status {
@ -208,14 +207,6 @@ details[open]:has(li) .element-switch::after {
content: '⚠';
}
#step-input > label {
padding-right: 4px;
}
#step-input > input {
width: 45px;
}
.diagnostics-panel {
margin-top: 10px;
min-height: 180px;

View file

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

View file

@ -1,14 +1,13 @@
use enum_iterator::{all, Sequence};
use nalgebra::{DMatrix, DVector, DVectorView};
use std::{
cell::Cell,
cmp::Ordering,
collections::{BTreeMap, BTreeSet},
cmp::Ordering,
fmt,
fmt::{Debug, Display, Formatter},
fmt::{Debug, Formatter},
hash::{Hash, Hasher},
rc::Rc,
sync::{atomic, atomic::AtomicU64},
sync::{atomic, atomic::AtomicU64}
};
use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
@ -17,6 +16,7 @@ use crate::{
components::{display::DisplayItem, outline::OutlineItem},
engine::{
Q,
change_half_curvature,
local_unif_to_std,
point,
project_point_to_normalized,
@ -27,10 +27,9 @@ use crate::{
ConfigSubspace,
ConstraintProblem,
DescentHistory,
MatrixEntry,
Realization,
Realization
},
specified::SpecifiedValue,
specified::SpecifiedValue
};
pub type ElementColor = [f32; 3];
@ -86,14 +85,6 @@ impl Ord for dyn Serial {
}
}
// Small helper function to generate consistent errors when there
// are indexing issues in a ProblemPoser
fn indexing_error(item: &str, name: &str, actor: &str) -> String {
format!(
"{item} \"{name}\" must be indexed before {actor} writes problem data"
)
}
pub trait ProblemPoser {
fn pose(&self, problem: &mut ConstraintProblem);
}
@ -135,8 +126,8 @@ pub trait Element: Serial + ProblemPoser + DisplayItem {
}
impl Debug for dyn Element {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.id(), f)
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
self.id().fmt(f)
}
}
@ -174,7 +165,7 @@ pub struct Sphere {
pub ghost: Signal<bool>,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
serial: u64,
column_index: Cell<Option<usize>>,
column_index: Cell<Option<usize>>
}
impl Sphere {
@ -184,17 +175,17 @@ impl Sphere {
id: String,
label: String,
color: ElementColor,
representation: DVector<f64>,
) -> Self {
Self {
id,
label,
color,
representation: DVector<f64>
) -> Sphere {
Sphere {
id: id,
label: label,
color: color,
representation: create_signal(representation),
ghost: create_signal(false),
regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(),
column_index: None.into(),
column_index: None.into()
}
}
}
@ -204,12 +195,12 @@ impl Element for Sphere {
"sphere".to_string()
}
fn default(id: String, id_num: u64) -> Self {
Self::new(
fn default(id: String, id_num: u64) -> Sphere {
Sphere::new(
id,
format!("Sphere {id_num}"),
[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)
)
}
@ -259,7 +250,8 @@ impl Serial for Sphere {
impl ProblemPoser for Sphere {
fn pose(&self, problem: &mut ConstraintProblem) {
let index = self.column_index().expect(
indexing_error("Sphere", &self.id, "it").as_str());
format!("Sphere \"{}\" should be indexed before writing problem data", self.id).as_str()
);
problem.gram.push_sym(index, index, 1.0);
problem.guess.set_column(index, &self.representation.get_clone_untracked());
}
@ -273,20 +265,19 @@ pub struct Point {
pub ghost: Signal<bool>,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
serial: u64,
column_index: Cell<Option<usize>>,
column_index: Cell<Option<usize>>
}
impl Point {
const WEIGHT_COMPONENT: usize = 3;
const NORM_COMPONENT: usize = 4;
pub fn new(
id: String,
label: String,
color: ElementColor,
representation: DVector<f64>,
) -> Self {
Self {
representation: DVector<f64>
) -> Point {
Point {
id,
label,
color,
@ -294,7 +285,7 @@ impl Point {
ghost: create_signal(false),
regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(),
column_index: None.into(),
column_index: None.into()
}
}
}
@ -304,23 +295,14 @@ impl Element for Point {
"point".to_string()
}
fn default(id: String, id_num: u64) -> Self {
Self::new(
fn default(id: String, id_num: u64) -> Point {
Point::new(
id,
format!("Point {id_num}"),
[0.75_f32, 0.75_f32, 0.75_f32],
point(0.0, 0.0, 0.0),
point(0.0, 0.0, 0.0)
)
}
fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> {
all::<Axis>()
.map(|axis| {
Rc::new(PointCoordinateRegulator::new(self.clone(), axis))
as Rc::<dyn Regulator>
})
.collect()
}
fn id(&self) -> &String {
&self.id
@ -364,9 +346,10 @@ impl Serial for Point {
impl ProblemPoser for Point {
fn pose(&self, problem: &mut ConstraintProblem) {
let index = self.column_index().expect(
indexing_error("Point", &self.id, "it").as_str());
format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str()
);
problem.gram.push_sym(index, index, 0.0);
problem.frozen.push(Self::WEIGHT_COMPONENT, index, 0.5);
problem.frozen.push(Point::WEIGHT_COMPONENT, index, 0.5);
problem.guess.set_column(index, &self.representation.get_clone_untracked());
}
}
@ -375,6 +358,16 @@ pub trait Regulator: Serial + ProblemPoser + OutlineItem {
fn subjects(&self) -> Vec<Rc<dyn Element>>;
fn measurement(&self) -> ReadSignal<f64>;
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 {
@ -407,11 +400,11 @@ pub struct InversiveDistanceRegulator {
pub subjects: [Rc<dyn Element>; 2],
pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>,
serial: u64,
serial: u64
}
impl InversiveDistanceRegulator {
pub fn new(subjects: [Rc<dyn Element>; 2]) -> Self {
pub fn new(subjects: [Rc<dyn Element>; 2]) -> InversiveDistanceRegulator {
let representations = subjects.each_ref().map(|subj| subj.representation());
let measurement = create_memo(move || {
representations[0].with(|rep_0|
@ -424,7 +417,7 @@ impl InversiveDistanceRegulator {
let set_point = create_signal(SpecifiedValue::from_empty_spec());
let serial = Self::next_serial();
Self { subjects, measurement, set_point, serial }
InversiveDistanceRegulator { subjects, measurement, set_point, serial }
}
}
@ -454,8 +447,8 @@ impl ProblemPoser for InversiveDistanceRegulator {
if let Some(val) = set_pt.value {
let [row, col] = self.subjects.each_ref().map(
|subj| subj.column_index().expect(
indexing_error("Subject", subj.id(),
"inversive distance regulator").as_str())
"Subjects should be indexed before inversive distance regulator writes problem data"
)
);
problem.gram.push_sym(row, col, val);
}
@ -464,14 +457,14 @@ impl ProblemPoser for InversiveDistanceRegulator {
}
pub struct HalfCurvatureRegulator {
pub subject: Rc<Sphere>,
pub subject: Rc<dyn Element>,
pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>,
serial: u64,
serial: u64
}
impl HalfCurvatureRegulator {
pub fn new(subject: Rc<Sphere>) -> Self {
pub fn new(subject: Rc<dyn Element>) -> HalfCurvatureRegulator {
let measurement = subject.representation().map(
|rep| rep[Sphere::CURVATURE_COMPONENT]
);
@ -479,7 +472,7 @@ impl HalfCurvatureRegulator {
let set_point = create_signal(SpecifiedValue::from_empty_spec());
let serial = Self::next_serial();
Self { subject, measurement, set_point, serial }
HalfCurvatureRegulator { subject, measurement, set_point, serial }
}
}
@ -495,6 +488,18 @@ impl Regulator for HalfCurvatureRegulator {
fn set_point(&self) -> Signal<SpecifiedValue> {
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 {
@ -508,89 +513,18 @@ impl ProblemPoser for HalfCurvatureRegulator {
self.set_point.with_untracked(|set_pt| {
if let Some(val) = set_pt.value {
let col = self.subject.column_index().expect(
indexing_error("Subject", &self.subject.id,
"half-curvature regulator").as_str());
"Subject should be indexed before half-curvature regulator writes problem data"
);
problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val);
}
});
}
}
#[derive(Clone, Copy, Sequence)]
pub enum Axis { X = 0, Y = 1, Z = 2 }
impl Axis {
fn name(&self) -> &'static str {
match self { Axis::X => "X", Axis::Y => "Y", Axis::Z => "Z" }
}
}
impl Display for Axis {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
pub struct PointCoordinateRegulator {
pub subject: Rc<Point>,
pub axis: Axis,
pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>,
serial: u64
}
impl PointCoordinateRegulator {
pub fn new(subject: Rc<Point>, axis: Axis) -> Self {
let measurement = subject.representation().map(
move |rep| rep[axis as usize]
);
let set_point = create_signal(SpecifiedValue::from_empty_spec());
Self { subject, axis, measurement, set_point, serial: Self::next_serial() }
}
}
impl Serial for PointCoordinateRegulator {
fn serial(&self) -> u64 { self.serial }
}
impl Regulator for PointCoordinateRegulator {
fn subjects(&self) -> Vec<Rc<dyn Element>> { vec![self.subject.clone()] }
fn measurement(&self) -> ReadSignal<f64> { self.measurement }
fn set_point(&self) -> Signal<SpecifiedValue> { self.set_point }
}
impl ProblemPoser for PointCoordinateRegulator {
fn pose(&self, problem: &mut ConstraintProblem) {
self.set_point.with_untracked(|set_pt| {
if let Some(val) = set_pt.value {
let col = self.subject.column_index().expect(
indexing_error("Subject", &self.subject.id,
"point-coordinate regulator").as_str());
problem.frozen.push(self.axis as usize, col, val);
// If all three of the subject's spatial coordinates have been
// frozen, then freeze its norm component:
let mut coords = [0.0; Axis::CARDINALITY];
let mut nset: usize = 0;
for &MatrixEntry {index, value} in &(problem.frozen) {
if index.1 == col && index.0 < Axis::CARDINALITY {
nset += 1;
coords[index.0] = value
}
}
if nset == Axis::CARDINALITY {
let [x, y, z] = coords;
problem.frozen.push(
Point::NORM_COMPONENT, col, point(x,y,z)[Point::NORM_COMPONENT]);
}
}
});
}
}
// the velocity is expressed in uniform coordinates
pub struct ElementMotion<'a> {
pub element: Rc<dyn Element>,
pub velocity: DVectorView<'a, f64>,
pub velocity: DVectorView<'a, f64>
}
type AssemblyMotion<'a> = Vec<ElementMotion<'a>>;
@ -618,12 +552,12 @@ pub struct Assembly {
pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>,
// realization control
pub realization_trigger: Signal<()>,
pub keep_realized: Signal<bool>,
pub needs_realization: Signal<bool>,
// realization diagnostics
pub realization_status: Signal<Result<(), String>>,
pub descent_history: Signal<DescentHistory>,
pub step: Signal<SpecifiedValue>,
pub descent_history: Signal<DescentHistory>
}
impl Assembly {
@ -634,35 +568,20 @@ impl Assembly {
regulators: create_signal(BTreeSet::new()),
tangent: create_signal(ConfigSubspace::zero(0)),
elements_by_id: create_signal(BTreeMap::default()),
realization_trigger: create_signal(()),
keep_realized: create_signal(true),
needs_realization: create_signal(false),
realization_status: create_signal(Ok(())),
descent_history: create_signal(DescentHistory::new()),
step: create_signal(SpecifiedValue::from_empty_spec()),
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_realization = assembly.clone();
// realize the assembly whenever it becomes simultaneously true that
// we're trying to keep it realized and it needs realization
let assembly_for_effect = assembly.clone();
create_effect(move || {
assembly_for_realization.elements.track();
assembly_for_realization.regulators.with(
|regs| for reg in regs {
reg.set_point().track();
}
);
assembly_for_realization.realization_trigger.track();
assembly_for_realization.realize();
});
// load a configuration from the descent history whenever the active
// step is updated
let assembly_for_step_selection = assembly.clone();
create_effect(move || {
if let Some(step) = assembly.step.with(|n| n.value) {
let config = assembly.descent_history.with_untracked(
|history| history.config[step as usize].clone()
);
assembly_for_step_selection.load_config(&config)
let should_realize = assembly_for_effect.keep_realized.get()
&& assembly_for_effect.needs_realization.get();
if should_realize {
assembly_for_effect.realize();
}
});
@ -727,6 +646,19 @@ impl Assembly {
regulators.update(|regs| regs.insert(regulator.clone()));
}
// request a 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.needs_realization.set(true);
}
});
/* DEBUG */
// print an updated list of regulators
console_log!("Regulators:");
@ -750,16 +682,6 @@ impl Assembly {
});
}
// --- updating the configuration ---
pub fn load_config(&self, config: &DMatrix<f64>) {
for elt in self.elements.get_clone_untracked() {
elt.representation().update(
|rep| rep.set_column(0, &config.column(elt.column_index().unwrap()))
);
}
}
// --- realization ---
pub fn realize(&self) {
@ -787,7 +709,6 @@ impl Assembly {
/* DEBUG */
// log the Gram matrix
console_log!("Gram matrix:\n{}", problem.gram);
console_log!("Frozen entries:\n{}", problem.frozen);
/* DEBUG */
// log the initial configuration matrix
@ -805,17 +726,14 @@ impl Assembly {
} else {
console_log!("✅️ Target accuracy achieved!");
}
if history.scaled_loss.len() > 0 {
console_log!("Steps: {}", history.scaled_loss.len() - 1);
console_log!("Loss: {}", history.scaled_loss.last().unwrap());
}
console_log!("Steps: {}", history.scaled_loss.len() - 1);
console_log!("Loss: {}", history.scaled_loss.last().unwrap());
// report the descent history
let step_cnt = history.config.len();
// report the loss history
self.descent_history.set(history);
match result {
Ok(ConfigNeighborhood { nbhd: tangent, .. }) => {
Ok(ConfigNeighborhood { config, nbhd: tangent }) => {
/* DEBUG */
// report the tangent dimension
console_log!("Tangent dimension: {}", tangent.dim());
@ -823,29 +741,26 @@ impl Assembly {
// report the realization status
self.realization_status.set(Ok(()));
// display the last realization step
self.step.set(
if step_cnt > 0 {
let last_step = step_cnt - 1;
SpecifiedValue::try_from(last_step.to_string()).unwrap()
} else {
SpecifiedValue::from_empty_spec()
}
);
// read out the solution
for elt in self.elements.get_clone_untracked() {
elt.representation().update(
|rep| rep.set_column(0, &config.column(elt.column_index().unwrap()))
);
}
// save the tangent space
self.tangent.set_silent(tangent);
// clear the realization request flag
self.needs_realization.set(false);
},
Err(message) => {
// report the realization status. the `Err(message)` we're
// setting the status to has a different type than the
// `Err(message)` we received from the match: we're changing the
// `Ok` type from `Realization` to `()`
self.realization_status.set(Err(message));
// display the initial guess
self.step.set(SpecifiedValue::from(Some(0.0)));
},
self.realization_status.set(Err(message))
}
}
}
@ -928,15 +843,15 @@ impl Assembly {
},
None => {
console_log!("No velocity to unpack for fresh element \"{}\"", elt.id())
},
}
};
});
}
// trigger a realization to bring the configuration back onto the
// request a realization to bring the configuration back onto the
// solution variety. this also gets the elements' column indices and the
// saved tangent space back in sync
self.realization_trigger.set(());
self.needs_realization.set(true);
}
}
@ -947,8 +862,7 @@ mod tests {
use crate::engine;
#[test]
#[should_panic(expected =
"Sphere \"sphere\" must be indexed before it writes problem data")]
#[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")]
fn unindexed_element_test() {
let _ = create_root(|| {
let elt = Sphere::default("sphere".to_string(), 0);
@ -957,8 +871,7 @@ mod tests {
}
#[test]
#[should_panic(expected = "Subject \"sphere1\" must be indexed before \
inversive distance regulator writes problem data")]
#[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")]
fn unindexed_subject_test_inversive_distance() {
let _ = create_root(|| {
let subjects = [0, 1].map(
@ -990,7 +903,7 @@ mod tests {
String::from(sphere_id),
String::from("Sphere 0"),
[0.75_f32, 0.75_f32, 0.75_f32],
engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS),
engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS)
)
);
@ -1004,7 +917,7 @@ mod tests {
vec![
ElementMotion {
element: sphere.clone(),
velocity: velocity.as_view(),
velocity: velocity.as_view()
}
]
);
@ -1019,4 +932,4 @@ mod tests {
assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL);
});
}
}
}

View file

@ -4,47 +4,32 @@ use sycamore::prelude::*;
use super::test_assembly_chooser::TestAssemblyChooser;
use crate::{
AppState,
assembly::{InversiveDistanceRegulator, Point, Sphere},
assembly::{InversiveDistanceRegulator, Point, Sphere}
};
#[component]
pub fn AddRemove() -> View {
view! {
div(id = "add-remove") {
div(id="add-remove") {
button(
on:click = |_| {
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>();
});
state.assembly.insert_element_default::<Sphere>();
}
) { "Add sphere" }
button(
on:click = |_| {
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 = {
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 = |_| {
on:click=|_| {
let state = use_context::<AppState>();
let subjects: [_; 2] = state.selection.with(
// the button is only enabled when two elements are

View file

@ -7,16 +7,18 @@ use charming::{
};
use sycamore::prelude::*;
use crate::{AppState, specified::SpecifiedValue};
use crate::AppState;
#[derive(Clone)]
struct DiagnosticsState {
active_tab: Signal<String>,
active_tab: Signal<String>
}
impl DiagnosticsState {
fn new(initial_tab: String) -> Self {
Self { active_tab: create_signal(initial_tab) }
fn new(initial_tab: String) -> DiagnosticsState {
DiagnosticsState {
active_tab: create_signal(initial_tab)
}
}
}
@ -27,20 +29,20 @@ fn RealizationStatus() -> View {
let realization_status = state.assembly.realization_status;
view! {
div(
id = "realization-status",
class = realization_status.with(
id="realization-status",
class=realization_status.with(
|status| match status {
Ok(_) => "",
Err(_) => "invalid",
Err(_) => "invalid"
}
)
) {
div(class = "status")
div(class="status")
div {
(realization_status.with(
|status| match status {
Ok(_) => "Target accuracy achieved".to_string(),
Err(message) => message.clone(),
Err(message) => message.clone()
}
))
}
@ -48,73 +50,10 @@ fn RealizationStatus() -> View {
}
}
// history step input
#[component]
fn StepInput() -> View {
// get the assembly
let state = use_context::<AppState>();
let assembly = state.assembly;
// the `last_step` signal holds the index of the last step
let last_step = assembly.descent_history.map(
|history| match history.config.len() {
0 => None,
n => Some(n - 1),
}
);
let input_max = last_step.map(|last| last.unwrap_or(0));
// these signals hold the entered step number
let value = create_signal(String::new());
let value_as_number = create_signal(0.0);
create_effect(move || {
value.set(assembly.step.with(|n| n.spec.clone()));
});
view! {
div(id = "step-input") {
label { "Step" }
input(
r#type = "number",
min = "0",
max = input_max.with(|max| max.to_string()),
bind:value = value,
bind:valueAsNumber = value_as_number,
on:change = move |_| {
if last_step.with(|last| last.is_some()) {
// clamp the step within its allowed range. the lower
// bound is redundant on browsers that make it
// impossible to type negative values into a number
// input with a non-negative `min`, but there's no harm
// in being careful
let step_raw = value.with(
|val| SpecifiedValue::try_from(val.clone())
.unwrap_or(SpecifiedValue::from_empty_spec()
)
);
let step = SpecifiedValue::from(
step_raw.value.map(
|val| val.clamp(0.0, input_max.get() as f64)
)
);
// set the input string and the assembly's active step
value.set(step.spec.clone());
assembly.step.set(step);
} else {
value.set(String::new());
}
},
)
}
}
}
fn into_log10_time_point((step, value): (usize, f64)) -> Vec<Option<f64>> {
vec![
Some(step as f64),
if value == 0.0 { None } else { Some(value.abs().log10()) },
if value == 0.0 { None } else { Some(value.abs().log10()) }
]
}
@ -166,7 +105,7 @@ fn LossHistory() -> View {
});
view! {
div(id = CONTAINER_ID, class = "diagnostics-chart")
div(id=CONTAINER_ID, class="diagnostics-chart")
}
}
@ -183,7 +122,7 @@ fn SpectrumHistory() -> View {
// positive, negative, and strictly-zero parts
let (
hess_eigvals_zero,
hess_eigvals_nonzero,
hess_eigvals_nonzero
): (Vec<_>, Vec<_>) = state.assembly.descent_history.with(
|history| history.hess_eigvals
.iter()
@ -204,7 +143,7 @@ fn SpectrumHistory() -> View {
.unwrap_or(1.0);
let (
hess_eigvals_pos,
hess_eigvals_neg,
hess_eigvals_neg
): (Vec<_>, Vec<_>) = hess_eigvals_nonzero
.into_iter()
.partition(|&(_, val)| val > 0.0);
@ -272,7 +211,7 @@ fn SpectrumHistory() -> View {
});
view! {
div(id = CONTAINER_ID, class = "diagnostics-chart")
div(id=CONTAINER_ID, class="diagnostics-chart")
}
}
@ -281,8 +220,8 @@ fn DiagnosticsPanel(name: &'static str, children: Children) -> View {
let diagnostics_state = use_context::<DiagnosticsState>();
view! {
div(
class = "diagnostics-panel",
"hidden" = diagnostics_state.active_tab.with(
class="diagnostics-panel",
"hidden"=diagnostics_state.active_tab.with(
|active_tab| {
if active_tab == name {
None
@ -304,17 +243,16 @@ pub fn Diagnostics() -> View {
provide_context(diagnostics_state);
view! {
div(id = "diagnostics") {
div(id = "diagnostics-bar") {
div(id="diagnostics") {
div(id="diagnostics-bar") {
RealizationStatus {}
select(bind:value = active_tab) {
option(value = "loss") { "Loss" }
option(value = "spectrum") { "Spectrum" }
select(bind:value=active_tab) {
option(value="loss") { "Loss" }
option(value="spectrum") { "Spectrum" }
}
StepInput {}
}
DiagnosticsPanel(name = "loss") { LossHistory {} }
DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} }
DiagnosticsPanel(name="loss") { LossHistory {} }
DiagnosticsPanel(name="spectrum") { SpectrumHistory {} }
}
}
}

View file

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

View file

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

View file

@ -6,17 +6,17 @@ use web_sys::{console, wasm_bindgen::JsValue};
use crate::{
AppState,
engine,
engine::DescentHistory,
assembly::{
Assembly,
Element,
ElementColor,
InversiveDistanceRegulator,
Point,
Sphere,
Sphere
},
engine,
engine::DescentHistory,
specified::SpecifiedValue,
specified::SpecifiedValue
};
// --- loaders ---
@ -26,13 +26,13 @@ use crate::{
// 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) {
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),
engine::sphere(0.5, 0.5, 0.0, 1.0)
)
);
let _ = assembly.try_insert_element(
@ -40,7 +40,7 @@ fn load_general(assembly: &Assembly) {
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),
engine::sphere(-0.5, -0.5, 0.0, 1.0)
)
);
let _ = assembly.try_insert_element(
@ -48,7 +48,7 @@ fn load_general(assembly: &Assembly) {
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),
engine::sphere(-0.5, 0.5, 0.0, 0.75)
)
);
let _ = assembly.try_insert_element(
@ -56,7 +56,7 @@ fn load_general(assembly: &Assembly) {
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),
engine::sphere(0.5, -0.5, 0.0, 0.5)
)
);
let _ = assembly.try_insert_element(
@ -64,7 +64,7 @@ fn load_general(assembly: &Assembly) {
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),
engine::sphere(0.0, 0.15, 1.0, 0.25)
)
);
let _ = assembly.try_insert_element(
@ -72,12 +72,12 @@ fn load_general(assembly: &Assembly) {
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),
engine::sphere(0.0, -0.15, -1.0, 0.25)
)
);
}
fn load_low_curvature(assembly: &Assembly) {
fn load_low_curv_assemb(assembly: &Assembly) {
// create the spheres
let a = 0.75_f64.sqrt();
let _ = assembly.try_insert_element(
@ -85,7 +85,7 @@ fn load_low_curvature(assembly: &Assembly) {
"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),
engine::sphere(0.0, 0.0, 0.0, 1.0)
)
);
let _ = assembly.try_insert_element(
@ -93,7 +93,7 @@ fn load_low_curvature(assembly: &Assembly) {
"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),
engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0)
)
);
let _ = assembly.try_insert_element(
@ -101,7 +101,7 @@ fn load_low_curvature(assembly: &Assembly) {
"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),
engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0)
)
);
let _ = assembly.try_insert_element(
@ -109,7 +109,7 @@ fn load_low_curvature(assembly: &Assembly) {
"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),
engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0)
)
);
let _ = assembly.try_insert_element(
@ -117,7 +117,7 @@ fn load_low_curvature(assembly: &Assembly) {
"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),
engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0)
)
);
let _ = assembly.try_insert_element(
@ -125,7 +125,7 @@ fn load_low_curvature(assembly: &Assembly) {
"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),
engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0)
)
);
let _ = assembly.try_insert_element(
@ -133,7 +133,7 @@ fn load_low_curvature(assembly: &Assembly) {
"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),
engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0)
)
);
let _ = assembly.try_insert_element(
@ -141,7 +141,7 @@ fn load_low_curvature(assembly: &Assembly) {
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),
engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0)
)
);
@ -196,13 +196,13 @@ fn load_low_curvature(assembly: &Assembly) {
}
}
fn load_pointed(assembly: &Assembly) {
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),
engine::point(0.0, 0.0, FRAC_1_SQRT_2)
)
);
let _ = assembly.try_insert_element(
@ -210,7 +210,7 @@ fn load_pointed(assembly: &Assembly) {
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),
engine::point(0.0, 0.0, -FRAC_1_SQRT_2)
)
);
for index_x in 0..=1 {
@ -223,7 +223,7 @@ fn load_pointed(assembly: &Assembly) {
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),
engine::sphere(x, y, 0.0, 1.0)
)
);
@ -232,7 +232,7 @@ fn load_pointed(assembly: &Assembly) {
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),
engine::point(x, y, 0.0)
)
);
}
@ -246,7 +246,7 @@ fn load_pointed(assembly: &Assembly) {
// B-C "
// C-C "
// A-C -0.25 * φ^2 = -0.6545084971874737
fn load_tridiminished_icosahedron(assembly: &Assembly) {
fn load_tridim_icosahedron_assemb(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];
@ -256,56 +256,56 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
"a1".to_string(),
"A₁".to_string(),
COLOR_A,
engine::point(0.25, 0.75, 0.75),
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),
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),
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),
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),
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),
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),
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),
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),
),
engine::point(-1.0, -1.0, 0.0)
)
];
for vertex in vertices {
let _ = assembly.try_insert_element(vertex);
@ -320,20 +320,20 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
"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),
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),
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),
),
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);
@ -409,14 +409,14 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
// 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) {
fn load_dodeca_packing_assemb(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),
engine::sphere(0.0, 0.0, 0.0, 1.0)
)
);
let substrate = assembly.elements_by_id.with_untracked(
@ -456,7 +456,7 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
id_a.clone(),
format!("A{label_sub}"),
COLOR_A,
engine::sphere(0.0, small_coord, big_coord, face_radii[k]),
engine::sphere(0.0, small_coord, big_coord, face_radii[k])
)
);
faces.push(
@ -472,7 +472,7 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
id_b.clone(),
format!("B{label_sub}"),
COLOR_B,
engine::sphere(small_coord, big_coord, 0.0, face_radii[k]),
engine::sphere(small_coord, big_coord, 0.0, face_radii[k])
)
);
faces.push(
@ -488,7 +488,7 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
id_c.clone(),
format!("C{label_sub}"),
COLOR_C,
engine::sphere(big_coord, 0.0, small_coord, face_radii[k]),
engine::sphere(big_coord, 0.0, small_coord, face_radii[k])
)
);
faces.push(
@ -550,7 +550,7 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
// 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) {
fn load_balanced_assemb(assembly: &Assembly) {
// create the spheres
const R_OUTER: f64 = 10.0;
const R_INNER: f64 = 4.0;
@ -559,19 +559,19 @@ fn load_balanced(assembly: &Assembly) {
"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),
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),
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),
engine::sphere(0.0, -4.0, 0.0, R_INNER)
),
];
for sphere in spheres {
@ -589,7 +589,7 @@ fn load_balanced(assembly: &Assembly) {
for (sphere, radius) in [
(outer.clone(), R_OUTER),
(a.clone(), R_INNER),
(b.clone(), R_INNER),
(b.clone(), R_INNER)
] {
let curvature_regulator = sphere.regulators().with_untracked(
|regs| regs.first().unwrap().clone()
@ -611,14 +611,14 @@ fn load_balanced(assembly: &Assembly) {
// 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) {
fn load_off_center_assemb(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),
engine::point(1e-9, 0.0, 0.0)
),
);
let _ = assembly.try_insert_element(
@ -626,7 +626,7 @@ fn load_off_center(assembly: &Assembly) {
"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),
engine::sphere(0.0, 0.0, 0.0, 1.0)
),
);
@ -648,7 +648,7 @@ fn load_off_center(assembly: &Assembly) {
// 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) {
fn load_radius_ratio_assemb(assembly: &Assembly) {
let index_range = 1..=4;
// create the spheres
@ -658,14 +658,14 @@ fn load_radius_ratio(assembly: &Assembly) {
"sphere_faces".to_string(),
"Insphere".to_string(),
GRAY,
engine::sphere(0.0, 0.0, 0.0, 0.5),
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),
),
engine::sphere(0.0, 0.0, 0.0, 0.25)
)
];
for sphere in spheres {
let _ = assembly.try_insert_element(sphere);
@ -678,13 +678,13 @@ fn load_radius_ratio(assembly: &Assembly) {
[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],
[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),
engine::point(0.6, 0.8, -0.6)
].into_iter()
).map(
|(k, color, representation)| {
@ -692,7 +692,7 @@ fn load_radius_ratio(assembly: &Assembly) {
format!("v{k}"),
format!("Vertex {k}"),
color,
representation,
representation
)
}
);
@ -709,13 +709,13 @@ fn load_radius_ratio(assembly: &Assembly) {
[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],
[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),
engine::sphere_with_offset(-base_dir[0], -base_dir[1], base_dir[2], offset, 0.0)
].into_iter()
).map(
|(k, color, representation)| {
@ -723,7 +723,7 @@ fn load_radius_ratio(assembly: &Assembly) {
format!("f{k}"),
format!("Face {k}"),
color,
representation,
representation
)
}
);
@ -736,7 +736,7 @@ fn load_radius_ratio(assembly: &Assembly) {
for j in index_range.clone() {
let [face_j, vertex_j] = [
format!("f{j}"),
format!("v{j}"),
format!("v{j}")
].map(
|id| assembly.elements_by_id.with_untracked(
|elts_by_id| elts_by_id[&id].clone()
@ -789,7 +789,7 @@ fn load_radius_ratio(assembly: &Assembly) {
// 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) {
fn load_irisawa_hexlet_assemb(assembly: &Assembly) {
let index_range = 1..=6;
let colors = [
[1.00_f32, 0.00_f32, 0.25_f32],
@ -797,7 +797,7 @@ fn load_irisawa_hexlet(assembly: &Assembly) {
[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],
[0.25_f32, 0.00_f32, 1.00_f32]
].into_iter();
// create the spheres
@ -806,19 +806,19 @@ fn load_irisawa_hexlet(assembly: &Assembly) {
"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),
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),
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),
engine::sphere(0.0, 0.75, 0.0, 0.75)
),
].into_iter().chain(
index_range.clone().zip(colors).map(
@ -828,7 +828,7 @@ fn load_irisawa_hexlet(assembly: &Assembly) {
format!("chain{k}"),
format!("Chain {k}"),
color,
engine::sphere(1.0 * ang.sin(), 0.0, 1.0 * ang.cos(), 0.5),
engine::sphere(1.0 * ang.sin(), 0.0, 1.0 * ang.cos(), 0.5)
)
}
)
@ -865,7 +865,7 @@ fn load_irisawa_hexlet(assembly: &Assembly) {
(outer.clone(), "1"),
(sun.clone(), "-1"),
(moon.clone(), "-1"),
(chain_sphere_next.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());
@ -900,6 +900,9 @@ pub fn TestAssemblyChooser() -> View {
let state = use_context::<AppState>();
let assembly = &state.assembly;
// pause realization
assembly.keep_realized.set(false);
// clear state
assembly.regulators.update(|regs| regs.clear());
assembly.elements.update(|elts| elts.clear());
@ -909,33 +912,36 @@ pub fn TestAssemblyChooser() -> View {
// 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),
_ => (),
"general" => load_gen_assemb(assembly),
"low-curv" => load_low_curv_assemb(assembly),
"pointed" => load_pointed_assemb(assembly),
"tridim-icosahedron" => load_tridim_icosahedron_assemb(assembly),
"dodeca-packing" => load_dodeca_packing_assemb(assembly),
"balanced" => load_balanced_assemb(assembly),
"off-center" => load_off_center_assemb(assembly),
"radius-ratio" => load_radius_ratio_assemb(assembly),
"irisawa-hexlet" => load_irisawa_hexlet_assemb(assembly),
_ => ()
};
// resume realization
assembly.keep_realized.set(true);
});
});
// 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" }
select(bind:value=assembly_name) {
option(value="general") { "General" }
option(value="low-curv") { "Low-curvature" }
option(value="pointed") { "Pointed" }
option(value="tridim-icosahedron") { "Tridiminished icosahedron" }
option(value="dodeca-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,6 +1,7 @@
use lazy_static::lazy_static;
use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen};
use std::fmt::{Display, Error, Formatter};
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
// --- elements ---
@ -16,7 +17,7 @@ pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVect
center_y / radius,
center_z / radius,
0.5 / radius,
0.5 * (center_norm_sq / radius - radius),
0.5 * (center_norm_sq / radius - radius)
])
}
@ -30,7 +31,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_z,
0.5 * curv,
off * (1.0 + 0.5 * off * curv),
off * (1.0 + 0.5 * off * curv)
])
}
@ -49,23 +50,57 @@ pub fn project_point_to_normalized(rep: &mut DVector<f64>) {
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 ---
pub struct MatrixEntry {
pub index: (usize, usize),
pub value: f64,
index: (usize, usize),
value: f64
}
pub struct PartialMatrix(Vec<MatrixEntry>);
impl PartialMatrix {
pub fn new() -> Self {
Self(Vec::<MatrixEntry>::new())
pub fn new() -> PartialMatrix {
PartialMatrix(Vec::<MatrixEntry>::new())
}
pub fn push(&mut self, row: usize, col: usize, value: f64) {
let Self(entries) = self;
entries.push(MatrixEntry { index: (row, col), value });
let PartialMatrix(entries) = self;
entries.push(MatrixEntry { index: (row, col), value: value });
}
pub fn push_sym(&mut self, row: usize, col: usize, value: f64) {
@ -114,7 +149,7 @@ impl IntoIterator for PartialMatrix {
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
let Self(entries) = self;
let PartialMatrix(entries) = self;
entries.into_iter()
}
}
@ -135,26 +170,22 @@ impl<'a> IntoIterator for &'a PartialMatrix {
pub struct ConfigSubspace {
assembly_dim: usize,
basis_std: Vec<DMatrix<f64>>,
basis_proj: Vec<DMatrix<f64>>,
basis_proj: Vec<DMatrix<f64>>
}
impl ConfigSubspace {
pub fn zero(assembly_dim: usize) -> Self {
Self {
assembly_dim,
pub fn zero(assembly_dim: usize) -> ConfigSubspace {
ConfigSubspace {
assembly_dim: assembly_dim,
basis_proj: Vec::new(),
basis_std: Vec::new(),
basis_std: Vec::new()
}
}
// approximate the kernel of a symmetric endomorphism of the configuration
// space for `assembly_dim` elements. we consider an eigenvector to be part
// of the kernel if its eigenvalue is smaller than the constant `THRESHOLD`
fn symmetric_kernel(
a: DMatrix<f64>,
proj_to_std: DMatrix<f64>,
assembly_dim: usize,
) -> Self {
fn symmetric_kernel(a: DMatrix<f64>, proj_to_std: DMatrix<f64>, assembly_dim: usize) -> ConfigSubspace {
// find a basis for the kernel. the basis is expressed in the projection
// coordinates, and it's orthonormal with respect to the projection
// inner product
@ -168,13 +199,20 @@ impl ConfigSubspace {
).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
let basis_std = proj_to_std * &basis_proj;
const ELEMENT_DIM: usize = 5;
const UNIFORM_DIM: usize = 4;
Self {
assembly_dim,
ConfigSubspace {
assembly_dim: assembly_dim,
basis_std: basis_std.column_iter().map(
|v| Into::<DMatrix<f64>>::into(
v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim))
@ -184,7 +222,7 @@ impl ConfigSubspace {
|v| Into::<DMatrix<f64>>::into(
v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim))
)
).collect(),
).collect()
}
}
@ -218,14 +256,14 @@ pub struct DescentHistory {
pub config: Vec<DMatrix<f64>>,
pub scaled_loss: Vec<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 backoff_steps: Vec<i32>,
pub backoff_steps: Vec<i32>
}
impl DescentHistory {
pub fn new() -> Self {
Self {
pub fn new() -> DescentHistory {
DescentHistory {
config: Vec::<DMatrix<f64>>::new(),
scaled_loss: Vec::<f64>::new(),
neg_grad: Vec::<DMatrix<f64>>::new(),
@ -245,21 +283,21 @@ pub struct ConstraintProblem {
}
impl ConstraintProblem {
pub fn new(element_count: usize) -> Self {
pub fn new(element_count: usize) -> ConstraintProblem {
const ELEMENT_DIM: usize = 5;
Self {
ConstraintProblem {
gram: PartialMatrix::new(),
frozen: PartialMatrix::new(),
guess: DMatrix::<f64>::zeros(ELEMENT_DIM, element_count),
guess: DMatrix::<f64>::zeros(ELEMENT_DIM, element_count)
}
}
#[cfg(feature = "dev")]
pub fn from_guess(guess_columns: &[DVector<f64>]) -> Self {
Self {
pub fn from_guess(guess_columns: &[DVector<f64>]) -> ConstraintProblem {
ConstraintProblem {
gram: PartialMatrix::new(),
frozen: PartialMatrix::new(),
guess: DMatrix::from_columns(guess_columns),
guess: DMatrix::from_columns(guess_columns)
}
}
}
@ -273,21 +311,25 @@ lazy_static! {
0.0, 1.0, 0.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, -2.0,
0.0, 0.0, 0.0, -2.0, 0.0,
0.0, 0.0, 0.0, -2.0, 0.0
]);
}
struct SearchState {
config: DMatrix<f64>,
err_proj: DMatrix<f64>,
loss: f64,
loss: f64
}
impl SearchState {
fn from_config(gram: &PartialMatrix, config: DMatrix<f64>) -> Self {
fn from_config(gram: &PartialMatrix, config: DMatrix<f64>) -> SearchState {
let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config));
let loss = err_proj.norm_squared();
Self { config, err_proj, loss }
SearchState {
config: config,
err_proj: err_proj,
loss: loss
}
}
}
@ -314,7 +356,7 @@ pub fn local_unif_to_std(v: DVectorView<f64>) -> DMatrix<f64> {
curv, 0.0, 0.0, 0.0, v[0],
0.0, curv, 0.0, 0.0, v[1],
0.0, 0.0, curv, 0.0, v[2],
0.0, 0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 0.0, 1.0
])
} else {
// `v` represents a sphere. the normalization condition says that the
@ -323,7 +365,7 @@ pub fn local_unif_to_std(v: DVectorView<f64>) -> DMatrix<f64> {
curv, 0.0, 0.0, 0.0, v[0],
0.0, curv, 0.0, 0.0, v[1],
0.0, 0.0, curv, 0.0, v[2],
curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0,
curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0
])
}
}
@ -336,7 +378,7 @@ fn seek_better_config(
base_target_improvement: f64,
min_efficiency: f64,
backoff: f64,
max_backoff_steps: i32,
max_backoff_steps: i32
) -> Option<(SearchState, i32)> {
let mut rate = 1.0;
for backoff_steps in 0..max_backoff_steps {
@ -353,13 +395,13 @@ fn seek_better_config(
// a first-order neighborhood of a configuration
pub struct ConfigNeighborhood {
#[cfg(feature = "dev")] pub config: DMatrix<f64>,
pub nbhd: ConfigSubspace,
pub config: DMatrix<f64>,
pub nbhd: ConfigSubspace
}
pub struct Realization {
pub result: Result<ConfigNeighborhood, String>,
pub history: DescentHistory,
pub history: DescentHistory
}
// seek a matrix `config` that matches the partial matrix `problem.frozen` and
@ -373,30 +415,19 @@ pub fn realize_gram(
backoff: f64,
reg_scale: f64,
max_descent_steps: i32,
max_backoff_steps: i32,
max_backoff_steps: i32
) -> Realization {
// destructure the problem data
let ConstraintProblem { gram, guess, frozen } = problem;
let ConstraintProblem {
gram, guess, frozen
} = problem;
// start the descent history
let mut history = DescentHistory::new();
// handle the case where the assembly is empty. our general realization
// routine can't handle this case because it builds the Hessian using
// `DMatrix::from_columns`, which panics when the list of columns is empty
let assembly_dim = guess.ncols();
if assembly_dim == 0 {
let result = Ok(
ConfigNeighborhood {
#[cfg(feature = "dev")] config: guess.clone(),
nbhd: ConfigSubspace::zero(0),
}
);
return Realization { result, history };
}
// find the dimension of the search space
let element_dim = guess.nrows();
let assembly_dim = guess.ncols();
let total_dim = element_dim * assembly_dim;
// scale the tolerance
@ -473,8 +504,8 @@ pub fn realize_gram(
Some(cholesky) => cholesky,
None => return Realization {
result: Err("Cholesky decomposition failed".to_string()),
history,
},
history
}
};
let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked);
let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim));
@ -483,16 +514,16 @@ pub fn realize_gram(
// use backtracking line search to find a better configuration
if let Some((better_state, backoff_steps)) = seek_better_config(
gram, &state, &base_step, neg_grad.dot(&base_step),
min_efficiency, backoff, max_backoff_steps,
min_efficiency, backoff, max_backoff_steps
) {
state = better_state;
history.backoff_steps.push(backoff_steps);
} else {
return Realization {
result: Err("Line search failed".to_string()),
history,
};
}
history
}
};
}
let result = if state.loss < tol {
// express the uniform basis in the standard basis
@ -509,7 +540,7 @@ pub fn realize_gram(
// find the kernel of the Hessian. give it the uniform inner product
let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim);
Ok(ConfigNeighborhood { #[cfg(feature = "dev")] config: state.config, nbhd: tangent })
Ok(ConfigNeighborhood { config: state.config, nbhd: tangent })
} else {
Err("Failed to reach target accuracy".to_string())
};
@ -537,7 +568,7 @@ pub mod examples {
[
sphere(0.0, 0.0, 0.0, 15.0),
sphere(0.0, 0.0, -9.0, 5.0),
sphere(0.0, 0.0, 11.0, 3.0),
sphere(0.0, 0.0, 11.0, 3.0)
].into_iter().chain(
(1..=6).map(
|k| {
@ -596,7 +627,7 @@ pub mod examples {
point(0.0, 0.0, 0.0),
point(ang_hor.cos(), ang_hor.sin(), 0.0),
point(x_vert, y_vert, -0.5),
point(x_vert, y_vert, 0.5),
point(x_vert, y_vert, 0.5)
]
}
).collect::<Vec<_>>().as_slice()
@ -639,15 +670,15 @@ mod tests {
MatrixEntry { index: (0, 0), value: 14.0 },
MatrixEntry { index: (0, 2), value: 28.0 },
MatrixEntry { index: (1, 1), value: 42.0 },
MatrixEntry { index: (1, 2), value: 49.0 },
MatrixEntry { index: (1, 2), value: 49.0 }
]);
let config = DMatrix::<f64>::from_row_slice(2, 3, &[
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, &[
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);
}
@ -658,15 +689,15 @@ mod tests {
MatrixEntry { index: (0, 0), value: 19.0 },
MatrixEntry { index: (0, 2), value: 39.0 },
MatrixEntry { index: (1, 1), value: 59.0 },
MatrixEntry { index: (1, 2), value: 69.0 },
MatrixEntry { index: (1, 2), value: 69.0 }
]);
let attempt = DMatrix::<f64>::from_row_slice(2, 3, &[
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, &[
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);
}
@ -684,7 +715,7 @@ mod tests {
DMatrix::from_columns(&[
sphere(1.0, 0.0, 0.0, a),
sphere(-0.5, a, 0.0, a),
sphere(-0.5, -a, 0.0, a),
sphere(-0.5, -a, 0.0, a)
])
};
let state = SearchState::from_config(&gram, config);
@ -698,7 +729,7 @@ mod tests {
fn frozen_entry_test() {
let mut problem = ConstraintProblem::from_guess(&[
point(0.0, 0.0, 2.0),
sphere(0.0, 0.0, 0.0, 0.95),
sphere(0.0, 0.0, 0.0, 0.95)
]);
for j in 0..2 {
for k in j..2 {
@ -742,7 +773,7 @@ mod tests {
let mut problem = ConstraintProblem::from_guess(&[
sphere(0.0, 0.0, 0.0, -2.0),
sphere(0.0, 0.0, 1.0, 1.0),
sphere(0.0, 0.0, -1.0, 1.0),
sphere(0.0, 0.0, -1.0, 1.0)
]);
for j in 0..3 {
for k in j..3 {
@ -772,8 +803,8 @@ mod tests {
DMatrix::<f64>::from_column_slice(UNIFORM_DIM, assembly_dim, &[
0.0, 0.0, 0.0, 0.0,
0.0, 0.0, -0.5, -0.5,
0.0, 0.0, -0.5, 0.5,
]),
0.0, 0.0, -0.5, 0.5
])
];
let tangent_motions_std = vec![
basis_matrix((0, 1), element_dim, assembly_dim),
@ -783,8 +814,8 @@ mod tests {
DMatrix::<f64>::from_column_slice(element_dim, assembly_dim, &[
0.0, 0.0, 0.0, 0.00, 0.0,
0.0, 0.0, -1.0, -0.25, -1.0,
0.0, 0.0, -1.0, 0.25, 1.0,
]),
0.0, 0.0, -1.0, 0.25, 1.0
])
];
// confirm that the dimension of the tangent space is no greater than
@ -860,10 +891,10 @@ mod tests {
DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]),
DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]),
DVector::from_column_slice(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]),
DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0]),
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(
|motion| DMatrix::from_columns(
@ -896,7 +927,7 @@ mod tests {
0.0, 1.0, 0.0, 0.0, dis[1],
0.0, 0.0, 1.0, 0.0, dis[2],
2.0*dis[0], 2.0*dis[1], 2.0*dis[2], 1.0, dis.norm_squared(),
0.0, 0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 0.0, 1.0
])
}
@ -908,7 +939,7 @@ mod tests {
const SCALED_TOL: f64 = 1.0e-12;
let mut problem_orig = ConstraintProblem::from_guess(&[
sphere(0.0, 0.0, 0.5, 1.0),
sphere(0.0, 0.0, -0.5, 1.0),
sphere(0.0, 0.0, -0.5, 1.0)
]);
problem_orig.gram.push_sym(0, 0, 1.0);
problem_orig.gram.push_sym(1, 1, 1.0);
@ -926,13 +957,13 @@ mod tests {
let a = 0.5 * FRAC_1_SQRT_2;
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)
])
};
let problem_tfm = ConstraintProblem {
gram: problem_orig.gram,
frozen: problem_orig.frozen,
guess: guess_tfm,
frozen: problem_orig.frozen
};
let Realization { result: result_tfm, history: history_tfm } = realize_gram(
&problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110
@ -960,7 +991,7 @@ mod tests {
0.0, 1.0, 0.0, 0.0, 0.0,
FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0, 0.0,
0.0, 0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 0.0, 1.0,
0.0, 0.0, 0.0, 0.0, 1.0
]);
let transl = translation(Vector3::new(0.0, 0.0, 7.0));
let motion_proj_tfm = transl * rot * motion_orig_proj;

View file

@ -14,20 +14,20 @@ use components::{
add_remove::AddRemove,
diagnostics::Diagnostics,
display::Display,
outline::Outline,
outline::Outline
};
#[derive(Clone)]
struct AppState {
assembly: Assembly,
selection: Signal<BTreeSet<Rc<dyn Element>>>,
selection: Signal<BTreeSet<Rc<dyn Element>>>
}
impl AppState {
fn new() -> Self {
Self {
fn new() -> AppState {
AppState {
assembly: Assembly::new(),
selection: create_signal(BTreeSet::default()),
selection: create_signal(BTreeSet::default())
}
}
@ -58,7 +58,7 @@ fn main() {
provide_context(AppState::new());
view! {
div(id = "sidebar") {
div(id="sidebar") {
AddRemove {}
Outline {}
Diagnostics {}

View file

@ -13,12 +13,12 @@ use std::num::ParseFloatError;
#[readonly::make]
pub struct SpecifiedValue {
pub spec: String,
pub value: Option<f64>,
pub value: Option<f64>
}
impl SpecifiedValue {
pub fn from_empty_spec() -> Self {
Self { spec: String::new(), value: None }
pub fn from_empty_spec() -> SpecifiedValue {
SpecifiedValue { spec: String::new(), value: None }
}
pub fn is_present(&self) -> bool {
@ -26,17 +26,6 @@ impl SpecifiedValue {
}
}
// a `SpecifiedValue` can be constructed from a floating-point option, which is
// given a canonical specification
impl From<Option<f64>> for SpecifiedValue {
fn from(value: Option<f64>) -> Self {
match value {
Some(x) => SpecifiedValue{ spec: x.to_string(), value },
None => SpecifiedValue::from_empty_spec(),
}
}
}
// a `SpecifiedValue` can be constructed from a specification string, formatted
// as described in the comment on the structure definition. the result is `Ok`
// if the specification is properly formatted, and `Error` if not
@ -45,10 +34,10 @@ impl TryFrom<String> for SpecifiedValue {
fn try_from(spec: String) -> Result<Self, Self::Error> {
if spec.is_empty() {
Ok(Self::from_empty_spec())
Ok(SpecifiedValue::from_empty_spec())
} else {
spec.parse::<f64>().map(
|value| Self { spec, value: Some(value) }
|value| SpecifiedValue { spec: spec, value: Some(value) }
)
}
}

5
deploy/.gitignore vendored
View file

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

View file

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