From f9df459a0de5c24577a3e756e6f239b9179314ef Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 23 Apr 2025 22:25:55 -0700 Subject: [PATCH] Introduce an element trait For now, this is just a thin wrapper around the old element structure, which was renamed to `Sphere` in the previous commit. The biggest organizational change is moving `cast` into the `DisplayItem` trait. --- app-proto/src/assembly.rs | 189 +++++++++++++++++++++----------------- app-proto/src/display.rs | 51 +++++++++- app-proto/src/outline.rs | 23 +++-- 3 files changed, 165 insertions(+), 98 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 41070fb..84b21f7 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,11 +1,17 @@ -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::{ + 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, @@ -33,28 +39,54 @@ 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>); +} + +pub trait Element: ProblemPoser + DisplayItem { + 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; + + // 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) + } } -#[derive(Clone, PartialEq)] 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 Sphere { @@ -84,57 +116,44 @@ impl Sphere { 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 + column_index: None.into() } } } +impl Element for Sphere { + 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( + 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); @@ -168,7 +187,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 +217,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,7 +240,7 @@ pub struct HalfCurvatureRegulator { impl HalfCurvatureRegulator { pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator { let measurement = assembly.elements.map( - move |elts| elts[subject].representation.with( + move |elts| elts[subject].representation().with( |rep| rep[Sphere::CURVATURE_COMPONENT] ) ); @@ -249,7 +268,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,10 +281,10 @@ 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(Sphere::CURVATURE_COMPONENT, col, val); @@ -286,7 +305,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 @@ -323,7 +342,7 @@ impl Assembly { fn insert_sphere_unchecked(&self, sphere: Sphere) -> ElementKey { // insert the sphere let id = sphere.id.clone(); - let key = self.elements.update(|elts| elts.insert(sphere)); + let key = self.elements.update(|elts| elts.insert(Rc::new(sphere))); self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); // regulate the sphere's curvature @@ -376,7 +395,7 @@ impl Assembly { let subjects = regulator_rc.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 { @@ -427,7 +446,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 +501,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 +540,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 +558,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 +574,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()) ) } @@ -570,8 +589,8 @@ impl Assembly { // since our test assemblies only include spheres, we assume that every // element is on the 1 mass shell 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); @@ -586,7 +605,7 @@ impl Assembly { }, 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()) )) } }; @@ -623,18 +642,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 mut elts = Slab::new(); + let mut elts = Slab::>::new(); let subjects = [0, 1].map(|k| { elts.insert( - Sphere::new( + Rc::new(Sphere::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); + 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 b6fb12e..46f0892 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -56,7 +56,7 @@ impl ScenePoints { } } -struct Scene { +pub struct Scene { spheres: SceneSpheres, points: ScenePoints } @@ -70,8 +70,13 @@ impl Scene { } } -trait DisplayItem { +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) -> Option; } impl DisplayItem for Sphere { @@ -82,6 +87,46 @@ impl DisplayItem for Sphere { 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) -> 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 + } + } } // --- WebGL utilities --- @@ -264,7 +309,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(); diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index e003a7c..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::Sphere) -> 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::Sphere) -> 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() ) } }