From fd3cbae1b4fd0f40500a7a868196fdbd868ec0ad Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 21 Aug 2024 13:01:33 -0700 Subject: [PATCH 001/100] 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 -- 2.34.1 From 5885189b04ca63750ce113f2f77496734d36ff8d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 21 Aug 2024 17:31:17 -0700 Subject: [PATCH 002/100] 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); }); }); -- 2.34.1 From 81f9b8e04007aa2289d865b48d6bae03b19f287a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 21 Aug 2024 22:36:56 -0700 Subject: [PATCH 003/100] 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); -- 2.34.1 From 80b210e667bcba411c64ac34dab92d286113cf06 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 21 Aug 2024 23:07:14 -0700 Subject: [PATCH 004/100] 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; -- 2.34.1 From 1fbeb23194cf1b90f6a26b1797af726750b81841 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 22 Aug 2024 00:04:58 -0700 Subject: [PATCH 005/100] 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, -- 2.34.1 From f274119da64d9d8b7aac1d3784f6249cbbacbc1c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 22 Aug 2024 18:17:01 -0700 Subject: [PATCH 006/100] 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); -- 2.34.1 From c78a041dc7bc1525fb9686ae36192082bfa5412b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 22 Aug 2024 22:08:34 -0700 Subject: [PATCH 007/100] 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 ) } } -- 2.34.1 From d2cecf69db88145e6529847055fcf64d4fc72ea3 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 23 Aug 2024 00:16:41 -0700 Subject: [PATCH 008/100] 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 + ) } } }); -- 2.34.1 From 2ef0fdd3e2a6e077824c302d70fcf7e5f053bd97 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 23 Aug 2024 12:56:54 -0700 Subject: [PATCH 009/100] 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 + ) } } }); -- 2.34.1 From 87763fc458c5c25aa74c617a1fb2cac5c09a5f40 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 00:08:22 -0700 Subject: [PATCH 010/100] 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); } -- 2.34.1 From f1029b31028e1483914f0a8c9dda4759e4c51329 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 01:01:13 -0700 Subject: [PATCH 011/100] 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.); } "##, ); -- 2.34.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/100] 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, -- 2.34.1 From 25da6ca06264b52cf5050d5b27664e081959f36b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 11:00:50 -0700 Subject: [PATCH 013/100] 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, -- 2.34.1 From 766d56027c566265ed3b26126ddaa623d142184d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 11:26:53 -0700 Subject: [PATCH 014/100] 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); -- 2.34.1 From b9587872d30249dffa2c073857ce1663dd8ef979 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 24 Aug 2024 11:31:22 -0700 Subject: [PATCH 015/100] 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); }); }); -- 2.34.1 From 8798683d25c8e24dba2592e06efa5905a6d37783 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 25 Aug 2024 00:00:28 -0700 Subject: [PATCH 016/100] 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); } -- 2.34.1 From c18cac642b23e0daeac65bb9b7b493a7749b16d7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 25 Aug 2024 00:47:36 -0700 Subject: [PATCH 017/100] 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); } } -- 2.34.1 From 206a2df480db850df30201382fa9f9ea14d80755 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 25 Aug 2024 16:41:31 -0700 Subject: [PATCH 018/100] 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 ) -- 2.34.1 From 5bf23fa78974f2addf6cd766f008814db852fd42 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 25 Aug 2024 21:40:46 -0700 Subject: [PATCH 019/100] 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 + ) } } }); -- 2.34.1 From c5fe725b1b8086366537a6983f36de1e13dcb5c5 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 25 Aug 2024 22:22:14 -0700 Subject: [PATCH 020/100] 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"); -- 2.34.1 From 85db7b9be08a11f66f0dc9cf6b192139bdefdf1e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 26 Aug 2024 00:43:42 -0700 Subject: [PATCH 021/100] 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( -- 2.34.1 From b9370ceb41537e83efa45094a4da1bbb4d6b1496 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 26 Aug 2024 01:47:53 -0700 Subject: [PATCH 022/100] 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 + ) + } } } }); -- 2.34.1 From cbec31f5df3284954f13f8c29725f54cf06d325e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 26 Aug 2024 13:41:34 -0700 Subject: [PATCH 023/100] 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 -- 2.34.1 From bf140efaf7bce9f1c9f03b97602589d9d7ca1bdd Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 26 Aug 2024 13:51:01 -0700 Subject: [PATCH 024/100] 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 ) } } -- 2.34.1 From 5e9c5db231dfd0061ebae387d01507019051acc5 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 26 Aug 2024 16:06:37 -0700 Subject: [PATCH 025/100] 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! { -- 2.34.1 From ec48592ef15876c5c824b10ab5ff0c2f5bd3a76a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 26 Aug 2024 23:39:51 -0700 Subject: [PATCH 026/100] 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" } -- 2.34.1 From f62f44b5a77fa2cbefad4608538aa07449d149b3 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 27 Aug 2024 00:00:48 -0700 Subject: [PATCH 027/100] 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); -- 2.34.1 From a40a110788e577417cfede4fb6fb3b130d86b37e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 27 Aug 2024 13:55:08 -0700 Subject: [PATCH 028/100] 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! { -- 2.34.1 From 01c2af6615a4a600c75fac3923276ec53516f709 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 27 Aug 2024 18:39:58 -0700 Subject: [PATCH 029/100] 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( -- 2.34.1 From c04e29f58672a8113a73a6a7ca29d0a7ba5c35db Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 28 Aug 2024 01:47:38 -0700 Subject: [PATCH 030/100] 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); -- 2.34.1 From 3d7ee98dd64267cb00732079f5a51b45d5e900f2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 28 Aug 2024 01:50:17 -0700 Subject: [PATCH 031/100] 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]; -- 2.34.1 From e80adf831d03e64d37cb359796297420c15dbfab Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 28 Aug 2024 01:59:46 -0700 Subject: [PATCH 032/100] 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; } -- 2.34.1 From 4afc82034b9ea6c6bd2f1ced53425dfc1420df0b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 28 Aug 2024 14:37:29 -0700 Subject: [PATCH 033/100] 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); -- 2.34.1 From 8fde202911c1aa17aab72439ec4a965575ba9bd4 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 28 Aug 2024 15:52:19 -0700 Subject: [PATCH 034/100] 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) { -- 2.34.1 From 3a721a4cc89bc5a8e41a075f4433c763dab1ede7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 28 Aug 2024 16:06:33 -0700 Subject: [PATCH 035/100] 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], -- 2.34.1 From 6db9f5be6c86af64226d21f220bb7795405c5196 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 28 Aug 2024 17:14:55 -0700 Subject: [PATCH 036/100] 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; -- 2.34.1 From f14855296485250277a7216c711eb598148d8346 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 29 Aug 2024 15:01:38 -0700 Subject: [PATCH 037/100] 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); } } } -- 2.34.1 From a4236a34dfc0f386182c7ff2d6729613ed73336f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 2 Sep 2024 15:15:18 -0700 Subject: [PATCH 038/100] 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; } } } -- 2.34.1 From 121934c4c3543768bca01e6b96522d59e10775a1 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 4 Sep 2024 12:58:55 -0700 Subject: [PATCH 039/100] 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; -- 2.34.1 From 3493a798d10c1a7771362e0a49830a4c15f18e90 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 4 Sep 2024 16:27:28 -0700 Subject: [PATCH 040/100] 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" } -- 2.34.1 From ab830b194e656522a6d48707de951346a3367cc8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 6 Sep 2024 18:57:59 -0700 Subject: [PATCH 041/100] 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 ) } } -- 2.34.1 From 163361184b4e621bb3f69a87571eaac18092492c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 8 Sep 2024 23:00:28 -0700 Subject: [PATCH 042/100] 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.); } } -- 2.34.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/100] 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; -- 2.34.1 From 0173b63e19e908b6fcdc01a48edd192d9e5b65e6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 8 Sep 2024 23:43:26 -0700 Subject: [PATCH 044/100] 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]); -- 2.34.1 From 69ab888d5bc643bae89f361be75455da60b30778 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Sep 2024 00:32:29 -0700 Subject: [PATCH 045/100] 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 ) } -- 2.34.1 From 2efc08d6c056f6c130467019a5d7f51338b9c63d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Sep 2024 02:15:04 -0700 Subject: [PATCH 046/100] 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" } -- 2.34.1 From c67f37c934ee229ad253caca82278fe4b575d18c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 9 Sep 2024 19:41:15 -0700 Subject: [PATCH 047/100] 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" } -- 2.34.1 From 20d072d61561215cdb1ab676bc8397e9275db8b9 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 10 Sep 2024 02:29:50 -0700 Subject: [PATCH 048/100] 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); -- 2.34.1 From aceac5e5c41006781cf88cd98c2e60b9709aaaf7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 10 Sep 2024 03:14:33 -0700 Subject: [PATCH 049/100] 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) { -- 2.34.1 From d3c9a08d22571fd5b28724fea9adf25d35f93cc0 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Tue, 10 Sep 2024 04:08:49 -0700 Subject: [PATCH 050/100] 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); } -- 2.34.1 From 336b940471a18332d692b3b637c34110b8bdd1a1 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 12 Sep 2024 15:24:41 -0700 Subject: [PATCH 051/100] 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 -- 2.34.1 From 634e97b6595ac16ef70817baeb6161bbab662cca Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 12 Sep 2024 22:36:54 -0700 Subject: [PATCH 052/100] 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() ) } } -- 2.34.1 From 20b96a9764cfe4413d779b40ac271d1286560253 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 12 Sep 2024 22:39:21 -0700 Subject: [PATCH 053/100] 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 -- 2.34.1 From d481181ef87162f660f0f74e16be3b629ff5f53e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 13 Sep 2024 00:07:49 -0700 Subject: [PATCH 054/100] 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( -- 2.34.1 From e6d1e0b8658b041e667785a370bc52c2db47f4c4 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 13 Sep 2024 00:40:34 -0700 Subject: [PATCH 055/100] 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; -- 2.34.1 From 0c2869d3f367521f7b7e709614a686f6ce5083da Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 13 Sep 2024 00:43:19 -0700 Subject: [PATCH 056/100] 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) } -- 2.34.1 From 49170671b4266ae5550fc764cb69e9768fa4c43b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 13 Sep 2024 14:53:12 -0700 Subject: [PATCH 057/100] 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 -- 2.34.1 From 959e4cc8b58c49b9e7a63d3a8d8c2e7c3ee7c4f6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 13 Sep 2024 15:15:55 -0700 Subject: [PATCH 058/100] 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 || -- 2.34.1 From 49655a8d62130d34cc63ec28780ba69d39c16ef7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 14 Sep 2024 10:58:46 -0700 Subject: [PATCH 059/100] 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); -- 2.34.1 From cd18d594e033fb47ebf21b70d65fe5e29471d08e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 14 Sep 2024 11:46:24 -0700 Subject: [PATCH 060/100] 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]) } ]) } -- 2.34.1 From f47be08d9849abc027009e6c102d983745016649 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 15 Sep 2024 11:31:22 -0700 Subject: [PATCH 061/100] 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]) } ]) } -- 2.34.1 From 7cb01bab82db1dc463718bd1e1ae1bc8c954e3d8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 15 Sep 2024 11:38:32 -0700 Subject: [PATCH 062/100] 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]; -- 2.34.1 From e2d3af2867024057400f2104d28b2b794fdfae5c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 15 Sep 2024 11:41:16 -0700 Subject: [PATCH 063/100] 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]; -- 2.34.1 From 93190e99da52c398ac568385b6d0ec44ccdc520a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 15 Sep 2024 11:54:39 -0700 Subject: [PATCH 064/100] 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 -- 2.34.1 From a60624884ad7741d916a5dec84881f7d08568ce6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 16 Sep 2024 11:29:44 -0700 Subject: [PATCH 065/100] 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) } } -- 2.34.1 From 96afad0c97ccfc5f5fca6825d50dd5e8079fd8ac Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 16 Sep 2024 15:46:45 -0700 Subject: [PATCH 066/100] 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); } -- 2.34.1 From 96f8b6b5f34c5374b4aebab3cf5d9e582e37ecb3 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 19 Sep 2024 16:08:55 -0700 Subject: [PATCH 067/100] 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(); + } } } ) { -- 2.34.1 From 78f8ef8215af8aeec841598d2957f25f50061d01 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 19 Sep 2024 17:53:07 -0700 Subject: [PATCH 068/100] 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(); } } -- 2.34.1 From d121385c18026bb76176c33ff97669476e1016dc Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 02:21:45 -0700 Subject: [PATCH 069/100] 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 ) } } -- 2.34.1 From 147e2758234b78ccfc2e24f6ca762b40e9f5d77b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 02:38:17 -0700 Subject: [PATCH 070/100] 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() ) } } -- 2.34.1 From 050e2373a64db94caebb4322fb7c70943186387b Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 14:05:40 -0700 Subject: [PATCH 071/100] 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) } } } }, -- 2.34.1 From 4a24a019285fb09ddba459b301c134bbd95d571c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 14:40:31 -0700 Subject: [PATCH 072/100] 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! { -- 2.34.1 From edee153e37480a013a78eb0709dbde199ea5d91a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 23:50:16 -0700 Subject: [PATCH 073/100] 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}; -- 2.34.1 From 7709c61f7112579b607511092428a09a23ce6ce2 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 22 Sep 2024 23:55:07 -0700 Subject: [PATCH 074/100] 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() - ) } } } -- 2.34.1 From fc85d15f8307a337341e33e533c1c236eb33378c Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 23 Sep 2024 00:39:14 -0700 Subject: [PATCH 075/100] 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() -- 2.34.1 From e6281cdcc6377fd7b0d3416748608be15efd5136 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 25 Sep 2024 14:48:58 -0700 Subject: [PATCH 076/100] 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" { -- 2.34.1 From 7ff1b9cb6591c48b3457a7415bfc644c54d4e4e4 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 13:22:48 -0700 Subject: [PATCH 077/100] 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 -- 2.34.1 From 4e3c86fb717fb7a0b25a63afe34427c76a60446a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 13:23:56 -0700 Subject: [PATCH 078/100] 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 -- 2.34.1 From f5486fb0dd2c4f5f59ea52782af464c2faa4c714 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 15:02:51 -0700 Subject: [PATCH 079/100] 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 {} } }); -- 2.34.1 From 9b39fe56b810d576f41fc43237f7cd85590e6f6d Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 19:10:34 -0700 Subject: [PATCH 080/100] 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()) ) } } -- 2.34.1 From b3afd6f5553f6f1ca39d80ab38c84f9af43a1261 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 19:16:41 -0700 Subject: [PATCH 081/100] 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()) } } } -- 2.34.1 From 2444649dd1d138b1d31b56ad026ff879e86586c6 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Thu, 26 Sep 2024 19:17:57 -0700 Subject: [PATCH 082/100] 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(); -- 2.34.1 From bd0982f821a0f8d4cd5d1128f4e1c72e88876831 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Fri, 27 Sep 2024 14:33:49 -0700 Subject: [PATCH 083/100] 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()) } } } -- 2.34.1 From b08dbd6f936df7c4ec6510dd15d558f8581b4872 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 28 Sep 2024 13:10:22 -0700 Subject: [PATCH 084/100] 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) { -- 2.34.1 From 28b1ecb8e936de8c2d0b2788815204dd60f261ec Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 28 Sep 2024 13:28:53 -0700 Subject: [PATCH 085/100] 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! { -- 2.34.1 From 4f8f36053fec599fa6617beef34f1df73186e800 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 28 Sep 2024 14:18:04 -0700 Subject: [PATCH 086/100] 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 } -- 2.34.1 From 721a8716d41453c46e89e136f487d11ebd570a2a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 28 Sep 2024 18:49:17 -0700 Subject: [PATCH 087/100] 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; -- 2.34.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/100] 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); }); -- 2.34.1 From 7977b11caf4ae62bacd19e63c7b437d63380f626 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 28 Sep 2024 18:56:33 -0700 Subject: [PATCH 089/100] 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") { -- 2.34.1 From 25fa108e9b2cfba27918c98fc1d89042fdcf85eb Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sat, 28 Sep 2024 19:37:43 -0700 Subject: [PATCH 090/100] 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), _ => () }; }); -- 2.34.1 From 70bd39b9e5b0808f75ce322be45b2b54c169fa79 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 29 Sep 2024 23:30:35 -0700 Subject: [PATCH 091/100] 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; -- 2.34.1 From edace8e4eaeb1050399d7b31a8f3762c55f894a7 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Sun, 29 Sep 2024 23:41:16 -0700 Subject: [PATCH 092/100] 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() + ) ) } } -- 2.34.1 From 18ebf3be2c8a9caf0cdec3338bd6ecb1333a144e Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 30 Sep 2024 00:44:13 -0700 Subject: [PATCH 093/100] 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); } }, -- 2.34.1 From e3120f7109c22095a4688b610c72feeb2e850cc8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 30 Sep 2024 16:48:36 -0700 Subject: [PATCH 094/100] 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 -- 2.34.1 From 19907838ce7766e6936fc38be68e03ca2ea20046 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 30 Sep 2024 17:59:48 -0700 Subject: [PATCH 095/100] 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 -- 2.34.1 From ee1c69178785a9a5b4649037ecd57f951023d383 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 14 Oct 2024 16:04:56 -0700 Subject: [PATCH 096/100] 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 -- 2.34.1 From abe231126d2601ec8c61536e4cb9149cb999913f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 14 Oct 2024 16:36:52 -0700 Subject: [PATCH 097/100] 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 -- 2.34.1 From cca5a781c4595036ef271ba0bab4b54cf765cce8 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 14 Oct 2024 16:43:13 -0700 Subject: [PATCH 098/100] 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 -- 2.34.1 From f1690b62e19e037a55ab968d50b40dc52840de2f Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 14 Oct 2024 17:08:44 -0700 Subject: [PATCH 099/100] 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 -- 2.34.1 From 517fd327fa5b2e47694e244ae1da749f3a26b7ef Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Wed, 16 Oct 2024 23:16:29 -0700 Subject: [PATCH 100/100] 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) } } -- 2.34.1