From fd3cbae1b4fd0f40500a7a868196fdbd868ec0ad Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 21 Aug 2024 13:01:33 -0700 Subject: [PATCH 001/126] Get a WebGL canvas working in Sycamore --- app-proto/inversive-display/.gitignore | 3 + app-proto/inversive-display/Cargo.toml | 37 ++++++ app-proto/inversive-display/index.html | 9 ++ app-proto/inversive-display/main.css | 23 ++++ app-proto/inversive-display/src/main.rs | 152 ++++++++++++++++++++++++ 5 files changed, 224 insertions(+) create mode 100644 app-proto/inversive-display/.gitignore create mode 100644 app-proto/inversive-display/Cargo.toml create mode 100644 app-proto/inversive-display/index.html create mode 100644 app-proto/inversive-display/main.css create mode 100644 app-proto/inversive-display/src/main.rs diff --git a/app-proto/inversive-display/.gitignore b/app-proto/inversive-display/.gitignore new file mode 100644 index 0000000..238273d --- /dev/null +++ b/app-proto/inversive-display/.gitignore @@ -0,0 +1,3 @@ +target +dist +Cargo.lock \ No newline at end of file diff --git a/app-proto/inversive-display/Cargo.toml b/app-proto/inversive-display/Cargo.toml new file mode 100644 index 0000000..42d4e17 --- /dev/null +++ b/app-proto/inversive-display/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "inversive-display" +version = "0.1.0" +authors = ["Aaron"] +edition = "2021" + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +js-sys = "0.3.70" +sycamore = "0.9.0-beta.2" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.7", optional = true } + +[dependencies.web-sys] +version = "0.3.69" +features = [ + 'HtmlCanvasElement', + 'WebGl2RenderingContext', + 'WebGlBuffer', + 'WebGlProgram', + 'WebGlShader', + 'WebGlVertexArrayObject', + 'Window' +] + +[dev-dependencies] +wasm-bindgen-test = "0.3.34" + +[profile.release] +opt-level = "s" # optimize for small code size +debug = true # include debug symbols diff --git a/app-proto/inversive-display/index.html b/app-proto/inversive-display/index.html new file mode 100644 index 0000000..9c45180 --- /dev/null +++ b/app-proto/inversive-display/index.html @@ -0,0 +1,9 @@ + + + + + Inversive display + + + + diff --git a/app-proto/inversive-display/main.css b/app-proto/inversive-display/main.css new file mode 100644 index 0000000..f79e62c --- /dev/null +++ b/app-proto/inversive-display/main.css @@ -0,0 +1,23 @@ +body { + margin-left: 20px; + margin-top: 20px; + color: #fcfcfc; + background-color: #202020; +} + +#app { + display: flex; + flex-direction: column; + width: 600px; +} + +canvas { + float: left; + background-color: #020202; + border-radius: 10px; + margin-top: 5px; +} + +input { + margin-top: 5px; +} \ No newline at end of file diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs new file mode 100644 index 0000000..6730a39 --- /dev/null +++ b/app-proto/inversive-display/src/main.rs @@ -0,0 +1,152 @@ +// based on the WebGL example in the `wasm-bindgen` guide +// +// https://rustwasm.github.io/wasm-bindgen/examples/webgl.html +// + +extern crate js_sys; +use std::f64::consts::PI as PI; +use sycamore::{prelude::*, rt::{JsCast, JsValue}}; +use web_sys::{console, WebGl2RenderingContext, WebGlProgram, WebGlShader}; + +fn compile_shader( + context: &WebGl2RenderingContext, + shader_type: u32, + source: &str, +) -> WebGlShader { + let shader = context.create_shader(shader_type).unwrap(); + context.shader_source(&shader, source); + context.compile_shader(&shader); + shader +} + +fn main() { + // set up a config option that forwards panic messages to `console.error` + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); + + sycamore::render(|| { + let tip = create_signal(0.0); + let display = create_node_ref(); + + on_mount(move || { + // get the display canvas + let canvas = display + .get::() + .unchecked_into::(); + let ctx = canvas + .get_context("webgl2") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap(); + + // load the vertex and fragment shaders + let vertex_shader = compile_shader( + &ctx, + WebGl2RenderingContext::VERTEX_SHADER, + r##"#version 300 es + + in vec4 position; + + void main() { + gl_Position = position; + } + "##, + ); + let fragment_shader = compile_shader( + &ctx, + WebGl2RenderingContext::FRAGMENT_SHADER, + r##"#version 300 es + + precision highp float; + out vec4 outColor; + + void main() { + outColor = vec4(gl_FragCoord.xy / 600., 1., 1.); + } + "##, + ); + 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)); + + // set up a repainting routine + create_effect(move || { + // list the vertices + let vertices: [f32; 9] = [ + -0.9, 0.9, 0.0, + -0.9, -0.9, 0.0, + 0.9*(tip.get() as f32), 0.0, 0.0 + ]; + + // create a vertex buffer and bind it to ARRAY_BUFFER + let position_attribute_location = ctx.get_attrib_location(&program, "position"); + let buffer = ctx.create_buffer().unwrap(); + ctx.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer)); + + // load the vertex list into the vertex 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 + unsafe { + ctx.buffer_data_with_array_buffer_view( + WebGl2RenderingContext::ARRAY_BUFFER, + &js_sys::Float32Array::view(&vertices), + WebGl2RenderingContext::STATIC_DRAW, + ); + } + + // create a vertex array and bind it to the graphics context + let vtx_array_obj = ctx.create_vertex_array().unwrap(); + ctx.bind_vertex_array(Some(&vtx_array_obj)); + + // use whatever is bound to ARRAY_BUFFER---here, the vertex + // buffer created above---as the vertex attribute array + // + // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer + // + ctx.vertex_attrib_pointer_with_i32( + position_attribute_location as u32, + 3, // dimension + WebGl2RenderingContext::FLOAT, // type + false, // normalized? + 0, // stride + 0, // offset + ); + ctx.enable_vertex_attrib_array(position_attribute_location as u32); + + // clear the screen and draw the scene + let vert_count = (vertices.len() / 3) as i32; + ctx.clear_color(0.0, 0.0, 0.0, 1.0); + ctx.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT); + ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, vert_count); + }); + }); + + view! { + div(id="app") { + canvas(ref=display, width="600", height="600") + input( + type="range", + max=1.0, + step=0.01, + bind:valueAsNumber=tip + ) + } + } + }); +} \ No newline at end of file From 5885189b04ca63750ce113f2f77496734d36ff8d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 21 Aug 2024 17:31:17 -0700 Subject: [PATCH 002/126] Draw a mesh in perspective, and in color --- app-proto/inversive-display/src/main.rs | 161 ++++++++++++++++-------- 1 file changed, 112 insertions(+), 49 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 6730a39..6768864 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -2,9 +2,13 @@ // // https://rustwasm.github.io/wasm-bindgen/examples/webgl.html // +// and this StackOverflow answer by wangdq +// +// https://stackoverflow.com/a/39684775 +// extern crate js_sys; -use std::f64::consts::PI as PI; +/* use std::f64::consts::PI as PI; */ use sycamore::{prelude::*, rt::{JsCast, JsValue}}; use web_sys::{console, WebGl2RenderingContext, WebGlProgram, WebGlShader}; @@ -19,6 +23,52 @@ fn compile_shader( shader } +// load the given data into the vertex input of the given name +fn bind_vertex_attrib( + context: &WebGl2RenderingContext, + program: &WebGlProgram, + name: &str, + size: i32, + data: &[f32] +) { + // create a data buffer and bind it to ARRAY_BUFFER + let buffer = context.create_buffer().unwrap(); + context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer)); + + // 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 + unsafe { + context.buffer_data_with_array_buffer_view( + WebGl2RenderingContext::ARRAY_BUFFER, + &js_sys::Float32Array::view(&data), + WebGl2RenderingContext::STATIC_DRAW, + ); + } + + // find the target attribute in the program's attribute list + let attrib_index = context.get_attrib_location(&program, name); + + // allow the target attribute to be used + context.enable_vertex_attrib_array(attrib_index as u32); + + // 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( + attrib_index as u32, + size, + WebGl2RenderingContext::FLOAT, + false, // don't normalize + 0, // zero stride + 0, // zero offset + ); +} + fn main() { // set up a config option that forwards panic messages to `console.error` #[cfg(feature = "console_error_panic_hook")] @@ -40,16 +90,31 @@ fn main() { .dyn_into::() .unwrap(); - // load the vertex and fragment shaders + // compile and attach the vertex and fragment shaders let vertex_shader = compile_shader( &ctx, WebGl2RenderingContext::VERTEX_SHADER, r##"#version 300 es - in vec4 position; + in vec3 position; + in vec3 color; + + out vec4 vertexColor; + + const float focal_length = 3.0; + const float near_clip = 0.1; + const float far_clip = 20.0; + const float depth_inv = 1. / (far_clip - near_clip); void main() { - gl_Position = position; + const mat4 world_to_clip = mat4( + focal_length, 0.0, 0.0, 0.0, + 0.0, focal_length, 0.0, 0.0, + 0.0, 0.0, (near_clip + far_clip) * depth_inv, -1., + 0.0, 0.0, 2. * near_clip * far_clip * depth_inv, 0.0 + ); + gl_Position = world_to_clip * vec4(position, 1.); + vertexColor = vec4(color, 1.); } "##, ); @@ -59,10 +124,13 @@ fn main() { r##"#version 300 es precision highp float; + + in vec4 vertexColor; + out vec4 outColor; void main() { - outColor = vec4(gl_FragCoord.xy / 600., 1., 1.); + outColor = vertexColor; } "##, ); @@ -82,58 +150,53 @@ fn main() { console::log_1(&JsValue::from(link_msg)); ctx.use_program(Some(&program)); + // 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 up a repainting routine create_effect(move || { - // list the vertices - let vertices: [f32; 9] = [ - -0.9, 0.9, 0.0, - -0.9, -0.9, 0.0, - 0.9*(tip.get() as f32), 0.0, 0.0 + const VERTEX_CNT: usize = 9; + + // set the vertex positions + let tip_shift = 4.0/3.0 * tip.get() as f32; + let positions: [f32; 3*VERTEX_CNT] = [ + // triangle 1 + 1.0 - tip_shift, 1.0 - tip_shift, -5.0 - tip_shift, + 1.0, -1.0, -7.0, + -1.0, 1.0, -7.0, + // triangle 2 + 1.0 - tip_shift, 1.0 - tip_shift, -5.0 - tip_shift, + -1.0, 1.0, -7.0, + -1.0, -1.0, -7.0, + // triangle 3 + 1.0 - tip_shift, 1.0 - tip_shift, -5.0 - tip_shift, + -1.0, -1.0, -7.0, + 1.0, -1.0, -7.0 ]; + bind_vertex_attrib(&ctx, &program, "position", 3, &positions); - // create a vertex buffer and bind it to ARRAY_BUFFER - let position_attribute_location = ctx.get_attrib_location(&program, "position"); - let buffer = ctx.create_buffer().unwrap(); - ctx.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer)); - - // load the vertex list into the vertex 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 - unsafe { - ctx.buffer_data_with_array_buffer_view( - WebGl2RenderingContext::ARRAY_BUFFER, - &js_sys::Float32Array::view(&vertices), - WebGl2RenderingContext::STATIC_DRAW, - ); - } - - // create a vertex array and bind it to the graphics context - let vtx_array_obj = ctx.create_vertex_array().unwrap(); - ctx.bind_vertex_array(Some(&vtx_array_obj)); - - // use whatever is bound to ARRAY_BUFFER---here, the vertex - // buffer created above---as the vertex attribute array - // - // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer - // - ctx.vertex_attrib_pointer_with_i32( - position_attribute_location as u32, - 3, // dimension - WebGl2RenderingContext::FLOAT, // type - false, // normalized? - 0, // stride - 0, // offset - ); - ctx.enable_vertex_attrib_array(position_attribute_location as u32); + // set the vertex colors + let colors: [f32; 3*VERTEX_CNT] = [ + // triangle 1 + 1.0, 0.0, 0.5, + 1.0, 0.0, 0.5, + 1.0, 0.0, 0.5, + // triangle 2 + 0.0, 0.5, 1.0, + 0.0, 0.5, 1.0, + 0.0, 0.5, 1.0, + // triangle 3 + 0.5, 0.0, 1.0, + 0.5, 0.0, 1.0, + 0.5, 0.0, 1.0 + ]; + bind_vertex_attrib(&ctx, &program, "color", 3, &colors); // clear the screen and draw the scene - let vert_count = (vertices.len() / 3) as i32; ctx.clear_color(0.0, 0.0, 0.0, 1.0); ctx.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT); - ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, vert_count); + ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); }); }); From 81f9b8e04007aa2289d865b48d6bae03b19f287a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 21 Aug 2024 22:36:56 -0700 Subject: [PATCH 003/126] Find vertex attribute indices in advance --- app-proto/inversive-display/src/main.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 6768864..ed8200c 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -26,8 +26,7 @@ fn compile_shader( // load the given data into the vertex input of the given name fn bind_vertex_attrib( context: &WebGl2RenderingContext, - program: &WebGlProgram, - name: &str, + index: u32, size: i32, data: &[f32] ) { @@ -48,11 +47,8 @@ fn bind_vertex_attrib( ); } - // find the target attribute in the program's attribute list - let attrib_index = context.get_attrib_location(&program, name); - // allow the target attribute to be used - context.enable_vertex_attrib_array(attrib_index as u32); + 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 @@ -60,7 +56,7 @@ fn bind_vertex_attrib( // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer // context.vertex_attrib_pointer_with_i32( - attrib_index as u32, + index, size, WebGl2RenderingContext::FLOAT, false, // don't normalize @@ -150,6 +146,10 @@ fn main() { console::log_1(&JsValue::from(link_msg)); ctx.use_program(Some(&program)); + // find indices of vertex attributes + let position_index = ctx.get_attrib_location(&program, "position") as u32; + let color_index = ctx.get_attrib_location(&program, "color") as u32; + // 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)); @@ -174,7 +174,7 @@ fn main() { -1.0, -1.0, -7.0, 1.0, -1.0, -7.0 ]; - bind_vertex_attrib(&ctx, &program, "position", 3, &positions); + bind_vertex_attrib(&ctx, position_index, 3, &positions); // set the vertex colors let colors: [f32; 3*VERTEX_CNT] = [ @@ -191,7 +191,7 @@ fn main() { 0.5, 0.0, 1.0, 0.5, 0.0, 1.0 ]; - bind_vertex_attrib(&ctx, &program, "color", 3, &colors); + bind_vertex_attrib(&ctx, color_index, 3, &colors); // clear the screen and draw the scene ctx.clear_color(0.0, 0.0, 0.0, 1.0); From 80b210e667bcba411c64ac34dab92d286113cf06 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 21 Aug 2024 23:07:14 -0700 Subject: [PATCH 004/126] Make the projection map a uniform --- app-proto/inversive-display/Cargo.toml | 1 + app-proto/inversive-display/src/main.rs | 29 +++++++++++++++---------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/app-proto/inversive-display/Cargo.toml b/app-proto/inversive-display/Cargo.toml index 42d4e17..3d7eb3f 100644 --- a/app-proto/inversive-display/Cargo.toml +++ b/app-proto/inversive-display/Cargo.toml @@ -25,6 +25,7 @@ features = [ 'WebGlBuffer', 'WebGlProgram', 'WebGlShader', + 'WebGlUniformLocation', 'WebGlVertexArrayObject', 'Window' ] diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index ed8200c..b10f31a 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -10,7 +10,7 @@ extern crate js_sys; /* use std::f64::consts::PI as PI; */ use sycamore::{prelude::*, rt::{JsCast, JsValue}}; -use web_sys::{console, WebGl2RenderingContext, WebGlProgram, WebGlShader}; +use web_sys::{console, WebGl2RenderingContext, WebGlShader}; fn compile_shader( context: &WebGl2RenderingContext, @@ -97,18 +97,9 @@ fn main() { out vec4 vertexColor; - const float focal_length = 3.0; - const float near_clip = 0.1; - const float far_clip = 20.0; - const float depth_inv = 1. / (far_clip - near_clip); + uniform mat4 world_to_clip; void main() { - const mat4 world_to_clip = mat4( - focal_length, 0.0, 0.0, 0.0, - 0.0, focal_length, 0.0, 0.0, - 0.0, 0.0, (near_clip + far_clip) * depth_inv, -1., - 0.0, 0.0, 2. * near_clip * far_clip * depth_inv, 0.0 - ); gl_Position = world_to_clip * vec4(position, 1.); vertexColor = vec4(color, 1.); } @@ -146,14 +137,28 @@ fn main() { console::log_1(&JsValue::from(link_msg)); ctx.use_program(Some(&program)); - // find indices of vertex attributes + // find indices of vertex attributes and uniforms let position_index = ctx.get_attrib_location(&program, "position") as u32; let color_index = ctx.get_attrib_location(&program, "color") as u32; + let world_to_clip_loc = ctx.get_uniform_location(&program, "world_to_clip"); // 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 projection map + let focal_length = 3.0_f32; + let near_clip = 0.1_f32; + let far_clip = 20_f32; + let depth_inv = 1_f32 / (far_clip - near_clip); + let world_to_clip: [f32; 16] = [ + focal_length, 0.0, 0.0, 0.0, + 0.0, focal_length, 0.0, 0.0, + 0.0, 0.0, (near_clip + far_clip) * depth_inv, -1., + 0.0, 0.0, 2. * near_clip * far_clip * depth_inv, 0.0 + ]; + ctx.uniform_matrix4fv_with_f32_array(world_to_clip_loc.as_ref(), false, &world_to_clip); + // set up a repainting routine create_effect(move || { const VERTEX_CNT: usize = 9; From 1fbeb23194cf1b90f6a26b1797af726750b81841 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 22 Aug 2024 00:04:58 -0700 Subject: [PATCH 005/126] Add rotation control In the process, find and correct an error in the --+ vertex, which was miswritten as ---. --- app-proto/inversive-display/src/main.rs | 43 ++++++++++++++++++------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index b10f31a..72846fc 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -8,7 +8,7 @@ // extern crate js_sys; -/* use std::f64::consts::PI as PI; */ +use std::f64::consts::PI as PI; use sycamore::{prelude::*, rt::{JsCast, JsValue}}; use web_sys::{console, WebGl2RenderingContext, WebGlShader}; @@ -71,6 +71,7 @@ fn main() { console_error_panic_hook::set_once(); sycamore::render(|| { + let turn = create_signal(0.0); let tip = create_signal(0.0); let display = create_node_ref(); @@ -98,9 +99,11 @@ fn main() { out vec4 vertexColor; uniform mat4 world_to_clip; + uniform mat3 rotation; void main() { - gl_Position = world_to_clip * vec4(position, 1.); + vec3 world_pos = rotation * position - vec3(0., 0., 6.); + gl_Position = world_to_clip * vec4(world_pos, 1.); vertexColor = vec4(color, 1.); } "##, @@ -141,6 +144,7 @@ fn main() { let position_index = ctx.get_attrib_location(&program, "position") as u32; let color_index = ctx.get_attrib_location(&program, "color") as u32; let world_to_clip_loc = ctx.get_uniform_location(&program, "world_to_clip"); + let rotation_loc = ctx.get_uniform_location(&program, "rotation"); // create a vertex array and bind it to the graphics context let vertex_array = ctx.create_vertex_array().unwrap(); @@ -167,17 +171,17 @@ fn main() { let tip_shift = 4.0/3.0 * tip.get() as f32; let positions: [f32; 3*VERTEX_CNT] = [ // triangle 1 - 1.0 - tip_shift, 1.0 - tip_shift, -5.0 - tip_shift, - 1.0, -1.0, -7.0, - -1.0, 1.0, -7.0, + 1.0 - tip_shift, 1.0 - tip_shift, 1.0 - tip_shift, + 1.0, -1.0, -1.0, + -1.0, 1.0, -1.0, // triangle 2 - 1.0 - tip_shift, 1.0 - tip_shift, -5.0 - tip_shift, - -1.0, 1.0, -7.0, - -1.0, -1.0, -7.0, + 1.0 - tip_shift, 1.0 - tip_shift, 1.0 - tip_shift, + -1.0, 1.0, -1.0, + -1.0, -1.0, 1.0, // triangle 3 - 1.0 - tip_shift, 1.0 - tip_shift, -5.0 - tip_shift, - -1.0, -1.0, -7.0, - 1.0, -1.0, -7.0 + 1.0 - tip_shift, 1.0 - tip_shift, 1.0 - tip_shift, + -1.0, -1.0, 1.0, + 1.0, -1.0, -1.0 ]; bind_vertex_attrib(&ctx, position_index, 3, &positions); @@ -198,6 +202,17 @@ fn main() { ]; bind_vertex_attrib(&ctx, color_index, 3, &colors); + // set the rotation + let angle_val = (2.0*PI*turn.get()) as f32; + let angle_cos = angle_val.cos(); + let angle_sin = angle_val.sin(); + let rotation: [f32; 9] = [ + angle_cos, 0.0, angle_sin, + 0.0, 1.0, 0.0, + -angle_sin, 0.0, angle_cos, + ]; + ctx.uniform_matrix3fv_with_f32_array(rotation_loc.as_ref(), false, &rotation); + // clear the screen and draw the scene ctx.clear_color(0.0, 0.0, 0.0, 1.0); ctx.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT); @@ -208,6 +223,12 @@ fn main() { view! { div(id="app") { canvas(ref=display, width="600", height="600") + input( + type="range", + max=1.0, + step=0.01, + bind:valueAsNumber=turn + ) input( type="range", max=1.0, From f274119da64d9d8b7aac1d3784f6249cbbacbc1c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 22 Aug 2024 18:17:01 -0700 Subject: [PATCH 006/126] Enable depth testing To get the right order, flip the sign of the `z` component in the output of the projection map. --- app-proto/inversive-display/src/main.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 72846fc..5afbb04 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -150,6 +150,9 @@ fn main() { let vertex_array = ctx.create_vertex_array().unwrap(); ctx.bind_vertex_array(Some(&vertex_array)); + // enable depth testing + ctx.enable(WebGl2RenderingContext::DEPTH_TEST); + // set the projection map let focal_length = 3.0_f32; let near_clip = 0.1_f32; @@ -158,8 +161,8 @@ fn main() { let world_to_clip: [f32; 16] = [ focal_length, 0.0, 0.0, 0.0, 0.0, focal_length, 0.0, 0.0, - 0.0, 0.0, (near_clip + far_clip) * depth_inv, -1., - 0.0, 0.0, 2. * near_clip * far_clip * depth_inv, 0.0 + 0.0, 0.0, -(near_clip + far_clip) * depth_inv, -1., + 0.0, 0.0, -2. * near_clip * far_clip * depth_inv, 0.0 ]; ctx.uniform_matrix4fv_with_f32_array(world_to_clip_loc.as_ref(), false, &world_to_clip); From c78a041dc7bc1525fb9686ae36192082bfa5412b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 22 Aug 2024 22:08:34 -0700 Subject: [PATCH 007/126] Write a ray-caster for inversive spheres --- app-proto/inversive-display/src/main.rs | 172 +++++++++++++----------- 1 file changed, 90 insertions(+), 82 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 5afbb04..12fe9ed 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -8,7 +8,6 @@ // extern crate js_sys; -use std::f64::consts::PI as PI; use sycamore::{prelude::*, rt::{JsCast, JsValue}}; use web_sys::{console, WebGl2RenderingContext, WebGlShader}; @@ -71,8 +70,8 @@ fn main() { console_error_panic_hook::set_once(); sycamore::render(|| { - let turn = create_signal(0.0); - let tip = create_signal(0.0); + let ctrl_x = create_signal(0.0); + let ctrl_y = create_signal(0.0); let display = create_node_ref(); on_mount(move || { @@ -93,18 +92,10 @@ fn main() { WebGl2RenderingContext::VERTEX_SHADER, r##"#version 300 es - in vec3 position; - in vec3 color; - - out vec4 vertexColor; - - uniform mat4 world_to_clip; - uniform mat3 rotation; + in vec4 position; void main() { - vec3 world_pos = rotation * position - vec3(0., 0., 6.); - gl_Position = world_to_clip * vec4(world_pos, 1.); - vertexColor = vec4(color, 1.); + gl_Position = position; } "##, ); @@ -115,12 +106,68 @@ fn main() { precision highp float; - in vec4 vertexColor; - out vec4 outColor; + uniform vec2 resolution; + uniform float shortdim; + + uniform vec2 ctrl; + + struct vecInv { + vec3 sp; + vec2 lt; + }; + + vecInv sphere(vec3 center, float radius) { + return vecInv( + center / radius, + vec2( + 0.5 / radius, + 0.5 * (dot(center, center) / radius - radius) + ) + ); + } + + const float focal_slope = 0.3; + const vec3 light_dir = normalize(vec3(2., 2., 1.)); + void main() { - outColor = vertexColor; + vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; + vec3 dir = vec3(focal_slope * scr, -1.); + + vecInv v = sphere(vec3(ctrl, -5.), 1.); + + float a = -v.lt.s * dot(dir, dir); + float b = dot(v.sp, dir); + float c = -v.lt.t; + + float scale = -b/(2.*a); + float adjust = 4.*a*c/(b*b); + float offset = sqrt(1. - adjust); + float u_front = scale * (1. - offset); + float u_back = scale * (1. + offset); + + vec3 color; + if (adjust < 1. && u_front > 0.) { + // the expression for normal needs to be checked. it's + // supposed to give the negative gradient of the lorentz + // product between the impact point vector and the sphere + // vector with respect to the coordinates of the impact + // point. i calculated it in my head and decided that + // the result looked good enough for now + vec3 pt_front = u_front * dir; + vec3 normal_front = normalize(-v.sp + 2.*v.lt.s*pt_front); + + float incidence = dot(normal_front, light_dir); + if (incidence < 0.) { + color = mix(vec3(0.2, 0.0, 0.4), vec3(0.1, 0.0, 0.2), -incidence); + } else { + color = mix(vec3(0.4, 0.0, 0.2), vec3(1., 0.8, 1.), incidence); + } + } else { + color = vec3(0.); + } + outColor = vec4(color, 1.); } "##, ); @@ -142,79 +189,38 @@ fn main() { // find indices of vertex attributes and uniforms let position_index = ctx.get_attrib_location(&program, "position") as u32; - let color_index = ctx.get_attrib_location(&program, "color") as u32; - let world_to_clip_loc = ctx.get_uniform_location(&program, "world_to_clip"); - let rotation_loc = ctx.get_uniform_location(&program, "rotation"); + let resolution_loc = ctx.get_uniform_location(&program, "resolution"); + let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); + let ctrl_loc = ctx.get_uniform_location(&program, "ctrl"); // 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)); - // enable depth testing - ctx.enable(WebGl2RenderingContext::DEPTH_TEST); - - // set the projection map - let focal_length = 3.0_f32; - let near_clip = 0.1_f32; - let far_clip = 20_f32; - let depth_inv = 1_f32 / (far_clip - near_clip); - let world_to_clip: [f32; 16] = [ - focal_length, 0.0, 0.0, 0.0, - 0.0, focal_length, 0.0, 0.0, - 0.0, 0.0, -(near_clip + far_clip) * depth_inv, -1., - 0.0, 0.0, -2. * near_clip * far_clip * depth_inv, 0.0 + // set the vertex positions + const VERTEX_CNT: usize = 6; + let positions: [f32; 3*VERTEX_CNT] = [ + // northwest triangle + -1.0, -1.0, 0.0, + -1.0, 1.0, 0.0, + 1.0, 1.0, 0.0, + // southeast triangle + -1.0, -1.0, 0.0, + 1.0, 1.0, 0.0, + 1.0, -1.0, 0.0 ]; - ctx.uniform_matrix4fv_with_f32_array(world_to_clip_loc.as_ref(), false, &world_to_clip); + bind_vertex_attrib(&ctx, position_index, 3, &positions); + + // 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)); // set up a repainting routine create_effect(move || { - const VERTEX_CNT: usize = 9; - - // set the vertex positions - let tip_shift = 4.0/3.0 * tip.get() as f32; - let positions: [f32; 3*VERTEX_CNT] = [ - // triangle 1 - 1.0 - tip_shift, 1.0 - tip_shift, 1.0 - tip_shift, - 1.0, -1.0, -1.0, - -1.0, 1.0, -1.0, - // triangle 2 - 1.0 - tip_shift, 1.0 - tip_shift, 1.0 - tip_shift, - -1.0, 1.0, -1.0, - -1.0, -1.0, 1.0, - // triangle 3 - 1.0 - tip_shift, 1.0 - tip_shift, 1.0 - tip_shift, - -1.0, -1.0, 1.0, - 1.0, -1.0, -1.0 - ]; - bind_vertex_attrib(&ctx, position_index, 3, &positions); - - // set the vertex colors - let colors: [f32; 3*VERTEX_CNT] = [ - // triangle 1 - 1.0, 0.0, 0.5, - 1.0, 0.0, 0.5, - 1.0, 0.0, 0.5, - // triangle 2 - 0.0, 0.5, 1.0, - 0.0, 0.5, 1.0, - 0.0, 0.5, 1.0, - // triangle 3 - 0.5, 0.0, 1.0, - 0.5, 0.0, 1.0, - 0.5, 0.0, 1.0 - ]; - bind_vertex_attrib(&ctx, color_index, 3, &colors); - - // set the rotation - let angle_val = (2.0*PI*turn.get()) as f32; - let angle_cos = angle_val.cos(); - let angle_sin = angle_val.sin(); - let rotation: [f32; 9] = [ - angle_cos, 0.0, angle_sin, - 0.0, 1.0, 0.0, - -angle_sin, 0.0, angle_cos, - ]; - ctx.uniform_matrix3fv_with_f32_array(rotation_loc.as_ref(), false, &rotation); + // pass the control parameters + ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); // clear the screen and draw the scene ctx.clear_color(0.0, 0.0, 0.0, 1.0); @@ -228,15 +234,17 @@ fn main() { canvas(ref=display, width="600", height="600") input( type="range", + min=-1.0, max=1.0, step=0.01, - bind:valueAsNumber=turn + bind:valueAsNumber=ctrl_x ) input( type="range", + min=-1.0, max=1.0, step=0.01, - bind:valueAsNumber=tip + bind:valueAsNumber=ctrl_y ) } } From d2cecf69db88145e6529847055fcf64d4fc72ea3 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 23 Aug 2024 00:16:41 -0700 Subject: [PATCH 008/126] Ray-cast a translucent sphere --- app-proto/inversive-display/src/main.rs | 81 ++++++++++++++++--------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 12fe9ed..baf7df6 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -72,6 +72,7 @@ fn main() { sycamore::render(|| { let ctrl_x = create_signal(0.0); let ctrl_y = create_signal(0.0); + let opacity = create_signal(0.6); let display = create_node_ref(); on_mount(move || { @@ -108,10 +109,17 @@ fn main() { out vec4 outColor; + // view uniform vec2 resolution; uniform float shortdim; + // controls uniform vec2 ctrl; + uniform float opacity; + + // light and camera + const float focal_slope = 0.3; + const vec3 light_dir = normalize(vec3(2., 2., 1.)); struct vecInv { vec3 sp; @@ -128,8 +136,24 @@ fn main() { ); } - const float focal_slope = 0.3; - const vec3 light_dir = normalize(vec3(2., 2., 1.)); + vec4 shade_sphere(vecInv v, vec3 pt) { + // the expression for normal needs to be checked. it's + // supposed to give the negative gradient of the lorentz + // product between the impact point vector and the sphere + // vector with respect to the coordinates of the impact + // point. i calculated it in my head and decided that + // the result looked good enough for now + vec3 normal_front = normalize(-v.sp + 2.*v.lt.s*pt); + + vec3 color; + float incidence = dot(normal_front, light_dir); + if (incidence < 0.) { + color = mix(vec3(0.2, 0.0, 0.4), vec3(0.1, 0.0, 0.2), -incidence); + } else { + color = mix(vec3(0.4, 0.0, 0.2), vec3(1.0, 0.8, 1.0), incidence); + } + return vec4(color, opacity); + } void main() { vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; @@ -143,29 +167,20 @@ fn main() { float scale = -b/(2.*a); float adjust = 4.*a*c/(b*b); - float offset = sqrt(1. - adjust); - float u_front = scale * (1. - offset); - float u_back = scale * (1. + offset); - vec3 color; - if (adjust < 1. && u_front > 0.) { - // the expression for normal needs to be checked. it's - // supposed to give the negative gradient of the lorentz - // product between the impact point vector and the sphere - // vector with respect to the coordinates of the impact - // point. i calculated it in my head and decided that - // the result looked good enough for now - vec3 pt_front = u_front * dir; - vec3 normal_front = normalize(-v.sp + 2.*v.lt.s*pt_front); - - float incidence = dot(normal_front, light_dir); - if (incidence < 0.) { - color = mix(vec3(0.2, 0.0, 0.4), vec3(0.1, 0.0, 0.2), -incidence); - } else { - color = mix(vec3(0.4, 0.0, 0.2), vec3(1., 0.8, 1.), incidence); + vec3 color = vec3(0.); + if (adjust < 1.) { + float offset = sqrt(1. - adjust); + float u_front = scale * (1. - offset); + float u_back = scale * (1. + offset); + if (u_back > 0.) { + vec4 sphere_color = shade_sphere(v, u_back * dir); + color = mix(color, sphere_color.rgb, sphere_color.a); + } + if (u_front > 0.) { + vec4 sphere_color = shade_sphere(v, u_front * dir); + color = mix(color, sphere_color.rgb, sphere_color.a); } - } else { - color = vec3(0.); } outColor = vec4(color, 1.); } @@ -192,6 +207,7 @@ fn main() { let resolution_loc = ctx.get_uniform_location(&program, "resolution"); let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); let ctrl_loc = ctx.get_uniform_location(&program, "ctrl"); + let opacity_loc = ctx.get_uniform_location(&program, "opacity"); // create a vertex array and bind it to the graphics context let vertex_array = ctx.create_vertex_array().unwrap(); @@ -211,16 +227,17 @@ fn main() { ]; bind_vertex_attrib(&ctx, position_index, 3, &positions); - // 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)); - // set up a repainting routine create_effect(move || { + // 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 control parameters ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); + ctx.uniform1f(opacity_loc.as_ref(), opacity.get() as f32); // clear the screen and draw the scene ctx.clear_color(0.0, 0.0, 0.0, 1.0); @@ -246,6 +263,12 @@ fn main() { step=0.01, bind:valueAsNumber=ctrl_y ) + input( + type="range", + max=1.0, + step=0.01, + bind:valueAsNumber=opacity + ) } } }); From 2ef0fdd3e2a6e077824c302d70fcf7e5f053bd97 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 23 Aug 2024 12:56:54 -0700 Subject: [PATCH 009/126] Ray-cast two spheres, with hard-coded depth sorting --- app-proto/inversive-display/src/main.rs | 115 ++++++++++++++++++------ 1 file changed, 89 insertions(+), 26 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index baf7df6..1937e2b 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -73,6 +73,7 @@ fn main() { let ctrl_x = create_signal(0.0); let ctrl_y = create_signal(0.0); let opacity = create_signal(0.6); + let layer_threshold = create_signal(0.0); let display = create_node_ref(); on_mount(move || { @@ -116,11 +117,14 @@ fn main() { // controls uniform vec2 ctrl; uniform float opacity; + uniform int layer_threshold; // light and camera const float focal_slope = 0.3; const vec3 light_dir = normalize(vec3(2., 2., 1.)); + // --- inversive geometry --- + struct vecInv { vec3 sp; vec2 lt; @@ -136,7 +140,26 @@ fn main() { ); } - vec4 shade_sphere(vecInv v, vec3 pt) { + // --- shading --- + + struct taggedFrag { + vec4 color; + float depth; + }; + + taggedFrag[2] sort(taggedFrag a, taggedFrag b) { + taggedFrag[2] result; + if (a.depth < b.depth) { + result[0] = a; + result[1] = b; + } else { + result[0] = b; + result[1] = a; + } + return result; + } + + taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color) { // the expression for normal needs to be checked. it's // supposed to give the negative gradient of the lorentz // product between the impact point vector and the sphere @@ -147,20 +170,13 @@ fn main() { vec3 color; float incidence = dot(normal_front, light_dir); - if (incidence < 0.) { - color = mix(vec3(0.2, 0.0, 0.4), vec3(0.1, 0.0, 0.2), -incidence); - } else { - color = mix(vec3(0.4, 0.0, 0.2), vec3(1.0, 0.8, 1.0), incidence); - } - return vec4(color, opacity); + float illum = mix(0.4, 1.0, max(incidence, 0.0)); + return taggedFrag(vec4(illum * base_color, opacity), -pt.z); } - void main() { - vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; - vec3 dir = vec3(focal_slope * scr, -1.); - - vecInv v = sphere(vec3(ctrl, -5.), 1.); - + // --- ray-casting --- + + vec2 sphere_cast(vecInv v, vec3 dir) { float a = -v.lt.s * dot(dir, dir); float b = dot(v.sp, dir); float c = -v.lt.t; @@ -168,18 +184,57 @@ fn main() { float scale = -b/(2.*a); float adjust = 4.*a*c/(b*b); - vec3 color = vec3(0.); if (adjust < 1.) { float offset = sqrt(1. - adjust); - float u_front = scale * (1. - offset); - float u_back = scale * (1. + offset); - if (u_back > 0.) { - vec4 sphere_color = shade_sphere(v, u_back * dir); - color = mix(color, sphere_color.rgb, sphere_color.a); - } - if (u_front > 0.) { - vec4 sphere_color = shade_sphere(v, u_front * dir); - color = mix(color, sphere_color.rgb, sphere_color.a); + return vec2( + scale * (1. - offset), + scale * (1. + offset) + ); + } else { + // these parameters describe points behind the camera, + // so the corresponding fragments won't be drawn + return vec2(-1., -1.); + } + } + + void main() { + vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; + vec3 dir = vec3(focal_slope * scr, -1.); + + // initialize two spheres + vecInv v0 = sphere(vec3(0.5, 0.5, -5. + ctrl.x), 1.); + vecInv v1 = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), 1.); + vec3 color0 = vec3(1.0, 0.5, 0.0); + vec3 color1 = vec3(0.0, 0.5, 1.0); + + // cast rays through the spheres + vec2 u0 = sphere_cast(v0, dir); + vec2 u1 = sphere_cast(v1, dir); + + // shade and depth-sort the impact points + taggedFrag front_hits[2] = sort( + sphere_shading(v0, u0[0] * dir, color0), + sphere_shading(v1, u1[0] * dir, color1) + ); + taggedFrag back_hits[2] = sort( + sphere_shading(v0, u0[1] * dir, color0), + sphere_shading(v1, u1[1] * dir, color1) + ); + taggedFrag middle_frags[2] = sort(front_hits[1], back_hits[0]); + + // finish depth sorting + taggedFrag frags_by_depth[4]; + frags_by_depth[0] = front_hits[0]; + frags_by_depth[1] = middle_frags[0]; + frags_by_depth[2] = middle_frags[1]; + frags_by_depth[3] = back_hits[1]; + + // composite the sphere fragments + vec3 color = vec3(0.); + for (int i = 3; i >= layer_threshold; --i) { + if (frags_by_depth[i].depth > 0.) { + vec4 frag_color = frags_by_depth[i].color; + color = mix(color, frag_color.rgb, frag_color.a); } } outColor = vec4(color, 1.); @@ -208,6 +263,7 @@ fn main() { let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); let ctrl_loc = ctx.get_uniform_location(&program, "ctrl"); let opacity_loc = ctx.get_uniform_location(&program, "opacity"); + let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); // create a vertex array and bind it to the graphics context let vertex_array = ctx.create_vertex_array().unwrap(); @@ -238,6 +294,7 @@ fn main() { // pass the control parameters ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); ctx.uniform1f(opacity_loc.as_ref(), opacity.get() as f32); + ctx.uniform1i(layer_threshold_loc.as_ref(), layer_threshold.get() as i32); // clear the screen and draw the scene ctx.clear_color(0.0, 0.0, 0.0, 1.0); @@ -253,22 +310,28 @@ fn main() { type="range", min=-1.0, max=1.0, - step=0.01, + step=0.001, bind:valueAsNumber=ctrl_x ) input( type="range", min=-1.0, max=1.0, - step=0.01, + step=0.001, bind:valueAsNumber=ctrl_y ) input( type="range", max=1.0, - step=0.01, + step=0.001, bind:valueAsNumber=opacity ) + input( + type="range", + max=3.0, + step=1.0, + bind:valueAsNumber=layer_threshold + ) } } }); From 87763fc458c5c25aa74c617a1fb2cac5c09a5f40 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 00:08:22 -0700 Subject: [PATCH 010/126] Ray-caster: tidy up sphere shading --- app-proto/inversive-display/src/main.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 1937e2b..d2bc742 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -166,10 +166,9 @@ fn main() { // vector with respect to the coordinates of the impact // point. i calculated it in my head and decided that // the result looked good enough for now - vec3 normal_front = normalize(-v.sp + 2.*v.lt.s*pt); + vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt); - vec3 color; - float incidence = dot(normal_front, light_dir); + float incidence = dot(normal, light_dir); float illum = mix(0.4, 1.0, max(incidence, 0.0)); return taggedFrag(vec4(illum * base_color, opacity), -pt.z); } From f1029b31028e1483914f0a8c9dda4759e4c51329 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 01:01:13 -0700 Subject: [PATCH 011/126] Ray-caster: map output into sRGB space Change the base color and default opacity to keep the picture looking broadly the same. --- app-proto/inversive-display/src/main.rs | 35 ++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index d2bc742..c19e8ab 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -72,7 +72,7 @@ fn main() { sycamore::render(|| { let ctrl_x = create_signal(0.0); let ctrl_y = create_signal(0.0); - let opacity = create_signal(0.6); + let opacity = create_signal(0.5); let layer_threshold = create_signal(0.0); let display = create_node_ref(); @@ -123,6 +123,33 @@ fn main() { const float focal_slope = 0.3; const vec3 light_dir = normalize(vec3(2., 2., 1.)); + // --- sRGB --- + + // map colors from RGB space to sRGB space, as specified in the + // sRGB standard (IEC 61966-2-1:1999) + // + // https://www.color.org/sRGB.pdf + // https://www.color.org/chardata/rgb/srgb.xalter + // + // in RGB space, color value is proportional to light intensity, + // so linear color-vector interpolation corresponds to physical + // light mixing. in sRGB space, the color encoding used by many + // monitors, we use more of the value interval to represent low + // intensities, and less of the interval to represent high + // intensities. this improves color quantization + + float sRGB(float t) { + if (t <= 0.0031308) { + return 12.92*t; + } else { + return 1.055*pow(t, 5./12.) - 0.055; + } + } + + vec3 sRGB(vec3 color) { + return vec3(sRGB(color.r), sRGB(color.g), sRGB(color.b)); + } + // --- inversive geometry --- struct vecInv { @@ -203,8 +230,8 @@ fn main() { // initialize two spheres vecInv v0 = sphere(vec3(0.5, 0.5, -5. + ctrl.x), 1.); vecInv v1 = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), 1.); - vec3 color0 = vec3(1.0, 0.5, 0.0); - vec3 color1 = vec3(0.0, 0.5, 1.0); + vec3 color0 = vec3(1., 0.214, 0.); + vec3 color1 = vec3(0., 0.214, 1.); // cast rays through the spheres vec2 u0 = sphere_cast(v0, dir); @@ -236,7 +263,7 @@ fn main() { color = mix(color, frag_color.rgb, frag_color.a); } } - outColor = vec4(color, 1.); + outColor = vec4(sRGB(color), 1.); } "##, ); From e3df765f16fe3ac349674b953ac9ef2cada772d9 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 01:38:06 -0700 Subject: [PATCH 012/126] Ray-caster: highlight intersections and cusps --- app-proto/inversive-display/src/main.rs | 71 ++++++++++++++++++++----- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index c19e8ab..e3e7135 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -72,6 +72,8 @@ fn main() { sycamore::render(|| { let ctrl_x = create_signal(0.0); let ctrl_y = create_signal(0.0); + let radius_x = create_signal(1.0); + let radius_y = create_signal(1.0); let opacity = create_signal(0.5); let layer_threshold = create_signal(0.0); let display = create_node_ref(); @@ -116,12 +118,14 @@ fn main() { // controls uniform vec2 ctrl; + uniform vec2 radius; uniform float opacity; uniform int layer_threshold; // light and camera const float focal_slope = 0.3; const vec3 light_dir = normalize(vec3(2., 2., 1.)); + const float ixn_threshold = 0.005; // --- sRGB --- @@ -170,13 +174,15 @@ fn main() { // --- shading --- struct taggedFrag { + int id; vec4 color; - float depth; + vec3 pt; + vec3 normal; }; taggedFrag[2] sort(taggedFrag a, taggedFrag b) { taggedFrag[2] result; - if (a.depth < b.depth) { + if (a.pt.z > b.pt.z) { result[0] = a; result[1] = b; } else { @@ -186,7 +192,7 @@ fn main() { return result; } - taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color) { + taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, int id) { // the expression for normal needs to be checked. it's // supposed to give the negative gradient of the lorentz // product between the impact point vector and the sphere @@ -197,7 +203,7 @@ fn main() { float incidence = dot(normal, light_dir); float illum = mix(0.4, 1.0, max(incidence, 0.0)); - return taggedFrag(vec4(illum * base_color, opacity), -pt.z); + return taggedFrag(id, vec4(illum * base_color, opacity), pt, normal); } // --- ray-casting --- @@ -228,23 +234,24 @@ fn main() { vec3 dir = vec3(focal_slope * scr, -1.); // initialize two spheres - vecInv v0 = sphere(vec3(0.5, 0.5, -5. + ctrl.x), 1.); - vecInv v1 = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), 1.); + vecInv v [2]; + v[0] = sphere(vec3(0.5, 0.5, -5. + ctrl.x), radius.x); + v[1] = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), radius.y); vec3 color0 = vec3(1., 0.214, 0.); vec3 color1 = vec3(0., 0.214, 1.); // cast rays through the spheres - vec2 u0 = sphere_cast(v0, dir); - vec2 u1 = sphere_cast(v1, dir); + vec2 u0 = sphere_cast(v[0], dir); + vec2 u1 = sphere_cast(v[1], dir); // shade and depth-sort the impact points taggedFrag front_hits[2] = sort( - sphere_shading(v0, u0[0] * dir, color0), - sphere_shading(v1, u1[0] * dir, color1) + sphere_shading(v[0], u0[0] * dir, color0, 0), + sphere_shading(v[1], u1[0] * dir, color1, 1) ); taggedFrag back_hits[2] = sort( - sphere_shading(v0, u0[1] * dir, color0), - sphere_shading(v1, u1[1] * dir, color1) + sphere_shading(v[0], u0[1] * dir, color0, 0), + sphere_shading(v[1], u1[1] * dir, color1, 1) ); taggedFrag middle_frags[2] = sort(front_hits[1], back_hits[0]); @@ -255,10 +262,32 @@ fn main() { frags_by_depth[2] = middle_frags[1]; frags_by_depth[3] = back_hits[1]; + // highlight intersections and cusps + for (int i = 3; i >= 1; --i) { + // intersections + taggedFrag frag0 = frags_by_depth[i]; + taggedFrag frag1 = frags_by_depth[i-1]; + float ixn_sin = length(cross(frag0.normal, frag1.normal)); + vec3 disp = frag0.pt - frag1.pt; + float ixn_dist = max( + abs(dot(frag1.normal, disp)), + abs(dot(frag0.normal, disp)) + ) / ixn_sin; + float ixn_highlight = 1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist); + frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), ixn_highlight); + frags_by_depth[i-1].color = mix(frags_by_depth[i-1].color, vec4(1.), ixn_highlight); + + // cusps + float cusp_cos = abs(dot(dir, frag0.normal)); + float cusp_threshold = 2.*sqrt(ixn_threshold * v[frag0.id].lt.s); + float cusp_highlight = 1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos); + frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), cusp_highlight); + } + // composite the sphere fragments vec3 color = vec3(0.); for (int i = 3; i >= layer_threshold; --i) { - if (frags_by_depth[i].depth > 0.) { + if (frags_by_depth[i].pt.z < 0.) { vec4 frag_color = frags_by_depth[i].color; color = mix(color, frag_color.rgb, frag_color.a); } @@ -288,6 +317,7 @@ fn main() { let resolution_loc = ctx.get_uniform_location(&program, "resolution"); let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); let ctrl_loc = ctx.get_uniform_location(&program, "ctrl"); + let radius_loc = ctx.get_uniform_location(&program, "radius"); let opacity_loc = ctx.get_uniform_location(&program, "opacity"); let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); @@ -319,6 +349,7 @@ fn main() { // pass the control parameters ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); + ctx.uniform2f(radius_loc.as_ref(), radius_x.get() as f32, radius_y.get() as f32); ctx.uniform1f(opacity_loc.as_ref(), opacity.get() as f32); ctx.uniform1i(layer_threshold_loc.as_ref(), layer_threshold.get() as i32); @@ -346,6 +377,20 @@ fn main() { step=0.001, bind:valueAsNumber=ctrl_y ) + input( + type="range", + min=0.5, + max=1.5, + step=0.001, + bind:valueAsNumber=radius_x + ) + input( + type="range", + min=0.5, + max=1.5, + step=0.001, + bind:valueAsNumber=radius_y + ) input( type="range", max=1.0, From 25da6ca06264b52cf5050d5b27664e081959f36b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 11:00:50 -0700 Subject: [PATCH 013/126] Ray-caster: adjust opacity of highlighting --- app-proto/inversive-display/src/main.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index e3e7135..b120f1a 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -75,6 +75,7 @@ fn main() { let radius_x = create_signal(1.0); let radius_y = create_signal(1.0); let opacity = create_signal(0.5); + let highlight = create_signal(0.2); let layer_threshold = create_signal(0.0); let display = create_node_ref(); @@ -120,6 +121,7 @@ fn main() { uniform vec2 ctrl; uniform vec2 radius; uniform float opacity; + uniform float highlight; uniform int layer_threshold; // light and camera @@ -273,14 +275,14 @@ fn main() { abs(dot(frag1.normal, disp)), abs(dot(frag0.normal, disp)) ) / ixn_sin; - float ixn_highlight = 1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist); + float ixn_highlight = 0.5 * highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), ixn_highlight); frags_by_depth[i-1].color = mix(frags_by_depth[i-1].color, vec4(1.), ixn_highlight); // cusps float cusp_cos = abs(dot(dir, frag0.normal)); float cusp_threshold = 2.*sqrt(ixn_threshold * v[frag0.id].lt.s); - float cusp_highlight = 1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos); + float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), cusp_highlight); } @@ -319,6 +321,7 @@ fn main() { let ctrl_loc = ctx.get_uniform_location(&program, "ctrl"); let radius_loc = ctx.get_uniform_location(&program, "radius"); let opacity_loc = ctx.get_uniform_location(&program, "opacity"); + let highlight_loc = ctx.get_uniform_location(&program, "highlight"); let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); // create a vertex array and bind it to the graphics context @@ -351,6 +354,7 @@ fn main() { ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); ctx.uniform2f(radius_loc.as_ref(), radius_x.get() as f32, radius_y.get() as f32); ctx.uniform1f(opacity_loc.as_ref(), opacity.get() as f32); + ctx.uniform1f(highlight_loc.as_ref(), highlight.get() as f32); ctx.uniform1i(layer_threshold_loc.as_ref(), layer_threshold.get() as i32); // clear the screen and draw the scene @@ -397,6 +401,12 @@ fn main() { step=0.001, bind:valueAsNumber=opacity ) + input( + type="range", + max=1.0, + step=0.001, + bind:valueAsNumber=highlight + ) input( type="range", max=3.0, From 766d56027c566265ed3b26126ddaa623d142184d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 11:26:53 -0700 Subject: [PATCH 014/126] Ray-caster: move shaders to separate files This properly reflects the modularity of the code, and it simplifies indentation and syntax highlighting. --- app-proto/inversive-display/src/identity.vert | 7 + .../inversive-display/src/inversive.frag | 187 ++++++++++++++++ app-proto/inversive-display/src/main.rs | 200 +----------------- 3 files changed, 196 insertions(+), 198 deletions(-) create mode 100644 app-proto/inversive-display/src/identity.vert create mode 100644 app-proto/inversive-display/src/inversive.frag diff --git a/app-proto/inversive-display/src/identity.vert b/app-proto/inversive-display/src/identity.vert new file mode 100644 index 0000000..183a65f --- /dev/null +++ b/app-proto/inversive-display/src/identity.vert @@ -0,0 +1,7 @@ +#version 300 es + +in vec4 position; + +void main() { + gl_Position = position; +} \ No newline at end of file diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag new file mode 100644 index 0000000..a4cbe8d --- /dev/null +++ b/app-proto/inversive-display/src/inversive.frag @@ -0,0 +1,187 @@ +#version 300 es + +precision highp float; + +out vec4 outColor; + +// view +uniform vec2 resolution; +uniform float shortdim; + +// controls +uniform vec2 ctrl; +uniform vec2 radius; +uniform float opacity; +uniform float highlight; +uniform int layer_threshold; + +// light and camera +const float focal_slope = 0.3; +const vec3 light_dir = normalize(vec3(2., 2., 1.)); +const float ixn_threshold = 0.005; + +// --- sRGB --- + +// map colors from RGB space to sRGB space, as specified in the sRGB standard +// (IEC 61966-2-1:1999) +// +// https://www.color.org/sRGB.pdf +// https://www.color.org/chardata/rgb/srgb.xalter +// +// in RGB space, color value is proportional to light intensity, so linear +// color-vector interpolation corresponds to physical light mixing. in sRGB +// space, the color encoding used by many monitors, we use more of the value +// interval to represent low intensities, and less of the interval to represent +// high intensities. this improves color quantization + +float sRGB(float t) { + if (t <= 0.0031308) { + return 12.92*t; + } else { + return 1.055*pow(t, 5./12.) - 0.055; + } +} + +vec3 sRGB(vec3 color) { + return vec3(sRGB(color.r), sRGB(color.g), sRGB(color.b)); +} + +// --- inversive geometry --- + +struct vecInv { + vec3 sp; + vec2 lt; +}; + +vecInv sphere(vec3 center, float radius) { + return vecInv( + center / radius, + vec2( + 0.5 / radius, + 0.5 * (dot(center, center) / radius - radius) + ) + ); +} + +// --- shading --- + +struct taggedFrag { + int id; + vec4 color; + vec3 pt; + vec3 normal; +}; + +taggedFrag[2] sort(taggedFrag a, taggedFrag b) { + taggedFrag[2] result; + if (a.pt.z > b.pt.z) { + result[0] = a; + result[1] = b; + } else { + result[0] = b; + result[1] = a; + } + return result; +} + +taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, int id) { + // the expression for normal needs to be checked. it's supposed to give the + // negative gradient of the lorentz product between the impact point vector + // and the sphere vector with respect to the coordinates of the impact + // point. i calculated it in my head and decided that the result looked good + // enough for now + vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt); + + float incidence = dot(normal, light_dir); + float illum = mix(0.4, 1.0, max(incidence, 0.0)); + return taggedFrag(id, vec4(illum * base_color, opacity), pt, normal); +} + +// --- ray-casting --- + +vec2 sphere_cast(vecInv v, vec3 dir) { + float a = -v.lt.s * dot(dir, dir); + float b = dot(v.sp, dir); + float c = -v.lt.t; + + float scale = -b/(2.*a); + float adjust = 4.*a*c/(b*b); + + if (adjust < 1.) { + float offset = sqrt(1. - adjust); + return vec2( + scale * (1. - offset), + scale * (1. + offset) + ); + } else { + // these parameters describe points behind the camera, so the + // corresponding fragments won't be drawn + return vec2(-1., -1.); + } +} + +void main() { + vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; + vec3 dir = vec3(focal_slope * scr, -1.); + + // initialize two spheres + vecInv v [2]; + v[0] = sphere(vec3(0.5, 0.5, -5. + ctrl.x), radius.x); + v[1] = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), radius.y); + vec3 color0 = vec3(1., 0.214, 0.); + vec3 color1 = vec3(0., 0.214, 1.); + + // cast rays through the spheres + vec2 u0 = sphere_cast(v[0], dir); + vec2 u1 = sphere_cast(v[1], dir); + + // shade and depth-sort the impact points + taggedFrag front_hits[2] = sort( + sphere_shading(v[0], u0[0] * dir, color0, 0), + sphere_shading(v[1], u1[0] * dir, color1, 1) + ); + taggedFrag back_hits[2] = sort( + sphere_shading(v[0], u0[1] * dir, color0, 0), + sphere_shading(v[1], u1[1] * dir, color1, 1) + ); + taggedFrag middle_frags[2] = sort(front_hits[1], back_hits[0]); + + // finish depth sorting + taggedFrag frags_by_depth[4]; + frags_by_depth[0] = front_hits[0]; + frags_by_depth[1] = middle_frags[0]; + frags_by_depth[2] = middle_frags[1]; + frags_by_depth[3] = back_hits[1]; + + // highlight intersections and cusps + for (int i = 3; i >= 1; --i) { + // intersections + taggedFrag frag0 = frags_by_depth[i]; + taggedFrag frag1 = frags_by_depth[i-1]; + float ixn_sin = length(cross(frag0.normal, frag1.normal)); + vec3 disp = frag0.pt - frag1.pt; + float ixn_dist = max( + abs(dot(frag1.normal, disp)), + abs(dot(frag0.normal, disp)) + ) / ixn_sin; + float ixn_highlight = 0.5 * highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); + frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), ixn_highlight); + frags_by_depth[i-1].color = mix(frags_by_depth[i-1].color, vec4(1.), ixn_highlight); + + // cusps + float cusp_cos = abs(dot(dir, frag0.normal)); + float cusp_threshold = 2.*sqrt(ixn_threshold * v[frag0.id].lt.s); + float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); + frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), cusp_highlight); + } + + // composite the sphere fragments + vec3 color = vec3(0.); + for (int i = 3; i >= layer_threshold; --i) { + if (frags_by_depth[i].pt.z < 0.) { + vec4 frag_color = frags_by_depth[i].color; + color = mix(color, frag_color.rgb, frag_color.a); + } + } + outColor = vec4(sRGB(color), 1.); +} \ No newline at end of file diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index b120f1a..4575e7a 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -95,208 +95,12 @@ fn main() { let vertex_shader = compile_shader( &ctx, WebGl2RenderingContext::VERTEX_SHADER, - r##"#version 300 es - - in vec4 position; - - void main() { - gl_Position = position; - } - "##, + include_str!("identity.vert"), ); let fragment_shader = compile_shader( &ctx, WebGl2RenderingContext::FRAGMENT_SHADER, - r##"#version 300 es - - precision highp float; - - out vec4 outColor; - - // view - uniform vec2 resolution; - uniform float shortdim; - - // controls - uniform vec2 ctrl; - uniform vec2 radius; - uniform float opacity; - uniform float highlight; - uniform int layer_threshold; - - // light and camera - const float focal_slope = 0.3; - const vec3 light_dir = normalize(vec3(2., 2., 1.)); - const float ixn_threshold = 0.005; - - // --- sRGB --- - - // map colors from RGB space to sRGB space, as specified in the - // sRGB standard (IEC 61966-2-1:1999) - // - // https://www.color.org/sRGB.pdf - // https://www.color.org/chardata/rgb/srgb.xalter - // - // in RGB space, color value is proportional to light intensity, - // so linear color-vector interpolation corresponds to physical - // light mixing. in sRGB space, the color encoding used by many - // monitors, we use more of the value interval to represent low - // intensities, and less of the interval to represent high - // intensities. this improves color quantization - - float sRGB(float t) { - if (t <= 0.0031308) { - return 12.92*t; - } else { - return 1.055*pow(t, 5./12.) - 0.055; - } - } - - vec3 sRGB(vec3 color) { - return vec3(sRGB(color.r), sRGB(color.g), sRGB(color.b)); - } - - // --- inversive geometry --- - - struct vecInv { - vec3 sp; - vec2 lt; - }; - - vecInv sphere(vec3 center, float radius) { - return vecInv( - center / radius, - vec2( - 0.5 / radius, - 0.5 * (dot(center, center) / radius - radius) - ) - ); - } - - // --- shading --- - - struct taggedFrag { - int id; - vec4 color; - vec3 pt; - vec3 normal; - }; - - taggedFrag[2] sort(taggedFrag a, taggedFrag b) { - taggedFrag[2] result; - if (a.pt.z > b.pt.z) { - result[0] = a; - result[1] = b; - } else { - result[0] = b; - result[1] = a; - } - return result; - } - - taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, int id) { - // the expression for normal needs to be checked. it's - // supposed to give the negative gradient of the lorentz - // product between the impact point vector and the sphere - // vector with respect to the coordinates of the impact - // point. i calculated it in my head and decided that - // the result looked good enough for now - vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt); - - float incidence = dot(normal, light_dir); - float illum = mix(0.4, 1.0, max(incidence, 0.0)); - return taggedFrag(id, vec4(illum * base_color, opacity), pt, normal); - } - - // --- ray-casting --- - - vec2 sphere_cast(vecInv v, vec3 dir) { - float a = -v.lt.s * dot(dir, dir); - float b = dot(v.sp, dir); - float c = -v.lt.t; - - float scale = -b/(2.*a); - float adjust = 4.*a*c/(b*b); - - if (adjust < 1.) { - float offset = sqrt(1. - adjust); - return vec2( - scale * (1. - offset), - scale * (1. + offset) - ); - } else { - // these parameters describe points behind the camera, - // so the corresponding fragments won't be drawn - return vec2(-1., -1.); - } - } - - void main() { - vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; - vec3 dir = vec3(focal_slope * scr, -1.); - - // initialize two spheres - vecInv v [2]; - v[0] = sphere(vec3(0.5, 0.5, -5. + ctrl.x), radius.x); - v[1] = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), radius.y); - vec3 color0 = vec3(1., 0.214, 0.); - vec3 color1 = vec3(0., 0.214, 1.); - - // cast rays through the spheres - vec2 u0 = sphere_cast(v[0], dir); - vec2 u1 = sphere_cast(v[1], dir); - - // shade and depth-sort the impact points - taggedFrag front_hits[2] = sort( - sphere_shading(v[0], u0[0] * dir, color0, 0), - sphere_shading(v[1], u1[0] * dir, color1, 1) - ); - taggedFrag back_hits[2] = sort( - sphere_shading(v[0], u0[1] * dir, color0, 0), - sphere_shading(v[1], u1[1] * dir, color1, 1) - ); - taggedFrag middle_frags[2] = sort(front_hits[1], back_hits[0]); - - // finish depth sorting - taggedFrag frags_by_depth[4]; - frags_by_depth[0] = front_hits[0]; - frags_by_depth[1] = middle_frags[0]; - frags_by_depth[2] = middle_frags[1]; - frags_by_depth[3] = back_hits[1]; - - // highlight intersections and cusps - for (int i = 3; i >= 1; --i) { - // intersections - taggedFrag frag0 = frags_by_depth[i]; - taggedFrag frag1 = frags_by_depth[i-1]; - float ixn_sin = length(cross(frag0.normal, frag1.normal)); - vec3 disp = frag0.pt - frag1.pt; - float ixn_dist = max( - abs(dot(frag1.normal, disp)), - abs(dot(frag0.normal, disp)) - ) / ixn_sin; - float ixn_highlight = 0.5 * highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); - frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), ixn_highlight); - frags_by_depth[i-1].color = mix(frags_by_depth[i-1].color, vec4(1.), ixn_highlight); - - // cusps - float cusp_cos = abs(dot(dir, frag0.normal)); - float cusp_threshold = 2.*sqrt(ixn_threshold * v[frag0.id].lt.s); - float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); - frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), cusp_highlight); - } - - // composite the sphere fragments - vec3 color = vec3(0.); - for (int i = 3; i >= layer_threshold; --i) { - if (frags_by_depth[i].pt.z < 0.) { - vec4 frag_color = frags_by_depth[i].color; - color = mix(color, frag_color.rgb, frag_color.a); - } - } - outColor = vec4(sRGB(color), 1.); - } - "##, + include_str!("inversive.frag"), ); let program = ctx.create_program().unwrap(); ctx.attach_shader(&program, &vertex_shader); From b9587872d30249dffa2c073857ce1663dd8ef979 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 11:31:22 -0700 Subject: [PATCH 015/126] Ray-caster: don't bother clearing the screen The ray-caster triangles cover the whole viewport, so they'll completely overdraw the previous frame. --- app-proto/inversive-display/src/main.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 4575e7a..186facf 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -161,9 +161,7 @@ fn main() { ctx.uniform1f(highlight_loc.as_ref(), highlight.get() as f32); ctx.uniform1i(layer_threshold_loc.as_ref(), layer_threshold.get() as i32); - // clear the screen and draw the scene - ctx.clear_color(0.0, 0.0, 0.0, 1.0); - ctx.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT); + // draw the scene ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); }); }); From 8798683d25c8e24dba2592e06efa5905a6d37783 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 25 Aug 2024 00:00:28 -0700 Subject: [PATCH 016/126] Ray-caster: store sphere data in arrays This is a first step toward general depth sorting. --- .../inversive-display/src/inversive.frag | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index a4cbe8d..12b4129 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -121,29 +121,31 @@ vec2 sphere_cast(vecInv v, vec3 dir) { } void main() { + const int sphere_cnt = 2; + vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; vec3 dir = vec3(focal_slope * scr, -1.); // initialize two spheres - vecInv v [2]; - v[0] = sphere(vec3(0.5, 0.5, -5. + ctrl.x), radius.x); - v[1] = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), radius.y); - vec3 color0 = vec3(1., 0.214, 0.); - vec3 color1 = vec3(0., 0.214, 1.); + vecInv sphere_list [sphere_cnt]; + sphere_list[0] = sphere(vec3(0.5, 0.5, -5. + ctrl.x), radius.x); + sphere_list[1] = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), radius.y); + vec3 color_list [sphere_cnt]; + color_list[0] = vec3(1., 0.214, 0.); + color_list[1] = vec3(0., 0.214, 1.); // cast rays through the spheres - vec2 u0 = sphere_cast(v[0], dir); - vec2 u1 = sphere_cast(v[1], dir); + vec2 depth_pairs [sphere_cnt]; + taggedFrag frags [2*sphere_cnt]; + for (int i = 0; i < sphere_cnt; i++) { + vec2 hit_depths = sphere_cast(sphere_list[i], dir); + frags[2*i] = sphere_shading(sphere_list[i], hit_depths[0] * dir, color_list[i], i); + frags[2*i+1] = sphere_shading(sphere_list[i], hit_depths[1] * dir, color_list[i], i); + } // shade and depth-sort the impact points - taggedFrag front_hits[2] = sort( - sphere_shading(v[0], u0[0] * dir, color0, 0), - sphere_shading(v[1], u1[0] * dir, color1, 1) - ); - taggedFrag back_hits[2] = sort( - sphere_shading(v[0], u0[1] * dir, color0, 0), - sphere_shading(v[1], u1[1] * dir, color1, 1) - ); + taggedFrag front_hits[2] = sort(frags[0], frags[2]); + taggedFrag back_hits[2] = sort(frags[1], frags[3]); taggedFrag middle_frags[2] = sort(front_hits[1], back_hits[0]); // finish depth sorting @@ -170,7 +172,7 @@ void main() { // cusps float cusp_cos = abs(dot(dir, frag0.normal)); - float cusp_threshold = 2.*sqrt(ixn_threshold * v[frag0.id].lt.s); + float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[frag0.id].lt.s); float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), cusp_highlight); } From c18cac642b23e0daeac65bb9b7b493a7749b16d7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 25 Aug 2024 00:47:36 -0700 Subject: [PATCH 017/126] Ray-caster: generalize depth sorting Switch from a hard-coded sorting network for four fragments to an insertion sort, which should work for any number of fragments. --- .../inversive-display/src/inversive.frag | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index 12b4129..93bdde4 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -137,29 +137,37 @@ void main() { // cast rays through the spheres vec2 depth_pairs [sphere_cnt]; taggedFrag frags [2*sphere_cnt]; - for (int i = 0; i < sphere_cnt; i++) { + int frag_cnt = 0; + for (int i = 0; i < sphere_cnt; ++i) { vec2 hit_depths = sphere_cast(sphere_list[i], dir); - frags[2*i] = sphere_shading(sphere_list[i], hit_depths[0] * dir, color_list[i], i); - frags[2*i+1] = sphere_shading(sphere_list[i], hit_depths[1] * dir, color_list[i], i); + if (!isnan(hit_depths[0])) { + frags[frag_cnt] = sphere_shading(sphere_list[i], hit_depths[0] * dir, color_list[i], i); + ++frag_cnt; + } + if (!isnan(hit_depths[1])) { + frags[frag_cnt] = sphere_shading(sphere_list[i], hit_depths[1] * dir, color_list[i], i); + ++frag_cnt; + } } - // shade and depth-sort the impact points - taggedFrag front_hits[2] = sort(frags[0], frags[2]); - taggedFrag back_hits[2] = sort(frags[1], frags[3]); - taggedFrag middle_frags[2] = sort(front_hits[1], back_hits[0]); - - // finish depth sorting - taggedFrag frags_by_depth[4]; - frags_by_depth[0] = front_hits[0]; - frags_by_depth[1] = middle_frags[0]; - frags_by_depth[2] = middle_frags[1]; - frags_by_depth[3] = back_hits[1]; + // sort the fragments by depth, using an insertion sort + for (int take = 1; take < frag_cnt; ++take) { + taggedFrag pulled = frags[take]; + for (int put = take; put >= 0; --put) { + if (put < 1 || frags[put-1].pt.z >= pulled.pt.z) { + frags[put] = pulled; + break; + } else { + frags[put] = frags[put-1]; + } + } + } // highlight intersections and cusps - for (int i = 3; i >= 1; --i) { + for (int i = frag_cnt-1; i >= 1; --i) { // intersections - taggedFrag frag0 = frags_by_depth[i]; - taggedFrag frag1 = frags_by_depth[i-1]; + taggedFrag frag0 = frags[i]; + taggedFrag frag1 = frags[i-1]; float ixn_sin = length(cross(frag0.normal, frag1.normal)); vec3 disp = frag0.pt - frag1.pt; float ixn_dist = max( @@ -167,21 +175,21 @@ void main() { abs(dot(frag0.normal, disp)) ) / ixn_sin; float ixn_highlight = 0.5 * highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); - frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), ixn_highlight); - frags_by_depth[i-1].color = mix(frags_by_depth[i-1].color, vec4(1.), ixn_highlight); + frags[i].color = mix(frags[i].color, vec4(1.), ixn_highlight); + frags[i-1].color = mix(frags[i-1].color, vec4(1.), ixn_highlight); // cusps float cusp_cos = abs(dot(dir, frag0.normal)); float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[frag0.id].lt.s); float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); - frags_by_depth[i].color = mix(frags_by_depth[i].color, vec4(1.), cusp_highlight); + frags[i].color = mix(frags[i].color, vec4(1.), cusp_highlight); } // composite the sphere fragments vec3 color = vec3(0.); - for (int i = 3; i >= layer_threshold; --i) { - if (frags_by_depth[i].pt.z < 0.) { - vec4 frag_color = frags_by_depth[i].color; + for (int i = frag_cnt-1; i >= layer_threshold; --i) { + if (frags[i].pt.z < 0.) { + vec4 frag_color = frags[i].color; color = mix(color, frag_color.rgb, frag_color.a); } } From 206a2df480db850df30201382fa9f9ea14d80755 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 25 Aug 2024 16:41:31 -0700 Subject: [PATCH 018/126] Ray-caster: add a third test sphere This helps confirm that the generalized depth-sorting is working. --- app-proto/inversive-display/src/inversive.frag | 8 +++++--- app-proto/inversive-display/src/main.rs | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index 93bdde4..dcc8610 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -121,7 +121,7 @@ vec2 sphere_cast(vecInv v, vec3 dir) { } void main() { - const int sphere_cnt = 2; + const int sphere_cnt = 3; vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; vec3 dir = vec3(focal_slope * scr, -1.); @@ -130,9 +130,11 @@ void main() { vecInv sphere_list [sphere_cnt]; sphere_list[0] = sphere(vec3(0.5, 0.5, -5. + ctrl.x), radius.x); sphere_list[1] = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), radius.y); + sphere_list[2] = sphere(vec3(-0.5, 0.5, -5.), 0.75); vec3 color_list [sphere_cnt]; - color_list[0] = vec3(1., 0.214, 0.); - color_list[1] = vec3(0., 0.214, 1.); + color_list[0] = vec3(1., 0.25, 0.); + color_list[1] = vec3(0., 0.25, 1.); + color_list[2] = vec3(0.25, 0., 1.0); // cast rays through the spheres vec2 depth_pairs [sphere_cnt]; diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 186facf..7bd2639 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -211,7 +211,7 @@ fn main() { ) input( type="range", - max=3.0, + max=5.0, step=1.0, bind:valueAsNumber=layer_threshold ) From 5bf23fa78974f2addf6cd766f008814db852fd42 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 25 Aug 2024 21:40:46 -0700 Subject: [PATCH 019/126] Ray-caster: pass spheres in through uniforms Keep the hard-coded spheres for comparison. --- app-proto/inversive-display/Cargo.toml | 1 + app-proto/inversive-display/src/engine.rs | 12 ++++ .../inversive-display/src/inversive.frag | 68 ++++++++++++------- app-proto/inversive-display/src/main.rs | 51 ++++++++++++-- 4 files changed, 103 insertions(+), 29 deletions(-) create mode 100644 app-proto/inversive-display/src/engine.rs diff --git a/app-proto/inversive-display/Cargo.toml b/app-proto/inversive-display/Cargo.toml index 3d7eb3f..bcf6b76 100644 --- a/app-proto/inversive-display/Cargo.toml +++ b/app-proto/inversive-display/Cargo.toml @@ -9,6 +9,7 @@ default = ["console_error_panic_hook"] [dependencies] js-sys = "0.3.70" +nalgebra = "0.33.0" sycamore = "0.9.0-beta.2" # The `console_error_panic_hook` crate provides better debugging of panics by diff --git a/app-proto/inversive-display/src/engine.rs b/app-proto/inversive-display/src/engine.rs new file mode 100644 index 0000000..7fbcd03 --- /dev/null +++ b/app-proto/inversive-display/src/engine.rs @@ -0,0 +1,12 @@ +use nalgebra::DVector; + +pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVector { + let center_norm_sq = center_x * center_x + center_y * center_y + center_z * center_z; + DVector::from_column_slice(&[ + center_x / radius, + center_y / radius, + center_z / radius, + 0.5 / radius, + 0.5 * (center_norm_sq / radius - radius) + ]) +} \ No newline at end of file diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index dcc8610..ff6ad6b 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -4,6 +4,29 @@ precision highp float; out vec4 outColor; +// --- inversive geometry --- + +struct vecInv { + vec3 sp; + vec2 lt; +}; + +vecInv sphere(vec3 center, float radius) { + return vecInv( + center / radius, + vec2( + 0.5 / radius, + 0.5 * (dot(center, center) / radius - radius) + ) + ); +} + +// --- uniforms --- + +// construction +const int SPHERE_MAX = 256; +uniform vecInv sphere_list[SPHERE_MAX]; + // view uniform vec2 resolution; uniform float shortdim; @@ -14,6 +37,7 @@ uniform vec2 radius; uniform float opacity; uniform float highlight; uniform int layer_threshold; +uniform bool use_test_construction; // light and camera const float focal_slope = 0.3; @@ -46,23 +70,6 @@ vec3 sRGB(vec3 color) { return vec3(sRGB(color.r), sRGB(color.g), sRGB(color.b)); } -// --- inversive geometry --- - -struct vecInv { - vec3 sp; - vec2 lt; -}; - -vecInv sphere(vec3 center, float radius) { - return vecInv( - center / radius, - vec2( - 0.5 / radius, - 0.5 * (dot(center, center) / radius - radius) - ) - ); -} - // --- shading --- struct taggedFrag { @@ -127,10 +134,21 @@ void main() { vec3 dir = vec3(focal_slope * scr, -1.); // initialize two spheres - vecInv sphere_list [sphere_cnt]; - sphere_list[0] = sphere(vec3(0.5, 0.5, -5. + ctrl.x), radius.x); - sphere_list[1] = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), radius.y); - sphere_list[2] = sphere(vec3(-0.5, 0.5, -5.), 0.75); + vecInv sphere_list_internal [sphere_cnt]; + if (use_test_construction) { + /* DEBUG */ + // spheres 0 and 1 are identical in the test construction hard-coded + // here and the construction passed in through uniforms. sphere 2 has + // a different radius in the construction; we can use that to show that + // the switch is working + sphere_list_internal[0] = sphere(vec3(0.5, 0.5, -5. + ctrl.x), radius.x); + sphere_list_internal[1] = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), radius.y); + sphere_list_internal[2] = sphere(vec3(-0.5, 0.5, -5.), 0.5); + } else { + for (int i = 0; i < sphere_cnt; ++i) { + sphere_list_internal[i] = sphere_list[i]; + } + } vec3 color_list [sphere_cnt]; color_list[0] = vec3(1., 0.25, 0.); color_list[1] = vec3(0., 0.25, 1.); @@ -141,13 +159,13 @@ void main() { taggedFrag frags [2*sphere_cnt]; int frag_cnt = 0; for (int i = 0; i < sphere_cnt; ++i) { - vec2 hit_depths = sphere_cast(sphere_list[i], dir); + vec2 hit_depths = sphere_cast(sphere_list_internal[i], dir); if (!isnan(hit_depths[0])) { - frags[frag_cnt] = sphere_shading(sphere_list[i], hit_depths[0] * dir, color_list[i], i); + frags[frag_cnt] = sphere_shading(sphere_list_internal[i], hit_depths[0] * dir, color_list[i], i); ++frag_cnt; } if (!isnan(hit_depths[1])) { - frags[frag_cnt] = sphere_shading(sphere_list[i], hit_depths[1] * dir, color_list[i], i); + frags[frag_cnt] = sphere_shading(sphere_list_internal[i], hit_depths[1] * dir, color_list[i], i); ++frag_cnt; } } @@ -182,7 +200,7 @@ void main() { // cusps float cusp_cos = abs(dot(dir, frag0.normal)); - float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[frag0.id].lt.s); + float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list_internal[frag0.id].lt.s); float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); frags[i].color = mix(frags[i].color, vec4(1.), cusp_highlight); } diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 7bd2639..83e05c0 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -8,9 +8,13 @@ // extern crate js_sys; +use core::array; +use nalgebra::DVector; use sycamore::{prelude::*, rt::{JsCast, JsValue}}; use web_sys::{console, WebGl2RenderingContext, WebGlShader}; +mod engine; + fn compile_shader( context: &WebGl2RenderingContext, shader_type: u32, @@ -70,6 +74,7 @@ fn main() { console_error_panic_hook::set_once(); sycamore::render(|| { + // controls let ctrl_x = create_signal(0.0); let ctrl_y = create_signal(0.0); let radius_x = create_signal(1.0); @@ -77,9 +82,16 @@ fn main() { let opacity = create_signal(0.5); let highlight = create_signal(0.2); let layer_threshold = create_signal(0.0); + let use_test_construction = create_signal(false); + + // display let display = create_node_ref(); on_mount(move || { + // list construction elements + const SPHERE_MAX: usize = 256; + let mut sphere_vec = Vec::>::new(); + // get the display canvas let canvas = display .get::() @@ -119,14 +131,21 @@ fn main() { ctx.use_program(Some(&program)); // find indices of vertex attributes and uniforms + let sphere_sp_locs = array::from_fn::<_, SPHERE_MAX, _>( + |n| ctx.get_uniform_location(&program, format!("sphere_list[{}].sp", n).as_str()) + ); + let sphere_lt_locs = array::from_fn::<_, SPHERE_MAX, _>( + |n| ctx.get_uniform_location(&program, format!("sphere_list[{}].lt", n).as_str()) + ); let position_index = ctx.get_attrib_location(&program, "position") as u32; let resolution_loc = ctx.get_uniform_location(&program, "resolution"); let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); - let ctrl_loc = ctx.get_uniform_location(&program, "ctrl"); - let radius_loc = ctx.get_uniform_location(&program, "radius"); + let ctrl_loc = ctx.get_uniform_location(&program, "ctrl"); /* DEBUG */ + let radius_loc = ctx.get_uniform_location(&program, "radius"); /* DEBUG */ let opacity_loc = ctx.get_uniform_location(&program, "opacity"); let highlight_loc = ctx.get_uniform_location(&program, "highlight"); let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); + let use_test_construction_loc = ctx.get_uniform_location(&program, "use_test_construction"); // create a vertex array and bind it to the graphics context let vertex_array = ctx.create_vertex_array().unwrap(); @@ -148,18 +167,38 @@ fn main() { // set up a repainting routine create_effect(move || { + // update the construction + sphere_vec.clear(); + sphere_vec.push(engine::sphere(0.5, 0.5, -5.0 + ctrl_x.get(), radius_x.get())); + sphere_vec.push(engine::sphere(-0.5, -0.5, -5.0 + ctrl_y.get(), radius_y.get())); + sphere_vec.push(engine::sphere(-0.5, 0.5, -5.0, 0.75)); + // 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 construction + for n in 0..sphere_vec.len() { + let v = &sphere_vec[n]; + ctx.uniform3f( + sphere_sp_locs[n].as_ref(), + v[0] as f32, v[1] as f32, v[2] as f32 + ); + ctx.uniform2f( + sphere_lt_locs[n].as_ref(), + v[3] as f32, v[4] as f32 + ); + } + // pass the control parameters - ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); - ctx.uniform2f(radius_loc.as_ref(), radius_x.get() as f32, radius_y.get() as f32); + ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); /* DEBUG */ + ctx.uniform2f(radius_loc.as_ref(), radius_x.get() as f32, radius_y.get() as f32); /* DEBUG */ ctx.uniform1f(opacity_loc.as_ref(), opacity.get() as f32); ctx.uniform1f(highlight_loc.as_ref(), highlight.get() as f32); ctx.uniform1i(layer_threshold_loc.as_ref(), layer_threshold.get() as i32); + ctx.uniform1i(use_test_construction_loc.as_ref(), use_test_construction.get() as i32); // draw the scene ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); @@ -215,6 +254,10 @@ fn main() { step=1.0, bind:valueAsNumber=layer_threshold ) + input( + type="checkbox", + bind:checked=use_test_construction + ) } } }); From c5fe725b1b8086366537a6983f36de1e13dcb5c5 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 25 Aug 2024 22:22:14 -0700 Subject: [PATCH 020/126] Ray-caster: automate getting uniform array locations --- app-proto/inversive-display/src/main.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 83e05c0..e2097e7 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -11,7 +11,7 @@ extern crate js_sys; use core::array; use nalgebra::DVector; use sycamore::{prelude::*, rt::{JsCast, JsValue}}; -use web_sys::{console, WebGl2RenderingContext, WebGlShader}; +use web_sys::{console, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation}; mod engine; @@ -26,6 +26,18 @@ fn compile_shader( shader } +fn get_uniform_array_locations( + context: &WebGl2RenderingContext, + program: &WebGlProgram, + var_name: &str, + member_name: &str +) -> [Option; N] { + array::from_fn(|n| { + let name = format!("{var_name}[{n}].{member_name}"); + context.get_uniform_location(&program, name.as_str()) + }) +} + // load the given data into the vertex input of the given name fn bind_vertex_attrib( context: &WebGl2RenderingContext, @@ -131,11 +143,11 @@ fn main() { ctx.use_program(Some(&program)); // find indices of vertex attributes and uniforms - let sphere_sp_locs = array::from_fn::<_, SPHERE_MAX, _>( - |n| ctx.get_uniform_location(&program, format!("sphere_list[{}].sp", n).as_str()) + let sphere_sp_locs = get_uniform_array_locations::( + &ctx, &program, "sphere_list", "sp" ); - let sphere_lt_locs = array::from_fn::<_, SPHERE_MAX, _>( - |n| ctx.get_uniform_location(&program, format!("sphere_list[{}].lt", n).as_str()) + let sphere_lt_locs = get_uniform_array_locations::( + &ctx, &program, "sphere_list", "lt" ); let position_index = ctx.get_attrib_location(&program, "position") as u32; let resolution_loc = ctx.get_uniform_location(&program, "resolution"); From 85db7b9be08a11f66f0dc9cf6b192139bdefdf1e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 26 Aug 2024 00:43:42 -0700 Subject: [PATCH 021/126] Ray-caster: pass the sphere count as a uniform In the process, start exploring array size limits of various kinds. --- .../inversive-display/src/inversive.frag | 16 ++++++------- app-proto/inversive-display/src/main.rs | 23 +++++++++++++++++-- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index ff6ad6b..fdfcc0b 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -23,8 +23,10 @@ vecInv sphere(vec3 center, float radius) { // --- uniforms --- -// construction -const int SPHERE_MAX = 256; +// construction. the SPHERE_MAX array size seems to affect frame rate a lot, +// even though we should only be using the first few elements of each array +const int SPHERE_MAX = 12; +uniform int sphere_cnt; uniform vecInv sphere_list[SPHERE_MAX]; // view @@ -128,13 +130,11 @@ vec2 sphere_cast(vecInv v, vec3 dir) { } void main() { - const int sphere_cnt = 3; - vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; vec3 dir = vec3(focal_slope * scr, -1.); // initialize two spheres - vecInv sphere_list_internal [sphere_cnt]; + vecInv sphere_list_internal [SPHERE_MAX]; if (use_test_construction) { /* DEBUG */ // spheres 0 and 1 are identical in the test construction hard-coded @@ -149,14 +149,14 @@ void main() { sphere_list_internal[i] = sphere_list[i]; } } - vec3 color_list [sphere_cnt]; + vec3 color_list [SPHERE_MAX]; color_list[0] = vec3(1., 0.25, 0.); color_list[1] = vec3(0., 0.25, 1.); color_list[2] = vec3(0.25, 0., 1.0); // cast rays through the spheres - vec2 depth_pairs [sphere_cnt]; - taggedFrag frags [2*sphere_cnt]; + vec2 depth_pairs [SPHERE_MAX]; + taggedFrag frags [2*SPHERE_MAX]; int frag_cnt = 0; for (int i = 0; i < sphere_cnt; ++i) { vec2 hit_depths = sphere_cast(sphere_list_internal[i], dir); diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index e2097e7..eceecd7 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -101,7 +101,7 @@ fn main() { on_mount(move || { // list construction elements - const SPHERE_MAX: usize = 256; + const SPHERE_MAX: usize = 12; let mut sphere_vec = Vec::>::new(); // get the display canvas @@ -142,14 +142,32 @@ fn main() { console::log_1(&JsValue::from(link_msg)); ctx.use_program(Some(&program)); + /* DEBUG */ + // print the maximum number of vectors that can be passed as + // uniforms to a fragment shader. the OpenGL ES 3.0 standard + // requires this maximum to be at least 224, as discussed in the + // documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter + // here: + // + // https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml + // + // there are also other size limits. for example, on Aaron's + // machine, the the length of a float or genType array seems to be + // capped at 1024 elements + console::log_2( + &ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(), + &JsValue::from("uniform vectors available") + ); + // find indices of vertex attributes and uniforms + let position_index = ctx.get_attrib_location(&program, "position") as u32; + let sphere_cnt_loc = ctx.get_uniform_location(&program, "sphere_cnt"); let sphere_sp_locs = get_uniform_array_locations::( &ctx, &program, "sphere_list", "sp" ); let sphere_lt_locs = get_uniform_array_locations::( &ctx, &program, "sphere_list", "lt" ); - let position_index = ctx.get_attrib_location(&program, "position") as u32; let resolution_loc = ctx.get_uniform_location(&program, "resolution"); let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); let ctrl_loc = ctx.get_uniform_location(&program, "ctrl"); /* DEBUG */ @@ -192,6 +210,7 @@ fn main() { ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); // pass the construction + ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_vec.len() as i32); for n in 0..sphere_vec.len() { let v = &sphere_vec[n]; ctx.uniform3f( From b9370ceb41537e83efa45094a4da1bbb4d6b1496 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 26 Aug 2024 01:47:53 -0700 Subject: [PATCH 022/126] Ray-caster: label controls --- app-proto/inversive-display/main.css | 12 ++- app-proto/inversive-display/src/main.rs | 132 +++++++++++++++--------- 2 files changed, 93 insertions(+), 51 deletions(-) diff --git a/app-proto/inversive-display/main.css b/app-proto/inversive-display/main.css index f79e62c..7cdc831 100644 --- a/app-proto/inversive-display/main.css +++ b/app-proto/inversive-display/main.css @@ -18,6 +18,16 @@ canvas { margin-top: 5px; } +.control { + display: flex; + flex-direction: row; + width: 600px; +} + +label { + width: 150px; +} + input { - margin-top: 5px; + flex-grow: 1; } \ No newline at end of file diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index eceecd7..6b52a12 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -239,56 +239,88 @@ fn main() { view! { div(id="app") { canvas(ref=display, width="600", height="600") - input( - type="range", - min=-1.0, - max=1.0, - step=0.001, - bind:valueAsNumber=ctrl_x - ) - input( - type="range", - min=-1.0, - max=1.0, - step=0.001, - bind:valueAsNumber=ctrl_y - ) - input( - type="range", - min=0.5, - max=1.5, - step=0.001, - bind:valueAsNumber=radius_x - ) - input( - type="range", - min=0.5, - max=1.5, - step=0.001, - bind:valueAsNumber=radius_y - ) - input( - type="range", - max=1.0, - step=0.001, - bind:valueAsNumber=opacity - ) - input( - type="range", - max=1.0, - step=0.001, - bind:valueAsNumber=highlight - ) - input( - type="range", - max=5.0, - step=1.0, - bind:valueAsNumber=layer_threshold - ) - input( - type="checkbox", - bind:checked=use_test_construction - ) + div(class="control") { + label(for="ctrl-x") { "Sphere 0 depth" } + input( + type="range", + id="ctrl-x", + min=-1.0, + max=1.0, + step=0.001, + bind:valueAsNumber=ctrl_x + ) + } + div(class="control") { + label(for="ctrl-y") { "Sphere 1 depth" } + input( + type="range", + id="ctrl-y", + min=-1.0, + max=1.0, + step=0.001, + bind:valueAsNumber=ctrl_y + ) + } + div(class="control") { + label(for="radius-x") { "Sphere 0 radius" } + input( + type="range", + id="radius-x", + min=0.5, + max=1.5, + step=0.001, + bind:valueAsNumber=radius_x + ) + } + div(class="control") { + label(for="radius-y") { "Sphere 1 radius" } + input( + type="range", + id="radius-y", + min=0.5, + max=1.5, + step=0.001, + bind:valueAsNumber=radius_y + ) + } + div(class="control") { + label(for="opacity") { "Opacity" } + input( + type="range", + id="opacity", + max=1.0, + step=0.001, + bind:valueAsNumber=opacity + ) + } + div(class="control") { + label(for="highlight") { "Highlight" } + input( + type="range", + id="highlight", + max=1.0, + step=0.001, + bind:valueAsNumber=highlight + ) + } + div(class="control") { + label(for="layer-threshold") { "Layer threshold" } + input( + type="range", + id="layer-threshold", + max=5.0, + step=1.0, + bind:valueAsNumber=layer_threshold + ) + } + div(class="control") { + label(for="use-test-construction") { "Use test values" } + input( + type="checkbox", + id="use-test-construction", + bind:checked=use_test_construction + ) + } } } }); From cbec31f5df3284954f13f8c29725f54cf06d325e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 26 Aug 2024 13:41:34 -0700 Subject: [PATCH 023/126] Ray-caster: pass colors in through uniforms --- .../inversive-display/src/inversive.frag | 14 ++++++----- app-proto/inversive-display/src/main.rs | 23 +++++++++++++++---- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index fdfcc0b..db1a3c0 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -28,6 +28,7 @@ vecInv sphere(vec3 center, float radius) { const int SPHERE_MAX = 12; uniform int sphere_cnt; uniform vecInv sphere_list[SPHERE_MAX]; +uniform vec3 color_list[SPHERE_MAX]; // view uniform vec2 resolution; @@ -135,6 +136,7 @@ void main() { // initialize two spheres vecInv sphere_list_internal [SPHERE_MAX]; + vec3 color_list_internal [SPHERE_MAX]; if (use_test_construction) { /* DEBUG */ // spheres 0 and 1 are identical in the test construction hard-coded @@ -144,15 +146,15 @@ void main() { sphere_list_internal[0] = sphere(vec3(0.5, 0.5, -5. + ctrl.x), radius.x); sphere_list_internal[1] = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), radius.y); sphere_list_internal[2] = sphere(vec3(-0.5, 0.5, -5.), 0.5); + color_list_internal[0] = vec3(1., 0.25, 0.); + color_list_internal[1] = vec3(0., 0.25, 1.); + color_list_internal[2] = vec3(0.25, 0., 1.0); } else { for (int i = 0; i < sphere_cnt; ++i) { sphere_list_internal[i] = sphere_list[i]; + color_list_internal[i] = color_list[i]; } } - vec3 color_list [SPHERE_MAX]; - color_list[0] = vec3(1., 0.25, 0.); - color_list[1] = vec3(0., 0.25, 1.); - color_list[2] = vec3(0.25, 0., 1.0); // cast rays through the spheres vec2 depth_pairs [SPHERE_MAX]; @@ -161,11 +163,11 @@ void main() { for (int i = 0; i < sphere_cnt; ++i) { vec2 hit_depths = sphere_cast(sphere_list_internal[i], dir); if (!isnan(hit_depths[0])) { - frags[frag_cnt] = sphere_shading(sphere_list_internal[i], hit_depths[0] * dir, color_list[i], i); + frags[frag_cnt] = sphere_shading(sphere_list_internal[i], hit_depths[0] * dir, color_list_internal[i], i); ++frag_cnt; } if (!isnan(hit_depths[1])) { - frags[frag_cnt] = sphere_shading(sphere_list_internal[i], hit_depths[1] * dir, color_list[i], i); + frags[frag_cnt] = sphere_shading(sphere_list_internal[i], hit_depths[1] * dir, color_list_internal[i], i); ++frag_cnt; } } diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 6b52a12..5a9b875 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -30,10 +30,13 @@ fn get_uniform_array_locations( context: &WebGl2RenderingContext, program: &WebGlProgram, var_name: &str, - member_name: &str + member_name_opt: Option<&str> ) -> [Option; N] { array::from_fn(|n| { - let name = format!("{var_name}[{n}].{member_name}"); + let name = match member_name_opt { + Some(member_name) => format!("{var_name}[{n}].{member_name}"), + None => format!("{var_name}[{n}]") + }; context.get_uniform_location(&program, name.as_str()) }) } @@ -103,6 +106,11 @@ fn main() { // list construction elements const SPHERE_MAX: usize = 12; let mut sphere_vec = Vec::>::new(); + let color_vec = vec![ + [1.00_f32, 0.25_f32, 0.00_f32], + [0.00_f32, 0.25_f32, 1.00_f32], + [0.25_f32, 0.00_f32, 1.00_f32] + ]; // get the display canvas let canvas = display @@ -163,10 +171,13 @@ fn main() { let position_index = ctx.get_attrib_location(&program, "position") as u32; let sphere_cnt_loc = ctx.get_uniform_location(&program, "sphere_cnt"); let sphere_sp_locs = get_uniform_array_locations::( - &ctx, &program, "sphere_list", "sp" + &ctx, &program, "sphere_list", Some("sp") ); let sphere_lt_locs = get_uniform_array_locations::( - &ctx, &program, "sphere_list", "lt" + &ctx, &program, "sphere_list", Some("lt") + ); + let color_locs = get_uniform_array_locations::( + &ctx, &program, "color_list", None ); let resolution_loc = ctx.get_uniform_location(&program, "resolution"); let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); @@ -221,6 +232,10 @@ fn main() { sphere_lt_locs[n].as_ref(), v[3] as f32, v[4] as f32 ); + ctx.uniform3fv_with_f32_array( + color_locs[n].as_ref(), + &color_vec[n] + ); } // pass the control parameters From bf140efaf7bce9f1c9f03b97602589d9d7ca1bdd Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 26 Aug 2024 13:51:01 -0700 Subject: [PATCH 024/126] Ray-caster: remove hard-coded test construction --- .../inversive-display/src/inversive.frag | 32 +++---------------- app-proto/inversive-display/src/main.rs | 12 +++---- 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index db1a3c0..51bc0ff 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -40,7 +40,7 @@ uniform vec2 radius; uniform float opacity; uniform float highlight; uniform int layer_threshold; -uniform bool use_test_construction; +uniform bool test_mode; // light and camera const float focal_slope = 0.3; @@ -134,40 +134,18 @@ void main() { vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; vec3 dir = vec3(focal_slope * scr, -1.); - // initialize two spheres - vecInv sphere_list_internal [SPHERE_MAX]; - vec3 color_list_internal [SPHERE_MAX]; - if (use_test_construction) { - /* DEBUG */ - // spheres 0 and 1 are identical in the test construction hard-coded - // here and the construction passed in through uniforms. sphere 2 has - // a different radius in the construction; we can use that to show that - // the switch is working - sphere_list_internal[0] = sphere(vec3(0.5, 0.5, -5. + ctrl.x), radius.x); - sphere_list_internal[1] = sphere(vec3(-0.5, -0.5, -5. + ctrl.y), radius.y); - sphere_list_internal[2] = sphere(vec3(-0.5, 0.5, -5.), 0.5); - color_list_internal[0] = vec3(1., 0.25, 0.); - color_list_internal[1] = vec3(0., 0.25, 1.); - color_list_internal[2] = vec3(0.25, 0., 1.0); - } else { - for (int i = 0; i < sphere_cnt; ++i) { - sphere_list_internal[i] = sphere_list[i]; - color_list_internal[i] = color_list[i]; - } - } - // cast rays through the spheres vec2 depth_pairs [SPHERE_MAX]; taggedFrag frags [2*SPHERE_MAX]; int frag_cnt = 0; for (int i = 0; i < sphere_cnt; ++i) { - vec2 hit_depths = sphere_cast(sphere_list_internal[i], dir); + vec2 hit_depths = sphere_cast(sphere_list[i], dir); if (!isnan(hit_depths[0])) { - frags[frag_cnt] = sphere_shading(sphere_list_internal[i], hit_depths[0] * dir, color_list_internal[i], i); + frags[frag_cnt] = sphere_shading(sphere_list[i], hit_depths[0] * dir, color_list[i], i); ++frag_cnt; } if (!isnan(hit_depths[1])) { - frags[frag_cnt] = sphere_shading(sphere_list_internal[i], hit_depths[1] * dir, color_list_internal[i], i); + frags[frag_cnt] = sphere_shading(sphere_list[i], hit_depths[1] * dir, color_list[i], i); ++frag_cnt; } } @@ -202,7 +180,7 @@ void main() { // cusps float cusp_cos = abs(dot(dir, frag0.normal)); - float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list_internal[frag0.id].lt.s); + float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[frag0.id].lt.s); float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); frags[i].color = mix(frags[i].color, vec4(1.), cusp_highlight); } diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 5a9b875..a5bfedc 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -97,7 +97,7 @@ fn main() { let opacity = create_signal(0.5); let highlight = create_signal(0.2); let layer_threshold = create_signal(0.0); - let use_test_construction = create_signal(false); + let debug_mode = create_signal(false); // display let display = create_node_ref(); @@ -186,7 +186,7 @@ fn main() { let opacity_loc = ctx.get_uniform_location(&program, "opacity"); let highlight_loc = ctx.get_uniform_location(&program, "highlight"); let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); - let use_test_construction_loc = ctx.get_uniform_location(&program, "use_test_construction"); + 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(); @@ -244,7 +244,7 @@ fn main() { ctx.uniform1f(opacity_loc.as_ref(), opacity.get() as f32); ctx.uniform1f(highlight_loc.as_ref(), highlight.get() as f32); ctx.uniform1i(layer_threshold_loc.as_ref(), layer_threshold.get() as i32); - ctx.uniform1i(use_test_construction_loc.as_ref(), use_test_construction.get() as i32); + ctx.uniform1i(debug_mode_loc.as_ref(), debug_mode.get() as i32); // draw the scene ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); @@ -329,11 +329,11 @@ fn main() { ) } div(class="control") { - label(for="use-test-construction") { "Use test values" } + label(for="debug-mode") { "Debug mode" } input( type="checkbox", - id="use-test-construction", - bind:checked=use_test_construction + id="debug-mode", + bind:checked=debug_mode ) } } From 5e9c5db231dfd0061ebae387d01507019051acc5 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 26 Aug 2024 16:06:37 -0700 Subject: [PATCH 025/126] Ray-caster: switch from draw effect to animation loop This wastes a lot of CPU time, as explained on lines 253--258 of `main.rs`, but it's better than the previous version, which could block graphics updates system-wide for seconds on end. --- app-proto/inversive-display/src/main.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index a5bfedc..ebbf713 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -10,7 +10,7 @@ extern crate js_sys; use core::array; use nalgebra::DVector; -use sycamore::{prelude::*, rt::{JsCast, JsValue}}; +use sycamore::{prelude::*, motion::create_raf, rt::{JsCast, JsValue}}; use web_sys::{console, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation}; mod engine; @@ -207,7 +207,7 @@ fn main() { bind_vertex_attrib(&ctx, position_index, 3, &positions); // set up a repainting routine - create_effect(move || { + let (_, start_updating_display, _) = create_raf(move || { // update the construction sphere_vec.clear(); sphere_vec.push(engine::sphere(0.5, 0.5, -5.0 + ctrl_x.get(), radius_x.get())); @@ -249,6 +249,14 @@ fn main() { // draw the scene ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); }); + + /* + this wastes CPU time by running an animation loop, which updates the + display even when nothing has changed. there should be a way to make + Sycamore do single-frame updates in response to changes, but i + haven't found it yet + */ + start_updating_display(); }); view! { From ec48592ef15876c5c824b10ab5ff0c2f5bd3a76a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 26 Aug 2024 23:39:51 -0700 Subject: [PATCH 026/126] Ray-caster: add a frame time monitor It's time to start optimizing. Frame time is easy to measure, and we can use it to gauge responsiveness. --- app-proto/inversive-display/Cargo.toml | 1 + app-proto/inversive-display/src/main.rs | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app-proto/inversive-display/Cargo.toml b/app-proto/inversive-display/Cargo.toml index bcf6b76..c0cbf3d 100644 --- a/app-proto/inversive-display/Cargo.toml +++ b/app-proto/inversive-display/Cargo.toml @@ -22,6 +22,7 @@ console_error_panic_hook = { version = "0.1.7", optional = true } version = "0.3.69" features = [ 'HtmlCanvasElement', + 'Performance', 'WebGl2RenderingContext', 'WebGlBuffer', 'WebGlProgram', diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index ebbf713..5f1987f 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -11,7 +11,7 @@ extern crate js_sys; use core::array; use nalgebra::DVector; use sycamore::{prelude::*, motion::create_raf, rt::{JsCast, JsValue}}; -use web_sys::{console, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation}; +use web_sys::{console, window, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation}; mod engine; @@ -99,6 +99,12 @@ fn main() { let layer_threshold = create_signal(0.0); let debug_mode = create_signal(false); + /* INSTRUMENTS */ + const SAMPLE_PERIOD: i32 = 20; + let mut last_frame_moment = 0.0; + let mut frames_since_last_sample = 0; + let frame_time = create_signal(0.0); + // display let display = create_node_ref(); @@ -112,6 +118,9 @@ fn main() { [0.25_f32, 0.00_f32, 1.00_f32] ]; + /* INSTRUMENTS */ + let performance = window().unwrap().performance().unwrap(); + // get the display canvas let canvas = display .get::() @@ -208,6 +217,16 @@ fn main() { // set up a repainting routine let (_, start_updating_display, _) = create_raf(move || { + /* INSTRUMENTS */ + // measure frame time + frames_since_last_sample += 1; + if frames_since_last_sample >= SAMPLE_PERIOD { + let frame_moment = performance.now(); + frame_time.set((frame_moment - last_frame_moment) / (SAMPLE_PERIOD as f64)); + last_frame_moment = frame_moment; + frames_since_last_sample = 0; + } + // update the construction sphere_vec.clear(); sphere_vec.push(engine::sphere(0.5, 0.5, -5.0 + ctrl_x.get(), radius_x.get())); @@ -261,6 +280,7 @@ fn main() { view! { div(id="app") { + div { (frame_time.get()) " ms" } canvas(ref=display, width="600", height="600") div(class="control") { label(for="ctrl-x") { "Sphere 0 depth" } From f62f44b5a77fa2cbefad4608538aa07449d149b3 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 27 Aug 2024 00:00:48 -0700 Subject: [PATCH 027/126] Optimization: decouple internal and uniform SPHERE_MAX --- app-proto/inversive-display/src/inversive.frag | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index 51bc0ff..ce6deae 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -25,10 +25,10 @@ vecInv sphere(vec3 center, float radius) { // construction. the SPHERE_MAX array size seems to affect frame rate a lot, // even though we should only be using the first few elements of each array -const int SPHERE_MAX = 12; +const int SPHERE_MAX_UNIFORM = 12; uniform int sphere_cnt; -uniform vecInv sphere_list[SPHERE_MAX]; -uniform vec3 color_list[SPHERE_MAX]; +uniform vecInv sphere_list[SPHERE_MAX_UNIFORM]; +uniform vec3 color_list[SPHERE_MAX_UNIFORM]; // view uniform vec2 resolution; @@ -135,8 +135,9 @@ void main() { vec3 dir = vec3(focal_slope * scr, -1.); // cast rays through the spheres - vec2 depth_pairs [SPHERE_MAX]; - taggedFrag frags [2*SPHERE_MAX]; + const int SPHERE_MAX_INTERNAL = 6; + vec2 depth_pairs [SPHERE_MAX_INTERNAL]; + taggedFrag frags [2*SPHERE_MAX_INTERNAL]; int frag_cnt = 0; for (int i = 0; i < sphere_cnt; ++i) { vec2 hit_depths = sphere_cast(sphere_list[i], dir); From a40a110788e577417cfede4fb6fb3b130d86b37e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 27 Aug 2024 13:55:08 -0700 Subject: [PATCH 028/126] Ray-caster: only draw when the scene is changed This is how I typically schedule draw calls in JavaScript applications. The baseline CPU activity for the display prototype is now in line with other pages (though perhaps a bit higher), and the profiler shows little time being spent in draw calls, even when I'm continually moving a slider. The interface feels pretty responsive overall, although the sliders seem to be lagging a bit. --- app-proto/inversive-display/src/main.rs | 130 +++++++++++++----------- 1 file changed, 73 insertions(+), 57 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 5f1987f..15c2518 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -108,6 +108,21 @@ fn main() { // display let display = create_node_ref(); + // change listener + let scene_changed = create_signal(true); + create_effect(move || { + ctrl_x.track(); + ctrl_y.track(); + radius_x.track(); + radius_y.track(); + opacity.track(); + highlight.track(); + layer_threshold.track(); + debug_mode.track(); + + scene_changed.set(true); + }); + on_mount(move || { // list construction elements const SPHERE_MAX: usize = 12; @@ -216,66 +231,67 @@ fn main() { bind_vertex_attrib(&ctx, position_index, 3, &positions); // set up a repainting routine - let (_, start_updating_display, _) = create_raf(move || { - /* INSTRUMENTS */ - // measure frame time - frames_since_last_sample += 1; - if frames_since_last_sample >= SAMPLE_PERIOD { - let frame_moment = performance.now(); - frame_time.set((frame_moment - last_frame_moment) / (SAMPLE_PERIOD as f64)); - last_frame_moment = frame_moment; + let (_, start_animation_loop, _) = create_raf(move || { + if scene_changed.get() { + /* INSTRUMENTS */ + // measure frame time + frames_since_last_sample += 1; + if frames_since_last_sample >= SAMPLE_PERIOD { + let frame_moment = performance.now(); + frame_time.set((frame_moment - last_frame_moment) / (SAMPLE_PERIOD as f64)); + last_frame_moment = frame_moment; + frames_since_last_sample = 0; + } + + // update the construction + sphere_vec.clear(); + sphere_vec.push(engine::sphere(0.5, 0.5, -5.0 + ctrl_x.get(), radius_x.get())); + sphere_vec.push(engine::sphere(-0.5, -0.5, -5.0 + ctrl_y.get(), radius_y.get())); + sphere_vec.push(engine::sphere(-0.5, 0.5, -5.0, 0.75)); + + // 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 construction + ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_vec.len() as i32); + for n in 0..sphere_vec.len() { + let v = &sphere_vec[n]; + ctx.uniform3f( + sphere_sp_locs[n].as_ref(), + v[0] as f32, v[1] as f32, v[2] as f32 + ); + ctx.uniform2f( + sphere_lt_locs[n].as_ref(), + v[3] as f32, v[4] as f32 + ); + ctx.uniform3fv_with_f32_array( + color_locs[n].as_ref(), + &color_vec[n] + ); + } + + // pass the control parameters + ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); /* DEBUG */ + ctx.uniform2f(radius_loc.as_ref(), radius_x.get() as f32, radius_y.get() as f32); /* DEBUG */ + ctx.uniform1f(opacity_loc.as_ref(), opacity.get() as f32); + ctx.uniform1f(highlight_loc.as_ref(), highlight.get() as f32); + ctx.uniform1i(layer_threshold_loc.as_ref(), layer_threshold.get() as i32); + ctx.uniform1i(debug_mode_loc.as_ref(), debug_mode.get() as i32); + + // draw the scene + ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); + + // clear scene change flag + scene_changed.set(false); + } else { frames_since_last_sample = 0; + frame_time.set(-1.0); } - - // update the construction - sphere_vec.clear(); - sphere_vec.push(engine::sphere(0.5, 0.5, -5.0 + ctrl_x.get(), radius_x.get())); - sphere_vec.push(engine::sphere(-0.5, -0.5, -5.0 + ctrl_y.get(), radius_y.get())); - sphere_vec.push(engine::sphere(-0.5, 0.5, -5.0, 0.75)); - - // 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 construction - ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_vec.len() as i32); - for n in 0..sphere_vec.len() { - let v = &sphere_vec[n]; - ctx.uniform3f( - sphere_sp_locs[n].as_ref(), - v[0] as f32, v[1] as f32, v[2] as f32 - ); - ctx.uniform2f( - sphere_lt_locs[n].as_ref(), - v[3] as f32, v[4] as f32 - ); - ctx.uniform3fv_with_f32_array( - color_locs[n].as_ref(), - &color_vec[n] - ); - } - - // pass the control parameters - ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); /* DEBUG */ - ctx.uniform2f(radius_loc.as_ref(), radius_x.get() as f32, radius_y.get() as f32); /* DEBUG */ - ctx.uniform1f(opacity_loc.as_ref(), opacity.get() as f32); - ctx.uniform1f(highlight_loc.as_ref(), highlight.get() as f32); - ctx.uniform1i(layer_threshold_loc.as_ref(), layer_threshold.get() as i32); - ctx.uniform1i(debug_mode_loc.as_ref(), debug_mode.get() as i32); - - // draw the scene - ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); }); - - /* - this wastes CPU time by running an animation loop, which updates the - display even when nothing has changed. there should be a way to make - Sycamore do single-frame updates in response to changes, but i - haven't found it yet - */ - start_updating_display(); + start_animation_loop(); }); view! { From 01c2af6615a4a600c75fac3923276ec53516f709 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 27 Aug 2024 18:39:58 -0700 Subject: [PATCH 029/126] Rotate and translate construction In the process, write code to make updates that depend on the time between frames. --- app-proto/inversive-display/src/main.rs | 82 ++++++++++++++++++++----- 1 file changed, 67 insertions(+), 15 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 15c2518..4da24f5 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -9,7 +9,7 @@ extern crate js_sys; use core::array; -use nalgebra::DVector; +use nalgebra::{DMatrix, DVector}; use sycamore::{prelude::*, motion::create_raf, rt::{JsCast, JsValue}}; use web_sys::{console, window, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation}; @@ -96,14 +96,15 @@ fn main() { let radius_y = create_signal(1.0); let opacity = create_signal(0.5); let highlight = create_signal(0.2); - let layer_threshold = create_signal(0.0); - let debug_mode = create_signal(false); + let turntable = create_signal(false); + let layer_threshold = create_signal(0.0); /* DEBUG */ + let debug_mode = create_signal(false); /* DEBUG */ /* INSTRUMENTS */ const SAMPLE_PERIOD: i32 = 20; - let mut last_frame_moment = 0.0; + let mut last_sample_time = 0.0; let mut frames_since_last_sample = 0; - let frame_time = create_signal(0.0); + let mean_frame_interval = create_signal(0.0); // display let display = create_node_ref(); @@ -117,6 +118,7 @@ fn main() { radius_y.track(); opacity.track(); highlight.track(); + turntable.track(); layer_threshold.track(); debug_mode.track(); @@ -133,6 +135,13 @@ fn main() { [0.25_f32, 0.00_f32, 1.00_f32] ]; + // timing + let mut last_time = 0.0; + + // scene parameters + const TURNTABLE_SPEED: f64 = 0.5; + let mut turntable_angle = 0.0; + /* INSTRUMENTS */ let performance = window().unwrap().performance().unwrap(); @@ -232,22 +241,57 @@ fn main() { // set up a repainting routine let (_, start_animation_loop, _) = create_raf(move || { + // get the time step + let time = performance.now(); + let time_step = 0.001*(time - last_time); + last_time = time; + + // move the turntable + let turntable_val = turntable.get(); + if turntable_val { + turntable_angle += TURNTABLE_SPEED * time_step; + } + if scene_changed.get() { /* INSTRUMENTS */ - // measure frame time + // measure mean frame interval frames_since_last_sample += 1; if frames_since_last_sample >= SAMPLE_PERIOD { - let frame_moment = performance.now(); - frame_time.set((frame_moment - last_frame_moment) / (SAMPLE_PERIOD as f64)); - last_frame_moment = frame_moment; + mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64)); + last_sample_time = time; frames_since_last_sample = 0; } + // set the orientation and translation + let orientation = { + let ang_cos = turntable_angle.cos(); + let ang_sin = turntable_angle.sin(); + DMatrix::from_column_slice(5, 5, &[ + ang_cos, 0.0, ang_sin, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, + -ang_sin, 0.0, ang_cos, 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 1.0 + ]) + }; + let translation = { + const LEN: f64 = -5.0; + const LEN_SQ: f64 = LEN*LEN; + DMatrix::from_column_slice(5, 5, &[ + 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, LEN, + 0.0, 0.0, 2.0*LEN, 1.0, LEN_SQ, + 0.0, 0.0, 0.0, 0.0, 1.0 + ]) + }; + let construction_to_world = &translation * orientation; + // update the construction sphere_vec.clear(); - sphere_vec.push(engine::sphere(0.5, 0.5, -5.0 + ctrl_x.get(), radius_x.get())); - sphere_vec.push(engine::sphere(-0.5, -0.5, -5.0 + ctrl_y.get(), radius_y.get())); - sphere_vec.push(engine::sphere(-0.5, 0.5, -5.0, 0.75)); + sphere_vec.push(&construction_to_world * engine::sphere(0.5, 0.5, ctrl_x.get(), radius_x.get())); + sphere_vec.push(&construction_to_world * engine::sphere(-0.5, -0.5, ctrl_y.get(), radius_y.get())); + sphere_vec.push(&construction_to_world * engine::sphere(-0.5, 0.5, 0.0, 0.75)); // set the resolution let width = canvas.width() as f32; @@ -285,10 +329,10 @@ fn main() { ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); // clear scene change flag - scene_changed.set(false); + scene_changed.set(turntable_val); } else { frames_since_last_sample = 0; - frame_time.set(-1.0); + mean_frame_interval.set(-1.0); } }); start_animation_loop(); @@ -296,7 +340,7 @@ fn main() { view! { div(id="app") { - div { (frame_time.get()) " ms" } + div { "Mean frame interval: " (mean_frame_interval.get()) " ms" } canvas(ref=display, width="600", height="600") div(class="control") { label(for="ctrl-x") { "Sphere 0 depth" } @@ -362,6 +406,14 @@ fn main() { bind:valueAsNumber=highlight ) } + div(class="control") { + label(for="turntable") { "Turntable" } + input( + type="checkbox", + id="turntable", + bind:checked=turntable + ) + } div(class="control") { label(for="layer-threshold") { "Layer threshold" } input( From c04e29f58672a8113a73a6a7ca29d0a7ba5c35db Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 28 Aug 2024 01:47:38 -0700 Subject: [PATCH 030/126] Reduce the frame interval sample frequency At the higher frequency we were using earlier, the overhead from updating the readout contributed significantly to the frame interval. --- app-proto/inversive-display/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 4da24f5..c0e67a4 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -101,7 +101,7 @@ fn main() { let debug_mode = create_signal(false); /* DEBUG */ /* INSTRUMENTS */ - const SAMPLE_PERIOD: i32 = 20; + const SAMPLE_PERIOD: i32 = 60; let mut last_sample_time = 0.0; let mut frames_since_last_sample = 0; let mean_frame_interval = create_signal(0.0); From 3d7ee98dd64267cb00732079f5a51b45d5e900f2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 28 Aug 2024 01:50:17 -0700 Subject: [PATCH 031/126] Ray-caster: check layer count In debug mode, show the layer count instead of the shaded image. This reveals a bug: testing whether the hit depth is NaN doesn't actually detect sphere misses, because `sphere_cast` returns -1 rather than NaN to indicate a miss. --- app-proto/inversive-display/src/inversive.frag | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index ce6deae..5620210 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -40,7 +40,7 @@ uniform vec2 radius; uniform float opacity; uniform float highlight; uniform int layer_threshold; -uniform bool test_mode; +uniform bool debug_mode; // light and camera const float focal_slope = 0.3; @@ -151,6 +151,19 @@ void main() { } } + /* DEBUG */ + // in debug mode, show the layer count instead of the shaded image + if (debug_mode) { + // at the bottom of the screen, show the color scale instead of the + // layer count + if (gl_FragCoord.y < 10.) frag_cnt = int(8. * gl_FragCoord.x / resolution.x); + + // convert number to color + ivec3 bits = frag_cnt / ivec3(1, 2, 4); + outColor = vec4(mod(vec3(bits), 2.), 1.); + return; + } + // sort the fragments by depth, using an insertion sort for (int take = 1; take < frag_cnt; ++take) { taggedFrag pulled = frags[take]; From e80adf831d03e64d37cb359796297420c15dbfab Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 28 Aug 2024 01:59:46 -0700 Subject: [PATCH 032/126] Ray-caster: correct hit detection --- app-proto/inversive-display/src/inversive.frag | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index 5620210..da2f415 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -141,11 +141,11 @@ void main() { int frag_cnt = 0; for (int i = 0; i < sphere_cnt; ++i) { vec2 hit_depths = sphere_cast(sphere_list[i], dir); - if (!isnan(hit_depths[0])) { + if (hit_depths[0] > 0.) { frags[frag_cnt] = sphere_shading(sphere_list[i], hit_depths[0] * dir, color_list[i], i); ++frag_cnt; } - if (!isnan(hit_depths[1])) { + if (hit_depths[1] > 0.) { frags[frag_cnt] = sphere_shading(sphere_list[i], hit_depths[1] * dir, color_list[i], i); ++frag_cnt; } From 4afc82034b9ea6c6bd2f1ced53425dfc1420df0b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 28 Aug 2024 14:37:29 -0700 Subject: [PATCH 033/126] Ray-caster: sort fragments while shading --- .../inversive-display/src/inversive.frag | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index da2f415..c469edc 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -138,16 +138,34 @@ void main() { const int SPHERE_MAX_INTERNAL = 6; vec2 depth_pairs [SPHERE_MAX_INTERNAL]; taggedFrag frags [2*SPHERE_MAX_INTERNAL]; - int frag_cnt = 0; - for (int i = 0; i < sphere_cnt; ++i) { - vec2 hit_depths = sphere_cast(sphere_list[i], dir); - if (hit_depths[0] > 0.) { - frags[frag_cnt] = sphere_shading(sphere_list[i], hit_depths[0] * dir, color_list[i], i); - ++frag_cnt; - } - if (hit_depths[1] > 0.) { - frags[frag_cnt] = sphere_shading(sphere_list[i], hit_depths[1] * dir, color_list[i], i); - ++frag_cnt; + int layer_cnt = 0; + for (int id = 0; id < sphere_cnt; ++id) { + // find out where the ray hits the sphere + vec2 hit_depths = sphere_cast(sphere_list[id], dir); + + // insertion-sort the fragments we hit into the fragment list + for (int side = 0; side < 2; ++side) { + if (hit_depths[side] > 0.) { + for (int layer = layer_cnt; layer >= 0; --layer) { + if (layer < 1 || frags[layer-1].pt.z >= -hit_depths[side]) { + // we're not as close to the screen as the fragment + // before the empty slot, so insert here + frags[layer] = sphere_shading( + sphere_list[id], + hit_depths[side] * dir, + color_list[id], + id + ); + break; + } else { + // we're closer to the screen than the fragment before + // the empty slot, so move that fragment into the empty + // slot + frags[layer] = frags[layer-1]; + } + } + ++layer_cnt; + } } } @@ -156,29 +174,16 @@ void main() { if (debug_mode) { // at the bottom of the screen, show the color scale instead of the // layer count - if (gl_FragCoord.y < 10.) frag_cnt = int(8. * gl_FragCoord.x / resolution.x); + if (gl_FragCoord.y < 10.) layer_cnt = int(8. * gl_FragCoord.x / resolution.x); // convert number to color - ivec3 bits = frag_cnt / ivec3(1, 2, 4); + ivec3 bits = layer_cnt / ivec3(1, 2, 4); outColor = vec4(mod(vec3(bits), 2.), 1.); return; } - // sort the fragments by depth, using an insertion sort - for (int take = 1; take < frag_cnt; ++take) { - taggedFrag pulled = frags[take]; - for (int put = take; put >= 0; --put) { - if (put < 1 || frags[put-1].pt.z >= pulled.pt.z) { - frags[put] = pulled; - break; - } else { - frags[put] = frags[put-1]; - } - } - } - // highlight intersections and cusps - for (int i = frag_cnt-1; i >= 1; --i) { + for (int i = layer_cnt-1; i >= 1; --i) { // intersections taggedFrag frag0 = frags[i]; taggedFrag frag1 = frags[i-1]; @@ -201,7 +206,7 @@ void main() { // composite the sphere fragments vec3 color = vec3(0.); - for (int i = frag_cnt-1; i >= layer_threshold; --i) { + for (int i = layer_cnt-1; i >= layer_threshold; --i) { if (frags[i].pt.z < 0.) { vec4 frag_color = frags[i].color; color = mix(color, frag_color.rgb, frag_color.a); From 8fde202911c1aa17aab72439ec4a965575ba9bd4 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 28 Aug 2024 15:52:19 -0700 Subject: [PATCH 034/126] Ray-caster: drop unused depth-pair array This doesn't affect GPU performance noticeably, so benchmarks before and after the change should be comparable. --- app-proto/inversive-display/src/inversive.frag | 1 - 1 file changed, 1 deletion(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index c469edc..213fbe2 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -136,7 +136,6 @@ void main() { // cast rays through the spheres const int SPHERE_MAX_INTERNAL = 6; - vec2 depth_pairs [SPHERE_MAX_INTERNAL]; taggedFrag frags [2*SPHERE_MAX_INTERNAL]; int layer_cnt = 0; for (int id = 0; id < sphere_cnt; ++id) { From 3a721a4cc89bc5a8e41a075f4433c763dab1ede7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 28 Aug 2024 16:06:33 -0700 Subject: [PATCH 035/126] Ray-caster: enlarge the construction data uniforms No noticeable effect on GPU performance. --- app-proto/inversive-display/src/inversive.frag | 2 +- app-proto/inversive-display/src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index 213fbe2..fbb60f6 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -25,7 +25,7 @@ vecInv sphere(vec3 center, float radius) { // construction. the SPHERE_MAX array size seems to affect frame rate a lot, // even though we should only be using the first few elements of each array -const int SPHERE_MAX_UNIFORM = 12; +const int SPHERE_MAX_UNIFORM = 200; uniform int sphere_cnt; uniform vecInv sphere_list[SPHERE_MAX_UNIFORM]; uniform vec3 color_list[SPHERE_MAX_UNIFORM]; diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index c0e67a4..1855ce2 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -127,7 +127,7 @@ fn main() { on_mount(move || { // list construction elements - const SPHERE_MAX: usize = 12; + const SPHERE_MAX: usize = 200; let mut sphere_vec = Vec::>::new(); let color_vec = vec![ [1.00_f32, 0.25_f32, 0.00_f32], From 6db9f5be6c86af64226d21f220bb7795405c5196 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 28 Aug 2024 17:14:55 -0700 Subject: [PATCH 036/126] Enlarge the test construction to six spheres In debug mode, assign most of the color scale to even layer counts. Odd layer counts are topologically prohibited, so we should rarely see bugs severe enough to produce them. --- app-proto/inversive-display/src/inversive.frag | 12 +++++++++--- app-proto/inversive-display/src/main.rs | 8 +++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index fbb60f6..7eb16b5 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -173,11 +173,17 @@ void main() { if (debug_mode) { // at the bottom of the screen, show the color scale instead of the // layer count - if (gl_FragCoord.y < 10.) layer_cnt = int(8. * gl_FragCoord.x / resolution.x); + if (gl_FragCoord.y < 10.) layer_cnt = 2 * int(8. * gl_FragCoord.x / resolution.x); // convert number to color - ivec3 bits = layer_cnt / ivec3(1, 2, 4); - outColor = vec4(mod(vec3(bits), 2.), 1.); + vec3 color; + if (layer_cnt % 2 == 0) { + ivec3 bits = layer_cnt / ivec3(2, 4, 8); + color = mod(vec3(bits), 2.); + } else { + color = vec3(0.5); + } + outColor = vec4(color, 1.); return; } diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 1855ce2..f5b42c5 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -132,7 +132,10 @@ fn main() { let color_vec = vec![ [1.00_f32, 0.25_f32, 0.00_f32], [0.00_f32, 0.25_f32, 1.00_f32], - [0.25_f32, 0.00_f32, 1.00_f32] + [0.25_f32, 0.00_f32, 1.00_f32], + [0.25_f32, 1.00_f32, 0.00_f32], + [0.75_f32, 0.75_f32, 0.00_f32], + [0.00_f32, 0.75_f32, 0.50_f32], ]; // timing @@ -292,6 +295,9 @@ fn main() { sphere_vec.push(&construction_to_world * engine::sphere(0.5, 0.5, ctrl_x.get(), radius_x.get())); sphere_vec.push(&construction_to_world * engine::sphere(-0.5, -0.5, ctrl_y.get(), radius_y.get())); sphere_vec.push(&construction_to_world * engine::sphere(-0.5, 0.5, 0.0, 0.75)); + sphere_vec.push(&construction_to_world * engine::sphere(0.5, -0.5, 0.0, 0.5)); + sphere_vec.push(&construction_to_world * engine::sphere(0.0, 0.15, 1.0, 0.25)); + sphere_vec.push(&construction_to_world * engine::sphere(0.0, -0.15, -1.0, 0.25)); // set the resolution let width = canvas.width() as f32; From f14855296485250277a7216c711eb598148d8346 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 29 Aug 2024 15:01:38 -0700 Subject: [PATCH 037/126] Ray-caster: only draw the top few fragments This seems to increase graphics pipe use from 50--60% to 55--65%. --- .../inversive-display/src/inversive.frag | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index 7eb16b5..2fcd86e 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -25,10 +25,10 @@ vecInv sphere(vec3 center, float radius) { // construction. the SPHERE_MAX array size seems to affect frame rate a lot, // even though we should only be using the first few elements of each array -const int SPHERE_MAX_UNIFORM = 200; +const int SPHERE_MAX = 200; uniform int sphere_cnt; -uniform vecInv sphere_list[SPHERE_MAX_UNIFORM]; -uniform vec3 color_list[SPHERE_MAX_UNIFORM]; +uniform vecInv sphere_list[SPHERE_MAX]; +uniform vec3 color_list[SPHERE_MAX]; // view uniform vec2 resolution; @@ -135,8 +135,8 @@ void main() { vec3 dir = vec3(focal_slope * scr, -1.); // cast rays through the spheres - const int SPHERE_MAX_INTERNAL = 6; - taggedFrag frags [2*SPHERE_MAX_INTERNAL]; + const int LAYER_MAX = 12; + taggedFrag frags [LAYER_MAX]; int layer_cnt = 0; for (int id = 0; id < sphere_cnt; ++id) { // find out where the ray hits the sphere @@ -144,17 +144,20 @@ void main() { // insertion-sort the fragments we hit into the fragment list for (int side = 0; side < 2; ++side) { - if (hit_depths[side] > 0.) { + float hit_z = -hit_depths[side]; + if (0. > hit_z) { for (int layer = layer_cnt; layer >= 0; --layer) { - if (layer < 1 || frags[layer-1].pt.z >= -hit_depths[side]) { + if (layer < 1 || frags[layer-1].pt.z >= hit_z) { // we're not as close to the screen as the fragment // before the empty slot, so insert here - frags[layer] = sphere_shading( - sphere_list[id], - hit_depths[side] * dir, - color_list[id], - id - ); + if (layer < LAYER_MAX) { + frags[layer] = sphere_shading( + sphere_list[id], + hit_depths[side] * dir, + color_list[id], + id + ); + } break; } else { // we're closer to the screen than the fragment before @@ -163,7 +166,7 @@ void main() { frags[layer] = frags[layer-1]; } } - ++layer_cnt; + layer_cnt = min(layer_cnt + 1, LAYER_MAX); } } } From a4236a34dfc0f386182c7ff2d6729613ed73336f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 2 Sep 2024 15:15:18 -0700 Subject: [PATCH 038/126] Ray-caster: dim the interior layers of spheres This seems to weaken the intersection disk illusion. --- app-proto/inversive-display/src/inversive.frag | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index 2fcd86e..9913b40 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -46,6 +46,7 @@ uniform bool debug_mode; const float focal_slope = 0.3; const vec3 light_dir = normalize(vec3(2., 2., 1.)); const float ixn_threshold = 0.005; +const float INTERIOR_DIMMING = 0.7; // --- sRGB --- @@ -143,6 +144,7 @@ void main() { vec2 hit_depths = sphere_cast(sphere_list[id], dir); // insertion-sort the fragments we hit into the fragment list + float dimming = 1.; for (int side = 0; side < 2; ++side) { float hit_z = -hit_depths[side]; if (0. > hit_z) { @@ -154,7 +156,7 @@ void main() { frags[layer] = sphere_shading( sphere_list[id], hit_depths[side] * dir, - color_list[id], + dimming * color_list[id], id ); } @@ -167,6 +169,7 @@ void main() { } } layer_cnt = min(layer_cnt + 1, LAYER_MAX); + dimming = INTERIOR_DIMMING; } } } From 121934c4c3543768bca01e6b96522d59e10775a1 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 4 Sep 2024 12:58:55 -0700 Subject: [PATCH 039/126] Encapsulate construction --- app-proto/inversive-display/src/main.rs | 28 +++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index f5b42c5..2042c08 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -83,6 +83,22 @@ fn bind_vertex_attrib( ); } +fn push_gen_test_construction( + sphere_vec: &mut Vec>, + construction_to_world: &DMatrix, + ctrl_x: f64, + ctrl_y: f64, + radius_x: f64, + radius_y: f64 +) { + sphere_vec.push(construction_to_world * engine::sphere(0.5, 0.5, ctrl_x, radius_x)); + sphere_vec.push(construction_to_world * engine::sphere(-0.5, -0.5, ctrl_y, radius_y)); + sphere_vec.push(construction_to_world * engine::sphere(-0.5, 0.5, 0.0, 0.75)); + sphere_vec.push(construction_to_world * engine::sphere(0.5, -0.5, 0.0, 0.5)); + sphere_vec.push(construction_to_world * engine::sphere(0.0, 0.15, 1.0, 0.25)); + sphere_vec.push(construction_to_world * engine::sphere(0.0, -0.15, -1.0, 0.25)); +} + fn main() { // set up a config option that forwards panic messages to `console.error` #[cfg(feature = "console_error_panic_hook")] @@ -292,12 +308,12 @@ fn main() { // update the construction sphere_vec.clear(); - sphere_vec.push(&construction_to_world * engine::sphere(0.5, 0.5, ctrl_x.get(), radius_x.get())); - sphere_vec.push(&construction_to_world * engine::sphere(-0.5, -0.5, ctrl_y.get(), radius_y.get())); - sphere_vec.push(&construction_to_world * engine::sphere(-0.5, 0.5, 0.0, 0.75)); - sphere_vec.push(&construction_to_world * engine::sphere(0.5, -0.5, 0.0, 0.5)); - sphere_vec.push(&construction_to_world * engine::sphere(0.0, 0.15, 1.0, 0.25)); - sphere_vec.push(&construction_to_world * engine::sphere(0.0, -0.15, -1.0, 0.25)); + push_gen_test_construction( + &mut sphere_vec, + &construction_to_world, + ctrl_x.get(), ctrl_y.get(), + radius_x.get(), radius_y.get() + ); // set the resolution let width = canvas.width() as f32; From 3493a798d10c1a7771362e0a49830a4c15f18e90 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 4 Sep 2024 16:27:28 -0700 Subject: [PATCH 040/126] Add low-curvature construction Also add infrastructure for switching between constructions. --- app-proto/inversive-display/main.css | 31 +++- app-proto/inversive-display/src/main.rs | 210 ++++++++++++++++++------ 2 files changed, 189 insertions(+), 52 deletions(-) diff --git a/app-proto/inversive-display/main.css b/app-proto/inversive-display/main.css index 7cdc831..7e8c710 100644 --- a/app-proto/inversive-display/main.css +++ b/app-proto/inversive-display/main.css @@ -18,14 +18,41 @@ canvas { margin-top: 5px; } -.control { +.hidden { + display: none; +} + +.control, .tab-pane { display: flex; flex-direction: row; width: 600px; } +input[type="radio"] { + display: none; +} + +.tab-pane > label { + border: 1px solid #aaa; + border-radius: 5px; + text-align: center; + padding: 5px; + margin-right: 10px; + margin-bottom: 5px; +} + +.tab-pane > label:has(:checked) { + border-color: #fcfcfc; + background-color: #555; +} + +.tab-pane > label:hover:not(:has(:checked)) { + border-color: #bbb; + background-color: #333; +} + label { - width: 150px; + width: 170px; } input { diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 2042c08..1fbef8d 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -10,6 +10,7 @@ extern crate js_sys; use core::array; use nalgebra::{DMatrix, DVector}; +use std::f64::consts::FRAC_1_SQRT_2; use sycamore::{prelude::*, motion::create_raf, rt::{JsCast, JsValue}}; use web_sys::{console, window, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation}; @@ -83,7 +84,7 @@ fn bind_vertex_attrib( ); } -fn push_gen_test_construction( +fn push_gen_construction( sphere_vec: &mut Vec>, construction_to_world: &DMatrix, ctrl_x: f64, @@ -99,17 +100,46 @@ fn push_gen_test_construction( sphere_vec.push(construction_to_world * engine::sphere(0.0, -0.15, -1.0, 0.25)); } +fn push_low_curv_construction( + sphere_vec: &mut Vec>, + construction_to_world: &DMatrix, + curv_x: f64, + curv_y: f64 +) { + sphere_vec.push(construction_to_world * DVector::from_column_slice(&[0.0, -1.0, 0.0, 0.5*curv_x, 0.0])); + sphere_vec.push(construction_to_world * DVector::from_column_slice(&[-FRAC_1_SQRT_2, 0.0, -FRAC_1_SQRT_2, 0.5*curv_y, 0.0])); + sphere_vec.push(construction_to_world * engine::sphere(0.5, 0.0, 0.5, FRAC_1_SQRT_2)); + sphere_vec.push(construction_to_world * engine::sphere(-0.5, 0.0, -0.5, FRAC_1_SQRT_2)); +} + +#[derive(Clone, Copy, PartialEq)] +enum Tab { + GenTab, + LowCurvTab +} + fn main() { // set up a config option that forwards panic messages to `console.error` #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); sycamore::render(|| { - // controls + // tab selection + let tab_selection = create_signal(Tab::GenTab); + + // controls for general example + let gen_controls = create_node_ref(); let ctrl_x = create_signal(0.0); let ctrl_y = create_signal(0.0); let radius_x = create_signal(1.0); let radius_y = create_signal(1.0); + + // controls for low-curvature example + let low_curv_controls = create_node_ref(); + let curv_x = create_signal(0.0); + let curv_y = create_signal(0.0); + + // shared controls let opacity = create_signal(0.5); let highlight = create_signal(0.2); let turntable = create_signal(false); @@ -128,10 +158,20 @@ fn main() { // change listener let scene_changed = create_signal(true); create_effect(move || { + // track tab selection + tab_selection.track(); + + // track controls for general example ctrl_x.track(); ctrl_y.track(); radius_x.track(); radius_y.track(); + + // track controls for low-curvature example + curv_x.track(); + curv_y.track(); + + // track shared controls opacity.track(); highlight.track(); turntable.track(); @@ -142,6 +182,23 @@ fn main() { }); on_mount(move || { + // tab listener + create_effect(move || { + // get the control panel nodes + let gen_controls_node = gen_controls.get::(); + let low_curv_controls_node = low_curv_controls.get::(); + + // hide all the control panels + gen_controls_node.add_class("hidden"); + low_curv_controls_node.add_class("hidden"); + + // show the selected control panel + match tab_selection.get() { + Tab::GenTab => gen_controls_node.remove_class("hidden"), + Tab::LowCurvTab => low_curv_controls_node.remove_class("hidden") + } + }); + // list construction elements const SPHERE_MAX: usize = 200; let mut sphere_vec = Vec::>::new(); @@ -308,12 +365,19 @@ fn main() { // update the construction sphere_vec.clear(); - push_gen_test_construction( - &mut sphere_vec, - &construction_to_world, - ctrl_x.get(), ctrl_y.get(), - radius_x.get(), radius_y.get() - ); + match tab_selection.get() { + Tab::GenTab => push_gen_construction( + &mut sphere_vec, + &construction_to_world, + ctrl_x.get(), ctrl_y.get(), + radius_x.get(), radius_y.get() + ), + Tab::LowCurvTab => push_low_curv_construction( + &mut sphere_vec, + &construction_to_world, + curv_x.get(), curv_y.get() + ) + }; // set the resolution let width = canvas.width() as f32; @@ -362,51 +426,97 @@ fn main() { view! { div(id="app") { + div(class="tab-pane") { + label { + "General" + input( + type="radio", + name="tab", + prop:checked=tab_selection.get() == Tab::GenTab, + on:click=move |_| tab_selection.set(Tab::GenTab) + ) + } + label { + "Low curvature" + input( + type="radio", + name="tab", + prop:checked=tab_selection.get() == Tab::LowCurvTab, + on:change=move |_| tab_selection.set(Tab::LowCurvTab) + ) + } + } div { "Mean frame interval: " (mean_frame_interval.get()) " ms" } canvas(ref=display, width="600", height="600") - div(class="control") { - label(for="ctrl-x") { "Sphere 0 depth" } - input( - type="range", - id="ctrl-x", - min=-1.0, - max=1.0, - step=0.001, - bind:valueAsNumber=ctrl_x - ) + div(ref=gen_controls) { + div(class="control") { + label(for="ctrl-x") { "Sphere 0 depth" } + input( + type="range", + id="ctrl-x", + min=-1.0, + max=1.0, + step=0.001, + bind:valueAsNumber=ctrl_x + ) + } + div(class="control") { + label(for="ctrl-y") { "Sphere 1 depth" } + input( + type="range", + id="ctrl-y", + min=-1.0, + max=1.0, + step=0.001, + bind:valueAsNumber=ctrl_y + ) + } + div(class="control") { + label(for="radius-x") { "Sphere 0 radius" } + input( + type="range", + id="radius-x", + min=0.5, + max=1.5, + step=0.001, + bind:valueAsNumber=radius_x + ) + } + div(class="control") { + label(for="radius-y") { "Sphere 1 radius" } + input( + type="range", + id="radius-y", + min=0.5, + max=1.5, + step=0.001, + bind:valueAsNumber=radius_y + ) + } } - div(class="control") { - label(for="ctrl-y") { "Sphere 1 depth" } - input( - type="range", - id="ctrl-y", - min=-1.0, - max=1.0, - step=0.001, - bind:valueAsNumber=ctrl_y - ) - } - div(class="control") { - label(for="radius-x") { "Sphere 0 radius" } - input( - type="range", - id="radius-x", - min=0.5, - max=1.5, - step=0.001, - bind:valueAsNumber=radius_x - ) - } - div(class="control") { - label(for="radius-y") { "Sphere 1 radius" } - input( - type="range", - id="radius-y", - min=0.5, - max=1.5, - step=0.001, - bind:valueAsNumber=radius_y - ) + div(ref=low_curv_controls) { + div(class="control") { + label(for="curv-x") { "Sphere 0 curvature" } + input( + type="range", + id="curv-x", + min=0.0, + max=2.0, + step=0.001, + bind:valueAsNumber=curv_x + ) + } + div(class="control") { + label(for="curv-y") { "Sphere 1 curvature" } + input( + type="range", + id="curv-y", + min=0.0, + max=2.0, + step=0.001, + bind:valueAsNumber=curv_y + ) + } } div(class="control") { label(for="opacity") { "Opacity" } From ab830b194e656522a6d48707de951346a3367cc8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 6 Sep 2024 18:57:59 -0700 Subject: [PATCH 041/126] Use circles in triangle as low-curvature construction In the process, add a way to build a sphere by offset and curvature. --- app-proto/inversive-display/src/engine.rs | 15 +++ app-proto/inversive-display/src/main.rs | 135 +++++++++++++++++----- 2 files changed, 121 insertions(+), 29 deletions(-) diff --git a/app-proto/inversive-display/src/engine.rs b/app-proto/inversive-display/src/engine.rs index 7fbcd03..79668bb 100644 --- a/app-proto/inversive-display/src/engine.rs +++ b/app-proto/inversive-display/src/engine.rs @@ -1,5 +1,6 @@ use nalgebra::DVector; +// the sphere with the given center and radius, with inward-pointing normals pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVector { let center_norm_sq = center_x * center_x + center_y * center_y + center_z * center_z; DVector::from_column_slice(&[ @@ -9,4 +10,18 @@ pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVect 0.5 / radius, 0.5 * (center_norm_sq / radius - radius) ]) +} + +// the sphere of curvature `curv` whose closest point to the origin has position +// `off * dir` and normal `dir`, where `dir` is a unit vector. setting the +// curvature to zero gives a plane +pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f64) -> DVector { + let norm_sp = 1.0 + off * curv; + DVector::from_column_slice(&[ + norm_sp * dir_x, + norm_sp * dir_y, + norm_sp * dir_z, + 0.5 * curv, + off * (1.0 + 0.5 * off * curv) + ]) } \ No newline at end of file diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 1fbef8d..8afe8ef 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -10,7 +10,6 @@ extern crate js_sys; use core::array; use nalgebra::{DMatrix, DVector}; -use std::f64::consts::FRAC_1_SQRT_2; use sycamore::{prelude::*, motion::create_raf, rt::{JsCast, JsValue}}; use web_sys::{console, window, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation}; @@ -86,30 +85,59 @@ fn bind_vertex_attrib( fn push_gen_construction( sphere_vec: &mut Vec>, + color_vec: &mut Vec<[f32; 3]>, construction_to_world: &DMatrix, ctrl_x: f64, ctrl_y: f64, radius_x: f64, radius_y: f64 ) { + // push spheres sphere_vec.push(construction_to_world * engine::sphere(0.5, 0.5, ctrl_x, radius_x)); sphere_vec.push(construction_to_world * engine::sphere(-0.5, -0.5, ctrl_y, radius_y)); sphere_vec.push(construction_to_world * engine::sphere(-0.5, 0.5, 0.0, 0.75)); sphere_vec.push(construction_to_world * engine::sphere(0.5, -0.5, 0.0, 0.5)); sphere_vec.push(construction_to_world * engine::sphere(0.0, 0.15, 1.0, 0.25)); sphere_vec.push(construction_to_world * engine::sphere(0.0, -0.15, -1.0, 0.25)); + + // push colors + color_vec.push([1.00_f32, 0.25_f32, 0.00_f32]); + color_vec.push([0.00_f32, 0.25_f32, 1.00_f32]); + color_vec.push([0.25_f32, 0.00_f32, 1.00_f32]); + color_vec.push([0.25_f32, 1.00_f32, 0.00_f32]); + color_vec.push([0.75_f32, 0.75_f32, 0.00_f32]); + color_vec.push([0.00_f32, 0.75_f32, 0.50_f32]); } fn push_low_curv_construction( sphere_vec: &mut Vec>, + color_vec: &mut Vec<[f32; 3]>, construction_to_world: &DMatrix, - curv_x: f64, - curv_y: f64 + off1: f64, + off2: f64, + off3: f64, + curv1: f64, + curv2: f64, + curv3: f64, ) { - sphere_vec.push(construction_to_world * DVector::from_column_slice(&[0.0, -1.0, 0.0, 0.5*curv_x, 0.0])); - sphere_vec.push(construction_to_world * DVector::from_column_slice(&[-FRAC_1_SQRT_2, 0.0, -FRAC_1_SQRT_2, 0.5*curv_y, 0.0])); - sphere_vec.push(construction_to_world * engine::sphere(0.5, 0.0, 0.5, FRAC_1_SQRT_2)); - sphere_vec.push(construction_to_world * engine::sphere(-0.5, 0.0, -0.5, FRAC_1_SQRT_2)); + // push spheres + let a = 0.75_f64.sqrt(); + sphere_vec.push(construction_to_world * engine::sphere(0.0, 0.0, 0.0, 1.0)); + sphere_vec.push(construction_to_world * engine::sphere_with_offset(1.0, 0.0, 0.0, off1, curv1)); + sphere_vec.push(construction_to_world * engine::sphere_with_offset(-0.5, a, 0.0, off2, curv2)); + sphere_vec.push(construction_to_world * engine::sphere_with_offset(-0.5, -a, 0.0, off3, curv3)); + sphere_vec.push(construction_to_world * engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0)); + sphere_vec.push(construction_to_world * engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0)); + sphere_vec.push(construction_to_world * engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0)); + + // push colors + color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); + color_vec.push([1.00_f32, 0.00_f32, 0.25_f32]); + color_vec.push([0.25_f32, 1.00_f32, 0.00_f32]); + color_vec.push([0.00_f32, 0.25_f32, 1.00_f32]); + color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); + color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); + color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); } #[derive(Clone, Copy, PartialEq)] @@ -136,8 +164,12 @@ fn main() { // controls for low-curvature example let low_curv_controls = create_node_ref(); - let curv_x = create_signal(0.0); - let curv_y = create_signal(0.0); + let curv1 = create_signal(0.0); + let curv2 = create_signal(0.0); + let curv3 = create_signal(0.0); + let off1 = create_signal(1.0); + let off2 = create_signal(1.0); + let off3 = create_signal(1.0); // shared controls let opacity = create_signal(0.5); @@ -168,8 +200,12 @@ fn main() { radius_y.track(); // track controls for low-curvature example - curv_x.track(); - curv_y.track(); + curv1.track(); + curv2.track(); + curv3.track(); + off1.track(); + off2.track(); + off3.track(); // track shared controls opacity.track(); @@ -199,17 +235,10 @@ fn main() { } }); - // list construction elements + // create list of construction elements const SPHERE_MAX: usize = 200; let mut sphere_vec = Vec::>::new(); - let color_vec = vec![ - [1.00_f32, 0.25_f32, 0.00_f32], - [0.00_f32, 0.25_f32, 1.00_f32], - [0.25_f32, 0.00_f32, 1.00_f32], - [0.25_f32, 1.00_f32, 0.00_f32], - [0.75_f32, 0.75_f32, 0.00_f32], - [0.00_f32, 0.75_f32, 0.50_f32], - ]; + let mut color_vec = Vec::<[f32; 3]>::new(); // timing let mut last_time = 0.0; @@ -365,17 +394,21 @@ fn main() { // update the construction sphere_vec.clear(); + color_vec.clear(); match tab_selection.get() { Tab::GenTab => push_gen_construction( &mut sphere_vec, + &mut color_vec, &construction_to_world, ctrl_x.get(), ctrl_y.get(), radius_x.get(), radius_y.get() ), Tab::LowCurvTab => push_low_curv_construction( &mut sphere_vec, + &mut color_vec, &construction_to_world, - curv_x.get(), curv_y.get() + off1.get(), off2.get(), off3.get(), + curv1.get(), curv2.get(), curv3.get(), ) }; @@ -496,25 +529,69 @@ fn main() { } div(ref=low_curv_controls) { div(class="control") { - label(for="curv-x") { "Sphere 0 curvature" } + label(for="off-1") { "Sphere 1 offset" } input( type="range", - id="curv-x", - min=0.0, - max=2.0, + id="off-1", + min=-1.0, + max=1.0, step=0.001, - bind:valueAsNumber=curv_x + bind:valueAsNumber=off1 ) } div(class="control") { - label(for="curv-y") { "Sphere 1 curvature" } + label(for="off-2") { "Sphere 2 offset" } input( type="range", - id="curv-y", + id="off-2", + min=-1.0, + max=1.0, + step=0.001, + bind:valueAsNumber=off2 + ) + } + div(class="control") { + label(for="off-3") { "Sphere 3 offset" } + input( + type="range", + id="off-3", + min=-1.0, + max=1.0, + step=0.001, + bind:valueAsNumber=off3 + ) + } + div(class="control") { + label(for="curv-1") { "Sphere 1 curvature" } + input( + type="range", + id="curv-1", min=0.0, max=2.0, step=0.001, - bind:valueAsNumber=curv_y + bind:valueAsNumber=curv1 + ) + } + div(class="control") { + label(for="curv-2") { "Sphere 2 curvature" } + input( + type="range", + id="curv-2", + min=0.0, + max=2.0, + step=0.001, + bind:valueAsNumber=curv2 + ) + } + div(class="control") { + label(for="curv-3") { "Sphere 3 curvature" } + input( + type="range", + id="curv-3", + min=0.0, + max=2.0, + step=0.001, + bind:valueAsNumber=curv3 ) } } From 163361184b4e621bb3f69a87571eaac18092492c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 8 Sep 2024 23:00:28 -0700 Subject: [PATCH 042/126] Ray-caster: avoid roundoff error in quadratic equation --- .../inversive-display/src/inversive.frag | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index 9913b40..c6fed13 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -110,23 +110,37 @@ taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, int id) { // --- ray-casting --- +// if `a/b` is less than this threshold, we approximate `a*u^2 + b*u + c` by +// the linear function `b*u + c` +const float DEG_THRESHOLD = 1e-9; + +// the depths, represented as multiples of `dir`, where the line generated by +// `dir` hits the sphere represented by `v`. if both depths are positive, the +// smaller one is returned in the first component. if only one depth is +// positive, it could be returned in either component vec2 sphere_cast(vecInv v, vec3 dir) { float a = -v.lt.s * dot(dir, dir); float b = dot(v.sp, dir); float c = -v.lt.t; - float scale = -b/(2.*a); float adjust = 4.*a*c/(b*b); - if (adjust < 1.) { - float offset = sqrt(1. - adjust); - return vec2( - scale * (1. - offset), - scale * (1. + offset) - ); + // 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` + float square_rect_ratio = 1. + sqrt(1. - adjust); + float lin_root = -(2.*c)/b / square_rect_ratio; + if (abs(a) > DEG_THRESHOLD * abs(b)) { + return vec2(lin_root, -b/(2.*a) * square_rect_ratio); + } else { + return vec2(lin_root, -1.); + } } else { - // these parameters describe points behind the camera, so the - // corresponding fragments won't be drawn + // the line through `dir` misses the sphere completely return vec2(-1., -1.); } } From b289d2d4c313f180c31ed34babc7aa8b728212d3 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 8 Sep 2024 23:31:48 -0700 Subject: [PATCH 043/126] Distinguish odd layer counts in debug mode The low-curvature construction admits odd layer counts. --- app-proto/inversive-display/src/inversive.frag | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index c6fed13..2e185a5 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -193,15 +193,13 @@ void main() { if (debug_mode) { // at the bottom of the screen, show the color scale instead of the // layer count - if (gl_FragCoord.y < 10.) layer_cnt = 2 * int(8. * gl_FragCoord.x / resolution.x); + if (gl_FragCoord.y < 10.) layer_cnt = int(16. * gl_FragCoord.x / resolution.x); // convert number to color - vec3 color; - if (layer_cnt % 2 == 0) { - ivec3 bits = layer_cnt / ivec3(2, 4, 8); - color = mod(vec3(bits), 2.); - } else { - color = vec3(0.5); + ivec3 bits = layer_cnt / ivec3(1, 2, 4); + vec3 color = mod(vec3(bits), 2.); + if (layer_cnt % 16 >= 8) { + color = mix(color, vec3(0.5), 0.5); } outColor = vec4(color, 1.); return; From 0173b63e19e908b6fcdc01a48edd192d9e5b65e6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 8 Sep 2024 23:43:26 -0700 Subject: [PATCH 044/126] Add picture plane to circles in triangle --- app-proto/inversive-display/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 8afe8ef..c3b4a59 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -123,6 +123,7 @@ fn push_low_curv_construction( // push spheres let a = 0.75_f64.sqrt(); sphere_vec.push(construction_to_world * engine::sphere(0.0, 0.0, 0.0, 1.0)); + sphere_vec.push(construction_to_world * engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0)); sphere_vec.push(construction_to_world * engine::sphere_with_offset(1.0, 0.0, 0.0, off1, curv1)); sphere_vec.push(construction_to_world * engine::sphere_with_offset(-0.5, a, 0.0, off2, curv2)); sphere_vec.push(construction_to_world * engine::sphere_with_offset(-0.5, -a, 0.0, off3, curv3)); @@ -132,6 +133,7 @@ fn push_low_curv_construction( // push colors color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); + color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); color_vec.push([1.00_f32, 0.00_f32, 0.25_f32]); color_vec.push([0.25_f32, 1.00_f32, 0.00_f32]); color_vec.push([0.00_f32, 0.25_f32, 1.00_f32]); From 69ab888d5bc643bae89f361be75455da60b30778 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Sep 2024 00:32:29 -0700 Subject: [PATCH 045/126] Simplify control labeling --- app-proto/inversive-display/main.css | 2 +- app-proto/inversive-display/src/main.rs | 75 ++++++++++--------------- 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/app-proto/inversive-display/main.css b/app-proto/inversive-display/main.css index 7e8c710..a16d04a 100644 --- a/app-proto/inversive-display/main.css +++ b/app-proto/inversive-display/main.css @@ -51,7 +51,7 @@ input[type="radio"] { background-color: #333; } -label { +.control > span { width: 170px; } diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index c3b4a59..6e6a0e8 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -484,44 +484,40 @@ fn main() { div { "Mean frame interval: " (mean_frame_interval.get()) " ms" } canvas(ref=display, width="600", height="600") div(ref=gen_controls) { - div(class="control") { - label(for="ctrl-x") { "Sphere 0 depth" } + label(class="control") { + span { "Sphere 0 depth" } input( type="range", - id="ctrl-x", min=-1.0, max=1.0, step=0.001, bind:valueAsNumber=ctrl_x ) } - div(class="control") { - label(for="ctrl-y") { "Sphere 1 depth" } + label(class="control") { + span { "Sphere 1 depth" } input( type="range", - id="ctrl-y", min=-1.0, max=1.0, step=0.001, bind:valueAsNumber=ctrl_y ) } - div(class="control") { - label(for="radius-x") { "Sphere 0 radius" } + label(class="control") { + span { "Sphere 0 radius" } input( type="range", - id="radius-x", min=0.5, max=1.5, step=0.001, bind:valueAsNumber=radius_x ) } - div(class="control") { - label(for="radius-y") { "Sphere 1 radius" } + label(class="control") { + span { "Sphere 1 radius" } input( type="range", - id="radius-y", min=0.5, max=1.5, step=0.001, @@ -530,66 +526,60 @@ fn main() { } } div(ref=low_curv_controls) { - div(class="control") { - label(for="off-1") { "Sphere 1 offset" } + label(class="control") { + span { "Sphere 1 offset" } input( type="range", - id="off-1", min=-1.0, max=1.0, step=0.001, bind:valueAsNumber=off1 ) } - div(class="control") { - label(for="off-2") { "Sphere 2 offset" } + label(class="control") { + span { "Sphere 2 offset" } input( type="range", - id="off-2", min=-1.0, max=1.0, step=0.001, bind:valueAsNumber=off2 ) } - div(class="control") { - label(for="off-3") { "Sphere 3 offset" } + label(class="control") { + span { "Sphere 3 offset" } input( type="range", - id="off-3", min=-1.0, max=1.0, step=0.001, bind:valueAsNumber=off3 ) } - div(class="control") { - label(for="curv-1") { "Sphere 1 curvature" } + label(class="control") { + span { "Sphere 1 curvature" } input( type="range", - id="curv-1", min=0.0, max=2.0, step=0.001, bind:valueAsNumber=curv1 ) } - div(class="control") { - label(for="curv-2") { "Sphere 2 curvature" } + label(class="control") { + span { "Sphere 2 curvature" } input( type="range", - id="curv-2", min=0.0, max=2.0, step=0.001, bind:valueAsNumber=curv2 ) } - div(class="control") { - label(for="curv-3") { "Sphere 3 curvature" } + label(class="control") { + span { "Sphere 3 curvature" } input( type="range", - id="curv-3", min=0.0, max=2.0, step=0.001, @@ -597,49 +587,44 @@ fn main() { ) } } - div(class="control") { - label(for="opacity") { "Opacity" } + label(class="control") { + span { "Opacity" } input( type="range", - id="opacity", max=1.0, step=0.001, bind:valueAsNumber=opacity ) } - div(class="control") { - label(for="highlight") { "Highlight" } + label(class="control") { + span { "Highlight" } input( type="range", - id="highlight", max=1.0, step=0.001, bind:valueAsNumber=highlight ) } - div(class="control") { - label(for="turntable") { "Turntable" } + label(class="control") { + span { "Turntable" } input( type="checkbox", - id="turntable", bind:checked=turntable ) } - div(class="control") { - label(for="layer-threshold") { "Layer threshold" } + label(class="control") { + span { "Layer threshold" } input( type="range", - id="layer-threshold", max=5.0, step=1.0, bind:valueAsNumber=layer_threshold ) } - div(class="control") { - label(for="debug-mode") { "Debug mode" } + label(class="control") { + span { "Debug mode" } input( type="checkbox", - id="debug-mode", bind:checked=debug_mode ) } From 2efc08d6c056f6c130467019a5d7f51338b9c63d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Sep 2024 02:15:04 -0700 Subject: [PATCH 046/126] Enable focus for tabs and display You can now switch tabs from the keyboard using the usual radio button interaction. --- app-proto/inversive-display/main.css | 16 +++++++++++++++- app-proto/inversive-display/src/main.rs | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app-proto/inversive-display/main.css b/app-proto/inversive-display/main.css index a16d04a..f8f7a96 100644 --- a/app-proto/inversive-display/main.css +++ b/app-proto/inversive-display/main.css @@ -14,10 +14,15 @@ body { canvas { float: left; background-color: #020202; + border: 1px solid #555; border-radius: 10px; margin-top: 5px; } +canvas:focus { + border-color: #aaa; +} + .hidden { display: none; } @@ -29,7 +34,12 @@ canvas { } input[type="radio"] { - display: none; + appearance: none; + width: 0px; + height: 0px; + padding: 0px; + margin: 0px; + outline: none; } .tab-pane > label { @@ -46,6 +56,10 @@ input[type="radio"] { background-color: #555; } +.tab-pane > label:has(:focus-visible) { + outline: medium auto currentColor; +} + .tab-pane > label:hover:not(:has(:checked)) { border-color: #bbb; background-color: #333; diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 6e6a0e8..3dd4653 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -482,7 +482,7 @@ fn main() { } } div { "Mean frame interval: " (mean_frame_interval.get()) " ms" } - canvas(ref=display, width="600", height="600") + canvas(ref=display, width=600, height=600, tabindex=0) div(ref=gen_controls) { label(class="control") { span { "Sphere 0 depth" } From c67f37c934ee229ad253caca82278fe4b575d18c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Sep 2024 19:41:15 -0700 Subject: [PATCH 047/126] Implement keyboard navigation --- app-proto/inversive-display/src/main.rs | 143 ++++++++++++++++++------ 1 file changed, 109 insertions(+), 34 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 3dd4653..ef2628a 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -9,9 +9,17 @@ extern crate js_sys; use core::array; -use nalgebra::{DMatrix, DVector}; +use nalgebra::{DMatrix, DVector, Rotation3, Vector3}; use sycamore::{prelude::*, motion::create_raf, rt::{JsCast, JsValue}}; -use web_sys::{console, window, WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation}; +use web_sys::{ + console, + window, + KeyboardEvent, + WebGl2RenderingContext, + WebGlProgram, + WebGlShader, + WebGlUniformLocation +}; mod engine; @@ -157,6 +165,12 @@ fn main() { // tab selection let tab_selection = create_signal(Tab::GenTab); + // navigation + let pitch_up = create_signal(0.0); + let pitch_down = create_signal(0.0); + let yaw_right = create_signal(0.0); + let yaw_left = create_signal(0.0); + // controls for general example let gen_controls = create_node_ref(); let ctrl_x = create_signal(0.0); @@ -246,8 +260,21 @@ fn main() { let mut last_time = 0.0; // scene parameters - const TURNTABLE_SPEED: f64 = 0.5; - let mut turntable_angle = 0.0; + const NAV_SPEED: f64 = 0.4; // in radians per second + const TURNTABLE_SPEED: f64 = 0.1; // in radians per second + let mut orientation = DMatrix::::identity(5, 5); + let mut rotation = DMatrix::::identity(5, 5); + let location = { + const LEN: f64 = -5.0; + const LEN_SQ: f64 = LEN*LEN; + DMatrix::from_column_slice(5, 5, &[ + 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, LEN, + 0.0, 0.0, 2.0*LEN, 1.0, LEN_SQ, + 0.0, 0.0, 0.0, 0.0, 1.0 + ]) + }; /* INSTRUMENTS */ let performance = window().unwrap().performance().unwrap(); @@ -353,11 +380,36 @@ fn main() { let time_step = 0.001*(time - last_time); last_time = time; - // move the turntable + // get the navigation state + let pitch_up_val = pitch_up.get(); + let pitch_down_val = pitch_down.get(); + let yaw_right_val = yaw_right.get(); + let yaw_left_val = yaw_left.get(); let turntable_val = turntable.get(); - if turntable_val { - turntable_angle += TURNTABLE_SPEED * time_step; - } + + // update the construction's orientation + let ang_vel = { + let pitch = pitch_up_val - pitch_down_val; + let yaw = yaw_right_val - yaw_left_val; + let ang_vel_from_keyboard = + if pitch != 0.0 || yaw != 0.0 { + NAV_SPEED * Vector3::new(-pitch, yaw, 0.0).normalize() + } else { + Vector3::zeros() + }; + let ang_vel_from_turntable = + if turntable_val { + Vector3::new(0.0, TURNTABLE_SPEED, 0.0) + } else { + Vector3::zeros() + }; + ang_vel_from_keyboard + ang_vel_from_turntable + }; + let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0); + rotation_sp.copy_from( + Rotation3::from_scaled_axis(time_step * ang_vel).matrix() + ); + orientation = &rotation * &orientation; if scene_changed.get() { /* INSTRUMENTS */ @@ -369,30 +421,8 @@ fn main() { frames_since_last_sample = 0; } - // set the orientation and translation - let orientation = { - let ang_cos = turntable_angle.cos(); - let ang_sin = turntable_angle.sin(); - DMatrix::from_column_slice(5, 5, &[ - ang_cos, 0.0, ang_sin, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, 0.0, - -ang_sin, 0.0, ang_cos, 0.0, 0.0, - 0.0, 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 1.0 - ]) - }; - let translation = { - const LEN: f64 = -5.0; - const LEN_SQ: f64 = LEN*LEN; - DMatrix::from_column_slice(5, 5, &[ - 1.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, LEN, - 0.0, 0.0, 2.0*LEN, 1.0, LEN_SQ, - 0.0, 0.0, 0.0, 0.0, 1.0 - ]) - }; - let construction_to_world = &translation * orientation; + // find the map from construction space to world space + let construction_to_world = &location * &orientation; // update the construction sphere_vec.clear(); @@ -450,7 +480,13 @@ fn main() { ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); // clear scene change flag - scene_changed.set(turntable_val); + scene_changed.set( + pitch_up_val != 0.0 + || pitch_down_val != 0.0 + || yaw_left_val != 0.0 + || yaw_right_val != 0.0 + || turntable_val + ); } else { frames_since_last_sample = 0; mean_frame_interval.set(-1.0); @@ -482,7 +518,46 @@ fn main() { } } div { "Mean frame interval: " (mean_frame_interval.get()) " ms" } - canvas(ref=display, width=600, height=600, tabindex=0) + canvas( + ref=display, + width=600, + height=600, + tabindex=0, + on:keydown=move |event: KeyboardEvent| { + let mut navigating = true; + match event.key().as_str() { + "ArrowUp" => pitch_up.set(1.0), + "ArrowDown" => pitch_down.set(1.0), + "ArrowRight" => yaw_right.set(1.0), + "ArrowLeft" => yaw_left.set(1.0), + _ => navigating = false + }; + if navigating { + scene_changed.set(true); + event.prevent_default(); + } + }, + on:keyup=move |event: KeyboardEvent| { + let mut navigating = true; + match event.key().as_str() { + "ArrowUp" => pitch_up.set(0.0), + "ArrowDown" => pitch_down.set(0.0), + "ArrowRight" => yaw_right.set(0.0), + "ArrowLeft" => yaw_left.set(0.0), + _ => navigating = false + }; + if navigating { + scene_changed.set(true); + event.prevent_default(); + } + }, + on:blur=move |_| { + pitch_up.set(0.0); + pitch_down.set(0.0); + yaw_right.set(0.0); + yaw_left.set(0.0); + } + ) div(ref=gen_controls) { label(class="control") { span { "Sphere 0 depth" } From 20d072d61561215cdb1ab676bc8397e9275db8b9 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 10 Sep 2024 02:29:50 -0700 Subject: [PATCH 048/126] Combine key-down and key-up handlers --- app-proto/inversive-display/src/main.rs | 45 ++++++++++--------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index ef2628a..f516ed8 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -495,6 +495,21 @@ fn main() { start_animation_loop(); }); + let set_nav_signal = move |event: KeyboardEvent, value: f64| { + let mut navigating = true; + match event.key().as_str() { + "ArrowUp" => pitch_up.set(value), + "ArrowDown" => pitch_down.set(value), + "ArrowRight" => yaw_right.set(value), + "ArrowLeft" => yaw_left.set(value), + _ => navigating = false + }; + if navigating { + scene_changed.set(true); + event.prevent_default(); + } + }; + view! { div(id="app") { div(class="tab-pane") { @@ -523,34 +538,8 @@ fn main() { width=600, height=600, tabindex=0, - on:keydown=move |event: KeyboardEvent| { - let mut navigating = true; - match event.key().as_str() { - "ArrowUp" => pitch_up.set(1.0), - "ArrowDown" => pitch_down.set(1.0), - "ArrowRight" => yaw_right.set(1.0), - "ArrowLeft" => yaw_left.set(1.0), - _ => navigating = false - }; - if navigating { - scene_changed.set(true); - event.prevent_default(); - } - }, - on:keyup=move |event: KeyboardEvent| { - let mut navigating = true; - match event.key().as_str() { - "ArrowUp" => pitch_up.set(0.0), - "ArrowDown" => pitch_down.set(0.0), - "ArrowRight" => yaw_right.set(0.0), - "ArrowLeft" => yaw_left.set(0.0), - _ => navigating = false - }; - if navigating { - scene_changed.set(true); - event.prevent_default(); - } - }, + on:keydown=move |event: KeyboardEvent| { set_nav_signal(event, 1.0); }, + on:keyup=move |event: KeyboardEvent| { set_nav_signal(event, 0.0); }, on:blur=move |_| { pitch_up.set(0.0); pitch_down.set(0.0); From aceac5e5c41006781cf88cd98c2e60b9709aaaf7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 10 Sep 2024 03:14:33 -0700 Subject: [PATCH 049/126] Add roll to keyboard controls --- app-proto/inversive-display/src/main.rs | 37 ++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index f516ed8..350c81c 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -170,6 +170,8 @@ fn main() { let pitch_down = create_signal(0.0); let yaw_right = create_signal(0.0); let yaw_left = create_signal(0.0); + let roll_ccw = create_signal(0.0); + let roll_cw = create_signal(0.0); // controls for general example let gen_controls = create_node_ref(); @@ -385,15 +387,18 @@ fn main() { let pitch_down_val = pitch_down.get(); let yaw_right_val = yaw_right.get(); let yaw_left_val = yaw_left.get(); + let roll_ccw_val = roll_ccw.get(); + let roll_cw_val = roll_cw.get(); let turntable_val = turntable.get(); // update the construction's orientation let ang_vel = { let pitch = pitch_up_val - pitch_down_val; let yaw = yaw_right_val - yaw_left_val; + let roll = roll_ccw_val - roll_cw_val; let ang_vel_from_keyboard = - if pitch != 0.0 || yaw != 0.0 { - NAV_SPEED * Vector3::new(-pitch, yaw, 0.0).normalize() + if pitch != 0.0 || yaw != 0.0 || roll != 0.0 { + NAV_SPEED * Vector3::new(-pitch, yaw, roll).normalize() } else { Vector3::zeros() }; @@ -485,6 +490,8 @@ fn main() { || pitch_down_val != 0.0 || yaw_left_val != 0.0 || yaw_right_val != 0.0 + || roll_cw_val != 0.0 + || roll_ccw_val != 0.0 || turntable_val ); } else { @@ -500,6 +507,8 @@ fn main() { match event.key().as_str() { "ArrowUp" => pitch_up.set(value), "ArrowDown" => pitch_down.set(value), + "ArrowRight" if event.shift_key() => roll_cw.set(value), + "ArrowLeft" if event.shift_key() => roll_ccw.set(value), "ArrowRight" => yaw_right.set(value), "ArrowLeft" => yaw_left.set(value), _ => navigating = false @@ -538,13 +547,33 @@ fn main() { width=600, height=600, tabindex=0, - on:keydown=move |event: KeyboardEvent| { set_nav_signal(event, 1.0); }, - on:keyup=move |event: KeyboardEvent| { set_nav_signal(event, 0.0); }, + on:keydown=move |event: KeyboardEvent| { + if event.key() == "Shift" { + roll_cw.set(yaw_right.get()); + roll_ccw.set(yaw_left.get()); + yaw_right.set(0.0); + yaw_left.set(0.0); + } else { + set_nav_signal(event, 1.0); + } + }, + on:keyup=move |event: KeyboardEvent| { + if event.key() == "Shift" { + yaw_right.set(roll_cw.get()); + yaw_left.set(roll_ccw.get()); + roll_cw.set(0.0); + roll_ccw.set(0.0); + } else { + set_nav_signal(event, 0.0); + } + }, on:blur=move |_| { pitch_up.set(0.0); pitch_down.set(0.0); yaw_right.set(0.0); yaw_left.set(0.0); + roll_ccw.set(0.0); + roll_cw.set(0.0); } ) div(ref=gen_controls) { From d3c9a08d22571fd5b28724fea9adf25d35f93cc0 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 10 Sep 2024 04:08:49 -0700 Subject: [PATCH 050/126] Add zoom to keyboard controls --- app-proto/inversive-display/src/main.rs | 52 ++++++++++++++++++------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index 350c81c..d351149 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -172,6 +172,8 @@ fn main() { let yaw_left = create_signal(0.0); let roll_ccw = create_signal(0.0); let roll_cw = create_signal(0.0); + let zoom_in = create_signal(0.0); + let zoom_out = create_signal(0.0); // controls for general example let gen_controls = create_node_ref(); @@ -262,21 +264,12 @@ fn main() { let mut last_time = 0.0; // scene parameters - const NAV_SPEED: f64 = 0.4; // in radians per second + const ROT_SPEED: f64 = 0.4; // in radians per second const TURNTABLE_SPEED: f64 = 0.1; // in radians per second + const ZOOM_SPEED: f64 = 0.15; let mut orientation = DMatrix::::identity(5, 5); let mut rotation = DMatrix::::identity(5, 5); - let location = { - const LEN: f64 = -5.0; - const LEN_SQ: f64 = LEN*LEN; - DMatrix::from_column_slice(5, 5, &[ - 1.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, LEN, - 0.0, 0.0, 2.0*LEN, 1.0, LEN_SQ, - 0.0, 0.0, 0.0, 0.0, 1.0 - ]) - }; + let mut location_z: f64 = 5.0; /* INSTRUMENTS */ let performance = window().unwrap().performance().unwrap(); @@ -389,6 +382,8 @@ fn main() { let yaw_left_val = yaw_left.get(); let roll_ccw_val = roll_ccw.get(); let roll_cw_val = roll_cw.get(); + let zoom_in_val = zoom_in.get(); + let zoom_out_val = zoom_out.get(); let turntable_val = turntable.get(); // update the construction's orientation @@ -398,7 +393,7 @@ fn main() { let roll = roll_ccw_val - roll_cw_val; let ang_vel_from_keyboard = if pitch != 0.0 || yaw != 0.0 || roll != 0.0 { - NAV_SPEED * Vector3::new(-pitch, yaw, roll).normalize() + ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize() } else { Vector3::zeros() }; @@ -416,6 +411,10 @@ fn main() { ); orientation = &rotation * &orientation; + // update the construction's location + let zoom = zoom_out_val - zoom_in_val; + location_z *= (time_step * ZOOM_SPEED * zoom).exp(); + if scene_changed.get() { /* INSTRUMENTS */ // measure mean frame interval @@ -427,6 +426,16 @@ fn main() { } // find the map from construction space to world space + let location = { + let u = -location_z; + DMatrix::from_column_slice(5, 5, &[ + 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, u, + 0.0, 0.0, 2.0*u, 1.0, u*u, + 0.0, 0.0, 0.0, 0.0, 1.0 + ]) + }; let construction_to_world = &location * &orientation; // update the construction @@ -492,6 +501,8 @@ fn main() { || yaw_right_val != 0.0 || roll_cw_val != 0.0 || roll_ccw_val != 0.0 + || zoom_in_val != 0.0 + || zoom_out_val != 0.0 || turntable_val ); } else { @@ -504,11 +515,14 @@ fn main() { let set_nav_signal = move |event: KeyboardEvent, value: f64| { let mut navigating = true; + let shift = event.shift_key(); match event.key().as_str() { + "ArrowUp" if shift => zoom_in.set(value), + "ArrowDown" if shift => zoom_out.set(value), "ArrowUp" => pitch_up.set(value), "ArrowDown" => pitch_down.set(value), - "ArrowRight" if event.shift_key() => roll_cw.set(value), - "ArrowLeft" if event.shift_key() => roll_ccw.set(value), + "ArrowRight" if shift => roll_cw.set(value), + "ArrowLeft" if shift => roll_ccw.set(value), "ArrowRight" => yaw_right.set(value), "ArrowLeft" => yaw_left.set(value), _ => navigating = false @@ -551,8 +565,12 @@ fn main() { if event.key() == "Shift" { roll_cw.set(yaw_right.get()); roll_ccw.set(yaw_left.get()); + zoom_in.set(pitch_up.get()); + zoom_out.set(pitch_down.get()); yaw_right.set(0.0); yaw_left.set(0.0); + pitch_up.set(0.0); + pitch_down.set(0.0); } else { set_nav_signal(event, 1.0); } @@ -561,8 +579,12 @@ fn main() { if event.key() == "Shift" { yaw_right.set(roll_cw.get()); yaw_left.set(roll_ccw.get()); + pitch_up.set(zoom_in.get()); + pitch_down.set(zoom_out.get()); roll_cw.set(0.0); roll_ccw.set(0.0); + zoom_in.set(0.0); + zoom_out.set(0.0); } else { set_nav_signal(event, 0.0); } From 336b940471a18332d692b3b637c34110b8bdd1a1 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 12 Sep 2024 15:24:41 -0700 Subject: [PATCH 051/126] Outline: start on editor state and outline view --- app-proto/sketch-outline/.gitignore | 3 ++ app-proto/sketch-outline/Cargo.toml | 29 ++++++++++++ app-proto/sketch-outline/index.html | 9 ++++ app-proto/sketch-outline/main.css | 50 +++++++++++++++++++++ app-proto/sketch-outline/src/editor.rs | 61 ++++++++++++++++++++++++++ app-proto/sketch-outline/src/main.rs | 13 ++++++ 6 files changed, 165 insertions(+) create mode 100644 app-proto/sketch-outline/.gitignore create mode 100644 app-proto/sketch-outline/Cargo.toml create mode 100644 app-proto/sketch-outline/index.html create mode 100644 app-proto/sketch-outline/main.css create mode 100644 app-proto/sketch-outline/src/editor.rs create mode 100644 app-proto/sketch-outline/src/main.rs diff --git a/app-proto/sketch-outline/.gitignore b/app-proto/sketch-outline/.gitignore new file mode 100644 index 0000000..238273d --- /dev/null +++ b/app-proto/sketch-outline/.gitignore @@ -0,0 +1,3 @@ +target +dist +Cargo.lock \ No newline at end of file diff --git a/app-proto/sketch-outline/Cargo.toml b/app-proto/sketch-outline/Cargo.toml new file mode 100644 index 0000000..522c994 --- /dev/null +++ b/app-proto/sketch-outline/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "sketch-outline" +version = "0.1.0" +authors = ["Aaron"] +edition = "2021" + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +js-sys = "0.3.70" +nalgebra = "0.33.0" +sycamore = "0.9.0-beta.3" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.7", optional = true } + +[dependencies.web-sys] +version = "0.3.69" + +[dev-dependencies] +wasm-bindgen-test = "0.3.34" + +[profile.release] +opt-level = "s" # optimize for small code size +debug = true # include debug symbols diff --git a/app-proto/sketch-outline/index.html b/app-proto/sketch-outline/index.html new file mode 100644 index 0000000..5474fe9 --- /dev/null +++ b/app-proto/sketch-outline/index.html @@ -0,0 +1,9 @@ + + + + + Sketch outline + + + + diff --git a/app-proto/sketch-outline/main.css b/app-proto/sketch-outline/main.css new file mode 100644 index 0000000..2e6aedc --- /dev/null +++ b/app-proto/sketch-outline/main.css @@ -0,0 +1,50 @@ +body { + margin-left: 20px; + margin-top: 20px; + color: #fcfcfc; + background-color: #222; +} + +ul { + width: 450px; + padding: 8px; + border: 1px solid #888; + border-radius: 16px; +} + +li { + display: flex; + padding: 3px; + list-style-type: none; + background-color: #444; + border-radius: 8px; +} + +li:not(:last-child) { + margin-bottom: 8px; +} + +li > .elt-name { + flex-grow: 1; + padding: 2px 0px 2px 4px; +} + +li > .elt-rep { + display: flex; +} + +li > .elt-rep > div { + padding: 2px; + margin-left: 3px; + text-align: center; + width: 60px; + background-color: #333; +} + +li > .elt-rep > div:first-child { + border-radius: 6px 0px 0px 6px; +} + +li > .elt-rep > div:last-child { + border-radius: 0px 6px 6px 0px; +} \ No newline at end of file diff --git a/app-proto/sketch-outline/src/editor.rs b/app-proto/sketch-outline/src/editor.rs new file mode 100644 index 0000000..b0d3f2e --- /dev/null +++ b/app-proto/sketch-outline/src/editor.rs @@ -0,0 +1,61 @@ +use nalgebra::DVector; +use sycamore::{prelude::*, web::tags::div}; + +#[derive(Clone, PartialEq)] +struct Element { + id: i64, + name: String, + rep: DVector, + color: [f32; 3] +} + +struct EditorState { + elements: Signal> +} + +#[component] +pub fn Editor() -> View { + let state = EditorState { + elements: create_signal(vec![ + Element { + id: 1, + name: String::from("Central"), + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.25, -1.0]), + color: [0.75_f32, 0.75_f32, 0.75_f32] + }, + Element { + id: 2, + name: String::from("Wing A"), + rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]), + color: [1.00_f32, 0.25_f32, 0.00_f32] + }, + Element { + id: 3, + name: String::from("Wing B"), + rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]), + color: [1.00_f32, 0.25_f32, 0.00_f32] + } + ]) + }; + + view! { + ul { + Keyed( + list=state.elements, + view=|elt| { + let name = elt.name.clone(); + let rep_components = elt.rep.iter().map( + |u| View::from(div().children(u.to_string().replace("-", "\u{2212}"))) + ).collect::>(); + view! { + li { + div(class="elt-name") { (name) } + div(class="elt-rep") { (rep_components) } + } + } + }, + key=|elt| elt.id + ) + } + } +} \ No newline at end of file diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs new file mode 100644 index 0000000..8b7ce30 --- /dev/null +++ b/app-proto/sketch-outline/src/main.rs @@ -0,0 +1,13 @@ +use sycamore::prelude::*; + +mod editor; + +use editor::Editor; + +fn main() { + sycamore::render(|| { + view! { + Editor {} + } + }); +} \ No newline at end of file From 634e97b6595ac16ef70817baeb6161bbab662cca Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 12 Sep 2024 22:36:54 -0700 Subject: [PATCH 052/126] Outline: switch to user-facing ID --- app-proto/sketch-outline/main.css | 2 +- app-proto/sketch-outline/src/editor.rs | 36 +++++++++++++------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app-proto/sketch-outline/main.css b/app-proto/sketch-outline/main.css index 2e6aedc..7e0fe01 100644 --- a/app-proto/sketch-outline/main.css +++ b/app-proto/sketch-outline/main.css @@ -24,7 +24,7 @@ li:not(:last-child) { margin-bottom: 8px; } -li > .elt-name { +li > .elt-label { flex-grow: 1; padding: 2px 0px 2px 4px; } diff --git a/app-proto/sketch-outline/src/editor.rs b/app-proto/sketch-outline/src/editor.rs index b0d3f2e..07c0084 100644 --- a/app-proto/sketch-outline/src/editor.rs +++ b/app-proto/sketch-outline/src/editor.rs @@ -3,10 +3,10 @@ use sycamore::{prelude::*, web::tags::div}; #[derive(Clone, PartialEq)] struct Element { - id: i64, - name: String, + id: String, + label: String, + color: [f32; 3], rep: DVector, - color: [f32; 3] } struct EditorState { @@ -18,22 +18,22 @@ pub fn Editor() -> View { let state = EditorState { elements: create_signal(vec![ Element { - id: 1, - name: String::from("Central"), - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.25, -1.0]), - color: [0.75_f32, 0.75_f32, 0.75_f32] + id: String::from("central"), + label: String::from("Central"), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.25, -1.0]) }, Element { - id: 2, - name: String::from("Wing A"), - rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]), - color: [1.00_f32, 0.25_f32, 0.00_f32] + id: String::from("wing_a"), + label: String::from("Wing A"), + color: [1.00_f32, 0.25_f32, 0.00_f32], + rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]) }, Element { - id: 3, - name: String::from("Wing B"), - rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]), - color: [1.00_f32, 0.25_f32, 0.00_f32] + id: String::from("wing_b"), + label: String::from("Wing B"), + color: [1.00_f32, 0.25_f32, 0.00_f32], + rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]) } ]) }; @@ -43,18 +43,18 @@ pub fn Editor() -> View { Keyed( list=state.elements, view=|elt| { - let name = elt.name.clone(); + let label = elt.label.clone(); let rep_components = elt.rep.iter().map( |u| View::from(div().children(u.to_string().replace("-", "\u{2212}"))) ).collect::>(); view! { li { - div(class="elt-name") { (name) } + div(class="elt-label") { (label) } div(class="elt-rep") { (rep_components) } } } }, - key=|elt| elt.id + key=|elt| elt.id.clone() ) } } From 20b96a9764cfe4413d779b40ac271d1286560253 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 12 Sep 2024 22:39:21 -0700 Subject: [PATCH 053/126] Outline: switch from "Editor" to "App" --- app-proto/sketch-outline/src/{editor.rs => app.rs} | 6 +++--- app-proto/sketch-outline/src/main.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) rename app-proto/sketch-outline/src/{editor.rs => app.rs} (95%) diff --git a/app-proto/sketch-outline/src/editor.rs b/app-proto/sketch-outline/src/app.rs similarity index 95% rename from app-proto/sketch-outline/src/editor.rs rename to app-proto/sketch-outline/src/app.rs index 07c0084..35503f0 100644 --- a/app-proto/sketch-outline/src/editor.rs +++ b/app-proto/sketch-outline/src/app.rs @@ -9,13 +9,13 @@ struct Element { rep: DVector, } -struct EditorState { +struct AppState { elements: Signal> } #[component] -pub fn Editor() -> View { - let state = EditorState { +pub fn App() -> View { + let state = AppState { elements: create_signal(vec![ Element { id: String::from("central"), diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index 8b7ce30..e46fff3 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -1,13 +1,13 @@ use sycamore::prelude::*; -mod editor; +mod app; -use editor::Editor; +use app::App; fn main() { sycamore::render(|| { view! { - Editor {} + App {} } }); } \ No newline at end of file From d481181ef87162f660f0f74e16be3b629ff5f53e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 13 Sep 2024 00:07:49 -0700 Subject: [PATCH 054/126] Outline: sort elements by ID --- app-proto/sketch-outline/Cargo.toml | 1 + app-proto/sketch-outline/src/app.rs | 25 ++++++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/app-proto/sketch-outline/Cargo.toml b/app-proto/sketch-outline/Cargo.toml index 522c994..b039ab8 100644 --- a/app-proto/sketch-outline/Cargo.toml +++ b/app-proto/sketch-outline/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" default = ["console_error_panic_hook"] [dependencies] +itertools = "0.13.0" js-sys = "0.3.70" nalgebra = "0.33.0" sycamore = "0.9.0-beta.3" diff --git a/app-proto/sketch-outline/src/app.rs b/app-proto/sketch-outline/src/app.rs index 35503f0..cc63ec0 100644 --- a/app-proto/sketch-outline/src/app.rs +++ b/app-proto/sketch-outline/src/app.rs @@ -1,3 +1,4 @@ +use itertools::Itertools; use nalgebra::DVector; use sycamore::{prelude::*, web::tags::div}; @@ -10,6 +11,7 @@ struct Element { } struct AppState { + // the order of the elements is arbitrary, and it could change at any time elements: Signal> } @@ -17,12 +19,6 @@ struct AppState { pub fn App() -> View { let state = AppState { elements: create_signal(vec![ - Element { - id: String::from("central"), - label: String::from("Central"), - color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.25, -1.0]) - }, Element { id: String::from("wing_a"), label: String::from("Wing A"), @@ -34,14 +30,29 @@ pub fn App() -> View { label: String::from("Wing B"), color: [1.00_f32, 0.25_f32, 0.00_f32], rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]) + }, + Element { + id: String::from("central"), + label: String::from("Central"), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.25, -1.0]) } ]) }; + // sort the elements alphabetically by ID + let elements_sorted = create_memo(move || + state.elements + .get_clone() + .into_iter() + .sorted_by_key(|elt| elt.id.clone()) + .collect() + ); + view! { ul { Keyed( - list=state.elements, + list=elements_sorted, view=|elt| { let label = elt.label.clone(); let rep_components = elt.rep.iter().map( From e6d1e0b8658b041e667785a370bc52c2db47f4c4 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 13 Sep 2024 00:40:34 -0700 Subject: [PATCH 055/126] Outline: encapsulate assembly data --- app-proto/sketch-outline/src/app.rs | 55 +++++++++++------------- app-proto/sketch-outline/src/assembly.rs | 16 +++++++ app-proto/sketch-outline/src/main.rs | 5 ++- 3 files changed, 44 insertions(+), 32 deletions(-) create mode 100644 app-proto/sketch-outline/src/assembly.rs diff --git a/app-proto/sketch-outline/src/app.rs b/app-proto/sketch-outline/src/app.rs index cc63ec0..1435fa9 100644 --- a/app-proto/sketch-outline/src/app.rs +++ b/app-proto/sketch-outline/src/app.rs @@ -2,47 +2,42 @@ use itertools::Itertools; use nalgebra::DVector; use sycamore::{prelude::*, web::tags::div}; -#[derive(Clone, PartialEq)] -struct Element { - id: String, - label: String, - color: [f32; 3], - rep: DVector, -} +use crate::assembly::{Assembly, Element}; struct AppState { - // the order of the elements is arbitrary, and it could change at any time - elements: Signal> + assembly: Assembly } #[component] pub fn App() -> View { let state = AppState { - elements: create_signal(vec![ - Element { - id: String::from("wing_a"), - label: String::from("Wing A"), - color: [1.00_f32, 0.25_f32, 0.00_f32], - rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]) - }, - Element { - id: String::from("wing_b"), - label: String::from("Wing B"), - color: [1.00_f32, 0.25_f32, 0.00_f32], - rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]) - }, - Element { - id: String::from("central"), - label: String::from("Central"), - color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.25, -1.0]) - } - ]) + assembly: Assembly { + elements: create_signal(vec![ + Element { + id: String::from("wing_a"), + label: String::from("Wing A"), + color: [1.00_f32, 0.25_f32, 0.00_f32], + rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]) + }, + Element { + id: String::from("wing_b"), + label: String::from("Wing B"), + color: [1.00_f32, 0.25_f32, 0.00_f32], + rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]) + }, + Element { + id: String::from("central"), + label: String::from("Central"), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.25, -1.0]) + } + ]) + } }; // sort the elements alphabetically by ID let elements_sorted = create_memo(move || - state.elements + state.assembly.elements .get_clone() .into_iter() .sorted_by_key(|elt| elt.id.clone()) diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/sketch-outline/src/assembly.rs new file mode 100644 index 0000000..e18242b --- /dev/null +++ b/app-proto/sketch-outline/src/assembly.rs @@ -0,0 +1,16 @@ +use nalgebra::DVector; +use sycamore::reactive::Signal; + +#[derive(Clone, PartialEq)] +pub struct Element { + pub id: String, + pub label: String, + pub color: [f32; 3], + pub rep: DVector, +} + +// a complete, view-independent description of an assembly +pub struct Assembly { + // the order of the elements is arbitrary, and it could change at any time + pub elements: Signal> +} \ No newline at end of file diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index e46fff3..06700e5 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -1,6 +1,7 @@ -use sycamore::prelude::*; - mod app; +mod assembly; + +use sycamore::prelude::*; use app::App; From 0c2869d3f367521f7b7e709614a686f6ce5083da Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 13 Sep 2024 00:43:19 -0700 Subject: [PATCH 056/126] Outline: improve code formatting --- app-proto/sketch-outline/src/app.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app-proto/sketch-outline/src/app.rs b/app-proto/sketch-outline/src/app.rs index 1435fa9..934c960 100644 --- a/app-proto/sketch-outline/src/app.rs +++ b/app-proto/sketch-outline/src/app.rs @@ -50,9 +50,10 @@ pub fn App() -> View { list=elements_sorted, view=|elt| { let label = elt.label.clone(); - let rep_components = elt.rep.iter().map( - |u| View::from(div().children(u.to_string().replace("-", "\u{2212}"))) - ).collect::>(); + let rep_components = elt.rep.iter().map(|u| { + let u_coord = u.to_string().replace("-", "\u{2212}"); + View::from(div().children(u_coord)) + }).collect::>(); view! { li { div(class="elt-label") { (label) } From 49170671b4266ae5550fc764cb69e9768fa4c43b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 13 Sep 2024 14:53:12 -0700 Subject: [PATCH 057/126] App: add display canvas --- app-proto/sketch-outline/Cargo.toml | 1 - app-proto/sketch-outline/main.css | 31 ++++++++++++++++++++++++- app-proto/sketch-outline/src/app.rs | 4 +++- app-proto/sketch-outline/src/display.rs | 10 ++++++++ app-proto/sketch-outline/src/main.rs | 3 +++ 5 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 app-proto/sketch-outline/src/display.rs diff --git a/app-proto/sketch-outline/Cargo.toml b/app-proto/sketch-outline/Cargo.toml index b039ab8..3afe26e 100644 --- a/app-proto/sketch-outline/Cargo.toml +++ b/app-proto/sketch-outline/Cargo.toml @@ -9,7 +9,6 @@ default = ["console_error_panic_hook"] [dependencies] itertools = "0.13.0" -js-sys = "0.3.70" nalgebra = "0.33.0" sycamore = "0.9.0-beta.3" diff --git a/app-proto/sketch-outline/main.css b/app-proto/sketch-outline/main.css index 7e0fe01..9e8cdb4 100644 --- a/app-proto/sketch-outline/main.css +++ b/app-proto/sketch-outline/main.css @@ -5,11 +5,17 @@ body { background-color: #222; } +/* outline */ + ul { + float: left; width: 450px; + height: 750px; + margin: 0px; padding: 8px; - border: 1px solid #888; + border: 1px solid #555; border-radius: 16px; + box-sizing: border-box; } li { @@ -20,6 +26,11 @@ li { border-radius: 8px; } +li:focus { + color: #fff; + background-color: #666; +} + li:not(:last-child) { margin-bottom: 8px; } @@ -41,10 +52,28 @@ li > .elt-rep > div { background-color: #333; } +li:focus > .elt-rep > div { + background-color: #555; +} + li > .elt-rep > div:first-child { border-radius: 6px 0px 0px 6px; } li > .elt-rep > div:last-child { border-radius: 0px 6px 6px 0px; +} + +/* display */ + +canvas { + float: left; + margin-left: 16px; + background-color: #020202; + border: 1px solid #555; + border-radius: 16px; +} + +canvas:focus { + border-color: #aaa; } \ No newline at end of file diff --git a/app-proto/sketch-outline/src/app.rs b/app-proto/sketch-outline/src/app.rs index 934c960..dca056a 100644 --- a/app-proto/sketch-outline/src/app.rs +++ b/app-proto/sketch-outline/src/app.rs @@ -55,7 +55,9 @@ pub fn App() -> View { View::from(div().children(u_coord)) }).collect::>(); view! { - li { + /* [TO DO] switch to integer-valued parameters whenever + that becomes possible again */ + li(tabindex="0") { div(class="elt-label") { (label) } div(class="elt-rep") { (rep_components) } } diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/sketch-outline/src/display.rs new file mode 100644 index 0000000..88336bf --- /dev/null +++ b/app-proto/sketch-outline/src/display.rs @@ -0,0 +1,10 @@ +use sycamore::prelude::*; + +#[component] +pub fn Display() -> View { + view! { + /* [TO DO] switch back to integer-valued parameters when that becomes + possible again */ + canvas(width="750", height="750", tabindex="0") + } +} \ No newline at end of file diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index 06700e5..c375a9c 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -1,14 +1,17 @@ mod app; mod assembly; +mod display; use sycamore::prelude::*; use app::App; +use display::Display; fn main() { sycamore::render(|| { view! { App {} + Display {} } }); } \ No newline at end of file From 959e4cc8b58c49b9e7a63d3a8d8c2e7c3ee7c4f6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 13 Sep 2024 15:15:55 -0700 Subject: [PATCH 058/126] App: pass app state into outline as context --- app-proto/sketch-outline/src/assembly.rs | 1 + app-proto/sketch-outline/src/main.rs | 40 +++++++++++++++++-- .../sketch-outline/src/{app.rs => outline.rs} | 34 ++-------------- 3 files changed, 41 insertions(+), 34 deletions(-) rename app-proto/sketch-outline/src/{app.rs => outline.rs} (51%) diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/sketch-outline/src/assembly.rs index e18242b..b53e38d 100644 --- a/app-proto/sketch-outline/src/assembly.rs +++ b/app-proto/sketch-outline/src/assembly.rs @@ -10,6 +10,7 @@ pub struct Element { } // a complete, view-independent description of an assembly +#[derive(Clone)] pub struct Assembly { // the order of the elements is arbitrary, and it could change at any time pub elements: Signal> diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index c375a9c..9e74d28 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -1,16 +1,50 @@ -mod app; mod assembly; mod display; +mod outline; +use nalgebra::DVector; use sycamore::prelude::*; -use app::App; +use assembly::{Assembly, Element}; use display::Display; +use outline::Outline; + +#[derive(Clone)] +struct AppState { + assembly: Assembly +} fn main() { sycamore::render(|| { + provide_context( + AppState { + assembly: Assembly { + elements: create_signal(vec![ + Element { + id: String::from("wing_a"), + label: String::from("Wing A"), + color: [1.00_f32, 0.25_f32, 0.00_f32], + rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]) + }, + Element { + id: String::from("wing_b"), + label: String::from("Wing B"), + color: [1.00_f32, 0.25_f32, 0.00_f32], + rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]) + }, + Element { + id: String::from("central"), + label: String::from("Central"), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.25, -1.0]) + } + ]) + } + } + ); + view! { - App {} + Outline {} Display {} } }); diff --git a/app-proto/sketch-outline/src/app.rs b/app-proto/sketch-outline/src/outline.rs similarity index 51% rename from app-proto/sketch-outline/src/app.rs rename to app-proto/sketch-outline/src/outline.rs index dca056a..546b272 100644 --- a/app-proto/sketch-outline/src/app.rs +++ b/app-proto/sketch-outline/src/outline.rs @@ -1,39 +1,11 @@ use itertools::Itertools; -use nalgebra::DVector; use sycamore::{prelude::*, web::tags::div}; -use crate::assembly::{Assembly, Element}; - -struct AppState { - assembly: Assembly -} +use crate::AppState; #[component] -pub fn App() -> View { - let state = AppState { - assembly: Assembly { - elements: create_signal(vec![ - Element { - id: String::from("wing_a"), - label: String::from("Wing A"), - color: [1.00_f32, 0.25_f32, 0.00_f32], - rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]) - }, - Element { - id: String::from("wing_b"), - label: String::from("Wing B"), - color: [1.00_f32, 0.25_f32, 0.00_f32], - rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]) - }, - Element { - id: String::from("central"), - label: String::from("Central"), - color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.25, -1.0]) - } - ]) - } - }; +pub fn Outline() -> View { + let state = use_context::(); // sort the elements alphabetically by ID let elements_sorted = create_memo(move || From 49655a8d62130d34cc63ec28780ba69d39c16ef7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 14 Sep 2024 10:58:46 -0700 Subject: [PATCH 059/126] Ray-caster: remove debug code Remove GPU code and uniforms that were used as scaffolding during initial development, but have now been replaced by CPU analogues. --- app-proto/inversive-display/src/inversive.frag | 12 ------------ app-proto/inversive-display/src/main.rs | 5 ----- 2 files changed, 17 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index 2e185a5..93bec50 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -11,16 +11,6 @@ struct vecInv { vec2 lt; }; -vecInv sphere(vec3 center, float radius) { - return vecInv( - center / radius, - vec2( - 0.5 / radius, - 0.5 * (dot(center, center) / radius - radius) - ) - ); -} - // --- uniforms --- // construction. the SPHERE_MAX array size seems to affect frame rate a lot, @@ -35,8 +25,6 @@ uniform vec2 resolution; uniform float shortdim; // controls -uniform vec2 ctrl; -uniform vec2 radius; uniform float opacity; uniform float highlight; uniform int layer_threshold; diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs index d351149..8d16732 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -7,7 +7,6 @@ // https://stackoverflow.com/a/39684775 // -extern crate js_sys; use core::array; use nalgebra::{DMatrix, DVector, Rotation3, Vector3}; use sycamore::{prelude::*, motion::create_raf, rt::{JsCast, JsValue}}; @@ -343,8 +342,6 @@ fn main() { ); let resolution_loc = ctx.get_uniform_location(&program, "resolution"); let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); - let ctrl_loc = ctx.get_uniform_location(&program, "ctrl"); /* DEBUG */ - let radius_loc = ctx.get_uniform_location(&program, "radius"); /* DEBUG */ let opacity_loc = ctx.get_uniform_location(&program, "opacity"); let highlight_loc = ctx.get_uniform_location(&program, "highlight"); let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); @@ -483,8 +480,6 @@ fn main() { } // pass the control parameters - ctx.uniform2f(ctrl_loc.as_ref(), ctrl_x.get() as f32, ctrl_y.get() as f32); /* DEBUG */ - ctx.uniform2f(radius_loc.as_ref(), radius_x.get() as f32, radius_y.get() as f32); /* DEBUG */ ctx.uniform1f(opacity_loc.as_ref(), opacity.get() as f32); ctx.uniform1f(highlight_loc.as_ref(), highlight.get() as f32); ctx.uniform1i(layer_threshold_loc.as_ref(), layer_threshold.get() as i32); From cd18d594e033fb47ebf21b70d65fe5e29471d08e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 14 Sep 2024 11:46:24 -0700 Subject: [PATCH 060/126] Display: bring in ray-casting code --- app-proto/sketch-outline/Cargo.toml | 11 + app-proto/sketch-outline/src/display.rs | 340 +++++++++++++++++++- app-proto/sketch-outline/src/identity.vert | 7 + app-proto/sketch-outline/src/inversive.frag | 227 +++++++++++++ app-proto/sketch-outline/src/main.rs | 4 +- 5 files changed, 583 insertions(+), 6 deletions(-) create mode 100644 app-proto/sketch-outline/src/identity.vert create mode 100644 app-proto/sketch-outline/src/inversive.frag diff --git a/app-proto/sketch-outline/Cargo.toml b/app-proto/sketch-outline/Cargo.toml index 3afe26e..7d1d608 100644 --- a/app-proto/sketch-outline/Cargo.toml +++ b/app-proto/sketch-outline/Cargo.toml @@ -9,6 +9,7 @@ default = ["console_error_panic_hook"] [dependencies] itertools = "0.13.0" +js-sys = "0.3.70" nalgebra = "0.33.0" sycamore = "0.9.0-beta.3" @@ -20,6 +21,16 @@ console_error_panic_hook = { version = "0.1.7", optional = true } [dependencies.web-sys] version = "0.3.69" +features = [ + 'HtmlCanvasElement', + 'Performance', + 'WebGl2RenderingContext', + 'WebGlBuffer', + 'WebGlProgram', + 'WebGlShader', + 'WebGlUniformLocation', + 'WebGlVertexArrayObject' +] [dev-dependencies] wasm-bindgen-test = "0.3.34" diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/sketch-outline/src/display.rs index 88336bf..b1bf6ea 100644 --- a/app-proto/sketch-outline/src/display.rs +++ b/app-proto/sketch-outline/src/display.rs @@ -1,10 +1,342 @@ -use sycamore::prelude::*; +use core::array; +use nalgebra::{DMatrix, DVector, Rotation3, Vector3}; +use sycamore::{prelude::*, motion::create_raf}; +use web_sys::{ + console, + window, + WebGl2RenderingContext, + WebGlProgram, + WebGlShader, + WebGlUniformLocation, + wasm_bindgen::{JsCast, JsValue} +}; + +fn compile_shader( + context: &WebGl2RenderingContext, + shader_type: u32, + source: &str, +) -> WebGlShader { + let shader = context.create_shader(shader_type).unwrap(); + context.shader_source(&shader, source); + context.compile_shader(&shader); + shader +} + +fn get_uniform_array_locations( + context: &WebGl2RenderingContext, + program: &WebGlProgram, + var_name: &str, + member_name_opt: Option<&str> +) -> [Option; N] { + array::from_fn(|n| { + let name = match member_name_opt { + Some(member_name) => format!("{var_name}[{n}].{member_name}"), + None => format!("{var_name}[{n}]") + }; + context.get_uniform_location(&program, name.as_str()) + }) +} + +// load the given data into the vertex input of the given name +fn bind_vertex_attrib( + context: &WebGl2RenderingContext, + index: u32, + size: i32, + data: &[f32] +) { + // create a data buffer and bind it to ARRAY_BUFFER + let buffer = context.create_buffer().unwrap(); + context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer)); + + // 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 + unsafe { + context.buffer_data_with_array_buffer_view( + WebGl2RenderingContext::ARRAY_BUFFER, + &js_sys::Float32Array::view(&data), + WebGl2RenderingContext::STATIC_DRAW, + ); + } + + // 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 + ); +} #[component] pub fn Display() -> View { + // canvas + let display = create_node_ref(); + + // navigation + let pitch_up = create_signal(0.0); + let pitch_down = create_signal(0.0); + let yaw_right = create_signal(0.0); + let yaw_left = create_signal(0.0); + let roll_ccw = create_signal(0.0); + let roll_cw = create_signal(0.0); + let zoom_in = create_signal(0.0); + let zoom_out = create_signal(0.0); + + // change listener + let scene_changed = create_signal(true); + + /* INSTRUMENTS */ + const SAMPLE_PERIOD: i32 = 60; + let mut last_sample_time = 0.0; + let mut frames_since_last_sample = 0; + let mean_frame_interval = create_signal(0.0); + + on_mount(move || { + /* SCAFFOLDING */ + /* create list of construction elements */ + const SPHERE_MAX: usize = 200; + let mut sphere_vec = Vec::>::new(); + let mut color_vec = Vec::<[f32; 3]>::new(); + + // timing + let mut last_time = 0.0; + + // viewpoint + const ROT_SPEED: f64 = 0.4; // in radians per second + const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second + let mut orientation = DMatrix::::identity(5, 5); + let mut rotation = DMatrix::::identity(5, 5); + let mut location_z: f64 = 5.0; + + // 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 */ + + /* INSTRUMENTS */ + let performance = window().unwrap().performance().unwrap(); + + // get the display canvas + let canvas = display.get().unchecked_into::(); + let ctx = canvas + .get_context("webgl2") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap(); + + // compile and attach the vertex and fragment shaders + let vertex_shader = compile_shader( + &ctx, + WebGl2RenderingContext::VERTEX_SHADER, + include_str!("identity.vert"), + ); + 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 */ + // print the maximum number of vectors that can be passed as + // uniforms to a fragment shader. the OpenGL ES 3.0 standard + // requires this maximum to be at least 224, as discussed in the + // documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter + // here: + // + // https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml + // + // there are also other size limits. for example, on Aaron's + // machine, the the length of a float or genType array seems to be + // capped at 1024 elements + console::log_2( + &ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(), + &JsValue::from("uniform vectors available") + ); + + // find indices of vertex attributes and uniforms + let position_index = ctx.get_attrib_location(&program, "position") as u32; + let sphere_cnt_loc = ctx.get_uniform_location(&program, "sphere_cnt"); + let sphere_sp_locs = get_uniform_array_locations::( + &ctx, &program, "sphere_list", Some("sp") + ); + let sphere_lt_locs = get_uniform_array_locations::( + &ctx, &program, "sphere_list", Some("lt") + ); + let color_locs = get_uniform_array_locations::( + &ctx, &program, "color_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 highlight_loc = ctx.get_uniform_location(&program, "highlight"); + 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] = [ + // northwest triangle + -1.0, -1.0, 0.0, + -1.0, 1.0, 0.0, + 1.0, 1.0, 0.0, + // southeast triangle + -1.0, -1.0, 0.0, + 1.0, 1.0, 0.0, + 1.0, -1.0, 0.0 + ]; + bind_vertex_attrib(&ctx, position_index, 3, &positions); + + // set up a repainting routine + let (_, start_animation_loop, _) = create_raf(move || { + // get the time step + let time = performance.now(); + let time_step = 0.001*(time - last_time); + last_time = time; + + // get the navigation state + let pitch_up_val = pitch_up.get(); + let pitch_down_val = pitch_down.get(); + let yaw_right_val = yaw_right.get(); + let yaw_left_val = yaw_left.get(); + let roll_ccw_val = roll_ccw.get(); + let roll_cw_val = roll_cw.get(); + let zoom_in_val = zoom_in.get(); + let zoom_out_val = zoom_out.get(); + + // update the assembly's orientation + let ang_vel = { + let pitch = pitch_up_val - pitch_down_val; + let yaw = yaw_right_val - yaw_left_val; + let roll = roll_ccw_val - roll_cw_val; + if pitch != 0.0 || yaw != 0.0 || roll != 0.0 { + ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize() + } else { + Vector3::zeros() + } + }; + let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0); + rotation_sp.copy_from( + Rotation3::from_scaled_axis(time_step * ang_vel).matrix() + ); + orientation = &rotation * &orientation; + + // update the assembly's location + let zoom = zoom_out_val - zoom_in_val; + location_z *= (time_step * ZOOM_SPEED * zoom).exp(); + + if scene_changed.get() { + /* INSTRUMENTS */ + // measure mean frame interval + frames_since_last_sample += 1; + if frames_since_last_sample >= SAMPLE_PERIOD { + mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64)); + last_sample_time = time; + frames_since_last_sample = 0; + } + + // find the map from construction space to world space + let location = { + let u = -location_z; + DMatrix::from_column_slice(5, 5, &[ + 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, u, + 0.0, 0.0, 2.0*u, 1.0, u*u, + 0.0, 0.0, 0.0, 0.0, 1.0 + ]) + }; + let construction_to_world = &location * &orientation; + + // update the construction + sphere_vec.clear(); + sphere_vec.push(&construction_to_world * DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25])); + sphere_vec.push(&construction_to_world * DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25])); + sphere_vec.push(&construction_to_world * DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625])); + color_vec.clear(); + color_vec.push([1.00_f32, 0.25_f32, 0.00_f32]); + color_vec.push([0.00_f32, 0.25_f32, 1.00_f32]); + color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); + + // 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 construction + ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_vec.len() as i32); + for n in 0..sphere_vec.len() { + let v = &sphere_vec[n]; + ctx.uniform3f( + sphere_sp_locs[n].as_ref(), + v[0] as f32, v[1] as f32, v[2] as f32 + ); + ctx.uniform2f( + sphere_lt_locs[n].as_ref(), + v[3] as f32, v[4] as f32 + ); + ctx.uniform3fv_with_f32_array( + color_locs[n].as_ref(), + &color_vec[n] + ); + } + + // pass the display parameters + ctx.uniform1f(opacity_loc.as_ref(), OPACITY); + ctx.uniform1f(highlight_loc.as_ref(), HIGHLIGHT); + ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); + ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); + + // draw the scene + ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); + + // clear the scene change flag + scene_changed.set(false); + } else { + frames_since_last_sample = 0; + mean_frame_interval.set(-1.0); + } + }); + start_animation_loop(); + }); + view! { - /* [TO DO] switch back to integer-valued parameters when that becomes - possible again */ - canvas(width="750", height="750", tabindex="0") + /* TO DO */ + // switch back to integer-valued parameters when that becomes possible + // again + canvas(ref=display, width="750", height="750", tabindex="0") } } \ No newline at end of file diff --git a/app-proto/sketch-outline/src/identity.vert b/app-proto/sketch-outline/src/identity.vert new file mode 100644 index 0000000..183a65f --- /dev/null +++ b/app-proto/sketch-outline/src/identity.vert @@ -0,0 +1,7 @@ +#version 300 es + +in vec4 position; + +void main() { + gl_Position = position; +} \ No newline at end of file diff --git a/app-proto/sketch-outline/src/inversive.frag b/app-proto/sketch-outline/src/inversive.frag new file mode 100644 index 0000000..93bec50 --- /dev/null +++ b/app-proto/sketch-outline/src/inversive.frag @@ -0,0 +1,227 @@ +#version 300 es + +precision highp float; + +out vec4 outColor; + +// --- inversive geometry --- + +struct vecInv { + vec3 sp; + vec2 lt; +}; + +// --- uniforms --- + +// construction. the SPHERE_MAX array size seems to affect frame rate a lot, +// even though we should only be using the first few elements of each array +const int SPHERE_MAX = 200; +uniform int sphere_cnt; +uniform vecInv sphere_list[SPHERE_MAX]; +uniform vec3 color_list[SPHERE_MAX]; + +// view +uniform vec2 resolution; +uniform float shortdim; + +// controls +uniform float opacity; +uniform float highlight; +uniform int layer_threshold; +uniform bool debug_mode; + +// light and camera +const float focal_slope = 0.3; +const vec3 light_dir = normalize(vec3(2., 2., 1.)); +const float ixn_threshold = 0.005; +const float INTERIOR_DIMMING = 0.7; + +// --- sRGB --- + +// map colors from RGB space to sRGB space, as specified in the sRGB standard +// (IEC 61966-2-1:1999) +// +// https://www.color.org/sRGB.pdf +// https://www.color.org/chardata/rgb/srgb.xalter +// +// in RGB space, color value is proportional to light intensity, so linear +// color-vector interpolation corresponds to physical light mixing. in sRGB +// space, the color encoding used by many monitors, we use more of the value +// interval to represent low intensities, and less of the interval to represent +// high intensities. this improves color quantization + +float sRGB(float t) { + if (t <= 0.0031308) { + return 12.92*t; + } else { + return 1.055*pow(t, 5./12.) - 0.055; + } +} + +vec3 sRGB(vec3 color) { + return vec3(sRGB(color.r), sRGB(color.g), sRGB(color.b)); +} + +// --- shading --- + +struct taggedFrag { + int id; + vec4 color; + vec3 pt; + vec3 normal; +}; + +taggedFrag[2] sort(taggedFrag a, taggedFrag b) { + taggedFrag[2] result; + if (a.pt.z > b.pt.z) { + result[0] = a; + result[1] = b; + } else { + result[0] = b; + result[1] = a; + } + return result; +} + +taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, int id) { + // the expression for normal needs to be checked. it's supposed to give the + // negative gradient of the lorentz product between the impact point vector + // and the sphere vector with respect to the coordinates of the impact + // point. i calculated it in my head and decided that the result looked good + // enough for now + vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt); + + float incidence = dot(normal, light_dir); + float illum = mix(0.4, 1.0, max(incidence, 0.0)); + return taggedFrag(id, vec4(illum * base_color, opacity), pt, normal); +} + +// --- ray-casting --- + +// if `a/b` is less than this threshold, we approximate `a*u^2 + b*u + c` by +// the linear function `b*u + c` +const float DEG_THRESHOLD = 1e-9; + +// the depths, represented as multiples of `dir`, where the line generated by +// `dir` hits the sphere represented by `v`. if both depths are positive, the +// smaller one is returned in the first component. if only one depth is +// positive, it could be returned in either component +vec2 sphere_cast(vecInv v, vec3 dir) { + float a = -v.lt.s * dot(dir, dir); + float b = dot(v.sp, dir); + float c = -v.lt.t; + + float adjust = 4.*a*c/(b*b); + if (adjust < 1.) { + // 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` + float square_rect_ratio = 1. + sqrt(1. - adjust); + float lin_root = -(2.*c)/b / square_rect_ratio; + if (abs(a) > DEG_THRESHOLD * abs(b)) { + return vec2(lin_root, -b/(2.*a) * square_rect_ratio); + } else { + return vec2(lin_root, -1.); + } + } else { + // the line through `dir` misses the sphere completely + return vec2(-1., -1.); + } +} + +void main() { + vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; + vec3 dir = vec3(focal_slope * scr, -1.); + + // cast rays through the spheres + const int LAYER_MAX = 12; + taggedFrag frags [LAYER_MAX]; + int layer_cnt = 0; + for (int id = 0; id < sphere_cnt; ++id) { + // find out where the ray hits the sphere + vec2 hit_depths = sphere_cast(sphere_list[id], dir); + + // insertion-sort the fragments we hit into the fragment list + float dimming = 1.; + for (int side = 0; side < 2; ++side) { + float hit_z = -hit_depths[side]; + if (0. > hit_z) { + for (int layer = layer_cnt; layer >= 0; --layer) { + if (layer < 1 || frags[layer-1].pt.z >= hit_z) { + // we're not as close to the screen as the fragment + // before the empty slot, so insert here + if (layer < LAYER_MAX) { + frags[layer] = sphere_shading( + sphere_list[id], + hit_depths[side] * dir, + dimming * color_list[id], + id + ); + } + break; + } else { + // we're closer to the screen than the fragment before + // the empty slot, so move that fragment into the empty + // slot + frags[layer] = frags[layer-1]; + } + } + layer_cnt = min(layer_cnt + 1, LAYER_MAX); + dimming = INTERIOR_DIMMING; + } + } + } + + /* DEBUG */ + // in debug mode, show the layer count instead of the shaded image + if (debug_mode) { + // at the bottom of the screen, show the color scale instead of the + // layer count + if (gl_FragCoord.y < 10.) layer_cnt = int(16. * gl_FragCoord.x / resolution.x); + + // convert number to color + ivec3 bits = layer_cnt / ivec3(1, 2, 4); + vec3 color = mod(vec3(bits), 2.); + if (layer_cnt % 16 >= 8) { + color = mix(color, vec3(0.5), 0.5); + } + outColor = vec4(color, 1.); + return; + } + + // highlight intersections and cusps + for (int i = layer_cnt-1; i >= 1; --i) { + // intersections + taggedFrag frag0 = frags[i]; + taggedFrag frag1 = frags[i-1]; + float ixn_sin = length(cross(frag0.normal, frag1.normal)); + vec3 disp = frag0.pt - frag1.pt; + float ixn_dist = max( + abs(dot(frag1.normal, disp)), + abs(dot(frag0.normal, disp)) + ) / ixn_sin; + float ixn_highlight = 0.5 * highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); + frags[i].color = mix(frags[i].color, vec4(1.), ixn_highlight); + frags[i-1].color = mix(frags[i-1].color, vec4(1.), ixn_highlight); + + // cusps + float cusp_cos = abs(dot(dir, frag0.normal)); + float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[frag0.id].lt.s); + float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); + frags[i].color = mix(frags[i].color, vec4(1.), cusp_highlight); + } + + // composite the sphere fragments + vec3 color = vec3(0.); + for (int i = layer_cnt-1; i >= layer_threshold; --i) { + if (frags[i].pt.z < 0.) { + vec4 frag_color = frags[i].color; + color = mix(color, frag_color.rgb, frag_color.a); + } + } + outColor = vec4(sRGB(color), 1.); +} \ No newline at end of file diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index 9e74d28..6c6608f 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -29,14 +29,14 @@ fn main() { Element { id: String::from("wing_b"), label: String::from("Wing B"), - color: [1.00_f32, 0.25_f32, 0.00_f32], + color: [0.00_f32, 0.25_f32, 1.00_f32], rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]) }, Element { id: String::from("central"), label: String::from("Central"), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.25, -1.0]) + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.25, 1.0]) } ]) } From f47be08d9849abc027009e6c102d983745016649 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 15 Sep 2024 11:31:22 -0700 Subject: [PATCH 061/126] Display: get the assembly from the app state --- app-proto/sketch-outline/src/display.rs | 35 +++++++++++-------------- app-proto/sketch-outline/src/main.rs | 2 +- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/sketch-outline/src/display.rs index b1bf6ea..59a72c9 100644 --- a/app-proto/sketch-outline/src/display.rs +++ b/app-proto/sketch-outline/src/display.rs @@ -1,5 +1,5 @@ use core::array; -use nalgebra::{DMatrix, DVector, Rotation3, Vector3}; +use nalgebra::{DMatrix, Rotation3, Vector3}; use sycamore::{prelude::*, motion::create_raf}; use web_sys::{ console, @@ -11,6 +11,8 @@ use web_sys::{ wasm_bindgen::{JsCast, JsValue} }; +use crate::AppState; + fn compile_shader( context: &WebGl2RenderingContext, shader_type: u32, @@ -81,6 +83,8 @@ fn bind_vertex_attrib( #[component] pub fn Display() -> View { + let state = use_context::(); + // canvas let display = create_node_ref(); @@ -104,12 +108,6 @@ pub fn Display() -> View { let mean_frame_interval = create_signal(0.0); on_mount(move || { - /* SCAFFOLDING */ - /* create list of construction elements */ - const SPHERE_MAX: usize = 200; - let mut sphere_vec = Vec::>::new(); - let mut color_vec = Vec::<[f32; 3]>::new(); - // timing let mut last_time = 0.0; @@ -183,6 +181,7 @@ pub fn Display() -> View { ); // find indices of 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 sphere_sp_locs = get_uniform_array_locations::( @@ -280,15 +279,11 @@ pub fn Display() -> View { }; let construction_to_world = &location * &orientation; - // update the construction - sphere_vec.clear(); - sphere_vec.push(&construction_to_world * DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25])); - sphere_vec.push(&construction_to_world * DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25])); - sphere_vec.push(&construction_to_world * DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625])); - color_vec.clear(); - color_vec.push([1.00_f32, 0.25_f32, 0.00_f32]); - color_vec.push([0.00_f32, 0.25_f32, 1.00_f32]); - color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); + // get the construction + let elements = state.assembly.elements.get_clone(); + let element_iter = (&elements).into_iter(); + let reps_world: Vec<_> = element_iter.clone().map(|elt| &construction_to_world * &elt.rep).collect(); + let colors: Vec<_> = element_iter.map(|elt| elt.color).collect(); // set the resolution let width = canvas.width() as f32; @@ -297,9 +292,9 @@ pub fn Display() -> View { ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); // pass the construction - ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_vec.len() as i32); - for n in 0..sphere_vec.len() { - let v = &sphere_vec[n]; + ctx.uniform1i(sphere_cnt_loc.as_ref(), elements.len() as i32); + for n in 0..reps_world.len() { + let v = &reps_world[n]; ctx.uniform3f( sphere_sp_locs[n].as_ref(), v[0] as f32, v[1] as f32, v[2] as f32 @@ -310,7 +305,7 @@ pub fn Display() -> View { ); ctx.uniform3fv_with_f32_array( color_locs[n].as_ref(), - &color_vec[n] + &colors[n] ); } diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index 6c6608f..ee7682a 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -36,7 +36,7 @@ fn main() { id: String::from("central"), label: String::from("Central"), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.25, 1.0]) + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]) } ]) } From 7cb01bab82db1dc463718bd1e1ae1bc8c954e3d8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 15 Sep 2024 11:38:32 -0700 Subject: [PATCH 062/126] Ray-caster: drop outdated performance comment The size of the internal fragment arrays is what really matters, as discussed in the "Display" page on the wiki. --- app-proto/inversive-display/src/inversive.frag | 3 +-- app-proto/sketch-outline/src/inversive.frag | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index 93bec50..ae3b930 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -13,8 +13,7 @@ struct vecInv { // --- uniforms --- -// construction. the SPHERE_MAX array size seems to affect frame rate a lot, -// even though we should only be using the first few elements of each array +// construction const int SPHERE_MAX = 200; uniform int sphere_cnt; uniform vecInv sphere_list[SPHERE_MAX]; diff --git a/app-proto/sketch-outline/src/inversive.frag b/app-proto/sketch-outline/src/inversive.frag index 93bec50..ae3b930 100644 --- a/app-proto/sketch-outline/src/inversive.frag +++ b/app-proto/sketch-outline/src/inversive.frag @@ -13,8 +13,7 @@ struct vecInv { // --- uniforms --- -// construction. the SPHERE_MAX array size seems to affect frame rate a lot, -// even though we should only be using the first few elements of each array +// construction const int SPHERE_MAX = 200; uniform int sphere_cnt; uniform vecInv sphere_list[SPHERE_MAX]; From e2d3af2867024057400f2104d28b2b794fdfae5c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 15 Sep 2024 11:41:16 -0700 Subject: [PATCH 063/126] Display: say "assembly" instead of "construction" Update variable names and comments in code from the display prototype. --- app-proto/sketch-outline/src/display.rs | 10 +++++----- app-proto/sketch-outline/src/inversive.frag | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/sketch-outline/src/display.rs index 59a72c9..efe42b2 100644 --- a/app-proto/sketch-outline/src/display.rs +++ b/app-proto/sketch-outline/src/display.rs @@ -266,7 +266,7 @@ pub fn Display() -> View { frames_since_last_sample = 0; } - // find the map from construction space to world space + // find the map from assembly space to world space let location = { let u = -location_z; DMatrix::from_column_slice(5, 5, &[ @@ -277,12 +277,12 @@ pub fn Display() -> View { 0.0, 0.0, 0.0, 0.0, 1.0 ]) }; - let construction_to_world = &location * &orientation; + let assembly_to_world = &location * &orientation; - // get the construction + // get the assembly let elements = state.assembly.elements.get_clone(); let element_iter = (&elements).into_iter(); - let reps_world: Vec<_> = element_iter.clone().map(|elt| &construction_to_world * &elt.rep).collect(); + let reps_world: Vec<_> = element_iter.clone().map(|elt| &assembly_to_world * &elt.rep).collect(); let colors: Vec<_> = element_iter.map(|elt| elt.color).collect(); // set the resolution @@ -291,7 +291,7 @@ pub fn Display() -> View { ctx.uniform2f(resolution_loc.as_ref(), width, height); ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); - // pass the construction + // pass the assembly ctx.uniform1i(sphere_cnt_loc.as_ref(), elements.len() as i32); for n in 0..reps_world.len() { let v = &reps_world[n]; diff --git a/app-proto/sketch-outline/src/inversive.frag b/app-proto/sketch-outline/src/inversive.frag index ae3b930..d75377e 100644 --- a/app-proto/sketch-outline/src/inversive.frag +++ b/app-proto/sketch-outline/src/inversive.frag @@ -13,7 +13,7 @@ struct vecInv { // --- uniforms --- -// construction +// assembly const int SPHERE_MAX = 200; uniform int sphere_cnt; uniform vecInv sphere_list[SPHERE_MAX]; From 93190e99da52c398ac568385b6d0ec44ccdc520a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 15 Sep 2024 11:54:39 -0700 Subject: [PATCH 064/126] Display: bring in keyboard navigation code --- app-proto/sketch-outline/src/display.rs | 75 ++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/sketch-outline/src/display.rs index efe42b2..f3804f6 100644 --- a/app-proto/sketch-outline/src/display.rs +++ b/app-proto/sketch-outline/src/display.rs @@ -4,6 +4,7 @@ use sycamore::{prelude::*, motion::create_raf}; use web_sys::{ console, window, + KeyboardEvent, WebGl2RenderingContext, WebGlProgram, WebGlShader, @@ -319,7 +320,16 @@ pub fn Display() -> View { ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); // clear the scene change flag - scene_changed.set(false); + scene_changed.set( + pitch_up_val != 0.0 + || pitch_down_val != 0.0 + || yaw_left_val != 0.0 + || yaw_right_val != 0.0 + || roll_cw_val != 0.0 + || roll_ccw_val != 0.0 + || zoom_in_val != 0.0 + || zoom_out_val != 0.0 + ); } else { frames_since_last_sample = 0; mean_frame_interval.set(-1.0); @@ -328,10 +338,71 @@ pub fn Display() -> View { start_animation_loop(); }); + let set_nav_signal = move |event: KeyboardEvent, value: f64| { + let mut navigating = true; + let shift = event.shift_key(); + match event.key().as_str() { + "ArrowUp" if shift => zoom_in.set(value), + "ArrowDown" if shift => zoom_out.set(value), + "ArrowUp" => pitch_up.set(value), + "ArrowDown" => pitch_down.set(value), + "ArrowRight" if shift => roll_cw.set(value), + "ArrowLeft" if shift => roll_ccw.set(value), + "ArrowRight" => yaw_right.set(value), + "ArrowLeft" => yaw_left.set(value), + _ => navigating = false + }; + if navigating { + scene_changed.set(true); + event.prevent_default(); + } + }; + view! { /* TO DO */ // switch back to integer-valued parameters when that becomes possible // again - canvas(ref=display, width="750", height="750", tabindex="0") + canvas( + ref=display, + width="750", + height="750", + tabindex="0", + on:keydown=move |event: KeyboardEvent| { + if event.key() == "Shift" { + roll_cw.set(yaw_right.get()); + roll_ccw.set(yaw_left.get()); + zoom_in.set(pitch_up.get()); + zoom_out.set(pitch_down.get()); + yaw_right.set(0.0); + yaw_left.set(0.0); + pitch_up.set(0.0); + pitch_down.set(0.0); + } else { + set_nav_signal(event, 1.0); + } + }, + on:keyup=move |event: KeyboardEvent| { + if event.key() == "Shift" { + yaw_right.set(roll_cw.get()); + yaw_left.set(roll_ccw.get()); + pitch_up.set(zoom_in.get()); + pitch_down.set(zoom_out.get()); + roll_cw.set(0.0); + roll_ccw.set(0.0); + zoom_in.set(0.0); + zoom_out.set(0.0); + } else { + set_nav_signal(event, 0.0); + } + }, + on:blur=move |_| { + pitch_up.set(0.0); + pitch_down.set(0.0); + yaw_right.set(0.0); + yaw_left.set(0.0); + roll_ccw.set(0.0); + roll_cw.set(0.0); + } + ) } } \ No newline at end of file From a60624884ad7741d916a5dec84881f7d08568ce6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 16 Sep 2024 11:29:44 -0700 Subject: [PATCH 065/126] App: add element selection --- app-proto/sketch-outline/main.css | 4 ++-- app-proto/sketch-outline/src/assembly.rs | 4 ++++ app-proto/sketch-outline/src/display.rs | 13 ++++++++++++- app-proto/sketch-outline/src/main.rs | 9 ++++++--- app-proto/sketch-outline/src/outline.rs | 18 +++++++++++++++++- 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/app-proto/sketch-outline/main.css b/app-proto/sketch-outline/main.css index 9e8cdb4..cd7bc44 100644 --- a/app-proto/sketch-outline/main.css +++ b/app-proto/sketch-outline/main.css @@ -26,7 +26,7 @@ li { border-radius: 8px; } -li:focus { +li.selected { color: #fff; background-color: #666; } @@ -52,7 +52,7 @@ li > .elt-rep > div { background-color: #333; } -li:focus > .elt-rep > div { +li.selected > .elt-rep > div { background-color: #555; } diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/sketch-outline/src/assembly.rs index b53e38d..dded2a1 100644 --- a/app-proto/sketch-outline/src/assembly.rs +++ b/app-proto/sketch-outline/src/assembly.rs @@ -7,6 +7,10 @@ pub struct Element { pub label: String, pub color: [f32; 3], pub rep: DVector, + + /* TO DO */ + // does this belong in the element data? + pub selected: Signal } // a complete, view-independent description of an assembly diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/sketch-outline/src/display.rs index f3804f6..70beacf 100644 --- a/app-proto/sketch-outline/src/display.rs +++ b/app-proto/sketch-outline/src/display.rs @@ -101,6 +101,11 @@ pub fn Display() -> View { // change listener let scene_changed = create_signal(true); + create_effect(move || { + let elements = state.assembly.elements.get_clone(); + for elt in elements { elt.selected.track(); } + scene_changed.set(true); + }); /* INSTRUMENTS */ const SAMPLE_PERIOD: i32 = 60; @@ -284,7 +289,13 @@ pub fn Display() -> View { let elements = state.assembly.elements.get_clone(); let element_iter = (&elements).into_iter(); let reps_world: Vec<_> = element_iter.clone().map(|elt| &assembly_to_world * &elt.rep).collect(); - let colors: Vec<_> = element_iter.map(|elt| elt.color).collect(); + let colors: Vec<_> = element_iter.map(|elt| + if elt.selected.get() { + elt.color.map(|ch| 0.5 + 0.5*ch) + } else { + elt.color + } + ).collect(); // set the resolution let width = canvas.width() as f32; diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index ee7682a..06cc43d 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -24,19 +24,22 @@ fn main() { id: String::from("wing_a"), label: String::from("Wing A"), color: [1.00_f32, 0.25_f32, 0.00_f32], - rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]) + rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]), + selected: create_signal(false) }, Element { id: String::from("wing_b"), label: String::from("Wing B"), color: [0.00_f32, 0.25_f32, 1.00_f32], - rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]) + rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]), + selected: create_signal(false) }, Element { id: String::from("central"), label: String::from("Central"), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]) + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]), + selected: create_signal(false) } ]) } diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs index 546b272..5d650b3 100644 --- a/app-proto/sketch-outline/src/outline.rs +++ b/app-proto/sketch-outline/src/outline.rs @@ -1,5 +1,6 @@ use itertools::Itertools; use sycamore::{prelude::*, web::tags::div}; +use web_sys::KeyboardEvent; use crate::AppState; @@ -21,6 +22,9 @@ pub fn Outline() -> View { Keyed( list=elements_sorted, view=|elt| { + let class = create_memo(move || + if elt.selected.get() { "selected" } else { "" } + ); let label = elt.label.clone(); let rep_components = elt.rep.iter().map(|u| { let u_coord = u.to_string().replace("-", "\u{2212}"); @@ -29,7 +33,19 @@ pub fn Outline() -> View { view! { /* [TO DO] switch to integer-valued parameters whenever that becomes possible again */ - li(tabindex="0") { + li( + class=class.get(), + tabindex="0", + on:click=move |_| { + elt.selected.set_fn(|sel| !sel); + }, + on:keydown=move |event: KeyboardEvent| { + if event.key() == "Enter" { + elt.selected.set_fn(|sel| !sel); + event.prevent_default(); + } + } + ) { div(class="elt-label") { (label) } div(class="elt-rep") { (rep_components) } } From 96afad0c97ccfc5f5fca6825d50dd5e8079fd8ac Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 16 Sep 2024 15:46:45 -0700 Subject: [PATCH 066/126] Display: highlight selected elements --- app-proto/sketch-outline/src/display.rs | 16 ++++++++++++---- app-proto/sketch-outline/src/inversive.frag | 12 ++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/sketch-outline/src/display.rs index 70beacf..373c857 100644 --- a/app-proto/sketch-outline/src/display.rs +++ b/app-proto/sketch-outline/src/display.rs @@ -199,10 +199,12 @@ pub fn Display() -> View { let color_locs = get_uniform_array_locations::( &ctx, &program, "color_list", None ); + let highlight_locs = get_uniform_array_locations::( + &ctx, &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 highlight_loc = ctx.get_uniform_location(&program, "highlight"); let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); let debug_mode_loc = ctx.get_uniform_location(&program, "debug_mode"); @@ -289,13 +291,16 @@ pub fn Display() -> View { let elements = state.assembly.elements.get_clone(); let element_iter = (&elements).into_iter(); let reps_world: Vec<_> = element_iter.clone().map(|elt| &assembly_to_world * &elt.rep).collect(); - let colors: Vec<_> = element_iter.map(|elt| + let colors: Vec<_> = element_iter.clone().map(|elt| if elt.selected.get() { - elt.color.map(|ch| 0.5 + 0.5*ch) + elt.color.map(|ch| 0.2 + 0.8*ch) } else { elt.color } ).collect(); + let highlights: Vec<_> = element_iter.map(|elt| + if elt.selected.get() { 1.0_f32 } else { HIGHLIGHT } + ).collect(); // set the resolution let width = canvas.width() as f32; @@ -319,11 +324,14 @@ pub fn Display() -> View { color_locs[n].as_ref(), &colors[n] ); + ctx.uniform1f( + highlight_locs[n].as_ref(), + highlights[n] + ); } // pass the display parameters ctx.uniform1f(opacity_loc.as_ref(), OPACITY); - ctx.uniform1f(highlight_loc.as_ref(), HIGHLIGHT); ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); diff --git a/app-proto/sketch-outline/src/inversive.frag b/app-proto/sketch-outline/src/inversive.frag index d75377e..47743eb 100644 --- a/app-proto/sketch-outline/src/inversive.frag +++ b/app-proto/sketch-outline/src/inversive.frag @@ -18,6 +18,7 @@ const int SPHERE_MAX = 200; uniform int sphere_cnt; uniform vecInv sphere_list[SPHERE_MAX]; uniform vec3 color_list[SPHERE_MAX]; +uniform float highlight_list[SPHERE_MAX]; // view uniform vec2 resolution; @@ -25,7 +26,6 @@ uniform float shortdim; // controls uniform float opacity; -uniform float highlight; uniform int layer_threshold; uniform bool debug_mode; @@ -66,6 +66,7 @@ vec3 sRGB(vec3 color) { struct taggedFrag { int id; vec4 color; + float highlight; vec3 pt; vec3 normal; }; @@ -82,7 +83,7 @@ taggedFrag[2] sort(taggedFrag a, taggedFrag b) { return result; } -taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, int id) { +taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, float highlight, int id) { // the expression for normal needs to be checked. it's supposed to give the // negative gradient of the lorentz product between the impact point vector // and the sphere vector with respect to the coordinates of the impact @@ -92,7 +93,7 @@ taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, int id) { float incidence = dot(normal, light_dir); float illum = mix(0.4, 1.0, max(incidence, 0.0)); - return taggedFrag(id, vec4(illum * base_color, opacity), pt, normal); + return taggedFrag(id, vec4(illum * base_color, opacity), highlight, pt, normal); } // --- ray-casting --- @@ -158,6 +159,7 @@ void main() { sphere_list[id], hit_depths[side] * dir, dimming * color_list[id], + highlight_list[id], id ); } @@ -203,13 +205,15 @@ void main() { abs(dot(frag1.normal, disp)), abs(dot(frag0.normal, disp)) ) / ixn_sin; - float ixn_highlight = 0.5 * highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); + float max_highlight = max(frags[i].highlight, frags[i-1].highlight); + float ixn_highlight = 0.5 * max_highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); frags[i].color = mix(frags[i].color, vec4(1.), ixn_highlight); frags[i-1].color = mix(frags[i-1].color, vec4(1.), ixn_highlight); // cusps float cusp_cos = abs(dot(dir, frag0.normal)); float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[frag0.id].lt.s); + float highlight = frags[i].highlight; float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); frags[i].color = mix(frags[i].color, vec4(1.), cusp_highlight); } From 96f8b6b5f34c5374b4aebab3cf5d9e582e37ecb3 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 19 Sep 2024 16:08:55 -0700 Subject: [PATCH 067/126] App: store selection in hash map Switch `Assembly.elements` to a hash map too, since that's probably closer to what we'll want in the future. --- app-proto/sketch-outline/Cargo.toml | 1 + app-proto/sketch-outline/src/assembly.rs | 9 ++-- app-proto/sketch-outline/src/display.rs | 13 +++-- app-proto/sketch-outline/src/main.rs | 69 ++++++++++++++---------- app-proto/sketch-outline/src/outline.rs | 50 ++++++++++++----- 5 files changed, 89 insertions(+), 53 deletions(-) diff --git a/app-proto/sketch-outline/Cargo.toml b/app-proto/sketch-outline/Cargo.toml index 7d1d608..35d199b 100644 --- a/app-proto/sketch-outline/Cargo.toml +++ b/app-proto/sketch-outline/Cargo.toml @@ -11,6 +11,7 @@ default = ["console_error_panic_hook"] itertools = "0.13.0" js-sys = "0.3.70" nalgebra = "0.33.0" +rustc-hash = "2.0.0" sycamore = "0.9.0-beta.3" # The `console_error_panic_hook` crate provides better debugging of panics by diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/sketch-outline/src/assembly.rs index dded2a1..fa7fc3e 100644 --- a/app-proto/sketch-outline/src/assembly.rs +++ b/app-proto/sketch-outline/src/assembly.rs @@ -1,4 +1,5 @@ use nalgebra::DVector; +use rustc_hash::FxHashMap; use sycamore::reactive::Signal; #[derive(Clone, PartialEq)] @@ -6,16 +7,12 @@ pub struct Element { pub id: String, pub label: String, pub color: [f32; 3], - pub rep: DVector, - - /* TO DO */ - // does this belong in the element data? - pub selected: Signal + pub rep: DVector } // a complete, view-independent description of an assembly #[derive(Clone)] pub struct Assembly { // the order of the elements is arbitrary, and it could change at any time - pub elements: Signal> + pub elements: Signal> } \ No newline at end of file diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/sketch-outline/src/display.rs index 373c857..2d0900d 100644 --- a/app-proto/sketch-outline/src/display.rs +++ b/app-proto/sketch-outline/src/display.rs @@ -102,8 +102,7 @@ pub fn Display() -> View { // change listener let scene_changed = create_signal(true); create_effect(move || { - let elements = state.assembly.elements.get_clone(); - for elt in elements { elt.selected.track(); } + state.selection.track(); scene_changed.set(true); }); @@ -289,17 +288,21 @@ pub fn Display() -> View { // get the assembly let elements = state.assembly.elements.get_clone(); - let element_iter = (&elements).into_iter(); + let element_iter = (&elements).values(); let reps_world: Vec<_> = element_iter.clone().map(|elt| &assembly_to_world * &elt.rep).collect(); let colors: Vec<_> = element_iter.clone().map(|elt| - if elt.selected.get() { + if state.selection.with(|sel| sel.contains(&elt.id)) { elt.color.map(|ch| 0.2 + 0.8*ch) } else { elt.color } ).collect(); let highlights: Vec<_> = element_iter.map(|elt| - if elt.selected.get() { 1.0_f32 } else { HIGHLIGHT } + if state.selection.with(|sel| sel.contains(&elt.id)) { + 1.0_f32 + } else { + HIGHLIGHT + } ).collect(); // set the resolution diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index 06cc43d..0d1b0c7 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -3,6 +3,7 @@ mod display; mod outline; use nalgebra::DVector; +use rustc_hash::{FxHashMap, FxHashSet}; use sycamore::prelude::*; use assembly::{Assembly, Element}; @@ -11,40 +12,52 @@ use outline::Outline; #[derive(Clone)] struct AppState { - assembly: Assembly + assembly: Assembly, + selection: Signal> } fn main() { sycamore::render(|| { - provide_context( - AppState { - assembly: Assembly { - elements: create_signal(vec![ - Element { - id: String::from("wing_a"), - label: String::from("Wing A"), - color: [1.00_f32, 0.25_f32, 0.00_f32], - rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]), - selected: create_signal(false) - }, - Element { - id: String::from("wing_b"), - label: String::from("Wing B"), - color: [0.00_f32, 0.25_f32, 1.00_f32], - rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]), - selected: create_signal(false) - }, - Element { - id: String::from("central"), - label: String::from("Central"), - color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]), - selected: create_signal(false) - } - ]) + let state = AppState { + assembly: Assembly { + elements: create_signal(FxHashMap::default()) + }, + selection: create_signal(FxHashSet::default()) + }; + state.assembly.elements.update( + |elts| elts.insert( + "wing_a".to_string(), + Element { + id: String::from("wing_a"), + label: String::from("Wing A"), + color: [1.00_f32, 0.25_f32, 0.00_f32], + rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]) } - } + ) ); + state.assembly.elements.update( + |elts| elts.insert( + "wing_b".to_string(), + Element { + id: String::from("wing_b"), + label: String::from("Wing B"), + color: [0.00_f32, 0.25_f32, 1.00_f32], + rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]) + }, + ) + ); + state.assembly.elements.update( + |elts| elts.insert( + "central".to_string(), + Element { + id: String::from("central"), + label: String::from("Central"), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]) + } + ) + ); + provide_context(state); view! { Outline {} diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs index 5d650b3..13e506e 100644 --- a/app-proto/sketch-outline/src/outline.rs +++ b/app-proto/sketch-outline/src/outline.rs @@ -6,25 +6,33 @@ use crate::AppState; #[component] pub fn Outline() -> View { - let state = use_context::(); - // sort the elements alphabetically by ID - let elements_sorted = create_memo(move || + let elements_sorted = create_memo(|| { + let state = use_context::(); state.assembly.elements .get_clone() .into_iter() - .sorted_by_key(|elt| elt.id.clone()) + .sorted_by_key(|(id, _)| id.clone()) + .map(|(_, elt)| elt) .collect() - ); + }); view! { ul { Keyed( list=elements_sorted, view=|elt| { - let class = create_memo(move || - if elt.selected.get() { "selected" } else { "" } - ); + let state = use_context::(); + let class = create_memo({ + let id = elt.id.clone(); + move || { + if state.selection.with(|sel| sel.contains(&id)) { + "selected" + } else { + "" + } + } + }); let label = elt.label.clone(); let rep_components = elt.rep.iter().map(|u| { let u_coord = u.to_string().replace("-", "\u{2212}"); @@ -36,13 +44,27 @@ pub fn Outline() -> View { li( class=class.get(), tabindex="0", - on:click=move |_| { - elt.selected.set_fn(|sel| !sel); + on:click={ + let id = elt.id.clone(); + move |_| { + state.selection.update(|sel| { + if !sel.remove(&id) { + sel.insert(id.clone()); + } + }); + } }, - on:keydown=move |event: KeyboardEvent| { - if event.key() == "Enter" { - elt.selected.set_fn(|sel| !sel); - event.prevent_default(); + on:keydown={ + let id = elt.id.clone(); + move |event: KeyboardEvent| { + if event.key() == "Enter" { + state.selection.update(|sel| { + if !sel.remove(&id) { + sel.insert(id.clone()); + } + }); + event.prevent_default(); + } } } ) { From 78f8ef8215af8aeec841598d2957f25f50061d01 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 19 Sep 2024 17:53:07 -0700 Subject: [PATCH 068/126] Outline: switch to single selection --- app-proto/sketch-outline/src/outline.rs | 42 ++++++++++++++++++------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs index 13e506e..8d13df1 100644 --- a/app-proto/sketch-outline/src/outline.rs +++ b/app-proto/sketch-outline/src/outline.rs @@ -1,6 +1,6 @@ use itertools::Itertools; use sycamore::{prelude::*, web::tags::div}; -use web_sys::KeyboardEvent; +use web_sys::{KeyboardEvent, MouseEvent}; use crate::AppState; @@ -18,7 +18,12 @@ pub fn Outline() -> View { }); view! { - ul { + ul( + on:click={ + let state = use_context::(); + move |_| state.selection.update(|sel| sel.clear()) + } + ) { Keyed( list=elements_sorted, view=|elt| { @@ -46,23 +51,38 @@ pub fn Outline() -> View { tabindex="0", on:click={ let id = elt.id.clone(); - move |_| { - state.selection.update(|sel| { - if !sel.remove(&id) { + move |event: MouseEvent| { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&id) { + sel.insert(id.clone()); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); sel.insert(id.clone()); - } - }); + }); + } + event.stop_propagation(); } }, on:keydown={ let id = elt.id.clone(); move |event: KeyboardEvent| { if event.key() == "Enter" { - state.selection.update(|sel| { - if !sel.remove(&id) { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&id) { + sel.insert(id.clone()); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); sel.insert(id.clone()); - } - }); + }); + } event.prevent_default(); } } From d121385c18026bb76176c33ff97669476e1016dc Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 02:21:45 -0700 Subject: [PATCH 069/126] App: store assembly elements in slab --- app-proto/sketch-outline/Cargo.toml | 1 + app-proto/sketch-outline/src/assembly.rs | 8 ++-- app-proto/sketch-outline/src/display.rs | 6 +-- app-proto/sketch-outline/src/main.rs | 49 ++++++++++++++---------- app-proto/sketch-outline/src/outline.rs | 21 +++++----- 5 files changed, 45 insertions(+), 40 deletions(-) diff --git a/app-proto/sketch-outline/Cargo.toml b/app-proto/sketch-outline/Cargo.toml index 35d199b..920469a 100644 --- a/app-proto/sketch-outline/Cargo.toml +++ b/app-proto/sketch-outline/Cargo.toml @@ -12,6 +12,7 @@ itertools = "0.13.0" js-sys = "0.3.70" nalgebra = "0.33.0" rustc-hash = "2.0.0" +slab = "0.4.9" sycamore = "0.9.0-beta.3" # The `console_error_panic_hook` crate provides better debugging of panics by diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/sketch-outline/src/assembly.rs index fa7fc3e..f34d373 100644 --- a/app-proto/sketch-outline/src/assembly.rs +++ b/app-proto/sketch-outline/src/assembly.rs @@ -1,5 +1,5 @@ use nalgebra::DVector; -use rustc_hash::FxHashMap; +use slab::Slab; use sycamore::reactive::Signal; #[derive(Clone, PartialEq)] @@ -7,12 +7,12 @@ pub struct Element { pub id: String, pub label: String, pub color: [f32; 3], - pub rep: DVector + pub rep: DVector, + pub key: usize } // a complete, view-independent description of an assembly #[derive(Clone)] pub struct Assembly { - // the order of the elements is arbitrary, and it could change at any time - pub elements: Signal> + pub elements: Signal> } \ No newline at end of file diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/sketch-outline/src/display.rs index 2d0900d..b0b9877 100644 --- a/app-proto/sketch-outline/src/display.rs +++ b/app-proto/sketch-outline/src/display.rs @@ -288,17 +288,17 @@ pub fn Display() -> View { // get the assembly let elements = state.assembly.elements.get_clone(); - let element_iter = (&elements).values(); + let element_iter = (&elements).into_iter().map(|(_, elt)| elt); let reps_world: Vec<_> = element_iter.clone().map(|elt| &assembly_to_world * &elt.rep).collect(); let colors: Vec<_> = element_iter.clone().map(|elt| - if state.selection.with(|sel| sel.contains(&elt.id)) { + if state.selection.with(|sel| sel.contains(&elt.key)) { elt.color.map(|ch| 0.2 + 0.8*ch) } else { elt.color } ).collect(); let highlights: Vec<_> = element_iter.map(|elt| - if state.selection.with(|sel| sel.contains(&elt.id)) { + if state.selection.with(|sel| sel.contains(&elt.key)) { 1.0_f32 } else { HIGHLIGHT diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index 0d1b0c7..f86abaf 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -3,7 +3,8 @@ mod display; mod outline; use nalgebra::DVector; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashSet; +use slab::Slab; use sycamore::prelude::*; use assembly::{Assembly, Element}; @@ -13,50 +14,56 @@ use outline::Outline; #[derive(Clone)] struct AppState { assembly: Assembly, - selection: Signal> + selection: Signal> } fn main() { sycamore::render(|| { let state = AppState { assembly: Assembly { - elements: create_signal(FxHashMap::default()) + elements: create_signal(Slab::new()) }, selection: create_signal(FxHashSet::default()) }; - state.assembly.elements.update( - |elts| elts.insert( - "wing_a".to_string(), + state.assembly.elements.update(|elts| { + let entry = elts.vacant_entry(); + let key = entry.key(); + entry.insert( Element { id: String::from("wing_a"), label: String::from("Wing A"), color: [1.00_f32, 0.25_f32, 0.00_f32], - rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]) + rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]), + key: key } - ) - ); - state.assembly.elements.update( - |elts| elts.insert( - "wing_b".to_string(), + ); + }); + state.assembly.elements.update(|elts| { + let entry = elts.vacant_entry(); + let key = entry.key(); + entry.insert( Element { id: String::from("wing_b"), label: String::from("Wing B"), color: [0.00_f32, 0.25_f32, 1.00_f32], - rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]) + rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]), + key: key }, - ) - ); - state.assembly.elements.update( - |elts| elts.insert( - "central".to_string(), + ); + }); + state.assembly.elements.update(|elts| { + let entry = elts.vacant_entry(); + let key = entry.key(); + entry.insert( Element { id: String::from("central"), label: String::from("Central"), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]) + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]), + key: key } - ) - ); + ); + }); provide_context(state); view! { diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs index 8d13df1..9913f32 100644 --- a/app-proto/sketch-outline/src/outline.rs +++ b/app-proto/sketch-outline/src/outline.rs @@ -12,8 +12,8 @@ pub fn Outline() -> View { state.assembly.elements .get_clone() .into_iter() - .sorted_by_key(|(id, _)| id.clone()) .map(|(_, elt)| elt) + .sorted_by_key(|elt| elt.id.clone()) .collect() }); @@ -29,9 +29,8 @@ pub fn Outline() -> View { view=|elt| { let state = use_context::(); let class = create_memo({ - let id = elt.id.clone(); move || { - if state.selection.with(|sel| sel.contains(&id)) { + if state.selection.with(|sel| sel.contains(&elt.key)) { "selected" } else { "" @@ -50,37 +49,35 @@ pub fn Outline() -> View { class=class.get(), tabindex="0", on:click={ - let id = elt.id.clone(); move |event: MouseEvent| { if event.shift_key() { state.selection.update(|sel| { - if !sel.remove(&id) { - sel.insert(id.clone()); + if !sel.remove(&elt.key) { + sel.insert(elt.key); } }); } else { state.selection.update(|sel| { sel.clear(); - sel.insert(id.clone()); + sel.insert(elt.key); }); } event.stop_propagation(); } }, on:keydown={ - let id = elt.id.clone(); move |event: KeyboardEvent| { if event.key() == "Enter" { if event.shift_key() { state.selection.update(|sel| { - if !sel.remove(&id) { - sel.insert(id.clone()); + if !sel.remove(&elt.key) { + sel.insert(elt.key); } }); } else { state.selection.update(|sel| { sel.clear(); - sel.insert(id.clone()); + sel.insert(elt.key); }); } event.prevent_default(); @@ -93,7 +90,7 @@ pub fn Outline() -> View { } } }, - key=|elt| elt.id.clone() + key=|elt| elt.key ) } } From 147e2758234b78ccfc2e24f6ca762b40e9f5d77b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 02:38:17 -0700 Subject: [PATCH 070/126] App: don't bother copying key into element When we access an element, we always have its key, either because the slab iterator yielded it along side the element or because we used it to get the element from the slab. --- app-proto/sketch-outline/src/assembly.rs | 3 +- app-proto/sketch-outline/src/display.rs | 12 ++++---- app-proto/sketch-outline/src/main.rs | 39 +++++++++--------------- app-proto/sketch-outline/src/outline.rs | 21 ++++++------- 4 files changed, 32 insertions(+), 43 deletions(-) diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/sketch-outline/src/assembly.rs index f34d373..79912b9 100644 --- a/app-proto/sketch-outline/src/assembly.rs +++ b/app-proto/sketch-outline/src/assembly.rs @@ -7,8 +7,7 @@ pub struct Element { pub id: String, pub label: String, pub color: [f32; 3], - pub rep: DVector, - pub key: usize + pub rep: DVector } // a complete, view-independent description of an assembly diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/sketch-outline/src/display.rs index b0b9877..2ac8ebf 100644 --- a/app-proto/sketch-outline/src/display.rs +++ b/app-proto/sketch-outline/src/display.rs @@ -288,17 +288,17 @@ pub fn Display() -> View { // get the assembly let elements = state.assembly.elements.get_clone(); - let element_iter = (&elements).into_iter().map(|(_, elt)| elt); - let reps_world: Vec<_> = element_iter.clone().map(|elt| &assembly_to_world * &elt.rep).collect(); - let colors: Vec<_> = element_iter.clone().map(|elt| - if state.selection.with(|sel| sel.contains(&elt.key)) { + let element_iter = (&elements).into_iter(); + let reps_world: Vec<_> = element_iter.clone().map(|(_, elt)| &assembly_to_world * &elt.rep).collect(); + let colors: Vec<_> = element_iter.clone().map(|(key, elt)| + if state.selection.with(|sel| sel.contains(&key)) { elt.color.map(|ch| 0.2 + 0.8*ch) } else { elt.color } ).collect(); - let highlights: Vec<_> = element_iter.map(|elt| - if state.selection.with(|sel| sel.contains(&elt.key)) { + let highlights: Vec<_> = element_iter.map(|(key, _)| + if state.selection.with(|sel| sel.contains(&key)) { 1.0_f32 } else { HIGHLIGHT diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index f86abaf..a43b020 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -25,45 +25,36 @@ fn main() { }, selection: create_signal(FxHashSet::default()) }; - state.assembly.elements.update(|elts| { - let entry = elts.vacant_entry(); - let key = entry.key(); - entry.insert( + state.assembly.elements.update( + |elts| elts.insert( Element { id: String::from("wing_a"), label: String::from("Wing A"), color: [1.00_f32, 0.25_f32, 0.00_f32], - rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]), - key: key + rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]) } - ); - }); - state.assembly.elements.update(|elts| { - let entry = elts.vacant_entry(); - let key = entry.key(); - entry.insert( + ) + ); + state.assembly.elements.update( + |elts| elts.insert( Element { id: String::from("wing_b"), label: String::from("Wing B"), color: [0.00_f32, 0.25_f32, 1.00_f32], - rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]), - key: key + rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]) }, - ); - }); - state.assembly.elements.update(|elts| { - let entry = elts.vacant_entry(); - let key = entry.key(); - entry.insert( + ) + ); + state.assembly.elements.update( + |elts| elts.insert( Element { id: String::from("central"), label: String::from("Central"), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]), - key: key + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]) } - ); - }); + ) + ); provide_context(state); view! { diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs index 9913f32..031f9bc 100644 --- a/app-proto/sketch-outline/src/outline.rs +++ b/app-proto/sketch-outline/src/outline.rs @@ -12,8 +12,7 @@ pub fn Outline() -> View { state.assembly.elements .get_clone() .into_iter() - .map(|(_, elt)| elt) - .sorted_by_key(|elt| elt.id.clone()) + .sorted_by_key(|(_, elt)| elt.id.clone()) .collect() }); @@ -26,11 +25,11 @@ pub fn Outline() -> View { ) { Keyed( list=elements_sorted, - view=|elt| { + view=|(key, elt)| { let state = use_context::(); let class = create_memo({ move || { - if state.selection.with(|sel| sel.contains(&elt.key)) { + if state.selection.with(|sel| sel.contains(&key)) { "selected" } else { "" @@ -52,14 +51,14 @@ pub fn Outline() -> View { move |event: MouseEvent| { if event.shift_key() { state.selection.update(|sel| { - if !sel.remove(&elt.key) { - sel.insert(elt.key); + if !sel.remove(&key) { + sel.insert(key); } }); } else { state.selection.update(|sel| { sel.clear(); - sel.insert(elt.key); + sel.insert(key); }); } event.stop_propagation(); @@ -70,14 +69,14 @@ pub fn Outline() -> View { if event.key() == "Enter" { if event.shift_key() { state.selection.update(|sel| { - if !sel.remove(&elt.key) { - sel.insert(elt.key); + if !sel.remove(&key) { + sel.insert(key); } }); } else { state.selection.update(|sel| { sel.clear(); - sel.insert(elt.key); + sel.insert(key); }); } event.prevent_default(); @@ -90,7 +89,7 @@ pub fn Outline() -> View { } } }, - key=|elt| elt.key + key=|(key, _)| key.clone() ) } } From 050e2373a64db94caebb4322fb7c70943186387b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 14:05:40 -0700 Subject: [PATCH 071/126] App: store constraints Draft listing of constraints in outline view. --- app-proto/sketch-outline/main.css | 43 +++++++++------ app-proto/sketch-outline/src/assembly.rs | 12 ++++- app-proto/sketch-outline/src/main.rs | 17 ++++-- app-proto/sketch-outline/src/outline.rs | 68 ++++++++++++++---------- 4 files changed, 89 insertions(+), 51 deletions(-) diff --git a/app-proto/sketch-outline/main.css b/app-proto/sketch-outline/main.css index cd7bc44..fbccab7 100644 --- a/app-proto/sketch-outline/main.css +++ b/app-proto/sketch-outline/main.css @@ -7,7 +7,7 @@ body { /* outline */ -ul { +#outline { float: left; width: 450px; height: 750px; @@ -19,32 +19,35 @@ ul { } li { - display: flex; - padding: 3px; list-style-type: none; - background-color: #444; - border-radius: 8px; -} - -li.selected { - color: #fff; - background-color: #666; } li:not(:last-child) { margin-bottom: 8px; } -li > .elt-label { +.elt { + display: flex; + padding: 3px; + background-color: #444; + border-radius: 8px; +} + +.elt.selected { + color: #fff; + background-color: #666; +} + +.elt > .elt-label { flex-grow: 1; padding: 2px 0px 2px 4px; } -li > .elt-rep { +.elt > .elt-rep { display: flex; } -li > .elt-rep > div { +.elt > .elt-rep > div { padding: 2px; margin-left: 3px; text-align: center; @@ -52,18 +55,26 @@ li > .elt-rep > div { background-color: #333; } -li.selected > .elt-rep > div { +.elt.selected > .elt-rep > div { background-color: #555; } -li > .elt-rep > div:first-child { +.elt-rep > div:first-child { border-radius: 6px 0px 0px 6px; } -li > .elt-rep > div:last-child { +.elt-rep > div:last-child { border-radius: 0px 6px 6px 0px; } +.constraints > li { + margin-top: 4px; + margin-bottom: 4px; + padding: 5px; + background-color: #444; + border-radius: 8px; +} + /* display */ canvas { diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/sketch-outline/src/assembly.rs index 79912b9..4cd769c 100644 --- a/app-proto/sketch-outline/src/assembly.rs +++ b/app-proto/sketch-outline/src/assembly.rs @@ -1,4 +1,5 @@ use nalgebra::DVector; +use rustc_hash::FxHashSet; use slab::Slab; use sycamore::reactive::Signal; @@ -7,11 +8,18 @@ pub struct Element { pub id: String, pub label: String, pub color: [f32; 3], - pub rep: DVector + pub rep: DVector, + pub constraints: FxHashSet +} + +pub struct Constraint { + pub args: (usize, usize), + pub rep: f64 } // a complete, view-independent description of an assembly #[derive(Clone)] pub struct Assembly { - pub elements: Signal> + pub elements: Signal>, + pub constraints: Signal> } \ No newline at end of file diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index a43b020..20ae651 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -21,7 +21,8 @@ fn main() { sycamore::render(|| { let state = AppState { assembly: Assembly { - elements: create_signal(Slab::new()) + elements: create_signal(Slab::new()), + constraints: create_signal(Slab::new()) }, selection: create_signal(FxHashSet::default()) }; @@ -31,7 +32,13 @@ fn main() { id: String::from("wing_a"), label: String::from("Wing A"), color: [1.00_f32, 0.25_f32, 0.00_f32], - rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]) + rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]), + constraints: { + let mut set = FxHashSet::default(); + set.insert(1); + set.insert(2); + set + } } ) ); @@ -41,7 +48,8 @@ fn main() { id: String::from("wing_b"), label: String::from("Wing B"), color: [0.00_f32, 0.25_f32, 1.00_f32], - rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]) + rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]), + constraints: FxHashSet::default() }, ) ); @@ -51,7 +59,8 @@ fn main() { id: String::from("central"), label: String::from("Central"), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]) + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]), + constraints: FxHashSet::default() } ) ); diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs index 031f9bc..4cb3901 100644 --- a/app-proto/sketch-outline/src/outline.rs +++ b/app-proto/sketch-outline/src/outline.rs @@ -18,6 +18,7 @@ pub fn Outline() -> View { view! { ul( + id="outline", on:click={ let state = use_context::(); move |_| state.selection.update(|sel| sel.clear()) @@ -30,9 +31,9 @@ pub fn Outline() -> View { let class = create_memo({ move || { if state.selection.with(|sel| sel.contains(&key)) { - "selected" + "elt selected" } else { - "" + "elt" } } }); @@ -44,29 +45,12 @@ pub fn Outline() -> View { view! { /* [TO DO] switch to integer-valued parameters whenever that becomes possible again */ - li( - class=class.get(), - tabindex="0", - on:click={ - move |event: MouseEvent| { - if event.shift_key() { - state.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); - } - }); - } else { - state.selection.update(|sel| { - sel.clear(); - sel.insert(key); - }); - } - event.stop_propagation(); - } - }, - on:keydown={ - move |event: KeyboardEvent| { - if event.key() == "Enter" { + li { + div( + class=class.get(), + tabindex="0", + on:click={ + move |event: MouseEvent| { if event.shift_key() { state.selection.update(|sel| { if !sel.remove(&key) { @@ -79,13 +63,39 @@ pub fn Outline() -> View { sel.insert(key); }); } - event.prevent_default(); + event.stop_propagation(); + } + }, + on:keydown={ + move |event: KeyboardEvent| { + if event.key() == "Enter" { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + event.prevent_default(); + } } } + ) { + div(class="elt-label") { (label) } + div(class="elt-rep") { (rep_components) } + } + ul(class="constraints") { + Keyed( + list=elt.constraints.into_iter().collect::>(), + view=|c_key: usize| view! { li { (c_key.to_string()) } }, + key=|c_key| c_key.clone() + ) } - ) { - div(class="elt-label") { (label) } - div(class="elt-rep") { (rep_components) } } } }, From 4a24a019285fb09ddba459b301c134bbd95d571c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 14:40:31 -0700 Subject: [PATCH 072/126] App: insert constraints consistently Also, write constructors for state objects. --- app-proto/sketch-outline/src/assembly.rs | 20 +++++++++++++- app-proto/sketch-outline/src/main.rs | 34 +++++++++++++----------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/sketch-outline/src/assembly.rs index 4cd769c..a4c9a0f 100644 --- a/app-proto/sketch-outline/src/assembly.rs +++ b/app-proto/sketch-outline/src/assembly.rs @@ -1,7 +1,7 @@ use nalgebra::DVector; use rustc_hash::FxHashSet; use slab::Slab; -use sycamore::reactive::Signal; +use sycamore::prelude::*; #[derive(Clone, PartialEq)] pub struct Element { @@ -22,4 +22,22 @@ pub struct Constraint { pub struct Assembly { pub elements: Signal>, pub constraints: Signal> +} + +impl Assembly { + pub fn new() -> Assembly { + Assembly { + elements: create_signal(Slab::new()), + constraints: create_signal(Slab::new()) + } + } + + pub fn insert_constraint(&self, constraint: Constraint) { + let args = constraint.args; + let key = self.constraints.update(|csts| csts.insert(constraint)); + self.elements.update(|elts| { + elts[args.0].constraints.insert(key); + elts[args.1].constraints.insert(key); + }) + } } \ No newline at end of file diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index 20ae651..b86f979 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -7,7 +7,7 @@ use rustc_hash::FxHashSet; use slab::Slab; use sycamore::prelude::*; -use assembly::{Assembly, Element}; +use assembly::{Assembly, Constraint, Element}; use display::Display; use outline::Outline; @@ -17,32 +17,30 @@ struct AppState { selection: Signal> } +impl AppState { + fn new() -> AppState { + AppState { + assembly: Assembly::new(), + selection: create_signal(FxHashSet::default()) + } + } +} + fn main() { sycamore::render(|| { - let state = AppState { - assembly: Assembly { - elements: create_signal(Slab::new()), - constraints: create_signal(Slab::new()) - }, - selection: create_signal(FxHashSet::default()) - }; - state.assembly.elements.update( + let state = AppState::new(); + let key_a = state.assembly.elements.update( |elts| elts.insert( Element { id: String::from("wing_a"), label: String::from("Wing A"), color: [1.00_f32, 0.25_f32, 0.00_f32], rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]), - constraints: { - let mut set = FxHashSet::default(); - set.insert(1); - set.insert(2); - set - } + constraints: FxHashSet::default() } ) ); - state.assembly.elements.update( + let key_b = state.assembly.elements.update( |elts| elts.insert( Element { id: String::from("wing_b"), @@ -64,6 +62,10 @@ fn main() { } ) ); + state.assembly.insert_constraint(Constraint { + args: (key_a, key_b), + rep: 0.5 + }); provide_context(state); view! { From edee153e37480a013a78eb0709dbde199ea5d91a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 23:50:16 -0700 Subject: [PATCH 073/126] App: remove unused import --- app-proto/sketch-outline/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/sketch-outline/src/main.rs index b86f979..0466724 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -4,7 +4,6 @@ mod outline; use nalgebra::DVector; use rustc_hash::FxHashSet; -use slab::Slab; use sycamore::prelude::*; use assembly::{Assembly, Constraint, Element}; From 7709c61f7112579b607511092428a09a23ce6ce2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 23:55:07 -0700 Subject: [PATCH 074/126] Outline: spruce up styling Use `details` elements to hide and show constraints. --- app-proto/sketch-outline/main.css | 95 +++++++++--------- app-proto/sketch-outline/src/outline.rs | 125 +++++++++++++++--------- 2 files changed, 123 insertions(+), 97 deletions(-) diff --git a/app-proto/sketch-outline/main.css b/app-proto/sketch-outline/main.css index fbccab7..c3317ea 100644 --- a/app-proto/sketch-outline/main.css +++ b/app-proto/sketch-outline/main.css @@ -1,6 +1,5 @@ body { - margin-left: 20px; - margin-top: 20px; + margin: 0px; color: #fcfcfc; background-color: #222; } @@ -10,76 +9,72 @@ body { #outline { float: left; width: 450px; - height: 750px; + height: 100vh; margin: 0px; - padding: 8px; - border: 1px solid #555; - border-radius: 16px; - box-sizing: border-box; + padding: 0px; + border-width: 0px 1px 0px 0px; + border-style: solid; + border-color: #555; + overflow-y: scroll; } -li { - list-style-type: none; -} - -li:not(:last-child) { - margin-bottom: 8px; -} - -.elt { +summary { display: flex; - padding: 3px; - background-color: #444; - border-radius: 8px; + user-select: none; } -.elt.selected { +summary.selected { color: #fff; - background-color: #666; + background-color: #444; } -.elt > .elt-label { +summary > div, .cst { + padding-top: 4px; + padding-bottom: 4px; +} + +.elt, .cst { + display: flex; flex-grow: 1; - padding: 2px 0px 2px 4px; + padding-left: 8px; + padding-right: 8px; } -.elt > .elt-rep { +.elt-switch { + width: 18px; + padding-left: 2px; + text-align: center; +} + +details:has(li) .elt-switch::after { + content: '▸'; +} + +details[open]:has(li) .elt-switch::after { + content: '▾'; +} + +.elt-label { + flex-grow: 1; +} + +.elt-rep { display: flex; } -.elt > .elt-rep > div { - padding: 2px; - margin-left: 3px; +.elt-rep > div { + padding: 2px 0px 0px 0px; + font-size: 10pt; text-align: center; - width: 60px; - background-color: #333; -} - -.elt.selected > .elt-rep > div { - background-color: #555; -} - -.elt-rep > div:first-child { - border-radius: 6px 0px 0px 6px; -} - -.elt-rep > div:last-child { - border-radius: 0px 6px 6px 0px; -} - -.constraints > li { - margin-top: 4px; - margin-bottom: 4px; - padding: 5px; - background-color: #444; - border-radius: 8px; + width: 56px; } /* display */ canvas { float: left; - margin-left: 16px; + margin-left: 20px; + margin-top: 20px; background-color: #020202; border: 1px solid #555; border-radius: 16px; diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs index 4cb3901..e71a4ab 100644 --- a/app-proto/sketch-outline/src/outline.rs +++ b/app-proto/sketch-outline/src/outline.rs @@ -1,6 +1,6 @@ use itertools::Itertools; use sycamore::{prelude::*, web::tags::div}; -use web_sys::{KeyboardEvent, MouseEvent}; +use web_sys::{Element, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; use crate::AppState; @@ -31,9 +31,9 @@ pub fn Outline() -> View { let class = create_memo({ move || { if state.selection.with(|sel| sel.contains(&key)) { - "elt selected" + "selected" } else { - "elt" + "" } } }); @@ -42,59 +42,90 @@ pub fn Outline() -> View { let u_coord = u.to_string().replace("-", "\u{2212}"); View::from(div().children(u_coord)) }).collect::>(); + let constrained = elt.constraints.len() > 0; + let details_node = create_node_ref(); view! { /* [TO DO] switch to integer-valued parameters whenever that becomes possible again */ li { - div( - class=class.get(), - tabindex="0", - on:click={ - move |event: MouseEvent| { - if event.shift_key() { - state.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); - } - }); - } else { - state.selection.update(|sel| { - sel.clear(); - sel.insert(key); - }); - } - event.stop_propagation(); - } - }, - on:keydown={ - move |event: KeyboardEvent| { - if event.key() == "Enter" { - if event.shift_key() { - state.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); + details(ref=details_node) { + summary( + class=class.get(), + on:keydown={ + move |event: KeyboardEvent| { + match event.key().as_str() { + "Enter" => { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); } - }); - } else { - state.selection.update(|sel| { - sel.clear(); - sel.insert(key); - }); + event.prevent_default(); + }, + "ArrowRight" if constrained => { + let _ = details_node + .get() + .unchecked_into::() + .set_attribute("open", ""); + }, + "ArrowLeft" => { + let _ = details_node + .get() + .unchecked_into::() + .remove_attribute("open"); + }, + _ => () } - event.prevent_default(); } } + ) { + div( + class="elt-switch", + on:click=|event: MouseEvent| event.stop_propagation() + ) + div( + class="elt", + on:click={ + move |event: MouseEvent| { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + event.stop_propagation(); + event.prevent_default(); + } + } + ) { + div(class="elt-label") { (label) } + div(class="elt-rep") { (rep_components) } + } + } + ul(class="constraints") { + Keyed( + list=elt.constraints.into_iter().collect::>(), + view=|c_key: usize| view! { + li(class="cst") { + (c_key.to_string()) + } + }, + key=|c_key| c_key.clone() + ) } - ) { - div(class="elt-label") { (label) } - div(class="elt-rep") { (rep_components) } - } - ul(class="constraints") { - Keyed( - list=elt.constraints.into_iter().collect::>(), - view=|c_key: usize| view! { li { (c_key.to_string()) } }, - key=|c_key| c_key.clone() - ) } } } From fc85d15f8307a337341e33e533c1c236eb33378c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 23 Sep 2024 00:39:14 -0700 Subject: [PATCH 075/126] Outline: show constraint details --- app-proto/sketch-outline/main.css | 15 +++++++++++++-- app-proto/sketch-outline/src/assembly.rs | 1 + app-proto/sketch-outline/src/outline.rs | 24 +++++++++++++++++++++--- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/app-proto/sketch-outline/main.css b/app-proto/sketch-outline/main.css index c3317ea..51cdb3c 100644 --- a/app-proto/sketch-outline/main.css +++ b/app-proto/sketch-outline/main.css @@ -18,9 +18,12 @@ body { overflow-y: scroll; } +li { + user-select: none; +} + summary { display: flex; - user-select: none; } summary.selected { @@ -58,17 +61,25 @@ details[open]:has(li) .elt-switch::after { flex-grow: 1; } +.cst-label { + flex-grow: 1; +} + .elt-rep { display: flex; } -.elt-rep > div { +.elt-rep > div, .cst-rep { padding: 2px 0px 0px 0px; font-size: 10pt; text-align: center; width: 56px; } +.cst { + font-style: italic; +} + /* display */ canvas { diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/sketch-outline/src/assembly.rs index a4c9a0f..6fac59f 100644 --- a/app-proto/sketch-outline/src/assembly.rs +++ b/app-proto/sketch-outline/src/assembly.rs @@ -12,6 +12,7 @@ pub struct Element { pub constraints: FxHashSet } +#[derive(Clone)] pub struct Constraint { pub args: (usize, usize), pub rep: f64 diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs index e71a4ab..be5a9b1 100644 --- a/app-proto/sketch-outline/src/outline.rs +++ b/app-proto/sketch-outline/src/outline.rs @@ -4,6 +4,12 @@ use web_sys::{Element, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; use crate::AppState; +// this component lists the elements of the assembly, showing the constraints +// on each element as a collapsible sub-list. its implementation is based on +// Kate Morley's HTML + CSS tree views: +// +// https://iamkate.com/code/tree-views/ +// #[component] pub fn Outline() -> View { // sort the elements alphabetically by ID @@ -118,9 +124,21 @@ pub fn Outline() -> View { ul(class="constraints") { Keyed( list=elt.constraints.into_iter().collect::>(), - view=|c_key: usize| view! { - li(class="cst") { - (c_key.to_string()) + view=move |c_key: usize| { + let c_state = use_context::(); + let assembly = &c_state.assembly; + let cst = assembly.constraints.with(|csts| csts[c_key].clone()); + let other_arg = if cst.args.0 == key { + cst.args.1 + } else { + cst.args.0 + }; + let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone()); + view! { + li(class="cst") { + div(class="cst-label") { (other_arg_label) } + div(class="cst-rep") { (cst.rep) } + } } }, key=|c_key| c_key.clone() From e6281cdcc6377fd7b0d3416748608be15efd5136 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 25 Sep 2024 14:48:58 -0700 Subject: [PATCH 076/126] Display: shrink canvas to 600px This makes profiling more comparable with `inversive-display`. --- app-proto/sketch-outline/src/display.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/sketch-outline/src/display.rs index 2ac8ebf..52b2ae9 100644 --- a/app-proto/sketch-outline/src/display.rs +++ b/app-proto/sketch-outline/src/display.rs @@ -386,8 +386,8 @@ pub fn Display() -> View { // again canvas( ref=display, - width="750", - height="750", + width="600", + height="600", tabindex="0", on:keydown=move |event: KeyboardEvent| { if event.key() == "Shift" { From 7ff1b9cb6591c48b3457a7415bfc644c54d4e4e4 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 13:22:48 -0700 Subject: [PATCH 077/126] App: rename directory --- app-proto/{sketch-outline => full-interface}/.gitignore | 0 app-proto/{sketch-outline => full-interface}/Cargo.toml | 0 app-proto/{sketch-outline => full-interface}/index.html | 0 app-proto/{sketch-outline => full-interface}/main.css | 0 app-proto/{sketch-outline => full-interface}/src/assembly.rs | 0 app-proto/{sketch-outline => full-interface}/src/display.rs | 0 app-proto/{sketch-outline => full-interface}/src/identity.vert | 0 app-proto/{sketch-outline => full-interface}/src/inversive.frag | 0 app-proto/{sketch-outline => full-interface}/src/main.rs | 0 app-proto/{sketch-outline => full-interface}/src/outline.rs | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename app-proto/{sketch-outline => full-interface}/.gitignore (100%) rename app-proto/{sketch-outline => full-interface}/Cargo.toml (100%) rename app-proto/{sketch-outline => full-interface}/index.html (100%) rename app-proto/{sketch-outline => full-interface}/main.css (100%) rename app-proto/{sketch-outline => full-interface}/src/assembly.rs (100%) rename app-proto/{sketch-outline => full-interface}/src/display.rs (100%) rename app-proto/{sketch-outline => full-interface}/src/identity.vert (100%) rename app-proto/{sketch-outline => full-interface}/src/inversive.frag (100%) rename app-proto/{sketch-outline => full-interface}/src/main.rs (100%) rename app-proto/{sketch-outline => full-interface}/src/outline.rs (100%) diff --git a/app-proto/sketch-outline/.gitignore b/app-proto/full-interface/.gitignore similarity index 100% rename from app-proto/sketch-outline/.gitignore rename to app-proto/full-interface/.gitignore diff --git a/app-proto/sketch-outline/Cargo.toml b/app-proto/full-interface/Cargo.toml similarity index 100% rename from app-proto/sketch-outline/Cargo.toml rename to app-proto/full-interface/Cargo.toml diff --git a/app-proto/sketch-outline/index.html b/app-proto/full-interface/index.html similarity index 100% rename from app-proto/sketch-outline/index.html rename to app-proto/full-interface/index.html diff --git a/app-proto/sketch-outline/main.css b/app-proto/full-interface/main.css similarity index 100% rename from app-proto/sketch-outline/main.css rename to app-proto/full-interface/main.css diff --git a/app-proto/sketch-outline/src/assembly.rs b/app-proto/full-interface/src/assembly.rs similarity index 100% rename from app-proto/sketch-outline/src/assembly.rs rename to app-proto/full-interface/src/assembly.rs diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/full-interface/src/display.rs similarity index 100% rename from app-proto/sketch-outline/src/display.rs rename to app-proto/full-interface/src/display.rs diff --git a/app-proto/sketch-outline/src/identity.vert b/app-proto/full-interface/src/identity.vert similarity index 100% rename from app-proto/sketch-outline/src/identity.vert rename to app-proto/full-interface/src/identity.vert diff --git a/app-proto/sketch-outline/src/inversive.frag b/app-proto/full-interface/src/inversive.frag similarity index 100% rename from app-proto/sketch-outline/src/inversive.frag rename to app-proto/full-interface/src/inversive.frag diff --git a/app-proto/sketch-outline/src/main.rs b/app-proto/full-interface/src/main.rs similarity index 100% rename from app-proto/sketch-outline/src/main.rs rename to app-proto/full-interface/src/main.rs diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/full-interface/src/outline.rs similarity index 100% rename from app-proto/sketch-outline/src/outline.rs rename to app-proto/full-interface/src/outline.rs From 4e3c86fb717fb7a0b25a63afe34427c76a60446a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 13:23:56 -0700 Subject: [PATCH 078/126] Ignore profiling folders --- app-proto/full-interface/.gitignore | 1 + app-proto/inversive-display/.gitignore | 1 + 2 files changed, 2 insertions(+) diff --git a/app-proto/full-interface/.gitignore b/app-proto/full-interface/.gitignore index 238273d..19aa86b 100644 --- a/app-proto/full-interface/.gitignore +++ b/app-proto/full-interface/.gitignore @@ -1,3 +1,4 @@ target dist +profiling Cargo.lock \ No newline at end of file diff --git a/app-proto/inversive-display/.gitignore b/app-proto/inversive-display/.gitignore index 238273d..19aa86b 100644 --- a/app-proto/inversive-display/.gitignore +++ b/app-proto/inversive-display/.gitignore @@ -1,3 +1,4 @@ target dist +profiling Cargo.lock \ No newline at end of file From f5486fb0dd2c4f5f59ea52782af464c2faa4c714 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 15:02:51 -0700 Subject: [PATCH 079/126] AddRemove: make a button that adds constraints --- app-proto/full-interface/main.css | 28 ++++++++++- app-proto/full-interface/src/add_remove.rs | 55 ++++++++++++++++++++++ app-proto/full-interface/src/main.rs | 7 ++- 3 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 app-proto/full-interface/src/add_remove.rs diff --git a/app-proto/full-interface/main.css b/app-proto/full-interface/main.css index 51cdb3c..a687aac 100644 --- a/app-proto/full-interface/main.css +++ b/app-proto/full-interface/main.css @@ -4,9 +4,11 @@ body { background-color: #222; } -/* outline */ +/* sidebar */ -#outline { +#sidebar { + display: flex; + flex-direction: column; float: left; width: 450px; height: 100vh; @@ -15,6 +17,28 @@ body { border-width: 0px 1px 0px 0px; border-style: solid; border-color: #555; +} + +/* add-remove */ + +#add-remove { + display: flex; + gap: 8px; + margin: 8px; +} + +#add-remove > button { + width: 32px; + height: 32px; + font-size: large; +} + +/* outline */ + +#outline { + flex-grow: 1; + margin: 0px; + padding: 0px; overflow-y: scroll; } diff --git a/app-proto/full-interface/src/add_remove.rs b/app-proto/full-interface/src/add_remove.rs new file mode 100644 index 0000000..59220ae --- /dev/null +++ b/app-proto/full-interface/src/add_remove.rs @@ -0,0 +1,55 @@ +use sycamore::prelude::*; +use web_sys::{MouseEvent, console, wasm_bindgen::JsValue}; + +use crate::AppState; +use crate::Constraint; + +#[component] +pub fn AddRemove() -> View { + let state = use_context::(); + + view! { + div(id="add-remove") { + button( + on:click=move |event: MouseEvent| { + console::log_1(&JsValue::from("constraints:")); + state.assembly.constraints.with(|csts| { + for (_, cst) in csts.into_iter() { + console::log_4( + &JsValue::from(cst.args.0), + &JsValue::from(cst.args.1), + &JsValue::from(":"), + &JsValue::from(cst.rep) + ); + } + }); + } + ) { "+" } + button( + disabled={ + state.selection.with(|sel| sel.len() != 2) + }, + on:click=move |event: MouseEvent| { + let args = state.selection.with( + |sel| { + let arg_vec: Vec<_> = sel.into_iter().collect(); + (arg_vec[0].clone(), arg_vec[1].clone()) + } + ); + console::log_5( + &JsValue::from("add constraint"), + &JsValue::from(args.0), + &JsValue::from(args.1), + &JsValue::from(":"), + &JsValue::from(0.0) + ); + state.assembly.insert_constraint(Constraint { + args: args, + rep: 0.0 + }); + state.selection.update(|sel| sel.clear()); + } + ) { "🔗" } + } + } +} \ No newline at end of file diff --git a/app-proto/full-interface/src/main.rs b/app-proto/full-interface/src/main.rs index 0466724..2f31ada 100644 --- a/app-proto/full-interface/src/main.rs +++ b/app-proto/full-interface/src/main.rs @@ -1,3 +1,4 @@ +mod add_remove; mod assembly; mod display; mod outline; @@ -6,6 +7,7 @@ use nalgebra::DVector; use rustc_hash::FxHashSet; use sycamore::prelude::*; +use add_remove::AddRemove; use assembly::{Assembly, Constraint, Element}; use display::Display; use outline::Outline; @@ -68,7 +70,10 @@ fn main() { provide_context(state); view! { - Outline {} + div(id="sidebar") { + AddRemove {} + Outline {} + } Display {} } }); From 9b39fe56b810d576f41fc43237f7cd85590e6f6d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 19:10:34 -0700 Subject: [PATCH 080/126] Outline: include constraints in element diff key This tells Sycamore that the outline view of an element should update when the element's constraint set has changed. To make the constraint set hashable, so we can include it in the diff key, we store it as a `BTreeSet` instead of an `FxHashSet`. --- app-proto/full-interface/src/assembly.rs | 4 ++-- app-proto/full-interface/src/main.rs | 7 ++++--- app-proto/full-interface/src/outline.rs | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app-proto/full-interface/src/assembly.rs b/app-proto/full-interface/src/assembly.rs index 6fac59f..1d0a6f8 100644 --- a/app-proto/full-interface/src/assembly.rs +++ b/app-proto/full-interface/src/assembly.rs @@ -1,6 +1,6 @@ use nalgebra::DVector; -use rustc_hash::FxHashSet; use slab::Slab; +use std::collections::BTreeSet; use sycamore::prelude::*; #[derive(Clone, PartialEq)] @@ -9,7 +9,7 @@ pub struct Element { pub label: String, pub color: [f32; 3], pub rep: DVector, - pub constraints: FxHashSet + pub constraints: BTreeSet } #[derive(Clone)] diff --git a/app-proto/full-interface/src/main.rs b/app-proto/full-interface/src/main.rs index 2f31ada..e867ad3 100644 --- a/app-proto/full-interface/src/main.rs +++ b/app-proto/full-interface/src/main.rs @@ -5,6 +5,7 @@ mod outline; use nalgebra::DVector; use rustc_hash::FxHashSet; +use std::collections::BTreeSet; use sycamore::prelude::*; use add_remove::AddRemove; @@ -37,7 +38,7 @@ fn main() { label: String::from("Wing A"), color: [1.00_f32, 0.25_f32, 0.00_f32], rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]), - constraints: FxHashSet::default() + constraints: BTreeSet::default() } ) ); @@ -48,7 +49,7 @@ fn main() { label: String::from("Wing B"), color: [0.00_f32, 0.25_f32, 1.00_f32], rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]), - constraints: FxHashSet::default() + constraints: BTreeSet::default() }, ) ); @@ -59,7 +60,7 @@ fn main() { label: String::from("Central"), color: [0.75_f32, 0.75_f32, 0.75_f32], rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]), - constraints: FxHashSet::default() + constraints: BTreeSet::default() } ) ); diff --git a/app-proto/full-interface/src/outline.rs b/app-proto/full-interface/src/outline.rs index be5a9b1..8a0a3fb 100644 --- a/app-proto/full-interface/src/outline.rs +++ b/app-proto/full-interface/src/outline.rs @@ -148,7 +148,7 @@ pub fn Outline() -> View { } } }, - key=|(key, _)| key.clone() + key=|(key, elt)| (key.clone(), elt.constraints.clone()) ) } } From b3afd6f5553f6f1ca39d80ab38c84f9af43a1261 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 19:16:41 -0700 Subject: [PATCH 081/126] App: Store selection in `BTreeSet` Since we're using `BTreeSet` for element constraint sets now, we might as well use it for the selection set too. This removes the `rustc-hash` dependency. --- app-proto/full-interface/Cargo.toml | 1 - app-proto/full-interface/src/main.rs | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app-proto/full-interface/Cargo.toml b/app-proto/full-interface/Cargo.toml index 920469a..7640b07 100644 --- a/app-proto/full-interface/Cargo.toml +++ b/app-proto/full-interface/Cargo.toml @@ -11,7 +11,6 @@ default = ["console_error_panic_hook"] itertools = "0.13.0" js-sys = "0.3.70" nalgebra = "0.33.0" -rustc-hash = "2.0.0" slab = "0.4.9" sycamore = "0.9.0-beta.3" diff --git a/app-proto/full-interface/src/main.rs b/app-proto/full-interface/src/main.rs index e867ad3..87e06db 100644 --- a/app-proto/full-interface/src/main.rs +++ b/app-proto/full-interface/src/main.rs @@ -4,7 +4,6 @@ mod display; mod outline; use nalgebra::DVector; -use rustc_hash::FxHashSet; use std::collections::BTreeSet; use sycamore::prelude::*; @@ -16,14 +15,14 @@ use outline::Outline; #[derive(Clone)] struct AppState { assembly: Assembly, - selection: Signal> + selection: Signal> } impl AppState { fn new() -> AppState { AppState { assembly: Assembly::new(), - selection: create_signal(FxHashSet::default()) + selection: create_signal(BTreeSet::default()) } } } From 2444649dd1d138b1d31b56ad026ff879e86586c6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 19:17:57 -0700 Subject: [PATCH 082/126] AddRemove: underscore unused event variables --- app-proto/full-interface/src/add_remove.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app-proto/full-interface/src/add_remove.rs b/app-proto/full-interface/src/add_remove.rs index 59220ae..e0ecf0f 100644 --- a/app-proto/full-interface/src/add_remove.rs +++ b/app-proto/full-interface/src/add_remove.rs @@ -1,5 +1,5 @@ use sycamore::prelude::*; -use web_sys::{MouseEvent, console, wasm_bindgen::JsValue}; +use web_sys::{console, wasm_bindgen::JsValue}; use crate::AppState; use crate::Constraint; @@ -11,7 +11,7 @@ pub fn AddRemove() -> View { view! { div(id="add-remove") { button( - on:click=move |event: MouseEvent| { + on:click=move |_| { console::log_1(&JsValue::from("constraints:")); state.assembly.constraints.with(|csts| { for (_, cst) in csts.into_iter() { @@ -29,7 +29,7 @@ pub fn AddRemove() -> View { disabled={ state.selection.with(|sel| sel.len() != 2) }, - on:click=move |event: MouseEvent| { + on:click=move |_| { let args = state.selection.with( |sel| { let arg_vec: Vec<_> = sel.into_iter().collect(); From bd0982f821a0f8d4cd5d1128f4e1c72e88876831 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 27 Sep 2024 14:33:49 -0700 Subject: [PATCH 083/126] AddRemove: make a button that adds elements In the process, switch selection storage back to `FxHashSet`, reverting commit b3afd6f. --- app-proto/full-interface/Cargo.toml | 1 + app-proto/full-interface/src/add_remove.rs | 68 +++++++++++++--------- app-proto/full-interface/src/assembly.rs | 33 ++++++++++- app-proto/full-interface/src/main.rs | 5 +- 4 files changed, 74 insertions(+), 33 deletions(-) diff --git a/app-proto/full-interface/Cargo.toml b/app-proto/full-interface/Cargo.toml index 7640b07..920469a 100644 --- a/app-proto/full-interface/Cargo.toml +++ b/app-proto/full-interface/Cargo.toml @@ -11,6 +11,7 @@ default = ["console_error_panic_hook"] itertools = "0.13.0" js-sys = "0.3.70" nalgebra = "0.33.0" +rustc-hash = "2.0.0" slab = "0.4.9" sycamore = "0.9.0-beta.3" diff --git a/app-proto/full-interface/src/add_remove.rs b/app-proto/full-interface/src/add_remove.rs index e0ecf0f..f93f31f 100644 --- a/app-proto/full-interface/src/add_remove.rs +++ b/app-proto/full-interface/src/add_remove.rs @@ -6,16 +6,51 @@ use crate::Constraint; #[component] pub fn AddRemove() -> View { - let state = use_context::(); - view! { div(id="add-remove") { button( - on:click=move |_| { + on:click=|_| { + let state = use_context::(); + state.assembly.insert_new_element(); + + /* DEBUG */ + // print updated list of elements by identifier + console::log_1(&JsValue::from("elements by identifier:")); + for (id, key) in state.assembly.elements_by_id.get_clone().iter() { + console::log_3( + &JsValue::from(" "), + &JsValue::from(id), + &JsValue::from(*key) + ); + } + } + ) { "+" } + button( + disabled={ + let state = use_context::(); + state.selection.with(|sel| sel.len() != 2) + }, + on:click=|_| { + let state = use_context::(); + let args = state.selection.with( + |sel| { + let arg_vec: Vec<_> = sel.into_iter().collect(); + (arg_vec[0].clone(), arg_vec[1].clone()) + } + ); + state.assembly.insert_constraint(Constraint { + args: args, + rep: 0.0 + }); + state.selection.update(|sel| sel.clear()); + + /* DEBUG */ + // print updated constraint list console::log_1(&JsValue::from("constraints:")); state.assembly.constraints.with(|csts| { for (_, cst) in csts.into_iter() { - console::log_4( + console::log_5( + &JsValue::from(" "), &JsValue::from(cst.args.0), &JsValue::from(cst.args.1), &JsValue::from(":"), @@ -24,31 +59,6 @@ pub fn AddRemove() -> View { } }); } - ) { "+" } - button( - disabled={ - state.selection.with(|sel| sel.len() != 2) - }, - on:click=move |_| { - let args = state.selection.with( - |sel| { - let arg_vec: Vec<_> = sel.into_iter().collect(); - (arg_vec[0].clone(), arg_vec[1].clone()) - } - ); - console::log_5( - &JsValue::from("add constraint"), - &JsValue::from(args.0), - &JsValue::from(args.1), - &JsValue::from(":"), - &JsValue::from(0.0) - ); - state.assembly.insert_constraint(Constraint { - args: args, - rep: 0.0 - }); - state.selection.update(|sel| sel.clear()); - } ) { "🔗" } } } diff --git a/app-proto/full-interface/src/assembly.rs b/app-proto/full-interface/src/assembly.rs index 1d0a6f8..aa4355e 100644 --- a/app-proto/full-interface/src/assembly.rs +++ b/app-proto/full-interface/src/assembly.rs @@ -1,4 +1,5 @@ use nalgebra::DVector; +use rustc_hash::FxHashMap; use slab::Slab; use std::collections::BTreeSet; use sycamore::prelude::*; @@ -21,18 +22,46 @@ pub struct Constraint { // a complete, view-independent description of an assembly #[derive(Clone)] pub struct Assembly { + // elements and constraints pub elements: Signal>, - pub constraints: Signal> + pub constraints: Signal>, + + // indexing + pub elements_by_id: Signal> } impl Assembly { pub fn new() -> Assembly { Assembly { elements: create_signal(Slab::new()), - constraints: create_signal(Slab::new()) + constraints: create_signal(Slab::new()), + elements_by_id: create_signal(FxHashMap::default()) } } + pub fn insert_new_element(&self) { + // find the next unused identifier in the default sequence + let mut id_num = 1; + let mut id = format!("sphere{}", id_num); + while self.elements_by_id.with( + |elts_by_id| elts_by_id.contains_key(&id) + ) { + id_num += 1; + id = format!("sphere{}", id_num); + } + + // create and insert a new element + let elt = Element { + id: id.clone(), + label: format!("Sphere {}", id_num), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]), + constraints: BTreeSet::default() + }; + let key = self.elements.update(|elts| elts.insert(elt)); + self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); + } + pub fn insert_constraint(&self, constraint: Constraint) { let args = constraint.args; let key = self.constraints.update(|csts| csts.insert(constraint)); diff --git a/app-proto/full-interface/src/main.rs b/app-proto/full-interface/src/main.rs index 87e06db..e867ad3 100644 --- a/app-proto/full-interface/src/main.rs +++ b/app-proto/full-interface/src/main.rs @@ -4,6 +4,7 @@ mod display; mod outline; use nalgebra::DVector; +use rustc_hash::FxHashSet; use std::collections::BTreeSet; use sycamore::prelude::*; @@ -15,14 +16,14 @@ use outline::Outline; #[derive(Clone)] struct AppState { assembly: Assembly, - selection: Signal> + selection: Signal> } impl AppState { fn new() -> AppState { AppState { assembly: Assembly::new(), - selection: create_signal(BTreeSet::default()) + selection: create_signal(FxHashSet::default()) } } } From b08dbd6f936df7c4ec6510dd15d558f8581b4872 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 28 Sep 2024 13:10:22 -0700 Subject: [PATCH 084/126] Assembly: factor out element insertion --- app-proto/full-interface/src/assembly.rs | 37 ++++++++++++++++++------ 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/app-proto/full-interface/src/assembly.rs b/app-proto/full-interface/src/assembly.rs index aa4355e..0660d0e 100644 --- a/app-proto/full-interface/src/assembly.rs +++ b/app-proto/full-interface/src/assembly.rs @@ -39,6 +39,25 @@ impl Assembly { } } + // insert an element into the assembly without checking whether we already + // have an element with the same identifier. any element that does have the + // same identifier will get kicked out of the `elements_by_id` index + fn insert_element_unchecked(&self, elt: Element) { + let id = elt.id.clone(); + let key = self.elements.update(|elts| elts.insert(elt)); + self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); + } + + pub fn try_insert_element(&self, elt: Element) -> bool { + let can_insert = self.elements_by_id.with( + |elts_by_id| !elts_by_id.contains_key(&elt.id) + ); + if can_insert { + self.insert_element_unchecked(elt); + } + can_insert + } + pub fn insert_new_element(&self) { // find the next unused identifier in the default sequence let mut id_num = 1; @@ -51,15 +70,15 @@ impl Assembly { } // create and insert a new element - let elt = Element { - id: id.clone(), - label: format!("Sphere {}", id_num), - color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]), - constraints: BTreeSet::default() - }; - let key = self.elements.update(|elts| elts.insert(elt)); - self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); + self.insert_element_unchecked( + Element { + id: id, + label: format!("Sphere {}", id_num), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]), + constraints: BTreeSet::default() + } + ); } pub fn insert_constraint(&self, constraint: Constraint) { From 28b1ecb8e936de8c2d0b2788815204dd60f261ec Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 28 Sep 2024 13:28:53 -0700 Subject: [PATCH 085/126] App: use element insertion method in test --- app-proto/full-interface/src/main.rs | 68 ++++++++++++++-------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/app-proto/full-interface/src/main.rs b/app-proto/full-interface/src/main.rs index e867ad3..5ed9d30 100644 --- a/app-proto/full-interface/src/main.rs +++ b/app-proto/full-interface/src/main.rs @@ -31,43 +31,43 @@ impl AppState { fn main() { sycamore::render(|| { let state = AppState::new(); - let key_a = state.assembly.elements.update( - |elts| elts.insert( - Element { - id: String::from("wing_a"), - label: String::from("Wing A"), - color: [1.00_f32, 0.25_f32, 0.00_f32], - rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]), - constraints: BTreeSet::default() - } - ) + let assemb = &state.assembly; + let _ = assemb.try_insert_element( + Element { + id: String::from("wing_a"), + label: String::from("Wing A"), + color: [1.00_f32, 0.25_f32, 0.00_f32], + rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]), + constraints: BTreeSet::default() + } ); - let key_b = state.assembly.elements.update( - |elts| elts.insert( - Element { - id: String::from("wing_b"), - label: String::from("Wing B"), - color: [0.00_f32, 0.25_f32, 1.00_f32], - rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]), - constraints: BTreeSet::default() - }, - ) + let _ = assemb.try_insert_element( + Element { + id: String::from("wing_b"), + label: String::from("Wing B"), + color: [0.00_f32, 0.25_f32, 1.00_f32], + rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]), + constraints: BTreeSet::default() + } ); - state.assembly.elements.update( - |elts| elts.insert( - Element { - id: String::from("central"), - label: String::from("Central"), - color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]), - constraints: BTreeSet::default() - } - ) + let _ = assemb.try_insert_element( + Element { + id: String::from("central"), + label: String::from("Central"), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]), + constraints: BTreeSet::default() + } + ); + assemb.insert_constraint( + Constraint { + args: ( + assemb.elements_by_id.with(|elts_by_id| elts_by_id["wing_a"]), + assemb.elements_by_id.with(|elts_by_id| elts_by_id["wing_b"]) + ), + rep: 0.5 + } ); - state.assembly.insert_constraint(Constraint { - args: (key_a, key_b), - rep: 0.5 - }); provide_context(state); view! { From 4f8f36053fec599fa6617beef34f1df73186e800 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 28 Sep 2024 14:18:04 -0700 Subject: [PATCH 086/126] App: use general test assembly from `inversive-display` This moves us toward dropping the separate display prototype. --- app-proto/full-interface/src/engine.rs | 27 +++++++++++++ app-proto/full-interface/src/main.rs | 52 ++++++++++++++++++++------ 2 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 app-proto/full-interface/src/engine.rs diff --git a/app-proto/full-interface/src/engine.rs b/app-proto/full-interface/src/engine.rs new file mode 100644 index 0000000..79668bb --- /dev/null +++ b/app-proto/full-interface/src/engine.rs @@ -0,0 +1,27 @@ +use nalgebra::DVector; + +// the sphere with the given center and radius, with inward-pointing normals +pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVector { + let center_norm_sq = center_x * center_x + center_y * center_y + center_z * center_z; + DVector::from_column_slice(&[ + center_x / radius, + center_y / radius, + center_z / radius, + 0.5 / radius, + 0.5 * (center_norm_sq / radius - radius) + ]) +} + +// the sphere of curvature `curv` whose closest point to the origin has position +// `off * dir` and normal `dir`, where `dir` is a unit vector. setting the +// curvature to zero gives a plane +pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f64) -> DVector { + let norm_sp = 1.0 + off * curv; + DVector::from_column_slice(&[ + norm_sp * dir_x, + norm_sp * dir_y, + norm_sp * dir_z, + 0.5 * curv, + off * (1.0 + 0.5 * off * curv) + ]) +} \ No newline at end of file diff --git a/app-proto/full-interface/src/main.rs b/app-proto/full-interface/src/main.rs index 5ed9d30..0ca2209 100644 --- a/app-proto/full-interface/src/main.rs +++ b/app-proto/full-interface/src/main.rs @@ -1,6 +1,7 @@ mod add_remove; mod assembly; mod display; +mod engine; mod outline; use nalgebra::DVector; @@ -34,36 +35,63 @@ fn main() { let assemb = &state.assembly; let _ = assemb.try_insert_element( Element { - id: String::from("wing_a"), - label: String::from("Wing A"), + id: String::from("gemini_a"), + label: String::from("Castor"), color: [1.00_f32, 0.25_f32, 0.00_f32], - rep: DVector::::from_column_slice(&[0.5, 0.5, 0.0, 0.5, -0.25]), + rep: engine::sphere(0.5, 0.5, 0.0, 1.0), constraints: BTreeSet::default() } ); let _ = assemb.try_insert_element( Element { - id: String::from("wing_b"), - label: String::from("Wing B"), + id: String::from("gemini_b"), + label: String::from("Pollux"), color: [0.00_f32, 0.25_f32, 1.00_f32], - rep: DVector::::from_column_slice(&[-0.5, -0.5, 0.0, 0.5, -0.25]), + rep: engine::sphere(-0.5, -0.5, 0.0, 1.0), constraints: BTreeSet::default() } ); let _ = assemb.try_insert_element( Element { - id: String::from("central"), - label: String::from("Central"), - color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.4, -0.625]), + id: String::from("ursa_major"), + label: String::from("Ursa major"), + color: [0.25_f32, 0.00_f32, 1.00_f32], + rep: engine::sphere(-0.5, 0.5, 0.0, 0.75), + constraints: BTreeSet::default() + } + ); + let _ = assemb.try_insert_element( + Element { + id: String::from("ursa_minor"), + label: String::from("Ursa minor"), + color: [0.25_f32, 1.00_f32, 0.00_f32], + rep: engine::sphere(0.5, -0.5, 0.0, 0.5), + constraints: BTreeSet::default() + } + ); + let _ = assemb.try_insert_element( + Element { + id: String::from("moon_deimos"), + label: String::from("Deimos"), + color: [0.75_f32, 0.75_f32, 0.00_f32], + rep: engine::sphere(0.0, 0.15, 1.0, 0.25), + constraints: BTreeSet::default() + } + ); + let _ = assemb.try_insert_element( + Element { + id: String::from("moon_phobos"), + label: String::from("Phobos"), + color: [0.00_f32, 0.75_f32, 0.50_f32], + rep: engine::sphere(0.0, -0.15, -1.0, 0.25), constraints: BTreeSet::default() } ); assemb.insert_constraint( Constraint { args: ( - assemb.elements_by_id.with(|elts_by_id| elts_by_id["wing_a"]), - assemb.elements_by_id.with(|elts_by_id| elts_by_id["wing_b"]) + assemb.elements_by_id.with(|elts_by_id| elts_by_id["gemini_a"]), + assemb.elements_by_id.with(|elts_by_id| elts_by_id["gemini_b"]) ), rep: 0.5 } From 721a8716d41453c46e89e136f487d11ebd570a2a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 28 Sep 2024 18:49:17 -0700 Subject: [PATCH 087/126] Assembly: don't track element list when inserting Calling `try_insert_element` or `insert_new_element` in a responsive context shouldn't make the context track `elements_by_id`. --- app-proto/full-interface/src/assembly.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app-proto/full-interface/src/assembly.rs b/app-proto/full-interface/src/assembly.rs index 0660d0e..c0c9959 100644 --- a/app-proto/full-interface/src/assembly.rs +++ b/app-proto/full-interface/src/assembly.rs @@ -49,7 +49,7 @@ impl Assembly { } pub fn try_insert_element(&self, elt: Element) -> bool { - let can_insert = self.elements_by_id.with( + let can_insert = self.elements_by_id.with_untracked( |elts_by_id| !elts_by_id.contains_key(&elt.id) ); if can_insert { @@ -62,7 +62,7 @@ impl Assembly { // find the next unused identifier in the default sequence let mut id_num = 1; let mut id = format!("sphere{}", id_num); - while self.elements_by_id.with( + while self.elements_by_id.with_untracked( |elts_by_id| elts_by_id.contains_key(&id) ) { id_num += 1; From 1c9fec36e5e1bab5c57fc639a60b8af6ee8293e5 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 28 Sep 2024 18:51:28 -0700 Subject: [PATCH 088/126] Display: make scene change flag track element list --- app-proto/full-interface/src/display.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/app-proto/full-interface/src/display.rs b/app-proto/full-interface/src/display.rs index 52b2ae9..67062b7 100644 --- a/app-proto/full-interface/src/display.rs +++ b/app-proto/full-interface/src/display.rs @@ -102,6 +102,7 @@ pub fn Display() -> View { // change listener let scene_changed = create_signal(true); create_effect(move || { + state.assembly.elements.track(); state.selection.track(); scene_changed.set(true); }); From 7977b11caf4ae62bacd19e63c7b437d63380f626 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 28 Sep 2024 18:56:33 -0700 Subject: [PATCH 089/126] AddRemove: switch between pre-made test assemblies --- app-proto/full-interface/src/add_remove.rs | 102 ++++++++++++++++++++- app-proto/full-interface/src/main.rs | 69 +------------- 2 files changed, 102 insertions(+), 69 deletions(-) diff --git a/app-proto/full-interface/src/add_remove.rs b/app-proto/full-interface/src/add_remove.rs index f93f31f..aaab8d0 100644 --- a/app-proto/full-interface/src/add_remove.rs +++ b/app-proto/full-interface/src/add_remove.rs @@ -1,11 +1,105 @@ +use std::collections::BTreeSet; /* DEBUG */ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; -use crate::AppState; -use crate::Constraint; +use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}}; + +/* DEBUG */ +fn load_gen_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Element { + id: String::from("gemini_a"), + label: String::from("Castor"), + color: [1.00_f32, 0.25_f32, 0.00_f32], + rep: engine::sphere(0.5, 0.5, 0.0, 1.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("gemini_b"), + label: String::from("Pollux"), + color: [0.00_f32, 0.25_f32, 1.00_f32], + rep: engine::sphere(-0.5, -0.5, 0.0, 1.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("ursa_major"), + label: String::from("Ursa major"), + color: [0.25_f32, 0.00_f32, 1.00_f32], + rep: engine::sphere(-0.5, 0.5, 0.0, 0.75), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("ursa_minor"), + label: String::from("Ursa minor"), + color: [0.25_f32, 1.00_f32, 0.00_f32], + rep: engine::sphere(0.5, -0.5, 0.0, 0.5), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("moon_deimos"), + label: String::from("Deimos"), + color: [0.75_f32, 0.75_f32, 0.00_f32], + rep: engine::sphere(0.0, 0.15, 1.0, 0.25), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("moon_phobos"), + label: String::from("Phobos"), + color: [0.00_f32, 0.75_f32, 0.50_f32], + rep: engine::sphere(0.0, -0.15, -1.0, 0.25), + constraints: BTreeSet::default() + } + ); + assembly.insert_constraint( + Constraint { + args: ( + assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_a"]), + assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_b"]) + ), + rep: 0.5 + } + ); +} #[component] pub fn AddRemove() -> View { + /* DEBUG */ + let assembly_name = create_signal("general".to_string()); + create_effect(move || { + // get name of chosen assembly + let name = assembly_name.get_clone(); + console::log_1( + &JsValue::from(format!("Showing assembly \"{}\"", name.clone())) + ); + + batch(|| { + let state = use_context::(); + let assembly = &state.assembly; + + // clear state + assembly.elements.update(|elts| elts.clear()); + assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); + state.selection.update(|sel| sel.clear()); + + // load assembly + match name.as_str() { + "general" => load_gen_assemb(assembly), + "low-curv" => /*load_low_curv_assemb(state)*/(), + _ => () + }; + }); + }); + view! { div(id="add-remove") { button( @@ -60,6 +154,10 @@ pub fn AddRemove() -> View { }); } ) { "🔗" } + select(bind:value=assembly_name) { /* DEBUG */ + option(value="general") { "General" } + option(value="low-curv") { "Low-curvature" } + } } } } \ No newline at end of file diff --git a/app-proto/full-interface/src/main.rs b/app-proto/full-interface/src/main.rs index 0ca2209..bca0378 100644 --- a/app-proto/full-interface/src/main.rs +++ b/app-proto/full-interface/src/main.rs @@ -10,7 +10,7 @@ use std::collections::BTreeSet; use sycamore::prelude::*; use add_remove::AddRemove; -use assembly::{Assembly, Constraint, Element}; +use assembly::Assembly; use display::Display; use outline::Outline; @@ -31,72 +31,7 @@ impl AppState { fn main() { sycamore::render(|| { - let state = AppState::new(); - let assemb = &state.assembly; - let _ = assemb.try_insert_element( - Element { - id: String::from("gemini_a"), - label: String::from("Castor"), - color: [1.00_f32, 0.25_f32, 0.00_f32], - rep: engine::sphere(0.5, 0.5, 0.0, 1.0), - constraints: BTreeSet::default() - } - ); - let _ = assemb.try_insert_element( - Element { - id: String::from("gemini_b"), - label: String::from("Pollux"), - color: [0.00_f32, 0.25_f32, 1.00_f32], - rep: engine::sphere(-0.5, -0.5, 0.0, 1.0), - constraints: BTreeSet::default() - } - ); - let _ = assemb.try_insert_element( - Element { - id: String::from("ursa_major"), - label: String::from("Ursa major"), - color: [0.25_f32, 0.00_f32, 1.00_f32], - rep: engine::sphere(-0.5, 0.5, 0.0, 0.75), - constraints: BTreeSet::default() - } - ); - let _ = assemb.try_insert_element( - Element { - id: String::from("ursa_minor"), - label: String::from("Ursa minor"), - color: [0.25_f32, 1.00_f32, 0.00_f32], - rep: engine::sphere(0.5, -0.5, 0.0, 0.5), - constraints: BTreeSet::default() - } - ); - let _ = assemb.try_insert_element( - Element { - id: String::from("moon_deimos"), - label: String::from("Deimos"), - color: [0.75_f32, 0.75_f32, 0.00_f32], - rep: engine::sphere(0.0, 0.15, 1.0, 0.25), - constraints: BTreeSet::default() - } - ); - let _ = assemb.try_insert_element( - Element { - id: String::from("moon_phobos"), - label: String::from("Phobos"), - color: [0.00_f32, 0.75_f32, 0.50_f32], - rep: engine::sphere(0.0, -0.15, -1.0, 0.25), - constraints: BTreeSet::default() - } - ); - assemb.insert_constraint( - Constraint { - args: ( - assemb.elements_by_id.with(|elts_by_id| elts_by_id["gemini_a"]), - assemb.elements_by_id.with(|elts_by_id| elts_by_id["gemini_b"]) - ), - rep: 0.5 - } - ); - provide_context(state); + provide_context(AppState::new()); view! { div(id="sidebar") { From 25fa108e9b2cfba27918c98fc1d89042fdcf85eb Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 28 Sep 2024 19:37:43 -0700 Subject: [PATCH 090/126] AddRemove: add low-curvature test assembly from `inversive-display` --- app-proto/full-interface/src/add_remove.rs | 79 +++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/app-proto/full-interface/src/add_remove.rs b/app-proto/full-interface/src/add_remove.rs index aaab8d0..40b0e98 100644 --- a/app-proto/full-interface/src/add_remove.rs +++ b/app-proto/full-interface/src/add_remove.rs @@ -71,6 +71,83 @@ fn load_gen_assemb(assembly: &Assembly) { ); } +/* DEBUG */ +fn load_low_curv_assemb(assembly: &Assembly) { + let a = 0.75_f64.sqrt(); + let _ = assembly.try_insert_element( + Element { + id: "central".to_string(), + label: "Central".to_string(), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: engine::sphere(0.0, 0.0, 0.0, 1.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "assemb_plane".to_string(), + label: "Assembly plane".to_string(), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "side1".to_string(), + label: "Side 1".to_string(), + color: [1.00_f32, 0.00_f32, 0.25_f32], + rep: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "side2".to_string(), + label: "Side 2".to_string(), + color: [0.25_f32, 1.00_f32, 0.00_f32], + rep: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "side3".to_string(), + label: "Side 3".to_string(), + color: [0.00_f32, 0.25_f32, 1.00_f32], + rep: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "corner1".to_string(), + label: "Corner 1".to_string(), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "corner2".to_string(), + label: "Corner 2".to_string(), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("corner3"), + label: String::from("Corner 3"), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0), + constraints: BTreeSet::default() + } + ); +} + #[component] pub fn AddRemove() -> View { /* DEBUG */ @@ -94,7 +171,7 @@ pub fn AddRemove() -> View { // load assembly match name.as_str() { "general" => load_gen_assemb(assembly), - "low-curv" => /*load_low_curv_assemb(state)*/(), + "low-curv" => load_low_curv_assemb(assembly), _ => () }; }); From 70bd39b9e5b0808f75ce322be45b2b54c169fa79 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 29 Sep 2024 23:30:35 -0700 Subject: [PATCH 091/126] App: remove unused imports --- app-proto/full-interface/src/main.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/app-proto/full-interface/src/main.rs b/app-proto/full-interface/src/main.rs index bca0378..2c71a83 100644 --- a/app-proto/full-interface/src/main.rs +++ b/app-proto/full-interface/src/main.rs @@ -4,9 +4,7 @@ mod display; mod engine; mod outline; -use nalgebra::DVector; use rustc_hash::FxHashSet; -use std::collections::BTreeSet; use sycamore::prelude::*; use add_remove::AddRemove; From edace8e4eaeb1050399d7b31a8f3762c55f894a7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 29 Sep 2024 23:41:16 -0700 Subject: [PATCH 092/126] Outline: include ID and label in element diff key --- app-proto/full-interface/src/outline.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app-proto/full-interface/src/outline.rs b/app-proto/full-interface/src/outline.rs index 8a0a3fb..c980887 100644 --- a/app-proto/full-interface/src/outline.rs +++ b/app-proto/full-interface/src/outline.rs @@ -148,7 +148,12 @@ pub fn Outline() -> View { } } }, - key=|(key, elt)| (key.clone(), elt.constraints.clone()) + key=|(key, elt)| ( + key.clone(), + elt.id.clone(), + elt.label.clone(), + elt.constraints.clone() + ) ) } } From 18ebf3be2c8a9caf0cdec3338bd6ecb1333a144e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 30 Sep 2024 00:44:13 -0700 Subject: [PATCH 093/126] Display: add turntable for benchmarking Together with 25fa108 and 4f8f360, this lets us do a benchmarking routine for `full-interface` which is comparable to the one we've been using for `inversive-display`. --- app-proto/full-interface/src/display.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app-proto/full-interface/src/display.rs b/app-proto/full-interface/src/display.rs index 67062b7..c32b470 100644 --- a/app-proto/full-interface/src/display.rs +++ b/app-proto/full-interface/src/display.rs @@ -98,6 +98,7 @@ pub fn Display() -> View { let roll_cw = create_signal(0.0); let zoom_in = create_signal(0.0); let zoom_out = create_signal(0.0); + let turntable = create_signal(false); /* BENCHMARKING */ // change listener let scene_changed = create_signal(true); @@ -120,6 +121,7 @@ pub fn Display() -> View { // viewpoint const ROT_SPEED: f64 = 0.4; // in radians per second const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second + const TURNTABLE_SPEED: f64 = 0.1; /* BENCHMARKING */ let mut orientation = DMatrix::::identity(5, 5); let mut rotation = DMatrix::::identity(5, 5); let mut location_z: f64 = 5.0; @@ -242,6 +244,7 @@ pub fn Display() -> View { let roll_cw_val = roll_cw.get(); let zoom_in_val = zoom_in.get(); let zoom_out_val = zoom_out.get(); + let turntable_val = turntable.get(); /* BENCHMARKING */ // update the assembly's orientation let ang_vel = { @@ -253,6 +256,10 @@ pub fn Display() -> View { } else { Vector3::zeros() } + } /* BENCHMARKING */ + if turntable_val { + Vector3::new(0.0, TURNTABLE_SPEED, 0.0) + } else { + Vector3::zeros() }; let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0); rotation_sp.copy_from( @@ -352,6 +359,7 @@ pub fn Display() -> View { || roll_ccw_val != 0.0 || zoom_in_val != 0.0 || zoom_out_val != 0.0 + || turntable_val /* BENCHMARKING */ ); } else { frames_since_last_sample = 0; @@ -401,6 +409,10 @@ pub fn Display() -> View { pitch_up.set(0.0); pitch_down.set(0.0); } else { + if event.key() == "Enter" { /* BENCHMARKING */ + turntable.set_fn(|turn| !turn); + scene_changed.set(true); + } set_nav_signal(event, 1.0); } }, From e3120f7109c22095a4688b610c72feeb2e850cc8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 30 Sep 2024 16:48:36 -0700 Subject: [PATCH 094/126] Display: remove unused fragment-sorting function --- app-proto/full-interface/src/inversive.frag | 12 ------------ app-proto/inversive-display/src/inversive.frag | 12 ------------ 2 files changed, 24 deletions(-) diff --git a/app-proto/full-interface/src/inversive.frag b/app-proto/full-interface/src/inversive.frag index 47743eb..8327f47 100644 --- a/app-proto/full-interface/src/inversive.frag +++ b/app-proto/full-interface/src/inversive.frag @@ -71,18 +71,6 @@ struct taggedFrag { vec3 normal; }; -taggedFrag[2] sort(taggedFrag a, taggedFrag b) { - taggedFrag[2] result; - if (a.pt.z > b.pt.z) { - result[0] = a; - result[1] = b; - } else { - result[0] = b; - result[1] = a; - } - return result; -} - taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, float highlight, int id) { // the expression for normal needs to be checked. it's supposed to give the // negative gradient of the lorentz product between the impact point vector diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index ae3b930..fc2a474 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -70,18 +70,6 @@ struct taggedFrag { vec3 normal; }; -taggedFrag[2] sort(taggedFrag a, taggedFrag b) { - taggedFrag[2] result; - if (a.pt.z > b.pt.z) { - result[0] = a; - result[1] = b; - } else { - result[0] = b; - result[1] = a; - } - return result; -} - taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, int id) { // the expression for normal needs to be checked. it's supposed to give the // negative gradient of the lorentz product between the impact point vector From 19907838ce7766e6936fc38be68e03ca2ea20046 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 30 Sep 2024 17:59:48 -0700 Subject: [PATCH 095/126] Display: remove redundant depth test --- app-proto/full-interface/src/inversive.frag | 6 ++---- app-proto/inversive-display/src/inversive.frag | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app-proto/full-interface/src/inversive.frag b/app-proto/full-interface/src/inversive.frag index 8327f47..b7fb100 100644 --- a/app-proto/full-interface/src/inversive.frag +++ b/app-proto/full-interface/src/inversive.frag @@ -209,10 +209,8 @@ void main() { // composite the sphere fragments vec3 color = vec3(0.); for (int i = layer_cnt-1; i >= layer_threshold; --i) { - if (frags[i].pt.z < 0.) { - vec4 frag_color = frags[i].color; - color = mix(color, frag_color.rgb, frag_color.a); - } + vec4 frag_color = frags[i].color; + color = mix(color, frag_color.rgb, frag_color.a); } outColor = vec4(sRGB(color), 1.); } \ No newline at end of file diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index fc2a474..f76995d 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -205,10 +205,8 @@ void main() { // composite the sphere fragments vec3 color = vec3(0.); for (int i = layer_cnt-1; i >= layer_threshold; --i) { - if (frags[i].pt.z < 0.) { - vec4 frag_color = frags[i].color; - color = mix(color, frag_color.rgb, frag_color.a); - } + vec4 frag_color = frags[i].color; + color = mix(color, frag_color.rgb, frag_color.a); } outColor = vec4(sRGB(color), 1.); } \ No newline at end of file From ee1c69178785a9a5b4649037ecd57f951023d383 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 14 Oct 2024 16:04:56 -0700 Subject: [PATCH 096/126] Display: shade fragments after depth sorting This reduces register pressure significantly. This stepping stone commit temporarily removes highlighting of intersections and cusps. --- app-proto/full-interface/src/inversive.frag | 95 ++++++++++----------- 1 file changed, 44 insertions(+), 51 deletions(-) diff --git a/app-proto/full-interface/src/inversive.frag b/app-proto/full-interface/src/inversive.frag index b7fb100..339be00 100644 --- a/app-proto/full-interface/src/inversive.frag +++ b/app-proto/full-interface/src/inversive.frag @@ -63,15 +63,13 @@ vec3 sRGB(vec3 color) { // --- shading --- -struct taggedFrag { - int id; - vec4 color; - float highlight; +struct Fragment { vec3 pt; vec3 normal; + vec4 color; }; -taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, float highlight, int id) { +Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) { // the expression for normal needs to be checked. it's supposed to give the // negative gradient of the lorentz product between the impact point vector // and the sphere vector with respect to the coordinates of the impact @@ -81,11 +79,26 @@ taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, float highlight, i float incidence = dot(normal, light_dir); float illum = mix(0.4, 1.0, max(incidence, 0.0)); - return taggedFrag(id, vec4(illum * base_color, opacity), highlight, pt, normal); + return Fragment(pt, normal, vec4(illum * base_color, opacity)); +} + +float intersection_dist(Fragment a, Fragment b) { + float intersection_sin = length(cross(a.normal, b.normal)); + vec3 disp = a.pt - b.pt; + return max( + abs(dot(a.normal, disp)), + abs(dot(b.normal, disp)) + ) / intersection_sin; } // --- ray-casting --- +struct TaggedDepth { + float depth; + float dimming; + int id; +}; + // if `a/b` is less than this threshold, we approximate `a*u^2 + b*u + c` by // the linear function `b*u + c` const float DEG_THRESHOLD = 1e-9; @@ -127,36 +140,29 @@ void main() { // cast rays through the spheres const int LAYER_MAX = 12; - taggedFrag frags [LAYER_MAX]; + TaggedDepth top_hits [LAYER_MAX]; int layer_cnt = 0; for (int id = 0; id < sphere_cnt; ++id) { // find out where the ray hits the sphere vec2 hit_depths = sphere_cast(sphere_list[id], dir); - // insertion-sort the fragments we hit into the fragment list + // insertion-sort the points we hit into the hit list float dimming = 1.; for (int side = 0; side < 2; ++side) { - float hit_z = -hit_depths[side]; - if (0. > hit_z) { + float depth = hit_depths[side]; + if (depth > 0.) { for (int layer = layer_cnt; layer >= 0; --layer) { - if (layer < 1 || frags[layer-1].pt.z >= hit_z) { - // we're not as close to the screen as the fragment - // before the empty slot, so insert here + if (layer < 1 || top_hits[layer-1].depth <= depth) { + // we're not as close to the screen as the hit before + // the empty slot, so insert here if (layer < LAYER_MAX) { - frags[layer] = sphere_shading( - sphere_list[id], - hit_depths[side] * dir, - dimming * color_list[id], - highlight_list[id], - id - ); + top_hits[layer] = TaggedDepth(depth, dimming, id); } break; } else { - // we're closer to the screen than the fragment before - // the empty slot, so move that fragment into the empty - // slot - frags[layer] = frags[layer-1]; + // we're closer to the screen than the hit before the + // empty slot, so move that hit into the empty slot + top_hits[layer] = top_hits[layer-1]; } } layer_cnt = min(layer_cnt + 1, LAYER_MAX); @@ -182,35 +188,22 @@ void main() { return; } - // highlight intersections and cusps - for (int i = layer_cnt-1; i >= 1; --i) { - // intersections - taggedFrag frag0 = frags[i]; - taggedFrag frag1 = frags[i-1]; - float ixn_sin = length(cross(frag0.normal, frag1.normal)); - vec3 disp = frag0.pt - frag1.pt; - float ixn_dist = max( - abs(dot(frag1.normal, disp)), - abs(dot(frag0.normal, disp)) - ) / ixn_sin; - float max_highlight = max(frags[i].highlight, frags[i-1].highlight); - float ixn_highlight = 0.5 * max_highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); - frags[i].color = mix(frags[i].color, vec4(1.), ixn_highlight); - frags[i-1].color = mix(frags[i-1].color, vec4(1.), ixn_highlight); - - // cusps - float cusp_cos = abs(dot(dir, frag0.normal)); - float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[frag0.id].lt.s); - float highlight = frags[i].highlight; - float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); - frags[i].color = mix(frags[i].color, vec4(1.), cusp_highlight); - } - // composite the sphere fragments vec3 color = vec3(0.); - for (int i = layer_cnt-1; i >= layer_threshold; --i) { - vec4 frag_color = frags[i].color; - color = mix(color, frag_color.rgb, frag_color.a); + int layer = layer_cnt - 1; + TaggedDepth hit; + for (; layer >= layer_threshold; --layer) { + // shade the current fragment + hit = top_hits[layer]; + Fragment frag = sphere_shading( + sphere_list[hit.id], + hit.depth * dir, + hit.dimming * color_list[hit.id] + ); + float highlight = highlight_list[hit.id]; + + // composite the current fragment + color = mix(color, frag.color.rgb, frag.color.a); } outColor = vec4(sRGB(color), 1.); } \ No newline at end of file From abe231126d2601ec8c61536e4cb9149cb999913f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 14 Oct 2024 16:36:52 -0700 Subject: [PATCH 097/126] Display: restore intersection and cusp highlighting This increases resource use a bit, because we now have to hold two fragments in memory at once instead of just one. It's still much better than holding all of the top twelve fragments, though! --- app-proto/full-interface/src/inversive.frag | 33 ++++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/app-proto/full-interface/src/inversive.frag b/app-proto/full-interface/src/inversive.frag index 339be00..d50cb1e 100644 --- a/app-proto/full-interface/src/inversive.frag +++ b/app-proto/full-interface/src/inversive.frag @@ -191,19 +191,44 @@ void main() { // composite the sphere fragments vec3 color = vec3(0.); int layer = layer_cnt - 1; - TaggedDepth hit; + TaggedDepth hit = top_hits[layer]; + Fragment frag_next = sphere_shading( + sphere_list[hit.id], + hit.depth * dir, + hit.dimming * color_list[hit.id] + ); + float highlight_next = highlight_list[hit.id]; + --layer; for (; layer >= layer_threshold; --layer) { - // shade the current fragment + // load the current fragment + Fragment frag = frag_next; + float highlight = highlight_next; + + // shade the next fragment hit = top_hits[layer]; - Fragment frag = sphere_shading( + frag_next = sphere_shading( sphere_list[hit.id], hit.depth * dir, hit.dimming * color_list[hit.id] ); - float highlight = highlight_list[hit.id]; + highlight_next = highlight_list[hit.id]; + + // highlight intersections + float ixn_dist = intersection_dist(frag, frag_next); + float max_highlight = max(highlight, highlight_next); + float ixn_highlight = 0.5 * max_highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); + frag.color = mix(frag.color, vec4(1.), ixn_highlight); + frag_next.color = mix(frag_next.color, vec4(1.), ixn_highlight); + + // highlight cusps + float cusp_cos = abs(dot(dir, frag.normal)); + float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[hit.id].lt.s); + float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); + frag.color = mix(frag.color, vec4(1.), cusp_highlight); // composite the current fragment color = mix(color, frag.color.rgb, frag.color.a); } + color = mix(color, frag_next.color.rgb, frag_next.color.a); outColor = vec4(sRGB(color), 1.); } \ No newline at end of file From cca5a781c4595036ef271ba0bab4b54cf765cce8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 14 Oct 2024 16:43:13 -0700 Subject: [PATCH 098/126] Remove standalone display prototype --- app-proto/inversive-display/.gitignore | 4 - app-proto/inversive-display/Cargo.toml | 40 - app-proto/inversive-display/index.html | 9 - app-proto/inversive-display/main.css | 74 -- app-proto/inversive-display/src/engine.rs | 27 - app-proto/inversive-display/src/identity.vert | 7 - .../inversive-display/src/inversive.frag | 212 ----- app-proto/inversive-display/src/main.rs | 744 ------------------ 8 files changed, 1117 deletions(-) delete mode 100644 app-proto/inversive-display/.gitignore delete mode 100644 app-proto/inversive-display/Cargo.toml delete mode 100644 app-proto/inversive-display/index.html delete mode 100644 app-proto/inversive-display/main.css delete mode 100644 app-proto/inversive-display/src/engine.rs delete mode 100644 app-proto/inversive-display/src/identity.vert delete mode 100644 app-proto/inversive-display/src/inversive.frag delete mode 100644 app-proto/inversive-display/src/main.rs diff --git a/app-proto/inversive-display/.gitignore b/app-proto/inversive-display/.gitignore deleted file mode 100644 index 19aa86b..0000000 --- a/app-proto/inversive-display/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -target -dist -profiling -Cargo.lock \ No newline at end of file diff --git a/app-proto/inversive-display/Cargo.toml b/app-proto/inversive-display/Cargo.toml deleted file mode 100644 index c0cbf3d..0000000 --- a/app-proto/inversive-display/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "inversive-display" -version = "0.1.0" -authors = ["Aaron"] -edition = "2021" - -[features] -default = ["console_error_panic_hook"] - -[dependencies] -js-sys = "0.3.70" -nalgebra = "0.33.0" -sycamore = "0.9.0-beta.2" - -# The `console_error_panic_hook` crate provides better debugging of panics by -# logging them with `console.error`. This is great for development, but requires -# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for -# code size when deploying. -console_error_panic_hook = { version = "0.1.7", optional = true } - -[dependencies.web-sys] -version = "0.3.69" -features = [ - 'HtmlCanvasElement', - 'Performance', - 'WebGl2RenderingContext', - 'WebGlBuffer', - 'WebGlProgram', - 'WebGlShader', - 'WebGlUniformLocation', - 'WebGlVertexArrayObject', - 'Window' -] - -[dev-dependencies] -wasm-bindgen-test = "0.3.34" - -[profile.release] -opt-level = "s" # optimize for small code size -debug = true # include debug symbols diff --git a/app-proto/inversive-display/index.html b/app-proto/inversive-display/index.html deleted file mode 100644 index 9c45180..0000000 --- a/app-proto/inversive-display/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - Inversive display - - - - diff --git a/app-proto/inversive-display/main.css b/app-proto/inversive-display/main.css deleted file mode 100644 index f8f7a96..0000000 --- a/app-proto/inversive-display/main.css +++ /dev/null @@ -1,74 +0,0 @@ -body { - margin-left: 20px; - margin-top: 20px; - color: #fcfcfc; - background-color: #202020; -} - -#app { - display: flex; - flex-direction: column; - width: 600px; -} - -canvas { - float: left; - background-color: #020202; - border: 1px solid #555; - border-radius: 10px; - margin-top: 5px; -} - -canvas:focus { - border-color: #aaa; -} - -.hidden { - display: none; -} - -.control, .tab-pane { - display: flex; - flex-direction: row; - width: 600px; -} - -input[type="radio"] { - appearance: none; - width: 0px; - height: 0px; - padding: 0px; - margin: 0px; - outline: none; -} - -.tab-pane > label { - border: 1px solid #aaa; - border-radius: 5px; - text-align: center; - padding: 5px; - margin-right: 10px; - margin-bottom: 5px; -} - -.tab-pane > label:has(:checked) { - border-color: #fcfcfc; - background-color: #555; -} - -.tab-pane > label:has(:focus-visible) { - outline: medium auto currentColor; -} - -.tab-pane > label:hover:not(:has(:checked)) { - border-color: #bbb; - background-color: #333; -} - -.control > span { - width: 170px; -} - -input { - flex-grow: 1; -} \ No newline at end of file diff --git a/app-proto/inversive-display/src/engine.rs b/app-proto/inversive-display/src/engine.rs deleted file mode 100644 index 79668bb..0000000 --- a/app-proto/inversive-display/src/engine.rs +++ /dev/null @@ -1,27 +0,0 @@ -use nalgebra::DVector; - -// the sphere with the given center and radius, with inward-pointing normals -pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVector { - let center_norm_sq = center_x * center_x + center_y * center_y + center_z * center_z; - DVector::from_column_slice(&[ - center_x / radius, - center_y / radius, - center_z / radius, - 0.5 / radius, - 0.5 * (center_norm_sq / radius - radius) - ]) -} - -// the sphere of curvature `curv` whose closest point to the origin has position -// `off * dir` and normal `dir`, where `dir` is a unit vector. setting the -// curvature to zero gives a plane -pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f64) -> DVector { - let norm_sp = 1.0 + off * curv; - DVector::from_column_slice(&[ - norm_sp * dir_x, - norm_sp * dir_y, - norm_sp * dir_z, - 0.5 * curv, - off * (1.0 + 0.5 * off * curv) - ]) -} \ No newline at end of file diff --git a/app-proto/inversive-display/src/identity.vert b/app-proto/inversive-display/src/identity.vert deleted file mode 100644 index 183a65f..0000000 --- a/app-proto/inversive-display/src/identity.vert +++ /dev/null @@ -1,7 +0,0 @@ -#version 300 es - -in vec4 position; - -void main() { - gl_Position = position; -} \ No newline at end of file diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag deleted file mode 100644 index f76995d..0000000 --- a/app-proto/inversive-display/src/inversive.frag +++ /dev/null @@ -1,212 +0,0 @@ -#version 300 es - -precision highp float; - -out vec4 outColor; - -// --- inversive geometry --- - -struct vecInv { - vec3 sp; - vec2 lt; -}; - -// --- uniforms --- - -// construction -const int SPHERE_MAX = 200; -uniform int sphere_cnt; -uniform vecInv sphere_list[SPHERE_MAX]; -uniform vec3 color_list[SPHERE_MAX]; - -// view -uniform vec2 resolution; -uniform float shortdim; - -// controls -uniform float opacity; -uniform float highlight; -uniform int layer_threshold; -uniform bool debug_mode; - -// light and camera -const float focal_slope = 0.3; -const vec3 light_dir = normalize(vec3(2., 2., 1.)); -const float ixn_threshold = 0.005; -const float INTERIOR_DIMMING = 0.7; - -// --- sRGB --- - -// map colors from RGB space to sRGB space, as specified in the sRGB standard -// (IEC 61966-2-1:1999) -// -// https://www.color.org/sRGB.pdf -// https://www.color.org/chardata/rgb/srgb.xalter -// -// in RGB space, color value is proportional to light intensity, so linear -// color-vector interpolation corresponds to physical light mixing. in sRGB -// space, the color encoding used by many monitors, we use more of the value -// interval to represent low intensities, and less of the interval to represent -// high intensities. this improves color quantization - -float sRGB(float t) { - if (t <= 0.0031308) { - return 12.92*t; - } else { - return 1.055*pow(t, 5./12.) - 0.055; - } -} - -vec3 sRGB(vec3 color) { - return vec3(sRGB(color.r), sRGB(color.g), sRGB(color.b)); -} - -// --- shading --- - -struct taggedFrag { - int id; - vec4 color; - vec3 pt; - vec3 normal; -}; - -taggedFrag sphere_shading(vecInv v, vec3 pt, vec3 base_color, int id) { - // the expression for normal needs to be checked. it's supposed to give the - // negative gradient of the lorentz product between the impact point vector - // and the sphere vector with respect to the coordinates of the impact - // point. i calculated it in my head and decided that the result looked good - // enough for now - vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt); - - float incidence = dot(normal, light_dir); - float illum = mix(0.4, 1.0, max(incidence, 0.0)); - return taggedFrag(id, vec4(illum * base_color, opacity), pt, normal); -} - -// --- ray-casting --- - -// if `a/b` is less than this threshold, we approximate `a*u^2 + b*u + c` by -// the linear function `b*u + c` -const float DEG_THRESHOLD = 1e-9; - -// the depths, represented as multiples of `dir`, where the line generated by -// `dir` hits the sphere represented by `v`. if both depths are positive, the -// smaller one is returned in the first component. if only one depth is -// positive, it could be returned in either component -vec2 sphere_cast(vecInv v, vec3 dir) { - float a = -v.lt.s * dot(dir, dir); - float b = dot(v.sp, dir); - float c = -v.lt.t; - - float adjust = 4.*a*c/(b*b); - if (adjust < 1.) { - // 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` - float square_rect_ratio = 1. + sqrt(1. - adjust); - float lin_root = -(2.*c)/b / square_rect_ratio; - if (abs(a) > DEG_THRESHOLD * abs(b)) { - return vec2(lin_root, -b/(2.*a) * square_rect_ratio); - } else { - return vec2(lin_root, -1.); - } - } else { - // the line through `dir` misses the sphere completely - return vec2(-1., -1.); - } -} - -void main() { - vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; - vec3 dir = vec3(focal_slope * scr, -1.); - - // cast rays through the spheres - const int LAYER_MAX = 12; - taggedFrag frags [LAYER_MAX]; - int layer_cnt = 0; - for (int id = 0; id < sphere_cnt; ++id) { - // find out where the ray hits the sphere - vec2 hit_depths = sphere_cast(sphere_list[id], dir); - - // insertion-sort the fragments we hit into the fragment list - float dimming = 1.; - for (int side = 0; side < 2; ++side) { - float hit_z = -hit_depths[side]; - if (0. > hit_z) { - for (int layer = layer_cnt; layer >= 0; --layer) { - if (layer < 1 || frags[layer-1].pt.z >= hit_z) { - // we're not as close to the screen as the fragment - // before the empty slot, so insert here - if (layer < LAYER_MAX) { - frags[layer] = sphere_shading( - sphere_list[id], - hit_depths[side] * dir, - dimming * color_list[id], - id - ); - } - break; - } else { - // we're closer to the screen than the fragment before - // the empty slot, so move that fragment into the empty - // slot - frags[layer] = frags[layer-1]; - } - } - layer_cnt = min(layer_cnt + 1, LAYER_MAX); - dimming = INTERIOR_DIMMING; - } - } - } - - /* DEBUG */ - // in debug mode, show the layer count instead of the shaded image - if (debug_mode) { - // at the bottom of the screen, show the color scale instead of the - // layer count - if (gl_FragCoord.y < 10.) layer_cnt = int(16. * gl_FragCoord.x / resolution.x); - - // convert number to color - ivec3 bits = layer_cnt / ivec3(1, 2, 4); - vec3 color = mod(vec3(bits), 2.); - if (layer_cnt % 16 >= 8) { - color = mix(color, vec3(0.5), 0.5); - } - outColor = vec4(color, 1.); - return; - } - - // highlight intersections and cusps - for (int i = layer_cnt-1; i >= 1; --i) { - // intersections - taggedFrag frag0 = frags[i]; - taggedFrag frag1 = frags[i-1]; - float ixn_sin = length(cross(frag0.normal, frag1.normal)); - vec3 disp = frag0.pt - frag1.pt; - float ixn_dist = max( - abs(dot(frag1.normal, disp)), - abs(dot(frag0.normal, disp)) - ) / ixn_sin; - float ixn_highlight = 0.5 * highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); - frags[i].color = mix(frags[i].color, vec4(1.), ixn_highlight); - frags[i-1].color = mix(frags[i-1].color, vec4(1.), ixn_highlight); - - // cusps - float cusp_cos = abs(dot(dir, frag0.normal)); - float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[frag0.id].lt.s); - float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); - frags[i].color = mix(frags[i].color, vec4(1.), cusp_highlight); - } - - // composite the sphere fragments - vec3 color = vec3(0.); - for (int i = layer_cnt-1; i >= layer_threshold; --i) { - vec4 frag_color = frags[i].color; - color = mix(color, frag_color.rgb, frag_color.a); - } - outColor = vec4(sRGB(color), 1.); -} \ No newline at end of file diff --git a/app-proto/inversive-display/src/main.rs b/app-proto/inversive-display/src/main.rs deleted file mode 100644 index 8d16732..0000000 --- a/app-proto/inversive-display/src/main.rs +++ /dev/null @@ -1,744 +0,0 @@ -// based on the WebGL example in the `wasm-bindgen` guide -// -// https://rustwasm.github.io/wasm-bindgen/examples/webgl.html -// -// and this StackOverflow answer by wangdq -// -// https://stackoverflow.com/a/39684775 -// - -use core::array; -use nalgebra::{DMatrix, DVector, Rotation3, Vector3}; -use sycamore::{prelude::*, motion::create_raf, rt::{JsCast, JsValue}}; -use web_sys::{ - console, - window, - KeyboardEvent, - WebGl2RenderingContext, - WebGlProgram, - WebGlShader, - WebGlUniformLocation -}; - -mod engine; - -fn compile_shader( - context: &WebGl2RenderingContext, - shader_type: u32, - source: &str, -) -> WebGlShader { - let shader = context.create_shader(shader_type).unwrap(); - context.shader_source(&shader, source); - context.compile_shader(&shader); - shader -} - -fn get_uniform_array_locations( - context: &WebGl2RenderingContext, - program: &WebGlProgram, - var_name: &str, - member_name_opt: Option<&str> -) -> [Option; N] { - array::from_fn(|n| { - let name = match member_name_opt { - Some(member_name) => format!("{var_name}[{n}].{member_name}"), - None => format!("{var_name}[{n}]") - }; - context.get_uniform_location(&program, name.as_str()) - }) -} - -// load the given data into the vertex input of the given name -fn bind_vertex_attrib( - context: &WebGl2RenderingContext, - index: u32, - size: i32, - data: &[f32] -) { - // create a data buffer and bind it to ARRAY_BUFFER - let buffer = context.create_buffer().unwrap(); - context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer)); - - // 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 - unsafe { - context.buffer_data_with_array_buffer_view( - WebGl2RenderingContext::ARRAY_BUFFER, - &js_sys::Float32Array::view(&data), - WebGl2RenderingContext::STATIC_DRAW, - ); - } - - // 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 - ); -} - -fn push_gen_construction( - sphere_vec: &mut Vec>, - color_vec: &mut Vec<[f32; 3]>, - construction_to_world: &DMatrix, - ctrl_x: f64, - ctrl_y: f64, - radius_x: f64, - radius_y: f64 -) { - // push spheres - sphere_vec.push(construction_to_world * engine::sphere(0.5, 0.5, ctrl_x, radius_x)); - sphere_vec.push(construction_to_world * engine::sphere(-0.5, -0.5, ctrl_y, radius_y)); - sphere_vec.push(construction_to_world * engine::sphere(-0.5, 0.5, 0.0, 0.75)); - sphere_vec.push(construction_to_world * engine::sphere(0.5, -0.5, 0.0, 0.5)); - sphere_vec.push(construction_to_world * engine::sphere(0.0, 0.15, 1.0, 0.25)); - sphere_vec.push(construction_to_world * engine::sphere(0.0, -0.15, -1.0, 0.25)); - - // push colors - color_vec.push([1.00_f32, 0.25_f32, 0.00_f32]); - color_vec.push([0.00_f32, 0.25_f32, 1.00_f32]); - color_vec.push([0.25_f32, 0.00_f32, 1.00_f32]); - color_vec.push([0.25_f32, 1.00_f32, 0.00_f32]); - color_vec.push([0.75_f32, 0.75_f32, 0.00_f32]); - color_vec.push([0.00_f32, 0.75_f32, 0.50_f32]); -} - -fn push_low_curv_construction( - sphere_vec: &mut Vec>, - color_vec: &mut Vec<[f32; 3]>, - construction_to_world: &DMatrix, - off1: f64, - off2: f64, - off3: f64, - curv1: f64, - curv2: f64, - curv3: f64, -) { - // push spheres - let a = 0.75_f64.sqrt(); - sphere_vec.push(construction_to_world * engine::sphere(0.0, 0.0, 0.0, 1.0)); - sphere_vec.push(construction_to_world * engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0)); - sphere_vec.push(construction_to_world * engine::sphere_with_offset(1.0, 0.0, 0.0, off1, curv1)); - sphere_vec.push(construction_to_world * engine::sphere_with_offset(-0.5, a, 0.0, off2, curv2)); - sphere_vec.push(construction_to_world * engine::sphere_with_offset(-0.5, -a, 0.0, off3, curv3)); - sphere_vec.push(construction_to_world * engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0)); - sphere_vec.push(construction_to_world * engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0)); - sphere_vec.push(construction_to_world * engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0)); - - // push colors - color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); - color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); - color_vec.push([1.00_f32, 0.00_f32, 0.25_f32]); - color_vec.push([0.25_f32, 1.00_f32, 0.00_f32]); - color_vec.push([0.00_f32, 0.25_f32, 1.00_f32]); - color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); - color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); - color_vec.push([0.75_f32, 0.75_f32, 0.75_f32]); -} - -#[derive(Clone, Copy, PartialEq)] -enum Tab { - GenTab, - LowCurvTab -} - -fn main() { - // set up a config option that forwards panic messages to `console.error` - #[cfg(feature = "console_error_panic_hook")] - console_error_panic_hook::set_once(); - - sycamore::render(|| { - // tab selection - let tab_selection = create_signal(Tab::GenTab); - - // navigation - let pitch_up = create_signal(0.0); - let pitch_down = create_signal(0.0); - let yaw_right = create_signal(0.0); - let yaw_left = create_signal(0.0); - let roll_ccw = create_signal(0.0); - let roll_cw = create_signal(0.0); - let zoom_in = create_signal(0.0); - let zoom_out = create_signal(0.0); - - // controls for general example - let gen_controls = create_node_ref(); - let ctrl_x = create_signal(0.0); - let ctrl_y = create_signal(0.0); - let radius_x = create_signal(1.0); - let radius_y = create_signal(1.0); - - // controls for low-curvature example - let low_curv_controls = create_node_ref(); - let curv1 = create_signal(0.0); - let curv2 = create_signal(0.0); - let curv3 = create_signal(0.0); - let off1 = create_signal(1.0); - let off2 = create_signal(1.0); - let off3 = create_signal(1.0); - - // shared controls - let opacity = create_signal(0.5); - let highlight = create_signal(0.2); - let turntable = create_signal(false); - let layer_threshold = create_signal(0.0); /* DEBUG */ - let debug_mode = create_signal(false); /* DEBUG */ - - /* INSTRUMENTS */ - const SAMPLE_PERIOD: i32 = 60; - let mut last_sample_time = 0.0; - let mut frames_since_last_sample = 0; - let mean_frame_interval = create_signal(0.0); - - // display - let display = create_node_ref(); - - // change listener - let scene_changed = create_signal(true); - create_effect(move || { - // track tab selection - tab_selection.track(); - - // track controls for general example - ctrl_x.track(); - ctrl_y.track(); - radius_x.track(); - radius_y.track(); - - // track controls for low-curvature example - curv1.track(); - curv2.track(); - curv3.track(); - off1.track(); - off2.track(); - off3.track(); - - // track shared controls - opacity.track(); - highlight.track(); - turntable.track(); - layer_threshold.track(); - debug_mode.track(); - - scene_changed.set(true); - }); - - on_mount(move || { - // tab listener - create_effect(move || { - // get the control panel nodes - let gen_controls_node = gen_controls.get::(); - let low_curv_controls_node = low_curv_controls.get::(); - - // hide all the control panels - gen_controls_node.add_class("hidden"); - low_curv_controls_node.add_class("hidden"); - - // show the selected control panel - match tab_selection.get() { - Tab::GenTab => gen_controls_node.remove_class("hidden"), - Tab::LowCurvTab => low_curv_controls_node.remove_class("hidden") - } - }); - - // create list of construction elements - const SPHERE_MAX: usize = 200; - let mut sphere_vec = Vec::>::new(); - let mut color_vec = Vec::<[f32; 3]>::new(); - - // timing - let mut last_time = 0.0; - - // scene parameters - const ROT_SPEED: f64 = 0.4; // in radians per second - const TURNTABLE_SPEED: f64 = 0.1; // in radians per second - const ZOOM_SPEED: f64 = 0.15; - let mut orientation = DMatrix::::identity(5, 5); - let mut rotation = DMatrix::::identity(5, 5); - let mut location_z: f64 = 5.0; - - /* INSTRUMENTS */ - let performance = window().unwrap().performance().unwrap(); - - // get the display canvas - let canvas = display - .get::() - .unchecked_into::(); - let ctx = canvas - .get_context("webgl2") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap(); - - // compile and attach the vertex and fragment shaders - let vertex_shader = compile_shader( - &ctx, - WebGl2RenderingContext::VERTEX_SHADER, - include_str!("identity.vert"), - ); - 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 */ - // print the maximum number of vectors that can be passed as - // uniforms to a fragment shader. the OpenGL ES 3.0 standard - // requires this maximum to be at least 224, as discussed in the - // documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter - // here: - // - // https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml - // - // there are also other size limits. for example, on Aaron's - // machine, the the length of a float or genType array seems to be - // capped at 1024 elements - console::log_2( - &ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(), - &JsValue::from("uniform vectors available") - ); - - // find indices of vertex attributes and uniforms - let position_index = ctx.get_attrib_location(&program, "position") as u32; - let sphere_cnt_loc = ctx.get_uniform_location(&program, "sphere_cnt"); - let sphere_sp_locs = get_uniform_array_locations::( - &ctx, &program, "sphere_list", Some("sp") - ); - let sphere_lt_locs = get_uniform_array_locations::( - &ctx, &program, "sphere_list", Some("lt") - ); - let color_locs = get_uniform_array_locations::( - &ctx, &program, "color_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 highlight_loc = ctx.get_uniform_location(&program, "highlight"); - 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] = [ - // northwest triangle - -1.0, -1.0, 0.0, - -1.0, 1.0, 0.0, - 1.0, 1.0, 0.0, - // southeast triangle - -1.0, -1.0, 0.0, - 1.0, 1.0, 0.0, - 1.0, -1.0, 0.0 - ]; - bind_vertex_attrib(&ctx, position_index, 3, &positions); - - // set up a repainting routine - let (_, start_animation_loop, _) = create_raf(move || { - // get the time step - let time = performance.now(); - let time_step = 0.001*(time - last_time); - last_time = time; - - // get the navigation state - let pitch_up_val = pitch_up.get(); - let pitch_down_val = pitch_down.get(); - let yaw_right_val = yaw_right.get(); - let yaw_left_val = yaw_left.get(); - let roll_ccw_val = roll_ccw.get(); - let roll_cw_val = roll_cw.get(); - let zoom_in_val = zoom_in.get(); - let zoom_out_val = zoom_out.get(); - let turntable_val = turntable.get(); - - // update the construction's orientation - let ang_vel = { - let pitch = pitch_up_val - pitch_down_val; - let yaw = yaw_right_val - yaw_left_val; - let roll = roll_ccw_val - roll_cw_val; - let ang_vel_from_keyboard = - if pitch != 0.0 || yaw != 0.0 || roll != 0.0 { - ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize() - } else { - Vector3::zeros() - }; - let ang_vel_from_turntable = - if turntable_val { - Vector3::new(0.0, TURNTABLE_SPEED, 0.0) - } else { - Vector3::zeros() - }; - ang_vel_from_keyboard + ang_vel_from_turntable - }; - let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0); - rotation_sp.copy_from( - Rotation3::from_scaled_axis(time_step * ang_vel).matrix() - ); - orientation = &rotation * &orientation; - - // update the construction's location - let zoom = zoom_out_val - zoom_in_val; - location_z *= (time_step * ZOOM_SPEED * zoom).exp(); - - if scene_changed.get() { - /* INSTRUMENTS */ - // measure mean frame interval - frames_since_last_sample += 1; - if frames_since_last_sample >= SAMPLE_PERIOD { - mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64)); - last_sample_time = time; - frames_since_last_sample = 0; - } - - // find the map from construction space to world space - let location = { - let u = -location_z; - DMatrix::from_column_slice(5, 5, &[ - 1.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, u, - 0.0, 0.0, 2.0*u, 1.0, u*u, - 0.0, 0.0, 0.0, 0.0, 1.0 - ]) - }; - let construction_to_world = &location * &orientation; - - // update the construction - sphere_vec.clear(); - color_vec.clear(); - match tab_selection.get() { - Tab::GenTab => push_gen_construction( - &mut sphere_vec, - &mut color_vec, - &construction_to_world, - ctrl_x.get(), ctrl_y.get(), - radius_x.get(), radius_y.get() - ), - Tab::LowCurvTab => push_low_curv_construction( - &mut sphere_vec, - &mut color_vec, - &construction_to_world, - off1.get(), off2.get(), off3.get(), - curv1.get(), curv2.get(), curv3.get(), - ) - }; - - // 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 construction - ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_vec.len() as i32); - for n in 0..sphere_vec.len() { - let v = &sphere_vec[n]; - ctx.uniform3f( - sphere_sp_locs[n].as_ref(), - v[0] as f32, v[1] as f32, v[2] as f32 - ); - ctx.uniform2f( - sphere_lt_locs[n].as_ref(), - v[3] as f32, v[4] as f32 - ); - ctx.uniform3fv_with_f32_array( - color_locs[n].as_ref(), - &color_vec[n] - ); - } - - // pass the control parameters - ctx.uniform1f(opacity_loc.as_ref(), opacity.get() as f32); - ctx.uniform1f(highlight_loc.as_ref(), highlight.get() as f32); - ctx.uniform1i(layer_threshold_loc.as_ref(), layer_threshold.get() as i32); - ctx.uniform1i(debug_mode_loc.as_ref(), debug_mode.get() as i32); - - // draw the scene - ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); - - // clear scene change flag - scene_changed.set( - pitch_up_val != 0.0 - || pitch_down_val != 0.0 - || yaw_left_val != 0.0 - || yaw_right_val != 0.0 - || roll_cw_val != 0.0 - || roll_ccw_val != 0.0 - || zoom_in_val != 0.0 - || zoom_out_val != 0.0 - || turntable_val - ); - } else { - frames_since_last_sample = 0; - mean_frame_interval.set(-1.0); - } - }); - start_animation_loop(); - }); - - let set_nav_signal = move |event: KeyboardEvent, value: f64| { - let mut navigating = true; - let shift = event.shift_key(); - match event.key().as_str() { - "ArrowUp" if shift => zoom_in.set(value), - "ArrowDown" if shift => zoom_out.set(value), - "ArrowUp" => pitch_up.set(value), - "ArrowDown" => pitch_down.set(value), - "ArrowRight" if shift => roll_cw.set(value), - "ArrowLeft" if shift => roll_ccw.set(value), - "ArrowRight" => yaw_right.set(value), - "ArrowLeft" => yaw_left.set(value), - _ => navigating = false - }; - if navigating { - scene_changed.set(true); - event.prevent_default(); - } - }; - - view! { - div(id="app") { - div(class="tab-pane") { - label { - "General" - input( - type="radio", - name="tab", - prop:checked=tab_selection.get() == Tab::GenTab, - on:click=move |_| tab_selection.set(Tab::GenTab) - ) - } - label { - "Low curvature" - input( - type="radio", - name="tab", - prop:checked=tab_selection.get() == Tab::LowCurvTab, - on:change=move |_| tab_selection.set(Tab::LowCurvTab) - ) - } - } - div { "Mean frame interval: " (mean_frame_interval.get()) " ms" } - canvas( - ref=display, - width=600, - height=600, - tabindex=0, - on:keydown=move |event: KeyboardEvent| { - if event.key() == "Shift" { - roll_cw.set(yaw_right.get()); - roll_ccw.set(yaw_left.get()); - zoom_in.set(pitch_up.get()); - zoom_out.set(pitch_down.get()); - yaw_right.set(0.0); - yaw_left.set(0.0); - pitch_up.set(0.0); - pitch_down.set(0.0); - } else { - set_nav_signal(event, 1.0); - } - }, - on:keyup=move |event: KeyboardEvent| { - if event.key() == "Shift" { - yaw_right.set(roll_cw.get()); - yaw_left.set(roll_ccw.get()); - pitch_up.set(zoom_in.get()); - pitch_down.set(zoom_out.get()); - roll_cw.set(0.0); - roll_ccw.set(0.0); - zoom_in.set(0.0); - zoom_out.set(0.0); - } else { - set_nav_signal(event, 0.0); - } - }, - on:blur=move |_| { - pitch_up.set(0.0); - pitch_down.set(0.0); - yaw_right.set(0.0); - yaw_left.set(0.0); - roll_ccw.set(0.0); - roll_cw.set(0.0); - } - ) - div(ref=gen_controls) { - label(class="control") { - span { "Sphere 0 depth" } - input( - type="range", - min=-1.0, - max=1.0, - step=0.001, - bind:valueAsNumber=ctrl_x - ) - } - label(class="control") { - span { "Sphere 1 depth" } - input( - type="range", - min=-1.0, - max=1.0, - step=0.001, - bind:valueAsNumber=ctrl_y - ) - } - label(class="control") { - span { "Sphere 0 radius" } - input( - type="range", - min=0.5, - max=1.5, - step=0.001, - bind:valueAsNumber=radius_x - ) - } - label(class="control") { - span { "Sphere 1 radius" } - input( - type="range", - min=0.5, - max=1.5, - step=0.001, - bind:valueAsNumber=radius_y - ) - } - } - div(ref=low_curv_controls) { - label(class="control") { - span { "Sphere 1 offset" } - input( - type="range", - min=-1.0, - max=1.0, - step=0.001, - bind:valueAsNumber=off1 - ) - } - label(class="control") { - span { "Sphere 2 offset" } - input( - type="range", - min=-1.0, - max=1.0, - step=0.001, - bind:valueAsNumber=off2 - ) - } - label(class="control") { - span { "Sphere 3 offset" } - input( - type="range", - min=-1.0, - max=1.0, - step=0.001, - bind:valueAsNumber=off3 - ) - } - label(class="control") { - span { "Sphere 1 curvature" } - input( - type="range", - min=0.0, - max=2.0, - step=0.001, - bind:valueAsNumber=curv1 - ) - } - label(class="control") { - span { "Sphere 2 curvature" } - input( - type="range", - min=0.0, - max=2.0, - step=0.001, - bind:valueAsNumber=curv2 - ) - } - label(class="control") { - span { "Sphere 3 curvature" } - input( - type="range", - min=0.0, - max=2.0, - step=0.001, - bind:valueAsNumber=curv3 - ) - } - } - label(class="control") { - span { "Opacity" } - input( - type="range", - max=1.0, - step=0.001, - bind:valueAsNumber=opacity - ) - } - label(class="control") { - span { "Highlight" } - input( - type="range", - max=1.0, - step=0.001, - bind:valueAsNumber=highlight - ) - } - label(class="control") { - span { "Turntable" } - input( - type="checkbox", - bind:checked=turntable - ) - } - label(class="control") { - span { "Layer threshold" } - input( - type="range", - max=5.0, - step=1.0, - bind:valueAsNumber=layer_threshold - ) - } - label(class="control") { - span { "Debug mode" } - input( - type="checkbox", - bind:checked=debug_mode - ) - } - } - } - }); -} \ No newline at end of file From f1690b62e19e037a55ab968d50b40dc52840de2f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 14 Oct 2024 17:08:44 -0700 Subject: [PATCH 099/126] Move full interface prototype to top level --- app-proto/{full-interface => }/.gitignore | 0 app-proto/{full-interface => }/Cargo.toml | 0 app-proto/{full-interface => }/index.html | 0 app-proto/{full-interface => }/main.css | 0 app-proto/{full-interface => }/src/add_remove.rs | 0 app-proto/{full-interface => }/src/assembly.rs | 0 app-proto/{full-interface => }/src/display.rs | 0 app-proto/{full-interface => }/src/engine.rs | 0 app-proto/{full-interface => }/src/identity.vert | 0 app-proto/{full-interface => }/src/inversive.frag | 0 app-proto/{full-interface => }/src/main.rs | 0 app-proto/{full-interface => }/src/outline.rs | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename app-proto/{full-interface => }/.gitignore (100%) rename app-proto/{full-interface => }/Cargo.toml (100%) rename app-proto/{full-interface => }/index.html (100%) rename app-proto/{full-interface => }/main.css (100%) rename app-proto/{full-interface => }/src/add_remove.rs (100%) rename app-proto/{full-interface => }/src/assembly.rs (100%) rename app-proto/{full-interface => }/src/display.rs (100%) rename app-proto/{full-interface => }/src/engine.rs (100%) rename app-proto/{full-interface => }/src/identity.vert (100%) rename app-proto/{full-interface => }/src/inversive.frag (100%) rename app-proto/{full-interface => }/src/main.rs (100%) rename app-proto/{full-interface => }/src/outline.rs (100%) diff --git a/app-proto/full-interface/.gitignore b/app-proto/.gitignore similarity index 100% rename from app-proto/full-interface/.gitignore rename to app-proto/.gitignore diff --git a/app-proto/full-interface/Cargo.toml b/app-proto/Cargo.toml similarity index 100% rename from app-proto/full-interface/Cargo.toml rename to app-proto/Cargo.toml diff --git a/app-proto/full-interface/index.html b/app-proto/index.html similarity index 100% rename from app-proto/full-interface/index.html rename to app-proto/index.html diff --git a/app-proto/full-interface/main.css b/app-proto/main.css similarity index 100% rename from app-proto/full-interface/main.css rename to app-proto/main.css diff --git a/app-proto/full-interface/src/add_remove.rs b/app-proto/src/add_remove.rs similarity index 100% rename from app-proto/full-interface/src/add_remove.rs rename to app-proto/src/add_remove.rs diff --git a/app-proto/full-interface/src/assembly.rs b/app-proto/src/assembly.rs similarity index 100% rename from app-proto/full-interface/src/assembly.rs rename to app-proto/src/assembly.rs diff --git a/app-proto/full-interface/src/display.rs b/app-proto/src/display.rs similarity index 100% rename from app-proto/full-interface/src/display.rs rename to app-proto/src/display.rs diff --git a/app-proto/full-interface/src/engine.rs b/app-proto/src/engine.rs similarity index 100% rename from app-proto/full-interface/src/engine.rs rename to app-proto/src/engine.rs diff --git a/app-proto/full-interface/src/identity.vert b/app-proto/src/identity.vert similarity index 100% rename from app-proto/full-interface/src/identity.vert rename to app-proto/src/identity.vert diff --git a/app-proto/full-interface/src/inversive.frag b/app-proto/src/inversive.frag similarity index 100% rename from app-proto/full-interface/src/inversive.frag rename to app-proto/src/inversive.frag diff --git a/app-proto/full-interface/src/main.rs b/app-proto/src/main.rs similarity index 100% rename from app-proto/full-interface/src/main.rs rename to app-proto/src/main.rs diff --git a/app-proto/full-interface/src/outline.rs b/app-proto/src/outline.rs similarity index 100% rename from app-proto/full-interface/src/outline.rs rename to app-proto/src/outline.rs From 517fd327fa5b2e47694e244ae1da749f3a26b7ef Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 16 Oct 2024 23:16:29 -0700 Subject: [PATCH 100/126] Assembly: mark constraints as active or not --- app-proto/main.css | 4 ++++ app-proto/src/add_remove.rs | 6 ++++-- app-proto/src/assembly.rs | 3 ++- app-proto/src/outline.rs | 1 + 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index a687aac..bdbacfb 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -104,6 +104,10 @@ details[open]:has(li) .elt-switch::after { font-style: italic; } +.cst > input { + margin: 0px 8px 0px 0px; +} + /* display */ canvas { diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 40b0e98..ab5db70 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -66,7 +66,8 @@ fn load_gen_assemb(assembly: &Assembly) { assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_a"]), assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_b"]) ), - rep: 0.5 + rep: 0.5, + active: create_signal(true) } ); } @@ -211,7 +212,8 @@ pub fn AddRemove() -> View { ); state.assembly.insert_constraint(Constraint { args: args, - rep: 0.0 + rep: 0.0, + active: create_signal(true) }); state.selection.update(|sel| sel.clear()); diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index c0c9959..e8dab79 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -16,7 +16,8 @@ pub struct Element { #[derive(Clone)] pub struct Constraint { pub args: (usize, usize), - pub rep: f64 + pub rep: f64, + pub active: Signal } // a complete, view-independent description of an assembly diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index c980887..4e4de9c 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -136,6 +136,7 @@ pub fn Outline() -> View { let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone()); view! { li(class="cst") { + input(r#type="checkbox", bind:checked=cst.active) div(class="cst-label") { (other_arg_label) } div(class="cst-rep") { (cst.rep) } } From 86fa682b311733a0b7bb0b538c6aaef53937d768 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 21 Oct 2024 23:38:27 +0000 Subject: [PATCH 101/126] feat: Application prototype (#14) Creates a prototype user interface for dyna3 in the `app-proto` folder. The interface is dynamically constructed using [Sycamore](https://sycamore.dev). The prototype includes: * An application state model (the `AppState` type) * A constraint problem model (the `Assembly` type), used in the application state * Two views * A 3D rendering of the assembly (the `Display` component) * A list of elements and constraints (the `Outline` component) The following features confirm that the views can reflect and send input to the model: * You can select elements by clicking and shift-clicking them in the outline. The selected elements are highlighted in the display. * You can add elements using a button above the outline. The new elements appear in the display. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/14 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/.gitignore | 4 + app-proto/Cargo.toml | 42 ++++ app-proto/index.html | 9 + app-proto/main.css | 124 ++++++++++ app-proto/src/add_remove.rs | 242 +++++++++++++++++++ app-proto/src/assembly.rs | 93 ++++++++ app-proto/src/display.rs | 443 +++++++++++++++++++++++++++++++++++ app-proto/src/engine.rs | 27 +++ app-proto/src/identity.vert | 7 + app-proto/src/inversive.frag | 234 ++++++++++++++++++ app-proto/src/main.rs | 42 ++++ app-proto/src/outline.rs | 161 +++++++++++++ 12 files changed, 1428 insertions(+) create mode 100644 app-proto/.gitignore create mode 100644 app-proto/Cargo.toml create mode 100644 app-proto/index.html create mode 100644 app-proto/main.css create mode 100644 app-proto/src/add_remove.rs create mode 100644 app-proto/src/assembly.rs create mode 100644 app-proto/src/display.rs create mode 100644 app-proto/src/engine.rs create mode 100644 app-proto/src/identity.vert create mode 100644 app-proto/src/inversive.frag create mode 100644 app-proto/src/main.rs create mode 100644 app-proto/src/outline.rs diff --git a/app-proto/.gitignore b/app-proto/.gitignore new file mode 100644 index 0000000..19aa86b --- /dev/null +++ b/app-proto/.gitignore @@ -0,0 +1,4 @@ +target +dist +profiling +Cargo.lock \ No newline at end of file diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml new file mode 100644 index 0000000..920469a --- /dev/null +++ b/app-proto/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "sketch-outline" +version = "0.1.0" +authors = ["Aaron"] +edition = "2021" + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +itertools = "0.13.0" +js-sys = "0.3.70" +nalgebra = "0.33.0" +rustc-hash = "2.0.0" +slab = "0.4.9" +sycamore = "0.9.0-beta.3" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.7", optional = true } + +[dependencies.web-sys] +version = "0.3.69" +features = [ + 'HtmlCanvasElement', + 'Performance', + 'WebGl2RenderingContext', + 'WebGlBuffer', + 'WebGlProgram', + 'WebGlShader', + 'WebGlUniformLocation', + 'WebGlVertexArrayObject' +] + +[dev-dependencies] +wasm-bindgen-test = "0.3.34" + +[profile.release] +opt-level = "s" # optimize for small code size +debug = true # include debug symbols diff --git a/app-proto/index.html b/app-proto/index.html new file mode 100644 index 0000000..5474fe9 --- /dev/null +++ b/app-proto/index.html @@ -0,0 +1,9 @@ + + + + + Sketch outline + + + + diff --git a/app-proto/main.css b/app-proto/main.css new file mode 100644 index 0000000..bdbacfb --- /dev/null +++ b/app-proto/main.css @@ -0,0 +1,124 @@ +body { + margin: 0px; + color: #fcfcfc; + background-color: #222; +} + +/* sidebar */ + +#sidebar { + display: flex; + flex-direction: column; + float: left; + width: 450px; + height: 100vh; + margin: 0px; + padding: 0px; + border-width: 0px 1px 0px 0px; + border-style: solid; + border-color: #555; +} + +/* add-remove */ + +#add-remove { + display: flex; + gap: 8px; + margin: 8px; +} + +#add-remove > button { + width: 32px; + height: 32px; + font-size: large; +} + +/* outline */ + +#outline { + flex-grow: 1; + margin: 0px; + padding: 0px; + overflow-y: scroll; +} + +li { + user-select: none; +} + +summary { + display: flex; +} + +summary.selected { + color: #fff; + background-color: #444; +} + +summary > div, .cst { + padding-top: 4px; + padding-bottom: 4px; +} + +.elt, .cst { + display: flex; + flex-grow: 1; + padding-left: 8px; + padding-right: 8px; +} + +.elt-switch { + width: 18px; + padding-left: 2px; + text-align: center; +} + +details:has(li) .elt-switch::after { + content: '▸'; +} + +details[open]:has(li) .elt-switch::after { + content: '▾'; +} + +.elt-label { + flex-grow: 1; +} + +.cst-label { + flex-grow: 1; +} + +.elt-rep { + display: flex; +} + +.elt-rep > div, .cst-rep { + padding: 2px 0px 0px 0px; + font-size: 10pt; + text-align: center; + width: 56px; +} + +.cst { + font-style: italic; +} + +.cst > input { + margin: 0px 8px 0px 0px; +} + +/* display */ + +canvas { + float: left; + margin-left: 20px; + margin-top: 20px; + background-color: #020202; + border: 1px solid #555; + border-radius: 16px; +} + +canvas:focus { + border-color: #aaa; +} \ No newline at end of file diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs new file mode 100644 index 0000000..ab5db70 --- /dev/null +++ b/app-proto/src/add_remove.rs @@ -0,0 +1,242 @@ +use std::collections::BTreeSet; /* DEBUG */ +use sycamore::prelude::*; +use web_sys::{console, wasm_bindgen::JsValue}; + +use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}}; + +/* DEBUG */ +fn load_gen_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Element { + id: String::from("gemini_a"), + label: String::from("Castor"), + color: [1.00_f32, 0.25_f32, 0.00_f32], + rep: engine::sphere(0.5, 0.5, 0.0, 1.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("gemini_b"), + label: String::from("Pollux"), + color: [0.00_f32, 0.25_f32, 1.00_f32], + rep: engine::sphere(-0.5, -0.5, 0.0, 1.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("ursa_major"), + label: String::from("Ursa major"), + color: [0.25_f32, 0.00_f32, 1.00_f32], + rep: engine::sphere(-0.5, 0.5, 0.0, 0.75), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("ursa_minor"), + label: String::from("Ursa minor"), + color: [0.25_f32, 1.00_f32, 0.00_f32], + rep: engine::sphere(0.5, -0.5, 0.0, 0.5), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("moon_deimos"), + label: String::from("Deimos"), + color: [0.75_f32, 0.75_f32, 0.00_f32], + rep: engine::sphere(0.0, 0.15, 1.0, 0.25), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("moon_phobos"), + label: String::from("Phobos"), + color: [0.00_f32, 0.75_f32, 0.50_f32], + rep: engine::sphere(0.0, -0.15, -1.0, 0.25), + constraints: BTreeSet::default() + } + ); + assembly.insert_constraint( + Constraint { + args: ( + assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_a"]), + assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_b"]) + ), + rep: 0.5, + active: create_signal(true) + } + ); +} + +/* DEBUG */ +fn load_low_curv_assemb(assembly: &Assembly) { + let a = 0.75_f64.sqrt(); + let _ = assembly.try_insert_element( + Element { + id: "central".to_string(), + label: "Central".to_string(), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: engine::sphere(0.0, 0.0, 0.0, 1.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "assemb_plane".to_string(), + label: "Assembly plane".to_string(), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "side1".to_string(), + label: "Side 1".to_string(), + color: [1.00_f32, 0.00_f32, 0.25_f32], + rep: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "side2".to_string(), + label: "Side 2".to_string(), + color: [0.25_f32, 1.00_f32, 0.00_f32], + rep: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "side3".to_string(), + label: "Side 3".to_string(), + color: [0.00_f32, 0.25_f32, 1.00_f32], + rep: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "corner1".to_string(), + label: "Corner 1".to_string(), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: "corner2".to_string(), + label: "Corner 2".to_string(), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0), + constraints: BTreeSet::default() + } + ); + let _ = assembly.try_insert_element( + Element { + id: String::from("corner3"), + label: String::from("Corner 3"), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0), + constraints: BTreeSet::default() + } + ); +} + +#[component] +pub fn AddRemove() -> View { + /* DEBUG */ + let assembly_name = create_signal("general".to_string()); + create_effect(move || { + // get name of chosen assembly + let name = assembly_name.get_clone(); + console::log_1( + &JsValue::from(format!("Showing assembly \"{}\"", name.clone())) + ); + + batch(|| { + let state = use_context::(); + let assembly = &state.assembly; + + // clear state + assembly.elements.update(|elts| elts.clear()); + assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); + state.selection.update(|sel| sel.clear()); + + // load assembly + match name.as_str() { + "general" => load_gen_assemb(assembly), + "low-curv" => load_low_curv_assemb(assembly), + _ => () + }; + }); + }); + + view! { + div(id="add-remove") { + button( + on:click=|_| { + let state = use_context::(); + state.assembly.insert_new_element(); + + /* DEBUG */ + // print updated list of elements by identifier + console::log_1(&JsValue::from("elements by identifier:")); + for (id, key) in state.assembly.elements_by_id.get_clone().iter() { + console::log_3( + &JsValue::from(" "), + &JsValue::from(id), + &JsValue::from(*key) + ); + } + } + ) { "+" } + button( + disabled={ + let state = use_context::(); + state.selection.with(|sel| sel.len() != 2) + }, + on:click=|_| { + let state = use_context::(); + let args = state.selection.with( + |sel| { + let arg_vec: Vec<_> = sel.into_iter().collect(); + (arg_vec[0].clone(), arg_vec[1].clone()) + } + ); + state.assembly.insert_constraint(Constraint { + args: args, + rep: 0.0, + active: create_signal(true) + }); + state.selection.update(|sel| sel.clear()); + + /* DEBUG */ + // print updated constraint list + console::log_1(&JsValue::from("constraints:")); + state.assembly.constraints.with(|csts| { + for (_, cst) in csts.into_iter() { + console::log_5( + &JsValue::from(" "), + &JsValue::from(cst.args.0), + &JsValue::from(cst.args.1), + &JsValue::from(":"), + &JsValue::from(cst.rep) + ); + } + }); + } + ) { "🔗" } + select(bind:value=assembly_name) { /* DEBUG */ + option(value="general") { "General" } + option(value="low-curv") { "Low-curvature" } + } + } + } +} \ No newline at end of file diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs new file mode 100644 index 0000000..e8dab79 --- /dev/null +++ b/app-proto/src/assembly.rs @@ -0,0 +1,93 @@ +use nalgebra::DVector; +use rustc_hash::FxHashMap; +use slab::Slab; +use std::collections::BTreeSet; +use sycamore::prelude::*; + +#[derive(Clone, PartialEq)] +pub struct Element { + pub id: String, + pub label: String, + pub color: [f32; 3], + pub rep: DVector, + pub constraints: BTreeSet +} + +#[derive(Clone)] +pub struct Constraint { + pub args: (usize, usize), + pub rep: f64, + pub active: Signal +} + +// a complete, view-independent description of an assembly +#[derive(Clone)] +pub struct Assembly { + // elements and constraints + pub elements: Signal>, + pub constraints: Signal>, + + // indexing + pub elements_by_id: Signal> +} + +impl Assembly { + pub fn new() -> Assembly { + Assembly { + elements: create_signal(Slab::new()), + constraints: create_signal(Slab::new()), + elements_by_id: create_signal(FxHashMap::default()) + } + } + + // insert an element into the assembly without checking whether we already + // have an element with the same identifier. any element that does have the + // same identifier will get kicked out of the `elements_by_id` index + fn insert_element_unchecked(&self, elt: Element) { + let id = elt.id.clone(); + let key = self.elements.update(|elts| elts.insert(elt)); + self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); + } + + pub fn try_insert_element(&self, elt: Element) -> bool { + let can_insert = self.elements_by_id.with_untracked( + |elts_by_id| !elts_by_id.contains_key(&elt.id) + ); + if can_insert { + self.insert_element_unchecked(elt); + } + can_insert + } + + pub fn insert_new_element(&self) { + // find the next unused identifier in the default sequence + let mut id_num = 1; + let mut id = format!("sphere{}", 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); + } + + // create and insert a new element + self.insert_element_unchecked( + Element { + id: id, + label: format!("Sphere {}", id_num), + color: [0.75_f32, 0.75_f32, 0.75_f32], + rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]), + constraints: BTreeSet::default() + } + ); + } + + pub fn insert_constraint(&self, constraint: Constraint) { + let args = constraint.args; + let key = self.constraints.update(|csts| csts.insert(constraint)); + self.elements.update(|elts| { + elts[args.0].constraints.insert(key); + elts[args.1].constraints.insert(key); + }) + } +} \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs new file mode 100644 index 0000000..c32b470 --- /dev/null +++ b/app-proto/src/display.rs @@ -0,0 +1,443 @@ +use core::array; +use nalgebra::{DMatrix, Rotation3, Vector3}; +use sycamore::{prelude::*, motion::create_raf}; +use web_sys::{ + console, + window, + KeyboardEvent, + WebGl2RenderingContext, + WebGlProgram, + WebGlShader, + WebGlUniformLocation, + wasm_bindgen::{JsCast, JsValue} +}; + +use crate::AppState; + +fn compile_shader( + context: &WebGl2RenderingContext, + shader_type: u32, + source: &str, +) -> WebGlShader { + let shader = context.create_shader(shader_type).unwrap(); + context.shader_source(&shader, source); + context.compile_shader(&shader); + shader +} + +fn get_uniform_array_locations( + context: &WebGl2RenderingContext, + program: &WebGlProgram, + var_name: &str, + member_name_opt: Option<&str> +) -> [Option; N] { + array::from_fn(|n| { + let name = match member_name_opt { + Some(member_name) => format!("{var_name}[{n}].{member_name}"), + None => format!("{var_name}[{n}]") + }; + context.get_uniform_location(&program, name.as_str()) + }) +} + +// load the given data into the vertex input of the given name +fn bind_vertex_attrib( + context: &WebGl2RenderingContext, + index: u32, + size: i32, + data: &[f32] +) { + // create a data buffer and bind it to ARRAY_BUFFER + let buffer = context.create_buffer().unwrap(); + context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer)); + + // 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 + unsafe { + context.buffer_data_with_array_buffer_view( + WebGl2RenderingContext::ARRAY_BUFFER, + &js_sys::Float32Array::view(&data), + WebGl2RenderingContext::STATIC_DRAW, + ); + } + + // 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 + ); +} + +#[component] +pub fn Display() -> View { + let state = use_context::(); + + // canvas + let display = create_node_ref(); + + // navigation + let pitch_up = create_signal(0.0); + let pitch_down = create_signal(0.0); + let yaw_right = create_signal(0.0); + let yaw_left = create_signal(0.0); + let roll_ccw = create_signal(0.0); + let roll_cw = create_signal(0.0); + let zoom_in = create_signal(0.0); + let zoom_out = create_signal(0.0); + let turntable = create_signal(false); /* BENCHMARKING */ + + // change listener + let scene_changed = create_signal(true); + create_effect(move || { + state.assembly.elements.track(); + state.selection.track(); + scene_changed.set(true); + }); + + /* INSTRUMENTS */ + const SAMPLE_PERIOD: i32 = 60; + let mut last_sample_time = 0.0; + let mut frames_since_last_sample = 0; + let mean_frame_interval = create_signal(0.0); + + on_mount(move || { + // timing + let mut last_time = 0.0; + + // viewpoint + const ROT_SPEED: f64 = 0.4; // in radians per second + const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second + const TURNTABLE_SPEED: f64 = 0.1; /* BENCHMARKING */ + let mut orientation = DMatrix::::identity(5, 5); + let mut rotation = DMatrix::::identity(5, 5); + let mut location_z: f64 = 5.0; + + // 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 */ + + /* INSTRUMENTS */ + let performance = window().unwrap().performance().unwrap(); + + // get the display canvas + let canvas = display.get().unchecked_into::(); + let ctx = canvas + .get_context("webgl2") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap(); + + // compile and attach the vertex and fragment shaders + let vertex_shader = compile_shader( + &ctx, + WebGl2RenderingContext::VERTEX_SHADER, + include_str!("identity.vert"), + ); + 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 */ + // print the maximum number of vectors that can be passed as + // uniforms to a fragment shader. the OpenGL ES 3.0 standard + // requires this maximum to be at least 224, as discussed in the + // documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter + // here: + // + // https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml + // + // there are also other size limits. for example, on Aaron's + // machine, the the length of a float or genType array seems to be + // capped at 1024 elements + console::log_2( + &ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(), + &JsValue::from("uniform vectors available") + ); + + // find indices of 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 sphere_sp_locs = get_uniform_array_locations::( + &ctx, &program, "sphere_list", Some("sp") + ); + let sphere_lt_locs = get_uniform_array_locations::( + &ctx, &program, "sphere_list", Some("lt") + ); + let color_locs = get_uniform_array_locations::( + &ctx, &program, "color_list", None + ); + let highlight_locs = get_uniform_array_locations::( + &ctx, &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"); + + // 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] = [ + // northwest triangle + -1.0, -1.0, 0.0, + -1.0, 1.0, 0.0, + 1.0, 1.0, 0.0, + // southeast triangle + -1.0, -1.0, 0.0, + 1.0, 1.0, 0.0, + 1.0, -1.0, 0.0 + ]; + bind_vertex_attrib(&ctx, position_index, 3, &positions); + + // set up a repainting routine + let (_, start_animation_loop, _) = create_raf(move || { + // get the time step + let time = performance.now(); + let time_step = 0.001*(time - last_time); + last_time = time; + + // get the navigation state + let pitch_up_val = pitch_up.get(); + let pitch_down_val = pitch_down.get(); + let yaw_right_val = yaw_right.get(); + let yaw_left_val = yaw_left.get(); + let roll_ccw_val = roll_ccw.get(); + let roll_cw_val = roll_cw.get(); + let zoom_in_val = zoom_in.get(); + let zoom_out_val = zoom_out.get(); + let turntable_val = turntable.get(); /* BENCHMARKING */ + + // update the assembly's orientation + let ang_vel = { + let pitch = pitch_up_val - pitch_down_val; + let yaw = yaw_right_val - yaw_left_val; + let roll = roll_ccw_val - roll_cw_val; + if pitch != 0.0 || yaw != 0.0 || roll != 0.0 { + ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize() + } else { + Vector3::zeros() + } + } /* BENCHMARKING */ + if turntable_val { + Vector3::new(0.0, TURNTABLE_SPEED, 0.0) + } else { + Vector3::zeros() + }; + let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0); + rotation_sp.copy_from( + Rotation3::from_scaled_axis(time_step * ang_vel).matrix() + ); + orientation = &rotation * &orientation; + + // update the assembly's location + let zoom = zoom_out_val - zoom_in_val; + location_z *= (time_step * ZOOM_SPEED * zoom).exp(); + + if scene_changed.get() { + /* INSTRUMENTS */ + // measure mean frame interval + frames_since_last_sample += 1; + if frames_since_last_sample >= SAMPLE_PERIOD { + mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64)); + last_sample_time = time; + frames_since_last_sample = 0; + } + + // find the map from assembly space to world space + let location = { + let u = -location_z; + DMatrix::from_column_slice(5, 5, &[ + 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, u, + 0.0, 0.0, 2.0*u, 1.0, u*u, + 0.0, 0.0, 0.0, 0.0, 1.0 + ]) + }; + let assembly_to_world = &location * &orientation; + + // get the assembly + let elements = state.assembly.elements.get_clone(); + let element_iter = (&elements).into_iter(); + let reps_world: Vec<_> = element_iter.clone().map(|(_, elt)| &assembly_to_world * &elt.rep).collect(); + let colors: Vec<_> = element_iter.clone().map(|(key, elt)| + if state.selection.with(|sel| sel.contains(&key)) { + elt.color.map(|ch| 0.2 + 0.8*ch) + } else { + elt.color + } + ).collect(); + let highlights: Vec<_> = element_iter.map(|(key, _)| + if state.selection.with(|sel| sel.contains(&key)) { + 1.0_f32 + } else { + HIGHLIGHT + } + ).collect(); + + // 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(), elements.len() as i32); + for n in 0..reps_world.len() { + let v = &reps_world[n]; + ctx.uniform3f( + sphere_sp_locs[n].as_ref(), + v[0] as f32, v[1] as f32, v[2] as f32 + ); + ctx.uniform2f( + sphere_lt_locs[n].as_ref(), + v[3] as f32, v[4] as f32 + ); + ctx.uniform3fv_with_f32_array( + color_locs[n].as_ref(), + &colors[n] + ); + ctx.uniform1f( + highlight_locs[n].as_ref(), + highlights[n] + ); + } + + // pass the display parameters + ctx.uniform1f(opacity_loc.as_ref(), OPACITY); + ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); + ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); + + // draw the scene + ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); + + // clear the scene change flag + scene_changed.set( + pitch_up_val != 0.0 + || pitch_down_val != 0.0 + || yaw_left_val != 0.0 + || yaw_right_val != 0.0 + || roll_cw_val != 0.0 + || roll_ccw_val != 0.0 + || zoom_in_val != 0.0 + || zoom_out_val != 0.0 + || turntable_val /* BENCHMARKING */ + ); + } else { + frames_since_last_sample = 0; + mean_frame_interval.set(-1.0); + } + }); + start_animation_loop(); + }); + + let set_nav_signal = move |event: KeyboardEvent, value: f64| { + let mut navigating = true; + let shift = event.shift_key(); + match event.key().as_str() { + "ArrowUp" if shift => zoom_in.set(value), + "ArrowDown" if shift => zoom_out.set(value), + "ArrowUp" => pitch_up.set(value), + "ArrowDown" => pitch_down.set(value), + "ArrowRight" if shift => roll_cw.set(value), + "ArrowLeft" if shift => roll_ccw.set(value), + "ArrowRight" => yaw_right.set(value), + "ArrowLeft" => yaw_left.set(value), + _ => navigating = false + }; + if navigating { + scene_changed.set(true); + event.prevent_default(); + } + }; + + view! { + /* TO DO */ + // switch back to integer-valued parameters when that becomes possible + // again + canvas( + ref=display, + width="600", + height="600", + tabindex="0", + on:keydown=move |event: KeyboardEvent| { + if event.key() == "Shift" { + roll_cw.set(yaw_right.get()); + roll_ccw.set(yaw_left.get()); + zoom_in.set(pitch_up.get()); + zoom_out.set(pitch_down.get()); + yaw_right.set(0.0); + yaw_left.set(0.0); + pitch_up.set(0.0); + pitch_down.set(0.0); + } else { + if event.key() == "Enter" { /* BENCHMARKING */ + turntable.set_fn(|turn| !turn); + scene_changed.set(true); + } + set_nav_signal(event, 1.0); + } + }, + on:keyup=move |event: KeyboardEvent| { + if event.key() == "Shift" { + yaw_right.set(roll_cw.get()); + yaw_left.set(roll_ccw.get()); + pitch_up.set(zoom_in.get()); + pitch_down.set(zoom_out.get()); + roll_cw.set(0.0); + roll_ccw.set(0.0); + zoom_in.set(0.0); + zoom_out.set(0.0); + } else { + set_nav_signal(event, 0.0); + } + }, + on:blur=move |_| { + pitch_up.set(0.0); + pitch_down.set(0.0); + yaw_right.set(0.0); + yaw_left.set(0.0); + roll_ccw.set(0.0); + roll_cw.set(0.0); + } + ) + } +} \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs new file mode 100644 index 0000000..79668bb --- /dev/null +++ b/app-proto/src/engine.rs @@ -0,0 +1,27 @@ +use nalgebra::DVector; + +// the sphere with the given center and radius, with inward-pointing normals +pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVector { + let center_norm_sq = center_x * center_x + center_y * center_y + center_z * center_z; + DVector::from_column_slice(&[ + center_x / radius, + center_y / radius, + center_z / radius, + 0.5 / radius, + 0.5 * (center_norm_sq / radius - radius) + ]) +} + +// the sphere of curvature `curv` whose closest point to the origin has position +// `off * dir` and normal `dir`, where `dir` is a unit vector. setting the +// curvature to zero gives a plane +pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f64) -> DVector { + let norm_sp = 1.0 + off * curv; + DVector::from_column_slice(&[ + norm_sp * dir_x, + norm_sp * dir_y, + norm_sp * dir_z, + 0.5 * curv, + off * (1.0 + 0.5 * off * curv) + ]) +} \ No newline at end of file diff --git a/app-proto/src/identity.vert b/app-proto/src/identity.vert new file mode 100644 index 0000000..183a65f --- /dev/null +++ b/app-proto/src/identity.vert @@ -0,0 +1,7 @@ +#version 300 es + +in vec4 position; + +void main() { + gl_Position = position; +} \ No newline at end of file diff --git a/app-proto/src/inversive.frag b/app-proto/src/inversive.frag new file mode 100644 index 0000000..d50cb1e --- /dev/null +++ b/app-proto/src/inversive.frag @@ -0,0 +1,234 @@ +#version 300 es + +precision highp float; + +out vec4 outColor; + +// --- inversive geometry --- + +struct vecInv { + vec3 sp; + vec2 lt; +}; + +// --- uniforms --- + +// assembly +const int SPHERE_MAX = 200; +uniform int sphere_cnt; +uniform vecInv sphere_list[SPHERE_MAX]; +uniform vec3 color_list[SPHERE_MAX]; +uniform float highlight_list[SPHERE_MAX]; + +// view +uniform vec2 resolution; +uniform float shortdim; + +// controls +uniform float opacity; +uniform int layer_threshold; +uniform bool debug_mode; + +// light and camera +const float focal_slope = 0.3; +const vec3 light_dir = normalize(vec3(2., 2., 1.)); +const float ixn_threshold = 0.005; +const float INTERIOR_DIMMING = 0.7; + +// --- sRGB --- + +// map colors from RGB space to sRGB space, as specified in the sRGB standard +// (IEC 61966-2-1:1999) +// +// https://www.color.org/sRGB.pdf +// https://www.color.org/chardata/rgb/srgb.xalter +// +// in RGB space, color value is proportional to light intensity, so linear +// color-vector interpolation corresponds to physical light mixing. in sRGB +// space, the color encoding used by many monitors, we use more of the value +// interval to represent low intensities, and less of the interval to represent +// high intensities. this improves color quantization + +float sRGB(float t) { + if (t <= 0.0031308) { + return 12.92*t; + } else { + return 1.055*pow(t, 5./12.) - 0.055; + } +} + +vec3 sRGB(vec3 color) { + return vec3(sRGB(color.r), sRGB(color.g), sRGB(color.b)); +} + +// --- shading --- + +struct Fragment { + vec3 pt; + vec3 normal; + vec4 color; +}; + +Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) { + // the expression for normal needs to be checked. it's supposed to give the + // negative gradient of the lorentz product between the impact point vector + // and the sphere vector with respect to the coordinates of the impact + // point. i calculated it in my head and decided that the result looked good + // enough for now + vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt); + + float incidence = dot(normal, light_dir); + float illum = mix(0.4, 1.0, max(incidence, 0.0)); + return Fragment(pt, normal, vec4(illum * base_color, opacity)); +} + +float intersection_dist(Fragment a, Fragment b) { + float intersection_sin = length(cross(a.normal, b.normal)); + vec3 disp = a.pt - b.pt; + return max( + abs(dot(a.normal, disp)), + abs(dot(b.normal, disp)) + ) / intersection_sin; +} + +// --- ray-casting --- + +struct TaggedDepth { + float depth; + float dimming; + int id; +}; + +// if `a/b` is less than this threshold, we approximate `a*u^2 + b*u + c` by +// the linear function `b*u + c` +const float DEG_THRESHOLD = 1e-9; + +// the depths, represented as multiples of `dir`, where the line generated by +// `dir` hits the sphere represented by `v`. if both depths are positive, the +// smaller one is returned in the first component. if only one depth is +// positive, it could be returned in either component +vec2 sphere_cast(vecInv v, vec3 dir) { + float a = -v.lt.s * dot(dir, dir); + float b = dot(v.sp, dir); + float c = -v.lt.t; + + float adjust = 4.*a*c/(b*b); + if (adjust < 1.) { + // 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` + float square_rect_ratio = 1. + sqrt(1. - adjust); + float lin_root = -(2.*c)/b / square_rect_ratio; + if (abs(a) > DEG_THRESHOLD * abs(b)) { + return vec2(lin_root, -b/(2.*a) * square_rect_ratio); + } else { + return vec2(lin_root, -1.); + } + } else { + // the line through `dir` misses the sphere completely + return vec2(-1., -1.); + } +} + +void main() { + vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim; + vec3 dir = vec3(focal_slope * scr, -1.); + + // cast rays through the spheres + const int LAYER_MAX = 12; + TaggedDepth top_hits [LAYER_MAX]; + int layer_cnt = 0; + for (int id = 0; id < sphere_cnt; ++id) { + // find out where the ray hits the sphere + vec2 hit_depths = sphere_cast(sphere_list[id], dir); + + // insertion-sort the points we hit into the hit list + float dimming = 1.; + for (int side = 0; side < 2; ++side) { + float depth = hit_depths[side]; + if (depth > 0.) { + for (int layer = layer_cnt; layer >= 0; --layer) { + if (layer < 1 || top_hits[layer-1].depth <= depth) { + // we're not as close to the screen as the hit before + // the empty slot, so insert here + if (layer < LAYER_MAX) { + top_hits[layer] = TaggedDepth(depth, dimming, id); + } + break; + } else { + // we're closer to the screen than the hit before the + // empty slot, so move that hit into the empty slot + top_hits[layer] = top_hits[layer-1]; + } + } + layer_cnt = min(layer_cnt + 1, LAYER_MAX); + dimming = INTERIOR_DIMMING; + } + } + } + + /* DEBUG */ + // in debug mode, show the layer count instead of the shaded image + if (debug_mode) { + // at the bottom of the screen, show the color scale instead of the + // layer count + if (gl_FragCoord.y < 10.) layer_cnt = int(16. * gl_FragCoord.x / resolution.x); + + // convert number to color + ivec3 bits = layer_cnt / ivec3(1, 2, 4); + vec3 color = mod(vec3(bits), 2.); + if (layer_cnt % 16 >= 8) { + color = mix(color, vec3(0.5), 0.5); + } + outColor = vec4(color, 1.); + return; + } + + // composite the sphere fragments + vec3 color = vec3(0.); + int layer = layer_cnt - 1; + TaggedDepth hit = top_hits[layer]; + Fragment frag_next = sphere_shading( + sphere_list[hit.id], + hit.depth * dir, + hit.dimming * color_list[hit.id] + ); + float highlight_next = highlight_list[hit.id]; + --layer; + for (; layer >= layer_threshold; --layer) { + // load the current fragment + Fragment frag = frag_next; + float highlight = highlight_next; + + // shade the next fragment + hit = top_hits[layer]; + frag_next = sphere_shading( + sphere_list[hit.id], + hit.depth * dir, + hit.dimming * color_list[hit.id] + ); + highlight_next = highlight_list[hit.id]; + + // highlight intersections + float ixn_dist = intersection_dist(frag, frag_next); + float max_highlight = max(highlight, highlight_next); + float ixn_highlight = 0.5 * max_highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist)); + frag.color = mix(frag.color, vec4(1.), ixn_highlight); + frag_next.color = mix(frag_next.color, vec4(1.), ixn_highlight); + + // highlight cusps + float cusp_cos = abs(dot(dir, frag.normal)); + float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[hit.id].lt.s); + float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos)); + frag.color = mix(frag.color, vec4(1.), cusp_highlight); + + // composite the current fragment + color = mix(color, frag.color.rgb, frag.color.a); + } + color = mix(color, frag_next.color.rgb, frag_next.color.a); + outColor = vec4(sRGB(color), 1.); +} \ No newline at end of file diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs new file mode 100644 index 0000000..2c71a83 --- /dev/null +++ b/app-proto/src/main.rs @@ -0,0 +1,42 @@ +mod add_remove; +mod assembly; +mod display; +mod engine; +mod outline; + +use rustc_hash::FxHashSet; +use sycamore::prelude::*; + +use add_remove::AddRemove; +use assembly::Assembly; +use display::Display; +use outline::Outline; + +#[derive(Clone)] +struct AppState { + assembly: Assembly, + selection: Signal> +} + +impl AppState { + fn new() -> AppState { + AppState { + assembly: Assembly::new(), + selection: create_signal(FxHashSet::default()) + } + } +} + +fn main() { + sycamore::render(|| { + provide_context(AppState::new()); + + view! { + div(id="sidebar") { + AddRemove {} + Outline {} + } + Display {} + } + }); +} \ No newline at end of file diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs new file mode 100644 index 0000000..4e4de9c --- /dev/null +++ b/app-proto/src/outline.rs @@ -0,0 +1,161 @@ +use itertools::Itertools; +use sycamore::{prelude::*, web::tags::div}; +use web_sys::{Element, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; + +use crate::AppState; + +// this component lists the elements of the assembly, showing the constraints +// on each element as a collapsible sub-list. its implementation is based on +// Kate Morley's HTML + CSS tree views: +// +// https://iamkate.com/code/tree-views/ +// +#[component] +pub fn Outline() -> View { + // sort the elements alphabetically by ID + let elements_sorted = create_memo(|| { + let state = use_context::(); + state.assembly.elements + .get_clone() + .into_iter() + .sorted_by_key(|(_, elt)| elt.id.clone()) + .collect() + }); + + view! { + ul( + id="outline", + on:click={ + let state = use_context::(); + move |_| state.selection.update(|sel| sel.clear()) + } + ) { + Keyed( + list=elements_sorted, + view=|(key, elt)| { + let state = use_context::(); + let class = create_memo({ + move || { + if state.selection.with(|sel| sel.contains(&key)) { + "selected" + } else { + "" + } + } + }); + let label = elt.label.clone(); + let rep_components = elt.rep.iter().map(|u| { + let u_coord = u.to_string().replace("-", "\u{2212}"); + View::from(div().children(u_coord)) + }).collect::>(); + let constrained = elt.constraints.len() > 0; + let details_node = create_node_ref(); + view! { + /* [TO DO] switch to integer-valued parameters whenever + that becomes possible again */ + li { + details(ref=details_node) { + summary( + class=class.get(), + on:keydown={ + move |event: KeyboardEvent| { + match event.key().as_str() { + "Enter" => { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + event.prevent_default(); + }, + "ArrowRight" if constrained => { + let _ = details_node + .get() + .unchecked_into::() + .set_attribute("open", ""); + }, + "ArrowLeft" => { + let _ = details_node + .get() + .unchecked_into::() + .remove_attribute("open"); + }, + _ => () + } + } + } + ) { + div( + class="elt-switch", + on:click=|event: MouseEvent| event.stop_propagation() + ) + div( + class="elt", + on:click={ + move |event: MouseEvent| { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + event.stop_propagation(); + event.prevent_default(); + } + } + ) { + div(class="elt-label") { (label) } + div(class="elt-rep") { (rep_components) } + } + } + ul(class="constraints") { + Keyed( + list=elt.constraints.into_iter().collect::>(), + view=move |c_key: usize| { + let c_state = use_context::(); + let assembly = &c_state.assembly; + let cst = assembly.constraints.with(|csts| csts[c_key].clone()); + let other_arg = if cst.args.0 == key { + cst.args.1 + } else { + cst.args.0 + }; + let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone()); + view! { + li(class="cst") { + input(r#type="checkbox", bind:checked=cst.active) + div(class="cst-label") { (other_arg_label) } + div(class="cst-rep") { (cst.rep) } + } + } + }, + key=|c_key| c_key.clone() + ) + } + } + } + } + }, + key=|(key, elt)| ( + key.clone(), + elt.id.clone(), + elt.label.clone(), + elt.constraints.clone() + ) + ) + } + } +} \ No newline at end of file From 707618cdd3adeb488c2b040a4e48f8789e20c7e9 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Tue, 12 Nov 2024 00:46:16 +0000 Subject: [PATCH 102/126] Integrate engine into application prototype (#15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the engine prototype to Rust, integrate it into the application prototype, and use it to enforce the constraints. ### Features To see the engine in action: 1. Add a constraint by shift-clicking to select two spheres in the outline view and then hitting the 🔗 button 2. Click a summary arrow to see the outline item for the new constraint 2. Set the constraint's Lorentz product by entering a value in the text field at the right end of the outline item * *The display should update as soon as you press* Enter *or focus away from the text field* The checkbox at the left end of a constraint outline item controls whether the constraint is active. Activating a constraint triggers a solution update. (Deactivating a constraint doesn't, since the remaining active constraints are still satisfied.) ### Precision The Julia prototype of the engine uses a generic scalar type, so you can pass in any type the linear algebra functions are implemented for. The examples use the [adjustable-precision](https://docs.julialang.org/en/v1/base/numbers/#Base.MPFR.setprecision) `BigFloat` type. In the Rust port of the engine, the scalar type is currently fixed at `f64`. Switching to generic scalars shouldn't be too hard, but I haven't looked into [which other types](https://www.nalgebra.org/docs/user_guide/generic_programming) the linear algebra functions are implemented for. ### Testing To confirm quantitatively that the Rust port of the engine is working, you can go to the `app-proto` folder and: * Run some automated tests by calling `cargo test`. * Inspect the optimization process in a few examples calling the `run-examples` script. The first example that prints is the same as the Irisawa hexlet example from the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then ``` include("irisawa-hexlet.jl") for (step, scaled_loss) in enumerate(history_alt.scaled_loss) println(rpad(step-1, 4), " | ", scaled_loss) end ``` you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show. ### A small engine revision The Rust port of the engine improves on the Julia prototype in one part of the constraint-solving routine: projecting the Hessian onto the subspace where the frozen entries stay constant. The Julia prototype does this by removing the rows and columns of the Hessian that correspond to the frozen entries, finding the Newton step from the resulting "compressed" Hessian, and then adding zero entries to the Newton step in the appropriate places. The Rust port instead replaces each frozen row and column with its corresponding standard unit vector, avoiding the finicky compressing and decompressing steps. To confirm that this version of the constraint-solving routine works the same as the original, I implemented it in Julia as `realize_gram_alt_proj`. The solutions we get from this routine match the ones we get from the original `realize_gram` to very high precision, and in the simplest examples (`sphere-in-tetrahedron.jl` and `tetrahedron-radius-ratio.jl`), the descent paths also match to very high precision. In a more complicated example (`irisawa-hexlet.jl`), the descent paths diverge about a quarter of the way into the search, even though they end up in the same place. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/15 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.toml | 2 + app-proto/main.css | 11 +- app-proto/run-examples | 8 + app-proto/src/add_remove.rs | 118 ++-- app-proto/src/assembly.rs | 128 ++++- app-proto/src/display.rs | 4 +- app-proto/src/engine.rs | 515 +++++++++++++++++- app-proto/src/main.rs | 4 +- app-proto/src/outline.rs | 52 +- engine-proto/gram-test/Engine.jl | 143 ++++- engine-proto/gram-test/irisawa-hexlet.jl | 11 +- .../gram-test/sphere-in-tetrahedron.jl | 11 +- .../gram-test/tetrahedron-radius-ratio.jl | 11 +- notes/inversive.md | 22 + 14 files changed, 947 insertions(+), 93 deletions(-) create mode 100755 app-proto/run-examples diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 920469a..e5bc05e 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -10,6 +10,7 @@ default = ["console_error_panic_hook"] [dependencies] itertools = "0.13.0" js-sys = "0.3.70" +lazy_static = "1.5.0" nalgebra = "0.33.0" rustc-hash = "2.0.0" slab = "0.4.9" @@ -25,6 +26,7 @@ console_error_panic_hook = { version = "0.1.7", optional = true } version = "0.3.69" features = [ 'HtmlCanvasElement', + 'HtmlInputElement', 'Performance', 'WebGl2RenderingContext', 'WebGlBuffer', diff --git a/app-proto/main.css b/app-proto/main.css index bdbacfb..32ae5bf 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -93,7 +93,7 @@ details[open]:has(li) .elt-switch::after { display: flex; } -.elt-rep > div, .cst-rep { +.elt-rep > div { padding: 2px 0px 0px 0px; font-size: 10pt; text-align: center; @@ -104,10 +104,17 @@ details[open]:has(li) .elt-switch::after { font-style: italic; } -.cst > input { +.cst > input[type=checkbox] { margin: 0px 8px 0px 0px; } +.cst > input[type=text] { + color: #fcfcfc; + background-color: inherit; + border: 1px solid #555; + border-radius: 2px; +} + /* display */ canvas { diff --git a/app-proto/run-examples b/app-proto/run-examples new file mode 100755 index 0000000..6a5e3ae --- /dev/null +++ b/app-proto/run-examples @@ -0,0 +1,8 @@ +# based on "Enabling print statements in Cargo tests", by Jon Almeida +# +# https://jonalmeida.com/posts/2015/01/23/print-cargo/ +# + +cargo test -- --nocapture engine::tests::irisawa_hexlet_test +cargo test -- --nocapture engine::tests::three_spheres_example +cargo test -- --nocapture engine::tests::point_on_sphere_example diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index ab5db70..19b4b8d 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -11,8 +11,9 @@ fn load_gen_assemb(assembly: &Assembly) { id: String::from("gemini_a"), label: String::from("Castor"), color: [1.00_f32, 0.25_f32, 0.00_f32], - rep: engine::sphere(0.5, 0.5, 0.0, 1.0), - constraints: BTreeSet::default() + representation: engine::sphere(0.5, 0.5, 0.0, 1.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -20,8 +21,9 @@ fn load_gen_assemb(assembly: &Assembly) { id: String::from("gemini_b"), label: String::from("Pollux"), color: [0.00_f32, 0.25_f32, 1.00_f32], - rep: engine::sphere(-0.5, -0.5, 0.0, 1.0), - constraints: BTreeSet::default() + representation: engine::sphere(-0.5, -0.5, 0.0, 1.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -29,8 +31,9 @@ fn load_gen_assemb(assembly: &Assembly) { id: String::from("ursa_major"), label: String::from("Ursa major"), color: [0.25_f32, 0.00_f32, 1.00_f32], - rep: engine::sphere(-0.5, 0.5, 0.0, 0.75), - constraints: BTreeSet::default() + representation: engine::sphere(-0.5, 0.5, 0.0, 0.75), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -38,8 +41,9 @@ fn load_gen_assemb(assembly: &Assembly) { id: String::from("ursa_minor"), label: String::from("Ursa minor"), color: [0.25_f32, 1.00_f32, 0.00_f32], - rep: engine::sphere(0.5, -0.5, 0.0, 0.5), - constraints: BTreeSet::default() + representation: engine::sphere(0.5, -0.5, 0.0, 0.5), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -47,8 +51,9 @@ fn load_gen_assemb(assembly: &Assembly) { id: String::from("moon_deimos"), label: String::from("Deimos"), color: [0.75_f32, 0.75_f32, 0.00_f32], - rep: engine::sphere(0.0, 0.15, 1.0, 0.25), - constraints: BTreeSet::default() + representation: engine::sphere(0.0, 0.15, 1.0, 0.25), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -56,18 +61,9 @@ fn load_gen_assemb(assembly: &Assembly) { id: String::from("moon_phobos"), label: String::from("Phobos"), color: [0.00_f32, 0.75_f32, 0.50_f32], - rep: engine::sphere(0.0, -0.15, -1.0, 0.25), - constraints: BTreeSet::default() - } - ); - assembly.insert_constraint( - Constraint { - args: ( - assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_a"]), - assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_b"]) - ), - rep: 0.5, - active: create_signal(true) + representation: engine::sphere(0.0, -0.15, -1.0, 0.25), + constraints: BTreeSet::default(), + index: 0 } ); } @@ -80,8 +76,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: "central".to_string(), label: "Central".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: engine::sphere(0.0, 0.0, 0.0, 1.0), - constraints: BTreeSet::default() + representation: engine::sphere(0.0, 0.0, 0.0, 1.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -89,8 +86,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: "assemb_plane".to_string(), label: "Assembly plane".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), - constraints: BTreeSet::default() + representation: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -98,8 +96,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: "side1".to_string(), label: "Side 1".to_string(), color: [1.00_f32, 0.00_f32, 0.25_f32], - rep: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), - constraints: BTreeSet::default() + representation: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -107,8 +106,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: "side2".to_string(), label: "Side 2".to_string(), color: [0.25_f32, 1.00_f32, 0.00_f32], - rep: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), - constraints: BTreeSet::default() + representation: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -116,8 +116,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: "side3".to_string(), label: "Side 3".to_string(), color: [0.00_f32, 0.25_f32, 1.00_f32], - rep: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), - constraints: BTreeSet::default() + representation: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -125,8 +126,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: "corner1".to_string(), label: "Corner 1".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), - constraints: BTreeSet::default() + representation: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -134,8 +136,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: "corner2".to_string(), label: "Corner 2".to_string(), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: BTreeSet::default() + representation: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0), + constraints: BTreeSet::default(), + index: 0 } ); let _ = assembly.try_insert_element( @@ -143,8 +146,9 @@ fn load_low_curv_assemb(assembly: &Assembly) { id: String::from("corner3"), label: String::from("Corner 3"), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: BTreeSet::default() + representation: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0), + constraints: BTreeSet::default(), + index: 0 } ); } @@ -204,33 +208,51 @@ pub fn AddRemove() -> View { }, on:click=|_| { let state = use_context::(); - let args = state.selection.with( + let subjects = state.selection.with( |sel| { - let arg_vec: Vec<_> = sel.into_iter().collect(); - (arg_vec[0].clone(), arg_vec[1].clone()) + let subject_vec: Vec<_> = sel.into_iter().collect(); + (subject_vec[0].clone(), subject_vec[1].clone()) } ); + let lorentz_prod = create_signal(0.0); + let active = create_signal(true); state.assembly.insert_constraint(Constraint { - args: args, - rep: 0.0, - active: create_signal(true) + subjects: subjects, + lorentz_prod: lorentz_prod, + lorentz_prod_text: create_signal(String::new()), + lorentz_prod_valid: create_signal(false), + active: active, }); + state.assembly.realize(); state.selection.update(|sel| sel.clear()); /* DEBUG */ // print updated constraint list - console::log_1(&JsValue::from("constraints:")); + console::log_1(&JsValue::from("Constraints:")); state.assembly.constraints.with(|csts| { for (_, cst) in csts.into_iter() { console::log_5( &JsValue::from(" "), - &JsValue::from(cst.args.0), - &JsValue::from(cst.args.1), + &JsValue::from(cst.subjects.0), + &JsValue::from(cst.subjects.1), &JsValue::from(":"), - &JsValue::from(cst.rep) + &JsValue::from(cst.lorentz_prod.get_untracked()) ); } }); + + // update the realization when the constraint activated, or + // edited while active + create_effect(move || { + lorentz_prod.track(); + console::log_2( + &JsValue::from("Lorentz product updated to"), + &JsValue::from(lorentz_prod.get_untracked()) + ); + if active.get() { + state.assembly.realize(); + } + }); } ) { "🔗" } select(bind:value=assembly_name) { /* DEBUG */ diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index e8dab79..0cdf61b 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,22 +1,40 @@ -use nalgebra::DVector; +use nalgebra::{DMatrix, DVector}; use rustc_hash::FxHashMap; use slab::Slab; use std::collections::BTreeSet; use sycamore::prelude::*; +use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ + +use crate::engine::{realize_gram, PartialMatrix}; + +// the types of the keys we use to access an assembly's elements and constraints +pub type ElementKey = usize; +pub type ConstraintKey = usize; + +pub type ElementColor = [f32; 3]; #[derive(Clone, PartialEq)] pub struct Element { pub id: String, pub label: String, - pub color: [f32; 3], - pub rep: DVector, - pub constraints: BTreeSet + pub color: ElementColor, + pub representation: DVector, + pub constraints: BTreeSet, + + // the configuration matrix column index that was assigned to this element + // last time the assembly was realized + /* TO DO */ + // this is public, as a kludge, because `Element` doesn't have a constructor + // yet. it should be made private as soon as the constructor is written + pub index: usize } #[derive(Clone)] pub struct Constraint { - pub args: (usize, usize), - pub rep: f64, + pub subjects: (ElementKey, ElementKey), + pub lorentz_prod: Signal, + pub lorentz_prod_text: Signal, + pub lorentz_prod_valid: Signal, pub active: Signal } @@ -28,7 +46,7 @@ pub struct Assembly { pub constraints: Signal>, // indexing - pub elements_by_id: Signal> + pub elements_by_id: Signal> } impl Assembly { @@ -40,6 +58,8 @@ impl Assembly { } } + // --- inserting elements and constraints --- + // 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 @@ -76,18 +96,100 @@ impl Assembly { id: id, label: format!("Sphere {}", id_num), color: [0.75_f32, 0.75_f32, 0.75_f32], - rep: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]), - constraints: BTreeSet::default() + representation: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]), + constraints: BTreeSet::default(), + index: 0 } ); } pub fn insert_constraint(&self, constraint: Constraint) { - let args = constraint.args; + let subjects = constraint.subjects; let key = self.constraints.update(|csts| csts.insert(constraint)); self.elements.update(|elts| { - elts[args.0].constraints.insert(key); - elts[args.1].constraints.insert(key); - }) + elts[subjects.0].constraints.insert(key); + elts[subjects.1].constraints.insert(key); + }); + } + + // --- realization --- + + pub fn realize(&self) { + // index the elements + self.elements.update_silent(|elts| { + for (index, (_, elt)) in elts.into_iter().enumerate() { + elt.index = index; + } + }); + + // set up the Gram matrix and the initial configuration matrix + let (gram, guess) = self.elements.with_untracked(|elts| { + // set up the off-diagonal part of the Gram matrix + let mut gram_to_be = PartialMatrix::new(); + self.constraints.with_untracked(|csts| { + for (_, cst) in csts { + if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() { + let subjects = cst.subjects; + let row = elts[subjects.0].index; + let col = elts[subjects.1].index; + gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked()); + } + } + }); + + // set up the initial configuration matrix and the diagonal of the + // Gram matrix + let mut guess_to_be = DMatrix::::zeros(5, elts.len()); + for (_, elt) in elts { + let index = elt.index; + gram_to_be.push_sym(index, index, 1.0); + guess_to_be.set_column(index, &elt.representation); + } + + (gram_to_be, guess_to_be) + }); + + /* DEBUG */ + // log the Gram matrix + console::log_1(&JsValue::from("Gram matrix:")); + gram.log_to_console(); + + /* DEBUG */ + // log the initial configuration matrix + console::log_1(&JsValue::from("Old configuration:")); + for j in 0..guess.nrows() { + let mut row_str = String::new(); + for k in 0..guess.ncols() { + row_str.push_str(format!(" {:>8.3}", guess[(j, k)]).as_str()); + } + console::log_1(&JsValue::from(row_str)); + } + + // look for a configuration with the given Gram matrix + let (config, success, history) = realize_gram( + &gram, guess, &[], + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + + /* DEBUG */ + // report the outcome of the search + console::log_1(&JsValue::from( + if success { + "Target accuracy achieved!" + } else { + "Failed to reach target accuracy" + } + )); + console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1)); + console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap())); + + if success { + // read out the solution + self.elements.update(|elts| { + for (_, elt) in elts.iter_mut() { + elt.representation.set_column(0, &config.column(elt.index)); + } + }); + } } } \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index c32b470..79199ec 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -297,7 +297,9 @@ pub fn Display() -> View { // get the assembly let elements = state.assembly.elements.get_clone(); let element_iter = (&elements).into_iter(); - let reps_world: Vec<_> = element_iter.clone().map(|(_, elt)| &assembly_to_world * &elt.rep).collect(); + let reps_world: Vec<_> = element_iter.clone().map( + |(_, elt)| &assembly_to_world * &elt.representation + ).collect(); let colors: Vec<_> = element_iter.clone().map(|(key, elt)| if state.selection.with(|sel| sel.contains(&key)) { elt.color.map(|ch| 0.2 + 0.8*ch) diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 79668bb..343b96e 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -1,4 +1,13 @@ -use nalgebra::DVector; +use lazy_static::lazy_static; +use nalgebra::{Const, DMatrix, DVector, Dyn}; +use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ + +// --- elements --- + +#[cfg(test)] +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)]) +} // the sphere with the given center and radius, with inward-pointing normals pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVector { @@ -24,4 +33,508 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 0.5 * curv, off * (1.0 + 0.5 * off * curv) ]) +} + +// --- partial matrices --- + +struct MatrixEntry { + index: (usize, usize), + value: f64 +} + +pub struct PartialMatrix(Vec); + +impl PartialMatrix { + pub fn new() -> PartialMatrix { + PartialMatrix(Vec::::new()) + } + + pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { + let PartialMatrix(entries) = self; + entries.push(MatrixEntry { index: (row, col), value: value }); + if row != col { + entries.push(MatrixEntry { index: (col, row), value: value }); + } + } + + /* DEBUG */ + pub fn log_to_console(&self) { + let PartialMatrix(entries) = self; + for ent in entries { + let ent_str = format!(" {} {} {}", ent.index.0, ent.index.1, ent.value); + console::log_1(&JsValue::from(ent_str.as_str())); + } + } + + fn proj(&self, a: &DMatrix) -> DMatrix { + let mut result = DMatrix::::zeros(a.nrows(), a.ncols()); + let PartialMatrix(entries) = self; + for ent in entries { + result[ent.index] = a[ent.index]; + } + result + } + + fn sub_proj(&self, rhs: &DMatrix) -> DMatrix { + let mut result = DMatrix::::zeros(rhs.nrows(), rhs.ncols()); + let PartialMatrix(entries) = self; + for ent in entries { + result[ent.index] = ent.value - rhs[ent.index]; + } + result + } +} + +// --- descent history --- + +pub struct DescentHistory { + pub config: Vec>, + pub scaled_loss: Vec, + pub neg_grad: Vec>, + pub min_eigval: Vec, + pub base_step: Vec>, + pub backoff_steps: Vec +} + +impl DescentHistory { + fn new() -> DescentHistory { + DescentHistory { + config: Vec::>::new(), + scaled_loss: Vec::::new(), + neg_grad: Vec::>::new(), + min_eigval: Vec::::new(), + base_step: Vec::>::new(), + backoff_steps: Vec::::new(), + } + } +} + +// --- gram matrix realization --- + +// the Lorentz form +lazy_static! { + static ref Q: DMatrix = DMatrix::from_row_slice(5, 5, &[ + 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, -2.0, + 0.0, 0.0, 0.0, -2.0, 0.0 + ]); +} + +struct SearchState { + config: DMatrix, + err_proj: DMatrix, + loss: f64 +} + +impl SearchState { + fn from_config(gram: &PartialMatrix, config: DMatrix) -> SearchState { + let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config)); + let loss = err_proj.norm_squared(); + SearchState { + config: config, + err_proj: err_proj, + loss: loss + } + } +} + +fn basis_matrix(index: (usize, usize), nrows: usize, ncols: usize) -> DMatrix { + let mut result = DMatrix::::zeros(nrows, ncols); + result[index] = 1.0; + result +} + +// use backtracking line search to find a better configuration +fn seek_better_config( + gram: &PartialMatrix, + state: &SearchState, + base_step: &DMatrix, + base_target_improvement: f64, + min_efficiency: f64, + backoff: f64, + max_backoff_steps: i32 +) -> Option<(SearchState, i32)> { + let mut rate = 1.0; + for backoff_steps in 0..max_backoff_steps { + let trial_config = &state.config + rate * base_step; + let trial_state = SearchState::from_config(gram, trial_config); + let improvement = state.loss - trial_state.loss; + if improvement >= min_efficiency * rate * base_target_improvement { + return Some((trial_state, backoff_steps)); + } + rate *= backoff; + } + None +} + +// seek a matrix `config` for which `config' * Q * config` matches the partial +// matrix `gram`. use gradient descent starting from `guess` +pub fn realize_gram( + gram: &PartialMatrix, + guess: DMatrix, + frozen: &[(usize, usize)], + scaled_tol: f64, + min_efficiency: f64, + backoff: f64, + reg_scale: f64, + max_descent_steps: i32, + max_backoff_steps: i32 +) -> (DMatrix, bool, DescentHistory) { + // start the descent history + let mut history = DescentHistory::new(); + + // find the dimension of the search space + let element_dim = guess.nrows(); + let assembly_dim = guess.ncols(); + let total_dim = element_dim * assembly_dim; + + // scale the tolerance + let scale_adjustment = (gram.0.len() as f64).sqrt(); + let tol = scale_adjustment * scaled_tol; + + // convert the frozen indices to stacked format + let frozen_stacked: Vec = frozen.into_iter().map( + |index| index.1*element_dim + index.0 + ).collect(); + + // use Newton's method with backtracking and gradient descent backup + let mut state = SearchState::from_config(gram, guess); + for _ in 0..max_descent_steps { + // stop if the loss is tolerably low + history.config.push(state.config.clone()); + history.scaled_loss.push(state.loss / scale_adjustment); + if state.loss < tol { break; } + + // find the negative gradient of the loss function + let neg_grad = 4.0 * &*Q * &state.config * &state.err_proj; + let mut neg_grad_stacked = neg_grad.clone().reshape_generic(Dyn(total_dim), Const::<1>); + history.neg_grad.push(neg_grad.clone()); + + // find the negative Hessian of the loss function + let mut hess_cols = Vec::>::with_capacity(total_dim); + for col in 0..assembly_dim { + for row in 0..element_dim { + let index = (row, col); + let basis_mat = basis_matrix(index, element_dim, assembly_dim); + let neg_d_err = + basis_mat.tr_mul(&*Q) * &state.config + + state.config.tr_mul(&*Q) * &basis_mat; + let neg_d_err_proj = gram.proj(&neg_d_err); + let deriv_grad = 4.0 * &*Q * ( + -&basis_mat * &state.err_proj + + &state.config * &neg_d_err_proj + ); + hess_cols.push(deriv_grad.reshape_generic(Dyn(total_dim), Const::<1>)); + } + } + let mut hess = DMatrix::from_columns(hess_cols.as_slice()); + + // regularize the Hessian + let min_eigval = hess.symmetric_eigenvalues().min(); + if min_eigval <= 0.0 { + hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); + } + history.min_eigval.push(min_eigval); + + // project the negative gradient and negative Hessian onto the + // orthogonal complement of the frozen subspace + let zero_col = DVector::zeros(total_dim); + let zero_row = zero_col.transpose(); + for &k in &frozen_stacked { + neg_grad_stacked[k] = 0.0; + hess.set_row(k, &zero_row); + hess.set_column(k, &zero_col); + hess[(k, k)] = 1.0; + } + + // compute the Newton step + /* + we need to either handle or eliminate the case where the minimum + eigenvalue of the Hessian is zero, so the regularized Hessian is + singular. right now, this causes the Cholesky decomposition to return + `None`, leading to a panic when we unrap + */ + let base_step_stacked = hess.cholesky().unwrap().solve(&neg_grad_stacked); + let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); + history.base_step.push(base_step.clone()); + + // use backtracking line search to find a better configuration + match seek_better_config( + gram, &state, &base_step, neg_grad.dot(&base_step), + min_efficiency, backoff, max_backoff_steps + ) { + Some((better_state, backoff_steps)) => { + state = better_state; + history.backoff_steps.push(backoff_steps); + }, + None => return (state.config, false, history) + }; + } + (state.config, state.loss < tol, history) +} + +// --- tests --- + +#[cfg(test)] +mod tests { + use std::{array, f64::consts::PI}; + + use super::*; + + #[test] + fn sub_proj_test() { + let target = PartialMatrix(vec![ + MatrixEntry { index: (0, 0), value: 19.0 }, + MatrixEntry { index: (0, 2), value: 39.0 }, + MatrixEntry { index: (1, 1), value: 59.0 }, + MatrixEntry { index: (1, 2), value: 69.0 } + ]); + let attempt = DMatrix::::from_row_slice(2, 3, &[ + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0 + ]); + let expected_result = DMatrix::::from_row_slice(2, 3, &[ + 18.0, 0.0, 36.0, + 0.0, 54.0, 63.0 + ]); + assert_eq!(target.sub_proj(&attempt), expected_result); + } + + #[test] + fn zero_loss_test() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for j in 0..3 { + for k in 0..3 { + entries.push(MatrixEntry { + index: (j, k), + value: if j == k { 1.0 } else { -1.0 } + }); + } + } + entries + }); + let config = { + let a: f64 = 0.75_f64.sqrt(); + DMatrix::from_columns(&[ + sphere(1.0, 0.0, 0.0, a), + sphere(-0.5, a, 0.0, a), + sphere(-0.5, -a, 0.0, a) + ]) + }; + let state = SearchState::from_config(&gram, config); + assert!(state.loss.abs() < f64::EPSILON); + } + + // this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article + // below includes a nice translation of the problem statement, which was + // recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and + // Present_) + // + // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki + // https://www.nippon.com/en/japan-topics/c12801/ + // + #[test] + fn irisawa_hexlet_test() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for s in 0..9 { + // each sphere is represented by a spacelike vector + entries.push(MatrixEntry { index: (s, s), value: 1.0 }); + + // the circumscribing sphere is tangent to all of the other + // spheres, with matching orientation + if s > 0 { + entries.push(MatrixEntry { index: (0, s), value: 1.0 }); + entries.push(MatrixEntry { index: (s, 0), value: 1.0 }); + } + + if s > 2 { + // each chain sphere is tangent to the "sun" and "moon" + // spheres, with opposing orientation + for n in 1..3 { + entries.push(MatrixEntry { index: (s, n), value: -1.0 }); + entries.push(MatrixEntry { index: (n, s), value: -1.0 }); + } + + // each chain sphere is tangent to the next chain sphere, + // with opposing orientation + let s_next = 3 + (s-2) % 6; + entries.push(MatrixEntry { index: (s, s_next), value: -1.0 }); + entries.push(MatrixEntry { index: (s_next, s), value: -1.0 }); + } + } + entries + }); + let guess = DMatrix::from_columns( + [ + sphere(0.0, 0.0, 0.0, 15.0), + sphere(0.0, 0.0, -9.0, 5.0), + sphere(0.0, 0.0, 11.0, 3.0) + ].into_iter().chain( + (1..=6).map( + |k| { + let ang = (k as f64) * PI/3.0; + sphere(9.0 * ang.cos(), 9.0 * ang.sin(), 0.0, 2.5) + } + ) + ).collect::>().as_slice() + ); + let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k)); + const SCALED_TOL: f64 = 1.0e-12; + let (config, success, history) = realize_gram( + &gram, guess, &frozen, + SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + ); + let entry_tol = SCALED_TOL.sqrt(); + let solution_diams = [30.0, 10.0, 6.0, 5.0, 15.0, 10.0, 3.75, 2.5, 2.0 + 8.0/11.0]; + for (k, diam) in solution_diams.into_iter().enumerate() { + assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol); + } + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + if success { + println!("\nChain diameters:"); + println!(" {} sun (given)", 1.0 / config[(3, 3)]); + for k in 4..9 { + println!(" {} sun", 1.0 / config[(3, k)]); + } + } + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } + } + + // --- process inspection examples --- + + // these tests are meant for human inspection, not automated use. run them + // one at a time in `--nocapture` mode and read through the results and + // optimization histories that they print out. the `run-examples` script + // will run all of them + + #[test] + fn three_spheres_example() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for j in 0..3 { + for k in 0..3 { + entries.push(MatrixEntry { + index: (j, k), + value: if j == k { 1.0 } else { -1.0 } + }); + } + } + entries + }); + let guess = { + let a: f64 = 0.75_f64.sqrt(); + DMatrix::from_columns(&[ + sphere(1.0, 0.0, 0.0, 1.0), + sphere(-0.5, a, 0.0, 1.0), + sphere(-0.5, -a, 0.0, 1.0) + ]) + }; + println!(); + let (config, success, history) = realize_gram( + &gram, guess, &[], + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } + } + + #[test] + fn point_on_sphere_example() { + let gram = PartialMatrix({ + let mut entries = Vec::::new(); + for j in 0..2 { + for k in 0..2 { + entries.push(MatrixEntry { + index: (j, k), + value: if (j, k) == (1, 1) { 1.0 } else { 0.0 } + }); + } + } + entries + }); + let guess = DMatrix::from_columns(&[ + point(0.0, 0.0, 2.0), + sphere(0.0, 0.0, 0.0, 1.0) + ]); + let frozen = [(3, 0)]; + println!(); + let (config, success, history) = realize_gram( + &gram, guess, &frozen, + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + print!("Configuration:{}", config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } + } + + /* TO DO */ + // --- new test placed here to avoid merge conflict --- + + // at the frozen indices, the optimization steps should have exact zeros, + // and the realized configuration should match the initial guess + #[test] + fn frozen_entry_test() { + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for j in 0..2 { + for k in j..2 { + gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + gram_to_be + }; + let guess = DMatrix::from_columns(&[ + point(0.0, 0.0, 2.0), + sphere(0.0, 0.0, 0.0, 1.0) + ]); + let frozen = [(3, 0), (3, 1)]; + println!(); + let (config, success, history) = realize_gram( + &gram, guess.clone(), &frozen, + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + assert_eq!(success, true); + for base_step in history.base_step.into_iter() { + for index in frozen { + assert_eq!(base_step[index], 0.0); + } + } + for index in frozen { + assert_eq!(config[index], guess[index]); + } + } } \ No newline at end of file diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index 2c71a83..897f9d4 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -8,14 +8,14 @@ use rustc_hash::FxHashSet; use sycamore::prelude::*; use add_remove::AddRemove; -use assembly::Assembly; +use assembly::{Assembly, ElementKey}; use display::Display; use outline::Outline; #[derive(Clone)] struct AppState { assembly: Assembly, - selection: Signal> + selection: Signal> } impl AppState { diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 4e4de9c..f7c975c 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,12 +1,40 @@ use itertools::Itertools; use sycamore::{prelude::*, web::tags::div}; -use web_sys::{Element, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; +use web_sys::{ + Element, + Event, + HtmlInputElement, + KeyboardEvent, + MouseEvent, + wasm_bindgen::JsCast +}; -use crate::AppState; +use crate::{AppState, assembly::Constraint}; -// this component lists the elements of the assembly, showing the constraints -// on each element as a collapsible sub-list. its implementation is based on -// Kate Morley's HTML + CSS tree views: +// an editable view of the Lorentz product representing a constraint +#[component(inline_props)] +fn LorentzProductInput(constraint: Constraint) -> View { + view! { + input( + r#type="text", + bind:value=constraint.lorentz_prod_text, + on:change=move |event: Event| { + let target: HtmlInputElement = event.target().unwrap().unchecked_into(); + match target.value().parse::() { + Ok(lorentz_prod) => batch(|| { + constraint.lorentz_prod.set(lorentz_prod); + constraint.lorentz_prod_valid.set(true); + }), + Err(_) => constraint.lorentz_prod_valid.set(false) + }; + } + ) + } +} + +// a component that lists the elements of the current assembly, showing the +// constraints on each element as a collapsible sub-list. its implementation +// is based on Kate Morley's HTML + CSS tree views: // // https://iamkate.com/code/tree-views/ // @@ -44,15 +72,13 @@ pub fn Outline() -> View { } }); let label = elt.label.clone(); - let rep_components = elt.rep.iter().map(|u| { + let rep_components = elt.representation.iter().map(|u| { let u_coord = u.to_string().replace("-", "\u{2212}"); View::from(div().children(u_coord)) }).collect::>(); let constrained = elt.constraints.len() > 0; let details_node = create_node_ref(); view! { - /* [TO DO] switch to integer-valued parameters whenever - that becomes possible again */ li { details(ref=details_node) { summary( @@ -124,21 +150,21 @@ pub fn Outline() -> View { ul(class="constraints") { Keyed( list=elt.constraints.into_iter().collect::>(), - view=move |c_key: usize| { + view=move |c_key| { let c_state = use_context::(); let assembly = &c_state.assembly; let cst = assembly.constraints.with(|csts| csts[c_key].clone()); - let other_arg = if cst.args.0 == key { - cst.args.1 + let other_arg = if cst.subjects.0 == key { + cst.subjects.1 } else { - cst.args.0 + cst.subjects.0 }; let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone()); view! { li(class="cst") { input(r#type="checkbox", bind:checked=cst.active) div(class="cst-label") { (other_arg_label) } - div(class="cst-rep") { (cst.rep) } + LorentzProductInput(constraint=cst) } } }, diff --git a/engine-proto/gram-test/Engine.jl b/engine-proto/gram-test/Engine.jl index 22f5914..6dfb6e9 100644 --- a/engine-proto/gram-test/Engine.jl +++ b/engine-proto/gram-test/Engine.jl @@ -8,7 +8,8 @@ using Optim export rand_on_shell, Q, DescentHistory, - realize_gram_gradient, realize_gram_newton, realize_gram_optim, realize_gram + realize_gram_gradient, realize_gram_newton, realize_gram_optim, + realize_gram_alt_proj, realize_gram # === guessing === @@ -143,7 +144,7 @@ function realize_gram_gradient( break end - # find negative gradient of loss function + # find the negative gradient of the loss function neg_grad = 4*Q*L*Δ_proj slope = norm(neg_grad) dir = neg_grad / slope @@ -232,7 +233,7 @@ function realize_gram_newton( break end - # find the negative gradient of loss function + # find the negative gradient of the loss function neg_grad = 4*Q*L*Δ_proj # find the negative Hessian of the loss function @@ -313,6 +314,129 @@ function realize_gram_optim( ) end +# seek a matrix `L` for which `L'QL` matches the sparse matrix `gram` at every +# explicit entry of `gram`. use gradient descent starting from `guess`, with an +# alternate technique for finding the projected base step from the unprojected +# Hessian +function realize_gram_alt_proj( + gram::SparseMatrixCSC{T, <:Any}, + guess::Matrix{T}, + frozen = CartesianIndex[]; + scaled_tol = 1e-30, + min_efficiency = 0.5, + backoff = 0.9, + reg_scale = 1.1, + max_descent_steps = 200, + max_backoff_steps = 110 +) where T <: Number + # start history + history = DescentHistory{T}() + + # find the dimension of the search space + dims = size(guess) + element_dim, construction_dim = dims + total_dim = element_dim * construction_dim + + # list the constrained entries of the gram matrix + J, K, _ = findnz(gram) + constrained = zip(J, K) + + # scale the tolerance + scale_adjustment = sqrt(T(length(constrained))) + tol = scale_adjustment * scaled_tol + + # convert the frozen indices to stacked format + frozen_stacked = [(index[2]-1)*element_dim + index[1] for index in frozen] + + # initialize search state + L = copy(guess) + Δ_proj = proj_diff(gram, L'*Q*L) + loss = dot(Δ_proj, Δ_proj) + + # use Newton's method with backtracking and gradient descent backup + for step in 1:max_descent_steps + # stop if the loss is tolerably low + if loss < tol + break + end + + # find the negative gradient of the loss function + neg_grad = 4*Q*L*Δ_proj + + # find the negative Hessian of the loss function + hess = Matrix{T}(undef, total_dim, total_dim) + indices = [(j, k) for k in 1:construction_dim for j in 1:element_dim] + for (j, k) in indices + basis_mat = basis_matrix(T, j, k, dims) + neg_dΔ = basis_mat'*Q*L + L'*Q*basis_mat + neg_dΔ_proj = proj_to_entries(neg_dΔ, constrained) + deriv_grad = 4*Q*(-basis_mat*Δ_proj + L*neg_dΔ_proj) + hess[:, (k-1)*element_dim + j] = reshape(deriv_grad, total_dim) + end + hess_sym = Hermitian(hess) + push!(history.hess, hess_sym) + + # regularize the Hessian + min_eigval = minimum(eigvals(hess_sym)) + push!(history.positive, min_eigval > 0) + if min_eigval <= 0 + hess -= reg_scale * min_eigval * I + end + + # compute the Newton step + neg_grad_stacked = reshape(neg_grad, total_dim) + for k in frozen_stacked + neg_grad_stacked[k] = 0 + hess[k, :] .= 0 + hess[:, k] .= 0 + hess[k, k] = 1 + end + base_step_stacked = Hermitian(hess) \ neg_grad_stacked + base_step = reshape(base_step_stacked, dims) + push!(history.base_step, base_step) + + # store the current position, loss, and slope + L_last = L + loss_last = loss + push!(history.scaled_loss, loss / scale_adjustment) + push!(history.neg_grad, neg_grad) + push!(history.slope, norm(neg_grad)) + + # find a good step size using backtracking line search + push!(history.stepsize, 0) + push!(history.backoff_steps, max_backoff_steps) + empty!(history.last_line_L) + empty!(history.last_line_loss) + rate = one(T) + step_success = false + base_target_improvement = dot(neg_grad, base_step) + for backoff_steps in 0:max_backoff_steps + history.stepsize[end] = rate + L = L_last + rate * base_step + Δ_proj = proj_diff(gram, L'*Q*L) + loss = dot(Δ_proj, Δ_proj) + improvement = loss_last - loss + push!(history.last_line_L, L) + push!(history.last_line_loss, loss / scale_adjustment) + if improvement >= min_efficiency * rate * base_target_improvement + history.backoff_steps[end] = backoff_steps + step_success = true + break + end + rate *= backoff + end + + # if we've hit a wall, quit + if !step_success + return L_last, false, history + end + end + + # return the factorization and its history + push!(history.scaled_loss, loss / scale_adjustment) + L, loss < tol, history +end + # seek a matrix `L` for which `L'QL` matches the sparse matrix `gram` at every # explicit entry of `gram`. use gradient descent starting from `guess` function realize_gram( @@ -321,7 +445,6 @@ function realize_gram( frozen = nothing; scaled_tol = 1e-30, min_efficiency = 0.5, - init_rate = 1.0, backoff = 0.9, reg_scale = 1.1, max_descent_steps = 200, @@ -352,20 +475,19 @@ function realize_gram( unfrozen_stacked = reshape(is_unfrozen, total_dim) end - # initialize variables - grad_rate = init_rate + # initialize search state L = copy(guess) - - # use Newton's method with backtracking and gradient descent backup Δ_proj = proj_diff(gram, L'*Q*L) loss = dot(Δ_proj, Δ_proj) + + # use Newton's method with backtracking and gradient descent backup for step in 1:max_descent_steps # stop if the loss is tolerably low if loss < tol break end - # find the negative gradient of loss function + # find the negative gradient of the loss function neg_grad = 4*Q*L*Δ_proj # find the negative Hessian of the loss function @@ -420,6 +542,7 @@ function realize_gram( empty!(history.last_line_loss) rate = one(T) step_success = false + base_target_improvement = dot(neg_grad, base_step) for backoff_steps in 0:max_backoff_steps history.stepsize[end] = rate L = L_last + rate * base_step @@ -428,7 +551,7 @@ function realize_gram( improvement = loss_last - loss push!(history.last_line_L, L) push!(history.last_line_loss, loss / scale_adjustment) - if improvement >= min_efficiency * rate * dot(neg_grad, base_step) + if improvement >= min_efficiency * rate * base_target_improvement history.backoff_steps[end] = backoff_steps step_success = true break diff --git a/engine-proto/gram-test/irisawa-hexlet.jl b/engine-proto/gram-test/irisawa-hexlet.jl index 67def8c..607db61 100644 --- a/engine-proto/gram-test/irisawa-hexlet.jl +++ b/engine-proto/gram-test/irisawa-hexlet.jl @@ -74,4 +74,13 @@ if success for k in 5:9 println(" ", 1 / L[4,k], " sun") end -end \ No newline at end of file +end + +# test an alternate technique for finding the projected base step from the +# unprojected Hessian +L_alt, success_alt, history_alt = Engine.realize_gram_alt_proj(gram, guess, frozen) +completed_gram_alt = L_alt'*Engine.Q*L_alt +println("\nDifference in result using alternate projection:\n") +display(completed_gram_alt - completed_gram) +println("\nDifference in steps: ", size(history_alt.scaled_loss, 1) - size(history.scaled_loss, 1)) +println("Difference in loss: ", history_alt.scaled_loss[end] - history.scaled_loss[end], "\n") \ No newline at end of file diff --git a/engine-proto/gram-test/sphere-in-tetrahedron.jl b/engine-proto/gram-test/sphere-in-tetrahedron.jl index 97f0720..5d479cf 100644 --- a/engine-proto/gram-test/sphere-in-tetrahedron.jl +++ b/engine-proto/gram-test/sphere-in-tetrahedron.jl @@ -64,4 +64,13 @@ else println("\nFailed to reach target accuracy") end println("Steps: ", size(history.scaled_loss, 1)) -println("Loss: ", history.scaled_loss[end], "\n") \ No newline at end of file +println("Loss: ", history.scaled_loss[end], "\n") + +# test an alternate technique for finding the projected base step from the +# unprojected Hessian +L_alt, success_alt, history_alt = Engine.realize_gram_alt_proj(gram, guess, frozen) +completed_gram_alt = L_alt'*Engine.Q*L_alt +println("\nDifference in result using alternate projection:\n") +display(completed_gram_alt - completed_gram) +println("\nDifference in steps: ", size(history_alt.scaled_loss, 1) - size(history.scaled_loss, 1)) +println("Difference in loss: ", history_alt.scaled_loss[end] - history.scaled_loss[end], "\n") \ No newline at end of file diff --git a/engine-proto/gram-test/tetrahedron-radius-ratio.jl b/engine-proto/gram-test/tetrahedron-radius-ratio.jl index 7ceb794..9fec28e 100644 --- a/engine-proto/gram-test/tetrahedron-radius-ratio.jl +++ b/engine-proto/gram-test/tetrahedron-radius-ratio.jl @@ -93,4 +93,13 @@ if success infty = BigFloat[0, 0, 0, 0, 1] radius_ratio = dot(infty, Engine.Q * L[:,5]) / dot(infty, Engine.Q * L[:,6]) println("\nCircumradius / inradius: ", radius_ratio) -end \ No newline at end of file +end + +# test an alternate technique for finding the projected base step from the +# unprojected Hessian +L_alt, success_alt, history_alt = Engine.realize_gram_alt_proj(gram, guess, frozen) +completed_gram_alt = L_alt'*Engine.Q*L_alt +println("\nDifference in result using alternate projection:\n") +display(completed_gram_alt - completed_gram) +println("\nDifference in steps: ", size(history_alt.scaled_loss, 1) - size(history.scaled_loss, 1)) +println("Difference in loss: ", history_alt.scaled_loss[end] - history.scaled_loss[end], "\n") \ No newline at end of file diff --git a/notes/inversive.md b/notes/inversive.md index 5ee6329..933eb35 100644 --- a/notes/inversive.md +++ b/notes/inversive.md @@ -41,3 +41,25 @@ I will have to work out formulas for the Euclidean distance between two entities In this vein, it seems as though if J1 and J2 are the reps of two points, then Q(J1,J2) = d^2/2. So then the sphere centered at J1 through J2 is (J1-(2Q(J1,J2),0,0,0,0))/sqrt(2Q(J1,J2)). Ugh has a sqrt in it. Similarly for sphere centered at J3 through J2, (J3-(2Q(J3,J2),0000))/sqrt(2Q(J3,J2)). J1,J2,J3 are collinear if these spheres are tangent, i.e. if those vectors have Q-inner-product 1, which is to say Q(J1,J3) - Q(J1,J2) - Q(J3,J2) = 2sqrt(Q(J1,J2)Q(J2,J3)). But maybe that's not the simplest way of putting it. After all, we can just say that the cross-product of the two differences is 0; that has no square roots in it. One conceivable way to canonicalize lines is to use the *perpendicular* plane that goes through the origin, that's uniquely defined, and anyway just amounts to I = (0,0,d) where d is the ordinary direction vector of the line; and a point J in that plane that the line goes through, which just amounts to J=(r^2,1,E) with Q(I,J) = 0, i.e. E\dot d = 0. It's also the point on the line closest to the origin. The reason that we don't usually use that point as the companion to the direction vector is that the resulting set of six coordinates is not homogeneous. But here that's not an issue, since we have our standard point coordinates and plane coordinates; and for a plane through the origin, only two of the direction coordinates are really free, and then we have the one dot-product relation, so only two of the point coordinates are really free, giving us the correct dimensionality of 4 for the set of lines. So in some sense this says that we could take naively as coordinates for a line the projection of the unit direction vector to the xy plane and the projection of the line's closest point to the origin to the xy plane. That doesn't seem to have any weird gimbal locks or discontinuities or anything. And with these coordinates, you can test if the point E=x,y,z is on the line (dx,dy,cx,cy) by extending (dx,dy) to d via dz = sqrt(1-dx^2 - dy^2), extending (cx,cy) to c by determining cz via d\dot c = 0, and then checking if d\cross(E-c) = 0. And you can see if two lines are parallel just by checking if they have the same direction vector, and if not, you can see if they are coplanar by projecting both of their closest points perpendicularly onto the line in the direction of the cross product of their directions, and if the projections match they are coplanar. + +#### Engine Conventions + +The coordinate conventions used in the engine are different from the ones used in these notes. Marking the engine vectors and coordinates with $'$, we have +$$I' = (x', y', z', b', c'),$$ +where +$$ +\begin{align*} +x' & = x & b' & = b/2 \\ +y' & = y & c' & = c/2. \\ +z' & = z +\end{align*} +$$ +The engine uses the quadratic form $Q' = -Q$, which is expressed in engine coordinates as +$$Q'(I'_1, I'_2) = x'_1 x'_2 + y'_1 y'_2 + z'_1 z'_2 - 2(b'_1c'_2 + c'_1 b'_2).$$ +In the `engine` module, the matrix of $Q'$ is encoded in the lazy static variable `Q`. + +In the engine's coordinate conventions, a sphere with radius $r > 0$ centered on $P = (P_x, P_y, P_z)$ is represented by the vector +$$I'_s = \left(\frac{P_x}{r}, \frac{P_y}{r}, \frac{P_z}{r}, \frac1{2r}, \frac{\|P\|^2 - r^2}{2r}\right),$$ +which has the normalization $Q'(I'_s, I'_s) = 1$. The point $P$ is represented by the vector +$$I'_P = \left(P_x, P_y, P_z, \frac{1}{2}, \frac{\|P\|^2}{2}\right).$$ +In the `engine` module, these formulas are encoded in the `sphere` and `point` functions. \ No newline at end of file From 65cee1ecc236402ef4b5a462778cd8df51c58908 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Fri, 15 Nov 2024 03:32:47 +0000 Subject: [PATCH 103/126] Clean up the outline view (#19) Clean up the source code and interface of the outline view. In addition, [fix a bug](commit/6e42681b719d7ec97c4225ca321225979bf87b56) that could cause `Assembly::realize` to react to itself under certain circumstances. Those circumstances arose, making the bug noticeable, while this branch was being written. #### Source code - Modularize the `Outline` component into smaller components. - Switch from static iteration to dynamic Sycamore lists. This reduces the amount of re-rendering that happens when an element or constraint changes. It also allows constraint details to stay open or closed during constraint updates, rather than resetting to closed. - Make `Element::index` private, as discussed [here](pulls/15#issuecomment-1816). #### Interface - Make constraints editable, updating the assembly realization on input. Flag constraints where the Lorentz product value doesn't parse. - Round element vector coordinates to prevent the displayed strings from overlapping. Note that issue #20 was created by this PR, but it will be addressed shortly. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/19 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.toml | 4 +- app-proto/index.html | 4 +- app-proto/main.css | 90 +++++++++--- app-proto/src/add_remove.rs | 222 +++++++++++++--------------- app-proto/src/assembly.rs | 71 +++++---- app-proto/src/display.rs | 61 +++++--- app-proto/src/outline.rs | 280 +++++++++++++++++++----------------- 7 files changed, 404 insertions(+), 328 deletions(-) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index e5bc05e..e623b26 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "sketch-outline" +name = "dyna3" version = "0.1.0" -authors = ["Aaron"] +authors = ["Aaron Fenyes", "Glen Whitney"] edition = "2021" [features] diff --git a/app-proto/index.html b/app-proto/index.html index 5474fe9..92238f4 100644 --- a/app-proto/index.html +++ b/app-proto/index.html @@ -2,8 +2,10 @@ - Sketch outline + dyna3 + + diff --git a/app-proto/main.css b/app-proto/main.css index 32ae5bf..b9fc0a1 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -1,7 +1,20 @@ +:root { + --text: #fcfcfc; /* almost white */ + --text-bright: white; + --text-invalid: #f58fc2; /* bright pink */ + --border: #555; /* light gray */ + --border-focus: #aaa; /* bright gray */ + --border-invalid: #70495c; /* dusky pink */ + --selection-highlight: #444; /* medium gray */ + --page-background: #222; /* dark gray */ + --display-background: #020202; /* almost black */ +} + body { margin: 0px; - color: #fcfcfc; - background-color: #222; + color: var(--text); + background-color: var(--page-background); + font-family: 'Fira Sans', sans-serif; } /* sidebar */ @@ -16,7 +29,7 @@ body { padding: 0px; border-width: 0px 1px 0px 0px; border-style: solid; - border-color: #555; + border-color: var(--border); } /* add-remove */ @@ -33,6 +46,15 @@ body { font-size: large; } +/* KLUDGE */ +/* + for convenience, we're using emoji as temporary icons for some buttons. these + buttons need to be displayed in an emoji font +*/ +#add-remove > button.emoji { + font-family: 'Noto Emoji', sans-serif; +} + /* outline */ #outline { @@ -51,81 +73,103 @@ summary { } summary.selected { - color: #fff; - background-color: #444; + color: var(--text-bright); + background-color: var(--selection-highlight); } -summary > div, .cst { +summary > div, .constraint { padding-top: 4px; padding-bottom: 4px; } -.elt, .cst { +.element, .constraint { display: flex; flex-grow: 1; padding-left: 8px; padding-right: 8px; } -.elt-switch { +.element-switch { width: 18px; padding-left: 2px; text-align: center; } -details:has(li) .elt-switch::after { +details:has(li) .element-switch::after { content: '▸'; } -details[open]:has(li) .elt-switch::after { +details[open]:has(li) .element-switch::after { content: '▾'; } -.elt-label { +.element-label { flex-grow: 1; } -.cst-label { +.constraint-label { flex-grow: 1; } -.elt-rep { +.element-representation { display: flex; } -.elt-rep > div { +.element-representation > div { padding: 2px 0px 0px 0px; font-size: 10pt; - text-align: center; + font-variant-numeric: tabular-nums; + text-align: right; width: 56px; } -.cst { +.constraint { font-style: italic; } -.cst > input[type=checkbox] { +.constraint.invalid { + color: var(--text-invalid); +} + +.constraint > input[type=checkbox] { margin: 0px 8px 0px 0px; } -.cst > input[type=text] { - color: #fcfcfc; +.constraint > input[type=text] { + color: inherit; background-color: inherit; - border: 1px solid #555; + border: 1px solid var(--border); border-radius: 2px; } +.constraint.invalid > input[type=text] { + border-color: var(--border-invalid); +} + +.status { + width: 20px; + padding-left: 4px; + text-align: center; + font-family: 'Noto Emoji'; + font-style: normal; +} + +.invalid > .status::after, details:has(.invalid):not([open]) .status::after { + content: '⚠'; + color: var(--text-invalid); +} + /* display */ canvas { float: left; margin-left: 20px; margin-top: 20px; - background-color: #020202; - border: 1px solid #555; + background-color: var(--display-background); + border: 1px solid var(--border); border-radius: 16px; } canvas:focus { - border-color: #aaa; + border-color: var(--border-focus); } \ No newline at end of file diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 19b4b8d..ba02e65 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -1,155 +1,130 @@ -use std::collections::BTreeSet; /* DEBUG */ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}}; /* DEBUG */ +// load an example assembly for testing. this code will be removed once we've +// built a more formal test assembly system fn load_gen_assemb(assembly: &Assembly) { let _ = assembly.try_insert_element( - Element { - id: String::from("gemini_a"), - label: String::from("Castor"), - color: [1.00_f32, 0.25_f32, 0.00_f32], - representation: engine::sphere(0.5, 0.5, 0.0, 1.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("gemini_a"), + String::from("Castor"), + [1.00_f32, 0.25_f32, 0.00_f32], + engine::sphere(0.5, 0.5, 0.0, 1.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("gemini_b"), - label: String::from("Pollux"), - color: [0.00_f32, 0.25_f32, 1.00_f32], - representation: engine::sphere(-0.5, -0.5, 0.0, 1.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("gemini_b"), + String::from("Pollux"), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere(-0.5, -0.5, 0.0, 1.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("ursa_major"), - label: String::from("Ursa major"), - color: [0.25_f32, 0.00_f32, 1.00_f32], - representation: engine::sphere(-0.5, 0.5, 0.0, 0.75), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("ursa_major"), + String::from("Ursa major"), + [0.25_f32, 0.00_f32, 1.00_f32], + engine::sphere(-0.5, 0.5, 0.0, 0.75) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("ursa_minor"), - label: String::from("Ursa minor"), - color: [0.25_f32, 1.00_f32, 0.00_f32], - representation: engine::sphere(0.5, -0.5, 0.0, 0.5), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("ursa_minor"), + String::from("Ursa minor"), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere(0.5, -0.5, 0.0, 0.5) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("moon_deimos"), - label: String::from("Deimos"), - color: [0.75_f32, 0.75_f32, 0.00_f32], - representation: engine::sphere(0.0, 0.15, 1.0, 0.25), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("moon_deimos"), + String::from("Deimos"), + [0.75_f32, 0.75_f32, 0.00_f32], + engine::sphere(0.0, 0.15, 1.0, 0.25) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("moon_phobos"), - label: String::from("Phobos"), - color: [0.00_f32, 0.75_f32, 0.50_f32], - representation: engine::sphere(0.0, -0.15, -1.0, 0.25), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("moon_phobos"), + String::from("Phobos"), + [0.00_f32, 0.75_f32, 0.50_f32], + engine::sphere(0.0, -0.15, -1.0, 0.25) + ) ); } /* DEBUG */ +// load an example assembly for testing. this code will be removed once we've +// built a more formal test assembly system fn load_low_curv_assemb(assembly: &Assembly) { let a = 0.75_f64.sqrt(); let _ = assembly.try_insert_element( - Element { - id: "central".to_string(), - label: "Central".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: engine::sphere(0.0, 0.0, 0.0, 1.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "central".to_string(), + "Central".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "assemb_plane".to_string(), - label: "Assembly plane".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "assemb_plane".to_string(), + "Assembly plane".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "side1".to_string(), - label: "Side 1".to_string(), - color: [1.00_f32, 0.00_f32, 0.25_f32], - representation: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "side1".to_string(), + "Side 1".to_string(), + [1.00_f32, 0.00_f32, 0.25_f32], + engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "side2".to_string(), - label: "Side 2".to_string(), - color: [0.25_f32, 1.00_f32, 0.00_f32], - representation: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "side2".to_string(), + "Side 2".to_string(), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "side3".to_string(), - label: "Side 3".to_string(), - color: [0.00_f32, 0.25_f32, 1.00_f32], - representation: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "side3".to_string(), + "Side 3".to_string(), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "corner1".to_string(), - label: "Corner 1".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "corner1".to_string(), + "Corner 1".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: "corner2".to_string(), - label: "Corner 2".to_string(), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + "corner2".to_string(), + "Corner 2".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) + ) ); let _ = assembly.try_insert_element( - Element { - id: String::from("corner3"), - label: String::from("Corner 3"), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + String::from("corner3"), + String::from("Corner 3"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0) + ) ); } @@ -202,6 +177,7 @@ pub fn AddRemove() -> View { } ) { "+" } button( + class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button disabled={ let state = use_context::(); state.selection.with(|sel| sel.len() != 2) @@ -215,15 +191,15 @@ pub fn AddRemove() -> View { } ); let lorentz_prod = create_signal(0.0); + let lorentz_prod_valid = create_signal(false); let active = create_signal(true); state.assembly.insert_constraint(Constraint { subjects: subjects, lorentz_prod: lorentz_prod, lorentz_prod_text: create_signal(String::new()), - lorentz_prod_valid: create_signal(false), + lorentz_prod_valid: lorentz_prod_valid, active: active, }); - state.assembly.realize(); state.selection.update(|sel| sel.clear()); /* DEBUG */ @@ -241,23 +217,23 @@ pub fn AddRemove() -> View { } }); - // update the realization when the constraint activated, or - // edited while active + // update the realization when the constraint becomes active + // and valid, or is edited while active and valid create_effect(move || { + console::log_1(&JsValue::from( + format!("Constraint ({}, {}) updated", subjects.0, subjects.1) + )); lorentz_prod.track(); - console::log_2( - &JsValue::from("Lorentz product updated to"), - &JsValue::from(lorentz_prod.get_untracked()) - ); - if active.get() { + if active.get() && lorentz_prod_valid.get() { state.assembly.realize(); } }); } ) { "🔗" } - select(bind:value=assembly_name) { /* DEBUG */ + select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser option(value="general") { "General" } option(value="low-curv") { "Low-curvature" } + option(value="empty") { "Empty" } } } } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 0cdf61b..35b4417 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -18,17 +18,33 @@ pub struct Element { pub id: String, pub label: String, pub color: ElementColor, - pub representation: DVector, - pub constraints: BTreeSet, + pub representation: Signal>, + pub constraints: Signal>, // the configuration matrix column index that was assigned to this element // last time the assembly was realized - /* TO DO */ - // this is public, as a kludge, because `Element` doesn't have a constructor - // yet. it should be made private as soon as the constructor is written - pub index: usize + column_index: usize } +impl Element { + pub fn new( + id: String, + label: String, + color: ElementColor, + representation: DVector + ) -> Element { + Element { + id: id, + label: label, + color: color, + representation: create_signal(representation), + constraints: create_signal(BTreeSet::default()), + column_index: 0 + } + } +} + + #[derive(Clone)] pub struct Constraint { pub subjects: (ElementKey, ElementKey), @@ -92,24 +108,23 @@ impl Assembly { // create and insert a new element self.insert_element_unchecked( - Element { - id: id, - label: format!("Sphere {}", id_num), - color: [0.75_f32, 0.75_f32, 0.75_f32], - representation: DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]), - constraints: BTreeSet::default(), - index: 0 - } + Element::new( + id, + format!("Sphere {}", id_num), + [0.75_f32, 0.75_f32, 0.75_f32], + DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]) + ) ); } pub fn insert_constraint(&self, constraint: Constraint) { let subjects = constraint.subjects; let key = self.constraints.update(|csts| csts.insert(constraint)); - self.elements.update(|elts| { - elts[subjects.0].constraints.insert(key); - elts[subjects.1].constraints.insert(key); - }); + let subject_constraints = self.elements.with( + |elts| (elts[subjects.0].constraints, elts[subjects.1].constraints) + ); + subject_constraints.0.update(|csts| csts.insert(key)); + subject_constraints.1.update(|csts| csts.insert(key)); } // --- realization --- @@ -118,7 +133,7 @@ impl Assembly { // index the elements self.elements.update_silent(|elts| { for (index, (_, elt)) in elts.into_iter().enumerate() { - elt.index = index; + elt.column_index = index; } }); @@ -130,8 +145,8 @@ impl Assembly { for (_, cst) in csts { if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() { let subjects = cst.subjects; - let row = elts[subjects.0].index; - let col = elts[subjects.1].index; + let row = elts[subjects.0].column_index; + let col = elts[subjects.1].column_index; gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked()); } } @@ -141,9 +156,9 @@ impl Assembly { // Gram matrix let mut guess_to_be = DMatrix::::zeros(5, elts.len()); for (_, elt) in elts { - let index = elt.index; + let index = elt.column_index; gram_to_be.push_sym(index, index, 1.0); - guess_to_be.set_column(index, &elt.representation); + guess_to_be.set_column(index, &elt.representation.get_clone_untracked()); } (gram_to_be, guess_to_be) @@ -185,11 +200,11 @@ impl Assembly { if success { // read out the solution - self.elements.update(|elts| { - for (_, elt) in elts.iter_mut() { - elt.representation.set_column(0, &config.column(elt.index)); - } - }); + for (_, elt) in self.elements.get_clone_untracked() { + elt.representation.update( + |rep| rep.set_column(0, &config.column(elt.column_index)) + ); + } } } } \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 79199ec..ee0af47 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -103,7 +103,11 @@ pub fn Display() -> View { // change listener let scene_changed = create_signal(true); create_effect(move || { - state.assembly.elements.track(); + state.assembly.elements.with(|elts| { + for (_, elt) in elts { + elt.representation.track(); + } + }); state.selection.track(); scene_changed.set(true); }); @@ -295,25 +299,40 @@ pub fn Display() -> View { let assembly_to_world = &location * &orientation; // get the assembly - let elements = state.assembly.elements.get_clone(); - let element_iter = (&elements).into_iter(); - let reps_world: Vec<_> = element_iter.clone().map( - |(_, elt)| &assembly_to_world * &elt.representation - ).collect(); - let colors: Vec<_> = element_iter.clone().map(|(key, elt)| - if state.selection.with(|sel| sel.contains(&key)) { - elt.color.map(|ch| 0.2 + 0.8*ch) - } else { - elt.color - } - ).collect(); - let highlights: Vec<_> = element_iter.map(|(key, _)| - if state.selection.with(|sel| sel.contains(&key)) { - 1.0_f32 - } else { - HIGHLIGHT - } - ).collect(); + let ( + elt_cnt, + reps_world, + colors, + highlights + ) = state.assembly.elements.with(|elts| { + ( + // number of elements + elts.len() as i32, + + // representation vectors in world coordinates + elts.iter().map( + |(_, elt)| elt.representation.with(|rep| &assembly_to_world * rep) + ).collect::>(), + + // colors + elts.iter().map(|(key, elt)| { + if state.selection.with(|sel| sel.contains(&key)) { + elt.color.map(|ch| 0.2 + 0.8*ch) + } else { + elt.color + } + }).collect::>(), + + // highlight levels + elts.iter().map(|(key, _)| { + if state.selection.with(|sel| sel.contains(&key)) { + 1.0_f32 + } else { + HIGHLIGHT + } + }).collect::>() + ) + }); // set the resolution let width = canvas.width() as f32; @@ -322,7 +341,7 @@ pub fn Display() -> View { ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); // pass the assembly - ctx.uniform1i(sphere_cnt_loc.as_ref(), elements.len() as i32); + ctx.uniform1i(sphere_cnt_loc.as_ref(), elt_cnt); for n in 0..reps_world.len() { let v = &reps_world[n]; ctx.uniform3f( diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index f7c975c..ee1603f 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,7 +1,6 @@ use itertools::Itertools; -use sycamore::{prelude::*, web::tags::div}; +use sycamore::prelude::*; use web_sys::{ - Element, Event, HtmlInputElement, KeyboardEvent, @@ -9,7 +8,7 @@ use web_sys::{ wasm_bindgen::JsCast }; -use crate::{AppState, assembly::Constraint}; +use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}}; // an editable view of the Lorentz product representing a constraint #[component(inline_props)] @@ -32,6 +31,143 @@ fn LorentzProductInput(constraint: Constraint) -> View { } } +// a list item that shows a constraint in an outline view of an element +#[component(inline_props)] +fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) -> View { + let state = use_context::(); + let assembly = &state.assembly; + let constraint = assembly.constraints.with(|csts| csts[constraint_key].clone()); + let other_subject = if constraint.subjects.0 == element_key { + constraint.subjects.1 + } else { + constraint.subjects.0 + }; + let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); + let class = constraint.lorentz_prod_valid.map( + |&lorentz_prod_valid| if lorentz_prod_valid { "constraint" } else { "constraint invalid" } + ); + view! { + li(class=class.get()) { + input(r#type="checkbox", bind:checked=constraint.active) + div(class="constraint-label") { (other_subject_label) } + LorentzProductInput(constraint=constraint) + div(class="status") + } + } +} + +// 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 { + let state = use_context::(); + let class = state.selection.map( + move |sel| if sel.contains(&key) { "selected" } else { "" } + ); + let label = element.label.clone(); + let rep_components = element.representation.map( + |rep| rep.iter().map( + |u| format!("{:.3}", u).replace("-", "\u{2212}") + ).collect() + ); + let constrained = element.constraints.map(|csts| csts.len() > 0); + let constraint_list = element.constraints.map( + |csts| csts.clone().into_iter().collect() + ); + let details_node = create_node_ref(); + view! { + li { + details(ref=details_node) { + summary( + class=class.get(), + on:keydown={ + move |event: KeyboardEvent| { + match event.key().as_str() { + "Enter" => { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + event.prevent_default(); + }, + "ArrowRight" if constrained.get() => { + let _ = details_node + .get() + .unchecked_into::() + .set_attribute("open", ""); + }, + "ArrowLeft" => { + let _ = details_node + .get() + .unchecked_into::() + .remove_attribute("open"); + }, + _ => () + } + } + } + ) { + div( + class="element-switch", + on:click=|event: MouseEvent| event.stop_propagation() + ) + div( + class="element", + on:click={ + move |event: MouseEvent| { + if event.shift_key() { + state.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + state.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + event.stop_propagation(); + event.prevent_default(); + } + } + ) { + div(class="element-label") { (label) } + div(class="element-representation") { + Indexed( + list=rep_components, + view=|coord_str| view! { + div { (coord_str) } + } + ) + } + div(class="status") + } + } + ul(class="constraints") { + Keyed( + list=constraint_list, + view=move |cst_key| view! { + ConstraintOutlineItem( + constraint_key=cst_key, + element_key=key + ) + }, + key=|cst_key| cst_key.clone() + ) + } + } + } + } +} + // a component that lists the elements of the current assembly, showing the // constraints on each element as a collapsible sub-list. its implementation // is based on Kate Morley's HTML + CSS tree views: @@ -40,15 +176,16 @@ fn LorentzProductInput(constraint: Constraint) -> View { // #[component] pub fn Outline() -> View { - // sort the elements alphabetically by ID - let elements_sorted = create_memo(|| { - let state = use_context::(); - state.assembly.elements - .get_clone() + let state = use_context::(); + + // list the elements alphabetically by ID + let element_list = state.assembly.elements.map( + |elts| elts + .clone() .into_iter() .sorted_by_key(|(_, elt)| elt.id.clone()) .collect() - }); + ); view! { ul( @@ -59,128 +196,11 @@ pub fn Outline() -> View { } ) { Keyed( - list=elements_sorted, - view=|(key, elt)| { - let state = use_context::(); - let class = create_memo({ - move || { - if state.selection.with(|sel| sel.contains(&key)) { - "selected" - } else { - "" - } - } - }); - let label = elt.label.clone(); - let rep_components = elt.representation.iter().map(|u| { - let u_coord = u.to_string().replace("-", "\u{2212}"); - View::from(div().children(u_coord)) - }).collect::>(); - let constrained = elt.constraints.len() > 0; - let details_node = create_node_ref(); - view! { - li { - details(ref=details_node) { - summary( - class=class.get(), - on:keydown={ - move |event: KeyboardEvent| { - match event.key().as_str() { - "Enter" => { - if event.shift_key() { - state.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); - } - }); - } else { - state.selection.update(|sel| { - sel.clear(); - sel.insert(key); - }); - } - event.prevent_default(); - }, - "ArrowRight" if constrained => { - let _ = details_node - .get() - .unchecked_into::() - .set_attribute("open", ""); - }, - "ArrowLeft" => { - let _ = details_node - .get() - .unchecked_into::() - .remove_attribute("open"); - }, - _ => () - } - } - } - ) { - div( - class="elt-switch", - on:click=|event: MouseEvent| event.stop_propagation() - ) - div( - class="elt", - on:click={ - move |event: MouseEvent| { - if event.shift_key() { - state.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); - } - }); - } else { - state.selection.update(|sel| { - sel.clear(); - sel.insert(key); - }); - } - event.stop_propagation(); - event.prevent_default(); - } - } - ) { - div(class="elt-label") { (label) } - div(class="elt-rep") { (rep_components) } - } - } - ul(class="constraints") { - Keyed( - list=elt.constraints.into_iter().collect::>(), - view=move |c_key| { - let c_state = use_context::(); - let assembly = &c_state.assembly; - let cst = assembly.constraints.with(|csts| csts[c_key].clone()); - let other_arg = if cst.subjects.0 == key { - cst.subjects.1 - } else { - cst.subjects.0 - }; - let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone()); - view! { - li(class="cst") { - input(r#type="checkbox", bind:checked=cst.active) - div(class="cst-label") { (other_arg_label) } - LorentzProductInput(constraint=cst) - } - } - }, - key=|c_key| c_key.clone() - ) - } - } - } - } + list=element_list, + view=|(key, elt)| view! { + ElementOutlineItem(key=key, element=elt) }, - key=|(key, elt)| ( - key.clone(), - elt.id.clone(), - elt.label.clone(), - elt.constraints.clone() - ) + key=|(key, _)| key.clone() ) } } From e917272c60f2046567c0da48543f721b20d913e2 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Fri, 22 Nov 2024 02:25:10 +0000 Subject: [PATCH 104/126] Give each element a serial number (#22) Give each `Element` a serial number, which identifies it uniquely. The serial number is assigned by the `Element::new` constructor. Because disallows potentially unsafe global state (at least without explicit `unsafe` blocks), the next serial number is stored in a thread-safe static atomic variable (`assembly::NEXT_ELEMENT_SERIAL`), as suggested in [this StackOverflow answer](https://stackoverflow.com/a/32936288). Since the overhead for keeping track of memory ordering should be minimal, we're using the strongest available ordering: [sequentially consistent](https://marabos.nl/atomics/memory-ordering.html#seqcst). Resolves #20. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/22 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/src/assembly.rs | 25 ++++++++++++++++++++++++- app-proto/src/outline.rs | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 35b4417..59cba41 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,7 +1,7 @@ use nalgebra::{DMatrix, DVector}; use rustc_hash::FxHashMap; use slab::Slab; -use std::collections::BTreeSet; +use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ @@ -13,6 +13,13 @@ pub type ConstraintKey = usize; pub type ElementColor = [f32; 3]; +/* KLUDGE */ +// we should reconsider this design when we build a system for switching between +// assemblies. at that point, we might want to switch to hierarchical keys, +// where each each element has a key that identifies it within its assembly and +// each assembly has a key that identifies it within the sesssion +static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0); + #[derive(Clone, PartialEq)] pub struct Element { pub id: String, @@ -20,6 +27,10 @@ pub struct Element { pub color: ElementColor, pub representation: Signal>, pub constraints: 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 @@ -33,12 +44,24 @@ impl Element { color: ElementColor, representation: DVector ) -> Element { + // take the next serial number, panicking if that was the last number we + // had left. the technique we use to panic on overflow is taken from + // _Rust Atomics and Locks_, by Mara Bos + // + // https://marabos.nl/atomics/atomics.html#example-handle-overflow + // + let serial = NEXT_ELEMENT_SERIAL.fetch_update( + Ordering::SeqCst, Ordering::SeqCst, + |serial| serial.checked_add(1) + ).expect("Out of serial numbers for elements"); + Element { id: id, label: label, color: color, representation: create_signal(representation), constraints: create_signal(BTreeSet::default()), + serial: serial, column_index: 0 } } diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index ee1603f..e2cf49c 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -200,7 +200,7 @@ pub fn Outline() -> View { view=|(key, elt)| view! { ElementOutlineItem(key=key, element=elt) }, - key=|(key, _)| key.clone() + key=|(_, elt)| elt.serial ) } } From a8e13b81103acce319c14ec7a25e354fb12693bc Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Tue, 26 Nov 2024 00:32:50 +0000 Subject: [PATCH 105/126] Turn non-automated tests into Cargo examples (#24) Some of the Cargo tests on the main branch are designed to print output for human inspection, not to verify computations automatically. The incoming branch turns these tests into Cargo examples. It also makes two organizational changes in pursuit of this goal: - It introduces a dyna3 library target, which the examples use as a dependency. In the future, this target could grow into an officially maintained dyna3 library. - It puts the code for realizing the Irisawa hexlet into a new conditionally compiled `engine::irisawa` module. This code is shared by a test and an example. Compilation is controlled by the `dev` feature, which is turned on by default in development mode. I've verified that printed output of the examples hasn't changed between the head (848f7d6) and base (e917272) of the incoming branch. Co-authored-by: Aaron Fenyes Co-authored-by: Glen Whitney Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/24 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- README.md | 48 +++++ app-proto/Cargo.toml | 6 + app-proto/examples/irisawa-hexlet.rs | 25 +++ app-proto/examples/point-on-sphere.rs | 38 ++++ app-proto/examples/three-spheres.rs | 40 +++++ app-proto/run-examples | 13 +- app-proto/src/engine.rs | 245 ++++++++------------------ app-proto/src/lib.rs | 1 + 8 files changed, 241 insertions(+), 175 deletions(-) create mode 100644 app-proto/examples/irisawa-hexlet.rs create mode 100644 app-proto/examples/point-on-sphere.rs create mode 100644 app-proto/examples/three-spheres.rs create mode 100644 app-proto/src/lib.rs diff --git a/README.md b/README.md index 9ea9cbf..1ea3302 100644 --- a/README.md +++ b/README.md @@ -17,3 +17,51 @@ Note that currently this is just the barest beginnings of the project, more of a * Able to run in browser (so implemented in WASM-compatible language) * Produce scalable graphics of 3D diagrams, and maybe STL files (or other fabricatable file format) as well. + +## Prototype + +The latest prototype is in the folder `app-proto`. It includes both a user interface and a numerical constraint-solving engine. + +### Install the prerequisites + +1. Install [`rustup`](https://rust-lang.github.io/rustup/): the officially recommended Rust toolchain manager + * It's available on Ubuntu as a [Snap](https://snapcraft.io/rustup) +2. Call `rustup default stable` to "download the latest stable release of Rust and set it as your default toolchain" + * If you forget, the `rustup` [help system](https://github.com/rust-lang/rustup/blob/d9b3601c3feb2e88cf3f8ca4f7ab4fdad71441fd/src/errors.rs#L109-L112) will remind you +3. Call `rustup target add wasm32-unknown-unknown` to add the [most generic 32-bit WebAssembly target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-unknown-unknown.html) +4. Call `cargo install wasm-pack` to install the [WebAssembly toolchain](https://rustwasm.github.io/docs/wasm-pack/) +5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool +6. Add the `.cargo/bin` folder in your home directory to your executable search path + * This lets you call Trunk, and other tools installed by Cargo, without specifying their paths + * On POSIX systems, the search path is stored in the `PATH` environment variable + +### Play with the prototype + +1. Go into the `app-proto` folder +2. Call `trunk serve --release` to build and serve the prototype + * *The crates the prototype depends on will be downloaded and served automatically* + * *For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag* +3. In a web browser, visit one of the URLs listed under the message `INFO 📡 server listening at:` + * *Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype* +4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype + +### Run the engine on some example problems + +1. Go into the `app-proto` folder +2. Call `./run-examples` + * *For each example problem, the engine will print the value of the loss function at each optimization step* + * *The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then* + + ```julia + include("irisawa-hexlet.jl") + for (step, scaled_loss) in enumerate(history_alt.scaled_loss) + println(rpad(step-1, 4), " | ", scaled_loss) + end + ``` + + *you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show* + +### Run the automated tests + +1. Go into the `app-proto` folder +2. Call `cargo test` diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index e623b26..38205a7 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [features] default = ["console_error_panic_hook"] +dev = [] [dependencies] itertools = "0.13.0" @@ -36,7 +37,12 @@ features = [ 'WebGlVertexArrayObject' ] +# the self-dependency specifies features to use for tests and examples +# +# https://github.com/rust-lang/cargo/issues/2911#issuecomment-1483256987 +# [dev-dependencies] +dyna3 = { path = ".", default-features = false, features = ["dev"] } wasm-bindgen-test = "0.3.34" [profile.release] diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs new file mode 100644 index 0000000..fc14f91 --- /dev/null +++ b/app-proto/examples/irisawa-hexlet.rs @@ -0,0 +1,25 @@ +use dyna3::engine::{Q, irisawa::realize_irisawa_hexlet}; + +fn main() { + const SCALED_TOL: f64 = 1.0e-12; + let (config, success, history) = realize_irisawa_hexlet(SCALED_TOL); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + if success { + println!("\nChain diameters:"); + println!(" {} sun (given)", 1.0 / config[(3, 3)]); + for k in 4..9 { + println!(" {} sun", 1.0 / config[(3, k)]); + } + } + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } +} \ No newline at end of file diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs new file mode 100644 index 0000000..0e4765a --- /dev/null +++ b/app-proto/examples/point-on-sphere.rs @@ -0,0 +1,38 @@ +use nalgebra::DMatrix; + +use dyna3::engine::{Q, point, realize_gram, sphere, PartialMatrix}; + +fn main() { + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for j in 0..2 { + for k in j..2 { + gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + gram_to_be + }; + let guess = DMatrix::from_columns(&[ + point(0.0, 0.0, 2.0), + sphere(0.0, 0.0, 0.0, 1.0) + ]); + let frozen = [(3, 0)]; + println!(); + let (config, success, history) = realize_gram( + &gram, guess, &frozen, + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + print!("Configuration:{}", config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } +} \ No newline at end of file diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs new file mode 100644 index 0000000..d348b18 --- /dev/null +++ b/app-proto/examples/three-spheres.rs @@ -0,0 +1,40 @@ +use nalgebra::DMatrix; + +use dyna3::engine::{Q, realize_gram, sphere, PartialMatrix}; + +fn main() { + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for j in 0..3 { + for k in j..3 { + gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); + } + } + gram_to_be + }; + let guess = { + let a: f64 = 0.75_f64.sqrt(); + DMatrix::from_columns(&[ + sphere(1.0, 0.0, 0.0, 1.0), + sphere(-0.5, a, 0.0, 1.0), + sphere(-0.5, -a, 0.0, 1.0) + ]) + }; + println!(); + let (config, success, history) = realize_gram( + &gram, guess, &[], + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } +} \ No newline at end of file diff --git a/app-proto/run-examples b/app-proto/run-examples index 6a5e3ae..bc7e933 100755 --- a/app-proto/run-examples +++ b/app-proto/run-examples @@ -1,8 +1,11 @@ -# based on "Enabling print statements in Cargo tests", by Jon Almeida +#!/bin/sh + +# run all Cargo examples, as described here: # -# https://jonalmeida.com/posts/2015/01/23/print-cargo/ +# Karol Kuczmarski. "Add examples to your Rust libraries" +# http://xion.io/post/code/rust-examples.html # -cargo test -- --nocapture engine::tests::irisawa_hexlet_test -cargo test -- --nocapture engine::tests::three_spheres_example -cargo test -- --nocapture engine::tests::point_on_sphere_example +cargo run --example irisawa-hexlet +cargo run --example three-spheres +cargo run --example point-on-sphere diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 343b96e..285a9c6 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -4,7 +4,7 @@ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ // --- elements --- -#[cfg(test)] +#[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)]) } @@ -113,7 +113,7 @@ impl DescentHistory { // the Lorentz form lazy_static! { - static ref Q: DMatrix = DMatrix::from_row_slice(5, 5, &[ + pub static ref Q: DMatrix = DMatrix::from_row_slice(5, 5, &[ 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, @@ -277,12 +277,79 @@ pub fn realize_gram( // --- tests --- -#[cfg(test)] -mod tests { +// this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article +// below includes a nice translation of the problem statement, which was +// recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and +// Present_) +// +// "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki +// https://www.nippon.com/en/japan-topics/c12801/ +// +#[cfg(feature = "dev")] +pub mod irisawa { use std::{array, f64::consts::PI}; use super::*; + pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, bool, DescentHistory) { + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for s in 0..9 { + // each sphere is represented by a spacelike vector + gram_to_be.push_sym(s, s, 1.0); + + // the circumscribing sphere is tangent to all of the other + // spheres, with matching orientation + if s > 0 { + gram_to_be.push_sym(0, s, 1.0); + } + + if s > 2 { + // each chain sphere is tangent to the "sun" and "moon" + // spheres, with opposing orientation + for n in 1..3 { + gram_to_be.push_sym(s, n, -1.0); + } + + // each chain sphere is tangent to the next chain sphere, + // with opposing orientation + let s_next = 3 + (s-2) % 6; + gram_to_be.push_sym(s, s_next, -1.0); + } + } + gram_to_be + }; + + let guess = DMatrix::from_columns( + [ + sphere(0.0, 0.0, 0.0, 15.0), + sphere(0.0, 0.0, -9.0, 5.0), + sphere(0.0, 0.0, 11.0, 3.0) + ].into_iter().chain( + (1..=6).map( + |k| { + let ang = (k as f64) * PI/3.0; + sphere(9.0 * ang.cos(), 9.0 * ang.sin(), 0.0, 2.5) + } + ) + ).collect::>().as_slice() + ); + + // the frozen entries fix the radii of the circumscribing sphere, the + // "sun" and "moon" spheres, and one of the chain spheres + let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k)); + + realize_gram( + &gram, guess, &frozen, + scaled_tol, 0.5, 0.9, 1.1, 200, 110 + ) + } +} + +#[cfg(test)] +mod tests { + use super::{*, irisawa::realize_irisawa_hexlet}; + #[test] fn sub_proj_test() { let target = PartialMatrix(vec![ @@ -328,182 +395,20 @@ mod tests { assert!(state.loss.abs() < f64::EPSILON); } - // this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article - // below includes a nice translation of the problem statement, which was - // recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and - // Present_) - // - // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki - // https://www.nippon.com/en/japan-topics/c12801/ - // #[test] fn irisawa_hexlet_test() { - let gram = PartialMatrix({ - let mut entries = Vec::::new(); - for s in 0..9 { - // each sphere is represented by a spacelike vector - entries.push(MatrixEntry { index: (s, s), value: 1.0 }); - - // the circumscribing sphere is tangent to all of the other - // spheres, with matching orientation - if s > 0 { - entries.push(MatrixEntry { index: (0, s), value: 1.0 }); - entries.push(MatrixEntry { index: (s, 0), value: 1.0 }); - } - - if s > 2 { - // each chain sphere is tangent to the "sun" and "moon" - // spheres, with opposing orientation - for n in 1..3 { - entries.push(MatrixEntry { index: (s, n), value: -1.0 }); - entries.push(MatrixEntry { index: (n, s), value: -1.0 }); - } - - // each chain sphere is tangent to the next chain sphere, - // with opposing orientation - let s_next = 3 + (s-2) % 6; - entries.push(MatrixEntry { index: (s, s_next), value: -1.0 }); - entries.push(MatrixEntry { index: (s_next, s), value: -1.0 }); - } - } - entries - }); - let guess = DMatrix::from_columns( - [ - sphere(0.0, 0.0, 0.0, 15.0), - sphere(0.0, 0.0, -9.0, 5.0), - sphere(0.0, 0.0, 11.0, 3.0) - ].into_iter().chain( - (1..=6).map( - |k| { - let ang = (k as f64) * PI/3.0; - sphere(9.0 * ang.cos(), 9.0 * ang.sin(), 0.0, 2.5) - } - ) - ).collect::>().as_slice() - ); - let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k)); + // solve Irisawa's problem const SCALED_TOL: f64 = 1.0e-12; - let (config, success, history) = realize_gram( - &gram, guess, &frozen, - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 - ); + let (config, _, _) = realize_irisawa_hexlet(SCALED_TOL); + + // check against Irisawa's solution let entry_tol = SCALED_TOL.sqrt(); let solution_diams = [30.0, 10.0, 6.0, 5.0, 15.0, 10.0, 3.75, 2.5, 2.0 + 8.0/11.0]; for (k, diam) in solution_diams.into_iter().enumerate() { assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol); } - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - if success { - println!("\nChain diameters:"); - println!(" {} sun (given)", 1.0 / config[(3, 3)]); - for k in 4..9 { - println!(" {} sun", 1.0 / config[(3, k)]); - } - } - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } } - // --- process inspection examples --- - - // these tests are meant for human inspection, not automated use. run them - // one at a time in `--nocapture` mode and read through the results and - // optimization histories that they print out. the `run-examples` script - // will run all of them - - #[test] - fn three_spheres_example() { - let gram = PartialMatrix({ - let mut entries = Vec::::new(); - for j in 0..3 { - for k in 0..3 { - entries.push(MatrixEntry { - index: (j, k), - value: if j == k { 1.0 } else { -1.0 } - }); - } - } - entries - }); - let guess = { - let a: f64 = 0.75_f64.sqrt(); - DMatrix::from_columns(&[ - sphere(1.0, 0.0, 0.0, 1.0), - sphere(-0.5, a, 0.0, 1.0), - sphere(-0.5, -a, 0.0, 1.0) - ]) - }; - println!(); - let (config, success, history) = realize_gram( - &gram, guess, &[], - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 - ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } - } - - #[test] - fn point_on_sphere_example() { - let gram = PartialMatrix({ - let mut entries = Vec::::new(); - for j in 0..2 { - for k in 0..2 { - entries.push(MatrixEntry { - index: (j, k), - value: if (j, k) == (1, 1) { 1.0 } else { 0.0 } - }); - } - } - entries - }); - let guess = DMatrix::from_columns(&[ - point(0.0, 0.0, 2.0), - sphere(0.0, 0.0, 0.0, 1.0) - ]); - let frozen = [(3, 0)]; - println!(); - let (config, success, history) = realize_gram( - &gram, guess, &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 - ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - print!("Configuration:{}", config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } - } - - /* TO DO */ - // --- new test placed here to avoid merge conflict --- - // at the frozen indices, the optimization steps should have exact zeros, // and the realized configuration should match the initial guess #[test] diff --git a/app-proto/src/lib.rs b/app-proto/src/lib.rs new file mode 100644 index 0000000..0d9bc4a --- /dev/null +++ b/app-proto/src/lib.rs @@ -0,0 +1 @@ +pub mod engine; \ No newline at end of file From b490c8707fd506c5d819692976bcbccd17127a9c Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Wed, 27 Nov 2024 05:02:06 +0000 Subject: [PATCH 106/126] Click the display to select spheres (#25) On the incoming branch, you can select a sphere by clicking it in the display. Holding *shift* while clicking enables multiple selection. These controls match the ones already implemented in the outline view. Since the selection routine is now used in multiple places, the incoming branch factors it out into the `AppState::select` method. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/25 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.toml | 1 + app-proto/src/assembly.rs | 45 ++++++++++++++++++++++++++++++- app-proto/src/display.rs | 57 ++++++++++++++++++++++++++++++++++++--- app-proto/src/main.rs | 18 +++++++++++++ app-proto/src/outline.rs | 13 +-------- 5 files changed, 118 insertions(+), 16 deletions(-) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 38205a7..c11fef4 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -26,6 +26,7 @@ console_error_panic_hook = { version = "0.1.7", optional = true } [dependencies.web-sys] version = "0.3.69" features = [ + 'DomRect', 'HtmlCanvasElement', 'HtmlInputElement', 'Performance', diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 59cba41..fb5bbf7 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,4 +1,4 @@ -use nalgebra::{DMatrix, DVector}; +use nalgebra::{DMatrix, DVector, Vector3}; use rustc_hash::FxHashMap; use slab::Slab; use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; @@ -65,6 +65,49 @@ impl Element { column_index: 0 } } + + // 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 + } + } } diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index ee0af47..c39e575 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -4,7 +4,9 @@ use sycamore::{prelude::*, motion::create_raf}; use web_sys::{ console, window, + Element, KeyboardEvent, + MouseEvent, WebGl2RenderingContext, WebGlProgram, WebGlShader, @@ -12,7 +14,7 @@ use web_sys::{ wasm_bindgen::{JsCast, JsValue} }; -use crate::AppState; +use crate::{AppState, assembly::ElementKey}; fn compile_shader( context: &WebGl2RenderingContext, @@ -82,6 +84,24 @@ fn bind_vertex_attrib( ); } +// 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 rect = target.get_bounding_client_rect(); + let width = rect.width(); + let height = rect.height(); + let shortdim = width.min(height); + + // this constant should be kept synchronized with `inversive.frag` + 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 + ) +} + #[component] pub fn Display() -> View { let state = use_context::(); @@ -89,6 +109,9 @@ pub fn Display() -> View { // canvas let display = create_node_ref(); + // viewpoint + let assembly_to_world = create_signal(DMatrix::::identity(5, 5)); + // navigation let pitch_up = create_signal(0.0); let pitch_down = create_signal(0.0); @@ -296,7 +319,7 @@ pub fn Display() -> View { 0.0, 0.0, 0.0, 0.0, 1.0 ]) }; - let assembly_to_world = &location * &orientation; + let asm_to_world = &location * &orientation; // get the assembly let ( @@ -311,7 +334,7 @@ pub fn Display() -> View { // representation vectors in world coordinates elts.iter().map( - |(_, elt)| elt.representation.with(|rep| &assembly_to_world * rep) + |(_, elt)| elt.representation.with(|rep| &asm_to_world * rep) ).collect::>(), // colors @@ -370,6 +393,9 @@ pub fn Display() -> View { // draw the scene ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); + // update the viewpoint + assembly_to_world.set(asm_to_world); + // clear the scene change flag scene_changed.set( pitch_up_val != 0.0 @@ -458,6 +484,31 @@ pub fn Display() -> View { yaw_left.set(0.0); roll_ccw.set(0.0); roll_cw.set(0.0); + }, + on:click=move |event: MouseEvent| { + // find the nearest element along the pointer direction + let dir = 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)) { + Some(depth) => match clicked { + Some((_, best_depth)) => { + if depth < best_depth { + clicked = Some((key, depth)) + } + }, + None => clicked = Some((key, depth)) + } + None => () + }; + } + + // if we clicked something, select it + match clicked { + Some((key, _)) => state.select(key, event.shift_key()), + None => state.selection.update(|sel| sel.clear()) + }; } ) } diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index 897f9d4..8a012d3 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -25,6 +25,24 @@ impl AppState { selection: create_signal(FxHashSet::default()) } } + + // in single-selection mode, select the element with the given key. in + // multiple-selection mode, toggle whether the element with the given key + // is selected + fn select(&self, key: ElementKey, multi: bool) { + if multi { + self.selection.update(|sel| { + if !sel.remove(&key) { + sel.insert(key); + } + }); + } else { + self.selection.update(|sel| { + sel.clear(); + sel.insert(key); + }); + } + } } fn main() { diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index e2cf49c..148f870 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -83,18 +83,7 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { move |event: KeyboardEvent| { match event.key().as_str() { "Enter" => { - if event.shift_key() { - state.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); - } - }); - } else { - state.selection.update(|sel| { - sel.clear(); - sel.insert(key); - }); - } + state.select(key, event.shift_key()); event.prevent_default(); }, "ArrowRight" if constrained.get() => { From 22870342f3370197fce91aa47ece13558a1b5dee Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 30 Dec 2024 22:53:07 +0000 Subject: [PATCH 107/126] Manipulate the assembly (#29) feat: Find tangent space of solution variety, use for perturbations ### Tangent space #### Implementation The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`. At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution. After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option` and enforce the following invariants: 1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices. 2. If an element is affected by a constraint, it has a column index. The comments in `assembly.rs` state the invariants and describe how they're enforced. #### Automated testing The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis. #### Limitations The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually. ### Deformation #### Implementation The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk. For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop. The function `Assembly::deform` works like this: 1. Take a list of element motions 2. Project them onto the tangent space of the solution variety 3. Sum them to get a deformation $v$ of the whole assembly 4. Step the assembly along the "mass shell" geodesic tangent to $v$ * This step stays on the solution variety to first order 5. Call `realize` to bring the assembly back onto the solution variety #### Manual testing To manipulate the assembly: 1. Select a sphere 2. Make sure the display has focus 3. Hold the following keys: * **A**/**D** for $x$ translation * **W**/**S** for $y$ translation * **shift**+**W**/**S** for $z$ translation #### Limitations Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus. Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere. When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console. During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]* Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/examples/irisawa-hexlet.rs | 2 +- app-proto/examples/point-on-sphere.rs | 2 +- app-proto/examples/three-spheres.rs | 2 +- app-proto/src/assembly.rs | 141 +++++++++++++++++++++-- app-proto/src/display.rs | 98 +++++++++++++++- app-proto/src/engine.rs | 159 +++++++++++++++++++++++--- app-proto/src/main.rs | 4 + 7 files changed, 374 insertions(+), 34 deletions(-) diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs index fc14f91..2bc94c0 100644 --- a/app-proto/examples/irisawa-hexlet.rs +++ b/app-proto/examples/irisawa-hexlet.rs @@ -2,7 +2,7 @@ use dyna3::engine::{Q, irisawa::realize_irisawa_hexlet}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let (config, success, history) = realize_irisawa_hexlet(SCALED_TOL); + let (config, _, success, history) = realize_irisawa_hexlet(SCALED_TOL); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); if success { println!("Target accuracy achieved!"); diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 0e4765a..13040e5 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -18,7 +18,7 @@ fn main() { ]); let frozen = [(3, 0)]; println!(); - let (config, success, history) = realize_gram( + let (config, _, success, history) = realize_gram( &gram, guess, &frozen, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index d348b18..19acfd1 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -21,7 +21,7 @@ fn main() { ]) }; println!(); - let (config, success, history) = realize_gram( + let (config, _, success, history) = realize_gram( &gram, guess, &[], 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index fb5bbf7..278a8f9 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,11 +1,11 @@ -use nalgebra::{DMatrix, DVector, Vector3}; +use nalgebra::{DMatrix, DVector, DVectorView, Vector3}; use rustc_hash::FxHashMap; use slab::Slab; use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ -use crate::engine::{realize_gram, PartialMatrix}; +use crate::engine::{realize_gram, ConfigSubspace, PartialMatrix, Q}; // the types of the keys we use to access an assembly's elements and constraints pub type ElementKey = usize; @@ -33,8 +33,9 @@ pub struct Element { pub serial: u64, // the configuration matrix column index that was assigned to this element - // last time the assembly was realized - column_index: usize + // last time the assembly was realized, or `None` if the element has never + // been through a realization + column_index: Option } impl Element { @@ -62,7 +63,7 @@ impl Element { representation: create_signal(representation), constraints: create_signal(BTreeSet::default()), serial: serial, - column_index: 0 + column_index: None } } @@ -109,7 +110,6 @@ impl Element { } } } - #[derive(Clone)] pub struct Constraint { @@ -120,6 +120,13 @@ pub struct Constraint { pub active: Signal } +pub struct ElementMotion<'a> { + pub key: ElementKey, + pub velocity: DVectorView<'a, f64> +} + +type AssemblyMotion<'a> = Vec>; + // a complete, view-independent description of an assembly #[derive(Clone)] pub struct Assembly { @@ -127,6 +134,18 @@ pub struct Assembly { pub elements: Signal>, pub constraints: Signal>, + // solution variety tangent space. the basis vectors are stored in + // configuration matrix format, ordered according to the elements' column + // indices. when you realize the assembly, every element that's present + // during realization gets a column index and is reflected in the tangent + // space. since the methods in this module never assign column indices + // without later realizing the assembly, we get the following invariant: + // + // (1) if an element has a column index, its tangent motions can be found + // in that column of the tangent space basis matrices + // + pub tangent: Signal, + // indexing pub elements_by_id: Signal> } @@ -136,6 +155,7 @@ impl Assembly { Assembly { elements: create_signal(Slab::new()), constraints: create_signal(Slab::new()), + tangent: create_signal(ConfigSubspace::zero(0)), elements_by_id: create_signal(FxHashMap::default()) } } @@ -199,7 +219,7 @@ impl Assembly { // index the elements self.elements.update_silent(|elts| { for (index, (_, elt)) in elts.into_iter().enumerate() { - elt.column_index = index; + elt.column_index = Some(index); } }); @@ -211,8 +231,8 @@ impl Assembly { for (_, cst) in csts { if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() { let subjects = cst.subjects; - let row = elts[subjects.0].column_index; - let col = elts[subjects.1].column_index; + let row = elts[subjects.0].column_index.unwrap(); + let col = elts[subjects.1].column_index.unwrap(); gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked()); } } @@ -222,7 +242,7 @@ impl Assembly { // Gram matrix let mut guess_to_be = DMatrix::::zeros(5, elts.len()); for (_, elt) in elts { - let index = elt.column_index; + let index = elt.column_index.unwrap(); gram_to_be.push_sym(index, index, 1.0); guess_to_be.set_column(index, &elt.representation.get_clone_untracked()); } @@ -247,7 +267,7 @@ impl Assembly { } // look for a configuration with the given Gram matrix - let (config, success, history) = realize_gram( + let (config, tangent, success, history) = realize_gram( &gram, guess, &[], 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); @@ -263,14 +283,111 @@ impl Assembly { )); console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1)); console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap())); + console::log_2(&JsValue::from("Tangent dimension:"), &JsValue::from(tangent.dim())); if success { // 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)) + |rep| rep.set_column(0, &config.column(elt.column_index.unwrap())) ); } + + // save the tangent space + self.tangent.set_silent(tangent); } } + + // --- deformation --- + + // project the given motion to the tangent space of the solution variety and + // move the assembly along it. the implementation is based on invariant (1) + // from above and the following additional invariant: + // + // (2) if an element is affected by a constraint, it has a column index + // + // we have this invariant because the assembly gets realized each time you + // add a constraint + pub fn deform(&self, motion: AssemblyMotion) { + /* KLUDGE */ + // when the tangent space is zero, deformation won't do anything, but + // the attempt to deform should be registered in the UI. this console + // message will do for now + if self.tangent.with(|tan| tan.dim() <= 0 && tan.assembly_dim() > 0) { + console::log_1(&JsValue::from("The assembly is rigid")); + } + + // give a column index to each moving element that doesn't have one yet. + // this temporarily breaks invariant (1), but the invariant will be + // restored when we realize the assembly at the end of the deformation. + // in the process, we find out how many matrix columns we'll need to + // hold the deformation + let realized_dim = self.tangent.with(|tan| tan.assembly_dim()); + let motion_dim = self.elements.update_silent(|elts| { + let 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); + next_column_index += 1; + } + } + next_column_index + }); + + // project the element motions onto the tangent space of the solution + // variety and sum them to get a deformation of the whole assembly. the + // matrix `motion_proj` that holds the deformation has extra columns for + // any moving elements that aren't reflected in the saved tangent space + const ELEMENT_DIM: usize = 5; + let mut motion_proj = DMatrix::zeros(ELEMENT_DIM, motion_dim); + for elt_motion in motion { + // we can unwrap the column index because we know that every moving + // element has one at this point + let column_index = self.elements.with_untracked( + |elts| elts[elt_motion.key].column_index.unwrap() + ); + + if column_index < realized_dim { + // this element had a column index when we started, so by + // invariant (1), it's reflected in the tangent space + let mut target_columns = motion_proj.columns_mut(0, realized_dim); + target_columns += self.tangent.with( + |tan| tan.proj(&elt_motion.velocity, column_index) + ); + } else { + // this element didn't have a column index when we started, so + // by invariant (2), it's unconstrained + let mut target_column = motion_proj.column_mut(column_index); + target_column += elt_motion.velocity; + } + } + + // step each element along the mass shell geodesic that matches its + // velocity in the deformation found above + /* KLUDGE */ + // 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 { + Some(column_index) => { + let rep_next = &*rep + motion_proj.column(column_index); + let normalizer = rep_next.dot(&(&*Q * &rep_next)); + rep.set_column(0, &(rep_next / normalizer)); + }, + None => { + console::log_1(&JsValue::from( + format!("No velocity to unpack for fresh element \"{}\"", elt.id) + )) + } + }; + }); + } + + // bring the configuration back onto the solution variety. this also + // gets the elements' column indices and the saved tangent space back in + // sync + self.realize(); + } } \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index c39e575..d8ee23c 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -1,5 +1,5 @@ use core::array; -use nalgebra::{DMatrix, Rotation3, Vector3}; +use nalgebra::{DMatrix, DVector, Rotation3, Vector3}; use sycamore::{prelude::*, motion::create_raf}; use web_sys::{ console, @@ -14,7 +14,7 @@ use web_sys::{ wasm_bindgen::{JsCast, JsValue} }; -use crate::{AppState, assembly::ElementKey}; +use crate::{AppState, assembly::{ElementKey, ElementMotion}}; fn compile_shader( context: &WebGl2RenderingContext, @@ -123,6 +123,14 @@ pub fn Display() -> View { let zoom_out = create_signal(0.0); let turntable = create_signal(false); /* BENCHMARKING */ + // manipulation + let translate_neg_x = create_signal(0.0); + let translate_pos_x = create_signal(0.0); + let translate_neg_y = create_signal(0.0); + let translate_pos_y = create_signal(0.0); + let translate_neg_z = create_signal(0.0); + let translate_pos_z = create_signal(0.0); + // change listener let scene_changed = create_signal(true); create_effect(move || { @@ -141,6 +149,7 @@ pub fn Display() -> View { let mut frames_since_last_sample = 0; let mean_frame_interval = create_signal(0.0); + let assembly_for_raf = state.assembly.clone(); on_mount(move || { // timing let mut last_time = 0.0; @@ -153,6 +162,9 @@ pub fn Display() -> View { let mut rotation = DMatrix::::identity(5, 5); let mut location_z: f64 = 5.0; + // manipulation + const TRANSLATION_SPEED: f64 = 0.15; // in length units per second + // display parameters const OPACITY: f32 = 0.5; /* SCAFFOLDING */ const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ @@ -273,6 +285,14 @@ pub fn Display() -> View { let zoom_out_val = zoom_out.get(); let turntable_val = turntable.get(); /* BENCHMARKING */ + // get the manipulation state + let translate_neg_x_val = translate_neg_x.get(); + let translate_pos_x_val = translate_pos_x.get(); + let translate_neg_y_val = translate_neg_y.get(); + let translate_pos_y_val = translate_pos_y.get(); + let translate_neg_z_val = translate_neg_z.get(); + let translate_pos_z_val = translate_pos_z.get(); + // update the assembly's orientation let ang_vel = { let pitch = pitch_up_val - pitch_down_val; @@ -298,6 +318,41 @@ pub fn Display() -> View { let zoom = zoom_out_val - zoom_in_val; location_z *= (time_step * ZOOM_SPEED * zoom).exp(); + // manipulate the assembly + if state.selection.with(|sel| sel.len() == 1) { + let sel = state.selection.with( + |sel| *sel.into_iter().next().unwrap() + ); + let rep = state.assembly.elements.with_untracked( + |elts| elts[sel].representation.get_clone_untracked() + ); + let translate_x = translate_pos_x_val - translate_neg_x_val; + let translate_y = translate_pos_y_val - translate_neg_y_val; + let translate_z = translate_pos_z_val - translate_neg_z_val; + if translate_x != 0.0 || translate_y != 0.0 || translate_z != 0.0 { + let vel_field = { + let u = Vector3::new(translate_x, translate_y, translate_z).normalize(); + DMatrix::from_column_slice(5, 5, &[ + 0.0, 0.0, 0.0, 0.0, u[0], + 0.0, 0.0, 0.0, 0.0, u[1], + 0.0, 0.0, 0.0, 0.0, u[2], + 2.0*u[0], 2.0*u[1], 2.0*u[2], 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0 + ]) + }; + let elt_motion: DVector = time_step * TRANSLATION_SPEED * vel_field * rep; + assembly_for_raf.deform( + vec![ + ElementMotion { + key: sel, + velocity: elt_motion.as_view() + } + ] + ); + scene_changed.set(true); + } + } + if scene_changed.get() { /* INSTRUMENTS */ // measure mean frame interval @@ -416,7 +471,7 @@ pub fn Display() -> View { start_animation_loop(); }); - let set_nav_signal = move |event: KeyboardEvent, value: f64| { + let set_nav_signal = move |event: &KeyboardEvent, value: f64| { let mut navigating = true; let shift = event.shift_key(); match event.key().as_str() { @@ -436,6 +491,23 @@ pub fn Display() -> View { } }; + let set_manip_signal = move |event: &KeyboardEvent, value: f64| { + let mut manipulating = true; + let shift = event.shift_key(); + match event.key().as_str() { + "d" | "D" => translate_pos_x.set(value), + "a" | "A" => translate_neg_x.set(value), + "w" | "W" if shift => translate_neg_z.set(value), + "s" | "S" if shift => translate_pos_z.set(value), + "w" | "W" => translate_pos_y.set(value), + "s" | "S" => translate_neg_y.set(value), + _ => manipulating = false + }; + if manipulating { + event.prevent_default(); + } + }; + view! { /* TO DO */ // switch back to integer-valued parameters when that becomes possible @@ -447,6 +519,7 @@ pub fn Display() -> View { tabindex="0", on:keydown=move |event: KeyboardEvent| { if event.key() == "Shift" { + // swap navigation inputs roll_cw.set(yaw_right.get()); roll_ccw.set(yaw_left.get()); zoom_in.set(pitch_up.get()); @@ -455,16 +528,24 @@ pub fn Display() -> View { yaw_left.set(0.0); pitch_up.set(0.0); pitch_down.set(0.0); + + // swap manipulation inputs + translate_pos_z.set(translate_neg_y.get()); + translate_neg_z.set(translate_pos_y.get()); + translate_pos_y.set(0.0); + translate_neg_y.set(0.0); } else { if event.key() == "Enter" { /* BENCHMARKING */ turntable.set_fn(|turn| !turn); scene_changed.set(true); } - set_nav_signal(event, 1.0); + set_nav_signal(&event, 1.0); + set_manip_signal(&event, 1.0); } }, on:keyup=move |event: KeyboardEvent| { if event.key() == "Shift" { + // swap navigation inputs yaw_right.set(roll_cw.get()); yaw_left.set(roll_ccw.get()); pitch_up.set(zoom_in.get()); @@ -473,8 +554,15 @@ pub fn Display() -> View { roll_ccw.set(0.0); zoom_in.set(0.0); zoom_out.set(0.0); + + // swap manipulation inputs + translate_pos_y.set(translate_neg_z.get()); + translate_neg_y.set(translate_pos_z.get()); + translate_pos_z.set(0.0); + translate_neg_z.set(0.0); } else { - set_nav_signal(event, 0.0); + set_nav_signal(&event, 0.0); + set_manip_signal(&event, 0.0); } }, on:blur=move |_| { diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 285a9c6..6a0202e 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -1,5 +1,5 @@ use lazy_static::lazy_static; -use nalgebra::{Const, DMatrix, DVector, Dyn}; +use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen}; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ // --- elements --- @@ -85,6 +85,75 @@ impl PartialMatrix { } } +// --- configuration subspaces --- + +#[derive(Clone)] +pub struct ConfigSubspace { + assembly_dim: usize, + basis: Vec> +} + +impl ConfigSubspace { + pub fn zero(assembly_dim: usize) -> ConfigSubspace { + ConfigSubspace { + assembly_dim: assembly_dim, + basis: Vec::new() + } + } + + // approximate the kernel of a symmetric endomorphism of the configuration + // space for `assembly_dim` elements. we consider an eigenvector to be part + // of the kernel if its eigenvalue is smaller than the constant `THRESHOLD` + fn symmetric_kernel(a: DMatrix, assembly_dim: usize) -> ConfigSubspace { + const ELEMENT_DIM: usize = 5; + const THRESHOLD: f64 = 1.0e-4; + let eig = SymmetricEigen::new(a); + let eig_vecs = eig.eigenvectors.column_iter(); + let eig_pairs = eig.eigenvalues.iter().zip(eig_vecs); + let basis = eig_pairs.filter_map( + |(λ, v)| (λ.abs() < THRESHOLD).then_some( + Into::>::into( + v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim)) + ) + ) + ); + + /* DEBUG */ + // print the eigenvalues + #[cfg(all(target_family = "wasm", target_os = "unknown"))] + console::log_1(&JsValue::from( + format!("Eigenvalues used to find kernel:{}", eig.eigenvalues) + )); + + ConfigSubspace { + assembly_dim: assembly_dim, + basis: basis.collect() + } + } + + pub fn dim(&self) -> usize { + self.basis.len() + } + + pub fn assembly_dim(&self) -> usize { + self.assembly_dim + } + + // find the projection onto this subspace, with respect to the Euclidean + // inner product, of the motion where the element with the given column + // index has velocity `v` + pub fn proj(&self, v: &DVectorView, column_index: usize) -> DMatrix { + if self.dim() == 0 { + const ELEMENT_DIM: usize = 5; + DMatrix::zeros(ELEMENT_DIM, self.assembly_dim) + } else { + self.basis.iter().map( + |b| b.column(column_index).dot(&v) * b + ).sum() + } + } +} + // --- descent history --- pub struct DescentHistory { @@ -181,7 +250,7 @@ pub fn realize_gram( reg_scale: f64, max_descent_steps: i32, max_backoff_steps: i32 -) -> (DMatrix, bool, DescentHistory) { +) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { // start the descent history let mut history = DescentHistory::new(); @@ -201,12 +270,8 @@ pub fn realize_gram( // use Newton's method with backtracking and gradient descent backup let mut state = SearchState::from_config(gram, guess); + let mut hess = DMatrix::zeros(element_dim, assembly_dim); for _ in 0..max_descent_steps { - // stop if the loss is tolerably low - history.config.push(state.config.clone()); - history.scaled_loss.push(state.loss / scale_adjustment); - if state.loss < tol { break; } - // find the negative gradient of the loss function let neg_grad = 4.0 * &*Q * &state.config * &state.err_proj; let mut neg_grad_stacked = neg_grad.clone().reshape_generic(Dyn(total_dim), Const::<1>); @@ -229,7 +294,7 @@ pub fn realize_gram( hess_cols.push(deriv_grad.reshape_generic(Dyn(total_dim), Const::<1>)); } } - let mut hess = DMatrix::from_columns(hess_cols.as_slice()); + hess = DMatrix::from_columns(hess_cols.as_slice()); // regularize the Hessian let min_eigval = hess.symmetric_eigenvalues().min(); @@ -249,6 +314,11 @@ pub fn realize_gram( hess[(k, k)] = 1.0; } + // stop if the loss is tolerably low + history.config.push(state.config.clone()); + history.scaled_loss.push(state.loss / scale_adjustment); + if state.loss < tol { break; } + // compute the Newton step /* we need to either handle or eliminate the case where the minimum @@ -256,7 +326,7 @@ pub fn realize_gram( singular. right now, this causes the Cholesky decomposition to return `None`, leading to a panic when we unrap */ - let base_step_stacked = hess.cholesky().unwrap().solve(&neg_grad_stacked); + let base_step_stacked = hess.clone().cholesky().unwrap().solve(&neg_grad_stacked); let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); history.base_step.push(base_step.clone()); @@ -269,10 +339,16 @@ pub fn realize_gram( state = better_state; history.backoff_steps.push(backoff_steps); }, - None => return (state.config, false, history) + None => return (state.config, ConfigSubspace::zero(assembly_dim), false, history) }; } - (state.config, state.loss < tol, history) + let success = state.loss < tol; + let tangent = if success { + ConfigSubspace::symmetric_kernel(hess, assembly_dim) + } else { + ConfigSubspace::zero(assembly_dim) + }; + (state.config, tangent, success, history) } // --- tests --- @@ -291,7 +367,7 @@ pub mod irisawa { use super::*; - pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, bool, DescentHistory) { + pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { let gram = { let mut gram_to_be = PartialMatrix::new(); for s in 0..9 { @@ -399,7 +475,7 @@ mod tests { fn irisawa_hexlet_test() { // solve Irisawa's problem const SCALED_TOL: f64 = 1.0e-12; - let (config, _, _) = realize_irisawa_hexlet(SCALED_TOL); + let (config, _, _, _) = realize_irisawa_hexlet(SCALED_TOL); // check against Irisawa's solution let entry_tol = SCALED_TOL.sqrt(); @@ -409,6 +485,61 @@ mod tests { } } + #[test] + fn tangent_test() { + const SCALED_TOL: f64 = 1.0e-12; + const ELEMENT_DIM: usize = 5; + const ASSEMBLY_DIM: usize = 3; + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for j in 0..3 { + for k in j..3 { + gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); + } + } + gram_to_be + }; + let guess = DMatrix::from_columns(&[ + sphere(0.0, 0.0, 0.0, -2.0), + sphere(0.0, 0.0, 1.0, 1.0), + sphere(0.0, 0.0, -1.0, 1.0) + ]); + let frozen: [_; 5] = std::array::from_fn(|k| (k, 0)); + let (config, tangent, success, history) = realize_gram( + &gram, guess.clone(), &frozen, + SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + ); + assert_eq!(config, guess); + assert_eq!(success, true); + assert_eq!(history.scaled_loss.len(), 1); + + // confirm that the tangent space has dimension five or less + let ConfigSubspace(ref tangent_basis) = tangent; + assert_eq!(tangent_basis.len(), 5); + + // confirm that the tangent space contains all the motions we expect it + // to. since we've already bounded the dimension of the tangent space, + // this confirms that the tangent space is what we expect it to be + let tangent_motions = vec![ + basis_matrix((0, 1), ELEMENT_DIM, ASSEMBLY_DIM), + basis_matrix((1, 1), ELEMENT_DIM, ASSEMBLY_DIM), + basis_matrix((0, 2), ELEMENT_DIM, ASSEMBLY_DIM), + basis_matrix((1, 2), ELEMENT_DIM, ASSEMBLY_DIM), + DMatrix::::from_column_slice(ELEMENT_DIM, 3, &[ + 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, -1.0, -0.25, -1.0, + 0.0, 0.0, -1.0, 0.25, 1.0 + ]) + ]; + let tol_sq = ((ELEMENT_DIM * ASSEMBLY_DIM) as f64) * SCALED_TOL * SCALED_TOL; + for motion in tangent_motions { + let motion_proj: DMatrix<_> = motion.column_iter().enumerate().map( + |(k, v)| tangent.proj(&v, k) + ).sum(); + assert!((motion - motion_proj).norm_squared() < tol_sq); + } + } + // at the frozen indices, the optimization steps should have exact zeros, // and the realized configuration should match the initial guess #[test] @@ -428,7 +559,7 @@ mod tests { ]); let frozen = [(3, 0), (3, 1)]; println!(); - let (config, success, history) = realize_gram( + let (config, _, success, history) = realize_gram( &gram, guess.clone(), &frozen, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index 8a012d3..f961504 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -46,6 +46,10 @@ impl AppState { } fn main() { + // set the console error panic hook + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); + sycamore::render(|| { provide_context(AppState::new()); From 817a446fad0c7b04c9a43c5bafd09ba2a3e68483 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Fri, 31 Jan 2025 19:34:33 +0000 Subject: [PATCH 108/126] Switch to Euclidean-invariant projection onto tangent space of solution variety (#34) This pull request addresses issues #32 and #33 by projecting nudges onto the tangent space of the solution variety using a Euclidean-invariant inner product, which I'm calling the *uniform* inner product. ### Definition of the uniform inner product For spheres and planes, the uniform inner product is defined on the tangent space of the hyperboloid $\langle v, v \rangle = 1$. For points, it's defined on the tangent space of the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$. The tangent space of an assembly can be expressed as the direct sum of the tangent spaces of the elements. We extend the uniform inner product to assemblies by declaring the tangent spaces of different elements to be orthogonal. #### For spheres and planes If $v = [x, y, z, b, c]^\top$ is on the hyperboloid $\langle v, v \rangle = 1$, the vectors $$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right],\;\left[ \begin{array}{l} 2bx \\ 2by \\ 2bz \\ 2b^2 \\ 2bc + 1 \end{array} \right]$$ form a basis for the tangent space of hyperboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product. The first three vectors in the basis are unit-speed translations along the coordinate axes. The last vector moves the surface at unit speed along its normal field. For spheres, this increases the radius at unit rate. For planes, this translates the plane parallel to itself at unit speed. This description makes it clear that the uniform inner product is invariant under Euclidean motions. #### For points If $v = [x, y, z, b, c]^\top$ is on the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$, the vectors $$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right]$$ form a basis for the tangent space of paraboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product. The meanings of the basis vectors, and the argument that the uniform inner product is Euclidean-invariant, are the same as for spheres and planes. In the engine, we pad the basis with $[0, 0, 0, 0, 1]^\top$ to keep the number of uniform coordinates consistent across element types. ### Confirmation of intended behavior Two new tests confirm that we've corrected the misbehaviors described in issues #32 and #33. Issue | Test ---|--- #32 | `proj_equivar_test` #33 | `tangent_test_kaleidocycle` Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/34 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/examples/kaleidocycle.rs | 72 ++++++ app-proto/run-examples | 1 + app-proto/src/assembly.rs | 12 +- app-proto/src/display.rs | 38 +-- app-proto/src/engine.rs | 363 ++++++++++++++++++++++++++--- 5 files changed, 432 insertions(+), 54 deletions(-) create mode 100644 app-proto/examples/kaleidocycle.rs diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs new file mode 100644 index 0000000..88116d3 --- /dev/null +++ b/app-proto/examples/kaleidocycle.rs @@ -0,0 +1,72 @@ +use nalgebra::{DMatrix, DVector}; +use std::{array, f64::consts::PI}; + +use dyna3::engine::{Q, point, realize_gram, PartialMatrix}; + +fn main() { + // set up a kaleidocycle, made of points with fixed distances between them, + // and find its tangent space + const N_POINTS: usize = 12; + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for block in (0..N_POINTS).step_by(2) { + let block_next = (block + 2) % N_POINTS; + for j in 0..2 { + // diagonal and hinge edges + for k in j..2 { + gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); + } + + // non-hinge edges + for k in 0..2 { + gram_to_be.push_sym(block + j, block_next + k, -0.625); + } + } + } + gram_to_be + }; + let guess = { + const N_HINGES: usize = 6; + let guess_elts = (0..N_HINGES).step_by(2).flat_map( + |n| { + let ang_hor = (n as f64) * PI/3.0; + let ang_vert = ((n + 1) as f64) * PI/3.0; + let x_vert = ang_vert.cos(); + let y_vert = ang_vert.sin(); + [ + point(0.0, 0.0, 0.0), + point(ang_hor.cos(), ang_hor.sin(), 0.0), + point(x_vert, y_vert, -0.5), + point(x_vert, y_vert, 0.5) + ] + } + ).collect::>(); + DMatrix::from_columns(&guess_elts) + }; + let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); + let (config, tangent, success, history) = realize_gram( + &gram, guess, &frozen, + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config); + print!("Configuration:{}", config); + if success { + println!("Target accuracy achieved!"); + } else { + println!("Failed to reach target accuracy"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}\n", history.scaled_loss.last().unwrap()); + + // find the kaleidocycle's twist motion + let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); + let down = -&up; + let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( + |n| [ + tangent.proj(&up.as_view(), n), + tangent.proj(&down.as_view(), n+1) + ] + ).sum(); + let normalization = 5.0 / twist_motion[(2, 0)]; + print!("Twist motion:{}", normalization * twist_motion); +} \ No newline at end of file diff --git a/app-proto/run-examples b/app-proto/run-examples index bc7e933..52173b0 100755 --- a/app-proto/run-examples +++ b/app-proto/run-examples @@ -9,3 +9,4 @@ cargo run --example irisawa-hexlet cargo run --example three-spheres cargo run --example point-on-sphere +cargo run --example kaleidocycle \ No newline at end of file diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 278a8f9..9b0e065 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -5,7 +5,7 @@ use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ -use crate::engine::{realize_gram, ConfigSubspace, PartialMatrix, Q}; +use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix, Q}; // the types of the keys we use to access an assembly's elements and constraints pub type ElementKey = usize; @@ -120,6 +120,7 @@ pub struct Constraint { pub active: Signal } +// the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub key: ElementKey, pub velocity: DVectorView<'a, f64> @@ -359,7 +360,14 @@ impl Assembly { // this element didn't have a column index when we started, so // by invariant (2), it's unconstrained let mut target_column = motion_proj.column_mut(column_index); - target_column += elt_motion.velocity; + let unif_to_std = self.elements.with_untracked( + |elts| { + elts[elt_motion.key].representation.with_untracked( + |rep| local_unif_to_std(rep.as_view()) + ) + } + ); + target_column += unif_to_std * elt_motion.velocity; } } diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index d8ee23c..4e0c7e4 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -130,6 +130,8 @@ pub fn Display() -> View { let translate_pos_y = create_signal(0.0); let translate_neg_z = create_signal(0.0); let translate_pos_z = create_signal(0.0); + let shrink_neg = create_signal(0.0); + let shrink_pos = create_signal(0.0); // change listener let scene_changed = create_signal(true); @@ -164,6 +166,7 @@ pub fn Display() -> View { // manipulation const TRANSLATION_SPEED: f64 = 0.15; // in length units per second + const SHRINKING_SPEED: f64 = 0.15; // in length units per second // display parameters const OPACITY: f32 = 0.5; /* SCAFFOLDING */ @@ -292,6 +295,8 @@ pub fn Display() -> View { let translate_pos_y_val = translate_pos_y.get(); let translate_neg_z_val = translate_neg_z.get(); let translate_pos_z_val = translate_pos_z.get(); + let shrink_neg_val = shrink_neg.get(); + let shrink_pos_val = shrink_pos.get(); // update the assembly's orientation let ang_vel = { @@ -323,24 +328,27 @@ pub fn Display() -> View { let sel = state.selection.with( |sel| *sel.into_iter().next().unwrap() ); - let rep = state.assembly.elements.with_untracked( - |elts| elts[sel].representation.get_clone_untracked() - ); let translate_x = translate_pos_x_val - translate_neg_x_val; let translate_y = translate_pos_y_val - translate_neg_y_val; let translate_z = translate_pos_z_val - translate_neg_z_val; - if translate_x != 0.0 || translate_y != 0.0 || translate_z != 0.0 { - let vel_field = { - let u = Vector3::new(translate_x, translate_y, translate_z).normalize(); - DMatrix::from_column_slice(5, 5, &[ - 0.0, 0.0, 0.0, 0.0, u[0], - 0.0, 0.0, 0.0, 0.0, u[1], - 0.0, 0.0, 0.0, 0.0, u[2], - 2.0*u[0], 2.0*u[1], 2.0*u[2], 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0 - ]) + let shrink = shrink_pos_val - shrink_neg_val; + let translating = + translate_x != 0.0 + || translate_y != 0.0 + || translate_z != 0.0; + if translating || shrink != 0.0 { + let elt_motion = { + let u = if translating { + TRANSLATION_SPEED * Vector3::new( + translate_x, translate_y, translate_z + ).normalize() + } else { + Vector3::zeros() + }; + time_step * DVector::from_column_slice( + &[u[0], u[1], u[2], SHRINKING_SPEED * shrink] + ) }; - let elt_motion: DVector = time_step * TRANSLATION_SPEED * vel_field * rep; assembly_for_raf.deform( vec![ ElementMotion { @@ -501,6 +509,8 @@ pub fn Display() -> View { "s" | "S" if shift => translate_pos_z.set(value), "w" | "W" => translate_pos_y.set(value), "s" | "S" => translate_neg_y.set(value), + "]" | "}" => shrink_neg.set(value), + "[" | "{" => shrink_pos.set(value), _ => manipulating = false }; if manipulating { diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 6a0202e..eee19ac 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -90,32 +90,34 @@ impl PartialMatrix { #[derive(Clone)] pub struct ConfigSubspace { assembly_dim: usize, - basis: Vec> + basis_std: Vec>, + basis_proj: Vec> } impl ConfigSubspace { pub fn zero(assembly_dim: usize) -> ConfigSubspace { ConfigSubspace { assembly_dim: assembly_dim, - basis: Vec::new() + basis_proj: Vec::new(), + basis_std: Vec::new() } } // approximate the kernel of a symmetric endomorphism of the configuration // space for `assembly_dim` elements. we consider an eigenvector to be part // of the kernel if its eigenvalue is smaller than the constant `THRESHOLD` - fn symmetric_kernel(a: DMatrix, assembly_dim: usize) -> ConfigSubspace { - const ELEMENT_DIM: usize = 5; - const THRESHOLD: f64 = 1.0e-4; - let eig = SymmetricEigen::new(a); + fn symmetric_kernel(a: DMatrix, proj_to_std: DMatrix, assembly_dim: usize) -> ConfigSubspace { + // find a basis for the kernel. the basis is expressed in the projection + // coordinates, and it's orthonormal with respect to the projection + // inner product + const THRESHOLD: f64 = 0.1; + let eig = SymmetricEigen::new(proj_to_std.tr_mul(&a) * &proj_to_std); let eig_vecs = eig.eigenvectors.column_iter(); let eig_pairs = eig.eigenvalues.iter().zip(eig_vecs); - let basis = eig_pairs.filter_map( - |(λ, v)| (λ.abs() < THRESHOLD).then_some( - Into::>::into( - v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim)) - ) - ) + let basis_proj = DMatrix::from_columns( + eig_pairs.filter_map( + |(λ, v)| (λ.abs() < THRESHOLD).then_some(v) + ).collect::>().as_slice() ); /* DEBUG */ @@ -125,30 +127,45 @@ impl ConfigSubspace { format!("Eigenvalues used to find kernel:{}", eig.eigenvalues) )); + // express the basis in the standard coordinates + let basis_std = proj_to_std * &basis_proj; + + const ELEMENT_DIM: usize = 5; + const UNIFORM_DIM: usize = 4; ConfigSubspace { assembly_dim: assembly_dim, - basis: basis.collect() + basis_std: basis_std.column_iter().map( + |v| Into::>::into( + v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim)) + ) + ).collect(), + basis_proj: basis_proj.column_iter().map( + |v| Into::>::into( + v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim)) + ) + ).collect() } } pub fn dim(&self) -> usize { - self.basis.len() + self.basis_std.len() } pub fn assembly_dim(&self) -> usize { self.assembly_dim } - // find the projection onto this subspace, with respect to the Euclidean - // inner product, of the motion where the element with the given column - // index has velocity `v` + // find the projection onto this subspace of the motion where the element + // with the given column index has velocity `v`. the velocity is given in + // projection coordinates, and the projection is done with respect to the + // projection inner product pub fn proj(&self, v: &DVectorView, column_index: usize) -> DMatrix { if self.dim() == 0 { const ELEMENT_DIM: usize = 5; DMatrix::zeros(ELEMENT_DIM, self.assembly_dim) } else { - self.basis.iter().map( - |b| b.column(column_index).dot(&v) * b + self.basis_proj.iter().zip(self.basis_std.iter()).map( + |(b_proj, b_std)| b_proj.column(column_index).dot(&v) * b_std ).sum() } } @@ -215,6 +232,37 @@ fn basis_matrix(index: (usize, usize), nrows: usize, ncols: usize) -> DMatrix) -> DMatrix { + const ELEMENT_DIM: usize = 5; + const UNIFORM_DIM: usize = 4; + let curv = 2.0*v[3]; + if v.dot(&(&*Q * v)) < 0.5 { + // `v` represents a point. the normalization condition says that the + // curvature component of `v` is 1/2 + DMatrix::from_column_slice(ELEMENT_DIM, UNIFORM_DIM, &[ + curv, 0.0, 0.0, 0.0, v[0], + 0.0, curv, 0.0, 0.0, v[1], + 0.0, 0.0, curv, 0.0, v[2], + 0.0, 0.0, 0.0, 0.0, 1.0 + ]) + } else { + // `v` represents a sphere. the normalization condition says that the + // Lorentz product of `v` with itself is 1 + DMatrix::from_column_slice(ELEMENT_DIM, UNIFORM_DIM, &[ + curv, 0.0, 0.0, 0.0, v[0], + 0.0, curv, 0.0, 0.0, v[1], + 0.0, 0.0, curv, 0.0, v[2], + curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0 + ]) + } +} + // use backtracking line search to find a better configuration fn seek_better_config( gram: &PartialMatrix, @@ -344,7 +392,19 @@ pub fn realize_gram( } let success = state.loss < tol; let tangent = if success { - ConfigSubspace::symmetric_kernel(hess, assembly_dim) + // express the uniform basis in the standard basis + const UNIFORM_DIM: usize = 4; + let total_dim_unif = UNIFORM_DIM * assembly_dim; + let mut unif_to_std = DMatrix::::zeros(total_dim, total_dim_unif); + for n in 0..assembly_dim { + let block_start = (element_dim * n, UNIFORM_DIM * n); + unif_to_std + .view_mut(block_start, (element_dim, UNIFORM_DIM)) + .copy_from(&local_unif_to_std(state.config.column(n))); + } + + // find the kernel of the Hessian. give it the uniform inner product + ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim) } else { ConfigSubspace::zero(assembly_dim) }; @@ -424,6 +484,9 @@ pub mod irisawa { #[cfg(test)] mod tests { + use nalgebra::Vector3; + use std::{array, f64::consts::{FRAC_1_SQRT_2, PI}, iter}; + use super::{*, irisawa::realize_irisawa_hexlet}; #[test] @@ -486,10 +549,8 @@ mod tests { } #[test] - fn tangent_test() { + fn tangent_test_three_spheres() { const SCALED_TOL: f64 = 1.0e-12; - const ELEMENT_DIM: usize = 5; - const ASSEMBLY_DIM: usize = 3; let gram = { let mut gram_to_be = PartialMatrix::new(); for j in 0..3 { @@ -514,32 +575,258 @@ mod tests { assert_eq!(history.scaled_loss.len(), 1); // confirm that the tangent space has dimension five or less - let ConfigSubspace(ref tangent_basis) = tangent; - assert_eq!(tangent_basis.len(), 5); + assert_eq!(tangent.basis_std.len(), 5); // confirm that the tangent space contains all the motions we expect it // to. since we've already bounded the dimension of the tangent space, // this confirms that the tangent space is what we expect it to be - let tangent_motions = vec![ - basis_matrix((0, 1), ELEMENT_DIM, ASSEMBLY_DIM), - basis_matrix((1, 1), ELEMENT_DIM, ASSEMBLY_DIM), - basis_matrix((0, 2), ELEMENT_DIM, ASSEMBLY_DIM), - basis_matrix((1, 2), ELEMENT_DIM, ASSEMBLY_DIM), - DMatrix::::from_column_slice(ELEMENT_DIM, 3, &[ - 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, -1.0, -0.25, -1.0, - 0.0, 0.0, -1.0, 0.25, 1.0 + const UNIFORM_DIM: usize = 4; + let element_dim = guess.nrows(); + let assembly_dim = guess.ncols(); + let tangent_motions_unif = vec![ + basis_matrix((0, 1), UNIFORM_DIM, assembly_dim), + basis_matrix((1, 1), UNIFORM_DIM, assembly_dim), + basis_matrix((0, 2), UNIFORM_DIM, assembly_dim), + basis_matrix((1, 2), UNIFORM_DIM, assembly_dim), + DMatrix::::from_column_slice(UNIFORM_DIM, assembly_dim, &[ + 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, -0.5, -0.5, + 0.0, 0.0, -0.5, 0.5 ]) ]; - let tol_sq = ((ELEMENT_DIM * ASSEMBLY_DIM) as f64) * SCALED_TOL * SCALED_TOL; - for motion in tangent_motions { - let motion_proj: DMatrix<_> = motion.column_iter().enumerate().map( + let tangent_motions_std = vec![ + basis_matrix((0, 1), element_dim, assembly_dim), + basis_matrix((1, 1), element_dim, assembly_dim), + basis_matrix((0, 2), element_dim, assembly_dim), + basis_matrix((1, 2), element_dim, assembly_dim), + DMatrix::::from_column_slice(element_dim, assembly_dim, &[ + 0.0, 0.0, 0.0, 0.00, 0.0, + 0.0, 0.0, -1.0, -0.25, -1.0, + 0.0, 0.0, -1.0, 0.25, 1.0 + ]) + ]; + let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL; + for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) { + let motion_proj: DMatrix<_> = motion_unif.column_iter().enumerate().map( |(k, v)| tangent.proj(&v, k) ).sum(); - assert!((motion - motion_proj).norm_squared() < tol_sq); + assert!((motion_std - motion_proj).norm_squared() < tol_sq); } } + fn translation_motion_unif(vel: &Vector3, assembly_dim: usize) -> Vec> { + let mut elt_motion = DVector::zeros(4); + elt_motion.fixed_rows_mut::<3>(0).copy_from(vel); + iter::repeat(elt_motion).take(assembly_dim).collect() + } + + fn rotation_motion_unif(ang_vel: &Vector3, points: Vec>) -> Vec> { + points.into_iter().map( + |pt| { + let vel = ang_vel.cross(&pt.fixed_rows::<3>(0)); + let mut elt_motion = DVector::zeros(4); + elt_motion.fixed_rows_mut::<3>(0).copy_from(&vel); + elt_motion + } + ).collect() + } + + #[test] + fn tangent_test_kaleidocycle() { + // set up a kaleidocycle, made of points with fixed distances between + // them, and find its tangent space + const N_POINTS: usize = 12; + const N_HINGES: usize = 6; + const SCALED_TOL: f64 = 1.0e-12; + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for block in (0..N_POINTS).step_by(2) { + let block_next = (block + 2) % N_POINTS; + for j in 0..2 { + // diagonal and hinge edges + for k in j..2 { + gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); + } + + // non-hinge edges + for k in 0..2 { + gram_to_be.push_sym(block + j, block_next + k, -0.625); + } + } + } + gram_to_be + }; + let guess = { + let guess_elts = (0..N_HINGES).step_by(2).flat_map( + |n| { + let ang_hor = (n as f64) * PI/3.0; + let ang_vert = ((n + 1) as f64) * PI/3.0; + let x_vert = ang_vert.cos(); + let y_vert = ang_vert.sin(); + [ + point(0.0, 0.0, 0.0), + point(ang_hor.cos(), ang_hor.sin(), 0.0), + point(x_vert, y_vert, -0.5), + point(x_vert, y_vert, 0.5) + ] + } + ).collect::>(); + DMatrix::from_columns(&guess_elts) + }; + let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); + let (config, tangent, success, history) = realize_gram( + &gram, guess.clone(), &frozen, + SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + ); + assert_eq!(config, guess); + assert_eq!(success, true); + assert_eq!(history.scaled_loss.len(), 1); + + // list some motions that should form a basis for the tangent space of + // the solution variety + let element_dim = guess.nrows(); + let assembly_dim = guess.ncols(); + let tangent_motions_unif = vec![ + // the translations along the coordinate axes + translation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), assembly_dim), + translation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), assembly_dim), + translation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), assembly_dim), + + // the rotations about the coordinate axes + rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), guess.column_iter().collect()), + rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), guess.column_iter().collect()), + rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), guess.column_iter().collect()), + + // the twist motion. more precisely: a motion that keeps the center + // of mass stationary and preserves the distances between the + // vertices to first order. this has to be the twist as long as: + // - twisting is the kaleidocycle's only internal degree of + // freedom + // - every first-order motion of the kaleidocycle comes from an + // actual motion + (0..N_HINGES).step_by(2).flat_map( + |n| { + let ang_vert = ((n + 1) as f64) * PI/3.0; + let vel_vert_x = 4.0 * ang_vert.cos(); + let vel_vert_y = 4.0 * ang_vert.sin(); + [ + DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]), + DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]), + DVector::from_column_slice(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]), + DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0]) + ] + } + ).collect::>() + ]; + let tangent_motions_std = tangent_motions_unif.iter().map( + |motion| DMatrix::from_columns( + &guess.column_iter().zip(motion).map( + |(v, elt_motion)| local_unif_to_std(v) * elt_motion + ).collect::>() + ) + ).collect::>(); + + // confirm that the dimension of the tangent space is no greater than + // expected + assert_eq!(tangent.basis_std.len(), tangent_motions_unif.len()); + + // confirm that the tangent space contains all the motions we expect it + // to. since we've already bounded the dimension of the tangent space, + // this confirms that the tangent space is what we expect it to be + let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL; + for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) { + let motion_proj: DMatrix<_> = motion_unif.into_iter().enumerate().map( + |(k, v)| tangent.proj(&v.as_view(), k) + ).sum(); + assert!((motion_std - motion_proj).norm_squared() < tol_sq); + } + } + + fn translation(dis: Vector3) -> DMatrix { + const ELEMENT_DIM: usize = 5; + DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[ + 1.0, 0.0, 0.0, 0.0, dis[0], + 0.0, 1.0, 0.0, 0.0, dis[1], + 0.0, 0.0, 1.0, 0.0, dis[2], + 2.0*dis[0], 2.0*dis[1], 2.0*dis[2], 1.0, dis.norm_squared(), + 0.0, 0.0, 0.0, 0.0, 1.0 + ]) + } + + // confirm that projection onto a configuration subspace is equivariant with + // respect to Euclidean motions + #[test] + fn proj_equivar_test() { + // find a pair of spheres that meet at 120° + const SCALED_TOL: f64 = 1.0e-12; + let gram = { + let mut gram_to_be = PartialMatrix::new(); + gram_to_be.push_sym(0, 0, 1.0); + gram_to_be.push_sym(1, 1, 1.0); + gram_to_be.push_sym(0, 1, 0.5); + gram_to_be + }; + let guess_orig = DMatrix::from_columns(&[ + sphere(0.0, 0.0, 0.5, 1.0), + sphere(0.0, 0.0, -0.5, 1.0) + ]); + let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram( + &gram, guess_orig.clone(), &[], + SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + ); + assert_eq!(config_orig, guess_orig); + assert_eq!(success_orig, true); + assert_eq!(history_orig.scaled_loss.len(), 1); + + // find another pair of spheres that meet at 120°. we'll think of this + // solution as a transformed version of the original one + let guess_tfm = { + let a = 0.5 * FRAC_1_SQRT_2; + DMatrix::from_columns(&[ + sphere(a, 0.0, 7.0 + a, 1.0), + sphere(-a, 0.0, 7.0 - a, 1.0) + ]) + }; + let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram( + &gram, guess_tfm.clone(), &[], + SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + ); + assert_eq!(config_tfm, guess_tfm); + assert_eq!(success_tfm, true); + assert_eq!(history_tfm.scaled_loss.len(), 1); + + // project a nudge to the tangent space of the solution variety at the + // original solution + let motion_orig = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); + let motion_orig_proj = tangent_orig.proj(&motion_orig.as_view(), 0); + + // project the equivalent nudge to the tangent space of the solution + // variety at the transformed solution + let motion_tfm = DVector::from_column_slice(&[FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0]); + let motion_tfm_proj = tangent_tfm.proj(&motion_tfm.as_view(), 0); + + // take the transformation that sends the original solution to the + // transformed solution and apply it to the motion that the original + // solution makes in response to the nudge + const ELEMENT_DIM: usize = 5; + let rot = DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[ + FRAC_1_SQRT_2, 0.0, -FRAC_1_SQRT_2, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, + FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 1.0 + ]); + let transl = translation(Vector3::new(0.0, 0.0, 7.0)); + let motion_proj_tfm = transl * rot * motion_orig_proj; + + // confirm that the projection of the nudge is equivariant. we loosen + // the comparison tolerance because the transformation seems to + // introduce some numerical error + const SCALED_TOL_TFM: f64 = 1.0e-9; + let tol_sq = ((guess_orig.nrows() * guess_orig.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM; + assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq); + } + // at the frozen indices, the optimization steps should have exact zeros, // and the realized configuration should match the initial guess #[test] From 25017176fd8ab03c875ba53b95803e72c9cca91a Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Thu, 6 Feb 2025 22:53:41 +0000 Subject: [PATCH 109/126] Adjust normalization step of nudge routine (#43) The brach to be merged partially addresses issue #42 by changing the way we normalize element representations after stepping them in a straight line through configuration space during a nudge. On the main branch, we rescale the whole representation vector. On the branch to be merged, we instead contract the representation vector toward the last coordinate axis by rescaling the spatial and curvature components. ### Improvement in leakage This change reduces the directional leakage described in #42. For a quantitative comparison, I used the [reproduction prodcedure](issues/42#user-content-leakage) from that issue, holding **W** until the second coordinate of Deimos had increased by 4 units (from 0.6 to 4.6). During this motion, the third coordinate changed by 0.158 units on the main branch, but only 0.007 units on the branch to be merged. In other words, this pull request decreased drift by roughly a factor of 20. ### Neutral changes in oscillation and jitter This change makes oscillation and jitter happen differently during the reproduction procedures from #42, but I wouldn't describe them as being better or worse. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/43 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/src/assembly.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 9b0e065..7073c9e 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -5,7 +5,7 @@ use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ -use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix, Q}; +use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix}; // the types of the keys we use to access an assembly's elements and constraints pub type ElementKey = usize; @@ -371,8 +371,8 @@ impl Assembly { } } - // step each element along the mass shell geodesic that matches its - // velocity in the deformation found above + // 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 @@ -380,9 +380,16 @@ impl Assembly { elt.representation.update_silent(|rep| { match elt.column_index { Some(column_index) => { - let rep_next = &*rep + motion_proj.column(column_index); - let normalizer = rep_next.dot(&(&*Q * &rep_next)); - rep.set_column(0, &(rep_next / normalizer)); + // 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); }, None => { console::log_1(&JsValue::from( From 46324fecc692d21aa3648c168b8df89c7031cbb6 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Sat, 8 Feb 2025 06:08:36 +0000 Subject: [PATCH 110/126] Use workaround to keep representation coordinates in order (#46) This fixes #41 by rendering representation vectors with a static list view rather than an `Indexed` view. The Sycamore maintainer has confirmed that `Indexed` is always supposed to display list items in order, so I think #41 is likely caused by a bug in `Indexed`. We should consider reverting this pull request when the bug is fixed. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/46 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/src/outline.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 148f870..a6e968d 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -64,11 +64,16 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { move |sel| if sel.contains(&key) { "selected" } else { "" } ); let label = element.label.clone(); - let rep_components = element.representation.map( - |rep| rep.iter().map( - |u| format!("{:.3}", u).replace("-", "\u{2212}") - ).collect() - ); + let rep_components = move || { + element.representation.with( + |rep| rep.iter().map( + |u| { + let u_str = format!("{:.3}", u).replace("-", "\u{2212}"); + view! { div { (u_str) } } + } + ).collect::>() + ) + }; let constrained = element.constraints.map(|csts| csts.len() > 0); let constraint_list = element.constraints.map( |csts| csts.clone().into_iter().collect() @@ -129,14 +134,7 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { } ) { div(class="element-label") { (label) } - div(class="element-representation") { - Indexed( - list=rep_components, - view=|coord_str| view! { - div { (coord_str) } - } - ) - } + div(class="element-representation") { (rep_components) } div(class="status") } } From da28bc99d2f46a2cc3141d05c8fe1a57e336fc71 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 10 Mar 2025 23:43:24 +0000 Subject: [PATCH 111/126] Generalize constraints to observables (#48) Unifies the interface elements for measuring and constraining real-valued observables, as proposed in issue #47. The resulting combination is called a "Regulator," at least in the code. They are presented as text inputs in the table view. When a Regulatore is in measurement mode (has no "set point"), the text field displays its value. Entering a desired value into the text field creates a set point, and then the Regulator acts to (attempt to) constrain the value. Setting the desired value to the empty string switches the observable back to measurement mode. If you enter a desired value that can't be parsed as a floating point number, the regulator input is flagged as invalid and it has no effect on the state of the regulator. The set point can in this case be restored to its previous value (or to no set point if that was its prior state) by pressing the "Esc" key. Co-authored-by: Aaron Fenyes Co-authored-by: glen Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/48 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.toml | 1 + app-proto/main.css | 47 +++++++++----- app-proto/src/add_remove.rs | 44 ++----------- app-proto/src/assembly.rs | 115 ++++++++++++++++++++++++--------- app-proto/src/main.rs | 1 + app-proto/src/outline.rs | 123 ++++++++++++++++++++++++------------ app-proto/src/specified.rs | 44 +++++++++++++ 7 files changed, 249 insertions(+), 126 deletions(-) create mode 100644 app-proto/src/specified.rs diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index c11fef4..8000327 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -13,6 +13,7 @@ itertools = "0.13.0" js-sys = "0.3.70" lazy_static = "1.5.0" nalgebra = "0.33.0" +readonly = "0.2.12" rustc-hash = "2.0.0" slab = "0.4.9" sycamore = "0.9.0-beta.3" diff --git a/app-proto/main.css b/app-proto/main.css index b9fc0a1..4726a27 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -3,7 +3,8 @@ --text-bright: white; --text-invalid: #f58fc2; /* bright pink */ --border: #555; /* light gray */ - --border-focus: #aaa; /* bright gray */ + --border-focus-dark: #aaa; /* bright gray */ + --border-focus-light: white; --border-invalid: #70495c; /* dusky pink */ --selection-highlight: #444; /* medium gray */ --page-background: #222; /* dark gray */ @@ -23,7 +24,7 @@ body { display: flex; flex-direction: column; float: left; - width: 450px; + width: 500px; height: 100vh; margin: 0px; padding: 0px; @@ -77,12 +78,12 @@ summary.selected { background-color: var(--selection-highlight); } -summary > div, .constraint { +summary > div, .regulator { padding-top: 4px; padding-bottom: 4px; } -.element, .constraint { +.element, .regulator { display: flex; flex-grow: 1; padding-left: 8px; @@ -107,7 +108,7 @@ details[open]:has(li) .element-switch::after { flex-grow: 1; } -.constraint-label { +.regulator-label { flex-grow: 1; } @@ -123,26 +124,34 @@ details[open]:has(li) .element-switch::after { width: 56px; } -.constraint { +.regulator { font-style: italic; } -.constraint.invalid { - color: var(--text-invalid); +.regulator-type { + padding: 2px 8px 0px 8px; + font-size: 10pt; } -.constraint > input[type=checkbox] { - margin: 0px 8px 0px 0px; -} - -.constraint > input[type=text] { +.regulator-input { color: inherit; background-color: inherit; border: 1px solid var(--border); border-radius: 2px; } -.constraint.invalid > input[type=text] { +.regulator-input::placeholder { + color: inherit; + opacity: 54%; + font-style: italic; +} + +.regulator-input.constraint { + background-color: var(--display-background); +} + +.regulator-input.invalid { + color: var(--text-invalid); border-color: var(--border-invalid); } @@ -154,7 +163,7 @@ details[open]:has(li) .element-switch::after { font-style: normal; } -.invalid > .status::after, details:has(.invalid):not([open]) .status::after { +.regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); } @@ -171,5 +180,11 @@ canvas { } canvas:focus { - border-color: var(--border-focus); + border-color: var(--border-focus-dark); + outline: none; +} + +input:focus { + border-color: var(--border-focus-light); + outline: none; } \ No newline at end of file diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index ba02e65..5fed411 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -1,7 +1,11 @@ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; -use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}}; +use crate::{ + engine, + AppState, + assembly::{Assembly, Element} +}; /* DEBUG */ // load an example assembly for testing. this code will be removed once we've @@ -190,44 +194,8 @@ pub fn AddRemove() -> View { (subject_vec[0].clone(), subject_vec[1].clone()) } ); - let lorentz_prod = create_signal(0.0); - let lorentz_prod_valid = create_signal(false); - let active = create_signal(true); - state.assembly.insert_constraint(Constraint { - subjects: subjects, - lorentz_prod: lorentz_prod, - lorentz_prod_text: create_signal(String::new()), - lorentz_prod_valid: lorentz_prod_valid, - active: active, - }); + state.assembly.insert_new_regulator(subjects); state.selection.update(|sel| sel.clear()); - - /* DEBUG */ - // print updated constraint list - console::log_1(&JsValue::from("Constraints:")); - state.assembly.constraints.with(|csts| { - for (_, cst) in csts.into_iter() { - console::log_5( - &JsValue::from(" "), - &JsValue::from(cst.subjects.0), - &JsValue::from(cst.subjects.1), - &JsValue::from(":"), - &JsValue::from(cst.lorentz_prod.get_untracked()) - ); - } - }); - - // update the realization when the constraint becomes active - // and valid, or is edited while active and valid - create_effect(move || { - console::log_1(&JsValue::from( - format!("Constraint ({}, {}) updated", subjects.0, subjects.1) - )); - lorentz_prod.track(); - if active.get() && lorentz_prod_valid.get() { - state.assembly.realize(); - } - }); } ) { "🔗" } select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 7073c9e..18176df 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -5,11 +5,14 @@ use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ -use crate::engine::{realize_gram, local_unif_to_std, ConfigSubspace, PartialMatrix}; +use crate::{ + engine::{Q, local_unif_to_std, realize_gram, ConfigSubspace, PartialMatrix}, + specified::SpecifiedValue +}; -// the types of the keys we use to access an assembly's elements and constraints +// the types of the keys we use to access an assembly's elements and regulators pub type ElementKey = usize; -pub type ConstraintKey = usize; +pub type RegulatorKey = usize; pub type ElementColor = [f32; 3]; @@ -26,8 +29,11 @@ pub struct Element { pub label: String, pub color: ElementColor, pub representation: Signal>, - pub constraints: Signal>, - + + // All regulators with this element as a subject. The assembly owning + // this element is responsible for keeping this set up to date. + pub regulators: Signal>, + // a serial number, assigned by `Element::new`, that uniquely identifies // each element pub serial: u64, @@ -61,7 +67,7 @@ impl Element { label: label, color: color, representation: create_signal(representation), - constraints: create_signal(BTreeSet::default()), + regulators: create_signal(BTreeSet::default()), serial: serial, column_index: None } @@ -111,13 +117,11 @@ impl Element { } } -#[derive(Clone)] -pub struct Constraint { +#[derive(Clone, Copy)] +pub struct Regulator { pub subjects: (ElementKey, ElementKey), - pub lorentz_prod: Signal, - pub lorentz_prod_text: Signal, - pub lorentz_prod_valid: Signal, - pub active: Signal + pub measurement: ReadSignal, + pub set_point: Signal } // the velocity is expressed in uniform coordinates @@ -131,9 +135,9 @@ type AssemblyMotion<'a> = Vec>; // a complete, view-independent description of an assembly #[derive(Clone)] pub struct Assembly { - // elements and constraints + // elements and regulators pub elements: Signal>, - pub constraints: Signal>, + pub regulators: Signal>, // solution variety tangent space. the basis vectors are stored in // configuration matrix format, ordered according to the elements' column @@ -155,13 +159,13 @@ impl Assembly { pub fn new() -> Assembly { Assembly { elements: create_signal(Slab::new()), - constraints: create_signal(Slab::new()), + regulators: create_signal(Slab::new()), tangent: create_signal(ConfigSubspace::zero(0)), elements_by_id: create_signal(FxHashMap::default()) } } - // --- inserting elements and constraints --- + // --- inserting elements and regulators --- // insert an element into the assembly without checking whether we already // have an element with the same identifier. any element that does have the @@ -204,14 +208,61 @@ impl Assembly { ); } - pub fn insert_constraint(&self, constraint: Constraint) { - let subjects = constraint.subjects; - let key = self.constraints.update(|csts| csts.insert(constraint)); - let subject_constraints = self.elements.with( - |elts| (elts[subjects.0].constraints, elts[subjects.1].constraints) + fn insert_regulator(&self, regulator: Regulator) { + let subjects = regulator.subjects; + let key = self.regulators.update(|regs| regs.insert(regulator)); + let subject_regulators = self.elements.with( + |elts| (elts[subjects.0].regulators, elts[subjects.1].regulators) ); - subject_constraints.0.update(|csts| csts.insert(key)); - subject_constraints.1.update(|csts| csts.insert(key)); + subject_regulators.0.update(|regs| regs.insert(key)); + subject_regulators.1.update(|regs| regs.insert(key)); + } + + pub fn insert_new_regulator(self, subjects: (ElementKey, ElementKey)) { + // create and insert a new regulator + let measurement = self.elements.map( + move |elts| { + let reps = ( + elts[subjects.0].representation.get_clone(), + elts[subjects.1].representation.get_clone() + ); + reps.0.dot(&(&*Q * reps.1)) + } + ); + let set_point = create_signal(SpecifiedValue::from_empty_spec()); + self.insert_regulator(Regulator { + subjects: subjects, + measurement: measurement, + set_point: set_point + }); + + /* DEBUG */ + // print an updated list of regulators + console::log_1(&JsValue::from("Regulators:")); + self.regulators.with(|regs| { + for (_, reg) in regs.into_iter() { + console::log_5( + &JsValue::from(" "), + &JsValue::from(reg.subjects.0), + &JsValue::from(reg.subjects.1), + &JsValue::from(":"), + ®.set_point.with_untracked( + |set_pt| JsValue::from(set_pt.spec.as_str()) + ) + ); + } + }); + + // update the realization when the regulator becomes a constraint, or is + // edited while acting as a constraint + create_effect(move || { + console::log_1(&JsValue::from( + format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) + )); + if set_point.with(|set_pt| set_pt.is_present()) { + self.realize(); + } + }); } // --- realization --- @@ -228,14 +279,16 @@ impl Assembly { let (gram, guess) = self.elements.with_untracked(|elts| { // set up the off-diagonal part of the Gram matrix let mut gram_to_be = PartialMatrix::new(); - self.constraints.with_untracked(|csts| { - for (_, cst) in csts { - if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() { - let subjects = cst.subjects; - let row = elts[subjects.0].column_index.unwrap(); - let col = elts[subjects.1].column_index.unwrap(); - gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked()); - } + self.regulators.with_untracked(|regs| { + for (_, reg) in regs { + reg.set_point.with_untracked(|set_pt| { + if let Some(val) = set_pt.value { + let subjects = reg.subjects; + let row = elts[subjects.0].column_index.unwrap(); + let col = elts[subjects.1].column_index.unwrap(); + gram_to_be.push_sym(row, col, val); + } + }); } }); diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index f961504..6ab3e49 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -3,6 +3,7 @@ mod assembly; mod display; mod engine; mod outline; +mod specified; use rustc_hash::FxHashSet; use sycamore::prelude::*; diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index a6e968d..002baea 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,56 +1,97 @@ use itertools::Itertools; use sycamore::prelude::*; use web_sys::{ - Event, - HtmlInputElement, KeyboardEvent, MouseEvent, wasm_bindgen::JsCast }; -use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}}; +use crate::{ + AppState, + assembly, + assembly::{ElementKey, Regulator, RegulatorKey}, + specified::SpecifiedValue +}; -// an editable view of the Lorentz product representing a constraint +// an editable view of a regulator #[component(inline_props)] -fn LorentzProductInput(constraint: Constraint) -> View { +fn RegulatorInput(regulator: Regulator) -> View { + let valid = create_signal(true); + let value = create_signal( + regulator.set_point.with_untracked(|set_pt| set_pt.spec.clone()) + ); + + // this closure resets the input value to the regulator's set point + // specification + let reset_value = move || { + batch(|| { + valid.set(true); + value.set(regulator.set_point.with(|set_pt| set_pt.spec.clone())); + }) + }; + + // reset the input value whenever the regulator's set point specification + // is updated + create_effect(reset_value); + view! { input( r#type="text", - bind:value=constraint.lorentz_prod_text, - on:change=move |event: Event| { - let target: HtmlInputElement = event.target().unwrap().unchecked_into(); - match target.value().parse::() { - Ok(lorentz_prod) => batch(|| { - constraint.lorentz_prod.set(lorentz_prod); - constraint.lorentz_prod_valid.set(true); - }), - Err(_) => constraint.lorentz_prod_valid.set(false) - }; + class=move || { + if valid.get() { + regulator.set_point.with(|set_pt| { + if set_pt.is_present() { + "regulator-input constraint" + } else { + "regulator-input" + } + }) + } else { + "regulator-input invalid" + } + }, + placeholder=regulator.measurement.with(|result| result.to_string()), + bind:value=value, + on:change=move |_| { + valid.set( + match SpecifiedValue::try_from(value.get_clone_untracked()) { + Ok(set_pt) => { + regulator.set_point.set(set_pt); + true + } + Err(_) => false + } + ) + }, + on:keydown={ + move |event: KeyboardEvent| { + match event.key().as_str() { + "Escape" => reset_value(), + _ => () + } + } } ) } } -// a list item that shows a constraint in an outline view of an element +// a list item that shows a regulator in an outline view of an element #[component(inline_props)] -fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) -> View { +fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View { let state = use_context::(); let assembly = &state.assembly; - let constraint = assembly.constraints.with(|csts| csts[constraint_key].clone()); - let other_subject = if constraint.subjects.0 == element_key { - constraint.subjects.1 + let regulator = assembly.regulators.with(|regs| regs[regulator_key]); + let other_subject = if regulator.subjects.0 == element_key { + regulator.subjects.1 } else { - constraint.subjects.0 + regulator.subjects.0 }; let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); - let class = constraint.lorentz_prod_valid.map( - |&lorentz_prod_valid| if lorentz_prod_valid { "constraint" } else { "constraint invalid" } - ); view! { - li(class=class.get()) { - input(r#type="checkbox", bind:checked=constraint.active) - div(class="constraint-label") { (other_subject_label) } - LorentzProductInput(constraint=constraint) + li(class="regulator") { + div(class="regulator-label") { (other_subject_label) } + div(class="regulator-type") { "Inversive distance" } + RegulatorInput(regulator=regulator) div(class="status") } } @@ -74,9 +115,9 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { ).collect::>() ) }; - let constrained = element.constraints.map(|csts| csts.len() > 0); - let constraint_list = element.constraints.map( - |csts| csts.clone().into_iter().collect() + let regulated = element.regulators.map(|regs| regs.len() > 0); + let regulator_list = element.regulators.map( + |regs| regs.clone().into_iter().collect() ); let details_node = create_node_ref(); view! { @@ -91,7 +132,7 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { state.select(key, event.shift_key()); event.prevent_default(); }, - "ArrowRight" if constrained.get() => { + "ArrowRight" if regulated.get() => { let _ = details_node .get() .unchecked_into::() @@ -138,16 +179,16 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { div(class="status") } } - ul(class="constraints") { + ul(class="regulators") { Keyed( - list=constraint_list, - view=move |cst_key| view! { - ConstraintOutlineItem( - constraint_key=cst_key, + list=regulator_list, + view=move |reg_key| view! { + RegulatorOutlineItem( + regulator_key=reg_key, element_key=key ) }, - key=|cst_key| cst_key.clone() + key=|reg_key| reg_key.clone() ) } } @@ -155,9 +196,9 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { } } -// a component that lists the elements of the current assembly, showing the -// constraints on each element as a collapsible sub-list. its implementation -// is based on Kate Morley's HTML + CSS tree views: +// a component that lists the elements of the current assembly, showing each +// element's regulators in a collapsible sub-list. its implementation is based +// on Kate Morley's HTML + CSS tree views: // // https://iamkate.com/code/tree-views/ // diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs new file mode 100644 index 0000000..cfe7fc3 --- /dev/null +++ b/app-proto/src/specified.rs @@ -0,0 +1,44 @@ +use std::num::ParseFloatError; + +// a real number described by a specification string. since the structure is +// read-only, we can guarantee that `spec` always specifies `value` in the +// following format +// ┌──────────────────────────────────────────────────────┬───────────┐ +// │ `spec` │ `value` │ +// ┝━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━┥ +// │ a string that parses to the floating-point value `x` │ `Some(x)` │ +// ├──────────────────────────────────────────────────────┼───────────┤ +// │ the empty string │ `None` │ +// └──────────────────────────────────────────────────────┴───────────┘ +#[readonly::make] +pub struct SpecifiedValue { + pub spec: String, + pub value: Option +} + +impl SpecifiedValue { + pub fn from_empty_spec() -> SpecifiedValue { + SpecifiedValue { spec: String::new(), value: None } + } + + pub fn is_present(&self) -> bool { + matches!(self.value, Some(_)) + } +} + +// a `SpecifiedValue` can be constructed from a specification string, formatted +// as described in the comment on the structure definition. the result is `Ok` +// if the specification is properly formatted, and `Error` if not +impl TryFrom for SpecifiedValue { + type Error = ParseFloatError; + + fn try_from(spec: String) -> Result { + if spec.is_empty() { + Ok(SpecifiedValue::from_empty_spec()) + } else { + spec.parse::().map( + |value| SpecifiedValue { spec: spec, value: Some(value) } + ) + } + } +} \ No newline at end of file From 2c4fd39c1fcb36c61f7828487185aa907912f591 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Wed, 12 Mar 2025 21:54:56 +0000 Subject: [PATCH 112/126] refactor: Tidy up engine tests (#72) ### `zero_loss_test` - Drop the redundant type hint in the definition of `a`. ### `tangent_test_three_spheres` - Get the dimension from the expected basis, rather than putting it in by hand. ### `tangent_test_kaleidocycle` - Factor out the realization code, in the same style as `realize_irisawa_hexlet`. - Rename the `irisawa` submodule to `examples`. ### `frozen_entry_test` - Move up into the section for simpler tests, between `zero_loss_test` and `irisawa_hexlet_test`. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/72 Reviewed-by: Glen Whitney Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/examples/irisawa-hexlet.rs | 2 +- app-proto/examples/kaleidocycle.rs | 52 +------ app-proto/src/engine.rs | 218 ++++++++++++++------------- 3 files changed, 121 insertions(+), 151 deletions(-) diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs index 2bc94c0..639a494 100644 --- a/app-proto/examples/irisawa-hexlet.rs +++ b/app-proto/examples/irisawa-hexlet.rs @@ -1,4 +1,4 @@ -use dyna3::engine::{Q, irisawa::realize_irisawa_hexlet}; +use dyna3::engine::{Q, examples::realize_irisawa_hexlet}; fn main() { const SCALED_TOL: f64 = 1.0e-12; diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs index 88116d3..2779ab1 100644 --- a/app-proto/examples/kaleidocycle.rs +++ b/app-proto/examples/kaleidocycle.rs @@ -1,53 +1,10 @@ use nalgebra::{DMatrix, DVector}; -use std::{array, f64::consts::PI}; -use dyna3::engine::{Q, point, realize_gram, PartialMatrix}; +use dyna3::engine::{Q, examples::realize_kaleidocycle}; fn main() { - // set up a kaleidocycle, made of points with fixed distances between them, - // and find its tangent space - const N_POINTS: usize = 12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for block in (0..N_POINTS).step_by(2) { - let block_next = (block + 2) % N_POINTS; - for j in 0..2 { - // diagonal and hinge edges - for k in j..2 { - gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); - } - - // non-hinge edges - for k in 0..2 { - gram_to_be.push_sym(block + j, block_next + k, -0.625); - } - } - } - gram_to_be - }; - let guess = { - const N_HINGES: usize = 6; - let guess_elts = (0..N_HINGES).step_by(2).flat_map( - |n| { - let ang_hor = (n as f64) * PI/3.0; - let ang_vert = ((n + 1) as f64) * PI/3.0; - let x_vert = ang_vert.cos(); - let y_vert = ang_vert.sin(); - [ - point(0.0, 0.0, 0.0), - point(ang_hor.cos(), ang_hor.sin(), 0.0), - point(x_vert, y_vert, -0.5), - point(x_vert, y_vert, 0.5) - ] - } - ).collect::>(); - DMatrix::from_columns(&guess_elts) - }; - let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); - let (config, tangent, success, history) = realize_gram( - &gram, guess, &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 - ); + const SCALED_TOL: f64 = 1.0e-12; + let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config); print!("Configuration:{}", config); if success { @@ -58,7 +15,8 @@ fn main() { println!("Steps: {}", history.scaled_loss.len() - 1); println!("Loss: {}\n", history.scaled_loss.last().unwrap()); - // find the kaleidocycle's twist motion + // find the kaleidocycle's twist motion by projecting onto the tangent space + const N_POINTS: usize = 12; let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); let down = -&up; let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index eee19ac..35f898c 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -413,20 +413,20 @@ pub fn realize_gram( // --- tests --- -// this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article -// below includes a nice translation of the problem statement, which was -// recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and -// Present_) -// -// "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki -// https://www.nippon.com/en/japan-topics/c12801/ -// #[cfg(feature = "dev")] -pub mod irisawa { +pub mod examples { use std::{array, f64::consts::PI}; use super::*; + // this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article + // below includes a nice translation of the problem statement, which was + // recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and + // Present_) + // + // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki + // https://www.nippon.com/en/japan-topics/c12801/ + // pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { let gram = { let mut gram_to_be = PartialMatrix::new(); @@ -480,14 +480,64 @@ pub mod irisawa { scaled_tol, 0.5, 0.9, 1.1, 200, 110 ) } + + // set up a kaleidocycle, made of points with fixed distances between them, + // and find its tangent space + pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + const N_POINTS: usize = 12; + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for block in (0..N_POINTS).step_by(2) { + let block_next = (block + 2) % N_POINTS; + for j in 0..2 { + // diagonal and hinge edges + for k in j..2 { + gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); + } + + // non-hinge edges + for k in 0..2 { + gram_to_be.push_sym(block + j, block_next + k, -0.625); + } + } + } + gram_to_be + }; + + let guess = { + const N_HINGES: usize = 6; + let guess_elts = (0..N_HINGES).step_by(2).flat_map( + |n| { + let ang_hor = (n as f64) * PI/3.0; + let ang_vert = ((n + 1) as f64) * PI/3.0; + let x_vert = ang_vert.cos(); + let y_vert = ang_vert.sin(); + [ + point(0.0, 0.0, 0.0), + point(ang_hor.cos(), ang_hor.sin(), 0.0), + point(x_vert, y_vert, -0.5), + point(x_vert, y_vert, 0.5) + ] + } + ).collect::>(); + DMatrix::from_columns(&guess_elts) + }; + + let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); + + realize_gram( + &gram, guess, &frozen, + scaled_tol, 0.5, 0.9, 1.1, 200, 110 + ) + } } #[cfg(test)] mod tests { use nalgebra::Vector3; - use std::{array, f64::consts::{FRAC_1_SQRT_2, PI}, iter}; + use std::{f64::consts::{FRAC_1_SQRT_2, PI}, iter}; - use super::{*, irisawa::realize_irisawa_hexlet}; + use super::{*, examples::*}; #[test] fn sub_proj_test() { @@ -523,7 +573,7 @@ mod tests { entries }); let config = { - let a: f64 = 0.75_f64.sqrt(); + let a = 0.75_f64.sqrt(); DMatrix::from_columns(&[ sphere(1.0, 0.0, 0.0, a), sphere(-0.5, a, 0.0, a), @@ -534,6 +584,40 @@ mod tests { assert!(state.loss.abs() < f64::EPSILON); } + // at the frozen indices, the optimization steps should have exact zeros, + // and the realized configuration should match the initial guess + #[test] + fn frozen_entry_test() { + let gram = { + let mut gram_to_be = PartialMatrix::new(); + for j in 0..2 { + for k in j..2 { + gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + gram_to_be + }; + let guess = DMatrix::from_columns(&[ + point(0.0, 0.0, 2.0), + sphere(0.0, 0.0, 0.0, 1.0) + ]); + let frozen = [(3, 0), (3, 1)]; + println!(); + let (config, _, success, history) = realize_gram( + &gram, guess.clone(), &frozen, + 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + ); + assert_eq!(success, true); + for base_step in history.base_step.into_iter() { + for index in frozen { + assert_eq!(base_step[index], 0.0); + } + } + for index in frozen { + assert_eq!(config[index], guess[index]); + } + } + #[test] fn irisawa_hexlet_test() { // solve Irisawa's problem @@ -574,12 +658,8 @@ mod tests { assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); - // confirm that the tangent space has dimension five or less - assert_eq!(tangent.basis_std.len(), 5); - - // confirm that the tangent space contains all the motions we expect it - // to. since we've already bounded the dimension of the tangent space, - // this confirms that the tangent space is what we expect it to be + // list some motions that should form a basis for the tangent space of + // the solution variety const UNIFORM_DIM: usize = 4; let element_dim = guess.nrows(); let assembly_dim = guess.ncols(); @@ -605,6 +685,14 @@ mod tests { 0.0, 0.0, -1.0, 0.25, 1.0 ]) ]; + + // confirm that the dimension of the tangent space is no greater than + // expected + assert_eq!(tangent.basis_std.len(), tangent_motions_std.len()); + + // confirm that the tangent space contains all the motions we expect it + // to. since we've already bounded the dimension of the tangent space, + // this confirms that the tangent space is what we expect it to be let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL; for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) { let motion_proj: DMatrix<_> = motion_unif.column_iter().enumerate().map( @@ -633,59 +721,17 @@ mod tests { #[test] fn tangent_test_kaleidocycle() { - // set up a kaleidocycle, made of points with fixed distances between - // them, and find its tangent space - const N_POINTS: usize = 12; - const N_HINGES: usize = 6; + // set up a kaleidocycle and find its tangent space const SCALED_TOL: f64 = 1.0e-12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for block in (0..N_POINTS).step_by(2) { - let block_next = (block + 2) % N_POINTS; - for j in 0..2 { - // diagonal and hinge edges - for k in j..2 { - gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); - } - - // non-hinge edges - for k in 0..2 { - gram_to_be.push_sym(block + j, block_next + k, -0.625); - } - } - } - gram_to_be - }; - let guess = { - let guess_elts = (0..N_HINGES).step_by(2).flat_map( - |n| { - let ang_hor = (n as f64) * PI/3.0; - let ang_vert = ((n + 1) as f64) * PI/3.0; - let x_vert = ang_vert.cos(); - let y_vert = ang_vert.sin(); - [ - point(0.0, 0.0, 0.0), - point(ang_hor.cos(), ang_hor.sin(), 0.0), - point(x_vert, y_vert, -0.5), - point(x_vert, y_vert, 0.5) - ] - } - ).collect::>(); - DMatrix::from_columns(&guess_elts) - }; - let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); - let (config, tangent, success, history) = realize_gram( - &gram, guess.clone(), &frozen, - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 - ); - assert_eq!(config, guess); + let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of // the solution variety - let element_dim = guess.nrows(); - let assembly_dim = guess.ncols(); + const N_HINGES: usize = 6; + let element_dim = config.nrows(); + let assembly_dim = config.ncols(); let tangent_motions_unif = vec![ // the translations along the coordinate axes translation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), assembly_dim), @@ -693,9 +739,9 @@ mod tests { translation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), assembly_dim), // the rotations about the coordinate axes - rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), guess.column_iter().collect()), - rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), guess.column_iter().collect()), - rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), guess.column_iter().collect()), + rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), config.column_iter().collect()), + rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), config.column_iter().collect()), + rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), config.column_iter().collect()), // the twist motion. more precisely: a motion that keeps the center // of mass stationary and preserves the distances between the @@ -720,7 +766,7 @@ mod tests { ]; let tangent_motions_std = tangent_motions_unif.iter().map( |motion| DMatrix::from_columns( - &guess.column_iter().zip(motion).map( + &config.column_iter().zip(motion).map( |(v, elt_motion)| local_unif_to_std(v) * elt_motion ).collect::>() ) @@ -826,38 +872,4 @@ mod tests { let tol_sq = ((guess_orig.nrows() * guess_orig.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM; assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq); } - - // at the frozen indices, the optimization steps should have exact zeros, - // and the realized configuration should match the initial guess - #[test] - fn frozen_entry_test() { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..2 { - for k in j..2 { - gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); - } - } - gram_to_be - }; - let guess = DMatrix::from_columns(&[ - point(0.0, 0.0, 2.0), - sphere(0.0, 0.0, 0.0, 1.0) - ]); - let frozen = [(3, 0), (3, 1)]; - println!(); - let (config, _, success, history) = realize_gram( - &gram, guess.clone(), &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 - ); - assert_eq!(success, true); - for base_step in history.base_step.into_iter() { - for index in frozen { - assert_eq!(base_step[index], 0.0); - } - } - for index in frozen { - assert_eq!(config[index], guess[index]); - } - } } \ No newline at end of file From b86f1761517c2b65a2ebabbd1d0d779ae59a1449 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Wed, 2 Apr 2025 20:31:42 +0000 Subject: [PATCH 113/126] feat: Continuous integration via Forgejo Actions/runners (#75) Adds a continuous integration workflow to the repository, using the [Forgejo Actions](https://forgejo.org/docs/next/user/actions/) framework. Concurrently, Aaron added a [wiki page](https://code.studioinfinity.org/glen/dyna3/wiki/Continuous-integration) to document the continuous integration system. In particular, this page explains how to [run continuous integration checks on a development machine](wiki/Continuous-integration#execution), either directly or in a container. Co-authored-by: Aaron Fenyes Co-authored-by: Glen Whitney Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/75 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- .forgejo/setup-trunk/action.yaml | 22 + .../workflows/continuous-integration.yaml | 29 + .gitignore | 8 +- app-proto/Cargo.lock | 788 ++++++++++++++++++ app-proto/Cargo.toml | 20 + app-proto/src/main.rs | 3 + app-proto/src/tests.rs | 14 + 7 files changed, 877 insertions(+), 7 deletions(-) create mode 100644 .forgejo/setup-trunk/action.yaml create mode 100644 .forgejo/workflows/continuous-integration.yaml create mode 100644 app-proto/Cargo.lock create mode 100644 app-proto/src/tests.rs diff --git a/.forgejo/setup-trunk/action.yaml b/.forgejo/setup-trunk/action.yaml new file mode 100644 index 0000000..6007527 --- /dev/null +++ b/.forgejo/setup-trunk/action.yaml @@ -0,0 +1,22 @@ +# set up the Trunk web build system +# +# https://trunkrs.dev +# +# the `curl` call is based on David Tolnay's `rust-toolchain` action +# +# https://github.com/dtolnay/rust-toolchain +# +runs: + using: "composite" + steps: + - run: rustup target add wasm32-unknown-unknown + + # install the Trunk binary to `ci-bin` within the workspace directory, which + # is determined by the `github.workspace` label and reflected in the + # `GITHUB_WORKSPACE` environment variable. then, make the `trunk` command + # available by placing the fully qualified path to `ci-bin` on the + # workflow's search path + - run: mkdir -p ci-bin + - run: curl --output - --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail 'https://github.com/trunk-rs/trunk/releases/download/v0.21.12/trunk-x86_64-unknown-linux-gnu.tar.gz' | tar --gunzip --extract --file - + working-directory: ci-bin + - run: echo "${{ github.workspace }}/ci-bin" >> $GITHUB_PATH diff --git a/.forgejo/workflows/continuous-integration.yaml b/.forgejo/workflows/continuous-integration.yaml new file mode 100644 index 0000000..daf8923 --- /dev/null +++ b/.forgejo/workflows/continuous-integration.yaml @@ -0,0 +1,29 @@ +on: + pull_request: + push: + branches: [main] +jobs: + # run the automated tests, reporting success if the tests pass and were built + # without warnings. the examples are run as tests, because we've configured + # each example target with `test = true` and `harness = false` in Cargo.toml. + # Trunk build failures caused by problems outside the Rust source code, like + # missing assets, should be caught by `trunk_build_test` + test: + runs-on: docker + container: + image: cimg/rust:1.85-node + defaults: + run: + # set the default working directory for each `run` step, relative to the + # workspace directory. this default only affects `run` steps (and if we + # tried to set the `working-directory` label for any other kind of step, + # it wouldn't be recognized anyway) + working-directory: app-proto + steps: + # Check out the repository so that its top-level directory is the + # workspace directory (action variable `github.workspace`, environment + # variable `$GITHUB_WORKSPACE`): + - uses: https://code.forgejo.org/actions/checkout@v4 + + - uses: ./.forgejo/setup-trunk + - run: RUSTFLAGS='-D warnings' cargo test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3e95fba..ba2944f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,2 @@ -node_modules -site -docbuild -__tests__ -coverage -dyna3.zip -tmpproj +ci-bin *~ diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock new file mode 100644 index 0000000..9738589 --- /dev/null +++ b/app-proto/Cargo.lock @@ -0,0 +1,788 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "dyna3" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "dyna3", + "itertools", + "js-sys", + "lazy_static", + "nalgebra", + "readonly", + "rustc-hash", + "slab", + "sycamore", + "wasm-bindgen-test", + "web-sys", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matrixmultiply" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "minicov" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "nalgebra" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c4b5f057b303842cf3262c27e465f4c303572e7f6b0648f60e16248ac3397f4" +dependencies = [ + "approx", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "readonly" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2a62d85ed81ca5305dc544bd42c8804c5060b78ffa5ad3c64b0fb6a8c13d062" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "safe_arch" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simba" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "sycamore" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dedaf7237c05913604a5b0b2536b613f6c8510c6b213d2583b1294869755cabd" +dependencies = [ + "hashbrown", + "indexmap", + "paste", + "sycamore-core", + "sycamore-macro", + "sycamore-reactive", + "sycamore-web", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "sycamore-core" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ddddc3d1bcb38c04ad55d2d1ab4f6a358e4daaeae0a0436892f1fade9fb31a" +dependencies = [ + "hashbrown", + "paste", + "sycamore-reactive", +] + +[[package]] +name = "sycamore-macro" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77181c27cb753e86065308901871ccc7456fb19527b6a4ffacad3b63175ed014" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "rand", + "sycamore-view-parser", + "syn", +] + +[[package]] +name = "sycamore-reactive" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa6870203507c07e850687c0ccf528eb0f04240e3596bac9137007ffb6c50b1" +dependencies = [ + "paste", + "slotmap", + "smallvec", +] + +[[package]] +name = "sycamore-view-parser" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6144640af2eafffc68a92f3aacbbfaa21f7fd31906e2336fe304fd100fe226b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sycamore-web" +version = "0.9.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca93dcf1b1830bf1aac93508ed51babcda92c1d32d96067ab416d94e4b7c475" +dependencies = [ + "html-escape", + "js-sys", + "once_cell", + "paste", + "smallvec", + "sycamore-core", + "sycamore-macro", + "sycamore-reactive", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "minicov", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wide" +version = "0.7.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b828f995bf1e9622031f8009f8481a85406ce1f4d4588ff746d872043e855690" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 8000327..5ab7299 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -50,3 +50,23 @@ wasm-bindgen-test = "0.3.34" [profile.release] opt-level = "s" # optimize for small code size debug = true # include debug symbols + +[[example]] +name = "irisawa-hexlet" +test = true +harness = false + +[[example]] +name = "kaleidocycle" +test = true +harness = false + +[[example]] +name = "point-on-sphere" +test = true +harness = false + +[[example]] +name = "three-spheres" +test = true +harness = false diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index 6ab3e49..e581997 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -5,6 +5,9 @@ mod engine; mod outline; mod specified; +#[cfg(test)] +mod tests; + use rustc_hash::FxHashSet; use sycamore::prelude::*; diff --git a/app-proto/src/tests.rs b/app-proto/src/tests.rs new file mode 100644 index 0000000..2c5436b --- /dev/null +++ b/app-proto/src/tests.rs @@ -0,0 +1,14 @@ +use std::process::Command; + +// build and bundle the application, reporting success if there are no errors or +// warnings. to see this test fail while others succeed, try moving `index.html` +// or one of the assets that it links to +#[test] +fn trunk_build_test() { + let build_status = Command::new("trunk") + .arg("build") + .env("RUSTFLAGS", "-D warnings") + .status() + .expect("Call to Trunk failed"); + assert!(build_status.success()); +} \ No newline at end of file From 23ba5acad7e6fb3025af63e99d908dd29bde78f4 Mon Sep 17 00:00:00 2001 From: glen Date: Fri, 18 Apr 2025 04:34:30 +0000 Subject: [PATCH 114/126] Add a top-level run command to the "play with prototype" in README (#81) It's convenient to stay in the top-level directory of a project. This change to the README explains how to run the prototype from the top level. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/81 Co-authored-by: glen Co-committed-by: glen --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1ea3302..3a29eb0 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,10 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter ### Play with the prototype -1. Go into the `app-proto` folder -2. Call `trunk serve --release` to build and serve the prototype +1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype * *The crates the prototype depends on will be downloaded and served automatically* * *For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag* + * *If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]`* from there instead. 3. In a web browser, visit one of the URLs listed under the message `INFO 📡 server listening at:` * *Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype* 4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype From 360ce12d8baf7f7200b422df5c43894765bd3e03 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 21 Apr 2025 23:40:42 +0000 Subject: [PATCH 115/126] feat: Curvature regulators (#80) Prior to this commit, there's only one kind of regulator: the one that regulates the inversive distance between two spheres (or, more generally, the Lorentz product between two element representation vectors). Adds a new kind of regulator, which regulates the curvature of a sphere (issue #55). In the process, introduces a general framework based on new traits for organizing and sharing code between different kinds of regulators. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/80 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/examples/point-on-sphere.rs | 25 +- app-proto/examples/three-spheres.rs | 29 +- app-proto/src/add_remove.rs | 63 ++--- app-proto/src/assembly.rs | 364 ++++++++++++++++++------ app-proto/src/engine.rs | 388 ++++++++++++++++---------- app-proto/src/outline.rs | 102 +++++-- 6 files changed, 640 insertions(+), 331 deletions(-) diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 13040e5..880d7b0 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -1,26 +1,19 @@ -use nalgebra::DMatrix; - -use dyna3::engine::{Q, point, realize_gram, sphere, PartialMatrix}; +use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem}; fn main() { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..2 { - for k in j..2 { - gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); - } - } - gram_to_be - }; - let guess = DMatrix::from_columns(&[ + let mut problem = ConstraintProblem::from_guess(&[ point(0.0, 0.0, 2.0), sphere(0.0, 0.0, 0.0, 1.0) ]); - let frozen = [(3, 0)]; + for j in 0..2 { + for k in j..2 { + problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + problem.frozen.push(3, 0, problem.guess[(3, 0)]); println!(); let (config, _, success, history) = realize_gram( - &gram, guess, &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); print!("Configuration:{}", config); diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index 19acfd1..3f3cc44 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -1,29 +1,22 @@ -use nalgebra::DMatrix; - -use dyna3::engine::{Q, realize_gram, sphere, PartialMatrix}; +use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem}; fn main() { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..3 { - for k in j..3 { - gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); - } - } - gram_to_be - }; - let guess = { + let mut problem = ConstraintProblem::from_guess({ let a: f64 = 0.75_f64.sqrt(); - DMatrix::from_columns(&[ + &[ sphere(1.0, 0.0, 0.0, 1.0), sphere(-0.5, a, 0.0, 1.0), sphere(-0.5, -a, 0.0, 1.0) - ]) - }; + ] + }); + for j in 0..3 { + for k in j..3 { + problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); + } + } println!(); let (config, _, success, history) = realize_gram( - &gram, guess, &[], - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); if success { diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 5fed411..14fcd41 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -4,14 +4,14 @@ use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ engine, AppState, - assembly::{Assembly, Element} + assembly::{Assembly, Element, InversiveDistanceRegulator} }; /* DEBUG */ // load an example assembly for testing. this code will be removed once we've // built a more formal test assembly system fn load_gen_assemb(assembly: &Assembly) { - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("gemini_a"), String::from("Castor"), @@ -19,7 +19,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(0.5, 0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("gemini_b"), String::from("Pollux"), @@ -27,7 +27,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(-0.5, -0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("ursa_major"), String::from("Ursa major"), @@ -35,7 +35,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(-0.5, 0.5, 0.0, 0.75) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("ursa_minor"), String::from("Ursa minor"), @@ -43,7 +43,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(0.5, -0.5, 0.0, 0.5) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("moon_deimos"), String::from("Deimos"), @@ -51,7 +51,7 @@ fn load_gen_assemb(assembly: &Assembly) { engine::sphere(0.0, 0.15, 1.0, 0.25) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("moon_phobos"), String::from("Phobos"), @@ -66,7 +66,7 @@ fn load_gen_assemb(assembly: &Assembly) { // built a more formal test assembly system fn load_low_curv_assemb(assembly: &Assembly) { let a = 0.75_f64.sqrt(); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "central".to_string(), "Central".to_string(), @@ -74,7 +74,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere(0.0, 0.0, 0.0, 1.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "assemb_plane".to_string(), "Assembly plane".to_string(), @@ -82,7 +82,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "side1".to_string(), "Side 1".to_string(), @@ -90,7 +90,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "side2".to_string(), "Side 2".to_string(), @@ -98,7 +98,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "side3".to_string(), "Side 3".to_string(), @@ -106,7 +106,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "corner1".to_string(), "Corner 1".to_string(), @@ -114,7 +114,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( "corner2".to_string(), "Corner 2".to_string(), @@ -122,7 +122,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) ) ); - let _ = assembly.try_insert_element( + let _ = assembly.try_insert_sphere( Element::new( String::from("corner3"), String::from("Corner 3"), @@ -148,6 +148,7 @@ pub fn AddRemove() -> View { let assembly = &state.assembly; // clear state + assembly.regulators.update(|regs| regs.clear()); assembly.elements.update(|elts| elts.clear()); assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); state.selection.update(|sel| sel.clear()); @@ -166,18 +167,7 @@ pub fn AddRemove() -> View { button( on:click=|_| { let state = use_context::(); - state.assembly.insert_new_element(); - - /* DEBUG */ - // print updated list of elements by identifier - console::log_1(&JsValue::from("elements by identifier:")); - for (id, key) in state.assembly.elements_by_id.get_clone().iter() { - console::log_3( - &JsValue::from(" "), - &JsValue::from(id), - &JsValue::from(*key) - ); - } + state.assembly.insert_new_sphere(); } ) { "+" } button( @@ -188,13 +178,20 @@ pub fn AddRemove() -> View { }, on:click=|_| { let state = use_context::(); - let subjects = state.selection.with( - |sel| { - let subject_vec: Vec<_> = sel.into_iter().collect(); - (subject_vec[0].clone(), subject_vec[1].clone()) - } + let subjects: [_; 2] = state.selection.with( + // the button is only enabled when two elements are + // selected, so we know the cast to a two-element array + // will succeed + |sel| sel + .clone() + .into_iter() + .collect::>() + .try_into() + .unwrap() + ); + state.assembly.insert_regulator( + InversiveDistanceRegulator::new(subjects, &state.assembly) ); - state.assembly.insert_new_regulator(subjects); state.selection.update(|sel| sel.clear()); } ) { "🔗" } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 18176df..5c926ca 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,12 +1,21 @@ use nalgebra::{DMatrix, DVector, DVectorView, Vector3}; use rustc_hash::FxHashMap; use slab::Slab; -use std::{collections::BTreeSet, sync::atomic::{AtomicU64, Ordering}}; +use std::{collections::BTreeSet, rc::Rc, sync::atomic::{AtomicU64, Ordering}}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::{ - engine::{Q, local_unif_to_std, realize_gram, ConfigSubspace, PartialMatrix}, + engine::{ + Q, + change_half_curvature, + local_unif_to_std, + realize_gram, + sphere, + ConfigSubspace, + ConstraintProblem + }, + outline::OutlineItem, specified::SpecifiedValue }; @@ -23,6 +32,10 @@ pub type ElementColor = [f32; 3]; // each assembly has a key that identifies it within the sesssion static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0); +pub trait ProblemPoser { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab); +} + #[derive(Clone, PartialEq)] pub struct Element { pub id: String, @@ -30,8 +43,8 @@ pub struct Element { pub color: ElementColor, pub representation: Signal>, - // All regulators with this element as a subject. The assembly owning - // this element is responsible for keeping this set up to date. + // the regulators this element is subject to. the assembly that owns the + // element is responsible for keeping this set up to date pub regulators: Signal>, // a serial number, assigned by `Element::new`, that uniquely identifies @@ -45,6 +58,8 @@ pub struct Element { } impl Element { + const CURVATURE_COMPONENT: usize = 3; + pub fn new( id: String, label: String, @@ -117,13 +132,148 @@ impl Element { } } -#[derive(Clone, Copy)] -pub struct Regulator { - pub subjects: (ElementKey, ElementKey), +impl ProblemPoser for Element { + fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab) { + let index = self.column_index.expect( + format!("Element \"{}\" should be indexed before writing problem data", self.id).as_str() + ); + problem.gram.push_sym(index, index, 1.0); + problem.guess.set_column(index, &self.representation.get_clone_untracked()); + } +} + +pub trait Regulator: ProblemPoser + OutlineItem { + fn subjects(&self) -> Vec; + fn measurement(&self) -> ReadSignal; + fn set_point(&self) -> Signal; + + // this method is used to responsively precondition the assembly for + // realization when the regulator becomes a constraint, or is edited while + // acting as a constraint. it should track the set point, do any desired + // preconditioning when the set point is present, and use its return value + // to report whether the set is present. the default implementation does no + // preconditioning + fn try_activate(&self, _assembly: &Assembly) -> bool { + self.set_point().with(|set_pt| set_pt.is_present()) + } +} + +pub struct InversiveDistanceRegulator { + pub subjects: [ElementKey; 2], pub measurement: ReadSignal, pub set_point: Signal } +impl InversiveDistanceRegulator { + pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> InversiveDistanceRegulator { + let measurement = assembly.elements.map( + move |elts| { + let representations = subjects.map(|subj| elts[subj].representation); + representations[0].with(|rep_0| + representations[1].with(|rep_1| + rep_0.dot(&(&*Q * rep_1)) + ) + ) + } + ); + + let set_point = create_signal(SpecifiedValue::from_empty_spec()); + + InversiveDistanceRegulator { subjects, measurement, set_point } + } +} + +impl Regulator for InversiveDistanceRegulator { + fn subjects(&self) -> Vec { + self.subjects.into() + } + + fn measurement(&self) -> ReadSignal { + self.measurement + } + + fn set_point(&self) -> Signal { + self.set_point + } +} + +impl ProblemPoser for InversiveDistanceRegulator { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { + self.set_point.with_untracked(|set_pt| { + if let Some(val) = set_pt.value { + let [row, col] = self.subjects.map( + |subj| elts[subj].column_index.expect( + "Subjects should be indexed before inversive distance regulator writes problem data" + ) + ); + problem.gram.push_sym(row, col, val); + } + }); + } +} + +pub struct HalfCurvatureRegulator { + pub subject: ElementKey, + pub measurement: ReadSignal, + pub set_point: Signal +} + +impl HalfCurvatureRegulator { + pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator { + let measurement = assembly.elements.map( + move |elts| elts[subject].representation.with( + |rep| rep[Element::CURVATURE_COMPONENT] + ) + ); + + let set_point = create_signal(SpecifiedValue::from_empty_spec()); + + HalfCurvatureRegulator { subject, measurement, set_point } + } +} + +impl Regulator for HalfCurvatureRegulator { + fn subjects(&self) -> Vec { + vec![self.subject] + } + + fn measurement(&self) -> ReadSignal { + self.measurement + } + + fn set_point(&self) -> Signal { + self.set_point + } + + fn try_activate(&self, assembly: &Assembly) -> bool { + match self.set_point.with(|set_pt| set_pt.value) { + Some(half_curv) => { + let representation = assembly.elements.with_untracked( + |elts| elts[self.subject].representation + ); + representation.update( + |rep| change_half_curvature(rep, half_curv) + ); + true + } + None => false + } + } +} + +impl ProblemPoser for HalfCurvatureRegulator { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { + self.set_point.with_untracked(|set_pt| { + if let Some(val) = set_pt.value { + let col = elts[self.subject].column_index.expect( + "Subject should be indexed before half-curvature regulator writes problem data" + ); + problem.frozen.push(Element::CURVATURE_COMPONENT, col, val); + } + }); + } +} + // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub key: ElementKey, @@ -137,7 +287,7 @@ type AssemblyMotion<'a> = Vec>; pub struct Assembly { // elements and regulators pub elements: Signal>, - pub regulators: Signal>, + pub regulators: Signal>>, // solution variety tangent space. the basis vectors are stored in // configuration matrix format, ordered according to the elements' column @@ -167,26 +317,33 @@ impl Assembly { // --- inserting elements and regulators --- - // insert an element into the assembly without checking whether we already + // insert a sphere into the assembly without checking whether we already // have an element with the same identifier. any element that does have the // same identifier will get kicked out of the `elements_by_id` index - fn insert_element_unchecked(&self, elt: Element) { + fn insert_sphere_unchecked(&self, elt: Element) -> ElementKey { + // insert the sphere let id = elt.id.clone(); let key = self.elements.update(|elts| elts.insert(elt)); self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); + + // regulate the sphere's curvature + self.insert_regulator(HalfCurvatureRegulator::new(key, &self)); + + key } - pub fn try_insert_element(&self, elt: Element) -> bool { + pub fn try_insert_sphere(&self, elt: Element) -> Option { let can_insert = self.elements_by_id.with_untracked( |elts_by_id| !elts_by_id.contains_key(&elt.id) ); if can_insert { - self.insert_element_unchecked(elt); + Some(self.insert_sphere_unchecked(elt)) + } else { + None } - can_insert } - pub fn insert_new_element(&self) { + pub fn insert_new_sphere(&self) { // find the next unused identifier in the default sequence let mut id_num = 1; let mut id = format!("sphere{}", id_num); @@ -197,70 +354,69 @@ impl Assembly { id = format!("sphere{}", id_num); } - // create and insert a new element - self.insert_element_unchecked( + // create and insert a sphere + let _ = self.insert_sphere_unchecked( Element::new( id, format!("Sphere {}", id_num), [0.75_f32, 0.75_f32, 0.75_f32], - DVector::::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]) + sphere(0.0, 0.0, 0.0, 1.0) ) ); } - fn insert_regulator(&self, regulator: Regulator) { - let subjects = regulator.subjects; - let key = self.regulators.update(|regs| regs.insert(regulator)); - let subject_regulators = self.elements.with( - |elts| (elts[subjects.0].regulators, elts[subjects.1].regulators) + pub fn insert_regulator(&self, regulator: T) { + // add the regulator to the assembly's regulator list + let regulator_rc = Rc::new(regulator); + let key = self.regulators.update( + |regs| regs.insert(regulator_rc.clone()) ); - subject_regulators.0.update(|regs| regs.insert(key)); - subject_regulators.1.update(|regs| regs.insert(key)); - } - - pub fn insert_new_regulator(self, subjects: (ElementKey, ElementKey)) { - // create and insert a new regulator - let measurement = self.elements.map( - move |elts| { - let reps = ( - elts[subjects.0].representation.get_clone(), - elts[subjects.1].representation.get_clone() - ); - reps.0.dot(&(&*Q * reps.1)) + + // add the regulator to each subject's regulator list + let subjects = regulator_rc.subjects(); + let subject_regulators: Vec<_> = self.elements.with_untracked( + |elts| subjects.into_iter().map( + |subj| elts[subj].regulators + ).collect() + ); + for regulators in subject_regulators { + regulators.update(|regs| regs.insert(key)); + } + + // update the realization when the regulator becomes a constraint, or is + // edited while acting as a constraint + let self_for_effect = self.clone(); + create_effect(move || { + /* DEBUG */ + // log the regulator update + console::log_1(&JsValue::from( + format!("Updated regulator with subjects {:?}", regulator_rc.subjects()) + )); + + if regulator_rc.try_activate(&self_for_effect) { + self_for_effect.realize(); } - ); - let set_point = create_signal(SpecifiedValue::from_empty_spec()); - self.insert_regulator(Regulator { - subjects: subjects, - measurement: measurement, - set_point: set_point }); /* DEBUG */ // print an updated list of regulators console::log_1(&JsValue::from("Regulators:")); - self.regulators.with(|regs| { + self.regulators.with_untracked(|regs| { for (_, reg) in regs.into_iter() { - console::log_5( - &JsValue::from(" "), - &JsValue::from(reg.subjects.0), - &JsValue::from(reg.subjects.1), - &JsValue::from(":"), - ®.set_point.with_untracked( - |set_pt| JsValue::from(set_pt.spec.as_str()) + console::log_1(&JsValue::from(format!( + " {:?}: {}", + reg.subjects(), + reg.set_point().with_untracked( + |set_pt| { + let spec = &set_pt.spec; + if spec.is_empty() { + "__".to_string() + } else { + spec.clone() + } + } ) - ); - } - }); - - // update the realization when the regulator becomes a constraint, or is - // edited while acting as a constraint - create_effect(move || { - console::log_1(&JsValue::from( - format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1) - )); - if set_point.with(|set_pt| set_pt.is_present()) { - self.realize(); + ))); } }); } @@ -275,55 +431,39 @@ impl Assembly { } }); - // set up the Gram matrix and the initial configuration matrix - let (gram, guess) = self.elements.with_untracked(|elts| { - // set up the off-diagonal part of the Gram matrix - let mut gram_to_be = PartialMatrix::new(); + // set up the constraint problem + let problem = self.elements.with_untracked(|elts| { + let mut problem = ConstraintProblem::new(elts.len()); + for (_, elt) in elts { + elt.pose(&mut problem, elts); + } self.regulators.with_untracked(|regs| { for (_, reg) in regs { - reg.set_point.with_untracked(|set_pt| { - if let Some(val) = set_pt.value { - let subjects = reg.subjects; - let row = elts[subjects.0].column_index.unwrap(); - let col = elts[subjects.1].column_index.unwrap(); - gram_to_be.push_sym(row, col, val); - } - }); + reg.pose(&mut problem, elts); } }); - - // set up the initial configuration matrix and the diagonal of the - // Gram matrix - let mut guess_to_be = DMatrix::::zeros(5, elts.len()); - for (_, elt) in elts { - let index = elt.column_index.unwrap(); - gram_to_be.push_sym(index, index, 1.0); - guess_to_be.set_column(index, &elt.representation.get_clone_untracked()); - } - - (gram_to_be, guess_to_be) + problem }); /* DEBUG */ // log the Gram matrix console::log_1(&JsValue::from("Gram matrix:")); - gram.log_to_console(); + problem.gram.log_to_console(); /* DEBUG */ // log the initial configuration matrix console::log_1(&JsValue::from("Old configuration:")); - for j in 0..guess.nrows() { + for j in 0..problem.guess.nrows() { let mut row_str = String::new(); - for k in 0..guess.ncols() { - row_str.push_str(format!(" {:>8.3}", guess[(j, k)]).as_str()); + for k in 0..problem.guess.ncols() { + row_str.push_str(format!(" {:>8.3}", problem.guess[(j, k)]).as_str()); } console::log_1(&JsValue::from(row_str)); } // look for a configuration with the given Gram matrix let (config, tangent, success, history) = realize_gram( - &gram, guess, &[], - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); /* DEBUG */ @@ -458,4 +598,48 @@ impl Assembly { // sync self.realize(); } +} + +#[cfg(test)] +mod tests { + use crate::engine; + + use super::*; + + #[test] + #[should_panic(expected = "Element \"sphere\" should be indexed before writing problem data")] + fn unindexed_element_test() { + let _ = create_root(|| { + Element::new( + "sphere".to_string(), + "Sphere".to_string(), + [1.0_f32, 1.0_f32, 1.0_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ).pose(&mut ConstraintProblem::new(1), &Slab::new()); + }); + } + + #[test] + #[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")] + fn unindexed_subject_test_inversive_distance() { + let _ = create_root(|| { + let mut elts = Slab::new(); + let subjects = [0, 1].map(|k| { + elts.insert( + Element::new( + format!("sphere{k}"), + format!("Sphere {k}"), + [1.0_f32, 1.0_f32, 1.0_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) + ) + }); + elts[subjects[0]].column_index = Some(0); + InversiveDistanceRegulator { + subjects: subjects, + measurement: create_memo(|| 0.0), + set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()) + }.pose(&mut ConstraintProblem::new(2), &elts); + }); + } } \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 35f898c..869a7de 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -35,9 +35,43 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 ]) } +// given a sphere's representation vector, change the sphere's half-curvature to +// `half-curv` and then restore normalization by contracting the representation +// vector toward the curvature axis +pub fn change_half_curvature(rep: &mut DVector, half_curv: f64) { + // set the sphere's half-curvature to the desired value + rep[3] = half_curv; + + // restore normalization by contracting toward the curvature axis + const SIZE_THRESHOLD: f64 = 1e-9; + let half_q_lt = -2.0 * half_curv * rep[4]; + let half_q_lt_sq = half_q_lt * half_q_lt; + let mut spatial = rep.fixed_rows_mut::<3>(0); + let q_sp = spatial.norm_squared(); + if q_sp < SIZE_THRESHOLD && half_q_lt_sq < SIZE_THRESHOLD { + spatial.copy_from_slice( + &[0.0, 0.0, (1.0 - 2.0 * half_q_lt).sqrt()] + ); + } else { + let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); + spatial.scale_mut(1.0 / scaling); + rep[4] /= scaling; + } + + /* DEBUG */ + // verify normalization + let rep_for_debug = rep.clone(); + console::log_1(&JsValue::from( + format!( + "Sphere self-product after curvature change: {}", + rep_for_debug.dot(&(&*Q * &rep_for_debug)) + ) + )); +} + // --- partial matrices --- -struct MatrixEntry { +pub struct MatrixEntry { index: (usize, usize), value: f64 } @@ -49,42 +83,72 @@ impl PartialMatrix { PartialMatrix(Vec::::new()) } - pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { + pub fn push(&mut self, row: usize, col: usize, value: f64) { let PartialMatrix(entries) = self; entries.push(MatrixEntry { index: (row, col), value: value }); + } + + pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { + self.push(row, col, value); if row != col { - entries.push(MatrixEntry { index: (col, row), value: value }); + self.push(col, row, value); } } /* DEBUG */ pub fn log_to_console(&self) { - let PartialMatrix(entries) = self; - for ent in entries { - let ent_str = format!(" {} {} {}", ent.index.0, ent.index.1, ent.value); - console::log_1(&JsValue::from(ent_str.as_str())); + for &MatrixEntry { index: (row, col), value } in self { + console::log_1(&JsValue::from( + format!(" {} {} {}", row, col, value) + )); } } + fn freeze(&self, a: &DMatrix) -> DMatrix { + let mut result = a.clone(); + for &MatrixEntry { index, value } in self { + result[index] = value; + } + result + } + fn proj(&self, a: &DMatrix) -> DMatrix { let mut result = DMatrix::::zeros(a.nrows(), a.ncols()); - let PartialMatrix(entries) = self; - for ent in entries { - result[ent.index] = a[ent.index]; + for &MatrixEntry { index, .. } in self { + result[index] = a[index]; } result } fn sub_proj(&self, rhs: &DMatrix) -> DMatrix { let mut result = DMatrix::::zeros(rhs.nrows(), rhs.ncols()); - let PartialMatrix(entries) = self; - for ent in entries { - result[ent.index] = ent.value - rhs[ent.index]; + for &MatrixEntry { index, value } in self { + result[index] = value - rhs[index]; } result } } +impl IntoIterator for PartialMatrix { + type Item = MatrixEntry; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + let PartialMatrix(entries) = self; + entries.into_iter() + } +} + +impl<'a> IntoIterator for &'a PartialMatrix { + type Item = &'a MatrixEntry; + type IntoIter = std::slice::Iter<'a, MatrixEntry>; + + fn into_iter(self) -> Self::IntoIter { + let PartialMatrix(entries) = self; + entries.into_iter() + } +} + // --- configuration subspaces --- #[derive(Clone)] @@ -195,6 +259,34 @@ impl DescentHistory { } } +// --- constraint problems --- + +pub struct ConstraintProblem { + pub gram: PartialMatrix, + pub frozen: PartialMatrix, + pub guess: DMatrix, +} + +impl ConstraintProblem { + pub fn new(element_count: usize) -> ConstraintProblem { + const ELEMENT_DIM: usize = 5; + ConstraintProblem { + gram: PartialMatrix::new(), + frozen: PartialMatrix::new(), + guess: DMatrix::::zeros(ELEMENT_DIM, element_count) + } + } + + #[cfg(feature = "dev")] + pub fn from_guess(guess_columns: &[DVector]) -> ConstraintProblem { + ConstraintProblem { + gram: PartialMatrix::new(), + frozen: PartialMatrix::new(), + guess: DMatrix::from_columns(guess_columns) + } + } +} + // --- gram matrix realization --- // the Lorentz form @@ -286,12 +378,12 @@ fn seek_better_config( None } -// seek a matrix `config` for which `config' * Q * config` matches the partial -// matrix `gram`. use gradient descent starting from `guess` +// seek a matrix `config` that matches the partial matrix `problem.frozen` and +// has `config' * Q * config` matching the partial matrix `problem.gram`. start +// at `problem.guess`, set the frozen entries to their desired values, and then +// use a regularized Newton's method to seek the desired Gram matrix pub fn realize_gram( - gram: &PartialMatrix, - guess: DMatrix, - frozen: &[(usize, usize)], + problem: &ConstraintProblem, scaled_tol: f64, min_efficiency: f64, backoff: f64, @@ -299,6 +391,11 @@ pub fn realize_gram( max_descent_steps: i32, max_backoff_steps: i32 ) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + // destructure the problem data + let ConstraintProblem { + gram, guess, frozen + } = problem; + // start the descent history let mut history = DescentHistory::new(); @@ -313,11 +410,11 @@ pub fn realize_gram( // convert the frozen indices to stacked format let frozen_stacked: Vec = frozen.into_iter().map( - |index| index.1*element_dim + index.0 + |MatrixEntry { index: (row, col), .. }| col*element_dim + row ).collect(); - // use Newton's method with backtracking and gradient descent backup - let mut state = SearchState::from_config(gram, guess); + // use a regularized Newton's method with backtracking + let mut state = SearchState::from_config(gram, frozen.freeze(guess)); let mut hess = DMatrix::zeros(element_dim, assembly_dim); for _ in 0..max_descent_steps { // find the negative gradient of the loss function @@ -415,7 +512,7 @@ pub fn realize_gram( #[cfg(feature = "dev")] pub mod examples { - use std::{array, f64::consts::PI}; + use std::f64::consts::PI; use super::*; @@ -428,35 +525,7 @@ pub mod examples { // https://www.nippon.com/en/japan-topics/c12801/ // pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for s in 0..9 { - // each sphere is represented by a spacelike vector - gram_to_be.push_sym(s, s, 1.0); - - // the circumscribing sphere is tangent to all of the other - // spheres, with matching orientation - if s > 0 { - gram_to_be.push_sym(0, s, 1.0); - } - - if s > 2 { - // each chain sphere is tangent to the "sun" and "moon" - // spheres, with opposing orientation - for n in 1..3 { - gram_to_be.push_sym(s, n, -1.0); - } - - // each chain sphere is tangent to the next chain sphere, - // with opposing orientation - let s_next = 3 + (s-2) % 6; - gram_to_be.push_sym(s, s_next, -1.0); - } - } - gram_to_be - }; - - let guess = DMatrix::from_columns( + let mut problem = ConstraintProblem::from_guess( [ sphere(0.0, 0.0, 0.0, 15.0), sphere(0.0, 0.0, -9.0, 5.0), @@ -471,42 +540,45 @@ pub mod examples { ).collect::>().as_slice() ); + for s in 0..9 { + // each sphere is represented by a spacelike vector + problem.gram.push_sym(s, s, 1.0); + + // the circumscribing sphere is tangent to all of the other + // spheres, with matching orientation + if s > 0 { + problem.gram.push_sym(0, s, 1.0); + } + + if s > 2 { + // each chain sphere is tangent to the "sun" and "moon" + // spheres, with opposing orientation + for n in 1..3 { + problem.gram.push_sym(s, n, -1.0); + } + + // each chain sphere is tangent to the next chain sphere, + // with opposing orientation + let s_next = 3 + (s-2) % 6; + problem.gram.push_sym(s, s_next, -1.0); + } + } + // the frozen entries fix the radii of the circumscribing sphere, the // "sun" and "moon" spheres, and one of the chain spheres - let frozen: [(usize, usize); 4] = array::from_fn(|k| (3, k)); + for k in 0..4 { + problem.frozen.push(3, k, problem.guess[(3, k)]); + } - realize_gram( - &gram, guess, &frozen, - scaled_tol, 0.5, 0.9, 1.1, 200, 110 - ) + realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110) } // set up a kaleidocycle, made of points with fixed distances between them, // and find its tangent space pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { - const N_POINTS: usize = 12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for block in (0..N_POINTS).step_by(2) { - let block_next = (block + 2) % N_POINTS; - for j in 0..2 { - // diagonal and hinge edges - for k in j..2 { - gram_to_be.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); - } - - // non-hinge edges - for k in 0..2 { - gram_to_be.push_sym(block + j, block_next + k, -0.625); - } - } - } - gram_to_be - }; - - let guess = { - const N_HINGES: usize = 6; - let guess_elts = (0..N_HINGES).step_by(2).flat_map( + const N_HINGES: usize = 6; + let mut problem = ConstraintProblem::from_guess( + (0..N_HINGES).step_by(2).flat_map( |n| { let ang_hor = (n as f64) * PI/3.0; let ang_vert = ((n + 1) as f64) * PI/3.0; @@ -519,16 +591,30 @@ pub mod examples { point(x_vert, y_vert, 0.5) ] } - ).collect::>(); - DMatrix::from_columns(&guess_elts) - }; + ).collect::>().as_slice() + ); - let frozen: [_; N_POINTS] = array::from_fn(|k| (3, k)); + const N_POINTS: usize = 2 * N_HINGES; + for block in (0..N_POINTS).step_by(2) { + let block_next = (block + 2) % N_POINTS; + for j in 0..2 { + // diagonal and hinge edges + for k in j..2 { + problem.gram.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); + } + + // non-hinge edges + for k in 0..2 { + problem.gram.push_sym(block + j, block_next + k, -0.625); + } + } + } - realize_gram( - &gram, guess, &frozen, - scaled_tol, 0.5, 0.9, 1.1, 200, 110 - ) + for k in 0..N_POINTS { + problem.frozen.push(3, k, problem.guess[(3, k)]) + } + + realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110) } } @@ -539,6 +625,25 @@ mod tests { use super::{*, examples::*}; + #[test] + fn freeze_test() { + let frozen = PartialMatrix(vec![ + MatrixEntry { index: (0, 0), value: 14.0 }, + MatrixEntry { index: (0, 2), value: 28.0 }, + MatrixEntry { index: (1, 1), value: 42.0 }, + MatrixEntry { index: (1, 2), value: 49.0 } + ]); + let config = DMatrix::::from_row_slice(2, 3, &[ + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0 + ]); + let expected_result = DMatrix::::from_row_slice(2, 3, &[ + 14.0, 2.0, 28.0, + 4.0, 42.0, 49.0 + ]); + assert_eq!(frozen.freeze(&config), expected_result); + } + #[test] fn sub_proj_test() { let target = PartialMatrix(vec![ @@ -560,18 +665,12 @@ mod tests { #[test] fn zero_loss_test() { - let gram = PartialMatrix({ - let mut entries = Vec::::new(); - for j in 0..3 { - for k in 0..3 { - entries.push(MatrixEntry { - index: (j, k), - value: if j == k { 1.0 } else { -1.0 } - }); - } + let mut gram = PartialMatrix::new(); + for j in 0..3 { + for k in 0..3 { + gram.push(j, k, if j == k { 1.0 } else { -1.0 }); } - entries - }); + } let config = { let a = 0.75_f64.sqrt(); DMatrix::from_columns(&[ @@ -584,37 +683,33 @@ mod tests { assert!(state.loss.abs() < f64::EPSILON); } + /* TO DO */ // at the frozen indices, the optimization steps should have exact zeros, - // and the realized configuration should match the initial guess + // and the realized configuration should have the desired values #[test] fn frozen_entry_test() { - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..2 { - for k in j..2 { - gram_to_be.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); - } - } - gram_to_be - }; - let guess = DMatrix::from_columns(&[ + let mut problem = ConstraintProblem::from_guess(&[ point(0.0, 0.0, 2.0), - sphere(0.0, 0.0, 0.0, 1.0) + sphere(0.0, 0.0, 0.0, 0.95) ]); - let frozen = [(3, 0), (3, 1)]; - println!(); + for j in 0..2 { + for k in j..2 { + problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + } + } + problem.frozen.push(3, 0, problem.guess[(3, 0)]); + problem.frozen.push(3, 1, 0.5); let (config, _, success, history) = realize_gram( - &gram, guess.clone(), &frozen, - 1.0e-12, 0.5, 0.9, 1.1, 200, 110 + &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); assert_eq!(success, true); for base_step in history.base_step.into_iter() { - for index in frozen { + for &MatrixEntry { index, .. } in &problem.frozen { assert_eq!(base_step[index], 0.0); } } - for index in frozen { - assert_eq!(config[index], guess[index]); + for MatrixEntry { index, value } in problem.frozen { + assert_eq!(config[index], value); } } @@ -635,34 +730,32 @@ mod tests { #[test] fn tangent_test_three_spheres() { const SCALED_TOL: f64 = 1.0e-12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - for j in 0..3 { - for k in j..3 { - gram_to_be.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); - } - } - gram_to_be - }; - let guess = DMatrix::from_columns(&[ + const ELEMENT_DIM: usize = 5; + let mut problem = ConstraintProblem::from_guess(&[ sphere(0.0, 0.0, 0.0, -2.0), sphere(0.0, 0.0, 1.0, 1.0), sphere(0.0, 0.0, -1.0, 1.0) ]); - let frozen: [_; 5] = std::array::from_fn(|k| (k, 0)); + for j in 0..3 { + for k in j..3 { + problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); + } + } + for n in 0..ELEMENT_DIM { + problem.frozen.push(n, 0, problem.guess[(n, 0)]); + } let (config, tangent, success, history) = realize_gram( - &gram, guess.clone(), &frozen, - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + &problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(config, guess); + assert_eq!(config, problem.guess); assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of // the solution variety const UNIFORM_DIM: usize = 4; - let element_dim = guess.nrows(); - let assembly_dim = guess.ncols(); + let element_dim = problem.guess.nrows(); + let assembly_dim = problem.guess.ncols(); let tangent_motions_unif = vec![ basis_matrix((0, 1), UNIFORM_DIM, assembly_dim), basis_matrix((1, 1), UNIFORM_DIM, assembly_dim), @@ -805,22 +898,17 @@ mod tests { fn proj_equivar_test() { // find a pair of spheres that meet at 120° const SCALED_TOL: f64 = 1.0e-12; - let gram = { - let mut gram_to_be = PartialMatrix::new(); - gram_to_be.push_sym(0, 0, 1.0); - gram_to_be.push_sym(1, 1, 1.0); - gram_to_be.push_sym(0, 1, 0.5); - gram_to_be - }; - let guess_orig = DMatrix::from_columns(&[ + let mut problem_orig = ConstraintProblem::from_guess(&[ sphere(0.0, 0.0, 0.5, 1.0), sphere(0.0, 0.0, -0.5, 1.0) ]); + problem_orig.gram.push_sym(0, 0, 1.0); + problem_orig.gram.push_sym(1, 1, 1.0); + problem_orig.gram.push_sym(0, 1, 0.5); let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram( - &gram, guess_orig.clone(), &[], - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + &problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(config_orig, guess_orig); + assert_eq!(config_orig, problem_orig.guess); assert_eq!(success_orig, true); assert_eq!(history_orig.scaled_loss.len(), 1); @@ -833,11 +921,15 @@ mod tests { sphere(-a, 0.0, 7.0 - a, 1.0) ]) }; + let problem_tfm = ConstraintProblem { + gram: problem_orig.gram, + guess: guess_tfm, + frozen: problem_orig.frozen + }; let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram( - &gram, guess_tfm.clone(), &[], - SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 + &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(config_tfm, guess_tfm); + assert_eq!(config_tfm, problem_tfm.guess); assert_eq!(success_tfm, true); assert_eq!(history_tfm.scaled_loss.len(), 1); @@ -869,7 +961,7 @@ mod tests { // the comparison tolerance because the transformation seems to // introduce some numerical error const SCALED_TOL_TFM: f64 = 1.0e-9; - let tol_sq = ((guess_orig.nrows() * guess_orig.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM; + let tol_sq = ((problem_orig.guess.nrows() * problem_orig.guess.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM; assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq); } } \ No newline at end of file diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 002baea..2446337 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -1,4 +1,5 @@ use itertools::Itertools; +use std::rc::Rc; use sycamore::prelude::*; use web_sys::{ KeyboardEvent, @@ -9,24 +10,38 @@ use web_sys::{ use crate::{ AppState, assembly, - assembly::{ElementKey, Regulator, RegulatorKey}, + assembly::{ + ElementKey, + HalfCurvatureRegulator, + InversiveDistanceRegulator, + Regulator, + RegulatorKey + }, specified::SpecifiedValue }; // an editable view of a regulator #[component(inline_props)] -fn RegulatorInput(regulator: Regulator) -> View { +fn RegulatorInput(regulator: Rc) -> View { + // get the regulator's measurement and set point signals + let measurement = regulator.measurement(); + let set_point = regulator.set_point(); + + // the `valid` signal tracks whether the last entered value is a valid set + // point specification let valid = create_signal(true); + + // the `value` signal holds the current set point specification let value = create_signal( - regulator.set_point.with_untracked(|set_pt| set_pt.spec.clone()) + set_point.with_untracked(|set_pt| set_pt.spec.clone()) ); - // this closure resets the input value to the regulator's set point - // specification + // this `reset_value` closure resets the input value to the regulator's set + // point specification let reset_value = move || { batch(|| { valid.set(true); - value.set(regulator.set_point.with(|set_pt| set_pt.spec.clone())); + value.set(set_point.with(|set_pt| set_pt.spec.clone())); }) }; @@ -39,7 +54,7 @@ fn RegulatorInput(regulator: Regulator) -> View { r#type="text", class=move || { if valid.get() { - regulator.set_point.with(|set_pt| { + set_point.with(|set_pt| { if set_pt.is_present() { "regulator-input constraint" } else { @@ -50,13 +65,13 @@ fn RegulatorInput(regulator: Regulator) -> View { "regulator-input invalid" } }, - placeholder=regulator.measurement.with(|result| result.to_string()), + placeholder=measurement.with(|result| result.to_string()), bind:value=value, on:change=move |_| { valid.set( match SpecifiedValue::try_from(value.get_clone_untracked()) { Ok(set_pt) => { - regulator.set_point.set(set_pt); + set_point.set(set_pt); true } Err(_) => false @@ -75,26 +90,53 @@ fn RegulatorInput(regulator: Regulator) -> View { } } +pub trait OutlineItem { + fn outline_item(self: Rc, element_key: ElementKey) -> View; +} + +impl OutlineItem for InversiveDistanceRegulator { + fn outline_item(self: Rc, element_key: ElementKey) -> View { + let state = use_context::(); + let other_subject = if self.subjects[0] == element_key { + self.subjects[1] + } else { + self.subjects[0] + }; + let other_subject_label = state.assembly.elements.with( + |elts| elts[other_subject].label.clone() + ); + view! { + li(class="regulator") { + div(class="regulator-label") { (other_subject_label) } + div(class="regulator-type") { "Inversive distance" } + RegulatorInput(regulator=self) + div(class="status") + } + } + } +} + +impl OutlineItem for HalfCurvatureRegulator { + fn outline_item(self: Rc, _element_key: ElementKey) -> View { + view! { + li(class="regulator") { + div(class="regulator-label") // for spacing + div(class="regulator-type") { "Half-curvature" } + RegulatorInput(regulator=self) + div(class="status") + } + } + } +} + // a list item that shows a regulator in an outline view of an element #[component(inline_props)] fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View { let state = use_context::(); - let assembly = &state.assembly; - let regulator = assembly.regulators.with(|regs| regs[regulator_key]); - let other_subject = if regulator.subjects.0 == element_key { - regulator.subjects.1 - } else { - regulator.subjects.0 - }; - let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone()); - view! { - li(class="regulator") { - div(class="regulator-label") { (other_subject_label) } - div(class="regulator-type") { "Inversive distance" } - RegulatorInput(regulator=regulator) - div(class="status") - } - } + let regulator = state.assembly.regulators.with( + |regs| regs[regulator_key].clone() + ); + regulator.outline_item(element_key) } // a list item that shows an element in an outline view of an assembly @@ -117,7 +159,15 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { }; let regulated = element.regulators.map(|regs| regs.len() > 0); let regulator_list = element.regulators.map( - |regs| regs.clone().into_iter().collect() + move |elt_reg_keys| elt_reg_keys + .clone() + .into_iter() + .sorted_by_key( + |®_key| state.assembly.regulators.with( + |regs| regs[reg_key].subjects().len() + ) + ) + .collect() ); let details_node = create_node_ref(); view! { From a2478febc128b218d833bc3e904482f2055865cb Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Thu, 1 May 2025 19:25:13 +0000 Subject: [PATCH 116/126] feat: Points (#82) Replaces the former sole Element entity by two, Sphere and Point, both implementing an Element trait. Adds Point display, uses the former Element display for Sphere. Adds a new "canned" configuration, and the ability to add, select, and nudge Point entities. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/82 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/main.css | 4 +- app-proto/src/add_remove.rs | 116 ++-- app-proto/src/assembly.rs | 435 ++++++++++----- app-proto/src/display.rs | 524 +++++++++++++----- app-proto/src/engine.rs | 1 - app-proto/src/outline.rs | 23 +- app-proto/src/point.frag | 18 + app-proto/src/point.vert | 24 + .../src/{inversive.frag => spheres.frag} | 0 9 files changed, 815 insertions(+), 330 deletions(-) create mode 100644 app-proto/src/point.frag create mode 100644 app-proto/src/point.vert rename app-proto/src/{inversive.frag => spheres.frag} (100%) diff --git a/app-proto/main.css b/app-proto/main.css index 4726a27..f787535 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -42,9 +42,7 @@ body { } #add-remove > button { - width: 32px; height: 32px; - font-size: large; } /* KLUDGE */ @@ -53,7 +51,9 @@ body { buttons need to be displayed in an emoji font */ #add-remove > button.emoji { + width: 32px; font-family: 'Noto Emoji', sans-serif; + font-size: large; } /* outline */ diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index 14fcd41..ea86186 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -1,58 +1,59 @@ +use std::{f64::consts::FRAC_1_SQRT_2, rc::Rc}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ engine, AppState, - assembly::{Assembly, Element, InversiveDistanceRegulator} + assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere} }; /* DEBUG */ // load an example assembly for testing. this code will be removed once we've // built a more formal test assembly system fn load_gen_assemb(assembly: &Assembly) { - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("gemini_a"), String::from("Castor"), [1.00_f32, 0.25_f32, 0.00_f32], engine::sphere(0.5, 0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("gemini_b"), String::from("Pollux"), [0.00_f32, 0.25_f32, 1.00_f32], engine::sphere(-0.5, -0.5, 0.0, 1.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("ursa_major"), String::from("Ursa major"), [0.25_f32, 0.00_f32, 1.00_f32], engine::sphere(-0.5, 0.5, 0.0, 0.75) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("ursa_minor"), String::from("Ursa minor"), [0.25_f32, 1.00_f32, 0.00_f32], engine::sphere(0.5, -0.5, 0.0, 0.5) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("moon_deimos"), String::from("Deimos"), [0.75_f32, 0.75_f32, 0.00_f32], engine::sphere(0.0, 0.15, 1.0, 0.25) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("moon_phobos"), String::from("Phobos"), [0.00_f32, 0.75_f32, 0.50_f32], @@ -66,64 +67,64 @@ fn load_gen_assemb(assembly: &Assembly) { // built a more formal test assembly system fn load_low_curv_assemb(assembly: &Assembly) { let a = 0.75_f64.sqrt(); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "central".to_string(), "Central".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], engine::sphere(0.0, 0.0, 0.0, 1.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "assemb_plane".to_string(), "Assembly plane".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "side1".to_string(), "Side 1".to_string(), [1.00_f32, 0.00_f32, 0.25_f32], engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "side2".to_string(), "Side 2".to_string(), [0.25_f32, 1.00_f32, 0.00_f32], engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "side3".to_string(), "Side 3".to_string(), [0.00_f32, 0.25_f32, 1.00_f32], engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "corner1".to_string(), "Corner 1".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( "corner2".to_string(), "Corner 2".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) ) ); - let _ = assembly.try_insert_sphere( - Element::new( + let _ = assembly.try_insert_element( + Sphere::new( String::from("corner3"), String::from("Corner 3"), [0.75_f32, 0.75_f32, 0.75_f32], @@ -132,6 +133,49 @@ fn load_low_curv_assemb(assembly: &Assembly) { ); } +fn load_pointed_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Point::new( + format!("point_front"), + format!("Front point"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(0.0, 0.0, FRAC_1_SQRT_2) + ) + ); + let _ = assembly.try_insert_element( + Point::new( + format!("point_back"), + format!("Back point"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(0.0, 0.0, -FRAC_1_SQRT_2) + ) + ); + for index_x in 0..=1 { + for index_y in 0..=1 { + let x = index_x as f64 - 0.5; + let y = index_y as f64 - 0.5; + + let _ = assembly.try_insert_element( + Sphere::new( + format!("sphere{index_x}{index_y}"), + format!("Sphere {index_x}{index_y}"), + [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + engine::sphere(x, y, 0.0, 1.0) + ) + ); + + let _ = assembly.try_insert_element( + Point::new( + format!("point{index_x}{index_y}"), + format!("Point {index_x}{index_y}"), + [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + engine::point(x, y, 0.0) + ) + ); + } + } +} + #[component] pub fn AddRemove() -> View { /* DEBUG */ @@ -157,6 +201,7 @@ pub fn AddRemove() -> View { match name.as_str() { "general" => load_gen_assemb(assembly), "low-curv" => load_low_curv_assemb(assembly), + "pointed" => load_pointed_assemb(assembly), _ => () }; }); @@ -167,9 +212,15 @@ pub fn AddRemove() -> View { button( on:click=|_| { let state = use_context::(); - state.assembly.insert_new_sphere(); + state.assembly.insert_element_default::(); } - ) { "+" } + ) { "Add sphere" } + button( + on:click=|_| { + let state = use_context::(); + state.assembly.insert_element_default::(); + } + ) { "Add point" } button( class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button disabled={ @@ -190,7 +241,7 @@ pub fn AddRemove() -> View { .unwrap() ); state.assembly.insert_regulator( - InversiveDistanceRegulator::new(subjects, &state.assembly) + Rc::new(InversiveDistanceRegulator::new(subjects, &state.assembly)) ); state.selection.update(|sel| sel.clear()); } @@ -198,6 +249,7 @@ pub fn AddRemove() -> View { select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser option(value="general") { "General" } option(value="low-curv") { "Low-curvature" } + option(value="pointed") { "Pointed" } option(value="empty") { "Empty" } } } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 5c926ca..343cef8 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,15 +1,23 @@ -use nalgebra::{DMatrix, DVector, DVectorView, Vector3}; +use nalgebra::{DMatrix, DVector, DVectorView}; use rustc_hash::FxHashMap; use slab::Slab; -use std::{collections::BTreeSet, rc::Rc, sync::atomic::{AtomicU64, Ordering}}; +use std::{ + any::{Any, TypeId}, + cell::Cell, + collections::BTreeSet, + rc::Rc, + sync::atomic::{AtomicU64, Ordering} +}; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::{ + display::DisplayItem, engine::{ Q, change_half_curvature, local_unif_to_std, + point, realize_gram, sphere, ConfigSubspace, @@ -33,31 +41,87 @@ pub type ElementColor = [f32; 3]; static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0); pub trait ProblemPoser { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab); + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>); } -#[derive(Clone, PartialEq)] -pub struct Element { +pub trait Element: ProblemPoser + DisplayItem { + // the default identifier for an element of this type + fn default_id() -> String where Self: Sized; + + // create the default example of an element of this type + fn default(id: String, id_num: u64) -> Self where Self: Sized; + + // the regulators that should be created when an element of this type is + // inserted into the given assembly with the given storage key + /* KLUDGE */ + // right now, this organization makes sense because regulators identify + // their subjects by storage key, so the element has to be inserted before + // its regulators can be created. if we change the way regulators identify + // their subjects, we should consider refactoring + fn default_regulators(_key: ElementKey, _assembly: &Assembly) -> Vec> where Self: Sized { + Vec::new() + } + + fn id(&self) -> &String; + fn label(&self) -> &String; + fn representation(&self) -> Signal>; + + // the regulators the element is subject to. the assembly that owns the + // element is responsible for keeping this set up to date + fn regulators(&self) -> Signal>; + + // a serial number that uniquely identifies this element + fn serial(&self) -> u64; + + // take the next serial number, panicking if that was the last one left + fn next_serial() -> u64 where Self: Sized { + // the technique we use to panic on overflow is taken from _Rust Atomics + // and Locks_, by Mara Bos + // + // https://marabos.nl/atomics/atomics.html#example-handle-overflow + // + NEXT_ELEMENT_SERIAL.fetch_update( + Ordering::SeqCst, Ordering::SeqCst, + |serial| serial.checked_add(1) + ).expect("Out of serial numbers for elements") + } + + // the configuration matrix column index that was assigned to the element + // last time the assembly was realized, or `None` if the element has never + // been through a realization + fn column_index(&self) -> Option; + + // assign the element a configuration matrix column index. this method must + // be used carefully to preserve invariant (1), described in the comment on + // the `tangent` field of the `Assembly` structure + fn set_column_index(&self, index: usize); +} + +// the `Element` trait needs to be dyn-compatible, so its method signatures can +// only use `Self` in the type of the receiver. that means `Element` can't +// implement `PartialEq`. if you need partial equivalence for `Element` trait +// objects, use this wrapper +#[derive(Clone)] +pub struct ElementRc(pub Rc); + +impl PartialEq for ElementRc { + fn eq(&self, ElementRc(other): &Self) -> bool { + let ElementRc(rc) = self; + Rc::ptr_eq(rc, &other) + } +} + +pub struct Sphere { pub id: String, pub label: String, pub color: ElementColor, pub representation: Signal>, - - // the regulators this element is subject to. the assembly that owns the - // element is responsible for keeping this set up to date pub regulators: Signal>, - - // a serial number, assigned by `Element::new`, that uniquely identifies - // each element pub serial: u64, - - // the configuration matrix column index that was assigned to this element - // last time the assembly was realized, or `None` if the element has never - // been through a realization - column_index: Option + column_index: Cell> } -impl Element { +impl Sphere { const CURVATURE_COMPONENT: usize = 3; pub fn new( @@ -65,83 +129,161 @@ impl Element { label: String, color: ElementColor, representation: DVector - ) -> Element { - // take the next serial number, panicking if that was the last number we - // had left. the technique we use to panic on overflow is taken from - // _Rust Atomics and Locks_, by Mara Bos - // - // https://marabos.nl/atomics/atomics.html#example-handle-overflow - // - let serial = NEXT_ELEMENT_SERIAL.fetch_update( - Ordering::SeqCst, Ordering::SeqCst, - |serial| serial.checked_add(1) - ).expect("Out of serial numbers for elements"); - - Element { + ) -> Sphere { + Sphere { id: id, label: label, color: color, representation: create_signal(representation), regulators: create_signal(BTreeSet::default()), - serial: serial, - column_index: None - } - } - - // the smallest positive depth, represented as a multiple of `dir`, where - // the line generated by `dir` hits the element (which is assumed to be a - // sphere). returns `None` if the line misses the sphere. this function - // should be kept synchronized with `sphere_cast` in `inversive.frag`, which - // does essentially the same thing on the GPU side - pub fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix) -> Option { - // if `a/b` is less than this threshold, we approximate - // `a*u^2 + b*u + c` by the linear function `b*u + c` - const DEG_THRESHOLD: f64 = 1e-9; - - let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); - let a = -rep[3] * dir.norm_squared(); - let b = rep.rows_range(..3).dot(&dir); - let c = -rep[4]; - - let adjust = 4.0*a*c/(b*b); - if adjust < 1.0 { - // as long as `b` is non-zero, the linear approximation of - // - // a*u^2 + b*u + c - // - // at `u = 0` will reach zero at a finite depth `u_lin`. the root of - // the quadratic adjacent to `u_lin` is stored in `lin_root`. if - // both roots have the same sign, `lin_root` will be the one closer - // to `u = 0` - let square_rect_ratio = 1.0 + (1.0 - adjust).sqrt(); - let lin_root = -(2.0*c)/b / square_rect_ratio; - if a.abs() > DEG_THRESHOLD * b.abs() { - if lin_root > 0.0 { - Some(lin_root) - } else { - let other_root = -b/(2.*a) * square_rect_ratio; - (other_root > 0.0).then_some(other_root) - } - } else { - (lin_root > 0.0).then_some(lin_root) - } - } else { - // the line through `dir` misses the sphere completely - None + serial: Self::next_serial(), + column_index: None.into() } } } -impl ProblemPoser for Element { - fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab) { - let index = self.column_index.expect( - format!("Element \"{}\" should be indexed before writing problem data", self.id).as_str() +impl Element for Sphere { + fn default_id() -> String { + "sphere".to_string() + } + + fn default(id: String, id_num: u64) -> Sphere { + Sphere::new( + id, + format!("Sphere {id_num}"), + [0.75_f32, 0.75_f32, 0.75_f32], + sphere(0.0, 0.0, 0.0, 1.0) + ) + } + + fn default_regulators(key: ElementKey, assembly: &Assembly) -> Vec> { + vec![Rc::new(HalfCurvatureRegulator::new(key, assembly))] + } + + fn id(&self) -> &String { + &self.id + } + + fn label(&self) -> &String { + &self.label + } + + fn representation(&self) -> Signal> { + self.representation + } + + fn regulators(&self) -> Signal> { + self.regulators + } + + fn serial(&self) -> u64 { + self.serial + } + + fn column_index(&self) -> Option { + self.column_index.get() + } + + fn set_column_index(&self, index: usize) { + self.column_index.set(Some(index)); + } +} + +impl ProblemPoser for Sphere { + fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab>) { + let index = self.column_index().expect( + format!("Sphere \"{}\" should be indexed before writing problem data", self.id).as_str() ); problem.gram.push_sym(index, index, 1.0); problem.guess.set_column(index, &self.representation.get_clone_untracked()); } } +pub struct Point { + pub id: String, + pub label: String, + pub color: ElementColor, + pub representation: Signal>, + pub regulators: Signal>, + pub serial: u64, + column_index: Cell> +} + +impl Point { + const WEIGHT_COMPONENT: usize = 3; + + pub fn new( + id: String, + label: String, + color: ElementColor, + representation: DVector + ) -> Point { + Point { + id, + label, + color, + representation: create_signal(representation), + regulators: create_signal(BTreeSet::default()), + serial: Self::next_serial(), + column_index: None.into() + } + } +} + +impl Element for Point { + fn default_id() -> String { + "point".to_string() + } + + fn default(id: String, id_num: u64) -> Point { + Point::new( + id, + format!("Point {id_num}"), + [0.75_f32, 0.75_f32, 0.75_f32], + point(0.0, 0.0, 0.0) + ) + } + + fn id(&self) -> &String { + &self.id + } + + fn label(&self) -> &String { + &self.label + } + + fn representation(&self) -> Signal> { + self.representation + } + + fn regulators(&self) -> Signal> { + self.regulators + } + + fn serial(&self) -> u64 { + self.serial + } + + fn column_index(&self) -> Option { + self.column_index.get() + } + + fn set_column_index(&self, index: usize) { + self.column_index.set(Some(index)); + } +} + +impl ProblemPoser for Point { + fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab>) { + let index = self.column_index().expect( + format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str() + ); + problem.gram.push_sym(index, index, 0.0); + problem.frozen.push(Point::WEIGHT_COMPONENT, index, 0.5); + problem.guess.set_column(index, &self.representation.get_clone_untracked()); + } +} + pub trait Regulator: ProblemPoser + OutlineItem { fn subjects(&self) -> Vec; fn measurement(&self) -> ReadSignal; @@ -168,7 +310,7 @@ impl InversiveDistanceRegulator { pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> InversiveDistanceRegulator { let measurement = assembly.elements.map( move |elts| { - let representations = subjects.map(|subj| elts[subj].representation); + let representations = subjects.map(|subj| elts[subj].representation()); representations[0].with(|rep_0| representations[1].with(|rep_1| rep_0.dot(&(&*Q * rep_1)) @@ -198,11 +340,11 @@ impl Regulator for InversiveDistanceRegulator { } impl ProblemPoser for InversiveDistanceRegulator { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { let [row, col] = self.subjects.map( - |subj| elts[subj].column_index.expect( + |subj| elts[subj].column_index().expect( "Subjects should be indexed before inversive distance regulator writes problem data" ) ); @@ -221,8 +363,8 @@ pub struct HalfCurvatureRegulator { impl HalfCurvatureRegulator { pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator { let measurement = assembly.elements.map( - move |elts| elts[subject].representation.with( - |rep| rep[Element::CURVATURE_COMPONENT] + move |elts| elts[subject].representation().with( + |rep| rep[Sphere::CURVATURE_COMPONENT] ) ); @@ -249,7 +391,7 @@ impl Regulator for HalfCurvatureRegulator { match self.set_point.with(|set_pt| set_pt.value) { Some(half_curv) => { let representation = assembly.elements.with_untracked( - |elts| elts[self.subject].representation + |elts| elts[self.subject].representation() ); representation.update( |rep| change_half_curvature(rep, half_curv) @@ -262,13 +404,13 @@ impl Regulator for HalfCurvatureRegulator { } impl ProblemPoser for HalfCurvatureRegulator { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab) { + fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { - let col = elts[self.subject].column_index.expect( + let col = elts[self.subject].column_index().expect( "Subject should be indexed before half-curvature regulator writes problem data" ); - problem.frozen.push(Element::CURVATURE_COMPONENT, col, val); + problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val); } }); } @@ -286,7 +428,7 @@ type AssemblyMotion<'a> = Vec>; #[derive(Clone)] pub struct Assembly { // elements and regulators - pub elements: Signal>, + pub elements: Signal>>, pub regulators: Signal>>, // solution variety tangent space. the basis vectors are stored in @@ -317,66 +459,61 @@ impl Assembly { // --- inserting elements and regulators --- - // insert a sphere into the assembly without checking whether we already + // insert an element into the assembly without checking whether we already // have an element with the same identifier. any element that does have the // same identifier will get kicked out of the `elements_by_id` index - fn insert_sphere_unchecked(&self, elt: Element) -> ElementKey { - // insert the sphere - let id = elt.id.clone(); - let key = self.elements.update(|elts| elts.insert(elt)); + fn insert_element_unchecked(&self, elt: T) -> ElementKey { + // insert the element + let id = elt.id().clone(); + let key = self.elements.update(|elts| elts.insert(Rc::new(elt))); self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); - // regulate the sphere's curvature - self.insert_regulator(HalfCurvatureRegulator::new(key, &self)); + // create and insert the element's default regulators + for reg in T::default_regulators(key, &self) { + self.insert_regulator(reg); + } key } - pub fn try_insert_sphere(&self, elt: Element) -> Option { + pub fn try_insert_element(&self, elt: impl Element + 'static) -> Option { let can_insert = self.elements_by_id.with_untracked( - |elts_by_id| !elts_by_id.contains_key(&elt.id) + |elts_by_id| !elts_by_id.contains_key(elt.id()) ); if can_insert { - Some(self.insert_sphere_unchecked(elt)) + Some(self.insert_element_unchecked(elt)) } else { None } } - pub fn insert_new_sphere(&self) { + pub fn insert_element_default(&self) { // find the next unused identifier in the default sequence + let default_id = T::default_id(); let mut id_num = 1; - let mut id = format!("sphere{}", id_num); + let mut id = format!("{default_id}{id_num}"); while self.elements_by_id.with_untracked( |elts_by_id| elts_by_id.contains_key(&id) ) { id_num += 1; - id = format!("sphere{}", id_num); + id = format!("{default_id}{id_num}"); } - // create and insert a sphere - let _ = self.insert_sphere_unchecked( - Element::new( - id, - format!("Sphere {}", id_num), - [0.75_f32, 0.75_f32, 0.75_f32], - sphere(0.0, 0.0, 0.0, 1.0) - ) - ); + // create and insert the default example of `T` + let _ = self.insert_element_unchecked(T::default(id, id_num)); } - pub fn insert_regulator(&self, regulator: T) { + pub fn insert_regulator(&self, regulator: Rc) { // add the regulator to the assembly's regulator list - let regulator_rc = Rc::new(regulator); let key = self.regulators.update( - |regs| regs.insert(regulator_rc.clone()) + |regs| regs.insert(regulator.clone()) ); // add the regulator to each subject's regulator list - let subjects = regulator_rc.subjects(); + let subjects = regulator.subjects(); let subject_regulators: Vec<_> = self.elements.with_untracked( |elts| subjects.into_iter().map( - |subj| elts[subj].regulators + |subj| elts[subj].regulators() ).collect() ); for regulators in subject_regulators { @@ -390,10 +527,10 @@ impl Assembly { /* DEBUG */ // log the regulator update console::log_1(&JsValue::from( - format!("Updated regulator with subjects {:?}", regulator_rc.subjects()) + format!("Updated regulator with subjects {:?}", regulator.subjects()) )); - if regulator_rc.try_activate(&self_for_effect) { + if regulator.try_activate(&self_for_effect) { self_for_effect.realize(); } }); @@ -427,7 +564,7 @@ impl Assembly { // index the elements self.elements.update_silent(|elts| { for (index, (_, elt)) in elts.into_iter().enumerate() { - elt.column_index = Some(index); + elt.set_column_index(index); } }); @@ -482,8 +619,8 @@ impl Assembly { if success { // read out the solution for (_, elt) in self.elements.get_clone_untracked() { - elt.representation.update( - |rep| rep.set_column(0, &config.column(elt.column_index.unwrap())) + elt.representation().update( + |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) ); } @@ -521,8 +658,8 @@ impl Assembly { let mut next_column_index = realized_dim; for elt_motion in motion.iter() { let moving_elt = &mut elts[elt_motion.key]; - if moving_elt.column_index.is_none() { - moving_elt.column_index = Some(next_column_index); + if moving_elt.column_index().is_none() { + moving_elt.set_column_index(next_column_index); next_column_index += 1; } } @@ -539,7 +676,7 @@ impl Assembly { // we can unwrap the column index because we know that every moving // element has one at this point let column_index = self.elements.with_untracked( - |elts| elts[elt_motion.key].column_index.unwrap() + |elts| elts[elt_motion.key].column_index().unwrap() ); if column_index < realized_dim { @@ -555,7 +692,7 @@ impl Assembly { let mut target_column = motion_proj.column_mut(column_index); let unif_to_std = self.elements.with_untracked( |elts| { - elts[elt_motion.key].representation.with_untracked( + elts[elt_motion.key].representation().with_untracked( |rep| local_unif_to_std(rep.as_view()) ) } @@ -567,26 +704,27 @@ impl Assembly { // step the assembly along the deformation. this changes the elements' // normalizations, so we restore those afterward /* KLUDGE */ - // since our test assemblies only include spheres, we assume that every - // element is on the 1 mass shell + // for now, we only restore the normalizations of spheres for (_, elt) in self.elements.get_clone_untracked() { - elt.representation.update_silent(|rep| { - match elt.column_index { + elt.representation().update_silent(|rep| { + match elt.column_index() { Some(column_index) => { // step the assembly along the deformation *rep += motion_proj.column(column_index); - // restore normalization by contracting toward the last - // coordinate axis - let q_sp = rep.fixed_rows::<3>(0).norm_squared(); - let half_q_lt = -2.0 * rep[3] * rep[4]; - let half_q_lt_sq = half_q_lt * half_q_lt; - let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); - rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); + if elt.type_id() == TypeId::of::() { + // restore normalization by contracting toward the + // last coordinate axis + let q_sp = rep.fixed_rows::<3>(0).norm_squared(); + let half_q_lt = -2.0 * rep[3] * rep[4]; + let half_q_lt_sq = half_q_lt * half_q_lt; + let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); + rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); + } }, None => { console::log_1(&JsValue::from( - format!("No velocity to unpack for fresh element \"{}\"", elt.id) + format!("No velocity to unpack for fresh element \"{}\"", elt.id()) )) } }; @@ -602,20 +740,14 @@ impl Assembly { #[cfg(test)] mod tests { - use crate::engine; - use super::*; #[test] - #[should_panic(expected = "Element \"sphere\" should be indexed before writing problem data")] + #[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")] fn unindexed_element_test() { let _ = create_root(|| { - Element::new( - "sphere".to_string(), - "Sphere".to_string(), - [1.0_f32, 1.0_f32, 1.0_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0) - ).pose(&mut ConstraintProblem::new(1), &Slab::new()); + let elt = Sphere::default("sphere".to_string(), 0); + elt.pose(&mut ConstraintProblem::new(1), &Slab::new()); }); } @@ -623,18 +755,13 @@ mod tests { #[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")] fn unindexed_subject_test_inversive_distance() { let _ = create_root(|| { - let mut elts = Slab::new(); + let mut elts = Slab::>::new(); let subjects = [0, 1].map(|k| { elts.insert( - Element::new( - format!("sphere{k}"), - format!("Sphere {k}"), - [1.0_f32, 1.0_f32, 1.0_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0) - ) + Rc::new(Sphere::default(format!("sphere{k}"), k)) ) }); - elts[subjects[0]].column_index = Some(0); + elts[subjects[0]].set_column_index(0); InversiveDistanceRegulator { subjects: subjects, measurement: create_memo(|| 0.0), diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 4e0c7e4..51b207d 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -4,17 +4,185 @@ use sycamore::{prelude::*, motion::create_raf}; use web_sys::{ console, window, - Element, KeyboardEvent, MouseEvent, WebGl2RenderingContext, + WebGlBuffer, WebGlProgram, WebGlShader, WebGlUniformLocation, wasm_bindgen::{JsCast, JsValue} }; -use crate::{AppState, assembly::{ElementKey, ElementMotion}}; +use crate::{ + AppState, + assembly::{ElementKey, ElementColor, ElementMotion, Point, Sphere} +}; + +// --- scene data --- + +struct SceneSpheres { + representations: Vec>, + colors: Vec, + highlights: Vec +} + +impl SceneSpheres { + fn new() -> SceneSpheres{ + SceneSpheres { + representations: Vec::new(), + colors: Vec::new(), + highlights: Vec::new() + } + } + + fn len_i32(&self) -> i32 { + self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer") + } + + fn push(&mut self, representation: DVector, color: ElementColor, highlight: f32) { + self.representations.push(representation); + self.colors.push(color); + self.highlights.push(highlight); + } +} + +struct ScenePoints { + representations: Vec>, + colors: Vec, + highlights: Vec, + selections: Vec +} + +impl ScenePoints { + fn new() -> ScenePoints { + ScenePoints { + representations: Vec::new(), + colors: Vec::new(), + highlights: Vec::new(), + selections: Vec::new() + } + } + + fn push(&mut self, representation: DVector, color: ElementColor, highlight: f32, selected: bool) { + self.representations.push(representation); + self.colors.push(color); + self.highlights.push(highlight); + self.selections.push(if selected { 1.0 } else { 0.0 }); + } +} + +pub struct Scene { + spheres: SceneSpheres, + points: ScenePoints +} + +impl Scene { + fn new() -> Scene { + Scene { + spheres: SceneSpheres::new(), + points: ScenePoints::new() + } + } +} + +pub trait DisplayItem { + fn show(&self, scene: &mut Scene, selected: bool); + + // the smallest positive depth, represented as a multiple of `dir`, where + // the line generated by `dir` hits the element. returns `None` if the line + // misses the element + fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, pixel_size: f64) -> Option; +} + +impl DisplayItem for Sphere { + fn show(&self, scene: &mut Scene, selected: bool) { + const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ + let representation = self.representation.get_clone_untracked(); + let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; + let highlight = if selected { 1.0 } else { HIGHLIGHT }; + scene.spheres.push(representation, color, highlight); + } + + // this method should be kept synchronized with `sphere_cast` in + // `spheres.frag`, which does essentially the same thing on the GPU side + fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, _pixel_size: f64) -> Option { + // if `a/b` is less than this threshold, we approximate + // `a*u^2 + b*u + c` by the linear function `b*u + c` + const DEG_THRESHOLD: f64 = 1e-9; + + let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); + let a = -rep[3] * dir.norm_squared(); + let b = rep.rows_range(..3).dot(&dir); + let c = -rep[4]; + + let adjust = 4.0*a*c/(b*b); + if adjust < 1.0 { + // as long as `b` is non-zero, the linear approximation of + // + // a*u^2 + b*u + c + // + // at `u = 0` will reach zero at a finite depth `u_lin`. the root of + // the quadratic adjacent to `u_lin` is stored in `lin_root`. if + // both roots have the same sign, `lin_root` will be the one closer + // to `u = 0` + let square_rect_ratio = 1.0 + (1.0 - adjust).sqrt(); + let lin_root = -(2.0*c)/b / square_rect_ratio; + if a.abs() > DEG_THRESHOLD * b.abs() { + if lin_root > 0.0 { + Some(lin_root) + } else { + let other_root = -b/(2.*a) * square_rect_ratio; + (other_root > 0.0).then_some(other_root) + } + } else { + (lin_root > 0.0).then_some(lin_root) + } + } else { + // the line through `dir` misses the sphere completely + None + } + } +} + +impl DisplayItem for Point { + fn show(&self, scene: &mut Scene, selected: bool) { + const HIGHLIGHT: f32 = 0.5; /* SCAFFOLDING */ + let representation = self.representation.get_clone_untracked(); + let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; + let highlight = if selected { 1.0 } else { HIGHLIGHT }; + scene.points.push(representation, color, highlight, selected); + } + + /* SCAFFOLDING */ + fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, pixel_size: f64) -> Option { + let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); + if rep[2] < 0.0 { + // this constant should be kept synchronized with `point.frag` + const POINT_RADIUS_PX: f64 = 4.0; + + // find the radius of the point in screen projection units + let point_radius_proj = POINT_RADIUS_PX * pixel_size; + + // find the squared distance between the screen projections of the + // ray and the point + let dir_proj = -dir.fixed_rows::<2>(0) / dir[2]; + let rep_proj = -rep.fixed_rows::<2>(0) / rep[2]; + let dist_sq = (dir_proj - rep_proj).norm_squared(); + + // if the ray hits the point, return its depth + if dist_sq < point_radius_proj * point_radius_proj { + Some(rep[2] / dir[2]) + } else { + None + } + } else { + None + } + } +} + +// --- WebGL utilities --- fn compile_shader( context: &WebGl2RenderingContext, @@ -27,6 +195,45 @@ fn compile_shader( shader } +fn set_up_program( + context: &WebGl2RenderingContext, + vertex_shader_source: &str, + fragment_shader_source: &str +) -> WebGlProgram { + // compile the shaders + let vertex_shader = compile_shader( + &context, + WebGl2RenderingContext::VERTEX_SHADER, + vertex_shader_source, + ); + let fragment_shader = compile_shader( + &context, + WebGl2RenderingContext::FRAGMENT_SHADER, + fragment_shader_source, + ); + + // create the program and attach the shaders + let program = context.create_program().unwrap(); + context.attach_shader(&program, &vertex_shader); + context.attach_shader(&program, &fragment_shader); + context.link_program(&program); + + /* DEBUG */ + // report whether linking succeeded + let link_status = context + .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS) + .as_bool() + .unwrap(); + let link_msg = if link_status { + "Linked successfully" + } else { + "Linking failed" + }; + console::log_1(&JsValue::from(link_msg)); + + program +} + fn get_uniform_array_locations( context: &WebGl2RenderingContext, program: &WebGlProgram, @@ -42,22 +249,39 @@ fn get_uniform_array_locations( }) } -// load the given data into the vertex input of the given name -fn bind_vertex_attrib( +// bind the given vertex buffer object to the given vertex attribute +fn bind_to_attribute( context: &WebGl2RenderingContext, - index: u32, - size: i32, - data: &[f32] + attr_index: u32, + attr_size: i32, + buffer: &Option ) { - // create a data buffer and bind it to ARRAY_BUFFER - let buffer = context.create_buffer().unwrap(); - context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer)); + context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref()); + context.vertex_attrib_pointer_with_i32( + attr_index, + attr_size, + WebGl2RenderingContext::FLOAT, + false, // don't normalize + 0, // zero stride + 0, // zero offset + ); +} + +// load the given data into a new vertex buffer object +fn load_new_buffer( + context: &WebGl2RenderingContext, + data: &[f32] +) -> Option { + // create a buffer and bind it to ARRAY_BUFFER + let buffer = context.create_buffer(); + context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref()); - // load the given data into the buffer. the function `Float32Array::view` - // creates a raw view into our module's `WebAssembly.Memory` buffer. - // allocating more memory will change the buffer, invalidating the view. - // that means we have to make sure we don't allocate any memory until the - // view is dropped + // load the given data into the buffer. this block is unsafe because + // `Float32Array::view` creates a raw view into our module's + // `WebAssembly.Memory` buffer. allocating more memory will change the + // buffer, invalidating the view, so we have to make sure we don't allocate + // any memory until the view is dropped. we're okay here because the view is + // used as soon as it's created unsafe { context.buffer_data_with_array_buffer_view( WebGl2RenderingContext::ARRAY_BUFFER, @@ -66,42 +290,43 @@ fn bind_vertex_attrib( ); } - // allow the target attribute to be used - context.enable_vertex_attrib_array(index); - - // take whatever's bound to ARRAY_BUFFER---here, the data buffer created - // above---and bind it to the target attribute - // - // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer - // - context.vertex_attrib_pointer_with_i32( - index, - size, - WebGl2RenderingContext::FLOAT, - false, // don't normalize - 0, // zero stride - 0, // zero offset - ); + buffer +} + +fn bind_new_buffer_to_attribute( + context: &WebGl2RenderingContext, + attr_index: u32, + attr_size: i32, + data: &[f32] +) { + let buffer = load_new_buffer(context, data); + bind_to_attribute(context, attr_index, attr_size, &buffer); } // the direction in camera space that a mouse event is pointing along -fn event_dir(event: &MouseEvent) -> Vector3 { - let target: Element = event.target().unwrap().unchecked_into(); +fn event_dir(event: &MouseEvent) -> (Vector3, f64) { + let target: web_sys::Element = event.target().unwrap().unchecked_into(); let rect = target.get_bounding_client_rect(); let width = rect.width(); let height = rect.height(); let shortdim = width.min(height); - // this constant should be kept synchronized with `inversive.frag` + // this constant should be kept synchronized with `spheres.frag` and + // `point.vert` const FOCAL_SLOPE: f64 = 0.3; - Vector3::new( - FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim, - FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim, - -1.0 + ( + Vector3::new( + FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim, + FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim, + -1.0 + ), + FOCAL_SLOPE * 2.0 / shortdim ) } +// --- display component --- + #[component] pub fn Display() -> View { let state = use_context::(); @@ -138,7 +363,7 @@ pub fn Display() -> View { create_effect(move || { state.assembly.elements.with(|elts| { for (_, elt) in elts { - elt.representation.track(); + elt.representation().track(); } }); state.selection.track(); @@ -170,7 +395,6 @@ pub fn Display() -> View { // display parameters const OPACITY: f32 = 0.5; /* SCAFFOLDING */ - const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ const LAYER_THRESHOLD: i32 = 0; /* DEBUG */ const DEBUG_MODE: i32 = 0; /* DEBUG */ @@ -186,32 +410,26 @@ pub fn Display() -> View { .dyn_into::() .unwrap(); - // compile and attach the vertex and fragment shaders - let vertex_shader = compile_shader( + // disable depth testing + ctx.disable(WebGl2RenderingContext::DEPTH_TEST); + + // set blend mode + ctx.enable(WebGl2RenderingContext::BLEND); + ctx.blend_func(WebGl2RenderingContext::SRC_ALPHA, WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA); + + // set up the sphere rendering program + let sphere_program = set_up_program( &ctx, - WebGl2RenderingContext::VERTEX_SHADER, include_str!("identity.vert"), + include_str!("spheres.frag") ); - let fragment_shader = compile_shader( + + // set up the point rendering program + let point_program = set_up_program( &ctx, - WebGl2RenderingContext::FRAGMENT_SHADER, - include_str!("inversive.frag"), + include_str!("point.vert"), + include_str!("point.frag") ); - let program = ctx.create_program().unwrap(); - ctx.attach_shader(&program, &vertex_shader); - ctx.attach_shader(&program, &fragment_shader); - ctx.link_program(&program); - let link_status = ctx - .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS) - .as_bool() - .unwrap(); - let link_msg = if link_status { - "Linked successfully" - } else { - "Linking failed" - }; - console::log_1(&JsValue::from(link_msg)); - ctx.use_program(Some(&program)); /* DEBUG */ // print the maximum number of vectors that can be passed as @@ -230,35 +448,33 @@ pub fn Display() -> View { &JsValue::from("uniform vectors available") ); - // find indices of vertex attributes and uniforms + // find the sphere program's vertex attribute + let viewport_position_attr = ctx.get_attrib_location(&sphere_program, "position") as u32; + + // find the sphere program's uniforms const SPHERE_MAX: usize = 200; - let position_index = ctx.get_attrib_location(&program, "position") as u32; - let sphere_cnt_loc = ctx.get_uniform_location(&program, "sphere_cnt"); + let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt"); let sphere_sp_locs = get_uniform_array_locations::( - &ctx, &program, "sphere_list", Some("sp") + &ctx, &sphere_program, "sphere_list", Some("sp") ); let sphere_lt_locs = get_uniform_array_locations::( - &ctx, &program, "sphere_list", Some("lt") + &ctx, &sphere_program, "sphere_list", Some("lt") ); - let color_locs = get_uniform_array_locations::( - &ctx, &program, "color_list", None + let sphere_color_locs = get_uniform_array_locations::( + &ctx, &sphere_program, "color_list", None ); - let highlight_locs = get_uniform_array_locations::( - &ctx, &program, "highlight_list", None + let sphere_highlight_locs = get_uniform_array_locations::( + &ctx, &sphere_program, "highlight_list", None ); - let resolution_loc = ctx.get_uniform_location(&program, "resolution"); - let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); - let opacity_loc = ctx.get_uniform_location(&program, "opacity"); - let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); - let debug_mode_loc = ctx.get_uniform_location(&program, "debug_mode"); + let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution"); + let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim"); + let opacity_loc = ctx.get_uniform_location(&sphere_program, "opacity"); + let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold"); + let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode"); - // create a vertex array and bind it to the graphics context - let vertex_array = ctx.create_vertex_array().unwrap(); - ctx.bind_vertex_array(Some(&vertex_array)); - - // set the vertex positions + // load the viewport vertex positions into a new vertex buffer object const VERTEX_CNT: usize = 6; - let positions: [f32; 3*VERTEX_CNT] = [ + let viewport_positions: [f32; 3*VERTEX_CNT] = [ // northwest triangle -1.0, -1.0, 0.0, -1.0, 1.0, 0.0, @@ -268,7 +484,13 @@ pub fn Display() -> View { 1.0, 1.0, 0.0, 1.0, -1.0, 0.0 ]; - bind_vertex_attrib(&ctx, position_index, 3, &positions); + let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions); + + // find the point program's vertex attributes + let point_position_attr = ctx.get_attrib_location(&point_program, "position") as u32; + let point_color_attr = ctx.get_attrib_location(&point_program, "color") as u32; + let point_highlight_attr = ctx.get_attrib_location(&point_program, "highlight") as u32; + let point_selection_attr = ctx.get_attrib_location(&point_program, "selected") as u32; // set up a repainting routine let (_, start_animation_loop, _) = create_raf(move || { @@ -362,6 +584,9 @@ pub fn Display() -> View { } if scene_changed.get() { + const SPACE_DIM: usize = 3; + const COLOR_SIZE: usize = 3; + /* INSTRUMENTS */ // measure mean frame interval frames_since_last_sample += 1; @@ -371,6 +596,10 @@ pub fn Display() -> View { frames_since_last_sample = 0; } + // --- get the assembly --- + + let mut scene = Scene::new(); + // find the map from assembly space to world space let location = { let u = -location_z; @@ -384,41 +613,27 @@ pub fn Display() -> View { }; let asm_to_world = &location * &orientation; - // get the assembly - let ( - elt_cnt, - reps_world, - colors, - highlights - ) = state.assembly.elements.with(|elts| { - ( - // number of elements - elts.len() as i32, - - // representation vectors in world coordinates - elts.iter().map( - |(_, elt)| elt.representation.with(|rep| &asm_to_world * rep) - ).collect::>(), - - // colors - elts.iter().map(|(key, elt)| { - if state.selection.with(|sel| sel.contains(&key)) { - elt.color.map(|ch| 0.2 + 0.8*ch) - } else { - elt.color - } - }).collect::>(), - - // highlight levels - elts.iter().map(|(key, _)| { - if state.selection.with(|sel| sel.contains(&key)) { - 1.0_f32 - } else { - HIGHLIGHT - } - }).collect::>() - ) - }); + // set up the scene + state.assembly.elements.with_untracked( + |elts| for (key, elt) in elts { + let selected = state.selection.with(|sel| sel.contains(&key)); + elt.show(&mut scene, selected); + } + ); + let sphere_cnt = scene.spheres.len_i32(); + + // --- draw the spheres --- + + // use the sphere rendering program + ctx.use_program(Some(&sphere_program)); + + // enable the sphere program's vertex attribute + ctx.enable_vertex_attrib_array(viewport_position_attr); + + // write the spheres in world coordinates + let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map( + |rep| (&asm_to_world * rep).cast::() + ).collect(); // set the resolution let width = canvas.width() as f32; @@ -426,25 +641,25 @@ pub fn Display() -> View { ctx.uniform2f(resolution_loc.as_ref(), width, height); ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); - // pass the assembly - ctx.uniform1i(sphere_cnt_loc.as_ref(), elt_cnt); - for n in 0..reps_world.len() { - let v = &reps_world[n]; - ctx.uniform3f( + // pass the scene data + ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt); + for n in 0..sphere_reps_world.len() { + let v = &sphere_reps_world[n]; + ctx.uniform3fv_with_f32_array( sphere_sp_locs[n].as_ref(), - v[0] as f32, v[1] as f32, v[2] as f32 + v.rows(0, 3).as_slice() ); - ctx.uniform2f( + ctx.uniform2fv_with_f32_array( sphere_lt_locs[n].as_ref(), - v[3] as f32, v[4] as f32 + v.rows(3, 2).as_slice() ); ctx.uniform3fv_with_f32_array( - color_locs[n].as_ref(), - &colors[n] + sphere_color_locs[n].as_ref(), + &scene.spheres.colors[n] ); ctx.uniform1f( - highlight_locs[n].as_ref(), - highlights[n] + sphere_highlight_locs[n].as_ref(), + scene.spheres.highlights[n] ); } @@ -453,9 +668,56 @@ pub fn Display() -> View { ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); + // bind the viewport vertex position buffer to the position + // attribute in the vertex shader + bind_to_attribute(&ctx, viewport_position_attr, SPACE_DIM as i32, &viewport_position_buffer); + // draw the scene ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); + // disable the sphere program's vertex attribute + ctx.disable_vertex_attrib_array(viewport_position_attr); + + // --- draw the points --- + + if !scene.points.representations.is_empty() { + // use the point rendering program + ctx.use_program(Some(&point_program)); + + // enable the point program's vertex attributes + ctx.enable_vertex_attrib_array(point_position_attr); + ctx.enable_vertex_attrib_array(point_color_attr); + ctx.enable_vertex_attrib_array(point_highlight_attr); + ctx.enable_vertex_attrib_array(point_selection_attr); + + // write the points in world coordinates + let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM); + let point_positions = DMatrix::from_columns( + &scene.points.representations.into_iter().map( + |rep| &asm_to_world_sp * rep + ).collect::>().as_slice() + ).cast::(); + + // load the point positions and colors into new buffers and + // bind them to the corresponding attributes in the vertex + // shader + bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice()); + bind_new_buffer_to_attribute(&ctx, point_color_attr, COLOR_SIZE as i32, scene.points.colors.concat().as_slice()); + bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice()); + bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice()); + + // draw the scene + ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32); + + // disable the point program's vertex attributes + ctx.disable_vertex_attrib_array(point_position_attr); + ctx.disable_vertex_attrib_array(point_color_attr); + ctx.disable_vertex_attrib_array(point_highlight_attr); + ctx.disable_vertex_attrib_array(point_selection_attr); + } + + // --- update the display state --- + // update the viewpoint assembly_to_world.set(asm_to_world); @@ -585,11 +847,11 @@ pub fn Display() -> View { }, on:click=move |event: MouseEvent| { // find the nearest element along the pointer direction - let dir = event_dir(&event); + let (dir, pixel_size) = event_dir(&event); console::log_1(&JsValue::from(dir.to_string())); let mut clicked: Option<(ElementKey, f64)> = None; for (key, elt) in state.assembly.elements.get_clone_untracked() { - match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world)) { + match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) { Some(depth) => match clicked { Some((_, best_depth)) => { if depth < best_depth { diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 869a7de..b0fa23d 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -4,7 +4,6 @@ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ // --- elements --- -#[cfg(feature = "dev")] pub fn point(x: f64, y: f64, z: f64) -> DVector { DVector::from_column_slice(&[x, y, z, 0.5, 0.5*(x*x + y*y + z*z)]) } diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 2446337..2893b6d 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -9,9 +9,10 @@ use web_sys::{ use crate::{ AppState, - assembly, assembly::{ + Element, ElementKey, + ElementRc, HalfCurvatureRegulator, InversiveDistanceRegulator, Regulator, @@ -103,7 +104,7 @@ impl OutlineItem for InversiveDistanceRegulator { self.subjects[0] }; let other_subject_label = state.assembly.elements.with( - |elts| elts[other_subject].label.clone() + |elts| elts[other_subject].label().clone() ); view! { li(class="regulator") { @@ -141,14 +142,15 @@ fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> // a list item that shows an element in an outline view of an assembly #[component(inline_props)] -fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { +fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { let state = use_context::(); let class = state.selection.map( move |sel| if sel.contains(&key) { "selected" } else { "" } ); - let label = element.label.clone(); + let label = element.label().clone(); + let representation = element.representation().clone(); let rep_components = move || { - element.representation.with( + representation.with( |rep| rep.iter().map( |u| { let u_str = format!("{:.3}", u).replace("-", "\u{2212}"); @@ -157,8 +159,8 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View { ).collect::>() ) }; - let regulated = element.regulators.map(|regs| regs.len() > 0); - let regulator_list = element.regulators.map( + let regulated = element.regulators().map(|regs| regs.len() > 0); + let regulator_list = element.regulators().map( move |elt_reg_keys| elt_reg_keys .clone() .into_iter() @@ -261,7 +263,8 @@ pub fn Outline() -> View { |elts| elts .clone() .into_iter() - .sorted_by_key(|(_, elt)| elt.id.clone()) + .sorted_by_key(|(_, elt)| elt.id().clone()) + .map(|(key, elt)| (key, ElementRc(elt))) .collect() ); @@ -275,10 +278,10 @@ pub fn Outline() -> View { ) { Keyed( list=element_list, - view=|(key, elt)| view! { + view=|(key, ElementRc(elt))| view! { ElementOutlineItem(key=key, element=elt) }, - key=|(_, elt)| elt.serial + key=|(_, ElementRc(elt))| elt.serial() ) } } diff --git a/app-proto/src/point.frag b/app-proto/src/point.frag new file mode 100644 index 0000000..3a361a8 --- /dev/null +++ b/app-proto/src/point.frag @@ -0,0 +1,18 @@ +#version 300 es + +precision highp float; + +in vec3 point_color; +in float point_highlight; +in float total_radius; + +out vec4 outColor; + +void main() { + float r = total_radius * length(2.*gl_PointCoord - vec2(1.)); + + const float POINT_RADIUS = 4.; + float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r); + vec3 color = mix(point_color, vec3(1.), border * point_highlight); + outColor = vec4(color, 1. - smoothstep(total_radius - 1., total_radius, r)); +} \ No newline at end of file diff --git a/app-proto/src/point.vert b/app-proto/src/point.vert new file mode 100644 index 0000000..6945010 --- /dev/null +++ b/app-proto/src/point.vert @@ -0,0 +1,24 @@ +#version 300 es + +in vec4 position; +in vec3 color; +in float highlight; +in float selected; + +out vec3 point_color; +out float point_highlight; +out float total_radius; + +// camera +const float focal_slope = 0.3; + +void main() { + total_radius = 5. + 0.5*selected; + + float depth = -focal_slope * position.z; + gl_Position = vec4(position.xy / depth, 0., 1.); + gl_PointSize = 2.*total_radius; + + point_color = color; + point_highlight = highlight; +} \ No newline at end of file diff --git a/app-proto/src/inversive.frag b/app-proto/src/spheres.frag similarity index 100% rename from app-proto/src/inversive.frag rename to app-proto/src/spheres.frag From 2adf4669f47ab8f2bff8d64ac011a2bd09f632fa Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Tue, 6 May 2025 19:17:30 +0000 Subject: [PATCH 117/126] Refactor: Use pointers to refer to elements and regulators (#84) Previously, dyna3 used storage keys to refer to elements, necessitating passing around element containers to various functions so that they could access the relevant elements. These storage keys have been replaced with reference-counted pointers, used for tasks like these: - Specifying the subjects of regulators. - Collecting the regulators each element is subject to - Handling selection. - Creating interface components. Also, systematizes the handling of serial numbers for entities, through a Serial trait. And updates to rust 1.86 and institutes explicit checking of the rust version. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/84 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- .../workflows/continuous-integration.yaml | 2 +- app-proto/Cargo.lock | 19 +- app-proto/Cargo.toml | 3 +- app-proto/src/add_remove.rs | 2 +- app-proto/src/assembly.rs | 396 ++++++++++-------- app-proto/src/display.rs | 23 +- app-proto/src/main.rs | 21 +- app-proto/src/outline.rs | 92 ++-- 8 files changed, 288 insertions(+), 270 deletions(-) diff --git a/.forgejo/workflows/continuous-integration.yaml b/.forgejo/workflows/continuous-integration.yaml index daf8923..f3b0130 100644 --- a/.forgejo/workflows/continuous-integration.yaml +++ b/.forgejo/workflows/continuous-integration.yaml @@ -11,7 +11,7 @@ jobs: test: runs-on: docker container: - image: cimg/rust:1.85-node + image: cimg/rust:1.86-node defaults: run: # set the default working directory for each `run` step, relative to the diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 9738589..3bf609c 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ahash" @@ -89,8 +89,6 @@ dependencies = [ "lazy_static", "nalgebra", "readonly", - "rustc-hash", - "slab", "sycamore", "wasm-bindgen-test", "web-sys", @@ -365,12 +363,6 @@ dependencies = [ "syn", ] -[[package]] -name = "rustc-hash" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" - [[package]] name = "safe_arch" version = "0.7.2" @@ -414,15 +406,6 @@ dependencies = [ "wide", ] -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - [[package]] name = "slotmap" version = "1.0.7" diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 5ab7299..844a0a6 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -3,6 +3,7 @@ name = "dyna3" version = "0.1.0" authors = ["Aaron Fenyes", "Glen Whitney"] edition = "2021" +rust-version = "1.86" [features] default = ["console_error_panic_hook"] @@ -14,8 +15,6 @@ js-sys = "0.3.70" lazy_static = "1.5.0" nalgebra = "0.33.0" readonly = "0.2.12" -rustc-hash = "2.0.0" -slab = "0.4.9" sycamore = "0.9.0-beta.3" # The `console_error_panic_hook` crate provides better debugging of panics by diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index ea86186..f3bbc97 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -241,7 +241,7 @@ pub fn AddRemove() -> View { .unwrap() ); state.assembly.insert_regulator( - Rc::new(InversiveDistanceRegulator::new(subjects, &state.assembly)) + Rc::new(InversiveDistanceRegulator::new(subjects)) ); state.selection.update(|sel| sel.clear()); } diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 343cef8..bd185c8 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,12 +1,14 @@ use nalgebra::{DMatrix, DVector, DVectorView}; -use rustc_hash::FxHashMap; -use slab::Slab; use std::{ any::{Any, TypeId}, cell::Cell, - collections::BTreeSet, + collections::{BTreeMap, BTreeSet}, + cmp::Ordering, + fmt, + fmt::{Debug, Formatter}, + hash::{Hash, Hasher}, rc::Rc, - sync::atomic::{AtomicU64, Ordering} + sync::{atomic, atomic::AtomicU64} }; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ @@ -27,49 +29,16 @@ use crate::{ specified::SpecifiedValue }; -// the types of the keys we use to access an assembly's elements and regulators -pub type ElementKey = usize; -pub type RegulatorKey = usize; - pub type ElementColor = [f32; 3]; /* KLUDGE */ // we should reconsider this design when we build a system for switching between // assemblies. at that point, we might want to switch to hierarchical keys, -// where each each element has a key that identifies it within its assembly and +// where each each item has a key that identifies it within its assembly and // each assembly has a key that identifies it within the sesssion -static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0); +static NEXT_SERIAL: AtomicU64 = AtomicU64::new(0); -pub trait ProblemPoser { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>); -} - -pub trait Element: ProblemPoser + DisplayItem { - // the default identifier for an element of this type - fn default_id() -> String where Self: Sized; - - // create the default example of an element of this type - fn default(id: String, id_num: u64) -> Self where Self: Sized; - - // the regulators that should be created when an element of this type is - // inserted into the given assembly with the given storage key - /* KLUDGE */ - // right now, this organization makes sense because regulators identify - // their subjects by storage key, so the element has to be inserted before - // its regulators can be created. if we change the way regulators identify - // their subjects, we should consider refactoring - fn default_regulators(_key: ElementKey, _assembly: &Assembly) -> Vec> where Self: Sized { - Vec::new() - } - - fn id(&self) -> &String; - fn label(&self) -> &String; - fn representation(&self) -> Signal>; - - // the regulators the element is subject to. the assembly that owns the - // element is responsible for keeping this set up to date - fn regulators(&self) -> Signal>; - +pub trait Serial { // a serial number that uniquely identifies this element fn serial(&self) -> u64; @@ -80,11 +49,62 @@ pub trait Element: ProblemPoser + DisplayItem { // // https://marabos.nl/atomics/atomics.html#example-handle-overflow // - NEXT_ELEMENT_SERIAL.fetch_update( - Ordering::SeqCst, Ordering::SeqCst, + NEXT_SERIAL.fetch_update( + atomic::Ordering::SeqCst, atomic::Ordering::SeqCst, |serial| serial.checked_add(1) ).expect("Out of serial numbers for elements") } +} + +impl Hash for dyn Serial { + fn hash(&self, state: &mut H) { + self.serial().hash(state) + } +} + +impl PartialEq for dyn Serial { + fn eq(&self, other: &Self) -> bool { + self.serial() == other.serial() + } +} + +impl Eq for dyn Serial {} + +impl PartialOrd for dyn Serial { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for dyn Serial { + fn cmp(&self, other: &Self) -> Ordering { + self.serial().cmp(&other.serial()) + } +} + +pub trait ProblemPoser { + fn pose(&self, problem: &mut ConstraintProblem); +} + +pub trait Element: Serial + ProblemPoser + DisplayItem { + // the default identifier for an element of this type + fn default_id() -> String where Self: Sized; + + // the default example of an element of this type + fn default(id: String, id_num: u64) -> Self where Self: Sized; + + // the default regulators that come with this element + fn default_regulators(self: Rc) -> Vec> { + Vec::new() + } + + fn id(&self) -> &String; + fn label(&self) -> &String; + fn representation(&self) -> Signal>; + + // the regulators the element is subject to. the assembly that owns the + // element is responsible for keeping this set up to date + fn regulators(&self) -> Signal>>; // the configuration matrix column index that was assigned to the element // last time the assembly was realized, or `None` if the element has never @@ -97,17 +117,35 @@ pub trait Element: ProblemPoser + DisplayItem { fn set_column_index(&self, index: usize); } -// the `Element` trait needs to be dyn-compatible, so its method signatures can -// only use `Self` in the type of the receiver. that means `Element` can't -// implement `PartialEq`. if you need partial equivalence for `Element` trait -// objects, use this wrapper -#[derive(Clone)] -pub struct ElementRc(pub Rc); +impl Debug for dyn Element { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { + self.id().fmt(f) + } +} -impl PartialEq for ElementRc { - fn eq(&self, ElementRc(other): &Self) -> bool { - let ElementRc(rc) = self; - Rc::ptr_eq(rc, &other) +impl Hash for dyn Element { + fn hash(&self, state: &mut H) { + ::hash(self, state) + } +} + +impl PartialEq for dyn Element { + fn eq(&self, other: &Self) -> bool { + ::eq(self, other) + } +} + +impl Eq for dyn Element {} + +impl PartialOrd for dyn Element { + fn partial_cmp(&self, other: &Self) -> Option { + ::partial_cmp(self, other) + } +} + +impl Ord for dyn Element { + fn cmp(&self, other: &Self) -> Ordering { + ::cmp(self, other) } } @@ -116,8 +154,8 @@ pub struct Sphere { pub label: String, pub color: ElementColor, pub representation: Signal>, - pub regulators: Signal>, - pub serial: u64, + pub regulators: Signal>>, + serial: u64, column_index: Cell> } @@ -135,7 +173,7 @@ impl Sphere { label: label, color: color, representation: create_signal(representation), - regulators: create_signal(BTreeSet::default()), + regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), column_index: None.into() } @@ -156,8 +194,8 @@ impl Element for Sphere { ) } - fn default_regulators(key: ElementKey, assembly: &Assembly) -> Vec> { - vec![Rc::new(HalfCurvatureRegulator::new(key, assembly))] + fn default_regulators(self: Rc) -> Vec> { + vec![Rc::new(HalfCurvatureRegulator::new(self))] } fn id(&self) -> &String { @@ -172,14 +210,10 @@ impl Element for Sphere { self.representation } - fn regulators(&self) -> Signal> { + fn regulators(&self) -> Signal>> { self.regulators } - fn serial(&self) -> u64 { - self.serial - } - fn column_index(&self) -> Option { self.column_index.get() } @@ -189,8 +223,14 @@ impl Element for Sphere { } } +impl Serial for Sphere { + fn serial(&self) -> u64 { + self.serial + } +} + impl ProblemPoser for Sphere { - fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab>) { + fn pose(&self, problem: &mut ConstraintProblem) { let index = self.column_index().expect( format!("Sphere \"{}\" should be indexed before writing problem data", self.id).as_str() ); @@ -204,8 +244,8 @@ pub struct Point { pub label: String, pub color: ElementColor, pub representation: Signal>, - pub regulators: Signal>, - pub serial: u64, + pub regulators: Signal>>, + serial: u64, column_index: Cell> } @@ -223,7 +263,7 @@ impl Point { label, color, representation: create_signal(representation), - regulators: create_signal(BTreeSet::default()), + regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), column_index: None.into() } @@ -256,14 +296,10 @@ impl Element for Point { self.representation } - fn regulators(&self) -> Signal> { + fn regulators(&self) -> Signal>> { self.regulators } - fn serial(&self) -> u64 { - self.serial - } - fn column_index(&self) -> Option { self.column_index.get() } @@ -273,8 +309,14 @@ impl Element for Point { } } +impl Serial for Point { + fn serial(&self) -> u64 { + self.serial + } +} + impl ProblemPoser for Point { - fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab>) { + fn pose(&self, problem: &mut ConstraintProblem) { let index = self.column_index().expect( format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str() ); @@ -284,8 +326,8 @@ impl ProblemPoser for Point { } } -pub trait Regulator: ProblemPoser + OutlineItem { - fn subjects(&self) -> Vec; +pub trait Regulator: Serial + ProblemPoser + OutlineItem { + fn subjects(&self) -> Vec>; fn measurement(&self) -> ReadSignal; fn set_point(&self) -> Signal; @@ -295,39 +337,65 @@ pub trait Regulator: ProblemPoser + OutlineItem { // preconditioning when the set point is present, and use its return value // to report whether the set is present. the default implementation does no // preconditioning - fn try_activate(&self, _assembly: &Assembly) -> bool { + fn try_activate(&self) -> bool { self.set_point().with(|set_pt| set_pt.is_present()) } } +impl Hash for dyn Regulator { + fn hash(&self, state: &mut H) { + ::hash(self, state) + } +} + +impl PartialEq for dyn Regulator { + fn eq(&self, other: &Self) -> bool { + ::eq(self, other) + } +} + +impl Eq for dyn Regulator {} + +impl PartialOrd for dyn Regulator { + fn partial_cmp(&self, other: &Self) -> Option { + ::partial_cmp(self, other) + } +} + +impl Ord for dyn Regulator { + fn cmp(&self, other: &Self) -> Ordering { + ::cmp(self, other) + } +} + pub struct InversiveDistanceRegulator { - pub subjects: [ElementKey; 2], + pub subjects: [Rc; 2], pub measurement: ReadSignal, - pub set_point: Signal + pub set_point: Signal, + serial: u64 } impl InversiveDistanceRegulator { - pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> InversiveDistanceRegulator { - let measurement = assembly.elements.map( - move |elts| { - let representations = subjects.map(|subj| elts[subj].representation()); - representations[0].with(|rep_0| - representations[1].with(|rep_1| - rep_0.dot(&(&*Q * rep_1)) - ) + pub fn new(subjects: [Rc; 2]) -> InversiveDistanceRegulator { + let representations = subjects.each_ref().map(|subj| subj.representation()); + let measurement = create_memo(move || { + representations[0].with(|rep_0| + representations[1].with(|rep_1| + rep_0.dot(&(&*Q * rep_1)) ) - } - ); + ) + }); let set_point = create_signal(SpecifiedValue::from_empty_spec()); + let serial = Self::next_serial(); - InversiveDistanceRegulator { subjects, measurement, set_point } + InversiveDistanceRegulator { subjects, measurement, set_point, serial } } } impl Regulator for InversiveDistanceRegulator { - fn subjects(&self) -> Vec { - self.subjects.into() + fn subjects(&self) -> Vec> { + self.subjects.clone().into() } fn measurement(&self) -> ReadSignal { @@ -339,12 +407,18 @@ impl Regulator for InversiveDistanceRegulator { } } +impl Serial for InversiveDistanceRegulator { + fn serial(&self) -> u64 { + self.serial + } +} + impl ProblemPoser for InversiveDistanceRegulator { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>) { + fn pose(&self, problem: &mut ConstraintProblem) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { - let [row, col] = self.subjects.map( - |subj| elts[subj].column_index().expect( + let [row, col] = self.subjects.each_ref().map( + |subj| subj.column_index().expect( "Subjects should be indexed before inversive distance regulator writes problem data" ) ); @@ -355,28 +429,28 @@ impl ProblemPoser for InversiveDistanceRegulator { } pub struct HalfCurvatureRegulator { - pub subject: ElementKey, + pub subject: Rc, pub measurement: ReadSignal, - pub set_point: Signal + pub set_point: Signal, + serial: u64 } impl HalfCurvatureRegulator { - pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator { - let measurement = assembly.elements.map( - move |elts| elts[subject].representation().with( - |rep| rep[Sphere::CURVATURE_COMPONENT] - ) + pub fn new(subject: Rc) -> HalfCurvatureRegulator { + let measurement = subject.representation().map( + |rep| rep[Sphere::CURVATURE_COMPONENT] ); let set_point = create_signal(SpecifiedValue::from_empty_spec()); + let serial = Self::next_serial(); - HalfCurvatureRegulator { subject, measurement, set_point } + HalfCurvatureRegulator { subject, measurement, set_point, serial } } } impl Regulator for HalfCurvatureRegulator { - fn subjects(&self) -> Vec { - vec![self.subject] + fn subjects(&self) -> Vec> { + vec![self.subject.clone()] } fn measurement(&self) -> ReadSignal { @@ -387,13 +461,10 @@ impl Regulator for HalfCurvatureRegulator { self.set_point } - fn try_activate(&self, assembly: &Assembly) -> bool { + fn try_activate(&self) -> bool { match self.set_point.with(|set_pt| set_pt.value) { Some(half_curv) => { - let representation = assembly.elements.with_untracked( - |elts| elts[self.subject].representation() - ); - representation.update( + self.subject.representation().update( |rep| change_half_curvature(rep, half_curv) ); true @@ -403,11 +474,17 @@ impl Regulator for HalfCurvatureRegulator { } } +impl Serial for HalfCurvatureRegulator { + fn serial(&self) -> u64 { + self.serial + } +} + impl ProblemPoser for HalfCurvatureRegulator { - fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab>) { + fn pose(&self, problem: &mut ConstraintProblem) { self.set_point.with_untracked(|set_pt| { if let Some(val) = set_pt.value { - let col = elts[self.subject].column_index().expect( + let col = self.subject.column_index().expect( "Subject should be indexed before half-curvature regulator writes problem data" ); problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val); @@ -418,7 +495,7 @@ impl ProblemPoser for HalfCurvatureRegulator { // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { - pub key: ElementKey, + pub element: Rc, pub velocity: DVectorView<'a, f64> } @@ -428,8 +505,8 @@ type AssemblyMotion<'a> = Vec>; #[derive(Clone)] pub struct Assembly { // elements and regulators - pub elements: Signal>>, - pub regulators: Signal>>, + pub elements: Signal>>, + pub regulators: Signal>>, // solution variety tangent space. the basis vectors are stored in // configuration matrix format, ordered according to the elements' column @@ -444,16 +521,16 @@ pub struct Assembly { pub tangent: Signal, // indexing - pub elements_by_id: Signal> + pub elements_by_id: Signal>> } impl Assembly { pub fn new() -> Assembly { Assembly { - elements: create_signal(Slab::new()), - regulators: create_signal(Slab::new()), + elements: create_signal(BTreeSet::new()), + regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), - elements_by_id: create_signal(FxHashMap::default()) + elements_by_id: create_signal(BTreeMap::default()) } } @@ -462,29 +539,27 @@ impl Assembly { // insert an element into the assembly without checking whether we already // have an element with the same identifier. any element that does have the // same identifier will get kicked out of the `elements_by_id` index - fn insert_element_unchecked(&self, elt: T) -> ElementKey { + fn insert_element_unchecked(&self, elt: impl Element + 'static) { // insert the element let id = elt.id().clone(); - let key = self.elements.update(|elts| elts.insert(Rc::new(elt))); - self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key)); + let elt_rc = Rc::new(elt); + self.elements.update(|elts| elts.insert(elt_rc.clone())); + self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, elt_rc.clone())); // create and insert the element's default regulators - for reg in T::default_regulators(key, &self) { + for reg in elt_rc.default_regulators() { self.insert_regulator(reg); } - - key } - pub fn try_insert_element(&self, elt: impl Element + 'static) -> Option { + pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool { let can_insert = self.elements_by_id.with_untracked( |elts_by_id| !elts_by_id.contains_key(elt.id()) ); if can_insert { - Some(self.insert_element_unchecked(elt)) - } else { - None + self.insert_element_unchecked(elt); } + can_insert } pub fn insert_element_default(&self) { @@ -505,19 +580,16 @@ impl Assembly { pub fn insert_regulator(&self, regulator: Rc) { // add the regulator to the assembly's regulator list - let key = self.regulators.update( + self.regulators.update( |regs| regs.insert(regulator.clone()) ); // add the regulator to each subject's regulator list - let subjects = regulator.subjects(); - let subject_regulators: Vec<_> = self.elements.with_untracked( - |elts| subjects.into_iter().map( - |subj| elts[subj].regulators() - ).collect() - ); + let subject_regulators: Vec<_> = regulator.subjects().into_iter().map( + |subj| subj.regulators() + ).collect(); for regulators in subject_regulators { - regulators.update(|regs| regs.insert(key)); + regulators.update(|regs| regs.insert(regulator.clone())); } // update the realization when the regulator becomes a constraint, or is @@ -530,7 +602,7 @@ impl Assembly { format!("Updated regulator with subjects {:?}", regulator.subjects()) )); - if regulator.try_activate(&self_for_effect) { + if regulator.try_activate() { self_for_effect.realize(); } }); @@ -539,7 +611,7 @@ impl Assembly { // print an updated list of regulators console::log_1(&JsValue::from("Regulators:")); self.regulators.with_untracked(|regs| { - for (_, reg) in regs.into_iter() { + for reg in regs.into_iter() { console::log_1(&JsValue::from(format!( " {:?}: {}", reg.subjects(), @@ -563,7 +635,7 @@ impl Assembly { pub fn realize(&self) { // index the elements self.elements.update_silent(|elts| { - for (index, (_, elt)) in elts.into_iter().enumerate() { + for (index, elt) in elts.iter().enumerate() { elt.set_column_index(index); } }); @@ -571,12 +643,12 @@ impl Assembly { // set up the constraint problem let problem = self.elements.with_untracked(|elts| { let mut problem = ConstraintProblem::new(elts.len()); - for (_, elt) in elts { - elt.pose(&mut problem, elts); + for elt in elts { + elt.pose(&mut problem); } self.regulators.with_untracked(|regs| { - for (_, reg) in regs { - reg.pose(&mut problem, elts); + for reg in regs { + reg.pose(&mut problem); } }); problem @@ -618,7 +690,7 @@ impl Assembly { if success { // read out the solution - for (_, elt) in self.elements.get_clone_untracked() { + for elt in self.elements.get_clone_untracked() { elt.representation().update( |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) ); @@ -654,17 +726,17 @@ impl Assembly { // in the process, we find out how many matrix columns we'll need to // hold the deformation let realized_dim = self.tangent.with(|tan| tan.assembly_dim()); - let motion_dim = self.elements.update_silent(|elts| { + let motion_dim = { let mut next_column_index = realized_dim; for elt_motion in motion.iter() { - let moving_elt = &mut elts[elt_motion.key]; + let moving_elt = &elt_motion.element; if moving_elt.column_index().is_none() { moving_elt.set_column_index(next_column_index); next_column_index += 1; } } next_column_index - }); + }; // project the element motions onto the tangent space of the solution // variety and sum them to get a deformation of the whole assembly. the @@ -675,9 +747,7 @@ impl Assembly { for elt_motion in motion { // we can unwrap the column index because we know that every moving // element has one at this point - let column_index = self.elements.with_untracked( - |elts| elts[elt_motion.key].column_index().unwrap() - ); + let column_index = elt_motion.element.column_index().unwrap(); if column_index < realized_dim { // this element had a column index when we started, so by @@ -690,12 +760,8 @@ impl Assembly { // this element didn't have a column index when we started, so // by invariant (2), it's unconstrained let mut target_column = motion_proj.column_mut(column_index); - let unif_to_std = self.elements.with_untracked( - |elts| { - elts[elt_motion.key].representation().with_untracked( - |rep| local_unif_to_std(rep.as_view()) - ) - } + let unif_to_std = elt_motion.element.representation().with_untracked( + |rep| local_unif_to_std(rep.as_view()) ); target_column += unif_to_std * elt_motion.velocity; } @@ -705,7 +771,7 @@ impl Assembly { // normalizations, so we restore those afterward /* KLUDGE */ // for now, we only restore the normalizations of spheres - for (_, elt) in self.elements.get_clone_untracked() { + for elt in self.elements.get_clone_untracked() { elt.representation().update_silent(|rep| { match elt.column_index() { Some(column_index) => { @@ -747,7 +813,7 @@ mod tests { fn unindexed_element_test() { let _ = create_root(|| { let elt = Sphere::default("sphere".to_string(), 0); - elt.pose(&mut ConstraintProblem::new(1), &Slab::new()); + elt.pose(&mut ConstraintProblem::new(1)); }); } @@ -755,18 +821,16 @@ mod tests { #[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")] fn unindexed_subject_test_inversive_distance() { let _ = create_root(|| { - let mut elts = Slab::>::new(); - let subjects = [0, 1].map(|k| { - elts.insert( - Rc::new(Sphere::default(format!("sphere{k}"), k)) - ) - }); - elts[subjects[0]].set_column_index(0); + let subjects = [0, 1].map( + |k| Rc::new(Sphere::default(format!("sphere{k}"), k)) as Rc + ); + subjects[0].set_column_index(0); InversiveDistanceRegulator { subjects: subjects, measurement: create_memo(|| 0.0), - set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()) - }.pose(&mut ConstraintProblem::new(2), &elts); + set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()), + serial: InversiveDistanceRegulator::next_serial() + }.pose(&mut ConstraintProblem::new(2)); }); } } \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 51b207d..a2fe4b6 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -1,5 +1,6 @@ use core::array; use nalgebra::{DMatrix, DVector, Rotation3, Vector3}; +use std::rc::Rc; use sycamore::{prelude::*, motion::create_raf}; use web_sys::{ console, @@ -16,7 +17,7 @@ use web_sys::{ use crate::{ AppState, - assembly::{ElementKey, ElementColor, ElementMotion, Point, Sphere} + assembly::{Element, ElementColor, ElementMotion, Point, Sphere} }; // --- scene data --- @@ -362,7 +363,7 @@ pub fn Display() -> View { let scene_changed = create_signal(true); create_effect(move || { state.assembly.elements.with(|elts| { - for (_, elt) in elts { + for elt in elts { elt.representation().track(); } }); @@ -548,7 +549,7 @@ pub fn Display() -> View { // manipulate the assembly if state.selection.with(|sel| sel.len() == 1) { let sel = state.selection.with( - |sel| *sel.into_iter().next().unwrap() + |sel| sel.into_iter().next().unwrap().clone() ); let translate_x = translate_pos_x_val - translate_neg_x_val; let translate_y = translate_pos_y_val - translate_neg_y_val; @@ -574,7 +575,7 @@ pub fn Display() -> View { assembly_for_raf.deform( vec![ ElementMotion { - key: sel, + element: sel, velocity: elt_motion.as_view() } ] @@ -615,8 +616,8 @@ pub fn Display() -> View { // set up the scene state.assembly.elements.with_untracked( - |elts| for (key, elt) in elts { - let selected = state.selection.with(|sel| sel.contains(&key)); + |elts| for elt in elts { + let selected = state.selection.with(|sel| sel.contains(elt)); elt.show(&mut scene, selected); } ); @@ -849,16 +850,16 @@ pub fn Display() -> View { // find the nearest element along the pointer direction let (dir, pixel_size) = event_dir(&event); console::log_1(&JsValue::from(dir.to_string())); - let mut clicked: Option<(ElementKey, f64)> = None; - for (key, elt) in state.assembly.elements.get_clone_untracked() { + let mut clicked: Option<(Rc, f64)> = None; + for elt in state.assembly.elements.get_clone_untracked() { match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) { Some(depth) => match clicked { Some((_, best_depth)) => { if depth < best_depth { - clicked = Some((key, depth)) + clicked = Some((elt, depth)) } }, - None => clicked = Some((key, depth)) + None => clicked = Some((elt, depth)) } None => () }; @@ -866,7 +867,7 @@ pub fn Display() -> View { // if we clicked something, select it match clicked { - Some((key, _)) => state.select(key, event.shift_key()), + Some((elt, _)) => state.select(&elt, event.shift_key()), None => state.selection.update(|sel| sel.clear()) }; } diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index e581997..b76859a 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -8,42 +8,41 @@ mod specified; #[cfg(test)] mod tests; -use rustc_hash::FxHashSet; +use std::{collections::BTreeSet, rc::Rc}; use sycamore::prelude::*; use add_remove::AddRemove; -use assembly::{Assembly, ElementKey}; +use assembly::{Assembly, Element}; use display::Display; use outline::Outline; #[derive(Clone)] struct AppState { assembly: Assembly, - selection: Signal> + selection: Signal>> } impl AppState { fn new() -> AppState { AppState { assembly: Assembly::new(), - selection: create_signal(FxHashSet::default()) + selection: create_signal(BTreeSet::default()) } } - // in single-selection mode, select the element with the given key. in - // multiple-selection mode, toggle whether the element with the given key - // is selected - fn select(&self, key: ElementKey, multi: bool) { + // in single-selection mode, select the given element. in multiple-selection + // mode, toggle whether the given element is selected + fn select(&self, element: &Rc, multi: bool) { if multi { self.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); + if !sel.remove(element) { + sel.insert(element.clone()); } }); } else { self.selection.update(|sel| { sel.clear(); - sel.insert(key); + sel.insert(element.clone()); }); } } diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 2893b6d..caf11e8 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -11,12 +11,9 @@ use crate::{ AppState, assembly::{ Element, - ElementKey, - ElementRc, HalfCurvatureRegulator, InversiveDistanceRegulator, - Regulator, - RegulatorKey + Regulator }, specified::SpecifiedValue }; @@ -92,20 +89,16 @@ fn RegulatorInput(regulator: Rc) -> View { } pub trait OutlineItem { - fn outline_item(self: Rc, element_key: ElementKey) -> View; + fn outline_item(self: Rc, element: &Rc) -> View; } impl OutlineItem for InversiveDistanceRegulator { - fn outline_item(self: Rc, element_key: ElementKey) -> View { - let state = use_context::(); - let other_subject = if self.subjects[0] == element_key { - self.subjects[1] + fn outline_item(self: Rc, element: &Rc) -> View { + let other_subject_label = if self.subjects[0] == element.clone() { + self.subjects[1].label() } else { - self.subjects[0] - }; - let other_subject_label = state.assembly.elements.with( - |elts| elts[other_subject].label().clone() - ); + self.subjects[0].label() + }.clone(); view! { li(class="regulator") { div(class="regulator-label") { (other_subject_label) } @@ -118,7 +111,7 @@ impl OutlineItem for InversiveDistanceRegulator { } impl OutlineItem for HalfCurvatureRegulator { - fn outline_item(self: Rc, _element_key: ElementKey) -> View { + fn outline_item(self: Rc, _element: &Rc) -> View { view! { li(class="regulator") { div(class="regulator-label") // for spacing @@ -130,23 +123,16 @@ impl OutlineItem for HalfCurvatureRegulator { } } -// a list item that shows a regulator in an outline view of an element -#[component(inline_props)] -fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View { - let state = use_context::(); - let regulator = state.assembly.regulators.with( - |regs| regs[regulator_key].clone() - ); - regulator.outline_item(element_key) -} - // a list item that shows an element in an outline view of an assembly #[component(inline_props)] -fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { +fn ElementOutlineItem(element: Rc) -> View { let state = use_context::(); - let class = state.selection.map( - move |sel| if sel.contains(&key) { "selected" } else { "" } - ); + let class = { + let element_for_class = element.clone(); + state.selection.map( + move |sel| if sel.contains(&element_for_class) { "selected" } else { "" } + ) + }; let label = element.label().clone(); let representation = element.representation().clone(); let rep_components = move || { @@ -161,14 +147,10 @@ fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { }; let regulated = element.regulators().map(|regs| regs.len() > 0); let regulator_list = element.regulators().map( - move |elt_reg_keys| elt_reg_keys + |regs| regs .clone() .into_iter() - .sorted_by_key( - |®_key| state.assembly.regulators.with( - |regs| regs[reg_key].subjects().len() - ) - ) + .sorted_by_key(|reg| reg.subjects().len()) .collect() ); let details_node = create_node_ref(); @@ -178,10 +160,11 @@ fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { summary( class=class.get(), on:keydown={ + let element_for_handler = element.clone(); move |event: KeyboardEvent| { match event.key().as_str() { "Enter" => { - state.select(key, event.shift_key()); + state.select(&element_for_handler, event.shift_key()); event.prevent_default(); }, "ArrowRight" if regulated.get() => { @@ -208,19 +191,10 @@ fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { div( class="element", on:click={ + let state_for_handler = state.clone(); + let element_for_handler = element.clone(); move |event: MouseEvent| { - if event.shift_key() { - state.selection.update(|sel| { - if !sel.remove(&key) { - sel.insert(key); - } - }); - } else { - state.selection.update(|sel| { - sel.clear(); - sel.insert(key); - }); - } + state_for_handler.select(&element_for_handler, event.shift_key()); event.stop_propagation(); event.prevent_default(); } @@ -234,13 +208,8 @@ fn ElementOutlineItem(key: ElementKey, element: Rc) -> View { ul(class="regulators") { Keyed( list=regulator_list, - view=move |reg_key| view! { - RegulatorOutlineItem( - regulator_key=reg_key, - element_key=key - ) - }, - key=|reg_key| reg_key.clone() + view=move |reg| reg.outline_item(&element), + key=|reg| reg.serial() ) } } @@ -259,12 +228,15 @@ pub fn Outline() -> View { let state = use_context::(); // list the elements alphabetically by ID + /* TO DO */ + // this code is designed to generalize easily to other sort keys. if we only + // ever wanted to sort by ID, we could do that more simply using the + // `elements_by_id` index let element_list = state.assembly.elements.map( |elts| elts .clone() .into_iter() - .sorted_by_key(|(_, elt)| elt.id().clone()) - .map(|(key, elt)| (key, ElementRc(elt))) + .sorted_by_key(|elt| elt.id().clone()) .collect() ); @@ -278,10 +250,10 @@ pub fn Outline() -> View { ) { Keyed( list=element_list, - view=|(key, ElementRc(elt))| view! { - ElementOutlineItem(key=key, element=elt) + view=|elt| view! { + ElementOutlineItem(element=elt) }, - key=|(_, ElementRc(elt))| elt.serial() + key=|elt| elt.serial() ) } } From a671a8273ae183bf35b8d82e6cb4a024ff956f1d Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 2 Jun 2025 15:56:06 +0000 Subject: [PATCH 118/126] Introduce ghost mode for elements (#85) Allows any element to be put in "ghost mode," decreasing its opacity and making it insensitive to click-to-select. Ghost mode is toggled using a checkbox in the outline view. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/85 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/main.css | 4 +++ app-proto/src/assembly.rs | 13 ++++++++ app-proto/src/display.rs | 61 ++++++++++++++++++++++++++------------ app-proto/src/outline.rs | 6 +++- app-proto/src/point.frag | 7 +++-- app-proto/src/point.vert | 4 +-- app-proto/src/spheres.frag | 13 ++++---- 7 files changed, 77 insertions(+), 31 deletions(-) diff --git a/app-proto/main.css b/app-proto/main.css index f787535..d56784f 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -90,6 +90,10 @@ summary > div, .regulator { padding-right: 8px; } +.element > input { + margin-left: 8px; +} + .element-switch { width: 18px; padding-left: 2px; diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index bd185c8..e48b802 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -101,6 +101,7 @@ pub trait Element: Serial + ProblemPoser + DisplayItem { fn id(&self) -> &String; fn label(&self) -> &String; fn representation(&self) -> Signal>; + fn ghost(&self) -> Signal; // the regulators the element is subject to. the assembly that owns the // element is responsible for keeping this set up to date @@ -154,6 +155,7 @@ pub struct Sphere { pub label: String, pub color: ElementColor, pub representation: Signal>, + pub ghost: Signal, pub regulators: Signal>>, serial: u64, column_index: Cell> @@ -173,6 +175,7 @@ impl Sphere { label: label, color: color, representation: create_signal(representation), + ghost: create_signal(false), regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), column_index: None.into() @@ -210,6 +213,10 @@ impl Element for Sphere { self.representation } + fn ghost(&self) -> Signal { + self.ghost + } + fn regulators(&self) -> Signal>> { self.regulators } @@ -244,6 +251,7 @@ pub struct Point { pub label: String, pub color: ElementColor, pub representation: Signal>, + pub ghost: Signal, pub regulators: Signal>>, serial: u64, column_index: Cell> @@ -263,6 +271,7 @@ impl Point { label, color, representation: create_signal(representation), + ghost: create_signal(false), regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), column_index: None.into() @@ -296,6 +305,10 @@ impl Element for Point { self.representation } + fn ghost(&self) -> Signal { + self.ghost + } + fn regulators(&self) -> Signal>> { self.regulators } diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index a2fe4b6..69a3659 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -20,11 +20,23 @@ use crate::{ assembly::{Element, ElementColor, ElementMotion, Point, Sphere} }; +// --- color --- + +const COLOR_SIZE: usize = 3; +type ColorWithOpacity = [f32; COLOR_SIZE + 1]; + +fn combine_channels(color: ElementColor, opacity: f32) -> ColorWithOpacity { + let mut color_with_opacity = [0.0; COLOR_SIZE + 1]; + color_with_opacity[..COLOR_SIZE].copy_from_slice(&color); + color_with_opacity[COLOR_SIZE] = opacity; + color_with_opacity +} + // --- scene data --- struct SceneSpheres { representations: Vec>, - colors: Vec, + colors_with_opacity: Vec, highlights: Vec } @@ -32,7 +44,7 @@ impl SceneSpheres { fn new() -> SceneSpheres{ SceneSpheres { representations: Vec::new(), - colors: Vec::new(), + colors_with_opacity: Vec::new(), highlights: Vec::new() } } @@ -41,16 +53,16 @@ impl SceneSpheres { self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer") } - fn push(&mut self, representation: DVector, color: ElementColor, highlight: f32) { + fn push(&mut self, representation: DVector, color: ElementColor, opacity: f32, highlight: f32) { self.representations.push(representation); - self.colors.push(color); + self.colors_with_opacity.push(combine_channels(color, opacity)); self.highlights.push(highlight); } } struct ScenePoints { representations: Vec>, - colors: Vec, + colors_with_opacity: Vec, highlights: Vec, selections: Vec } @@ -59,15 +71,15 @@ impl ScenePoints { fn new() -> ScenePoints { ScenePoints { representations: Vec::new(), - colors: Vec::new(), + colors_with_opacity: Vec::new(), highlights: Vec::new(), selections: Vec::new() } } - fn push(&mut self, representation: DVector, color: ElementColor, highlight: f32, selected: bool) { + fn push(&mut self, representation: DVector, color: ElementColor, opacity: f32, highlight: f32, selected: bool) { self.representations.push(representation); - self.colors.push(color); + self.colors_with_opacity.push(combine_channels(color, opacity)); self.highlights.push(highlight); self.selections.push(if selected { 1.0 } else { 0.0 }); } @@ -98,11 +110,16 @@ pub trait DisplayItem { impl DisplayItem for Sphere { fn show(&self, scene: &mut Scene, selected: bool) { - const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ + /* SCAFFOLDING */ + const DEFAULT_OPACITY: f32 = 0.5; + const GHOST_OPACITY: f32 = 0.2; + const HIGHLIGHT: f32 = 0.2; + let representation = self.representation.get_clone_untracked(); let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; + let opacity = if self.ghost.get() { GHOST_OPACITY } else { DEFAULT_OPACITY }; let highlight = if selected { 1.0 } else { HIGHLIGHT }; - scene.spheres.push(representation, color, highlight); + scene.spheres.push(representation, color, opacity, highlight); } // this method should be kept synchronized with `sphere_cast` in @@ -148,11 +165,15 @@ impl DisplayItem for Sphere { impl DisplayItem for Point { fn show(&self, scene: &mut Scene, selected: bool) { - const HIGHLIGHT: f32 = 0.5; /* SCAFFOLDING */ + /* SCAFFOLDING */ + const GHOST_OPACITY: f32 = 0.4; + const HIGHLIGHT: f32 = 0.5; + let representation = self.representation.get_clone_untracked(); let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; + let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 }; let highlight = if selected { 1.0 } else { HIGHLIGHT }; - scene.points.push(representation, color, highlight, selected); + scene.points.push(representation, color, opacity, highlight, selected); } /* SCAFFOLDING */ @@ -365,6 +386,7 @@ pub fn Display() -> View { state.assembly.elements.with(|elts| { for elt in elts { elt.representation().track(); + elt.ghost().track(); } }); state.selection.track(); @@ -395,7 +417,6 @@ pub fn Display() -> View { const SHRINKING_SPEED: f64 = 0.15; // in length units per second // display parameters - const OPACITY: f32 = 0.5; /* SCAFFOLDING */ const LAYER_THRESHOLD: i32 = 0; /* DEBUG */ const DEBUG_MODE: i32 = 0; /* DEBUG */ @@ -469,7 +490,6 @@ pub fn Display() -> View { ); let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution"); let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim"); - let opacity_loc = ctx.get_uniform_location(&sphere_program, "opacity"); let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold"); let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode"); @@ -654,9 +674,9 @@ pub fn Display() -> View { sphere_lt_locs[n].as_ref(), v.rows(3, 2).as_slice() ); - ctx.uniform3fv_with_f32_array( + ctx.uniform4fv_with_f32_array( sphere_color_locs[n].as_ref(), - &scene.spheres.colors[n] + &scene.spheres.colors_with_opacity[n] ); ctx.uniform1f( sphere_highlight_locs[n].as_ref(), @@ -665,7 +685,6 @@ pub fn Display() -> View { } // pass the display parameters - ctx.uniform1f(opacity_loc.as_ref(), OPACITY); ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); @@ -703,7 +722,7 @@ pub fn Display() -> View { // bind them to the corresponding attributes in the vertex // shader bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice()); - bind_new_buffer_to_attribute(&ctx, point_color_attr, COLOR_SIZE as i32, scene.points.colors.concat().as_slice()); + bind_new_buffer_to_attribute(&ctx, point_color_attr, (COLOR_SIZE + 1) as i32, scene.points.colors_with_opacity.concat().as_slice()); bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice()); bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice()); @@ -851,7 +870,11 @@ pub fn Display() -> View { let (dir, pixel_size) = event_dir(&event); console::log_1(&JsValue::from(dir.to_string())); let mut clicked: Option<(Rc, f64)> = None; - for elt in state.assembly.elements.get_clone_untracked() { + let tangible_elts = state.assembly.elements + .get_clone_untracked() + .into_iter() + .filter(|elt| !elt.ghost().get()); + for elt in tangible_elts { match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) { Some(depth) => match clicked { Some((_, best_depth)) => { diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index caf11e8..59bbdcc 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -202,7 +202,11 @@ fn ElementOutlineItem(element: Rc) -> View { ) { div(class="element-label") { (label) } div(class="element-representation") { (rep_components) } - div(class="status") + input( + r#type="checkbox", + bind:checked=element.ghost(), + on:click=|event: MouseEvent| event.stop_propagation() + ) } } ul(class="regulators") { diff --git a/app-proto/src/point.frag b/app-proto/src/point.frag index 3a361a8..194a072 100644 --- a/app-proto/src/point.frag +++ b/app-proto/src/point.frag @@ -2,7 +2,7 @@ precision highp float; -in vec3 point_color; +in vec4 point_color; in float point_highlight; in float total_radius; @@ -13,6 +13,7 @@ void main() { const float POINT_RADIUS = 4.; float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r); - vec3 color = mix(point_color, vec3(1.), border * point_highlight); - outColor = vec4(color, 1. - smoothstep(total_radius - 1., total_radius, r)); + float disk = 1. - smoothstep(total_radius - 1., total_radius, r); + vec4 color = mix(point_color, vec4(1.), border * point_highlight); + outColor = vec4(vec3(1.), disk) * color; } \ No newline at end of file diff --git a/app-proto/src/point.vert b/app-proto/src/point.vert index 6945010..0b76bc1 100644 --- a/app-proto/src/point.vert +++ b/app-proto/src/point.vert @@ -1,11 +1,11 @@ #version 300 es in vec4 position; -in vec3 color; +in vec4 color; in float highlight; in float selected; -out vec3 point_color; +out vec4 point_color; out float point_highlight; out float total_radius; diff --git a/app-proto/src/spheres.frag b/app-proto/src/spheres.frag index d50cb1e..fa317a8 100644 --- a/app-proto/src/spheres.frag +++ b/app-proto/src/spheres.frag @@ -17,7 +17,7 @@ struct vecInv { const int SPHERE_MAX = 200; uniform int sphere_cnt; uniform vecInv sphere_list[SPHERE_MAX]; -uniform vec3 color_list[SPHERE_MAX]; +uniform vec4 color_list[SPHERE_MAX]; uniform float highlight_list[SPHERE_MAX]; // view @@ -25,7 +25,6 @@ uniform vec2 resolution; uniform float shortdim; // controls -uniform float opacity; uniform int layer_threshold; uniform bool debug_mode; @@ -69,7 +68,7 @@ struct Fragment { vec4 color; }; -Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) { +Fragment sphere_shading(vecInv v, vec3 pt, vec4 base_color) { // the expression for normal needs to be checked. it's supposed to give the // negative gradient of the lorentz product between the impact point vector // and the sphere vector with respect to the coordinates of the impact @@ -79,7 +78,7 @@ Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) { float incidence = dot(normal, light_dir); float illum = mix(0.4, 1.0, max(incidence, 0.0)); - return Fragment(pt, normal, vec4(illum * base_color, opacity)); + return Fragment(pt, normal, vec4(illum * base_color.rgb, base_color.a)); } float intersection_dist(Fragment a, Fragment b) { @@ -192,10 +191,11 @@ void main() { vec3 color = vec3(0.); int layer = layer_cnt - 1; TaggedDepth hit = top_hits[layer]; + vec4 sphere_color = color_list[hit.id]; Fragment frag_next = sphere_shading( sphere_list[hit.id], hit.depth * dir, - hit.dimming * color_list[hit.id] + vec4(hit.dimming * sphere_color.rgb, sphere_color.a) ); float highlight_next = highlight_list[hit.id]; --layer; @@ -206,10 +206,11 @@ void main() { // shade the next fragment hit = top_hits[layer]; + sphere_color = color_list[hit.id]; frag_next = sphere_shading( sphere_list[hit.id], hit.depth * dir, - hit.dimming * color_list[hit.id] + vec4(hit.dimming * sphere_color.rgb, sphere_color.a) ); highlight_next = highlight_list[hit.id]; From e447e7ea966880d24bada011bf5df08e927b3871 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Wed, 4 Jun 2025 21:01:12 +0000 Subject: [PATCH 119/126] Dispatch normalization routines correctly (#87) Addresses issue #86 by correctly dispatching the routine used to normalize spheres during nudging. Adds a test that would have detected the issue. Since the tests aren't built for WebAssembly, we have to replace `console::log` with `console_log!` in all of the functions used by `assembly::curvature_drift_test`. We'll eventually want to do this replacement everywhere. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/87 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.toml | 7 +++ app-proto/src/assembly.rs | 123 +++++++++++++++++++++++++------------- app-proto/src/engine.rs | 34 ++++++++--- 3 files changed, 112 insertions(+), 52 deletions(-) diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 844a0a6..9b46b2b 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -46,6 +46,13 @@ features = [ dyna3 = { path = ".", default-features = false, features = ["dev"] } wasm-bindgen-test = "0.3.34" +# turn off spurious warnings about the custom config that Sycamore uses +# +# https://sycamore.dev/book/troubleshooting#unexpected-cfg-condition-name--sycamore-force-ssr +# +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(sycamore_force_ssr)"] } + [profile.release] opt-level = "s" # optimize for small code size debug = true # include debug symbols diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index e48b802..6c91fc0 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,6 +1,5 @@ use nalgebra::{DMatrix, DVector, DVectorView}; use std::{ - any::{Any, TypeId}, cell::Cell, collections::{BTreeMap, BTreeSet}, cmp::Ordering, @@ -20,6 +19,8 @@ use crate::{ change_half_curvature, local_unif_to_std, point, + project_point_to_normalized, + project_sphere_to_normalized, realize_gram, sphere, ConfigSubspace, @@ -107,6 +108,10 @@ pub trait Element: Serial + ProblemPoser + DisplayItem { // element is responsible for keeping this set up to date fn regulators(&self) -> Signal>>; + // project a representation vector for this kind of element onto its + // normalization variety + fn project_to_normalized(&self, rep: &mut DVector); + // the configuration matrix column index that was assigned to the element // last time the assembly was realized, or `None` if the element has never // been through a realization @@ -221,6 +226,10 @@ impl Element for Sphere { self.regulators } + fn project_to_normalized(&self, rep: &mut DVector) { + project_sphere_to_normalized(rep); + } + fn column_index(&self) -> Option { self.column_index.get() } @@ -313,6 +322,10 @@ impl Element for Point { self.regulators } + fn project_to_normalized(&self, rep: &mut DVector) { + project_point_to_normalized(rep); + } + fn column_index(&self) -> Option { self.column_index.get() } @@ -611,9 +624,7 @@ impl Assembly { create_effect(move || { /* DEBUG */ // log the regulator update - console::log_1(&JsValue::from( - format!("Updated regulator with subjects {:?}", regulator.subjects()) - )); + console_log!("Updated regulator with subjects {:?}", regulator.subjects()); if regulator.try_activate() { self_for_effect.realize(); @@ -622,10 +633,10 @@ impl Assembly { /* DEBUG */ // print an updated list of regulators - console::log_1(&JsValue::from("Regulators:")); + console_log!("Regulators:"); self.regulators.with_untracked(|regs| { for reg in regs.into_iter() { - console::log_1(&JsValue::from(format!( + console_log!( " {:?}: {}", reg.subjects(), reg.set_point().with_untracked( @@ -638,7 +649,7 @@ impl Assembly { } } ) - ))); + ); } }); } @@ -669,19 +680,11 @@ impl Assembly { /* DEBUG */ // log the Gram matrix - console::log_1(&JsValue::from("Gram matrix:")); - problem.gram.log_to_console(); + console_log!("Gram matrix:\n{}", problem.gram); /* DEBUG */ // log the initial configuration matrix - console::log_1(&JsValue::from("Old configuration:")); - for j in 0..problem.guess.nrows() { - let mut row_str = String::new(); - for k in 0..problem.guess.ncols() { - row_str.push_str(format!(" {:>8.3}", problem.guess[(j, k)]).as_str()); - } - console::log_1(&JsValue::from(row_str)); - } + console_log!("Old configuration:{:>8.3}", problem.guess); // look for a configuration with the given Gram matrix let (config, tangent, success, history) = realize_gram( @@ -690,16 +693,14 @@ impl Assembly { /* DEBUG */ // report the outcome of the search - console::log_1(&JsValue::from( - if success { - "Target accuracy achieved!" - } else { - "Failed to reach target accuracy" - } - )); - console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1)); - console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap())); - console::log_2(&JsValue::from("Tangent dimension:"), &JsValue::from(tangent.dim())); + if success { + console_log!("Target accuracy achieved!") + } else { + console_log!("Failed to reach target accuracy") + } + console_log!("Steps: {}", history.scaled_loss.len() - 1); + console_log!("Loss: {}", *history.scaled_loss.last().unwrap()); + console_log!("Tangent dimension: {}", tangent.dim()); if success { // read out the solution @@ -782,29 +783,17 @@ impl Assembly { // step the assembly along the deformation. this changes the elements' // normalizations, so we restore those afterward - /* KLUDGE */ - // for now, we only restore the normalizations of spheres for elt in self.elements.get_clone_untracked() { elt.representation().update_silent(|rep| { match elt.column_index() { Some(column_index) => { - // step the assembly along the deformation + // step the element along the deformation and then + // restore its normalization *rep += motion_proj.column(column_index); - - if elt.type_id() == TypeId::of::() { - // restore normalization by contracting toward the - // last coordinate axis - let q_sp = rep.fixed_rows::<3>(0).norm_squared(); - let half_q_lt = -2.0 * rep[3] * rep[4]; - let half_q_lt_sq = half_q_lt * half_q_lt; - let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); - rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); - } + elt.project_to_normalized(rep); }, None => { - console::log_1(&JsValue::from( - format!("No velocity to unpack for fresh element \"{}\"", elt.id()) - )) + console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) } }; }); @@ -821,6 +810,8 @@ impl Assembly { mod tests { use super::*; + use crate::engine; + #[test] #[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")] fn unindexed_element_test() { @@ -846,4 +837,50 @@ mod tests { }.pose(&mut ConstraintProblem::new(2)); }); } + + #[test] + fn curvature_drift_test() { + const INITIAL_RADIUS: f64 = 0.25; + let _ = create_root(|| { + // set up an assembly containing a single sphere centered at the + // origin + let assembly = Assembly::new(); + let sphere_id = "sphere0"; + let _ = assembly.try_insert_element( + // we create the sphere by hand for two reasons: to choose the + // curvature (which can affect drift rate) and to make the test + // independent of `Sphere::default` + Sphere::new( + String::from(sphere_id), + String::from("Sphere 0"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS) + ) + ); + + // nudge the sphere repeatedly along the `z` axis + const STEP_SIZE: f64 = 0.0025; + const STEP_CNT: usize = 400; + let sphere = assembly.elements_by_id.with(|elts_by_id| elts_by_id[sphere_id].clone()); + let velocity = DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]); + for _ in 0..STEP_CNT { + assembly.deform( + vec![ + ElementMotion { + element: sphere.clone(), + velocity: velocity.as_view() + } + ] + ); + } + + // check how much the sphere's curvature has drifted + const INITIAL_HALF_CURV: f64 = 0.5 / INITIAL_RADIUS; + const DRIFT_TOL: f64 = 0.015; + let final_half_curv = sphere.representation().with_untracked( + |rep| rep[Sphere::CURVATURE_COMPONENT] + ); + assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL); + }); + } } \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index b0fa23d..c5d7b00 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -1,5 +1,6 @@ use lazy_static::lazy_static; use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen}; +use std::fmt::{Display, Error, Formatter}; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ // --- elements --- @@ -34,6 +35,21 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 ]) } +// project a sphere's representation vector to the normalization variety by +// contracting toward the last coordinate axis +pub fn project_sphere_to_normalized(rep: &mut DVector) { + let q_sp = rep.fixed_rows::<3>(0).norm_squared(); + let half_q_lt = -2.0 * rep[3] * rep[4]; + let half_q_lt_sq = half_q_lt * half_q_lt; + let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); + rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling); +} + +// normalize a point's representation vector by scaling +pub fn project_point_to_normalized(rep: &mut DVector) { + rep.scale_mut(0.5 / rep[3]); +} + // given a sphere's representation vector, change the sphere's half-curvature to // `half-curv` and then restore normalization by contracting the representation // vector toward the curvature axis @@ -94,15 +110,6 @@ impl PartialMatrix { } } - /* DEBUG */ - pub fn log_to_console(&self) { - for &MatrixEntry { index: (row, col), value } in self { - console::log_1(&JsValue::from( - format!(" {} {} {}", row, col, value) - )); - } - } - fn freeze(&self, a: &DMatrix) -> DMatrix { let mut result = a.clone(); for &MatrixEntry { index, value } in self { @@ -128,6 +135,15 @@ impl PartialMatrix { } } +impl Display for PartialMatrix { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + for &MatrixEntry { index: (row, col), value } in self { + writeln!(f, " {row} {col} {value}")?; + } + Ok(()) + } +} + impl IntoIterator for PartialMatrix { type Item = MatrixEntry; type IntoIter = std::vec::IntoIter; From 4cb32625556743de30b49eed4b6bfce8ab9190dc Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Thu, 26 Jun 2025 22:11:02 +0000 Subject: [PATCH 120/126] chore: Update Sycamore to 0.9.1 (#91) Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/91 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.lock | 25 +++++++++++++------------ app-proto/Cargo.toml | 2 +- app-proto/src/outline.rs | 4 ++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 3bf609c..55e8686 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -423,9 +423,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "sycamore" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dedaf7237c05913604a5b0b2536b613f6c8510c6b213d2583b1294869755cabd" +checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc" dependencies = [ "hashbrown", "indexmap", @@ -440,9 +440,9 @@ dependencies = [ [[package]] name = "sycamore-core" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ddddc3d1bcb38c04ad55d2d1ab4f6a358e4daaeae0a0436892f1fade9fb31a" +checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187" dependencies = [ "hashbrown", "paste", @@ -451,9 +451,9 @@ dependencies = [ [[package]] name = "sycamore-macro" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77181c27cb753e86065308901871ccc7456fb19527b6a4ffacad3b63175ed014" +checksum = "a0c1d2eddc94db6d03e67eb832df5512b967e81053a573cd01bf3e1c3db00137" dependencies = [ "once_cell", "proc-macro2", @@ -465,20 +465,21 @@ dependencies = [ [[package]] name = "sycamore-reactive" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aa6870203507c07e850687c0ccf528eb0f04240e3596bac9137007ffb6c50b1" +checksum = "f2bacf810535efc2701187a716a5652197ad241d620d5b00fb12caa6dfa23add" dependencies = [ "paste", "slotmap", "smallvec", + "wasm-bindgen", ] [[package]] name = "sycamore-view-parser" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6144640af2eafffc68a92f3aacbbfaa21f7fd31906e2336fe304fd100fe226b" +checksum = "6c22875843db83cd4d49c0123a195e433bdc74e13ed0fff4ace0e77bb0a67033" dependencies = [ "proc-macro2", "quote", @@ -487,9 +488,9 @@ dependencies = [ [[package]] name = "sycamore-web" -version = "0.9.0-beta.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca93dcf1b1830bf1aac93508ed51babcda92c1d32d96067ab416d94e4b7c475" +checksum = "4b17aa5875f59f541cdf6fb58751ec702a6ed9801f30dd2b4d5f2279025b98bd" dependencies = [ "html-escape", "js-sys", diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 9b46b2b..6932b72 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -15,7 +15,7 @@ js-sys = "0.3.70" lazy_static = "1.5.0" nalgebra = "0.33.0" readonly = "0.2.12" -sycamore = "0.9.0-beta.3" +sycamore = "0.9.1" # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs index 59bbdcc..77d8575 100644 --- a/app-proto/src/outline.rs +++ b/app-proto/src/outline.rs @@ -151,7 +151,7 @@ fn ElementOutlineItem(element: Rc) -> View { .clone() .into_iter() .sorted_by_key(|reg| reg.subjects().len()) - .collect() + .collect::>() ); let details_node = create_node_ref(); view! { @@ -241,7 +241,7 @@ pub fn Outline() -> View { .clone() .into_iter() .sorted_by_key(|elt| elt.id().clone()) - .collect() + .collect::>() ); view! { From 5864017e6fd4e45f138b3fa4dac4cbd362a95641 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 21 Jul 2025 04:18:49 +0000 Subject: [PATCH 121/126] feat: Engine diagnostics (#92) Adds a `Diagnostics` component that shows the following diagnostics from the last realization: - Confirmation of success or a short description of what failed. - The value of the loss function at each step. - The spectrum of the Hessian at each step. The loss and spectrum plots are shown on switchable panels. Also includes some refactoring/renaming of existing code. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/92 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/Cargo.lock | 565 +++++++++++++++++++++++++- app-proto/Cargo.toml | 3 + app-proto/examples/common/print.rs | 36 ++ app-proto/examples/irisawa-hexlet.rs | 28 +- app-proto/examples/kaleidocycle.rs | 48 +-- app-proto/examples/point-on-sphere.rs | 32 +- app-proto/examples/three-spheres.rs | 29 +- app-proto/index.html | 6 + app-proto/main.css | 66 ++- app-proto/run-examples | 12 - app-proto/run-examples.sh | 20 + app-proto/src/add_remove.rs | 4 +- app-proto/src/assembly.rs | 65 ++- app-proto/src/diagnostics.rs | 258 ++++++++++++ app-proto/src/display.rs | 1 + app-proto/src/engine.rs | 94 +++-- app-proto/src/main.rs | 3 + 17 files changed, 1120 insertions(+), 150 deletions(-) create mode 100644 app-proto/examples/common/print.rs delete mode 100755 app-proto/run-examples create mode 100644 app-proto/run-examples.sh create mode 100644 app-proto/src/diagnostics.rs diff --git a/app-proto/Cargo.lock b/app-proto/Cargo.lock index 55e8686..4f75c45 100644 --- a/app-proto/Cargo.lock +++ b/app-proto/Cargo.lock @@ -20,6 +20,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "approx" version = "0.5.1" @@ -35,6 +50,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -68,6 +98,35 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "charming" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ffae2e616ae7d66b2e9ea369f1c7650042bdcdc1dc08b04b027107007b4f09" +dependencies = [ + "handlebars", + "js-sys", + "serde", + "serde-wasm-bindgen", + "serde_json", + "serde_with", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -78,10 +137,122 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dyna3" version = "0.1.0" dependencies = [ + "charming", "console_error_panic_hook", "dyna3", "itertools", @@ -106,6 +277,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -117,6 +304,28 @@ dependencies = [ "wasi", ] +[[package]] +name = "handlebars" +version = "6.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" +dependencies = [ + "derive_builder", + "log", + "num-order", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -127,6 +336,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "html-escape" version = "0.2.13" @@ -136,6 +351,47 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.5.0" @@ -143,7 +399,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", + "serde", ] [[package]] @@ -155,6 +412,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.70" @@ -192,6 +455,12 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "minicov" version = "0.3.5" @@ -248,6 +517,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -257,6 +532,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -289,6 +579,57 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pest" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -363,6 +704,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "safe_arch" version = "0.7.2" @@ -387,6 +734,90 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.5.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -421,14 +852,20 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "sycamore" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f38201dcb10aa609e81ca6f7547758a7eb602240a5ff682e668909fd0f7b2cc" dependencies = [ - "hashbrown", - "indexmap", + "hashbrown 0.14.5", + "indexmap 2.5.0", "paste", "sycamore-core", "sycamore-macro", @@ -444,7 +881,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41dc04bf0de321c6486356b2be751fac82fabb06c992d25b6748587561bba187" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", "paste", "sycamore-reactive", ] @@ -506,21 +943,78 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.13" @@ -677,6 +1171,65 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/app-proto/Cargo.toml b/app-proto/Cargo.toml index 6932b72..1230b47 100644 --- a/app-proto/Cargo.toml +++ b/app-proto/Cargo.toml @@ -17,6 +17,9 @@ nalgebra = "0.33.0" readonly = "0.2.12" sycamore = "0.9.1" +# We use Charming to help display engine diagnostics +charming = { version = "0.5.1", features = ["wasm"] } + # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for diff --git a/app-proto/examples/common/print.rs b/app-proto/examples/common/print.rs new file mode 100644 index 0000000..2aa6a39 --- /dev/null +++ b/app-proto/examples/common/print.rs @@ -0,0 +1,36 @@ +#![allow(dead_code)] + +use nalgebra::DMatrix; + +use dyna3::engine::{Q, DescentHistory, Realization}; + +pub fn title(title: &str) { + println!("─── {title} ───"); +} + +pub fn realization_diagnostics(realization: &Realization) { + let Realization { result, history } = realization; + println!(); + if let Err(ref message) = result { + println!("❌️ {message}"); + } else { + println!("✅️ Target accuracy achieved!"); + } + println!("Steps: {}", history.scaled_loss.len() - 1); + println!("Loss: {}", history.scaled_loss.last().unwrap()); +} + +pub fn gram_matrix(config: &DMatrix) { + println!("\nCompleted Gram matrix:{}", (config.tr_mul(&*Q) * config).to_string().trim_end()); +} + +pub fn config(config: &DMatrix) { + println!("\nConfiguration:{}", config.to_string().trim_end()); +} + +pub fn loss_history(history: &DescentHistory) { + println!("\nStep │ Loss\n─────┼────────────────────────────────"); + for (step, scaled_loss) in history.scaled_loss.iter().enumerate() { + println!("{:<4} │ {}", step, scaled_loss); + } +} \ No newline at end of file diff --git a/app-proto/examples/irisawa-hexlet.rs b/app-proto/examples/irisawa-hexlet.rs index 639a494..0d710ff 100644 --- a/app-proto/examples/irisawa-hexlet.rs +++ b/app-proto/examples/irisawa-hexlet.rs @@ -1,25 +1,23 @@ -use dyna3::engine::{Q, examples::realize_irisawa_hexlet}; +#[path = "common/print.rs"] +mod print; + +use dyna3::engine::{ConfigNeighborhood, examples::realize_irisawa_hexlet}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let (config, _, success, history) = realize_irisawa_hexlet(SCALED_TOL); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - if success { + let realization = realize_irisawa_hexlet(SCALED_TOL); + print::title("Irisawa hexlet"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood { config, .. }) = realization.result { + // print the diameters of the chain spheres println!("\nChain diameters:"); println!(" {} sun (given)", 1.0 / config[(3, 3)]); for k in 4..9 { println!(" {} sun", 1.0 / config[(3, k)]); } + + // print the completed Gram matrix + print::gram_matrix(&config); } - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); - } + print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs index 2779ab1..7ca1f97 100644 --- a/app-proto/examples/kaleidocycle.rs +++ b/app-proto/examples/kaleidocycle.rs @@ -1,30 +1,32 @@ +#[path = "common/print.rs"] +mod print; + use nalgebra::{DMatrix, DVector}; -use dyna3::engine::{Q, examples::realize_kaleidocycle}; +use dyna3::engine::{ConfigNeighborhood, examples::realize_kaleidocycle}; fn main() { const SCALED_TOL: f64 = 1.0e-12; - let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); - print!("Completed Gram matrix:{}", config.tr_mul(&*Q) * &config); - print!("Configuration:{}", config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); + let realization = realize_kaleidocycle(SCALED_TOL); + print::title("Kaleidocycle"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood { config, nbhd: tangent }) = realization.result { + // print the completed Gram matrix and the realized configuration + print::gram_matrix(&config); + print::config(&config); + + // find the kaleidocycle's twist motion by projecting onto the tangent + // space + const N_POINTS: usize = 12; + let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); + let down = -&up; + let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( + |n| [ + tangent.proj(&up.as_view(), n), + tangent.proj(&down.as_view(), n+1) + ] + ).sum(); + let normalization = 5.0 / twist_motion[(2, 0)]; + println!("\nTwist motion:{}", (normalization * twist_motion).to_string().trim_end()); } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}\n", history.scaled_loss.last().unwrap()); - - // find the kaleidocycle's twist motion by projecting onto the tangent space - const N_POINTS: usize = 12; - let up = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]); - let down = -&up; - let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( - |n| [ - tangent.proj(&up.as_view(), n), - tangent.proj(&down.as_view(), n+1) - ] - ).sum(); - let normalization = 5.0 / twist_motion[(2, 0)]; - print!("Twist motion:{}", normalization * twist_motion); } \ No newline at end of file diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 880d7b0..89dee76 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -1,4 +1,13 @@ -use dyna3::engine::{Q, point, realize_gram, sphere, ConstraintProblem}; +#[path = "common/print.rs"] +mod print; + +use dyna3::engine::{ + point, + realize_gram, + sphere, + ConfigNeighborhood, + ConstraintProblem +}; fn main() { let mut problem = ConstraintProblem::from_guess(&[ @@ -11,21 +20,14 @@ fn main() { } } problem.frozen.push(3, 0, problem.guess[(3, 0)]); - println!(); - let (config, _, success, history) = realize_gram( + let realization = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - print!("Configuration:{}", config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); + print::title("Point on a sphere"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood{ config, .. }) = realization.result { + print::gram_matrix(&config); + print::config(&config); } + print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index 3f3cc44..aa5a105 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -1,4 +1,12 @@ -use dyna3::engine::{Q, realize_gram, sphere, ConstraintProblem}; +#[path = "common/print.rs"] +mod print; + +use dyna3::engine::{ + realize_gram, + sphere, + ConfigNeighborhood, + ConstraintProblem +}; fn main() { let mut problem = ConstraintProblem::from_guess({ @@ -14,20 +22,13 @@ fn main() { problem.gram.push_sym(j, k, if j == k { 1.0 } else { -1.0 }); } } - println!(); - let (config, _, success, history) = realize_gram( + let realization = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - print!("\nCompleted Gram matrix:{}", config.tr_mul(&*Q) * &config); - if success { - println!("Target accuracy achieved!"); - } else { - println!("Failed to reach target accuracy"); - } - println!("Steps: {}", history.scaled_loss.len() - 1); - println!("Loss: {}", history.scaled_loss.last().unwrap()); - println!("\nStep │ Loss\n─────┼────────────────────────────────"); - for (step, scaled_loss) in history.scaled_loss.into_iter().enumerate() { - println!("{:<4} │ {}", step, scaled_loss); + print::title("Three spheres"); + print::realization_diagnostics(&realization); + if let Ok(ConfigNeighborhood{ config, .. }) = realization.result { + print::gram_matrix(&config); } + print::loss_history(&realization.history); } \ No newline at end of file diff --git a/app-proto/index.html b/app-proto/index.html index 92238f4..4fbe52f 100644 --- a/app-proto/index.html +++ b/app-proto/index.html @@ -6,6 +6,12 @@ + + + diff --git a/app-proto/main.css b/app-proto/main.css index d56784f..7981285 100644 --- a/app-proto/main.css +++ b/app-proto/main.css @@ -18,6 +18,17 @@ body { font-family: 'Fira Sans', sans-serif; } +.invalid { + color: var(--text-invalid); +} + +.status { + width: 20px; + text-align: center; + font-family: 'Noto Emoji'; + font-style: normal; +} + /* sidebar */ #sidebar { @@ -138,6 +149,7 @@ details[open]:has(li) .element-switch::after { } .regulator-input { + margin-right: 4px; color: inherit; background-color: inherit; border: 1px solid var(--border); @@ -159,22 +171,56 @@ details[open]:has(li) .element-switch::after { border-color: var(--border-invalid); } -.status { - width: 20px; - padding-left: 4px; - text-align: center; - font-family: 'Noto Emoji'; - font-style: normal; -} - .regulator-input.invalid + .status::after, details:has(.invalid):not([open]) .status::after { content: '⚠'; color: var(--text-invalid); } +/* diagnostics */ + +#diagnostics { + margin: 10px; +} + +#diagnostics-bar { + display: flex; +} + +#realization-status { + display: flex; + flex-grow: 1; +} + +#realization-status .status { + margin-right: 4px; +} + +#realization-status :not(.status) { + flex-grow: 1; +} + +#realization-status .status::after { + content: '✓'; +} + +#realization-status.invalid .status::after { + content: '⚠'; +} + +.diagnostics-panel { + margin-top: 10px; + min-height: 180px; +} + +.diagnostics-chart { + background-color: var(--display-background); + border: 1px solid var(--border); + border-radius: 8px; +} + /* display */ -canvas { +#display { float: left; margin-left: 20px; margin-top: 20px; @@ -183,7 +229,7 @@ canvas { border-radius: 16px; } -canvas:focus { +#display:focus { border-color: var(--border-focus-dark); outline: none; } diff --git a/app-proto/run-examples b/app-proto/run-examples deleted file mode 100755 index 52173b0..0000000 --- a/app-proto/run-examples +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -# run all Cargo examples, as described here: -# -# Karol Kuczmarski. "Add examples to your Rust libraries" -# http://xion.io/post/code/rust-examples.html -# - -cargo run --example irisawa-hexlet -cargo run --example three-spheres -cargo run --example point-on-sphere -cargo run --example kaleidocycle \ No newline at end of file diff --git a/app-proto/run-examples.sh b/app-proto/run-examples.sh new file mode 100644 index 0000000..861addf --- /dev/null +++ b/app-proto/run-examples.sh @@ -0,0 +1,20 @@ +# run all Cargo examples, as described here: +# +# Karol Kuczmarski. "Add examples to your Rust libraries" +# http://xion.io/post/code/rust-examples.html +# +# you should invoke this script by calling `sh` or another interpreter, rather +# than calling `souce`, to ensure that the script can find the manifest file for +# the application prototype + +# find the manifest file for the application prototype +MANIFEST="$(dirname -- $0)/Cargo.toml" + +# set up the command that runs each example +RUN_EXAMPLE="cargo run --manifest-path $MANIFEST --example" + +# run the examples +$RUN_EXAMPLE irisawa-hexlet; echo +$RUN_EXAMPLE three-spheres; echo +$RUN_EXAMPLE point-on-sphere; echo +$RUN_EXAMPLE kaleidocycle \ No newline at end of file diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index f3bbc97..d737c79 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -3,8 +3,9 @@ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ - engine, AppState, + engine, + engine::DescentHistory, assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere} }; @@ -195,6 +196,7 @@ pub fn AddRemove() -> View { assembly.regulators.update(|regs| regs.clear()); assembly.elements.update(|elts| elts.clear()); assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); + assembly.descent_history.set(DescentHistory::new()); state.selection.update(|sel| sel.clear()); // load assembly diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 6c91fc0..c3b0c6b 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -23,8 +23,11 @@ use crate::{ project_sphere_to_normalized, realize_gram, sphere, + ConfigNeighborhood, ConfigSubspace, - ConstraintProblem + ConstraintProblem, + DescentHistory, + Realization }, outline::OutlineItem, specified::SpecifiedValue @@ -547,7 +550,11 @@ pub struct Assembly { pub tangent: Signal, // indexing - pub elements_by_id: Signal>> + pub elements_by_id: Signal>>, + + // realization diagnostics + pub realization_status: Signal>, + pub descent_history: Signal } impl Assembly { @@ -556,7 +563,9 @@ impl Assembly { elements: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), - elements_by_id: create_signal(BTreeMap::default()) + elements_by_id: create_signal(BTreeMap::default()), + realization_status: create_signal(Ok(())), + descent_history: create_signal(DescentHistory::new()) } } @@ -687,31 +696,49 @@ impl Assembly { console_log!("Old configuration:{:>8.3}", problem.guess); // look for a configuration with the given Gram matrix - let (config, tangent, success, history) = realize_gram( + let Realization { result, history } = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); /* DEBUG */ - // report the outcome of the search - if success { - console_log!("Target accuracy achieved!") + // report the outcome of the search in the browser console + if let Err(ref message) = result { + console_log!("❌️ {message}"); } else { - console_log!("Failed to reach target accuracy") + console_log!("✅️ Target accuracy achieved!"); } console_log!("Steps: {}", history.scaled_loss.len() - 1); - console_log!("Loss: {}", *history.scaled_loss.last().unwrap()); - console_log!("Tangent dimension: {}", tangent.dim()); + console_log!("Loss: {}", history.scaled_loss.last().unwrap()); - if success { - // read out the solution - for elt in self.elements.get_clone_untracked() { - elt.representation().update( - |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) - ); + // report the loss history + self.descent_history.set(history); + + match result { + Ok(ConfigNeighborhood { config, nbhd: tangent }) => { + /* DEBUG */ + // report the tangent dimension + console_log!("Tangent dimension: {}", tangent.dim()); + + // report the realization status + self.realization_status.set(Ok(())); + + // read out the solution + for elt in self.elements.get_clone_untracked() { + elt.representation().update( + |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) + ); + } + + // save the tangent space + self.tangent.set_silent(tangent); + }, + Err(message) => { + // report the realization status. the `Err(message)` we're + // setting the status to has a different type than the + // `Err(message)` we received from the match: we're changing the + // `Ok` type from `Realization` to `()` + self.realization_status.set(Err(message)) } - - // save the tangent space - self.tangent.set_silent(tangent); } } diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/diagnostics.rs new file mode 100644 index 0000000..a2f090a --- /dev/null +++ b/app-proto/src/diagnostics.rs @@ -0,0 +1,258 @@ +use charming::{ + Chart, + WasmRenderer, + component::{Axis, DataZoom, Grid}, + element::{AxisType, Symbol}, + series::{Line, Scatter}, +}; +use sycamore::prelude::*; + +use crate::AppState; + +#[derive(Clone)] +struct DiagnosticsState { + active_tab: Signal +} + +impl DiagnosticsState { + fn new(initial_tab: String) -> DiagnosticsState { + DiagnosticsState { + active_tab: create_signal(initial_tab) + } + } +} + +// a realization status indicator +#[component] +fn RealizationStatus() -> View { + let state = use_context::(); + let realization_status = state.assembly.realization_status; + view! { + div( + id="realization-status", + class=realization_status.with( + |status| match status { + Ok(_) => "", + Err(_) => "invalid" + } + ) + ) { + div(class="status") + div { + (realization_status.with( + |status| match status { + Ok(_) => "Target accuracy achieved".to_string(), + Err(message) => message.clone() + } + )) + } + } + } +} + +fn into_log10_time_point((step, value): (usize, f64)) -> Vec> { + vec![ + Some(step as f64), + if value == 0.0 { None } else { Some(value.abs().log10()) } + ] +} + +// the loss history from the last realization +#[component] +fn LossHistory() -> View { + const CONTAINER_ID: &str = "loss-history"; + let state = use_context::(); + let renderer = WasmRenderer::new_opt(None, Some(178)); + + on_mount(move || { + create_effect(move || { + // get the loss history + let scaled_loss: Vec<_> = state.assembly.descent_history.with( + |history| history.scaled_loss + .iter() + .enumerate() + .map(|(step, &loss)| (step, loss)) + .map(into_log10_time_point) + .collect() + ); + + // initialize the chart axes + let step_axis = Axis::new() + .type_(AxisType::Category) + .boundary_gap(false); + let scaled_loss_axis = Axis::new(); + + // load the chart data. when there's no history, we load the data + // point (0, None) to clear the chart. it would feel more natural to + // load empty data vectors, but that turns out not to clear the + // chart: it instead leads to previous data being re-used + let scaled_loss_series = Line::new().data( + if scaled_loss.len() > 0 { + scaled_loss + } else { + vec![vec![Some(0.0), None::]] + } + ); + let chart = Chart::new() + .animation(false) + .data_zoom(DataZoom::new().y_axis_index(0).right(40)) + .x_axis(step_axis) + .y_axis(scaled_loss_axis) + .grid(Grid::new().top(20).right(80).bottom(30).left(60)) + .series(scaled_loss_series); + renderer.render(CONTAINER_ID, &chart).unwrap(); + }); + }); + + view! { + div(id=CONTAINER_ID, class="diagnostics-chart") + } +} + +// the spectrum of the Hessian during the last realization +#[component] +fn SpectrumHistory() -> View { + const CONTAINER_ID: &str = "spectrum-history"; + let state = use_context::(); + let renderer = WasmRenderer::new(478, 178); + + on_mount(move || { + create_effect(move || { + // get the spectrum of the Hessian at each step, split into its + // positive, negative, and strictly-zero parts + let ( + hess_eigvals_zero, + hess_eigvals_nonzero + ): (Vec<_>, Vec<_>) = state.assembly.descent_history.with( + |history| history.hess_eigvals + .iter() + .enumerate() + .map( + |(step, eigvals)| eigvals.iter().map( + move |&val| (step, val) + ) + ) + .flatten() + .partition(|&(_, val)| val == 0.0) + ); + let zero_level = hess_eigvals_nonzero + .iter() + .map(|(_, val)| val.abs()) + .reduce(f64::min) + .map(|val| 0.1 * val) + .unwrap_or(1.0); + let ( + hess_eigvals_pos, + hess_eigvals_neg + ): (Vec<_>, Vec<_>) = hess_eigvals_nonzero + .into_iter() + .partition(|&(_, val)| val > 0.0); + + // initialize the chart axes + let step_axis = Axis::new() + .type_(AxisType::Category) + .boundary_gap(false); + let eigval_axis = Axis::new(); + + // load the chart data. when there's no history, we load the data + // point (0, None) to clear the chart. it would feel more natural to + // load empty data vectors, but that turns out not to clear the + // chart: it instead leads to previous data being re-used + let eigval_series_pos = Scatter::new() + .symbol_size(4.5) + .data( + if hess_eigvals_pos.len() > 0 { + hess_eigvals_pos + .into_iter() + .map(into_log10_time_point) + .collect() + } else { + vec![vec![Some(0.0), None::]] + } + ); + let eigval_series_neg = Scatter::new() + .symbol(Symbol::Diamond) + .symbol_size(6.0) + .data( + if hess_eigvals_neg.len() > 0 { + hess_eigvals_neg + .into_iter() + .map(into_log10_time_point) + .collect() + } else { + vec![vec![Some(0.0), None::]] + } + ); + let eigval_series_zero = Scatter::new() + .symbol(Symbol::Triangle) + .symbol_size(5.0) + .data( + if hess_eigvals_zero.len() > 0 { + hess_eigvals_zero + .into_iter() + .map(|(step, _)| (step, zero_level)) + .map(into_log10_time_point) + .collect() + } else { + vec![vec![Some(0.0), None::]] + } + ); + let chart = Chart::new() + .animation(false) + .data_zoom(DataZoom::new().y_axis_index(0).right(40)) + .x_axis(step_axis) + .y_axis(eigval_axis) + .grid(Grid::new().top(20).right(80).bottom(30).left(60)) + .series(eigval_series_pos) + .series(eigval_series_neg) + .series(eigval_series_zero); + renderer.render(CONTAINER_ID, &chart).unwrap(); + }); + }); + + view! { + div(id=CONTAINER_ID, class="diagnostics-chart") + } +} + +#[component(inline_props)] +fn DiagnosticsPanel(name: &'static str, children: Children) -> View { + let diagnostics_state = use_context::(); + view! { + div( + class="diagnostics-panel", + "hidden"=diagnostics_state.active_tab.with( + |active_tab| { + if active_tab == name { + None + } else { + Some("") + } + } + ) + ) { + (children) + } + } +} + +#[component] +pub fn Diagnostics() -> View { + let diagnostics_state = DiagnosticsState::new("loss".to_string()); + let active_tab = diagnostics_state.active_tab.clone(); + provide_context(diagnostics_state); + + view! { + div(id="diagnostics") { + div(id="diagnostics-bar") { + RealizationStatus {} + select(bind:value=active_tab) { + option(value="loss") { "Loss" } + option(value="spectrum") { "Spectrum" } + } + } + DiagnosticsPanel(name="loss") { LossHistory {} } + DiagnosticsPanel(name="spectrum") { SpectrumHistory {} } + } + } +} \ No newline at end of file diff --git a/app-proto/src/display.rs b/app-proto/src/display.rs index 69a3659..1646c4e 100644 --- a/app-proto/src/display.rs +++ b/app-proto/src/display.rs @@ -806,6 +806,7 @@ pub fn Display() -> View { // again canvas( ref=display, + id="display", width="600", height="600", tabindex="0", diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index c5d7b00..e6ffa25 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -256,18 +256,18 @@ pub struct DescentHistory { pub config: Vec>, pub scaled_loss: Vec, pub neg_grad: Vec>, - pub min_eigval: Vec, + pub hess_eigvals: Vec::>, pub base_step: Vec>, pub backoff_steps: Vec } impl DescentHistory { - fn new() -> DescentHistory { + pub fn new() -> DescentHistory { DescentHistory { config: Vec::>::new(), scaled_loss: Vec::::new(), neg_grad: Vec::>::new(), - min_eigval: Vec::::new(), + hess_eigvals: Vec::>::new(), base_step: Vec::>::new(), backoff_steps: Vec::::new(), } @@ -393,6 +393,17 @@ fn seek_better_config( None } +// a first-order neighborhood of a configuration +pub struct ConfigNeighborhood { + pub config: DMatrix, + pub nbhd: ConfigSubspace +} + +pub struct Realization { + pub result: Result, + pub history: DescentHistory +} + // seek a matrix `config` that matches the partial matrix `problem.frozen` and // has `config' * Q * config` matching the partial matrix `problem.gram`. start // at `problem.guess`, set the frozen entries to their desired values, and then @@ -405,7 +416,7 @@ pub fn realize_gram( reg_scale: f64, max_descent_steps: i32, max_backoff_steps: i32 -) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { +) -> Realization { // destructure the problem data let ConstraintProblem { gram, guess, frozen @@ -457,11 +468,12 @@ pub fn realize_gram( hess = DMatrix::from_columns(hess_cols.as_slice()); // regularize the Hessian - let min_eigval = hess.symmetric_eigenvalues().min(); + let hess_eigvals = hess.symmetric_eigenvalues(); + let min_eigval = hess_eigvals.min(); if min_eigval <= 0.0 { hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); } - history.min_eigval.push(min_eigval); + history.hess_eigvals.push(hess_eigvals); // project the negative gradient and negative Hessian onto the // orthogonal complement of the frozen subspace @@ -480,30 +492,40 @@ pub fn realize_gram( if state.loss < tol { break; } // compute the Newton step + /* TO DO */ /* - we need to either handle or eliminate the case where the minimum - eigenvalue of the Hessian is zero, so the regularized Hessian is - singular. right now, this causes the Cholesky decomposition to return - `None`, leading to a panic when we unrap + we should change our regularization to ensure that the Hessian is + is positive-definite, rather than just positive-semidefinite. ideally, + that would guarantee the success of the Cholesky decomposition--- + although we'd still need the error-handling routine in case of + numerical hiccups */ - let base_step_stacked = hess.clone().cholesky().unwrap().solve(&neg_grad_stacked); + let hess_cholesky = match hess.clone().cholesky() { + Some(cholesky) => cholesky, + None => return Realization { + result: Err("Cholesky decomposition failed".to_string()), + history + } + }; + let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked); let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); history.base_step.push(base_step.clone()); // use backtracking line search to find a better configuration - match seek_better_config( + if let Some((better_state, backoff_steps)) = seek_better_config( gram, &state, &base_step, neg_grad.dot(&base_step), min_efficiency, backoff, max_backoff_steps ) { - Some((better_state, backoff_steps)) => { - state = better_state; - history.backoff_steps.push(backoff_steps); - }, - None => return (state.config, ConfigSubspace::zero(assembly_dim), false, history) + state = better_state; + history.backoff_steps.push(backoff_steps); + } else { + return Realization { + result: Err("Line search failed".to_string()), + history + } }; } - let success = state.loss < tol; - let tangent = if success { + let result = if state.loss < tol { // express the uniform basis in the standard basis const UNIFORM_DIM: usize = 4; let total_dim_unif = UNIFORM_DIM * assembly_dim; @@ -516,11 +538,13 @@ pub fn realize_gram( } // find the kernel of the Hessian. give it the uniform inner product - ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim) + let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim); + + Ok(ConfigNeighborhood { config: state.config, nbhd: tangent }) } else { - ConfigSubspace::zero(assembly_dim) + Err("Failed to reach target accuracy".to_string()) }; - (state.config, tangent, success, history) + Realization { result, history } } // --- tests --- @@ -539,7 +563,7 @@ pub mod examples { // "Japan's 'Wasan' Mathematical Tradition", by Abe Haruki // https://www.nippon.com/en/japan-topics/c12801/ // - pub fn realize_irisawa_hexlet(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + pub fn realize_irisawa_hexlet(scaled_tol: f64) -> Realization { let mut problem = ConstraintProblem::from_guess( [ sphere(0.0, 0.0, 0.0, 15.0), @@ -590,7 +614,7 @@ pub mod examples { // set up a kaleidocycle, made of points with fixed distances between them, // and find its tangent space - pub fn realize_kaleidocycle(scaled_tol: f64) -> (DMatrix, ConfigSubspace, bool, DescentHistory) { + pub fn realize_kaleidocycle(scaled_tol: f64) -> Realization { const N_HINGES: usize = 6; let mut problem = ConstraintProblem::from_guess( (0..N_HINGES).step_by(2).flat_map( @@ -714,10 +738,10 @@ mod tests { } problem.frozen.push(3, 0, problem.guess[(3, 0)]); problem.frozen.push(3, 1, 0.5); - let (config, _, success, history) = realize_gram( + let Realization { result, history } = realize_gram( &problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110 ); - assert_eq!(success, true); + let config = result.unwrap().config; for base_step in history.base_step.into_iter() { for &MatrixEntry { index, .. } in &problem.frozen { assert_eq!(base_step[index], 0.0); @@ -732,7 +756,7 @@ mod tests { fn irisawa_hexlet_test() { // solve Irisawa's problem const SCALED_TOL: f64 = 1.0e-12; - let (config, _, _, _) = realize_irisawa_hexlet(SCALED_TOL); + let config = realize_irisawa_hexlet(SCALED_TOL).result.unwrap().config; // check against Irisawa's solution let entry_tol = SCALED_TOL.sqrt(); @@ -759,11 +783,11 @@ mod tests { for n in 0..ELEMENT_DIM { problem.frozen.push(n, 0, problem.guess[(n, 0)]); } - let (config, tangent, success, history) = realize_gram( + let Realization { result, history } = realize_gram( &problem, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); assert_eq!(config, problem.guess); - assert_eq!(success, true); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of @@ -831,8 +855,8 @@ mod tests { fn tangent_test_kaleidocycle() { // set up a kaleidocycle and find its tangent space const SCALED_TOL: f64 = 1.0e-12; - let (config, tangent, success, history) = realize_kaleidocycle(SCALED_TOL); - assert_eq!(success, true); + let Realization { result, history } = realize_kaleidocycle(SCALED_TOL); + let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap(); assert_eq!(history.scaled_loss.len(), 1); // list some motions that should form a basis for the tangent space of @@ -920,11 +944,11 @@ mod tests { problem_orig.gram.push_sym(0, 0, 1.0); problem_orig.gram.push_sym(1, 1, 1.0); problem_orig.gram.push_sym(0, 1, 0.5); - let (config_orig, tangent_orig, success_orig, history_orig) = realize_gram( + let Realization { result: result_orig, history: history_orig } = realize_gram( &problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap(); assert_eq!(config_orig, problem_orig.guess); - assert_eq!(success_orig, true); assert_eq!(history_orig.scaled_loss.len(), 1); // find another pair of spheres that meet at 120°. we'll think of this @@ -941,11 +965,11 @@ mod tests { guess: guess_tfm, frozen: problem_orig.frozen }; - let (config_tfm, tangent_tfm, success_tfm, history_tfm) = realize_gram( + let Realization { result: result_tfm, history: history_tfm } = realize_gram( &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 ); + let ConfigNeighborhood { config: config_tfm, nbhd: tangent_tfm } = result_tfm.unwrap(); assert_eq!(config_tfm, problem_tfm.guess); - assert_eq!(success_tfm, true); assert_eq!(history_tfm.scaled_loss.len(), 1); // project a nudge to the tangent space of the solution variety at the diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index b76859a..f905c46 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -1,5 +1,6 @@ mod add_remove; mod assembly; +mod diagnostics; mod display; mod engine; mod outline; @@ -13,6 +14,7 @@ use sycamore::prelude::*; use add_remove::AddRemove; use assembly::{Assembly, Element}; +use diagnostics::Diagnostics; use display::Display; use outline::Outline; @@ -60,6 +62,7 @@ fn main() { div(id="sidebar") { AddRemove {} Outline {} + Diagnostics {} } Display {} } From 0801200210094b9514974d63bb09eaad88ba74de Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Tue, 22 Jul 2025 22:01:37 +0000 Subject: [PATCH 122/126] Add more test assemblies (#103) This PR helps probe the capabilities of the engine. Also adjusts the realization triggering system to reduce redundant realizations as we set an assembly's regulators during loading. Specificially, consolidates all calls to `realize()` into a single effect, which is triggered by the `needs_realization` signal. Also introduces a `keep_realized` signal and use it to pause realization while loading assemblies, but this signal is planned for removal as ultimately we do not want a separate "mode" of interpreting commands during loading, for maximal reproducibility of results (and simplicity of system). Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/103 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/src/add_remove.rs | 259 ----- app-proto/src/assembly.rs | 42 +- app-proto/src/components.rs | 5 + app-proto/src/components/add_remove.rs | 54 + app-proto/src/{ => components}/diagnostics.rs | 0 app-proto/src/{ => components}/display.rs | 0 app-proto/src/{ => components}/identity.vert | 0 app-proto/src/{ => components}/outline.rs | 0 app-proto/src/{ => components}/point.frag | 0 app-proto/src/{ => components}/point.vert | 0 app-proto/src/{ => components}/spheres.frag | 0 .../src/components/test_assembly_chooser.rs | 947 ++++++++++++++++++ app-proto/src/main.rs | 15 +- 13 files changed, 1045 insertions(+), 277 deletions(-) delete mode 100644 app-proto/src/add_remove.rs create mode 100644 app-proto/src/components.rs create mode 100644 app-proto/src/components/add_remove.rs rename app-proto/src/{ => components}/diagnostics.rs (100%) rename app-proto/src/{ => components}/display.rs (100%) rename app-proto/src/{ => components}/identity.vert (100%) rename app-proto/src/{ => components}/outline.rs (100%) rename app-proto/src/{ => components}/point.frag (100%) rename app-proto/src/{ => components}/point.vert (100%) rename app-proto/src/{ => components}/spheres.frag (100%) create mode 100644 app-proto/src/components/test_assembly_chooser.rs diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs deleted file mode 100644 index d737c79..0000000 --- a/app-proto/src/add_remove.rs +++ /dev/null @@ -1,259 +0,0 @@ -use std::{f64::consts::FRAC_1_SQRT_2, rc::Rc}; -use sycamore::prelude::*; -use web_sys::{console, wasm_bindgen::JsValue}; - -use crate::{ - AppState, - engine, - engine::DescentHistory, - assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere} -}; - -/* DEBUG */ -// load an example assembly for testing. this code will be removed once we've -// built a more formal test assembly system -fn load_gen_assemb(assembly: &Assembly) { - let _ = assembly.try_insert_element( - Sphere::new( - String::from("gemini_a"), - String::from("Castor"), - [1.00_f32, 0.25_f32, 0.00_f32], - engine::sphere(0.5, 0.5, 0.0, 1.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("gemini_b"), - String::from("Pollux"), - [0.00_f32, 0.25_f32, 1.00_f32], - engine::sphere(-0.5, -0.5, 0.0, 1.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("ursa_major"), - String::from("Ursa major"), - [0.25_f32, 0.00_f32, 1.00_f32], - engine::sphere(-0.5, 0.5, 0.0, 0.75) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("ursa_minor"), - String::from("Ursa minor"), - [0.25_f32, 1.00_f32, 0.00_f32], - engine::sphere(0.5, -0.5, 0.0, 0.5) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("moon_deimos"), - String::from("Deimos"), - [0.75_f32, 0.75_f32, 0.00_f32], - engine::sphere(0.0, 0.15, 1.0, 0.25) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("moon_phobos"), - String::from("Phobos"), - [0.00_f32, 0.75_f32, 0.50_f32], - engine::sphere(0.0, -0.15, -1.0, 0.25) - ) - ); -} - -/* DEBUG */ -// load an example assembly for testing. this code will be removed once we've -// built a more formal test assembly system -fn load_low_curv_assemb(assembly: &Assembly) { - let a = 0.75_f64.sqrt(); - let _ = assembly.try_insert_element( - Sphere::new( - "central".to_string(), - "Central".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "assemb_plane".to_string(), - "Assembly plane".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "side1".to_string(), - "Side 1".to_string(), - [1.00_f32, 0.00_f32, 0.25_f32], - engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "side2".to_string(), - "Side 2".to_string(), - [0.25_f32, 1.00_f32, 0.00_f32], - engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "side3".to_string(), - "Side 3".to_string(), - [0.00_f32, 0.25_f32, 1.00_f32], - engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "corner1".to_string(), - "Corner 1".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "corner2".to_string(), - "Corner 2".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("corner3"), - String::from("Corner 3"), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0) - ) - ); -} - -fn load_pointed_assemb(assembly: &Assembly) { - let _ = assembly.try_insert_element( - Point::new( - format!("point_front"), - format!("Front point"), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::point(0.0, 0.0, FRAC_1_SQRT_2) - ) - ); - let _ = assembly.try_insert_element( - Point::new( - format!("point_back"), - format!("Back point"), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::point(0.0, 0.0, -FRAC_1_SQRT_2) - ) - ); - for index_x in 0..=1 { - for index_y in 0..=1 { - let x = index_x as f64 - 0.5; - let y = index_y as f64 - 0.5; - - let _ = assembly.try_insert_element( - Sphere::new( - format!("sphere{index_x}{index_y}"), - format!("Sphere {index_x}{index_y}"), - [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], - engine::sphere(x, y, 0.0, 1.0) - ) - ); - - let _ = assembly.try_insert_element( - Point::new( - format!("point{index_x}{index_y}"), - format!("Point {index_x}{index_y}"), - [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], - engine::point(x, y, 0.0) - ) - ); - } - } -} - -#[component] -pub fn AddRemove() -> View { - /* DEBUG */ - let assembly_name = create_signal("general".to_string()); - create_effect(move || { - // get name of chosen assembly - let name = assembly_name.get_clone(); - console::log_1( - &JsValue::from(format!("Showing assembly \"{}\"", name.clone())) - ); - - batch(|| { - let state = use_context::(); - let assembly = &state.assembly; - - // clear state - assembly.regulators.update(|regs| regs.clear()); - assembly.elements.update(|elts| elts.clear()); - assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); - assembly.descent_history.set(DescentHistory::new()); - state.selection.update(|sel| sel.clear()); - - // load assembly - match name.as_str() { - "general" => load_gen_assemb(assembly), - "low-curv" => load_low_curv_assemb(assembly), - "pointed" => load_pointed_assemb(assembly), - _ => () - }; - }); - }); - - view! { - div(id="add-remove") { - button( - on:click=|_| { - let state = use_context::(); - state.assembly.insert_element_default::(); - } - ) { "Add sphere" } - button( - on:click=|_| { - let state = use_context::(); - state.assembly.insert_element_default::(); - } - ) { "Add point" } - button( - class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button - disabled={ - let state = use_context::(); - state.selection.with(|sel| sel.len() != 2) - }, - on:click=|_| { - let state = use_context::(); - let subjects: [_; 2] = state.selection.with( - // the button is only enabled when two elements are - // selected, so we know the cast to a two-element array - // will succeed - |sel| sel - .clone() - .into_iter() - .collect::>() - .try_into() - .unwrap() - ); - state.assembly.insert_regulator( - Rc::new(InversiveDistanceRegulator::new(subjects)) - ); - state.selection.update(|sel| sel.clear()); - } - ) { "🔗" } - select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser - option(value="general") { "General" } - option(value="low-curv") { "Low-curvature" } - option(value="pointed") { "Pointed" } - option(value="empty") { "Empty" } - } - } - } -} \ No newline at end of file diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index c3b0c6b..68fcd8b 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -13,7 +13,7 @@ use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ use crate::{ - display::DisplayItem, + components::{display::DisplayItem, outline::OutlineItem}, engine::{ Q, change_half_curvature, @@ -29,7 +29,6 @@ use crate::{ DescentHistory, Realization }, - outline::OutlineItem, specified::SpecifiedValue }; @@ -552,6 +551,10 @@ pub struct Assembly { // indexing pub elements_by_id: Signal>>, + // realization control + pub keep_realized: Signal, + pub needs_realization: Signal, + // realization diagnostics pub realization_status: Signal>, pub descent_history: Signal @@ -559,14 +562,30 @@ pub struct Assembly { impl Assembly { pub fn new() -> Assembly { - Assembly { + // create an assembly + let assembly = Assembly { elements: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), elements_by_id: create_signal(BTreeMap::default()), + keep_realized: create_signal(true), + needs_realization: create_signal(false), realization_status: create_signal(Ok(())), descent_history: create_signal(DescentHistory::new()) - } + }; + + // realize the assembly whenever it becomes simultaneously true that + // we're trying to keep it realized and it needs realization + let assembly_for_effect = assembly.clone(); + create_effect(move || { + let should_realize = assembly_for_effect.keep_realized.get() + && assembly_for_effect.needs_realization.get(); + if should_realize { + assembly_for_effect.realize(); + } + }); + + assembly } // --- inserting elements and regulators --- @@ -627,7 +646,7 @@ impl Assembly { regulators.update(|regs| regs.insert(regulator.clone())); } - // update the realization when the regulator becomes a constraint, or is + // request a realization when the regulator becomes a constraint, or is // edited while acting as a constraint let self_for_effect = self.clone(); create_effect(move || { @@ -636,7 +655,7 @@ impl Assembly { console_log!("Updated regulator with subjects {:?}", regulator.subjects()); if regulator.try_activate() { - self_for_effect.realize(); + self_for_effect.needs_realization.set(true); } }); @@ -731,6 +750,9 @@ impl Assembly { // save the tangent space self.tangent.set_silent(tangent); + + // clear the realization request flag + self.needs_realization.set(false); }, Err(message) => { // report the realization status. the `Err(message)` we're @@ -826,10 +848,10 @@ impl Assembly { }); } - // bring the configuration back onto the solution variety. this also - // gets the elements' column indices and the saved tangent space back in - // sync - self.realize(); + // request a realization to bring the configuration back onto the + // solution variety. this also gets the elements' column indices and the + // saved tangent space back in sync + self.needs_realization.set(true); } } diff --git a/app-proto/src/components.rs b/app-proto/src/components.rs new file mode 100644 index 0000000..7387d58 --- /dev/null +++ b/app-proto/src/components.rs @@ -0,0 +1,5 @@ +pub mod add_remove; +pub mod diagnostics; +pub mod display; +pub mod outline; +pub mod test_assembly_chooser; \ No newline at end of file diff --git a/app-proto/src/components/add_remove.rs b/app-proto/src/components/add_remove.rs new file mode 100644 index 0000000..3b0f9e0 --- /dev/null +++ b/app-proto/src/components/add_remove.rs @@ -0,0 +1,54 @@ +use std::rc::Rc; +use sycamore::prelude::*; + +use super::test_assembly_chooser::TestAssemblyChooser; +use crate::{ + AppState, + assembly::{InversiveDistanceRegulator, Point, Sphere} +}; + +#[component] +pub fn AddRemove() -> View { + view! { + div(id="add-remove") { + button( + on:click=|_| { + let state = use_context::(); + state.assembly.insert_element_default::(); + } + ) { "Add sphere" } + button( + on:click=|_| { + let state = use_context::(); + state.assembly.insert_element_default::(); + } + ) { "Add point" } + button( + class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button + disabled={ + let state = use_context::(); + state.selection.with(|sel| sel.len() != 2) + }, + on:click=|_| { + let state = use_context::(); + let subjects: [_; 2] = state.selection.with( + // the button is only enabled when two elements are + // selected, so we know the cast to a two-element array + // will succeed + |sel| sel + .clone() + .into_iter() + .collect::>() + .try_into() + .unwrap() + ); + state.assembly.insert_regulator( + Rc::new(InversiveDistanceRegulator::new(subjects)) + ); + state.selection.update(|sel| sel.clear()); + } + ) { "🔗" } + TestAssemblyChooser {} + } + } +} \ No newline at end of file diff --git a/app-proto/src/diagnostics.rs b/app-proto/src/components/diagnostics.rs similarity index 100% rename from app-proto/src/diagnostics.rs rename to app-proto/src/components/diagnostics.rs diff --git a/app-proto/src/display.rs b/app-proto/src/components/display.rs similarity index 100% rename from app-proto/src/display.rs rename to app-proto/src/components/display.rs diff --git a/app-proto/src/identity.vert b/app-proto/src/components/identity.vert similarity index 100% rename from app-proto/src/identity.vert rename to app-proto/src/components/identity.vert diff --git a/app-proto/src/outline.rs b/app-proto/src/components/outline.rs similarity index 100% rename from app-proto/src/outline.rs rename to app-proto/src/components/outline.rs diff --git a/app-proto/src/point.frag b/app-proto/src/components/point.frag similarity index 100% rename from app-proto/src/point.frag rename to app-proto/src/components/point.frag diff --git a/app-proto/src/point.vert b/app-proto/src/components/point.vert similarity index 100% rename from app-proto/src/point.vert rename to app-proto/src/components/point.vert diff --git a/app-proto/src/spheres.frag b/app-proto/src/components/spheres.frag similarity index 100% rename from app-proto/src/spheres.frag rename to app-proto/src/components/spheres.frag diff --git a/app-proto/src/components/test_assembly_chooser.rs b/app-proto/src/components/test_assembly_chooser.rs new file mode 100644 index 0000000..232cda3 --- /dev/null +++ b/app-proto/src/components/test_assembly_chooser.rs @@ -0,0 +1,947 @@ +use itertools::izip; +use std::{f64::consts::{FRAC_1_SQRT_2, PI}, rc::Rc}; +use nalgebra::Vector3; +use sycamore::prelude::*; +use web_sys::{console, wasm_bindgen::JsValue}; + +use crate::{ + AppState, + engine, + engine::DescentHistory, + assembly::{ + Assembly, + Element, + ElementColor, + InversiveDistanceRegulator, + Point, + Sphere + }, + specified::SpecifiedValue +}; + +// --- loaders --- + +/* DEBUG */ +// each of these functions loads an example assembly for testing. once we've +// done more work on saving and loading assemblies, we should come back to this +// code to see if it can be simplified + +fn load_gen_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Sphere::new( + String::from("gemini_a"), + String::from("Castor"), + [1.00_f32, 0.25_f32, 0.00_f32], + engine::sphere(0.5, 0.5, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("gemini_b"), + String::from("Pollux"), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere(-0.5, -0.5, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("ursa_major"), + String::from("Ursa major"), + [0.25_f32, 0.00_f32, 1.00_f32], + engine::sphere(-0.5, 0.5, 0.0, 0.75) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("ursa_minor"), + String::from("Ursa minor"), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere(0.5, -0.5, 0.0, 0.5) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("moon_deimos"), + String::from("Deimos"), + [0.75_f32, 0.75_f32, 0.00_f32], + engine::sphere(0.0, 0.15, 1.0, 0.25) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("moon_phobos"), + String::from("Phobos"), + [0.00_f32, 0.75_f32, 0.50_f32], + engine::sphere(0.0, -0.15, -1.0, 0.25) + ) + ); +} + +fn load_low_curv_assemb(assembly: &Assembly) { + // create the spheres + let a = 0.75_f64.sqrt(); + let _ = assembly.try_insert_element( + Sphere::new( + "central".to_string(), + "Central".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "assemb_plane".to_string(), + "Assembly plane".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "side1".to_string(), + "Side 1".to_string(), + [1.00_f32, 0.00_f32, 0.25_f32], + engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "side2".to_string(), + "Side 2".to_string(), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "side3".to_string(), + "Side 3".to_string(), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "corner1".to_string(), + "Corner 1".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "corner2".to_string(), + "Corner 2".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("corner3"), + String::from("Corner 3"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0) + ) + ); + + // impose the desired tangencies and make the sides planar + let index_range = 1..=3; + let [central, assemb_plane] = ["central", "assemb_plane"].map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[id].clone() + ) + ); + let sides = index_range.clone().map( + |k| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("side{k}")].clone() + ) + ); + let corners = index_range.map( + |k| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("corner{k}")].clone() + ) + ); + for plane in [assemb_plane.clone()].into_iter().chain(sides.clone()) { + // fix the curvature of each plane + let curvature = plane.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + curvature.set_point().set(SpecifiedValue::try_from("0".to_string()).unwrap()); + } + let all_perpendicular = [central.clone()].into_iter() + .chain(sides.clone()) + .chain(corners.clone()); + for sphere in all_perpendicular { + // make each side and packed sphere perpendicular to the assembly plane + let right_angle = InversiveDistanceRegulator::new([sphere, assemb_plane.clone()]); + right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(right_angle)); + } + for sphere in sides.clone().chain(corners.clone()) { + // make each side and corner sphere tangent to the central sphere + let tangency = InversiveDistanceRegulator::new([sphere.clone(), central.clone()]); + tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(tangency)); + } + for (side_index, side) in sides.enumerate() { + // make each side tangent to the two adjacent corner spheres + for (corner_index, corner) in corners.clone().enumerate() { + if side_index != corner_index { + let tangency = InversiveDistanceRegulator::new([side.clone(), corner]); + tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(tangency)); + } + } + } +} + +fn load_pointed_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Point::new( + format!("point_front"), + format!("Front point"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(0.0, 0.0, FRAC_1_SQRT_2) + ) + ); + let _ = assembly.try_insert_element( + Point::new( + format!("point_back"), + format!("Back point"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(0.0, 0.0, -FRAC_1_SQRT_2) + ) + ); + for index_x in 0..=1 { + for index_y in 0..=1 { + let x = index_x as f64 - 0.5; + let y = index_y as f64 - 0.5; + + let _ = assembly.try_insert_element( + Sphere::new( + format!("sphere{index_x}{index_y}"), + format!("Sphere {index_x}{index_y}"), + [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + engine::sphere(x, y, 0.0, 1.0) + ) + ); + + let _ = assembly.try_insert_element( + Point::new( + format!("point{index_x}{index_y}"), + format!("Point {index_x}{index_y}"), + [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + engine::point(x, y, 0.0) + ) + ); + } + } +} + +// to finish describing the tridiminished icosahedron, set the inversive +// distance regulators as follows: +// A-A -0.25 +// A-B " +// B-C " +// C-C " +// A-C -0.25 * φ^2 = -0.6545084971874737 +fn load_tridim_icosahedron_assemb(assembly: &Assembly) { + // create the vertices + const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.25_f32]; + const COLOR_B: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; + const COLOR_C: ElementColor = [0.25_f32, 0.50_f32, 1.00_f32]; + let vertices = [ + Point::new( + "a1".to_string(), + "A₁".to_string(), + COLOR_A, + engine::point(0.25, 0.75, 0.75) + ), + Point::new( + "a2".to_string(), + "A₂".to_string(), + COLOR_A, + engine::point(0.75, 0.25, 0.75) + ), + Point::new( + "a3".to_string(), + "A₃".to_string(), + COLOR_A, + engine::point(0.75, 0.75, 0.25) + ), + Point::new( + "b1".to_string(), + "B₁".to_string(), + COLOR_B, + engine::point(0.75, -0.25, -0.25) + ), + Point::new( + "b2".to_string(), + "B₂".to_string(), + COLOR_B, + engine::point(-0.25, 0.75, -0.25) + ), + Point::new( + "b3".to_string(), + "B₃".to_string(), + COLOR_B, + engine::point(-0.25, -0.25, 0.75) + ), + Point::new( + "c1".to_string(), + "C₁".to_string(), + COLOR_C, + engine::point(0.0, -1.0, -1.0) + ), + Point::new( + "c2".to_string(), + "C₂".to_string(), + COLOR_C, + engine::point(-1.0, 0.0, -1.0) + ), + Point::new( + "c3".to_string(), + "C₃".to_string(), + COLOR_C, + engine::point(-1.0, -1.0, 0.0) + ) + ]; + for vertex in vertices { + let _ = assembly.try_insert_element(vertex); + } + + // create the faces + const COLOR_FACE: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; + let frac_1_sqrt_6 = 1.0 / 6.0_f64.sqrt(); + let frac_2_sqrt_6 = 2.0 * frac_1_sqrt_6; + let faces = [ + Sphere::new( + "face1".to_string(), + "Face 1".to_string(), + COLOR_FACE, + engine::sphere_with_offset(frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0) + ), + Sphere::new( + "face2".to_string(), + "Face 2".to_string(), + COLOR_FACE, + engine::sphere_with_offset(-frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0) + ), + Sphere::new( + "face3".to_string(), + "Face 3".to_string(), + COLOR_FACE, + engine::sphere_with_offset(-frac_1_sqrt_6, -frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, 0.0) + ) + ]; + for face in faces { + face.ghost().set(true); + let _ = assembly.try_insert_element(face); + } + + let index_range = 1..=3; + for j in index_range.clone() { + // make each face planar + let face = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("face{j}")].clone() + ); + let curvature_regulator = face.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + curvature_regulator.set_point().set( + SpecifiedValue::try_from("0".to_string()).unwrap() + ); + + // put each A vertex on the face it belongs to + let vertex_a = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("a{j}")].clone() + ); + let incidence_a = InversiveDistanceRegulator::new([face.clone(), vertex_a.clone()]); + incidence_a.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(incidence_a)); + + // regulate the B-C vertex distances + let vertices_bc = ["b", "c"].map( + |series| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("{series}{j}")].clone() + ) + ); + assembly.insert_regulator( + Rc::new(InversiveDistanceRegulator::new(vertices_bc)) + ); + + // get the pair of indices adjacent to `j` + let adjacent_indices = [j % 3 + 1, (j + 1) % 3 + 1]; + + for k in adjacent_indices.clone() { + for series in ["b", "c"] { + // put each B and C vertex on the faces it belongs to + let vertex = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("{series}{k}")].clone() + ); + let incidence = InversiveDistanceRegulator::new([face.clone(), vertex.clone()]); + incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(incidence)); + + // regulate the A-B and A-C vertex distances + assembly.insert_regulator( + Rc::new(InversiveDistanceRegulator::new([vertex_a.clone(), vertex])) + ); + } + } + + // regulate the A-A and C-C vertex distances + let adjacent_pairs = ["a", "c"].map( + |series| adjacent_indices.map( + |index| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("{series}{index}")].clone() + ) + ) + ); + for pair in adjacent_pairs { + assembly.insert_regulator( + Rc::new(InversiveDistanceRegulator::new(pair)) + ); + } + } +} + +// to finish describing the dodecahedral circle packing, set the inversive +// distance regulators to -1. some of the regulators have already been set +fn load_dodeca_packing_assemb(assembly: &Assembly) { + // add the substrate + let _ = assembly.try_insert_element( + Sphere::new( + "substrate".to_string(), + "Substrate".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) + ); + let substrate = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id["substrate"].clone() + ); + + // fix the substrate's curvature + substrate.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ).set_point().set( + SpecifiedValue::try_from("0.5".to_string()).unwrap() + ); + + // add the circles to be packed + const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.00_f32]; + const COLOR_B: ElementColor = [1.00_f32, 0.00_f32, 0.25_f32]; + const COLOR_C: ElementColor = [0.25_f32, 0.00_f32, 1.00_f32]; + let phi = 0.5 + 1.25_f64.sqrt(); /* TO DO */ // replace with std::f64::consts::PHI when that gets stabilized + let phi_inv = 1.0 / phi; + let coord_scale = (phi + 2.0).sqrt(); + let face_scales = [phi_inv, (13.0 / 12.0) / coord_scale]; + let face_radii = [phi_inv, 5.0 / 12.0]; + let mut faces = Vec::>::new(); + let subscripts = ["₀", "₁"]; + for j in 0..2 { + for k in 0..2 { + let small_coord = face_scales[k] * (2.0*(j as f64) - 1.0); + let big_coord = face_scales[k] * (2.0*(k as f64) - 1.0) * phi; + + let id_num = format!("{j}{k}"); + let label_sub = format!("{}{}", subscripts[j], subscripts[k]); + + // add the A face + let id_a = format!("a{id_num}"); + let _ = assembly.try_insert_element( + Sphere::new( + id_a.clone(), + format!("A{label_sub}"), + COLOR_A, + engine::sphere(0.0, small_coord, big_coord, face_radii[k]) + ) + ); + faces.push( + assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id_a].clone() + ) + ); + + // add the B face + let id_b = format!("b{id_num}"); + let _ = assembly.try_insert_element( + Sphere::new( + id_b.clone(), + format!("B{label_sub}"), + COLOR_B, + engine::sphere(small_coord, big_coord, 0.0, face_radii[k]) + ) + ); + faces.push( + assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id_b].clone() + ) + ); + + // add the C face + let id_c = format!("c{id_num}"); + let _ = assembly.try_insert_element( + Sphere::new( + id_c.clone(), + format!("C{label_sub}"), + COLOR_C, + engine::sphere(big_coord, 0.0, small_coord, face_radii[k]) + ) + ); + faces.push( + assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id_c].clone() + ) + ); + } + } + + // make each face sphere perpendicular to the substrate + for face in faces { + let right_angle = InversiveDistanceRegulator::new([face, substrate.clone()]); + right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(right_angle)); + } + + // set up the tangencies that define the packing + for [long_edge_plane, short_edge_plane] in [["a", "b"], ["b", "c"], ["c", "a"]] { + for k in 0..2 { + let long_edge_ids = [ + format!("{long_edge_plane}{k}0"), + format!("{long_edge_plane}{k}1") + ]; + let short_edge_ids = [ + format!("{short_edge_plane}0{k}"), + format!("{short_edge_plane}1{k}") + ]; + let [long_edge, short_edge] = [long_edge_ids, short_edge_ids].map( + |edge_ids| edge_ids.map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id].clone() + ) + ) + ); + + // set up the short-edge tangency + let short_tangency = InversiveDistanceRegulator::new(short_edge.clone()); + if k == 0 { + short_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + } + assembly.insert_regulator(Rc::new(short_tangency)); + + // set up the side tangencies + for i in 0..2 { + for j in 0..2 { + let side_tangency = InversiveDistanceRegulator::new( + [long_edge[i].clone(), short_edge[j].clone()] + ); + if i == 0 && k == 0 { + side_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + } + assembly.insert_regulator(Rc::new(side_tangency)); + } + } + } + } +} + +// the initial configuration of this test assembly deliberately violates the +// constraints, so loading the assembly will trigger a non-trivial realization +fn load_balanced_assemb(assembly: &Assembly) { + // create the spheres + const R_OUTER: f64 = 10.0; + const R_INNER: f64 = 4.0; + let spheres = [ + Sphere::new( + "outer".to_string(), + "Outer".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, R_OUTER) + ), + Sphere::new( + "a".to_string(), + "A".to_string(), + [1.00_f32, 0.00_f32, 0.25_f32], + engine::sphere(0.0, 4.0, 0.0, R_INNER) + ), + Sphere::new( + "b".to_string(), + "B".to_string(), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere(0.0, -4.0, 0.0, R_INNER) + ), + ]; + for sphere in spheres { + let _ = assembly.try_insert_element(sphere); + } + + // get references to the spheres + let [outer, a, b] = ["outer", "a", "b"].map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[id].clone() + ) + ); + + // fix the diameters of the outer, sun, and moon spheres + for (sphere, radius) in [ + (outer.clone(), R_OUTER), + (a.clone(), R_INNER), + (b.clone(), R_INNER) + ] { + let curvature_regulator = sphere.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + let curvature = 0.5 / radius; + curvature_regulator.set_point().set( + SpecifiedValue::try_from(curvature.to_string()).unwrap() + ); + } + + // set the inversive distances between the spheres. as described above, the + // initial configuration deliberately violates these constraints + for inner in [a, b] { + let tangency = InversiveDistanceRegulator::new([outer.clone(), inner]); + tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(tangency)); + } +} + +// the initial configuration of this test assembly deliberately violates the +// constraints, so loading the assembly will trigger a non-trivial realization +fn load_off_center_assemb(assembly: &Assembly) { + // create a point almost at the origin and a sphere centered on the origin + let _ = assembly.try_insert_element( + Point::new( + "point".to_string(), + "Point".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(1e-9, 0.0, 0.0) + ), + ); + let _ = assembly.try_insert_element( + Sphere::new( + "sphere".to_string(), + "Sphere".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ), + ); + + // get references to the elements + let point_and_sphere = ["point", "sphere"].map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[id].clone() + ) + ); + + // put the point on the sphere + let incidence = InversiveDistanceRegulator::new(point_and_sphere); + incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(incidence)); +} + +// setting the inversive distances between the vertices to -2 gives a regular +// tetrahedron with side length 1, whose insphere and circumsphere have radii +// sqrt(1/6) and sqrt(3/2), respectively. to measure those radii, set an +// inversive distance of -1 between the insphere and each face, and then set an +// inversive distance of 0 between the circumsphere and each vertex +fn load_radius_ratio_assemb(assembly: &Assembly) { + let index_range = 1..=4; + + // create the spheres + const GRAY: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; + let spheres = [ + Sphere::new( + "sphere_faces".to_string(), + "Insphere".to_string(), + GRAY, + engine::sphere(0.0, 0.0, 0.0, 0.5) + ), + Sphere::new( + "sphere_vertices".to_string(), + "Circumsphere".to_string(), + GRAY, + engine::sphere(0.0, 0.0, 0.0, 0.25) + ) + ]; + for sphere in spheres { + let _ = assembly.try_insert_element(sphere); + } + + // create the vertices + let vertices = izip!( + index_range.clone(), + [ + [1.00_f32, 0.50_f32, 0.75_f32], + [1.00_f32, 0.75_f32, 0.50_f32], + [1.00_f32, 1.00_f32, 0.50_f32], + [0.75_f32, 0.50_f32, 1.00_f32] + ].into_iter(), + [ + engine::point(-0.6, -0.8, -0.6), + engine::point(-0.6, 0.8, 0.6), + engine::point(0.6, -0.8, 0.6), + engine::point(0.6, 0.8, -0.6) + ].into_iter() + ).map( + |(k, color, representation)| { + Point::new( + format!("v{k}"), + format!("Vertex {k}"), + color, + representation + ) + } + ); + for vertex in vertices { + let _ = assembly.try_insert_element(vertex); + } + + // create the faces + let base_dir = Vector3::new(1.0, 0.75, 1.0).normalize(); + let offset = base_dir.dot(&Vector3::new(-0.6, 0.8, 0.6)); + let faces = izip!( + index_range.clone(), + [ + [1.00_f32, 0.00_f32, 0.25_f32], + [1.00_f32, 0.25_f32, 0.00_f32], + [0.75_f32, 0.75_f32, 0.00_f32], + [0.25_f32, 0.00_f32, 1.00_f32] + ].into_iter(), + [ + engine::sphere_with_offset(base_dir[0], base_dir[1], base_dir[2], offset, 0.0), + engine::sphere_with_offset(base_dir[0], -base_dir[1], -base_dir[2], offset, 0.0), + engine::sphere_with_offset(-base_dir[0], base_dir[1], -base_dir[2], offset, 0.0), + engine::sphere_with_offset(-base_dir[0], -base_dir[1], base_dir[2], offset, 0.0) + ].into_iter() + ).map( + |(k, color, representation)| { + Sphere::new( + format!("f{k}"), + format!("Face {k}"), + color, + representation + ) + } + ); + for face in faces { + face.ghost().set(true); + let _ = assembly.try_insert_element(face); + } + + // impose the constraints + for j in index_range.clone() { + let [face_j, vertex_j] = [ + format!("f{j}"), + format!("v{j}") + ].map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id].clone() + ) + ); + + // make the faces planar + let curvature_regulator = face_j.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + curvature_regulator.set_point().set( + SpecifiedValue::try_from("0".to_string()).unwrap() + ); + + for k in index_range.clone().filter(|&index| index != j) { + let vertex_k = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("v{k}")].clone() + ); + + // fix the distances between the vertices + if j < k { + let distance_regulator = InversiveDistanceRegulator::new( + [vertex_j.clone(), vertex_k.clone()] + ); + assembly.insert_regulator(Rc::new(distance_regulator)); + } + + // put the vertices on the faces + let incidence_regulator = InversiveDistanceRegulator::new([face_j.clone(), vertex_k.clone()]); + incidence_regulator.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(incidence_regulator)); + } + } +} + +// to finish setting up the problem, fix the following curvatures: +// sun 1 +// moon 5/3 = 1.666666666666666... +// chain1 2 +// a tiny `x` or `z` nudge of the outer sphere reliably prevents realization +// failures before they happen, or resolves them after they happen. the result +// depends sensitively on the translation direction, suggesting that realization +// is failing because the engine is having trouble breaking a symmetry +// /* TO DO */ +// the engine's performance on this problem is scale-dependent! with the current +// initial conditions, realization fails for any order of imposing the remaining +// curvature constraints. scaling everything up by a factor of ten, as done in +// the original problem, makes realization succeed reliably. one potentially +// relevant difference is that a lot of the numbers in the current initial +// conditions are exactly representable as floats, unlike the analogous numbers +// in the scaled-up problem. the inexact representations might break the +// symmetry that's getting the engine stuck +fn load_irisawa_hexlet_assemb(assembly: &Assembly) { + let index_range = 1..=6; + let colors = [ + [1.00_f32, 0.00_f32, 0.25_f32], + [1.00_f32, 0.25_f32, 0.00_f32], + [0.75_f32, 0.75_f32, 0.00_f32], + [0.25_f32, 1.00_f32, 0.00_f32], + [0.00_f32, 0.25_f32, 1.00_f32], + [0.25_f32, 0.00_f32, 1.00_f32] + ].into_iter(); + + // create the spheres + let spheres = [ + Sphere::new( + "outer".to_string(), + "Outer".to_string(), + [0.5_f32, 0.5_f32, 0.5_f32], + engine::sphere(0.0, 0.0, 0.0, 1.5) + ), + Sphere::new( + "sun".to_string(), + "Sun".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, -0.75, 0.0, 0.75) + ), + Sphere::new( + "moon".to_string(), + "Moon".to_string(), + [0.25_f32, 0.25_f32, 0.25_f32], + engine::sphere(0.0, 0.75, 0.0, 0.75) + ), + ].into_iter().chain( + index_range.clone().zip(colors).map( + |(k, color)| { + let ang = (k as f64) * PI/3.0; + Sphere::new( + format!("chain{k}"), + format!("Chain {k}"), + color, + engine::sphere(1.0 * ang.sin(), 0.0, 1.0 * ang.cos(), 0.5) + ) + } + ) + ); + for sphere in spheres { + let _ = assembly.try_insert_element(sphere); + } + + // put the outer sphere in ghost mode and fix its curvature + let outer = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id["outer"].clone() + ); + outer.ghost().set(true); + let outer_curvature_regulator = outer.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + outer_curvature_regulator.set_point().set( + SpecifiedValue::try_from((1.0 / 3.0).to_string()).unwrap() + ); + + // impose the desired tangencies + let [outer, sun, moon] = ["outer", "sun", "moon"].map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[id].clone() + ) + ); + let chain = index_range.map( + |k| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("chain{k}")].clone() + ) + ); + for (chain_sphere, chain_sphere_next) in chain.clone().zip(chain.cycle().skip(1)) { + for (other_sphere, inversive_distance) in [ + (outer.clone(), "1"), + (sun.clone(), "-1"), + (moon.clone(), "-1"), + (chain_sphere_next.clone(), "-1") + ] { + let tangency = InversiveDistanceRegulator::new([chain_sphere.clone(), other_sphere]); + tangency.set_point.set(SpecifiedValue::try_from(inversive_distance.to_string()).unwrap()); + assembly.insert_regulator(Rc::new(tangency)); + } + } + + let outer_sun_tangency = InversiveDistanceRegulator::new([outer.clone(), sun]); + outer_sun_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(outer_sun_tangency)); + + let outer_moon_tangency = InversiveDistanceRegulator::new([outer.clone(), moon]); + outer_moon_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap()); + assembly.insert_regulator(Rc::new(outer_moon_tangency)); +} + +// --- chooser --- + +/* DEBUG */ +#[component] +pub fn TestAssemblyChooser() -> View { + // create an effect that loads the selected test assembly + let assembly_name = create_signal("general".to_string()); + create_effect(move || { + // get name of chosen assembly + let name = assembly_name.get_clone(); + console::log_1( + &JsValue::from(format!("Showing assembly \"{}\"", name.clone())) + ); + + batch(|| { + let state = use_context::(); + let assembly = &state.assembly; + + // pause realization + assembly.keep_realized.set(false); + + // clear state + assembly.regulators.update(|regs| regs.clear()); + assembly.elements.update(|elts| elts.clear()); + assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); + assembly.descent_history.set(DescentHistory::new()); + state.selection.update(|sel| sel.clear()); + + // load assembly + match name.as_str() { + "general" => load_gen_assemb(assembly), + "low-curv" => load_low_curv_assemb(assembly), + "pointed" => load_pointed_assemb(assembly), + "tridim-icosahedron" => load_tridim_icosahedron_assemb(assembly), + "dodeca-packing" => load_dodeca_packing_assemb(assembly), + "balanced" => load_balanced_assemb(assembly), + "off-center" => load_off_center_assemb(assembly), + "radius-ratio" => load_radius_ratio_assemb(assembly), + "irisawa-hexlet" => load_irisawa_hexlet_assemb(assembly), + _ => () + }; + + // resume realization + assembly.keep_realized.set(true); + }); + }); + + // build the chooser + view! { + select(bind:value=assembly_name) { + option(value="general") { "General" } + option(value="low-curv") { "Low-curvature" } + option(value="pointed") { "Pointed" } + option(value="tridim-icosahedron") { "Tridiminished icosahedron" } + option(value="dodeca-packing") { "Dodecahedral packing" } + option(value="balanced") { "Balanced" } + option(value="off-center") { "Off-center" } + option(value="radius-ratio") { "Radius ratio" } + option(value="irisawa-hexlet") { "Irisawa hexlet" } + option(value="empty") { "Empty" } + } + } +} \ No newline at end of file diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index f905c46..152d11c 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -1,9 +1,6 @@ -mod add_remove; mod assembly; -mod diagnostics; -mod display; +mod components; mod engine; -mod outline; mod specified; #[cfg(test)] @@ -12,11 +9,13 @@ mod tests; use std::{collections::BTreeSet, rc::Rc}; use sycamore::prelude::*; -use add_remove::AddRemove; use assembly::{Assembly, Element}; -use diagnostics::Diagnostics; -use display::Display; -use outline::Outline; +use components::{ + add_remove::AddRemove, + diagnostics::Diagnostics, + display::Display, + outline::Outline +}; #[derive(Clone)] struct AppState { From 2eba80fb69318ae78337c012b71909bba0c13461 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Thu, 31 Jul 2025 22:21:32 +0000 Subject: [PATCH 123/126] Simplify the realization triggering system (#105) Simplifies the system that reactively triggers realizations, at the cost of removing the preconditioning step described in issue #101 and doing unnecessary realizations after certain kinds of updates. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/105 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/src/assembly.rs | 72 +++++-------------- app-proto/src/components/add_remove.rs | 17 ++++- .../src/components/test_assembly_chooser.rs | 6 -- app-proto/src/engine.rs | 57 ++++----------- 4 files changed, 48 insertions(+), 104 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 68fcd8b..26fb4aa 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -16,7 +16,6 @@ use crate::{ components::{display::DisplayItem, outline::OutlineItem}, engine::{ Q, - change_half_curvature, local_unif_to_std, point, project_point_to_normalized, @@ -358,16 +357,6 @@ pub trait Regulator: Serial + ProblemPoser + OutlineItem { fn subjects(&self) -> Vec>; fn measurement(&self) -> ReadSignal; fn set_point(&self) -> Signal; - - // this method is used to responsively precondition the assembly for - // realization when the regulator becomes a constraint, or is edited while - // acting as a constraint. it should track the set point, do any desired - // preconditioning when the set point is present, and use its return value - // to report whether the set is present. the default implementation does no - // preconditioning - fn try_activate(&self) -> bool { - self.set_point().with(|set_pt| set_pt.is_present()) - } } impl Hash for dyn Regulator { @@ -488,18 +477,6 @@ impl Regulator for HalfCurvatureRegulator { fn set_point(&self) -> Signal { self.set_point } - - fn try_activate(&self) -> bool { - match self.set_point.with(|set_pt| set_pt.value) { - Some(half_curv) => { - self.subject.representation().update( - |rep| change_half_curvature(rep, half_curv) - ); - true - } - None => false - } - } } impl Serial for HalfCurvatureRegulator { @@ -552,8 +529,7 @@ pub struct Assembly { pub elements_by_id: Signal>>, // realization control - pub keep_realized: Signal, - pub needs_realization: Signal, + pub realization_trigger: Signal<()>, // realization diagnostics pub realization_status: Signal>, @@ -568,21 +544,23 @@ impl Assembly { regulators: create_signal(BTreeSet::new()), tangent: create_signal(ConfigSubspace::zero(0)), elements_by_id: create_signal(BTreeMap::default()), - keep_realized: create_signal(true), - needs_realization: create_signal(false), + realization_trigger: create_signal(()), realization_status: create_signal(Ok(())), descent_history: create_signal(DescentHistory::new()) }; - // realize the assembly whenever it becomes simultaneously true that - // we're trying to keep it realized and it needs realization + // realize the assembly whenever the element list, the regulator list, + // a regulator's set point, or the realization trigger is updated let assembly_for_effect = assembly.clone(); create_effect(move || { - let should_realize = assembly_for_effect.keep_realized.get() - && assembly_for_effect.needs_realization.get(); - if should_realize { - assembly_for_effect.realize(); - } + assembly_for_effect.elements.track(); + assembly_for_effect.regulators.with( + |regs| for reg in regs { + reg.set_point().track(); + } + ); + assembly_for_effect.realization_trigger.track(); + assembly_for_effect.realize(); }); assembly @@ -646,19 +624,6 @@ impl Assembly { regulators.update(|regs| regs.insert(regulator.clone())); } - // request a realization when the regulator becomes a constraint, or is - // edited while acting as a constraint - let self_for_effect = self.clone(); - create_effect(move || { - /* DEBUG */ - // log the regulator update - console_log!("Updated regulator with subjects {:?}", regulator.subjects()); - - if regulator.try_activate() { - self_for_effect.needs_realization.set(true); - } - }); - /* DEBUG */ // print an updated list of regulators console_log!("Regulators:"); @@ -726,8 +691,10 @@ impl Assembly { } else { console_log!("✅️ Target accuracy achieved!"); } - console_log!("Steps: {}", history.scaled_loss.len() - 1); - console_log!("Loss: {}", history.scaled_loss.last().unwrap()); + if history.scaled_loss.len() > 0 { + console_log!("Steps: {}", history.scaled_loss.len() - 1); + console_log!("Loss: {}", history.scaled_loss.last().unwrap()); + } // report the loss history self.descent_history.set(history); @@ -750,9 +717,6 @@ impl Assembly { // save the tangent space self.tangent.set_silent(tangent); - - // clear the realization request flag - self.needs_realization.set(false); }, Err(message) => { // report the realization status. the `Err(message)` we're @@ -848,10 +812,10 @@ impl Assembly { }); } - // request a realization to bring the configuration back onto the + // trigger a realization to bring the configuration back onto the // solution variety. this also gets the elements' column indices and the // saved tangent space back in sync - self.needs_realization.set(true); + self.realization_trigger.set(()); } } diff --git a/app-proto/src/components/add_remove.rs b/app-proto/src/components/add_remove.rs index 3b0f9e0..a685482 100644 --- a/app-proto/src/components/add_remove.rs +++ b/app-proto/src/components/add_remove.rs @@ -14,7 +14,22 @@ pub fn AddRemove() -> View { button( on:click=|_| { let state = use_context::(); - state.assembly.insert_element_default::(); + batch(|| { + // this call is batched to avoid redundant realizations. + // it updates the element list and the regulator list, + // which are both tracked by the realization effect + /* TO DO */ + // it would make more to do the batching inside + // `insert_element_default`, but that will have to wait + // until Sycamore handles nested batches correctly. + // + // https://github.com/sycamore-rs/sycamore/issues/802 + // + // the nested batch issue is relevant here because the + // assembly loaders in the test assembly chooser use + // `insert_element_default` within larger batches + state.assembly.insert_element_default::(); + }); } ) { "Add sphere" } button( diff --git a/app-proto/src/components/test_assembly_chooser.rs b/app-proto/src/components/test_assembly_chooser.rs index 232cda3..b58dd1a 100644 --- a/app-proto/src/components/test_assembly_chooser.rs +++ b/app-proto/src/components/test_assembly_chooser.rs @@ -900,9 +900,6 @@ pub fn TestAssemblyChooser() -> View { let state = use_context::(); let assembly = &state.assembly; - // pause realization - assembly.keep_realized.set(false); - // clear state assembly.regulators.update(|regs| regs.clear()); assembly.elements.update(|elts| elts.clear()); @@ -923,9 +920,6 @@ pub fn TestAssemblyChooser() -> View { "irisawa-hexlet" => load_irisawa_hexlet_assemb(assembly), _ => () }; - - // resume realization - assembly.keep_realized.set(true); }); }); diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index e6ffa25..602fc57 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -1,7 +1,6 @@ use lazy_static::lazy_static; use nalgebra::{Const, DMatrix, DVector, DVectorView, Dyn, SymmetricEigen}; use std::fmt::{Display, Error, Formatter}; -use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ // --- elements --- @@ -50,40 +49,6 @@ pub fn project_point_to_normalized(rep: &mut DVector) { rep.scale_mut(0.5 / rep[3]); } -// given a sphere's representation vector, change the sphere's half-curvature to -// `half-curv` and then restore normalization by contracting the representation -// vector toward the curvature axis -pub fn change_half_curvature(rep: &mut DVector, half_curv: f64) { - // set the sphere's half-curvature to the desired value - rep[3] = half_curv; - - // restore normalization by contracting toward the curvature axis - const SIZE_THRESHOLD: f64 = 1e-9; - let half_q_lt = -2.0 * half_curv * rep[4]; - let half_q_lt_sq = half_q_lt * half_q_lt; - let mut spatial = rep.fixed_rows_mut::<3>(0); - let q_sp = spatial.norm_squared(); - if q_sp < SIZE_THRESHOLD && half_q_lt_sq < SIZE_THRESHOLD { - spatial.copy_from_slice( - &[0.0, 0.0, (1.0 - 2.0 * half_q_lt).sqrt()] - ); - } else { - let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt(); - spatial.scale_mut(1.0 / scaling); - rep[4] /= scaling; - } - - /* DEBUG */ - // verify normalization - let rep_for_debug = rep.clone(); - console::log_1(&JsValue::from( - format!( - "Sphere self-product after curvature change: {}", - rep_for_debug.dot(&(&*Q * &rep_for_debug)) - ) - )); -} - // --- partial matrices --- pub struct MatrixEntry { @@ -199,13 +164,6 @@ impl ConfigSubspace { ).collect::>().as_slice() ); - /* DEBUG */ - // print the eigenvalues - #[cfg(all(target_family = "wasm", target_os = "unknown"))] - console::log_1(&JsValue::from( - format!("Eigenvalues used to find kernel:{}", eig.eigenvalues) - )); - // express the basis in the standard coordinates let basis_std = proj_to_std * &basis_proj; @@ -425,9 +383,22 @@ pub fn realize_gram( // start the descent history let mut history = DescentHistory::new(); + // handle the case where the assembly is empty. our general realization + // routine can't handle this case because it builds the Hessian using + // `DMatrix::from_columns`, which panics when the list of columns is empty + let assembly_dim = guess.ncols(); + if assembly_dim == 0 { + let result = Ok( + ConfigNeighborhood { + config: guess.clone(), + nbhd: ConfigSubspace::zero(0) + } + ); + return Realization { result, history } + } + // find the dimension of the search space let element_dim = guess.nrows(); - let assembly_dim = guess.ncols(); let total_dim = element_dim * assembly_dim; // scale the tolerance From ef1a579ac0893a150c37f570729a4ba77d62b6e8 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 4 Aug 2025 23:34:33 +0000 Subject: [PATCH 124/126] refactor: Code formatting (#108) Primarily, switch to using trailing commas. Also uniformizes commas with respect to switch branches, makes function call layout more consistent, line breaking more consistent, alphabetizes imports, uses the field init shorthand when possible, etc. Resolves #99. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/108 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/examples/kaleidocycle.rs | 2 +- app-proto/examples/point-on-sphere.rs | 4 +- app-proto/examples/three-spheres.rs | 6 +- app-proto/src/assembly.rs | 48 +++--- app-proto/src/components/add_remove.rs | 14 +- app-proto/src/components/diagnostics.rs | 44 +++--- app-proto/src/components/display.rs | 117 ++++++++------ app-proto/src/components/outline.rs | 94 ++++++----- .../src/components/test_assembly_chooser.rs | 148 +++++++++--------- app-proto/src/engine.rs | 118 +++++++------- app-proto/src/main.rs | 8 +- app-proto/src/specified.rs | 4 +- 12 files changed, 310 insertions(+), 297 deletions(-) diff --git a/app-proto/examples/kaleidocycle.rs b/app-proto/examples/kaleidocycle.rs index 7ca1f97..ae4eb07 100644 --- a/app-proto/examples/kaleidocycle.rs +++ b/app-proto/examples/kaleidocycle.rs @@ -23,7 +23,7 @@ fn main() { let twist_motion: DMatrix<_> = (0..N_POINTS).step_by(4).flat_map( |n| [ tangent.proj(&up.as_view(), n), - tangent.proj(&down.as_view(), n+1) + tangent.proj(&down.as_view(), n+1), ] ).sum(); let normalization = 5.0 / twist_motion[(2, 0)]; diff --git a/app-proto/examples/point-on-sphere.rs b/app-proto/examples/point-on-sphere.rs index 89dee76..a73490e 100644 --- a/app-proto/examples/point-on-sphere.rs +++ b/app-proto/examples/point-on-sphere.rs @@ -6,7 +6,7 @@ use dyna3::engine::{ realize_gram, sphere, ConfigNeighborhood, - ConstraintProblem + ConstraintProblem, }; fn main() { @@ -25,7 +25,7 @@ fn main() { ); print::title("Point on a sphere"); print::realization_diagnostics(&realization); - if let Ok(ConfigNeighborhood{ config, .. }) = realization.result { + if let Ok(ConfigNeighborhood { config, .. }) = realization.result { print::gram_matrix(&config); print::config(&config); } diff --git a/app-proto/examples/three-spheres.rs b/app-proto/examples/three-spheres.rs index aa5a105..7901e31 100644 --- a/app-proto/examples/three-spheres.rs +++ b/app-proto/examples/three-spheres.rs @@ -5,7 +5,7 @@ use dyna3::engine::{ realize_gram, sphere, ConfigNeighborhood, - ConstraintProblem + ConstraintProblem, }; fn main() { @@ -14,7 +14,7 @@ fn main() { &[ sphere(1.0, 0.0, 0.0, 1.0), sphere(-0.5, a, 0.0, 1.0), - sphere(-0.5, -a, 0.0, 1.0) + sphere(-0.5, -a, 0.0, 1.0), ] }); for j in 0..3 { @@ -27,7 +27,7 @@ fn main() { ); print::title("Three spheres"); print::realization_diagnostics(&realization); - if let Ok(ConfigNeighborhood{ config, .. }) = realization.result { + if let Ok(ConfigNeighborhood { config, .. }) = realization.result { print::gram_matrix(&config); } print::loss_history(&realization.history); diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 26fb4aa..43066fd 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -1,13 +1,13 @@ use nalgebra::{DMatrix, DVector, DVectorView}; use std::{ cell::Cell, - collections::{BTreeMap, BTreeSet}, cmp::Ordering, + collections::{BTreeMap, BTreeSet}, fmt, fmt::{Debug, Formatter}, hash::{Hash, Hasher}, rc::Rc, - sync::{atomic, atomic::AtomicU64} + sync::{atomic, atomic::AtomicU64}, }; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */ @@ -26,9 +26,9 @@ use crate::{ ConfigSubspace, ConstraintProblem, DescentHistory, - Realization + Realization, }, - specified::SpecifiedValue + specified::SpecifiedValue, }; pub type ElementColor = [f32; 3]; @@ -164,7 +164,7 @@ pub struct Sphere { pub ghost: Signal, pub regulators: Signal>>, serial: u64, - column_index: Cell> + column_index: Cell>, } impl Sphere { @@ -174,17 +174,17 @@ impl Sphere { id: String, label: String, color: ElementColor, - representation: DVector + representation: DVector, ) -> Sphere { Sphere { - id: id, - label: label, - color: color, + id, + label, + color, representation: create_signal(representation), ghost: create_signal(false), regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), - column_index: None.into() + column_index: None.into(), } } } @@ -199,7 +199,7 @@ impl Element for Sphere { id, format!("Sphere {id_num}"), [0.75_f32, 0.75_f32, 0.75_f32], - sphere(0.0, 0.0, 0.0, 1.0) + sphere(0.0, 0.0, 0.0, 1.0), ) } @@ -264,7 +264,7 @@ pub struct Point { pub ghost: Signal, pub regulators: Signal>>, serial: u64, - column_index: Cell> + column_index: Cell>, } impl Point { @@ -274,7 +274,7 @@ impl Point { id: String, label: String, color: ElementColor, - representation: DVector + representation: DVector, ) -> Point { Point { id, @@ -284,7 +284,7 @@ impl Point { ghost: create_signal(false), regulators: create_signal(BTreeSet::new()), serial: Self::next_serial(), - column_index: None.into() + column_index: None.into(), } } } @@ -299,7 +299,7 @@ impl Element for Point { id, format!("Point {id_num}"), [0.75_f32, 0.75_f32, 0.75_f32], - point(0.0, 0.0, 0.0) + point(0.0, 0.0, 0.0), ) } @@ -389,7 +389,7 @@ pub struct InversiveDistanceRegulator { pub subjects: [Rc; 2], pub measurement: ReadSignal, pub set_point: Signal, - serial: u64 + serial: u64, } impl InversiveDistanceRegulator { @@ -449,7 +449,7 @@ pub struct HalfCurvatureRegulator { pub subject: Rc, pub measurement: ReadSignal, pub set_point: Signal, - serial: u64 + serial: u64, } impl HalfCurvatureRegulator { @@ -501,7 +501,7 @@ impl ProblemPoser for HalfCurvatureRegulator { // the velocity is expressed in uniform coordinates pub struct ElementMotion<'a> { pub element: Rc, - pub velocity: DVectorView<'a, f64> + pub velocity: DVectorView<'a, f64>, } type AssemblyMotion<'a> = Vec>; @@ -533,7 +533,7 @@ pub struct Assembly { // realization diagnostics pub realization_status: Signal>, - pub descent_history: Signal + pub descent_history: Signal, } impl Assembly { @@ -546,7 +546,7 @@ impl Assembly { elements_by_id: create_signal(BTreeMap::default()), realization_trigger: create_signal(()), realization_status: create_signal(Ok(())), - descent_history: create_signal(DescentHistory::new()) + descent_history: create_signal(DescentHistory::new()), }; // realize the assembly whenever the element list, the regulator list, @@ -724,7 +724,7 @@ impl Assembly { // `Err(message)` we received from the match: we're changing the // `Ok` type from `Realization` to `()` self.realization_status.set(Err(message)) - } + }, } } @@ -807,7 +807,7 @@ impl Assembly { }, None => { console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) - } + }, }; }); } @@ -867,7 +867,7 @@ mod tests { String::from(sphere_id), String::from("Sphere 0"), [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS) + engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS), ) ); @@ -881,7 +881,7 @@ mod tests { vec![ ElementMotion { element: sphere.clone(), - velocity: velocity.as_view() + velocity: velocity.as_view(), } ] ); diff --git a/app-proto/src/components/add_remove.rs b/app-proto/src/components/add_remove.rs index a685482..4196640 100644 --- a/app-proto/src/components/add_remove.rs +++ b/app-proto/src/components/add_remove.rs @@ -4,15 +4,15 @@ use sycamore::prelude::*; use super::test_assembly_chooser::TestAssemblyChooser; use crate::{ AppState, - assembly::{InversiveDistanceRegulator, Point, Sphere} + assembly::{InversiveDistanceRegulator, Point, Sphere}, }; #[component] pub fn AddRemove() -> View { view! { - div(id="add-remove") { + div(id = "add-remove") { button( - on:click=|_| { + on:click = |_| { let state = use_context::(); batch(|| { // this call is batched to avoid redundant realizations. @@ -33,18 +33,18 @@ pub fn AddRemove() -> View { } ) { "Add sphere" } button( - on:click=|_| { + on:click = |_| { let state = use_context::(); state.assembly.insert_element_default::(); } ) { "Add point" } button( - class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button - disabled={ + class = "emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button + disabled = { let state = use_context::(); state.selection.with(|sel| sel.len() != 2) }, - on:click=|_| { + on:click = |_| { let state = use_context::(); let subjects: [_; 2] = state.selection.with( // the button is only enabled when two elements are diff --git a/app-proto/src/components/diagnostics.rs b/app-proto/src/components/diagnostics.rs index a2f090a..b274dca 100644 --- a/app-proto/src/components/diagnostics.rs +++ b/app-proto/src/components/diagnostics.rs @@ -11,14 +11,12 @@ use crate::AppState; #[derive(Clone)] struct DiagnosticsState { - active_tab: Signal + active_tab: Signal, } impl DiagnosticsState { fn new(initial_tab: String) -> DiagnosticsState { - DiagnosticsState { - active_tab: create_signal(initial_tab) - } + DiagnosticsState { active_tab: create_signal(initial_tab) } } } @@ -29,20 +27,20 @@ fn RealizationStatus() -> View { let realization_status = state.assembly.realization_status; view! { div( - id="realization-status", - class=realization_status.with( + id = "realization-status", + class = realization_status.with( |status| match status { Ok(_) => "", - Err(_) => "invalid" + Err(_) => "invalid", } ) ) { - div(class="status") + div(class = "status") div { (realization_status.with( |status| match status { Ok(_) => "Target accuracy achieved".to_string(), - Err(message) => message.clone() + Err(message) => message.clone(), } )) } @@ -53,7 +51,7 @@ fn RealizationStatus() -> View { fn into_log10_time_point((step, value): (usize, f64)) -> Vec> { vec![ Some(step as f64), - if value == 0.0 { None } else { Some(value.abs().log10()) } + if value == 0.0 { None } else { Some(value.abs().log10()) }, ] } @@ -105,7 +103,7 @@ fn LossHistory() -> View { }); view! { - div(id=CONTAINER_ID, class="diagnostics-chart") + div(id = CONTAINER_ID, class = "diagnostics-chart") } } @@ -122,7 +120,7 @@ fn SpectrumHistory() -> View { // positive, negative, and strictly-zero parts let ( hess_eigvals_zero, - hess_eigvals_nonzero + hess_eigvals_nonzero, ): (Vec<_>, Vec<_>) = state.assembly.descent_history.with( |history| history.hess_eigvals .iter() @@ -143,7 +141,7 @@ fn SpectrumHistory() -> View { .unwrap_or(1.0); let ( hess_eigvals_pos, - hess_eigvals_neg + hess_eigvals_neg, ): (Vec<_>, Vec<_>) = hess_eigvals_nonzero .into_iter() .partition(|&(_, val)| val > 0.0); @@ -211,7 +209,7 @@ fn SpectrumHistory() -> View { }); view! { - div(id=CONTAINER_ID, class="diagnostics-chart") + div(id = CONTAINER_ID, class = "diagnostics-chart") } } @@ -220,8 +218,8 @@ fn DiagnosticsPanel(name: &'static str, children: Children) -> View { let diagnostics_state = use_context::(); view! { div( - class="diagnostics-panel", - "hidden"=diagnostics_state.active_tab.with( + class = "diagnostics-panel", + "hidden" = diagnostics_state.active_tab.with( |active_tab| { if active_tab == name { None @@ -243,16 +241,16 @@ pub fn Diagnostics() -> View { provide_context(diagnostics_state); view! { - div(id="diagnostics") { - div(id="diagnostics-bar") { + div(id = "diagnostics") { + div(id = "diagnostics-bar") { RealizationStatus {} - select(bind:value=active_tab) { - option(value="loss") { "Loss" } - option(value="spectrum") { "Spectrum" } + select(bind:value = active_tab) { + option(value = "loss") { "Loss" } + option(value = "spectrum") { "Spectrum" } } } - DiagnosticsPanel(name="loss") { LossHistory {} } - DiagnosticsPanel(name="spectrum") { SpectrumHistory {} } + DiagnosticsPanel(name = "loss") { LossHistory {} } + DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} } } } } \ No newline at end of file diff --git a/app-proto/src/components/display.rs b/app-proto/src/components/display.rs index 1646c4e..a0cdba6 100644 --- a/app-proto/src/components/display.rs +++ b/app-proto/src/components/display.rs @@ -12,12 +12,12 @@ use web_sys::{ WebGlProgram, WebGlShader, WebGlUniformLocation, - wasm_bindgen::{JsCast, JsValue} + wasm_bindgen::{JsCast, JsValue}, }; use crate::{ AppState, - assembly::{Element, ElementColor, ElementMotion, Point, Sphere} + assembly::{Element, ElementColor, ElementMotion, Point, Sphere}, }; // --- color --- @@ -37,15 +37,15 @@ fn combine_channels(color: ElementColor, opacity: f32) -> ColorWithOpacity { struct SceneSpheres { representations: Vec>, colors_with_opacity: Vec, - highlights: Vec + highlights: Vec, } impl SceneSpheres { - fn new() -> SceneSpheres{ + fn new() -> SceneSpheres { SceneSpheres { representations: Vec::new(), colors_with_opacity: Vec::new(), - highlights: Vec::new() + highlights: Vec::new(), } } @@ -53,7 +53,10 @@ impl SceneSpheres { self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer") } - fn push(&mut self, representation: DVector, color: ElementColor, opacity: f32, highlight: f32) { + fn push( + &mut self, representation: DVector, + color: ElementColor, opacity: f32, highlight: f32, + ) { self.representations.push(representation); self.colors_with_opacity.push(combine_channels(color, opacity)); self.highlights.push(highlight); @@ -64,7 +67,7 @@ struct ScenePoints { representations: Vec>, colors_with_opacity: Vec, highlights: Vec, - selections: Vec + selections: Vec, } impl ScenePoints { @@ -73,11 +76,14 @@ impl ScenePoints { representations: Vec::new(), colors_with_opacity: Vec::new(), highlights: Vec::new(), - selections: Vec::new() + selections: Vec::new(), } } - fn push(&mut self, representation: DVector, color: ElementColor, opacity: f32, highlight: f32, selected: bool) { + fn push( + &mut self, representation: DVector, + color: ElementColor, opacity: f32, highlight: f32, selected: bool, + ) { self.representations.push(representation); self.colors_with_opacity.push(combine_channels(color, opacity)); self.highlights.push(highlight); @@ -87,14 +93,14 @@ impl ScenePoints { pub struct Scene { spheres: SceneSpheres, - points: ScenePoints + points: ScenePoints, } impl Scene { fn new() -> Scene { Scene { spheres: SceneSpheres::new(), - points: ScenePoints::new() + points: ScenePoints::new(), } } } @@ -105,7 +111,12 @@ pub trait DisplayItem { // the smallest positive depth, represented as a multiple of `dir`, where // the line generated by `dir` hits the element. returns `None` if the line // misses the element - fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, pixel_size: f64) -> Option; + fn cast( + &self, + dir: Vector3, + assembly_to_world: &DMatrix, + pixel_size: f64, + ) -> Option; } impl DisplayItem for Sphere { @@ -124,7 +135,12 @@ impl DisplayItem for Sphere { // this method should be kept synchronized with `sphere_cast` in // `spheres.frag`, which does essentially the same thing on the GPU side - fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, _pixel_size: f64) -> Option { + fn cast( + &self, + dir: Vector3, + assembly_to_world: &DMatrix, + _pixel_size: f64, + ) -> Option { // if `a/b` is less than this threshold, we approximate // `a*u^2 + b*u + c` by the linear function `b*u + c` const DEG_THRESHOLD: f64 = 1e-9; @@ -177,7 +193,12 @@ impl DisplayItem for Point { } /* SCAFFOLDING */ - fn cast(&self, dir: Vector3, assembly_to_world: &DMatrix, pixel_size: f64) -> Option { + fn cast( + &self, + dir: Vector3, + assembly_to_world: &DMatrix, + pixel_size: f64, + ) -> Option { let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); if rep[2] < 0.0 { // this constant should be kept synchronized with `point.frag` @@ -220,7 +241,7 @@ fn compile_shader( fn set_up_program( context: &WebGl2RenderingContext, vertex_shader_source: &str, - fragment_shader_source: &str + fragment_shader_source: &str, ) -> WebGlProgram { // compile the shaders let vertex_shader = compile_shader( @@ -260,12 +281,12 @@ fn get_uniform_array_locations( context: &WebGl2RenderingContext, program: &WebGlProgram, var_name: &str, - member_name_opt: Option<&str> + member_name_opt: Option<&str>, ) -> [Option; N] { array::from_fn(|n| { let name = match member_name_opt { Some(member_name) => format!("{var_name}[{n}].{member_name}"), - None => format!("{var_name}[{n}]") + None => format!("{var_name}[{n}]"), }; context.get_uniform_location(&program, name.as_str()) }) @@ -276,7 +297,7 @@ fn bind_to_attribute( context: &WebGl2RenderingContext, attr_index: u32, attr_size: i32, - buffer: &Option + buffer: &Option, ) { context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref()); context.vertex_attrib_pointer_with_i32( @@ -292,7 +313,7 @@ fn bind_to_attribute( // load the given data into a new vertex buffer object fn load_new_buffer( context: &WebGl2RenderingContext, - data: &[f32] + data: &[f32], ) -> Option { // create a buffer and bind it to ARRAY_BUFFER let buffer = context.create_buffer(); @@ -319,7 +340,7 @@ fn bind_new_buffer_to_attribute( context: &WebGl2RenderingContext, attr_index: u32, attr_size: i32, - data: &[f32] + data: &[f32], ) { let buffer = load_new_buffer(context, data); bind_to_attribute(context, attr_index, attr_size, &buffer); @@ -341,9 +362,9 @@ fn event_dir(event: &MouseEvent) -> (Vector3, f64) { Vector3::new( FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim, FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim, - -1.0 + -1.0, ), - FOCAL_SLOPE * 2.0 / shortdim + FOCAL_SLOPE * 2.0 / shortdim, ) } @@ -443,14 +464,14 @@ pub fn Display() -> View { let sphere_program = set_up_program( &ctx, include_str!("identity.vert"), - include_str!("spheres.frag") + include_str!("spheres.frag"), ); // set up the point rendering program let point_program = set_up_program( &ctx, include_str!("point.vert"), - include_str!("point.frag") + include_str!("point.frag"), ); /* DEBUG */ @@ -467,7 +488,7 @@ pub fn Display() -> View { // capped at 1024 elements console::log_2( &ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(), - &JsValue::from("uniform vectors available") + &JsValue::from("uniform vectors available"), ); // find the sphere program's vertex attribute @@ -503,7 +524,7 @@ pub fn Display() -> View { // southeast triangle -1.0, -1.0, 0.0, 1.0, 1.0, 0.0, - 1.0, -1.0, 0.0 + 1.0, -1.0, 0.0, ]; let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions); @@ -596,7 +617,7 @@ pub fn Display() -> View { vec![ ElementMotion { element: sel, - velocity: elt_motion.as_view() + velocity: elt_motion.as_view(), } ] ); @@ -629,7 +650,7 @@ pub fn Display() -> View { 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, u, 0.0, 0.0, 2.0*u, 1.0, u*u, - 0.0, 0.0, 0.0, 0.0, 1.0 + 0.0, 0.0, 0.0, 0.0, 1.0, ]) }; let asm_to_world = &location * &orientation; @@ -668,19 +689,19 @@ pub fn Display() -> View { let v = &sphere_reps_world[n]; ctx.uniform3fv_with_f32_array( sphere_sp_locs[n].as_ref(), - v.rows(0, 3).as_slice() + v.rows(0, 3).as_slice(), ); ctx.uniform2fv_with_f32_array( sphere_lt_locs[n].as_ref(), - v.rows(3, 2).as_slice() + v.rows(3, 2).as_slice(), ); ctx.uniform4fv_with_f32_array( sphere_color_locs[n].as_ref(), - &scene.spheres.colors_with_opacity[n] + &scene.spheres.colors_with_opacity[n], ); ctx.uniform1f( sphere_highlight_locs[n].as_ref(), - scene.spheres.highlights[n] + scene.spheres.highlights[n], ); } @@ -773,7 +794,7 @@ pub fn Display() -> View { "ArrowLeft" if shift => roll_ccw.set(value), "ArrowRight" => yaw_right.set(value), "ArrowLeft" => yaw_left.set(value), - _ => navigating = false + _ => navigating = false, }; if navigating { scene_changed.set(true); @@ -793,7 +814,7 @@ pub fn Display() -> View { "s" | "S" => translate_neg_y.set(value), "]" | "}" => shrink_neg.set(value), "[" | "{" => shrink_pos.set(value), - _ => manipulating = false + _ => manipulating = false, }; if manipulating { event.prevent_default(); @@ -805,12 +826,12 @@ pub fn Display() -> View { // switch back to integer-valued parameters when that becomes possible // again canvas( - ref=display, - id="display", - width="600", - height="600", - tabindex="0", - on:keydown=move |event: KeyboardEvent| { + ref = display, + id = "display", + width = "600", + height = "600", + tabindex = "0", + on:keydown = move |event: KeyboardEvent| { if event.key() == "Shift" { // swap navigation inputs roll_cw.set(yaw_right.get()); @@ -836,7 +857,7 @@ pub fn Display() -> View { set_manip_signal(&event, 1.0); } }, - on:keyup=move |event: KeyboardEvent| { + on:keyup = move |event: KeyboardEvent| { if event.key() == "Shift" { // swap navigation inputs yaw_right.set(roll_cw.get()); @@ -858,7 +879,7 @@ pub fn Display() -> View { set_manip_signal(&event, 0.0); } }, - on:blur=move |_| { + on:blur = move |_| { pitch_up.set(0.0); pitch_down.set(0.0); yaw_right.set(0.0); @@ -866,7 +887,7 @@ pub fn Display() -> View { roll_ccw.set(0.0); roll_cw.set(0.0); }, - on:click=move |event: MouseEvent| { + on:click = move |event: MouseEvent| { // find the nearest element along the pointer direction let (dir, pixel_size) = event_dir(&event); console::log_1(&JsValue::from(dir.to_string())); @@ -883,18 +904,18 @@ pub fn Display() -> View { clicked = Some((elt, depth)) } }, - None => clicked = Some((elt, depth)) - } - None => () + None => clicked = Some((elt, depth)), + }, + None => (), }; } // if we clicked something, select it match clicked { Some((elt, _)) => state.select(&elt, event.shift_key()), - None => state.selection.update(|sel| sel.clear()) + None => state.selection.update(|sel| sel.clear()), }; - } + }, ) } } \ No newline at end of file diff --git a/app-proto/src/components/outline.rs b/app-proto/src/components/outline.rs index 77d8575..5355042 100644 --- a/app-proto/src/components/outline.rs +++ b/app-proto/src/components/outline.rs @@ -1,11 +1,7 @@ use itertools::Itertools; use std::rc::Rc; use sycamore::prelude::*; -use web_sys::{ - KeyboardEvent, - MouseEvent, - wasm_bindgen::JsCast -}; +use web_sys::{KeyboardEvent, MouseEvent, wasm_bindgen::JsCast}; use crate::{ AppState, @@ -13,7 +9,7 @@ use crate::{ Element, HalfCurvatureRegulator, InversiveDistanceRegulator, - Regulator + Regulator, }, specified::SpecifiedValue }; @@ -49,8 +45,8 @@ fn RegulatorInput(regulator: Rc) -> View { view! { input( - r#type="text", - class=move || { + r#type = "text", + class = move || { if valid.get() { set_point.with(|set_pt| { if set_pt.is_present() { @@ -63,27 +59,27 @@ fn RegulatorInput(regulator: Rc) -> View { "regulator-input invalid" } }, - placeholder=measurement.with(|result| result.to_string()), - bind:value=value, - on:change=move |_| { + placeholder = measurement.with(|result| result.to_string()), + bind:value = value, + on:change = move |_| { valid.set( match SpecifiedValue::try_from(value.get_clone_untracked()) { Ok(set_pt) => { set_point.set(set_pt); true - } - Err(_) => false + }, + Err(_) => false, } ) }, - on:keydown={ + on:keydown = { move |event: KeyboardEvent| { match event.key().as_str() { "Escape" => reset_value(), - _ => () + _ => (), } } - } + }, ) } } @@ -100,11 +96,11 @@ impl OutlineItem for InversiveDistanceRegulator { self.subjects[0].label() }.clone(); view! { - li(class="regulator") { - div(class="regulator-label") { (other_subject_label) } - div(class="regulator-type") { "Inversive distance" } - RegulatorInput(regulator=self) - div(class="status") + li(class = "regulator") { + div(class = "regulator-label") { (other_subject_label) } + div(class = "regulator-type") { "Inversive distance" } + RegulatorInput(regulator = self) + div(class = "status") } } } @@ -113,11 +109,11 @@ impl OutlineItem for InversiveDistanceRegulator { impl OutlineItem for HalfCurvatureRegulator { fn outline_item(self: Rc, _element: &Rc) -> View { view! { - li(class="regulator") { - div(class="regulator-label") // for spacing - div(class="regulator-type") { "Half-curvature" } - RegulatorInput(regulator=self) - div(class="status") + li(class = "regulator") { + div(class = "regulator-label") // for spacing + div(class = "regulator-type") { "Half-curvature" } + RegulatorInput(regulator = self) + div(class = "status") } } } @@ -156,10 +152,10 @@ fn ElementOutlineItem(element: Rc) -> View { let details_node = create_node_ref(); view! { li { - details(ref=details_node) { + details(ref = details_node) { summary( - class=class.get(), - on:keydown={ + class = class.get(), + on:keydown = { let element_for_handler = element.clone(); move |event: KeyboardEvent| { match event.key().as_str() { @@ -179,18 +175,18 @@ fn ElementOutlineItem(element: Rc) -> View { .unchecked_into::() .remove_attribute("open"); }, - _ => () + _ => (), } } } ) { div( - class="element-switch", - on:click=|event: MouseEvent| event.stop_propagation() + class = "element-switch", + on:click = |event: MouseEvent| event.stop_propagation() ) div( - class="element", - on:click={ + class = "element", + on:click = { let state_for_handler = state.clone(); let element_for_handler = element.clone(); move |event: MouseEvent| { @@ -200,20 +196,20 @@ fn ElementOutlineItem(element: Rc) -> View { } } ) { - div(class="element-label") { (label) } - div(class="element-representation") { (rep_components) } + div(class = "element-label") { (label) } + div(class = "element-representation") { (rep_components) } input( - r#type="checkbox", - bind:checked=element.ghost(), - on:click=|event: MouseEvent| event.stop_propagation() + r#type = "checkbox", + bind:checked = element.ghost(), + on:click = |event: MouseEvent| event.stop_propagation() ) } } - ul(class="regulators") { + ul(class = "regulators") { Keyed( - list=regulator_list, - view=move |reg| reg.outline_item(&element), - key=|reg| reg.serial() + list = regulator_list, + view = move |reg| reg.outline_item(&element), + key = |reg| reg.serial() ) } } @@ -246,18 +242,18 @@ pub fn Outline() -> View { view! { ul( - id="outline", - on:click={ + id = "outline", + on:click = { let state = use_context::(); move |_| state.selection.update(|sel| sel.clear()) } ) { Keyed( - list=element_list, - view=|elt| view! { - ElementOutlineItem(element=elt) + list = element_list, + view = |elt| view! { + ElementOutlineItem(element = elt) }, - key=|elt| elt.serial() + key = |elt| elt.serial() ) } } diff --git a/app-proto/src/components/test_assembly_chooser.rs b/app-proto/src/components/test_assembly_chooser.rs index b58dd1a..5ed94ad 100644 --- a/app-proto/src/components/test_assembly_chooser.rs +++ b/app-proto/src/components/test_assembly_chooser.rs @@ -6,17 +6,17 @@ use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ AppState, - engine, - engine::DescentHistory, assembly::{ Assembly, Element, ElementColor, InversiveDistanceRegulator, Point, - Sphere + Sphere, }, - specified::SpecifiedValue + engine, + engine::DescentHistory, + specified::SpecifiedValue, }; // --- loaders --- @@ -32,7 +32,7 @@ fn load_gen_assemb(assembly: &Assembly) { String::from("gemini_a"), String::from("Castor"), [1.00_f32, 0.25_f32, 0.00_f32], - engine::sphere(0.5, 0.5, 0.0, 1.0) + engine::sphere(0.5, 0.5, 0.0, 1.0), ) ); let _ = assembly.try_insert_element( @@ -40,7 +40,7 @@ fn load_gen_assemb(assembly: &Assembly) { String::from("gemini_b"), String::from("Pollux"), [0.00_f32, 0.25_f32, 1.00_f32], - engine::sphere(-0.5, -0.5, 0.0, 1.0) + engine::sphere(-0.5, -0.5, 0.0, 1.0), ) ); let _ = assembly.try_insert_element( @@ -48,7 +48,7 @@ fn load_gen_assemb(assembly: &Assembly) { String::from("ursa_major"), String::from("Ursa major"), [0.25_f32, 0.00_f32, 1.00_f32], - engine::sphere(-0.5, 0.5, 0.0, 0.75) + engine::sphere(-0.5, 0.5, 0.0, 0.75), ) ); let _ = assembly.try_insert_element( @@ -56,7 +56,7 @@ fn load_gen_assemb(assembly: &Assembly) { String::from("ursa_minor"), String::from("Ursa minor"), [0.25_f32, 1.00_f32, 0.00_f32], - engine::sphere(0.5, -0.5, 0.0, 0.5) + engine::sphere(0.5, -0.5, 0.0, 0.5), ) ); let _ = assembly.try_insert_element( @@ -64,7 +64,7 @@ fn load_gen_assemb(assembly: &Assembly) { String::from("moon_deimos"), String::from("Deimos"), [0.75_f32, 0.75_f32, 0.00_f32], - engine::sphere(0.0, 0.15, 1.0, 0.25) + engine::sphere(0.0, 0.15, 1.0, 0.25), ) ); let _ = assembly.try_insert_element( @@ -72,7 +72,7 @@ fn load_gen_assemb(assembly: &Assembly) { String::from("moon_phobos"), String::from("Phobos"), [0.00_f32, 0.75_f32, 0.50_f32], - engine::sphere(0.0, -0.15, -1.0, 0.25) + engine::sphere(0.0, -0.15, -1.0, 0.25), ) ); } @@ -85,7 +85,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { "central".to_string(), "Central".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0) + engine::sphere(0.0, 0.0, 0.0, 1.0), ) ); let _ = assembly.try_insert_element( @@ -93,7 +93,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { "assemb_plane".to_string(), "Assembly plane".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) + engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), ) ); let _ = assembly.try_insert_element( @@ -101,7 +101,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { "side1".to_string(), "Side 1".to_string(), [1.00_f32, 0.00_f32, 0.25_f32], - engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) + engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), ) ); let _ = assembly.try_insert_element( @@ -109,7 +109,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { "side2".to_string(), "Side 2".to_string(), [0.25_f32, 1.00_f32, 0.00_f32], - engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) + engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), ) ); let _ = assembly.try_insert_element( @@ -117,7 +117,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { "side3".to_string(), "Side 3".to_string(), [0.00_f32, 0.25_f32, 1.00_f32], - engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) + engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), ) ); let _ = assembly.try_insert_element( @@ -125,7 +125,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { "corner1".to_string(), "Corner 1".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) + engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), ) ); let _ = assembly.try_insert_element( @@ -133,7 +133,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { "corner2".to_string(), "Corner 2".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) + engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0), ) ); let _ = assembly.try_insert_element( @@ -141,7 +141,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { String::from("corner3"), String::from("Corner 3"), [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0) + engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0), ) ); @@ -202,7 +202,7 @@ fn load_pointed_assemb(assembly: &Assembly) { format!("point_front"), format!("Front point"), [0.75_f32, 0.75_f32, 0.75_f32], - engine::point(0.0, 0.0, FRAC_1_SQRT_2) + engine::point(0.0, 0.0, FRAC_1_SQRT_2), ) ); let _ = assembly.try_insert_element( @@ -210,7 +210,7 @@ fn load_pointed_assemb(assembly: &Assembly) { format!("point_back"), format!("Back point"), [0.75_f32, 0.75_f32, 0.75_f32], - engine::point(0.0, 0.0, -FRAC_1_SQRT_2) + engine::point(0.0, 0.0, -FRAC_1_SQRT_2), ) ); for index_x in 0..=1 { @@ -223,7 +223,7 @@ fn load_pointed_assemb(assembly: &Assembly) { format!("sphere{index_x}{index_y}"), format!("Sphere {index_x}{index_y}"), [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], - engine::sphere(x, y, 0.0, 1.0) + engine::sphere(x, y, 0.0, 1.0), ) ); @@ -232,7 +232,7 @@ fn load_pointed_assemb(assembly: &Assembly) { format!("point{index_x}{index_y}"), format!("Point {index_x}{index_y}"), [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], - engine::point(x, y, 0.0) + engine::point(x, y, 0.0), ) ); } @@ -256,56 +256,56 @@ fn load_tridim_icosahedron_assemb(assembly: &Assembly) { "a1".to_string(), "A₁".to_string(), COLOR_A, - engine::point(0.25, 0.75, 0.75) + engine::point(0.25, 0.75, 0.75), ), Point::new( "a2".to_string(), "A₂".to_string(), COLOR_A, - engine::point(0.75, 0.25, 0.75) + engine::point(0.75, 0.25, 0.75), ), Point::new( "a3".to_string(), "A₃".to_string(), COLOR_A, - engine::point(0.75, 0.75, 0.25) + engine::point(0.75, 0.75, 0.25), ), Point::new( "b1".to_string(), "B₁".to_string(), COLOR_B, - engine::point(0.75, -0.25, -0.25) + engine::point(0.75, -0.25, -0.25), ), Point::new( "b2".to_string(), "B₂".to_string(), COLOR_B, - engine::point(-0.25, 0.75, -0.25) + engine::point(-0.25, 0.75, -0.25), ), Point::new( "b3".to_string(), "B₃".to_string(), COLOR_B, - engine::point(-0.25, -0.25, 0.75) + engine::point(-0.25, -0.25, 0.75), ), Point::new( "c1".to_string(), "C₁".to_string(), COLOR_C, - engine::point(0.0, -1.0, -1.0) + engine::point(0.0, -1.0, -1.0), ), Point::new( "c2".to_string(), "C₂".to_string(), COLOR_C, - engine::point(-1.0, 0.0, -1.0) + engine::point(-1.0, 0.0, -1.0), ), Point::new( "c3".to_string(), "C₃".to_string(), COLOR_C, - engine::point(-1.0, -1.0, 0.0) - ) + engine::point(-1.0, -1.0, 0.0), + ), ]; for vertex in vertices { let _ = assembly.try_insert_element(vertex); @@ -320,20 +320,20 @@ fn load_tridim_icosahedron_assemb(assembly: &Assembly) { "face1".to_string(), "Face 1".to_string(), COLOR_FACE, - engine::sphere_with_offset(frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0) + engine::sphere_with_offset(frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0), ), Sphere::new( "face2".to_string(), "Face 2".to_string(), COLOR_FACE, - engine::sphere_with_offset(-frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0) + engine::sphere_with_offset(-frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0), ), Sphere::new( "face3".to_string(), "Face 3".to_string(), COLOR_FACE, - engine::sphere_with_offset(-frac_1_sqrt_6, -frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, 0.0) - ) + engine::sphere_with_offset(-frac_1_sqrt_6, -frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, 0.0), + ), ]; for face in faces { face.ghost().set(true); @@ -416,7 +416,7 @@ fn load_dodeca_packing_assemb(assembly: &Assembly) { "substrate".to_string(), "Substrate".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0) + engine::sphere(0.0, 0.0, 0.0, 1.0), ) ); let substrate = assembly.elements_by_id.with_untracked( @@ -456,7 +456,7 @@ fn load_dodeca_packing_assemb(assembly: &Assembly) { id_a.clone(), format!("A{label_sub}"), COLOR_A, - engine::sphere(0.0, small_coord, big_coord, face_radii[k]) + engine::sphere(0.0, small_coord, big_coord, face_radii[k]), ) ); faces.push( @@ -472,7 +472,7 @@ fn load_dodeca_packing_assemb(assembly: &Assembly) { id_b.clone(), format!("B{label_sub}"), COLOR_B, - engine::sphere(small_coord, big_coord, 0.0, face_radii[k]) + engine::sphere(small_coord, big_coord, 0.0, face_radii[k]), ) ); faces.push( @@ -488,7 +488,7 @@ fn load_dodeca_packing_assemb(assembly: &Assembly) { id_c.clone(), format!("C{label_sub}"), COLOR_C, - engine::sphere(big_coord, 0.0, small_coord, face_radii[k]) + engine::sphere(big_coord, 0.0, small_coord, face_radii[k]), ) ); faces.push( @@ -559,19 +559,19 @@ fn load_balanced_assemb(assembly: &Assembly) { "outer".to_string(), "Outer".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, R_OUTER) + engine::sphere(0.0, 0.0, 0.0, R_OUTER), ), Sphere::new( "a".to_string(), "A".to_string(), [1.00_f32, 0.00_f32, 0.25_f32], - engine::sphere(0.0, 4.0, 0.0, R_INNER) + engine::sphere(0.0, 4.0, 0.0, R_INNER), ), Sphere::new( "b".to_string(), "B".to_string(), [0.00_f32, 0.25_f32, 1.00_f32], - engine::sphere(0.0, -4.0, 0.0, R_INNER) + engine::sphere(0.0, -4.0, 0.0, R_INNER), ), ]; for sphere in spheres { @@ -589,7 +589,7 @@ fn load_balanced_assemb(assembly: &Assembly) { for (sphere, radius) in [ (outer.clone(), R_OUTER), (a.clone(), R_INNER), - (b.clone(), R_INNER) + (b.clone(), R_INNER), ] { let curvature_regulator = sphere.regulators().with_untracked( |regs| regs.first().unwrap().clone() @@ -618,7 +618,7 @@ fn load_off_center_assemb(assembly: &Assembly) { "point".to_string(), "Point".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], - engine::point(1e-9, 0.0, 0.0) + engine::point(1e-9, 0.0, 0.0), ), ); let _ = assembly.try_insert_element( @@ -626,7 +626,7 @@ fn load_off_center_assemb(assembly: &Assembly) { "sphere".to_string(), "Sphere".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0) + engine::sphere(0.0, 0.0, 0.0, 1.0), ), ); @@ -658,14 +658,14 @@ fn load_radius_ratio_assemb(assembly: &Assembly) { "sphere_faces".to_string(), "Insphere".to_string(), GRAY, - engine::sphere(0.0, 0.0, 0.0, 0.5) + engine::sphere(0.0, 0.0, 0.0, 0.5), ), Sphere::new( "sphere_vertices".to_string(), "Circumsphere".to_string(), GRAY, - engine::sphere(0.0, 0.0, 0.0, 0.25) - ) + engine::sphere(0.0, 0.0, 0.0, 0.25), + ), ]; for sphere in spheres { let _ = assembly.try_insert_element(sphere); @@ -678,13 +678,13 @@ fn load_radius_ratio_assemb(assembly: &Assembly) { [1.00_f32, 0.50_f32, 0.75_f32], [1.00_f32, 0.75_f32, 0.50_f32], [1.00_f32, 1.00_f32, 0.50_f32], - [0.75_f32, 0.50_f32, 1.00_f32] + [0.75_f32, 0.50_f32, 1.00_f32], ].into_iter(), [ engine::point(-0.6, -0.8, -0.6), engine::point(-0.6, 0.8, 0.6), engine::point(0.6, -0.8, 0.6), - engine::point(0.6, 0.8, -0.6) + engine::point(0.6, 0.8, -0.6), ].into_iter() ).map( |(k, color, representation)| { @@ -692,7 +692,7 @@ fn load_radius_ratio_assemb(assembly: &Assembly) { format!("v{k}"), format!("Vertex {k}"), color, - representation + representation, ) } ); @@ -709,13 +709,13 @@ fn load_radius_ratio_assemb(assembly: &Assembly) { [1.00_f32, 0.00_f32, 0.25_f32], [1.00_f32, 0.25_f32, 0.00_f32], [0.75_f32, 0.75_f32, 0.00_f32], - [0.25_f32, 0.00_f32, 1.00_f32] + [0.25_f32, 0.00_f32, 1.00_f32], ].into_iter(), [ engine::sphere_with_offset(base_dir[0], base_dir[1], base_dir[2], offset, 0.0), engine::sphere_with_offset(base_dir[0], -base_dir[1], -base_dir[2], offset, 0.0), engine::sphere_with_offset(-base_dir[0], base_dir[1], -base_dir[2], offset, 0.0), - engine::sphere_with_offset(-base_dir[0], -base_dir[1], base_dir[2], offset, 0.0) + engine::sphere_with_offset(-base_dir[0], -base_dir[1], base_dir[2], offset, 0.0), ].into_iter() ).map( |(k, color, representation)| { @@ -723,7 +723,7 @@ fn load_radius_ratio_assemb(assembly: &Assembly) { format!("f{k}"), format!("Face {k}"), color, - representation + representation, ) } ); @@ -736,7 +736,7 @@ fn load_radius_ratio_assemb(assembly: &Assembly) { for j in index_range.clone() { let [face_j, vertex_j] = [ format!("f{j}"), - format!("v{j}") + format!("v{j}"), ].map( |id| assembly.elements_by_id.with_untracked( |elts_by_id| elts_by_id[&id].clone() @@ -797,7 +797,7 @@ fn load_irisawa_hexlet_assemb(assembly: &Assembly) { [0.75_f32, 0.75_f32, 0.00_f32], [0.25_f32, 1.00_f32, 0.00_f32], [0.00_f32, 0.25_f32, 1.00_f32], - [0.25_f32, 0.00_f32, 1.00_f32] + [0.25_f32, 0.00_f32, 1.00_f32], ].into_iter(); // create the spheres @@ -806,19 +806,19 @@ fn load_irisawa_hexlet_assemb(assembly: &Assembly) { "outer".to_string(), "Outer".to_string(), [0.5_f32, 0.5_f32, 0.5_f32], - engine::sphere(0.0, 0.0, 0.0, 1.5) + engine::sphere(0.0, 0.0, 0.0, 1.5), ), Sphere::new( "sun".to_string(), "Sun".to_string(), [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, -0.75, 0.0, 0.75) + engine::sphere(0.0, -0.75, 0.0, 0.75), ), Sphere::new( "moon".to_string(), "Moon".to_string(), [0.25_f32, 0.25_f32, 0.25_f32], - engine::sphere(0.0, 0.75, 0.0, 0.75) + engine::sphere(0.0, 0.75, 0.0, 0.75), ), ].into_iter().chain( index_range.clone().zip(colors).map( @@ -828,7 +828,7 @@ fn load_irisawa_hexlet_assemb(assembly: &Assembly) { format!("chain{k}"), format!("Chain {k}"), color, - engine::sphere(1.0 * ang.sin(), 0.0, 1.0 * ang.cos(), 0.5) + engine::sphere(1.0 * ang.sin(), 0.0, 1.0 * ang.cos(), 0.5), ) } ) @@ -865,7 +865,7 @@ fn load_irisawa_hexlet_assemb(assembly: &Assembly) { (outer.clone(), "1"), (sun.clone(), "-1"), (moon.clone(), "-1"), - (chain_sphere_next.clone(), "-1") + (chain_sphere_next.clone(), "-1"), ] { let tangency = InversiveDistanceRegulator::new([chain_sphere.clone(), other_sphere]); tangency.set_point.set(SpecifiedValue::try_from(inversive_distance.to_string()).unwrap()); @@ -918,24 +918,24 @@ pub fn TestAssemblyChooser() -> View { "off-center" => load_off_center_assemb(assembly), "radius-ratio" => load_radius_ratio_assemb(assembly), "irisawa-hexlet" => load_irisawa_hexlet_assemb(assembly), - _ => () + _ => (), }; }); }); // build the chooser view! { - select(bind:value=assembly_name) { - option(value="general") { "General" } - option(value="low-curv") { "Low-curvature" } - option(value="pointed") { "Pointed" } - option(value="tridim-icosahedron") { "Tridiminished icosahedron" } - option(value="dodeca-packing") { "Dodecahedral packing" } - option(value="balanced") { "Balanced" } - option(value="off-center") { "Off-center" } - option(value="radius-ratio") { "Radius ratio" } - option(value="irisawa-hexlet") { "Irisawa hexlet" } - option(value="empty") { "Empty" } + select(bind:value = assembly_name) { + option(value = "general") { "General" } + option(value = "low-curv") { "Low-curvature" } + option(value = "pointed") { "Pointed" } + option(value = "tridim-icosahedron") { "Tridiminished icosahedron" } + option(value = "dodeca-packing") { "Dodecahedral packing" } + option(value = "balanced") { "Balanced" } + option(value = "off-center") { "Off-center" } + option(value = "radius-ratio") { "Radius ratio" } + option(value = "irisawa-hexlet") { "Irisawa hexlet" } + option(value = "empty") { "Empty" } } } } \ No newline at end of file diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 602fc57..dc6b470 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -16,7 +16,7 @@ pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVect center_y / radius, center_z / radius, 0.5 / radius, - 0.5 * (center_norm_sq / radius - radius) + 0.5 * (center_norm_sq / radius - radius), ]) } @@ -30,7 +30,7 @@ pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f6 norm_sp * dir_y, norm_sp * dir_z, 0.5 * curv, - off * (1.0 + 0.5 * off * curv) + off * (1.0 + 0.5 * off * curv), ]) } @@ -53,7 +53,7 @@ pub fn project_point_to_normalized(rep: &mut DVector) { pub struct MatrixEntry { index: (usize, usize), - value: f64 + value: f64, } pub struct PartialMatrix(Vec); @@ -65,7 +65,7 @@ impl PartialMatrix { pub fn push(&mut self, row: usize, col: usize, value: f64) { let PartialMatrix(entries) = self; - entries.push(MatrixEntry { index: (row, col), value: value }); + entries.push(MatrixEntry { index: (row, col), value }); } pub fn push_sym(&mut self, row: usize, col: usize, value: f64) { @@ -135,22 +135,26 @@ impl<'a> IntoIterator for &'a PartialMatrix { pub struct ConfigSubspace { assembly_dim: usize, basis_std: Vec>, - basis_proj: Vec> + basis_proj: Vec>, } impl ConfigSubspace { pub fn zero(assembly_dim: usize) -> ConfigSubspace { ConfigSubspace { - assembly_dim: assembly_dim, + assembly_dim, basis_proj: Vec::new(), - basis_std: Vec::new() + basis_std: Vec::new(), } } // approximate the kernel of a symmetric endomorphism of the configuration // space for `assembly_dim` elements. we consider an eigenvector to be part // of the kernel if its eigenvalue is smaller than the constant `THRESHOLD` - fn symmetric_kernel(a: DMatrix, proj_to_std: DMatrix, assembly_dim: usize) -> ConfigSubspace { + fn symmetric_kernel( + a: DMatrix, + proj_to_std: DMatrix, + assembly_dim: usize, + ) -> ConfigSubspace { // find a basis for the kernel. the basis is expressed in the projection // coordinates, and it's orthonormal with respect to the projection // inner product @@ -170,7 +174,7 @@ impl ConfigSubspace { const ELEMENT_DIM: usize = 5; const UNIFORM_DIM: usize = 4; ConfigSubspace { - assembly_dim: assembly_dim, + assembly_dim, basis_std: basis_std.column_iter().map( |v| Into::>::into( v.reshape_generic(Dyn(ELEMENT_DIM), Dyn(assembly_dim)) @@ -180,7 +184,7 @@ impl ConfigSubspace { |v| Into::>::into( v.reshape_generic(Dyn(UNIFORM_DIM), Dyn(assembly_dim)) ) - ).collect() + ).collect(), } } @@ -214,9 +218,9 @@ pub struct DescentHistory { pub config: Vec>, pub scaled_loss: Vec, pub neg_grad: Vec>, - pub hess_eigvals: Vec::>, + pub hess_eigvals: Vec>, pub base_step: Vec>, - pub backoff_steps: Vec + pub backoff_steps: Vec, } impl DescentHistory { @@ -246,7 +250,7 @@ impl ConstraintProblem { ConstraintProblem { gram: PartialMatrix::new(), frozen: PartialMatrix::new(), - guess: DMatrix::::zeros(ELEMENT_DIM, element_count) + guess: DMatrix::::zeros(ELEMENT_DIM, element_count), } } @@ -255,7 +259,7 @@ impl ConstraintProblem { ConstraintProblem { gram: PartialMatrix::new(), frozen: PartialMatrix::new(), - guess: DMatrix::from_columns(guess_columns) + guess: DMatrix::from_columns(guess_columns), } } } @@ -269,25 +273,21 @@ lazy_static! { 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -2.0, - 0.0, 0.0, 0.0, -2.0, 0.0 + 0.0, 0.0, 0.0, -2.0, 0.0, ]); } struct SearchState { config: DMatrix, err_proj: DMatrix, - loss: f64 + loss: f64, } impl SearchState { fn from_config(gram: &PartialMatrix, config: DMatrix) -> SearchState { let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config)); let loss = err_proj.norm_squared(); - SearchState { - config: config, - err_proj: err_proj, - loss: loss - } + SearchState { config, err_proj, loss } } } @@ -314,7 +314,7 @@ pub fn local_unif_to_std(v: DVectorView) -> DMatrix { curv, 0.0, 0.0, 0.0, v[0], 0.0, curv, 0.0, 0.0, v[1], 0.0, 0.0, curv, 0.0, v[2], - 0.0, 0.0, 0.0, 0.0, 1.0 + 0.0, 0.0, 0.0, 0.0, 1.0, ]) } else { // `v` represents a sphere. the normalization condition says that the @@ -323,7 +323,7 @@ pub fn local_unif_to_std(v: DVectorView) -> DMatrix { curv, 0.0, 0.0, 0.0, v[0], 0.0, curv, 0.0, 0.0, v[1], 0.0, 0.0, curv, 0.0, v[2], - curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0 + curv*v[0], curv*v[1], curv*v[2], curv*v[3], curv*v[4] + 1.0, ]) } } @@ -336,7 +336,7 @@ fn seek_better_config( base_target_improvement: f64, min_efficiency: f64, backoff: f64, - max_backoff_steps: i32 + max_backoff_steps: i32, ) -> Option<(SearchState, i32)> { let mut rate = 1.0; for backoff_steps in 0..max_backoff_steps { @@ -354,12 +354,12 @@ fn seek_better_config( // a first-order neighborhood of a configuration pub struct ConfigNeighborhood { pub config: DMatrix, - pub nbhd: ConfigSubspace + pub nbhd: ConfigSubspace, } pub struct Realization { pub result: Result, - pub history: DescentHistory + pub history: DescentHistory, } // seek a matrix `config` that matches the partial matrix `problem.frozen` and @@ -373,12 +373,10 @@ pub fn realize_gram( backoff: f64, reg_scale: f64, max_descent_steps: i32, - max_backoff_steps: i32 + max_backoff_steps: i32, ) -> Realization { // destructure the problem data - let ConstraintProblem { - gram, guess, frozen - } = problem; + let ConstraintProblem { gram, guess, frozen } = problem; // start the descent history let mut history = DescentHistory::new(); @@ -391,10 +389,10 @@ pub fn realize_gram( let result = Ok( ConfigNeighborhood { config: guess.clone(), - nbhd: ConfigSubspace::zero(0) + nbhd: ConfigSubspace::zero(0), } ); - return Realization { result, history } + return Realization { result, history }; } // find the dimension of the search space @@ -475,8 +473,8 @@ pub fn realize_gram( Some(cholesky) => cholesky, None => return Realization { result: Err("Cholesky decomposition failed".to_string()), - history - } + history, + }, }; let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked); let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim)); @@ -485,16 +483,16 @@ pub fn realize_gram( // use backtracking line search to find a better configuration if let Some((better_state, backoff_steps)) = seek_better_config( gram, &state, &base_step, neg_grad.dot(&base_step), - min_efficiency, backoff, max_backoff_steps + min_efficiency, backoff, max_backoff_steps, ) { state = better_state; history.backoff_steps.push(backoff_steps); } else { return Realization { result: Err("Line search failed".to_string()), - history - } - }; + history, + }; + } } let result = if state.loss < tol { // express the uniform basis in the standard basis @@ -539,7 +537,7 @@ pub mod examples { [ sphere(0.0, 0.0, 0.0, 15.0), sphere(0.0, 0.0, -9.0, 5.0), - sphere(0.0, 0.0, 11.0, 3.0) + sphere(0.0, 0.0, 11.0, 3.0), ].into_iter().chain( (1..=6).map( |k| { @@ -598,7 +596,7 @@ pub mod examples { point(0.0, 0.0, 0.0), point(ang_hor.cos(), ang_hor.sin(), 0.0), point(x_vert, y_vert, -0.5), - point(x_vert, y_vert, 0.5) + point(x_vert, y_vert, 0.5), ] } ).collect::>().as_slice() @@ -641,15 +639,15 @@ mod tests { MatrixEntry { index: (0, 0), value: 14.0 }, MatrixEntry { index: (0, 2), value: 28.0 }, MatrixEntry { index: (1, 1), value: 42.0 }, - MatrixEntry { index: (1, 2), value: 49.0 } + MatrixEntry { index: (1, 2), value: 49.0 }, ]); let config = DMatrix::::from_row_slice(2, 3, &[ 1.0, 2.0, 3.0, - 4.0, 5.0, 6.0 + 4.0, 5.0, 6.0, ]); let expected_result = DMatrix::::from_row_slice(2, 3, &[ 14.0, 2.0, 28.0, - 4.0, 42.0, 49.0 + 4.0, 42.0, 49.0, ]); assert_eq!(frozen.freeze(&config), expected_result); } @@ -660,15 +658,15 @@ mod tests { MatrixEntry { index: (0, 0), value: 19.0 }, MatrixEntry { index: (0, 2), value: 39.0 }, MatrixEntry { index: (1, 1), value: 59.0 }, - MatrixEntry { index: (1, 2), value: 69.0 } + MatrixEntry { index: (1, 2), value: 69.0 }, ]); let attempt = DMatrix::::from_row_slice(2, 3, &[ 1.0, 2.0, 3.0, - 4.0, 5.0, 6.0 + 4.0, 5.0, 6.0, ]); let expected_result = DMatrix::::from_row_slice(2, 3, &[ 18.0, 0.0, 36.0, - 0.0, 54.0, 63.0 + 0.0, 54.0, 63.0, ]); assert_eq!(target.sub_proj(&attempt), expected_result); } @@ -686,7 +684,7 @@ mod tests { DMatrix::from_columns(&[ sphere(1.0, 0.0, 0.0, a), sphere(-0.5, a, 0.0, a), - sphere(-0.5, -a, 0.0, a) + sphere(-0.5, -a, 0.0, a), ]) }; let state = SearchState::from_config(&gram, config); @@ -700,7 +698,7 @@ mod tests { fn frozen_entry_test() { let mut problem = ConstraintProblem::from_guess(&[ point(0.0, 0.0, 2.0), - sphere(0.0, 0.0, 0.0, 0.95) + sphere(0.0, 0.0, 0.0, 0.95), ]); for j in 0..2 { for k in j..2 { @@ -744,7 +742,7 @@ mod tests { let mut problem = ConstraintProblem::from_guess(&[ sphere(0.0, 0.0, 0.0, -2.0), sphere(0.0, 0.0, 1.0, 1.0), - sphere(0.0, 0.0, -1.0, 1.0) + sphere(0.0, 0.0, -1.0, 1.0), ]); for j in 0..3 { for k in j..3 { @@ -774,8 +772,8 @@ mod tests { DMatrix::::from_column_slice(UNIFORM_DIM, assembly_dim, &[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.5, -0.5, - 0.0, 0.0, -0.5, 0.5 - ]) + 0.0, 0.0, -0.5, 0.5, + ]), ]; let tangent_motions_std = vec![ basis_matrix((0, 1), element_dim, assembly_dim), @@ -785,8 +783,8 @@ mod tests { DMatrix::::from_column_slice(element_dim, assembly_dim, &[ 0.0, 0.0, 0.0, 0.00, 0.0, 0.0, 0.0, -1.0, -0.25, -1.0, - 0.0, 0.0, -1.0, 0.25, 1.0 - ]) + 0.0, 0.0, -1.0, 0.25, 1.0, + ]), ]; // confirm that the dimension of the tangent space is no greater than @@ -862,10 +860,10 @@ mod tests { DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]), DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]), DVector::from_column_slice(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]), - DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0]) + DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0]), ] } - ).collect::>() + ).collect::>(), ]; let tangent_motions_std = tangent_motions_unif.iter().map( |motion| DMatrix::from_columns( @@ -898,7 +896,7 @@ mod tests { 0.0, 1.0, 0.0, 0.0, dis[1], 0.0, 0.0, 1.0, 0.0, dis[2], 2.0*dis[0], 2.0*dis[1], 2.0*dis[2], 1.0, dis.norm_squared(), - 0.0, 0.0, 0.0, 0.0, 1.0 + 0.0, 0.0, 0.0, 0.0, 1.0, ]) } @@ -910,7 +908,7 @@ mod tests { const SCALED_TOL: f64 = 1.0e-12; let mut problem_orig = ConstraintProblem::from_guess(&[ sphere(0.0, 0.0, 0.5, 1.0), - sphere(0.0, 0.0, -0.5, 1.0) + sphere(0.0, 0.0, -0.5, 1.0), ]); problem_orig.gram.push_sym(0, 0, 1.0); problem_orig.gram.push_sym(1, 1, 1.0); @@ -928,13 +926,13 @@ mod tests { let a = 0.5 * FRAC_1_SQRT_2; DMatrix::from_columns(&[ sphere(a, 0.0, 7.0 + a, 1.0), - sphere(-a, 0.0, 7.0 - a, 1.0) + sphere(-a, 0.0, 7.0 - a, 1.0), ]) }; let problem_tfm = ConstraintProblem { gram: problem_orig.gram, + frozen: problem_orig.frozen, guess: guess_tfm, - frozen: problem_orig.frozen }; let Realization { result: result_tfm, history: history_tfm } = realize_gram( &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 @@ -962,7 +960,7 @@ mod tests { 0.0, 1.0, 0.0, 0.0, 0.0, FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 1.0 + 0.0, 0.0, 0.0, 0.0, 1.0, ]); let transl = translation(Vector3::new(0.0, 0.0, 7.0)); let motion_proj_tfm = transl * rot * motion_orig_proj; diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index 152d11c..7ca0731 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -14,20 +14,20 @@ use components::{ add_remove::AddRemove, diagnostics::Diagnostics, display::Display, - outline::Outline + outline::Outline, }; #[derive(Clone)] struct AppState { assembly: Assembly, - selection: Signal>> + selection: Signal>>, } impl AppState { fn new() -> AppState { AppState { assembly: Assembly::new(), - selection: create_signal(BTreeSet::default()) + selection: create_signal(BTreeSet::default()), } } @@ -58,7 +58,7 @@ fn main() { provide_context(AppState::new()); view! { - div(id="sidebar") { + div(id = "sidebar") { AddRemove {} Outline {} Diagnostics {} diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs index cfe7fc3..ea1731c 100644 --- a/app-proto/src/specified.rs +++ b/app-proto/src/specified.rs @@ -13,7 +13,7 @@ use std::num::ParseFloatError; #[readonly::make] pub struct SpecifiedValue { pub spec: String, - pub value: Option + pub value: Option, } impl SpecifiedValue { @@ -37,7 +37,7 @@ impl TryFrom for SpecifiedValue { Ok(SpecifiedValue::from_empty_spec()) } else { spec.parse::().map( - |value| SpecifiedValue { spec: spec, value: Some(value) } + |value| SpecifiedValue { spec, value: Some(value) } ) } } From a4565281d5c3e7042ab9e1a2bdaa22bc52126b08 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Thu, 7 Aug 2025 23:24:07 +0000 Subject: [PATCH 125/126] Refactor: rename loaders and adopt 'Self' type convention (#111) Resolves #109. Resolves #110. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/111 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- app-proto/src/assembly.rs | 26 ++++++------ app-proto/src/components/diagnostics.rs | 4 +- app-proto/src/components/display.rs | 12 +++--- .../src/components/test_assembly_chooser.rs | 42 +++++++++---------- app-proto/src/engine.rs | 32 +++++++------- app-proto/src/main.rs | 4 +- app-proto/src/specified.rs | 8 ++-- 7 files changed, 64 insertions(+), 64 deletions(-) diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 43066fd..94e7b3c 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -175,8 +175,8 @@ impl Sphere { label: String, color: ElementColor, representation: DVector, - ) -> Sphere { - Sphere { + ) -> Self { + Self { id, label, color, @@ -194,8 +194,8 @@ impl Element for Sphere { "sphere".to_string() } - fn default(id: String, id_num: u64) -> Sphere { - Sphere::new( + fn default(id: String, id_num: u64) -> Self { + Self::new( id, format!("Sphere {id_num}"), [0.75_f32, 0.75_f32, 0.75_f32], @@ -275,8 +275,8 @@ impl Point { label: String, color: ElementColor, representation: DVector, - ) -> Point { - Point { + ) -> Self { + Self { id, label, color, @@ -294,8 +294,8 @@ impl Element for Point { "point".to_string() } - fn default(id: String, id_num: u64) -> Point { - Point::new( + fn default(id: String, id_num: u64) -> Self { + Self::new( id, format!("Point {id_num}"), [0.75_f32, 0.75_f32, 0.75_f32], @@ -348,7 +348,7 @@ impl ProblemPoser for Point { format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str() ); problem.gram.push_sym(index, index, 0.0); - problem.frozen.push(Point::WEIGHT_COMPONENT, index, 0.5); + problem.frozen.push(Self::WEIGHT_COMPONENT, index, 0.5); problem.guess.set_column(index, &self.representation.get_clone_untracked()); } } @@ -393,7 +393,7 @@ pub struct InversiveDistanceRegulator { } impl InversiveDistanceRegulator { - pub fn new(subjects: [Rc; 2]) -> InversiveDistanceRegulator { + pub fn new(subjects: [Rc; 2]) -> Self { let representations = subjects.each_ref().map(|subj| subj.representation()); let measurement = create_memo(move || { representations[0].with(|rep_0| @@ -406,7 +406,7 @@ impl InversiveDistanceRegulator { let set_point = create_signal(SpecifiedValue::from_empty_spec()); let serial = Self::next_serial(); - InversiveDistanceRegulator { subjects, measurement, set_point, serial } + Self { subjects, measurement, set_point, serial } } } @@ -453,7 +453,7 @@ pub struct HalfCurvatureRegulator { } impl HalfCurvatureRegulator { - pub fn new(subject: Rc) -> HalfCurvatureRegulator { + pub fn new(subject: Rc) -> Self { let measurement = subject.representation().map( |rep| rep[Sphere::CURVATURE_COMPONENT] ); @@ -461,7 +461,7 @@ impl HalfCurvatureRegulator { let set_point = create_signal(SpecifiedValue::from_empty_spec()); let serial = Self::next_serial(); - HalfCurvatureRegulator { subject, measurement, set_point, serial } + Self { subject, measurement, set_point, serial } } } diff --git a/app-proto/src/components/diagnostics.rs b/app-proto/src/components/diagnostics.rs index b274dca..e265982 100644 --- a/app-proto/src/components/diagnostics.rs +++ b/app-proto/src/components/diagnostics.rs @@ -15,8 +15,8 @@ struct DiagnosticsState { } impl DiagnosticsState { - fn new(initial_tab: String) -> DiagnosticsState { - DiagnosticsState { active_tab: create_signal(initial_tab) } + fn new(initial_tab: String) -> Self { + Self { active_tab: create_signal(initial_tab) } } } diff --git a/app-proto/src/components/display.rs b/app-proto/src/components/display.rs index a0cdba6..da921dd 100644 --- a/app-proto/src/components/display.rs +++ b/app-proto/src/components/display.rs @@ -41,8 +41,8 @@ struct SceneSpheres { } impl SceneSpheres { - fn new() -> SceneSpheres { - SceneSpheres { + fn new() -> Self { + Self { representations: Vec::new(), colors_with_opacity: Vec::new(), highlights: Vec::new(), @@ -71,8 +71,8 @@ struct ScenePoints { } impl ScenePoints { - fn new() -> ScenePoints { - ScenePoints { + fn new() -> Self { + Self { representations: Vec::new(), colors_with_opacity: Vec::new(), highlights: Vec::new(), @@ -97,8 +97,8 @@ pub struct Scene { } impl Scene { - fn new() -> Scene { - Scene { + fn new() -> Self { + Self { spheres: SceneSpheres::new(), points: ScenePoints::new(), } diff --git a/app-proto/src/components/test_assembly_chooser.rs b/app-proto/src/components/test_assembly_chooser.rs index 5ed94ad..0d387d3 100644 --- a/app-proto/src/components/test_assembly_chooser.rs +++ b/app-proto/src/components/test_assembly_chooser.rs @@ -26,7 +26,7 @@ use crate::{ // done more work on saving and loading assemblies, we should come back to this // code to see if it can be simplified -fn load_gen_assemb(assembly: &Assembly) { +fn load_general(assembly: &Assembly) { let _ = assembly.try_insert_element( Sphere::new( String::from("gemini_a"), @@ -77,7 +77,7 @@ fn load_gen_assemb(assembly: &Assembly) { ); } -fn load_low_curv_assemb(assembly: &Assembly) { +fn load_low_curvature(assembly: &Assembly) { // create the spheres let a = 0.75_f64.sqrt(); let _ = assembly.try_insert_element( @@ -196,7 +196,7 @@ fn load_low_curv_assemb(assembly: &Assembly) { } } -fn load_pointed_assemb(assembly: &Assembly) { +fn load_pointed(assembly: &Assembly) { let _ = assembly.try_insert_element( Point::new( format!("point_front"), @@ -246,7 +246,7 @@ fn load_pointed_assemb(assembly: &Assembly) { // B-C " // C-C " // A-C -0.25 * φ^2 = -0.6545084971874737 -fn load_tridim_icosahedron_assemb(assembly: &Assembly) { +fn load_tridiminished_icosahedron(assembly: &Assembly) { // create the vertices const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.25_f32]; const COLOR_B: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; @@ -409,7 +409,7 @@ fn load_tridim_icosahedron_assemb(assembly: &Assembly) { // to finish describing the dodecahedral circle packing, set the inversive // distance regulators to -1. some of the regulators have already been set -fn load_dodeca_packing_assemb(assembly: &Assembly) { +fn load_dodecahedral_packing(assembly: &Assembly) { // add the substrate let _ = assembly.try_insert_element( Sphere::new( @@ -550,7 +550,7 @@ fn load_dodeca_packing_assemb(assembly: &Assembly) { // the initial configuration of this test assembly deliberately violates the // constraints, so loading the assembly will trigger a non-trivial realization -fn load_balanced_assemb(assembly: &Assembly) { +fn load_balanced(assembly: &Assembly) { // create the spheres const R_OUTER: f64 = 10.0; const R_INNER: f64 = 4.0; @@ -611,7 +611,7 @@ fn load_balanced_assemb(assembly: &Assembly) { // the initial configuration of this test assembly deliberately violates the // constraints, so loading the assembly will trigger a non-trivial realization -fn load_off_center_assemb(assembly: &Assembly) { +fn load_off_center(assembly: &Assembly) { // create a point almost at the origin and a sphere centered on the origin let _ = assembly.try_insert_element( Point::new( @@ -648,7 +648,7 @@ fn load_off_center_assemb(assembly: &Assembly) { // sqrt(1/6) and sqrt(3/2), respectively. to measure those radii, set an // inversive distance of -1 between the insphere and each face, and then set an // inversive distance of 0 between the circumsphere and each vertex -fn load_radius_ratio_assemb(assembly: &Assembly) { +fn load_radius_ratio(assembly: &Assembly) { let index_range = 1..=4; // create the spheres @@ -789,7 +789,7 @@ fn load_radius_ratio_assemb(assembly: &Assembly) { // conditions are exactly representable as floats, unlike the analogous numbers // in the scaled-up problem. the inexact representations might break the // symmetry that's getting the engine stuck -fn load_irisawa_hexlet_assemb(assembly: &Assembly) { +fn load_irisawa_hexlet(assembly: &Assembly) { let index_range = 1..=6; let colors = [ [1.00_f32, 0.00_f32, 0.25_f32], @@ -909,15 +909,15 @@ pub fn TestAssemblyChooser() -> View { // load assembly match name.as_str() { - "general" => load_gen_assemb(assembly), - "low-curv" => load_low_curv_assemb(assembly), - "pointed" => load_pointed_assemb(assembly), - "tridim-icosahedron" => load_tridim_icosahedron_assemb(assembly), - "dodeca-packing" => load_dodeca_packing_assemb(assembly), - "balanced" => load_balanced_assemb(assembly), - "off-center" => load_off_center_assemb(assembly), - "radius-ratio" => load_radius_ratio_assemb(assembly), - "irisawa-hexlet" => load_irisawa_hexlet_assemb(assembly), + "general" => load_general(assembly), + "low-curvature" => load_low_curvature(assembly), + "pointed" => load_pointed(assembly), + "tridiminished-icosahedron" => load_tridiminished_icosahedron(assembly), + "dodecahedral-packing" => load_dodecahedral_packing(assembly), + "balanced" => load_balanced(assembly), + "off-center" => load_off_center(assembly), + "radius-ratio" => load_radius_ratio(assembly), + "irisawa-hexlet" => load_irisawa_hexlet(assembly), _ => (), }; }); @@ -927,10 +927,10 @@ pub fn TestAssemblyChooser() -> View { view! { select(bind:value = assembly_name) { option(value = "general") { "General" } - option(value = "low-curv") { "Low-curvature" } + option(value = "low-curvature") { "Low-curvature" } option(value = "pointed") { "Pointed" } - option(value = "tridim-icosahedron") { "Tridiminished icosahedron" } - option(value = "dodeca-packing") { "Dodecahedral packing" } + option(value = "tridiminished-icosahedron") { "Tridiminished icosahedron" } + option(value = "dodecahedral-packing") { "Dodecahedral packing" } option(value = "balanced") { "Balanced" } option(value = "off-center") { "Off-center" } option(value = "radius-ratio") { "Radius ratio" } diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index dc6b470..d033c01 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -59,12 +59,12 @@ pub struct MatrixEntry { pub struct PartialMatrix(Vec); impl PartialMatrix { - pub fn new() -> PartialMatrix { - PartialMatrix(Vec::::new()) + pub fn new() -> Self { + Self(Vec::::new()) } pub fn push(&mut self, row: usize, col: usize, value: f64) { - let PartialMatrix(entries) = self; + let Self(entries) = self; entries.push(MatrixEntry { index: (row, col), value }); } @@ -114,7 +114,7 @@ impl IntoIterator for PartialMatrix { type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { - let PartialMatrix(entries) = self; + let Self(entries) = self; entries.into_iter() } } @@ -139,8 +139,8 @@ pub struct ConfigSubspace { } impl ConfigSubspace { - pub fn zero(assembly_dim: usize) -> ConfigSubspace { - ConfigSubspace { + pub fn zero(assembly_dim: usize) -> Self { + Self { assembly_dim, basis_proj: Vec::new(), basis_std: Vec::new(), @@ -154,7 +154,7 @@ impl ConfigSubspace { a: DMatrix, proj_to_std: DMatrix, assembly_dim: usize, - ) -> ConfigSubspace { + ) -> Self { // find a basis for the kernel. the basis is expressed in the projection // coordinates, and it's orthonormal with respect to the projection // inner product @@ -173,7 +173,7 @@ impl ConfigSubspace { const ELEMENT_DIM: usize = 5; const UNIFORM_DIM: usize = 4; - ConfigSubspace { + Self { assembly_dim, basis_std: basis_std.column_iter().map( |v| Into::>::into( @@ -224,8 +224,8 @@ pub struct DescentHistory { } impl DescentHistory { - pub fn new() -> DescentHistory { - DescentHistory { + pub fn new() -> Self { + Self { config: Vec::>::new(), scaled_loss: Vec::::new(), neg_grad: Vec::>::new(), @@ -245,9 +245,9 @@ pub struct ConstraintProblem { } impl ConstraintProblem { - pub fn new(element_count: usize) -> ConstraintProblem { + pub fn new(element_count: usize) -> Self { const ELEMENT_DIM: usize = 5; - ConstraintProblem { + Self { gram: PartialMatrix::new(), frozen: PartialMatrix::new(), guess: DMatrix::::zeros(ELEMENT_DIM, element_count), @@ -255,8 +255,8 @@ impl ConstraintProblem { } #[cfg(feature = "dev")] - pub fn from_guess(guess_columns: &[DVector]) -> ConstraintProblem { - ConstraintProblem { + pub fn from_guess(guess_columns: &[DVector]) -> Self { + Self { gram: PartialMatrix::new(), frozen: PartialMatrix::new(), guess: DMatrix::from_columns(guess_columns), @@ -284,10 +284,10 @@ struct SearchState { } impl SearchState { - fn from_config(gram: &PartialMatrix, config: DMatrix) -> SearchState { + fn from_config(gram: &PartialMatrix, config: DMatrix) -> Self { let err_proj = gram.sub_proj(&(config.tr_mul(&*Q) * &config)); let loss = err_proj.norm_squared(); - SearchState { config, err_proj, loss } + Self { config, err_proj, loss } } } diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index 7ca0731..a03b026 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -24,8 +24,8 @@ struct AppState { } impl AppState { - fn new() -> AppState { - AppState { + fn new() -> Self { + Self { assembly: Assembly::new(), selection: create_signal(BTreeSet::default()), } diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs index ea1731c..788460b 100644 --- a/app-proto/src/specified.rs +++ b/app-proto/src/specified.rs @@ -17,8 +17,8 @@ pub struct SpecifiedValue { } impl SpecifiedValue { - pub fn from_empty_spec() -> SpecifiedValue { - SpecifiedValue { spec: String::new(), value: None } + pub fn from_empty_spec() -> Self { + Self { spec: String::new(), value: None } } pub fn is_present(&self) -> bool { @@ -34,10 +34,10 @@ impl TryFrom for SpecifiedValue { fn try_from(spec: String) -> Result { if spec.is_empty() { - Ok(SpecifiedValue::from_empty_spec()) + Ok(Self::from_empty_spec()) } else { spec.parse::().map( - |value| SpecifiedValue { spec, value: Some(value) } + |value| Self { spec, value: Some(value) } ) } } From af18a8e7d19067523de7bb4c8555c397a6f13d87 Mon Sep 17 00:00:00 2001 From: Vectornaut Date: Mon, 11 Aug 2025 03:33:19 +0000 Subject: [PATCH 126/126] Write a deployment packaging script (#113) Adds a packaging script to help automate deployment. Documents the deployment process in `README.md`. Also, moves `run-examples.sh` into the tools folder that we created for the packaging script. Co-authored-by: Aaron Fenyes Reviewed-on: https://code.studioinfinity.org/StudioInfinity/dyna3/pulls/113 Co-authored-by: Vectornaut Co-committed-by: Vectornaut --- README.md | 46 ++++++++++++++++++++-------- app-proto/Trunk.toml | 2 ++ deploy/.gitignore | 5 +++ tools/package-for-deployment.sh | 16 ++++++++++ {app-proto => tools}/run-examples.sh | 2 +- 5 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 app-proto/Trunk.toml create mode 100644 deploy/.gitignore create mode 100644 tools/package-for-deployment.sh rename {app-proto => tools}/run-examples.sh (89%) diff --git a/README.md b/README.md index 3a29eb0..cf3e589 100644 --- a/README.md +++ b/README.md @@ -25,32 +25,37 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter ### Install the prerequisites 1. Install [`rustup`](https://rust-lang.github.io/rustup/): the officially recommended Rust toolchain manager - * It's available on Ubuntu as a [Snap](https://snapcraft.io/rustup) + - It's available on Ubuntu as a [Snap](https://snapcraft.io/rustup) 2. Call `rustup default stable` to "download the latest stable release of Rust and set it as your default toolchain" - * If you forget, the `rustup` [help system](https://github.com/rust-lang/rustup/blob/d9b3601c3feb2e88cf3f8ca4f7ab4fdad71441fd/src/errors.rs#L109-L112) will remind you + - If you forget, the `rustup` [help system](https://github.com/rust-lang/rustup/blob/d9b3601c3feb2e88cf3f8ca4f7ab4fdad71441fd/src/errors.rs#L109-L112) will remind you 3. Call `rustup target add wasm32-unknown-unknown` to add the [most generic 32-bit WebAssembly target](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-unknown-unknown.html) 4. Call `cargo install wasm-pack` to install the [WebAssembly toolchain](https://rustwasm.github.io/docs/wasm-pack/) 5. Call `cargo install trunk` to install the [Trunk](https://trunkrs.dev/) web-build tool 6. Add the `.cargo/bin` folder in your home directory to your executable search path - * This lets you call Trunk, and other tools installed by Cargo, without specifying their paths - * On POSIX systems, the search path is stored in the `PATH` environment variable + - This lets you call Trunk, and other tools installed by Cargo, without specifying their paths + - On POSIX systems, the search path is stored in the `PATH` environment variable ### Play with the prototype 1. From the `app-proto` folder, call `trunk serve --release` to build and serve the prototype - * *The crates the prototype depends on will be downloaded and served automatically* - * *For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag* - * *If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]`* from there instead. + - The crates the prototype depends on will be downloaded and served automatically + - For a faster build, at the expense of a much slower prototype, you can call `trunk serve` without the `--release` flag + - If you want to stay in the top-level folder, you can call `trunk serve --config app-proto [--release]` from there instead. 3. In a web browser, visit one of the URLs listed under the message `INFO 📡 server listening at:` - * *Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype* + - Touching any file in the `app-proto` folder will make Trunk rebuild and live-reload the prototype 4. Press *ctrl+C* in the shell where Trunk is running to stop serving the prototype ### Run the engine on some example problems -1. Go into the `app-proto` folder -2. Call `./run-examples` - * *For each example problem, the engine will print the value of the loss function at each optimization step* - * *The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then* +1. Use `sh` to run the script `tools/run-examples.sh` + - The script is location-independent, so you can do this from anywhere in the dyna3 repository + - The call from the top level of the repository is: + + ```bash + sh tools/run-examples.sh + ``` + - For each example problem, the engine will print the value of the loss function at each optimization step + - The first example that prints is the same as the Irisawa hexlet example from the Julia version of the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then ```julia include("irisawa-hexlet.jl") @@ -59,9 +64,24 @@ The latest prototype is in the folder `app-proto`. It includes both a user inter end ``` - *you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show* + you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show ### Run the automated tests 1. Go into the `app-proto` folder 2. Call `cargo test` + +### Deploy the prototype + +1. From the `app-proto` folder, call `trunk build --release` + - Building in [release mode](https://doc.rust-lang.org/cargo/reference/profiles.html#release) produces an executable which is smaller and often much faster, but harder to debug and more time-consuming to build + - If you want to stay in the top-level folder, you can call `trunk build --config app-proto --release` from there instead +2. Use `sh` to run the packaging script `tools/package-for-deployment.sh`. + - The script is location-independent, so you can do this from anywhere in the dyna3 repository + - The call from the top level of the repository is: + ```bash + sh tools/package-for-deployment.sh + ``` + - This will overwrite or replace the files in `deploy/dyna3` +3. Put the contents of `deploy/dyna3` in the folder on your server that the prototype will be served from. + - To simplify uploading, you might want to combine these files into an archive called `deploy/dyna3.zip`. Git has been set to ignore this path \ No newline at end of file diff --git a/app-proto/Trunk.toml b/app-proto/Trunk.toml new file mode 100644 index 0000000..017deba --- /dev/null +++ b/app-proto/Trunk.toml @@ -0,0 +1,2 @@ +[build] +public_url = "./" \ No newline at end of file diff --git a/deploy/.gitignore b/deploy/.gitignore new file mode 100644 index 0000000..192f529 --- /dev/null +++ b/deploy/.gitignore @@ -0,0 +1,5 @@ +/dyna3.zip +/dyna3/index.html +/dyna3/dyna3-*.js +/dyna3/dyna3-*.wasm +/dyna3/main-*.css \ No newline at end of file diff --git a/tools/package-for-deployment.sh b/tools/package-for-deployment.sh new file mode 100644 index 0000000..fdda434 --- /dev/null +++ b/tools/package-for-deployment.sh @@ -0,0 +1,16 @@ +# set paths. this technique for getting the script location comes from +# `mklement0` on Stack Overflow +# +# https://stackoverflow.com/a/24114056 +# +TOOLS=$(dirname -- $0) +SRC="$TOOLS/../app-proto/dist" +DEST="$TOOLS/../deploy/dyna3" + +# remove the old hash-named files +[ -e "$DEST"/dyna3-*.js ] && rm "$DEST"/dyna3-*.js +[ -e "$DEST"/dyna3-*.wasm ] && rm "$DEST"/dyna3-*.wasm +[ -e "$DEST"/main-*.css ] && rm "$DEST"/main-*.css + +# copy the distribution +cp -r "$SRC/." "$DEST" diff --git a/app-proto/run-examples.sh b/tools/run-examples.sh similarity index 89% rename from app-proto/run-examples.sh rename to tools/run-examples.sh index 861addf..0946d92 100644 --- a/app-proto/run-examples.sh +++ b/tools/run-examples.sh @@ -8,7 +8,7 @@ # the application prototype # find the manifest file for the application prototype -MANIFEST="$(dirname -- $0)/Cargo.toml" +MANIFEST="$(dirname -- $0)/../app-proto/Cargo.toml" # set up the command that runs each example RUN_EXAMPLE="cargo run --manifest-path $MANIFEST --example"