Compare commits

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

5 commits

Author SHA1 Message Date
2c8c09d20d feat: Point coordinate regulators (#118)
Implement regulators for the Euclidean coordinates of `Point` entities, automatically creating all three of them for each added point entity. When such a regulator is set, it freezes the corresponding representation coordinate to the set point. In addition, if all three coordinates of a given `Point` are set, the coradius coordinate (which holds the norm of the point) is frozen as well.

Note that a `PointCoordinateRegulator` must be created with a `Point` as the subject. This commit modifies `HalfCurvatureRegulator` analogously, so that it can only be created with a `Sphere`.
Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Co-committed-by: Glen Whitney <glen@studioinfinity.org>
2025-10-13 22:52:02 +00:00
978f70aac7 Rewind through the descent history (#114)
You can now rewind through the descent history of the last realization using the *Step* control that's been added to the diagnostics panel.

The starting value of the *Step* control depends on the realization status. After a successful realization, we show the realized state (the last step). After an unsuccessful realization, we show the initial guess (step zero).

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: StudioInfinity/dyna3#114
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-09-18 23:31:17 +00:00
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
20 changed files with 735 additions and 415 deletions

View file

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

21
app-proto/Cargo.lock generated
View file

@ -255,6 +255,7 @@ dependencies = [
"charming",
"console_error_panic_hook",
"dyna3",
"enum-iterator",
"itertools",
"js-sys",
"lazy_static",
@ -271,6 +272,26 @@ 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,6 +10,7 @@ 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"

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(
|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,6 +184,7 @@ details[open]:has(li) .element-switch::after {
#diagnostics-bar {
display: flex;
gap: 8px;
}
#realization-status {
@ -207,6 +208,14 @@ 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

@ -1,13 +1,14 @@
use enum_iterator::{all, Sequence};
use nalgebra::{DMatrix, DVector, DVectorView};
use std::{
cell::Cell,
collections::{BTreeMap, BTreeSet},
cmp::Ordering,
collections::{BTreeMap, BTreeSet},
fmt,
fmt::{Debug, Formatter},
fmt::{Debug, Display, 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 */
@ -26,9 +27,10 @@ use crate::{
ConfigSubspace,
ConstraintProblem,
DescentHistory,
Realization
MatrixEntry,
Realization,
},
specified::SpecifiedValue
specified::SpecifiedValue,
};
pub type ElementColor = [f32; 3];
@ -84,6 +86,14 @@ 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);
}
@ -125,8 +135,8 @@ pub trait Element: Serial + ProblemPoser + DisplayItem {
}
impl Debug for dyn Element {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
self.id().fmt(f)
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.id(), f)
}
}
@ -164,7 +174,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 {
@ -174,17 +184,17 @@ impl Sphere {
id: String,
label: String,
color: ElementColor,
representation: DVector<f64>
) -> Sphere {
Sphere {
id: id,
label: label,
color: color,
representation: DVector<f64>,
) -> Self {
Self {
id,
label,
color,
representation: create_signal(representation),
ghost: create_signal(false),
regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(),
column_index: None.into()
column_index: None.into(),
}
}
}
@ -194,12 +204,12 @@ impl Element for Sphere {
"sphere".to_string()
}
fn default(id: String, id_num: u64) -> Sphere {
Sphere::new(
fn default(id: String, id_num: u64) -> Self {
Self::new(
id,
format!("Sphere {id_num}"),
[0.75_f32, 0.75_f32, 0.75_f32],
sphere(0.0, 0.0, 0.0, 1.0)
sphere(0.0, 0.0, 0.0, 1.0),
)
}
@ -249,8 +259,7 @@ impl Serial for Sphere {
impl ProblemPoser for Sphere {
fn pose(&self, problem: &mut ConstraintProblem) {
let index = self.column_index().expect(
format!("Sphere \"{}\" should be indexed before writing problem data", self.id).as_str()
);
indexing_error("Sphere", &self.id, "it").as_str());
problem.gram.push_sym(index, index, 1.0);
problem.guess.set_column(index, &self.representation.get_clone_untracked());
}
@ -264,19 +273,20 @@ 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>
) -> Point {
Point {
representation: DVector<f64>,
) -> Self {
Self {
id,
label,
color,
@ -284,7 +294,7 @@ impl Point {
ghost: create_signal(false),
regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(),
column_index: None.into()
column_index: None.into(),
}
}
}
@ -294,14 +304,23 @@ impl Element for Point {
"point".to_string()
}
fn default(id: String, id_num: u64) -> Point {
Point::new(
fn default(id: String, id_num: u64) -> Self {
Self::new(
id,
format!("Point {id_num}"),
[0.75_f32, 0.75_f32, 0.75_f32],
point(0.0, 0.0, 0.0)
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
@ -345,10 +364,9 @@ impl Serial for Point {
impl ProblemPoser for Point {
fn pose(&self, problem: &mut ConstraintProblem) {
let index = self.column_index().expect(
format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str()
);
indexing_error("Point", &self.id, "it").as_str());
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());
}
}
@ -389,11 +407,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]) -> InversiveDistanceRegulator {
pub fn new(subjects: [Rc<dyn Element>; 2]) -> Self {
let representations = subjects.each_ref().map(|subj| subj.representation());
let measurement = create_memo(move || {
representations[0].with(|rep_0|
@ -406,7 +424,7 @@ impl InversiveDistanceRegulator {
let set_point = create_signal(SpecifiedValue::from_empty_spec());
let serial = Self::next_serial();
InversiveDistanceRegulator { subjects, measurement, set_point, serial }
Self { subjects, measurement, set_point, serial }
}
}
@ -436,8 +454,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(
"Subjects should be indexed before inversive distance regulator writes problem data"
)
indexing_error("Subject", subj.id(),
"inversive distance regulator").as_str())
);
problem.gram.push_sym(row, col, val);
}
@ -446,14 +464,14 @@ impl ProblemPoser for InversiveDistanceRegulator {
}
pub struct HalfCurvatureRegulator {
pub subject: Rc<dyn Element>,
pub subject: Rc<Sphere>,
pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>,
serial: u64
serial: u64,
}
impl HalfCurvatureRegulator {
pub fn new(subject: Rc<dyn Element>) -> HalfCurvatureRegulator {
pub fn new(subject: Rc<Sphere>) -> Self {
let measurement = subject.representation().map(
|rep| rep[Sphere::CURVATURE_COMPONENT]
);
@ -461,7 +479,7 @@ impl HalfCurvatureRegulator {
let set_point = create_signal(SpecifiedValue::from_empty_spec());
let serial = Self::next_serial();
HalfCurvatureRegulator { subject, measurement, set_point, serial }
Self { subject, measurement, set_point, serial }
}
}
@ -490,18 +508,89 @@ 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(
"Subject should be indexed before half-curvature regulator writes problem data"
);
indexing_error("Subject", &self.subject.id,
"half-curvature regulator").as_str());
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>>;
@ -533,7 +622,8 @@ pub struct Assembly {
// realization diagnostics
pub realization_status: Signal<Result<(), String>>,
pub descent_history: Signal<DescentHistory>
pub descent_history: Signal<DescentHistory>,
pub step: Signal<SpecifiedValue>,
}
impl Assembly {
@ -546,21 +636,34 @@ impl Assembly {
elements_by_id: create_signal(BTreeMap::default()),
realization_trigger: create_signal(()),
realization_status: create_signal(Ok(())),
descent_history: create_signal(DescentHistory::new())
descent_history: create_signal(DescentHistory::new()),
step: create_signal(SpecifiedValue::from_empty_spec()),
};
// 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();
let assembly_for_realization = assembly.clone();
create_effect(move || {
assembly_for_effect.elements.track();
assembly_for_effect.regulators.with(
assembly_for_realization.elements.track();
assembly_for_realization.regulators.with(
|regs| for reg in regs {
reg.set_point().track();
}
);
assembly_for_effect.realization_trigger.track();
assembly_for_effect.realize();
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)
}
});
assembly
@ -647,6 +750,16 @@ 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) {
@ -674,6 +787,7 @@ 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
@ -696,11 +810,12 @@ impl Assembly {
console_log!("Loss: {}", history.scaled_loss.last().unwrap());
}
// report the loss history
// report the descent history
let step_cnt = history.config.len();
self.descent_history.set(history);
match result {
Ok(ConfigNeighborhood { config, nbhd: tangent }) => {
Ok(ConfigNeighborhood { nbhd: tangent, .. }) => {
/* DEBUG */
// report the tangent dimension
console_log!("Tangent dimension: {}", tangent.dim());
@ -708,12 +823,15 @@ impl Assembly {
// report the realization status
self.realization_status.set(Ok(()));
// read out the solution
for elt in self.elements.get_clone_untracked() {
elt.representation().update(
|rep| rep.set_column(0, &config.column(elt.column_index().unwrap()))
);
}
// 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()
}
);
// save the tangent space
self.tangent.set_silent(tangent);
@ -723,8 +841,11 @@ impl Assembly {
// 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))
}
self.realization_status.set(Err(message));
// display the initial guess
self.step.set(SpecifiedValue::from(Some(0.0)));
},
}
}
@ -807,7 +928,7 @@ impl Assembly {
},
None => {
console_log!("No velocity to unpack for fresh element \"{}\"", elt.id())
}
},
};
});
}
@ -826,7 +947,8 @@ mod tests {
use crate::engine;
#[test]
#[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")]
#[should_panic(expected =
"Sphere \"sphere\" must be indexed before it writes problem data")]
fn unindexed_element_test() {
let _ = create_root(|| {
let elt = Sphere::default("sphere".to_string(), 0);
@ -835,7 +957,8 @@ mod tests {
}
#[test]
#[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")]
#[should_panic(expected = "Subject \"sphere1\" must be indexed before \
inversive distance regulator writes problem data")]
fn unindexed_subject_test_inversive_distance() {
let _ = create_root(|| {
let subjects = [0, 1].map(
@ -867,7 +990,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),
)
);
@ -881,7 +1004,7 @@ mod tests {
vec![
ElementMotion {
element: sphere.clone(),
velocity: velocity.as_view()
velocity: velocity.as_view(),
}
]
);
@ -896,4 +1019,4 @@ mod tests {
assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL);
});
}
}
}

