diff --git a/app-proto/inversive-display/src/inversive.frag b/app-proto/inversive-display/src/inversive.frag index ae3b930..2e185a5 100644 --- a/app-proto/inversive-display/src/inversive.frag +++ b/app-proto/inversive-display/src/inversive.frag @@ -11,9 +11,20 @@ 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 +// 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]; @@ -24,6 +35,8 @@ 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 8d16732..d351149 100644 --- a/app-proto/inversive-display/src/main.rs +++ b/app-proto/inversive-display/src/main.rs @@ -7,6 +7,7 @@ // 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}}; @@ -342,6 +343,8 @@ 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"); @@ -480,6 +483,8 @@ 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); diff --git a/app-proto/sketch-outline/Cargo.toml b/app-proto/sketch-outline/Cargo.toml index 7d1d608..522c994 100644 --- a/app-proto/sketch-outline/Cargo.toml +++ b/app-proto/sketch-outline/Cargo.toml @@ -8,7 +8,6 @@ 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" @@ -21,16 +20,6 @@ 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/main.css b/app-proto/sketch-outline/main.css index cd7bc44..2e6aedc 100644 --- a/app-proto/sketch-outline/main.css +++ b/app-proto/sketch-outline/main.css @@ -5,17 +5,11 @@ body { background-color: #222; } -/* outline */ - ul { - float: left; width: 450px; - height: 750px; - margin: 0px; padding: 8px; - border: 1px solid #555; + border: 1px solid #888; border-radius: 16px; - box-sizing: border-box; } li { @@ -26,16 +20,11 @@ li { border-radius: 8px; } -li.selected { - color: #fff; - background-color: #666; -} - li:not(:last-child) { margin-bottom: 8px; } -li > .elt-label { +li > .elt-name { flex-grow: 1; padding: 2px 0px 2px 4px; } @@ -52,28 +41,10 @@ li > .elt-rep > div { background-color: #333; } -li.selected > .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/assembly.rs b/app-proto/sketch-outline/src/assembly.rs deleted file mode 100644 index dded2a1..0000000 --- a/app-proto/sketch-outline/src/assembly.rs +++ /dev/null @@ -1,21 +0,0 @@ -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, - - /* TO DO */ - // does this belong in the element data? - pub selected: Signal -} - -// 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> -} \ No newline at end of file diff --git a/app-proto/sketch-outline/src/display.rs b/app-proto/sketch-outline/src/display.rs deleted file mode 100644 index 373c857..0000000 --- a/app-proto/sketch-outline/src/display.rs +++ /dev/null @@ -1,427 +0,0 @@ -use core::array; -use nalgebra::{DMatrix, Rotation3, Vector3}; -use sycamore::{prelude::*, motion::create_raf}; -use web_sys::{ - console, - window, - KeyboardEvent, - WebGl2RenderingContext, - WebGlProgram, - WebGlShader, - WebGlUniformLocation, - wasm_bindgen::{JsCast, JsValue} -}; - -use crate::AppState; - -fn compile_shader( - context: &WebGl2RenderingContext, - shader_type: u32, - source: &str, -) -> WebGlShader { - let shader = context.create_shader(shader_type).unwrap(); - context.shader_source(&shader, source); - context.compile_shader(&shader); - shader -} - -fn get_uniform_array_locations( - context: &WebGl2RenderingContext, - program: &WebGlProgram, - var_name: &str, - member_name_opt: Option<&str> -) -> [Option; N] { - array::from_fn(|n| { - let name = match member_name_opt { - Some(member_name) => format!("{var_name}[{n}].{member_name}"), - None => format!("{var_name}[{n}]") - }; - context.get_uniform_location(&program, name.as_str()) - }) -} - -// load the given data into the vertex input of the given name -fn bind_vertex_attrib( - context: &WebGl2RenderingContext, - index: u32, - size: i32, - data: &[f32] -) { - // create a data buffer and bind it to ARRAY_BUFFER - let buffer = context.create_buffer().unwrap(); - context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer)); - - // load the given data into the buffer. the function `Float32Array::view` - // creates a raw view into our module's `WebAssembly.Memory` buffer. - // allocating more memory will change the buffer, invalidating the view. - // that means we have to make sure we don't allocate any memory until the - // view is dropped - unsafe { - context.buffer_data_with_array_buffer_view( - WebGl2RenderingContext::ARRAY_BUFFER, - &js_sys::Float32Array::view(&data), - WebGl2RenderingContext::STATIC_DRAW, - ); - } - - // allow the target attribute to be used - context.enable_vertex_attrib_array(index); - - // take whatever's bound to ARRAY_BUFFER---here, the data buffer created - // above---and bind it to the target attribute - // - // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer - // - context.vertex_attrib_pointer_with_i32( - index, - size, - WebGl2RenderingContext::FLOAT, - false, // don't normalize - 0, // zero stride - 0, // zero offset - ); -} - -#[component] -pub fn Display() -> View { - let state = use_context::(); - - // canvas - let display = create_node_ref(); - - // navigation - let pitch_up = create_signal(0.0); - let pitch_down = create_signal(0.0); - let yaw_right = create_signal(0.0); - let yaw_left = create_signal(0.0); - let roll_ccw = create_signal(0.0); - let roll_cw = create_signal(0.0); - let zoom_in = create_signal(0.0); - let zoom_out = create_signal(0.0); - - // 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; - let mut last_sample_time = 0.0; - let mut frames_since_last_sample = 0; - let mean_frame_interval = create_signal(0.0); - - on_mount(move || { - // timing - let mut last_time = 0.0; - - // viewpoint - const ROT_SPEED: f64 = 0.4; // in radians per second - const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second - let mut orientation = DMatrix::::identity(5, 5); - let mut rotation = DMatrix::::identity(5, 5); - let mut location_z: f64 = 5.0; - - // display parameters - const OPACITY: f32 = 0.5; /* SCAFFOLDING */ - const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ - const LAYER_THRESHOLD: i32 = 0; /* DEBUG */ - const DEBUG_MODE: i32 = 0; /* DEBUG */ - - /* INSTRUMENTS */ - let performance = window().unwrap().performance().unwrap(); - - // get the display canvas - let canvas = display.get().unchecked_into::(); - let ctx = canvas - .get_context("webgl2") - .unwrap() - .unwrap() - .dyn_into::() - .unwrap(); - - // compile and attach the vertex and fragment shaders - let vertex_shader = compile_shader( - &ctx, - WebGl2RenderingContext::VERTEX_SHADER, - include_str!("identity.vert"), - ); - let fragment_shader = compile_shader( - &ctx, - WebGl2RenderingContext::FRAGMENT_SHADER, - include_str!("inversive.frag"), - ); - let program = ctx.create_program().unwrap(); - ctx.attach_shader(&program, &vertex_shader); - ctx.attach_shader(&program, &fragment_shader); - ctx.link_program(&program); - let link_status = ctx - .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS) - .as_bool() - .unwrap(); - let link_msg = if link_status { - "Linked successfully" - } else { - "Linking failed" - }; - console::log_1(&JsValue::from(link_msg)); - ctx.use_program(Some(&program)); - - /* DEBUG */ - // print the maximum number of vectors that can be passed as - // uniforms to a fragment shader. the OpenGL ES 3.0 standard - // requires this maximum to be at least 224, as discussed in the - // documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter - // here: - // - // https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml - // - // there are also other size limits. for example, on Aaron's - // machine, the the length of a float or genType array seems to be - // capped at 1024 elements - console::log_2( - &ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(), - &JsValue::from("uniform vectors available") - ); - - // find indices of vertex attributes and uniforms - const SPHERE_MAX: usize = 200; - let position_index = ctx.get_attrib_location(&program, "position") as u32; - let sphere_cnt_loc = ctx.get_uniform_location(&program, "sphere_cnt"); - let sphere_sp_locs = get_uniform_array_locations::( - &ctx, &program, "sphere_list", Some("sp") - ); - let sphere_lt_locs = get_uniform_array_locations::( - &ctx, &program, "sphere_list", Some("lt") - ); - let color_locs = get_uniform_array_locations::( - &ctx, &program, "color_list", None - ); - let highlight_locs = get_uniform_array_locations::( - &ctx, &program, "highlight_list", None - ); - let resolution_loc = ctx.get_uniform_location(&program, "resolution"); - let shortdim_loc = ctx.get_uniform_location(&program, "shortdim"); - let opacity_loc = ctx.get_uniform_location(&program, "opacity"); - let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold"); - let debug_mode_loc = ctx.get_uniform_location(&program, "debug_mode"); - - // create a vertex array and bind it to the graphics context - let vertex_array = ctx.create_vertex_array().unwrap(); - ctx.bind_vertex_array(Some(&vertex_array)); - - // set the vertex positions - const VERTEX_CNT: usize = 6; - let positions: [f32; 3*VERTEX_CNT] = [ - // northwest triangle - -1.0, -1.0, 0.0, - -1.0, 1.0, 0.0, - 1.0, 1.0, 0.0, - // southeast triangle - -1.0, -1.0, 0.0, - 1.0, 1.0, 0.0, - 1.0, -1.0, 0.0 - ]; - bind_vertex_attrib(&ctx, position_index, 3, &positions); - - // set up a repainting routine - let (_, start_animation_loop, _) = create_raf(move || { - // get the time step - let time = performance.now(); - let time_step = 0.001*(time - last_time); - last_time = time; - - // get the navigation state - let pitch_up_val = pitch_up.get(); - let pitch_down_val = pitch_down.get(); - let yaw_right_val = yaw_right.get(); - let yaw_left_val = yaw_left.get(); - let roll_ccw_val = roll_ccw.get(); - let roll_cw_val = roll_cw.get(); - let zoom_in_val = zoom_in.get(); - let zoom_out_val = zoom_out.get(); - - // 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 assembly space to world space - let location = { - let u = -location_z; - DMatrix::from_column_slice(5, 5, &[ - 1.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, u, - 0.0, 0.0, 2.0*u, 1.0, u*u, - 0.0, 0.0, 0.0, 0.0, 1.0 - ]) - }; - let assembly_to_world = &location * &orientation; - - // get the assembly - let elements = state.assembly.elements.get_clone(); - let element_iter = (&elements).into_iter(); - let reps_world: Vec<_> = element_iter.clone().map(|elt| &assembly_to_world * &elt.rep).collect(); - let colors: Vec<_> = element_iter.clone().map(|elt| - if elt.selected.get() { - 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; - let height = canvas.height() as f32; - ctx.uniform2f(resolution_loc.as_ref(), width, height); - ctx.uniform1f(shortdim_loc.as_ref(), width.min(height)); - - // pass the assembly - ctx.uniform1i(sphere_cnt_loc.as_ref(), elements.len() as i32); - for n in 0..reps_world.len() { - let v = &reps_world[n]; - ctx.uniform3f( - sphere_sp_locs[n].as_ref(), - v[0] as f32, v[1] as f32, v[2] as f32 - ); - ctx.uniform2f( - sphere_lt_locs[n].as_ref(), - v[3] as f32, v[4] as f32 - ); - ctx.uniform3fv_with_f32_array( - color_locs[n].as_ref(), - &colors[n] - ); - ctx.uniform1f( - highlight_locs[n].as_ref(), - highlights[n] - ); - } - - // pass the display parameters - ctx.uniform1f(opacity_loc.as_ref(), OPACITY); - ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); - ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); - - // draw the scene - ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32); - - // clear the scene change flag - scene_changed.set( - pitch_up_val != 0.0 - || pitch_down_val != 0.0 - || yaw_left_val != 0.0 - || yaw_right_val != 0.0 - || roll_cw_val != 0.0 - || roll_ccw_val != 0.0 - || zoom_in_val != 0.0 - || zoom_out_val != 0.0 - ); - } else { - frames_since_last_sample = 0; - mean_frame_interval.set(-1.0); - } - }); - start_animation_loop(); - }); - - let set_nav_signal = move |event: KeyboardEvent, value: f64| { - let mut navigating = true; - let shift = event.shift_key(); - match event.key().as_str() { - "ArrowUp" if shift => zoom_in.set(value), - "ArrowDown" if shift => zoom_out.set(value), - "ArrowUp" => pitch_up.set(value), - "ArrowDown" => pitch_down.set(value), - "ArrowRight" if shift => roll_cw.set(value), - "ArrowLeft" if shift => roll_ccw.set(value), - "ArrowRight" => yaw_right.set(value), - "ArrowLeft" => yaw_left.set(value), - _ => navigating = false - }; - if navigating { - scene_changed.set(true); - event.prevent_default(); - } - }; - - view! { - /* TO DO */ - // switch back to integer-valued parameters when that becomes possible - // again - canvas( - ref=display, - width="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 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/identity.vert b/app-proto/sketch-outline/src/identity.vert deleted file mode 100644 index 183a65f..0000000 --- a/app-proto/sketch-outline/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/sketch-outline/src/inversive.frag b/app-proto/sketch-outline/src/inversive.frag deleted file mode 100644 index 47743eb..0000000 --- a/app-proto/sketch-outline/src/inversive.frag +++ /dev/null @@ -1,230 +0,0 @@ -#version 300 es - -precision highp float; - -out vec4 outColor; - -// --- inversive geometry --- - -struct vecInv { - vec3 sp; - vec2 lt; -}; - -// --- uniforms --- - -// assembly -const int SPHERE_MAX = 200; -uniform int sphere_cnt; -uniform vecInv sphere_list[SPHERE_MAX]; -uniform vec3 color_list[SPHERE_MAX]; -uniform float highlight_list[SPHERE_MAX]; - -// view -uniform vec2 resolution; -uniform float shortdim; - -// controls -uniform float opacity; -uniform int layer_threshold; -uniform bool debug_mode; - -// light and camera -const float focal_slope = 0.3; -const vec3 light_dir = normalize(vec3(2., 2., 1.)); -const float ixn_threshold = 0.005; -const float INTERIOR_DIMMING = 0.7; - -// --- sRGB --- - -// map colors from RGB space to sRGB space, as specified in the sRGB standard -// (IEC 61966-2-1:1999) -// -// https://www.color.org/sRGB.pdf -// https://www.color.org/chardata/rgb/srgb.xalter -// -// in RGB space, color value is proportional to light intensity, so linear -// color-vector interpolation corresponds to physical light mixing. in sRGB -// space, the color encoding used by many monitors, we use more of the value -// interval to represent low intensities, and less of the interval to represent -// high intensities. this improves color quantization - -float sRGB(float t) { - if (t <= 0.0031308) { - return 12.92*t; - } else { - return 1.055*pow(t, 5./12.) - 0.055; - } -} - -vec3 sRGB(vec3 color) { - return vec3(sRGB(color.r), sRGB(color.g), sRGB(color.b)); -} - -// --- shading --- - -struct taggedFrag { - int id; - vec4 color; - float highlight; - 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, 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 - // 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), highlight, 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], - highlight_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 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) { - 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 06cc43d..8b7ce30 100644 --- a/app-proto/sketch-outline/src/main.rs +++ b/app-proto/sketch-outline/src/main.rs @@ -1,54 +1,13 @@ -mod assembly; -mod display; -mod outline; - -use nalgebra::DVector; use sycamore::prelude::*; -use assembly::{Assembly, Element}; -use display::Display; -use outline::Outline; +mod editor; -#[derive(Clone)] -struct AppState { - assembly: Assembly -} +use editor::Editor; 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) - } - ]) - } - } - ); - view! { - Outline {} - Display {} + Editor {} } }); } \ No newline at end of file diff --git a/app-proto/sketch-outline/src/outline.rs b/app-proto/sketch-outline/src/outline.rs deleted file mode 100644 index 5d650b3..0000000 --- a/app-proto/sketch-outline/src/outline.rs +++ /dev/null @@ -1,58 +0,0 @@ -use itertools::Itertools; -use sycamore::{prelude::*, web::tags::div}; -use web_sys::KeyboardEvent; - -use crate::AppState; - -#[component] -pub fn Outline() -> View { - let state = use_context::(); - - // sort the elements alphabetically by ID - let elements_sorted = create_memo(move || - state.assembly.elements - .get_clone() - .into_iter() - .sorted_by_key(|elt| elt.id.clone()) - .collect() - ); - - view! { - ul { - 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}"); - View::from(div().children(u_coord)) - }).collect::>(); - view! { - /* [TO DO] switch to integer-valued parameters whenever - that becomes possible again */ - 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) } - } - } - }, - key=|elt| elt.id.clone() - ) - } - } -} \ No newline at end of file