From c263d5b8a6b4acbc654fb4971d4fd65057e74e95 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 19 May 2025 18:54:45 -0700 Subject: [PATCH 1/9] Add a trait method to normalize representations This corrects the dispatching of the normalization routine for spheres, and it adds a (perhaps redundant) normalization routine for points. --- app-proto/src/assembly.rs | 30 ++++++++++++++++-------------- app-proto/src/engine.rs | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index bd185c8..c4690d2 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, @@ -19,6 +18,8 @@ use crate::{ Q, change_half_curvature, local_unif_to_std, + normalize_mut_point, + normalize_mut_sphere, point, realize_gram, sphere, @@ -106,6 +107,9 @@ pub trait Element: Serial + ProblemPoser + DisplayItem { // element is responsible for keeping this set up to date fn regulators(&self) -> Signal>>; + // normalize a representation vector for this kind of element + fn normalize_mut_rep(&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 @@ -214,6 +218,10 @@ impl Element for Sphere { self.regulators } + fn normalize_mut_rep(&self, rep: &mut DVector) { + normalize_mut_sphere(rep); + } + fn column_index(&self) -> Option { self.column_index.get() } @@ -300,6 +308,10 @@ impl Element for Point { self.regulators } + fn normalize_mut_rep(&self, rep: &mut DVector) { + normalize_mut_point(rep); + } + fn column_index(&self) -> Option { self.column_index.get() } @@ -769,24 +781,14 @@ 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.normalize_mut_rep(rep); }, None => { console::log_1(&JsValue::from( diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index b0fa23d..7bc2bdc 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -34,6 +34,21 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 ]) } +// normalize a sphere's representation vector by contracting toward the last +// coordinate axis +pub fn normalize_mut_sphere(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 normalize_mut_point(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 From f1da5155a3115f073c8f2f54e7811924d41a7275 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 20 May 2025 17:26:23 -0700 Subject: [PATCH 2/9] Test curvature drift during position nudging The test fails, as expected, if you disable sphere normalization by making `Sphere::normalize_mut_rep` do nothing. Since the tests aren't built for WebAssembly, we have to replace `console::log` with `console_log!` in all of the functions they use. We'll eventually want to do this replacement everywhere. --- app-proto/Cargo.toml | 3 ++ app-proto/src/assembly.rs | 92 +++++++++++++++++++++++++++------------ app-proto/src/engine.rs | 19 ++++---- 3 files changed, 76 insertions(+), 38 deletions(-) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 844a0a6..5d97193 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -46,6 +46,9 @@ features = [ dyna3 = { path = ".", default-features = false, features = ["dev"] } wasm-bindgen-test = "0.3.34" +[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 c4690d2..c1ebc91 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -610,9 +610,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(); @@ -621,10 +619,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( @@ -637,7 +635,7 @@ impl Assembly { } } ) - ))); + ); } }); } @@ -668,19 +666,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( @@ -689,16 +679,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 @@ -791,9 +779,7 @@ impl Assembly { elt.normalize_mut_rep(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()) } }; }); @@ -810,6 +796,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() { @@ -835,4 +823,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 7bc2bdc..03d13a9 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 --- @@ -109,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 { @@ -143,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 8732cb77c2456282280b8761a1fc9910fb2d4e2a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 29 May 2025 17:52:55 -0700 Subject: [PATCH 3/9] Explain the new `check-cfg` lint --- app-proto/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 5d97193..9b46b2b 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -46,6 +46,10 @@ 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)"] } From 54b34e05828a4bee4864e7087c9691cbb6d8001f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 1 Jun 2025 10:49:50 -0700 Subject: [PATCH 4/9] Improve the naming of the normalization methods --- app-proto/src/assembly.rs | 19 ++++++++++--------- app-proto/src/engine.rs | 8 ++++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index c1ebc91..436ac35 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -18,9 +18,9 @@ use crate::{ Q, change_half_curvature, local_unif_to_std, - normalize_mut_point, - normalize_mut_sphere, point, + project_point_to_normalized, + project_sphere_to_normalized, realize_gram, sphere, ConfigSubspace, @@ -107,8 +107,9 @@ pub trait Element: Serial + ProblemPoser + DisplayItem { // element is responsible for keeping this set up to date fn regulators(&self) -> Signal>>; - // normalize a representation vector for this kind of element - fn normalize_mut_rep(&self, rep: &mut DVector); + // 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 @@ -218,8 +219,8 @@ impl Element for Sphere { self.regulators } - fn normalize_mut_rep(&self, rep: &mut DVector) { - normalize_mut_sphere(rep); + fn project_to_normalized(&self, rep: &mut DVector) { + project_sphere_to_normalized(rep); } fn column_index(&self) -> Option { @@ -308,8 +309,8 @@ impl Element for Point { self.regulators } - fn normalize_mut_rep(&self, rep: &mut DVector) { - normalize_mut_point(rep); + fn project_to_normalized(&self, rep: &mut DVector) { + project_point_to_normalized(rep); } fn column_index(&self) -> Option { @@ -776,7 +777,7 @@ impl Assembly { // step the element along the deformation and then // restore its normalization *rep += motion_proj.column(column_index); - elt.normalize_mut_rep(rep); + elt.project_to_normalized(rep); }, None => { console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 03d13a9..c5d7b00 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -35,9 +35,9 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 ]) } -// normalize a sphere's representation vector by contracting toward the last -// coordinate axis -pub fn normalize_mut_sphere(rep: &mut DVector) { +// 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; @@ -46,7 +46,7 @@ pub fn normalize_mut_sphere(rep: &mut DVector) { } // normalize a point's representation vector by scaling -pub fn normalize_mut_point(rep: &mut DVector) { +pub fn project_point_to_normalized(rep: &mut DVector) { rep.scale_mut(0.5 / rep[3]); } From a671a8273ae183bf35b8d82e6cb4a024ff956f1d Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 2 Jun 2025 15:56:06 +0000 Subject: [PATCH 5/9] 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 f4e5c34fde35e5fe2eaa71d0f9d6fb5a7abb1d3d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 19 May 2025 18:54:45 -0700 Subject: [PATCH 6/9] Add a trait method to normalize representations This corrects the dispatching of the normalization routine for spheres, and it adds a (perhaps redundant) normalization routine for points. --- app-proto/src/assembly.rs | 30 ++++++++++++++++-------------- app-proto/src/engine.rs | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index e48b802..0aa6dab 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, @@ -19,6 +18,8 @@ use crate::{ Q, change_half_curvature, local_unif_to_std, + normalize_mut_point, + normalize_mut_sphere, point, realize_gram, sphere, @@ -107,6 +108,9 @@ pub trait Element: Serial + ProblemPoser + DisplayItem { // element is responsible for keeping this set up to date fn regulators(&self) -> Signal>>; + // normalize a representation vector for this kind of element + fn normalize_mut_rep(&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 +225,10 @@ impl Element for Sphere { self.regulators } + fn normalize_mut_rep(&self, rep: &mut DVector) { + normalize_mut_sphere(rep); + } + fn column_index(&self) -> Option { self.column_index.get() } @@ -313,6 +321,10 @@ impl Element for Point { self.regulators } + fn normalize_mut_rep(&self, rep: &mut DVector) { + normalize_mut_point(rep); + } + fn column_index(&self) -> Option { self.column_index.get() } @@ -782,24 +794,14 @@ 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.normalize_mut_rep(rep); }, None => { console::log_1(&JsValue::from( diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index b0fa23d..7bc2bdc 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -34,6 +34,21 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 ]) } +// normalize a sphere's representation vector by contracting toward the last +// coordinate axis +pub fn normalize_mut_sphere(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 normalize_mut_point(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 From 0cfdd59e237921355b9e72526c782d72d61ad504 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 20 May 2025 17:26:23 -0700 Subject: [PATCH 7/9] Test curvature drift during position nudging The test fails, as expected, if you disable sphere normalization by making `Sphere::normalize_mut_rep` do nothing. Since the tests aren't built for WebAssembly, we have to replace `console::log` with `console_log!` in all of the functions they use. We'll eventually want to do this replacement everywhere. --- app-proto/Cargo.toml | 3 ++ app-proto/src/assembly.rs | 92 +++++++++++++++++++++++++++------------ app-proto/src/engine.rs | 19 ++++---- 3 files changed, 76 insertions(+), 38 deletions(-) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 844a0a6..5d97193 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -46,6 +46,9 @@ features = [ dyna3 = { path = ".", default-features = false, features = ["dev"] } wasm-bindgen-test = "0.3.34" +[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 0aa6dab..2a23b4a 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -623,9 +623,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(); @@ -634,10 +632,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( @@ -650,7 +648,7 @@ impl Assembly { } } ) - ))); + ); } }); } @@ -681,19 +679,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( @@ -702,16 +692,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 @@ -804,9 +792,7 @@ impl Assembly { elt.normalize_mut_rep(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()) } }; }); @@ -823,6 +809,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() { @@ -848,4 +836,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 7bc2bdc..03d13a9 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 --- @@ -109,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 { @@ -143,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 e19792d961b4e2fa41d083251f6814ca61cd0e61 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 29 May 2025 17:52:55 -0700 Subject: [PATCH 8/9] Explain the new `check-cfg` lint --- app-proto/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 5d97193..9b46b2b 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -46,6 +46,10 @@ 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)"] } From f332f755e0966ef8e643e82d13860d3cc18fac86 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 1 Jun 2025 10:49:50 -0700 Subject: [PATCH 9/9] Improve the naming of the normalization methods --- app-proto/src/assembly.rs | 19 ++++++++++--------- app-proto/src/engine.rs | 8 ++++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 2a23b4a..6c91fc0 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -18,9 +18,9 @@ use crate::{ Q, change_half_curvature, local_unif_to_std, - normalize_mut_point, - normalize_mut_sphere, point, + project_point_to_normalized, + project_sphere_to_normalized, realize_gram, sphere, ConfigSubspace, @@ -108,8 +108,9 @@ pub trait Element: Serial + ProblemPoser + DisplayItem { // element is responsible for keeping this set up to date fn regulators(&self) -> Signal>>; - // normalize a representation vector for this kind of element - fn normalize_mut_rep(&self, rep: &mut DVector); + // 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 @@ -225,8 +226,8 @@ impl Element for Sphere { self.regulators } - fn normalize_mut_rep(&self, rep: &mut DVector) { - normalize_mut_sphere(rep); + fn project_to_normalized(&self, rep: &mut DVector) { + project_sphere_to_normalized(rep); } fn column_index(&self) -> Option { @@ -321,8 +322,8 @@ impl Element for Point { self.regulators } - fn normalize_mut_rep(&self, rep: &mut DVector) { - normalize_mut_point(rep); + fn project_to_normalized(&self, rep: &mut DVector) { + project_point_to_normalized(rep); } fn column_index(&self) -> Option { @@ -789,7 +790,7 @@ impl Assembly { // step the element along the deformation and then // restore its normalization *rep += motion_proj.column(column_index); - elt.normalize_mut_rep(rep); + elt.project_to_normalized(rep); }, None => { console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 03d13a9..c5d7b00 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -35,9 +35,9 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 ]) } -// normalize a sphere's representation vector by contracting toward the last -// coordinate axis -pub fn normalize_mut_sphere(rep: &mut DVector) { +// 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; @@ -46,7 +46,7 @@ pub fn normalize_mut_sphere(rep: &mut DVector) { } // normalize a point's representation vector by scaling -pub fn normalize_mut_point(rep: &mut DVector) { +pub fn project_point_to_normalized(rep: &mut DVector) { rep.scale_mut(0.5 / rep[3]); }