diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 3bf609c..9738589 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 = 4 +version = 3 [[package]] name = "ahash" @@ -89,6 +89,8 @@ dependencies = [ "lazy_static", "nalgebra", "readonly", + "rustc-hash", + "slab", "sycamore", "wasm-bindgen-test", "web-sys", @@ -363,6 +365,12 @@ 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" @@ -406,6 +414,15 @@ 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 844a0a6..5ab7299 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -3,7 +3,6 @@ name = "dyna3" version = "0.1.0" authors = ["Aaron Fenyes", "Glen Whitney"] edition = "2021" -rust-version = "1.86" [features] default = ["console_error_panic_hook"] @@ -15,6 +14,8 @@ 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 f3bbc97..ea86186 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)) + Rc::new(InversiveDistanceRegulator::new(subjects, &state.assembly)) ); state.selection.update(|sel| sel.clear()); } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index bd185c8..343cef8 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,14 +1,12 @@ use nalgebra::{DMatrix, DVector, DVectorView}; +use rustc_hash::FxHashMap; +use slab::Slab; use std::{ any::{Any, TypeId}, cell::Cell, - collections::{BTreeMap, BTreeSet}, - cmp::Ordering, - fmt, - fmt::{Debug, Formatter}, - hash::{Hash, Hasher}, + collections::BTreeSet, rc::Rc, - sync::{atomic, atomic::AtomicU64} + sync::atomic::{AtomicU64, Ordering} }; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ @@ -29,16 +27,49 @@ 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 item has a key that identifies it within its assembly and +// where each each element has a key that identifies it within its assembly and // each assembly has a key that identifies it within the sesssion -static NEXT_SERIAL: AtomicU64 = AtomicU64::new(0); +static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0); -pub trait Serial { +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>; + // a serial number that uniquely identifies this element fn serial(&self) -> u64; @@ -49,62 +80,11 @@ pub trait Serial { // // https://marabos.nl/atomics/atomics.html#example-handle-overflow // - NEXT_SERIAL.fetch_update( - atomic::Ordering::SeqCst, atomic::Ordering::SeqCst, + NEXT_ELEMENT_SERIAL.fetch_update( + Ordering::SeqCst, 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 @@ -117,35 +97,17 @@ pub trait Element: Serial + ProblemPoser + DisplayItem { fn set_column_index(&self, index: usize); } -impl Debug for dyn Element { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { - self.id().fmt(f) - } -} +// 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 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) +impl PartialEq for ElementRc { + fn eq(&self, ElementRc(other): &Self) -> bool { + let ElementRc(rc) = self; + Rc::ptr_eq(rc, &other) } } @@ -154,8 +116,8 @@ pub struct Sphere { pub label: String, pub color: ElementColor, pub representation: Signal>, - pub regulators: Signal>>, - serial: u64, + pub regulators: Signal>, + pub serial: u64, column_index: Cell> } @@ -173,7 +135,7 @@ impl Sphere { label: label, color: color, representation: create_signal(representation), - regulators: create_signal(BTreeSet::new()), + regulators: create_signal(BTreeSet::default()), serial: Self::next_serial(), column_index: None.into() } @@ -194,8 +156,8 @@ impl Element for Sphere { ) } - fn default_regulators(self: Rc) -> Vec> { - vec![Rc::new(HalfCurvatureRegulator::new(self))] + fn default_regulators(key: ElementKey, assembly: &Assembly) -> Vec> { + vec![Rc::new(HalfCurvatureRegulator::new(key, assembly))] } fn id(&self) -> &String { @@ -210,10 +172,14 @@ 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() } @@ -223,14 +189,8 @@ impl Element for Sphere { } } -impl Serial for Sphere { - fn serial(&self) -> u64 { - self.serial - } -} - impl ProblemPoser for Sphere { - fn pose(&self, problem: &mut ConstraintProblem) { + 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() ); @@ -244,8 +204,8 @@ pub struct Point { pub label: String, pub color: ElementColor, pub representation: Signal>, - pub regulators: Signal>>, - serial: u64, + pub regulators: Signal>, + pub serial: u64, column_index: Cell> } @@ -263,7 +223,7 @@ impl Point { label, color, representation: create_signal(representation), - regulators: create_signal(BTreeSet::new()), + regulators: create_signal(BTreeSet::default()), serial: Self::next_serial(), column_index: None.into() } @@ -296,10 +256,14 @@ 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() } @@ -309,14 +273,8 @@ impl Element for Point { } } -impl Serial for Point { - fn serial(&self) -> u64 { - self.serial - } -} - impl ProblemPoser for Point { - fn pose(&self, problem: &mut ConstraintProblem) { + 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() ); @@ -326,8 +284,8 @@ impl ProblemPoser for Point { } } -pub trait Regulator: Serial + ProblemPoser + OutlineItem { - fn subjects(&self) -> Vec>; +pub trait Regulator: ProblemPoser + OutlineItem { + fn subjects(&self) -> Vec; fn measurement(&self) -> ReadSignal; fn set_point(&self) -> Signal; @@ -337,65 +295,39 @@ pub trait Regulator: Serial + 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) -> bool { + fn try_activate(&self, _assembly: &Assembly) -> 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: [Rc; 2], + pub subjects: [ElementKey; 2], pub measurement: ReadSignal, - pub set_point: Signal, - serial: u64 + pub set_point: Signal } impl InversiveDistanceRegulator { - 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)) + 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()); - let serial = Self::next_serial(); - InversiveDistanceRegulator { subjects, measurement, set_point, serial } + InversiveDistanceRegulator { subjects, measurement, set_point } } } impl Regulator for InversiveDistanceRegulator { - fn subjects(&self) -> Vec> { - self.subjects.clone().into() + fn subjects(&self) -> Vec { + self.subjects.into() } fn measurement(&self) -> ReadSignal { @@ -407,18 +339,12 @@ impl Regulator for InversiveDistanceRegulator { } } -impl Serial for InversiveDistanceRegulator { - fn serial(&self) -> u64 { - self.serial - } -} - impl ProblemPoser for InversiveDistanceRegulator { - fn pose(&self, problem: &mut ConstraintProblem) { + 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.each_ref().map( - |subj| subj.column_index().expect( + let [row, col] = self.subjects.map( + |subj| elts[subj].column_index().expect( "Subjects should be indexed before inversive distance regulator writes problem data" ) ); @@ -429,28 +355,28 @@ impl ProblemPoser for InversiveDistanceRegulator { } pub struct HalfCurvatureRegulator { - pub subject: Rc, + pub subject: ElementKey, pub measurement: ReadSignal, - pub set_point: Signal, - serial: u64 + pub set_point: Signal } impl HalfCurvatureRegulator { - pub fn new(subject: Rc) -> HalfCurvatureRegulator { - let measurement = subject.representation().map( - |rep| rep[Sphere::CURVATURE_COMPONENT] + pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator { + let measurement = assembly.elements.map( + move |elts| elts[subject].representation().with( + |rep| rep[Sphere::CURVATURE_COMPONENT] + ) ); let set_point = create_signal(SpecifiedValue::from_empty_spec()); - let serial = Self::next_serial(); - HalfCurvatureRegulator { subject, measurement, set_point, serial } + HalfCurvatureRegulator { subject, measurement, set_point } } } impl Regulator for HalfCurvatureRegulator { - fn subjects(&self) -> Vec> { - vec![self.subject.clone()] + fn subjects(&self) -> Vec { + vec![self.subject] } fn measurement(&self) -> ReadSignal { @@ -461,10 +387,13 @@ impl Regulator for HalfCurvatureRegulator { self.set_point } - fn try_activate(&self) -> bool { + fn try_activate(&self, assembly: &Assembly) -> bool { match self.set_point.with(|set_pt| set_pt.value) { Some(half_curv) => { - self.subject.representation().update( + let representation = assembly.elements.with_untracked( + |elts| elts[self.subject].representation() + ); + representation.update( |rep| change_half_curvature(rep, half_curv) ); true @@ -474,17 +403,11 @@ impl Regulator for HalfCurvatureRegulator { } } -impl Serial for HalfCurvatureRegulator { - fn serial(&self) -> u64 { - self.serial - } -} - impl ProblemPoser for HalfCurvatureRegulator { - fn pose(&self, problem: &mut ConstraintProblem) { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { - let col = 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(Sphere::CURVATURE_COMPONENT, col, val); @@ -495,7 +418,7 @@ impl ProblemPoser for HalfCurvatureRegulator { // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { - pub element: Rc, + pub key: ElementKey, pub velocity: DVectorView<'a, f64> } @@ -505,8 +428,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 @@ -521,16 +444,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(BTreeSet::new()), - regulators: create_signal(BTreeSet::new()), + elements: create_signal(Slab::new()), + regulators: create_signal(Slab::new()), tangent: create_signal(ConfigSubspace::zero(0)), - elements_by_id: create_signal(BTreeMap::default()) + elements_by_id: create_signal(FxHashMap::default()) } } @@ -539,27 +462,29 @@ 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: impl Element + 'static) { + fn insert_element_unchecked(&self, elt: T) -> ElementKey { // insert the element let id = elt.id().clone(); - 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())); + 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)); // create and insert the element's default regulators - for reg in elt_rc.default_regulators() { + for reg in T::default_regulators(key, &self) { self.insert_regulator(reg); } + + key } - pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool { + 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()) ); if can_insert { - self.insert_element_unchecked(elt); + Some(self.insert_element_unchecked(elt)) + } else { + None } - can_insert } pub fn insert_element_default(&self) { @@ -580,16 +505,19 @@ impl Assembly { pub fn insert_regulator(&self, regulator: Rc) { // add the regulator to the assembly's regulator list - self.regulators.update( + let key = self.regulators.update( |regs| regs.insert(regulator.clone()) ); // add the regulator to each subject's regulator list - let subject_regulators: Vec<_> = regulator.subjects().into_iter().map( - |subj| subj.regulators() - ).collect(); + let subjects = regulator.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(regulator.clone())); + regulators.update(|regs| regs.insert(key)); } // update the realization when the regulator becomes a constraint, or is @@ -602,7 +530,7 @@ impl Assembly { format!("Updated regulator with subjects {:?}", regulator.subjects()) )); - if regulator.try_activate() { + if regulator.try_activate(&self_for_effect) { self_for_effect.realize(); } }); @@ -611,7 +539,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(), @@ -635,7 +563,7 @@ impl Assembly { pub fn realize(&self) { // index the elements self.elements.update_silent(|elts| { - for (index, elt) in elts.iter().enumerate() { + for (index, (_, elt)) in elts.into_iter().enumerate() { elt.set_column_index(index); } }); @@ -643,12 +571,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); + for (_, elt) in elts { + elt.pose(&mut problem, elts); } self.regulators.with_untracked(|regs| { - for reg in regs { - reg.pose(&mut problem); + for (_, reg) in regs { + reg.pose(&mut problem, elts); } }); problem @@ -690,7 +618,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())) ); @@ -726,17 +654,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 = { + let motion_dim = self.elements.update_silent(|elts| { let mut next_column_index = realized_dim; for elt_motion in motion.iter() { - let moving_elt = &elt_motion.element; + let moving_elt = &mut elts[elt_motion.key]; 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 @@ -747,7 +675,9 @@ 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 = elt_motion.element.column_index().unwrap(); + let column_index = self.elements.with_untracked( + |elts| elts[elt_motion.key].column_index().unwrap() + ); if column_index < realized_dim { // this element had a column index when we started, so by @@ -760,8 +690,12 @@ 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 = elt_motion.element.representation().with_untracked( - |rep| local_unif_to_std(rep.as_view()) + let unif_to_std = self.elements.with_untracked( + |elts| { + elts[elt_motion.key].representation().with_untracked( + |rep| local_unif_to_std(rep.as_view()) + ) + } ); target_column += unif_to_std * elt_motion.velocity; } @@ -771,7 +705,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) => { @@ -813,7 +747,7 @@ mod tests { fn unindexed_element_test() { let _ = create_root(|| { let elt = Sphere::default("sphere".to_string(), 0); - elt.pose(&mut ConstraintProblem::new(1)); + elt.pose(&mut ConstraintProblem::new(1), &Slab::new()); }); } @@ -821,16 +755,18 @@ 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 subjects = [0, 1].map( - |k| Rc::new(Sphere::default(format!("sphere{k}"), k)) as Rc - ); - subjects[0].set_column_index(0); + 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); InversiveDistanceRegulator { subjects: subjects, measurement: create_memo(|| 0.0), - set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()), - serial: InversiveDistanceRegulator::next_serial() - }.pose(&mut ConstraintProblem::new(2)); + 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/display.rs b/app-proto/src/display.rs index a2fe4b6..51b207d 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -1,6 +1,5 @@ use core::array; use nalgebra::{DMatrix, DVector, Rotation3, Vector3}; -use std::rc::Rc; use sycamore::{prelude::*, motion::create_raf}; use web_sys::{ console, @@ -17,7 +16,7 @@ use web_sys::{ use crate::{ AppState, - assembly::{Element, ElementColor, ElementMotion, Point, Sphere} + assembly::{ElementKey, ElementColor, ElementMotion, Point, Sphere} }; // --- scene data --- @@ -363,7 +362,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(); } }); @@ -549,7 +548,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().clone() + |sel| *sel.into_iter().next().unwrap() ); let translate_x = translate_pos_x_val - translate_neg_x_val; let translate_y = translate_pos_y_val - translate_neg_y_val; @@ -575,7 +574,7 @@ pub fn Display() -> View { assembly_for_raf.deform( vec![ ElementMotion { - element: sel, + key: sel, velocity: elt_motion.as_view() } ] @@ -616,8 +615,8 @@ pub fn Display() -> View { // set up the scene state.assembly.elements.with_untracked( - |elts| for elt in elts { - let selected = state.selection.with(|sel| sel.contains(elt)); + |elts| for (key, elt) in elts { + let selected = state.selection.with(|sel| sel.contains(&key)); elt.show(&mut scene, selected); } ); @@ -850,16 +849,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<(Rc, f64)> = None; - for elt in state.assembly.elements.get_clone_untracked() { + 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, pixel_size)) { Some(depth) => match clicked { Some((_, best_depth)) => { if depth < best_depth { - clicked = Some((elt, depth)) + clicked = Some((key, depth)) } }, - None => clicked = Some((elt, depth)) + None => clicked = Some((key, depth)) } None => () }; @@ -867,7 +866,7 @@ pub fn Display() -> View { // if we clicked something, select it match clicked { - Some((elt, _)) => state.select(&elt, event.shift_key()), + Some((key, _)) => state.select(key, 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 b76859a..e581997 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -8,41 +8,42 @@ mod specified; #[cfg(test)] mod tests; -use std::{collections::BTreeSet, rc::Rc}; +use rustc_hash::FxHashSet; use sycamore::prelude::*; use add_remove::AddRemove; -use assembly::{Assembly, Element}; +use assembly::{Assembly, ElementKey}; 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(BTreeSet::default()) + selection: create_signal(FxHashSet::default()) } } - // in single-selection mode, select the given element. in multiple-selection - // mode, toggle whether the given element is selected - fn select(&self, element: &Rc, multi: bool) { + // 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) { if multi { self.selection.update(|sel| { - if !sel.remove(element) { - sel.insert(element.clone()); + if !sel.remove(&key) { + sel.insert(key); } }); } else { self.selection.update(|sel| { sel.clear(); - sel.insert(element.clone()); + sel.insert(key); }); } } diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index caf11e8..2893b6d 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -11,9 +11,12 @@ use crate::{ AppState, assembly::{ Element, + ElementKey, + ElementRc, HalfCurvatureRegulator, InversiveDistanceRegulator, - Regulator + Regulator, + RegulatorKey }, specified::SpecifiedValue }; @@ -89,16 +92,20 @@ fn RegulatorInput(regulator: Rc) -> View { } pub trait OutlineItem { - fn outline_item(self: Rc, element: &Rc) -> View; + fn outline_item(self: Rc, element_key: ElementKey) -> View; } impl OutlineItem for InversiveDistanceRegulator { - fn outline_item(self: Rc, element: &Rc) -> View { - let other_subject_label = if self.subjects[0] == element.clone() { - self.subjects[1].label() + 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].label() - }.clone(); + 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) } @@ -111,7 +118,7 @@ impl OutlineItem for InversiveDistanceRegulator { } impl OutlineItem for HalfCurvatureRegulator { - fn outline_item(self: Rc, _element: &Rc) -> View { + fn outline_item(self: Rc, _element_key: ElementKey) -> View { view! { li(class="regulator") { div(class="regulator-label") // for spacing @@ -123,16 +130,23 @@ 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(element: Rc) -> View { +fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { let state = use_context::(); - let class = { - let element_for_class = element.clone(); - state.selection.map( - move |sel| if sel.contains(&element_for_class) { "selected" } else { "" } - ) - }; + let class = state.selection.map( + move |sel| if sel.contains(&key) { "selected" } else { "" } + ); let label = element.label().clone(); let representation = element.representation().clone(); let rep_components = move || { @@ -147,10 +161,14 @@ fn ElementOutlineItem(element: Rc) -> View { }; let regulated = element.regulators().map(|regs| regs.len() > 0); let regulator_list = element.regulators().map( - |regs| regs + move |elt_reg_keys| elt_reg_keys .clone() .into_iter() - .sorted_by_key(|reg| reg.subjects().len()) + .sorted_by_key( + |®_key| state.assembly.regulators.with( + |regs| regs[reg_key].subjects().len() + ) + ) .collect() ); let details_node = create_node_ref(); @@ -160,11 +178,10 @@ fn ElementOutlineItem(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(&element_for_handler, event.shift_key()); + state.select(key, event.shift_key()); event.prevent_default(); }, "ArrowRight" if regulated.get() => { @@ -191,10 +208,19 @@ fn ElementOutlineItem(element: Rc) -> View { div( class="element", on:click={ - let state_for_handler = state.clone(); - let element_for_handler = element.clone(); move |event: MouseEvent| { - state_for_handler.select(&element_for_handler, event.shift_key()); + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } event.stop_propagation(); event.prevent_default(); } @@ -208,8 +234,13 @@ fn ElementOutlineItem(element: Rc) -> View { ul(class="regulators") { Keyed( list=regulator_list, - view=move |reg| reg.outline_item(&element), - key=|reg| reg.serial() + view=move |reg_key| view! { + RegulatorOutlineItem( + regulator_key=reg_key, + element_key=key + ) + }, + key=|reg_key| reg_key.clone() ) } } @@ -228,15 +259,12 @@ 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()) + .sorted_by_key(|(_, elt)| elt.id().clone()) + .map(|(key, elt)| (key, ElementRc(elt))) .collect() ); @@ -250,10 +278,10 @@ pub fn Outline() -> View { ) { Keyed( list=element_list, - view=|elt| view! { - ElementOutlineItem(element=elt) + view=|(key, ElementRc(elt))| view! { + ElementOutlineItem(key=key, element=elt) }, - key=|elt| elt.serial() + key=|(_, ElementRc(elt))| elt.serial() ) } }