From fb8e391587f3e3ffde9e7c7d68940fd24dc80a6f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 25 Jan 2025 13:00:18 -0800 Subject: [PATCH 01/41] Generalize constraints to observables --- app-proto/main.css | 8 +++---- app-proto/src/add_remove.rs | 44 ++++++++++++++++++++++++++----------- app-proto/src/assembly.rs | 18 ++++++++++----- app-proto/src/outline.rs | 42 +++++++++++++++++++++++++---------- 4 files changed, 77 insertions(+), 35 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index b9fc0a1..9c45c29 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -131,10 +131,6 @@ details[open]:has(li) .element-switch::after { color: var(--text-invalid); } -.constraint > input[type=checkbox] { - margin: 0px 8px 0px 0px; -} - .constraint > input[type=text] { color: inherit; background-color: inherit; @@ -154,6 +150,10 @@ details[open]:has(li) .element-switch::after { font-style: normal; } +.constrained > .status::after, details:has(.constrained):not([open]) .status::after { + content: '🔗'; +} + .invalid > .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index ba02e65..360a12d 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -1,7 +1,17 @@ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; -use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}}; +use crate::{ + engine, + AppState, + assembly::{ + Assembly, + Constraint, + ConstraintRole, + Element + }, + engine::Q +}; /* DEBUG */ // load an example assembly for testing. this code will be removed once we've @@ -190,15 +200,23 @@ pub fn AddRemove() -> View { (subject_vec[0].clone(), subject_vec[1].clone()) } ); - let lorentz_prod = create_signal(0.0); - let lorentz_prod_valid = create_signal(false); - let active = create_signal(true); + let measured = state.assembly.elements.map( + move |elts| { + let reps = ( + elts[subjects.0].representation.get_clone(), + elts[subjects.1].representation.get_clone() + ); + reps.0.dot(&(&*Q * reps.1)) + } + ); + let desired = create_signal(0.0); + let role = create_signal(ConstraintRole::Measure); state.assembly.insert_constraint(Constraint { subjects: subjects, - lorentz_prod: lorentz_prod, - lorentz_prod_text: create_signal(String::new()), - lorentz_prod_valid: lorentz_prod_valid, - active: active, + measured: measured, + desired: desired, + desired_text: create_signal(String::new()), + role: role, }); state.selection.update(|sel| sel.clear()); @@ -212,19 +230,19 @@ pub fn AddRemove() -> View { &JsValue::from(cst.subjects.0), &JsValue::from(cst.subjects.1), &JsValue::from(":"), - &JsValue::from(cst.lorentz_prod.get_untracked()) + &JsValue::from(cst.desired.get_untracked()) ); } }); - // update the realization when the constraint becomes active - // and valid, or is edited while active and valid + // update the realization when the observable becomes + // constrained, or is edited while constrained create_effect(move || { console::log_1(&JsValue::from( format!("Constraint ({}, {}) updated", subjects.0, subjects.1) )); - lorentz_prod.track(); - if active.get() && lorentz_prod_valid.get() { + desired.track(); + if role.with(|r| matches!(r, ConstraintRole::Constrain)) { state.assembly.realize(); } }); diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 7073c9e..37bd484 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -111,13 +111,19 @@ impl Element { } } +pub enum ConstraintRole { + Measure, + Constrain, + Invalid +} + #[derive(Clone)] pub struct Constraint { pub subjects: (ElementKey, ElementKey), - pub lorentz_prod: Signal, - pub lorentz_prod_text: Signal, - pub lorentz_prod_valid: Signal, - pub active: Signal + pub measured: ReadSignal, + pub desired: Signal, + pub desired_text: Signal, + pub role: Signal } // the velocity is expressed in uniform coordinates @@ -230,11 +236,11 @@ impl Assembly { let mut gram_to_be = PartialMatrix::new(); self.constraints.with_untracked(|csts| { for (_, cst) in csts { - if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() { + if cst.role.with_untracked(|role| matches!(role, ConstraintRole::Constrain)) { let subjects = cst.subjects; let row = elts[subjects.0].column_index.unwrap(); let col = elts[subjects.1].column_index.unwrap(); - gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked()); + gram_to_be.push_sym(row, col, cst.desired.get_untracked()); } } }); diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index a6e968d..8459262 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -8,7 +8,16 @@ use web_sys::{ wasm_bindgen::JsCast }; -use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}}; +use crate::{ + AppState, + assembly, + assembly::{ + Constraint, + ConstraintKey, + ConstraintRole::*, + ElementKey + } +}; // an editable view of the Lorentz product representing a constraint #[component(inline_props)] @@ -16,16 +25,22 @@ fn LorentzProductInput(constraint: Constraint) -> View { view! { input( r#type="text", - bind:value=constraint.lorentz_prod_text, + placeholder=constraint.measured.with(|result| result.to_string()), + bind:value=constraint.desired_text, on:change=move |event: Event| { let target: HtmlInputElement = event.target().unwrap().unchecked_into(); - match target.value().parse::() { - Ok(lorentz_prod) => batch(|| { - constraint.lorentz_prod.set(lorentz_prod); - constraint.lorentz_prod_valid.set(true); - }), - Err(_) => constraint.lorentz_prod_valid.set(false) - }; + let value = target.value(); + if value.is_empty() { + constraint.role.set(Measure); + } else { + match target.value().parse::() { + Ok(desired) => batch(|| { + constraint.desired.set(desired); + constraint.role.set(Constrain); + }), + Err(_) => constraint.role.set(Invalid) + }; + } } ) } @@ -43,12 +58,15 @@ fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) constraint.subjects.0 }; let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); - let class = constraint.lorentz_prod_valid.map( - |&lorentz_prod_valid| if lorentz_prod_valid { "constraint" } else { "constraint invalid" } + let class = constraint.role.map( + |role| match role { + Measure => "constraint", + Constrain => "constraint constrained", + Invalid => "constraint invalid" + } ); view! { li(class=class.get()) { - input(r#type="checkbox", bind:checked=constraint.active) div(class="constraint-label") { (other_subject_label) } LorentzProductInput(constraint=constraint) div(class="status") From 677ef47544d1b99cfd82989dcdb83b9b6176d838 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 26 Jan 2025 10:50:15 -0800 Subject: [PATCH 02/41] Rename constraints to observables --- app-proto/main.css | 14 ++++---- app-proto/src/add_remove.rs | 26 +++++++------- app-proto/src/assembly.rs | 46 ++++++++++++------------ app-proto/src/outline.rs | 70 ++++++++++++++++++------------------- 4 files changed, 78 insertions(+), 78 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index 9c45c29..e41fcd9 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -77,12 +77,12 @@ summary.selected { background-color: var(--selection-highlight); } -summary > div, .constraint { +summary > div, .observable { padding-top: 4px; padding-bottom: 4px; } -.element, .constraint { +.element, .observable { display: flex; flex-grow: 1; padding-left: 8px; @@ -107,7 +107,7 @@ details[open]:has(li) .element-switch::after { flex-grow: 1; } -.constraint-label { +.observable-label { flex-grow: 1; } @@ -123,22 +123,22 @@ details[open]:has(li) .element-switch::after { width: 56px; } -.constraint { +.observable { font-style: italic; } -.constraint.invalid { +.observable.invalid { color: var(--text-invalid); } -.constraint > input[type=text] { +.observable > input[type=text] { color: inherit; background-color: inherit; border: 1px solid var(--border); border-radius: 2px; } -.constraint.invalid > input[type=text] { +.observable.invalid > input[type=text] { border-color: var(--border-invalid); } diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 360a12d..07b4c28 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -6,8 +6,8 @@ use crate::{ AppState, assembly::{ Assembly, - Constraint, - ConstraintRole, + Observable, + ObservableRole, Element }, engine::Q @@ -210,8 +210,8 @@ pub fn AddRemove() -> View { } ); let desired = create_signal(0.0); - let role = create_signal(ConstraintRole::Measure); - state.assembly.insert_constraint(Constraint { + let role = create_signal(ObservableRole::Measure); + state.assembly.insert_observable(Observable { subjects: subjects, measured: measured, desired: desired, @@ -221,16 +221,16 @@ pub fn AddRemove() -> View { state.selection.update(|sel| sel.clear()); /* DEBUG */ - // print updated constraint list - console::log_1(&JsValue::from("Constraints:")); - state.assembly.constraints.with(|csts| { - for (_, cst) in csts.into_iter() { + // print updated observable list + console::log_1(&JsValue::from("Observables:")); + state.assembly.observables.with(|obsls| { + for (_, obs) in obsls.into_iter() { console::log_5( &JsValue::from(" "), - &JsValue::from(cst.subjects.0), - &JsValue::from(cst.subjects.1), + &JsValue::from(obs.subjects.0), + &JsValue::from(obs.subjects.1), &JsValue::from(":"), - &JsValue::from(cst.desired.get_untracked()) + &JsValue::from(obs.desired.get_untracked()) ); } }); @@ -239,10 +239,10 @@ pub fn AddRemove() -> View { // constrained, or is edited while constrained create_effect(move || { console::log_1(&JsValue::from( - format!("Constraint ({}, {}) updated", subjects.0, subjects.1) + format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) )); desired.track(); - if role.with(|r| matches!(r, ConstraintRole::Constrain)) { + if role.with(|r| matches!(r, ObservableRole::Constrain)) { state.assembly.realize(); } }); diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 37bd484..e438934 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -7,9 +7,9 @@ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix}; -// the types of the keys we use to access an assembly's elements and constraints +// the types of the keys we use to access an assembly's elements and observables pub type ElementKey = usize; -pub type ConstraintKey = usize; +pub type ObservableKey = usize; pub type ElementColor = [f32; 3]; @@ -26,7 +26,7 @@ pub struct Element { pub label: String, pub color: ElementColor, pub representation: Signal>, - pub constraints: Signal>, + pub observables: Signal>, // a serial number, assigned by `Element::new`, that uniquely identifies // each element @@ -61,7 +61,7 @@ impl Element { label: label, color: color, representation: create_signal(representation), - constraints: create_signal(BTreeSet::default()), + observables: create_signal(BTreeSet::default()), serial: serial, column_index: None } @@ -111,19 +111,19 @@ impl Element { } } -pub enum ConstraintRole { +pub enum ObservableRole { Measure, Constrain, Invalid } #[derive(Clone)] -pub struct Constraint { +pub struct Observable { pub subjects: (ElementKey, ElementKey), pub measured: ReadSignal, pub desired: Signal, pub desired_text: Signal, - pub role: Signal + pub role: Signal } // the velocity is expressed in uniform coordinates @@ -137,9 +137,9 @@ type AssemblyMotion<'a> = Vec>; // a complete, view-independent description of an assembly #[derive(Clone)] pub struct Assembly { - // elements and constraints + // elements and observables pub elements: Signal>, - pub constraints: Signal>, + pub observables: Signal>, // solution variety tangent space. the basis vectors are stored in // configuration matrix format, ordered according to the elements' column @@ -161,13 +161,13 @@ impl Assembly { pub fn new() -> Assembly { Assembly { elements: create_signal(Slab::new()), - constraints: create_signal(Slab::new()), + observables: create_signal(Slab::new()), tangent: create_signal(ConfigSubspace::zero(0)), elements_by_id: create_signal(FxHashMap::default()) } } - // --- inserting elements and constraints --- + // --- inserting elements and observables --- // insert an element into the assembly without checking whether we already // have an element with the same identifier. any element that does have the @@ -210,14 +210,14 @@ impl Assembly { ); } - pub fn insert_constraint(&self, constraint: Constraint) { - let subjects = constraint.subjects; - let key = self.constraints.update(|csts| csts.insert(constraint)); - let subject_constraints = self.elements.with( - |elts| (elts[subjects.0].constraints, elts[subjects.1].constraints) + pub fn insert_observable(&self, observable: Observable) { + let subjects = observable.subjects; + let key = self.observables.update(|obsls| obsls.insert(observable)); + let subject_observables = self.elements.with( + |elts| (elts[subjects.0].observables, elts[subjects.1].observables) ); - subject_constraints.0.update(|csts| csts.insert(key)); - subject_constraints.1.update(|csts| csts.insert(key)); + subject_observables.0.update(|obsls| obsls.insert(key)); + subject_observables.1.update(|obsls| obsls.insert(key)); } // --- realization --- @@ -234,13 +234,13 @@ impl Assembly { let (gram, guess) = self.elements.with_untracked(|elts| { // set up the off-diagonal part of the Gram matrix let mut gram_to_be = PartialMatrix::new(); - self.constraints.with_untracked(|csts| { - for (_, cst) in csts { - if cst.role.with_untracked(|role| matches!(role, ConstraintRole::Constrain)) { - let subjects = cst.subjects; + self.observables.with_untracked(|obsls| { + for (_, obs) in obsls { + if obs.role.with_untracked(|role| matches!(role, ObservableRole::Constrain)) { + let subjects = obs.subjects; let row = elts[subjects.0].column_index.unwrap(); let col = elts[subjects.1].column_index.unwrap(); - gram_to_be.push_sym(row, col, cst.desired.get_untracked()); + gram_to_be.push_sym(row, col, obs.desired.get_untracked()); } } }); diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 8459262..5444974 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -12,33 +12,33 @@ use crate::{ AppState, assembly, assembly::{ - Constraint, - ConstraintKey, - ConstraintRole::*, + Observable, + ObservableKey, + ObservableRole::*, ElementKey } }; -// an editable view of the Lorentz product representing a constraint +// an editable view of the Lorentz product representing an observable #[component(inline_props)] -fn LorentzProductInput(constraint: Constraint) -> View { +fn ObservableInput(observable: Observable) -> View { view! { input( r#type="text", - placeholder=constraint.measured.with(|result| result.to_string()), - bind:value=constraint.desired_text, + placeholder=observable.measured.with(|result| result.to_string()), + bind:value=observable.desired_text, on:change=move |event: Event| { let target: HtmlInputElement = event.target().unwrap().unchecked_into(); let value = target.value(); if value.is_empty() { - constraint.role.set(Measure); + observable.role.set(Measure); } else { match target.value().parse::() { Ok(desired) => batch(|| { - constraint.desired.set(desired); - constraint.role.set(Constrain); + observable.desired.set(desired); + observable.role.set(Constrain); }), - Err(_) => constraint.role.set(Invalid) + Err(_) => observable.role.set(Invalid) }; } } @@ -46,29 +46,29 @@ fn LorentzProductInput(constraint: Constraint) -> View { } } -// a list item that shows a constraint in an outline view of an element +// a list item that shows an observable in an outline view of an element #[component(inline_props)] -fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) -> View { +fn ObservableOutlineItem(observable_key: ObservableKey, element_key: ElementKey) -> View { let state = use_context::(); let assembly = &state.assembly; - let constraint = assembly.constraints.with(|csts| csts[constraint_key].clone()); - let other_subject = if constraint.subjects.0 == element_key { - constraint.subjects.1 + let observable = assembly.observables.with(|obsls| obsls[observable_key].clone()); + let other_subject = if observable.subjects.0 == element_key { + observable.subjects.1 } else { - constraint.subjects.0 + observable.subjects.0 }; let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); - let class = constraint.role.map( + let class = observable.role.map( |role| match role { - Measure => "constraint", - Constrain => "constraint constrained", - Invalid => "constraint invalid" + Measure => "observable", + Constrain => "observable constrained", + Invalid => "observable invalid" } ); view! { li(class=class.get()) { - div(class="constraint-label") { (other_subject_label) } - LorentzProductInput(constraint=constraint) + div(class="observable-label") { (other_subject_label) } + ObservableInput(observable=observable) div(class="status") } } @@ -92,9 +92,9 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { ).collect::>() ) }; - let constrained = element.constraints.map(|csts| csts.len() > 0); - let constraint_list = element.constraints.map( - |csts| csts.clone().into_iter().collect() + let observed = element.observables.map(|obsls| obsls.len() > 0); + let observable_list = element.observables.map( + |obsls| obsls.clone().into_iter().collect() ); let details_node = create_node_ref(); view! { @@ -109,7 +109,7 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { state.select(key, event.shift_key()); event.prevent_default(); }, - "ArrowRight" if constrained.get() => { + "ArrowRight" if observed.get() => { let _ = details_node .get() .unchecked_into::() @@ -156,16 +156,16 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { div(class="status") } } - ul(class="constraints") { + ul(class="observables") { Keyed( - list=constraint_list, - view=move |cst_key| view! { - ConstraintOutlineItem( - constraint_key=cst_key, + list=observable_list, + view=move |obs_key| view! { + ObservableOutlineItem( + observable_key=obs_key, element_key=key ) }, - key=|cst_key| cst_key.clone() + key=|obs_key| obs_key.clone() ) } } @@ -174,8 +174,8 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { } // a component that lists the elements of the current assembly, showing the -// constraints on each element as a collapsible sub-list. its implementation -// is based on Kate Morley's HTML + CSS tree views: +// observables associated with each element as a collapsible sub-list. its +// implementation is based on Kate Morley's HTML + CSS tree views: // // https://iamkate.com/code/tree-views/ // From af2724f934e6cea8e4528428d2b4c8d3aff5b6de Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 26 Jan 2025 17:48:32 -0800 Subject: [PATCH 03/41] Rename `ObservableRole` variants Also rename corresponding CSS classes and add methods to check roles. --- app-proto/main.css | 8 ++++---- app-proto/src/add_remove.rs | 4 ++-- app-proto/src/assembly.rs | 22 ++++++++++++++++++---- app-proto/src/outline.rs | 12 ++++++------ 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index e41fcd9..f544e19 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -127,7 +127,7 @@ details[open]:has(li) .element-switch::after { font-style: italic; } -.observable.invalid { +.observable.invalid-constraint { color: var(--text-invalid); } @@ -138,7 +138,7 @@ details[open]:has(li) .element-switch::after { border-radius: 2px; } -.observable.invalid > input[type=text] { +.observable.invalid-constraint > input[type=text] { border-color: var(--border-invalid); } @@ -150,11 +150,11 @@ details[open]:has(li) .element-switch::after { font-style: normal; } -.constrained > .status::after, details:has(.constrained):not([open]) .status::after { +.valid-constraint > .status::after, details:has(.valid-constraint):not([open]) .status::after { content: '🔗'; } -.invalid > .status::after, details:has(.invalid):not([open]) .status::after { +.invalid-constraint > .status::after, details:has(.invalid-constraint):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); } diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 07b4c28..640925a 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -210,7 +210,7 @@ pub fn AddRemove() -> View { } ); let desired = create_signal(0.0); - let role = create_signal(ObservableRole::Measure); + let role = create_signal(ObservableRole::Measurement); state.assembly.insert_observable(Observable { subjects: subjects, measured: measured, @@ -242,7 +242,7 @@ pub fn AddRemove() -> View { format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) )); desired.track(); - if role.with(|r| matches!(r, ObservableRole::Constrain)) { + if role.with(|rl| rl.is_valid_constraint()) { state.assembly.realize(); } }); diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index e438934..ba2f9c5 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -112,9 +112,17 @@ impl Element { } pub enum ObservableRole { - Measure, - Constrain, - Invalid + Measurement, + Constraint(bool) +} + +impl ObservableRole { + pub fn is_valid_constraint(&self) -> bool { + match self { + ObservableRole::Measurement => false, + ObservableRole::Constraint(valid) => *valid + } + } } #[derive(Clone)] @@ -126,6 +134,12 @@ pub struct Observable { pub role: Signal } +impl Observable { + fn role_is_valid_constraint_untracked(&self) -> bool { + self.role.with_untracked(|role| role.is_valid_constraint()) + } +} + // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub key: ElementKey, @@ -236,7 +250,7 @@ impl Assembly { let mut gram_to_be = PartialMatrix::new(); self.observables.with_untracked(|obsls| { for (_, obs) in obsls { - if obs.role.with_untracked(|role| matches!(role, ObservableRole::Constrain)) { + if obs.role_is_valid_constraint_untracked() { let subjects = obs.subjects; let row = elts[subjects.0].column_index.unwrap(); let col = elts[subjects.1].column_index.unwrap(); diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 5444974..d2f9058 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -31,14 +31,14 @@ fn ObservableInput(observable: Observable) -> View { let target: HtmlInputElement = event.target().unwrap().unchecked_into(); let value = target.value(); if value.is_empty() { - observable.role.set(Measure); + observable.role.set(Measurement); } else { match target.value().parse::() { Ok(desired) => batch(|| { observable.desired.set(desired); - observable.role.set(Constrain); + observable.role.set(Constraint(true)); }), - Err(_) => observable.role.set(Invalid) + Err(_) => observable.role.set(Constraint(false)) }; } } @@ -60,9 +60,9 @@ fn ObservableOutlineItem(observable_key: ObservableKey, element_key: ElementKey) let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); let class = observable.role.map( |role| match role { - Measure => "observable", - Constrain => "observable constrained", - Invalid => "observable invalid" + Measurement => "observable", + Constraint(true) => "observable valid-constraint", + Constraint(false) => "observable invalid-constraint" } ); view! { From dc8330df6a3ce414648028993a43a2ee58456468 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 9 Feb 2025 22:25:44 -0800 Subject: [PATCH 04/41] Revise observable styling Distinguish constraints from observables using dark background rather than marker. Customize focus highlighting. Drop the input type selector that used to make styling apply to the Lorentz product field but not the constraint activation check box. --- app-proto/main.css | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index f544e19..46bdf84 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -3,7 +3,8 @@ --text-bright: white; --text-invalid: #f58fc2; /* bright pink */ --border: #555; /* light gray */ - --border-focus: #aaa; /* bright gray */ + --border-focus-dark: #aaa; /* bright gray */ + --border-focus-light: white; --border-invalid: #70495c; /* dusky pink */ --selection-highlight: #444; /* medium gray */ --page-background: #222; /* dark gray */ @@ -131,14 +132,24 @@ details[open]:has(li) .element-switch::after { color: var(--text-invalid); } -.observable > input[type=text] { +.observable > input { color: inherit; background-color: inherit; border: 1px solid var(--border); border-radius: 2px; } -.observable.invalid-constraint > input[type=text] { +.observable > input::placeholder { + color: inherit; + opacity: 54%; + font-style: italic; +} + +.observable.valid-constraint > input { + background-color: var(--display-background); +} + +.observable.invalid-constraint > input { border-color: var(--border-invalid); } @@ -150,10 +161,6 @@ details[open]:has(li) .element-switch::after { font-style: normal; } -.valid-constraint > .status::after, details:has(.valid-constraint):not([open]) .status::after { - content: '🔗'; -} - .invalid-constraint > .status::after, details:has(.invalid-constraint):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); @@ -171,5 +178,11 @@ canvas { } canvas:focus { - border-color: var(--border-focus); + border-color: var(--border-focus-dark); + outline: none; +} + +input:focus { + border-color: var(--border-focus-light); + outline: none; } \ No newline at end of file From de7122d871b9bb9d4e7caf2a9a47f655bd6920fb Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 12 Feb 2025 10:37:48 -0800 Subject: [PATCH 05/41] Label observable type Right now, there's only one type of observable, so the label can be hard-coded. --- app-proto/main.css | 7 ++++++- app-proto/src/outline.rs | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app-proto/main.css b/app-proto/main.css index 46bdf84..a1ff9da 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -24,7 +24,7 @@ body { display: flex; flex-direction: column; float: left; - width: 450px; + width: 500px; height: 100vh; margin: 0px; padding: 0px; @@ -128,6 +128,11 @@ details[open]:has(li) .element-switch::after { font-style: italic; } +.observable-type { + padding: 2px 8px 0px 8px; + font-size: 10pt; +} + .observable.invalid-constraint { color: var(--text-invalid); } diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index d2f9058..f341357 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -68,6 +68,7 @@ fn ObservableOutlineItem(observable_key: ObservableKey, element_key: ElementKey) view! { li(class=class.get()) { div(class="observable-label") { (other_subject_label) } + div(class="observable-type") { "Inversive distance" } ObservableInput(observable=observable) div(class="status") } From 24139ad5e9a1822cc0e240a6021d6a91e5ddd22a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 12 Feb 2025 11:35:07 -0800 Subject: [PATCH 06/41] Rename observables to regulators --- app-proto/main.css | 20 +++++----- app-proto/src/add_remove.rs | 26 ++++++------- app-proto/src/assembly.rs | 54 +++++++++++++-------------- app-proto/src/outline.rs | 74 ++++++++++++++++++------------------- 4 files changed, 87 insertions(+), 87 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index a1ff9da..d71ce4a 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -78,12 +78,12 @@ summary.selected { background-color: var(--selection-highlight); } -summary > div, .observable { +summary > div, .regulator { padding-top: 4px; padding-bottom: 4px; } -.element, .observable { +.element, .regulator { display: flex; flex-grow: 1; padding-left: 8px; @@ -108,7 +108,7 @@ details[open]:has(li) .element-switch::after { flex-grow: 1; } -.observable-label { +.regulator-label { flex-grow: 1; } @@ -124,37 +124,37 @@ details[open]:has(li) .element-switch::after { width: 56px; } -.observable { +.regulator { font-style: italic; } -.observable-type { +.regulator-type { padding: 2px 8px 0px 8px; font-size: 10pt; } -.observable.invalid-constraint { +.regulator.invalid-constraint { color: var(--text-invalid); } -.observable > input { +.regulator > input { color: inherit; background-color: inherit; border: 1px solid var(--border); border-radius: 2px; } -.observable > input::placeholder { +.regulator > input::placeholder { color: inherit; opacity: 54%; font-style: italic; } -.observable.valid-constraint > input { +.regulator.valid-constraint > input { background-color: var(--display-background); } -.observable.invalid-constraint > input { +.regulator.invalid-constraint > input { border-color: var(--border-invalid); } diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 640925a..d8d6163 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -6,8 +6,8 @@ use crate::{ AppState, assembly::{ Assembly, - Observable, - ObservableRole, + Regulator, + RegulatorRole, Element }, engine::Q @@ -210,8 +210,8 @@ pub fn AddRemove() -> View { } ); let desired = create_signal(0.0); - let role = create_signal(ObservableRole::Measurement); - state.assembly.insert_observable(Observable { + let role = create_signal(RegulatorRole::Measurement); + state.assembly.insert_regulator(Regulator { subjects: subjects, measured: measured, desired: desired, @@ -221,22 +221,22 @@ pub fn AddRemove() -> View { state.selection.update(|sel| sel.clear()); /* DEBUG */ - // print updated observable list - console::log_1(&JsValue::from("Observables:")); - state.assembly.observables.with(|obsls| { - for (_, obs) in obsls.into_iter() { + // print updated regulator list + console::log_1(&JsValue::from("Regulators:")); + state.assembly.regulators.with(|regs| { + for (_, reg) in regs.into_iter() { console::log_5( &JsValue::from(" "), - &JsValue::from(obs.subjects.0), - &JsValue::from(obs.subjects.1), + &JsValue::from(reg.subjects.0), + &JsValue::from(reg.subjects.1), &JsValue::from(":"), - &JsValue::from(obs.desired.get_untracked()) + &JsValue::from(reg.desired.get_untracked()) ); } }); - // update the realization when the observable becomes - // constrained, or is edited while constrained + // update the realization when the regulator becomes + // a constraint, or is edited while acting as a constraint create_effect(move || { console::log_1(&JsValue::from( format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index ba2f9c5..7c9418e 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -7,9 +7,9 @@ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix}; -// the types of the keys we use to access an assembly's elements and observables +// the types of the keys we use to access an assembly's elements and regulators pub type ElementKey = usize; -pub type ObservableKey = usize; +pub type RegulatorKey = usize; pub type ElementColor = [f32; 3]; @@ -26,7 +26,7 @@ pub struct Element { pub label: String, pub color: ElementColor, pub representation: Signal>, - pub observables: Signal>, + pub regulators: Signal>, // a serial number, assigned by `Element::new`, that uniquely identifies // each element @@ -61,7 +61,7 @@ impl Element { label: label, color: color, representation: create_signal(representation), - observables: create_signal(BTreeSet::default()), + regulators: create_signal(BTreeSet::default()), serial: serial, column_index: None } @@ -111,30 +111,30 @@ impl Element { } } -pub enum ObservableRole { +pub enum RegulatorRole { Measurement, Constraint(bool) } -impl ObservableRole { +impl RegulatorRole { pub fn is_valid_constraint(&self) -> bool { match self { - ObservableRole::Measurement => false, - ObservableRole::Constraint(valid) => *valid + RegulatorRole::Measurement => false, + RegulatorRole::Constraint(valid) => *valid } } } #[derive(Clone)] -pub struct Observable { +pub struct Regulator { pub subjects: (ElementKey, ElementKey), pub measured: ReadSignal, pub desired: Signal, pub desired_text: Signal, - pub role: Signal + pub role: Signal } -impl Observable { +impl Regulator { fn role_is_valid_constraint_untracked(&self) -> bool { self.role.with_untracked(|role| role.is_valid_constraint()) } @@ -151,9 +151,9 @@ type AssemblyMotion<'a> = Vec>; // a complete, view-independent description of an assembly #[derive(Clone)] pub struct Assembly { - // elements and observables + // elements and regulators pub elements: Signal>, - pub observables: Signal>, + pub regulators: Signal>, // solution variety tangent space. the basis vectors are stored in // configuration matrix format, ordered according to the elements' column @@ -175,13 +175,13 @@ impl Assembly { pub fn new() -> Assembly { Assembly { elements: create_signal(Slab::new()), - observables: create_signal(Slab::new()), + regulators: create_signal(Slab::new()), tangent: create_signal(ConfigSubspace::zero(0)), elements_by_id: create_signal(FxHashMap::default()) } } - // --- inserting elements and observables --- + // --- inserting elements and regulators --- // insert an element into the assembly without checking whether we already // have an element with the same identifier. any element that does have the @@ -224,14 +224,14 @@ impl Assembly { ); } - pub fn insert_observable(&self, observable: Observable) { - let subjects = observable.subjects; - let key = self.observables.update(|obsls| obsls.insert(observable)); - let subject_observables = self.elements.with( - |elts| (elts[subjects.0].observables, elts[subjects.1].observables) + pub fn insert_regulator(&self, regulator: Regulator) { + let subjects = regulator.subjects; + let key = self.regulators.update(|regs| regs.insert(regulator)); + let subject_regulators = self.elements.with( + |elts| (elts[subjects.0].regulators, elts[subjects.1].regulators) ); - subject_observables.0.update(|obsls| obsls.insert(key)); - subject_observables.1.update(|obsls| obsls.insert(key)); + subject_regulators.0.update(|regs| regs.insert(key)); + subject_regulators.1.update(|regs| regs.insert(key)); } // --- realization --- @@ -248,13 +248,13 @@ impl Assembly { let (gram, guess) = self.elements.with_untracked(|elts| { // set up the off-diagonal part of the Gram matrix let mut gram_to_be = PartialMatrix::new(); - self.observables.with_untracked(|obsls| { - for (_, obs) in obsls { - if obs.role_is_valid_constraint_untracked() { - let subjects = obs.subjects; + self.regulators.with_untracked(|regs| { + for (_, reg) in regs { + if reg.role_is_valid_constraint_untracked() { + let subjects = reg.subjects; let row = elts[subjects.0].column_index.unwrap(); let col = elts[subjects.1].column_index.unwrap(); - gram_to_be.push_sym(row, col, obs.desired.get_untracked()); + gram_to_be.push_sym(row, col, reg.desired.get_untracked()); } } }); diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index f341357..6e3e0d3 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -12,33 +12,33 @@ use crate::{ AppState, assembly, assembly::{ - Observable, - ObservableKey, - ObservableRole::*, + Regulator, + RegulatorKey, + RegulatorRole::*, ElementKey } }; -// an editable view of the Lorentz product representing an observable +// an editable view of a regulator #[component(inline_props)] -fn ObservableInput(observable: Observable) -> View { +fn RegulatorInput(regulator: Regulator) -> View { view! { input( r#type="text", - placeholder=observable.measured.with(|result| result.to_string()), - bind:value=observable.desired_text, + placeholder=regulator.measured.with(|result| result.to_string()), + bind:value=regulator.desired_text, on:change=move |event: Event| { let target: HtmlInputElement = event.target().unwrap().unchecked_into(); let value = target.value(); if value.is_empty() { - observable.role.set(Measurement); + regulator.role.set(Measurement); } else { match target.value().parse::() { Ok(desired) => batch(|| { - observable.desired.set(desired); - observable.role.set(Constraint(true)); + regulator.desired.set(desired); + regulator.role.set(Constraint(true)); }), - Err(_) => observable.role.set(Constraint(false)) + Err(_) => regulator.role.set(Constraint(false)) }; } } @@ -46,30 +46,30 @@ fn ObservableInput(observable: Observable) -> View { } } -// a list item that shows an observable in an outline view of an element +// a list item that shows a regulator in an outline view of an element #[component(inline_props)] -fn ObservableOutlineItem(observable_key: ObservableKey, element_key: ElementKey) -> View { +fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View { let state = use_context::(); let assembly = &state.assembly; - let observable = assembly.observables.with(|obsls| obsls[observable_key].clone()); - let other_subject = if observable.subjects.0 == element_key { - observable.subjects.1 + let regulator = assembly.regulators.with(|regs| regs[regulator_key].clone()); + let other_subject = if regulator.subjects.0 == element_key { + regulator.subjects.1 } else { - observable.subjects.0 + regulator.subjects.0 }; let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); - let class = observable.role.map( + let class = regulator.role.map( |role| match role { - Measurement => "observable", - Constraint(true) => "observable valid-constraint", - Constraint(false) => "observable invalid-constraint" + Measurement => "regulator", + Constraint(true) => "regulator valid-constraint", + Constraint(false) => "regulator invalid-constraint" } ); view! { li(class=class.get()) { - div(class="observable-label") { (other_subject_label) } - div(class="observable-type") { "Inversive distance" } - ObservableInput(observable=observable) + div(class="regulator-label") { (other_subject_label) } + div(class="regulator-type") { "Inversive distance" } + RegulatorInput(regulator=regulator) div(class="status") } } @@ -93,9 +93,9 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { ).collect::>() ) }; - let observed = element.observables.map(|obsls| obsls.len() > 0); - let observable_list = element.observables.map( - |obsls| obsls.clone().into_iter().collect() + let regulated = element.regulators.map(|regs| regs.len() > 0); + let regulator_list = element.regulators.map( + |regs| regs.clone().into_iter().collect() ); let details_node = create_node_ref(); view! { @@ -110,7 +110,7 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { state.select(key, event.shift_key()); event.prevent_default(); }, - "ArrowRight" if observed.get() => { + "ArrowRight" if regulated.get() => { let _ = details_node .get() .unchecked_into::() @@ -157,16 +157,16 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { div(class="status") } } - ul(class="observables") { + ul(class="regulators") { Keyed( - list=observable_list, - view=move |obs_key| view! { - ObservableOutlineItem( - observable_key=obs_key, + list=regulator_list, + view=move |reg_key| view! { + RegulatorOutlineItem( + regulator_key=reg_key, element_key=key ) }, - key=|obs_key| obs_key.clone() + key=|reg_key| reg_key.clone() ) } } @@ -174,9 +174,9 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { } } -// a component that lists the elements of the current assembly, showing the -// observables associated with each element as a collapsible sub-list. its -// implementation is based on Kate Morley's HTML + CSS tree views: +// a component that lists the elements of the current assembly, showing each +// element's regulators in a collapsible sub-list. its implementation is based +// on Kate Morley's HTML + CSS tree views: // // https://iamkate.com/code/tree-views/ // From b3e4e902f39d4abc195878755203799fe1302fa1 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 12 Feb 2025 11:55:45 -0800 Subject: [PATCH 07/41] Rename `Regulator` fields --- app-proto/src/add_remove.rs | 14 +++++++------- app-proto/src/assembly.rs | 8 ++++---- app-proto/src/outline.rs | 8 ++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index d8d6163..3280dac 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -200,7 +200,7 @@ pub fn AddRemove() -> View { (subject_vec[0].clone(), subject_vec[1].clone()) } ); - let measured = state.assembly.elements.map( + let measurement = state.assembly.elements.map( move |elts| { let reps = ( elts[subjects.0].representation.get_clone(), @@ -209,13 +209,13 @@ pub fn AddRemove() -> View { reps.0.dot(&(&*Q * reps.1)) } ); - let desired = create_signal(0.0); + let set_point = create_signal(0.0); let role = create_signal(RegulatorRole::Measurement); state.assembly.insert_regulator(Regulator { subjects: subjects, - measured: measured, - desired: desired, - desired_text: create_signal(String::new()), + measurement: measurement, + set_point: set_point, + set_point_text: create_signal(String::new()), role: role, }); state.selection.update(|sel| sel.clear()); @@ -230,7 +230,7 @@ pub fn AddRemove() -> View { &JsValue::from(reg.subjects.0), &JsValue::from(reg.subjects.1), &JsValue::from(":"), - &JsValue::from(reg.desired.get_untracked()) + &JsValue::from(reg.set_point.get_untracked()) ); } }); @@ -241,7 +241,7 @@ pub fn AddRemove() -> View { console::log_1(&JsValue::from( format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) )); - desired.track(); + set_point.track(); if role.with(|rl| rl.is_valid_constraint()) { state.assembly.realize(); } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 7c9418e..c052a3b 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -128,9 +128,9 @@ impl RegulatorRole { #[derive(Clone)] pub struct Regulator { pub subjects: (ElementKey, ElementKey), - pub measured: ReadSignal, - pub desired: Signal, - pub desired_text: Signal, + pub measurement: ReadSignal, + pub set_point: Signal, + pub set_point_text: Signal, pub role: Signal } @@ -254,7 +254,7 @@ impl Assembly { let subjects = reg.subjects; let row = elts[subjects.0].column_index.unwrap(); let col = elts[subjects.1].column_index.unwrap(); - gram_to_be.push_sym(row, col, reg.desired.get_untracked()); + gram_to_be.push_sym(row, col, reg.set_point.get_untracked()); } } }); diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 6e3e0d3..8dcda93 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -25,8 +25,8 @@ fn RegulatorInput(regulator: Regulator) -> View { view! { input( r#type="text", - placeholder=regulator.measured.with(|result| result.to_string()), - bind:value=regulator.desired_text, + placeholder=regulator.measurement.with(|result| result.to_string()), + bind:value=regulator.set_point_text, on:change=move |event: Event| { let target: HtmlInputElement = event.target().unwrap().unchecked_into(); let value = target.value(); @@ -34,8 +34,8 @@ fn RegulatorInput(regulator: Regulator) -> View { regulator.role.set(Measurement); } else { match target.value().parse::() { - Ok(desired) => batch(|| { - regulator.desired.set(desired); + Ok(set_pt) => batch(|| { + regulator.set_point.set(set_pt); regulator.role.set(Constraint(true)); }), Err(_) => regulator.role.set(Constraint(false)) From fef4127f69cff3e86e5070aacd209c863993c09b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 17 Feb 2025 14:01:27 -0800 Subject: [PATCH 08/41] Only sync regulator inputs on change This lets us infer a regulator's role from whether it has a set point and what text specifies the set point. --- app-proto/src/add_remove.rs | 10 +++------- app-proto/src/assembly.rs | 38 +++++++++++++----------------------- app-proto/src/outline.rs | 39 +++++++++++++------------------------ 3 files changed, 30 insertions(+), 57 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 3280dac..c96d5ab 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -7,7 +7,6 @@ use crate::{ assembly::{ Assembly, Regulator, - RegulatorRole, Element }, engine::Q @@ -209,14 +208,12 @@ pub fn AddRemove() -> View { reps.0.dot(&(&*Q * reps.1)) } ); - let set_point = create_signal(0.0); - let role = create_signal(RegulatorRole::Measurement); + let set_point = create_signal(None); state.assembly.insert_regulator(Regulator { subjects: subjects, measurement: measurement, set_point: set_point, - set_point_text: create_signal(String::new()), - role: role, + set_point_spec: create_signal(String::new()) }); state.selection.update(|sel| sel.clear()); @@ -241,8 +238,7 @@ pub fn AddRemove() -> View { console::log_1(&JsValue::from( format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) )); - set_point.track(); - if role.with(|rl| rl.is_valid_constraint()) { + if set_point.with(|set_pt| set_pt.is_some()) { state.assembly.realize(); } }); diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index c052a3b..c1eba1e 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -111,32 +111,17 @@ impl Element { } } -pub enum RegulatorRole { - Measurement, - Constraint(bool) -} - -impl RegulatorRole { - pub fn is_valid_constraint(&self) -> bool { - match self { - RegulatorRole::Measurement => false, - RegulatorRole::Constraint(valid) => *valid - } - } -} - -#[derive(Clone)] +#[derive(Clone, Copy)] pub struct Regulator { pub subjects: (ElementKey, ElementKey), pub measurement: ReadSignal, - pub set_point: Signal, - pub set_point_text: Signal, - pub role: Signal + pub set_point: Signal>, + pub set_point_spec: Signal } impl Regulator { - fn role_is_valid_constraint_untracked(&self) -> bool { - self.role.with_untracked(|role| role.is_valid_constraint()) + pub fn has_no_set_point_spec(&self) -> bool { + self.set_point_spec.with(|spec| spec.is_empty()) } } @@ -250,11 +235,14 @@ impl Assembly { let mut gram_to_be = PartialMatrix::new(); self.regulators.with_untracked(|regs| { for (_, reg) in regs { - if reg.role_is_valid_constraint_untracked() { - let subjects = reg.subjects; - let row = elts[subjects.0].column_index.unwrap(); - let col = elts[subjects.1].column_index.unwrap(); - gram_to_be.push_sym(row, col, reg.set_point.get_untracked()); + match reg.set_point.get_untracked() { + Some(set_pt) => { + let subjects = reg.subjects; + let row = elts[subjects.0].column_index.unwrap(); + let col = elts[subjects.1].column_index.unwrap(); + gram_to_be.push_sym(row, col, set_pt); + }, + None => () } } }); diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 8dcda93..d892f0f 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,8 +1,6 @@ use itertools::Itertools; use sycamore::prelude::*; use web_sys::{ - Event, - HtmlInputElement, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast @@ -14,7 +12,6 @@ use crate::{ assembly::{ Regulator, RegulatorKey, - RegulatorRole::*, ElementKey } }; @@ -22,25 +19,17 @@ use crate::{ // an editable view of a regulator #[component(inline_props)] fn RegulatorInput(regulator: Regulator) -> View { + let value = create_signal(regulator.set_point_spec.get_clone_untracked()); + create_effect(move || value.set(regulator.set_point_spec.get_clone())); view! { input( r#type="text", placeholder=regulator.measurement.with(|result| result.to_string()), - bind:value=regulator.set_point_text, - on:change=move |event: Event| { - let target: HtmlInputElement = event.target().unwrap().unchecked_into(); - let value = target.value(); - if value.is_empty() { - regulator.role.set(Measurement); - } else { - match target.value().parse::() { - Ok(set_pt) => batch(|| { - regulator.set_point.set(set_pt); - regulator.role.set(Constraint(true)); - }), - Err(_) => regulator.role.set(Constraint(false)) - }; - } + bind:value=value, + on:change=move |_| { + let value_val = value.get_clone_untracked(); + regulator.set_point.set(value_val.parse::().ok()); + regulator.set_point_spec.set(value_val); } ) } @@ -51,20 +40,20 @@ fn RegulatorInput(regulator: Regulator) -> View { fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View { let state = use_context::(); let assembly = &state.assembly; - let regulator = assembly.regulators.with(|regs| regs[regulator_key].clone()); + let regulator = assembly.regulators.with(|regs| regs[regulator_key]); let other_subject = if regulator.subjects.0 == element_key { regulator.subjects.1 } else { regulator.subjects.0 }; let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); - let class = regulator.role.map( - |role| match role { - Measurement => "regulator", - Constraint(true) => "regulator valid-constraint", - Constraint(false) => "regulator invalid-constraint" + let class = create_memo(move || { + match regulator.set_point.get() { + None if regulator.has_no_set_point_spec() => "regulator", + None => "regulator invalid-constraint", + Some(_) => "regulator valid-constraint" } - ); + }); view! { li(class=class.get()) { div(class="regulator-label") { (other_subject_label) } From 302d93638d36ccb6a7b75aba1f9d3c06e7c6f81d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 18 Feb 2025 01:27:11 -0800 Subject: [PATCH 09/41] Require regulators to have valid specifications When an invalid specification is entered into a regulator input, keep it confined to that input. Reset a regulator input by pressing *escape*. --- app-proto/main.css | 15 +++++------ app-proto/src/assembly.rs | 7 +----- app-proto/src/outline.rs | 53 +++++++++++++++++++++++++++++++-------- 3 files changed, 49 insertions(+), 26 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index d71ce4a..7ba1206 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -133,28 +133,25 @@ details[open]:has(li) .element-switch::after { font-size: 10pt; } -.regulator.invalid-constraint { - color: var(--text-invalid); -} - -.regulator > input { +.regulator-input { color: inherit; background-color: inherit; border: 1px solid var(--border); border-radius: 2px; } -.regulator > input::placeholder { +.regulator-input::placeholder { color: inherit; opacity: 54%; font-style: italic; } -.regulator.valid-constraint > input { +.regulator-input.constraint { background-color: var(--display-background); } -.regulator.invalid-constraint > input { +.regulator-input.invalid { + color: var(--text-invalid); border-color: var(--border-invalid); } @@ -166,7 +163,7 @@ details[open]:has(li) .element-switch::after { font-style: normal; } -.invalid-constraint > .status::after, details:has(.invalid-constraint):not([open]) .status::after { +.regulator:has(.invalid) > .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index c1eba1e..e8a8dc0 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -111,6 +111,7 @@ impl Element { } } +// `set_point_spec` must always be a valid specification of `set_point` #[derive(Clone, Copy)] pub struct Regulator { pub subjects: (ElementKey, ElementKey), @@ -119,12 +120,6 @@ pub struct Regulator { pub set_point_spec: Signal } -impl Regulator { - pub fn has_no_set_point_spec(&self) -> bool { - self.set_point_spec.with(|spec| spec.is_empty()) - } -} - // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub key: ElementKey, diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index d892f0f..b0347f6 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -19,17 +19,55 @@ use crate::{ // an editable view of a regulator #[component(inline_props)] fn RegulatorInput(regulator: Regulator) -> View { + let valid = create_signal(true); let value = create_signal(regulator.set_point_spec.get_clone_untracked()); - create_effect(move || value.set(regulator.set_point_spec.get_clone())); + + // this closure resets the input value to the regulator's set point + // specification, which is always a valid specification + let reset_value = move || { + batch(|| { + valid.set(true); + value.set(regulator.set_point_spec.get_clone()); + }) + }; + + // reset the input value whenever the regulator's set point specification + // is updated + create_effect(reset_value); + view! { input( r#type="text", + class=move || { + if valid.get() { + match regulator.set_point.get() { + Some(_) => "regulator-input constraint", + None => "regulator-input" + } + } else { + "regulator-input invalid" + } + }, placeholder=regulator.measurement.with(|result| result.to_string()), bind:value=value, on:change=move |_| { let value_val = value.get_clone_untracked(); - regulator.set_point.set(value_val.parse::().ok()); - regulator.set_point_spec.set(value_val); + match value_val.parse::() { + Err(_) if !value_val.is_empty() => valid.set(false), + set_pt => batch(|| { + regulator.set_point.set(set_pt.ok()); + regulator.set_point_spec.set(value_val); + valid.set(true); + }) + }; + }, + on:keydown={ + move |event: KeyboardEvent| { + match event.key().as_str() { + "Escape" => reset_value(), + _ => () + } + } } ) } @@ -47,15 +85,8 @@ fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> regulator.subjects.0 }; let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); - let class = create_memo(move || { - match regulator.set_point.get() { - None if regulator.has_no_set_point_spec() => "regulator", - None => "regulator invalid-constraint", - Some(_) => "regulator valid-constraint" - } - }); view! { - li(class=class.get()) { + li(class="regulator") { div(class="regulator-label") { (other_subject_label) } div(class="regulator-type") { "Inversive distance" } RegulatorInput(regulator=regulator) From bbd0835a8fee603a28de61054aa9637c74fdc08c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 18 Feb 2025 12:28:55 -0800 Subject: [PATCH 10/41] Move set point spec validation into `Regulator` --- app-proto/src/assembly.rs | 13 +++++++++++++ app-proto/src/outline.rs | 14 +++----------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index e8a8dc0..20b893d 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -120,6 +120,19 @@ pub struct Regulator { pub set_point_spec: Signal } +impl Regulator { + pub fn try_specify_set_point(&self, spec: String) -> bool { + match spec.parse::() { + Err(_) if !spec.is_empty() => false, + set_pt => { + self.set_point.set(set_pt.ok()); + self.set_point_spec.set(spec); + true + } + } + } +} + // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub key: ElementKey, diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index b0347f6..a48377b 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -50,17 +50,9 @@ fn RegulatorInput(regulator: Regulator) -> View { }, placeholder=regulator.measurement.with(|result| result.to_string()), bind:value=value, - on:change=move |_| { - let value_val = value.get_clone_untracked(); - match value_val.parse::() { - Err(_) if !value_val.is_empty() => valid.set(false), - set_pt => batch(|| { - regulator.set_point.set(set_pt.ok()); - regulator.set_point_spec.set(value_val); - valid.set(true); - }) - }; - }, + on:change=move |_| valid.set( + regulator.try_specify_set_point(value.get_clone_untracked()) + ), on:keydown={ move |event: KeyboardEvent| { match event.key().as_str() { From f2e84fb64a87356e7ad43927dc3d2b7bd46bcd7b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 18 Feb 2025 13:29:10 -0800 Subject: [PATCH 11/41] Enforce the validity of set point specifications Make a regulator's set point specification private, and split the set point into a private writable signal and a public read-only signal. The set point can now be initialized only through the factory method `insert_new_regulator` and changed only through the setter method `try_specify_set_point`, which both ensure that the set point specification is valid and consistent with the set point. --- app-proto/src/add_remove.rs | 47 +---------------------- app-proto/src/assembly.rs | 75 ++++++++++++++++++++++++++++++++++--- app-proto/src/outline.rs | 4 +- 3 files changed, 74 insertions(+), 52 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index c96d5ab..cdc35e6 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -6,10 +6,8 @@ use crate::{ AppState, assembly::{ Assembly, - Regulator, Element - }, - engine::Q + } }; /* DEBUG */ @@ -199,49 +197,8 @@ pub fn AddRemove() -> View { (subject_vec[0].clone(), subject_vec[1].clone()) } ); - let measurement = state.assembly.elements.map( - move |elts| { - let reps = ( - elts[subjects.0].representation.get_clone(), - elts[subjects.1].representation.get_clone() - ); - reps.0.dot(&(&*Q * reps.1)) - } - ); - let set_point = create_signal(None); - state.assembly.insert_regulator(Regulator { - subjects: subjects, - measurement: measurement, - set_point: set_point, - set_point_spec: create_signal(String::new()) - }); + state.assembly.insert_new_regulator(subjects); state.selection.update(|sel| sel.clear()); - - /* DEBUG */ - // print updated regulator list - console::log_1(&JsValue::from("Regulators:")); - state.assembly.regulators.with(|regs| { - for (_, reg) in regs.into_iter() { - console::log_5( - &JsValue::from(" "), - &JsValue::from(reg.subjects.0), - &JsValue::from(reg.subjects.1), - &JsValue::from(":"), - &JsValue::from(reg.set_point.get_untracked()) - ); - } - }); - - // update the realization when the regulator becomes - // a constraint, or is edited while acting as a constraint - create_effect(move || { - console::log_1(&JsValue::from( - format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) - )); - if set_point.with(|set_pt| set_pt.is_some()) { - state.assembly.realize(); - } - }); } ) { "🔗" } select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 20b893d..21421b6 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -5,7 +5,7 @@ use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ -use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix}; +use crate::engine::{Q, local_unif_to_std, realize_gram, ConfigSubspace, PartialMatrix}; // the types of the keys we use to access an assembly's elements and regulators pub type ElementKey = usize; @@ -111,21 +111,38 @@ impl Element { } } -// `set_point_spec` must always be a valid specification of `set_point` +// `set_point_spec` is always a valid specification of `set_point` +// ┌────────────┬─────────────────────────────────────────────────────┐ +// │`set_point` │ `set_point_spec` │ +// ┝━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┥ +// │`Some(x)` │ a string that parses to the floating-point value `x`│ +// ├────────────┼─────────────────────────────────────────────────────┤ +// │`None` │ the empty string │ +// └────────────┴─────────────────────────────────────────────────────┘ #[derive(Clone, Copy)] pub struct Regulator { pub subjects: (ElementKey, ElementKey), pub measurement: ReadSignal, - pub set_point: Signal>, - pub set_point_spec: Signal + pub set_point: ReadSignal>, + + set_point_writable: Signal>, + set_point_spec: Signal } impl Regulator { + pub fn get_set_point_spec_clone(&self) -> String { + self.set_point_spec.get_clone() + } + + pub fn get_set_point_spec_clone_untracked(&self) -> String { + self.set_point_spec.get_clone_untracked() + } + pub fn try_specify_set_point(&self, spec: String) -> bool { match spec.parse::() { Err(_) if !spec.is_empty() => false, set_pt => { - self.set_point.set(set_pt.ok()); + self.set_point_writable.set(set_pt.ok()); self.set_point_spec.set(spec); true } @@ -227,6 +244,54 @@ impl Assembly { subject_regulators.1.update(|regs| regs.insert(key)); } + pub fn insert_new_regulator(self, subjects: (ElementKey, ElementKey)) { + // create and insert a new regulator + let measurement = self.elements.map( + move |elts| { + let reps = ( + elts[subjects.0].representation.get_clone(), + elts[subjects.1].representation.get_clone() + ); + reps.0.dot(&(&*Q * reps.1)) + } + ); + let set_point_writable = create_signal(None); + let set_point = set_point_writable.split().0; + self.insert_regulator(Regulator { + subjects: subjects, + measurement: measurement, + set_point: set_point, + set_point_writable: set_point_writable, + set_point_spec: create_signal(String::new()) + }); + + /* DEBUG */ + // print updated regulator list + console::log_1(&JsValue::from("Regulators:")); + self.regulators.with(|regs| { + for (_, reg) in regs.into_iter() { + console::log_5( + &JsValue::from(" "), + &JsValue::from(reg.subjects.0), + &JsValue::from(reg.subjects.1), + &JsValue::from(":"), + &JsValue::from(reg.set_point.get_untracked()) + ); + } + }); + + // update the realization when the regulator becomes a constraint, or is + // edited while acting as a constraint + create_effect(move || { + console::log_1(&JsValue::from( + format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) + )); + if set_point.with(|set_pt| set_pt.is_some()) { + self.realize(); + } + }); + } + // --- realization --- pub fn realize(&self) { diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index a48377b..4898aba 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -20,14 +20,14 @@ use crate::{ #[component(inline_props)] fn RegulatorInput(regulator: Regulator) -> View { let valid = create_signal(true); - let value = create_signal(regulator.set_point_spec.get_clone_untracked()); + let value = create_signal(regulator.get_set_point_spec_clone_untracked()); // this closure resets the input value to the regulator's set point // specification, which is always a valid specification let reset_value = move || { batch(|| { valid.set(true); - value.set(regulator.set_point_spec.get_clone()); + value.set(regulator.get_set_point_spec_clone()); }) }; From c54b6bc16599124f08686d52b3193422f140e2c9 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 18 Feb 2025 13:37:30 -0800 Subject: [PATCH 12/41] Tie invalidity indicator to regulator input Invalid attempts to specify a regulator's set point are now local to each view of the regulator, and don't affect the regulator model. In particular, a regulator will be valid and in force even when one of its regulator input views is showing an invalid specification attempt. The invalidity indicator should therefore be tied to the input showing the invalid specification, not to the whole regulator outline item. --- app-proto/main.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-proto/main.css b/app-proto/main.css index 7ba1206..4726a27 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -163,7 +163,7 @@ details[open]:has(li) .element-switch::after { font-style: normal; } -.regulator:has(.invalid) > .status::after, details:has(.invalid):not([open]) .status::after { +.regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); } From befadd25c9d35c6a9e782d9ada981002ebc91fc4 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 19 Feb 2025 01:22:42 -0800 Subject: [PATCH 13/41] Get the read-only set point signal more simply --- app-proto/src/assembly.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 21421b6..8403406 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -256,7 +256,7 @@ impl Assembly { } ); let set_point_writable = create_signal(None); - let set_point = set_point_writable.split().0; + let set_point = *set_point_writable; self.insert_regulator(Regulator { subjects: subjects, measurement: measurement, From 6c31a25822ca9d5b8dc3a50dc7ffac4e94f19afc Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 23 Feb 2025 23:51:24 -0800 Subject: [PATCH 14/41] Consolidate set point data --- app-proto/src/assembly.rs | 127 +++++++++++++++++++++++++------------- app-proto/src/outline.rs | 23 ++++--- 2 files changed, 97 insertions(+), 53 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 8403406..2ccfc69 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,7 +1,11 @@ use nalgebra::{DMatrix, DVector, DVectorView, Vector3}; use rustc_hash::FxHashMap; use slab::Slab; -use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; +use std::{ + collections::BTreeSet, + num::ParseFloatError, + sync::atomic::{AtomicU64, Ordering} +}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ @@ -111,41 +115,75 @@ impl Element { } } -// `set_point_spec` is always a valid specification of `set_point` -// ┌────────────┬─────────────────────────────────────────────────────┐ -// │`set_point` │ `set_point_spec` │ -// ┝━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┥ -// │`Some(x)` │ a string that parses to the floating-point value `x`│ -// ├────────────┼─────────────────────────────────────────────────────┤ -// │`None` │ the empty string │ -// └────────────┴─────────────────────────────────────────────────────┘ +// to construct a `SpecifiedValue` that might be `Present`, use the associated +// function `try_from`. this ensures that `spec` is always a valid specification +// of `value` according to the format discussed above the implementation of +// `TryFrom` +pub enum SpecifiedValue { + Absent, + Present { + spec: String, + value: f64 + } +} + +use SpecifiedValue::*; + +impl SpecifiedValue { + // get the specification for this value. the associated function `try_from` + // is essentially a left inverse of this method: + // + // SpecifiedValue::try_from(x.spec()) == Ok(x) + // + pub fn spec(&self) -> String { + match self { + Absent => String::new(), + Present { spec, .. } => spec.clone() + } + } + + fn is_present(&self) -> bool { + match self { + Absent => false, + Present { .. } => true + } + } +} + +// we can try to turn a specification string into a `SpecifiedValue`. if the +// specification is empty, the `SpecifiedValue` is `Absent`. if the +// specification parses to a floating-point value `x`, the `SpecifiedValue` is +// `Present`, with a `value` of `x`, and the specification is stored in `spec`. +// these are the only valid specifications; any other produces an error +impl TryFrom for SpecifiedValue { + type Error = ParseFloatError; + + fn try_from(spec: String) -> Result { + if spec.is_empty() { + Ok(Absent) + } else { + spec.parse::().map( + |value| Present { spec: spec, value: value } + ) + } + } +} + #[derive(Clone, Copy)] pub struct Regulator { pub subjects: (ElementKey, ElementKey), pub measurement: ReadSignal, - pub set_point: ReadSignal>, - - set_point_writable: Signal>, - set_point_spec: Signal + pub set_point: Signal } impl Regulator { - pub fn get_set_point_spec_clone(&self) -> String { - self.set_point_spec.get_clone() - } - - pub fn get_set_point_spec_clone_untracked(&self) -> String { - self.set_point_spec.get_clone_untracked() - } - - pub fn try_specify_set_point(&self, spec: String) -> bool { - match spec.parse::() { - Err(_) if !spec.is_empty() => false, - set_pt => { - self.set_point_writable.set(set_pt.ok()); - self.set_point_spec.set(spec); + pub fn try_set(&self, set_pt_spec: String) -> bool { + match SpecifiedValue::try_from(set_pt_spec) { + Ok(set_pt) => { + self.set_point.set(set_pt); true } + Err(_) => false, } } } @@ -255,18 +293,15 @@ impl Assembly { reps.0.dot(&(&*Q * reps.1)) } ); - let set_point_writable = create_signal(None); - let set_point = *set_point_writable; + let set_point = create_signal(Absent); self.insert_regulator(Regulator { subjects: subjects, measurement: measurement, - set_point: set_point, - set_point_writable: set_point_writable, - set_point_spec: create_signal(String::new()) + set_point: set_point }); /* DEBUG */ - // print updated regulator list + // print an updated list of regulators console::log_1(&JsValue::from("Regulators:")); self.regulators.with(|regs| { for (_, reg) in regs.into_iter() { @@ -275,7 +310,9 @@ impl Assembly { &JsValue::from(reg.subjects.0), &JsValue::from(reg.subjects.1), &JsValue::from(":"), - &JsValue::from(reg.set_point.get_untracked()) + &JsValue::from(reg.set_point.with_untracked( + |set_pt| set_pt.spec() + )) ); } }); @@ -286,7 +323,7 @@ impl Assembly { console::log_1(&JsValue::from( format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) )); - if set_point.with(|set_pt| set_pt.is_some()) { + if set_point.with(|set_pt| set_pt.is_present()) { self.realize(); } }); @@ -308,15 +345,17 @@ impl Assembly { let mut gram_to_be = PartialMatrix::new(); self.regulators.with_untracked(|regs| { for (_, reg) in regs { - match reg.set_point.get_untracked() { - Some(set_pt) => { - let subjects = reg.subjects; - let row = elts[subjects.0].column_index.unwrap(); - let col = elts[subjects.1].column_index.unwrap(); - gram_to_be.push_sym(row, col, set_pt); - }, - None => () - } + reg.set_point.with_untracked(|set_pt| { + match set_pt { + Absent => (), + Present { value, .. } => { + let subjects = reg.subjects; + let row = elts[subjects.0].column_index.unwrap(); + let col = elts[subjects.1].column_index.unwrap(); + gram_to_be.push_sym(row, col, *value); + } + }; + }); } }); diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 4898aba..04bda9a 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -10,9 +10,10 @@ use crate::{ AppState, assembly, assembly::{ + ElementKey, Regulator, RegulatorKey, - ElementKey + SpecifiedValue::* } }; @@ -20,14 +21,16 @@ use crate::{ #[component(inline_props)] fn RegulatorInput(regulator: Regulator) -> View { let valid = create_signal(true); - let value = create_signal(regulator.get_set_point_spec_clone_untracked()); + let value = create_signal( + regulator.set_point.with_untracked(|set_pt| set_pt.spec()) + ); // this closure resets the input value to the regulator's set point - // specification, which is always a valid specification + // specification let reset_value = move || { batch(|| { valid.set(true); - value.set(regulator.get_set_point_spec_clone()); + value.set(regulator.set_point.with(|set_pt| set_pt.spec())); }) }; @@ -40,10 +43,12 @@ fn RegulatorInput(regulator: Regulator) -> View { r#type="text", class=move || { if valid.get() { - match regulator.set_point.get() { - Some(_) => "regulator-input constraint", - None => "regulator-input" - } + regulator.set_point.with(|set_pt| { + match set_pt { + Absent => "regulator-input", + Present { .. } => "regulator-input constraint" + } + }) } else { "regulator-input invalid" } @@ -51,7 +56,7 @@ fn RegulatorInput(regulator: Regulator) -> View { placeholder=regulator.measurement.with(|result| result.to_string()), bind:value=value, on:change=move |_| valid.set( - regulator.try_specify_set_point(value.get_clone_untracked()) + regulator.try_set(value.get_clone_untracked()) ), on:keydown={ move |event: KeyboardEvent| { From c368a38803a735ff915e92609de28732f2e0348d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 26 Feb 2025 22:22:58 -0800 Subject: [PATCH 15/41] Make `insert_regulator` private --- app-proto/src/assembly.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 2ccfc69..739dfc1 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -272,7 +272,7 @@ impl Assembly { ); } - pub fn insert_regulator(&self, regulator: Regulator) { + fn insert_regulator(&self, regulator: Regulator) { let subjects = regulator.subjects; let key = self.regulators.update(|regs| regs.insert(regulator)); let subject_regulators = self.elements.with( From 7cbd92618b867fcf6d52cb755d454a492768c349 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 27 Feb 2025 00:26:54 -0800 Subject: [PATCH 16/41] Improve the comments in the `assembly` module Also, remove trailing comma. --- app-proto/src/assembly.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 739dfc1..50b0783 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -30,8 +30,11 @@ pub struct Element { pub label: String, pub color: ElementColor, pub representation: Signal>, + + // the regulators that affect this element. the ambient assembly is + // responsible for keeping this set up to date pub regulators: Signal>, - + // a serial number, assigned by `Element::new`, that uniquely identifies // each element pub serial: u64, @@ -117,7 +120,7 @@ impl Element { // to construct a `SpecifiedValue` that might be `Present`, use the associated // function `try_from`. this ensures that `spec` is always a valid specification -// of `value` according to the format discussed above the implementation of +// of `value` according to the format discussed at the implementation of // `TryFrom` pub enum SpecifiedValue { Absent, @@ -154,7 +157,8 @@ impl SpecifiedValue { // specification is empty, the `SpecifiedValue` is `Absent`. if the // specification parses to a floating-point value `x`, the `SpecifiedValue` is // `Present`, with a `value` of `x`, and the specification is stored in `spec`. -// these are the only valid specifications; any other produces an error +// these are currently the only valid specifications; any other produces an +// error impl TryFrom for SpecifiedValue { type Error = ParseFloatError; @@ -183,7 +187,7 @@ impl Regulator { self.set_point.set(set_pt); true } - Err(_) => false, + Err(_) => false } } } From 874c823dbef83542228783361789c82740a205b0 Mon Sep 17 00:00:00 2001 From: glen Date: Sat, 1 Mar 2025 01:27:45 +0000 Subject: [PATCH 17/41] Minor wording changes in description of Element.regulators. --- app-proto/src/assembly.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 50b0783..d30a371 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -31,8 +31,8 @@ pub struct Element { pub color: ElementColor, pub representation: Signal>, - // the regulators that affect this element. the ambient assembly is - // responsible for keeping this set up to date + // All regulators with this element as a subject. The assembly owning + // this element is responsible for keeping this set up to date. pub regulators: Signal>, // a serial number, assigned by `Element::new`, that uniquely identifies From 894931a6e77815b5f2aabd6ba8bb1055d4e9ba52 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 3 Mar 2025 11:43:43 -0800 Subject: [PATCH 18/41] Replace `try_set` with `set_if_ok` --- app-proto/src/assembly.rs | 12 +++++------- app-proto/src/outline.rs | 9 ++++++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index d30a371..9b123f5 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -181,13 +181,11 @@ pub struct Regulator { } impl Regulator { - pub fn try_set(&self, set_pt_spec: String) -> bool { - match SpecifiedValue::try_from(set_pt_spec) { - Ok(set_pt) => { - self.set_point.set(set_pt); - true - } - Err(_) => false + /* TO DO */ + // if it's called for, add a `set` method that takes a bare SpecifiedValue + pub fn set_if_ok(&self, set_pt_result: Result) { + if let Ok(set_pt) = set_pt_result { + self.set_point.set(set_pt); } } } diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 04bda9a..8f81a82 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -13,6 +13,7 @@ use crate::{ ElementKey, Regulator, RegulatorKey, + SpecifiedValue, SpecifiedValue::* } }; @@ -55,9 +56,11 @@ fn RegulatorInput(regulator: Regulator) -> View { }, placeholder=regulator.measurement.with(|result| result.to_string()), bind:value=value, - on:change=move |_| valid.set( - regulator.try_set(value.get_clone_untracked()) - ), + on:change=move |_| { + let set_pt_result = SpecifiedValue::try_from(value.get_clone_untracked()); + valid.set(set_pt_result.is_ok()); + regulator.set_if_ok(set_pt_result); + }, on:keydown={ move |event: KeyboardEvent| { match event.key().as_str() { From 309b0881dfdc63e73b10db0641b272b2350b6cb3 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 3 Mar 2025 12:22:43 -0800 Subject: [PATCH 19/41] Move `SpecifiedValue` into its own module --- app-proto/src/assembly.rs | 66 +++----------------------------------- app-proto/src/main.rs | 1 + app-proto/src/outline.rs | 7 ++-- app-proto/src/specified.rs | 56 ++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 65 deletions(-) create mode 100644 app-proto/src/specified.rs diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 9b123f5..01dee9d 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,15 +1,14 @@ use nalgebra::{DMatrix, DVector, DVectorView, Vector3}; use rustc_hash::FxHashMap; use slab::Slab; -use std::{ - collections::BTreeSet, - num::ParseFloatError, - sync::atomic::{AtomicU64, Ordering} -}; +use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ -use crate::engine::{Q, local_unif_to_std, realize_gram, ConfigSubspace, PartialMatrix}; +use crate::{ + engine::{Q, local_unif_to_std, realize_gram, ConfigSubspace, PartialMatrix}, + specified::{SpecifiedValue, SpecifiedValue::{Absent, Present}} +}; // the types of the keys we use to access an assembly's elements and regulators pub type ElementKey = usize; @@ -118,61 +117,6 @@ impl Element { } } -// to construct a `SpecifiedValue` that might be `Present`, use the associated -// function `try_from`. this ensures that `spec` is always a valid specification -// of `value` according to the format discussed at the implementation of -// `TryFrom` -pub enum SpecifiedValue { - Absent, - Present { - spec: String, - value: f64 - } -} - -use SpecifiedValue::*; - -impl SpecifiedValue { - // get the specification for this value. the associated function `try_from` - // is essentially a left inverse of this method: - // - // SpecifiedValue::try_from(x.spec()) == Ok(x) - // - pub fn spec(&self) -> String { - match self { - Absent => String::new(), - Present { spec, .. } => spec.clone() - } - } - - fn is_present(&self) -> bool { - match self { - Absent => false, - Present { .. } => true - } - } -} - -// we can try to turn a specification string into a `SpecifiedValue`. if the -// specification is empty, the `SpecifiedValue` is `Absent`. if the -// specification parses to a floating-point value `x`, the `SpecifiedValue` is -// `Present`, with a `value` of `x`, and the specification is stored in `spec`. -// these are currently the only valid specifications; any other produces an -// error -impl TryFrom for SpecifiedValue { - type Error = ParseFloatError; - - fn try_from(spec: String) -> Result { - if spec.is_empty() { - Ok(Absent) - } else { - spec.parse::().map( - |value| Present { spec: spec, value: value } - ) - } - } -} - #[derive(Clone, Copy)] pub struct Regulator { pub subjects: (ElementKey, ElementKey), diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index f961504..6ab3e49 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -3,6 +3,7 @@ mod assembly; mod display; mod engine; mod outline; +mod specified; use rustc_hash::FxHashSet; use sycamore::prelude::*; diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 8f81a82..31837bb 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -12,10 +12,9 @@ use crate::{ assembly::{ ElementKey, Regulator, - RegulatorKey, - SpecifiedValue, - SpecifiedValue::* - } + RegulatorKey + }, + specified::{SpecifiedValue, SpecifiedValue::{Absent, Present}} }; // an editable view of a regulator diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs new file mode 100644 index 0000000..ea873e9 --- /dev/null +++ b/app-proto/src/specified.rs @@ -0,0 +1,56 @@ +use std::num::ParseFloatError; + +// to construct a `SpecifiedValue` that might be `Present`, use the associated +// function `try_from`. this ensures that `spec` is always a valid specification +// of `value` according to the format discussed at the implementation of +// `TryFrom` +pub enum SpecifiedValue { + Absent, + Present { + spec: String, + value: f64 + } +} + +use SpecifiedValue::{Absent, Present}; + +impl SpecifiedValue { + // get the specification for this value. the associated function `try_from` + // is essentially a left inverse of this method: + // + // SpecifiedValue::try_from(x.spec()) == Ok(x) + // + pub fn spec(&self) -> String { + match self { + Absent => String::new(), + Present { spec, .. } => spec.clone() + } + } + + pub fn is_present(&self) -> bool { + match self { + Absent => false, + Present { .. } => true + } + } +} + +// we can try to turn a specification string into a `SpecifiedValue`. if the +// specification is empty, the `SpecifiedValue` is `Absent`. if the +// specification parses to a floating-point value `x`, the `SpecifiedValue` is +// `Present`, with a `value` of `x`, and the specification is stored in `spec`. +// these are currently the only valid specifications; any other produces an +// error +impl TryFrom for SpecifiedValue { + type Error = ParseFloatError; + + fn try_from(spec: String) -> Result { + if spec.is_empty() { + Ok(Absent) + } else { + spec.parse::().map( + |value| Present { spec: spec, value: value } + ) + } + } +} \ No newline at end of file From c58fed073db9748d4c1e4d81fdb757286c7e6a25 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 3 Mar 2025 12:31:30 -0800 Subject: [PATCH 20/41] Drop the `is_present` utility method --- app-proto/src/assembly.rs | 2 +- app-proto/src/specified.rs | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 01dee9d..002b420 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -269,7 +269,7 @@ impl Assembly { console::log_1(&JsValue::from( format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) )); - if set_point.with(|set_pt| set_pt.is_present()) { + if set_point.with(|set_pt| matches!(set_pt, Present { .. })) { self.realize(); } }); diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs index ea873e9..a943f2b 100644 --- a/app-proto/src/specified.rs +++ b/app-proto/src/specified.rs @@ -26,13 +26,6 @@ impl SpecifiedValue { Present { spec, .. } => spec.clone() } } - - pub fn is_present(&self) -> bool { - match self { - Absent => false, - Present { .. } => true - } - } } // we can try to turn a specification string into a `SpecifiedValue`. if the From 8b4a72c60c13955a8680a50c995cb288d36bf722 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 3 Mar 2025 15:15:30 -0800 Subject: [PATCH 21/41] Simplify `match` to `if let` --- app-proto/src/assembly.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 002b420..f9b4620 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -292,15 +292,12 @@ impl Assembly { self.regulators.with_untracked(|regs| { for (_, reg) in regs { reg.set_point.with_untracked(|set_pt| { - match set_pt { - Absent => (), - Present { value, .. } => { - let subjects = reg.subjects; - let row = elts[subjects.0].column_index.unwrap(); - let col = elts[subjects.1].column_index.unwrap(); - gram_to_be.push_sym(row, col, *value); - } - }; + if let Present { value, .. } = set_pt { + let subjects = reg.subjects; + let row = elts[subjects.0].column_index.unwrap(); + let col = elts[subjects.1].column_index.unwrap(); + gram_to_be.push_sym(row, col, *value); + } }); } }); From 84bfdefccbc090c6e5a758671a94416986cb621a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 3 Mar 2025 23:10:28 -0800 Subject: [PATCH 22/41] Rewrite `SpecifiedValue` as a read-only structure --- app-proto/Cargo.toml | 1 + app-proto/src/assembly.rs | 16 +++++------ app-proto/src/outline.rs | 13 ++++----- app-proto/src/specified.rs | 55 +++++++++++++++++--------------------- 4 files changed, 41 insertions(+), 44 deletions(-) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index c11fef4..8000327 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -13,6 +13,7 @@ itertools = "0.13.0" js-sys = "0.3.70" lazy_static = "1.5.0" nalgebra = "0.33.0" +readonly = "0.2.12" rustc-hash = "2.0.0" slab = "0.4.9" sycamore = "0.9.0-beta.3" diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index f9b4620..0881741 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -7,7 +7,7 @@ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::{ engine::{Q, local_unif_to_std, realize_gram, ConfigSubspace, PartialMatrix}, - specified::{SpecifiedValue, SpecifiedValue::{Absent, Present}} + specified::SpecifiedValue }; // the types of the keys we use to access an assembly's elements and regulators @@ -239,7 +239,7 @@ impl Assembly { reps.0.dot(&(&*Q * reps.1)) } ); - let set_point = create_signal(Absent); + let set_point = create_signal(SpecifiedValue::from_empty_spec()); self.insert_regulator(Regulator { subjects: subjects, measurement: measurement, @@ -256,9 +256,9 @@ impl Assembly { &JsValue::from(reg.subjects.0), &JsValue::from(reg.subjects.1), &JsValue::from(":"), - &JsValue::from(reg.set_point.with_untracked( - |set_pt| set_pt.spec() - )) + ®.set_point.with_untracked( + |set_pt| JsValue::from(set_pt.spec.as_str()) + ) ); } }); @@ -269,7 +269,7 @@ impl Assembly { console::log_1(&JsValue::from( format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) )); - if set_point.with(|set_pt| matches!(set_pt, Present { .. })) { + if set_point.with(|set_pt| set_pt.is_present()) { self.realize(); } }); @@ -292,11 +292,11 @@ impl Assembly { self.regulators.with_untracked(|regs| { for (_, reg) in regs { reg.set_point.with_untracked(|set_pt| { - if let Present { value, .. } = set_pt { + if let Some(val) = set_pt.value { let subjects = reg.subjects; let row = elts[subjects.0].column_index.unwrap(); let col = elts[subjects.1].column_index.unwrap(); - gram_to_be.push_sym(row, col, *value); + gram_to_be.push_sym(row, col, val); } }); } diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 31837bb..bc920f3 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -14,7 +14,7 @@ use crate::{ Regulator, RegulatorKey }, - specified::{SpecifiedValue, SpecifiedValue::{Absent, Present}} + specified::SpecifiedValue }; // an editable view of a regulator @@ -22,7 +22,7 @@ use crate::{ fn RegulatorInput(regulator: Regulator) -> View { let valid = create_signal(true); let value = create_signal( - regulator.set_point.with_untracked(|set_pt| set_pt.spec()) + regulator.set_point.with_untracked(|set_pt| set_pt.spec.clone()) ); // this closure resets the input value to the regulator's set point @@ -30,7 +30,7 @@ fn RegulatorInput(regulator: Regulator) -> View { let reset_value = move || { batch(|| { valid.set(true); - value.set(regulator.set_point.with(|set_pt| set_pt.spec())); + value.set(regulator.set_point.with(|set_pt| set_pt.spec.clone())); }) }; @@ -44,9 +44,10 @@ fn RegulatorInput(regulator: Regulator) -> View { class=move || { if valid.get() { regulator.set_point.with(|set_pt| { - match set_pt { - Absent => "regulator-input", - Present { .. } => "regulator-input constraint" + if set_pt.is_present() { + "regulator-input constraint" + } else { + "regulator-input" } }) } else { diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs index a943f2b..0422291 100644 --- a/app-proto/src/specified.rs +++ b/app-proto/src/specified.rs @@ -1,48 +1,43 @@ use std::num::ParseFloatError; -// to construct a `SpecifiedValue` that might be `Present`, use the associated -// function `try_from`. this ensures that `spec` is always a valid specification -// of `value` according to the format discussed at the implementation of -// `TryFrom` -pub enum SpecifiedValue { - Absent, - Present { - spec: String, - value: f64 - } +// a real number described by a specification string. since the structure is +// read-only, we can guarantee that `spec` always specifies `value` in the +// following format +// ┌──────────────────────────────────────────────────────┬───────────┐ +// │ `spec` │ `value` │ +// ┝━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━┥ +// │ a string that parses to the floating-point value `x` │ `Some(x)` │ +// ├──────────────────────────────────────────────────────┼───────────┤ +// │ the empty string │ `None` │ +// └──────────────────────────────────────────────────────┴───────────┘ +#[readonly::make] +pub struct SpecifiedValue { + pub spec: String, + pub value: Option } -use SpecifiedValue::{Absent, Present}; - impl SpecifiedValue { - // get the specification for this value. the associated function `try_from` - // is essentially a left inverse of this method: - // - // SpecifiedValue::try_from(x.spec()) == Ok(x) - // - pub fn spec(&self) -> String { - match self { - Absent => String::new(), - Present { spec, .. } => spec.clone() - } + pub fn from_empty_spec() -> SpecifiedValue { + SpecifiedValue { spec: String::new(), value: None } + } + + pub fn is_present(&self) -> bool { + matches!(self.value, Some(_)) } } -// we can try to turn a specification string into a `SpecifiedValue`. if the -// specification is empty, the `SpecifiedValue` is `Absent`. if the -// specification parses to a floating-point value `x`, the `SpecifiedValue` is -// `Present`, with a `value` of `x`, and the specification is stored in `spec`. -// these are currently the only valid specifications; any other produces an -// error +// a `SpecifiedValue` can be constructed from a specification string, formatted +// as described in the comment on the structure definition. the result is `Ok` +// if the specification is properly formatted, and `Error` if not impl TryFrom for SpecifiedValue { type Error = ParseFloatError; fn try_from(spec: String) -> Result { if spec.is_empty() { - Ok(Absent) + Ok(SpecifiedValue { spec: spec, value: None }) } else { spec.parse::().map( - |value| Present { spec: spec, value: value } + |value| SpecifiedValue { spec: spec, value: Some(value) } ) } } From 6eeeb1c6fddc2c4122ec2eaf9146a0c839bf7def Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 5 Mar 2025 15:05:00 -0800 Subject: [PATCH 23/41] Bring back `try_set` as inline code This essentially reverts commit 894931a and then inlines `try_set` in the one place where it's used. If we ever want to reuse that code, we'll factor it out again. --- app-proto/src/assembly.rs | 10 ---------- app-proto/src/outline.rs | 12 +++++++++--- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 0881741..18176df 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -124,16 +124,6 @@ pub struct Regulator { pub set_point: Signal } -impl Regulator { - /* TO DO */ - // if it's called for, add a `set` method that takes a bare SpecifiedValue - pub fn set_if_ok(&self, set_pt_result: Result) { - if let Ok(set_pt) = set_pt_result { - self.set_point.set(set_pt); - } - } -} - // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub key: ElementKey, diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index bc920f3..8e40140 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -57,9 +57,15 @@ fn RegulatorInput(regulator: Regulator) -> View { placeholder=regulator.measurement.with(|result| result.to_string()), bind:value=value, on:change=move |_| { - let set_pt_result = SpecifiedValue::try_from(value.get_clone_untracked()); - valid.set(set_pt_result.is_ok()); - regulator.set_if_ok(set_pt_result); + valid.set( + match SpecifiedValue::try_from(value.get_clone_untracked()) { + Ok(set_pt) => { + regulator.set_point.set(set_pt); + true + } + Err(_) => false + } + ) }, on:keydown={ move |event: KeyboardEvent| { From b9db7a569987cf9333f588fcdff2759d62da5bcc Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 8 Mar 2025 15:02:57 -0800 Subject: [PATCH 24/41] Make `use` declarations more compact --- app-proto/src/add_remove.rs | 5 +---- app-proto/src/outline.rs | 6 +----- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index cdc35e6..5fed411 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -4,10 +4,7 @@ use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ engine, AppState, - assembly::{ - Assembly, - Element - } + assembly::{Assembly, Element} }; /* DEBUG */ diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 8e40140..002baea 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -9,11 +9,7 @@ use web_sys::{ use crate::{ AppState, assembly, - assembly::{ - ElementKey, - Regulator, - RegulatorKey - }, + assembly::{ElementKey, Regulator, RegulatorKey}, specified::SpecifiedValue }; From 08ec838334a5fc1bfe7961ba7047c3b60c8e70fb Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 8 Mar 2025 15:11:37 -0800 Subject: [PATCH 25/41] Consolidate constructions from empty specification To reduce the potential for inconsistency, we should only have one piece of code that constructs a `SpecifiedValue` from the empty specification. --- app-proto/src/specified.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs index 0422291..cfe7fc3 100644 --- a/app-proto/src/specified.rs +++ b/app-proto/src/specified.rs @@ -34,7 +34,7 @@ impl TryFrom for SpecifiedValue { fn try_from(spec: String) -> Result { if spec.is_empty() { - Ok(SpecifiedValue { spec: spec, value: None }) + Ok(SpecifiedValue::from_empty_spec()) } else { spec.parse::().map( |value| SpecifiedValue { spec: spec, value: Some(value) } From da28bc99d2f46a2cc3141d05c8fe1a57e336fc71 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 10 Mar 2025 23:43:24 +0000 Subject: [PATCH 26/41] Generalize constraints to observables (#48) Unifies the interface elements for measuring and constraining real-valued observables, as proposed in issue #47. The resulting combination is called a "Regulator," at least in the code. They are presented as text inputs in the table view. When a Regulatore is in measurement mode (has no "set point"), the text field displays its value. Entering a desired value into the text field creates a set point, and then the Regulator acts to (attempt to) constrain the value. Setting the desired value to the empty string switches the observable back to measurement mode. If you enter a desired value that can't be parsed as a floating point number, the regulator input is flagged as invalid and it has no effect on the state of the regulator. The set point can in this case be restored to its previous value (or to no set point if that was its prior state) by pressing the "Esc" key. Co-authored-by: Aaron Fenyes Co-authored-by: glen Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/48 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.toml | 1 + app-proto/main.css | 47 +++++++++----- app-proto/src/add_remove.rs | 44 ++----------- app-proto/src/assembly.rs | 115 ++++++++++++++++++++++++--------- app-proto/src/main.rs | 1 + app-proto/src/outline.rs | 123 ++++++++++++++++++++++++------------ app-proto/src/specified.rs | 44 +++++++++++++ 7 files changed, 249 insertions(+), 126 deletions(-) create mode 100644 app-proto/src/specified.rs diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index c11fef4..8000327 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -13,6 +13,7 @@ itertools = "0.13.0" js-sys = "0.3.70" lazy_static = "1.5.0" nalgebra = "0.33.0" +readonly = "0.2.12" rustc-hash = "2.0.0" slab = "0.4.9" sycamore = "0.9.0-beta.3" diff --git a/app-proto/main.css b/app-proto/main.css index b9fc0a1..4726a27 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -3,7 +3,8 @@ --text-bright: white; --text-invalid: #f58fc2; /* bright pink */ --border: #555; /* light gray */ - --border-focus: #aaa; /* bright gray */ + --border-focus-dark: #aaa; /* bright gray */ + --border-focus-light: white; --border-invalid: #70495c; /* dusky pink */ --selection-highlight: #444; /* medium gray */ --page-background: #222; /* dark gray */ @@ -23,7 +24,7 @@ body { display: flex; flex-direction: column; float: left; - width: 450px; + width: 500px; height: 100vh; margin: 0px; padding: 0px; @@ -77,12 +78,12 @@ summary.selected { background-color: var(--selection-highlight); } -summary > div, .constraint { +summary > div, .regulator { padding-top: 4px; padding-bottom: 4px; } -.element, .constraint { +.element, .regulator { display: flex; flex-grow: 1; padding-left: 8px; @@ -107,7 +108,7 @@ details[open]:has(li) .element-switch::after { flex-grow: 1; } -.constraint-label { +.regulator-label { flex-grow: 1; } @@ -123,26 +124,34 @@ details[open]:has(li) .element-switch::after { width: 56px; } -.constraint { +.regulator { font-style: italic; } -.constraint.invalid { - color: var(--text-invalid); +.regulator-type { + padding: 2px 8px 0px 8px; + font-size: 10pt; } -.constraint > input[type=checkbox] { - margin: 0px 8px 0px 0px; -} - -.constraint > input[type=text] { +.regulator-input { color: inherit; background-color: inherit; border: 1px solid var(--border); border-radius: 2px; } -.constraint.invalid > input[type=text] { +.regulator-input::placeholder { + color: inherit; + opacity: 54%; + font-style: italic; +} + +.regulator-input.constraint { + background-color: var(--display-background); +} + +.regulator-input.invalid { + color: var(--text-invalid); border-color: var(--border-invalid); } @@ -154,7 +163,7 @@ details[open]:has(li) .element-switch::after { font-style: normal; } -.invalid > .status::after, details:has(.invalid):not([open]) .status::after { +.regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); } @@ -171,5 +180,11 @@ canvas { } canvas:focus { - border-color: var(--border-focus); + border-color: var(--border-focus-dark); + outline: none; +} + +input:focus { + border-color: var(--border-focus-light); + outline: none; } \ No newline at end of file diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index ba02e65..5fed411 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -1,7 +1,11 @@ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; -use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}}; +use crate::{ + engine, + AppState, + assembly::{Assembly, Element} +}; /* DEBUG */ // load an example assembly for testing. this code will be removed once we've @@ -190,44 +194,8 @@ pub fn AddRemove() -> View { (subject_vec[0].clone(), subject_vec[1].clone()) } ); - let lorentz_prod = create_signal(0.0); - let lorentz_prod_valid = create_signal(false); - let active = create_signal(true); - state.assembly.insert_constraint(Constraint { - subjects: subjects, - lorentz_prod: lorentz_prod, - lorentz_prod_text: create_signal(String::new()), - lorentz_prod_valid: lorentz_prod_valid, - active: active, - }); + state.assembly.insert_new_regulator(subjects); state.selection.update(|sel| sel.clear()); - - /* DEBUG */ - // print updated constraint list - console::log_1(&JsValue::from("Constraints:")); - state.assembly.constraints.with(|csts| { - for (_, cst) in csts.into_iter() { - console::log_5( - &JsValue::from(" "), - &JsValue::from(cst.subjects.0), - &JsValue::from(cst.subjects.1), - &JsValue::from(":"), - &JsValue::from(cst.lorentz_prod.get_untracked()) - ); - } - }); - - // update the realization when the constraint becomes active - // and valid, or is edited while active and valid - create_effect(move || { - console::log_1(&JsValue::from( - format!("Constraint ({}, {}) updated", subjects.0, subjects.1) - )); - lorentz_prod.track(); - if active.get() && lorentz_prod_valid.get() { - state.assembly.realize(); - } - }); } ) { "🔗" } select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 7073c9e..18176df 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -5,11 +5,14 @@ use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ -use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix}; +use crate::{ + engine::{Q, local_unif_to_std, realize_gram, ConfigSubspace, PartialMatrix}, + specified::SpecifiedValue +}; -// the types of the keys we use to access an assembly's elements and constraints +// the types of the keys we use to access an assembly's elements and regulators pub type ElementKey = usize; -pub type ConstraintKey = usize; +pub type RegulatorKey = usize; pub type ElementColor = [f32; 3]; @@ -26,8 +29,11 @@ pub struct Element { pub label: String, pub color: ElementColor, pub representation: Signal>, - pub constraints: Signal>, - + + // All regulators with this element as a subject. The assembly owning + // this element is responsible for keeping this set up to date. + pub regulators: Signal>, + // a serial number, assigned by `Element::new`, that uniquely identifies // each element pub serial: u64, @@ -61,7 +67,7 @@ impl Element { label: label, color: color, representation: create_signal(representation), - constraints: create_signal(BTreeSet::default()), + regulators: create_signal(BTreeSet::default()), serial: serial, column_index: None } @@ -111,13 +117,11 @@ impl Element { } } -#[derive(Clone)] -pub struct Constraint { +#[derive(Clone, Copy)] +pub struct Regulator { pub subjects: (ElementKey, ElementKey), - pub lorentz_prod: Signal, - pub lorentz_prod_text: Signal, - pub lorentz_prod_valid: Signal, - pub active: Signal + pub measurement: ReadSignal, + pub set_point: Signal } // the velocity is expressed in uniform coordinates @@ -131,9 +135,9 @@ type AssemblyMotion<'a> = Vec>; // a complete, view-independent description of an assembly #[derive(Clone)] pub struct Assembly { - // elements and constraints + // elements and regulators pub elements: Signal>, - pub constraints: Signal>, + pub regulators: Signal>, // solution variety tangent space. the basis vectors are stored in // configuration matrix format, ordered according to the elements' column @@ -155,13 +159,13 @@ impl Assembly { pub fn new() -> Assembly { Assembly { elements: create_signal(Slab::new()), - constraints: create_signal(Slab::new()), + regulators: create_signal(Slab::new()), tangent: create_signal(ConfigSubspace::zero(0)), elements_by_id: create_signal(FxHashMap::default()) } } - // --- inserting elements and constraints --- + // --- inserting elements and regulators --- // insert an element into the assembly without checking whether we already // have an element with the same identifier. any element that does have the @@ -204,14 +208,61 @@ impl Assembly { ); } - pub fn insert_constraint(&self, constraint: Constraint) { - let subjects = constraint.subjects; - let key = self.constraints.update(|csts| csts.insert(constraint)); - let subject_constraints = self.elements.with( - |elts| (elts[subjects.0].constraints, elts[subjects.1].constraints) + fn insert_regulator(&self, regulator: Regulator) { + let subjects = regulator.subjects; + let key = self.regulators.update(|regs| regs.insert(regulator)); + let subject_regulators = self.elements.with( + |elts| (elts[subjects.0].regulators, elts[subjects.1].regulators) ); - subject_constraints.0.update(|csts| csts.insert(key)); - subject_constraints.1.update(|csts| csts.insert(key)); + subject_regulators.0.update(|regs| regs.insert(key)); + subject_regulators.1.update(|regs| regs.insert(key)); + } + + pub fn insert_new_regulator(self, subjects: (ElementKey, ElementKey)) { + // create and insert a new regulator + let measurement = self.elements.map( + move |elts| { + let reps = ( + elts[subjects.0].representation.get_clone(), + elts[subjects.1].representation.get_clone() + ); + reps.0.dot(&(&*Q * reps.1)) + } + ); + let set_point = create_signal(SpecifiedValue::from_empty_spec()); + self.insert_regulator(Regulator { + subjects: subjects, + measurement: measurement, + set_point: set_point + }); + + /* DEBUG */ + // print an updated list of regulators + console::log_1(&JsValue::from("Regulators:")); + self.regulators.with(|regs| { + for (_, reg) in regs.into_iter() { + console::log_5( + &JsValue::from(" "), + &JsValue::from(reg.subjects.0), + &JsValue::from(reg.subjects.1), + &JsValue::from(":"), + ®.set_point.with_untracked( + |set_pt| JsValue::from(set_pt.spec.as_str()) + ) + ); + } + }); + + // update the realization when the regulator becomes a constraint, or is + // edited while acting as a constraint + create_effect(move || { + console::log_1(&JsValue::from( + format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) + )); + if set_point.with(|set_pt| set_pt.is_present()) { + self.realize(); + } + }); } // --- realization --- @@ -228,14 +279,16 @@ impl Assembly { let (gram, guess) = self.elements.with_untracked(|elts| { // set up the off-diagonal part of the Gram matrix let mut gram_to_be = PartialMatrix::new(); - self.constraints.with_untracked(|csts| { - for (_, cst) in csts { - if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() { - let subjects = cst.subjects; - let row = elts[subjects.0].column_index.unwrap(); - let col = elts[subjects.1].column_index.unwrap(); - gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked()); - } + self.regulators.with_untracked(|regs| { + for (_, reg) in regs { + reg.set_point.with_untracked(|set_pt| { + if let Some(val) = set_pt.value { + let subjects = reg.subjects; + let row = elts[subjects.0].column_index.unwrap(); + let col = elts[subjects.1].column_index.unwrap(); + gram_to_be.push_sym(row, col, val); + } + }); } }); diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index f961504..6ab3e49 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -3,6 +3,7 @@ mod assembly; mod display; mod engine; mod outline; +mod specified; use rustc_hash::FxHashSet; use sycamore::prelude::*; diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index a6e968d..002baea 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,56 +1,97 @@ use itertools::Itertools; use sycamore::prelude::*; use web_sys::{ - Event, - HtmlInputElement, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast }; -use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}}; +use crate::{ + AppState, + assembly, + assembly::{ElementKey, Regulator, RegulatorKey}, + specified::SpecifiedValue +}; -// an editable view of the Lorentz product representing a constraint +// an editable view of a regulator #[component(inline_props)] -fn LorentzProductInput(constraint: Constraint) -> View { +fn RegulatorInput(regulator: Regulator) -> View { + let valid = create_signal(true); + let value = create_signal( + regulator.set_point.with_untracked(|set_pt| set_pt.spec.clone()) + ); + + // this closure resets the input value to the regulator's set point + // specification + let reset_value = move || { + batch(|| { + valid.set(true); + value.set(regulator.set_point.with(|set_pt| set_pt.spec.clone())); + }) + }; + + // reset the input value whenever the regulator's set point specification + // is updated + create_effect(reset_value); + view! { input( r#type="text", - bind:value=constraint.lorentz_prod_text, - on:change=move |event: Event| { - let target: HtmlInputElement = event.target().unwrap().unchecked_into(); - match target.value().parse::() { - Ok(lorentz_prod) => batch(|| { - constraint.lorentz_prod.set(lorentz_prod); - constraint.lorentz_prod_valid.set(true); - }), - Err(_) => constraint.lorentz_prod_valid.set(false) - }; + class=move || { + if valid.get() { + regulator.set_point.with(|set_pt| { + if set_pt.is_present() { + "regulator-input constraint" + } else { + "regulator-input" + } + }) + } else { + "regulator-input invalid" + } + }, + placeholder=regulator.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) => { + regulator.set_point.set(set_pt); + true + } + Err(_) => false + } + ) + }, + on:keydown={ + move |event: KeyboardEvent| { + match event.key().as_str() { + "Escape" => reset_value(), + _ => () + } + } } ) } } -// a list item that shows a constraint in an outline view of an element +// a list item that shows a regulator in an outline view of an element #[component(inline_props)] -fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) -> View { +fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View { let state = use_context::(); let assembly = &state.assembly; - let constraint = assembly.constraints.with(|csts| csts[constraint_key].clone()); - let other_subject = if constraint.subjects.0 == element_key { - constraint.subjects.1 + let regulator = assembly.regulators.with(|regs| regs[regulator_key]); + let other_subject = if regulator.subjects.0 == element_key { + regulator.subjects.1 } else { - constraint.subjects.0 + regulator.subjects.0 }; let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); - let class = constraint.lorentz_prod_valid.map( - |&lorentz_prod_valid| if lorentz_prod_valid { "constraint" } else { "constraint invalid" } - ); view! { - li(class=class.get()) { - input(r#type="checkbox", bind:checked=constraint.active) - div(class="constraint-label") { (other_subject_label) } - LorentzProductInput(constraint=constraint) + li(class="regulator") { + div(class="regulator-label") { (other_subject_label) } + div(class="regulator-type") { "Inversive distance" } + RegulatorInput(regulator=regulator) div(class="status") } } @@ -74,9 +115,9 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { ).collect::>() ) }; - let constrained = element.constraints.map(|csts| csts.len() > 0); - let constraint_list = element.constraints.map( - |csts| csts.clone().into_iter().collect() + let regulated = element.regulators.map(|regs| regs.len() > 0); + let regulator_list = element.regulators.map( + |regs| regs.clone().into_iter().collect() ); let details_node = create_node_ref(); view! { @@ -91,7 +132,7 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { state.select(key, event.shift_key()); event.prevent_default(); }, - "ArrowRight" if constrained.get() => { + "ArrowRight" if regulated.get() => { let _ = details_node .get() .unchecked_into::() @@ -138,16 +179,16 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { div(class="status") } } - ul(class="constraints") { + ul(class="regulators") { Keyed( - list=constraint_list, - view=move |cst_key| view! { - ConstraintOutlineItem( - constraint_key=cst_key, + list=regulator_list, + view=move |reg_key| view! { + RegulatorOutlineItem( + regulator_key=reg_key, element_key=key ) }, - key=|cst_key| cst_key.clone() + key=|reg_key| reg_key.clone() ) } } @@ -155,9 +196,9 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { } } -// a component that lists the elements of the current assembly, showing the -// constraints on each element as a collapsible sub-list. its implementation -// is based on Kate Morley's HTML + CSS tree views: +// a component that lists the elements of the current assembly, showing each +// element's regulators in a collapsible sub-list. its implementation is based +// on Kate Morley's HTML + CSS tree views: // // https://iamkate.com/code/tree-views/ // diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs new file mode 100644 index 0000000..cfe7fc3 --- /dev/null +++ b/app-proto/src/specified.rs @@ -0,0 +1,44 @@ +use std::num::ParseFloatError; + +// a real number described by a specification string. since the structure is +// read-only, we can guarantee that `spec` always specifies `value` in the +// following format +// ┌──────────────────────────────────────────────────────┬───────────┐ +// │ `spec` │ `value` │ +// ┝━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━┥ +// │ a string that parses to the floating-point value `x` │ `Some(x)` │ +// ├──────────────────────────────────────────────────────┼───────────┤ +// │ the empty string │ `None` │ +// └──────────────────────────────────────────────────────┴───────────┘ +#[readonly::make] +pub struct SpecifiedValue { + pub spec: String, + pub value: Option +} + +impl SpecifiedValue { + pub fn from_empty_spec() -> SpecifiedValue { + SpecifiedValue { spec: String::new(), value: None } + } + + pub fn is_present(&self) -> bool { + matches!(self.value, Some(_)) + } +} + +// a `SpecifiedValue` can be constructed from a specification string, formatted +// as described in the comment on the structure definition. the result is `Ok` +// if the specification is properly formatted, and `Error` if not +impl TryFrom for SpecifiedValue { + type Error = ParseFloatError; + + fn try_from(spec: String) -> Result { + if spec.is_empty() { + Ok(SpecifiedValue::from_empty_spec()) + } else { + spec.parse::().map( + |value| SpecifiedValue { spec: spec, value: Some(value) } + ) + } + } +} \ No newline at end of file From 2c4fd39c1fcb36c61f7828487185aa907912f591 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Wed, 12 Mar 2025 21:54:56 +0000 Subject: [PATCH 27/41] refactor: Tidy up engine tests (#72) ### `zero_loss_test` - Drop the redundant type hint in the definition of `a`. ### `tangent_test_three_spheres` - Get the dimension from the expected basis, rather than putting it in by hand. ### `tangent_test_kaleidocycle` - Factor out the realization code, in the same style as `realize_irisawa_hexlet`. - Rename the `irisawa` submodule to `examples`. ### `frozen_entry_test` - Move up into the section for simpler tests, between `zero_loss_test` and `irisawa_hexlet_test`. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/72 Reviewed-by: Glen Whitney Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/examples/irisawa-hexlet.rs | 2 +- app-proto/examples/kaleidocycle.rs | 52 +------ app-proto/src/engine.rs | 218 ++++++++++++++------------- 3 files changed, 121 insertions(+), 151 deletions(-) diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs index 2bc94c0..639a494 100644 --- a/app-proto/examples/irisawa-hexlet.rs +++ b/app-proto/examples/irisawa-hexlet.rs @@ -1,4 +1,4 @@ -use dyna3::engine::{Q, irisawa::realize_irisawa_hexlet}; +use dyna3::engine::{Q, examples::realize_irisawa_hexlet}; fn main() { const SCALED_TOL: f64 = 1.0e-12; diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs index 88116d3..2779ab1 100644 --- a/app-proto/examples/kaleidocycle.rs +++ b/app-proto/examples/kaleidocycle.rs @@ -1,53 +1,10 @@ use nalgebra::{DMatrix, DVector}; -use std::{array, f64::consts::PI}; -use dyna3::engine::{Q, point, realize_gram, PartialMatrix}; +use dyna3::engine::{Q, examples::realize_kaleidocycle}; fn main() { - // set up a kaleidocycle, made of points with fixed distances between them, - // and find its tangent space - const N_POINTS: usize = 12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for block in (0..N_POINTS).step_by(2) { - let block_next = (block + 2) % N_POINTS; - for j in 0..2 { - // diagonal and hinge edges - for k in j..2 { - gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); - } - - // non-hinge edges - for k in 0..2 { - gram_to_be.push_sym(block + j, block_next + k, -0.625); - } - } - } - gram_to_be - }; - let guess = { - const N_HINGES: usize = 6; - let guess_elts = (0..N_HINGES).step_by(2).flat_map( - |n| { - let ang_hor = (n as f64) * PI/3.0; - let ang_vert = ((n + 1) as f64) * PI/3.0; - let x_vert = ang_vert.cos(); - let y_vert = ang_vert.sin(); - [ - 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) - ] - } - ).collect::>(); - DMatrix::from_columns(&guess_elts) - }; - let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); - let (config, tangent, success, history) = realize_gram( - &gram, guess, &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 - ); + const SCALED_TOL: f64 = 1.0e-12; + let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config); print!("Configuration:{}", config); if success { @@ -58,7 +15,8 @@ fn main() { println!("Steps: {}", history.scaled_loss.len() - 1); println!("Loss: {}\n", history.scaled_loss.last().unwrap()); - // find the kaleidocycle's twist motion + // find the kaleidocycle's twist motion by projecting onto the tangent space + const N_POINTS: usize = 12; let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); let down = -&up; let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index eee19ac..35f898c 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -413,20 +413,20 @@ pub fn realize_gram( // --- tests --- -// this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article -// below includes a nice translation of the problem statement, which was -// recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and -// Present_) -// -// "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki -// https://www.nippon.com/en/japan-topics/c12801/ -// #[cfg(feature = "dev")] -pub mod irisawa { +pub mod examples { use std::{array, f64::consts::PI}; use super::*; + // this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article + // below includes a nice translation of the problem statement, which was + // recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and + // Present_) + // + // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki + // https://www.nippon.com/en/japan-topics/c12801/ + // pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { let gram = { let mut gram_to_be = PartialMatrix::new(); @@ -480,14 +480,64 @@ pub mod irisawa { scaled_tol, 0.5, 0.9, 1.1, 200, 110 ) } + + // set up a kaleidocycle, made of points with fixed distances between them, + // and find its tangent space + pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + const N_POINTS: usize = 12; + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for block in (0..N_POINTS).step_by(2) { + let block_next = (block + 2) % N_POINTS; + for j in 0..2 { + // diagonal and hinge edges + for k in j..2 { + gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); + } + + // non-hinge edges + for k in 0..2 { + gram_to_be.push_sym(block + j, block_next + k, -0.625); + } + } + } + gram_to_be + }; + + let guess = { + const N_HINGES: usize = 6; + let guess_elts = (0..N_HINGES).step_by(2).flat_map( + |n| { + let ang_hor = (n as f64) * PI/3.0; + let ang_vert = ((n + 1) as f64) * PI/3.0; + let x_vert = ang_vert.cos(); + let y_vert = ang_vert.sin(); + [ + 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) + ] + } + ).collect::>(); + DMatrix::from_columns(&guess_elts) + }; + + let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); + + realize_gram( + &gram, guess, &frozen, + scaled_tol, 0.5, 0.9, 1.1, 200, 110 + ) + } } #[cfg(test)] mod tests { use nalgebra::Vector3; - use std::{array, f64::consts::{FRAC_1_SQRT_2, PI}, iter}; + use std::{f64::consts::{FRAC_1_SQRT_2, PI}, iter}; - use super::{*, irisawa::realize_irisawa_hexlet}; + use super::{*, examples::*}; #[test] fn sub_proj_test() { @@ -523,7 +573,7 @@ mod tests { entries }); let config = { - let a: f64 = 0.75_f64.sqrt(); + let a = 0.75_f64.sqrt(); DMatrix::from_columns(&[ sphere(1.0, 0.0, 0.0, a), sphere(-0.5, a, 0.0, a), @@ -534,6 +584,40 @@ mod tests { assert!(state.loss.abs() < f64::EPSILON); } + // at the frozen indices, the optimization steps should have exact zeros, + // and the realized configuration should match the initial guess + #[test] + fn frozen_entry_test() { + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for j in 0..2 { + for k in j..2 { + gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + gram_to_be + }; + let guess = DMatrix::from_columns(&[ + point(0.0, 0.0, 2.0), + sphere(0.0, 0.0, 0.0, 1.0) + ]); + let frozen = [(3, 0), (3, 1)]; + println!(); + let (config, _, success, history) = realize_gram( + &gram, guess.clone(), &frozen, + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + assert_eq!(success, true); + for base_step in history.base_step.into_iter() { + for index in frozen { + assert_eq!(base_step[index], 0.0); + } + } + for index in frozen { + assert_eq!(config[index], guess[index]); + } + } + #[test] fn irisawa_hexlet_test() { // solve Irisawa's problem @@ -574,12 +658,8 @@ mod tests { assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); - // confirm that the tangent space has dimension five or less - assert_eq!(tangent.basis_std.len(), 5); - - // confirm that the tangent space contains all the motions we expect it - // to. since we've already bounded the dimension of the tangent space, - // this confirms that the tangent space is what we expect it to be + // list some motions that should form a basis for the tangent space of + // the solution variety const UNIFORM_DIM: usize = 4; let element_dim = guess.nrows(); let assembly_dim = guess.ncols(); @@ -605,6 +685,14 @@ mod tests { 0.0, 0.0, -1.0, 0.25, 1.0 ]) ]; + + // confirm that the dimension of the tangent space is no greater than + // expected + assert_eq!(tangent.basis_std.len(), tangent_motions_std.len()); + + // confirm that the tangent space contains all the motions we expect it + // to. since we've already bounded the dimension of the tangent space, + // this confirms that the tangent space is what we expect it to be let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL; for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) { let motion_proj: DMatrix<_> = motion_unif.column_iter().enumerate().map( @@ -633,59 +721,17 @@ mod tests { #[test] fn tangent_test_kaleidocycle() { - // set up a kaleidocycle, made of points with fixed distances between - // them, and find its tangent space - const N_POINTS: usize = 12; - const N_HINGES: usize = 6; + // set up a kaleidocycle and find its tangent space const SCALED_TOL: f64 = 1.0e-12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for block in (0..N_POINTS).step_by(2) { - let block_next = (block + 2) % N_POINTS; - for j in 0..2 { - // diagonal and hinge edges - for k in j..2 { - gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); - } - - // non-hinge edges - for k in 0..2 { - gram_to_be.push_sym(block + j, block_next + k, -0.625); - } - } - } - gram_to_be - }; - let guess = { - let guess_elts = (0..N_HINGES).step_by(2).flat_map( - |n| { - let ang_hor = (n as f64) * PI/3.0; - let ang_vert = ((n + 1) as f64) * PI/3.0; - let x_vert = ang_vert.cos(); - let y_vert = ang_vert.sin(); - [ - 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) - ] - } - ).collect::>(); - DMatrix::from_columns(&guess_elts) - }; - let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); - let (config, tangent, success, history) = realize_gram( - &gram, guess.clone(), &frozen, - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 - ); - assert_eq!(config, guess); + let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of // the solution variety - let element_dim = guess.nrows(); - let assembly_dim = guess.ncols(); + const N_HINGES: usize = 6; + let element_dim = config.nrows(); + let assembly_dim = config.ncols(); let tangent_motions_unif = vec![ // the translations along the coordinate axes translation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), assembly_dim), @@ -693,9 +739,9 @@ mod tests { translation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), assembly_dim), // the rotations about the coordinate axes - rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), guess.column_iter().collect()), - rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), guess.column_iter().collect()), - rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), guess.column_iter().collect()), + rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), config.column_iter().collect()), + rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), config.column_iter().collect()), + rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), config.column_iter().collect()), // the twist motion. more precisely: a motion that keeps the center // of mass stationary and preserves the distances between the @@ -720,7 +766,7 @@ mod tests { ]; let tangent_motions_std = tangent_motions_unif.iter().map( |motion| DMatrix::from_columns( - &guess.column_iter().zip(motion).map( + &config.column_iter().zip(motion).map( |(v, elt_motion)| local_unif_to_std(v) * elt_motion ).collect::>() ) @@ -826,38 +872,4 @@ mod tests { let tol_sq = ((guess_orig.nrows() * guess_orig.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM; assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq); } - - // at the frozen indices, the optimization steps should have exact zeros, - // and the realized configuration should match the initial guess - #[test] - fn frozen_entry_test() { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..2 { - for k in j..2 { - gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); - } - } - gram_to_be - }; - let guess = DMatrix::from_columns(&[ - point(0.0, 0.0, 2.0), - sphere(0.0, 0.0, 0.0, 1.0) - ]); - let frozen = [(3, 0), (3, 1)]; - println!(); - let (config, _, success, history) = realize_gram( - &gram, guess.clone(), &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 - ); - assert_eq!(success, true); - for base_step in history.base_step.into_iter() { - for index in frozen { - assert_eq!(base_step[index], 0.0); - } - } - for index in frozen { - assert_eq!(config[index], guess[index]); - } - } } \ No newline at end of file From b86f1761517c2b65a2ebabbd1d0d779ae59a1449 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Wed, 2 Apr 2025 20:31:42 +0000 Subject: [PATCH 28/41] feat: Continuous integration via Forgejo Actions/runners (#75) Adds a continuous integration workflow to the repository, using the [Forgejo Actions](https://forgejo.org/docs/next/user/actions/) framework. Concurrently, Aaron added a [wiki page](https://code.studioinfinity.org/glen/dyna3/wiki/Continuous-integration) to document the continuous integration system. In particular, this page explains how to [run continuous integration checks on a development machine](wiki/Continuous-integration#execution), either directly or in a container. Co-authored-by: Aaron Fenyes Co-authored-by: Glen Whitney Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/75 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- .forgejo/setup-trunk/action.yaml | 22 + .../workflows/continuous-integration.yaml | 29 + .gitignore | 8 +- app-proto/Cargo.lock | 788 ++++++++++++++++++ app-proto/Cargo.toml | 20 + app-proto/src/main.rs | 3 + app-proto/src/tests.rs | 14 + 7 files changed, 877 insertions(+), 7 deletions(-) create mode 100644 .forgejo/setup-trunk/action.yaml create mode 100644 .forgejo/workflows/continuous-integration.yaml create mode 100644 app-proto/Cargo.lock create mode 100644 app-proto/src/tests.rs diff --git a/.forgejo/setup-trunk/action.yaml b/.forgejo/setup-trunk/action.yaml new file mode 100644 index 0000000..6007527 --- /dev/null +++ b/.forgejo/setup-trunk/action.yaml @@ -0,0 +1,22 @@ +# set up the Trunk web build system +# +# https://trunkrs.dev +# +# the `curl` call is based on David Tolnay's `rust-toolchain` action +# +# https://github.com/dtolnay/rust-toolchain +# +runs: + using: "composite" + steps: + - run: rustup target add wasm32-unknown-unknown + + # install the Trunk binary to `ci-bin` within the workspace directory, which + # is determined by the `github.workspace` label and reflected in the + # `GITHUB_WORKSPACE` environment variable. then, make the `trunk` command + # available by placing the fully qualified path to `ci-bin` on the + # workflow's search path + - run: mkdir -p ci-bin + - run: curl --output - --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail 'https://github.com/trunk-rs/trunk/releases/download/v0.21.12/trunk-x86_64-unknown-linux-gnu.tar.gz' | tar --gunzip --extract --file - + working-directory: ci-bin + - run: echo "${{ github.workspace }}/ci-bin" >> $GITHUB_PATH diff --git a/.forgejo/workflows/continuous-integration.yaml b/.forgejo/workflows/continuous-integration.yaml new file mode 100644 index 0000000..daf8923 --- /dev/null +++ b/.forgejo/workflows/continuous-integration.yaml @@ -0,0 +1,29 @@ +on: + pull_request: + push: + branches: [main] +jobs: + # run the automated tests, reporting success if the tests pass and were built + # without warnings. the examples are run as tests, because we've configured + # each example target with `test = true` and `harness = false` in Cargo.toml. + # Trunk build failures caused by problems outside the Rust source code, like + # missing assets, should be caught by `trunk_build_test` + test: + runs-on: docker + container: + image: cimg/rust:1.85-node + defaults: + run: + # set the default working directory for each `run` step, relative to the + # workspace directory. this default only affects `run` steps (and if we + # tried to set the `working-directory` label for any other kind of step, + # it wouldn't be recognized anyway) + working-directory: app-proto + steps: + # Check out the repository so that its top-level directory is the + # workspace directory (action variable `github.workspace`, environment + # variable `$GITHUB_WORKSPACE`): + - uses: https://code.forgejo.org/actions/checkout@v4 + + - uses: ./.forgejo/setup-trunk + - run: RUSTFLAGS='-D warnings' cargo test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3e95fba..ba2944f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,2 @@ -node_modules -site -docbuild -__tests__ -coverage -dyna3.zip -tmpproj +ci-bin *~ diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock new file mode 100644 index 0000000..9738589 --- /dev/null +++ b/app-proto/Cargo.lock @@ -0,0 +1,788 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "dyna3" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "dyna3", + "itertools", + "js-sys", + "lazy_static", + "nalgebra", + "readonly", + "rustc-hash", + "slab", + "sycamore", + "wasm-bindgen-test", + "web-sys", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matrixmultiply" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "minicov" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "nalgebra" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c4b5f057b303842cf3262c27e465f4c303572e7f6b0648f60e16248ac3397f4" +dependencies = [ + "approx", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "readonly" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2a62d85ed81ca5305dc544bd42c8804c5060b78ffa5ad3c64b0fb6a8c13d062" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "safe_arch" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simba" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "sycamore" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dedaf7237c05913604a5b0b2536b613f6c8510c6b213d2583b1294869755cabd" +dependencies = [ + "hashbrown", + "indexmap", + "paste", + "sycamore-core", + "sycamore-macro", + "sycamore-reactive", + "sycamore-web", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "sycamore-core" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ddddc3d1bcb38c04ad55d2d1ab4f6a358e4daaeae0a0436892f1fade9fb31a" +dependencies = [ + "hashbrown", + "paste", + "sycamore-reactive", +] + +[[package]] +name = "sycamore-macro" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77181c27cb753e86065308901871ccc7456fb19527b6a4ffacad3b63175ed014" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "rand", + "sycamore-view-parser", + "syn", +] + +[[package]] +name = "sycamore-reactive" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa6870203507c07e850687c0ccf528eb0f04240e3596bac9137007ffb6c50b1" +dependencies = [ + "paste", + "slotmap", + "smallvec", +] + +[[package]] +name = "sycamore-view-parser" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6144640af2eafffc68a92f3aacbbfaa21f7fd31906e2336fe304fd100fe226b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sycamore-web" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca93dcf1b1830bf1aac93508ed51babcda92c1d32d96067ab416d94e4b7c475" +dependencies = [ + "html-escape", + "js-sys", + "once_cell", + "paste", + "smallvec", + "sycamore-core", + "sycamore-macro", + "sycamore-reactive", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "minicov", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wide" +version = "0.7.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b828f995bf1e9622031f8009f8481a85406ce1f4d4588ff746d872043e855690" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 8000327..5ab7299 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -50,3 +50,23 @@ wasm-bindgen-test = "0.3.34" [profile.release] opt-level = "s" # optimize for small code size debug = true # include debug symbols + +[[example]] +name = "irisawa-hexlet" +test = true +harness = false + +[[example]] +name = "kaleidocycle" +test = true +harness = false + +[[example]] +name = "point-on-sphere" +test = true +harness = false + +[[example]] +name = "three-spheres" +test = true +harness = false diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index 6ab3e49..e581997 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -5,6 +5,9 @@ mod engine; mod outline; mod specified; +#[cfg(test)] +mod tests; + use rustc_hash::FxHashSet; use sycamore::prelude::*; diff --git a/app-proto/src/tests.rs b/app-proto/src/tests.rs new file mode 100644 index 0000000..2c5436b --- /dev/null +++ b/app-proto/src/tests.rs @@ -0,0 +1,14 @@ +use std::process::Command; + +// build and bundle the application, reporting success if there are no errors or +// warnings. to see this test fail while others succeed, try moving `index.html` +// or one of the assets that it links to +#[test] +fn trunk_build_test() { + let build_status = Command::new("trunk") + .arg("build") + .env("RUSTFLAGS", "-D warnings") + .status() + .expect("Call to Trunk failed"); + assert!(build_status.success()); +} \ No newline at end of file From 23ba5acad7e6fb3025af63e99d908dd29bde78f4 Mon Sep 17 00:00:00 2001 From: glen Date: Fri, 18 Apr 2025 04:34:30 +0000 Subject: [PATCH 29/41] Add a top-level run command to the "play with prototype" in README (#81) It's convenient to stay in the top-level directory of a project. This change to the README explains how to run the prototype from the top level. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/81 Co-authored-by: glen Co-committed-by: glen --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1ea3302..3a29eb0 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,10 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter ### Play with the prototype -1. Go into the `app-proto` folder -2. Call `trunk serve --release` to build and serve the prototype +1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype * *The crates the prototype depends on will be downloaded and served automatically* * *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 From 360ce12d8baf7f7200b422df5c43894765bd3e03 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 21 Apr 2025 23:40:42 +0000 Subject: [PATCH 30/41] feat: Curvature regulators (#80) Prior to this commit, there's only one kind of regulator: the one that regulates the inversive distance between two spheres (or, more generally, the Lorentz product between two element representation vectors). Adds a new kind of regulator, which regulates the curvature of a sphere (issue #55). In the process, introduces a general framework based on new traits for organizing and sharing code between different kinds of regulators. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/80 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/examples/point-on-sphere.rs | 25 +- app-proto/examples/three-spheres.rs | 29 +- app-proto/src/add_remove.rs | 63 ++--- app-proto/src/assembly.rs | 364 ++++++++++++++++++------ app-proto/src/engine.rs | 388 ++++++++++++++++---------- app-proto/src/outline.rs | 102 +++++-- 6 files changed, 640 insertions(+), 331 deletions(-) diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 13040e5..880d7b0 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -1,26 +1,19 @@ -use nalgebra::DMatrix; - -use dyna3::engine::{Q, point, realize_gram, sphere, PartialMatrix}; +use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem}; fn main() { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..2 { - for k in j..2 { - gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); - } - } - gram_to_be - }; - let guess = DMatrix::from_columns(&[ + let mut problem = ConstraintProblem::from_guess(&[ point(0.0, 0.0, 2.0), sphere(0.0, 0.0, 0.0, 1.0) ]); - let frozen = [(3, 0)]; + for j in 0..2 { + for k in j..2 { + problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + problem.frozen.push(3, 0, problem.guess[(3, 0)]); println!(); let (config, _, success, history) = realize_gram( - &gram, guess, &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); print!("Configuration:{}", config); diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index 19acfd1..3f3cc44 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -1,29 +1,22 @@ -use nalgebra::DMatrix; - -use dyna3::engine::{Q, realize_gram, sphere, PartialMatrix}; +use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem}; fn main() { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..3 { - for k in j..3 { - gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); - } - } - gram_to_be - }; - let guess = { + let mut problem = ConstraintProblem::from_guess({ let a: f64 = 0.75_f64.sqrt(); - DMatrix::from_columns(&[ + &[ sphere(1.0, 0.0, 0.0, 1.0), sphere(-0.5, a, 0.0, 1.0), sphere(-0.5, -a, 0.0, 1.0) - ]) - }; + ] + }); + for j in 0..3 { + for k in j..3 { + problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); + } + } println!(); let (config, _, success, history) = realize_gram( - &gram, guess, &[], - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); if success { diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 5fed411..14fcd41 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -4,14 +4,14 @@ use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ engine, AppState, - assembly::{Assembly, Element} + assembly::{Assembly, Element, InversiveDistanceRegulator} }; /* DEBUG */ // load an example assembly for testing. this code will be removed once we've // built a more formal test assembly system fn load_gen_assemb(assembly: &Assembly) { - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("gemini_a"), String::from("Castor"), @@ -19,7 +19,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(0.5, 0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("gemini_b"), String::from("Pollux"), @@ -27,7 +27,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(-0.5, -0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("ursa_major"), String::from("Ursa major"), @@ -35,7 +35,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(-0.5, 0.5, 0.0, 0.75) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("ursa_minor"), String::from("Ursa minor"), @@ -43,7 +43,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(0.5, -0.5, 0.0, 0.5) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("moon_deimos"), String::from("Deimos"), @@ -51,7 +51,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(0.0, 0.15, 1.0, 0.25) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("moon_phobos"), String::from("Phobos"), @@ -66,7 +66,7 @@ fn load_gen_assemb(assembly: &Assembly) { // built a more formal test assembly system fn load_low_curv_assemb(assembly: &Assembly) { let a = 0.75_f64.sqrt(); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "central".to_string(), "Central".to_string(), @@ -74,7 +74,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere(0.0, 0.0, 0.0, 1.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "assemb_plane".to_string(), "Assembly plane".to_string(), @@ -82,7 +82,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "side1".to_string(), "Side 1".to_string(), @@ -90,7 +90,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "side2".to_string(), "Side 2".to_string(), @@ -98,7 +98,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "side3".to_string(), "Side 3".to_string(), @@ -106,7 +106,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "corner1".to_string(), "Corner 1".to_string(), @@ -114,7 +114,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "corner2".to_string(), "Corner 2".to_string(), @@ -122,7 +122,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("corner3"), String::from("Corner 3"), @@ -148,6 +148,7 @@ pub fn AddRemove() -> View { let assembly = &state.assembly; // clear state + assembly.regulators.update(|regs| regs.clear()); assembly.elements.update(|elts| elts.clear()); assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); state.selection.update(|sel| sel.clear()); @@ -166,18 +167,7 @@ pub fn AddRemove() -> View { button( on:click=|_| { let state = use_context::(); - state.assembly.insert_new_element(); - - /* DEBUG */ - // print updated list of elements by identifier - console::log_1(&JsValue::from("elements by identifier:")); - for (id, key) in state.assembly.elements_by_id.get_clone().iter() { - console::log_3( - &JsValue::from(" "), - &JsValue::from(id), - &JsValue::from(*key) - ); - } + state.assembly.insert_new_sphere(); } ) { "+" } button( @@ -188,13 +178,20 @@ pub fn AddRemove() -> View { }, on:click=|_| { let state = use_context::(); - let subjects = state.selection.with( - |sel| { - let subject_vec: Vec<_> = sel.into_iter().collect(); - (subject_vec[0].clone(), subject_vec[1].clone()) - } + let subjects: [_; 2] = state.selection.with( + // the button is only enabled when two elements are + // selected, so we know the cast to a two-element array + // will succeed + |sel| sel + .clone() + .into_iter() + .collect::>() + .try_into() + .unwrap() + ); + state.assembly.insert_regulator( + InversiveDistanceRegulator::new(subjects, &state.assembly) ); - state.assembly.insert_new_regulator(subjects); state.selection.update(|sel| sel.clear()); } ) { "🔗" } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 18176df..5c926ca 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,12 +1,21 @@ use nalgebra::{DMatrix, DVector, DVectorView, Vector3}; use rustc_hash::FxHashMap; use slab::Slab; -use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; +use std::{collections::BTreeSet, rc::Rc, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::{ - engine::{Q, local_unif_to_std, realize_gram, ConfigSubspace, PartialMatrix}, + engine::{ + Q, + change_half_curvature, + local_unif_to_std, + realize_gram, + sphere, + ConfigSubspace, + ConstraintProblem + }, + outline::OutlineItem, specified::SpecifiedValue }; @@ -23,6 +32,10 @@ pub type ElementColor = [f32; 3]; // each assembly has a key that identifies it within the sesssion static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0); +pub trait ProblemPoser { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab); +} + #[derive(Clone, PartialEq)] pub struct Element { pub id: String, @@ -30,8 +43,8 @@ pub struct Element { pub color: ElementColor, pub representation: Signal>, - // All regulators with this element as a subject. The assembly owning - // this element is responsible for keeping this set up to date. + // the regulators this element is subject to. the assembly that owns the + // element is responsible for keeping this set up to date pub regulators: Signal>, // a serial number, assigned by `Element::new`, that uniquely identifies @@ -45,6 +58,8 @@ pub struct Element { } impl Element { + const CURVATURE_COMPONENT: usize = 3; + pub fn new( id: String, label: String, @@ -117,13 +132,148 @@ impl Element { } } -#[derive(Clone, Copy)] -pub struct Regulator { - pub subjects: (ElementKey, ElementKey), +impl ProblemPoser for Element { + fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab) { + let index = self.column_index.expect( + format!("Element \"{}\" 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()); + } +} + +pub trait Regulator: ProblemPoser + OutlineItem { + fn subjects(&self) -> Vec; + fn measurement(&self) -> ReadSignal; + fn set_point(&self) -> Signal; + + // 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, _assembly: &Assembly) -> bool { + self.set_point().with(|set_pt| set_pt.is_present()) + } +} + +pub struct InversiveDistanceRegulator { + pub subjects: [ElementKey; 2], pub measurement: ReadSignal, pub set_point: Signal } +impl InversiveDistanceRegulator { + pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> InversiveDistanceRegulator { + let measurement = assembly.elements.map( + move |elts| { + let representations = subjects.map(|subj| elts[subj].representation); + representations[0].with(|rep_0| + representations[1].with(|rep_1| + rep_0.dot(&(&*Q * rep_1)) + ) + ) + } + ); + + let set_point = create_signal(SpecifiedValue::from_empty_spec()); + + InversiveDistanceRegulator { subjects, measurement, set_point } + } +} + +impl Regulator for InversiveDistanceRegulator { + fn subjects(&self) -> Vec { + self.subjects.into() + } + + fn measurement(&self) -> ReadSignal { + self.measurement + } + + fn set_point(&self) -> Signal { + self.set_point + } +} + +impl ProblemPoser for InversiveDistanceRegulator { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { + self.set_point.with_untracked(|set_pt| { + if let Some(val) = set_pt.value { + let [row, col] = self.subjects.map( + |subj| elts[subj].column_index.expect( + "Subjects should be indexed before inversive distance regulator writes problem data" + ) + ); + problem.gram.push_sym(row, col, val); + } + }); + } +} + +pub struct HalfCurvatureRegulator { + pub subject: ElementKey, + pub measurement: ReadSignal, + pub set_point: Signal +} + +impl HalfCurvatureRegulator { + pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator { + let measurement = assembly.elements.map( + move |elts| elts[subject].representation.with( + |rep| rep[Element::CURVATURE_COMPONENT] + ) + ); + + let set_point = create_signal(SpecifiedValue::from_empty_spec()); + + HalfCurvatureRegulator { subject, measurement, set_point } + } +} + +impl Regulator for HalfCurvatureRegulator { + fn subjects(&self) -> Vec { + vec![self.subject] + } + + fn measurement(&self) -> ReadSignal { + self.measurement + } + + fn set_point(&self) -> Signal { + self.set_point + } + + fn try_activate(&self, assembly: &Assembly) -> bool { + match self.set_point.with(|set_pt| set_pt.value) { + Some(half_curv) => { + let representation = assembly.elements.with_untracked( + |elts| elts[self.subject].representation + ); + representation.update( + |rep| change_half_curvature(rep, half_curv) + ); + true + } + None => false + } + } +} + +impl ProblemPoser for HalfCurvatureRegulator { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { + self.set_point.with_untracked(|set_pt| { + if let Some(val) = set_pt.value { + let col = elts[self.subject].column_index.expect( + "Subject should be indexed before half-curvature regulator writes problem data" + ); + problem.frozen.push(Element::CURVATURE_COMPONENT, col, val); + } + }); + } +} + // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub key: ElementKey, @@ -137,7 +287,7 @@ type AssemblyMotion<'a> = Vec>; pub struct Assembly { // elements and regulators pub elements: Signal>, - pub regulators: Signal>, + pub regulators: Signal>>, // solution variety tangent space. the basis vectors are stored in // configuration matrix format, ordered according to the elements' column @@ -167,26 +317,33 @@ impl Assembly { // --- inserting elements and regulators --- - // insert an element into the assembly without checking whether we already + // insert a sphere into the assembly without checking whether we already // have an element with the same identifier. any element that does have the // same identifier will get kicked out of the `elements_by_id` index - fn insert_element_unchecked(&self, elt: Element) { + fn insert_sphere_unchecked(&self, elt: Element) -> ElementKey { + // insert the sphere let id = elt.id.clone(); let key = self.elements.update(|elts| elts.insert(elt)); self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); + + // regulate the sphere's curvature + self.insert_regulator(HalfCurvatureRegulator::new(key, &self)); + + key } - pub fn try_insert_element(&self, elt: Element) -> bool { + pub fn try_insert_sphere(&self, elt: Element) -> Option { let can_insert = self.elements_by_id.with_untracked( |elts_by_id| !elts_by_id.contains_key(&elt.id) ); if can_insert { - self.insert_element_unchecked(elt); + Some(self.insert_sphere_unchecked(elt)) + } else { + None } - can_insert } - pub fn insert_new_element(&self) { + pub fn insert_new_sphere(&self) { // find the next unused identifier in the default sequence let mut id_num = 1; let mut id = format!("sphere{}", id_num); @@ -197,70 +354,69 @@ impl Assembly { id = format!("sphere{}", id_num); } - // create and insert a new element - self.insert_element_unchecked( + // create and insert a sphere + let _ = self.insert_sphere_unchecked( Element::new( id, format!("Sphere {}", id_num), [0.75_f32, 0.75_f32, 0.75_f32], - DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]) + sphere(0.0, 0.0, 0.0, 1.0) ) ); } - fn insert_regulator(&self, regulator: Regulator) { - let subjects = regulator.subjects; - let key = self.regulators.update(|regs| regs.insert(regulator)); - let subject_regulators = self.elements.with( - |elts| (elts[subjects.0].regulators, elts[subjects.1].regulators) + pub fn insert_regulator(&self, regulator: T) { + // add the regulator to the assembly's regulator list + let regulator_rc = Rc::new(regulator); + let key = self.regulators.update( + |regs| regs.insert(regulator_rc.clone()) ); - subject_regulators.0.update(|regs| regs.insert(key)); - subject_regulators.1.update(|regs| regs.insert(key)); - } - - pub fn insert_new_regulator(self, subjects: (ElementKey, ElementKey)) { - // create and insert a new regulator - let measurement = self.elements.map( - move |elts| { - let reps = ( - elts[subjects.0].representation.get_clone(), - elts[subjects.1].representation.get_clone() - ); - reps.0.dot(&(&*Q * reps.1)) + + // add the regulator to each subject's regulator list + let subjects = regulator_rc.subjects(); + let subject_regulators: Vec<_> = self.elements.with_untracked( + |elts| subjects.into_iter().map( + |subj| elts[subj].regulators + ).collect() + ); + for regulators in subject_regulators { + regulators.update(|regs| regs.insert(key)); + } + + // update the realization when the regulator becomes a constraint, or is + // edited while acting as a constraint + let self_for_effect = self.clone(); + create_effect(move || { + /* DEBUG */ + // log the regulator update + console::log_1(&JsValue::from( + format!("Updated regulator with subjects {:?}", regulator_rc.subjects()) + )); + + if regulator_rc.try_activate(&self_for_effect) { + self_for_effect.realize(); } - ); - let set_point = create_signal(SpecifiedValue::from_empty_spec()); - self.insert_regulator(Regulator { - subjects: subjects, - measurement: measurement, - set_point: set_point }); /* DEBUG */ // print an updated list of regulators console::log_1(&JsValue::from("Regulators:")); - self.regulators.with(|regs| { + self.regulators.with_untracked(|regs| { for (_, reg) in regs.into_iter() { - console::log_5( - &JsValue::from(" "), - &JsValue::from(reg.subjects.0), - &JsValue::from(reg.subjects.1), - &JsValue::from(":"), - ®.set_point.with_untracked( - |set_pt| JsValue::from(set_pt.spec.as_str()) + console::log_1(&JsValue::from(format!( + " {:?}: {}", + reg.subjects(), + reg.set_point().with_untracked( + |set_pt| { + let spec = &set_pt.spec; + if spec.is_empty() { + "__".to_string() + } else { + spec.clone() + } + } ) - ); - } - }); - - // update the realization when the regulator becomes a constraint, or is - // edited while acting as a constraint - create_effect(move || { - console::log_1(&JsValue::from( - format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) - )); - if set_point.with(|set_pt| set_pt.is_present()) { - self.realize(); + ))); } }); } @@ -275,55 +431,39 @@ impl Assembly { } }); - // set up the Gram matrix and the initial configuration matrix - let (gram, guess) = self.elements.with_untracked(|elts| { - // set up the off-diagonal part of the Gram matrix - let mut gram_to_be = PartialMatrix::new(); + // set up the constraint problem + let problem = self.elements.with_untracked(|elts| { + let mut problem = ConstraintProblem::new(elts.len()); + for (_, elt) in elts { + elt.pose(&mut problem, elts); + } self.regulators.with_untracked(|regs| { for (_, reg) in regs { - reg.set_point.with_untracked(|set_pt| { - if let Some(val) = set_pt.value { - let subjects = reg.subjects; - let row = elts[subjects.0].column_index.unwrap(); - let col = elts[subjects.1].column_index.unwrap(); - gram_to_be.push_sym(row, col, val); - } - }); + reg.pose(&mut problem, elts); } }); - - // set up the initial configuration matrix and the diagonal of the - // Gram matrix - let mut guess_to_be = DMatrix::::zeros(5, elts.len()); - for (_, elt) in elts { - let index = elt.column_index.unwrap(); - gram_to_be.push_sym(index, index, 1.0); - guess_to_be.set_column(index, &elt.representation.get_clone_untracked()); - } - - (gram_to_be, guess_to_be) + problem }); /* DEBUG */ // log the Gram matrix console::log_1(&JsValue::from("Gram matrix:")); - gram.log_to_console(); + problem.gram.log_to_console(); /* DEBUG */ // log the initial configuration matrix console::log_1(&JsValue::from("Old configuration:")); - for j in 0..guess.nrows() { + for j in 0..problem.guess.nrows() { let mut row_str = String::new(); - for k in 0..guess.ncols() { - row_str.push_str(format!(" {:>8.3}", guess[(j, k)]).as_str()); + for k in 0..problem.guess.ncols() { + row_str.push_str(format!(" {:>8.3}", problem.guess[(j, k)]).as_str()); } console::log_1(&JsValue::from(row_str)); } // look for a configuration with the given Gram matrix let (config, tangent, success, history) = realize_gram( - &gram, guess, &[], - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); /* DEBUG */ @@ -458,4 +598,48 @@ impl Assembly { // sync self.realize(); } +} + +#[cfg(test)] +mod tests { + use crate::engine; + + use super::*; + + #[test] + #[should_panic(expected = "Element \"sphere\" should be indexed before writing problem data")] + fn unindexed_element_test() { + let _ = create_root(|| { + Element::new( + "sphere".to_string(), + "Sphere".to_string(), + [1.0_f32, 1.0_f32, 1.0_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ).pose(&mut ConstraintProblem::new(1), &Slab::new()); + }); + } + + #[test] + #[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")] + fn unindexed_subject_test_inversive_distance() { + let _ = create_root(|| { + let mut elts = Slab::new(); + let subjects = [0, 1].map(|k| { + elts.insert( + Element::new( + format!("sphere{k}"), + format!("Sphere {k}"), + [1.0_f32, 1.0_f32, 1.0_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) + ) + }); + elts[subjects[0]].column_index = Some(0); + InversiveDistanceRegulator { + subjects: subjects, + measurement: create_memo(|| 0.0), + set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()) + }.pose(&mut ConstraintProblem::new(2), &elts); + }); + } } \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 35f898c..869a7de 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -35,9 +35,43 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 ]) } +// 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, 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 --- -struct MatrixEntry { +pub struct MatrixEntry { index: (usize, usize), value: f64 } @@ -49,42 +83,72 @@ impl PartialMatrix { PartialMatrix(Vec::::new()) } - pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { + pub fn push(&mut self, row: usize, col: usize, value: f64) { let PartialMatrix(entries) = self; entries.push(MatrixEntry { index: (row, col), value: value }); + } + + pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { + self.push(row, col, value); if row != col { - entries.push(MatrixEntry { index: (col, row), value: value }); + self.push(col, row, value); } } /* DEBUG */ pub fn log_to_console(&self) { - let PartialMatrix(entries) = self; - for ent in entries { - let ent_str = format!(" {} {} {}", ent.index.0, ent.index.1, ent.value); - console::log_1(&JsValue::from(ent_str.as_str())); + for &MatrixEntry { index: (row, col), value } in self { + console::log_1(&JsValue::from( + format!(" {} {} {}", row, col, value) + )); } } + fn freeze(&self, a: &DMatrix) -> DMatrix { + let mut result = a.clone(); + for &MatrixEntry { index, value } in self { + result[index] = value; + } + result + } + fn proj(&self, a: &DMatrix) -> DMatrix { let mut result = DMatrix::::zeros(a.nrows(), a.ncols()); - let PartialMatrix(entries) = self; - for ent in entries { - result[ent.index] = a[ent.index]; + for &MatrixEntry { index, .. } in self { + result[index] = a[index]; } result } fn sub_proj(&self, rhs: &DMatrix) -> DMatrix { let mut result = DMatrix::::zeros(rhs.nrows(), rhs.ncols()); - let PartialMatrix(entries) = self; - for ent in entries { - result[ent.index] = ent.value - rhs[ent.index]; + for &MatrixEntry { index, value } in self { + result[index] = value - rhs[index]; } result } } +impl IntoIterator for PartialMatrix { + type Item = MatrixEntry; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + let PartialMatrix(entries) = self; + entries.into_iter() + } +} + +impl<'a> IntoIterator for &'a PartialMatrix { + type Item = &'a MatrixEntry; + type IntoIter = std::slice::Iter<'a, MatrixEntry>; + + fn into_iter(self) -> Self::IntoIter { + let PartialMatrix(entries) = self; + entries.into_iter() + } +} + // --- configuration subspaces --- #[derive(Clone)] @@ -195,6 +259,34 @@ impl DescentHistory { } } +// --- constraint problems --- + +pub struct ConstraintProblem { + pub gram: PartialMatrix, + pub frozen: PartialMatrix, + pub guess: DMatrix, +} + +impl ConstraintProblem { + pub fn new(element_count: usize) -> ConstraintProblem { + const ELEMENT_DIM: usize = 5; + ConstraintProblem { + gram: PartialMatrix::new(), + frozen: PartialMatrix::new(), + guess: DMatrix::::zeros(ELEMENT_DIM, element_count) + } + } + + #[cfg(feature = "dev")] + pub fn from_guess(guess_columns: &[DVector]) -> ConstraintProblem { + ConstraintProblem { + gram: PartialMatrix::new(), + frozen: PartialMatrix::new(), + guess: DMatrix::from_columns(guess_columns) + } + } +} + // --- gram matrix realization --- // the Lorentz form @@ -286,12 +378,12 @@ fn seek_better_config( None } -// seek a matrix `config` for which `config' * Q * config` matches the partial -// matrix `gram`. use gradient descent starting from `guess` +// seek a matrix `config` that matches the partial matrix `problem.frozen` and +// has `config' * Q * config` matching the partial matrix `problem.gram`. start +// at `problem.guess`, set the frozen entries to their desired values, and then +// use a regularized Newton's method to seek the desired Gram matrix pub fn realize_gram( - gram: &PartialMatrix, - guess: DMatrix, - frozen: &[(usize, usize)], + problem: &ConstraintProblem, scaled_tol: f64, min_efficiency: f64, backoff: f64, @@ -299,6 +391,11 @@ pub fn realize_gram( max_descent_steps: i32, max_backoff_steps: i32 ) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + // destructure the problem data + let ConstraintProblem { + gram, guess, frozen + } = problem; + // start the descent history let mut history = DescentHistory::new(); @@ -313,11 +410,11 @@ pub fn realize_gram( // convert the frozen indices to stacked format let frozen_stacked: Vec = frozen.into_iter().map( - |index| index.1*element_dim + index.0 + |MatrixEntry { index: (row, col), .. }| col*element_dim + row ).collect(); - // use Newton's method with backtracking and gradient descent backup - let mut state = SearchState::from_config(gram, guess); + // use a regularized Newton's method with backtracking + let mut state = SearchState::from_config(gram, frozen.freeze(guess)); let mut hess = DMatrix::zeros(element_dim, assembly_dim); for _ in 0..max_descent_steps { // find the negative gradient of the loss function @@ -415,7 +512,7 @@ pub fn realize_gram( #[cfg(feature = "dev")] pub mod examples { - use std::{array, f64::consts::PI}; + use std::f64::consts::PI; use super::*; @@ -428,35 +525,7 @@ pub mod examples { // https://www.nippon.com/en/japan-topics/c12801/ // pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for s in 0..9 { - // each sphere is represented by a spacelike vector - gram_to_be.push_sym(s, s, 1.0); - - // the circumscribing sphere is tangent to all of the other - // spheres, with matching orientation - if s > 0 { - gram_to_be.push_sym(0, s, 1.0); - } - - if s > 2 { - // each chain sphere is tangent to the "sun" and "moon" - // spheres, with opposing orientation - for n in 1..3 { - gram_to_be.push_sym(s, n, -1.0); - } - - // each chain sphere is tangent to the next chain sphere, - // with opposing orientation - let s_next = 3 + (s-2) % 6; - gram_to_be.push_sym(s, s_next, -1.0); - } - } - gram_to_be - }; - - let guess = DMatrix::from_columns( + let mut problem = ConstraintProblem::from_guess( [ sphere(0.0, 0.0, 0.0, 15.0), sphere(0.0, 0.0, -9.0, 5.0), @@ -471,42 +540,45 @@ pub mod examples { ).collect::>().as_slice() ); + for s in 0..9 { + // each sphere is represented by a spacelike vector + problem.gram.push_sym(s, s, 1.0); + + // the circumscribing sphere is tangent to all of the other + // spheres, with matching orientation + if s > 0 { + problem.gram.push_sym(0, s, 1.0); + } + + if s > 2 { + // each chain sphere is tangent to the "sun" and "moon" + // spheres, with opposing orientation + for n in 1..3 { + problem.gram.push_sym(s, n, -1.0); + } + + // each chain sphere is tangent to the next chain sphere, + // with opposing orientation + let s_next = 3 + (s-2) % 6; + problem.gram.push_sym(s, s_next, -1.0); + } + } + // the frozen entries fix the radii of the circumscribing sphere, the // "sun" and "moon" spheres, and one of the chain spheres - let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k)); + for k in 0..4 { + problem.frozen.push(3, k, problem.guess[(3, k)]); + } - realize_gram( - &gram, guess, &frozen, - scaled_tol, 0.5, 0.9, 1.1, 200, 110 - ) + realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110) } // set up a kaleidocycle, made of points with fixed distances between them, // and find its tangent space pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { - const N_POINTS: usize = 12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for block in (0..N_POINTS).step_by(2) { - let block_next = (block + 2) % N_POINTS; - for j in 0..2 { - // diagonal and hinge edges - for k in j..2 { - gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); - } - - // non-hinge edges - for k in 0..2 { - gram_to_be.push_sym(block + j, block_next + k, -0.625); - } - } - } - gram_to_be - }; - - let guess = { - const N_HINGES: usize = 6; - let guess_elts = (0..N_HINGES).step_by(2).flat_map( + const N_HINGES: usize = 6; + let mut problem = ConstraintProblem::from_guess( + (0..N_HINGES).step_by(2).flat_map( |n| { let ang_hor = (n as f64) * PI/3.0; let ang_vert = ((n + 1) as f64) * PI/3.0; @@ -519,16 +591,30 @@ pub mod examples { point(x_vert, y_vert, 0.5) ] } - ).collect::>(); - DMatrix::from_columns(&guess_elts) - }; + ).collect::>().as_slice() + ); - let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); + const N_POINTS: usize = 2 * N_HINGES; + for block in (0..N_POINTS).step_by(2) { + let block_next = (block + 2) % N_POINTS; + for j in 0..2 { + // diagonal and hinge edges + for k in j..2 { + problem.gram.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); + } + + // non-hinge edges + for k in 0..2 { + problem.gram.push_sym(block + j, block_next + k, -0.625); + } + } + } - realize_gram( - &gram, guess, &frozen, - scaled_tol, 0.5, 0.9, 1.1, 200, 110 - ) + for k in 0..N_POINTS { + problem.frozen.push(3, k, problem.guess[(3, k)]) + } + + realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110) } } @@ -539,6 +625,25 @@ mod tests { use super::{*, examples::*}; + #[test] + fn freeze_test() { + let frozen = PartialMatrix(vec![ + 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 } + ]); + let config = DMatrix::::from_row_slice(2, 3, &[ + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0 + ]); + let expected_result = DMatrix::::from_row_slice(2, 3, &[ + 14.0, 2.0, 28.0, + 4.0, 42.0, 49.0 + ]); + assert_eq!(frozen.freeze(&config), expected_result); + } + #[test] fn sub_proj_test() { let target = PartialMatrix(vec![ @@ -560,18 +665,12 @@ mod tests { #[test] fn zero_loss_test() { - let gram = PartialMatrix({ - let mut entries = Vec::::new(); - for j in 0..3 { - for k in 0..3 { - entries.push(MatrixEntry { - index: (j, k), - value: if j == k { 1.0 } else { -1.0 } - }); - } + let mut gram = PartialMatrix::new(); + for j in 0..3 { + for k in 0..3 { + gram.push(j, k, if j == k { 1.0 } else { -1.0 }); } - entries - }); + } let config = { let a = 0.75_f64.sqrt(); DMatrix::from_columns(&[ @@ -584,37 +683,33 @@ mod tests { assert!(state.loss.abs() < f64::EPSILON); } + /* TO DO */ // at the frozen indices, the optimization steps should have exact zeros, - // and the realized configuration should match the initial guess + // and the realized configuration should have the desired values #[test] fn frozen_entry_test() { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..2 { - for k in j..2 { - gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); - } - } - gram_to_be - }; - let guess = DMatrix::from_columns(&[ + let mut problem = ConstraintProblem::from_guess(&[ point(0.0, 0.0, 2.0), - sphere(0.0, 0.0, 0.0, 1.0) + sphere(0.0, 0.0, 0.0, 0.95) ]); - let frozen = [(3, 0), (3, 1)]; - println!(); + for j in 0..2 { + for k in j..2 { + problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + problem.frozen.push(3, 0, problem.guess[(3, 0)]); + problem.frozen.push(3, 1, 0.5); let (config, _, success, history) = realize_gram( - &gram, guess.clone(), &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); assert_eq!(success, true); for base_step in history.base_step.into_iter() { - for index in frozen { + for &MatrixEntry { index, .. } in &problem.frozen { assert_eq!(base_step[index], 0.0); } } - for index in frozen { - assert_eq!(config[index], guess[index]); + for MatrixEntry { index, value } in problem.frozen { + assert_eq!(config[index], value); } } @@ -635,34 +730,32 @@ mod tests { #[test] fn tangent_test_three_spheres() { const SCALED_TOL: f64 = 1.0e-12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..3 { - for k in j..3 { - gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); - } - } - gram_to_be - }; - let guess = DMatrix::from_columns(&[ + const ELEMENT_DIM: usize = 5; + 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) ]); - let frozen: [_; 5] = std::array::from_fn(|k| (k, 0)); + for j in 0..3 { + for k in j..3 { + problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); + } + } + for n in 0..ELEMENT_DIM { + problem.frozen.push(n, 0, problem.guess[(n, 0)]); + } let (config, tangent, success, history) = realize_gram( - &gram, guess.clone(), &frozen, - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + &problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(config, guess); + assert_eq!(config, problem.guess); assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of // the solution variety const UNIFORM_DIM: usize = 4; - let element_dim = guess.nrows(); - let assembly_dim = guess.ncols(); + let element_dim = problem.guess.nrows(); + let assembly_dim = problem.guess.ncols(); let tangent_motions_unif = vec![ basis_matrix((0, 1), UNIFORM_DIM, assembly_dim), basis_matrix((1, 1), UNIFORM_DIM, assembly_dim), @@ -805,22 +898,17 @@ mod tests { fn proj_equivar_test() { // find a pair of spheres that meet at 120° const SCALED_TOL: f64 = 1.0e-12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - gram_to_be.push_sym(0, 0, 1.0); - gram_to_be.push_sym(1, 1, 1.0); - gram_to_be.push_sym(0, 1, 0.5); - gram_to_be - }; - let guess_orig = DMatrix::from_columns(&[ + 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) ]); + problem_orig.gram.push_sym(0, 0, 1.0); + problem_orig.gram.push_sym(1, 1, 1.0); + problem_orig.gram.push_sym(0, 1, 0.5); let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram( - &gram, guess_orig.clone(), &[], - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + &problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(config_orig, guess_orig); + assert_eq!(config_orig, problem_orig.guess); assert_eq!(success_orig, true); assert_eq!(history_orig.scaled_loss.len(), 1); @@ -833,11 +921,15 @@ mod tests { sphere(-a, 0.0, 7.0 - a, 1.0) ]) }; + let problem_tfm = ConstraintProblem { + gram: problem_orig.gram, + guess: guess_tfm, + frozen: problem_orig.frozen + }; let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram( - &gram, guess_tfm.clone(), &[], - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(config_tfm, guess_tfm); + assert_eq!(config_tfm, problem_tfm.guess); assert_eq!(success_tfm, true); assert_eq!(history_tfm.scaled_loss.len(), 1); @@ -869,7 +961,7 @@ mod tests { // the comparison tolerance because the transformation seems to // introduce some numerical error const SCALED_TOL_TFM: f64 = 1.0e-9; - let tol_sq = ((guess_orig.nrows() * guess_orig.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM; + let tol_sq = ((problem_orig.guess.nrows() * problem_orig.guess.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM; assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq); } } \ No newline at end of file diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 002baea..2446337 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,4 +1,5 @@ use itertools::Itertools; +use std::rc::Rc; use sycamore::prelude::*; use web_sys::{ KeyboardEvent, @@ -9,24 +10,38 @@ use web_sys::{ use crate::{ AppState, assembly, - assembly::{ElementKey, Regulator, RegulatorKey}, + assembly::{ + ElementKey, + HalfCurvatureRegulator, + InversiveDistanceRegulator, + Regulator, + RegulatorKey + }, specified::SpecifiedValue }; // an editable view of a regulator #[component(inline_props)] -fn RegulatorInput(regulator: Regulator) -> View { +fn RegulatorInput(regulator: Rc) -> View { + // get the regulator's measurement and set point signals + let measurement = regulator.measurement(); + let set_point = regulator.set_point(); + + // the `valid` signal tracks whether the last entered value is a valid set + // point specification let valid = create_signal(true); + + // the `value` signal holds the current set point specification let value = create_signal( - regulator.set_point.with_untracked(|set_pt| set_pt.spec.clone()) + set_point.with_untracked(|set_pt| set_pt.spec.clone()) ); - // this closure resets the input value to the regulator's set point - // specification + // this `reset_value` closure resets the input value to the regulator's set + // point specification let reset_value = move || { batch(|| { valid.set(true); - value.set(regulator.set_point.with(|set_pt| set_pt.spec.clone())); + value.set(set_point.with(|set_pt| set_pt.spec.clone())); }) }; @@ -39,7 +54,7 @@ fn RegulatorInput(regulator: Regulator) -> View { r#type="text", class=move || { if valid.get() { - regulator.set_point.with(|set_pt| { + set_point.with(|set_pt| { if set_pt.is_present() { "regulator-input constraint" } else { @@ -50,13 +65,13 @@ fn RegulatorInput(regulator: Regulator) -> View { "regulator-input invalid" } }, - placeholder=regulator.measurement.with(|result| result.to_string()), + 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) => { - regulator.set_point.set(set_pt); + set_point.set(set_pt); true } Err(_) => false @@ -75,26 +90,53 @@ fn RegulatorInput(regulator: Regulator) -> View { } } +pub trait OutlineItem { + fn outline_item(self: Rc, element_key: ElementKey) -> View; +} + +impl OutlineItem for InversiveDistanceRegulator { + fn outline_item(self: Rc, element_key: ElementKey) -> View { + let state = use_context::(); + let other_subject = if self.subjects[0] == element_key { + self.subjects[1] + } else { + self.subjects[0] + }; + let other_subject_label = state.assembly.elements.with( + |elts| elts[other_subject].label.clone() + ); + view! { + li(class="regulator") { + div(class="regulator-label") { (other_subject_label) } + div(class="regulator-type") { "Inversive distance" } + RegulatorInput(regulator=self) + div(class="status") + } + } + } +} + +impl OutlineItem for HalfCurvatureRegulator { + fn outline_item(self: Rc, _element_key: ElementKey) -> View { + view! { + li(class="regulator") { + div(class="regulator-label") // for spacing + div(class="regulator-type") { "Half-curvature" } + RegulatorInput(regulator=self) + div(class="status") + } + } + } +} + // a list item that shows a regulator in an outline view of an element #[component(inline_props)] fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View { let state = use_context::(); - let assembly = &state.assembly; - let regulator = assembly.regulators.with(|regs| regs[regulator_key]); - let other_subject = if regulator.subjects.0 == element_key { - regulator.subjects.1 - } else { - regulator.subjects.0 - }; - let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); - view! { - li(class="regulator") { - div(class="regulator-label") { (other_subject_label) } - div(class="regulator-type") { "Inversive distance" } - RegulatorInput(regulator=regulator) - div(class="status") - } - } + let regulator = state.assembly.regulators.with( + |regs| regs[regulator_key].clone() + ); + regulator.outline_item(element_key) } // a list item that shows an element in an outline view of an assembly @@ -117,7 +159,15 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { }; let regulated = element.regulators.map(|regs| regs.len() > 0); let regulator_list = element.regulators.map( - |regs| regs.clone().into_iter().collect() + move |elt_reg_keys| elt_reg_keys + .clone() + .into_iter() + .sorted_by_key( + |®_key| state.assembly.regulators.with( + |regs| regs[reg_key].subjects().len() + ) + ) + .collect() ); let details_node = create_node_ref(); view! { From a2478febc128b218d833bc3e904482f2055865cb Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Thu, 1 May 2025 19:25:13 +0000 Subject: [PATCH 31/41] feat: Points (#82) Replaces the former sole Element entity by two, Sphere and Point, both implementing an Element trait. Adds Point display, uses the former Element display for Sphere. Adds a new "canned" configuration, and the ability to add, select, and nudge Point entities. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/82 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/main.css | 4 +- app-proto/src/add_remove.rs | 116 ++-- app-proto/src/assembly.rs | 435 ++++++++++----- app-proto/src/display.rs | 524 +++++++++++++----- app-proto/src/engine.rs | 1 - app-proto/src/outline.rs | 23 +- app-proto/src/point.frag | 18 + app-proto/src/point.vert | 24 + .../src/{inversive.frag => spheres.frag} | 0 9 files changed, 815 insertions(+), 330 deletions(-) create mode 100644 app-proto/src/point.frag create mode 100644 app-proto/src/point.vert rename app-proto/src/{inversive.frag => spheres.frag} (100%) diff --git a/app-proto/main.css b/app-proto/main.css index 4726a27..f787535 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -42,9 +42,7 @@ body { } #add-remove > button { - width: 32px; height: 32px; - font-size: large; } /* KLUDGE */ @@ -53,7 +51,9 @@ body { buttons need to be displayed in an emoji font */ #add-remove > button.emoji { + width: 32px; font-family: 'Noto Emoji', sans-serif; + font-size: large; } /* outline */ diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 14fcd41..ea86186 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -1,58 +1,59 @@ +use std::{f64::consts::FRAC_1_SQRT_2, rc::Rc}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ engine, AppState, - assembly::{Assembly, Element, InversiveDistanceRegulator} + assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere} }; /* DEBUG */ // load an example assembly for testing. this code will be removed once we've // built a more formal test assembly system fn load_gen_assemb(assembly: &Assembly) { - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("gemini_a"), String::from("Castor"), [1.00_f32, 0.25_f32, 0.00_f32], engine::sphere(0.5, 0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("gemini_b"), String::from("Pollux"), [0.00_f32, 0.25_f32, 1.00_f32], engine::sphere(-0.5, -0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("ursa_major"), String::from("Ursa major"), [0.25_f32, 0.00_f32, 1.00_f32], engine::sphere(-0.5, 0.5, 0.0, 0.75) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("ursa_minor"), String::from("Ursa minor"), [0.25_f32, 1.00_f32, 0.00_f32], engine::sphere(0.5, -0.5, 0.0, 0.5) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("moon_deimos"), String::from("Deimos"), [0.75_f32, 0.75_f32, 0.00_f32], engine::sphere(0.0, 0.15, 1.0, 0.25) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("moon_phobos"), String::from("Phobos"), [0.00_f32, 0.75_f32, 0.50_f32], @@ -66,64 +67,64 @@ fn load_gen_assemb(assembly: &Assembly) { // built a more formal test assembly system fn load_low_curv_assemb(assembly: &Assembly) { let a = 0.75_f64.sqrt(); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "central".to_string(), "Central".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], engine::sphere(0.0, 0.0, 0.0, 1.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "assemb_plane".to_string(), "Assembly plane".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "side1".to_string(), "Side 1".to_string(), [1.00_f32, 0.00_f32, 0.25_f32], engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "side2".to_string(), "Side 2".to_string(), [0.25_f32, 1.00_f32, 0.00_f32], engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "side3".to_string(), "Side 3".to_string(), [0.00_f32, 0.25_f32, 1.00_f32], engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "corner1".to_string(), "Corner 1".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "corner2".to_string(), "Corner 2".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("corner3"), String::from("Corner 3"), [0.75_f32, 0.75_f32, 0.75_f32], @@ -132,6 +133,49 @@ fn load_low_curv_assemb(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) + ) + ); + let _ = assembly.try_insert_element( + Point::new( + format!("point_back"), + format!("Back point"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(0.0, 0.0, -FRAC_1_SQRT_2) + ) + ); + for index_x in 0..=1 { + for index_y in 0..=1 { + let x = index_x as f64 - 0.5; + let y = index_y as f64 - 0.5; + + let _ = assembly.try_insert_element( + Sphere::new( + format!("sphere{index_x}{index_y}"), + format!("Sphere {index_x}{index_y}"), + [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + engine::sphere(x, y, 0.0, 1.0) + ) + ); + + let _ = assembly.try_insert_element( + Point::new( + format!("point{index_x}{index_y}"), + format!("Point {index_x}{index_y}"), + [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + engine::point(x, y, 0.0) + ) + ); + } + } +} + #[component] pub fn AddRemove() -> View { /* DEBUG */ @@ -157,6 +201,7 @@ pub fn AddRemove() -> View { match name.as_str() { "general" => load_gen_assemb(assembly), "low-curv" => load_low_curv_assemb(assembly), + "pointed" => load_pointed_assemb(assembly), _ => () }; }); @@ -167,9 +212,15 @@ pub fn AddRemove() -> View { button( on:click=|_| { let state = use_context::(); - state.assembly.insert_new_sphere(); + state.assembly.insert_element_default::(); } - ) { "+" } + ) { "Add sphere" } + button( + on:click=|_| { + let state = use_context::(); + state.assembly.insert_element_default::(); + } + ) { "Add point" } button( class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button disabled={ @@ -190,7 +241,7 @@ pub fn AddRemove() -> View { .unwrap() ); state.assembly.insert_regulator( - InversiveDistanceRegulator::new(subjects, &state.assembly) + Rc::new(InversiveDistanceRegulator::new(subjects, &state.assembly)) ); state.selection.update(|sel| sel.clear()); } @@ -198,6 +249,7 @@ pub fn AddRemove() -> View { select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser option(value="general") { "General" } option(value="low-curv") { "Low-curvature" } + option(value="pointed") { "Pointed" } option(value="empty") { "Empty" } } } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 5c926ca..343cef8 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,15 +1,23 @@ -use nalgebra::{DMatrix, DVector, DVectorView, Vector3}; +use nalgebra::{DMatrix, DVector, DVectorView}; use rustc_hash::FxHashMap; use slab::Slab; -use std::{collections::BTreeSet, rc::Rc, sync::atomic::{AtomicU64, Ordering}}; +use std::{ + any::{Any, TypeId}, + cell::Cell, + collections::BTreeSet, + rc::Rc, + sync::atomic::{AtomicU64, Ordering} +}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::{ + display::DisplayItem, engine::{ Q, change_half_curvature, local_unif_to_std, + point, realize_gram, sphere, ConfigSubspace, @@ -33,31 +41,87 @@ pub type ElementColor = [f32; 3]; static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0); pub trait ProblemPoser { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab); + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>); } -#[derive(Clone, PartialEq)] -pub struct Element { +pub trait Element: ProblemPoser + DisplayItem { + // the default identifier for an element of this type + fn default_id() -> String where Self: Sized; + + // create the default example of an element of this type + fn default(id: String, id_num: u64) -> Self where Self: Sized; + + // the regulators that should be created when an element of this type is + // inserted into the given assembly with the given storage key + /* KLUDGE */ + // right now, this organization makes sense because regulators identify + // their subjects by storage key, so the element has to be inserted before + // its regulators can be created. if we change the way regulators identify + // their subjects, we should consider refactoring + fn default_regulators(_key: ElementKey, _assembly: &Assembly) -> Vec> where Self: Sized { + Vec::new() + } + + fn id(&self) -> &String; + fn label(&self) -> &String; + fn representation(&self) -> Signal>; + + // the regulators the element is subject to. the assembly that owns the + // element is responsible for keeping this set up to date + fn regulators(&self) -> Signal>; + + // a serial number that uniquely identifies this element + fn serial(&self) -> u64; + + // take the next serial number, panicking if that was the last one left + fn next_serial() -> u64 where Self: Sized { + // the technique we use to panic on overflow is taken from _Rust Atomics + // and Locks_, by Mara Bos + // + // https://marabos.nl/atomics/atomics.html#example-handle-overflow + // + NEXT_ELEMENT_SERIAL.fetch_update( + Ordering::SeqCst, Ordering::SeqCst, + |serial| serial.checked_add(1) + ).expect("Out of serial numbers for elements") + } + + // the configuration matrix column index that was assigned to the element + // last time the assembly was realized, or `None` if the element has never + // been through a realization + fn column_index(&self) -> Option; + + // assign the element a configuration matrix column index. this method must + // be used carefully to preserve invariant (1), described in the comment on + // the `tangent` field of the `Assembly` structure + fn set_column_index(&self, index: usize); +} + +// the `Element` trait needs to be dyn-compatible, so its method signatures can +// only use `Self` in the type of the receiver. that means `Element` can't +// implement `PartialEq`. if you need partial equivalence for `Element` trait +// objects, use this wrapper +#[derive(Clone)] +pub struct ElementRc(pub Rc); + +impl PartialEq for ElementRc { + fn eq(&self, ElementRc(other): &Self) -> bool { + let ElementRc(rc) = self; + Rc::ptr_eq(rc, &other) + } +} + +pub struct Sphere { pub id: String, pub label: String, pub color: ElementColor, pub representation: Signal>, - - // the regulators this element is subject to. the assembly that owns the - // element is responsible for keeping this set up to date pub regulators: Signal>, - - // a serial number, assigned by `Element::new`, that uniquely identifies - // each element pub serial: u64, - - // the configuration matrix column index that was assigned to this element - // last time the assembly was realized, or `None` if the element has never - // been through a realization - column_index: Option + column_index: Cell> } -impl Element { +impl Sphere { const CURVATURE_COMPONENT: usize = 3; pub fn new( @@ -65,83 +129,161 @@ impl Element { label: String, color: ElementColor, representation: DVector - ) -> Element { - // take the next serial number, panicking if that was the last number we - // had left. the technique we use to panic on overflow is taken from - // _Rust Atomics and Locks_, by Mara Bos - // - // https://marabos.nl/atomics/atomics.html#example-handle-overflow - // - let serial = NEXT_ELEMENT_SERIAL.fetch_update( - Ordering::SeqCst, Ordering::SeqCst, - |serial| serial.checked_add(1) - ).expect("Out of serial numbers for elements"); - - Element { + ) -> Sphere { + Sphere { id: id, label: label, color: color, representation: create_signal(representation), regulators: create_signal(BTreeSet::default()), - serial: serial, - column_index: None - } - } - - // the smallest positive depth, represented as a multiple of `dir`, where - // the line generated by `dir` hits the element (which is assumed to be a - // sphere). returns `None` if the line misses the sphere. this function - // should be kept synchronized with `sphere_cast` in `inversive.frag`, which - // does essentially the same thing on the GPU side - pub fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix) -> Option { - // if `a/b` is less than this threshold, we approximate - // `a*u^2 + b*u + c` by the linear function `b*u + c` - const DEG_THRESHOLD: f64 = 1e-9; - - let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); - let a = -rep[3] * dir.norm_squared(); - let b = rep.rows_range(..3).dot(&dir); - let c = -rep[4]; - - let adjust = 4.0*a*c/(b*b); - if adjust < 1.0 { - // as long as `b` is non-zero, the linear approximation of - // - // a*u^2 + b*u + c - // - // at `u = 0` will reach zero at a finite depth `u_lin`. the root of - // the quadratic adjacent to `u_lin` is stored in `lin_root`. if - // both roots have the same sign, `lin_root` will be the one closer - // to `u = 0` - let square_rect_ratio = 1.0 + (1.0 - adjust).sqrt(); - let lin_root = -(2.0*c)/b / square_rect_ratio; - if a.abs() > DEG_THRESHOLD * b.abs() { - if lin_root > 0.0 { - Some(lin_root) - } else { - let other_root = -b/(2.*a) * square_rect_ratio; - (other_root > 0.0).then_some(other_root) - } - } else { - (lin_root > 0.0).then_some(lin_root) - } - } else { - // the line through `dir` misses the sphere completely - None + serial: Self::next_serial(), + column_index: None.into() } } } -impl ProblemPoser for Element { - fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab) { - let index = self.column_index.expect( - format!("Element \"{}\" should be indexed before writing problem data", self.id).as_str() +impl Element for Sphere { + fn default_id() -> String { + "sphere".to_string() + } + + 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) + ) + } + + fn default_regulators(key: ElementKey, assembly: &Assembly) -> Vec> { + vec![Rc::new(HalfCurvatureRegulator::new(key, assembly))] + } + + fn id(&self) -> &String { + &self.id + } + + fn label(&self) -> &String { + &self.label + } + + fn representation(&self) -> Signal> { + self.representation + } + + fn regulators(&self) -> Signal> { + self.regulators + } + + fn serial(&self) -> u64 { + self.serial + } + + fn column_index(&self) -> Option { + self.column_index.get() + } + + fn set_column_index(&self, index: usize) { + self.column_index.set(Some(index)); + } +} + +impl ProblemPoser for Sphere { + fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab>) { + let index = self.column_index().expect( + 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()); } } +pub struct Point { + pub id: String, + pub label: String, + pub color: ElementColor, + pub representation: Signal>, + pub regulators: Signal>, + pub serial: u64, + column_index: Cell> +} + +impl Point { + const WEIGHT_COMPONENT: usize = 3; + + pub fn new( + id: String, + label: String, + color: ElementColor, + representation: DVector + ) -> Point { + Point { + id, + label, + color, + representation: create_signal(representation), + regulators: create_signal(BTreeSet::default()), + serial: Self::next_serial(), + column_index: None.into() + } + } +} + +impl Element for Point { + fn default_id() -> String { + "point".to_string() + } + + 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) + ) + } + + fn id(&self) -> &String { + &self.id + } + + fn label(&self) -> &String { + &self.label + } + + fn representation(&self) -> Signal> { + self.representation + } + + fn regulators(&self) -> Signal> { + self.regulators + } + + fn serial(&self) -> u64 { + self.serial + } + + fn column_index(&self) -> Option { + self.column_index.get() + } + + fn set_column_index(&self, index: usize) { + self.column_index.set(Some(index)); + } +} + +impl ProblemPoser for Point { + fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab>) { + let index = self.column_index().expect( + format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str() + ); + problem.gram.push_sym(index, index, 0.0); + problem.frozen.push(Point::WEIGHT_COMPONENT, index, 0.5); + problem.guess.set_column(index, &self.representation.get_clone_untracked()); + } +} + pub trait Regulator: ProblemPoser + OutlineItem { fn subjects(&self) -> Vec; fn measurement(&self) -> ReadSignal; @@ -168,7 +310,7 @@ impl InversiveDistanceRegulator { pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> InversiveDistanceRegulator { let measurement = assembly.elements.map( move |elts| { - let representations = subjects.map(|subj| elts[subj].representation); + let representations = subjects.map(|subj| elts[subj].representation()); representations[0].with(|rep_0| representations[1].with(|rep_1| rep_0.dot(&(&*Q * rep_1)) @@ -198,11 +340,11 @@ impl Regulator for InversiveDistanceRegulator { } impl ProblemPoser for InversiveDistanceRegulator { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { let [row, col] = self.subjects.map( - |subj| elts[subj].column_index.expect( + |subj| elts[subj].column_index().expect( "Subjects should be indexed before inversive distance regulator writes problem data" ) ); @@ -221,8 +363,8 @@ pub struct HalfCurvatureRegulator { impl HalfCurvatureRegulator { pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator { let measurement = assembly.elements.map( - move |elts| elts[subject].representation.with( - |rep| rep[Element::CURVATURE_COMPONENT] + move |elts| elts[subject].representation().with( + |rep| rep[Sphere::CURVATURE_COMPONENT] ) ); @@ -249,7 +391,7 @@ impl Regulator for HalfCurvatureRegulator { match self.set_point.with(|set_pt| set_pt.value) { Some(half_curv) => { let representation = assembly.elements.with_untracked( - |elts| elts[self.subject].representation + |elts| elts[self.subject].representation() ); representation.update( |rep| change_half_curvature(rep, half_curv) @@ -262,13 +404,13 @@ impl Regulator for HalfCurvatureRegulator { } impl ProblemPoser for HalfCurvatureRegulator { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { - let col = elts[self.subject].column_index.expect( + let col = elts[self.subject].column_index().expect( "Subject should be indexed before half-curvature regulator writes problem data" ); - problem.frozen.push(Element::CURVATURE_COMPONENT, col, val); + problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val); } }); } @@ -286,7 +428,7 @@ type AssemblyMotion<'a> = Vec>; #[derive(Clone)] pub struct Assembly { // elements and regulators - pub elements: Signal>, + pub elements: Signal>>, pub regulators: Signal>>, // solution variety tangent space. the basis vectors are stored in @@ -317,66 +459,61 @@ impl Assembly { // --- inserting elements and regulators --- - // insert a sphere into the assembly without checking whether we already + // insert an element into the assembly without checking whether we already // have an element with the same identifier. any element that does have the // same identifier will get kicked out of the `elements_by_id` index - fn insert_sphere_unchecked(&self, elt: Element) -> ElementKey { - // insert the sphere - let id = elt.id.clone(); - let key = self.elements.update(|elts| elts.insert(elt)); + fn insert_element_unchecked(&self, elt: T) -> ElementKey { + // insert the element + let id = elt.id().clone(); + let key = self.elements.update(|elts| elts.insert(Rc::new(elt))); self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); - // regulate the sphere's curvature - self.insert_regulator(HalfCurvatureRegulator::new(key, &self)); + // create and insert the element's default regulators + for reg in T::default_regulators(key, &self) { + self.insert_regulator(reg); + } key } - pub fn try_insert_sphere(&self, elt: Element) -> Option { + pub fn try_insert_element(&self, elt: impl Element + 'static) -> Option { let can_insert = self.elements_by_id.with_untracked( - |elts_by_id| !elts_by_id.contains_key(&elt.id) + |elts_by_id| !elts_by_id.contains_key(elt.id()) ); if can_insert { - Some(self.insert_sphere_unchecked(elt)) + Some(self.insert_element_unchecked(elt)) } else { None } } - pub fn insert_new_sphere(&self) { + pub fn insert_element_default(&self) { // find the next unused identifier in the default sequence + let default_id = T::default_id(); let mut id_num = 1; - let mut id = format!("sphere{}", id_num); + let mut id = format!("{default_id}{id_num}"); while self.elements_by_id.with_untracked( |elts_by_id| elts_by_id.contains_key(&id) ) { id_num += 1; - id = format!("sphere{}", id_num); + id = format!("{default_id}{id_num}"); } - // create and insert a sphere - let _ = self.insert_sphere_unchecked( - Element::new( - id, - format!("Sphere {}", id_num), - [0.75_f32, 0.75_f32, 0.75_f32], - sphere(0.0, 0.0, 0.0, 1.0) - ) - ); + // create and insert the default example of `T` + let _ = self.insert_element_unchecked(T::default(id, id_num)); } - pub fn insert_regulator(&self, regulator: T) { + pub fn insert_regulator(&self, regulator: Rc) { // add the regulator to the assembly's regulator list - let regulator_rc = Rc::new(regulator); let key = self.regulators.update( - |regs| regs.insert(regulator_rc.clone()) + |regs| regs.insert(regulator.clone()) ); // add the regulator to each subject's regulator list - let subjects = regulator_rc.subjects(); + let subjects = regulator.subjects(); let subject_regulators: Vec<_> = self.elements.with_untracked( |elts| subjects.into_iter().map( - |subj| elts[subj].regulators + |subj| elts[subj].regulators() ).collect() ); for regulators in subject_regulators { @@ -390,10 +527,10 @@ impl Assembly { /* DEBUG */ // log the regulator update console::log_1(&JsValue::from( - format!("Updated regulator with subjects {:?}", regulator_rc.subjects()) + format!("Updated regulator with subjects {:?}", regulator.subjects()) )); - if regulator_rc.try_activate(&self_for_effect) { + if regulator.try_activate(&self_for_effect) { self_for_effect.realize(); } }); @@ -427,7 +564,7 @@ impl Assembly { // index the elements self.elements.update_silent(|elts| { for (index, (_, elt)) in elts.into_iter().enumerate() { - elt.column_index = Some(index); + elt.set_column_index(index); } }); @@ -482,8 +619,8 @@ impl Assembly { if success { // 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())) + elt.representation().update( + |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) ); } @@ -521,8 +658,8 @@ impl Assembly { let mut next_column_index = realized_dim; for elt_motion in motion.iter() { let moving_elt = &mut elts[elt_motion.key]; - if moving_elt.column_index.is_none() { - moving_elt.column_index = Some(next_column_index); + if moving_elt.column_index().is_none() { + moving_elt.set_column_index(next_column_index); next_column_index += 1; } } @@ -539,7 +676,7 @@ impl Assembly { // we can unwrap the column index because we know that every moving // element has one at this point let column_index = self.elements.with_untracked( - |elts| elts[elt_motion.key].column_index.unwrap() + |elts| elts[elt_motion.key].column_index().unwrap() ); if column_index < realized_dim { @@ -555,7 +692,7 @@ impl Assembly { let mut target_column = motion_proj.column_mut(column_index); let unif_to_std = self.elements.with_untracked( |elts| { - elts[elt_motion.key].representation.with_untracked( + elts[elt_motion.key].representation().with_untracked( |rep| local_unif_to_std(rep.as_view()) ) } @@ -567,26 +704,27 @@ impl Assembly { // step the assembly along the deformation. this changes the elements' // normalizations, so we restore those afterward /* KLUDGE */ - // since our test assemblies only include spheres, we assume that every - // element is on the 1 mass shell + // for now, we only restore the normalizations of spheres for (_, elt) in self.elements.get_clone_untracked() { - elt.representation.update_silent(|rep| { - match elt.column_index { + elt.representation().update_silent(|rep| { + match elt.column_index() { Some(column_index) => { // step the assembly along the deformation *rep += motion_proj.column(column_index); - // restore normalization by contracting toward the last - // coordinate axis - let q_sp = rep.fixed_rows::<3>(0).norm_squared(); - let half_q_lt = -2.0 * rep[3] * rep[4]; - let half_q_lt_sq = half_q_lt * half_q_lt; - let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); - rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); + if elt.type_id() == TypeId::of::() { + // restore normalization by contracting toward the + // last coordinate axis + let q_sp = rep.fixed_rows::<3>(0).norm_squared(); + let half_q_lt = -2.0 * rep[3] * rep[4]; + let half_q_lt_sq = half_q_lt * half_q_lt; + let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); + rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); + } }, None => { console::log_1(&JsValue::from( - format!("No velocity to unpack for fresh element \"{}\"", elt.id) + format!("No velocity to unpack for fresh element \"{}\"", elt.id()) )) } }; @@ -602,20 +740,14 @@ impl Assembly { #[cfg(test)] mod tests { - use crate::engine; - use super::*; #[test] - #[should_panic(expected = "Element \"sphere\" should be indexed before writing problem data")] + #[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")] fn unindexed_element_test() { let _ = create_root(|| { - Element::new( - "sphere".to_string(), - "Sphere".to_string(), - [1.0_f32, 1.0_f32, 1.0_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0) - ).pose(&mut ConstraintProblem::new(1), &Slab::new()); + let elt = Sphere::default("sphere".to_string(), 0); + elt.pose(&mut ConstraintProblem::new(1), &Slab::new()); }); } @@ -623,18 +755,13 @@ mod tests { #[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")] fn unindexed_subject_test_inversive_distance() { let _ = create_root(|| { - let mut elts = Slab::new(); + let mut elts = Slab::>::new(); let subjects = [0, 1].map(|k| { elts.insert( - Element::new( - format!("sphere{k}"), - format!("Sphere {k}"), - [1.0_f32, 1.0_f32, 1.0_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0) - ) + Rc::new(Sphere::default(format!("sphere{k}"), k)) ) }); - elts[subjects[0]].column_index = Some(0); + elts[subjects[0]].set_column_index(0); InversiveDistanceRegulator { subjects: subjects, measurement: create_memo(|| 0.0), diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 4e0c7e4..51b207d 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -4,17 +4,185 @@ use sycamore::{prelude::*, motion::create_raf}; use web_sys::{ console, window, - Element, KeyboardEvent, MouseEvent, WebGl2RenderingContext, + WebGlBuffer, WebGlProgram, WebGlShader, WebGlUniformLocation, wasm_bindgen::{JsCast, JsValue} }; -use crate::{AppState, assembly::{ElementKey, ElementMotion}}; +use crate::{ + AppState, + assembly::{ElementKey, ElementColor, ElementMotion, Point, Sphere} +}; + +// --- scene data --- + +struct SceneSpheres { + representations: Vec>, + colors: Vec, + highlights: Vec +} + +impl SceneSpheres { + fn new() -> SceneSpheres{ + SceneSpheres { + representations: Vec::new(), + colors: Vec::new(), + highlights: Vec::new() + } + } + + fn len_i32(&self) -> i32 { + self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer") + } + + fn push(&mut self, representation: DVector, color: ElementColor, highlight: f32) { + self.representations.push(representation); + self.colors.push(color); + self.highlights.push(highlight); + } +} + +struct ScenePoints { + representations: Vec>, + colors: Vec, + highlights: Vec, + selections: Vec +} + +impl ScenePoints { + fn new() -> ScenePoints { + ScenePoints { + representations: Vec::new(), + colors: Vec::new(), + highlights: Vec::new(), + selections: Vec::new() + } + } + + fn push(&mut self, representation: DVector, color: ElementColor, highlight: f32, selected: bool) { + self.representations.push(representation); + self.colors.push(color); + self.highlights.push(highlight); + self.selections.push(if selected { 1.0 } else { 0.0 }); + } +} + +pub struct Scene { + spheres: SceneSpheres, + points: ScenePoints +} + +impl Scene { + fn new() -> Scene { + Scene { + spheres: SceneSpheres::new(), + points: ScenePoints::new() + } + } +} + +pub trait DisplayItem { + fn show(&self, scene: &mut Scene, selected: bool); + + // the smallest positive depth, represented as a multiple of `dir`, where + // the line generated by `dir` hits the element. returns `None` if the line + // misses the element + fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, pixel_size: f64) -> Option; +} + +impl DisplayItem for Sphere { + fn show(&self, scene: &mut Scene, selected: bool) { + const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ + let representation = self.representation.get_clone_untracked(); + let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; + let highlight = if selected { 1.0 } else { HIGHLIGHT }; + scene.spheres.push(representation, color, highlight); + } + + // this method should be kept synchronized with `sphere_cast` in + // `spheres.frag`, which does essentially the same thing on the GPU side + fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, _pixel_size: f64) -> Option { + // if `a/b` is less than this threshold, we approximate + // `a*u^2 + b*u + c` by the linear function `b*u + c` + const DEG_THRESHOLD: f64 = 1e-9; + + let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); + let a = -rep[3] * dir.norm_squared(); + let b = rep.rows_range(..3).dot(&dir); + let c = -rep[4]; + + let adjust = 4.0*a*c/(b*b); + if adjust < 1.0 { + // as long as `b` is non-zero, the linear approximation of + // + // a*u^2 + b*u + c + // + // at `u = 0` will reach zero at a finite depth `u_lin`. the root of + // the quadratic adjacent to `u_lin` is stored in `lin_root`. if + // both roots have the same sign, `lin_root` will be the one closer + // to `u = 0` + let square_rect_ratio = 1.0 + (1.0 - adjust).sqrt(); + let lin_root = -(2.0*c)/b / square_rect_ratio; + if a.abs() > DEG_THRESHOLD * b.abs() { + if lin_root > 0.0 { + Some(lin_root) + } else { + let other_root = -b/(2.*a) * square_rect_ratio; + (other_root > 0.0).then_some(other_root) + } + } else { + (lin_root > 0.0).then_some(lin_root) + } + } else { + // the line through `dir` misses the sphere completely + None + } + } +} + +impl DisplayItem for Point { + fn show(&self, scene: &mut Scene, selected: bool) { + const HIGHLIGHT: f32 = 0.5; /* SCAFFOLDING */ + let representation = self.representation.get_clone_untracked(); + let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; + let highlight = if selected { 1.0 } else { HIGHLIGHT }; + scene.points.push(representation, color, highlight, selected); + } + + /* SCAFFOLDING */ + fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, pixel_size: f64) -> Option { + let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); + if rep[2] < 0.0 { + // this constant should be kept synchronized with `point.frag` + const POINT_RADIUS_PX: f64 = 4.0; + + // find the radius of the point in screen projection units + let point_radius_proj = POINT_RADIUS_PX * pixel_size; + + // find the squared distance between the screen projections of the + // ray and the point + let dir_proj = -dir.fixed_rows::<2>(0) / dir[2]; + let rep_proj = -rep.fixed_rows::<2>(0) / rep[2]; + let dist_sq = (dir_proj - rep_proj).norm_squared(); + + // if the ray hits the point, return its depth + if dist_sq < point_radius_proj * point_radius_proj { + Some(rep[2] / dir[2]) + } else { + None + } + } else { + None + } + } +} + +// --- WebGL utilities --- fn compile_shader( context: &WebGl2RenderingContext, @@ -27,6 +195,45 @@ fn compile_shader( shader } +fn set_up_program( + context: &WebGl2RenderingContext, + vertex_shader_source: &str, + fragment_shader_source: &str +) -> WebGlProgram { + // compile the shaders + let vertex_shader = compile_shader( + &context, + WebGl2RenderingContext::VERTEX_SHADER, + vertex_shader_source, + ); + let fragment_shader = compile_shader( + &context, + WebGl2RenderingContext::FRAGMENT_SHADER, + fragment_shader_source, + ); + + // create the program and attach the shaders + let program = context.create_program().unwrap(); + context.attach_shader(&program, &vertex_shader); + context.attach_shader(&program, &fragment_shader); + context.link_program(&program); + + /* DEBUG */ + // report whether linking succeeded + let link_status = context + .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS) + .as_bool() + .unwrap(); + let link_msg = if link_status { + "Linked successfully" + } else { + "Linking failed" + }; + console::log_1(&JsValue::from(link_msg)); + + program +} + fn get_uniform_array_locations( context: &WebGl2RenderingContext, program: &WebGlProgram, @@ -42,22 +249,39 @@ fn get_uniform_array_locations( }) } -// load the given data into the vertex input of the given name -fn bind_vertex_attrib( +// bind the given vertex buffer object to the given vertex attribute +fn bind_to_attribute( context: &WebGl2RenderingContext, - index: u32, - size: i32, - data: &[f32] + attr_index: u32, + attr_size: i32, + buffer: &Option ) { - // create a data buffer and bind it to ARRAY_BUFFER - let buffer = context.create_buffer().unwrap(); - context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer)); + context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref()); + context.vertex_attrib_pointer_with_i32( + attr_index, + attr_size, + WebGl2RenderingContext::FLOAT, + false, // don't normalize + 0, // zero stride + 0, // zero offset + ); +} + +// load the given data into a new vertex buffer object +fn load_new_buffer( + context: &WebGl2RenderingContext, + data: &[f32] +) -> Option { + // create a buffer and bind it to ARRAY_BUFFER + let buffer = context.create_buffer(); + context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref()); - // load the given data into the buffer. the function `Float32Array::view` - // creates a raw view into our module's `WebAssembly.Memory` buffer. - // allocating more memory will change the buffer, invalidating the view. - // that means we have to make sure we don't allocate any memory until the - // view is dropped + // load the given data into the buffer. this block is unsafe because + // `Float32Array::view` creates a raw view into our module's + // `WebAssembly.Memory` buffer. allocating more memory will change the + // buffer, invalidating the view, so we have to make sure we don't allocate + // any memory until the view is dropped. we're okay here because the view is + // used as soon as it's created unsafe { context.buffer_data_with_array_buffer_view( WebGl2RenderingContext::ARRAY_BUFFER, @@ -66,42 +290,43 @@ fn bind_vertex_attrib( ); } - // allow the target attribute to be used - context.enable_vertex_attrib_array(index); - - // take whatever's bound to ARRAY_BUFFER---here, the data buffer created - // above---and bind it to the target attribute - // - // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer - // - context.vertex_attrib_pointer_with_i32( - index, - size, - WebGl2RenderingContext::FLOAT, - false, // don't normalize - 0, // zero stride - 0, // zero offset - ); + buffer +} + +fn bind_new_buffer_to_attribute( + context: &WebGl2RenderingContext, + attr_index: u32, + attr_size: i32, + data: &[f32] +) { + let buffer = load_new_buffer(context, data); + bind_to_attribute(context, attr_index, attr_size, &buffer); } // the direction in camera space that a mouse event is pointing along -fn event_dir(event: &MouseEvent) -> Vector3 { - let target: Element = event.target().unwrap().unchecked_into(); +fn event_dir(event: &MouseEvent) -> (Vector3, f64) { + let target: web_sys::Element = event.target().unwrap().unchecked_into(); let rect = target.get_bounding_client_rect(); let width = rect.width(); let height = rect.height(); let shortdim = width.min(height); - // this constant should be kept synchronized with `inversive.frag` + // this constant should be kept synchronized with `spheres.frag` and + // `point.vert` const FOCAL_SLOPE: f64 = 0.3; - Vector3::new( - FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim, - FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim, - -1.0 + ( + Vector3::new( + FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim, + FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim, + -1.0 + ), + FOCAL_SLOPE * 2.0 / shortdim ) } +// --- display component --- + #[component] pub fn Display() -> View { let state = use_context::(); @@ -138,7 +363,7 @@ pub fn Display() -> View { create_effect(move || { state.assembly.elements.with(|elts| { for (_, elt) in elts { - elt.representation.track(); + elt.representation().track(); } }); state.selection.track(); @@ -170,7 +395,6 @@ pub fn Display() -> View { // display parameters const OPACITY: f32 = 0.5; /* SCAFFOLDING */ - const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ const LAYER_THRESHOLD: i32 = 0; /* DEBUG */ const DEBUG_MODE: i32 = 0; /* DEBUG */ @@ -186,32 +410,26 @@ pub fn Display() -> View { .dyn_into::() .unwrap(); - // compile and attach the vertex and fragment shaders - let vertex_shader = compile_shader( + // disable depth testing + ctx.disable(WebGl2RenderingContext::DEPTH_TEST); + + // set blend mode + ctx.enable(WebGl2RenderingContext::BLEND); + ctx.blend_func(WebGl2RenderingContext::SRC_ALPHA, WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA); + + // set up the sphere rendering program + let sphere_program = set_up_program( &ctx, - WebGl2RenderingContext::VERTEX_SHADER, include_str!("identity.vert"), + include_str!("spheres.frag") ); - let fragment_shader = compile_shader( + + // set up the point rendering program + let point_program = set_up_program( &ctx, - WebGl2RenderingContext::FRAGMENT_SHADER, - include_str!("inversive.frag"), + include_str!("point.vert"), + include_str!("point.frag") ); - let program = ctx.create_program().unwrap(); - ctx.attach_shader(&program, &vertex_shader); - ctx.attach_shader(&program, &fragment_shader); - ctx.link_program(&program); - let link_status = ctx - .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS) - .as_bool() - .unwrap(); - let link_msg = if link_status { - "Linked successfully" - } else { - "Linking failed" - }; - console::log_1(&JsValue::from(link_msg)); - ctx.use_program(Some(&program)); /* DEBUG */ // print the maximum number of vectors that can be passed as @@ -230,35 +448,33 @@ pub fn Display() -> View { &JsValue::from("uniform vectors available") ); - // find indices of vertex attributes and uniforms + // find the sphere program's vertex attribute + let viewport_position_attr = ctx.get_attrib_location(&sphere_program, "position") as u32; + + // find the sphere program's uniforms const SPHERE_MAX: usize = 200; - let position_index = ctx.get_attrib_location(&program, "position") as u32; - let sphere_cnt_loc = ctx.get_uniform_location(&program, "sphere_cnt"); + let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt"); let sphere_sp_locs = get_uniform_array_locations::( - &ctx, &program, "sphere_list", Some("sp") + &ctx, &sphere_program, "sphere_list", Some("sp") ); let sphere_lt_locs = get_uniform_array_locations::( - &ctx, &program, "sphere_list", Some("lt") + &ctx, &sphere_program, "sphere_list", Some("lt") ); - let color_locs = get_uniform_array_locations::( - &ctx, &program, "color_list", None + let sphere_color_locs = get_uniform_array_locations::( + &ctx, &sphere_program, "color_list", None ); - let highlight_locs = get_uniform_array_locations::( - &ctx, &program, "highlight_list", None + let sphere_highlight_locs = get_uniform_array_locations::( + &ctx, &sphere_program, "highlight_list", None ); - let resolution_loc = ctx.get_uniform_location(&program, "resolution"); - let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); - let opacity_loc = ctx.get_uniform_location(&program, "opacity"); - let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); - let debug_mode_loc = ctx.get_uniform_location(&program, "debug_mode"); + let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution"); + let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim"); + let opacity_loc = ctx.get_uniform_location(&sphere_program, "opacity"); + let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold"); + let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode"); - // create a vertex array and bind it to the graphics context - let vertex_array = ctx.create_vertex_array().unwrap(); - ctx.bind_vertex_array(Some(&vertex_array)); - - // set the vertex positions + // load the viewport vertex positions into a new vertex buffer object const VERTEX_CNT: usize = 6; - let positions: [f32; 3*VERTEX_CNT] = [ + let viewport_positions: [f32; 3*VERTEX_CNT] = [ // northwest triangle -1.0, -1.0, 0.0, -1.0, 1.0, 0.0, @@ -268,7 +484,13 @@ pub fn Display() -> View { 1.0, 1.0, 0.0, 1.0, -1.0, 0.0 ]; - bind_vertex_attrib(&ctx, position_index, 3, &positions); + let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions); + + // find the point program's vertex attributes + let point_position_attr = ctx.get_attrib_location(&point_program, "position") as u32; + let point_color_attr = ctx.get_attrib_location(&point_program, "color") as u32; + let point_highlight_attr = ctx.get_attrib_location(&point_program, "highlight") as u32; + let point_selection_attr = ctx.get_attrib_location(&point_program, "selected") as u32; // set up a repainting routine let (_, start_animation_loop, _) = create_raf(move || { @@ -362,6 +584,9 @@ pub fn Display() -> View { } if scene_changed.get() { + const SPACE_DIM: usize = 3; + const COLOR_SIZE: usize = 3; + /* INSTRUMENTS */ // measure mean frame interval frames_since_last_sample += 1; @@ -371,6 +596,10 @@ pub fn Display() -> View { frames_since_last_sample = 0; } + // --- get the assembly --- + + let mut scene = Scene::new(); + // find the map from assembly space to world space let location = { let u = -location_z; @@ -384,41 +613,27 @@ pub fn Display() -> View { }; let asm_to_world = &location * &orientation; - // get the assembly - let ( - elt_cnt, - reps_world, - colors, - highlights - ) = state.assembly.elements.with(|elts| { - ( - // number of elements - elts.len() as i32, - - // representation vectors in world coordinates - elts.iter().map( - |(_, elt)| elt.representation.with(|rep| &asm_to_world * rep) - ).collect::>(), - - // colors - elts.iter().map(|(key, elt)| { - if state.selection.with(|sel| sel.contains(&key)) { - elt.color.map(|ch| 0.2 + 0.8*ch) - } else { - elt.color - } - }).collect::>(), - - // highlight levels - elts.iter().map(|(key, _)| { - if state.selection.with(|sel| sel.contains(&key)) { - 1.0_f32 - } else { - HIGHLIGHT - } - }).collect::>() - ) - }); + // set up the scene + state.assembly.elements.with_untracked( + |elts| for (key, elt) in elts { + let selected = state.selection.with(|sel| sel.contains(&key)); + elt.show(&mut scene, selected); + } + ); + let sphere_cnt = scene.spheres.len_i32(); + + // --- draw the spheres --- + + // use the sphere rendering program + ctx.use_program(Some(&sphere_program)); + + // enable the sphere program's vertex attribute + ctx.enable_vertex_attrib_array(viewport_position_attr); + + // write the spheres in world coordinates + let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map( + |rep| (&asm_to_world * rep).cast::() + ).collect(); // set the resolution let width = canvas.width() as f32; @@ -426,25 +641,25 @@ pub fn Display() -> View { ctx.uniform2f(resolution_loc.as_ref(), width, height); ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); - // pass the assembly - ctx.uniform1i(sphere_cnt_loc.as_ref(), elt_cnt); - for n in 0..reps_world.len() { - let v = &reps_world[n]; - ctx.uniform3f( + // pass the scene data + ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt); + for n in 0..sphere_reps_world.len() { + let v = &sphere_reps_world[n]; + ctx.uniform3fv_with_f32_array( sphere_sp_locs[n].as_ref(), - v[0] as f32, v[1] as f32, v[2] as f32 + v.rows(0, 3).as_slice() ); - ctx.uniform2f( + ctx.uniform2fv_with_f32_array( sphere_lt_locs[n].as_ref(), - v[3] as f32, v[4] as f32 + v.rows(3, 2).as_slice() ); ctx.uniform3fv_with_f32_array( - color_locs[n].as_ref(), - &colors[n] + sphere_color_locs[n].as_ref(), + &scene.spheres.colors[n] ); ctx.uniform1f( - highlight_locs[n].as_ref(), - highlights[n] + sphere_highlight_locs[n].as_ref(), + scene.spheres.highlights[n] ); } @@ -453,9 +668,56 @@ pub fn Display() -> View { ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); + // bind the viewport vertex position buffer to the position + // attribute in the vertex shader + bind_to_attribute(&ctx, viewport_position_attr, SPACE_DIM as i32, &viewport_position_buffer); + // draw the scene ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); + // disable the sphere program's vertex attribute + ctx.disable_vertex_attrib_array(viewport_position_attr); + + // --- draw the points --- + + if !scene.points.representations.is_empty() { + // use the point rendering program + ctx.use_program(Some(&point_program)); + + // enable the point program's vertex attributes + ctx.enable_vertex_attrib_array(point_position_attr); + ctx.enable_vertex_attrib_array(point_color_attr); + ctx.enable_vertex_attrib_array(point_highlight_attr); + ctx.enable_vertex_attrib_array(point_selection_attr); + + // write the points in world coordinates + let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM); + let point_positions = DMatrix::from_columns( + &scene.points.representations.into_iter().map( + |rep| &asm_to_world_sp * rep + ).collect::>().as_slice() + ).cast::(); + + // load the point positions and colors into new buffers and + // bind them to the corresponding attributes in the vertex + // shader + bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice()); + bind_new_buffer_to_attribute(&ctx, point_color_attr, COLOR_SIZE as i32, scene.points.colors.concat().as_slice()); + bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice()); + bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice()); + + // draw the scene + ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32); + + // disable the point program's vertex attributes + ctx.disable_vertex_attrib_array(point_position_attr); + ctx.disable_vertex_attrib_array(point_color_attr); + ctx.disable_vertex_attrib_array(point_highlight_attr); + ctx.disable_vertex_attrib_array(point_selection_attr); + } + + // --- update the display state --- + // update the viewpoint assembly_to_world.set(asm_to_world); @@ -585,11 +847,11 @@ pub fn Display() -> View { }, on:click=move |event: MouseEvent| { // find the nearest element along the pointer direction - let dir = event_dir(&event); + let (dir, pixel_size) = event_dir(&event); console::log_1(&JsValue::from(dir.to_string())); let mut clicked: Option<(ElementKey, f64)> = None; for (key, elt) in state.assembly.elements.get_clone_untracked() { - match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world)) { + match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) { Some(depth) => match clicked { Some((_, best_depth)) => { if depth < best_depth { diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 869a7de..b0fa23d 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -4,7 +4,6 @@ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ // --- elements --- -#[cfg(feature = "dev")] pub fn point(x: f64, y: f64, z: f64) -> DVector { DVector::from_column_slice(&[x, y, z, 0.5, 0.5*(x*x + y*y + z*z)]) } diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 2446337..2893b6d 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -9,9 +9,10 @@ use web_sys::{ use crate::{ AppState, - assembly, assembly::{ + Element, ElementKey, + ElementRc, HalfCurvatureRegulator, InversiveDistanceRegulator, Regulator, @@ -103,7 +104,7 @@ impl OutlineItem for InversiveDistanceRegulator { self.subjects[0] }; let other_subject_label = state.assembly.elements.with( - |elts| elts[other_subject].label.clone() + |elts| elts[other_subject].label().clone() ); view! { li(class="regulator") { @@ -141,14 +142,15 @@ fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> // a list item that shows an element in an outline view of an assembly #[component(inline_props)] -fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { +fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { let state = use_context::(); let class = state.selection.map( move |sel| if sel.contains(&key) { "selected" } else { "" } ); - let label = element.label.clone(); + let label = element.label().clone(); + let representation = element.representation().clone(); let rep_components = move || { - element.representation.with( + representation.with( |rep| rep.iter().map( |u| { let u_str = format!("{:.3}", u).replace("-", "\u{2212}"); @@ -157,8 +159,8 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { ).collect::>() ) }; - let regulated = element.regulators.map(|regs| regs.len() > 0); - let regulator_list = element.regulators.map( + let regulated = element.regulators().map(|regs| regs.len() > 0); + let regulator_list = element.regulators().map( move |elt_reg_keys| elt_reg_keys .clone() .into_iter() @@ -261,7 +263,8 @@ pub fn Outline() -> View { |elts| elts .clone() .into_iter() - .sorted_by_key(|(_, elt)| elt.id.clone()) + .sorted_by_key(|(_, elt)| elt.id().clone()) + .map(|(key, elt)| (key, ElementRc(elt))) .collect() ); @@ -275,10 +278,10 @@ pub fn Outline() -> View { ) { Keyed( list=element_list, - view=|(key, elt)| view! { + view=|(key, ElementRc(elt))| view! { ElementOutlineItem(key=key, element=elt) }, - key=|(_, elt)| elt.serial + key=|(_, ElementRc(elt))| elt.serial() ) } } diff --git a/app-proto/src/point.frag b/app-proto/src/point.frag new file mode 100644 index 0000000..3a361a8 --- /dev/null +++ b/app-proto/src/point.frag @@ -0,0 +1,18 @@ +#version 300 es + +precision highp float; + +in vec3 point_color; +in float point_highlight; +in float total_radius; + +out vec4 outColor; + +void main() { + float r = total_radius * length(2.*gl_PointCoord - vec2(1.)); + + const float POINT_RADIUS = 4.; + float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r); + vec3 color = mix(point_color, vec3(1.), border * point_highlight); + outColor = vec4(color, 1. - smoothstep(total_radius - 1., total_radius, r)); +} \ No newline at end of file diff --git a/app-proto/src/point.vert b/app-proto/src/point.vert new file mode 100644 index 0000000..6945010 --- /dev/null +++ b/app-proto/src/point.vert @@ -0,0 +1,24 @@ +#version 300 es + +in vec4 position; +in vec3 color; +in float highlight; +in float selected; + +out vec3 point_color; +out float point_highlight; +out float total_radius; + +// camera +const float focal_slope = 0.3; + +void main() { + total_radius = 5. + 0.5*selected; + + float depth = -focal_slope * position.z; + gl_Position = vec4(position.xy / depth, 0., 1.); + gl_PointSize = 2.*total_radius; + + point_color = color; + point_highlight = highlight; +} \ No newline at end of file diff --git a/app-proto/src/inversive.frag b/app-proto/src/spheres.frag similarity index 100% rename from app-proto/src/inversive.frag rename to app-proto/src/spheres.frag From 2adf4669f47ab8f2bff8d64ac011a2bd09f632fa Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Tue, 6 May 2025 19:17:30 +0000 Subject: [PATCH 32/41] Refactor: Use pointers to refer to elements and regulators (#84) Previously, dyna3 used storage keys to refer to elements, necessitating passing around element containers to various functions so that they could access the relevant elements. These storage keys have been replaced with reference-counted pointers, used for tasks like these: - Specifying the subjects of regulators. - Collecting the regulators each element is subject to - Handling selection. - Creating interface components. Also, systematizes the handling of serial numbers for entities, through a Serial trait. And updates to rust 1.86 and institutes explicit checking of the rust version. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/84 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- .../workflows/continuous-integration.yaml | 2 +- app-proto/Cargo.lock | 19 +- app-proto/Cargo.toml | 3 +- app-proto/src/add_remove.rs | 2 +- app-proto/src/assembly.rs | 396 ++++++++++-------- app-proto/src/display.rs | 23 +- app-proto/src/main.rs | 21 +- app-proto/src/outline.rs | 92 ++-- 8 files changed, 288 insertions(+), 270 deletions(-) diff --git a/.forgejo/workflows/continuous-integration.yaml b/.forgejo/workflows/continuous-integration.yaml index daf8923..f3b0130 100644 --- a/.forgejo/workflows/continuous-integration.yaml +++ b/.forgejo/workflows/continuous-integration.yaml @@ -11,7 +11,7 @@ jobs: test: runs-on: docker container: - image: cimg/rust:1.85-node + image: cimg/rust:1.86-node defaults: run: # set the default working directory for each `run` step, relative to the diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 9738589..3bf609c 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ahash" @@ -89,8 +89,6 @@ dependencies = [ "lazy_static", "nalgebra", "readonly", - "rustc-hash", - "slab", "sycamore", "wasm-bindgen-test", "web-sys", @@ -365,12 +363,6 @@ dependencies = [ "syn", ] -[[package]] -name = "rustc-hash" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" - [[package]] name = "safe_arch" version = "0.7.2" @@ -414,15 +406,6 @@ dependencies = [ "wide", ] -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - [[package]] name = "slotmap" version = "1.0.7" diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 5ab7299..844a0a6 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -3,6 +3,7 @@ name = "dyna3" version = "0.1.0" authors = ["Aaron Fenyes", "Glen Whitney"] edition = "2021" +rust-version = "1.86" [features] default = ["console_error_panic_hook"] @@ -14,8 +15,6 @@ js-sys = "0.3.70" lazy_static = "1.5.0" nalgebra = "0.33.0" readonly = "0.2.12" -rustc-hash = "2.0.0" -slab = "0.4.9" sycamore = "0.9.0-beta.3" # The `console_error_panic_hook` crate provides better debugging of panics by diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index ea86186..f3bbc97 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -241,7 +241,7 @@ pub fn AddRemove() -> View { .unwrap() ); state.assembly.insert_regulator( - Rc::new(InversiveDistanceRegulator::new(subjects, &state.assembly)) + Rc::new(InversiveDistanceRegulator::new(subjects)) ); state.selection.update(|sel| sel.clear()); } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 343cef8..bd185c8 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,12 +1,14 @@ use nalgebra::{DMatrix, DVector, DVectorView}; -use rustc_hash::FxHashMap; -use slab::Slab; use std::{ any::{Any, TypeId}, cell::Cell, - collections::BTreeSet, + collections::{BTreeMap, BTreeSet}, + cmp::Ordering, + fmt, + fmt::{Debug, Formatter}, + hash::{Hash, Hasher}, rc::Rc, - sync::atomic::{AtomicU64, Ordering} + sync::{atomic, atomic::AtomicU64} }; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ @@ -27,49 +29,16 @@ use crate::{ specified::SpecifiedValue }; -// the types of the keys we use to access an assembly's elements and regulators -pub type ElementKey = usize; -pub type RegulatorKey = usize; - pub type ElementColor = [f32; 3]; /* KLUDGE */ // we should reconsider this design when we build a system for switching between // assemblies. at that point, we might want to switch to hierarchical keys, -// where each each element has a key that identifies it within its assembly and +// where each each item has a key that identifies it within its assembly and // each assembly has a key that identifies it within the sesssion -static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0); +static NEXT_SERIAL: AtomicU64 = AtomicU64::new(0); -pub trait ProblemPoser { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>); -} - -pub trait Element: ProblemPoser + DisplayItem { - // the default identifier for an element of this type - fn default_id() -> String where Self: Sized; - - // create the default example of an element of this type - fn default(id: String, id_num: u64) -> Self where Self: Sized; - - // the regulators that should be created when an element of this type is - // inserted into the given assembly with the given storage key - /* KLUDGE */ - // right now, this organization makes sense because regulators identify - // their subjects by storage key, so the element has to be inserted before - // its regulators can be created. if we change the way regulators identify - // their subjects, we should consider refactoring - fn default_regulators(_key: ElementKey, _assembly: &Assembly) -> Vec> where Self: Sized { - Vec::new() - } - - fn id(&self) -> &String; - fn label(&self) -> &String; - fn representation(&self) -> Signal>; - - // the regulators the element is subject to. the assembly that owns the - // element is responsible for keeping this set up to date - fn regulators(&self) -> Signal>; - +pub trait Serial { // a serial number that uniquely identifies this element fn serial(&self) -> u64; @@ -80,11 +49,62 @@ pub trait Element: ProblemPoser + DisplayItem { // // https://marabos.nl/atomics/atomics.html#example-handle-overflow // - NEXT_ELEMENT_SERIAL.fetch_update( - Ordering::SeqCst, Ordering::SeqCst, + NEXT_SERIAL.fetch_update( + atomic::Ordering::SeqCst, atomic::Ordering::SeqCst, |serial| serial.checked_add(1) ).expect("Out of serial numbers for elements") } +} + +impl Hash for dyn Serial { + fn hash(&self, state: &mut H) { + self.serial().hash(state) + } +} + +impl PartialEq for dyn Serial { + fn eq(&self, other: &Self) -> bool { + self.serial() == other.serial() + } +} + +impl Eq for dyn Serial {} + +impl PartialOrd for dyn Serial { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for dyn Serial { + fn cmp(&self, other: &Self) -> Ordering { + self.serial().cmp(&other.serial()) + } +} + +pub trait ProblemPoser { + fn pose(&self, problem: &mut ConstraintProblem); +} + +pub trait Element: Serial + ProblemPoser + DisplayItem { + // the default identifier for an element of this type + fn default_id() -> String where Self: Sized; + + // the default example of an element of this type + fn default(id: String, id_num: u64) -> Self where Self: Sized; + + // the default regulators that come with this element + fn default_regulators(self: Rc) -> Vec> { + Vec::new() + } + + fn id(&self) -> &String; + fn label(&self) -> &String; + fn representation(&self) -> Signal>; + + // the regulators the element is subject to. the assembly that owns the + // element is responsible for keeping this set up to date + fn regulators(&self) -> Signal>>; // the configuration matrix column index that was assigned to the element // last time the assembly was realized, or `None` if the element has never @@ -97,17 +117,35 @@ pub trait Element: ProblemPoser + DisplayItem { fn set_column_index(&self, index: usize); } -// the `Element` trait needs to be dyn-compatible, so its method signatures can -// only use `Self` in the type of the receiver. that means `Element` can't -// implement `PartialEq`. if you need partial equivalence for `Element` trait -// objects, use this wrapper -#[derive(Clone)] -pub struct ElementRc(pub Rc); +impl Debug for dyn Element { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { + self.id().fmt(f) + } +} -impl PartialEq for ElementRc { - fn eq(&self, ElementRc(other): &Self) -> bool { - let ElementRc(rc) = self; - Rc::ptr_eq(rc, &other) +impl Hash for dyn Element { + fn hash(&self, state: &mut H) { + ::hash(self, state) + } +} + +impl PartialEq for dyn Element { + fn eq(&self, other: &Self) -> bool { + ::eq(self, other) + } +} + +impl Eq for dyn Element {} + +impl PartialOrd for dyn Element { + fn partial_cmp(&self, other: &Self) -> Option { + ::partial_cmp(self, other) + } +} + +impl Ord for dyn Element { + fn cmp(&self, other: &Self) -> Ordering { + ::cmp(self, other) } } @@ -116,8 +154,8 @@ pub struct Sphere { pub label: String, pub color: ElementColor, pub representation: Signal>, - pub regulators: Signal>, - pub serial: u64, + pub regulators: Signal>>, + serial: u64, column_index: Cell> } @@ -135,7 +173,7 @@ impl Sphere { label: label, color: color, representation: create_signal(representation), - regulators: create_signal(BTreeSet::default()), + regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), column_index: None.into() } @@ -156,8 +194,8 @@ impl Element for Sphere { ) } - fn default_regulators(key: ElementKey, assembly: &Assembly) -> Vec> { - vec![Rc::new(HalfCurvatureRegulator::new(key, assembly))] + fn default_regulators(self: Rc) -> Vec> { + vec![Rc::new(HalfCurvatureRegulator::new(self))] } fn id(&self) -> &String { @@ -172,14 +210,10 @@ impl Element for Sphere { self.representation } - fn regulators(&self) -> Signal> { + fn regulators(&self) -> Signal>> { self.regulators } - fn serial(&self) -> u64 { - self.serial - } - fn column_index(&self) -> Option { self.column_index.get() } @@ -189,8 +223,14 @@ impl Element for Sphere { } } +impl Serial for Sphere { + fn serial(&self) -> u64 { + self.serial + } +} + impl ProblemPoser for Sphere { - fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab>) { + 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() ); @@ -204,8 +244,8 @@ pub struct Point { pub label: String, pub color: ElementColor, pub representation: Signal>, - pub regulators: Signal>, - pub serial: u64, + pub regulators: Signal>>, + serial: u64, column_index: Cell> } @@ -223,7 +263,7 @@ impl Point { label, color, representation: create_signal(representation), - regulators: create_signal(BTreeSet::default()), + regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), column_index: None.into() } @@ -256,14 +296,10 @@ impl Element for Point { self.representation } - fn regulators(&self) -> Signal> { + fn regulators(&self) -> Signal>> { self.regulators } - fn serial(&self) -> u64 { - self.serial - } - fn column_index(&self) -> Option { self.column_index.get() } @@ -273,8 +309,14 @@ impl Element for Point { } } +impl Serial for Point { + fn serial(&self) -> u64 { + self.serial + } +} + impl ProblemPoser for Point { - fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab>) { + 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() ); @@ -284,8 +326,8 @@ impl ProblemPoser for Point { } } -pub trait Regulator: ProblemPoser + OutlineItem { - fn subjects(&self) -> Vec; +pub trait Regulator: Serial + ProblemPoser + OutlineItem { + fn subjects(&self) -> Vec>; fn measurement(&self) -> ReadSignal; fn set_point(&self) -> Signal; @@ -295,39 +337,65 @@ pub trait Regulator: ProblemPoser + OutlineItem { // 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, _assembly: &Assembly) -> bool { + fn try_activate(&self) -> bool { self.set_point().with(|set_pt| set_pt.is_present()) } } +impl Hash for dyn Regulator { + fn hash(&self, state: &mut H) { + ::hash(self, state) + } +} + +impl PartialEq for dyn Regulator { + fn eq(&self, other: &Self) -> bool { + ::eq(self, other) + } +} + +impl Eq for dyn Regulator {} + +impl PartialOrd for dyn Regulator { + fn partial_cmp(&self, other: &Self) -> Option { + ::partial_cmp(self, other) + } +} + +impl Ord for dyn Regulator { + fn cmp(&self, other: &Self) -> Ordering { + ::cmp(self, other) + } +} + pub struct InversiveDistanceRegulator { - pub subjects: [ElementKey; 2], + pub subjects: [Rc; 2], pub measurement: ReadSignal, - pub set_point: Signal + pub set_point: Signal, + serial: u64 } impl InversiveDistanceRegulator { - pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> InversiveDistanceRegulator { - let measurement = assembly.elements.map( - move |elts| { - let representations = subjects.map(|subj| elts[subj].representation()); - representations[0].with(|rep_0| - representations[1].with(|rep_1| - rep_0.dot(&(&*Q * rep_1)) - ) + pub fn new(subjects: [Rc; 2]) -> InversiveDistanceRegulator { + let representations = subjects.each_ref().map(|subj| subj.representation()); + let measurement = create_memo(move || { + representations[0].with(|rep_0| + representations[1].with(|rep_1| + rep_0.dot(&(&*Q * rep_1)) ) - } - ); + ) + }); let set_point = create_signal(SpecifiedValue::from_empty_spec()); + let serial = Self::next_serial(); - InversiveDistanceRegulator { subjects, measurement, set_point } + InversiveDistanceRegulator { subjects, measurement, set_point, serial } } } impl Regulator for InversiveDistanceRegulator { - fn subjects(&self) -> Vec { - self.subjects.into() + fn subjects(&self) -> Vec> { + self.subjects.clone().into() } fn measurement(&self) -> ReadSignal { @@ -339,12 +407,18 @@ impl Regulator for InversiveDistanceRegulator { } } +impl Serial for InversiveDistanceRegulator { + fn serial(&self) -> u64 { + self.serial + } +} + impl ProblemPoser for InversiveDistanceRegulator { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>) { + fn pose(&self, problem: &mut ConstraintProblem) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { - let [row, col] = self.subjects.map( - |subj| elts[subj].column_index().expect( + let [row, col] = self.subjects.each_ref().map( + |subj| subj.column_index().expect( "Subjects should be indexed before inversive distance regulator writes problem data" ) ); @@ -355,28 +429,28 @@ impl ProblemPoser for InversiveDistanceRegulator { } pub struct HalfCurvatureRegulator { - pub subject: ElementKey, + pub subject: Rc, pub measurement: ReadSignal, - pub set_point: Signal + pub set_point: Signal, + serial: u64 } impl HalfCurvatureRegulator { - pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator { - let measurement = assembly.elements.map( - move |elts| elts[subject].representation().with( - |rep| rep[Sphere::CURVATURE_COMPONENT] - ) + pub fn new(subject: Rc) -> HalfCurvatureRegulator { + let measurement = subject.representation().map( + |rep| rep[Sphere::CURVATURE_COMPONENT] ); let set_point = create_signal(SpecifiedValue::from_empty_spec()); + let serial = Self::next_serial(); - HalfCurvatureRegulator { subject, measurement, set_point } + HalfCurvatureRegulator { subject, measurement, set_point, serial } } } impl Regulator for HalfCurvatureRegulator { - fn subjects(&self) -> Vec { - vec![self.subject] + fn subjects(&self) -> Vec> { + vec![self.subject.clone()] } fn measurement(&self) -> ReadSignal { @@ -387,13 +461,10 @@ impl Regulator for HalfCurvatureRegulator { self.set_point } - fn try_activate(&self, assembly: &Assembly) -> bool { + fn try_activate(&self) -> bool { match self.set_point.with(|set_pt| set_pt.value) { Some(half_curv) => { - let representation = assembly.elements.with_untracked( - |elts| elts[self.subject].representation() - ); - representation.update( + self.subject.representation().update( |rep| change_half_curvature(rep, half_curv) ); true @@ -403,11 +474,17 @@ impl Regulator for HalfCurvatureRegulator { } } +impl Serial for HalfCurvatureRegulator { + fn serial(&self) -> u64 { + self.serial + } +} + impl ProblemPoser for HalfCurvatureRegulator { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>) { + fn pose(&self, problem: &mut ConstraintProblem) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { - let col = elts[self.subject].column_index().expect( + let col = self.subject.column_index().expect( "Subject should be indexed before half-curvature regulator writes problem data" ); problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val); @@ -418,7 +495,7 @@ impl ProblemPoser for HalfCurvatureRegulator { // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { - pub key: ElementKey, + pub element: Rc, pub velocity: DVectorView<'a, f64> } @@ -428,8 +505,8 @@ type AssemblyMotion<'a> = Vec>; #[derive(Clone)] pub struct Assembly { // elements and regulators - pub elements: Signal>>, - pub regulators: Signal>>, + pub elements: Signal>>, + pub regulators: Signal>>, // solution variety tangent space. the basis vectors are stored in // configuration matrix format, ordered according to the elements' column @@ -444,16 +521,16 @@ pub struct Assembly { pub tangent: Signal, // indexing - pub elements_by_id: Signal> + pub elements_by_id: Signal>> } impl Assembly { pub fn new() -> Assembly { Assembly { - elements: create_signal(Slab::new()), - regulators: create_signal(Slab::new()), + elements: create_signal(BTreeSet::new()), + regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), - elements_by_id: create_signal(FxHashMap::default()) + elements_by_id: create_signal(BTreeMap::default()) } } @@ -462,29 +539,27 @@ impl Assembly { // insert an element into the assembly without checking whether we already // have an element with the same identifier. any element that does have the // same identifier will get kicked out of the `elements_by_id` index - fn insert_element_unchecked(&self, elt: T) -> ElementKey { + fn insert_element_unchecked(&self, elt: impl Element + 'static) { // insert the element let id = elt.id().clone(); - let key = self.elements.update(|elts| elts.insert(Rc::new(elt))); - self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); + let elt_rc = Rc::new(elt); + self.elements.update(|elts| elts.insert(elt_rc.clone())); + self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, elt_rc.clone())); // create and insert the element's default regulators - for reg in T::default_regulators(key, &self) { + for reg in elt_rc.default_regulators() { self.insert_regulator(reg); } - - key } - pub fn try_insert_element(&self, elt: impl Element + 'static) -> Option { + pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool { let can_insert = self.elements_by_id.with_untracked( |elts_by_id| !elts_by_id.contains_key(elt.id()) ); if can_insert { - Some(self.insert_element_unchecked(elt)) - } else { - None + self.insert_element_unchecked(elt); } + can_insert } pub fn insert_element_default(&self) { @@ -505,19 +580,16 @@ impl Assembly { pub fn insert_regulator(&self, regulator: Rc) { // add the regulator to the assembly's regulator list - let key = self.regulators.update( + self.regulators.update( |regs| regs.insert(regulator.clone()) ); // add the regulator to each subject's regulator list - let subjects = regulator.subjects(); - let subject_regulators: Vec<_> = self.elements.with_untracked( - |elts| subjects.into_iter().map( - |subj| elts[subj].regulators() - ).collect() - ); + let subject_regulators: Vec<_> = regulator.subjects().into_iter().map( + |subj| subj.regulators() + ).collect(); for regulators in subject_regulators { - regulators.update(|regs| regs.insert(key)); + regulators.update(|regs| regs.insert(regulator.clone())); } // update the realization when the regulator becomes a constraint, or is @@ -530,7 +602,7 @@ impl Assembly { format!("Updated regulator with subjects {:?}", regulator.subjects()) )); - if regulator.try_activate(&self_for_effect) { + if regulator.try_activate() { self_for_effect.realize(); } }); @@ -539,7 +611,7 @@ impl Assembly { // print an updated list of regulators console::log_1(&JsValue::from("Regulators:")); self.regulators.with_untracked(|regs| { - for (_, reg) in regs.into_iter() { + for reg in regs.into_iter() { console::log_1(&JsValue::from(format!( " {:?}: {}", reg.subjects(), @@ -563,7 +635,7 @@ impl Assembly { pub fn realize(&self) { // index the elements self.elements.update_silent(|elts| { - for (index, (_, elt)) in elts.into_iter().enumerate() { + for (index, elt) in elts.iter().enumerate() { elt.set_column_index(index); } }); @@ -571,12 +643,12 @@ impl Assembly { // set up the constraint problem let problem = self.elements.with_untracked(|elts| { let mut problem = ConstraintProblem::new(elts.len()); - for (_, elt) in elts { - elt.pose(&mut problem, elts); + for elt in elts { + elt.pose(&mut problem); } self.regulators.with_untracked(|regs| { - for (_, reg) in regs { - reg.pose(&mut problem, elts); + for reg in regs { + reg.pose(&mut problem); } }); problem @@ -618,7 +690,7 @@ impl Assembly { if success { // read out the solution - for (_, elt) in self.elements.get_clone_untracked() { + for elt in self.elements.get_clone_untracked() { elt.representation().update( |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) ); @@ -654,17 +726,17 @@ impl Assembly { // in the process, we find out how many matrix columns we'll need to // hold the deformation let realized_dim = self.tangent.with(|tan| tan.assembly_dim()); - let motion_dim = self.elements.update_silent(|elts| { + let motion_dim = { let mut next_column_index = realized_dim; for elt_motion in motion.iter() { - let moving_elt = &mut elts[elt_motion.key]; + let moving_elt = &elt_motion.element; if moving_elt.column_index().is_none() { moving_elt.set_column_index(next_column_index); next_column_index += 1; } } next_column_index - }); + }; // project the element motions onto the tangent space of the solution // variety and sum them to get a deformation of the whole assembly. the @@ -675,9 +747,7 @@ impl Assembly { for elt_motion in motion { // we can unwrap the column index because we know that every moving // element has one at this point - let column_index = self.elements.with_untracked( - |elts| elts[elt_motion.key].column_index().unwrap() - ); + let column_index = elt_motion.element.column_index().unwrap(); if column_index < realized_dim { // this element had a column index when we started, so by @@ -690,12 +760,8 @@ impl Assembly { // this element didn't have a column index when we started, so // by invariant (2), it's unconstrained let mut target_column = motion_proj.column_mut(column_index); - let unif_to_std = self.elements.with_untracked( - |elts| { - elts[elt_motion.key].representation().with_untracked( - |rep| local_unif_to_std(rep.as_view()) - ) - } + let unif_to_std = elt_motion.element.representation().with_untracked( + |rep| local_unif_to_std(rep.as_view()) ); target_column += unif_to_std * elt_motion.velocity; } @@ -705,7 +771,7 @@ impl Assembly { // normalizations, so we restore those afterward /* KLUDGE */ // for now, we only restore the normalizations of spheres - for (_, elt) in self.elements.get_clone_untracked() { + for elt in self.elements.get_clone_untracked() { elt.representation().update_silent(|rep| { match elt.column_index() { Some(column_index) => { @@ -747,7 +813,7 @@ mod tests { fn unindexed_element_test() { let _ = create_root(|| { let elt = Sphere::default("sphere".to_string(), 0); - elt.pose(&mut ConstraintProblem::new(1), &Slab::new()); + elt.pose(&mut ConstraintProblem::new(1)); }); } @@ -755,18 +821,16 @@ mod tests { #[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")] fn unindexed_subject_test_inversive_distance() { let _ = create_root(|| { - let mut elts = Slab::>::new(); - let subjects = [0, 1].map(|k| { - elts.insert( - Rc::new(Sphere::default(format!("sphere{k}"), k)) - ) - }); - elts[subjects[0]].set_column_index(0); + let subjects = [0, 1].map( + |k| Rc::new(Sphere::default(format!("sphere{k}"), k)) as Rc + ); + subjects[0].set_column_index(0); InversiveDistanceRegulator { subjects: subjects, measurement: create_memo(|| 0.0), - set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()) - }.pose(&mut ConstraintProblem::new(2), &elts); + set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()), + serial: InversiveDistanceRegulator::next_serial() + }.pose(&mut ConstraintProblem::new(2)); }); } } \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 51b207d..a2fe4b6 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -1,5 +1,6 @@ use core::array; use nalgebra::{DMatrix, DVector, Rotation3, Vector3}; +use std::rc::Rc; use sycamore::{prelude::*, motion::create_raf}; use web_sys::{ console, @@ -16,7 +17,7 @@ use web_sys::{ use crate::{ AppState, - assembly::{ElementKey, ElementColor, ElementMotion, Point, Sphere} + assembly::{Element, ElementColor, ElementMotion, Point, Sphere} }; // --- scene data --- @@ -362,7 +363,7 @@ pub fn Display() -> View { let scene_changed = create_signal(true); create_effect(move || { state.assembly.elements.with(|elts| { - for (_, elt) in elts { + for elt in elts { elt.representation().track(); } }); @@ -548,7 +549,7 @@ pub fn Display() -> View { // manipulate the assembly if state.selection.with(|sel| sel.len() == 1) { let sel = state.selection.with( - |sel| *sel.into_iter().next().unwrap() + |sel| sel.into_iter().next().unwrap().clone() ); let translate_x = translate_pos_x_val - translate_neg_x_val; let translate_y = translate_pos_y_val - translate_neg_y_val; @@ -574,7 +575,7 @@ pub fn Display() -> View { assembly_for_raf.deform( vec![ ElementMotion { - key: sel, + element: sel, velocity: elt_motion.as_view() } ] @@ -615,8 +616,8 @@ pub fn Display() -> View { // set up the scene state.assembly.elements.with_untracked( - |elts| for (key, elt) in elts { - let selected = state.selection.with(|sel| sel.contains(&key)); + |elts| for elt in elts { + let selected = state.selection.with(|sel| sel.contains(elt)); elt.show(&mut scene, selected); } ); @@ -849,16 +850,16 @@ pub fn Display() -> View { // find the nearest element along the pointer direction let (dir, pixel_size) = event_dir(&event); console::log_1(&JsValue::from(dir.to_string())); - let mut clicked: Option<(ElementKey, f64)> = None; - for (key, elt) in state.assembly.elements.get_clone_untracked() { + let mut clicked: Option<(Rc, f64)> = None; + for elt in state.assembly.elements.get_clone_untracked() { match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) { Some(depth) => match clicked { Some((_, best_depth)) => { if depth < best_depth { - clicked = Some((key, depth)) + clicked = Some((elt, depth)) } }, - None => clicked = Some((key, depth)) + None => clicked = Some((elt, depth)) } None => () }; @@ -866,7 +867,7 @@ pub fn Display() -> View { // if we clicked something, select it match clicked { - Some((key, _)) => state.select(key, event.shift_key()), + Some((elt, _)) => state.select(&elt, event.shift_key()), None => state.selection.update(|sel| sel.clear()) }; } diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index e581997..b76859a 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -8,42 +8,41 @@ mod specified; #[cfg(test)] mod tests; -use rustc_hash::FxHashSet; +use std::{collections::BTreeSet, rc::Rc}; use sycamore::prelude::*; use add_remove::AddRemove; -use assembly::{Assembly, ElementKey}; +use assembly::{Assembly, Element}; use display::Display; use outline::Outline; #[derive(Clone)] struct AppState { assembly: Assembly, - selection: Signal> + selection: Signal>> } impl AppState { fn new() -> AppState { AppState { assembly: Assembly::new(), - selection: create_signal(FxHashSet::default()) + selection: create_signal(BTreeSet::default()) } } - // in single-selection mode, select the element with the given key. in - // multiple-selection mode, toggle whether the element with the given key - // is selected - fn select(&self, key: ElementKey, multi: bool) { + // in single-selection mode, select the given element. in multiple-selection + // mode, toggle whether the given element is selected + fn select(&self, element: &Rc, multi: bool) { if multi { self.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); + if !sel.remove(element) { + sel.insert(element.clone()); } }); } else { self.selection.update(|sel| { sel.clear(); - sel.insert(key); + sel.insert(element.clone()); }); } } diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 2893b6d..caf11e8 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -11,12 +11,9 @@ use crate::{ AppState, assembly::{ Element, - ElementKey, - ElementRc, HalfCurvatureRegulator, InversiveDistanceRegulator, - Regulator, - RegulatorKey + Regulator }, specified::SpecifiedValue }; @@ -92,20 +89,16 @@ fn RegulatorInput(regulator: Rc) -> View { } pub trait OutlineItem { - fn outline_item(self: Rc, element_key: ElementKey) -> View; + fn outline_item(self: Rc, element: &Rc) -> View; } impl OutlineItem for InversiveDistanceRegulator { - fn outline_item(self: Rc, element_key: ElementKey) -> View { - let state = use_context::(); - let other_subject = if self.subjects[0] == element_key { - self.subjects[1] + fn outline_item(self: Rc, element: &Rc) -> View { + let other_subject_label = if self.subjects[0] == element.clone() { + self.subjects[1].label() } else { - self.subjects[0] - }; - let other_subject_label = state.assembly.elements.with( - |elts| elts[other_subject].label().clone() - ); + self.subjects[0].label() + }.clone(); view! { li(class="regulator") { div(class="regulator-label") { (other_subject_label) } @@ -118,7 +111,7 @@ impl OutlineItem for InversiveDistanceRegulator { } impl OutlineItem for HalfCurvatureRegulator { - fn outline_item(self: Rc, _element_key: ElementKey) -> View { + fn outline_item(self: Rc, _element: &Rc) -> View { view! { li(class="regulator") { div(class="regulator-label") // for spacing @@ -130,23 +123,16 @@ impl OutlineItem for HalfCurvatureRegulator { } } -// a list item that shows a regulator in an outline view of an element -#[component(inline_props)] -fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View { - let state = use_context::(); - let regulator = state.assembly.regulators.with( - |regs| regs[regulator_key].clone() - ); - regulator.outline_item(element_key) -} - // a list item that shows an element in an outline view of an assembly #[component(inline_props)] -fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { +fn ElementOutlineItem(element: Rc) -> View { let state = use_context::(); - let class = state.selection.map( - move |sel| if sel.contains(&key) { "selected" } else { "" } - ); + let class = { + let element_for_class = element.clone(); + state.selection.map( + move |sel| if sel.contains(&element_for_class) { "selected" } else { "" } + ) + }; let label = element.label().clone(); let representation = element.representation().clone(); let rep_components = move || { @@ -161,14 +147,10 @@ fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { }; let regulated = element.regulators().map(|regs| regs.len() > 0); let regulator_list = element.regulators().map( - move |elt_reg_keys| elt_reg_keys + |regs| regs .clone() .into_iter() - .sorted_by_key( - |®_key| state.assembly.regulators.with( - |regs| regs[reg_key].subjects().len() - ) - ) + .sorted_by_key(|reg| reg.subjects().len()) .collect() ); let details_node = create_node_ref(); @@ -178,10 +160,11 @@ fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { summary( class=class.get(), on:keydown={ + let element_for_handler = element.clone(); move |event: KeyboardEvent| { match event.key().as_str() { "Enter" => { - state.select(key, event.shift_key()); + state.select(&element_for_handler, event.shift_key()); event.prevent_default(); }, "ArrowRight" if regulated.get() => { @@ -208,19 +191,10 @@ fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { div( class="element", on:click={ + let state_for_handler = state.clone(); + let element_for_handler = element.clone(); move |event: MouseEvent| { - if event.shift_key() { - state.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); - } - }); - } else { - state.selection.update(|sel| { - sel.clear(); - sel.insert(key); - }); - } + state_for_handler.select(&element_for_handler, event.shift_key()); event.stop_propagation(); event.prevent_default(); } @@ -234,13 +208,8 @@ fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { ul(class="regulators") { Keyed( list=regulator_list, - view=move |reg_key| view! { - RegulatorOutlineItem( - regulator_key=reg_key, - element_key=key - ) - }, - key=|reg_key| reg_key.clone() + view=move |reg| reg.outline_item(&element), + key=|reg| reg.serial() ) } } @@ -259,12 +228,15 @@ pub fn Outline() -> View { let state = use_context::(); // list the elements alphabetically by ID + /* TO DO */ + // this code is designed to generalize easily to other sort keys. if we only + // ever wanted to sort by ID, we could do that more simply using the + // `elements_by_id` index let element_list = state.assembly.elements.map( |elts| elts .clone() .into_iter() - .sorted_by_key(|(_, elt)| elt.id().clone()) - .map(|(key, elt)| (key, ElementRc(elt))) + .sorted_by_key(|elt| elt.id().clone()) .collect() ); @@ -278,10 +250,10 @@ pub fn Outline() -> View { ) { Keyed( list=element_list, - view=|(key, ElementRc(elt))| view! { - ElementOutlineItem(key=key, element=elt) + view=|elt| view! { + ElementOutlineItem(element=elt) }, - key=|(_, ElementRc(elt))| elt.serial() + key=|elt| elt.serial() ) } } From a671a8273ae183bf35b8d82e6cb4a024ff956f1d Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 2 Jun 2025 15:56:06 +0000 Subject: [PATCH 33/41] Introduce ghost mode for elements (#85) Allows any element to be put in "ghost mode," decreasing its opacity and making it insensitive to click-to-select. Ghost mode is toggled using a checkbox in the outline view. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/85 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/main.css | 4 +++ app-proto/src/assembly.rs | 13 ++++++++ app-proto/src/display.rs | 61 ++++++++++++++++++++++++++------------ app-proto/src/outline.rs | 6 +++- app-proto/src/point.frag | 7 +++-- app-proto/src/point.vert | 4 +-- app-proto/src/spheres.frag | 13 ++++---- 7 files changed, 77 insertions(+), 31 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index f787535..d56784f 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -90,6 +90,10 @@ summary > div, .regulator { padding-right: 8px; } +.element > input { + margin-left: 8px; +} + .element-switch { width: 18px; padding-left: 2px; diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index bd185c8..e48b802 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -101,6 +101,7 @@ pub trait Element: Serial + ProblemPoser + DisplayItem { fn id(&self) -> &String; fn label(&self) -> &String; fn representation(&self) -> Signal>; + fn ghost(&self) -> Signal; // the regulators the element is subject to. the assembly that owns the // element is responsible for keeping this set up to date @@ -154,6 +155,7 @@ pub struct Sphere { pub label: String, pub color: ElementColor, pub representation: Signal>, + pub ghost: Signal, pub regulators: Signal>>, serial: u64, column_index: Cell> @@ -173,6 +175,7 @@ impl Sphere { 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() @@ -210,6 +213,10 @@ impl Element for Sphere { self.representation } + fn ghost(&self) -> Signal { + self.ghost + } + fn regulators(&self) -> Signal>> { self.regulators } @@ -244,6 +251,7 @@ pub struct Point { pub label: String, pub color: ElementColor, pub representation: Signal>, + pub ghost: Signal, pub regulators: Signal>>, serial: u64, column_index: Cell> @@ -263,6 +271,7 @@ impl Point { label, color, representation: create_signal(representation), + ghost: create_signal(false), regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), column_index: None.into() @@ -296,6 +305,10 @@ impl Element for Point { self.representation } + fn ghost(&self) -> Signal { + self.ghost + } + fn regulators(&self) -> Signal>> { self.regulators } diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index a2fe4b6..69a3659 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -20,11 +20,23 @@ use crate::{ assembly::{Element, ElementColor, ElementMotion, Point, Sphere} }; +// --- color --- + +const COLOR_SIZE: usize = 3; +type ColorWithOpacity = [f32; COLOR_SIZE + 1]; + +fn combine_channels(color: ElementColor, opacity: f32) -> ColorWithOpacity { + let mut color_with_opacity = [0.0; COLOR_SIZE + 1]; + color_with_opacity[..COLOR_SIZE].copy_from_slice(&color); + color_with_opacity[COLOR_SIZE] = opacity; + color_with_opacity +} + // --- scene data --- struct SceneSpheres { representations: Vec>, - colors: Vec, + colors_with_opacity: Vec, highlights: Vec } @@ -32,7 +44,7 @@ impl SceneSpheres { fn new() -> SceneSpheres{ SceneSpheres { representations: Vec::new(), - colors: Vec::new(), + colors_with_opacity: Vec::new(), highlights: Vec::new() } } @@ -41,16 +53,16 @@ impl SceneSpheres { self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer") } - fn push(&mut self, representation: DVector, color: ElementColor, highlight: f32) { + fn push(&mut self, representation: DVector, color: ElementColor, opacity: f32, highlight: f32) { self.representations.push(representation); - self.colors.push(color); + self.colors_with_opacity.push(combine_channels(color, opacity)); self.highlights.push(highlight); } } struct ScenePoints { representations: Vec>, - colors: Vec, + colors_with_opacity: Vec, highlights: Vec, selections: Vec } @@ -59,15 +71,15 @@ impl ScenePoints { fn new() -> ScenePoints { ScenePoints { representations: Vec::new(), - colors: Vec::new(), + colors_with_opacity: Vec::new(), highlights: Vec::new(), selections: Vec::new() } } - fn push(&mut self, representation: DVector, color: ElementColor, highlight: f32, selected: bool) { + fn push(&mut self, representation: DVector, color: ElementColor, opacity: f32, highlight: f32, selected: bool) { self.representations.push(representation); - self.colors.push(color); + self.colors_with_opacity.push(combine_channels(color, opacity)); self.highlights.push(highlight); self.selections.push(if selected { 1.0 } else { 0.0 }); } @@ -98,11 +110,16 @@ pub trait DisplayItem { impl DisplayItem for Sphere { fn show(&self, scene: &mut Scene, selected: bool) { - const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ + /* SCAFFOLDING */ + const DEFAULT_OPACITY: f32 = 0.5; + const GHOST_OPACITY: f32 = 0.2; + const HIGHLIGHT: f32 = 0.2; + let representation = self.representation.get_clone_untracked(); let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; + let opacity = if self.ghost.get() { GHOST_OPACITY } else { DEFAULT_OPACITY }; let highlight = if selected { 1.0 } else { HIGHLIGHT }; - scene.spheres.push(representation, color, highlight); + scene.spheres.push(representation, color, opacity, highlight); } // this method should be kept synchronized with `sphere_cast` in @@ -148,11 +165,15 @@ impl DisplayItem for Sphere { impl DisplayItem for Point { fn show(&self, scene: &mut Scene, selected: bool) { - const HIGHLIGHT: f32 = 0.5; /* SCAFFOLDING */ + /* SCAFFOLDING */ + const GHOST_OPACITY: f32 = 0.4; + const HIGHLIGHT: f32 = 0.5; + let representation = self.representation.get_clone_untracked(); let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; + let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 }; let highlight = if selected { 1.0 } else { HIGHLIGHT }; - scene.points.push(representation, color, highlight, selected); + scene.points.push(representation, color, opacity, highlight, selected); } /* SCAFFOLDING */ @@ -365,6 +386,7 @@ pub fn Display() -> View { state.assembly.elements.with(|elts| { for elt in elts { elt.representation().track(); + elt.ghost().track(); } }); state.selection.track(); @@ -395,7 +417,6 @@ pub fn Display() -> View { const SHRINKING_SPEED: f64 = 0.15; // in length units per second // display parameters - const OPACITY: f32 = 0.5; /* SCAFFOLDING */ const LAYER_THRESHOLD: i32 = 0; /* DEBUG */ const DEBUG_MODE: i32 = 0; /* DEBUG */ @@ -469,7 +490,6 @@ pub fn Display() -> View { ); let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution"); let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim"); - let opacity_loc = ctx.get_uniform_location(&sphere_program, "opacity"); let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold"); let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode"); @@ -654,9 +674,9 @@ pub fn Display() -> View { sphere_lt_locs[n].as_ref(), v.rows(3, 2).as_slice() ); - ctx.uniform3fv_with_f32_array( + ctx.uniform4fv_with_f32_array( sphere_color_locs[n].as_ref(), - &scene.spheres.colors[n] + &scene.spheres.colors_with_opacity[n] ); ctx.uniform1f( sphere_highlight_locs[n].as_ref(), @@ -665,7 +685,6 @@ pub fn Display() -> View { } // pass the display parameters - ctx.uniform1f(opacity_loc.as_ref(), OPACITY); ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); @@ -703,7 +722,7 @@ pub fn Display() -> View { // bind them to the corresponding attributes in the vertex // shader bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice()); - bind_new_buffer_to_attribute(&ctx, point_color_attr, COLOR_SIZE as i32, scene.points.colors.concat().as_slice()); + bind_new_buffer_to_attribute(&ctx, point_color_attr, (COLOR_SIZE + 1) as i32, scene.points.colors_with_opacity.concat().as_slice()); bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice()); bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice()); @@ -851,7 +870,11 @@ pub fn Display() -> View { let (dir, pixel_size) = event_dir(&event); console::log_1(&JsValue::from(dir.to_string())); let mut clicked: Option<(Rc, f64)> = None; - for elt in state.assembly.elements.get_clone_untracked() { + let tangible_elts = state.assembly.elements + .get_clone_untracked() + .into_iter() + .filter(|elt| !elt.ghost().get()); + for elt in tangible_elts { match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) { Some(depth) => match clicked { Some((_, best_depth)) => { diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index caf11e8..59bbdcc 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -202,7 +202,11 @@ fn ElementOutlineItem(element: Rc) -> View { ) { div(class="element-label") { (label) } div(class="element-representation") { (rep_components) } - div(class="status") + input( + r#type="checkbox", + bind:checked=element.ghost(), + on:click=|event: MouseEvent| event.stop_propagation() + ) } } ul(class="regulators") { diff --git a/app-proto/src/point.frag b/app-proto/src/point.frag index 3a361a8..194a072 100644 --- a/app-proto/src/point.frag +++ b/app-proto/src/point.frag @@ -2,7 +2,7 @@ precision highp float; -in vec3 point_color; +in vec4 point_color; in float point_highlight; in float total_radius; @@ -13,6 +13,7 @@ void main() { const float POINT_RADIUS = 4.; float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r); - vec3 color = mix(point_color, vec3(1.), border * point_highlight); - outColor = vec4(color, 1. - smoothstep(total_radius - 1., total_radius, r)); + float disk = 1. - smoothstep(total_radius - 1., total_radius, r); + vec4 color = mix(point_color, vec4(1.), border * point_highlight); + outColor = vec4(vec3(1.), disk) * color; } \ No newline at end of file diff --git a/app-proto/src/point.vert b/app-proto/src/point.vert index 6945010..0b76bc1 100644 --- a/app-proto/src/point.vert +++ b/app-proto/src/point.vert @@ -1,11 +1,11 @@ #version 300 es in vec4 position; -in vec3 color; +in vec4 color; in float highlight; in float selected; -out vec3 point_color; +out vec4 point_color; out float point_highlight; out float total_radius; diff --git a/app-proto/src/spheres.frag b/app-proto/src/spheres.frag index d50cb1e..fa317a8 100644 --- a/app-proto/src/spheres.frag +++ b/app-proto/src/spheres.frag @@ -17,7 +17,7 @@ struct vecInv { const int SPHERE_MAX = 200; uniform int sphere_cnt; uniform vecInv sphere_list[SPHERE_MAX]; -uniform vec3 color_list[SPHERE_MAX]; +uniform vec4 color_list[SPHERE_MAX]; uniform float highlight_list[SPHERE_MAX]; // view @@ -25,7 +25,6 @@ uniform vec2 resolution; uniform float shortdim; // controls -uniform float opacity; uniform int layer_threshold; uniform bool debug_mode; @@ -69,7 +68,7 @@ struct Fragment { vec4 color; }; -Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) { +Fragment sphere_shading(vecInv v, vec3 pt, vec4 base_color) { // the expression for normal needs to be checked. it's supposed to give the // negative gradient of the lorentz product between the impact point vector // and the sphere vector with respect to the coordinates of the impact @@ -79,7 +78,7 @@ Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) { float incidence = dot(normal, light_dir); float illum = mix(0.4, 1.0, max(incidence, 0.0)); - return Fragment(pt, normal, vec4(illum * base_color, opacity)); + return Fragment(pt, normal, vec4(illum * base_color.rgb, base_color.a)); } float intersection_dist(Fragment a, Fragment b) { @@ -192,10 +191,11 @@ void main() { vec3 color = vec3(0.); int layer = layer_cnt - 1; TaggedDepth hit = top_hits[layer]; + vec4 sphere_color = color_list[hit.id]; Fragment frag_next = sphere_shading( sphere_list[hit.id], hit.depth * dir, - hit.dimming * color_list[hit.id] + vec4(hit.dimming * sphere_color.rgb, sphere_color.a) ); float highlight_next = highlight_list[hit.id]; --layer; @@ -206,10 +206,11 @@ void main() { // shade the next fragment hit = top_hits[layer]; + sphere_color = color_list[hit.id]; frag_next = sphere_shading( sphere_list[hit.id], hit.depth * dir, - hit.dimming * color_list[hit.id] + vec4(hit.dimming * sphere_color.rgb, sphere_color.a) ); highlight_next = highlight_list[hit.id]; From e447e7ea966880d24bada011bf5df08e927b3871 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Wed, 4 Jun 2025 21:01:12 +0000 Subject: [PATCH 34/41] Dispatch normalization routines correctly (#87) Addresses issue #86 by correctly dispatching the routine used to normalize spheres during nudging. Adds a test that would have detected the issue. Since the tests aren't built for WebAssembly, we have to replace `console::log` with `console_log!` in all of the functions used by `assembly::curvature_drift_test`. We'll eventually want to do this replacement everywhere. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/87 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.toml | 7 +++ app-proto/src/assembly.rs | 123 +++++++++++++++++++++++++------------- app-proto/src/engine.rs | 34 ++++++++--- 3 files changed, 112 insertions(+), 52 deletions(-) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 844a0a6..9b46b2b 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -46,6 +46,13 @@ features = [ dyna3 = { path = ".", default-features = false, features = ["dev"] } wasm-bindgen-test = "0.3.34" +# turn off spurious warnings about the custom config that Sycamore uses +# +# https://sycamore.dev/book/troubleshooting#unexpected-cfg-condition-name--sycamore-force-ssr +# +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(sycamore_force_ssr)"] } + [profile.release] opt-level = "s" # optimize for small code size debug = true # include debug symbols diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index e48b802..6c91fc0 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,6 +1,5 @@ use nalgebra::{DMatrix, DVector, DVectorView}; use std::{ - any::{Any, TypeId}, cell::Cell, collections::{BTreeMap, BTreeSet}, cmp::Ordering, @@ -20,6 +19,8 @@ use crate::{ change_half_curvature, local_unif_to_std, point, + project_point_to_normalized, + project_sphere_to_normalized, realize_gram, sphere, ConfigSubspace, @@ -107,6 +108,10 @@ pub trait Element: Serial + ProblemPoser + DisplayItem { // element is responsible for keeping this set up to date fn regulators(&self) -> Signal>>; + // project a representation vector for this kind of element onto its + // normalization variety + fn project_to_normalized(&self, rep: &mut DVector); + // the configuration matrix column index that was assigned to the element // last time the assembly was realized, or `None` if the element has never // been through a realization @@ -221,6 +226,10 @@ impl Element for Sphere { self.regulators } + fn project_to_normalized(&self, rep: &mut DVector) { + project_sphere_to_normalized(rep); + } + fn column_index(&self) -> Option { self.column_index.get() } @@ -313,6 +322,10 @@ impl Element for Point { self.regulators } + fn project_to_normalized(&self, rep: &mut DVector) { + project_point_to_normalized(rep); + } + fn column_index(&self) -> Option { self.column_index.get() } @@ -611,9 +624,7 @@ impl Assembly { create_effect(move || { /* DEBUG */ // log the regulator update - console::log_1(&JsValue::from( - format!("Updated regulator with subjects {:?}", regulator.subjects()) - )); + console_log!("Updated regulator with subjects {:?}", regulator.subjects()); if regulator.try_activate() { self_for_effect.realize(); @@ -622,10 +633,10 @@ impl Assembly { /* DEBUG */ // print an updated list of regulators - console::log_1(&JsValue::from("Regulators:")); + console_log!("Regulators:"); self.regulators.with_untracked(|regs| { for reg in regs.into_iter() { - console::log_1(&JsValue::from(format!( + console_log!( " {:?}: {}", reg.subjects(), reg.set_point().with_untracked( @@ -638,7 +649,7 @@ impl Assembly { } } ) - ))); + ); } }); } @@ -669,19 +680,11 @@ impl Assembly { /* DEBUG */ // log the Gram matrix - console::log_1(&JsValue::from("Gram matrix:")); - problem.gram.log_to_console(); + console_log!("Gram matrix:\n{}", problem.gram); /* DEBUG */ // log the initial configuration matrix - console::log_1(&JsValue::from("Old configuration:")); - for j in 0..problem.guess.nrows() { - let mut row_str = String::new(); - for k in 0..problem.guess.ncols() { - row_str.push_str(format!(" {:>8.3}", problem.guess[(j, k)]).as_str()); - } - console::log_1(&JsValue::from(row_str)); - } + console_log!("Old configuration:{:>8.3}", problem.guess); // look for a configuration with the given Gram matrix let (config, tangent, success, history) = realize_gram( @@ -690,16 +693,14 @@ impl Assembly { /* DEBUG */ // report the outcome of the search - console::log_1(&JsValue::from( - if success { - "Target accuracy achieved!" - } else { - "Failed to reach target accuracy" - } - )); - console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1)); - console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap())); - console::log_2(&JsValue::from("Tangent dimension:"), &JsValue::from(tangent.dim())); + if success { + console_log!("Target accuracy achieved!") + } else { + console_log!("Failed to reach target accuracy") + } + console_log!("Steps: {}", history.scaled_loss.len() - 1); + console_log!("Loss: {}", *history.scaled_loss.last().unwrap()); + console_log!("Tangent dimension: {}", tangent.dim()); if success { // read out the solution @@ -782,29 +783,17 @@ impl Assembly { // step the assembly along the deformation. this changes the elements' // normalizations, so we restore those afterward - /* KLUDGE */ - // for now, we only restore the normalizations of spheres for elt in self.elements.get_clone_untracked() { elt.representation().update_silent(|rep| { match elt.column_index() { Some(column_index) => { - // step the assembly along the deformation + // step the element along the deformation and then + // restore its normalization *rep += motion_proj.column(column_index); - - if elt.type_id() == TypeId::of::() { - // restore normalization by contracting toward the - // last coordinate axis - let q_sp = rep.fixed_rows::<3>(0).norm_squared(); - let half_q_lt = -2.0 * rep[3] * rep[4]; - let half_q_lt_sq = half_q_lt * half_q_lt; - let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); - rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); - } + elt.project_to_normalized(rep); }, None => { - console::log_1(&JsValue::from( - format!("No velocity to unpack for fresh element \"{}\"", elt.id()) - )) + console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) } }; }); @@ -821,6 +810,8 @@ impl Assembly { mod tests { use super::*; + use crate::engine; + #[test] #[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")] fn unindexed_element_test() { @@ -846,4 +837,50 @@ mod tests { }.pose(&mut ConstraintProblem::new(2)); }); } + + #[test] + fn curvature_drift_test() { + const INITIAL_RADIUS: f64 = 0.25; + let _ = create_root(|| { + // set up an assembly containing a single sphere centered at the + // origin + let assembly = Assembly::new(); + let sphere_id = "sphere0"; + let _ = assembly.try_insert_element( + // we create the sphere by hand for two reasons: to choose the + // curvature (which can affect drift rate) and to make the test + // independent of `Sphere::default` + Sphere::new( + 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) + ) + ); + + // nudge the sphere repeatedly along the `z` axis + const STEP_SIZE: f64 = 0.0025; + const STEP_CNT: usize = 400; + let sphere = assembly.elements_by_id.with(|elts_by_id| elts_by_id[sphere_id].clone()); + let velocity = DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]); + for _ in 0..STEP_CNT { + assembly.deform( + vec![ + ElementMotion { + element: sphere.clone(), + velocity: velocity.as_view() + } + ] + ); + } + + // check how much the sphere's curvature has drifted + const INITIAL_HALF_CURV: f64 = 0.5 / INITIAL_RADIUS; + const DRIFT_TOL: f64 = 0.015; + let final_half_curv = sphere.representation().with_untracked( + |rep| rep[Sphere::CURVATURE_COMPONENT] + ); + assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL); + }); + } } \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index b0fa23d..c5d7b00 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -1,5 +1,6 @@ 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 --- @@ -34,6 +35,21 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 ]) } +// project a sphere's representation vector to the normalization variety by +// contracting toward the last coordinate axis +pub fn project_sphere_to_normalized(rep: &mut DVector) { + let q_sp = rep.fixed_rows::<3>(0).norm_squared(); + let half_q_lt = -2.0 * rep[3] * rep[4]; + let half_q_lt_sq = half_q_lt * half_q_lt; + let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); + rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); +} + +// normalize a point's representation vector by scaling +pub fn project_point_to_normalized(rep: &mut DVector) { + 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 @@ -94,15 +110,6 @@ impl PartialMatrix { } } - /* DEBUG */ - pub fn log_to_console(&self) { - for &MatrixEntry { index: (row, col), value } in self { - console::log_1(&JsValue::from( - format!(" {} {} {}", row, col, value) - )); - } - } - fn freeze(&self, a: &DMatrix) -> DMatrix { let mut result = a.clone(); for &MatrixEntry { index, value } in self { @@ -128,6 +135,15 @@ impl PartialMatrix { } } +impl Display for PartialMatrix { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + for &MatrixEntry { index: (row, col), value } in self { + writeln!(f, " {row} {col} {value}")?; + } + Ok(()) + } +} + impl IntoIterator for PartialMatrix { type Item = MatrixEntry; type IntoIter = std::vec::IntoIter; From 4cb32625556743de30b49eed4b6bfce8ab9190dc Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Thu, 26 Jun 2025 22:11:02 +0000 Subject: [PATCH 35/41] chore: Update Sycamore to 0.9.1 (#91) Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/91 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.lock | 25 +++++++++++++------------ app-proto/Cargo.toml | 2 +- app-proto/src/outline.rs | 4 ++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 3bf609c..55e8686 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -423,9 +423,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "sycamore" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dedaf7237c05913604a5b0b2536b613f6c8510c6b213d2583b1294869755cabd" +checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc" dependencies = [ "hashbrown", "indexmap", @@ -440,9 +440,9 @@ dependencies = [ [[package]] name = "sycamore-core" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ddddc3d1bcb38c04ad55d2d1ab4f6a358e4daaeae0a0436892f1fade9fb31a" +checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187" dependencies = [ "hashbrown", "paste", @@ -451,9 +451,9 @@ dependencies = [ [[package]] name = "sycamore-macro" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77181c27cb753e86065308901871ccc7456fb19527b6a4ffacad3b63175ed014" +checksum = "a0c1d2eddc94db6d03e67eb832df5512b967e81053a573cd01bf3e1c3db00137" dependencies = [ "once_cell", "proc-macro2", @@ -465,20 +465,21 @@ dependencies = [ [[package]] name = "sycamore-reactive" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aa6870203507c07e850687c0ccf528eb0f04240e3596bac9137007ffb6c50b1" +checksum = "f2bacf810535efc2701187a716a5652197ad241d620d5b00fb12caa6dfa23add" dependencies = [ "paste", "slotmap", "smallvec", + "wasm-bindgen", ] [[package]] name = "sycamore-view-parser" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6144640af2eafffc68a92f3aacbbfaa21f7fd31906e2336fe304fd100fe226b" +checksum = "6c22875843db83cd4d49c0123a195e433bdc74e13ed0fff4ace0e77bb0a67033" dependencies = [ "proc-macro2", "quote", @@ -487,9 +488,9 @@ dependencies = [ [[package]] name = "sycamore-web" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca93dcf1b1830bf1aac93508ed51babcda92c1d32d96067ab416d94e4b7c475" +checksum = "4b17aa5875f59f541cdf6fb58751ec702a6ed9801f30dd2b4d5f2279025b98bd" dependencies = [ "html-escape", "js-sys", diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 9b46b2b..6932b72 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -15,7 +15,7 @@ js-sys = "0.3.70" lazy_static = "1.5.0" nalgebra = "0.33.0" readonly = "0.2.12" -sycamore = "0.9.0-beta.3" +sycamore = "0.9.1" # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 59bbdcc..77d8575 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -151,7 +151,7 @@ fn ElementOutlineItem(element: Rc) -> View { .clone() .into_iter() .sorted_by_key(|reg| reg.subjects().len()) - .collect() + .collect::>() ); let details_node = create_node_ref(); view! { @@ -241,7 +241,7 @@ pub fn Outline() -> View { .clone() .into_iter() .sorted_by_key(|elt| elt.id().clone()) - .collect() + .collect::>() ); view! { From 5864017e6fd4e45f138b3fa4dac4cbd362a95641 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 21 Jul 2025 04:18:49 +0000 Subject: [PATCH 36/41] feat: Engine diagnostics (#92) Adds a `Diagnostics` component that shows the following diagnostics from the last realization: - Confirmation of success or a short description of what failed. - The value of the loss function at each step. - The spectrum of the Hessian at each step. The loss and spectrum plots are shown on switchable panels. Also includes some refactoring/renaming of existing code. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/92 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.lock | 565 +++++++++++++++++++++++++- app-proto/Cargo.toml | 3 + app-proto/examples/common/print.rs | 36 ++ app-proto/examples/irisawa-hexlet.rs | 28 +- app-proto/examples/kaleidocycle.rs | 48 +-- app-proto/examples/point-on-sphere.rs | 32 +- app-proto/examples/three-spheres.rs | 29 +- app-proto/index.html | 6 + app-proto/main.css | 66 ++- app-proto/run-examples | 12 - app-proto/run-examples.sh | 20 + app-proto/src/add_remove.rs | 4 +- app-proto/src/assembly.rs | 65 ++- app-proto/src/diagnostics.rs | 258 ++++++++++++ app-proto/src/display.rs | 1 + app-proto/src/engine.rs | 94 +++-- app-proto/src/main.rs | 3 + 17 files changed, 1120 insertions(+), 150 deletions(-) create mode 100644 app-proto/examples/common/print.rs delete mode 100755 app-proto/run-examples create mode 100644 app-proto/run-examples.sh create mode 100644 app-proto/src/diagnostics.rs diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 55e8686..4f75c45 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -20,6 +20,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "approx" version = "0.5.1" @@ -35,6 +50,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -68,6 +98,35 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "charming" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ffae2e616ae7d66b2e9ea369f1c7650042bdcdc1dc08b04b027107007b4f09" +dependencies = [ + "handlebars", + "js-sys", + "serde", + "serde-wasm-bindgen", + "serde_json", + "serde_with", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -78,10 +137,122 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dyna3" version = "0.1.0" dependencies = [ + "charming", "console_error_panic_hook", "dyna3", "itertools", @@ -106,6 +277,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -117,6 +304,28 @@ dependencies = [ "wasi", ] +[[package]] +name = "handlebars" +version = "6.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" +dependencies = [ + "derive_builder", + "log", + "num-order", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -127,6 +336,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "html-escape" version = "0.2.13" @@ -136,6 +351,47 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.5.0" @@ -143,7 +399,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", + "serde", ] [[package]] @@ -155,6 +412,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.70" @@ -192,6 +455,12 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "minicov" version = "0.3.5" @@ -248,6 +517,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -257,6 +532,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -289,6 +579,57 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pest" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -363,6 +704,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "safe_arch" version = "0.7.2" @@ -387,6 +734,90 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.5.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -421,14 +852,20 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "sycamore" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc" dependencies = [ - "hashbrown", - "indexmap", + "hashbrown 0.14.5", + "indexmap 2.5.0", "paste", "sycamore-core", "sycamore-macro", @@ -444,7 +881,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", "paste", "sycamore-reactive", ] @@ -506,21 +943,78 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.13" @@ -677,6 +1171,65 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 6932b72..1230b47 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -17,6 +17,9 @@ nalgebra = "0.33.0" readonly = "0.2.12" sycamore = "0.9.1" +# We use Charming to help display engine diagnostics +charming = { version = "0.5.1", features = ["wasm"] } + # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for diff --git a/app-proto/examples/common/print.rs b/app-proto/examples/common/print.rs new file mode 100644 index 0000000..2aa6a39 --- /dev/null +++ b/app-proto/examples/common/print.rs @@ -0,0 +1,36 @@ +#![allow(dead_code)] + +use nalgebra::DMatrix; + +use dyna3::engine::{Q, DescentHistory, Realization}; + +pub fn title(title: &str) { + println!("─── {title} ───"); +} + +pub fn realization_diagnostics(realization: &Realization) { + let Realization { result, history } = realization; + println!(); + if let Err(ref message) = result { + println!("❌️ {message}"); + } else { + println!("✅️ Target accuracy achieved!"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); +} + +pub fn gram_matrix(config: &DMatrix) { + println!("\nCompleted Gram matrix:{}", (config.tr_mul(&*Q) * config).to_string().trim_end()); +} + +pub fn config(config: &DMatrix) { + println!("\nConfiguration:{}", config.to_string().trim_end()); +} + +pub fn loss_history(history: &DescentHistory) { + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } +} \ No newline at end of file diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs index 639a494..0d710ff 100644 --- a/app-proto/examples/irisawa-hexlet.rs +++ b/app-proto/examples/irisawa-hexlet.rs @@ -1,25 +1,23 @@ -use dyna3::engine::{Q, examples::realize_irisawa_hexlet}; +#[path = "common/print.rs"] +mod print; + +use dyna3::engine::{ConfigNeighborhood, examples::realize_irisawa_hexlet}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let (config, _, success, history) = realize_irisawa_hexlet(SCALED_TOL); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - if success { + let realization = realize_irisawa_hexlet(SCALED_TOL); + print::title("Irisawa hexlet"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood { config, .. }) = realization.result { + // print the diameters of the chain spheres println!("\nChain diameters:"); println!(" {} sun (given)", 1.0 / config[(3, 3)]); for k in 4..9 { println!(" {} sun", 1.0 / config[(3, k)]); } + + // print the completed Gram matrix + print::gram_matrix(&config); } - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } + print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs index 2779ab1..7ca1f97 100644 --- a/app-proto/examples/kaleidocycle.rs +++ b/app-proto/examples/kaleidocycle.rs @@ -1,30 +1,32 @@ +#[path = "common/print.rs"] +mod print; + use nalgebra::{DMatrix, DVector}; -use dyna3::engine::{Q, examples::realize_kaleidocycle}; +use dyna3::engine::{ConfigNeighborhood, examples::realize_kaleidocycle}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); - print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config); - print!("Configuration:{}", config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); + let realization = realize_kaleidocycle(SCALED_TOL); + print::title("Kaleidocycle"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood { config, nbhd: tangent }) = realization.result { + // print the completed Gram matrix and the realized configuration + print::gram_matrix(&config); + print::config(&config); + + // find the kaleidocycle's twist motion by projecting onto the tangent + // space + const N_POINTS: usize = 12; + let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); + let down = -&up; + let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( + |n| [ + tangent.proj(&up.as_view(), n), + tangent.proj(&down.as_view(), n+1) + ] + ).sum(); + let normalization = 5.0 / twist_motion[(2, 0)]; + println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end()); } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}\n", history.scaled_loss.last().unwrap()); - - // find the kaleidocycle's twist motion by projecting onto the tangent space - const N_POINTS: usize = 12; - let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); - let down = -&up; - let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( - |n| [ - tangent.proj(&up.as_view(), n), - tangent.proj(&down.as_view(), n+1) - ] - ).sum(); - let normalization = 5.0 / twist_motion[(2, 0)]; - print!("Twist motion:{}", normalization * twist_motion); } \ No newline at end of file diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 880d7b0..89dee76 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -1,4 +1,13 @@ -use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem}; +#[path = "common/print.rs"] +mod print; + +use dyna3::engine::{ + point, + realize_gram, + sphere, + ConfigNeighborhood, + ConstraintProblem +}; fn main() { let mut problem = ConstraintProblem::from_guess(&[ @@ -11,21 +20,14 @@ fn main() { } } problem.frozen.push(3, 0, problem.guess[(3, 0)]); - println!(); - let (config, _, success, history) = realize_gram( + let realization = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - print!("Configuration:{}", config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); + print::title("Point on a sphere"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood{ config, .. }) = realization.result { + print::gram_matrix(&config); + print::config(&config); } + print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index 3f3cc44..aa5a105 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -1,4 +1,12 @@ -use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem}; +#[path = "common/print.rs"] +mod print; + +use dyna3::engine::{ + realize_gram, + sphere, + ConfigNeighborhood, + ConstraintProblem +}; fn main() { let mut problem = ConstraintProblem::from_guess({ @@ -14,20 +22,13 @@ fn main() { problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); } } - println!(); - let (config, _, success, history) = realize_gram( + let realization = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); + print::title("Three spheres"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood{ config, .. }) = realization.result { + print::gram_matrix(&config); } + print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/index.html b/app-proto/index.html index 92238f4..4fbe52f 100644 --- a/app-proto/index.html +++ b/app-proto/index.html @@ -6,6 +6,12 @@ + + + diff --git a/app-proto/main.css b/app-proto/main.css index d56784f..7981285 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -18,6 +18,17 @@ body { font-family: 'Fira Sans', sans-serif; } +.invalid { + color: var(--text-invalid); +} + +.status { + width: 20px; + text-align: center; + font-family: 'Noto Emoji'; + font-style: normal; +} + /* sidebar */ #sidebar { @@ -138,6 +149,7 @@ details[open]:has(li) .element-switch::after { } .regulator-input { + margin-right: 4px; color: inherit; background-color: inherit; border: 1px solid var(--border); @@ -159,22 +171,56 @@ details[open]:has(li) .element-switch::after { border-color: var(--border-invalid); } -.status { - width: 20px; - padding-left: 4px; - text-align: center; - font-family: 'Noto Emoji'; - font-style: normal; -} - .regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); } +/* diagnostics */ + +#diagnostics { + margin: 10px; +} + +#diagnostics-bar { + display: flex; +} + +#realization-status { + display: flex; + flex-grow: 1; +} + +#realization-status .status { + margin-right: 4px; +} + +#realization-status :not(.status) { + flex-grow: 1; +} + +#realization-status .status::after { + content: '✓'; +} + +#realization-status.invalid .status::after { + content: '⚠'; +} + +.diagnostics-panel { + margin-top: 10px; + min-height: 180px; +} + +.diagnostics-chart { + background-color: var(--display-background); + border: 1px solid var(--border); + border-radius: 8px; +} + /* display */ -canvas { +#display { float: left; margin-left: 20px; margin-top: 20px; @@ -183,7 +229,7 @@ canvas { border-radius: 16px; } -canvas:focus { +#display:focus { border-color: var(--border-focus-dark); outline: none; } diff --git a/app-proto/run-examples b/app-proto/run-examples deleted file mode 100755 index 52173b0..0000000 --- a/app-proto/run-examples +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -# run all Cargo examples, as described here: -# -# Karol Kuczmarski. "Add examples to your Rust libraries" -# http://xion.io/post/code/rust-examples.html -# - -cargo run --example irisawa-hexlet -cargo run --example three-spheres -cargo run --example point-on-sphere -cargo run --example kaleidocycle \ No newline at end of file diff --git a/app-proto/run-examples.sh b/app-proto/run-examples.sh new file mode 100644 index 0000000..861addf --- /dev/null +++ b/app-proto/run-examples.sh @@ -0,0 +1,20 @@ +# run all Cargo examples, as described here: +# +# Karol Kuczmarski. "Add examples to your Rust libraries" +# http://xion.io/post/code/rust-examples.html +# +# you should invoke this script by calling `sh` or another interpreter, rather +# than calling `souce`, to ensure that the script can find the manifest file for +# the application prototype + +# find the manifest file for the application prototype +MANIFEST="$(dirname -- $0)/Cargo.toml" + +# set up the command that runs each example +RUN_EXAMPLE="cargo run --manifest-path $MANIFEST --example" + +# run the examples +$RUN_EXAMPLE irisawa-hexlet; echo +$RUN_EXAMPLE three-spheres; echo +$RUN_EXAMPLE point-on-sphere; echo +$RUN_EXAMPLE kaleidocycle \ No newline at end of file diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index f3bbc97..d737c79 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -3,8 +3,9 @@ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ - engine, AppState, + engine, + engine::DescentHistory, assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere} }; @@ -195,6 +196,7 @@ pub fn AddRemove() -> View { assembly.regulators.update(|regs| regs.clear()); assembly.elements.update(|elts| elts.clear()); assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); + assembly.descent_history.set(DescentHistory::new()); state.selection.update(|sel| sel.clear()); // load assembly diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 6c91fc0..c3b0c6b 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -23,8 +23,11 @@ use crate::{ project_sphere_to_normalized, realize_gram, sphere, + ConfigNeighborhood, ConfigSubspace, - ConstraintProblem + ConstraintProblem, + DescentHistory, + Realization }, outline::OutlineItem, specified::SpecifiedValue @@ -547,7 +550,11 @@ pub struct Assembly { pub tangent: Signal, // indexing - pub elements_by_id: Signal>> + pub elements_by_id: Signal>>, + + // realization diagnostics + pub realization_status: Signal>, + pub descent_history: Signal } impl Assembly { @@ -556,7 +563,9 @@ impl Assembly { elements: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), - elements_by_id: create_signal(BTreeMap::default()) + elements_by_id: create_signal(BTreeMap::default()), + realization_status: create_signal(Ok(())), + descent_history: create_signal(DescentHistory::new()) } } @@ -687,31 +696,49 @@ impl Assembly { console_log!("Old configuration:{:>8.3}", problem.guess); // look for a configuration with the given Gram matrix - let (config, tangent, success, history) = realize_gram( + let Realization { result, history } = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); /* DEBUG */ - // report the outcome of the search - if success { - console_log!("Target accuracy achieved!") + // report the outcome of the search in the browser console + if let Err(ref message) = result { + console_log!("❌️ {message}"); } else { - console_log!("Failed to reach target accuracy") + console_log!("✅️ Target accuracy achieved!"); } console_log!("Steps: {}", history.scaled_loss.len() - 1); - console_log!("Loss: {}", *history.scaled_loss.last().unwrap()); - console_log!("Tangent dimension: {}", tangent.dim()); + console_log!("Loss: {}", history.scaled_loss.last().unwrap()); - if success { - // 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())) - ); + // report the loss history + self.descent_history.set(history); + + match result { + Ok(ConfigNeighborhood { config, nbhd: tangent }) => { + /* DEBUG */ + // report the tangent dimension + console_log!("Tangent dimension: {}", tangent.dim()); + + // 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())) + ); + } + + // save the tangent space + self.tangent.set_silent(tangent); + }, + 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)) } - - // save the tangent space - self.tangent.set_silent(tangent); } } diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs new file mode 100644 index 0000000..a2f090a --- /dev/null +++ b/app-proto/src/diagnostics.rs @@ -0,0 +1,258 @@ +use charming::{ + Chart, + WasmRenderer, + component::{Axis, DataZoom, Grid}, + element::{AxisType, Symbol}, + series::{Line, Scatter}, +}; +use sycamore::prelude::*; + +use crate::AppState; + +#[derive(Clone)] +struct DiagnosticsState { + active_tab: Signal +} + +impl DiagnosticsState { + fn new(initial_tab: String) -> DiagnosticsState { + DiagnosticsState { + active_tab: create_signal(initial_tab) + } + } +} + +// a realization status indicator +#[component] +fn RealizationStatus() -> View { + let state = use_context::(); + let realization_status = state.assembly.realization_status; + view! { + div( + id="realization-status", + class=realization_status.with( + |status| match status { + Ok(_) => "", + Err(_) => "invalid" + } + ) + ) { + div(class="status") + div { + (realization_status.with( + |status| match status { + Ok(_) => "Target accuracy achieved".to_string(), + Err(message) => message.clone() + } + )) + } + } + } +} + +fn into_log10_time_point((step, value): (usize, f64)) -> Vec> { + vec![ + Some(step as f64), + if value == 0.0 { None } else { Some(value.abs().log10()) } + ] +} + +// the loss history from the last realization +#[component] +fn LossHistory() -> View { + const CONTAINER_ID: &str = "loss-history"; + let state = use_context::(); + let renderer = WasmRenderer::new_opt(None, Some(178)); + + on_mount(move || { + create_effect(move || { + // get the loss history + let scaled_loss: Vec<_> = state.assembly.descent_history.with( + |history| history.scaled_loss + .iter() + .enumerate() + .map(|(step, &loss)| (step, loss)) + .map(into_log10_time_point) + .collect() + ); + + // initialize the chart axes + let step_axis = Axis::new() + .type_(AxisType::Category) + .boundary_gap(false); + let scaled_loss_axis = Axis::new(); + + // load the chart data. when there's no history, we load the data + // point (0, None) to clear the chart. it would feel more natural to + // load empty data vectors, but that turns out not to clear the + // chart: it instead leads to previous data being re-used + let scaled_loss_series = Line::new().data( + if scaled_loss.len() > 0 { + scaled_loss + } else { + vec![vec![Some(0.0), None::]] + } + ); + let chart = Chart::new() + .animation(false) + .data_zoom(DataZoom::new().y_axis_index(0).right(40)) + .x_axis(step_axis) + .y_axis(scaled_loss_axis) + .grid(Grid::new().top(20).right(80).bottom(30).left(60)) + .series(scaled_loss_series); + renderer.render(CONTAINER_ID, &chart).unwrap(); + }); + }); + + view! { + div(id=CONTAINER_ID, class="diagnostics-chart") + } +} + +// the spectrum of the Hessian during the last realization +#[component] +fn SpectrumHistory() -> View { + const CONTAINER_ID: &str = "spectrum-history"; + let state = use_context::(); + let renderer = WasmRenderer::new(478, 178); + + on_mount(move || { + create_effect(move || { + // get the spectrum of the Hessian at each step, split into its + // positive, negative, and strictly-zero parts + let ( + hess_eigvals_zero, + hess_eigvals_nonzero + ): (Vec<_>, Vec<_>) = state.assembly.descent_history.with( + |history| history.hess_eigvals + .iter() + .enumerate() + .map( + |(step, eigvals)| eigvals.iter().map( + move |&val| (step, val) + ) + ) + .flatten() + .partition(|&(_, val)| val == 0.0) + ); + let zero_level = hess_eigvals_nonzero + .iter() + .map(|(_, val)| val.abs()) + .reduce(f64::min) + .map(|val| 0.1 * val) + .unwrap_or(1.0); + let ( + hess_eigvals_pos, + hess_eigvals_neg + ): (Vec<_>, Vec<_>) = hess_eigvals_nonzero + .into_iter() + .partition(|&(_, val)| val > 0.0); + + // initialize the chart axes + let step_axis = Axis::new() + .type_(AxisType::Category) + .boundary_gap(false); + let eigval_axis = Axis::new(); + + // load the chart data. when there's no history, we load the data + // point (0, None) to clear the chart. it would feel more natural to + // load empty data vectors, but that turns out not to clear the + // chart: it instead leads to previous data being re-used + let eigval_series_pos = Scatter::new() + .symbol_size(4.5) + .data( + if hess_eigvals_pos.len() > 0 { + hess_eigvals_pos + .into_iter() + .map(into_log10_time_point) + .collect() + } else { + vec![vec![Some(0.0), None::]] + } + ); + let eigval_series_neg = Scatter::new() + .symbol(Symbol::Diamond) + .symbol_size(6.0) + .data( + if hess_eigvals_neg.len() > 0 { + hess_eigvals_neg + .into_iter() + .map(into_log10_time_point) + .collect() + } else { + vec![vec![Some(0.0), None::]] + } + ); + let eigval_series_zero = Scatter::new() + .symbol(Symbol::Triangle) + .symbol_size(5.0) + .data( + if hess_eigvals_zero.len() > 0 { + hess_eigvals_zero + .into_iter() + .map(|(step, _)| (step, zero_level)) + .map(into_log10_time_point) + .collect() + } else { + vec![vec![Some(0.0), None::]] + } + ); + let chart = Chart::new() + .animation(false) + .data_zoom(DataZoom::new().y_axis_index(0).right(40)) + .x_axis(step_axis) + .y_axis(eigval_axis) + .grid(Grid::new().top(20).right(80).bottom(30).left(60)) + .series(eigval_series_pos) + .series(eigval_series_neg) + .series(eigval_series_zero); + renderer.render(CONTAINER_ID, &chart).unwrap(); + }); + }); + + view! { + div(id=CONTAINER_ID, class="diagnostics-chart") + } +} + +#[component(inline_props)] +fn DiagnosticsPanel(name: &'static str, children: Children) -> View { + let diagnostics_state = use_context::(); + view! { + div( + class="diagnostics-panel", + "hidden"=diagnostics_state.active_tab.with( + |active_tab| { + if active_tab == name { + None + } else { + Some("") + } + } + ) + ) { + (children) + } + } +} + +#[component] +pub fn Diagnostics() -> View { + let diagnostics_state = DiagnosticsState::new("loss".to_string()); + let active_tab = diagnostics_state.active_tab.clone(); + provide_context(diagnostics_state); + + view! { + div(id="diagnostics") { + div(id="diagnostics-bar") { + RealizationStatus {} + select(bind:value=active_tab) { + option(value="loss") { "Loss" } + option(value="spectrum") { "Spectrum" } + } + } + DiagnosticsPanel(name="loss") { LossHistory {} } + DiagnosticsPanel(name="spectrum") { SpectrumHistory {} } + } + } +} \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 69a3659..1646c4e 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -806,6 +806,7 @@ pub fn Display() -> View { // again canvas( ref=display, + id="display", width="600", height="600", tabindex="0", diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index c5d7b00..e6ffa25 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -256,18 +256,18 @@ pub struct DescentHistory { pub config: Vec>, pub scaled_loss: Vec, pub neg_grad: Vec>, - pub min_eigval: Vec, + pub hess_eigvals: Vec::>, pub base_step: Vec>, pub backoff_steps: Vec } impl DescentHistory { - fn new() -> DescentHistory { + pub fn new() -> DescentHistory { DescentHistory { config: Vec::>::new(), scaled_loss: Vec::::new(), neg_grad: Vec::>::new(), - min_eigval: Vec::::new(), + hess_eigvals: Vec::>::new(), base_step: Vec::>::new(), backoff_steps: Vec::::new(), } @@ -393,6 +393,17 @@ fn seek_better_config( None } +// a first-order neighborhood of a configuration +pub struct ConfigNeighborhood { + pub config: DMatrix, + pub nbhd: ConfigSubspace +} + +pub struct Realization { + pub result: Result, + pub history: DescentHistory +} + // seek a matrix `config` that matches the partial matrix `problem.frozen` and // has `config' * Q * config` matching the partial matrix `problem.gram`. start // at `problem.guess`, set the frozen entries to their desired values, and then @@ -405,7 +416,7 @@ pub fn realize_gram( reg_scale: f64, max_descent_steps: i32, max_backoff_steps: i32 -) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { +) -> Realization { // destructure the problem data let ConstraintProblem { gram, guess, frozen @@ -457,11 +468,12 @@ pub fn realize_gram( hess = DMatrix::from_columns(hess_cols.as_slice()); // regularize the Hessian - let min_eigval = hess.symmetric_eigenvalues().min(); + let hess_eigvals = hess.symmetric_eigenvalues(); + let min_eigval = hess_eigvals.min(); if min_eigval <= 0.0 { hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); } - history.min_eigval.push(min_eigval); + history.hess_eigvals.push(hess_eigvals); // project the negative gradient and negative Hessian onto the // orthogonal complement of the frozen subspace @@ -480,30 +492,40 @@ pub fn realize_gram( if state.loss < tol { break; } // compute the Newton step + /* TO DO */ /* - we need to either handle or eliminate the case where the minimum - eigenvalue of the Hessian is zero, so the regularized Hessian is - singular. right now, this causes the Cholesky decomposition to return - `None`, leading to a panic when we unrap + we should change our regularization to ensure that the Hessian is + is positive-definite, rather than just positive-semidefinite. ideally, + that would guarantee the success of the Cholesky decomposition--- + although we'd still need the error-handling routine in case of + numerical hiccups */ - let base_step_stacked = hess.clone().cholesky().unwrap().solve(&neg_grad_stacked); + let hess_cholesky = match hess.clone().cholesky() { + Some(cholesky) => cholesky, + None => return Realization { + result: Err("Cholesky decomposition failed".to_string()), + 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)); history.base_step.push(base_step.clone()); // use backtracking line search to find a better configuration - match seek_better_config( + 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 ) { - Some((better_state, backoff_steps)) => { - state = better_state; - history.backoff_steps.push(backoff_steps); - }, - None => return (state.config, ConfigSubspace::zero(assembly_dim), false, history) + state = better_state; + history.backoff_steps.push(backoff_steps); + } else { + return Realization { + result: Err("Line search failed".to_string()), + history + } }; } - let success = state.loss < tol; - let tangent = if success { + let result = if state.loss < tol { // express the uniform basis in the standard basis const UNIFORM_DIM: usize = 4; let total_dim_unif = UNIFORM_DIM * assembly_dim; @@ -516,11 +538,13 @@ pub fn realize_gram( } // find the kernel of the Hessian. give it the uniform inner product - ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim) + let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim); + + Ok(ConfigNeighborhood { config: state.config, nbhd: tangent }) } else { - ConfigSubspace::zero(assembly_dim) + Err("Failed to reach target accuracy".to_string()) }; - (state.config, tangent, success, history) + Realization { result, history } } // --- tests --- @@ -539,7 +563,7 @@ pub mod examples { // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki // https://www.nippon.com/en/japan-topics/c12801/ // - pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + pub fn realize_irisawa_hexlet(scaled_tol: f64) -> Realization { let mut problem = ConstraintProblem::from_guess( [ sphere(0.0, 0.0, 0.0, 15.0), @@ -590,7 +614,7 @@ pub mod examples { // set up a kaleidocycle, made of points with fixed distances between them, // and find its tangent space - pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + pub fn realize_kaleidocycle(scaled_tol: f64) -> Realization { const N_HINGES: usize = 6; let mut problem = ConstraintProblem::from_guess( (0..N_HINGES).step_by(2).flat_map( @@ -714,10 +738,10 @@ mod tests { } problem.frozen.push(3, 0, problem.guess[(3, 0)]); problem.frozen.push(3, 1, 0.5); - let (config, _, success, history) = realize_gram( + let Realization { result, history } = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(success, true); + let config = result.unwrap().config; for base_step in history.base_step.into_iter() { for &MatrixEntry { index, .. } in &problem.frozen { assert_eq!(base_step[index], 0.0); @@ -732,7 +756,7 @@ mod tests { fn irisawa_hexlet_test() { // solve Irisawa's problem const SCALED_TOL: f64 = 1.0e-12; - let (config, _, _, _) = realize_irisawa_hexlet(SCALED_TOL); + let config = realize_irisawa_hexlet(SCALED_TOL).result.unwrap().config; // check against Irisawa's solution let entry_tol = SCALED_TOL.sqrt(); @@ -759,11 +783,11 @@ mod tests { for n in 0..ELEMENT_DIM { problem.frozen.push(n, 0, problem.guess[(n, 0)]); } - let (config, tangent, success, history) = realize_gram( + let Realization { result, history } = realize_gram( &problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); assert_eq!(config, problem.guess); - assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of @@ -831,8 +855,8 @@ mod tests { fn tangent_test_kaleidocycle() { // set up a kaleidocycle and find its tangent space const SCALED_TOL: f64 = 1.0e-12; - let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); - assert_eq!(success, true); + let Realization { result, history } = realize_kaleidocycle(SCALED_TOL); + let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of @@ -920,11 +944,11 @@ mod tests { problem_orig.gram.push_sym(0, 0, 1.0); problem_orig.gram.push_sym(1, 1, 1.0); problem_orig.gram.push_sym(0, 1, 0.5); - let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram( + let Realization { result: result_orig, history: history_orig } = realize_gram( &problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap(); assert_eq!(config_orig, problem_orig.guess); - assert_eq!(success_orig, true); assert_eq!(history_orig.scaled_loss.len(), 1); // find another pair of spheres that meet at 120°. we'll think of this @@ -941,11 +965,11 @@ mod tests { guess: guess_tfm, frozen: problem_orig.frozen }; - let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram( + let Realization { result: result_tfm, history: history_tfm } = realize_gram( &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let ConfigNeighborhood { config: config_tfm, nbhd: tangent_tfm } = result_tfm.unwrap(); assert_eq!(config_tfm, problem_tfm.guess); - assert_eq!(success_tfm, true); assert_eq!(history_tfm.scaled_loss.len(), 1); // project a nudge to the tangent space of the solution variety at the diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index b76859a..f905c46 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -1,5 +1,6 @@ mod add_remove; mod assembly; +mod diagnostics; mod display; mod engine; mod outline; @@ -13,6 +14,7 @@ use sycamore::prelude::*; use add_remove::AddRemove; use assembly::{Assembly, Element}; +use diagnostics::Diagnostics; use display::Display; use outline::Outline; @@ -60,6 +62,7 @@ fn main() { div(id="sidebar") { AddRemove {} Outline {} + Diagnostics {} } Display {} } From 0801200210094b9514974d63bb09eaad88ba74de Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Tue, 22 Jul 2025 22:01:37 +0000 Subject: [PATCH 37/41] Add more test assemblies (#103) This PR helps probe the capabilities of the engine. Also adjusts the realization triggering system to reduce redundant realizations as we set an assembly's regulators during loading. Specificially, consolidates all calls to `realize()` into a single effect, which is triggered by the `needs_realization` signal. Also introduces a `keep_realized` signal and use it to pause realization while loading assemblies, but this signal is planned for removal as ultimately we do not want a separate "mode" of interpreting commands during loading, for maximal reproducibility of results (and simplicity of system). Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/103 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/src/add_remove.rs | 259 ----- app-proto/src/assembly.rs | 42 +- app-proto/src/components.rs | 5 + app-proto/src/components/add_remove.rs | 54 + app-proto/src/{ => components}/diagnostics.rs | 0 app-proto/src/{ => components}/display.rs | 0 app-proto/src/{ => components}/identity.vert | 0 app-proto/src/{ => components}/outline.rs | 0 app-proto/src/{ => components}/point.frag | 0 app-proto/src/{ => components}/point.vert | 0 app-proto/src/{ => components}/spheres.frag | 0 .../src/components/test_assembly_chooser.rs | 947 ++++++++++++++++++ app-proto/src/main.rs | 15 +- 13 files changed, 1045 insertions(+), 277 deletions(-) delete mode 100644 app-proto/src/add_remove.rs create mode 100644 app-proto/src/components.rs create mode 100644 app-proto/src/components/add_remove.rs rename app-proto/src/{ => components}/diagnostics.rs (100%) rename app-proto/src/{ => components}/display.rs (100%) rename app-proto/src/{ => components}/identity.vert (100%) rename app-proto/src/{ => components}/outline.rs (100%) rename app-proto/src/{ => components}/point.frag (100%) rename app-proto/src/{ => components}/point.vert (100%) rename app-proto/src/{ => components}/spheres.frag (100%) create mode 100644 app-proto/src/components/test_assembly_chooser.rs diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs deleted file mode 100644 index d737c79..0000000 --- a/app-proto/src/add_remove.rs +++ /dev/null @@ -1,259 +0,0 @@ -use std::{f64::consts::FRAC_1_SQRT_2, rc::Rc}; -use sycamore::prelude::*; -use web_sys::{console, wasm_bindgen::JsValue}; - -use crate::{ - AppState, - engine, - engine::DescentHistory, - assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere} -}; - -/* DEBUG */ -// load an example assembly for testing. this code will be removed once we've -// built a more formal test assembly system -fn load_gen_assemb(assembly: &Assembly) { - let _ = assembly.try_insert_element( - Sphere::new( - String::from("gemini_a"), - String::from("Castor"), - [1.00_f32, 0.25_f32, 0.00_f32], - engine::sphere(0.5, 0.5, 0.0, 1.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("gemini_b"), - String::from("Pollux"), - [0.00_f32, 0.25_f32, 1.00_f32], - engine::sphere(-0.5, -0.5, 0.0, 1.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("ursa_major"), - String::from("Ursa major"), - [0.25_f32, 0.00_f32, 1.00_f32], - engine::sphere(-0.5, 0.5, 0.0, 0.75) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("ursa_minor"), - String::from("Ursa minor"), - [0.25_f32, 1.00_f32, 0.00_f32], - engine::sphere(0.5, -0.5, 0.0, 0.5) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("moon_deimos"), - String::from("Deimos"), - [0.75_f32, 0.75_f32, 0.00_f32], - engine::sphere(0.0, 0.15, 1.0, 0.25) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("moon_phobos"), - String::from("Phobos"), - [0.00_f32, 0.75_f32, 0.50_f32], - engine::sphere(0.0, -0.15, -1.0, 0.25) - ) - ); -} - -/* DEBUG */ -// load an example assembly for testing. this code will be removed once we've -// built a more formal test assembly system -fn load_low_curv_assemb(assembly: &Assembly) { - let a = 0.75_f64.sqrt(); - let _ = assembly.try_insert_element( - Sphere::new( - "central".to_string(), - "Central".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "assemb_plane".to_string(), - "Assembly plane".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "side1".to_string(), - "Side 1".to_string(), - [1.00_f32, 0.00_f32, 0.25_f32], - engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "side2".to_string(), - "Side 2".to_string(), - [0.25_f32, 1.00_f32, 0.00_f32], - engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "side3".to_string(), - "Side 3".to_string(), - [0.00_f32, 0.25_f32, 1.00_f32], - engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "corner1".to_string(), - "Corner 1".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "corner2".to_string(), - "Corner 2".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("corner3"), - String::from("Corner 3"), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0) - ) - ); -} - -fn load_pointed_assemb(assembly: &Assembly) { - let _ = assembly.try_insert_element( - Point::new( - format!("point_front"), - format!("Front point"), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::point(0.0, 0.0, FRAC_1_SQRT_2) - ) - ); - let _ = assembly.try_insert_element( - Point::new( - format!("point_back"), - format!("Back point"), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::point(0.0, 0.0, -FRAC_1_SQRT_2) - ) - ); - for index_x in 0..=1 { - for index_y in 0..=1 { - let x = index_x as f64 - 0.5; - let y = index_y as f64 - 0.5; - - let _ = assembly.try_insert_element( - Sphere::new( - format!("sphere{index_x}{index_y}"), - format!("Sphere {index_x}{index_y}"), - [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], - engine::sphere(x, y, 0.0, 1.0) - ) - ); - - let _ = assembly.try_insert_element( - Point::new( - format!("point{index_x}{index_y}"), - format!("Point {index_x}{index_y}"), - [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], - engine::point(x, y, 0.0) - ) - ); - } - } -} - -#[component] -pub fn AddRemove() -> View { - /* DEBUG */ - let assembly_name = create_signal("general".to_string()); - create_effect(move || { - // get name of chosen assembly - let name = assembly_name.get_clone(); - console::log_1( - &JsValue::from(format!("Showing assembly \"{}\"", name.clone())) - ); - - batch(|| { - let state = use_context::(); - let assembly = &state.assembly; - - // clear state - assembly.regulators.update(|regs| regs.clear()); - assembly.elements.update(|elts| elts.clear()); - assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); - assembly.descent_history.set(DescentHistory::new()); - state.selection.update(|sel| sel.clear()); - - // load assembly - match name.as_str() { - "general" => load_gen_assemb(assembly), - "low-curv" => load_low_curv_assemb(assembly), - "pointed" => load_pointed_assemb(assembly), - _ => () - }; - }); - }); - - view! { - div(id="add-remove") { - button( - on:click=|_| { - let state = use_context::(); - state.assembly.insert_element_default::(); - } - ) { "Add sphere" } - button( - on:click=|_| { - let state = use_context::(); - state.assembly.insert_element_default::(); - } - ) { "Add point" } - button( - class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button - disabled={ - let state = use_context::(); - state.selection.with(|sel| sel.len() != 2) - }, - on:click=|_| { - let state = use_context::(); - let subjects: [_; 2] = state.selection.with( - // the button is only enabled when two elements are - // selected, so we know the cast to a two-element array - // will succeed - |sel| sel - .clone() - .into_iter() - .collect::>() - .try_into() - .unwrap() - ); - state.assembly.insert_regulator( - Rc::new(InversiveDistanceRegulator::new(subjects)) - ); - state.selection.update(|sel| sel.clear()); - } - ) { "🔗" } - select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser - option(value="general") { "General" } - option(value="low-curv") { "Low-curvature" } - option(value="pointed") { "Pointed" } - option(value="empty") { "Empty" } - } - } - } -} \ No newline at end of file diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index c3b0c6b..68fcd8b 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -13,7 +13,7 @@ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::{ - display::DisplayItem, + components::{display::DisplayItem, outline::OutlineItem}, engine::{ Q, change_half_curvature, @@ -29,7 +29,6 @@ use crate::{ DescentHistory, Realization }, - outline::OutlineItem, specified::SpecifiedValue }; @@ -552,6 +551,10 @@ pub struct Assembly { // indexing pub elements_by_id: Signal>>, + // realization control + pub keep_realized: Signal, + pub needs_realization: Signal, + // realization diagnostics pub realization_status: Signal>, pub descent_history: Signal @@ -559,14 +562,30 @@ pub struct Assembly { impl Assembly { pub fn new() -> Assembly { - Assembly { + // create an assembly + let assembly = Assembly { elements: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), elements_by_id: create_signal(BTreeMap::default()), + keep_realized: create_signal(true), + needs_realization: create_signal(false), realization_status: create_signal(Ok(())), descent_history: create_signal(DescentHistory::new()) - } + }; + + // 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 || { + let should_realize = assembly_for_effect.keep_realized.get() + && assembly_for_effect.needs_realization.get(); + if should_realize { + assembly_for_effect.realize(); + } + }); + + assembly } // --- inserting elements and regulators --- @@ -627,7 +646,7 @@ impl Assembly { regulators.update(|regs| regs.insert(regulator.clone())); } - // update the realization when the regulator becomes a constraint, or is + // 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 || { @@ -636,7 +655,7 @@ impl Assembly { console_log!("Updated regulator with subjects {:?}", regulator.subjects()); if regulator.try_activate() { - self_for_effect.realize(); + self_for_effect.needs_realization.set(true); } }); @@ -731,6 +750,9 @@ impl Assembly { // 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 @@ -826,10 +848,10 @@ impl Assembly { }); } - // bring the configuration back onto the solution variety. this also - // gets the elements' column indices and the saved tangent space back in - // sync - self.realize(); + // 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.needs_realization.set(true); } } diff --git a/app-proto/src/components.rs b/app-proto/src/components.rs new file mode 100644 index 0000000..7387d58 --- /dev/null +++ b/app-proto/src/components.rs @@ -0,0 +1,5 @@ +pub mod add_remove; +pub mod diagnostics; +pub mod display; +pub mod outline; +pub mod test_assembly_chooser; \ No newline at end of file diff --git a/app-proto/src/components/add_remove.rs b/app-proto/src/components/add_remove.rs new file mode 100644 index 0000000..3b0f9e0 --- /dev/null +++ b/app-proto/src/components/add_remove.rs @@ -0,0 +1,54 @@ +use std::rc::Rc; +use sycamore::prelude::*; + +use super::test_assembly_chooser::TestAssemblyChooser; +use crate::{ + AppState, + assembly::{InversiveDistanceRegulator, Point, Sphere} +}; + +#[component] +pub fn AddRemove() -> View { + view! { + div(id="add-remove") { + button( + on:click=|_| { + let state = use_context::(); + state.assembly.insert_element_default::(); + } + ) { "Add sphere" } + button( + on:click=|_| { + let state = use_context::(); + state.assembly.insert_element_default::(); + } + ) { "Add point" } + button( + class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button + disabled={ + let state = use_context::(); + state.selection.with(|sel| sel.len() != 2) + }, + on:click=|_| { + let state = use_context::(); + let subjects: [_; 2] = state.selection.with( + // the button is only enabled when two elements are + // selected, so we know the cast to a two-element array + // will succeed + |sel| sel + .clone() + .into_iter() + .collect::>() + .try_into() + .unwrap() + ); + state.assembly.insert_regulator( + Rc::new(InversiveDistanceRegulator::new(subjects)) + ); + state.selection.update(|sel| sel.clear()); + } + ) { "🔗" } + TestAssemblyChooser {} + } + } +} \ No newline at end of file diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/components/diagnostics.rs similarity index 100% rename from app-proto/src/diagnostics.rs rename to app-proto/src/components/diagnostics.rs diff --git a/app-proto/src/display.rs b/app-proto/src/components/display.rs similarity index 100% rename from app-proto/src/display.rs rename to app-proto/src/components/display.rs diff --git a/app-proto/src/identity.vert b/app-proto/src/components/identity.vert similarity index 100% rename from app-proto/src/identity.vert rename to app-proto/src/components/identity.vert diff --git a/app-proto/src/outline.rs b/app-proto/src/components/outline.rs similarity index 100% rename from app-proto/src/outline.rs rename to app-proto/src/components/outline.rs diff --git a/app-proto/src/point.frag b/app-proto/src/components/point.frag similarity index 100% rename from app-proto/src/point.frag rename to app-proto/src/components/point.frag diff --git a/app-proto/src/point.vert b/app-proto/src/components/point.vert similarity index 100% rename from app-proto/src/point.vert rename to app-proto/src/components/point.vert diff --git a/app-proto/src/spheres.frag b/app-proto/src/components/spheres.frag similarity index 100% rename from app-proto/src/spheres.frag rename to app-proto/src/components/spheres.frag diff --git a/app-proto/src/components/test_assembly_chooser.rs b/app-proto/src/components/test_assembly_chooser.rs new file mode 100644 index 0000000..232cda3 --- /dev/null +++ b/app-proto/src/components/test_assembly_chooser.rs @@ -0,0 +1,947 @@ +use itertools::izip; +use std::{f64::consts::{FRAC_1_SQRT_2, PI}, rc::Rc}; +use nalgebra::Vector3; +use sycamore::prelude::*; +use web_sys::{console, wasm_bindgen::JsValue}; + +use crate::{ + AppState, + engine, + engine::DescentHistory, + assembly::{ + Assembly, + Element, + ElementColor, + InversiveDistanceRegulator, + Point, + Sphere + }, + specified::SpecifiedValue +}; + +// --- loaders --- + +/* DEBUG */ +// each of these functions loads an example assembly for testing. once we've +// done more work on saving and loading assemblies, we should come back to this +// code to see if it can be simplified + +fn load_gen_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Sphere::new( + String::from("gemini_a"), + String::from("Castor"), + [1.00_f32, 0.25_f32, 0.00_f32], + engine::sphere(0.5, 0.5, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("gemini_b"), + String::from("Pollux"), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere(-0.5, -0.5, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("ursa_major"), + String::from("Ursa major"), + [0.25_f32, 0.00_f32, 1.00_f32], + engine::sphere(-0.5, 0.5, 0.0, 0.75) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("ursa_minor"), + String::from("Ursa minor"), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere(0.5, -0.5, 0.0, 0.5) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("moon_deimos"), + String::from("Deimos"), + [0.75_f32, 0.75_f32, 0.00_f32], + engine::sphere(0.0, 0.15, 1.0, 0.25) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("moon_phobos"), + String::from("Phobos"), + [0.00_f32, 0.75_f32, 0.50_f32], + engine::sphere(0.0, -0.15, -1.0, 0.25) + ) + ); +} + +fn load_low_curv_assemb(assembly: &Assembly) { + // create the spheres + let a = 0.75_f64.sqrt(); + let _ = assembly.try_insert_element( + Sphere::new( + "central".to_string(), + "Central".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "assemb_plane".to_string(), + "Assembly plane".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "side1".to_string(), + "Side 1".to_string(), + [1.00_f32, 0.00_f32, 0.25_f32], + engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "side2".to_string(), + "Side 2".to_string(), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "side3".to_string(), + "Side 3".to_string(), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "corner1".to_string(), + "Corner 1".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "corner2".to_string(), + "Corner 2".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("corner3"), + String::from("Corner 3"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0) + ) + ); + + // impose the desired tangencies and make the sides planar + let index_range = 1..=3; + let [central, assemb_plane] = ["central", "assemb_plane"].map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[id].clone() + ) + ); + let sides = index_range.clone().map( + |k| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("side{k}")].clone() + ) + ); + let corners = index_range.map( + |k| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("corner{k}")].clone() + ) + ); + for plane in [assemb_plane.clone()].into_iter().chain(sides.clone()) { + // fix the curvature of each plane + let curvature = plane.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + curvature.set_point().set(SpecifiedValue::try_from("0".to_string()).unwrap()); + } + let all_perpendicular = [central.clone()].into_iter() + .chain(sides.clone()) + .chain(corners.clone()); + for sphere in all_perpendicular { + // make each side and packed sphere perpendicular to the assembly plane + let right_angle = InversiveDistanceRegulator::new([sphere, assemb_plane.clone()]); + right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(right_angle)); + } + for sphere in sides.clone().chain(corners.clone()) { + // make each side and corner sphere tangent to the central sphere + let tangency = InversiveDistanceRegulator::new([sphere.clone(), central.clone()]); + tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(tangency)); + } + for (side_index, side) in sides.enumerate() { + // make each side tangent to the two adjacent corner spheres + for (corner_index, corner) in corners.clone().enumerate() { + if side_index != corner_index { + let tangency = InversiveDistanceRegulator::new([side.clone(), corner]); + tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(tangency)); + } + } + } +} + +fn load_pointed_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Point::new( + format!("point_front"), + format!("Front point"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(0.0, 0.0, FRAC_1_SQRT_2) + ) + ); + let _ = assembly.try_insert_element( + Point::new( + format!("point_back"), + format!("Back point"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(0.0, 0.0, -FRAC_1_SQRT_2) + ) + ); + for index_x in 0..=1 { + for index_y in 0..=1 { + let x = index_x as f64 - 0.5; + let y = index_y as f64 - 0.5; + + let _ = assembly.try_insert_element( + Sphere::new( + format!("sphere{index_x}{index_y}"), + format!("Sphere {index_x}{index_y}"), + [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + engine::sphere(x, y, 0.0, 1.0) + ) + ); + + let _ = assembly.try_insert_element( + Point::new( + format!("point{index_x}{index_y}"), + format!("Point {index_x}{index_y}"), + [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + engine::point(x, y, 0.0) + ) + ); + } + } +} + +// to finish describing the tridiminished icosahedron, set the inversive +// distance regulators as follows: +// A-A -0.25 +// A-B " +// B-C " +// C-C " +// A-C -0.25 * φ^2 = -0.6545084971874737 +fn load_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]; + const COLOR_C: ElementColor = [0.25_f32, 0.50_f32, 1.00_f32]; + let vertices = [ + Point::new( + "a1".to_string(), + "A₁".to_string(), + COLOR_A, + engine::point(0.25, 0.75, 0.75) + ), + Point::new( + "a2".to_string(), + "A₂".to_string(), + COLOR_A, + engine::point(0.75, 0.25, 0.75) + ), + Point::new( + "a3".to_string(), + "A₃".to_string(), + COLOR_A, + engine::point(0.75, 0.75, 0.25) + ), + Point::new( + "b1".to_string(), + "B₁".to_string(), + COLOR_B, + engine::point(0.75, -0.25, -0.25) + ), + Point::new( + "b2".to_string(), + "B₂".to_string(), + COLOR_B, + engine::point(-0.25, 0.75, -0.25) + ), + Point::new( + "b3".to_string(), + "B₃".to_string(), + COLOR_B, + engine::point(-0.25, -0.25, 0.75) + ), + Point::new( + "c1".to_string(), + "C₁".to_string(), + COLOR_C, + engine::point(0.0, -1.0, -1.0) + ), + Point::new( + "c2".to_string(), + "C₂".to_string(), + COLOR_C, + engine::point(-1.0, 0.0, -1.0) + ), + Point::new( + "c3".to_string(), + "C₃".to_string(), + COLOR_C, + engine::point(-1.0, -1.0, 0.0) + ) + ]; + for vertex in vertices { + let _ = assembly.try_insert_element(vertex); + } + + // create the faces + const COLOR_FACE: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; + let frac_1_sqrt_6 = 1.0 / 6.0_f64.sqrt(); + let frac_2_sqrt_6 = 2.0 * frac_1_sqrt_6; + let faces = [ + Sphere::new( + "face1".to_string(), + "Face 1".to_string(), + COLOR_FACE, + engine::sphere_with_offset(frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0) + ), + Sphere::new( + "face2".to_string(), + "Face 2".to_string(), + COLOR_FACE, + engine::sphere_with_offset(-frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0) + ), + Sphere::new( + "face3".to_string(), + "Face 3".to_string(), + COLOR_FACE, + engine::sphere_with_offset(-frac_1_sqrt_6, -frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, 0.0) + ) + ]; + for face in faces { + face.ghost().set(true); + let _ = assembly.try_insert_element(face); + } + + let index_range = 1..=3; + for j in index_range.clone() { + // make each face planar + let face = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("face{j}")].clone() + ); + let curvature_regulator = face.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + curvature_regulator.set_point().set( + SpecifiedValue::try_from("0".to_string()).unwrap() + ); + + // put each A vertex on the face it belongs to + let vertex_a = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("a{j}")].clone() + ); + let incidence_a = InversiveDistanceRegulator::new([face.clone(), vertex_a.clone()]); + incidence_a.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(incidence_a)); + + // regulate the B-C vertex distances + let vertices_bc = ["b", "c"].map( + |series| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("{series}{j}")].clone() + ) + ); + assembly.insert_regulator( + Rc::new(InversiveDistanceRegulator::new(vertices_bc)) + ); + + // get the pair of indices adjacent to `j` + let adjacent_indices = [j % 3 + 1, (j + 1) % 3 + 1]; + + for k in adjacent_indices.clone() { + for series in ["b", "c"] { + // put each B and C vertex on the faces it belongs to + let vertex = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("{series}{k}")].clone() + ); + let incidence = InversiveDistanceRegulator::new([face.clone(), vertex.clone()]); + incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(incidence)); + + // regulate the A-B and A-C vertex distances + assembly.insert_regulator( + Rc::new(InversiveDistanceRegulator::new([vertex_a.clone(), vertex])) + ); + } + } + + // regulate the A-A and C-C vertex distances + let adjacent_pairs = ["a", "c"].map( + |series| adjacent_indices.map( + |index| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("{series}{index}")].clone() + ) + ) + ); + for pair in adjacent_pairs { + assembly.insert_regulator( + Rc::new(InversiveDistanceRegulator::new(pair)) + ); + } + } +} + +// to finish describing the dodecahedral circle packing, set the inversive +// distance regulators to -1. some of the regulators have already been set +fn load_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) + ) + ); + let substrate = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id["substrate"].clone() + ); + + // fix the substrate's curvature + substrate.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ).set_point().set( + SpecifiedValue::try_from("0.5".to_string()).unwrap() + ); + + // add the circles to be packed + const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.00_f32]; + const COLOR_B: ElementColor = [1.00_f32, 0.00_f32, 0.25_f32]; + const COLOR_C: ElementColor = [0.25_f32, 0.00_f32, 1.00_f32]; + let phi = 0.5 + 1.25_f64.sqrt(); /* TO DO */ // replace with std::f64::consts::PHI when that gets stabilized + let phi_inv = 1.0 / phi; + let coord_scale = (phi + 2.0).sqrt(); + let face_scales = [phi_inv, (13.0 / 12.0) / coord_scale]; + let face_radii = [phi_inv, 5.0 / 12.0]; + let mut faces = Vec::>::new(); + let subscripts = ["₀", "₁"]; + for j in 0..2 { + for k in 0..2 { + let small_coord = face_scales[k] * (2.0*(j as f64) - 1.0); + let big_coord = face_scales[k] * (2.0*(k as f64) - 1.0) * phi; + + let id_num = format!("{j}{k}"); + let label_sub = format!("{}{}", subscripts[j], subscripts[k]); + + // add the A face + let id_a = format!("a{id_num}"); + let _ = assembly.try_insert_element( + Sphere::new( + id_a.clone(), + format!("A{label_sub}"), + COLOR_A, + engine::sphere(0.0, small_coord, big_coord, face_radii[k]) + ) + ); + faces.push( + assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id_a].clone() + ) + ); + + // add the B face + let id_b = format!("b{id_num}"); + let _ = assembly.try_insert_element( + Sphere::new( + id_b.clone(), + format!("B{label_sub}"), + COLOR_B, + engine::sphere(small_coord, big_coord, 0.0, face_radii[k]) + ) + ); + faces.push( + assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id_b].clone() + ) + ); + + // add the C face + let id_c = format!("c{id_num}"); + let _ = assembly.try_insert_element( + Sphere::new( + id_c.clone(), + format!("C{label_sub}"), + COLOR_C, + engine::sphere(big_coord, 0.0, small_coord, face_radii[k]) + ) + ); + faces.push( + assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id_c].clone() + ) + ); + } + } + + // make each face sphere perpendicular to the substrate + for face in faces { + let right_angle = InversiveDistanceRegulator::new([face, substrate.clone()]); + right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(right_angle)); + } + + // set up the tangencies that define the packing + for [long_edge_plane, short_edge_plane] in [["a", "b"], ["b", "c"], ["c", "a"]] { + for k in 0..2 { + let long_edge_ids = [ + format!("{long_edge_plane}{k}0"), + format!("{long_edge_plane}{k}1") + ]; + let short_edge_ids = [ + format!("{short_edge_plane}0{k}"), + format!("{short_edge_plane}1{k}") + ]; + let [long_edge, short_edge] = [long_edge_ids, short_edge_ids].map( + |edge_ids| edge_ids.map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id].clone() + ) + ) + ); + + // set up the short-edge tangency + let short_tangency = InversiveDistanceRegulator::new(short_edge.clone()); + if k == 0 { + short_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + } + assembly.insert_regulator(Rc::new(short_tangency)); + + // set up the side tangencies + for i in 0..2 { + for j in 0..2 { + let side_tangency = InversiveDistanceRegulator::new( + [long_edge[i].clone(), short_edge[j].clone()] + ); + if i == 0 && k == 0 { + side_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + } + assembly.insert_regulator(Rc::new(side_tangency)); + } + } + } + } +} + +// the initial configuration of this test assembly deliberately violates the +// constraints, so loading the assembly will trigger a non-trivial realization +fn load_balanced_assemb(assembly: &Assembly) { + // create the spheres + const R_OUTER: f64 = 10.0; + const R_INNER: f64 = 4.0; + let spheres = [ + Sphere::new( + "outer".to_string(), + "Outer".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, R_OUTER) + ), + Sphere::new( + "a".to_string(), + "A".to_string(), + [1.00_f32, 0.00_f32, 0.25_f32], + engine::sphere(0.0, 4.0, 0.0, R_INNER) + ), + Sphere::new( + "b".to_string(), + "B".to_string(), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere(0.0, -4.0, 0.0, R_INNER) + ), + ]; + for sphere in spheres { + let _ = assembly.try_insert_element(sphere); + } + + // get references to the spheres + let [outer, a, b] = ["outer", "a", "b"].map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[id].clone() + ) + ); + + // fix the diameters of the outer, sun, and moon spheres + for (sphere, radius) in [ + (outer.clone(), R_OUTER), + (a.clone(), R_INNER), + (b.clone(), R_INNER) + ] { + let curvature_regulator = sphere.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + let curvature = 0.5 / radius; + curvature_regulator.set_point().set( + SpecifiedValue::try_from(curvature.to_string()).unwrap() + ); + } + + // set the inversive distances between the spheres. as described above, the + // initial configuration deliberately violates these constraints + for inner in [a, b] { + let tangency = InversiveDistanceRegulator::new([outer.clone(), inner]); + tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(tangency)); + } +} + +// the initial configuration of this test assembly deliberately violates the +// constraints, so loading the assembly will trigger a non-trivial realization +fn load_off_center_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) + ), + ); + let _ = assembly.try_insert_element( + Sphere::new( + "sphere".to_string(), + "Sphere".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ), + ); + + // get references to the elements + let point_and_sphere = ["point", "sphere"].map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[id].clone() + ) + ); + + // put the point on the sphere + let incidence = InversiveDistanceRegulator::new(point_and_sphere); + incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(incidence)); +} + +// setting the inversive distances between the vertices to -2 gives a regular +// tetrahedron with side length 1, whose insphere and circumsphere have radii +// sqrt(1/6) and sqrt(3/2), respectively. to measure those radii, set an +// inversive distance of -1 between the insphere and each face, and then set an +// inversive distance of 0 between the circumsphere and each vertex +fn load_radius_ratio_assemb(assembly: &Assembly) { + let index_range = 1..=4; + + // create the spheres + const GRAY: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; + let spheres = [ + Sphere::new( + "sphere_faces".to_string(), + "Insphere".to_string(), + GRAY, + engine::sphere(0.0, 0.0, 0.0, 0.5) + ), + Sphere::new( + "sphere_vertices".to_string(), + "Circumsphere".to_string(), + GRAY, + engine::sphere(0.0, 0.0, 0.0, 0.25) + ) + ]; + for sphere in spheres { + let _ = assembly.try_insert_element(sphere); + } + + // create the vertices + let vertices = izip!( + index_range.clone(), + [ + [1.00_f32, 0.50_f32, 0.75_f32], + [1.00_f32, 0.75_f32, 0.50_f32], + [1.00_f32, 1.00_f32, 0.50_f32], + [0.75_f32, 0.50_f32, 1.00_f32] + ].into_iter(), + [ + engine::point(-0.6, -0.8, -0.6), + engine::point(-0.6, 0.8, 0.6), + engine::point(0.6, -0.8, 0.6), + engine::point(0.6, 0.8, -0.6) + ].into_iter() + ).map( + |(k, color, representation)| { + Point::new( + format!("v{k}"), + format!("Vertex {k}"), + color, + representation + ) + } + ); + for vertex in vertices { + let _ = assembly.try_insert_element(vertex); + } + + // create the faces + let base_dir = Vector3::new(1.0, 0.75, 1.0).normalize(); + let offset = base_dir.dot(&Vector3::new(-0.6, 0.8, 0.6)); + let faces = izip!( + index_range.clone(), + [ + [1.00_f32, 0.00_f32, 0.25_f32], + [1.00_f32, 0.25_f32, 0.00_f32], + [0.75_f32, 0.75_f32, 0.00_f32], + [0.25_f32, 0.00_f32, 1.00_f32] + ].into_iter(), + [ + engine::sphere_with_offset(base_dir[0], base_dir[1], base_dir[2], offset, 0.0), + engine::sphere_with_offset(base_dir[0], -base_dir[1], -base_dir[2], offset, 0.0), + engine::sphere_with_offset(-base_dir[0], base_dir[1], -base_dir[2], offset, 0.0), + engine::sphere_with_offset(-base_dir[0], -base_dir[1], base_dir[2], offset, 0.0) + ].into_iter() + ).map( + |(k, color, representation)| { + Sphere::new( + format!("f{k}"), + format!("Face {k}"), + color, + representation + ) + } + ); + for face in faces { + face.ghost().set(true); + let _ = assembly.try_insert_element(face); + } + + // impose the constraints + for j in index_range.clone() { + let [face_j, vertex_j] = [ + format!("f{j}"), + format!("v{j}") + ].map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id].clone() + ) + ); + + // make the faces planar + let curvature_regulator = face_j.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + curvature_regulator.set_point().set( + SpecifiedValue::try_from("0".to_string()).unwrap() + ); + + for k in index_range.clone().filter(|&index| index != j) { + let vertex_k = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("v{k}")].clone() + ); + + // fix the distances between the vertices + if j < k { + let distance_regulator = InversiveDistanceRegulator::new( + [vertex_j.clone(), vertex_k.clone()] + ); + assembly.insert_regulator(Rc::new(distance_regulator)); + } + + // put the vertices on the faces + let incidence_regulator = InversiveDistanceRegulator::new([face_j.clone(), vertex_k.clone()]); + incidence_regulator.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(incidence_regulator)); + } + } +} + +// to finish setting up the problem, fix the following curvatures: +// sun 1 +// moon 5/3 = 1.666666666666666... +// chain1 2 +// a tiny `x` or `z` nudge of the outer sphere reliably prevents realization +// failures before they happen, or resolves them after they happen. the result +// depends sensitively on the translation direction, suggesting that realization +// is failing because the engine is having trouble breaking a symmetry +// /* TO DO */ +// the engine's performance on this problem is scale-dependent! with the current +// initial conditions, realization fails for any order of imposing the remaining +// curvature constraints. scaling everything up by a factor of ten, as done in +// the original problem, makes realization succeed reliably. one potentially +// relevant difference is that a lot of the numbers in the current initial +// conditions are exactly representable as floats, unlike the analogous numbers +// in the scaled-up problem. the inexact representations might break the +// symmetry that's getting the engine stuck +fn load_irisawa_hexlet_assemb(assembly: &Assembly) { + let index_range = 1..=6; + let colors = [ + [1.00_f32, 0.00_f32, 0.25_f32], + [1.00_f32, 0.25_f32, 0.00_f32], + [0.75_f32, 0.75_f32, 0.00_f32], + [0.25_f32, 1.00_f32, 0.00_f32], + [0.00_f32, 0.25_f32, 1.00_f32], + [0.25_f32, 0.00_f32, 1.00_f32] + ].into_iter(); + + // create the spheres + let spheres = [ + Sphere::new( + "outer".to_string(), + "Outer".to_string(), + [0.5_f32, 0.5_f32, 0.5_f32], + engine::sphere(0.0, 0.0, 0.0, 1.5) + ), + Sphere::new( + "sun".to_string(), + "Sun".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, -0.75, 0.0, 0.75) + ), + Sphere::new( + "moon".to_string(), + "Moon".to_string(), + [0.25_f32, 0.25_f32, 0.25_f32], + engine::sphere(0.0, 0.75, 0.0, 0.75) + ), + ].into_iter().chain( + index_range.clone().zip(colors).map( + |(k, color)| { + let ang = (k as f64) * PI/3.0; + Sphere::new( + format!("chain{k}"), + format!("Chain {k}"), + color, + engine::sphere(1.0 * ang.sin(), 0.0, 1.0 * ang.cos(), 0.5) + ) + } + ) + ); + for sphere in spheres { + let _ = assembly.try_insert_element(sphere); + } + + // put the outer sphere in ghost mode and fix its curvature + let outer = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id["outer"].clone() + ); + outer.ghost().set(true); + let outer_curvature_regulator = outer.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + outer_curvature_regulator.set_point().set( + SpecifiedValue::try_from((1.0 / 3.0).to_string()).unwrap() + ); + + // impose the desired tangencies + let [outer, sun, moon] = ["outer", "sun", "moon"].map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[id].clone() + ) + ); + let chain = index_range.map( + |k| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("chain{k}")].clone() + ) + ); + for (chain_sphere, chain_sphere_next) in chain.clone().zip(chain.cycle().skip(1)) { + for (other_sphere, inversive_distance) in [ + (outer.clone(), "1"), + (sun.clone(), "-1"), + (moon.clone(), "-1"), + (chain_sphere_next.clone(), "-1") + ] { + let tangency = InversiveDistanceRegulator::new([chain_sphere.clone(), other_sphere]); + tangency.set_point.set(SpecifiedValue::try_from(inversive_distance.to_string()).unwrap()); + assembly.insert_regulator(Rc::new(tangency)); + } + } + + let outer_sun_tangency = InversiveDistanceRegulator::new([outer.clone(), sun]); + outer_sun_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(outer_sun_tangency)); + + let outer_moon_tangency = InversiveDistanceRegulator::new([outer.clone(), moon]); + outer_moon_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(outer_moon_tangency)); +} + +// --- chooser --- + +/* DEBUG */ +#[component] +pub fn TestAssemblyChooser() -> View { + // create an effect that loads the selected test assembly + let assembly_name = create_signal("general".to_string()); + create_effect(move || { + // get name of chosen assembly + let name = assembly_name.get_clone(); + console::log_1( + &JsValue::from(format!("Showing assembly \"{}\"", name.clone())) + ); + + batch(|| { + let state = use_context::(); + 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()); + assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); + assembly.descent_history.set(DescentHistory::new()); + state.selection.update(|sel| sel.clear()); + + // load assembly + match name.as_str() { + "general" => load_gen_assemb(assembly), + "low-curv" => load_low_curv_assemb(assembly), + "pointed" => load_pointed_assemb(assembly), + "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-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" } + } + } +} \ No newline at end of file diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index f905c46..152d11c 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -1,9 +1,6 @@ -mod add_remove; mod assembly; -mod diagnostics; -mod display; +mod components; mod engine; -mod outline; mod specified; #[cfg(test)] @@ -12,11 +9,13 @@ mod tests; use std::{collections::BTreeSet, rc::Rc}; use sycamore::prelude::*; -use add_remove::AddRemove; use assembly::{Assembly, Element}; -use diagnostics::Diagnostics; -use display::Display; -use outline::Outline; +use components::{ + add_remove::AddRemove, + diagnostics::Diagnostics, + display::Display, + outline::Outline +}; #[derive(Clone)] struct AppState { From 2eba80fb69318ae78337c012b71909bba0c13461 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Thu, 31 Jul 2025 22:21:32 +0000 Subject: [PATCH 38/41] Simplify the realization triggering system (#105) Simplifies the system that reactively triggers realizations, at the cost of removing the preconditioning step described in issue #101 and doing unnecessary realizations after certain kinds of updates. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/105 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/src/assembly.rs | 72 +++++-------------- app-proto/src/components/add_remove.rs | 17 ++++- .../src/components/test_assembly_chooser.rs | 6 -- app-proto/src/engine.rs | 57 ++++----------- 4 files changed, 48 insertions(+), 104 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 68fcd8b..26fb4aa 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -16,7 +16,6 @@ use crate::{ components::{display::DisplayItem, outline::OutlineItem}, engine::{ Q, - change_half_curvature, local_unif_to_std, point, project_point_to_normalized, @@ -358,16 +357,6 @@ pub trait Regulator: Serial + ProblemPoser + OutlineItem { fn subjects(&self) -> Vec>; fn measurement(&self) -> ReadSignal; fn set_point(&self) -> Signal; - - // 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 { @@ -488,18 +477,6 @@ impl Regulator for HalfCurvatureRegulator { fn set_point(&self) -> Signal { 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 { @@ -552,8 +529,7 @@ pub struct Assembly { pub elements_by_id: Signal>>, // realization control - pub keep_realized: Signal, - pub needs_realization: Signal, + pub realization_trigger: Signal<()>, // realization diagnostics pub realization_status: Signal>, @@ -568,21 +544,23 @@ impl Assembly { regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), elements_by_id: create_signal(BTreeMap::default()), - keep_realized: create_signal(true), - needs_realization: create_signal(false), + realization_trigger: create_signal(()), realization_status: create_signal(Ok(())), descent_history: create_signal(DescentHistory::new()) }; - // realize the assembly whenever it becomes simultaneously true that - // we're trying to keep it realized and it needs realization + // realize the assembly whenever the element list, the regulator list, + // a regulator's set point, or the realization trigger is updated let assembly_for_effect = assembly.clone(); create_effect(move || { - let should_realize = assembly_for_effect.keep_realized.get() - && assembly_for_effect.needs_realization.get(); - if should_realize { - assembly_for_effect.realize(); - } + assembly_for_effect.elements.track(); + assembly_for_effect.regulators.with( + |regs| for reg in regs { + reg.set_point().track(); + } + ); + assembly_for_effect.realization_trigger.track(); + assembly_for_effect.realize(); }); assembly @@ -646,19 +624,6 @@ 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:"); @@ -726,8 +691,10 @@ impl Assembly { } else { console_log!("✅️ Target accuracy achieved!"); } - console_log!("Steps: {}", history.scaled_loss.len() - 1); - console_log!("Loss: {}", history.scaled_loss.last().unwrap()); + if history.scaled_loss.len() > 0 { + console_log!("Steps: {}", history.scaled_loss.len() - 1); + console_log!("Loss: {}", history.scaled_loss.last().unwrap()); + } // report the loss history self.descent_history.set(history); @@ -750,9 +717,6 @@ impl Assembly { // 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 @@ -848,10 +812,10 @@ impl Assembly { }); } - // request a realization to bring the configuration back onto the + // trigger 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.needs_realization.set(true); + self.realization_trigger.set(()); } } diff --git a/app-proto/src/components/add_remove.rs b/app-proto/src/components/add_remove.rs index 3b0f9e0..a685482 100644 --- a/app-proto/src/components/add_remove.rs +++ b/app-proto/src/components/add_remove.rs @@ -14,7 +14,22 @@ pub fn AddRemove() -> View { button( on:click=|_| { let state = use_context::(); - state.assembly.insert_element_default::(); + 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::(); + }); } ) { "Add sphere" } button( diff --git a/app-proto/src/components/test_assembly_chooser.rs b/app-proto/src/components/test_assembly_chooser.rs index 232cda3..b58dd1a 100644 --- a/app-proto/src/components/test_assembly_chooser.rs +++ b/app-proto/src/components/test_assembly_chooser.rs @@ -900,9 +900,6 @@ pub fn TestAssemblyChooser() -> View { let state = use_context::(); 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()); @@ -923,9 +920,6 @@ pub fn TestAssemblyChooser() -> View { "irisawa-hexlet" => load_irisawa_hexlet_assemb(assembly), _ => () }; - - // resume realization - assembly.keep_realized.set(true); }); }); diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index e6ffa25..602fc57 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -1,7 +1,6 @@ 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 --- @@ -50,40 +49,6 @@ pub fn project_point_to_normalized(rep: &mut DVector) { 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, 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 { @@ -199,13 +164,6 @@ impl ConfigSubspace { ).collect::>().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; @@ -425,9 +383,22 @@ pub fn realize_gram( // 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 { + 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 From ef1a579ac0893a150c37f570729a4ba77d62b6e8 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 4 Aug 2025 23:34:33 +0000 Subject: [PATCH 39/41] 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 Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/108 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/examples/kaleidocycle.rs | 2 +- app-proto/examples/point-on-sphere.rs | 4 +- app-proto/examples/three-spheres.rs | 6 +- app-proto/src/assembly.rs | 48 +++--- app-proto/src/components/add_remove.rs | 14 +- app-proto/src/components/diagnostics.rs | 44 +++--- app-proto/src/components/display.rs | 117 ++++++++------ app-proto/src/components/outline.rs | 94 ++++++----- .../src/components/test_assembly_chooser.rs | 148 +++++++++--------- app-proto/src/engine.rs | 118 +++++++------- app-proto/src/main.rs | 8 +- app-proto/src/specified.rs | 4 +- 12 files changed, 310 insertions(+), 297 deletions(-) diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs index 7ca1f97..ae4eb07 100644 --- a/app-proto/examples/kaleidocycle.rs +++ b/app-proto/examples/kaleidocycle.rs @@ -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)]; diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 89dee76..a73490e 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -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); } diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index aa5a105..7901e31 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -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); diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 26fb4aa..43066fd 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,13 +1,13 @@ use nalgebra::{DMatrix, DVector, DVectorView}; use std::{ cell::Cell, - collections::{BTreeMap, BTreeSet}, cmp::Ordering, + collections::{BTreeMap, BTreeSet}, fmt, 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 */ @@ -26,9 +26,9 @@ use crate::{ ConfigSubspace, ConstraintProblem, DescentHistory, - Realization + Realization, }, - specified::SpecifiedValue + specified::SpecifiedValue, }; pub type ElementColor = [f32; 3]; @@ -164,7 +164,7 @@ pub struct Sphere { pub ghost: Signal, pub regulators: Signal>>, serial: u64, - column_index: Cell> + column_index: Cell>, } impl Sphere { @@ -174,17 +174,17 @@ impl Sphere { id: String, label: String, color: ElementColor, - representation: DVector + representation: DVector, ) -> Sphere { Sphere { - id: id, - label: label, - color: color, + 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(), } } } @@ -199,7 +199,7 @@ impl Element for Sphere { 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), ) } @@ -264,7 +264,7 @@ pub struct Point { pub ghost: Signal, pub regulators: Signal>>, serial: u64, - column_index: Cell> + column_index: Cell>, } impl Point { @@ -274,7 +274,7 @@ impl Point { id: String, label: String, color: ElementColor, - representation: DVector + representation: DVector, ) -> Point { Point { id, @@ -284,7 +284,7 @@ impl Point { ghost: create_signal(false), regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), - column_index: None.into() + column_index: None.into(), } } } @@ -299,7 +299,7 @@ impl Element for Point { 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), ) } @@ -389,7 +389,7 @@ pub struct InversiveDistanceRegulator { pub subjects: [Rc; 2], pub measurement: ReadSignal, pub set_point: Signal, - serial: u64 + serial: u64, } impl InversiveDistanceRegulator { @@ -449,7 +449,7 @@ pub struct HalfCurvatureRegulator { pub subject: Rc, pub measurement: ReadSignal, pub set_point: Signal, - serial: u64 + serial: u64, } impl HalfCurvatureRegulator { @@ -501,7 +501,7 @@ impl ProblemPoser for HalfCurvatureRegulator { // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub element: Rc, - pub velocity: DVectorView<'a, f64> + pub velocity: DVectorView<'a, f64>, } type AssemblyMotion<'a> = Vec>; @@ -533,7 +533,7 @@ pub struct Assembly { // realization diagnostics pub realization_status: Signal>, - pub descent_history: Signal + pub descent_history: Signal, } impl Assembly { @@ -546,7 +546,7 @@ 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()), }; // realize the assembly whenever the element list, the regulator list, @@ -724,7 +724,7 @@ impl Assembly { // `Err(message)` we received from the match: we're changing the // `Ok` type from `Realization` to `()` self.realization_status.set(Err(message)) - } + }, } } @@ -807,7 +807,7 @@ impl Assembly { }, None => { console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) - } + }, }; }); } @@ -867,7 +867,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 +881,7 @@ mod tests { vec![ ElementMotion { element: sphere.clone(), - velocity: velocity.as_view() + velocity: velocity.as_view(), } ] ); diff --git a/app-proto/src/components/add_remove.rs b/app-proto/src/components/add_remove.rs index a685482..4196640 100644 --- a/app-proto/src/components/add_remove.rs +++ b/app-proto/src/components/add_remove.rs @@ -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::(); 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::(); state.assembly.insert_element_default::(); } ) { "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::(); state.selection.with(|sel| sel.len() != 2) }, - on:click=|_| { + on:click = |_| { let state = use_context::(); let subjects: [_; 2] = state.selection.with( // the button is only enabled when two elements are diff --git a/app-proto/src/components/diagnostics.rs b/app-proto/src/components/diagnostics.rs index a2f090a..b274dca 100644 --- a/app-proto/src/components/diagnostics.rs +++ b/app-proto/src/components/diagnostics.rs @@ -11,14 +11,12 @@ use crate::AppState; #[derive(Clone)] struct DiagnosticsState { - active_tab: Signal + active_tab: Signal, } impl DiagnosticsState { fn new(initial_tab: String) -> DiagnosticsState { - DiagnosticsState { - active_tab: create_signal(initial_tab) - } + DiagnosticsState { 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(), } )) } @@ -53,7 +51,7 @@ fn RealizationStatus() -> View { fn into_log10_time_point((step, value): (usize, f64)) -> Vec> { 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 +103,7 @@ fn LossHistory() -> View { }); view! { - div(id=CONTAINER_ID, class="diagnostics-chart") + div(id = CONTAINER_ID, class = "diagnostics-chart") } } @@ -122,7 +120,7 @@ fn SpectrumHistory() -> View { // positive, negative, and strictly-zero parts let ( hess_eigvals_zero, - hess_eigvals_nonzero + hess_eigvals_nonzero, ): (Vec<_>, Vec<_>) = state.assembly.descent_history.with( |history| history.hess_eigvals .iter() @@ -143,7 +141,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 +209,7 @@ fn SpectrumHistory() -> View { }); view! { - div(id=CONTAINER_ID, class="diagnostics-chart") + div(id = CONTAINER_ID, class = "diagnostics-chart") } } @@ -220,8 +218,8 @@ fn DiagnosticsPanel(name: &'static str, children: Children) -> View { let diagnostics_state = use_context::(); 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 +241,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" } } } - DiagnosticsPanel(name="loss") { LossHistory {} } - DiagnosticsPanel(name="spectrum") { SpectrumHistory {} } + DiagnosticsPanel(name = "loss") { LossHistory {} } + DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} } } } } \ No newline at end of file diff --git a/app-proto/src/components/display.rs b/app-proto/src/components/display.rs index 1646c4e..a0cdba6 100644 --- a/app-proto/src/components/display.rs +++ b/app-proto/src/components/display.rs @@ -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>, colors_with_opacity: Vec, - highlights: Vec + highlights: Vec, } impl SceneSpheres { - fn new() -> SceneSpheres{ + fn new() -> SceneSpheres { SceneSpheres { 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, color: ElementColor, opacity: f32, highlight: f32) { + fn push( + &mut self, representation: DVector, + color: ElementColor, opacity: f32, highlight: f32, + ) { self.representations.push(representation); self.colors_with_opacity.push(combine_channels(color, opacity)); self.highlights.push(highlight); @@ -64,7 +67,7 @@ struct ScenePoints { representations: Vec>, colors_with_opacity: Vec, highlights: Vec, - selections: Vec + selections: Vec, } impl ScenePoints { @@ -73,11 +76,14 @@ impl ScenePoints { representations: Vec::new(), colors_with_opacity: Vec::new(), highlights: Vec::new(), - selections: Vec::new() + selections: Vec::new(), } } - fn push(&mut self, representation: DVector, color: ElementColor, opacity: f32, highlight: f32, selected: bool) { + fn push( + &mut self, representation: DVector, + 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 { 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, assembly_to_world: &DMatrix, pixel_size: f64) -> Option; + fn cast( + &self, + dir: Vector3, + assembly_to_world: &DMatrix, + pixel_size: f64, + ) -> Option; } 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, assembly_to_world: &DMatrix, _pixel_size: f64) -> Option { + fn cast( + &self, + dir: Vector3, + assembly_to_world: &DMatrix, + _pixel_size: f64, + ) -> Option { // 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, assembly_to_world: &DMatrix, pixel_size: f64) -> Option { + fn cast( + &self, + dir: Vector3, + assembly_to_world: &DMatrix, + pixel_size: f64, + ) -> Option { 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( context: &WebGl2RenderingContext, program: &WebGlProgram, var_name: &str, - member_name_opt: Option<&str> + member_name_opt: Option<&str>, ) -> [Option; 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 + buffer: &Option, ) { 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 { // 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) { 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); @@ -596,7 +617,7 @@ pub fn Display() -> View { vec![ ElementMotion { element: sel, - velocity: elt_motion.as_view() + velocity: elt_motion.as_view(), } ] ); @@ -629,7 +650,7 @@ pub fn Display() -> View { 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 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 +689,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 +794,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 +814,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 +826,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 +857,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 +879,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 +887,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 +904,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()), }; - } + }, ) } } \ No newline at end of file diff --git a/app-proto/src/components/outline.rs b/app-proto/src/components/outline.rs index 77d8575..5355042 100644 --- a/app-proto/src/components/outline.rs +++ b/app-proto/src/components/outline.rs @@ -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,7 @@ use crate::{ Element, HalfCurvatureRegulator, InversiveDistanceRegulator, - Regulator + Regulator, }, specified::SpecifiedValue }; @@ -49,8 +45,8 @@ fn RegulatorInput(regulator: Rc) -> 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 +59,27 @@ fn RegulatorInput(regulator: Rc) -> 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 +96,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 +109,11 @@ impl OutlineItem for InversiveDistanceRegulator { impl OutlineItem for HalfCurvatureRegulator { fn outline_item(self: Rc, _element: &Rc) -> 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") } } } @@ -156,10 +152,10 @@ fn ElementOutlineItem(element: Rc) -> 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 +175,18 @@ fn ElementOutlineItem(element: Rc) -> View { .unchecked_into::() .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 +196,20 @@ fn ElementOutlineItem(element: Rc) -> 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 +242,18 @@ pub fn Outline() -> View { view! { ul( - id="outline", - on:click={ + id = "outline", + on:click = { let state = use_context::(); 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() ) } } diff --git a/app-proto/src/components/test_assembly_chooser.rs b/app-proto/src/components/test_assembly_chooser.rs index b58dd1a..5ed94ad 100644 --- a/app-proto/src/components/test_assembly_chooser.rs +++ b/app-proto/src/components/test_assembly_chooser.rs @@ -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 --- @@ -32,7 +32,7 @@ fn load_gen_assemb(assembly: &Assembly) { 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,7 +72,7 @@ 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), ) ); } @@ -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), ) ); @@ -202,7 +202,7 @@ fn load_pointed_assemb(assembly: &Assembly) { 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), ) ); } @@ -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); @@ -416,7 +416,7 @@ fn load_dodeca_packing_assemb(assembly: &Assembly) { "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( @@ -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() @@ -618,7 +618,7 @@ fn load_off_center_assemb(assembly: &Assembly) { "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), ), ); @@ -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() @@ -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()); @@ -918,24 +918,24 @@ pub fn TestAssemblyChooser() -> View { "off-center" => load_off_center_assemb(assembly), "radius-ratio" => load_radius_ratio_assemb(assembly), "irisawa-hexlet" => load_irisawa_hexlet_assemb(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-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" } } } } \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 602fc57..dc6b470 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -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), ]) } @@ -53,7 +53,7 @@ pub fn project_point_to_normalized(rep: &mut DVector) { pub struct MatrixEntry { index: (usize, usize), - value: f64 + value: f64, } pub struct PartialMatrix(Vec); @@ -65,7 +65,7 @@ impl PartialMatrix { pub fn push(&mut self, row: usize, col: usize, value: f64) { let PartialMatrix(entries) = self; - entries.push(MatrixEntry { index: (row, col), value: value }); + entries.push(MatrixEntry { index: (row, col), value }); } pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { @@ -135,22 +135,26 @@ impl<'a> IntoIterator for &'a PartialMatrix { pub struct ConfigSubspace { assembly_dim: usize, basis_std: Vec>, - basis_proj: Vec> + basis_proj: Vec>, } impl ConfigSubspace { pub fn zero(assembly_dim: usize) -> ConfigSubspace { ConfigSubspace { - assembly_dim: 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, proj_to_std: DMatrix, assembly_dim: usize) -> ConfigSubspace { + fn symmetric_kernel( + a: DMatrix, + proj_to_std: DMatrix, + 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 @@ -170,7 +174,7 @@ impl ConfigSubspace { const ELEMENT_DIM: usize = 5; const UNIFORM_DIM: usize = 4; ConfigSubspace { - assembly_dim: assembly_dim, + assembly_dim, basis_std: basis_std.column_iter().map( |v| Into::>::into( v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim)) @@ -180,7 +184,7 @@ impl ConfigSubspace { |v| Into::>::into( v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim)) ) - ).collect() + ).collect(), } } @@ -214,9 +218,9 @@ pub struct DescentHistory { pub config: Vec>, pub scaled_loss: Vec, pub neg_grad: Vec>, - pub hess_eigvals: Vec::>, + pub hess_eigvals: Vec>, pub base_step: Vec>, - pub backoff_steps: Vec + pub backoff_steps: Vec, } impl DescentHistory { @@ -246,7 +250,7 @@ impl ConstraintProblem { ConstraintProblem { gram: PartialMatrix::new(), frozen: PartialMatrix::new(), - guess: DMatrix::::zeros(ELEMENT_DIM, element_count) + guess: DMatrix::::zeros(ELEMENT_DIM, element_count), } } @@ -255,7 +259,7 @@ impl ConstraintProblem { ConstraintProblem { 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, err_proj: DMatrix, - loss: f64 + loss: f64, } impl SearchState { fn from_config(gram: &PartialMatrix, config: DMatrix) -> SearchState { 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 - } + SearchState { config, err_proj, loss } } } @@ -314,7 +314,7 @@ pub fn local_unif_to_std(v: DVectorView) -> DMatrix { 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) -> DMatrix { 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 { @@ -354,12 +354,12 @@ fn seek_better_config( // a first-order neighborhood of a configuration pub struct ConfigNeighborhood { pub config: DMatrix, - pub nbhd: ConfigSubspace + pub nbhd: ConfigSubspace, } pub struct Realization { pub result: Result, - 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(); @@ -391,10 +389,10 @@ pub fn realize_gram( let result = Ok( ConfigNeighborhood { config: guess.clone(), - nbhd: ConfigSubspace::zero(0) + 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 @@ -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::>().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::::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::::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::::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::::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::::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::::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::>() + ).collect::>(), ]; 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; diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index 152d11c..7ca0731 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -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>> + selection: Signal>>, } impl AppState { 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 {} diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs index cfe7fc3..ea1731c 100644 --- a/app-proto/src/specified.rs +++ b/app-proto/src/specified.rs @@ -13,7 +13,7 @@ use std::num::ParseFloatError; #[readonly::make] pub struct SpecifiedValue { pub spec: String, - pub value: Option + pub value: Option, } impl SpecifiedValue { @@ -37,7 +37,7 @@ impl TryFrom for SpecifiedValue { Ok(SpecifiedValue::from_empty_spec()) } else { spec.parse::().map( - |value| SpecifiedValue { spec: spec, value: Some(value) } + |value| SpecifiedValue { spec, value: Some(value) } ) } } From a4565281d5c3e7042ab9e1a2bdaa22bc52126b08 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Thu, 7 Aug 2025 23:24:07 +0000 Subject: [PATCH 40/41] Refactor: rename loaders and adopt 'Self' type convention (#111) Resolves #109. Resolves #110. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/111 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/src/assembly.rs | 26 ++++++------ app-proto/src/components/diagnostics.rs | 4 +- app-proto/src/components/display.rs | 12 +++--- .../src/components/test_assembly_chooser.rs | 42 +++++++++---------- app-proto/src/engine.rs | 32 +++++++------- app-proto/src/main.rs | 4 +- app-proto/src/specified.rs | 8 ++-- 7 files changed, 64 insertions(+), 64 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 43066fd..94e7b3c 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -175,8 +175,8 @@ impl Sphere { label: String, color: ElementColor, representation: DVector, - ) -> Sphere { - Sphere { + ) -> Self { + Self { id, label, color, @@ -194,8 +194,8 @@ 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], @@ -275,8 +275,8 @@ impl Point { label: String, color: ElementColor, representation: DVector, - ) -> Point { - Point { + ) -> Self { + Self { id, label, color, @@ -294,8 +294,8 @@ 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], @@ -348,7 +348,7 @@ impl ProblemPoser for Point { format!("Point \"{}\" should be indexed before writing problem data", self.id).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()); } } @@ -393,7 +393,7 @@ pub struct InversiveDistanceRegulator { } impl InversiveDistanceRegulator { - pub fn new(subjects: [Rc; 2]) -> InversiveDistanceRegulator { + pub fn new(subjects: [Rc; 2]) -> Self { let representations = subjects.each_ref().map(|subj| subj.representation()); let measurement = create_memo(move || { representations[0].with(|rep_0| @@ -406,7 +406,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 } } } @@ -453,7 +453,7 @@ pub struct HalfCurvatureRegulator { } impl HalfCurvatureRegulator { - pub fn new(subject: Rc) -> HalfCurvatureRegulator { + pub fn new(subject: Rc) -> Self { let measurement = subject.representation().map( |rep| rep[Sphere::CURVATURE_COMPONENT] ); @@ -461,7 +461,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 } } } diff --git a/app-proto/src/components/diagnostics.rs b/app-proto/src/components/diagnostics.rs index b274dca..e265982 100644 --- a/app-proto/src/components/diagnostics.rs +++ b/app-proto/src/components/diagnostics.rs @@ -15,8 +15,8 @@ struct DiagnosticsState { } 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) } } } diff --git a/app-proto/src/components/display.rs b/app-proto/src/components/display.rs index a0cdba6..da921dd 100644 --- a/app-proto/src/components/display.rs +++ b/app-proto/src/components/display.rs @@ -41,8 +41,8 @@ struct SceneSpheres { } impl SceneSpheres { - fn new() -> SceneSpheres { - SceneSpheres { + fn new() -> Self { + Self { representations: Vec::new(), colors_with_opacity: Vec::new(), highlights: Vec::new(), @@ -71,8 +71,8 @@ struct ScenePoints { } impl ScenePoints { - fn new() -> ScenePoints { - ScenePoints { + fn new() -> Self { + Self { representations: Vec::new(), colors_with_opacity: Vec::new(), highlights: Vec::new(), @@ -97,8 +97,8 @@ pub struct Scene { } impl Scene { - fn new() -> Scene { - Scene { + fn new() -> Self { + Self { spheres: SceneSpheres::new(), points: ScenePoints::new(), } diff --git a/app-proto/src/components/test_assembly_chooser.rs b/app-proto/src/components/test_assembly_chooser.rs index 5ed94ad..0d387d3 100644 --- a/app-proto/src/components/test_assembly_chooser.rs +++ b/app-proto/src/components/test_assembly_chooser.rs @@ -26,7 +26,7 @@ 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"), @@ -77,7 +77,7 @@ fn load_gen_assemb(assembly: &Assembly) { ); } -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( @@ -196,7 +196,7 @@ 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"), @@ -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]; @@ -409,7 +409,7 @@ 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( @@ -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; @@ -611,7 +611,7 @@ 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( @@ -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 @@ -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], @@ -909,15 +909,15 @@ 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), _ => (), }; }); @@ -927,10 +927,10 @@ pub fn TestAssemblyChooser() -> View { view! { select(bind:value = assembly_name) { option(value = "general") { "General" } - option(value = "low-curv") { "Low-curvature" } + option(value = "low-curvature") { "Low-curvature" } option(value = "pointed") { "Pointed" } - option(value = "tridim-icosahedron") { "Tridiminished icosahedron" } - option(value = "dodeca-packing") { "Dodecahedral packing" } + 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" } diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index dc6b470..d033c01 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -59,12 +59,12 @@ pub struct MatrixEntry { pub struct PartialMatrix(Vec); impl PartialMatrix { - pub fn new() -> PartialMatrix { - PartialMatrix(Vec::::new()) + pub fn new() -> Self { + Self(Vec::::new()) } pub fn push(&mut self, row: usize, col: usize, value: f64) { - let PartialMatrix(entries) = self; + let Self(entries) = self; entries.push(MatrixEntry { index: (row, col), value }); } @@ -114,7 +114,7 @@ impl IntoIterator for PartialMatrix { type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { - let PartialMatrix(entries) = self; + let Self(entries) = self; entries.into_iter() } } @@ -139,8 +139,8 @@ pub struct ConfigSubspace { } impl ConfigSubspace { - pub fn zero(assembly_dim: usize) -> ConfigSubspace { - ConfigSubspace { + pub fn zero(assembly_dim: usize) -> Self { + Self { assembly_dim, basis_proj: Vec::new(), basis_std: Vec::new(), @@ -154,7 +154,7 @@ impl ConfigSubspace { a: DMatrix, proj_to_std: DMatrix, assembly_dim: usize, - ) -> ConfigSubspace { + ) -> 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 @@ -173,7 +173,7 @@ impl ConfigSubspace { const ELEMENT_DIM: usize = 5; const UNIFORM_DIM: usize = 4; - ConfigSubspace { + Self { assembly_dim, basis_std: basis_std.column_iter().map( |v| Into::>::into( @@ -224,8 +224,8 @@ pub struct DescentHistory { } impl DescentHistory { - pub fn new() -> DescentHistory { - DescentHistory { + pub fn new() -> Self { + Self { config: Vec::>::new(), scaled_loss: Vec::::new(), neg_grad: Vec::>::new(), @@ -245,9 +245,9 @@ 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::::zeros(ELEMENT_DIM, element_count), @@ -255,8 +255,8 @@ impl ConstraintProblem { } #[cfg(feature = "dev")] - pub fn from_guess(guess_columns: &[DVector]) -> ConstraintProblem { - ConstraintProblem { + pub fn from_guess(guess_columns: &[DVector]) -> Self { + Self { gram: PartialMatrix::new(), frozen: PartialMatrix::new(), guess: DMatrix::from_columns(guess_columns), @@ -284,10 +284,10 @@ struct SearchState { } impl SearchState { - fn from_config(gram: &PartialMatrix, config: DMatrix) -> SearchState { + fn from_config(gram: &PartialMatrix, config: DMatrix) -> Self { let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config)); let loss = err_proj.norm_squared(); - SearchState { config, err_proj, loss } + Self { config, err_proj, loss } } } diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index 7ca0731..a03b026 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -24,8 +24,8 @@ struct AppState { } impl AppState { - fn new() -> AppState { - AppState { + fn new() -> Self { + Self { assembly: Assembly::new(), selection: create_signal(BTreeSet::default()), } diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs index ea1731c..788460b 100644 --- a/app-proto/src/specified.rs +++ b/app-proto/src/specified.rs @@ -17,8 +17,8 @@ pub struct SpecifiedValue { } 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 { @@ -34,10 +34,10 @@ impl TryFrom for SpecifiedValue { fn try_from(spec: String) -> Result { if spec.is_empty() { - Ok(SpecifiedValue::from_empty_spec()) + Ok(Self::from_empty_spec()) } else { spec.parse::().map( - |value| SpecifiedValue { spec, value: Some(value) } + |value| Self { spec, value: Some(value) } ) } } From af18a8e7d19067523de7bb4c8555c397a6f13d87 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 11 Aug 2025 03:33:19 +0000 Subject: [PATCH 41/41] 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 Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/113 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- README.md | 46 ++++++++++++++++++++-------- app-proto/Trunk.toml | 2 ++ deploy/.gitignore | 5 +++ tools/package-for-deployment.sh | 16 ++++++++++ {app-proto => tools}/run-examples.sh | 2 +- 5 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 app-proto/Trunk.toml create mode 100644 deploy/.gitignore create mode 100644 tools/package-for-deployment.sh rename {app-proto => tools}/run-examples.sh (89%) diff --git a/README.md b/README.md index 3a29eb0..cf3e589 100644 --- a/README.md +++ b/README.md @@ -25,32 +25,37 @@ 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) + - 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 + - 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 + - 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. + - 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* + - 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 ```julia include("irisawa-hexlet.jl") @@ -59,9 +64,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` + +### 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 \ No newline at end of file diff --git a/app-proto/Trunk.toml b/app-proto/Trunk.toml new file mode 100644 index 0000000..017deba --- /dev/null +++ b/app-proto/Trunk.toml @@ -0,0 +1,2 @@ +[build] +public_url = "./" \ No newline at end of file diff --git a/deploy/.gitignore b/deploy/.gitignore new file mode 100644 index 0000000..192f529 --- /dev/null +++ b/deploy/.gitignore @@ -0,0 +1,5 @@ +/dyna3.zip +/dyna3/index.html +/dyna3/dyna3-*.js +/dyna3/dyna3-*.wasm +/dyna3/main-*.css \ No newline at end of file diff --git a/tools/package-for-deployment.sh b/tools/package-for-deployment.sh new file mode 100644 index 0000000..fdda434 --- /dev/null +++ b/tools/package-for-deployment.sh @@ -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" diff --git a/app-proto/run-examples.sh b/tools/run-examples.sh similarity index 89% rename from app-proto/run-examples.sh rename to tools/run-examples.sh index 861addf..0946d92 100644 --- a/app-proto/run-examples.sh +++ b/tools/run-examples.sh @@ -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"