From 8a82fe457fc1aab47f082d4d53963d6c7781d005 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 21 Apr 2025 15:56:32 -0700 Subject: [PATCH 01/16] Encapsulate rendering program setup --- app-proto/src/display.rs | 64 ++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 4e0c7e4..e7478c1 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -27,6 +27,45 @@ fn compile_shader( shader } +fn create_program_with_shaders( + 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, @@ -186,31 +225,12 @@ pub fn Display() -> View { .dyn_into::() .unwrap(); - // compile and attach the vertex and fragment shaders - let vertex_shader = compile_shader( + // create and use the rendering program + let program = create_program_with_shaders( &ctx, - WebGl2RenderingContext::VERTEX_SHADER, include_str!("identity.vert"), + include_str!("inversive.frag") ); - let fragment_shader = compile_shader( - &ctx, - WebGl2RenderingContext::FRAGMENT_SHADER, - include_str!("inversive.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 */ -- 2.43.0 From 3590b1ad4ee6c6089b6cd3045bc0d5986f895bc2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 21 Apr 2025 20:06:54 -0700 Subject: [PATCH 02/16] Drop unused vertex array object --- app-proto/src/display.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index e7478c1..903193c 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -272,10 +272,6 @@ pub fn Display() -> View { let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); let debug_mode_loc = ctx.get_uniform_location(&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 const VERTEX_CNT: usize = 6; let positions: [f32; 3*VERTEX_CNT] = [ -- 2.43.0 From 23f395331aae1547e83a2ed1d13ea925ee467f58 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 21 Apr 2025 20:35:17 -0700 Subject: [PATCH 03/16] Sketch a point rendering pipeline --- app-proto/src/display.rs | 131 +++++++++++++----- app-proto/src/engine.rs | 1 - app-proto/src/point.frag | 9 ++ app-proto/src/point.vert | 12 ++ .../src/{inversive.frag => spheres.frag} | 0 5 files changed, 116 insertions(+), 37 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/src/display.rs b/app-proto/src/display.rs index 903193c..67e36e9 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -27,7 +27,7 @@ fn compile_shader( shader } -fn create_program_with_shaders( +fn set_up_program( context: &WebGl2RenderingContext, vertex_shader_source: &str, fragment_shader_source: &str @@ -131,7 +131,8 @@ fn event_dir(event: &MouseEvent) -> Vector3 { 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( @@ -225,13 +226,22 @@ pub fn Display() -> View { .dyn_into::() .unwrap(); - // create and use the rendering program - let program = create_program_with_shaders( + // disable depth testing + ctx.disable(WebGl2RenderingContext::DEPTH_TEST); + + // set up the sphere rendering program + let sphere_program = set_up_program( &ctx, include_str!("identity.vert"), - include_str!("inversive.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") ); - ctx.use_program(Some(&program)); /* DEBUG */ // print the maximum number of vectors that can be passed as @@ -250,31 +260,31 @@ pub fn Display() -> View { &JsValue::from("uniform vectors available") ); - // find indices of vertex attributes and uniforms + // find indices of sphere vertex attributes and 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 viewport_position_index = ctx.get_attrib_location(&sphere_program, "position") as u32; + 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"); - // set the vertex positions + // set the viewport vertex positions 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, @@ -284,7 +294,9 @@ pub fn Display() -> View { 1.0, 1.0, 0.0, 1.0, -1.0, 0.0 ]; - bind_vertex_attrib(&ctx, position_index, 3, &positions); + + // find indices of point vertex attributes and uniforms + let point_position_index = ctx.get_attrib_location(&point_program, "position") as u32; // set up a repainting routine let (_, start_animation_loop, _) = create_raf(move || { @@ -387,6 +399,8 @@ pub fn Display() -> View { frames_since_last_sample = 0; } + // --- get the assembly --- + // find the map from assembly space to world space let location = { let u = -location_z; @@ -400,12 +414,12 @@ pub fn Display() -> View { }; let asm_to_world = &location * &orientation; - // get the assembly + // get the spheres let ( - elt_cnt, - reps_world, - colors, - highlights + sphere_cnt, + sphere_reps_world, + sphere_colors, + sphere_highlights ) = state.assembly.elements.with(|elts| { ( // number of elements @@ -436,16 +450,45 @@ pub fn Display() -> View { ) }); + /* SCAFFOLDING */ + // get the points + let point_positions = { + use crate::engine::point; + + /* DEBUG */ + // hard-code the origin and the centers of the spheres in + // the general test assembly + let point_reps = [ + point(0.0, 0.0, 0.0), + point(0.5, 0.5, 0.0), + point(-0.5, -0.5, 0.0), + point(-0.5, 0.5, 0.0), + point(0.5, -0.5, 0.0), + point(0.0, 0.15, 1.0), + point(0.0, -0.15, -1.0) + ]; + + const SPACE_DIM: usize = 3; + let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM); + let point_reps_world_sp = point_reps.map(|rep| &asm_to_world_sp * rep); + DMatrix::from_columns(&point_reps_world_sp).cast::() + }; + + // --- draw the spheres --- + + // use the sphere rendering program + ctx.use_program(Some(&sphere_program)); + // set the resolution let width = canvas.width() as f32; let height = canvas.height() as f32; 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]; + // pass the assembly 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.uniform3f( sphere_sp_locs[n].as_ref(), v[0] as f32, v[1] as f32, v[2] as f32 @@ -455,12 +498,12 @@ pub fn Display() -> View { v[3] as f32, v[4] as f32 ); ctx.uniform3fv_with_f32_array( - color_locs[n].as_ref(), - &colors[n] + sphere_color_locs[n].as_ref(), + &sphere_colors[n] ); ctx.uniform1f( - highlight_locs[n].as_ref(), - highlights[n] + sphere_highlight_locs[n].as_ref(), + sphere_highlights[n] ); } @@ -469,9 +512,25 @@ pub fn Display() -> View { ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); + // pass the viewport vertex positions + bind_vertex_attrib(&ctx, viewport_position_index, 3, &viewport_positions); + // draw the scene ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); + // --- draw the points --- + + // use the point rendering program + ctx.use_program(Some(&point_program)); + + // pass the point vertex positions + bind_vertex_attrib(&ctx, point_position_index, 3, point_positions.as_slice()); + + // draw the scene + ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32); + + // --- update the display state --- + // update the viewpoint assembly_to_world.set(asm_to_world); 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/point.frag b/app-proto/src/point.frag new file mode 100644 index 0000000..984d1c9 --- /dev/null +++ b/app-proto/src/point.frag @@ -0,0 +1,9 @@ +#version 300 es + +precision highp float; + +out vec4 outColor; + +void main() { + outColor = vec4(vec3(1.), 1.); +} \ 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..6b8c739 --- /dev/null +++ b/app-proto/src/point.vert @@ -0,0 +1,12 @@ +#version 300 es + +in vec4 position; + +// camera +const float focal_slope = 0.3; + +void main() { + float depth = -focal_slope * position.z; + gl_Position = vec4(position.xy / depth, 0., 1.); + gl_PointSize = 5.; +} \ 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 -- 2.43.0 From 5a1d8bc2011c6e342c53bd54deb1d9de6c8c10cf Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 22 Apr 2025 12:03:04 -0700 Subject: [PATCH 04/16] Only recreate buffers when their contents change --- app-proto/src/display.rs | 97 ++++++++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 38 deletions(-) diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 67e36e9..2e9b4f0 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -8,6 +8,7 @@ use web_sys::{ KeyboardEvent, MouseEvent, WebGl2RenderingContext, + WebGlBuffer, WebGlProgram, WebGlShader, WebGlUniformLocation, @@ -81,22 +82,51 @@ fn get_uniform_array_locations( }) } -// load the given data into the vertex input of the given name -fn bind_vertex_attrib( +// find the vertex attribute called `attr_name` in the given program. enable it +// and return its index +fn find_and_enable_attribute( context: &WebGl2RenderingContext, - index: u32, - size: i32, - data: &[f32] + program: &WebGlProgram, + attr_name: &str +) -> u32 { + let attr_index = context.get_attrib_location(program, attr_name) as u32; + context.enable_vertex_attrib_array(attr_index); + attr_index +} + +// bind the given vertex buffer object to the given vertex attribute +fn bind_to_attribute( + context: &WebGl2RenderingContext, + 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, @@ -105,22 +135,7 @@ 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 } // the direction in camera space that a mouse event is pointing along @@ -260,9 +275,11 @@ pub fn Display() -> View { &JsValue::from("uniform vectors available") ); - // find indices of sphere vertex attributes and uniforms + // find and enable the sphere program's sole vertex attribute + let viewport_position_attr = find_and_enable_attribute(&ctx, &sphere_program, "position"); + + // find the sphere program's uniforms const SPHERE_MAX: usize = 200; - let viewport_position_index = ctx.get_attrib_location(&sphere_program, "position") as u32; let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt"); let sphere_sp_locs = get_uniform_array_locations::( &ctx, &sphere_program, "sphere_list", Some("sp") @@ -282,7 +299,7 @@ pub fn Display() -> View { let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold"); let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode"); - // set the viewport vertex positions + // load the viewport vertex positions into a new vertex buffer object const VERTEX_CNT: usize = 6; let viewport_positions: [f32; 3*VERTEX_CNT] = [ // northwest triangle @@ -294,9 +311,10 @@ pub fn Display() -> View { 1.0, 1.0, 0.0, 1.0, -1.0, 0.0 ]; + let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions); - // find indices of point vertex attributes and uniforms - let point_position_index = ctx.get_attrib_location(&point_program, "position") as u32; + // find and enable the point program's sole vertex attribute + let point_position_attr = find_and_enable_attribute(&ctx, &point_program, "position"); // set up a repainting routine let (_, start_animation_loop, _) = create_raf(move || { @@ -512,8 +530,9 @@ pub fn Display() -> View { ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); - // pass the viewport vertex positions - bind_vertex_attrib(&ctx, viewport_position_index, 3, &viewport_positions); + // bind the viewport vertex position buffer to the position + // attribute in the vertex shader + bind_to_attribute(&ctx, viewport_position_attr, 3, &viewport_position_buffer); // draw the scene ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); @@ -523,8 +542,10 @@ pub fn Display() -> View { // use the point rendering program ctx.use_program(Some(&point_program)); - // pass the point vertex positions - bind_vertex_attrib(&ctx, point_position_index, 3, point_positions.as_slice()); + // load the point positions into a new buffer and bind it to the + // position attribute in the vertex shader + let point_position_buffer = load_new_buffer(&ctx, point_positions.as_slice()); + bind_to_attribute(&ctx, point_position_attr, 3, &point_position_buffer); // draw the scene ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32); -- 2.43.0 From d5eaf11a173b4246f97d066c5f629c4803ee4945 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 22 Apr 2025 12:10:31 -0700 Subject: [PATCH 05/16] Name attribute sizes --- app-proto/src/display.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 2e9b4f0..59d0dca 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -408,6 +408,8 @@ pub fn Display() -> View { } if scene_changed.get() { + const SPACE_DIM: usize = 3; + /* INSTRUMENTS */ // measure mean frame interval frames_since_last_sample += 1; @@ -486,7 +488,6 @@ pub fn Display() -> View { point(0.0, -0.15, -1.0) ]; - const SPACE_DIM: usize = 3; let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM); let point_reps_world_sp = point_reps.map(|rep| &asm_to_world_sp * rep); DMatrix::from_columns(&point_reps_world_sp).cast::() @@ -532,7 +533,7 @@ pub fn Display() -> View { // bind the viewport vertex position buffer to the position // attribute in the vertex shader - bind_to_attribute(&ctx, viewport_position_attr, 3, &viewport_position_buffer); + 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); @@ -545,7 +546,7 @@ pub fn Display() -> View { // load the point positions into a new buffer and bind it to the // position attribute in the vertex shader let point_position_buffer = load_new_buffer(&ctx, point_positions.as_slice()); - bind_to_attribute(&ctx, point_position_attr, 3, &point_position_buffer); + bind_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, &point_position_buffer); // draw the scene ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32); -- 2.43.0 From cedb1d5b83c1c588c8987276554d5c9174375245 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 22 Apr 2025 15:55:08 -0700 Subject: [PATCH 06/16] Encapsulate scene data --- app-proto/src/display.rs | 154 ++++++++++++++++++++++++++------------- 1 file changed, 102 insertions(+), 52 deletions(-) diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 59d0dca..14b132a 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -4,7 +4,6 @@ use sycamore::{prelude::*, motion::create_raf}; use web_sys::{ console, window, - Element, KeyboardEvent, MouseEvent, WebGl2RenderingContext, @@ -15,7 +14,77 @@ use web_sys::{ wasm_bindgen::{JsCast, JsValue} }; -use crate::{AppState, assembly::{ElementKey, ElementMotion}}; +use crate::{AppState, assembly::{Element, ElementKey, ElementColor, ElementMotion}}; + +// --- 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> +} + +impl ScenePoints { + fn new() -> ScenePoints { + ScenePoints { + representations: Vec::new() + } + } +} + +struct Scene { + spheres: SceneSpheres, + points: ScenePoints +} + +impl Scene { + fn new() -> Scene { + Scene { + spheres: SceneSpheres::new(), + points: ScenePoints::new() + } + } +} + +trait DisplayItem { + fn show(&self, scene: &mut Scene, selected: bool); +} + +impl DisplayItem for Element { + 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); + } +} + +// --- WebGL utilities --- fn compile_shader( context: &WebGl2RenderingContext, @@ -140,7 +209,7 @@ fn load_new_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(); + 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(); @@ -157,6 +226,8 @@ fn event_dir(event: &MouseEvent) -> Vector3 { ) } +// --- display component --- + #[component] pub fn Display() -> View { let state = use_context::(); @@ -225,7 +296,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 */ @@ -421,6 +491,8 @@ pub fn Display() -> View { // --- get the assembly --- + let mut scene = Scene::new(); + // find the map from assembly space to world space let location = { let u = -location_z; @@ -435,50 +507,24 @@ pub fn Display() -> View { let asm_to_world = &location * &orientation; // get the spheres - let ( - sphere_cnt, - sphere_reps_world, - sphere_colors, - sphere_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::>() - ) - }); + 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(); + + // write the spheres in world coordinates + let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map( + |rep| &asm_to_world * rep + ).collect(); /* SCAFFOLDING */ // get the points - let point_positions = { + scene.points.representations.append({ use crate::engine::point; - - /* DEBUG */ - // hard-code the origin and the centers of the spheres in - // the general test assembly - let point_reps = [ + &mut vec![ point(0.0, 0.0, 0.0), point(0.5, 0.5, 0.0), point(-0.5, -0.5, 0.0), @@ -486,12 +532,16 @@ pub fn Display() -> View { point(0.5, -0.5, 0.0), point(0.0, 0.15, 1.0), point(0.0, -0.15, -1.0) - ]; - - let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM); - let point_reps_world_sp = point_reps.map(|rep| &asm_to_world_sp * rep); - DMatrix::from_columns(&point_reps_world_sp).cast::() - }; + ] + }); + + // 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::(); // --- draw the spheres --- @@ -518,11 +568,11 @@ pub fn Display() -> View { ); ctx.uniform3fv_with_f32_array( sphere_color_locs[n].as_ref(), - &sphere_colors[n] + &scene.spheres.colors[n] ); ctx.uniform1f( sphere_highlight_locs[n].as_ref(), - sphere_highlights[n] + scene.spheres.highlights[n] ); } -- 2.43.0 From 68abc2ad444a6e0131120916f6c1287f9ff63696 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 22 Apr 2025 15:58:24 -0700 Subject: [PATCH 07/16] Simplify sphere data passing --- app-proto/src/display.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 14b132a..6f7ac71 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -517,7 +517,7 @@ pub fn Display() -> View { // write the spheres in world coordinates let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map( - |rep| &asm_to_world * rep + |rep| (&asm_to_world * rep).cast::() ).collect(); /* SCAFFOLDING */ @@ -554,17 +554,17 @@ pub fn Display() -> View { ctx.uniform2f(resolution_loc.as_ref(), width, height); ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); - // pass the assembly data + // 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.uniform3f( + 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( sphere_color_locs[n].as_ref(), -- 2.43.0 From a1e23543cbde81bcd1afcbeea1dce3420181812f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 23 Apr 2025 01:01:59 -0700 Subject: [PATCH 08/16] Rename the `Element` structure to `Sphere` This makes way for an `Element` trait. Some `Sphere` variables, like the arguments of the sphere insertion methods, have been renamed to show that they refer specifically to spheres. Others, like the argument of `ElementOutlineItem`, have kept their general names, because I expect them to become `Element` trait objects. --- app-proto/src/add_remove.rs | 30 ++++++++++++------------ app-proto/src/assembly.rs | 46 ++++++++++++++++++------------------- app-proto/src/display.rs | 4 ++-- app-proto/src/outline.rs | 2 +- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 14fcd41..62b3ebe 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -4,7 +4,7 @@ use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ engine, AppState, - assembly::{Assembly, Element, InversiveDistanceRegulator} + assembly::{Assembly, InversiveDistanceRegulator, Sphere} }; /* DEBUG */ @@ -12,7 +12,7 @@ use crate::{ // built a more formal test assembly system fn load_gen_assemb(assembly: &Assembly) { let _ = assembly.try_insert_sphere( - Element::new( + Sphere::new( String::from("gemini_a"), String::from("Castor"), [1.00_f32, 0.25_f32, 0.00_f32], @@ -20,7 +20,7 @@ fn load_gen_assemb(assembly: &Assembly) { ) ); let _ = assembly.try_insert_sphere( - Element::new( + Sphere::new( String::from("gemini_b"), String::from("Pollux"), [0.00_f32, 0.25_f32, 1.00_f32], @@ -28,7 +28,7 @@ fn load_gen_assemb(assembly: &Assembly) { ) ); let _ = assembly.try_insert_sphere( - Element::new( + Sphere::new( String::from("ursa_major"), String::from("Ursa major"), [0.25_f32, 0.00_f32, 1.00_f32], @@ -36,7 +36,7 @@ fn load_gen_assemb(assembly: &Assembly) { ) ); let _ = assembly.try_insert_sphere( - Element::new( + Sphere::new( String::from("ursa_minor"), String::from("Ursa minor"), [0.25_f32, 1.00_f32, 0.00_f32], @@ -44,7 +44,7 @@ fn load_gen_assemb(assembly: &Assembly) { ) ); let _ = assembly.try_insert_sphere( - Element::new( + Sphere::new( String::from("moon_deimos"), String::from("Deimos"), [0.75_f32, 0.75_f32, 0.00_f32], @@ -52,7 +52,7 @@ fn load_gen_assemb(assembly: &Assembly) { ) ); let _ = assembly.try_insert_sphere( - Element::new( + Sphere::new( String::from("moon_phobos"), String::from("Phobos"), [0.00_f32, 0.75_f32, 0.50_f32], @@ -67,7 +67,7 @@ fn load_gen_assemb(assembly: &Assembly) { fn load_low_curv_assemb(assembly: &Assembly) { let a = 0.75_f64.sqrt(); let _ = assembly.try_insert_sphere( - Element::new( + Sphere::new( "central".to_string(), "Central".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], @@ -75,7 +75,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { ) ); let _ = assembly.try_insert_sphere( - Element::new( + Sphere::new( "assemb_plane".to_string(), "Assembly plane".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], @@ -83,7 +83,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { ) ); let _ = assembly.try_insert_sphere( - Element::new( + Sphere::new( "side1".to_string(), "Side 1".to_string(), [1.00_f32, 0.00_f32, 0.25_f32], @@ -91,7 +91,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { ) ); let _ = assembly.try_insert_sphere( - Element::new( + Sphere::new( "side2".to_string(), "Side 2".to_string(), [0.25_f32, 1.00_f32, 0.00_f32], @@ -99,7 +99,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { ) ); let _ = assembly.try_insert_sphere( - Element::new( + Sphere::new( "side3".to_string(), "Side 3".to_string(), [0.00_f32, 0.25_f32, 1.00_f32], @@ -107,7 +107,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { ) ); let _ = assembly.try_insert_sphere( - Element::new( + Sphere::new( "corner1".to_string(), "Corner 1".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], @@ -115,7 +115,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { ) ); let _ = assembly.try_insert_sphere( - Element::new( + Sphere::new( "corner2".to_string(), "Corner 2".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], @@ -123,7 +123,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { ) ); let _ = assembly.try_insert_sphere( - Element::new( + Sphere::new( String::from("corner3"), String::from("Corner 3"), [0.75_f32, 0.75_f32, 0.75_f32], diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 5c926ca..41070fb 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -33,11 +33,11 @@ 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 struct Sphere { pub id: String, pub label: String, pub color: ElementColor, @@ -57,7 +57,7 @@ pub struct Element { column_index: Option } -impl Element { +impl Sphere { const CURVATURE_COMPONENT: usize = 3; pub fn new( @@ -65,7 +65,7 @@ impl Element { label: String, color: ElementColor, representation: DVector - ) -> Element { + ) -> Sphere { // 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 @@ -77,7 +77,7 @@ impl Element { |serial| serial.checked_add(1) ).expect("Out of serial numbers for elements"); - Element { + Sphere { id: id, label: label, color: color, @@ -132,10 +132,10 @@ impl Element { } } -impl ProblemPoser for Element { - fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab) { +impl ProblemPoser for Sphere { + 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() + 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()); @@ -198,7 +198,7 @@ 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( @@ -222,7 +222,7 @@ 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] + |rep| rep[Sphere::CURVATURE_COMPONENT] ) ); @@ -262,13 +262,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( "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 +286,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 @@ -320,10 +320,10 @@ impl Assembly { // 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_sphere_unchecked(&self, elt: Element) -> ElementKey { + fn insert_sphere_unchecked(&self, sphere: Sphere) -> ElementKey { // insert the sphere - let id = elt.id.clone(); - let key = self.elements.update(|elts| elts.insert(elt)); + let id = sphere.id.clone(); + let key = self.elements.update(|elts| elts.insert(sphere)); self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); // regulate the sphere's curvature @@ -332,12 +332,12 @@ impl Assembly { key } - pub fn try_insert_sphere(&self, elt: Element) -> Option { + pub fn try_insert_sphere(&self, sphere: Sphere) -> 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(&sphere.id) ); if can_insert { - Some(self.insert_sphere_unchecked(elt)) + Some(self.insert_sphere_unchecked(sphere)) } else { None } @@ -356,7 +356,7 @@ impl Assembly { // create and insert a sphere let _ = self.insert_sphere_unchecked( - Element::new( + Sphere::new( id, format!("Sphere {}", id_num), [0.75_f32, 0.75_f32, 0.75_f32], @@ -607,10 +607,10 @@ mod tests { 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::new( "sphere".to_string(), "Sphere".to_string(), [1.0_f32, 1.0_f32, 1.0_f32], @@ -626,7 +626,7 @@ mod tests { let mut elts = Slab::new(); let subjects = [0, 1].map(|k| { elts.insert( - Element::new( + Sphere::new( format!("sphere{k}"), format!("Sphere {k}"), [1.0_f32, 1.0_f32, 1.0_f32], diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 6f7ac71..b6fb12e 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -14,7 +14,7 @@ use web_sys::{ wasm_bindgen::{JsCast, JsValue} }; -use crate::{AppState, assembly::{Element, ElementKey, ElementColor, ElementMotion}}; +use crate::{AppState, assembly::{ElementKey, ElementColor, ElementMotion, Sphere}}; // --- scene data --- @@ -74,7 +74,7 @@ trait DisplayItem { fn show(&self, scene: &mut Scene, selected: bool); } -impl DisplayItem for Element { +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(); diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 2446337..e003a7c 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -141,7 +141,7 @@ 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: assembly::Sphere) -> View { let state = use_context::(); let class = state.selection.map( move |sel| if sel.contains(&key) { "selected" } else { "" } -- 2.43.0 From f9df459a0de5c24577a3e756e6f239b9179314ef Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 23 Apr 2025 22:25:55 -0700 Subject: [PATCH 09/16] 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() ) } } -- 2.43.0 From 873de78f2d7ebbaf4bc5da41f3e7d0f4ae95386e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 24 Apr 2025 11:14:05 -0700 Subject: [PATCH 10/16] Generalize the element insertion methods --- app-proto/src/add_remove.rs | 33 +++++----- app-proto/src/assembly.rs | 128 +++++++++++++++++++++--------------- 2 files changed, 91 insertions(+), 70 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 62b3ebe..b7d6c40 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -1,3 +1,4 @@ +use std::rc::Rc; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; @@ -11,7 +12,7 @@ use crate::{ // 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( + let _ = assembly.try_insert_element( Sphere::new( String::from("gemini_a"), String::from("Castor"), @@ -19,7 +20,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(0.5, 0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_sphere( + let _ = assembly.try_insert_element( Sphere::new( String::from("gemini_b"), String::from("Pollux"), @@ -27,7 +28,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(-0.5, -0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_sphere( + let _ = assembly.try_insert_element( Sphere::new( String::from("ursa_major"), String::from("Ursa major"), @@ -35,7 +36,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(-0.5, 0.5, 0.0, 0.75) ) ); - let _ = assembly.try_insert_sphere( + let _ = assembly.try_insert_element( Sphere::new( String::from("ursa_minor"), String::from("Ursa minor"), @@ -43,7 +44,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(0.5, -0.5, 0.0, 0.5) ) ); - let _ = assembly.try_insert_sphere( + let _ = assembly.try_insert_element( Sphere::new( String::from("moon_deimos"), String::from("Deimos"), @@ -51,7 +52,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(0.0, 0.15, 1.0, 0.25) ) ); - let _ = assembly.try_insert_sphere( + let _ = assembly.try_insert_element( Sphere::new( String::from("moon_phobos"), String::from("Phobos"), @@ -66,7 +67,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_sphere( + let _ = assembly.try_insert_element( Sphere::new( "central".to_string(), "Central".to_string(), @@ -74,7 +75,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere(0.0, 0.0, 0.0, 1.0) ) ); - let _ = assembly.try_insert_sphere( + let _ = assembly.try_insert_element( Sphere::new( "assemb_plane".to_string(), "Assembly plane".to_string(), @@ -82,7 +83,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_sphere( + let _ = assembly.try_insert_element( Sphere::new( "side1".to_string(), "Side 1".to_string(), @@ -90,7 +91,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_sphere( + let _ = assembly.try_insert_element( Sphere::new( "side2".to_string(), "Side 2".to_string(), @@ -98,7 +99,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_sphere( + let _ = assembly.try_insert_element( Sphere::new( "side3".to_string(), "Side 3".to_string(), @@ -106,7 +107,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_sphere( + let _ = assembly.try_insert_element( Sphere::new( "corner1".to_string(), "Corner 1".to_string(), @@ -114,7 +115,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_sphere( + let _ = assembly.try_insert_element( Sphere::new( "corner2".to_string(), "Corner 2".to_string(), @@ -122,7 +123,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_sphere( + let _ = assembly.try_insert_element( Sphere::new( String::from("corner3"), String::from("Corner 3"), @@ -167,7 +168,7 @@ pub fn AddRemove() -> View { button( on:click=|_| { let state = use_context::(); - state.assembly.insert_new_sphere(); + state.assembly.insert_element_default::(); } ) { "+" } button( @@ -190,7 +191,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()); } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 84b21f7..000a619 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -43,6 +43,23 @@ pub trait ProblemPoser { } 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>; @@ -54,6 +71,19 @@ pub trait Element: ProblemPoser + DisplayItem { // 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 @@ -98,30 +128,36 @@ impl Sphere { color: ElementColor, representation: DVector ) -> Sphere { - // 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"); - Sphere { id: id, label: label, color: color, representation: create_signal(representation), regulators: create_signal(BTreeSet::default()), - serial: serial, + serial: Self::next_serial(), column_index: None.into() } } } 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 } @@ -336,63 +372,58 @@ 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, sphere: Sphere) -> ElementKey { - // insert the sphere - let id = sphere.id.clone(); - let key = self.elements.update(|elts| elts.insert(Rc::new(sphere))); + 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, sphere: Sphere) -> 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(&sphere.id) + |elts_by_id| !elts_by_id.contains_key(elt.id()) ); if can_insert { - Some(self.insert_sphere_unchecked(sphere)) + 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( - 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) - ) - ); + // 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() @@ -409,10 +440,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(); } }); @@ -621,20 +652,14 @@ impl Assembly { #[cfg(test)] mod tests { - use crate::engine; - use super::*; #[test] #[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")] fn unindexed_element_test() { let _ = create_root(|| { - Sphere::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()); }); } @@ -645,12 +670,7 @@ mod tests { let mut elts = Slab::>::new(); let subjects = [0, 1].map(|k| { elts.insert( - 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) - )) + Rc::new(Sphere::default(format!("sphere{k}"), k)) ) }); elts[subjects[0]].set_column_index(0); -- 2.43.0 From 19450865861476fb579ba2b4006532d193b7c2e8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 24 Apr 2025 14:08:15 -0700 Subject: [PATCH 11/16] Add a point element Also add a new test assembly, "Pointed," to try out the new element. --- app-proto/src/add_remove.rs | 46 +++++++++++++++- app-proto/src/assembly.rs | 102 ++++++++++++++++++++++++++++++++---- app-proto/src/display.rs | 82 +++++++++++++++-------------- 3 files changed, 179 insertions(+), 51 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index b7d6c40..c9b73de 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -1,11 +1,11 @@ -use std::rc::Rc; +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, InversiveDistanceRegulator, Sphere} + assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere} }; /* DEBUG */ @@ -133,6 +133,46 @@ 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"), + engine::point(0.0, 0.0, FRAC_1_SQRT_2) + ) + ); + let _ = assembly.try_insert_element( + Point::new( + format!("point_back"), + format!("Back point"), + 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}"), + engine::point(x, y, 0.0) + ) + ); + } + } +} + #[component] pub fn AddRemove() -> View { /* DEBUG */ @@ -158,6 +198,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), _ => () }; }); @@ -199,6 +240,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 000a619..fae10f5 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -2,6 +2,7 @@ use nalgebra::{DMatrix, DVector, DVectorView}; use rustc_hash::FxHashMap; use slab::Slab; use std::{ + any::{Any, TypeId}, cell::Cell, collections::BTreeSet, rc::Rc, @@ -16,6 +17,7 @@ use crate::{ Q, change_half_curvature, local_unif_to_std, + point, realize_gram, sphere, ConfigSubspace, @@ -197,6 +199,87 @@ impl ProblemPoser for Sphere { } } +pub struct Point { + pub id: String, + pub label: String, + 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, + representation: DVector + ) -> Point { + Point { + id, + label, + 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}"), + 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; @@ -617,8 +700,7 @@ 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() { @@ -626,13 +708,15 @@ impl Assembly { // 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( diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 46f0892..115f1df 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -14,7 +14,10 @@ use web_sys::{ wasm_bindgen::{JsCast, JsValue} }; -use crate::{AppState, assembly::{ElementKey, ElementColor, ElementMotion, Sphere}}; +use crate::{ + AppState, + assembly::{ElementKey, ElementColor, ElementMotion, Point, Sphere} +}; // --- scene data --- @@ -129,6 +132,18 @@ impl DisplayItem for Sphere { } } +impl DisplayItem for Point { + fn show(&self, scene: &mut Scene, _selected: bool) { + let representation = self.representation.get_clone_untracked(); + scene.points.representations.push(representation); + } + + /* SCAFFOLDING */ + fn cast(&self, _dir: Vector3, _assembly_to_world: &DMatrix) -> Option { + None + } +} + // --- WebGL utilities --- fn compile_shader( @@ -551,7 +566,7 @@ pub fn Display() -> View { }; let asm_to_world = &location * &orientation; - // get the spheres + // set up the scene state.assembly.elements.with_untracked( |elts| for (key, elt) in elts { let selected = state.selection.with(|sel| sel.contains(&key)); @@ -560,39 +575,16 @@ pub fn Display() -> View { ); let sphere_cnt = scene.spheres.len_i32(); - // write the spheres in world coordinates - let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map( - |rep| (&asm_to_world * rep).cast::() - ).collect(); - - /* SCAFFOLDING */ - // get the points - scene.points.representations.append({ - use crate::engine::point; - &mut vec![ - point(0.0, 0.0, 0.0), - point(0.5, 0.5, 0.0), - point(-0.5, -0.5, 0.0), - point(-0.5, 0.5, 0.0), - point(0.5, -0.5, 0.0), - point(0.0, 0.15, 1.0), - point(0.0, -0.15, -1.0) - ] - }); - - // 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::(); - // --- draw the spheres --- // use the sphere rendering program ctx.use_program(Some(&sphere_program)); + // 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; let height = canvas.height() as f32; @@ -635,16 +627,26 @@ pub fn Display() -> View { // --- draw the points --- - // use the point rendering program - ctx.use_program(Some(&point_program)); - - // load the point positions into a new buffer and bind it to the - // position attribute in the vertex shader - let point_position_buffer = load_new_buffer(&ctx, point_positions.as_slice()); - bind_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, &point_position_buffer); - - // draw the scene - ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32); + if !scene.points.representations.is_empty() { + // use the point rendering program + ctx.use_program(Some(&point_program)); + + // 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 into a new buffer and bind it to the + // position attribute in the vertex shader + let point_position_buffer = load_new_buffer(&ctx, point_positions.as_slice()); + bind_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, &point_position_buffer); + + // draw the scene + ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32); + } // --- update the display state --- -- 2.43.0 From 0fbb0715067232826e7391f1a3cb999e3fdf9147 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 25 Apr 2025 12:06:21 -0700 Subject: [PATCH 12/16] Color points In the process, find and correct a misunderstanding about vertex attributes. Here's my current understanding. Vertex attributes are program-independent. When you make a draw call, every enabled attribute has to have a vertex buffer object bound to it. A simple way to make sure this happens is to enable only the attributes used by the active program. --- app-proto/src/add_remove.rs | 3 ++ app-proto/src/assembly.rs | 4 +++ app-proto/src/display.rs | 68 ++++++++++++++++++++++++------------- app-proto/src/point.frag | 4 ++- app-proto/src/point.vert | 5 +++ 5 files changed, 60 insertions(+), 24 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index c9b73de..ed05c16 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -138,6 +138,7 @@ fn load_pointed_assemb(assembly: &Assembly) { Point::new( format!("point_front"), format!("Front point"), + [0.875_f32, 0.875_f32, 0.875_f32], engine::point(0.0, 0.0, FRAC_1_SQRT_2) ) ); @@ -145,6 +146,7 @@ fn load_pointed_assemb(assembly: &Assembly) { Point::new( format!("point_back"), format!("Back point"), + [0.875_f32, 0.875_f32, 0.875_f32], engine::point(0.0, 0.0, -FRAC_1_SQRT_2) ) ); @@ -166,6 +168,7 @@ fn load_pointed_assemb(assembly: &Assembly) { Point::new( format!("point{index_x}{index_y}"), format!("Point {index_x}{index_y}"), + [0.4*(2.0 + x) as f32, 0.4*(2.0 + y) as f32, 0.4*(2.0 - x*y) as f32], engine::point(x, y, 0.0) ) ); diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index fae10f5..5cf1ba8 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -202,6 +202,7 @@ impl ProblemPoser for Sphere { pub struct Point { pub id: String, pub label: String, + pub color: ElementColor, pub representation: Signal>, pub regulators: Signal>, pub serial: u64, @@ -214,11 +215,13 @@ impl Point { 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(), @@ -236,6 +239,7 @@ impl Element for Point { Point::new( id, format!("Point {id_num}"), + [0.875_f32, 0.875_f32, 0.875_f32], point(0.0, 0.0, 0.0) ) } diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 115f1df..154d495 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -48,15 +48,22 @@ impl SceneSpheres { } struct ScenePoints { - representations: Vec> + representations: Vec>, + colors: Vec, } impl ScenePoints { fn new() -> ScenePoints { ScenePoints { - representations: Vec::new() + representations: Vec::new(), + colors: Vec::new() } } + + fn push(&mut self, representation: DVector, color: ElementColor) { + self.representations.push(representation); + self.colors.push(color); + } } pub struct Scene { @@ -135,7 +142,7 @@ impl DisplayItem for Sphere { impl DisplayItem for Point { fn show(&self, scene: &mut Scene, _selected: bool) { let representation = self.representation.get_clone_untracked(); - scene.points.representations.push(representation); + scene.points.push(representation, self.color); } /* SCAFFOLDING */ @@ -211,18 +218,6 @@ fn get_uniform_array_locations( }) } -// find the vertex attribute called `attr_name` in the given program. enable it -// and return its index -fn find_and_enable_attribute( - context: &WebGl2RenderingContext, - program: &WebGlProgram, - attr_name: &str -) -> u32 { - let attr_index = context.get_attrib_location(program, attr_name) as u32; - context.enable_vertex_attrib_array(attr_index); - attr_index -} - // bind the given vertex buffer object to the given vertex attribute fn bind_to_attribute( context: &WebGl2RenderingContext, @@ -267,6 +262,16 @@ fn load_new_buffer( 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: web_sys::Element = event.target().unwrap().unchecked_into(); @@ -405,8 +410,8 @@ pub fn Display() -> View { &JsValue::from("uniform vectors available") ); - // find and enable the sphere program's sole vertex attribute - let viewport_position_attr = find_and_enable_attribute(&ctx, &sphere_program, "position"); + // 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; @@ -443,8 +448,9 @@ pub fn Display() -> View { ]; let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions); - // find and enable the point program's sole vertex attribute - let point_position_attr = find_and_enable_attribute(&ctx, &point_program, "position"); + // 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; // set up a repainting routine let (_, start_animation_loop, _) = create_raf(move || { @@ -539,6 +545,7 @@ pub fn Display() -> View { if scene_changed.get() { const SPACE_DIM: usize = 3; + const COLOR_SIZE: usize = 3; /* INSTRUMENTS */ // measure mean frame interval @@ -580,6 +587,9 @@ pub fn Display() -> View { // 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::() @@ -625,12 +635,19 @@ pub fn Display() -> View { // 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); + // write the points in world coordinates let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM); let point_positions = DMatrix::from_columns( @@ -639,13 +656,18 @@ pub fn Display() -> View { ).collect::>().as_slice() ).cast::(); - // load the point positions into a new buffer and bind it to the - // position attribute in the vertex shader - let point_position_buffer = load_new_buffer(&ctx, point_positions.as_slice()); - bind_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, &point_position_buffer); + // 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()); // 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); } // --- update the display state --- diff --git a/app-proto/src/point.frag b/app-proto/src/point.frag index 984d1c9..2abba0e 100644 --- a/app-proto/src/point.frag +++ b/app-proto/src/point.frag @@ -2,8 +2,10 @@ precision highp float; +in vec3 point_color; + out vec4 outColor; void main() { - outColor = vec4(vec3(1.), 1.); + outColor = vec4(point_color, 1.); } \ No newline at end of file diff --git a/app-proto/src/point.vert b/app-proto/src/point.vert index 6b8c739..49d584b 100644 --- a/app-proto/src/point.vert +++ b/app-proto/src/point.vert @@ -1,6 +1,9 @@ #version 300 es in vec4 position; +in vec3 color; + +out vec3 point_color; // camera const float focal_slope = 0.3; @@ -9,4 +12,6 @@ void main() { float depth = -focal_slope * position.z; gl_Position = vec4(position.xy / depth, 0., 1.); gl_PointSize = 5.; + + point_color = color; } \ No newline at end of file -- 2.43.0 From bbd4ee08b60b31e18ebaa077bfc008406f26aba4 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 27 Apr 2025 22:23:41 -0700 Subject: [PATCH 13/16] Highlight selected points In the process, make points round, since the highlighting works better visually that way. --- app-proto/src/add_remove.rs | 6 +++--- app-proto/src/assembly.rs | 2 +- app-proto/src/display.rs | 29 +++++++++++++++++++++++++---- app-proto/src/point.frag | 9 ++++++++- app-proto/src/point.vert | 9 ++++++++- 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index ed05c16..146cfe0 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -138,7 +138,7 @@ fn load_pointed_assemb(assembly: &Assembly) { Point::new( format!("point_front"), format!("Front point"), - [0.875_f32, 0.875_f32, 0.875_f32], + [0.75_f32, 0.75_f32, 0.75_f32], engine::point(0.0, 0.0, FRAC_1_SQRT_2) ) ); @@ -146,7 +146,7 @@ fn load_pointed_assemb(assembly: &Assembly) { Point::new( format!("point_back"), format!("Back point"), - [0.875_f32, 0.875_f32, 0.875_f32], + [0.75_f32, 0.75_f32, 0.75_f32], engine::point(0.0, 0.0, -FRAC_1_SQRT_2) ) ); @@ -168,7 +168,7 @@ fn load_pointed_assemb(assembly: &Assembly) { Point::new( format!("point{index_x}{index_y}"), format!("Point {index_x}{index_y}"), - [0.4*(2.0 + x) as f32, 0.4*(2.0 + y) as f32, 0.4*(2.0 - x*y) as f32], + [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) ) ); diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 5cf1ba8..343cef8 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -239,7 +239,7 @@ impl Element for Point { Point::new( id, format!("Point {id_num}"), - [0.875_f32, 0.875_f32, 0.875_f32], + [0.75_f32, 0.75_f32, 0.75_f32], point(0.0, 0.0, 0.0) ) } diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 154d495..7256c60 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -50,19 +50,25 @@ impl SceneSpheres { struct ScenePoints { representations: Vec>, colors: Vec, + highlights: Vec, + selections: Vec } impl ScenePoints { fn new() -> ScenePoints { ScenePoints { representations: Vec::new(), - colors: Vec::new() + colors: Vec::new(), + highlights: Vec::new(), + selections: Vec::new() } } - fn push(&mut self, representation: DVector, color: ElementColor) { + 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 }); } } @@ -140,9 +146,12 @@ impl DisplayItem for Sphere { } impl DisplayItem for Point { - fn show(&self, scene: &mut Scene, _selected: bool) { + fn show(&self, scene: &mut Scene, selected: bool) { + const HIGHLIGHT: f32 = 0.5; /* SCAFFOLDING */ let representation = self.representation.get_clone_untracked(); - scene.points.push(representation, self.color); + 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 */ @@ -379,6 +388,10 @@ pub fn Display() -> View { // 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, @@ -451,6 +464,8 @@ pub fn Display() -> View { // 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 || { @@ -647,6 +662,8 @@ pub fn Display() -> View { // 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); @@ -661,6 +678,8 @@ pub fn Display() -> View { // 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); @@ -668,6 +687,8 @@ pub fn Display() -> View { // 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 --- diff --git a/app-proto/src/point.frag b/app-proto/src/point.frag index 2abba0e..3a361a8 100644 --- a/app-proto/src/point.frag +++ b/app-proto/src/point.frag @@ -3,9 +3,16 @@ precision highp float; in vec3 point_color; +in float point_highlight; +in float total_radius; out vec4 outColor; void main() { - outColor = vec4(point_color, 1.); + 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 index 49d584b..6945010 100644 --- a/app-proto/src/point.vert +++ b/app-proto/src/point.vert @@ -2,16 +2,23 @@ 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 = 5.; + gl_PointSize = 2.*total_radius; point_color = color; + point_highlight = highlight; } \ No newline at end of file -- 2.43.0 From b7375e7101c69c2a58428a8892abc3abb651f8c5 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 28 Apr 2025 00:30:38 -0700 Subject: [PATCH 14/16] Click the display to select points --- app-proto/src/display.rs | 47 ++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 7256c60..51b207d 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -92,7 +92,7 @@ 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) -> Option; + fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, pixel_size: f64) -> Option; } impl DisplayItem for Sphere { @@ -106,7 +106,7 @@ 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) -> 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; @@ -155,8 +155,30 @@ impl DisplayItem for Point { } /* SCAFFOLDING */ - fn cast(&self, _dir: Vector3, _assembly_to_world: &DMatrix) -> Option { - None + 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 + } } } @@ -282,7 +304,7 @@ fn bind_new_buffer_to_attribute( } // the direction in camera space that a mouse event is pointing along -fn event_dir(event: &MouseEvent) -> Vector3 { +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(); @@ -293,10 +315,13 @@ fn event_dir(event: &MouseEvent) -> Vector3 { // `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 ) } @@ -822,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 { -- 2.43.0 From 07a415843d3d88bdc9c7b16749f2db6e67ef53b2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 28 Apr 2025 11:38:45 -0700 Subject: [PATCH 15/16] AddRemove: Make a button that adds points --- app-proto/main.css | 6 ++++-- app-proto/src/add_remove.rs | 8 +++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index 4726a27..6aef0d2 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -42,9 +42,9 @@ body { } #add-remove > button { - width: 32px; + /*width: 32px;*/ height: 32px; - font-size: large; + /*font-size: large;*/ } /* KLUDGE */ @@ -53,7 +53,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 146cfe0..ea86186 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -214,7 +214,13 @@ pub fn AddRemove() -> View { 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={ -- 2.43.0 From 35689e32411bfca9bb6b8b1989e3fe28661a2b5f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 29 Apr 2025 10:08:57 -0700 Subject: [PATCH 16/16] Drop commented-out CSS declarations --- app-proto/main.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index 6aef0d2..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 */ -- 2.43.0