View file

@ -4,15 +4,15 @@ 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.
@ -33,18 +33,18 @@ pub fn AddRemove() -> View {
}
) { "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,18 +7,16 @@ use charming::{
};
use sycamore::prelude::*;
use crate::AppState;
use crate::{AppState, specified::SpecifiedValue};
#[derive(Clone)]
struct DiagnosticsState {
active_tab: Signal<String>
active_tab: Signal<String>,
}
impl DiagnosticsState {
fn new(initial_tab: String) -> DiagnosticsState {
DiagnosticsState {
active_tab: create_signal(initial_tab)
}
fn new(initial_tab: String) -> Self {
Self { active_tab: create_signal(initial_tab) }
}
}
@ -29,20 +27,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(),
}
))
}
@ -50,10 +48,73 @@ 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()) },
]
}
@ -105,7 +166,7 @@ fn LossHistory() -> View {
});
view! {
div(id=CONTAINER_ID, class="diagnostics-chart")
div(id = CONTAINER_ID, class = "diagnostics-chart")
}
}
@ -122,7 +183,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()
@ -143,7 +204,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);
@ -211,7 +272,7 @@ fn SpectrumHistory() -> View {
});
view! {
div(id=CONTAINER_ID, class="diagnostics-chart")
div(id = CONTAINER_ID, class = "diagnostics-chart")
}
}
@ -220,8 +281,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
@ -243,16 +304,17 @@ 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() -> SceneSpheres{
SceneSpheres {
fn new() -> Self {
Self {
representations: 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")
}
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);
@ -64,20 +67,23 @@ struct ScenePoints {
representations: Vec<DVector<f64>>,
colors_with_opacity: Vec<ColorWithOpacity>,
highlights: Vec<f32>,
selections: Vec<f32>
selections: Vec<f32>,
}
impl ScenePoints {
fn new() -> ScenePoints {
ScenePoints {
fn new() -> Self {
Self {
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);
@ -87,14 +93,14 @@ impl ScenePoints {
pub struct Scene {
spheres: SceneSpheres,
points: ScenePoints
points: ScenePoints,
}
impl Scene {
fn new() -> Scene {
Scene {
fn new() -> Self {
Self {
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 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 {
@ -124,7 +135,12 @@ 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;
@ -177,7 +193,12 @@ 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`
@ -220,7 +241,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(
@ -260,12 +281,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())
})
@ -276,7 +297,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(
@ -292,7 +313,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();
@ -319,7 +340,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);
@ -341,9 +362,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,
)
}
@ -443,14 +464,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 */
@ -467,7 +488,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
@ -503,7 +524,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);
@ -567,7 +588,25 @@ pub fn Display() -> View {
location_z *= (time_step * ZOOM_SPEED * zoom).exp();
// manipulate the assembly
if state.selection.with(|sel| sel.len() == 1) {
/* KLUDGE */
// to avoid the complexity of making tangent space projection
// conditional and dealing with unnormalized representation vectors,
// we only allow manipulation when we're looking at the last step of
// a successful realization
let realization_successful = state.assembly.realization_status.with(
|status| status.is_ok()
);
let step_val = state.assembly.step.with_untracked(|step| step.value);
let on_init_step = step_val.is_some_and(|n| n == 0.0);
let on_last_step = step_val.is_some_and(
|n| state.assembly.descent_history.with_untracked(
|history| n as usize + 1 == history.config.len().max(1)
)
);
let on_manipulable_step =
!realization_successful && on_init_step
|| realization_successful && on_last_step;
if on_manipulable_step && state.selection.with(|sel| sel.len() == 1) {
let sel = state.selection.with(
|sel| sel.into_iter().next().unwrap().clone()
);
@ -596,7 +635,7 @@ pub fn Display() -> View {
vec![
ElementMotion {
element: sel,
velocity: elt_motion.as_view()
velocity: elt_motion.as_view(),
}
]
);
@ -629,7 +668,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;
@ -668,19 +707,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],
);
}
@ -773,7 +812,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);
@ -793,7 +832,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();
@ -805,12 +844,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());
@ -836,7 +875,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());
@ -858,7 +897,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);
@ -866,7 +905,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()));
@ -883,18 +922,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,11 +1,7 @@
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,
@ -13,7 +9,8 @@ use crate::{
Element,
HalfCurvatureRegulator,
InversiveDistanceRegulator,
Regulator
PointCoordinateRegulator,
Regulator,
},
specified::SpecifiedValue
};
@ -49,8 +46,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() {
@ -63,27 +60,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(),
_ => ()
_ => (),
}
}
}
},
)
}
}
@ -100,11 +97,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")
}
}
}
@ -113,11 +110,25 @@ 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")
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")
}
}
}
@ -156,10 +167,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() {
@ -179,18 +190,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| {
@ -200,20 +211,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()
)
}
}
@ -246,18 +257,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,
},
specified::SpecifiedValue
engine,
engine::DescentHistory,
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_gen_assemb(assembly: &Assembly) {
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)
engine::sphere(0.5, 0.5, 0.0, 1.0),
)
);
let _ = assembly.try_insert_element(
@ -40,7 +40,7 @@ fn load_gen_assemb(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_gen_assemb(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_gen_assemb(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_gen_assemb(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_gen_assemb(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_curv_assemb(assembly: &Assembly) {
fn load_low_curvature(assembly: &Assembly) {
// create the spheres
let a = 0.75_f64.sqrt();
let _ = assembly.try_insert_element(
@ -85,7 +85,7 @@ fn load_low_curv_assemb(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_curv_assemb(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_curv_assemb(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_curv_assemb(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_curv_assemb(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_curv_assemb(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_curv_assemb(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_curv_assemb(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_curv_assemb(assembly: &Assembly) {
}
}
fn load_pointed_assemb(assembly: &Assembly) {
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)
engine::point(0.0, 0.0, FRAC_1_SQRT_2),
)
);
let _ = assembly.try_insert_element(
@ -210,7 +210,7 @@ fn load_pointed_assemb(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_assemb(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_assemb(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_assemb(assembly: &Assembly) {
// B-C "
// C-C "
// A-C -0.25 * φ^2 = -0.6545084971874737
fn load_tridim_icosahedron_assemb(assembly: &Assembly) {
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];
@ -256,56 +256,56 @@ fn load_tridim_icosahedron_assemb(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_tridim_icosahedron_assemb(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_tridim_icosahedron_assemb(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_dodeca_packing_assemb(assembly: &Assembly) {
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)
engine::sphere(0.0, 0.0, 0.0, 1.0),
)
);
let substrate = assembly.elements_by_id.with_untracked(
@ -456,7 +456,7 @@ fn load_dodeca_packing_assemb(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_dodeca_packing_assemb(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_dodeca_packing_assemb(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_dodeca_packing_assemb(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_assemb(assembly: &Assembly) {
fn load_balanced(assembly: &Assembly) {
// create the spheres
const R_OUTER: f64 = 10.0;
const R_INNER: f64 = 4.0;
@ -559,19 +559,19 @@ fn load_balanced_assemb(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_assemb(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_assemb(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_assemb(assembly: &Assembly) {
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)
engine::point(1e-9, 0.0, 0.0),
),
);
let _ = assembly.try_insert_element(
@ -626,7 +626,7 @@ fn load_off_center_assemb(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_assemb(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_assemb(assembly: &Assembly) {
fn load_radius_ratio(assembly: &Assembly) {
let index_range = 1..=4;
// create the spheres
@ -658,14 +658,14 @@ fn load_radius_ratio_assemb(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_assemb(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_assemb(assembly: &Assembly) {
format!("v{k}"),
format!("Vertex {k}"),
color,
representation
representation,
)
}
);
@ -709,13 +709,13 @@ fn load_radius_ratio_assemb(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_assemb(assembly: &Assembly) {
format!("f{k}"),
format!("Face {k}"),
color,
representation
representation,
)
}
);
@ -736,7 +736,7 @@ fn load_radius_ratio_assemb(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_assemb(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_assemb(assembly: &Assembly) {
fn load_irisawa_hexlet(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_assemb(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_assemb(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_assemb(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_assemb(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());
@ -909,33 +909,33 @@ pub fn TestAssemblyChooser() -> View {
// load assembly
match name.as_str() {
"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),
_ => ()
"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-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" }
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

@ -16,7 +16,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 +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_z,
0.5 * curv,
off * (1.0 + 0.5 * off * curv)
off * (1.0 + 0.5 * off * curv),
])
}
@ -52,20 +52,20 @@ pub fn project_point_to_normalized(rep: &mut DVector<f64>) {
// --- partial matrices ---
pub struct MatrixEntry {
index: (usize, usize),
value: f64
pub index: (usize, usize),
pub value: f64,
}
pub struct PartialMatrix(Vec<MatrixEntry>);
impl PartialMatrix {
pub fn new() -> PartialMatrix {
PartialMatrix(Vec::<MatrixEntry>::new())
pub fn new() -> Self {
Self(Vec::<MatrixEntry>::new())
}
pub fn push(&mut self, row: usize, col: usize, value: f64) {
let PartialMatrix(entries) = self;
entries.push(MatrixEntry { index: (row, col), value: value });
let Self(entries) = self;
entries.push(MatrixEntry { index: (row, col), value });
}
pub fn push_sym(&mut self, row: usize, col: usize, value: f64) {
@ -114,7 +114,7 @@ impl IntoIterator for PartialMatrix {
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
let PartialMatrix(entries) = self;
let Self(entries) = self;
entries.into_iter()
}
}
@ -135,22 +135,26 @@ 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) -> ConfigSubspace {
ConfigSubspace {
assembly_dim: assembly_dim,
pub fn zero(assembly_dim: usize) -> Self {
Self {
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) -> 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
// coordinates, and it's orthonormal with respect to the projection
// inner product
@ -169,8 +173,8 @@ impl ConfigSubspace {
const ELEMENT_DIM: usize = 5;
const UNIFORM_DIM: usize = 4;
ConfigSubspace {
assembly_dim: assembly_dim,
Self {
assembly_dim,
basis_std: basis_std.column_iter().map(
|v| Into::<DMatrix<f64>>::into(
v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim))
@ -180,7 +184,7 @@ impl ConfigSubspace {
|v| Into::<DMatrix<f64>>::into(
v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim))
)
).collect()
).collect(),
}
}
@ -214,14 +218,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() -> DescentHistory {
DescentHistory {
pub fn new() -> Self {
Self {
config: Vec::<DMatrix<f64>>::new(),
scaled_loss: Vec::<f64>::new(),
neg_grad: Vec::<DMatrix<f64>>::new(),
@ -241,21 +245,21 @@ pub struct ConstraintProblem {
}
impl ConstraintProblem {
pub fn new(element_count: usize) -> ConstraintProblem {
pub fn new(element_count: usize) -> Self {
const ELEMENT_DIM: usize = 5;
ConstraintProblem {
Self {
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>]) -> ConstraintProblem {
ConstraintProblem {
pub fn from_guess(guess_columns: &[DVector<f64>]) -> Self {
Self {
gram: PartialMatrix::new(),
frozen: PartialMatrix::new(),
guess: DMatrix::from_columns(guess_columns)
guess: DMatrix::from_columns(guess_columns),
}
}
}
@ -269,25 +273,21 @@ 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>) -> SearchState {
fn from_config(gram: &PartialMatrix, config: DMatrix<f64>) -> Self {
let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config));
let loss = err_proj.norm_squared();
SearchState {
config: config,
err_proj: err_proj,
loss: loss
}
Self { config, err_proj, loss }
}
}
@ -314,7 +314,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 +323,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 +336,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 +353,13 @@ fn seek_better_config(
// a first-order neighborhood of a configuration
pub struct ConfigNeighborhood {
pub config: DMatrix<f64>,
pub nbhd: ConfigSubspace
#[cfg(feature = "dev")] 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,12 +373,10 @@ 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();
@ -390,11 +388,11 @@ pub fn realize_gram(
if assembly_dim == 0 {
let result = Ok(
ConfigNeighborhood {
config: guess.clone(),
nbhd: ConfigSubspace::zero(0)
#[cfg(feature = "dev")] config: guess.clone(),
nbhd: ConfigSubspace::zero(0),
}
);
return Realization { result, history }
return Realization { result, history };
}
// find the dimension of the search space
@ -475,8 +473,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));
@ -485,16 +483,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
@ -511,7 +509,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 { config: state.config, nbhd: tangent })
Ok(ConfigNeighborhood { #[cfg(feature = "dev")] config: state.config, nbhd: tangent })
} else {
Err("Failed to reach target accuracy".to_string())
};
@ -539,7 +537,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| {
@ -598,7 +596,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()
@ -641,15 +639,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);
}
@ -660,15 +658,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);
}
@ -686,7 +684,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);
@ -700,7 +698,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 {
@ -744,7 +742,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 {
@ -774,8 +772,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),
@ -785,8 +783,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
@ -862,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, 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(
@ -898,7 +896,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,
])
}
@ -910,7 +908,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);
@ -928,13 +926,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
@ -962,7 +960,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() -> AppState {
AppState {
fn new() -> Self {
Self {
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() -> SpecifiedValue {
SpecifiedValue { spec: String::new(), value: None }
pub fn from_empty_spec() -> Self {
Self { spec: String::new(), value: None }
}
pub fn is_present(&self) -> bool {
@ -26,6 +26,17 @@ 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
@ -34,10 +45,10 @@ impl TryFrom<String> for SpecifiedValue {
fn try_from(spec: String) -> Result<Self, Self::Error> {
if spec.is_empty() {
Ok(SpecifiedValue::from_empty_spec())
Ok(Self::from_empty_spec())
} else {
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
# 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
RUN_EXAMPLE="cargo run --manifest-path $MANIFEST --example"